# ==================================================================
# Gossamer Links - enhanced directory management system
#
#   Website  : http://gossamer-threads.com/
#   Support  : http://gossamer-threads.com/scripts/support/
#   CVS Info : 087,071,086,086,085      
#   Revision : $Id: Upgrade.pm,v 1.50 2009/05/11 05:57:45 brewt Exp $
# 
# Copyright (c) 2005 Gossamer Threads Inc.  All Rights Reserved.
# Redistribution in part or in whole strictly prohibited. Please
# see LICENSE file for full details.
# ==================================================================
#

package Links::Upgrade;

use strict;
use vars qw/%VERSION_TREE @VERSION_HIDDEN/;
use Carp;
BEGIN {
    # 1.01 below should be updated if this file depends on fixes/additions to GT::SQL::Upgrade
    if (exists $INC{'GT/SQL/Upgrade.pm'} and $GT::SQL::Upgrade::VERSION < 1.01) {
        delete $INC{'GT/SQL/Upgrade.pm'};
    }
}
use GT::SQL::Upgrade;

# This has to be updated every release so that an upgrade can "walk" the tree
# to find any upgrade code.
%VERSION_TREE = (
    '2.0.0' => '2.0.1',
    '2.0.1' => '2.0.2',
    '2.0.2' => '2.0.3',
    '2.0.3' => '2.0.4',
    '2.0.4' => '2.0.5',
    '2.0.5' => '2.1.0',
    '2.1.0' => '2.1.1',
    '2.1.1' => '2.1.2',
    '2.1.2' => '2.2.0',
    '2.2.0' => '2.2.1',
    '2.2.1' => '2.99.0',
    '2.99.0' => '2.99.1',
    '2.99.1' => '3.0.0',
    '3.0.0' => '3.0.1',
    '3.0.1' => '3.0.2',
    '3.0.2' => '3.0.3',
    '3.0.3' => '3.0.4',
    '3.0.4' => '3.1.0',
    '3.1.0' => '3.2.0',
    '3.2.0' => '3.3.0',
);

# These versions won't show up in the available upgrade list returned by
# upgrades_available().
@VERSION_HIDDEN = ('2.99.0', '2.99.1');

sub PERFORM ($$) { "\nPerforming " . (substr($_[0], 0, 1) == 2 ? "Links SQL" : "Gossamer Links") . " $_[0] -> $_[1] upgrades...\n" }
sub DONE ($$) { (substr($_[0], 0, 1) == 2 ? "Links SQL" : "Gossamer Links") . " $_[0] -> $_[1] upgrades performed.\n\n" }

# In list context, returns a list of versions that are available to be upgraded
# from.  In scalar context, returns a hash reference containing a
# upgrades_available key with a value of an array reference containing these
# versions (i.e. for use in a template).  Only versions from %VERSION_TREE are
# included that actually have some upgrade code - in other words, 2.0.1 won't
# be includeded because there is no actual 2.0.1 upgrade code.
sub upgrades_available {
    my %skip = map { $_ => 1 } @VERSION_HIDDEN;
    my @avail =
        map $_->[0],
        sort { for my $i (1 .. (@$a > @$b ? @$a : @$b)) { my $c = $a->[$i] <=> $b->[$i]; return $c if $c } 0 }
        map { ($skip{$_} or !__PACKAGE__->can_upgrade($_)) ? () : [$_, split /\./] }
        keys %VERSION_TREE;
    return wantarray ? @avail : { upgrades_available => \@avail };
}

# Usage:
# Links::Upgrade->upgrade(
#     from => $version, # e.g.  2.2.1
#     output => $coderef, # code reference will be called with any output
#     config => $config, # config object or hash reference (may be changed by upgrade code)
# );
sub upgrade {
    my $class = shift;
    my %opts = @_;
    for (qw/from config/) {
        $opts{$_} or croak "Links::Upgrade->upgrade requires a '$_' option";
    }

    my ($version) = $opts{from} =~ /(\d+\.\d+\.\d+)/;
    $version or croak "Invalid version passed to Links::Upgrade->upgrade: '$opts{from}'";

    ref $opts{config} eq 'HASH' or UNIVERSAL::isa($opts{config}, 'GT::Config') or croak "Invalid 'config' value passed to Links::Upgrade->upgrade: '$opts{config}' not a hash reference or GT::Config object.";
    not $opts{output} or ref $opts{output} eq 'CODE' or croak "Invalid 'output' value passed to Links::Upgrade->upgrade: '$opts{output}' not a code reference.";
    $opts{output} ||= sub { print @_ };

    my $safety;
    while ($version) {
        if (my $sub = $class->can_upgrade($version)) {
            $sub->($opts{output}, $opts{config});
        }
        $safety++ < 100 or croak "Internal upgrade error: $version => $VERSION_TREE{$version} appears to be recursing.";
    }
    continue { $version = $VERSION_TREE{$version} }

# Walk the version upgrade tree
    1;
}

# Takes a version, returns a code reference if the version -> next version
# upgrade code exists, undef otherwise.
sub can_upgrade {
    my ($class, $from) = @_;
    my $to = $VERSION_TREE{$from};
    for ($from, $to) { y/./_/ }
    $class->can("upgrade__${from}__$to");
}

# Called from GT::Template with the version to upgrade from.  Returns either an
# error tag or a message tag.
sub browser_upgrade {
    my ($from, $stream) = @_;
    $from and $from =~ /\d\.\d+\.\d/ or return { error => 'Invalid upgrade version entered.' };
    my $ret = '';
    __PACKAGE__->upgrade(
        from => $from,
        config => $Links::CFG,
        output => sub { if ($stream) { print @_ } else { $ret .= join '', @_ } }
    );

    return {
        upgrade_successful => 1,
        upgrade_result => $ret
    };
}

