# ================================================================== # 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 = ; } 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;