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

623 lines
20 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: Newsletter.pm,v 1.15 2007/09/06 01:43:45 brewt Exp $
#
# Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved.
# Redistribution in part or in whole strictly prohibited. Please
# see LICENSE file for full details.
# ==================================================================
# Notes about the Newsletter code:
# ================================
# Example category structure:
# a
# b
# c
# d
# If a user is subscribed to a category (eg. category a), then they will
# be automatically subscribed to all the subcategories of that category
# (ie. b, c, d). If the user is already subscribed to a subcategory
# (eg. b), then that subscription will be removed when they subscribe to
# a parent category (ie. a). This keeps listing subscribed categories
# simple.
#
# Remember that the root category (0) is a special category and needs to be
# handled appropriately. It is not a real category as it does not exist in
# the Category table.
package Links::Newsletter;
use strict;
use Links qw/:objects/;
use Links::SiteHTML;
use GT::Dumper;
sub handle {
# ---------------------------------------------------
# Determine what to do.
#
my $res;
my $action = lc $IN->param('action');
require Links::Build;
my $mtl = Links::Build::build('title', Links::language('LINKS_NEWSLETTER'), "$CFG->{db_cgi_url}/subscribe.cgi");
# Custom lists
if ($IN->param('list')) {
my $email = $IN->param('email');
if ($email and $action eq 'subscribe') {
$res = $PLG->dispatch('custom_list_subscribe', \&custom_list_subscribe);
}
elsif ($email and $action eq 'unsubscribe') {
$res = $PLG->dispatch('custom_list_unsubscribe', \&custom_list_unsubscribe);
}
else {
$res = { error => Links::language('SUBSCRIBE_ERROR') };
}
$res->{main_title_loop} ||= $mtl;
print $IN->header();
print Links::SiteHTML::display('newsletter', $res);
}
# With the old Newsletter code, anyone could sign up to it. This is bad since
# no e-mail validation is performed. The new code will only allow signed up
# users to sign up.
unless ($USER) {
print $IN->redirect(Links::redirect_login_url('subscribe'));
return;
}
my $page;
if ($CFG->{newsletter_global_subscribe}) {
$page = 'newsletter_global';
if ($action eq 'subscribe') {
$res = $PLG->dispatch('newsletter_global_sub', \&global_subscribe);
}
elsif ($action eq 'unsubscribe') {
$res = $PLG->dispatch('newsletter_global_unsub', \&global_unsubscribe);
}
}
elsif ($action eq 'list') {
$page = 'newsletter_list';
}
elsif ($action eq 'unsubscribe') {
$res = $PLG->dispatch('newsletter_unsubscribe', \&unsubscribe);
$page = $IN->param('page') || 'newsletter';
}
elsif ($action eq 'subscribe') {
$res = $PLG->dispatch('newsletter_subscribe', \&subscribe);
$page = 'newsletter';
}
elsif ($action eq 'update') {
$res = $PLG->dispatch('newsletter_update', \&update_subscription);
$page = 'newsletter_browse';
}
else {
$page = 'newsletter_browse';
}
$res->{main_title_loop} ||= $mtl;
print $IN->header();
print Links::SiteHTML::display($page, $res);
}
sub custom_list_subscribe {
# ---------------------------------------------------
# Subscribe to a custom list
#
my $list = $IN->param('list');
my $email = $IN->param('email');
my $mli = $DB->table('MailingListIndex');
my $ml = $DB->table('MailingList');
unless ($mli->count({ Name => $list })) {
return { error => Links::language('SUBSCRIBE_INVALIDLIST', $list) };
}
my $id = $mli->select('ID', { Name => $list })->fetchrow;
if ($ml->count({ Email => $email, ID => $id })) {
return { error => Links::language('SUBSCRIBE_ALREADYSUB') };
}
$ml->insert({ Email => $email, ID => $id });
return { message => Links::language('SUBSCRIBE_SUCCESS') };
}
sub custom_list_unsubscribe {
# ---------------------------------------------------
# Unsubscribe from a custom list
#
my $list = $IN->param('list');
my $email = $IN->param('email');
my $mli = $DB->table('MailingListIndex');
my $ml = $DB->table('MailingList');
unless ($mli->count({ Name => $list })) {
return { error => Links::language('SUBSCRIBE_INVALIDLIST', $list) };
}
my $id = $mli->select('ID', { Name => $list })->fetchrow;
unless ($ml->count({ Email => $email, ID => $id })) {
return { error => Links::language('SUBSCRIBE_NOTSUB') };
}
$ml->delete({ Email => $email, ID => $id });
return { message => Links::language('SUBSCRIBE_UNSUBSUCCESS') };
}
sub global_subscribe {
# ---------------------------------------------------
# Global subscribe to the newsletter. If the admin option is enabled, then this
# will behave like the newsletter did in 2.x, where there is only one global
# newsletter. The only difference is that only registered users can subscribe.
#
my $ns = $DB->table('NewsletterSubscription');
if ($ns->count({ UserID => $USER->{Username}, CategoryID => 0 })) {
return { error => Links::language('NEWSLETTERERR_ALREADYSUB') };
}
_subscribe(0);
return { message => Links::language('NEWSLETTER_SUBSCRIBED') };
}
sub global_unsubscribe {
# ---------------------------------------------------
# Unsubscribe from the newsletter.
#
my $ns = $DB->table('NewsletterSubscription');
if ($ns->count({ UserID => $USER->{Username}, CategoryID => 0 })) {
_unsubscribe(0);
return { message => Links::language('NEWSLETTER_UNSUBSCRIBED') };
}
return { error => Links::language('NEWSLETTERERR_NOTSUB') };
}
sub global_subscribe_info {
# ---------------------------------------------------
# Returns information about the user's newsletter subscription.
#
return { subscribed => $DB->table('NewsletterSubscription')->count({ UserID => $USER->{Username}, CategoryID => 0 }) };
}
sub list_subscribed {
# ---------------------------------------------------
# Returns a list of categories they are subscribed to.
#
my $ns = $DB->table('NewsletterSubscription');
my $nsc = $DB->table('NewsletterSubscription', 'Category');
if ($ns->count({ UserID => $USER->{Username}, CategoryID => 0 })) {
return { subscribed => [_root()] };
}
$nsc->select_options("ORDER BY Full_Name");
my $list = $nsc->select({ UserID => $USER->{Username} })->fetchall_hashref;
return { subscribed => $list };
}
sub unsubscribe {
# ---------------------------------------------------
# Unsubscribe from one or more categories.
#
my @unsub = $IN->param('ID');
@unsub = @_ unless @unsub;
return { error => Links::language('NEWSLETTERERR_NOCATSUB') } unless @unsub;
_unsubscribe(@unsub);
return { message => Links::language('NEWSLETTER_CATUNSUB') };
}
sub subscribe {
# ---------------------------------------------------
# Subscribe to one or more categories.
#
my @sub = $IN->param('ID');
@sub = @_ unless @sub;
return { error => Links::language('NEWSLETTER_NOCATUNSUB') } unless @sub;
_subscribe(@sub);
return { message => Links::language('NEWSLETTER_CATSUB') };
}
sub update_subscription {
# ---------------------------------------------------
# Update a User's category subscriptions from their browse selection.
#
# These should be the original subscribe states of the categories. S<ID> are the
# categories which they wish to be subscribed to.
my @presub = $IN->param('subscribed');
my @preunsub = $IN->param('unsubscribed');
my (@sub, @unsub);
for (@presub) {
next if $_ =~ /\D/;
push @unsub, $_ unless defined $IN->param("S$_");
}
_unsubscribe(@unsub);
for (@preunsub) {
next if $_ =~ /\D/;
push @sub, $_ if defined $IN->param("S$_");
}
_subscribe(@sub);
return { message => Links::language('NEWSLETTER_CATUPDATED') };
}
sub browse {
# ---------------------------------------------------
# Browse the categories.
#
my $root = $IN->param('root') || 0;
my $cat = $DB->table('Category');
my $ns = $DB->table('NewsletterSubscription');
if ($root != 0 and not $cat->count({ ID => $root })) {
$root = 0;
}
my $root_cat;
if ($root == 0) {
$root_cat = _root();
$root_cat->{CatDepth} = -1;
}
else {
$root_cat = $cat->select({ ID => $root })->fetchrow_hashref or return { error => $GT::SQL::error };
}
my $tree = $cat->tree;
my $cats;
# When root = 0, max_depth is kind of weird because there isn't actually a Category with ID = 0.
# Because of this GT::SQL::Tree doesn't handle the case where max_depth = 1 and root = 0, so
# we'll handle it ourselves.
if ($root == 0 and $CFG->{newsletter_max_depth} == 1) {
$cat->select_options("ORDER BY Full_Name");
$cats = $cat->select({ FatherID => 0 })->fetchall_hashref;
}
else {
$cats = $tree->children(id => $root, max_depth => ($root == 0 ? $CFG->{newsletter_max_depth} - 1 : $CFG->{newsletter_max_depth}), sort_col => 'Full_Name');
}
# Insert the root category as the first element
splice @$cats, 0, 0, $root_cat;
my @parents;
my %catids;
for (0 .. $#$cats) {
my $c = $cats->[$_];
# ID to $cats index mapping
$catids{$c->{ID}}->{index} = $_;
# List of children (only ones which are shown in the trimmed tree)
$catids{$c->{ID}}->{children} = [];
# Fix CatDepth to be relative to $root
if ($_) {
$c->{CatDepth} -= $root_cat->{CatDepth};
}
# Keep track of categories which could have sub categories (that are past max_depth)
if ($CFG->{newsletter_max_depth} > 0 and $c->{CatDepth} == $CFG->{newsletter_max_depth}) {
$catids{$c->{ID}}->{check_child}++;
}
else {
$c->{HasMoreChildren} = 0;
}
$c->{Subscribed} = 0;
# Find all the children
while (@parents and @parents > $c->{CatDepth}) {
my $p = pop @parents;
for (@parents) {
push @{$catids{$_}->{children}}, $p;
}
}
push @parents, $c->{ID};
}
while (@parents) {
my $p = pop @parents;
for (@parents) {
push @{$catids{$_}->{children}}, $p;
}
}
$cats->[0]->{CatDepth} = 0;
if (%catids) {
for (keys %catids) {
$cats->[$catids{$_}->{index}]->{Children} = $catids{$_}->{children};
}
# Figure out which categories the user has subscribed to
my @subscribed = $ns->select('CategoryID', { UserID => $USER->{Username}, CategoryID => [keys %catids] })->fetchall_list;
for (@subscribed) {
$cats->[$catids{$_}->{index}]->{Subscribed}++;
}
# Check to see which categories have sub categories
my @check = grep $catids{$_}->{check_child}, keys %catids;
if (@check) {
my $subcats = $tree->child_ids(id => \@check);
for (keys %$subcats) {
$cats->[$catids{$_}->{index}]->{HasMoreChildren} = @{$subcats->{$_}};
}
}
}
my %previous = (PPID => '');
my $parent_subscribed;
if ($root != 0) {
my @parents = @{$tree->parent_ids(id => $root)};
splice(@parents, 0, 0, 0);
$parent_subscribed = $ns->count({ UserID => $USER->{Username}, CategoryID => \@parents });
my $parent;
if (@parents < $CFG->{newsletter_max_depth}) {
$parent = $parents[0];
}
else {
$parent = $parents[-$CFG->{newsletter_max_depth}];
}
# Get the previous parent's info
if ($parent == 0) {
$parent = _root();
}
else {
$parent = $cat->select({ ID => $parent })->fetchrow_hashref;
}
%previous = map { "PP" . $_ => $parent->{$_} } keys %$parent;
}
return { %previous, category => $cats, parent_subscribed => $parent_subscribed };
}
sub admin_browse {
# ---------------------------------------------------
# Browse the categories (admin side).
#
my $root = $IN->param('root') || 0;
my $cat = $DB->table('Category');
my $ns = $DB->table('NewsletterSubscription');
if ($root != 0 and not $cat->count({ ID => $root })) {
$root = 0;
}
my $root_cat;
if ($root == 0) {
$root_cat = _root();
$root_cat->{CatDepth} = -1;
}
else {
$root_cat = $cat->select({ ID => $root })->fetchrow_hashref or return { error => $GT::SQL::error };
}
my $tree = $cat->tree;
my $cats;
# root (0) isn't a 'real' category in the tree, so we have to select it ourselves
if ($root == 0 and $CFG->{newsletter_max_depth} == 1) {
$cat->select_options("ORDER BY Full_Name");
$cats = $cat->select({ FatherID => 0 })->fetchall_hashref;
}
else {
$cats = $tree->children(id => $root, max_depth => ($root == 0 ? $CFG->{newsletter_max_depth} - 1 : $CFG->{newsletter_max_depth}), sort_col => 'Full_Name');
}
# Insert the root category as the first element of the list of categories
splice @$cats, 0, 0, $root_cat;
my %catids;
for (0 .. $#$cats) {
my $c = $cats->[$_];
# ID to $cats index mapping
$catids{$c->{ID}}->{index} = $_;
# List of children (only ones which are shown in the trimmed tree)
$catids{$c->{ID}}->{children} = [];
# Fix CatDepth to be relative to $root
if ($_) {
$c->{CatDepth} -= $root_cat->{CatDepth};
}
# Keep track of categories which could have sub categories (that are past max_depth)
if ($CFG->{newsletter_max_depth} > 0 and $c->{CatDepth} == $CFG->{newsletter_max_depth}) {
$catids{$c->{ID}}->{check_child}++;
}
else {
$c->{HasMoreChildren} = 0;
}
$c->{DirectSubscribers} = 0;
}
$cats->[0]->{CatDepth} = 0;
# Get a list of the root's parents (this is used twice below)
my @root_parents = $root == 0 ? () : (0, @{$tree->parent_ids(id => $root)});
if (%catids) {
# Calculate the number of direct subscribers for each category
my %subscribers;
$ns->select_options("GROUP BY CategoryID");
my $sth = $ns->select('CategoryID', 'COUNT(*)', { CategoryID => [@root_parents, keys %catids] });
while (my ($catid, $count) = $sth->fetchrow_array) {
if (exists $catids{$catid}) {
$cats->[$catids{$catid}->{index}]->{DirectSubscribers} = $count;
}
# Save the counts to calculate the total subscribers
$subscribers{$catid} = $count;
}
# Calculate the number of subscribers for each category (if a newsletter was
# sent to this category, it would go to this many people)
my $parents = $tree->parent_ids(id => [keys %catids]);
for my $catid (keys %$parents) {
for (@{$parents->{$catid}}, $catid) {
$cats->[$catids{$catid}->{index}]->{Subscribers} += $subscribers{$_};
}
$cats->[$catids{$catid}->{index}]->{Subscribers} += $subscribers{0} if $catid;
}
# Check to see which categories have sub categories
my @check = grep $catids{$_}->{check_child}, keys %catids;
if (@check) {
my $subcats = $tree->child_ids(id => \@check);
for (keys %$subcats) {
$cats->[$catids{$_}->{index}]->{HasMoreChildren} = @{$subcats->{$_}};
}
}
}
my %previous = (PPID => '');
if ($root != 0) {
my $parent;
if (@root_parents < $CFG->{newsletter_max_depth}) {
$parent = $root_parents[0];
}
else {
$parent = $root_parents[-$CFG->{newsletter_max_depth}];
}
# Get the previous parent's info
if ($parent == 0) {
$parent = _root();
}
else {
$parent = $cat->select({ ID => $parent })->fetchrow_hashref;
}
%previous = map { "PP" . $_ => $parent->{$_} } keys %$parent;
}
return { %previous, category => $cats };
}
sub subscriber_info {
# ---------------------------------------------------
# Returns information about the subscribers of a category.
#
my $catid = $IN->param('ID');
my $direct = $IN->param('direct');
my $cat = $DB->table('Category');
my $nsu = $DB->table('NewsletterSubscription', 'Users');
if (not defined $catid or not ($catid == 0 or $cat->count({ ID => $catid }))) {
return { error => 'Invalid ID' };
}
my $tree = $cat->tree;
my @parents = $direct || $catid == 0 ? ($catid) : (0, @{$tree->parent_ids(id => $catid)}, $catid);
$nsu->select_options("ORDER BY Username");
my $subscribers = $nsu->select({ CategoryID => \@parents })->fetchall_hashref;
return { subscribers => $subscribers };
}
sub subscription_info {
# ---------------------------------------------------
# Returns subscription information about a category.
# 0 = not subscribed
# 1 = indirectly subscribed (parent is subscribed)
# 2 = directly subscribed
#
my $catid = $IN->param('ID') || shift;
my $ns = $DB->table('NewsletterSubscription');
my $tree = $DB->table('Category')->tree;
if ($ns->count({ UserID => $USER->{Username}, CategoryID => $catid })) {
return { SubscriptionStatus => 2 };
}
if ($catid == 0) {
return { SubscriptionStatus => 0 };
}
my @parents = (0, @{$tree->parent_ids(id => $catid)});
my @pids = $ns->select('CategoryID', { UserID => $USER->{Username}, CategoryID => \@parents })->fetchall_list;
if (@pids) {
return { SubscriptionStatus => 1 };
}
return { SubscriptionStatus => 0 };
}
sub _root {
# ---------------------------------------------------
# Since there is no real root category, return what a select from the Category
# table would return.
#
my $ns = $DB->table('NewsletterSubscription');
return {
ID => 0,
Name => Links::language('NEWSLETTER_ROOTCAT'),
CatDepth => 0,
Full_Name => Links::language('NEWSLETTER_ROOTCAT'),
Description => '',
Subscribed => $USER->{Username} ? $ns->count({ UserID => $USER->{Username}, CategoryID => 0 }) : 0,
};
}
sub _subscribe {
# ---------------------------------------------------
# Subscribe to the categories passed in.
#
my @sub = @_;
return 0 unless @sub;
my $cat = $DB->table('Category');
my $ns = $DB->table('NewsletterSubscription');
my $tree = $cat->tree;
# Already subscribed to root category
if ($ns->count({ UserID => $USER->{Username}, CategoryID => 0 })) {
return 0;
}
@sub = sort { $a <=> $b } @sub;
if ($sub[0] == 0) {
@sub = (0);
}
else {
# Filter out the invalid category ID's
my @s = $cat->select('ID', { ID => \@sub })->fetchall_list;
# Filter out categories which are already subscribed to by being a subcat
@sub = ();
my $parents = $tree->parent_ids(id => \@s);
for (@s) {
unless (@{$parents->{$_}} and $ns->count({ UserID => $USER->{Username}, CategoryID => $parents->{$_} })) {
push @sub, $_;
}
}
}
return 0 unless @sub;
# Subscribing to the root, subscribes you to all, so remove any existing subscriptions.
$ns->delete({ UserID => $USER->{Username} }) if $sub[0] == 0;
$ns->insert_multiple([qw/UserID CategoryID/], map { [$USER->{Username}, $_] } @sub);
# Remove any subscribed subcats of the ones we just added
if ($sub[0] != 0) {
my $c = $tree->child_ids(id => \@sub);
my @subcats = map { @{$c->{$_}} } keys %$c;
if (@subcats) {
$ns->delete({ UserID => $USER->{Username}, CategoryID => \@subcats });
}
}
# FIXME need to take into account how many were deleted
return scalar @sub;
}
sub _unsubscribe {
# ---------------------------------------------------
# Unsubscribe from categories passed in. Returns the number of categories
# unsubscribed from.
#
my @unsub = @_;
return 0 unless @unsub;
return $DB->table('NewsletterSubscription')->delete({ UserID => $USER->{Username}, CategoryID => \@unsub });
}
1;