# Although not strictly upgrade-specific (you can force a tree rebuild) it is
# here as it is primarily an upgrade feature.
# Takes 2-3 arguments - an output subroutine, a GT::SQL object, and an optional
# force value - if specified and true, a rebuild will be forced.
sub create_cat_tree {
    my ($out, $DB, $force) = @_;
    $out ||= sub { print @_ };

    require GT::SQL::Tree;
    require GT::SQL::Tree::Rebuild;
    my $t = $DB->table('Category');
    my %roots;
    my $rebuild = GT::SQL::Tree::Rebuild->new(
        table => $t,
        order_by => 'Full_Name', # Ensure that parents come before children
        cols => [qw/ID FatherID Full_Name/],
        missing_root => sub {
            my ($row, $table) = @_;
            my ($id, $father) = @$row{qw/ID FatherID/};
            if (!$father) {
                return $roots{$id} = 0;
            }
            my $root;
            if (exists $roots{$father}) {
                return $roots{$id} = $roots{$father} || $father;
            }
            else {
                die "No parent category found for $row->{Full_Name}! Your Category table is corrupted.";
            }
        },
        missing_depth => sub {
            my ($row, $table) = @_;
            my $full_name = $row->{Full_Name};
            return $row->{Full_Name} =~ y|/||;
        }
    );

    my $e = $DB->editor('Category');
    $out->("Adding Category tree...\n");
    my $ret = $e->add_tree(father => "FatherID", root => "CatRoot", depth => "CatDepth", force => $force ? 'force' : 'check', rebuild => $rebuild);
    $out->($ret ? "\tOkay!\n" : "\tAn error occured: $GT::SQL::error\n");
    $ret;
}

sub browser_cat_tree {

    my $stream = shift;
    my $message;
    my $okay = create_cat_tree(sub { if ($stream) { print @_ } else { for (@_) { $message .= $_ } } }, $Links::DB, 1);
    return { browser_cat_tree_success => $okay, browser_cat_tree_message => $message };
}

sub upgrade__3_2_0__3_3_0 {
# ---------------------------------------------------------------
# Upgrade from 3.2.0 to 3.3.0
#
    my ($out, $cfg) = @_;
    $out->(PERFORM '3.2.0' => '3.3.0');

    import lib $cfg->{admin_root_path};
    require GT::SQL;
    require GT::SQL::Table;
    $Links::DB = my $DB = GT::SQL->new($cfg->{admin_root_path} . '/defs');
    require GT::Config;
    my $lang = GT::Config->load("$cfg->{admin_root_path}/templates/admin/language.txt");

# Gossamer Links German fix
    if ($cfg->{date_review_format} eq '%dd.%mm%.%yyyy% %HH%:%MM%') {
        $cfg->{date_review_format} = '%dd%.%mm%.%yyyy% %HH%:%MM%';
    }

# These options were split into two, so they should retain the same value as the original option
    if ($cfg->{build_new_date_span_pages} ne $cfg->{build_span_pages}) {
        $cfg->{build_new_date_span_pages} = $cfg->{build_span_pages};
    }
    if ($cfg->{email_review_add} ne $cfg->{email_add}) {
        $cfg->{email_review_add} = $cfg->{email_add};
    }

    $out->("Turning on build_format_compat...\n\tOkay!\n");
    $cfg->{build_format_compat} = 2;

# Add new Reviews subclass
    $out->("Adding Reviews subclass...\n");
    my $t = $DB->table('Reviews');
    $t->subclass(
        table => { Reviews => "Links::Table::Reviews" }
    );
    $t->save_state();
    $out->("\tOkay!\n");

    $out->(DONE '3.2.0' => '3.3.0');
}

sub upgrade__3_1_0__3_2_0 {
# ---------------------------------------------------------------
# Upgrade from 3.1.0 to 3.2.0
#
    my ($out, $cfg) = @_;
    $out->(PERFORM '3.1.0' => '3.2.0');

    import lib $cfg->{admin_root_path};
    require GT::SQL;
    require GT::SQL::Table;
    $Links::DB = my $DB = GT::SQL->new($cfg->{admin_root_path} . '/defs');
    require GT::Config;
    my $lang = GT::Config->load("$cfg->{admin_root_path}/templates/admin/language.txt");

    $out->("Updating PayPal postback check...\n");
    for my $postback (@{$cfg->{payment}->{postback}}) {
        next unless $postback->{method} eq 'PayPal';
        $postback->{var} = 'txn_type';
        last;
    }
    $out->("\tOkay!\n");

    $out->("Updating review e-mail settings...\n");
    if ($cfg->{admin_email_review_add}) {
        $cfg->{admin_email_review_add} = $cfg->{admin_email_add};
    }
    if ($cfg->{admin_email_review_mod}) {
        $cfg->{admin_email_review_mod} = $cfg->{admin_email_mod};
    }
    $out->("\tOkay!\n");

    $out->("Removing old browser templates from admin template set...\n");
    my $dir = "$cfg->{admin_root_path}/templates/admin";
    opendir TPL, $dir or die "Could not open '$dir': $!";
    while (defined(my $file = readdir TPL)) {
        next unless -f "$dir/$file" and $file =~ /^browser.*\.html$/;
        unlink "$dir/$file";
    }
    $out->("\tOkay!\n");

    add_column($out, $DB, Sessions => session_expires => { type => 'TINYINT', default => 1 });
    add_column($out, $DB, Reviews => Review_ModifyDate => { type => 'DATETIME', not_null => 1, default => '0000-00-00 00:00:00', form_display => $lang->{prompt_Review_ModifyDate} });
    alter_column($out, $DB, Reviews => Review_Date => { type => 'DATETIME', not_null => 1, form_display => $lang->{prompt_Review_Date} });

    $out->(DONE '3.1.0' => '3.2.0');
}

