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