# ==================================================================
# 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: Modify.pm,v 1.82 2013/02/01 04:43:56 brewt Exp $
#
# Copyright (c) 2001 Gossamer Threads Inc.  All Rights Reserved.
# Redistribution in part or in whole strictly prohibited. Please
# see LICENSE file for full details.
# ==================================================================

package Links::User::Modify;
# ==================================================================
use strict;
use Links qw/:objects :payment/;
use Links::Build;
use Links::SiteHTML;

sub handle {
# ---------------------------------------------------
# Determine what to do.
#
    my $link_id = $IN->param('LinkID');
    if ($CFG->{user_required} and !$USER) {
        print $IN->redirect(Links::redirect_login_url('modify'));
        return;
    }

# Perform the link modification
    if ($IN->param('modify')) {
        _modify();
    }
    elsif ($USER) {
# Display the link modify form (for a specific link)
        if ($IN->param('LinkID')) {
            _modify_passed_in();
        }
        else {
            _list_owned_links();
        }
    }
# Display the link modify form (used when user_required is off)
    else {
        _modify_form();
    }
}

# ==============================================================

sub _modify {
# --------------------------------------------------------
# Modifies a link.
#
# If payment is enabled and we are processing a payment
    if ($CFG->{payment}->{enabled} and $IN->param('process_payment')) {
        my $payment_term = $IN->param('payment_term') || '';
        my $do = $IN->param('do') || '';
        if ($payment_term eq 'free') {
            print $IN->header();
            my $link = $DB->table('Links')->get(scalar $IN->param('link_id'));
            my $mtl = Links::Build::build('title', Links::language('LINKS_MODIFY_SUCCESS'), "$CFG->{db_cgi_url}/modify.cgi");
            if (not $link or ($CFG->{user_required} and $link->{LinkOwner} ne $USER->{Username})) {
                print Links::SiteHTML::display('error', { error => !$link ? $GT::SQL::ERRORS : Links::language('PAYMENTERR_NOTOWNER'), main_title_loop => $mtl });
                return;
            }
            $link = Links::SiteHTML::tags('link', $link);

# Add some special tags for formatting.
            $link->{Category} = $DB->table('Category', 'CatLinks')->select('Category.Full_Name', { 'CatLinks.LinkID' => $link->{ID} })->fetchrow;

# Set ExpiryDate to free
            $link->{'CatLinks.CategoryID'} = $IN->param('cat_id');
            $link->{ExpiryDate} = FREE;
            $link->{ExpiryNotify}= 0;
# Update the link
            $DB->table('Links')->update({ ExpiryDate => FREE, ExpiryNotify => 0 }, { ID => $link->{ID} });
# Update the Timestmp for link's categories so they get rebuilt with build changed
            my @cats = $DB->table('Links', 'CatLinks')->select('CategoryID', { LinkID => $link->{ID} })->fetchall_list;
            $DB->table('Category')->update({ Timestmp => \'NOW()' }, { ID => \@cats });

            print Links::SiteHTML::display('modify_success', { %$link, main_title_loop => $mtl });
        }
        elsif ($do eq 'payment_linked') {
            print $IN->header;
            my $link = $DB->table('Links', 'CatLinks')->select({ ID => scalar $IN->param('ID') })->fetchrow_hashref;
            if (!$link) {
                print Links::SiteHTML::display('error', { error => Links::language('MODIFY_NOLINKS') });
                return;
            }
            elsif ($CFG->{user_required} and $link->{LinkOwner} ne $USER->{Username}) {
                print Links::SiteHTML::display('error', { error => Links::language('PAYMENTERR_NOTOWNER') });
                return;
            }
            $link = Links::SiteHTML::tags('link', $link);

            require Links::Payment;
            my @cid = $DB->table('CatLinks')->select('CategoryID', { LinkID => $link->{ID} })->fetchall_list;
            my $opt = Links::Payment::load_cat_price(\@cid);
            if ($opt->{payment_mode} == NOT_ACCEPTED) {
                print Links::SiteHTML::display('error', { error => Links::language('PAYMENTERR_NOTACCEPTED') });
                return;
            }
            $link->{link_id} = $link->{ID}; # we need a different tag since both Category and Link have ID
            $opt->{CategoryID} = delete $opt->{ID}; # remove category id
            $opt->{CategoryDescription} = delete $opt->{Description};
            $link->{main_title_loop} = Links::Build::build('title', Links::language('LINKS_PAYMENT'), "$CFG->{db_cgi_url}/modify.cgi?do=payment_linked;process_payment=1;modify=1;ID=$link->{link_id}");
            print Links::SiteHTML::display('payment', { %$link, %$opt });
        }
        elsif ($do =~ /^payment_(method|form|direct)$/) {
            require Links::Payment;
            my $vars = Links::Payment->$1();
            my $page = $IN->param('page') || $IN->param('do');
            my $opt  = Links::Payment::load_cat_price($IN->param('cat_id'));
            if ($opt->{payment_mode} == NOT_ACCEPTED) {
                print Links::SiteHTML::display('error', { error => Links::language('PAYMENTERR_NOTACCEPTED') });
                return;
            }
            my $link = $DB->table('Links')->get($IN->param('link_id'));
            print $IN->header();
            if (not $link or $link->{LinkOwner} ne $USER->{Username}) {
                print Links::SiteHTML::display('error', { error => !$link ? $GT::SQL::ERRORS : Links::language('PAYMENTERR_NOTOWNER') });
                return;
            }
            $link = Links::SiteHTML::tags('link', $link);

            $link->{main_title_loop} = Links::Build::build('title', Links::language('LINKS_PAYMENT'), "$CFG->{db_cgi_url}/modify.cgi?do=payment_linked;process_payment=1;modify=1;ID=$link->{ID}");
            print Links::SiteHTML::display($page, { %$vars, %$opt, %$link });
        }
        else {
            print $IN->header;
            print Links::SiteHTML::display('error', { error => "Invalid action" });
        }
    }
# Otherwise, modify the link
    else {
        my $results = $PLG->dispatch('user_modify_link', \&modify_link, {});
        if (defined $results->{error}) {
            print $IN->header();
            print Links::SiteHTML::display('modify', $results);
        }
        else {
            $results->{main_title_loop} = Links::Build::build('title', Links::language('LINKS_MODIFY_SUCCESS'), "$CFG->{db_cgi_url}/modify.cgi");
            if ($CFG->{payment}->{enabled}) {
                require Links::Payment;
                my @cid = $IN->param('CatLinks.CategoryID');
                my $opt = Links::Payment::load_cat_price(\@cid);
                print $IN->header();
                if (exists $opt->{error}) {
                    print Links::SiteHTML::display('error', $opt);
                }
                elsif ($opt->{payment_mode} == NOT_ACCEPTED or ($results->{ExpiryDate} >= time)) {
                    print Links::SiteHTML::display('modify_success', $results);
                }
                else {# display payment form if the link is expired or payment mode for this category is required or optional
                    $results->{link_id} = $results->{ID}; # we need a different tag since both Category and Link have ID
                    $opt->{CategoryID} = delete $opt->{ID}; # remove category id
                    $opt->{CategoryDescription} = delete $opt->{Description};
                    $results->{main_title_loop} = Links::Build::build('title', Links::language('LINKS_PAYMENT'), "$CFG->{db_cgi_url}/modify.cgi?do=payment_linked;process_payment=1;modify=1;ID=$results->{link_id}");
                    print Links::SiteHTML::display('payment', {%$results,%$opt});
                }
            }
            else {
                print $IN->header();
                print Links::SiteHTML::display('modify_success', $results);
            }
        }
    }
}