sub upgrade__3_0_4__3_1_0 {
# -----------------------------------------------------------------------------
# Upgrade from 3.0.4 to 3.0.5
    my ($out, $cfg) = @_;

    $out->(PERFORM '3.0.4' => '3.1.0');

    import lib $cfg->{admin_root_path};
    require GT::SQL;
    require GT::SQL::Table;
    $Links::DB = my $DB = GT::SQL->new($cfg->{admin_root_path} . '/defs');
    require GT::Config;
    my $lang = GT::Config->load("$cfg->{admin_root_path}/templates/admin/language.txt");

    drop_index($out, $DB, CatLinks => 'catlnndx');

    $out->("Scanning for and removing duplicate entries from CatLinks table...\n");
    # Do some hackery to get a non-subclassed CatLinks table
    #my $catlinks = $DB->table('CatLinks');
    my $catlinks = GT::SQL::Table->new(
        name => $DB->prefix . 'CatLinks',
        connect => $DB->{connect},
        debug => $DB->{_debug},
        _err_pkg => 'GT::SQL::Table'
    );
    $catlinks->select_options('GROUP BY LinkID, CategoryID', 'HAVING COUNT(*) > 1');
    my $sth = $catlinks->select(qw[LinkID CategoryID COUNT(*)]);
    my $count;
    while (my ($linkid, $catid) = $sth->fetchrow) {
        my $deleted = $catlinks->delete({ LinkID => $linkid, CategoryID => $catid });
        $count += $deleted - 1;
        $catlinks->insert({ LinkID => $linkid, CategoryID => $catid });
    }
    $out->("\tOkay! " . ($count ? "$count duplicate entries found and removed.\n" : "No duplicate entries found.\n"));

    add_unique($out, $DB, CatLinks => { cl_cl_q => [qw/CategoryID LinkID/] });


    if ($cfg->{updates}) {
        $out->("Moving update data from Links/Config/Data.pm to Links/Config/Updates.pm\n");
        my $cfg_updates = delete $cfg->{updates};
        require GT::Config;
        my $updates = GT::Config->load("$cfg->{admin_root_path}/Links/Config/Updates.pm", { create_ok => 1 });
        for (keys %$cfg_updates) {
            $updates->{$_} ||= $cfg_updates->{$_};
        }
        $updates->save;
        $out->("\tOkay!\n");
    }

    add_column($out, $DB, Links => LinkExpired => { type => 'INT', form_display => $lang->{prompt_LinkExpired}, form_type => 'hidden' });
    add_index($out, $DB, Category => { c_p => ['Payment_Mode'] });

    $out->(DONE '3.0.4' => '3.1.0');
}

sub upgrade__3_0_3__3_0_4 {
# ---------------------------------------------------------------
# Upgrade from 3.0.3 to 3.0.4
#
    my ($out, $cfg) = @_;
    $out->(PERFORM '3.0.3' => '3.0.4');
    $out->(DONE '3.0.3' => '3.0.4');
}

sub upgrade__3_0_2__3_0_3 {
# ---------------------------------------------------------------
# Upgrade from 3.0.2 to 3.0.3
#
    my ($out, $cfg) = @_;
    $out->(PERFORM '3.0.2' => '3.0.3');
    $out->(DONE '3.0.2' => '3.0.3');
}

sub upgrade__3_0_1__3_0_2 {
# ---------------------------------------------------------------
# Upgrade from 3.0.1 to 3.0.2
#
    my ($out, $cfg) = @_;
    $out->(PERFORM '3.0.1' => '3.0.2');
    $out->(DONE '3.0.1' => '3.0.2');
}

sub upgrade__3_0_0__3_0_1 {
# ---------------------------------------------------------------
# Upgrade from 3.0.0 to 3.0.1
#
    my ($out, $cfg) = @_;
    $out->(PERFORM '3.0.0' => '3.0.1');

    import lib $cfg->{admin_root_path};
    require GT::SQL;
    $Links::DB = my $DB = GT::SQL->new($cfg->{admin_root_path} . '/defs');

    recreate_table($out, $DB, 'MailingListIndex', sub { my $table = shift; my $cols = $table->cols; $cols->{Name}->{type} eq 'TEXT' },
        cols => [
            ID           => { type => 'INT', unsigned => 1, not_null => 1 },
            Name         => { type => 'CHAR', size => 255, not_null => 1 },
            DateModified => { type => 'INT', not_null => 1 },
            DateCreated  => { type => 'INT', not_null => 1 }
        ],
        pk => 'ID',
        ai => 'ID'
    );

    if (-e(my $oldconfig = "$cfg->{admin_root_path}/Links/ConfigData.pm")) {
        $out->("Removing old Links/ConfigData.pm file (has been replaced with Links/Config/Data.pm)...\n");
        require GT::File::Tools;
        my $ret = GT::File::Tools::move($oldconfig, "$oldconfig.old");
        $out->($ret ? "\tOkay!\n" : "\tAn error occured: $!\n");
    }

    $out->(DONE '3.0.0' => '3.0.1');
}

sub upgrade__2_99_1__3_0_0 {
# -----------------------------------------------------------------------------
# Placeholders that currently just prints a 2.2.1 -> 3.0.0 success message.  If
# a 2.99.1 is released, this will become 2_99_1__3_0_0 and so on.  This allows
# transparent handling of the beta versions without duplicating any code and
# without needing to mention the beta in the upgrade output.
#
    my ($out, $cfg) = @_;

    $out->("Updating build_static_path, _url...\n");
    $cfg->{build_static_path} ||= "$cfg->{build_root_path}/static";
    $cfg->{build_static_url} ||= "$cfg->{build_root_url}/static";
    $out->("\tOkay!\n");

    $out->(DONE '2.2.1' => '3.0.0');
}

sub upgrade__2_99_0__2_99_1 {
# ---------------------------------------------------------------
# Upgrade from 2.99.0 to 2.99.1
#
    my ($out, $cfg) = @_;
    import lib $cfg->{admin_root_path};
    require GT::Config;
    my $lang = GT::Config->load("$cfg->{admin_root_path}/templates/admin/language.txt");

    require GT::SQL;
    $Links::DB = my $DB = GT::SQL->new("$cfg->{admin_root_path}/defs");

# Drop unnecessary Bookmark columns added to the Users table in 2.99.0
    my $usercols = $DB->table('Users')->cols;
    for (qw/FolderSortField FolderSortOrd/) {
        drop_column($out, $DB, 'Users', $_) if exists $usercols->{$_};
    }

    delete $cfg->{bookmark_folder_sort};
    delete $cfg->{bookmark_folder_sort_order};
    delete $cfg->{bookmark_user_sort};
    delete $cfg->{bookmark_user_sort_order};

# Don't print here - the final 2.99.x -> 3.0.0 code prints the final message.
    1;
}

