discourse-legacysite-perl/site/slowtwitch.com/cgi-bin/articles/admin/Links/Build.pm
2024-06-17 21:49:12 +10:00

1278 lines
48 KiB
Perl

# ==================================================================
# 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: Build.pm,v 1.136 2008/06/25 19:23:48 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::Build;
# ==================================================================
use strict;
use Links qw/:objects :payment/;
use Links::SiteHTML;
use GT::AutoLoader;
use vars qw/$GRAND_TOTAL/;
sub build {
# -----------------------------------------------------------------
# Returns a specified template parsed.
#
my $build = shift;
my $code = exists $Links::Build::{"build_$build"} ? *{$Links::Build::{"build_$build"}}{CODE} : _compile("build_$build");
defined $code or die "Invalid method: build_$build called.";
$PLG->dispatch("build_$build", $code, @_);
}
$COMPILE{build_home} = __LINE__ . <<'END_OF_SUB';
sub build_home {
# ------------------------------------------------------------------
# Generate the home page.
#
$GRAND_TOTAL ||= _grand_total();
my $category = $DB->table('Category');
$category->select_options("ORDER BY $CFG->{build_category_sort}") if $CFG->{build_category_sort};
my $sth = $category->select({ FatherID => 0 });
my $root = [];
while (my $cat = $sth->fetchrow_hashref) {
$cat->{URL} = "$CFG->{build_root_url}/" . $category->as_url($cat->{Full_Name}) . "/" . ($CFG->{build_index_include} ? $CFG->{build_index} : '');
push @$root, $cat;
}
# Plugins can use the build_category_loop hook to change the category
# results before they are returned to the template.
$PLG->dispatch(build_category_loop => sub { } => $root);
my $print_cat;
my $cat_list = sub {
return $print_cat if defined $print_cat;
return $print_cat = Links::SiteHTML::display('print_cat', [{}, @$root])
};
return Links::SiteHTML::display('home', { category => $cat_list, category_loop => $root, grand_total => $GRAND_TOTAL });
}
END_OF_SUB
$COMPILE{build_new} = __LINE__ . <<'END_OF_SUB';
sub build_new {
# ------------------------------------------------------------------
# Generate the what's new page. Takes as options:
#
# mh => new links per page
# nh => page number
# sb => field to sort by
# so => ascending or descending order
# gb => 1/0 (group by category name).
#
my $opts = shift;
if (ref $opts ne 'HASH') {
Links::debug("Invalid option passed to build_new: $opts") if $Links::DEBUG;
return;
}
if (! exists $opts->{sb}) {
my $sort = $CFG->{build_sort_order_new} || '';
$opts->{sb} = $sort;
$opts->{so} = '';
}
$opts->{mh} = exists $opts->{mh} ? $opts->{mh} : 1000;
$opts->{nh} = exists $opts->{nh} ? $opts->{nh} : 1;
$opts->{sb} = exists $opts->{sb} ? $opts->{sb} : 'Add_Date';
$opts->{gb} = exists $opts->{gb} ? $opts->{gb} : $CFG->{build_new_gb};
my $cat_db = $DB->table('Category');
my $link_db = $DB->table('Links');
$GRAND_TOTAL ||= _grand_total();
my %filter = (
isValidated => 'Yes',
isNew => 'Yes',
mh => $opts->{mh},
nh => $opts->{nh},
sb => $opts->{sb},
so => $opts->{so},
ww => 1
);
# Should not select unpaid and expired links if payment is enabled
$filter{ExpiryDate} = '>=' . time if $CFG->{payment}->{enabled};
my $sth = $link_db->query_sth(\%filter);
my $total = $link_db->hits;
my $cur_date = '';
Links::init_date();
my (@date_order, %grouped_output);
# Get the links.
my $results = $sth->fetchall_hashref;
$link_db->add_reviews($results);
# Get the category names.
my $catlink = $DB->table('CatLinks','Category');
my %names = $catlink->select('LinkID', 'Full_Name', { LinkID => [map $_->{ID}, @$results] })->fetchall_list;
for my $link (@$results) {
my $date = $link->{Add_Date};
my $long_date = GT::Date::date_transform($link->{Add_Date}, GT::Date::FORMAT_DATE, $CFG->{date_long_format});
$date ne $cur_date and push @date_order, $long_date;
if ($opts->{gb}) {
push @{$grouped_output{$long_date}->{$names{$link->{ID}}}}, Links::SiteHTML::tags('link', $link);
}
else {
push @{$grouped_output{$long_date}->{'none'}}, Links::SiteHTML::tags('link', $link);
}
$cur_date = $date;
}
my @dates;
foreach my $date (@date_order) {
my @links;
if ($opts->{gb}) {
foreach my $cat (sort keys %{$grouped_output{$date}}) {
$grouped_output{$date}->{$cat}->[0]->{title_linked} = sub { build('title_linked', { name => $cat, complete => 1 }) };
$grouped_output{$date}->{$cat}->[0]->{title_loop} = build('title', $cat);
push @links, @{$grouped_output{$date}->{$cat}};
}
}
else {
push @links, @{$grouped_output{$date}->{'none'}};
}
push @dates, { new_date => $date, links => \@links };
}
my $ret;
my $output = sub {
return $ret if defined $ret;
$ret = '';
for my $date (@date_order) {
$ret .= "</blockquote></p>" if $ret;
$ret .= "<p><b>$date</b><blockquote>";
my @links;
if ($opts->{gb}) {
for my $cat (sort keys %{$grouped_output{$date}}) {
my $title = build('title_linked', { name => $cat, complete => 1 });
$ret .= $title;
$ret .= join("", map { Links::SiteHTML::display('link', $_) } @{$grouped_output{$date}->{$cat}});
}
}
else {
$ret .= join("", map { Links::SiteHTML::display('link', $_) } @{$grouped_output{$date}->{'none'}});
}
}
$ret .= "</blockquote></p>";
return $ret;
};
my $new = Links::language('LINKS_NEW');
return Links::SiteHTML::display(new => {
total => $total,
grand_total => $GRAND_TOTAL,
link_results => $output,
link_results_loop => $results,
main_title_loop => build('title', $new),
title => sub { build('title_unlinked', $new) },
main_title_linked => sub { build('title_linked', $new) },
title_linked_loop => \@dates
});
}
END_OF_SUB
$COMPILE{build_new_index} = __LINE__ . <<'END_OF_SUB';
sub build_new_index {
# ------------------------------------------------------------------
# Build a what's new index page grouped by long date.
#
Links::init_date();
require GT::SQL::Condition;
my $cond = GT::SQL::Condition->new(
isNew => '=' => 'Yes',
VIEWABLE
);
my $link_db = $DB->table('Links');
$link_db->select_options("GROUP BY Add_Date", "ORDER BY Add_Date DESC");
my $sth = $link_db->select(Add_Date => 'COUNT(*)' => $cond);
my $total = 0;
$GRAND_TOTAL ||= _grand_total();
my $results = '';
my @loop;
while (my ($date, $count) = $sth->fetchrow_array) {
my $long_date = GT::Date::date_transform($date, GT::Date::FORMAT_DATE, $CFG->{date_long_format});
$date =~ s/\s(.*)//;
$results .= qq|<li><a href="$CFG->{build_new_url}/$date$CFG->{build_extension}">$long_date</a> ($count)</li>|;
$total += $count;
push @loop, { date => $long_date, url => "$CFG->{build_new_url}/$date$CFG->{build_extension}", count => $count };
}
my $new = Links::language('LINKS_NEW');
return Links::SiteHTML::display(new => {
total => $total,
grand_total => $GRAND_TOTAL,
link_results => $results,
link_results_loop => \@loop,
main_title_loop => build('title', $new),
title => sub { build(title_unlinked => $new) },
main_title_linked => sub { build(title_linked => $new) },
new_index => 1
});
}
END_OF_SUB
$COMPILE{build_new_subpage} = __LINE__ . <<'END_OF_SUB';
sub build_new_subpage {
# -----------------------------------------------------------------------------
# Generate a what's new sub page. Takes a hash reference containing a date key
# and value.
#
Links::init_date();
my $opts = shift;
if (ref $opts ne 'HASH') {
Links::debug("Invalid argument to build_new_subpage: $opts") if $Links::DEBUG;
return;
}
my $date = $opts->{date};
if (!GT::Date::date_is_valid($date)) {
Links::debug("Invalid date passed to build_new_subpage: $date") if $Links::DEBUG;
return;
}
my $link_db = $DB->table('Links');
my $cat_db = $DB->table('Category');
my $mh = $opts->{mh};
my $nh = $opts->{nh};
$link_db->select_options("ORDER BY $CFG->{build_sort_order_new}") if $CFG->{build_sort_order_new};
if ($mh and $nh) {
my $offset = ($nh-1) * $mh;
$link_db->select_options("LIMIT $offset, $mh");
}
my $sth = $link_db->select({ isNew => 'Yes', Add_Date => $date }, VIEWABLE);
my $new_total = $link_db->hits;
my $long_date = GT::Date::date_transform($date, GT::Date::FORMAT_DATE, $CFG->{date_long_format});
my $total = $link_db->hits;
my $gb = $CFG->{build_new_gb};
$GRAND_TOTAL ||= _grand_total();
my $output = {};
my $out = '';
# Get the links.
my $results = $sth->fetchall_hashref;
$link_db->add_reviews($results);
my @link_results_loop;
@link_results_loop = map Links::SiteHTML::tags('link', $_) => @$results unless $gb;
# Get the category names.
my $catlink = $DB->table('CatLinks','Category');
my %names = $catlink->select('LinkID', 'Full_Name', { LinkID => [map $_->{ID}, @$results] })->fetchall_list;
if ($gb) {
for my $link (@$results) {
push @{$output->{$names{$link->{ID}}}}, Links::SiteHTML::tags('link', $link);
}
}
my ($toolbar, %paging);
if ($mh and ($total > $mh)) {
my $first_url = $CFG->{build_new_url} . "/" . $date . $CFG->{build_extension};
my $next_url = $CFG->{build_new_url} . "/" . $date . "_";
$toolbar = build('toolbar', { first_url => $first_url, next_url => $next_url, numlinks => $new_total, nh => $nh, mh => $mh });
my ($new_url) = $CFG->{build_new_url} =~ m|^\Q$CFG->{build_root_url}\E/(.+)|;
%paging = (
page => "$new_url/$date",
page_format => 2,
num_hits => $new_total,
max_hits => $mh,
current_page => $nh
);
}
if ($gb) {
foreach my $name (sort { lc $a cmp lc $b } keys %$output) {
$output->{$name}->[0]->{title_linked} = sub { build('title_linked', { name => $name, complete => 1 }) };
$output->{$name}->[0]->{title_loop} = build('title', $name);
push @link_results_loop, @{$output->{$name}};
}
}
my $links;
$out = sub {
return $links if defined $links;
$gb or return $links = join "", map { Links::SiteHTML::display(link => $_) } @$results;
for my $name (sort { lc $a cmp lc $b } keys %$output) {
my $title = build(title_linked => { name => $name, complete => 1 });
$links .= "<p>$title" . join "", map { Links::SiteHTML::display(link => $_) } @{$output->{$name}};
}
return $links;
};
my $new = Links::language('LINKS_NEW');
return Links::SiteHTML::display(new => {
total => $total,
grand_total => $GRAND_TOTAL,
link_results => $out,
link_results_loop => \@link_results_loop,
main_title_loop => build('title', "$new/$long_date"),
title => sub { build('title_unlinked', "$new/$long_date") },
main_title_linked => sub { build('title_linked', "$new/$long_date") },
next_span => $toolbar,
paging => \%paging
});
}
END_OF_SUB
$COMPILE{build_cool} = __LINE__ . <<'END_OF_SUB';
sub build_cool {
# ------------------------------------------------------------------
# Generate the what's cool page. Takes as options:
#
# mh => new links per page
# nh => page number
# sb => field to sort by
# so => ascending or descending order
# gb => 1/0 (group by category name).
#
my $opts = shift;
if (ref $opts ne 'HASH') {
Links::debug("Invalid option passed to build_cool: $opts") if $Links::DEBUG;
return;
}
if (! exists $opts->{sb}) {
$opts->{sb} = $CFG->{build_sort_order_cool} || '';
$opts->{so} = '';
}
$opts->{mh} ||= $CFG->{build_links_per_page} || 25;
$opts->{nh} ||= 1;
$opts->{gb} ||= $CFG->{build_cool_gb};
my $cat_db = $DB->table('Category');
my $link_db = $DB->table('Links');
$GRAND_TOTAL ||= _grand_total();
my %filter = (
isPopular => 'Yes',
isValidated => 'Yes',
mh => $opts->{mh},
nh => $opts->{nh},
sb => $opts->{sb},
so => $opts->{so},
ww => 1
);
# Should not select unpaid and expired links if payment is enabled
$filter{ExpiryDate} = '>=' . time if $CFG->{payment}->{enabled};
my $sth = $link_db->query_sth(%filter);
my $total = $link_db->hits;
my $sub_total = 0;
my $output = '';
my %grouped_output;
# Get the links.
my $results = $sth->fetchall_hashref;
$link_db->add_reviews($results);
my @link_results_loop;
@link_results_loop = map Links::SiteHTML::tags('link', $_) => @$results unless $opts->{gb};
# Get the category names.
my $catlink = $DB->table('CatLinks','Category');
my %names = $catlink->select(LinkID => Full_Name => { LinkID => [map $_->{ID}, @$results] })->fetchall_list;
for my $link (@$results) {
$opts->{gb} and push @{$grouped_output{$names{$link->{ID}}}}, Links::SiteHTML::tags('link', $link);
$sub_total++;
}
if ($opts->{gb}) {
foreach my $cat (sort { lc $a cmp lc $b } keys %grouped_output) {
$grouped_output{$cat}->[0]->{title_linked} = sub { build(title_linked => { name => $cat, complete => 1 }) };
$grouped_output{$cat}->[0]->{title_loop} = build('title', $cat);
push @link_results_loop, @{$grouped_output{$cat}};
}
}
my $links;
$output = sub {
return $links if defined $links;
$opts->{gb} or return $links = join '', map { Links::SiteHTML::display('link', $_) } @$results;
$links = '';
foreach my $cat (sort { lc $a cmp lc $b } keys %grouped_output) {
my $title = build('title_linked', { name => $cat, complete => 1 });
$links .= "<p>$title" . join '', map { Links::SiteHTML::display(link => $_) } @{$grouped_output{$cat}};
}
return $links;
};
my $percent = ($CFG->{build_pop_cutoff} < 1) ? $CFG->{build_pop_cutoff} * 100 . "%" : $CFG->{build_pop_cutoff};
my ($toolbar, %paging);
if ($total > $opts->{mh}) {
$toolbar = build(toolbar => { url => "$CFG->{build_cool_url}/", numlinks => $total, nh => $opts->{nh}, mh => $opts->{mh} });
my ($cool_url) = $CFG->{build_cool_url} =~ m|^\Q$CFG->{build_root_url}\E/(.+)|;
%paging = (
page => "$cool_url/",
page_format => 1,
num_hits => $total,
max_hits => $opts->{mh},
current_page => $opts->{nh}
);
}
my $cool = Links::language('LINKS_COOL');
return Links::SiteHTML::display(cool => {
total => $total,
grand_total => $GRAND_TOTAL,
link_results => $output,
main_title_loop => build('title', $cool),
title => sub { build(title_unlinked => $cool) },
main_title_linked => sub { build(title_linked => $cool) },
percent => $percent,
next_span => $toolbar,
paging => \%paging,
link_results_loop => \@link_results_loop
});
}
END_OF_SUB
$COMPILE{build_rating} = __LINE__ . <<'END_OF_SUB';
sub build_rating {
# ------------------------------------------------------------------
# Generate the rating pages.
#
my $links = $DB->table('Links');
require GT::SQL::Condition;
my $cond = GT::SQL::Condition->new('Votes', '>=', 10, VIEWABLE);
$GRAND_TOTAL ||= _grand_total();
$links->select_options("ORDER BY Rating DESC", "LIMIT 10");
my $rated = $links->select($cond);
$links->select_options("ORDER BY Votes DESC", "LIMIT 10");
my $voted = $links->select($cond);
# Now build the html.
my ($top_rated, $top_votes) = ('', '');
my (@top_votes_loop, @top_rated_loop);
while (my $link = $voted->fetchrow_hashref) {
$link->{detailed_url} = $CFG->{build_detail_url} . '/' . $links->detailed_url($link->{ID}) if $CFG->{build_detailed};
push @top_votes_loop, $link;
$top_votes .= qq~<tr><td align=center>$link->{Rating}</td><td align=center>$link->{Votes}</td><td><a href="$link->{URL}">$link->{Title}</a></td></tr>\n~;
}
while (my $link = $rated->fetchrow_hashref) {
$link->{detailed_url} = $CFG->{build_detail_url} . '/' . $links->detailed_url($link->{ID}) if $CFG->{build_detailed};
push @top_rated_loop, $link;
$top_rated .= qq~<tr><td align=center>$link->{Rating}</td><td align=center>$link->{Votes}</td><td><a href="$link->{URL}">$link->{Title}</a></td></tr>\n~;
}
# And write it to a file.
return Links::SiteHTML::display(rate_top => {
top_rated => $top_rated,
top_rated_loop => \@top_rated_loop,
top_votes_loop => \@top_votes_loop,
top_votes => $top_votes,
main_title_loop => build('title', Links::language('LINKS_TOPRATED'), $CFG->{build_ratings_url})
});
}
END_OF_SUB
$COMPILE{build_detailed} = __LINE__ . <<'END_OF_SUB';
sub build_detailed {
# ------------------------------------------------------------------
# Builds a single detailed page, takes either a link hash ref, or
# a link id.
#
if (! $CFG->{build_detailed}) {
return Links::SiteHTML::display(error => { error => Links::language('BUILD_DETAILED_DISABLED') });
}
my $link_db = $DB->table('Links');
$GRAND_TOTAL ||= _grand_total();
my $link = shift;
if (ref $link ne 'HASH') {
return Links::SiteHTML::display(error => { error => Links::language('BUILD_DETAILED_ARGS', $link) });
}
$link->{ID} ||= $IN->param('ID');
if (exists $link->{ID}) {
my $id = $link->{ID};
my $link_row = $link_db->get($id, 'HASH');
@$link{keys %$link_row} = values %$link_row;
$link_db->add_reviews([ $link ]);
if (! $link) {
return Links::SiteHTML::display(error => { error => Links::language('BUILD_DETAILED_INVALIDID', $id) });
}
elsif ($link->{isValidated} ne 'Yes') {
return Links::SiteHTML::display(error => { error => Links::language('BUILD_DETAILED_UNVAL') });
}
elsif ($CFG->{payment}->{enabled} and $link->{ExpiryDate} < time) {
return Links::SiteHTML::display(error => { error => Links::language('BUILD_DETAILED_EXPIRED') });
}
}
my ($cat_id, $cat_name);
if ($link->{CategoryID} and $link->{Full_Name}) {
$cat_id = $link->{CategoryID};
$cat_name = $link->{Full_Name};
}
else {
($cat_id, $cat_name) = each %{$link_db->get_categories($link->{ID})};
}
# Figure out the next/prev links.
my $catlnk_db = $DB->table('Links', 'CatLinks');
$catlnk_db->select_options("ORDER BY $CFG->{build_sort_order_category}") if $CFG->{build_sort_order_category};
my $sth = $catlnk_db->select('Links.ID' => { CategoryID => $cat_id }, VIEWABLE);
my ($next, $prev);
while (my $id = $sth->fetchrow) {
if ($id == $link->{ID}) {
$next = $sth->fetchrow;
last;
}
else {
$prev = $id;
}
}
my ($next_url, $prev_url) = $DB->table('Links')->category_detailed_url($cat_id, $next, $prev);
$link->{Category_Template} = $DB->table('Category')->template_set($cat_id);
# build_title generates the title loop by using / as a category delimiter.
# Since the link title can have /'s in it, we'll push that entry onto the loop
# ourselves. Note that the deprecated title and title_linked variables are
# still broken.
my $title_loop = build('title', $cat_name);
push @$title_loop, { Name => $link->{Title}, URL => $CFG->{build_detail_url} . '/' . $link_db->detailed_url($link->{ID}) };
return Links::SiteHTML::display('detailed', {
%$link,
title_loop => $title_loop,
title => sub { build('title_unlinked', "$cat_name/$link->{Title}") },
title_linked => sub { build('title_linked', "$cat_name/$link->{Title}") },
grand_total => $GRAND_TOTAL,
next => $next,
prev => $prev,
next_url => $next_url ? "$CFG->{build_detail_url}/$next_url" : '',
prev_url => $prev_url ? "$CFG->{build_detail_url}/$prev_url" : ''
});
}
END_OF_SUB
$COMPILE{build_category} = __LINE__ . <<'END_OF_SUB';
sub build_category {
# ------------------------------------------------------------------
# Build a single category page. Takes as argument a category hash or id
# number and an options hash.
#
my $cat_db = $DB->table('Category');
my $link_db = $DB->table('Links');
my $catlink_db = $DB->table('Links', 'CatLinks');
my $related_db = $DB->table('CatRelations');
$GRAND_TOTAL ||= _grand_total();
my $opts = shift;
if (ref $opts ne 'HASH') {
Links::debug("Invalid argument passed to build_category: $opts") if $Links::DEBUG;
return;
}
# Load our category info.
my $category;
$opts->{id} ||= $IN->param('ID');
if ($opts->{id}) {
$category = $cat_db->get($opts->{id}, 'HASH');
if (! $category) {
Links::debug("Invalid category id passed to build_category: $opts->{id}") if $Links::DEBUG;
return;
}
}
# Get our options.
$opts->{mh} = exists $opts->{mh} ? $opts->{mh} : $CFG->{build_span_pages} ? $CFG->{build_links_per_page} : 5000;
$opts->{nh} = exists $opts->{nh} ? $opts->{nh} : 1;
$opts->{sb} = exists $opts->{sb} ? $opts->{sb} : $CFG->{build_sort_order_category};
$opts->{so} = exists $opts->{so} ? $opts->{so} : '';
if ($opts->{sb} =~ /\b(?:asc|desc)\b/i) {
$opts->{so} = '';
}
$opts->{cat_sb} = exists $opts->{cat_sb} ? $opts->{cat_sb} : $CFG->{build_category_sort};
$opts->{cat_so} = exists $opts->{cat_so} ? $opts->{cat_so} : '';
if ($opts->{cat_sb} =~ /\b(?:asc|desc)\b/i) {
$opts->{cat_so} = '';
}
# Figure out the template set to use.
$category->{Category_Template} ||= $cat_db->template_set($category->{ID});
# Get our output vars.
my %tplvars = (
%$category,
category_id => $category->{ID},
category_name => $category->{Full_Name},
header => $category->{Header},
footer => $category->{Footer},
meta_name => $category->{Meta_Description},
meta_keywords => $category->{Meta_Keywords},
description => $category->{Description},
random => int rand 10000,
random1 => int rand 10000,
random2 => int rand 10000,
random3 => int rand 10000
);
# Clean up the name.
my $clean_name = $cat_db->as_url($category->{Full_Name});
my $build_title = $category->{Full_Name};
$build_title .= '/' . Links::language('LINKS_PAGE', $opts->{nh}) if $opts->{nh} and $opts->{nh} > 1;
$tplvars{title_loop} = build('title', $build_title);
$tplvars{title_linked} = sub { build('title_linked', $build_title) };
$tplvars{title} = sub { build('title_unlinked', $build_title) };
$tplvars{category_name_escaped} = GT::CGI->escape($category->{Full_Name});
$tplvars{category_clean} = $tplvars{title};
($tplvars{category_short}) = $tplvars{category_name} =~ m|([^/]+)$|;
# Prepare the condition; don't add the ExpiryDate handling - it gets added later
my $cond = GT::SQL::Condition->new(
CategoryID => '=' => $category->{ID},
isValidated => '=' => 'Yes'
);
# "Optional" payment categories are a hassle, as we have to do two selects,
# then balance out the mh/nh variables between the two.
my ($optional_sth, $sth);
my @select_options;
push @select_options, "ORDER BY $opts->{sb} $opts->{so}" if $opts->{sb};
# Load payment info if payment is enabled. Change sort order by paid links
# first then free links if payment for this category is optional. If payment
# is required, we need to remove unpaid links
if ($CFG->{payment}->{enabled}) {
require Links::Payment;
my $payment_info = Links::Payment::cat_payment_info($opts->{id});
if ($payment_info->{mode} == OPTIONAL and $CFG->{build_sort_paid_first}) {
my $paycond = GT::SQL::Condition->new($cond);
$paycond->add(ExpiryDate => '>=' => time, ExpiryDate => '<=' => UNLIMITED);
my $offset = ($opts->{nh} - 1) * $opts->{mh};
$catlink_db->select_options(@select_options);
$catlink_db->select_options("LIMIT $opts->{mh} OFFSET $offset");
$optional_sth = $catlink_db->select('Links.*', $paycond);
$cond->add(ExpiryDate => '=' => FREE);
}
else {
# 1) This is an else (instead of elsif ($payment_info->{mode} == REQUIRED)) because the
# run-time count updating code cannot efficiently take category settings into account
# as doing so requires either subselects (which older MySQL doesn't support), or a fair
# bit of Perl code; a single fast count to determine whether the check is necessary
# won't work. The end result is that counts would be off.
# 2) Even if this was an elsif, we can't include ExpiryDate <= UNLIMITED (to exclude
# free links) because links being free is the default for imported, upgraded, and
# admin-added links, which we don't want to exclude from REQUIRED categories.
$cond->add(ExpiryDate => '>=' => time);
}
}
my @results;
my ($paid_hits, $paid_rows, $offset, $max_hits) = (0, 0, ($opts->{nh} - 1) * $opts->{mh}, $opts->{mh});
if ($optional_sth) {
push @results, @{$optional_sth->fetchall_hashref};
$paid_rows = $optional_sth->rows;
$paid_hits = $catlink_db->hits;
if ($paid_rows == $opts->{mh}) {
$offset = $max_hits = 0;
}
elsif ($paid_rows > 0) {
$offset = 0;
$max_hits = $opts->{mh} - $paid_rows;
}
else {
$offset -= $paid_hits;
}
}
my $hits;
# Select links from required categories, not-accepted categories, and optional
# categories whose paid hits haven't filled the page
if ($max_hits) { # $max_hits will be 0 when mh paid links are already listed
$catlink_db->select_options(@select_options);
$catlink_db->select_options("LIMIT $max_hits OFFSET $offset");
my $sth = $catlink_db->select('Links.*' => $cond);
push @results, @{$sth->fetchall_hashref};
$hits = $catlink_db->hits;
}
else {
$hits = $catlink_db->count($cond);
}
my $numlinks = $tplvars{total} = $hits + $paid_hits;
$tplvars{total_optional_paid} = $paid_hits;
# Get the links.
$link_db->add_reviews(\@results);
my @links_loop = map Links::SiteHTML::tags('link', $_, $category->{ID}) => @results;
$tplvars{links_loop} = \@links_loop;
$tplvars{links_count} = @links_loop;
my $links;
$tplvars{links} = sub {
return $links if defined $links;
$links = '';
for my $link (@results) {
$link->{Category_Template} = $category->{Category_Template} if $category->{Category_Template};
$links .= Links::SiteHTML::display('link', $link);
}
return $links;
};
# Get the subcategories and related categories as either Yahoo style (integrated) or
# separated into two outputs..
my @cat_loop;
$tplvars{category_loop} = \@cat_loop;
if ($CFG->{build_category_yahoo}) {
my @subcat_ids = $cat_db->select(ID => { FatherID => $category->{ID} })->fetchall_list;
my %related_ids = $related_db->select(qw/RelatedID RelationName/ => { CategoryID => $category->{ID} })->fetchall_list;
if (@subcat_ids or keys %related_ids) {
$cat_db->select_options("ORDER BY $opts->{cat_sb} $opts->{cat_so}") if $opts->{cat_sb};
my $sth = $cat_db->select({ ID => [@subcat_ids, keys %related_ids] });
my @rel_loop;
while (my $cat = $sth->fetchrow_hashref) {
$cat->{URL} = "$CFG->{build_root_url}/" . $cat_db->as_url($cat->{Full_Name}) . "/" . ($CFG->{build_index_include} ? $CFG->{build_index} : '');
$cat->{RelationName} = '';
if (exists $related_ids{$cat->{ID}}) {
$cat->{Related} = 1;
$cat->{RelationName} = $related_ids{$cat->{ID}};
# Relations with a custom name need to be re-sorted
if ($cat->{RelationName}) {
push @rel_loop, $cat;
next;
}
}
push @cat_loop, $cat;
}
# Re-sort related categories using their RelationName rather than the related
# category's name
RELATION: while (my $cat = pop @rel_loop) {
for (my $i = 0; $i < @cat_loop; $i++) {
my $name = $cat_loop[$i]->{RelationName} ? $cat_loop[$i]->{RelationName} : $cat_loop[$i]->{Name};
if (lc $cat->{RelationName} lt lc $name) {
splice @cat_loop, $i, 0, $cat;
next RELATION;
}
}
push @cat_loop, $cat;
}
my $print_cat;
$tplvars{category} = sub {
return $print_cat if defined $print_cat;
return $print_cat = Links::SiteHTML::display('print_cat', [$category, @cat_loop]);
};
}
else {
$tplvars{category} = '';
}
}
else {
# Separate the output.
$cat_db->select_options("ORDER BY $opts->{cat_sb} $opts->{cat_so}") if $opts->{cat_sb};
$sth = $cat_db->select({ FatherID => $category->{ID} });
while (my $cat = $sth->fetchrow_hashref) {
$cat->{URL} = "$CFG->{build_root_url}/" . $cat_db->as_url($cat->{Full_Name}) . "/" . ($CFG->{build_index_include} ? $CFG->{build_index} : '');
push @cat_loop, $cat;
}
if (@cat_loop) {
my $print_cat;
$tplvars{category} = sub {
return $print_cat if defined $print_cat;
return $print_cat = Links::SiteHTML::display('print_cat', [$category, @cat_loop]);
};
}
else {
$tplvars{category} = '';
}
$tplvars{related} = '';
$tplvars{related_loop} = [];
my %related_ids = $related_db->select(qw/RelatedID RelationName/ => { CategoryID => $category->{ID} })->fetchall_list;
if (keys %related_ids) {
$cat_db->select_options("ORDER BY $opts->{cat_sb} $opts->{cat_so}") if $opts->{cat_sb};
my $sth = $cat_db->select({ ID => [keys %related_ids] });
while (my $cat = $sth->fetchrow_hashref) {
my $url = $CFG->{build_root_url} . "/" . $cat_db->as_url($cat->{Full_Name}) . "/" . ($CFG->{build_index_include} ? $CFG->{build_index} : '');
$cat->{URL} = $url;
$cat->{RelationName} = $related_ids{$cat->{ID}};
push @{$tplvars{related_loop}}, $cat;
$tplvars{related} .= qq|<li><a href="$url">| . ($related_ids{$cat->{ID}} || $cat->{Full_Name}) . "</a></li>";
}
}
}
# Plugins can use the build_category_loop hook to change the category
# results before they are returned to the template.
$PLG->dispatch(build_category_loop => sub { } => \@cat_loop);
# Get the header and footer from file if it exists, otherwise assume it is html.
if ($tplvars{header} and $tplvars{header} =~ /^\S{1,20}$/ and -e "$CFG->{admin_root_path}/headers/$tplvars{header}") {
local (@ARGV, $/) = "$CFG->{admin_root_path}/headers/$tplvars{header}";
$tplvars{header} = <>;
}
if ($tplvars{footer} and $tplvars{footer} =~ /^\S{1,20}$/ and -e "$CFG->{admin_root_path}/footers/$tplvars{footer}") {
local (@ARGV, $/) = "$CFG->{admin_root_path}/footers/$tplvars{footer}";
$tplvars{footer} = <>;
}
# If we are spanning pages, figure out toolbars and such.
if ($CFG->{build_span_pages}) {
my $lpp = $CFG->{build_links_per_page};
my $nh = $opts->{nh};
my $url = $CFG->{build_root_url} . "/" . $clean_name;
$tplvars{next} = $tplvars{prev} = "";
if ($numlinks > ($nh * $lpp)) {
$tplvars{next} = "$url/$CFG->{build_more}" . ($nh + 1) . "$CFG->{build_extension}";
}
if ($nh == 2) {
$tplvars{prev} = "$url/" . ($CFG->{build_index_include} ? $CFG->{build_index} : '');
}
elsif ($nh > 2) {
$tplvars{prev} = "$url/$CFG->{build_more}" . ($nh - 1) . "$CFG->{build_extension}";
}
if ($tplvars{next} or $tplvars{prev}) {
$tplvars{next_span} = build('toolbar', { url => $url, numlinks => $numlinks, nh => $nh });
$tplvars{paging} = {
page => "$clean_name/",
page_format => 1,
num_hits => $numlinks,
max_hits => $opts->{mh},
current_page => $opts->{nh}
};
}
}
return Links::SiteHTML::display('category', \%tplvars);
}
END_OF_SUB
$COMPILE{build_reset_hits} = __LINE__ . <<'END_OF_SUB';
sub build_reset_hits {
# ------------------------------------------------------------------
# Remove old hit and rate tracking information.
#
Links::init_date();
my $delete_by = GT::Date::date_get(time - 2*24*60*60, '%yyyy%-%mm%-%dd%');
my $click = $DB->table('ClickTrack');
$click->delete(GT::SQL::Condition->new('Created', '<', $delete_by));
}
END_OF_SUB
$COMPILE{build_orphan_check} = __LINE__ . <<'END_OF_SUB';
sub build_orphan_check {
# ------------------------------------------------------------------
# Checks for orphaned links and returns a list of ids.
#
my $opts = shift;
my $sel = $opts->{select} || 'ID';
my $rel = $DB->table('Links', 'CatLinks');
my $sth = $rel->select('left_join', $sel, { LinkID => undef });
my @output;
if ($opts->{select}) {
while (my $link = $sth->fetchrow_hashref) {
push @output, $link;
}
}
else {
while (my $id = $sth->fetchrow_array) {
push @output, $id;
}
}
return @output;
}
END_OF_SUB
$COMPILE{build_catlinks_orphan_check} = __LINE__ . <<'END_OF_SUB';
sub build_catlinks_orphan_check {
# ------------------------------------------------------------------
# Checks for orphaned CatLinks and returns a list of CategoryID/LinkIDs.
#
return (@{$DB->table('CatLinks', 'Links')->select('left_join', 'CategoryID', 'LinkID', { ID => undef })->fetchall_hashref},
@{$DB->table('CatLinks', 'Category')->select('left_join', 'CategoryID', 'LinkID', { ID => undef })->fetchall_hashref});
}
END_OF_SUB
$COMPILE{build_new_flags} = __LINE__ . <<'END_OF_SUB';
sub build_new_flags {
# ------------------------------------------------------------------
# Update the isNew flag based on Add_Date.
#
Links::init_date();
my $opts = shift || {};
my $date = GT::Date::date_sub(GT::Date::date_get(), $CFG->{build_new_cutoff});
my $links = $DB->table('Links');
$links->indexing(0);
my $cond = GT::SQL::Condition->new(
isNew => '=' => 'Yes',
Add_Date => '<' => $date,
VIEWABLE
);
# Unset isNew for links which aren't new anymore
my $old_rows = $links->count($cond);
if ($old_rows) {
$links->update({ isNew => 'No' }, $cond, { GT_SQL_SKIP_CHECK => 1 });
}
# Find links which need isNew set, but isn't set (the Links table subclass
# already update's the Category table's Has_New_Links flag)
$cond = GT::SQL::Condition->new(
isNew => '=' => 'No',
Add_Date => '>=' => $date,
VIEWABLE
);
my $new_rows = $links->count($cond);
if ($new_rows) {
$links->update({ isNew => 'Yes' }, $cond, { GT_SQL_SKIP_CHECK => 1 });
}
# Update the category flags
if ($new_rows or $old_rows or $opts->{reset}) {
my $links_cl = $DB->table('Links', 'CatLinks');
my $category = $DB->table('Category');
$category->indexing(0);
my %stalelinks = map { $_ => 1 } $category->select('ID', { Has_New_Links => 'Yes' })->fetchall_list;
$links_cl->select_options('ORDER BY Add_Date ASC');
my $sth = $links_cl->select('CategoryID', 'Add_Date', { isNew => 'Yes' }, VIEWABLE);
my %updated;
while (my ($cat_id, $date) = $sth->fetchrow) {
next if exists $updated{$cat_id};
my @ids = ($cat_id, @{$category->parents($cat_id)});
$category->update(
{ Newest_Link => $date, Has_New_Links => 'Yes' },
{ ID => \@ids, Has_New_Links => 'No' },
{ GT_SQL_SKIP_CHECK => 1 }
);
for (@ids) {
delete $stalelinks{$_};
$updated{$_}++;
}
}
# Whatever's left in %stalelinks are the categories which don't have new links anymore
$category->update({ Has_New_Links => 'No' }, { ID => [keys %stalelinks] }, { GT_SQL_SKIP_CHECK => 1 });
$category->indexing(1);
}
$links->indexing(1);
return $new_rows + $old_rows;
}
END_OF_SUB
$COMPILE{build_changed_flags} = __LINE__ . <<'END_OF_SUB';
sub build_changed_flags {
# ------------------------------------------------------------------
# Update isChanged flags, based on Mod_Date.
#
Links::init_date();
my $opts = shift || {};
my $date = GT::Date::date_sub(GT::Date::date_get(), $CFG->{build_new_cutoff});
my $links = $DB->table('Links');
$links->indexing(0);
my $cond = GT::SQL::Condition->new(
isChanged => '=' => 'Yes',
VIEWABLE,
GT::SQL::Condition->new(
Mod_Date => '<' => $date,
isNew => '=' => 'Yes',
'OR'
)
);
# Unset isChanged for links which aren't 'new' anymore
my $old_rows = $links->count($cond);
if ($old_rows) {
$links->update({ isChanged => 'No' }, $cond, { GT_SQL_SKIP_CHECK => 1 });
}
# Find links which need isChanged set
$cond = GT::SQL::Condition->new(
isChanged => '=' => 'No',
Mod_Date => '>=' => $date,
Add_Date => '<' => $date,
VIEWABLE
);
my $new_rows = $links->count($cond);
if ($new_rows) {
$links->update({ isChanged => 'Yes' }, $cond, { GT_SQL_SKIP_CHECK => 1 });
}
# Update the category flags
if ($new_rows or $old_rows or $opts->{reset}) {
my $category = $DB->table('Category');
$category->indexing(0);
my %stalelinks = map { $_ => 1 } $category->select('ID', { Has_Changed_Links => 'Yes' })->fetchall_list;
my $sth = $DB->table('Links', 'CatLinks')->select('CategoryID', 'Add_Date', { isChanged => 'Yes' });
my %updated;
while (my ($cat_id, $date) = $sth->fetchrow_array) {
next if exists $updated{$cat_id};
my @ids = ($cat_id, @{$category->parents($cat_id)});
$category->update(
{ Has_Changed_Links => 'Yes' },
{ ID => \@ids, Has_Changed_Links => 'No' },
{ GT_SQL_SKIP_CHECK => 1 }
);
for (@ids) {
delete $stalelinks{$_};
$updated{$_}++;
}
}
# Whatever's left in %stalelinks are the categories which don't have changed links anymore
$category->update({ Has_Changed_Links => 'No' }, { ID => [keys %stalelinks] }, { GT_SQL_SKIP_CHECK => 1 });
$category->indexing(1);
}
$links->indexing(1);
return $new_rows + $old_rows;
}
END_OF_SUB
$COMPILE{build_cool_flags} = __LINE__ . <<'END_OF_SUB';
sub build_cool_flags {
# ------------------------------------------------------------------
# Update the isPopular flag determined on number of Hits.
#
# Work out based either on percentage, or top x links.
my $link_db = $DB->table('Links');
$GRAND_TOTAL ||= _grand_total();
my $limit = int($CFG->{build_pop_cutoff} > 1 ? $CFG->{build_pop_cutoff} : $CFG->{build_pop_cutoff} * $GRAND_TOTAL) || 1;
# Select the popular links and update the flags if neccessary.
$link_db->indexing(0);
$link_db->select_options("ORDER BY Hits DESC", "LIMIT $limit");
my $sth = $link_db->select(
'ID', 'isPopular',
GT::SQL::Condition->new(
Hits => '>=' => 2,
VIEWABLE
)
);
my (@ids, @update_pop);
while (my ($id, $ispop) = $sth->fetchrow) {
push @ids, $id;
push @update_pop, $id unless $ispop eq 'Yes';
}
$link_db->update({ isPopular => 'Yes' }, { ID => \@update_pop }, { GT_SQL_SKIP_CHECK => 1 })
if @update_pop;
# Change any links that aren't popular back.
my $cond = GT::SQL::Condition->new('isPopular', '=', 'Yes');
$cond->add(ID => '!=' => \@ids) if @ids;
$link_db->update({ isPopular => 'No' }, $cond);
$link_db->indexing(1);
}
END_OF_SUB
$COMPILE{build_title} = __LINE__ . <<'END_OF_SUB';
sub build_title {
# ------------------------------------------------------------------
# Generate a title. Unlike build_title_{un,}linked, returns something more
# useful: an array ref of hashes of Name and URL.
# The 2nd argument is an optional override URL for the last item.
#
my ($input, $url) = @_;
my $cat = $DB->table('Category');
my $top = Links::language('LINKS_TOP');
my @cats = ({ Name => $top, URL => "$CFG->{build_root_url}/" . ($CFG->{build_home} || ($CFG->{build_index_include} ? $CFG->{build_index} : '')) });
my @si = split /\//, $input;
for (0 .. $#si) {
my $curl;
if ($_ == 0 and $si[$_] eq Links::language('LINKS_NEW')) {
$curl = "$CFG->{build_new_url}/";
}
elsif ($_ == $#si and $url) {
$curl = $url;
}
else {
$curl = "$CFG->{build_root_url}/" . $cat->as_url(join "/", @si[0 .. $_]) . "/" . ($CFG->{build_index_include} ? $CFG->{build_index} : '');
}
push @cats, { Name => $si[$_], URL => $curl };
}
return \@cats;
}
END_OF_SUB
$COMPILE{build_title_linked} = __LINE__ . <<'END_OF_SUB';
sub build_title_linked {
# ------------------------------------------------------------------
# Generate a linked title.
#
my $input = shift;
my $complete = 0;
my $home = 1;
if (ref $input) {
$complete = $input->{complete} || 0;
$home = defined $input->{home} ? $input->{home} : 1;
$input = $input->{name};
}
my $db = $DB->table('Category');
my $top = Links::language('LINKS_TOP');
my @dirs = split /\//, $input;
my $last;
$last = pop @dirs unless $complete;
my @paths;
push @paths, qq{<a href="$CFG->{build_root_url}/} . ($CFG->{build_home} || ($CFG->{build_index_include} ? $CFG->{build_index} : '')) . qq{">$top</a>} if $home;
for (0 .. $#dirs) {
my $path = "/" . $db->as_url(join "/", @dirs[0 .. $_]);
push @paths, qq{<a href="$CFG->{build_root_url}$path/} . ($CFG->{build_index_include} ? $CFG->{build_index} : '') . qq{">$dirs[$_]</a>};
}
push @paths, $last unless $complete;
return join ': ', @paths;
}
END_OF_SUB
$COMPILE{build_title_unlinked} = __LINE__ . <<'END_OF_SUB';
sub build_title_unlinked {
# --------------------------------------------------------
# Returns a string of the current category broken up by section.
# Useful for printing in the title.
#
my $input = shift;
my $output = join ': ', split /\//, $input;
return $output;
}
END_OF_SUB
$COMPILE{build_toolbar} = __LINE__ . <<'END_OF_SUB';
sub build_toolbar {
# --------------------------------------------------------
# Create an Altavista style toolbar for the next and previous pages.
#
my $opts = shift;
my $root_url = $opts->{url};
my $first_url = $opts->{first_url} || "$root_url/" . ($CFG->{build_index_include} ? $CFG->{build_index} : '');
my $next_url = $opts->{next_url} || "$root_url/$CFG->{build_more}";
my $numhits = $opts->{numlinks};
my $nh = $opts->{nh};
my $maxhits = $opts->{mh} || $CFG->{build_links_per_page};
my ($next_hit, $prev_hit) = ($nh + 1, $nh - 1);
# First, set how many pages we have on the left and the right.
my ($left, $right) = ($nh, int($numhits / $maxhits) - $nh);
# Then work out what page number we can go above and below.
my $lower = $left > 7 ? $left - 7 : 1;
my $upper = $right > 7 ? $nh + 7 : int($numhits/$maxhits) + 1;
# Finally, adjust those page numbers if we are near an endpoint.
$upper += 8 - $nh if 7 - $nh >= 0;
$lower -= $nh - int($numhits/$maxhits - 7) - 1 if $nh > $numhits / $maxhits - 7;
my $url = "";
# Then let's go through the pages and build the HTML.
$url .= $nh == 2 ? qq~<a href="$first_url">[<<]</a> ~ : qq~<a href="$next_url$prev_hit$CFG->{build_extension}">[<<]</a> ~
if $nh > 1;
for (my $i = 1; $i <= int($numhits/$maxhits) + 1; $i++) {
if ($i < $lower) { $url .= " ... "; $i = $lower - 1; next }
if ($i > $upper) { $url .= " ... "; last }
$url .= $i == $nh
? "$i "
: $i == 1
? qq~<a href="$first_url">$i</a> ~
: qq~<a href="$next_url$i$CFG->{build_extension}">$i</a> ~;
if ($i * $maxhits == $numhits) { $next_hit = $i if $nh == $i; last }
}
$url .= qq~<a href="$next_url$next_hit$CFG->{build_extension}">[>>]</a> ~
unless $next_hit == $nh or $nh * $maxhits > $numhits;
$url;
}
END_OF_SUB
$COMPILE{build_search_toolbar} = __LINE__ . <<'END_OF_SUB';
sub build_search_toolbar {
# --------------------------------------------------------
# Create an Altavista style toolbar for the next and previous pages.
#
my $opts = shift;
my $root_url = $opts->{url};
my $numhits = $opts->{numlinks};
my $nh = $opts->{nh};
my $maxhits = $opts->{mh} || $CFG->{build_links_per_page};
my ($next_hit, $prev_hit) = ($nh + 1, $nh - 1);
# First, set how many pages we have on the left and the right.
my ($left, $right) = ($nh, int($numhits / $maxhits) - $nh);
# Then work out what page number we can go above and below.
my $lower = $left > 7 ? $left - 7 : 1;
my $upper = $right > 7 ? $nh + 7 : int($numhits/$maxhits) + 1;
# Finally, adjust those page numbers if we are near an endpoint.
$upper += 8 - $nh if 7 - $nh >= 0;
$lower -= $nh - int($numhits/$maxhits - 7) - 1 if $nh > $numhits / $maxhits - 7;
my $url = "";
# Then let's go through the pages and build the HTML.
$url .= qq~<a href="$root_url;nh=$prev_hit">[<<]</a> ~ if $nh > 1;
for (my $i = 1; $i <= int($numhits/$maxhits) + ($numhits % $maxhits ? 1 : 0); $i++) {
if ($i < $lower) { $url .= " ... "; $i = $lower - 1; next }
if ($i > $upper) { $url .= " ... "; last }
$url .= $i == $nh ? "$i " : qq~<a href="$root_url;nh=$i">$i</a> ~;
if ($i * $maxhits == $numhits) { $next_hit = $i if $nh == $i; last }
}
$url .= qq~<a href="$root_url;nh=$next_hit">[>>]</a> ~
unless $next_hit == $nh or $nh * $maxhits > $numhits;
$url;
}
END_OF_SUB
sub _compile {
# -------------------------------------------------------------------
# Compile will check %COMPILE for the subroutine, and if it exists compile
# it (with an eval). Returns a code reference.
#
my $sub = shift;
if (exists $COMPILE{$sub}) {
GT::AutoLoader::_compile(\%COMPILE, __PACKAGE__, $sub) if $COMPILE{$sub};
return *{$Links::Build::{$sub}}{CODE};
}
else {
die "Invalid method: $sub";
}
}
sub _grand_total {
# -------------------------------------------------------------------
# Calculates the total in three queries as it can be significantly faster
# on large db's.
#
# Create a category object just to make sure the db maintenance code has run:
$DB->table('Category');
my $total = $DB->table('CatLinks')->count - $DB->table('Links')->count({ isValidated => 'No' });
if ($CFG->{payment}->{enabled}) {
$total -= $DB->table('Links')->count(
GT::SQL::Condition->new(
ExpiryDate => '<' => time,
isValidated => '=' => 'Yes'
)
);
}
return $total;
}
1;