sub _modify_passed_in {
# --------------------------------------------------------
# Display link that was passed in.
#
    my $lid = $IN->param('LinkID');
    my $link_db = $DB->table('Links');
    my $mtl = Links::Build::build('title', Links::language('LINKS_MODIFY'), "$CFG->{db_cgi_url}/modify.cgi?LinkID=$lid");
    my $sth = $link_db->select({ ID => $lid, LinkOwner => $USER->{Username} }, VIEWABLE);
    if ($sth->rows) {
        my $link = $sth->fetchrow_hashref;
        my @ids = $DB->table('CatLinks')->select('CategoryID', { LinkID => $link->{ID} })->fetchall_list;
        $IN->param('CatLinks.CategoryID', \@ids);

        $link->{Contact_Name}  ||= $USER->{Name} || $USER->{Username};
        $link->{Contact_Email} ||= $USER->{Email};

        my $category = {};
        if ($CFG->{db_gen_category_list} < 2) {
            require Links::Tools;
            $category = Links::Tools::category_list();
            $category->{Category} = sub { Links::Tools::category_list_html() };
        }
        print $IN->header();
        print Links::SiteHTML::display('modify', {
            main_title_loop => $mtl,
            %$link,
            %$category
        });
    }
    elsif (!$CFG->{user_required}) {
        _modify_form();
    }
    else {
        print $IN->header();
        print Links::SiteHTML::display('error', { error => Links::language('MODIFY_NOTOWNER'), LinkID => $lid, main_title_loop => $mtl });
    }
}