sub upgrade__2_2_1__2_99_0 {
# ---------------------------------------------------------------
# Upgrade from 2.2.1 to 2.99.0
#
    my ($out, $cfg) = @_;
    $out->(PERFORM '2.2.1' => '3.0.0');
    import lib $cfg->{admin_root_path};
    require GT::Config;
    my $lang = GT::Config->load("$cfg->{admin_root_path}/templates/admin/language.txt");

    require GT::SQL;
    $Links::DB = my $DB = GT::SQL->new("$cfg->{admin_root_path}/defs");
    $Links::STASH{clicktrack_cleanup} = 1;
    $Links::STASH{expired_links} = 1;

    require Links::Plugins;
    require GT::Plugins::Manager;
    my $plugin = GT::Config->load("$cfg->{admin_root_path}/Plugins/plugin.cfg", { create_ok => 1 });
    {
        package FakeCGI;
        sub param { $_[0]->{$_[1]} }
    }

    add_column($out, $DB, Category => CatRoot => { type => 'INT', not_null => 1, unsigned => 1, default => 0, form_type => 'hidden' });
    add_column($out, $DB, Category => CatDepth => { type => 'INT', not_null => 1, unsigned => 1, default => 0, form_type => 'hidden' });
    add_column($out, $DB, Category => Direct_Links => { type => 'INT', not_null => 1, default => 0, form_display => $lang->{prompt_Direct_Links} });

    create_cat_tree($out, $DB);

    my $t = $DB->table('Category');
    $out->("Updating Category fk to reference itself...\n");
    my $ret = $t->fk(Category => { FatherID => 'ID' });
    $out->($ret ? "\tOkay!\n" : "\tAn error occured: $GT::SQL::error\n");

    $out->("Updating CatLinks subclass...\n");
    $t = $DB->table('CatLinks');
    $t->subclass(
        table => { CatLinks => "Links::Table::CatLinks" }
    );
    $t->save_state();
    $out->("\tOkay!\n");

    $out->("Updating ClickTrack subclass...\n");
    $t = $DB->table('ClickTrack');
    $t->subclass(
        table => { ClickTrack => "Links::Table::ClickTrack" }
    );
    $t->save_state();
    $out->("\tOkay!\n");

    $out->("Updating Direct_Links values...\n");
    my $rel = $DB->table(qw/CatLinks Links/);
    $rel->select_options("GROUP BY CategoryID");
    my $where = GT::SQL::Condition->new(isValidated => '=' => 'Yes');
    $where->add(ExpiryDate => '>=' => time) if $cfg->{payment}->{enabled};
    my %catlinks = $rel->select(qw/CategoryID COUNT(ID)/ => $where)->fetchall_list;
    $t = $DB->table('Category');
    for (keys %catlinks) {
        $t->update({ Direct_Links => $catlinks{$_} }, { ID => $_ }, { GT_SQL_SKIP_CHECK => 1, GT_SQL_SKIP_INDEX => 1 });
    }
    $out->("\tOkay!\n");

    if ($plugin and exists $plugin->{Bookmark}) {
        my $bcfg = Links::Plugins::get_plugin_user_cfg('Bookmark');
        $out->("Bookmark plugin detected, importing Bookmark settings...\n");
        $out->("\tImporting Bookmark configuration...\n");
        for (keys %$bcfg) {
            $cfg->{"bookmark_$_"} = $bcfg->{$_};
        }
        $out->("\t\tDone!\n");

        $out->("\tUninstalling Bookmark plugin...\n");
        my $fakein = bless { plugin_name => "Bookmark", skip_uninstall => 1 }, "FakeCGI";
        my $man = new GT::Plugins::Manager(cgi => $fakein, plugin_dir => "$cfg->{admin_root_path}/Plugins");
        $ret = $man->uninstall;
        $out->($ret->{error} ? "\t\tAn error occured: $ret->{error}\n" : "\t\tDone!\n");
    }

    add_table($out, $DB, 'Bookmark_Folders',
        cols => [
            my_folder_id               => { type => 'INT', not_null => 1, unsigned => 1 },
            my_folder_name             => { type => 'VARCHAR', not_null => 1, size => 255 },
            my_folder_description      => { type => 'VARCHAR', size => 255 },
            my_folder_user_username_fk => { type => 'VARCHAR', size => 50 },
            my_folder_default          => { type => 'TINYINT', not_null => 1, default => 0, unsigned => 1 },
            my_folder_public           => { type => 'TINYINT', not_null => 1, default => 0, unsigned => 1 }
        ],
        pk => 'my_folder_id',
        ai => 'my_folder_id',
        fk => {
            Users => { my_folder_user_username_fk => 'Username' }
        }
    );

    add_table($out, $DB, 'Bookmark_Links',
        cols => [
            my_id               => { type => 'INT', not_null => 1, unsigned => 1 },
            my_link_id_fk       => { type => 'INT', not_null => 1, unsigned => 1 },
            my_user_username_fk => { type => 'VARCHAR', size  => 50 },
            my_folder_id_fk     => { type => 'INT', not_null => 1, unsigned => 1 },
            my_comment          => { type => 'VARCHAR', size  => '255' }
        ],
        pk => 'my_id',
        ai => 'my_id',
        fk => {
            Users            => { my_user_username_fk => 'Username' },
            Bookmark_Folders => { my_folder_id_fk => 'my_folder_id' },
            Links            => { my_link_id_fk => 'ID' },
        }
    );

# Commented out columns were removed in 2.99.1
#    add_column($out, $DB, Users => FolderSortField => { type => 'VARCHAR', size => 255, not_null => 1, regex => '^[\s\w]+$', default => 'my_folder_name', form_display => $lang->{prompt_FolderSortField} });
#    add_column($out, $DB, Users => FolderSortOrd => { type => 'ENUM', values => ['ASC', 'DESC'], not_null => 1, default => 'ASC', form_display => $lang->{prompt_FolderSortOrd} });
    add_column($out, $DB, Users => SortField => { type => 'VARCHAR', size => 255, not_null => 1, regex => '^[\s\w]+$', default => 'Title', form_display => $lang->{prompt_SortField} });
    add_column($out, $DB, Users => SortOrd => { type => 'ENUM', values => ['ASC', 'DESC'], not_null => 1, default => 'ASC', form_display => $lang->{prompt_SortOrd} });
    add_column($out, $DB, Users => PerPage => { type => 'INT', not_null => 1, unsigned => 1, default => 15, form_display => $lang->{prompt_PerPage} });
    add_column($out, $DB, Users => Grouping => { type => 'TINYINT', not_null => 1, unsigned => 1, default => 0, form_display => $lang->{prompt_Grouping} });

    add_column($out, $DB, Editors => CanModReview => { type => 'ENUM', values => ['No', 'Yes'], not_null => 1, default => 'No' });

# Integrate the user's email template changes into the new templates
    $out->("Upgrading email templates...\n");
    require GT::File::Tools;
    require GT::Mail;
    require GT::Mail::Parse;
    my %files = (
        'email-add.txt' => {
            new_name => 'link_added.eml',
            subject  => 'VAL_APPROVESUB',
        },
        'email-del.txt' => {
            new_name => 'link_rejected.eml',
            subject  => 'VAL_REJECTSUB',
        },
        'email-mod.txt' => {
            new_name => 'link_modified.eml',
            subject  => 'VAL_APPROVECHGSUB',
        },
        'email-notify.txt' => {
            new_name => 'link_expiry_notify.eml',
            subject  => 'LINKS_NOTIFY_SUBJECT',
        },
        'email-expired.txt' => {
            new_name => 'link_expired.eml',
            subject  => 'LINKS_NOTIFY_SUBJECT',
        },
        'email-password.txt' => {
            new_name => 'password.eml',
            subject  => 'USER_LOSTPASSSUB',
        },
        'email-validate.txt' => {
            new_name => 'validate.eml',
            subject  => 'USER_VALEMAILSUB',
        },
        'review-email-add.txt' => {
            new_name => 'review_added.eml',
            subject  => 'REVIEW_VAL_APPROVESUB',
        },
        'review-email-del.txt' => {
            new_name => 'review_rejected.eml',
            subject  => 'VAL_REJECTSUB',
        }
    );
    my $new_template = 'luna';
    my $template_path = "$cfg->{admin_root_path}/templates";
    my $fh = \do { local *FH; *FH };
    opendir $fh, $template_path or die "Could not open '$template_path': $!";
    while (my $template_set = readdir $fh) {
        next if $template_set =~ /^\./ or $template_set eq 'admin' or $template_set eq $new_template
            or $template_set =~ /_php$/ or $template_set =~ /^lang_.*\./ or not -d "$template_path/$template_set"
            or $template_set eq 'browser' or $template_set eq 'CVS';
        $out->("\tUpgrading $template_set template set...\n");
        my $l = GT::Template::Inheritance->get_path(file => "language.txt", path => "$template_path/$template_set", use_local => 1, use_inheritance => 1);
        unless (-e $l) {
            $out->("\t\t(no language.txt found, not a template set?)\n");
            next;
        }
        my $clang = GT::Config->load($l);
        for my $file (keys %files) {
            if (not -e "$template_path/$template_set/$files{$file}->{new_name}" and -e "$template_path/$new_template/$files{$file}->{new_name}") {
                GT::File::Tools::copy("$template_path/$new_template/$files{$file}->{new_name}", "$template_path/$template_set/$files{$file}->{new_name}");
            }
            next unless -e "$template_path/$template_set/local/$file" and -r _ and not -e "$template_path/$template_set/local/$files{$file}->{new_name}";
            $out->("\t\tCreating $files{$file}->{new_name} from $file... ");
            open BODY, "$template_path/$template_set/local/$file" or die "Couldn't open template $template_path/$template_set/local/$file: $!";
            my $body;
            {
                local $/;
                $body = <BODY>;
            }
            close BODY;

            next unless -e "$template_path/$new_template/$files{$file}->{new_name}";
            my $top = GT::Mail::Parse->new(
                in_file        => "$template_path/$new_template/$files{$file}->{new_name}",
                crlf           => "\n",
                headers_intact => 0
            )->parse();

            $top->body_data($body) if $body;
            $top->set(Subject => $clang->{$files{$file}->{subject}}) if $clang->{$files{$file}->{subject}};

            if ($body or $clang->{$files{$file}->{subject}}) {
                my $mail = new GT::Mail;
                $mail->top_part($top);
                $mail->write("$template_path/$template_set/local/$files{$file}->{new_name}");
            }
            $out->("done!\n");
# We could also delete the subject language keys and the old email templates,
# but it's better we leave them around than delete something they might not
# want to lose or if something else is still using them.
        }
        $out->("\t\tOkay!\n");
    }
    closedir $fh;
    $out->("\tOkay!\n");

    if (delete $cfg->{foreign_char}) {
        $cfg->{build_category_format} = '%Full_ID%';
        $cfg->{build_category_dynamic} = 'ID';
    }
    elsif (my $f = delete $cfg->{build_directory_field}) {
        $cfg->{build_category_format} = $f ? "%$f%" : '';
        $cfg->{build_category_dynamic} = $f;
    }

    add_table($out, $DB, 'SearchLogs',
        cols => [
            slog_query => { type => 'VARCHAR', not_null => 1, size => 255 },
            slog_count => { type => 'INT', not_null => 1, default => 0 },
            slog_hits  => { type => 'INT', not_null => 1, default => 0 },
            slog_time  => { type => 'FLOAT' },
            slog_last  => { type => 'INT', not_null => 1, default => 0 },
        ],
        pk => 'slog_query'
    );

    if ($plugin and exists $plugin->{SearchLogger} and $DB->table('SearchLog')) {
        $out->("SearchLogger plugin detected, importing SearchLogger settings...\n");
        my $old = $DB->table('SearchLog');
        my $new = $DB->table('SearchLogs');
        require GT::Date;
        $out->("\tTransferring old search logs...\n");
        my $sth = $old->select(qw/Term HitCount Results Last_Hit/);
        my $i;
        if ($sth) {
            while (my $row = $sth->fetchrow_hashref) {
                $i++;
                my %slog_row = (
                    slog_query => $row->{Term},
                    slog_count => $row->{HitCount},
                    slog_hits  => $row->{Results},
                    slog_time  => undef
                );
                my @time;
                if ($row->{Last_Hit} =~ /^(\d{4}-\d\d-\d\d ( ?)\d?\d:\d\d:\d\d)/) {
                    @time = GT::Date::parse_format($1, "%yyyy%-%mm%-%dd% $2%H%:%MM%:%ss%");
                }
                elsif ($row->{Last_Hit} =~ /^(\d{4}-\d\d-\d\d)/) {
                    @time = GT::Date::parse_format($1, '%yyyy%-%mm%-%dd%');
                }
                $slog_row{slog_last} = @time ? GT::Date::timelocal(@time) : 0;
                $ret = $new->insert(\%slog_row);
                $out->("\t\tAn error occured: $GT::SQL::error\n") unless $ret;
            }
            $out->("\t\t$i rows imported.\n");
        }
        else {
            $out->("\t\tAn error occured: $GT::SQL::error\n");
        }

        $out->("\tDropping SearchLog table...\n");
        my $e = $DB->editor('SearchLog');
        my $ret = $e->drop_table;
        $out->($ret ? "\t\tOkay!\n" : "\t\tAn error occured: $GT::SQL::error\n");

        $out->("\tUninstalling SearchLogger plugin...\n");
        my $fakein = bless { plugin_name => "SearchLogger", skip_uninstall => 1 }, "FakeCGI";
        my $man = new GT::Plugins::Manager(cgi => $fakein, plugin_dir => "$cfg->{admin_root_path}/Plugins");
        $ret = $man->uninstall;
        $out->("\t\tOkay!\n");
    }

    add_table($out, $DB, 'NewsletterSubscription',
        cols => [
            UserID     => { type => 'CHAR', size => 50 },
            CategoryID => { type => 'INT', not_null => 1 },
        ],
        unique => {
            ns_uc => ['UserID', 'CategoryID']
        },
        fk => {
            Users    => { UserID => 'Username' },
            Category => { CategoryID => 'ID' }
        }
    );

    if (exists $DB->table('Users')->cols->{Newsletter}) {
        $out->("Importing User Newsletter settings...\n");
        my $sth = $DB->table('Users')->select('Username', { Newsletter => 'Yes' });
        my $ns = $DB->table('NewsletterSubscription');
        if ($sth) {
            while (my $user = $sth->fetchrow) {
                $ns->insert({ UserID => $user, CategoryID => 0 });
            }
            $out->("\tOkay!\n");
        }
        else {
            $out->("\tAn error occured: $GT::SQL::error\n");
        }

        drop_column($out, $DB, 'Users', 'Newsletter');
    }

# Don't print here - the final 2.99.x -> 3.0.0 code prints the final message.
#    $out->("Links SQL 2.2.1 -> 3.0.0 upgrades performed.\n");
    1;
}

sub upgrade__2_2_0__2_2_1 {
# ---------------------------------------------------------------
# Upgrade from 2.2.0 to 2.2.1
#
    my ($out, $cfg) = @_;
    $out->(PERFORM '2.2.0' => '2.2.1');

    import lib $cfg->{admin_root_path};
    require GT::SQL;
    $Links::DB = my $DB = GT::SQL->new($cfg->{admin_root_path} . '/defs');
    $Links::STASH{expired_links} = 1;

    require GT::Config;
    my $lang = GT::Config->load($cfg->{admin_root_path} . '/templates/admin/language.txt');

# Update Rating regex
    alter_column($out, $DB, Links => Rating => { type => 'DECIMAL', precision => 4, scale => 2, not_null => 1, default => 0, regex => '^(?:10(?:\.0*)?|\d(?:\.\d*)?)$', form_display => $lang->{prompt_Rating} });

# Update payments_term from CHAR(8) to CHAR(10)
    alter_column($out, $DB, Payments => payments_term => { type => 'CHAR', not_null => 1, size => 10 });

# Fix fk_tables that might have been deleted due to the SQL database overwrite bug
    my %fk_tables = (
        Category => [qw/CatPrice CatLinks CatRelations Editors/],
        Links    => [qw/Payments Changes Reviews CatLinks Verify/],
        Payments => [qw/PaymentLogs/],
        Users    => [qw/Links Changes Reviews Editors Sessions/]
    );
    $out->("Checking fk_tables...\n");
    my $p = $DB->prefix;
    while (my ($table, $tables) = each %fk_tables) {
        my $tb = $DB->table($table);
        my $changed;
        for (@$tables) {
            $tb->_add_fk_table("$p$_") and $changed++;
        }
        if ($changed) {
            $tb->save_state;
            $out->("\t\t$table table's fk_tables repaired\n");
        }
    }
    $out->("\tOkay!\n");

    $out->(DONE '2.2.0' => '2.2.1');
}