sub _list_owned_links {
# --------------------------------------------------------
# Display a list of links the user owns.
#
    my $link_db = $DB->table('Links');
    my ($limit, $offset, $nh) = Links::limit_offset();
    my $mtl = Links::Build::build('title', Links::language('LINKS_MODIFY'), "$CFG->{db_cgi_url}/modify.cgi");
    $link_db->select_options("ORDER BY Title ASC", "LIMIT $limit OFFSET $offset");
    my $sth = $link_db->select({
        LinkOwner => $USER->{Username},
        # If payment is enabled, we want to show non-validated links to allow
        # payment to occur, otherwise only show validated ones
        ($CFG->{payment}->{enabled} ? () : (isValidated => 'Yes'))
    });
    my $total = $link_db->hits;
    if (! $sth->rows) {
        print $IN->header();
        print Links::SiteHTML::display('error', { error => Links::language('MODIFY_NOLINKS'), main_title_loop => $mtl });
        return;
    }
    my ($toolbar, %paging);
    my @links;
    while (my $hash = $sth->fetchrow_hashref) {
        push @links, Links::SiteHTML::tags('link', $hash);
    }
    if ($total > $limit) {
        my $url  = $CFG->{db_cgi_url} . "/" . $IN->url;
        $toolbar = $DB->html(['Links'], $IN)->toolbar($nh, $limit, $total, $url);
        %paging = (
            url          => $url,
            num_hits     => $total,
            max_hits     => $limit,
            current_page => $nh
        );
    }
    print $IN->header();
    print Links::SiteHTML::display('modify_select', {
        link_results_loop => \@links,
        main_title_loop   => $mtl,
        total             => $total,
        next              => $toolbar,
        paging            => \%paging
    });
}

sub _modify_form {
# --------------------------------------------------------
# Just display the regular form.
#
    my @id = $IN->param('ID'); # Category ID.
    my $link = {};
    print $IN->header();
    if ($IN->param('LinkID')) {
        my $lid = $IN->param('LinkID');
        $link = $DB->table('Links')->select({ ID => $lid }, VIEWABLE)->fetchrow_hashref;
        if (!$link) {
            print Links::SiteHTML::display('error', { error => Links::language('MODIFY_INVALIDLINKID', $lid) });
            return;
        }
        if (!@id) {
            @id = $DB->table('CatLinks')->select('CategoryID', { LinkID => $lid })->fetchall_list;
# Set ID to the categories that the link is in so Links::Tools::category_list
# pre-selects them
            $IN->param(ID => \@id);
        }
    }

    if (!@id and !$CFG->{db_gen_category_list}) {
        print Links::SiteHTML::display('error', { error => Links::language('MODIFY_SELCAT') });
    }
    else {
        my $category = {};
        if ($CFG->{db_gen_category_list} < 2) {
            require Links::Tools;
            $category = Links::Tools::category_list();
            $category->{Category} = sub { Links::Tools::category_list_html() };
        }
        print Links::SiteHTML::display('modify', {
            main_title_loop => Links::Build::build('title', Links::language('LINKS_MODIFY'), "$CFG->{db_cgi_url}/modify.cgi" . (@id ? "?ID=" . join(';ID=', @id) : '')),
            %$category,
            %$link
        });
    }
}