sub upgrade__2_1_2__2_2_0 {
# ---------------------------------------------------------------
# Upgrade from 2.1.2 to 2.2.0
#
    my ($out, $cfg) = @_;
    $out->(PERFORM '2.1.2' => '2.2.0');

    import lib $cfg->{admin_root_path};
    require GT::Config;

    # Check to see that the PPC plugin <1.93 is not installed.  Versions prior
    # to 1.93 conflict with Links SQL's 'Payments' table.
    my $plugin_cfg = GT::Config->load("$cfg->{admin_root_path}/Plugins/plugin.cfg");
    if (exists $plugin_cfg->{PPC} and (!$plugin_cfg->{PPC}->{version} or $plugin_cfg->{PPC}->{version} < 1.93)) {
        $out->("Old PPC plugin detected - you must upgrade the PPC plugin to 1.93 or above before upgrading Links SQL.");
        die "Old PPC plugin detected - you must upgrade the PPC plugin to 1.93 or above before upgrading Links SQL.";
    }

    require GT::SQL;
    $Links::DB = my $DB = GT::SQL->new($cfg->{admin_root_path} . '/defs');
    $Links::STASH{clicktrack_cleanup} = 1;
    $Links::STASH{expired_links} = 1;

    my $lang = GT::Config->load("$cfg->{admin_root_path}/templates/admin/language.txt");

    for my $table (qw/Users Links Category/) {
        $out->("Updating $table subclasses...\n");
        # Create a new GT::SQL::Table object manually as I do _not_ want to
        # load the existing subclasses.
        my $t = GT::SQL::Table->new(
            name     => "$DB->{connect}->{PREFIX}$table",
            connect  => $DB->{connect},
            debug    => $DB->{_debug},
            _err_pkg => 'GT::SQL::Table'
        );
        $t->subclass(
            table => { $table => "Links::Table::$table" },
            html  => { $table => "Links::HTML::$table" }
        );
        $t->save_state();
        $out->("\tOkay!\n");
    }

    add_column($out, $DB, Category => Payment_Mode => { type => 'TINYINT', not_null => 1, default => 0, form_size => 1, form_names => [0,1,2,3], form_values => ['Use global settings','Not accepted','Optional','Required'], form_type => 'SELECT', form_display => $lang->{prompt_Payment_Mode} });
    add_column($out, $DB, Category => Payment_Description => { type => 'TEXT', form_display => $lang->{prompt_Payment_Description} });
    add_column($out, $DB, Links => ExpiryDate => { type => 'INT', not_null => 1, default => 0x7fff_ffff, form_display => $lang->{prompt_ExpiryDate}, form_size => 35 });
    add_column($out, $DB, Links => ExpiryCounted => { type => 'TINYINT', not_null => 1, default => 0, form_display => $lang->{prompt_ExpiryCounted}, form_type => 'hidden' });
    add_column($out, $DB, Links => ExpiryNotify  => { type => 'TINYINT', not_null => 1, default => 0, form_display => $lang->{prompt_ExpiryNotify},  form_type => 'hidden' });
    drop_index($out, $DB, Links => 'valndx');
    add_index($out, $DB, Links => {
        valexpndx => [qw/isValidated ExpiryDate/],
        expiryndx => [qw/ExpiryDate ExpiryNotify/],
        expcntndx => [qw/ExpiryCounted ExpiryDate/]
    });

    add_table($out, $DB, "CatPrice",
        cols => [
            cp_id          => { type => 'INT', not_null => 1, unsigned => 1 },
            cp_cat_id_fk   => { type => 'INT', not_null => 1, unsigned => 1 },
            cp_term        => { type => 'CHAR', not_null => 1, size => 10 }, # e.g. 8d, 1m, 2y, 3w, unlimited, etc.
            cp_cost        => { type => 'FLOAT', not_null => 1 },
            cp_type        => { type => 'TINYINT', not_null => 1, unsigned => 1 }, # 0 = signup, 1 = renewal, 2 = recurring
            cp_description => { type => 'TEXT' }
        ],
        pk => 'cp_id',
        ai => 'cp_id',
        fk => { Category => { cp_cat_id_fk => 'ID' } }
    );

    add_table($out, $DB, "Payments",
        cols => [
            payments_id     => { type => 'CHAR', not_null => 1, size => 16 },
            payments_linkid => { type => 'INT', unsigned => 1, not_null => 1 },
            payments_status => { type => 'INT', not_null => 1, default => 0, unsigned => 1 }, # 0 = pending, 1 = completed, 2 = declined, 3 = error
            payments_method => { type => 'CHAR', not_null => 1, size => 25 },
            payments_type   => { type => 'TINYINT', not_null => 1, unsigned => 1 }, # 0 = initial payment, 1 = renewal payment, 2 = recurring payment
            payments_amount => { type => 'FLOAT', not_null => 1 },
            payments_term   => { type => 'CHAR', not_null => 1, size => 8 }, # e.g. 8d, 1m, 2y, 3w, etc.
            payments_start  => { type => 'INT', not_null => 1, unsigned => 1 },
            payments_last   => { type => 'INT', not_null => 1, unsigned => 1 },
        ],
        pk => 'payments_id',
        fk => { Links => { payments_linkid => 'ID' } },
        index => {
            p_sl => ['payments_status', 'payments_last'],
            p_ll => ['payments_linkid', 'payments_last'],
            p_al => ['payments_amount', 'payments_last'],
        }
    );

    add_table($out, $DB, "PaymentLogs",
        cols => [
            paylogs_id          => { type => 'INT', not_null => 1, unsigned => 1 },
            paylogs_payments_id => { type => 'CHAR', not_null => 1, size => 16 },
            paylogs_type        => { type => 'INT', not_null => 1, default => 0, unsigned => 1 }, # 0 = info, 1 = accepted, 2 = declined, 3 = error
            paylogs_time        => { type => 'INT', not_null => 1, unsigned => 1 },
            paylogs_viewed      => { type => 'TINYINT', not_null => 1, default => 0, unsigned => 1 },
            paylogs_text        => { type => 'TEXT' },
        ],
        pk => 'paylogs_id',
        ai => 'paylogs_id',
        fk => { Payments => { paylogs_payments_id => 'payments_id' } },
        index => {
            pl_yt => ['paylogs_type', 'paylogs_time'],
            pl_t => ['paylogs_time']
        }
    );

    recreate_table($out => $DB => ClickTrack => sub { my $table = shift; ($table->pk and @{$table->pk} != 0) },
        cols => [
            LinkID    => { type => 'INT', not_null => 1 },
            IP        => { type => 'CHAR', size => 25, not_null => 1 },
            ClickType => { type => 'ENUM', values => ['Rate', 'Hits','Review'], not_null => 1 },
            ReviewID  => { type => 'INT', not_null => 1, default => 0},
            Created   => { type => 'TIMESTAMP' }
        ],
        unique => {
            ct_licr => ['LinkID', 'IP', 'ClickType','ReviewID']
        },
        index => {
            cndx => ['Created']
        }
    );

    if (-e(my $oldconfig = "$cfg->{admin_root_path}/Links/ConfigData.pm")) {
        $out->("Removing old Links/ConfigData.pm file (has been replaced with Links/Config/Data.pm)...\n");
        require GT::File::Tools;
        my $ret = GT::File::Tools::move($oldconfig, "$oldconfig.old");
        $out->($ret ? "\tOkay!\n" : "\tAn error occured: $!\n");
    }

    $out->(DONE '2.1.2' => '2.2.0');
}

sub upgrade__2_1_1__2_1_2 {
# ---------------------------------------------------------------
# Upgrade from 2.1.1 to 2.1.2
#
    my ($out, $cfg) = @_;
    $out->(PERFORM '2.1.1' => '2.1.2');

# Add session table.
    import lib $cfg->{admin_root_path};
    require GT::SQL;
    my $DB = GT::SQL->new( $cfg->{admin_root_path} . '/defs' );
    add_table($out, $DB, 'Sessions',
        cols => [
            session_id      =>  { type => 'CHAR', size => 32, not_null => 1, binary => '1' },
            session_user_id =>  { type => 'CHAR', not_null => 1 },
            session_date    =>  { type => 'INT',  not_null => 1 },
            session_data    =>  { type => 'TEXT' }
        ],
        pk => 'session_id',
        fk => { Users => { session_user_id => 'Username' } }
    );

    $out->(DONE '2.1.1' => '2.1.2');
}

sub upgrade__2_0_5__2_1_0 {
# ---------------------------------------------------------------
# Upgrade from 2.0.5 to 2.1.0
#
    my ($out, $cfg) = @_;
    $out->(PERFORM '2.0.5' => '2.1.0');

# Add the review table.
    import lib $cfg->{admin_root_path};
    require GT::SQL;
    require GT::Config;
    my $lang  = GT::Config->load( $cfg->{admin_root_path} . '/templates/admin/language.txt' );
    my $DB = GT::SQL->new($cfg->{admin_root_path} . '/defs');

    add_table($out, $DB, 'Reviews',
        cols => [
            ReviewID            =>  { type => 'INT', not_null => 1, unsigned => 1, form_display => $lang->{'prompt_ReviewID'} },
            Review_LinkID       =>  { type => 'INT', not_null => 1, unsigned => 1, regex => '^\d+$', form_display => $lang->{'prompt_Review_LinkID'} },
            Review_Owner        =>  { type => 'CHAR', size => 50, not_null => 1, form_display => $lang->{'prompt_Review_Owner'} },
            Review_Rating       =>  { type => 'SMALLINT', unsigned => 1, not_null => 1, default => 0, regex => '^\d+$', form_display => $lang->{'prompt_Review_Rating'} },
            Review_Date         =>  { type => 'DATE', not_null => 1, form_display => $lang->{'prompt_Review_Date'} },
            Review_Subject      =>  { type => 'CHAR', size => 100, not_null => 1, form_display => $lang->{'prompt_Review_Subject'} },
            Review_Contents     =>  { type => 'TEXT', not_null => 1, form_display => $lang->{'prompt_Review_Contents'} },
            Review_ByLine       =>  { type => 'CHAR', size => 50, form_display => $lang->{'prompt_Review_ByLine'} },
            Review_WasHelpful   =>  { type => 'INT', unsigned => 1, regex => '^\d+$', form_display => $lang->{'prompt_Review_WasHelpful'} },
            Review_WasNotHelpful=>  { type => 'INT', unsigned => 1, regex => '^\d+$', form_display => $lang->{'prompt_Review_WasNotHelpful'} },
            Review_Validated    =>  { type => 'ENUM', values => ['No', 'Yes'], not_null => 1, default => 'No', form_display => $lang->{'prompt_Review_Validated'} },
            Review_GuestName    =>  { type => 'CHAR', size => 75, form_display => $lang->{'prompt_Review_GuestName'} },
            Review_GuestEmail   =>  { type => 'CHAR', size => 75, regex => '^(?:(?:.+\@.+\..+)|\s*)$', form_display => $lang->{'prompt_Review_GuestEmail'} },
        ],
        pk => 'ReviewID',
        ai => 'ReviewID',
        index => { rownerndx => ['Review_Owner'], rdatendx => ['Review_Date'], rlinkndx => ['Review_LinkID'] },
        fk => { Links => { Review_LinkID => 'ID' }, Users => { Review_Owner => 'Username' }}
    );

    add_column($out, $DB, ClickTrack => ReviewID => { type => 'INT', not_null => 1, default => 0 });

# Set default review options.
    my %default_review = (
        user_review_required   => 1,
        reviews_per_page       => 5,
        review_sort_by         => 'Review_Date',
        review_convert_br_tags => 1,
        review_days_old        => 7
    );
    while (my ($k, $v) = each %default_review) {
        $cfg->{$k} = $v unless exists $cfg->{$k};
    }

    $out->(DONE '2.0.5' => '2.1.0');
}

sub upgrade__2_0_3__2_0_4 {
# ---------------------------------------------------------------
# Upgrade from 2.0.3 to 2.0.4.
#
    my ($out, $cfg) = @_;

    $out->(PERFORM '2.0.3' => '2.0.4');

    import lib $cfg->{admin_root_path};
    require GT::SQL;
    my $db = GT::SQL->new($cfg->{admin_root_path} . '/defs');

    add_column($out, $db, Links => Contact_Name => { type => 'CHAR', size => 255 });
    add_column($out, $db, Links => Contact_Email => { type => 'CHAR', size => 255 });
    add_column($out, $db, Category => Category_Template => { type => 'CHAR', size => 40 });
    add_column($out, $db, MailingIndex => messageformat => { type => 'ENUM', values => [qw[text html]], not_null => 1, default => 'text' });

    $out->(DONE '2.0.3' => '2.0.4');
}

1;