sub modify_link {
# --------------------------------------------------------
# Change the requested link.
#
    my $args     = $IN->get_hash();
    my $db       = $DB->table('Links');
    my %cols     = $db->cols;

# Make it possible to use any column to find the link we're modifying.
# Normally, we use the LinkID to find the link, but in some conditions the URL
# is used.  Using this isn't recommended as you're not guaranteed to get the
# same or unique results.
    my ($column, $value);
    foreach my $col (keys %cols) {
        if (exists $args->{'Current_' . $col} and $args->{'Current_' . $col}) {
            $column = $col;
            $value  = $args->{'Current_' . $col};
            last;
        }
    }

    my $lid = $args->{LinkID};
    my %ret;
    if ($CFG->{db_gen_category_list} < 2) {
        require Links::Tools;
        %ret = %{Links::Tools::category_list()};
        $ret{Category} = sub { Links::Tools::category_list_html() };
    }
    $ret{main_title_loop} = Links::Build::build('title', Links::language('LINKS_MODIFY'), "$CFG->{db_cgi_url}/modify.cgi" . ($lid ? "?LinkID=$lid" : ''));
    $ret{LinkID} = $lid;
    unless ($value or ($lid and $USER)) {
        return { error => Links::language('MODIFY_NOURL'), %ret };
    }

# Find the requested link
    my ($link, $sth);
    if ($USER and $lid) {
        #if ($CFG->{user_required}) {
# Mod added back on April 10 by Virginia 
        if ($CFG->{user_required} and $USER->{Status} ne 'Administrator') {  # mod by Virginia Lo on Oct 29, 2007
            $sth = $db->select({ ID => $lid, LinkOwner => $USER->{Username} });
        }
        else {
            $sth = $db->select({ ID => $lid });
        }
        $sth->rows or return { error => Links::language('MODIFY_INVALIDLINKID', $lid), %ret };
    }
    else {
        $sth = $db->select({ $column => $value });
        $sth->rows or return { error => Links::language('MODIFY_BADURL', $value), %ret };
    }
    $link = $sth->fetchrow_hashref;

# Make sure to only allow modifications to validated links.  We currently allow
# the user to modify expired links.
    unless ($link->{isValidated} eq 'Yes') {
        return { error => Links::language('MODIFY_NOLINKS'), %ret };
    }

    my $new = {%$args};

# Forced system fields (these aren't in the add_system_fields option)
    my @system = qw/ID LinkOwner Add_Date Mod_Date Timestmp Date_Checked ExpiryDate ExpiryCounted ExpiryNotify LinkExpired/;
    my %system = map { $_ => 1 } @system;

    for my $key (keys %cols) {
# Users can't modify system fields, so remove them so the columns don't get
# modified
        if (exists $system{$key} or exists $CFG->{add_system_fields}->{$key}) {
            delete $new->{$key};
            next;
        }

# Use the original link value if it hasn't been passed in from cgi.  This is
# done to make sure all Links columns pass the column checks (not null, regex,
# etc checks).  It has to be done for all columns, since column definitions may
# have changed since the record was originally inserted.
        $new->{$key} = $link->{$key} unless defined $args->{$key};
    }

# Check that the ExpiryDate is valid for the categories the link is in
    if ($CFG->{payment}->{enabled}) {
        require Links::Payment;
        my $expiry = Links::Payment::check_expiry_date($link);
        $new->{ExpiryDate} = $expiry if $expiry;
    }

# modify() needs the primary key to perform the update
    $new->{ID} = $link->{ID};

    Links::init_date();
    $new->{Mod_Date} = GT::Date::date_get();

# Backwards compatibility
    $new->{Contact_Name}  = $args->{Contact_Name}  || $args->{'Contact Name'}  || ($USER ? $USER->{Name}  : '');
    $new->{Contact_Email} = $args->{Contact_Email} || $args->{'Contact Email'} || ($USER ? $USER->{Email} : '');

# Setup the language for GT::SQL
    local $GT::SQL::ERRORS->{ILLEGALVAL} = Links::language('ADD_ILLEGALVAL');
    local $GT::SQL::ERRORS->{UNIQUE} = Links::language('ADD_UNIQUE');
    local $GT::SQL::ERRORS->{NOTNULL} = Links::language('ADD_NOTNULL');
    local $Links::Table::Links::ERRORS->{NOCATEGORY} = Links::language('MODIFY_NOCATEGORY');
    $Links::Table::Links::ERRORS if 0; # silence -w

# On error, file column values need to be restored (since they need to get
# re-uploaded).  This is done so that the templates show the correct fields on
# an error.
    my %fcols = $db->_file_cols();
    for (keys %fcols) {
        $ret{$_} = $link->{$_};
    }

# Because we store the change request in the Changes table and do not perform
# the modify directly, all the column checks that modify() would normally do
# need to be done now.
    my $fset;
    unless ($USER and $CFG->{user_direct_mod}) {
        if (keys %fcols) {
            require GT::SQL::File;
            my $file = GT::SQL::File->new({ parent_table => $DB->table('Links'), connect => $DB->{connect} });
            $fset = $file->pre_file_actions(\%fcols, $new, $args, $new->{ID}) or return { error => $GT::SQL::error, %ret };
        }

# The following block of code modifies $new (so that _check_update() works
# properly), but we don't want that later on, so make a shallow copy of it.
        my $new_copy = { %$new };

# This block of code is pulled from GT::SQL::Table::modify (minus the comments)
        my $cols = $db->{schema}->{cols};
        for my $col (keys %$cols) {
            next unless exists $new_copy->{$col};

            if ($cols->{$col}->{type} eq 'TIMESTAMP') {
                delete $new_copy->{$col};
            }
            elsif ($cols->{$col}->{type} =~ /^(?:.*INT|INTEGER|FLOAT|REAL|DOUBLE|DECIMAL|DATE|TIME|DATETIME)$/ and defined $new_copy->{$col} and $new_copy->{$col} eq '') {
                $new_copy->{$col} = undef;
            }
            elsif ($cols->{$col}->{not_null} and not (defined $new_copy->{$col} and length $new_copy->{$col})) {
                $new_copy->{$col} = undef;
            }
        }

        $db->_check_update($new_copy, { ID => $new_copy->{ID} }) or return { error => $GT::SQL::error, %ret };
    }

# Make sure the category id's are valid
    $IN->param('CatLinks.CategoryID')
        or return { error => Links::language('MODIFY_NOCATEGORY'), %ret };

# Set the Category ID's
    my @c_ids = $IN->param('CatLinks.CategoryID');
    $new->{'CatLinks.CategoryID'} = $db->clean_category_ids(\@c_ids)
        or return { error => $GT::SQL::error, %ret };

# Check if the link is valid
    if ($CFG->{user_link_validation}) {
        require Links::Tools;
        my $status = Links::Tools::link_status($new->{URL});
        if ($status and $Links::Tools::STATUS_BAD{$status}) {
            return { error => Links::language('MODIFY_BADSTATUS', $Links::Tools::STATUS_BAD{$status}), %ret };
        }
    }

    my $orig_cats = $db->get_categories($new->{ID});
    my $new_cats;

# Add the link either directly in, or into the change request table.
    if ($USER and $CFG->{user_direct_mod}) {
        if ($USER->{Status} ne 'Administrator' and $link->{LinkOwner} ne $USER->{Username}) {
            return { error => Links::language('MODIFY_NOTOWNER'), %ret };
        }

        my $res = $db->modify($new) or return { error => $GT::SQL::error, %ret };
        $new_cats = $db->get_categories($new->{ID});
    }
    else {
        require GT::Dumper;
        my $chg_db = $DB->table('Changes');

# Remove any columns which haven't changed
        for my $key (keys %cols) {
            next if not exists $new->{$key} or $key eq 'ID';

            delete $new->{$key} if $new->{$key} eq (defined $link->{$key} ? $link->{$key} : '');
        }

# Handle updating the expiry date later on (when the admin does the change
# validation).  It can't be done here because payments can be made to the link
# before the change validation occurs, losing the user's updated expiry date.
        delete $new->{ExpiryDate};

# pre_file_actions() pulls the file columns out of the $new hash; put them back
# in and save the uploaded file(s) in a temporary location for processing upon
# change validation.
        foreach my $col (keys %fcols) {
            if (exists $fset->{$col}) {
                my $fh = $fset->{$col};
                my $fname = GT::CGI->escape(get_filename($fh));
                my $fpath = "$CFG->{admin_root_path}/tmp/$new->{ID}-$fname";

                open F, ">$fpath";
                binmode F; binmode $fh;
                my $buf;
                while (read $fh, $buf, 4096) { print F $buf; };
                close F;

                $new->{$col} = $fpath;
                $new->{"${col}_filename"} = $fset->{"${col}_filename"} || get_filename($fh);
            }
            elsif (exists $fset->{"${col}_del"}) {
                $new->{"${col}_del"} = $fset->{"${col}_del"};
            }
        }

        my $count = $chg_db->count({ LinkID => $new->{ID} });
        if ($count) {
            my $href = $chg_db->select('ChgRequest', { LinkID => $new->{ID} })->fetchrow;
            $href = eval $href;
            foreach (keys %fcols) {
                my $fpath = $href->{$_} or next;
                $fpath ne $new->{$_} or next;
                $fpath !~ /\.\./ or next;
                $fpath =~ /^[\w\\\/\-\.%]+$/ or next;
                -e $fpath or next;
                $fpath =~ m,^\Q$CFG->{admin_root_path}\E/tmp/, or next;
                unlink $fpath;
            }
            $chg_db->update({ LinkID => $new->{ID}, Username => $link->{LinkOwner}, ChgRequest => GT::Dumper->dump({ data => $new, var => '' }) }, { LinkID => $new->{ID} })
                or return { error => $GT::SQL::error, %ret };
        }
        else {
            $chg_db->insert({ LinkID => $new->{ID}, Username => $link->{LinkOwner}, ChgRequest => GT::Dumper->dump({ data => $new, var => '' }) })
                or return { error => $GT::SQL::error, %ret };
        }
        my $cdb = $DB->table('Category');
        foreach my $id (@c_ids) {
            my $cat = $cdb->get($id, 'HASH', ['Full_Name']);
            $new_cats->{$id} = $cat->{Full_Name};
        }
    }

# Now email the site admin.
    if ($CFG->{admin_email_mod}) {
        my %tags;
        for my $key (keys %$link) {
            $tags{"Original_" . $key} = $link->{$key};
            $tags{"New_" . $key} = exists $new->{$key} ? $new->{$key} : $link->{$key};
        }
# Pull in the extra fields that might be in $new (eg. extra file data)
        for my $key (keys %$new) {
            next if exists $tags{"New_" . $key};
            $tags{"New_" . $key} = $new->{$key};
        }
        $tags{Original_Category} = join "\n", sort values %$orig_cats;
        $tags{Original_Category_loop} = [sort values %$orig_cats];
        $tags{New_Category} = join "\n", sort values %$new_cats;
        $tags{New_Category_loop} = [sort values %$new_cats];

        $GT::Mail::error ||= '';
        Links::send_email('link_modified.eml', \%tags, { admin_email => 1 }) or die "Unable to send message: $GT::Mail::error";
    }
    $new->{Category} = join("\n", sort values %$new_cats);
    $new->{Category_loop} = [sort values %$new_cats];

# All done!
    return { %$args, %$link, %$new };
}

sub get_filename {
# -------------------------------------------------------------------
    my $fpath = shift;
    my @path  = split /[\\\/]/, $fpath;
    return pop @path;
}

1;
