OSDN Git Service

Model: add compatibility modules
authorhylom <hylom@users.sourceforge.jp>
Tue, 25 Oct 2016 12:34:33 +0000 (21:34 +0900)
committerhylom <hylom@users.sourceforge.jp>
Tue, 25 Oct 2016 12:34:33 +0000 (21:34 +0900)
src/newslash_web/lib/Newslash/Model/LegacyFirehose.pm [new file with mode: 0644]
src/newslash_web/lib/Newslash/Model/SlashConstants.pm [new file with mode: 0755]
src/newslash_web/lib/Newslash/Model/SlashUtility.pm [new file with mode: 0644]

diff --git a/src/newslash_web/lib/Newslash/Model/LegacyFirehose.pm b/src/newslash_web/lib/Newslash/Model/LegacyFirehose.pm
new file mode 100644 (file)
index 0000000..dbfb113
--- /dev/null
@@ -0,0 +1,5682 @@
+# This code is a part of Slash, and is released under the GPL.
+# Copyright 1997-2009 by Geeknet, Inc. See README
+# and COPYING for more information, or see http://slashcode.com/.
+
+package Newslash::Model::LegacyFirehose;
+use base Newslash::Model::LegacyDB;
+
+=head1 NAME
+
+Slash::FireHose - Perl extension for FireHose
+
+
+=head1 SYNOPSIS
+
+       use Slash::FireHose;
+
+
+=head1 DESCRIPTION
+
+LONG DESCRIPTION.
+
+
+=head1 EXPORTED FUNCTIONS
+
+=cut
+
+use strict;
+use Data::Dumper;
+use Data::JavaScript::Anon;
+use Date::Calc qw(Days_in_Month Add_Delta_YMD Add_Delta_DHMS);
+use Digest::MD5 'md5_hex';
+use POSIX qw(ceil);
+use LWP::UserAgent;
+use URI;
+use URI::Split qw(uri_split);
+use Time::HiRes;
+use File::Spec::Functions qw(catdir);
+use File::Path qw(mkpath);
+
+#use Slash;
+#use Slash::Display;
+#use Slash::Utility;
+#use Slash::Slashboxes;
+#use Slash::Tags;
+#use Sphinx::Search 0.14;
+
+#use base 'Slash::Plugin';
+
+#our $VERSION = $Slash::Constants::VERSION;
+
+use constant {
+    SPH_SORT_ATTR_DESC => 0,
+      SPH_SORT_ATTR_ASC => 0,
+      SPH_MATCH_EXTENDED2 => 0,
+  };
+
+sub createFireHose {
+       my($self, $data) = @_;
+       $data->{dept} ||= "";
+       $data->{discussion} = 0 if !defined $data->{discussion} || !$data->{discussion};
+       $data->{-createtime} = "NOW()" if !$data->{createtime} && !$data->{-createtime};
+       $data->{discussion} ||= 0 if defined $data->{discussion};
+       $data->{popularity} ||= 0;
+       $data->{editorpop} ||= 0;
+       $data->{body_length} ||= $data->{bodytext} ? length($data->{bodytext}) : 0;
+       $data->{word_count} = countWords($data->{introtext}) + countWords($data->{bodytext});
+       $data->{mediatype} ||= "none";
+       $data->{email} ||= '';
+
+       my $text_data = {};
+       $text_data->{title} = delete $data->{title};
+       $text_data->{introtext} = delete $data->{introtext};
+       $text_data->{bodytext} = delete $data->{bodytext};
+       $text_data->{media} = delete $data->{media};
+
+       $self->sqlDo('SET AUTOCOMMIT=0');
+       my $ok = $self->sqlInsert("firehose", $data);
+       if (!$ok) {
+               warn "could not create firehose row, '$ok'";
+       }
+       if ($ok) {
+               $text_data->{id} = $self->getLastInsertId({ table => 'firehose', prime => 'id' });
+
+               $ok = $self->sqlInsert("firehose_text", $text_data);
+               if (!$ok) {
+                       warn "could not create firehose_text row for id '$text_data->{id}'";
+               }
+       }
+
+       if ($ok) {
+               $self->sqlDo('COMMIT');
+       } else {
+               $self->sqlDo('ROLLBACK');
+       }
+       $self->sqlDo('SET AUTOCOMMIT=1');
+
+       # set topics rendered appropriately
+       if ($ok) {
+               if ($data->{type} eq "story") {
+                       my $tids = $self->sqlSelectColArrayref("tid", "story_topics_rendered", "stoid='$data->{srcid}'");
+                       $self->setTopicsRenderedForStory($data->{srcid}, $tids);
+               } else {
+                       $self->setTopicsRenderedBySkidForItem($text_data->{id}, $data->{primaryskid});
+               }
+       }
+
+       return $text_data->{id};
+}
+
+sub createUpdateItemFromJournal {
+       my($self, $id) = @_;
+       my $journal_db = getObject("Slash::Journal");
+       my $journal = $journal_db->get($id);
+       if ($journal) {
+               my $globjid = $self->getGlobjidCreate("journals", $journal->{id});
+               my $globjid_q = $self->sqlQuote($globjid);
+               my($itemid) = $self->sqlSelect("id", "firehose", "globjid=$globjid_q");
+               if ($itemid) {
+                       my $bodytext  = $journal_db->fixJournalText($journal->{article}, $journal->{posttype}, $journal->{uid});
+                       my $introtext = $journal->{introtext} || $bodytext;
+
+                       my $body_length = 0;
+                       if ($introtext ne $bodytext) {
+                               $body_length = length($bodytext) - length($introtext);
+                               $body_length = 0 if $body_length < 0; # just in case
+                       }
+
+                       $self->setFireHose($itemid, {
+                               introtext   => $introtext,
+                               bodytext    => $bodytext,
+                               body_length => $body_length,
+                               title       => $journal->{description},
+                               tid         => $journal->{tid},
+                               discussion  => $journal->{discussion},
+                               word_count  => countWords($introtext)
+                       });
+                       $self->setFireHose($itemid, { public => "no" }) if ($self->getCurrentStatic("firehose_disable_to_show_publicized_journals") && $journal->{promotetype} eq "publicize");
+                       return $itemid;
+               } else {
+                       return $self->createItemFromJournal($id);
+               }
+       }
+}
+
+sub deleteHideFireHoseSection {
+       my($self, $id) = @_;
+       my $user = getCurrentUser();
+
+       my $cur_section = $self->getFireHoseSection($id);
+       return if $user->{is_anon} || !$cur_section;
+
+       if ($cur_section->{uid} == $user->{uid}) {
+               $self->sqlDelete("firehose_section", "fsid=$cur_section->{fsid}");
+       } elsif ($cur_section->{uid} == 0) {
+               $self->setFireHoseSectionPrefs($id, {
+                       display         => "no",
+                       section_name    => $cur_section->{section_name},
+                       section_filter  => $cur_section->{section_filter},
+                       view_id         => $cur_section->{view_id},
+               });
+       }
+}
+
+sub setFireHoseSectionPrefs {
+       my($self, $id, $data) = @_;
+       my $user = getCurrentUser();
+       my $constants = $self->getCurrentStatic();
+
+       my $cur_section = $self->getFireHoseSection($id);
+       return if $user->{is_anon} || !$cur_section;
+
+       if ( $data->{section_default} ) {
+               my $slashdb = getCurrentDB();
+               $slashdb->setUser($user->{uid}, {firehose_default_section => $cur_section->{fsid}});
+       }
+
+       if ($cur_section->{uid} == $user->{uid}) {
+
+               $self->sqlUpdate("firehose_section", $data, "fsid = $cur_section->{fsid}");
+       } elsif ($cur_section->{uid} == 0) {
+               if ($cur_section->{skid} == $constants->{mainpage_skid}) {
+                       $data->{section_name} = $cur_section->{section_name};
+               }
+               my $cur_prefs = $self->getSectionUserPrefs($cur_section->{fsid});
+               if ($cur_prefs) {
+                       $self->sqlUpdate("firehose_section_settings", $data, "id=$cur_prefs->{id}");
+               } else {
+                       $data->{uid} = $user->{uid};
+                       $data->{fsid} = $cur_section->{fsid};
+                       $self->sqlInsert("firehose_section_settings", $data);
+               }
+       }
+}
+
+sub setFireHoseViewPrefs {
+       my($self, $id, $data) = @_;
+       my $user = getCurrentUser();
+       my $constants = $self->getCurrentStatic();
+
+       return if $user->{is_anon};
+       my $uid_q = $self->sqlQuote($user->{uid});
+
+       my $cur_view = $self->getUserViewById($id);
+       return if $cur_view->{editable} eq "no";
+
+       if ($cur_view) {
+               my $cur_prefs = $self->getViewUserPrefs($cur_view->{id});
+               if ($cur_prefs) {
+                       $self->sqlUpdate("firehose_view_settings", $data, "id=$cur_prefs->{id} AND uid=$uid_q");
+               } else {
+                       $data->{uid} = $user->{uid};
+                       $data->{id} = $cur_view->{id};
+                       $self->sqlInsert("firehose_view_settings", $data);
+               }
+       }
+}
+
+sub removeUserPrefsForView {
+       my($self, $id) = @_;
+       my $user = getCurrentUser();
+       return if $user->{is_anon};
+
+       my $id_q = $self->sqlQuote($id);
+       my $uid_q = $self->sqlQuote($user->{uid});
+
+       $self->sqlDelete("firehose_view_settings", "id=$id_q and uid=$uid_q");
+}
+
+sub removeUserSections {
+       my($self, $id) = @_;
+       my $user = getCurrentUser();
+       return if $user->{is_anon};
+
+       my $uid_q = $self->sqlQuote($user->{uid});
+       $self->sqlDelete("firehose_section_settings", "uid=$uid_q");
+       $self->sqlDelete("firehose_section", "uid=$uid_q");
+       $self->setUser($user->{uid}, { firehose_default_section => undef });
+}
+
+sub getFireHoseFromDiscussion {
+       my($self, $disc) = @_;
+       $disc = ref $disc ? $disc : $self->getDiscussion($disc);
+
+       return 0 if !($disc && $disc->{id});
+
+       my $kinds = $self->getDescriptions('discussion_kinds');
+
+       my $kind = $kinds->{$disc->{dkid}};
+
+       if ($disc->{stoid} && ($kind eq 'story' || $kind eq 'journal_story')) {
+               return $self->getFireHoseByTypeSrcid('story', $disc->{stoid});
+       } elsif ($kind eq 'submission' || $kind eq 'feed' || $kind eq 'project') {
+               my($id) = $disc->{url} =~ /id=(\d+)/;
+               if ($id) {
+                       return $self->getFireHose($id);
+               }
+       } elsif ($kind eq 'journal') {
+               my $id = 0;
+               if($disc->{url} =~ m|/journal/(\d+)|) {
+                       $id = $1;
+               } elsif ($disc->{url} =~ /id=(\d+)/) {
+                       $id = $1;
+               }
+               return $self->getFireHoseByTypeSrcid('journal', $id) if $id
+       }
+       return 0;
+}
+
+sub getFireHoseSectionsMenu {
+       my($self, $fsid, $layout) = @_;
+       my $user = getCurrentUser();
+       my($uid_q) = $self->sqlQuote($user->{uid});
+       my $secure = apacheConnectionSSL();
+
+       $layout ||= 'yui';
+
+       my $layout_q = $self->sqlQuote($layout);
+
+       my $fsid_limit;
+       if ($fsid) {
+               my $fsid_q = $self->sqlQuote($fsid);
+               $fsid_limit = " AND firehose_section.fsid=$fsid_q ";
+       }
+
+       my $css = $self->sqlSelectAllHashrefArray(
+               "css.*, skins.name as skin_name, fsid",
+               "css, skins, firehose_section",
+               "css.skin=skins.name and css.layout=$layout_q and admin='no' AND skins.skid=firehose_section.skid AND firehose_section.uid=0"
+       );
+
+       my $css_hr = {};
+
+       foreach (@$css) {
+               push @{$css_hr->{$_->{fsid}}}, $_;
+       }
+
+
+       #XXXNEWTHEME deprecated - no longer utilizing user sections or settings, so replacing current user with 0 which will only return system section settings
+
+       my $sections = $self->sqlSelectAllHashrefArray(
+               "firehose_section.*, firehose_section_settings.display AS user_display, firehose_section_settings.section_name as user_section_name, firehose_section_settings.section_filter AS user_section_filter, firehose_section_settings.view_id AS user_view_id, firehose_section_settings.section_color AS user_section_color",
+               "firehose_section LEFT JOIN firehose_section_settings on firehose_section.fsid=firehose_section_settings.fsid AND firehose_section_settings.uid=0",
+               "firehose_section.uid in (0) $fsid_limit",
+               "ORDER BY uid, ordernum, section_name"
+       );
+
+       foreach (@$sections) {
+               $_->{data}{id}          = $_->{fsid};
+               $_->{data}{name}        = $_->{user_section_name} ? $_->{user_section_name} : $_->{section_name};
+               $_->{data}{filter}      = $_->{user_section_filter} ? $_->{user_section_filter} : $_->{section_filter};
+               my $viewid              = $_->{user_view_id} ? $_->{user_view_id} : $_->{view_id};
+
+               my $view = $self->getUserViewById($viewid);
+               my $viewname = $view->{viewname} || "stories";
+
+               $_->{data}{viewname}    = $viewname;
+               $_->{data}{color}       = $_->{user_section_color} ? $_->{user_section_color} : $_->{section_color};
+
+               if ( $_->{skid} && ((!$_->{user_section_filter}) || ($_->{section_filter} eq $_->{user_section_filter})) ) {
+                       if ($css_hr->{$_->{fsid}}) {
+                               foreach my $css(@{$css_hr->{$_->{fsid}}}) {
+                                       $css->{file} =~ s/\.css/.ssl.css/ if $secure;
+                                       $_->{data}{skin} .= getData('alternate_section_stylesheet', { css => $css, }, 'firehose');
+                               }
+                       }
+               }
+       }
+
+       if (!$user->{firehose_section_order}) {
+               return $sections;
+       } else {
+               my %sections_hash = map { $_->{fsid}  => $_ } @$sections;
+               my @ordered_sections;
+               foreach (split /,/, $user->{firehose_section_order}) {
+                       if ($sections_hash{$_}) {
+                               push @ordered_sections, delete $sections_hash{$_};
+                       }
+               }
+
+               foreach (@$sections) {
+                       push @ordered_sections, $_ if $sections_hash{$_->{fsid}}
+               }
+               return \@ordered_sections;
+       }
+}
+
+sub getMicrobinCounts {
+       my($self) = @_;
+       my $constants = $self->getCurrentStatic();
+       my $count = {};
+       my $tags = {
+               direct          => $constants->{twitter_bin_featured_tag},
+               feeds           => 'rss',
+               following       => 'follow',
+       };
+
+       foreach (keys %$tags) {
+               my $tag_q = $self->sqlQuote("\%$tags->{$_}\%");
+               $count->{$_} = $self->sqlCount("microbin", "tags like $tag_q AND active='yes'");
+       }
+       return $count;
+}
+
+sub getMicrobinItems {
+       my($self, $limit, $tag) = @_;
+       $limit ||= 20;
+       my $where_clause = 'active="yes"';
+       if ($tag) {
+               my $tag_q = $self->sqlQuote("\%$tag%");
+               $where_clause .= " AND tags like $tag_q";
+       }
+       my $items = $self->sqlSelectAllHashrefArray('*','microbin', $where_clause, "order by ts desc limit $limit");
+       return $items;
+}
+
+sub getMicrobinActiveCount {
+       my($self, $tag, $options) = @_;
+       my $where_clause = 'active="yes"';
+       if ($tag) {
+               my $tag_q = $self->sqlQuote("\%$tag%");
+               $where_clause .= " AND tags like $tag_q";
+       }
+       return $self->sqlCount("microbin", $where_clause);
+}
+
+sub microbinUpdatedSince {
+       my($self, $time) = @_;
+       my $time_q = $self->sqlQuote($time);
+       return $self->sqlCount("microbin","ts>=$time_q");
+}
+
+sub ajaxMicrobinFetch {
+       my($slashdb, $constants, $user, $form, $options) = @_;
+       my $fh = getObject("Slash::FireHose");
+       my $total_limit = 100;
+       my $tag = $form->{tag} || $constants->{twitter_bin_featured_tag};
+       
+       my $type;
+       if ($form->{type} eq 'narrow') {
+               $type = 'narrow';
+               $total_limit = 15;
+       }
+
+       my $items = $fh->getMicrobinItems($total_limit, $tag);
+       my $count = $fh->getMicrobinCounts();
+
+       my $html = {
+               'microbin-direct'       => "Direct ($count->{direct})",
+               'microbin-following'    => "Following ($count->{following})",
+               'microbin-feeds'        => "Feeds ($count->{feeds})",
+       };
+
+       my $pane_id;
+
+       if ($tag eq 'follow') {
+               $pane_id = 'bin-fragment-2';
+       } elsif ($tag eq 'rss') {
+               $pane_id = 'bin-fragment-3';
+       } else {
+               $pane_id = 'bin-fragment-1';
+       };
+
+       my $pane = slashDisplay("microbin", {
+               items           => $items,
+               type            => $type,
+               contentsonly    => 1,
+               tag             => $tag,
+       }, { Return => 1, Page => 'firehose'});
+
+       $html->{$pane_id} = $pane;
+
+       return Data::JavaScript::Anon->anon_dump({
+               html            => $html,
+       });
+
+}
+
+sub showMicrobin {
+       my($self, $limit, $type, $options) = @_;
+       $options ||= {};
+       my $constants = $self->getCurrentStatic();
+       my $total_limit = 15;
+       my $stub = $options->{stub} || '';
+
+       my $items = $self->getMicrobinItems($total_limit, $constants->{twitter_bin_featured_tag});
+       return slashDisplay("microbin", {
+               items           => $items,
+               type            => 'narrow',
+               stub            => $options->{stub}
+       }, { Return => 1, Page => 'firehose'});
+}
+
+sub createFireHoseSection {
+       my($self, $data) = @_;
+       $self->sqlInsert("firehose_section", $data);
+       return $self->getLastInsertId({ table => 'firehose_section', prime => 'fsid' });
+}
+
+sub ajaxSaveFireHoseSections {
+       my($slashdb, $constants, $user, $form, $options) = @_;
+       return if $user->{is_anon};
+       $slashdb->setUser($user->{uid}, {firehose_section_order => $form->{fsids}});
+}
+
+sub ajaxSaveHideSectionMenu {
+       my($slashdb, $constants, $user, $form, $options) = @_;
+       return if $user->{is_anon};
+       my $hide = $form->{hide_section_menu} && ($form->{hide_section_menu} ne 'false');
+       $slashdb->setUser($user->{uid}, {firehose_hide_section_menu => $hide});
+}
+
+sub ajaxSaveAutoUpdate {
+       my($slashdb, $constants, $user, $form, $options) = @_;
+       return if $user->{is_anon};
+       $slashdb->setUser($user->{uid}, {firehose_autoupdate => $form->{autoupdate}});
+}
+
+sub ajaxSetFireHoseDefaultSection {
+       my($slashdb, $constants, $user, $form, $options) = @_;
+       return if $user->{is_anon};
+       $slashdb->setUser($user->{uid}, {firehose_default_section => $form->{default_section}});
+}
+
+sub ajaxDeleteFireHoseSection {
+       my($slashdb, $constants, $user, $form, $options) = @_;
+       my $fh = getObject("Slash::FireHose");
+       if ($form->{id}) {
+               if ($form->{undo}) {
+                       $fh->setFireHoseSectionPrefs($form->{id}, { display => "yes" });
+               } else {
+                       $fh->deleteHideFireHoseSection($form->{id});
+               }
+       }
+}
+
+sub ajaxFirehoseSetDiscSystem {
+       my($slashdb, $constants, $user, $form, $options) = @_;
+
+       return if ($user->{is_anon} || !exists($form->{disctype2}) || ((lc($form->{disctype2}) ne 'true') && (lc($form->{disctype2}) ne 'false')));
+
+       my $r = (lc($form->{disctype2}) eq 'true')?'slashdot':'none';
+
+       $slashdb->setUser($user->{uid}, {discussion2 => $r});
+
+       my %data = ();
+       if ($r eq 'slashdot') {
+               $data{eval_first} = "getModalPrefSub('d2');";
+       } else {
+               $data{eval_first} = "getModalPrefSub('d1');";
+       }
+
+       return Data::JavaScript::Anon->anon_dump(\%data);
+}
+
+sub ajaxNewFireHoseSection {
+       my($slashdb, $constants, $user, $form, $options) = @_;
+       my $fh = getObject("Slash::FireHose");
+       return if $user->{is_anon};
+       my $data = {
+               section_name    => $form->{name},
+               section_color   => $form->{color},
+               section_filter  => $form->{fhfilter},
+               uid             => $user->{uid},
+               view_id         => $form->{view_id}||0
+       };
+
+       my $fsid = $fh->createFireHoseSection($data);
+
+       my $data_dump = {};
+
+       if ($fsid) {
+               if ( $data->{as_default} ) {
+                       $slashdb->setUser($user->{uid}, {firehose_default_section => $fsid});
+               }
+
+               my $res = $fh->getFireHoseSectionsMenu($fsid);
+               my $fh_section = $res->[0];
+
+               $data_dump =  Data::JavaScript::Anon->anon_dump({
+                       id      => $fsid,
+                       li      => getData('newsectionli', { id => $fsid, name => $form->{name}, fh_section => $fh_section }, 'firehose')
+               });
+       }
+       return $data_dump;
+}
+
+sub getSectionSkidFromFilter {
+       my($self, $filter) = @_;
+       my $got_section = 0;
+
+       foreach (split(/\s+/, $filter)) {
+               last if $got_section;
+               my $cur_skin = $self->getSkin($_);
+               if ($cur_skin) {
+                       $got_section = $cur_skin->{skid};
+               }
+       }
+       return $got_section;
+}
+
+sub getCSSForSkid {
+       my($self,$skid,$layout) = @_;
+       my $form = getCurrentForm();
+       my $secure = apacheConnectionSSL();
+
+       $layout = defined $layout ? $layout:
+               defined $form->{layout} ? $form->{layout} : "yui";
+
+       my $layout_q = $self->sqlQuote($layout);
+
+       my $css = [];
+       if ($skid) {
+               my $skid_q = $self->sqlQuote($skid);
+               $css = $self->sqlSelectAllHashrefArray(
+                       "css.*, skins.name as skin_name",
+                       "css, skins",
+                       "css.skin=skins.name and css.layout=$layout_q and admin='no' AND skins.skid=$skid_q"
+               );
+       }
+
+       my $retval = "";
+       my $skin_name = "";
+       foreach (@$css) {
+               $skin_name = $_->{skin_name};
+               $_->{file} =~ s/\.css/.ssl.css/ if $secure;
+               $retval .= getData('alternate_section_stylesheet', { css => $_, }, 'firehose');
+       }
+       return $retval;
+}
+
+sub ajaxFireHoseSectionCSS {
+       my($slashdb, $constants, $user, $form, $options) = @_;
+       my $fh = getObject("Slash::FireHose");
+       my $section = $fh->getFireHoseSection($form->{section});
+       my $got_section = 0;
+
+       if ($section && $section->{fsid}) {
+               $got_section = $fh->getSectionSkidFromFilter($section->{section_filter});
+       }
+
+       my $retval = $fh->getCSSForSkid($got_section, $form->{layout});
+       my $skin_name;
+       my $skin = $slashdb->getSkin($got_section);
+       $skin_name = $skin->{name};
+       my $data_dump =  Data::JavaScript::Anon->anon_dump({
+               skin_name               => $skin_name,
+               css_includes            => $retval,
+       });
+
+       return $data_dump;
+}
+
+sub ajaxTogglePickerSearch {
+       my($slashdb, $constants, $user, $form, $options) = @_;
+       return if $user->{is_anon};
+       $slashdb->setUser($user->{uid}, {firehose_disable_picker_search => undef});
+}
+
+sub ajaxToggleSmallScreen {
+       my($slashdb, $constants, $user, $form, $options) = @_;
+       return if $user->{is_anon};
+
+       my $prefs = {
+               smallscreen => undef,
+               firehose_noslashboxes => undef,
+               firehose_hide_section_menu => undef,
+               disable_ua_check       => 1,
+       };
+       $slashdb->setUser($user->{uid}, $prefs);
+}
+
+sub ajaxToggleSimpleDesign {
+       my($slashdb, $constants, $user, $form, $options) = @_;
+       return if $user->{is_anon};
+
+       my $prefs = {
+               simpledesign => undef,
+               firehose_noslashboxes => undef,
+               firehose_hide_section_menu => undef,
+               firehose_nographics => undef,
+       };
+       $slashdb->setUser($user->{uid}, $prefs);
+}
+
+sub getFireHoseSectionBySkid {
+       my($self, $skid) = @_;
+       my $skid_q = $self->sqlQuote($skid);
+       return $self->sqlSelectHashref("*", "firehose_section","uid=0 AND skid=$skid_q");
+}
+
+sub getFireHoseSection {
+       my($self, $fsid) = @_;
+
+       my $user = getCurrentUser();
+       my $uid_q = $self->sqlQuote($user->{uid});
+       my $fsid_q = $self->sqlQuote($fsid);
+
+       return $self->sqlSelectHashref("*","firehose_section","uid in(0,$uid_q) AND fsid=$fsid_q");
+}
+
+# XXXNEWTHEME deprecated, so just short-circuiting
+sub getSectionUserPrefs {
+       my($self, $fsid) = @_;
+       return;
+       #my $user = getCurrentUser();
+       #return if $user->{is_anon};
+       #my $fsid_q = $self->sqlQuote($fsid);
+       #my $uid_q = $self->sqlQuote($user->{uid});
+       # return $self->sqlSelectHashref("*", "firehose_section_settings", "uid=$uid_q AND fsid=$fsid_q");
+}
+
+sub getViewUserPrefs {
+       my($self, $id) = @_;
+       my $user = getCurrentUser();
+       return if $user->{is_anon};
+       my $id_q = $self->sqlQuote($id);
+       my $uid_q = $self->sqlQuote($user->{uid});
+       return $self->sqlSelectHashref("*", "firehose_view_settings", "uid=$uid_q AND id=$id_q");
+}
+
+{
+#my $constants = $self->getCurrentStatic();
+#my $color_str = $constants->{firehose_color_labels} || '';
+my $color_str = 'red|orange|yellow|green|blue|indigo|violet|black';
+my $color_a = [ split(/\|/, $color_str) ];
+my $color_h = {};
+my $i = 0;
+$color_h->{$_} = ++$i for @$color_a;
+
+sub getFireHoseColors {
+       my($self, $array) = @_;
+       return $color_a if $array;
+       return $color_h;
+}
+
+sub getFireHoseColor {
+       my($self, $color) = @_;
+       if ($color =~ /\D/) {
+               return $color_h->{$color} || scalar @$color_a;
+       } else {
+               my $i = $color-1;
+               $i = 0 if $i < 0;
+               return $color_a->[$i] || $color_a->[-1];
+       }
+}
+
+sub getFireHoseColorDelta {
+       my($self, $start, $delta) = @_;
+       my $color = $start =~ /\D/ ? $self->getFireHoseColor($start) : $start;
+
+       my $i = ($color-1) + $delta;
+       $i = 0 if $i < 0;
+       return $color_a->[$i] || $color_a->[-1];
+}
+}
+
+sub createUpdateItemFromComment {
+       my($self, $cid) = @_;
+       my $comment = $self->getComment($cid);
+       my $text = $self->getCommentText($cid);
+
+       my $item = $self->getFireHoseByTypeSrcid("comment", $cid);
+       my $fhid;
+
+       if ($item && $item->{id}) {
+               # update item or do nothing
+               $fhid = $item->{id};
+       } else {
+               $fhid = $self->createItemFromComment($cid);
+       }
+       return $fhid;
+}
+
+sub createItemFromComment {
+       my($self, $cid) = @_;
+       my $comment = $self->getComment($cid);
+       my $text = $self->getCommentText($cid);
+       my $globjid = $self->getGlobjidCreate("comments", $cid);
+
+       # Set initial popularity scores -- we'll be forcing a quick
+       # recalculation of them so these scores don't much matter.
+       my($popularity, $editorpop, $neediness);
+       $popularity = $self->getEntryPopularityForColorLevel(7);
+       $editorpop = $self->getEntryPopularityForColorLevel(7);
+       $neediness = $self->getEntryPopularityForColorLevel(6);
+
+       my $data = {
+               uid             => $comment->{uid},
+               public          => "yes",
+               title           => $comment->{subject},
+               introtext       => $text,
+               ipid            => $comment->{ipid},
+               subnetid        => $comment->{subnetid},
+               type            => "comment",
+               srcid           => $comment->{cid},
+               popularity      => $popularity,
+               editorpop       => $editorpop,
+               globjid         => $globjid,
+               discussion      => $comment->{sid},
+               createtime      => $comment->{date},
+       };
+       my $fhid = $self->createFireHose($data);
+
+       if (!isAnon($comment->{uid})) {
+               my $constants = $self->getCurrentStatic();
+               my $tags = getObject('Slash::Tags');
+               $tags->createTag({
+                       uid                     => $comment->{uid},
+                       name                    => $constants->{tags_upvote_tagname},
+                       globjid                 => $globjid,
+                       private                 => 1,
+               });
+       }
+
+       my $tagboxdb = getObject('Slash::Tagbox');
+       if ($tagboxdb) {
+               for my $tbname (qw( FireHoseScores FHEditorPop CommentScoreReason )) {
+                       my $tagbox = $tagboxdb->getTagboxes($tbname);
+                       next unless $tagbox;
+                       $tagbox->{object}->forceFeederRecalc($globjid);
+               }
+       }
+
+       return $fhid;
+}
+
+
+sub createItemFromJournal {
+       my($self, $id) = @_;
+       my $user = getCurrentUser();
+       my $journal_db = getObject("Slash::Journal");
+       my $journal = $journal_db->get($id);
+
+       if ($journal) {
+               my $constants = $self->getCurrentStatic();
+               my $globjid = $self->getGlobjidCreate("journals", $journal->{id});
+
+               my $bodytext  = $journal_db->fixJournalText($journal->{article}, $journal->{posttype}, $journal->{uid});
+               my $introtext = $journal->{introtext} || $bodytext;
+
+               my $body_length = 0;
+               if ($introtext ne $bodytext) {
+                       $body_length = length($bodytext) - length($introtext);
+                       $body_length = 0 if $body_length < 0; # just in case
+               }
+
+               my $publicize  = $journal->{promotetype} eq 'publicize';
+               my $publish    = $journal->{promotetype} eq 'publish';
+               my $color_lvl  = $publicize ? 5 : $publish ? 6 : 7; # post == 7
+               my $editor_lvl = $publicize ? 5 : $publish ? 6 : 8; # post == 8
+               my $public     = 'yes';
+               my $popularity = $self->getEntryPopularityForColorLevel($color_lvl);
+               my $editorpop  = $self->getEntryPopularityForColorLevel($editor_lvl);
+
+               my $type = $user->{acl}{vendor} ? "vendor" : "journal";
+
+               my $data = {
+                       title                   => $journal->{description},
+                       globjid                 => $globjid,
+                       uid                     => $journal->{uid},
+                       attention_needed        => "yes",
+                       public                  => $public,
+                       introtext               => $introtext,
+                       bodytext                => $bodytext,
+                       body_length             => $body_length,
+                       popularity              => $popularity,
+                       editorpop               => $editorpop,
+                       tid                     => $journal->{tid},
+                       srcid                   => $id,
+                       discussion              => $journal->{discussion},
+                       type                    => $type,
+                       ipid                    => $user->{ipid},
+                       subnetid                => $user->{subnetid},
+                       createtime              => $journal->{date}
+               };
+
+               my $id = $self->createFireHose($data);
+
+               my $tags = getObject('Slash::Tags');
+               $tags->createTag({
+                       uid             => $journal->{uid},
+                       name            => $constants->{tags_upvote_tagname},
+                       globjid         => $globjid,
+                       private         => 1,
+               });
+
+               return $id;
+       }
+}
+
+sub getUserBookmarkForUrl {
+       my($self, $uid, $url_id) = @_;
+       my $uid_q = $self->sqlQuote($uid);
+       my $url_id_q = $self->sqlQuote($url_id);
+       return $self->sqlSelectHashref("*", "bookmarks", "uid=$uid_q and url_id=$url_id_q");
+}
+
+sub createUpdateItemFromBookmark {
+       my($self, $id, $options) = @_;
+       $options ||= {};
+       my $constants = $self->getCurrentStatic();
+       my $bookmark_db = getObject("Slash::Bookmark");
+       my $bookmark = $bookmark_db->getBookmark($id);
+       my $url_globjid = $self->getGlobjidCreate("urls", $bookmark->{url_id});
+       my $type = $options->{type} || "bookmark";
+       my($count) = $self->sqlCount("firehose", "globjid=$url_globjid");
+       my $firehose_id;
+       my $popularity = undef;
+       $popularity = $options->{popularity} if defined $options->{popularity};
+       if (!defined $popularity) {
+               my $cl = 7;
+               my $wanted = $constants->{postedout_wanted} || 2;
+               if ($type eq 'feed' && $self->countStoriesPostedOut() < $wanted) {
+                       # If there aren't "enough" posted-out stories and
+                       # this bookmark is a feed item, it gets bumped up
+                       # one color level.
+                       $cl = 7;
+               }
+               $popularity = $self->getEntryPopularityForColorLevel($cl);
+       }
+
+       my $activity = defined $options->{activity} ? $options->{activity} : 1;
+
+       if ($count) {
+               # $self->sqlUpdate("firehose", { -popularity => "popularity + 1" }, "globjid=$url_globjid");
+       } else {
+
+               my $data = {
+                       globjid         => $url_globjid,
+                       title           => $bookmark->{title},
+                       url_id          => $bookmark->{url_id},
+                       uid             => $bookmark->{uid},
+                       popularity      => $popularity,
+                       editorpop       => $popularity,
+                       activity        => $activity,
+                       public          => "yes",
+                       type            => $type,
+                       srcid           => $id,
+                       createtime      => $bookmark->{createdtime},
+               };
+               $data->{introtext} = $options->{introtext} if $options->{introtext};
+               if ($type eq "feed") {
+                       $data->{srcname} = $bookmark->{srcname};
+               }
+               $firehose_id = $self->createFireHose($data);
+               if ($firehose_id && $type eq "feed") {
+                       my $discussion_id = $self->createDiscussion({
+                               uid             => 0,
+                               kind            => 'feed',
+                               title           => $bookmark->{title},
+                               commentstatus   => 'logged_in',
+                               url             => "$constants->{rootdir}/firehose.pl?op=view&id=$firehose_id"
+                       });
+                       if ($discussion_id) {
+                               $self->setFireHose($firehose_id, {
+                                       discussion      => $discussion_id,
+                               });
+                       }
+               }
+
+               if (!isAnon($bookmark->{uid}) && !$options->{no_nod}) {
+                       my $constants = $self->getCurrentStatic();
+                       my $tags = getObject('Slash::Tags');
+                       $tags->createTag({
+                               uid                     => $bookmark->{uid},
+                               name                    => $constants->{tags_upvote_tagname},
+                               globjid                 => $url_globjid,
+                               private                 => 1,
+                       });
+               }
+       }
+       return $firehose_id;
+}
+
+sub createItemFromSubmission {
+       my($self, $id) = @_;
+       my $submission = $self->getSubmission($id, "", 1);
+       if ($submission) {
+               my $globjid = $self->getGlobjidCreate("submissions", $submission->{subid});
+               my $midpop = $self->getEntryPopularityForColorLevel(5);
+               my $data = {
+                       title                   => $submission->{subj},
+                       globjid                 => $globjid,
+                       uid                     => $submission->{uid},
+                       introtext               => $submission->{story},
+                       popularity              => $midpop,
+                       editorpop               => $midpop,
+                       public                  => "yes",
+                       attention_needed        => "yes",
+                       type                    => "submission",
+                       primaryskid             => $submission->{primaryskid},
+                       tid                     => $submission->{tid},
+                       srcid                   => $id,
+                       ipid                    => $submission->{ipid},
+                       subnetid                => $submission->{subnetid},
+                       email                   => $submission->{email},
+                       emaildomain             => $submission->{emaildomain},
+                       name                    => $submission->{name},
+                       mediatype               => $submission->{mediatype},
+                       createtime              => $submission->{time}
+               };
+               $data->{url_id} = $submission->{url_id} if $submission->{url_id};
+               my $firehose_id = $self->createFireHose($data);
+               if (!isAnon($submission->{uid})) {
+                       my $constants = $self->getCurrentStatic();
+                       my $tags = getObject('Slash::Tags');
+                       $tags->createTag({
+                               uid                     => $submission->{uid},
+                               name                    => $constants->{tags_upvote_tagname},
+                               globjid                 => $globjid,
+                               private                 => 1,
+                       });
+               }
+               return $firehose_id;
+       }
+
+}
+
+sub createItemFromProject {
+       my($self, $id) = @_;
+       my $constants = $self->getCurrentStatic();
+       my $proj = $self->getProject($id);
+       my $globjid = $self->getGlobjidCreate("projects", $proj->{id});
+       my $midpop = $self->getEntryPopularityForColorLevel(5);
+
+       my $data = {
+               uid             => $proj->{uid},
+               title           => $proj->{textname},
+               srcid           => $proj->{id},
+               type            => "project",
+               url_id          => $proj->{url_id},
+               globjid         => $globjid,
+               srcname         => $proj->{srcname},
+               introtext       => $proj->{description},
+               public          => "yes",
+               editorpop       => $midpop,
+               popularity      => $midpop,
+               createtime      => $proj->{createtime}
+       };
+       my $firehose_id = $self->createFireHose($data);
+       my $discussion_id = $self->createDiscussion({
+               uid             => 0,
+               kind            => 'project',
+               title           => $proj->{textname},
+               commentstatus   => 'logged_in',
+               url             => "$constants->{rootdir}/firehose.pl?op=view&id=$firehose_id"
+       });
+
+       if ($discussion_id) {
+               $self->setFireHose($firehose_id, {
+                       discussion      => $discussion_id,
+               });
+       }
+       return $firehose_id;
+}
+
+sub updateItemFromProject {
+       my($self, $id);
+       my $proj = $self->getProject($id);
+       if ($proj && $proj->{id}) {
+               my $item = $self->getFireHoseByTypeSrcid("project", $proj->{id});
+               if ($item && $item->{id}) {
+                       my $data = {
+                               uid             => $proj->{uid},
+                               url_id          => $proj->{url_id},
+                               title           => $proj->{textname},
+                               introtext       => $proj->{description},
+                               createtime      => $proj->{createtime}
+                       };
+                       $self->setFireHose($item->{id}, $data);
+               }
+       }
+}
+
+sub updateItemFromStory {
+       my($self, $id) = @_;
+       my $constants = $self->getCurrentStatic();
+       my %ignore_skids = map {$_ => 1 } @{$constants->{firehose_story_ignore_skids}};
+        my $slash_db = $self->new_instance_of("Newslash::Model::SlashDB");
+        my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
+
+       my $story = $slash_db->getStory($id, "", 1);
+       if ($story) {
+               my $globjid = $globjs->getGlobjidCreate("stories", $story->{stoid});
+               my $id = $self->getFireHoseIdFromGlobjid($globjid);
+               if ($id) {
+                       # If a story is getting its primary skid to an ignored value set its firehose entry to non-public
+                       my $public = ($story->{neverdisplay} || $ignore_skids{$story->{primaryskid}}) ? "no" : "yes";
+                       my $data = {
+                               title           => $story->{title},
+                               uid             => $story->{uid},
+                               createtime      => $story->{time},
+                               introtext       => parseSlashizedLinks($story->{introtext}),
+                               bodytext        => parseSlashizedLinks($story->{bodytext}),
+                               media           => $story->{media},
+                               primaryskid     => $story->{primaryskid},
+                               tid             => $story->{tid},
+                               public          => $public,
+                               dept            => $story->{dept},
+                               discussion      => $story->{discussion},
+                               body_length     => $story->{body_length},
+                               word_count      => $story->{word_count},
+                               thumb           => $story->{thumb},
+                       };
+                       $data->{offmainpage} = "no";
+                       $data->{offmainpage} = "yes" if defined $story->{offmainpage} && $story->{offmainpage};
+
+                       if (defined $story->{mediatype}) {
+                               if (!$story->{mediatype}) {
+                                       $data->{mediatype} = "none";
+                               } else {
+                                       $data->{mediatype} = $story->{mediatype};
+                               }
+                       }
+                       $self->setFireHose($id, $data);
+               }
+       }
+}
+
+sub setTopicsRenderedBySkidForItem {
+       my($self, $id, $primaryskid) = @_;
+       my $constants = $self->getCurrentStatic();
+       my $skin = $self->getSkin($primaryskid);
+
+       # if no primaryskid assign to mainpage skid
+       my $nexus = $skin && $skin->{nexus} ? $skin->{nexus} : $constants->{mainpage_nexus_tid};
+
+       $self->sqlDelete("firehose_topics_rendered", "id = $id");
+       $self->sqlInsert("firehose_topics_rendered", { id => $id, tid => $nexus });
+       $self->setFireHose($id, { nexuslist => " $nexus " });
+}
+
+sub setTopicsRenderedForStory {
+       my($self, $stoid, $tids) = @_;
+       my $the_tids = [ @$tids ]; # Copy tids so any changes we make don't affect the caller
+       my $constants = $self->getCurrentStatic();
+        my $db = $self->new_instance_of("Newslash::Model::SlashDB");
+        my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
+       my $story = $db->getStory($stoid, "", 1);
+       if ($story) {
+                my $globjid = $globjs->getGlobjidCreate("stories", $story->{stoid});
+               my $id = $self->getFireHoseIdFromGlobjid($globjid);
+               my @nexus_topics;
+               if ($id) {
+                       $self->sqlDelete("firehose_topics_rendered", "id = $id");
+                       foreach (@$the_tids) {
+                               $self->sqlInsert("firehose_topics_rendered", { id => $id, tid => $_});
+                       }
+                       my $tree = $self->getTopicTree();
+                       @nexus_topics = grep { $tree->{$_}->{nexus} } @$the_tids;
+                       my $nexus_list = join ' ', @nexus_topics;
+                       $nexus_list = " $nexus_list ";
+                       $self->setFireHose($id, { nexuslist => $nexus_list });
+               }
+       }
+}
+
+sub createItemFromStory {
+       my($self, $id) = @_;
+       my $constants = $self->getCurrentStatic();
+       # If a story is created with an ignored primary skid it'll never be created as a firehose entry currently
+       my %ignore_skids = map {$_ => 1 } @{$constants->{firehose_story_ignore_skids}};
+       my $story = $self->getStory($id, '', 1);
+
+       my $popularity = $self->getEntryPopularityForColorLevel(2);
+       if ($story->{story_topics_rendered}{$constants->{mainpage_nexus_tid}}) {
+               $popularity = $self->getEntryPopularityForColorLevel(1);
+       }
+
+       if ($story && !$ignore_skids{$story->{primaryskid}}) {
+               my $globjid = $self->getGlobjidCreate("stories", $story->{stoid});
+               my $public = $story->{neverdisplay} ? "no" : "yes";
+               my $data = {
+                       title           => $story->{title},
+                       globjid         => $globjid,
+                       uid             => $story->{uid},
+                       createtime      => $story->{time},
+                       introtext       => parseSlashizedLinks($story->{introtext}),
+                       bodytext        => parseSlashizedLinks($story->{bodytext}),
+                       media           => $story->{media},
+                       popularity      => $popularity,
+                       editorpop       => $popularity,
+                       primaryskid     => $story->{primaryskid},
+                       tid             => $story->{tid},
+                       srcid           => $id,
+                       type            => "story",
+                       public          => $public,
+                       dept            => $story->{dept},
+                       discussion      => $story->{discussion},
+                       thumb           => $story->{thumb},
+               };
+
+               $data->{offmainpage} = "no";
+               $data->{offmainpage} = "yes" if defined $story->{offmainpage} && $story->{offmainpage};
+
+               if (defined $story->{mediatype}) {
+                       if (!$story->{mediatype}) {
+                               $data->{mediatype} = "none";
+                       } else {
+                               $data->{mediatype} = $story->{mediatype};
+                       }
+               }
+               $self->createFireHose($data);
+       }
+}
+
+sub getFireHoseCount {
+       my($self) = @_;
+       my $pop = $self->getEntryPopularityForColorLevel(6);
+
+       #XXXFH - add time limit later?
+       return $self->sqlCount("firehose",
+               "editorpop >= $pop AND rejected='no' AND accepted='no' AND type != 'story' and category='' and preview='no' and createtime >= DATE_SUB(NOW(),INTERVAL 7 DAY)");
+}
+
+{
+my %cat_types = ('', 0, 'back', 1, 'hold', 2, 'quik', 3);
+
+my %firehose_types = (
+       story      => 1,
+       submission => 3,
+       journal    => 4,
+       comment    => 5,
+       discussion => 7,
+       project    => 11,
+       bookmark   => 12,
+       feed       => 13,
+       vendor     => 14,
+       misc       => 15,
+       tagname    => 17,
+);
+
+my %sphinx_orderby = (
+       createtime => 'createtime_ut',
+       popularity => 'popularity',
+       editorpop  => 'editorpop',
+       neediness  => 'neediness'
+);
+
+# SPH_SORT_RELEVANCE ATTR_DESC ATTR_ASC TIME_SEGMENTS EXTENDED EXPR
+my %sphinx_orderdir = (
+       ASC        => SPH_SORT_ATTR_ASC,
+       DESC       => SPH_SORT_ATTR_DESC
+);
+
+# SPH_MATCH_ALL ANY PHRASE BOOLEAN EXTENDED2
+#my %sphinx_mode = (
+#      all        => SPH_MATCH_ALL,
+#      any        => SPH_MATCH_ANY,
+#      boolean    => SPH_MATCH_BOOLEAN,
+#      extended2  => SPH_MATCH_EXTENDED2
+#);
+
+# This method will allow multiple search paths to be followed, for merging
+# back together later.  ** DO NOT ** use this unless you really know what
+# you're doing.  Srsly.  For every set of params you push on here, you
+# *multiply*, not *add" to, the number of queries needed.  -- pudge
+sub getFireHoseEssentialsPushMulti {
+       my($multi, $new) = @_;
+
+       return unless ref($multi) && ref($new);
+       return unless @$new;
+
+       if (!@$multi) {
+               # deepcopy
+               for (@$new) { push @$multi, [ @$_ ] }
+               return;
+       }
+
+       my $tmp = [];
+       for my $mar (@$multi) {
+               for my $nar (@$new) {
+                       push @$tmp, [ @$mar, @$nar ];
+               }
+       }
+
+       @$multi = @$tmp;
+}
+
+sub getFireHoseEssentialsParams {
+       my($self, $options, $sphinx) = @_;
+       my $user = getCurrentUser();
+       my $constants = $self->getCurrentStatic();
+
+       my(@sphinx_opts, @sphinx_opts_multi, @sphinx_terms, @sphinx_where);
+       my @sphinx_tables = ('sphinx_search');
+
+       my $cur_time = $self->getTime({ unix_format => 1 });
+
+       my $tags = getObject('Slash::Tags');
+       my $need_tagged = 0;
+       $need_tagged = 1 if $options->{tagged_by_uid} && $options->{tagged_as};
+       $need_tagged = 2 if $options->{tagged_by_uid} && $options->{tagged_non_negative};
+       $need_tagged = 3 if $options->{tagged_by_uid} && $options->{tagged_for_homepage};
+
+       if ($need_tagged) {
+               my $tagged_by_uid = $options->{tagged_by_uid};
+               @$tagged_by_uid = grep $_ && !/\D/, @$tagged_by_uid;
+
+               if ($need_tagged == 1) {
+                       # This combination of options means to restrict to only
+                       # those hose entries tagged by one particular user with
+                       # one particular tag, e.g. /~foo/tags/bar
+                       push @sphinx_tables, 'tags';
+                       # note: see below, where this clause can be manipulated for $sph
+                       push @sphinx_where, 'tags.globjid = sphinx_search.globjid';
+                       my $tag_id = $tags->getTagnameidFromNameIfExists($options->{tagged_as}) || 0;
+                       push @sphinx_where, "tags.tagnameid = $tag_id";
+                       push @sphinx_where, "tags.uid = $tagged_by_uid";
+                       $sphinx->{check_sql} = 1;
+                       push @sphinx_opts, [ filter => tfh => $tagged_by_uid ];
+               } elsif ($need_tagged == 2) {
+                       # This combination of options means to restrict to only
+                       # those hose entries tagged by one particular user with
+                       # any "tagged for hose" tags (/~foo/firehose).
+                       push @sphinx_opts, [ filter => tfh => $tagged_by_uid ];
+               } elsif ($need_tagged == 3) {
+                       # This combination of options means to restrict to only
+                       # those hose entries tagged by one particular user with
+                       # any "tagged for homepage" tags (/~foo).
+                       push @sphinx_opts, [ filter => tfhp => $tagged_by_uid ];
+               }
+               push @sphinx_where, 'inactivated IS NULL';
+       }
+       if ($options->{startdateraw}) {
+               my $st_sphinx = timeCalc($options->{startdateraw}, '%s',0);
+               if ($options->{orderby} eq "ASC") {
+                       push @sphinx_opts, [ range => createtime_ut => 0, $st_sphinx, 1 ];
+               } else {
+                       push @sphinx_opts, [ range => createtime_ut => 0, $st_sphinx ];
+               }
+       } elsif ($options->{startdate}) {
+               my $startdate = $options->{startdate};
+
+               my($db_levels, $db_order) = getDayBreakLevels();
+               my $level = parseDayBreakLevel($startdate) || 'day';
+               my @arr = $startdate =~ $db_levels->{$level}{re};
+               $startdate = $db_levels->{$level}{timefmt}->(@arr);
+
+               $startdate = $options->{startdate} if $options->{spritegen};
+
+               my $st_sphinx  = timeCalc($startdate, '%s', -$user->{off_set});
+
+               if (defined $options->{duration} && $options->{duration} >= 0) {
+                       my $dur_q = $self->sqlQuote($options->{duration});
+                       my $enddate = $self->getDayFromDay($options->{startdate}, -$options->{duration}); # minus means add here
+                       my @arr = $enddate =~ $db_levels->{$level}{re};
+                       $enddate = $db_levels->{$level}{timefmt}->(@arr);
+
+                       my $end_sphinx  = timeCalc($enddate, '%s', -$user->{off_set});
+                       push @sphinx_opts, [ range => createtime_ut => $st_sphinx, $end_sphinx ];
+
+               } elsif ($options->{duration} == -1) {
+                       if ($options->{orderdir} eq "ASC") {
+                               # ! is for negating, since this is >, not <
+                               my $time = $st_sphinx-1;
+                               push @sphinx_opts, [ range => createtime_ut => 0, $time, 1 ];
+
+                       } else {
+                               my $enddate = $self->getDayFromDay($options->{startdate}, -1); # add one
+                               my @arr = $enddate =~ $db_levels->{$level}{re};
+                               $enddate = $db_levels->{$level}{timefmt}->(@arr);
+                               @arr = $enddate =~ /(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/;
+                               $enddate = sprintf "%04d-%02d-%02d %02d:%02d:%02d", Add_Delta_DHMS(@arr, 0, 0, 0, -1);
+
+                               my $end_sphinx = timeCalc($enddate, '%s', -$user->{off_set});
+                               push @sphinx_opts, [ range => createtime_ut => 0, $end_sphinx ];
+                       }
+               }
+
+       } elsif (defined $options->{duration} && $options->{duration} >= 0) {
+               my $dur_sphinx = $options->{duration} * 86400;
+               my $dur_time = ($cur_time - $dur_sphinx) - 1;
+               push @sphinx_opts, [ range => createtime_ut => 0, $dur_time, 1 ];
+       }
+
+       if ($user->{is_admin} || $user->{acl}{signoff_allowed}) {
+               $sphinx->{no_mcd} = 1;
+
+               if ($options->{unsigned}) {
+                       push @sphinx_opts, [ filter => signoff => [ $user->{uid} ], 1 ];
+
+                       my $days_relevant = 30;
+                       my $time_back = $cur_time - (86400 * $days_relevant);
+
+#                      if ((!$options->{type} || $options->{type} eq 'story') && (!$options->{not_type} || $options->{not_type} ne 'story')) {
+#                              push @sphinx_opts, [ range => createtime_ut => 0, $time_back, 1 ];
+#                      }
+
+                       # SSS sample pseudocode for multi
+                       my $time_filter = [ range => createtime_ut => 0, $time_back, 1 ];
+                       if (!$options->{type} && (!$options->{not_type} || $options->{not_type} ne 'story')) {
+                               getFireHoseEssentialsPushMulti(\@sphinx_opts_multi, [[
+                                       [ filter => type => [$firehose_types{story}] ],
+                                       $time_filter
+                               ], [
+                                       [ filter => type => [$firehose_types{story}], 1 ],
+                               ]]);
+                       } elsif ($options->{type} eq 'story') {
+                               push @sphinx_opts, $time_filter;
+                       }
+
+               } elsif ($options->{signed}) {
+                       push @sphinx_opts, [ filter => signoff => [ $user->{uid} ] ];
+               }
+       }
+
+       if ($options->{createtime_no_future}) {
+               push @sphinx_opts, [ range => createtime_ut => 0, $cur_time ];
+       }
+
+       if ($options->{createtime_subscriber_future}) {
+               my $future_secs = $constants->{subscribe_future_secs};
+               my $future_time = $cur_time + $future_secs;
+               push @sphinx_opts, [ range => createtime_ut => 0, $future_time ];
+       }
+
+       if ($options->{offmainpage}) {
+               my $off = $options->{offmainpage} eq 'yes' ? 1 : 0;
+               push @sphinx_opts, [ filter => offmainpage => [ $off ] ];
+       }
+
+       if ($options->{public}) {
+               my $pub = $options->{public} eq 'yes' ? 1 : 0;
+               push @sphinx_opts, [ filter => public => [ $pub ] ];
+       }
+
+       # For now we never want a previewed item returned, if we want it we'll ask with getFireHose
+       push @sphinx_opts, [ filter => preview => [ 0 ] ];
+
+
+       if ($options->{nexus}) {
+               push @sphinx_opts, [ filter => tid => $options->{nexus} ];
+       }
+
+       if ($options->{not_nexus}) {
+               push @sphinx_opts, [ filter => tid => $options->{not_nexus}, 1 ];
+       }
+
+       if ($options->{attention_needed}) {
+               my $needed = $options->{attention_needed} eq 'yes' ? 1 : 0;
+               push @sphinx_opts, [ filter => attention_needed => [ $needed ] ];
+       }
+
+       if ($options->{accepted}) {
+               my $accepted = $options->{accepted} eq 'yes' ? 1 : 0;
+               push @sphinx_opts, [ filter => accepted => [ $accepted ] ];
+       }
+
+       if ($options->{rejected}) {
+               my $rejected = $options->{rejected} eq 'yes' ? 1 : 0;
+               push @sphinx_opts, [ filter => rejected => [ $rejected ] ];
+       }
+
+       if ($options->{'pop'}) {
+               my $field = $user->{is_admin} && !$options->{usermode} ? 'editorpop' : 'popularity';
+               # in sphinx index, popularity has 1000000 added to it
+               my $max = int($options->{'pop'}) + 1_000_000 - 1;
+               push @sphinx_opts, [ range => $field => 0, $max, 1 ];
+       }
+
+       if (defined $options->{category} || ($user->{is_admin} && $options->{admin_filters})) {
+               $options->{category} ||= '';
+               my $val = $cat_types{lc $options->{category}};
+               $val = 9999 unless defined $val;
+               push @sphinx_opts, [ filter => category => [ $val ] ];
+       }
+
+       foreach my $prefix ("","not_") {
+               foreach my $base (qw(primaryskid uid type)) {
+                       if ($options->{"$prefix$base"}) {
+                               my $not = $prefix eq "not_" ? 1 : 0;
+                               my $cur_opt = $options->{"$prefix$base"};
+                               $cur_opt = [$cur_opt] if !ref $cur_opt;
+
+                               # not sure if OK to manipulate $cur_opt, so we copy -- pudge
+                               my $newbase = $base;
+                               my @new_opt = @$cur_opt;
+                               if ($base eq 'type') {
+                                       $_ = $firehose_types{$_} || 9999 for @new_opt;
+                               }
+                               push @sphinx_opts, [ filter => $newbase => \@new_opt, ($not ? 1 : 0) ];
+                       }
+               }
+       }
+
+
+       if ($options->{not_id}) {
+               my $globjid = $self->getFireHose($options->{not_id})->{globjid};
+               push @sphinx_opts, [ filter => globjidattr => [ $globjid ], 1 ];
+       }
+
+       if ($options->{filter}) {
+### clean and quate query term for ngram & e2 mode
+               my $query = $options->{filter};
+               $query =~ s/[^\x{0021}-\x{007E}\x{2010}-\x{201F}\x{203E}\x{22C5}\x{301C}-\x{FFFF}]+/ /g;
+               # 0..9, a-z
+               $query =~ tr/0-9A-Za-z/\x{FF10}-\x{FF19}\x{FF41}-\x{FF5A}\x{FF41}-\x{FF5A}/;
+
+               # clean up spaces and quote term
+               $query =~ s/^ */"/;
+               $query =~ s/ *$/"/;
+               $query =~ s/ +/" "/g;
+
+#              $sphinx->{mode} = 'extended2';
+
+#              my $query;
+#              $sphinx->{mode} = $constants->{sphinx_match_mode} || 'boolean';
+#              ($query, $sphinx->{mode}) = sphinxFilterQuery($options->{filter}, $sphinx->{mode});
+               push @sphinx_terms, $query;
+       }
+
+       foreach ('is_spam', 'bayes_spam',  'collateral_spam') {
+               if ($options->{$_}) {
+                       push @sphinx_opts, [ filter => $_ => [ 1 ] ];
+               }
+       }
+
+       # XXX This is not available for use until sphinx main index is corrected
+       # if (defined $options->{stuck}) {
+       #       push @sphinx_opts, [ filter => 'stuck' => [ $options->{stuck} ? 1 : 0 ]];
+       # }
+
+
+       return(\@sphinx_opts, \@sphinx_opts_multi, \@sphinx_terms, \@sphinx_where, \@sphinx_tables);
+}
+
+
+sub getFireHoseEssentials {
+       my($self, $options) = @_;
+       my $user = getCurrentUser();
+       my $constants = $self->getCurrentStatic();
+
+       my $stuck_item;
+       my $stuck_pos;
+
+       if ($options->{view} eq 'stories') {
+               $options->{stuck} = 0;
+       }
+
+       # Only do this on page 1
+       if (!$options->{offset} && $options->{view} eq 'stories') {
+               $stuck_item = $self->getStuckStory();
+               if ($stuck_item) {
+                       $stuck_pos = $stuck_item->{stuckpos};
+                       use Data::Dumper;
+                       print STDERR "STUCK ITEM: " . Dumper($stuck_item);
+                       print STDERR Dumper($options->{nexus});
+               }
+       }
+
+       # SEARCH SETUP
+       my($sphinx_debug, $sphinx_other) = (0, 0);
+       my $sphinx = {
+               no_mcd    => $user->{is_admin} && !$options->{usermode} ? 1 : 0,
+               check_sql => 0,
+               mode      => ''
+       };
+
+
+       # (very!) temporarily hardcode admin to sphinx02 -- pudge 2009.11.11
+       my %sphinx_dbs = map { $_->{virtual_user} => $_ }
+               grep { $_->{type} eq "sphinx" && $_->{isalive} eq "yes" }
+               values %{$self->getDBs};
+       my $virtual_user = 'sphinx01';
+#      $virtual_user = 'sphinx02' if $user->{is_admin} && $sphinx_dbs{sphinx02};
+       my $sphinxdb = getObject('Slash::Sphinx', {
+               db_type      => 'sphinx',
+               timeout      => 2,
+               virutal_user => $virtual_user
+       });
+
+       my $sph;
+       if (($sph = $sphinxdb->{_sph}) && $sph->Status) {
+               if ($sphinx_debug) {
+                       use Data::Dumper; local $Data::Dumper::Indent = 0; local $Data::Dumper::Sortkeys = 1;
+                       print STDERR "STATUS: $$: " . Dumper($sph->Status);
+               }
+               $sph->ResetFilters;
+               $sphinxdb->{_sphpersistent}++;
+               
+       } else {
+               #use Log::Log4perl qw(:easy);
+               #Log::Log4perl->easy_init({
+               #       level   => $TRACE,
+               #       file     => 'STDERR',
+               #});
+               #my $logger = get_logger();
+               #$sphinxdb->{_sph} = $sph = Sphinx::Search->new({'log' => $logger, debug => 1});
+               $sph = Sphinx::Search->new;
+               #$sphinxdb->{_sph} = $sph;
+               my $vu = DBIx::Password::getVirtualUser( $sphinxdb->{virtual_user} );
+               $sphinxdb->{_sphhost} = my $host = $constants->{sphinx_01_hostname_searchd} || $vu->{host};
+               $sphinxdb->{_sphport} = my $port = $constants->{sphinx_01_port} || 3312;
+               $sph->SetServer($host, $port);
+               $sph->SetConnectTimeout(5);
+#              $sph->Open;
+               $sphinxdb->{_sphpersistent} = 0;
+               $sphinxdb->{_sphts} = '';
+       }
+
+       if ($sphinx_debug) {
+               use Data::Dumper; local $Data::Dumper::Indent = 0; local $Data::Dumper::Sortkeys = 1;
+               print STDERR "sphinx/gFHE option dump for $$: $sphinxdb->{_sphhost}:$sphinxdb->{_sphport} (persistent:$sphinxdb->{_sphpersistent}): ", Dumper($options), "\n";
+       }
+
+
+
+       # SEARCH OPTION SETUP
+       my($items, $results) = ([], {});
+
+       my $colors = $self->getFireHoseColors();
+       $options ||= {};
+       $options->{limit} ||= 50;
+       my $page_size = $options->{limit} || 1;
+
+       my $fetch_extra = 0;
+       my($day_num, $day_label, $day_count);
+
+       $options->{limit} += $options->{more_num} if $options->{more_num};
+
+       my $qoptions = {};
+       $qoptions->{fetch_size} = $options->{limit};
+       if ($options->{orderby} && $options->{orderby} eq "createtime" && $options->{duration} != -1) {
+               $fetch_extra = 1;
+               $qoptions->{fetch_size}++;
+       }
+
+       $options->{'pop'} = $self->getMinPopularityForColorLevel($colors->{$options->{color}})
+               if $options->{color} && $colors->{$options->{color}};
+
+       $options->{orderby} ||= 'createtime';
+       $options->{orderdir} = uc($options->{orderdir}) eq 'ASC' ? 'ASC' : 'DESC';
+
+
+
+       # SEARCH PARAM SETUP
+       my($sphinx_opts, $sphinx_opts_multi, $sphinx_terms, $sphinx_where, $sphinx_tables) = $self->getFireHoseEssentialsParams($options, $sphinx);
+#use Data::Dumper; print STDERR Dumper [$sphinx_opts, $sphinx_opts_multi, $sphinx_terms, $sphinx_where, $sphinx_tables];
+
+
+       # CACHE CHECK
+       my($sphinx_ar, $sphinx_stats) = ([], {});
+       my $mcd = $self->getMCD;
+       my($mcdkey_data, $mcdkey_stats);
+
+       # ignore memcached if admin, or if usermode is on
+       if ($mcd && !$sphinx->{no_mcd}) {
+               my $serial = $self->serializeOptions($options, $user);
+               my $id = md5_hex($serial);
+
+               $mcdkey_data  = "$self->{_mcd_keyprefix}:gfhe_sphinx:$id";
+               $mcdkey_stats = "$self->{_mcd_keyprefix}:gfhe_sphinxstats:$id";
+               $sphinx_ar    = $mcd->get($mcdkey_data);
+               $sphinx_stats = $mcd->get($mcdkey_stats);
+               if ($sphinx_debug) {
+                       my $arhit = defined($sphinx_ar)    ? 'HIT' : '';
+                       my $sthit = defined($sphinx_stats) ? 'HIT' : '';
+                       my $scnt  = defined($sphinx_ar)    ? scalar(@$sphinx_ar) : 0;
+                       print STDERR scalar(gmtime) . " gFHE mcd $0 '$arhit' '$sthit' $scnt $id $serial\n";
+               }
+               $sphinx_ar    ||= [];
+               $sphinx_stats ||= {};
+       }
+
+
+
+       # QUERY OPTION SETUP
+       $qoptions->{offset_num} = defined $options->{offset} ? $options->{offset} : '';
+       $qoptions->{offset_num} = '' if $qoptions->{offset_num} !~ /^\d+$/;
+
+       $qoptions->{orderby}  = $sphinx_orderby{$options->{orderby}}   || 'createtime_ut';
+       $qoptions->{orderdir} = $sphinx_orderdir{$options->{orderdir}} || SPH_SORT_ATTR_DESC;
+       $sph->SetSortMode($qoptions->{orderdir}, $qoptions->{orderby});
+#      $sph->SetMatchMode( ($sphinx->{mode} && $sphinx_mode{$sphinx->{mode}})
+#              ? $sphinx_mode{$sphinx->{mode}}
+#              : SPH_MATCH_ALL
+#      );
+       # for ngram
+       $sph->SetMatchMode(SPH_MATCH_EXTENDED2);
+
+       $qoptions->{maxmatches} = 0;
+       # in both these cases, we need to do a secondary filter run, so we are
+       # getting a large number from the initial query(ies), and then getting
+       # the smaller number from a MySQL query
+       if (@$sphinx_tables > 1 || @$sphinx_opts_multi) {
+               my $offset = length $qoptions->{offset_num} ? "$qoptions->{offset_num}, " : '';
+               $sphinx_other = "LIMIT $offset$qoptions->{fetch_size}";
+
+               $qoptions->{maxmatches} = $constants->{sphinx_01_max_matches} || 10000;
+               $sph->SetLimits(0, $qoptions->{maxmatches}, $qoptions->{maxmatches});
+       } else {
+               $qoptions->{maxmatches} = $qoptions->{fetch_size} > 1000 ? $qoptions->{fetch_size} : undef; # SSS make 1000 a var?
+               $sph->SetLimits($qoptions->{offset_num} || 0, $qoptions->{fetch_size}, $qoptions->{maxmatches});
+       }
+
+
+
+       # SPHINX CALL
+       my($sdebug_idset_elapsed, $sdebug_get_elapsed) = (0, 0);
+       if (!@$sphinx_ar) {
+               $sdebug_idset_elapsed = Time::HiRes::time;
+               my(@sphinx_ars, @sphinx_statses);
+               # make sure we'll go through loop with dummy data if there's no actual multi data
+               @$sphinx_opts_multi = [] unless @$sphinx_opts_multi;
+               for my $multi (@$sphinx_opts_multi) {
+#                      if ($constants->{sphinx_se}) {
+#                              my @sphinxse_opts;
+#                              for my $opt (@$sphinx_opts, @$multi) {
+#                                      my $neg;
+#                                      my $opt_str = "$opt->[0]=$opt->[1],";
+#                                      if ($opt->[0] eq 'filter') {
+#                                              $opt_str .= join ',', @{$opt->[2]};
+#                                              $neg = $opt->[3];
+#                                      } elsif ($opt->[0] eq 'range') {
+#                                              $opt_str .= "$opt->[2],$opt->[3]";
+#                                              $neg = $opt->[4];
+#                                      }
+#                                      if ($neg) {
+#                                              $opt_str = '!' . $opt_str;
+#                                      }
+#                                      push @sphinxse_opts, $opt_str;
+#                              }
+#
+#                              $qoptions->{orderdir} = $options->{orderdir} eq 'ASC' ? 'attr_asc' : 'attr_desc';
+#                              push @sphinxse_opts, "sort=$qoptions->{orderdir}:$qoptions->{orderby}";
+#                              push @sphinxse_opts, "mode=$sphinx->{mode}" if $sphinx->{mode};
+#
+#                              if (@$sphinx_tables > 1 || @$sphinx_opts_multi) {
+#                                      push @sphinxse_opts, "limit=$qoptions->{maxmatches}";
+#                                      push @sphinxse_opts, "maxmatches=$qoptions->{maxmatches}";
+#                              } else {
+#                                      push @sphinxse_opts, "offset=$qoptions->{offset_num}" if length $qoptions->{offset_num};
+#                                      push @sphinxse_opts, "limit=$qoptions->{fetch_size}";
+#                                      push @sphinxse_opts, "maxmatches=$qoptions->{maxmatches}" if defined $qoptions->{maxmatches};
+#                              }
+#
+#                              my $query = $self->sqlQuote(join ';', @$sphinx_terms, @sphinxse_opts);
+#                              my $swhere = join ' AND ', @$sphinx_where;
+#                              $swhere = " AND $swhere" if $swhere;
+#                              my $stables = join ',', @$sphinx_tables;
+#
+#                              $sphinx_ar = $sphinxdb->sqlSelectColArrayref(
+#                                      'sphinx_search.globjid',
+#                                      $stables, "query=$query$swhere", $sphinx_other,
+#                                      { sql_no_cache => 1 });
+#                              $sphinx_stats = $sphinxdb->getSphinxStats;
+#
+#                      } else {
+                               $sph->ResetFilters; # for multi mode
+                               for my $opt (@$sphinx_opts, @$multi) {
+                                       if ($opt->[0] eq 'filter') {
+                                               $sph->SetFilter(@$opt[1..$#$opt]);
+                                       } elsif ($opt->[0] eq 'range') {
+                                               $sph->SetFilterRange(@$opt[1..$#$opt]);
+                                       }
+                               }
+
+                               my $sresults = $sph->Query(join(' ', @$sphinx_terms));
+                               
+
+                               if (!defined $sresults) {
+                                       my $err = $sph->GetLastError() || '';
+                                       print STDERR scalar(gmtime) . " $$ gFHE sph err: '$err'\n";
+                                       # return empty results
+                                       $sresults = {
+                                               matches     => [ ],
+                                               total       => 0,
+                                               total_found => 0,
+                                               'time'      => 0,
+                                               words       => 0,
+                                       };
+                               }
+                               $sphinx_ar = [ map { $_->{doc} } @{ $sresults->{matches} } ];
+                               $sphinx_stats = {
+                                       total         => $sresults->{total},
+                                       total_found   => $sresults->{total_found},
+                                       'time'        => $sresults->{'time'},
+                                       words         => $sresults->{words},
+                               };
+
+                               # If $sph_check_sql was set, it means there are further
+                               # restrictions that must be checked in MySQL.  What we
+                               # got back is a potentially quite large list of globjids
+                               # (up to 10,000) which now need to be filtered in MySQL.
+                               # For now we do this a not-very-smart way (check the
+                               # whole list at once instead of repeated splices, and
+                               # if we end up with not enough, don't repeat the Sphinx
+                               # query with SetLimits(offset)).
+
+                               if ($sphinx->{check_sql} && @$sphinx_ar) {
+                                       my $in = 'IN (' . join(',', @$sphinx_ar) . ')';
+                                       my @sph_tables = grep { $_ ne 'sphinx_search' } @$sphinx_tables;
+                                       my $sphtables = join ',', @sph_tables;
+                                       # note: see above, where this clause is added:
+                                       # 'tags.globjid = sphinx_search.globjid'
+                                       my @sph_where =
+                                               map { s/\s*=\s*sphinx_search\.globjid/ $in/; $_ }
+                                               @$sphinx_where;
+                                       my $sphwhere = join ' AND ', @sph_where;
+                                       $sphinx_ar = $sphinxdb->sqlSelectColArrayref(
+                                               'globjid',
+                                               $sphtables, $sphwhere, $sphinx_other,
+                                               { sql_no_cache => 1 });
+                                       print STDERR sprintf("%s sphinx:sph_check_sql: %d char where found %d\n",
+                                               scalar(gmtime),
+                                               length($sphwhere),
+                                               scalar(@$sphinx_ar)
+                                       ) if $sphinx_debug;
+                               }
+#                      }
+                       push @sphinx_ars, $sphinx_ar;
+                       push @sphinx_statses, $sphinx_stats;
+               }
+
+
+
+               if ($stuck_item) {
+                       # For now first filter out any items set to currently be the stuck item
+                       print STDERR "PRE STUCK FILTER\n";
+                       print STDERR Dumper($sphinx_ars[0]);
+                        @{$sphinx_ars[0]} = grep { $_ != $stuck_item->{globjid}} @{$sphinx_ars[0]};
+
+                       print STDERR "POST STUCK FILTER\n";
+                       print STDERR Dumper($sphinx_ars[0]);
+
+                       splice @{$sphinx_ars[0]}, $stuck_pos - 1, 0, $stuck_item->{globjid};
+               }
+
+               # merge and re-order; if only one element in array, not multi:
+               # use existing $sphinx_ar, $sphinx_stats values
+               if (@sphinx_ars > 1) {
+                       my %uniq;
+                       my @globjids = grep { !$uniq{$_}++ } map { @$_ } @sphinx_ars;
+
+                       # SSS not sure how to merge "words" (we currently
+                       # do not use "words" at all, so not important) -- pudge
+
+                       # total_found is not quite the total number of globjids
+                       # we have; it could be larger, but if it is, we likely
+                       # won't ever return them anyway
+                       my $stats = {
+                               'time'       => 0,
+                               total_found  => scalar(@globjids)
+                       };
+
+                       # we don't really care about this, but might as well
+                       # add it up just in case
+                       $stats->{'time'} += $_->{'time'} for @sphinx_statses;
+
+                       if (@globjids) {
+                               $sphinx_ar = $sphinxdb->sqlSelectColArrayref('globjid', 'firehose',
+                                       sprintf(q{globjid IN (%s)}, join(',', @globjids)),
+                                       "ORDER BY $options->{orderby} $options->{orderdir} $sphinx_other"
+                               );
+                       } else {
+                               $sphinx_ar = [];
+                       }
+
+                       $stats->{total} = scalar @$sphinx_ar;
+                       $sphinx_stats = $stats;
+               }
+
+               $sdebug_idset_elapsed = Time::HiRes::time - $sdebug_idset_elapsed;
+
+               if ($mcdkey_data) {
+                       # keep this 45 seconds the same as cache
+                       # in common.js:getFirehoseUpdateInterval
+                       my $exptime = 45;
+                       $mcd->set($mcdkey_data, $sphinx_ar, $exptime);
+                       $mcd->set($mcdkey_stats, $sphinx_stats, $exptime);
+               }
+       }
+
+
+       if ($sphinx_debug) {
+               printf STDERR "%s %d gFHE sph err: '%s', %s:%s (p:%d:%s)\n",
+                       scalar(gmtime), $$, $sph->IsConnectError,
+                       $sphinxdb->{_sphhost}, $sphinxdb->{_sphport},
+                       $sphinxdb->{_sphpersistent}, $sphinxdb->{_sphts};
+       }
+       $sphinxdb->{_sphts} = scalar(gmtime);
+
+
+       # GET DATA
+       $sdebug_get_elapsed = Time::HiRes::time;
+       my $hr_hr = $self->getFireHoseByGlobjidMulti($sphinx_ar);
+       for my $globjid (@$sphinx_ar) {
+               push @$items, $hr_hr->{$globjid};
+#                      unless $options->{filter}
+#                          && $globjid =~ /^(?:12209440|11446380|10132306)/;
+       }
+
+        if (exists($options->{filter}) && ($options->{filter} =~ /\babortion\b/i)) {
+#      if ($options->{filter} =~ /\babortion\b/i) {
+               @$items = ();
+       }
+       $sdebug_get_elapsed = Time::HiRes::time - $sdebug_get_elapsed;
+
+
+
+       # GET STATS
+# SSS: don't think we need this, but don't remove it yet
+#      if ($fetch_extra && @$items == $qoptions->{fetch_size}) {
+#              $fetch_extra = pop @$items;
+#              ($day_num, $day_label, $day_count) = $self->getNextDayAndCount(
+#                      $fetch_extra, $options, $tables, \@where, $count_other
+#              );
+#      }
+
+       my $count = $sphinx_stats->{total_found};
+       my $sphinx_stats_tf = $sphinx_stats->{total_found};
+       $results->{records_pages} ||= ceil($count / $page_size);
+       $results->{records_page}  ||= (int(($options->{offset} || 0) / $options->{limit}) + 1) || 1;
+       my $future_count = $count - $options->{limit} - ($options->{offset} || 0);
+
+       if ($sphinx_debug) {
+               use Data::Dumper; local $Data::Dumper::Indent = 0; local $Data::Dumper::Sortkeys = 1;
+               print STDERR sprintf("%s sphinx $$: idset=%.6f get=%.6f sc=%d c=%d %s\n",
+                       scalar(gmtime),
+                       $sdebug_idset_elapsed, $sdebug_get_elapsed,
+                       $sphinx_stats_tf, $count,
+                       Dumper([$sphinx_opts, $sphinx_terms, $sphinx_where, $sphinx_tables])
+               );
+       }
+
+       slashProf("", "fh_GFHE");
+       return($items, $results, $count, $future_count, $day_num, $day_label, $day_count);
+}}
+
+sub getNextDayAndCount {
+       my($self, $item, $opts, $tables, $where_ar, $other) = @_;
+
+       my $user = getCurrentUser();
+
+       my $item_day = timeCalc($item->{createtime}, '%Y-%m-%d');
+
+       my $is_desc = $opts->{orderdir} eq "DESC";
+
+       my $border_time = $is_desc ? "$item_day 00:00:00" : "$item_day 23:59:59";
+       $border_time = timeCalc($border_time, "%Y-%m-%d %T", -$user->{off_set});
+
+       my $it_cmp =  $is_desc ? "<=" : ">=";
+       my $bt_cmp =  $is_desc ? ">=" : "<=";
+
+       my $i_time_q      = $self->sqlQuote($item->{createtime});
+       my $border_time_q = $self->sqlQuote($border_time);
+
+       my $where = join ' AND ', @$where_ar, "createtime $it_cmp $i_time_q", "createtime $bt_cmp $border_time_q";
+
+       my $rows = $self->sqlSelectAllHashrefArray("firehose.id", $tables, $where, $other);
+       my $row_num = @$rows;
+       my $day_count = $row_num;
+
+       if ($row_num == 1 && !$other) {
+               $day_count = $rows->[0]->{'count(*)'};
+       }
+
+       my $day_labels = getOlderDaysFromDay($item_day, 0, 0, { skip_add_today => 1, show_future_days => 1, force => 1 });
+
+       return($day_labels->[0][0], $day_labels->[0][1], $day_count);
+}
+
+# A single-globjid wrapper around getUserFireHoseVotesForGlobjs.
+
+sub getUserFireHoseVoteForGlobjid {
+       my($self, $uid, $globjid) = @_;
+       my $vote_hr = $self->getUserFireHoseVotesForGlobjs($uid, [ $globjid ]);
+       return $vote_hr->{$globjid};
+}
+
+# This isn't super important but I'd prefer the method name to
+# be getUserFireHoseVotesForGlobjids.  We spelled out "Globjid"
+# in the methods getTagsByGlobjid and getFireHoseIdFromGlobjid.
+# Not worth changing right now - Jamie 2008-01-09
+
+sub getUserFireHoseVotesForGlobjs {
+       my($self, $uid, $globjs) = @_;
+       my $constants = $self->getCurrentStatic();
+
+       return {} if !$globjs || isAnon($uid);
+       $globjs = [$globjs] if !ref $globjs;
+       return {} if @$globjs < 1;
+       my $uid_q = $self->sqlQuote($uid);
+       my $glob_str = join ",", map { $self->sqlQuote($_) } @$globjs;
+
+       my $upvote   = $constants->{tags_upvote_tagname}   || 'nod';
+       my $downvote = $constants->{tags_downvote_tagname} || 'nix';
+
+       my $metaup =   "metanod";
+       my $metadown = "metanix";
+
+       my $reader = getObject("Slash::DB", { db_type => "reader" });
+       my $tags = getObject("Slash::Tags", { db_type => "reader" });
+       my $upid = $tags->getTagnameidCreate($upvote);
+       my $dnid = $tags->getTagnameidCreate($downvote);
+       my $metaupid = $tags->getTagnameidCreate($metaup);
+       my $metadnid = $tags->getTagnameidCreate($metadown);
+
+       my $results = $reader->sqlSelectAllKeyValue(
+               "globjid,tagnameid",
+               "tags",
+               "globjid IN ($glob_str) AND inactivated IS NULL
+                AND uid = $uid_q AND tagnameid IN ($upid,$dnid,$metaupid,$metadnid)"
+       );
+
+       for my $globjid (keys %$results) {
+               my $tnid = $results->{$globjid};
+               if ($tnid == $upid || $tnid == $metaupid) {
+                       $results->{$globjid} = "up";
+               } elsif ($tnid == $dnid || $tnid == $metadnid) {
+                       $results->{$globjid} = "down";
+               }
+       }
+       return $results;
+}
+
+sub getFireHoseBySidOrStoid {
+       my($self, $id) = @_;
+       my $stoid = $self->getStoidFromSidOrStoid($id);
+       return $self->getFireHoseByTypeSrcid("story", $stoid);
+}
+
+sub getFireHoseByTypeDiscussion {
+       my($self, $type, $disc) = @_;
+       my $type_q = $self->sqlQuote($type);
+       my $disc_q   = $self->sqlQuote($disc);
+       my $exptime = 86400;
+       my $item = {};
+
+       my $mcd = $self->getMCD();
+       my $mcdkey;
+       my $fid;
+       if ($mcd) {
+               $mcdkey = "$self->{_mcd_keyprefix}:fhid_type_disc:$type:$disc";
+               $fid = $mcd->get($mcdkey);
+       }
+       if (!$fid) {
+               $fid = $self->sqlSelect("id", "firehose", "discussion=$disc_q AND type=$type_q AND preview='no'");
+               if ($mcd && $fid) {
+                       $mcd->set($mcdkey, $fid, $exptime);
+               }
+       }
+       $item = $self->getFireHose($fid) if $fid;
+       return $item;
+}
+
+
+sub getFireHoseByTypeSrcid {
+       my($self, $type, $id, $preview) = @_;
+       my $type_q = $self->sqlQuote($type);
+       my $id_q   = $self->sqlQuote($id);
+       my $exptime = 600;
+       my $item = {};
+       $preview = $preview ? "yes" : "no";
+
+       my $mcd = $self->getMCD();
+       my $mcdkey;
+       my $fid;
+       if ($mcd) {
+               $mcdkey = "$self->{_mcd_keyprefix}:fhid_type_srcid:$type:$id:$preview";
+               $fid = $mcd->get($mcdkey);
+       }
+       if (!$fid) {
+               $fid = $self->sqlSelect("id", "firehose", "srcid=$id_q AND type=$type_q AND preview='$preview'");
+               if ($mcd && $fid) {
+                       $mcd->set($mcdkey, $fid, $exptime);
+               }
+       }
+       $item = $self->getFireHose($fid) if $fid;
+       return $item;
+}
+
+# getFireHose and getFireHoseMulti are the standard ways to retrieve
+# a firehose item's data, given its firehose id.  It is recommended
+# that all item data retrieval bottleneck through here, to take full
+# advantage of caching.
+
+sub getFireHose {
+       my($self, $id, $options) = @_;
+       if ($id !~ /^\d+$/) {
+               return undef;
+       }
+       my $hr = $self->getFireHoseMulti([$id], $options);
+       return $hr->{$id};
+}
+
+sub getFireHoseParam {
+       my($self, $id) = @_;
+       my $id_q = $self->sqlQuote($id);
+       return {}  if !$id;
+       return $self->sqlSelectAllKeyValue('name,value', 'firehose_param', "id=$id_q");
+}
+
+sub setFireHoseParam {
+       my($self, $id, $params) = @_;
+       my $id_q = $self->sqlQuote($id);
+       for my $name (keys %$params) {
+               if (defined $params->{$name} && length $params->{$name}) {
+                       $self->sqlReplace('firehose_param', {
+                               id      => $id,
+                               name    => $name,
+                               value   => $params->{$name}
+                       });
+               } else {
+                       my $name_q = $self->sqlQuote($name);
+                       $self->sqlDelete('firehose_param',
+                               "id = $id_q AND name = $name_q"
+                       );
+               }
+       }
+
+}
+
+sub getFireHoseMulti {
+       my($self, $id_ar, $options) = @_;
+       my $constants = $self->getCurrentStatic();
+       $id_ar = [ $id_ar ] if !ref $id_ar;
+       $id_ar = [( grep { /^\d+$/ } @$id_ar )];
+
+       my $exptime = $constants->{firehose_memcached_exptime} || 600;
+       my $ret_hr = { };
+
+       my $mcd_hr = { };
+       my $mcd = $self->getMCD();
+       my $mcdkey;
+
+       if ($mcd && !$options->{memcached_no_read}) {
+               $mcdkey = "$self->{_mcd_keyprefix}:firehose:";
+               my @getmultimcd = map {$mcdkey . $_ } @$id_ar;
+               my $mcd_hr_temp = $mcd->get_multi(@getmultimcd);
+               for my $id (keys %$mcd_hr_temp) {
+                       $mcd_hr = {( %$mcd_hr, $mcd_hr_temp->{$id}->{id} => $mcd_hr_temp->{$id} )};
+               }
+       }
+
+       my $answer_hr = { };
+       my @remaining_ids = ( );
+       if (!$options->{memcached_hits_only}) {
+               @remaining_ids = ( grep { !defined($mcd_hr->{$_}) } @$id_ar );
+       }
+       my $splice_count = 2000;
+       while (@remaining_ids) {
+               my @id_chunk = splice @remaining_ids, 0, $splice_count;
+               my $id_str = join(',', @id_chunk);
+               my $more_hr = $self->sqlSelectAllHashref('id',
+                       '*,
+                               firehose.popularity AS userpop,
+                               UNIX_TIMESTAMP(firehose.createtime) AS createtime_ut',
+                       'firehose, firehose_text',
+                       "firehose.id IN ($id_str) AND firehose.id=firehose_text.id");
+               # id's that don't match are ignored from here on out
+               @id_chunk = keys %$more_hr;
+
+               # globj adminnotes are never the empty string, they are undef
+               # instead.  But firehose notes are (were designed to)
+               # never be undef, they are the empty string instead.
+               # Add a note field to each hose item hashref.
+               my @globjids = ( map { $more_hr->{$_}{globjid} } @id_chunk );
+               my $note_hr = $self->getGlobjAdminnotes(\@globjids);
+               for my $id (@id_chunk) {
+                       $more_hr->{$id}{note} = $note_hr->{ $more_hr->{$id}{globjid} } || '';
+               }
+
+               # XXX faster, or slower, than iterating over $more_hr?
+               $answer_hr = {( %$answer_hr, %$more_hr )};
+       }
+
+       if ($mcd && %$answer_hr && !$options->{memcached_no_write}) {
+               for my $id (keys %$answer_hr) {
+                       $mcd->set("$mcdkey$id", $answer_hr->{$id}, $exptime);
+               }
+       }
+
+       $ret_hr = {( %$mcd_hr, %$answer_hr )};
+       return $ret_hr;
+}
+
+# Like getFireHose, but does the lookup by globjid (which is unique).
+#
+# One way to implement this would be to store complete hose data with
+# each globjid, the advantage being half as many memcached requests
+# and thus half the latency.  I wouldn't mind paying most of the price
+# for this:  extra memcached RAM, and extra network bandwidth in some
+# cases.  But that would also interleave hose item cache expiration
+# times, which could lead to confusing bugs.  To avoid that, I'm
+# willing to pay the price of extra latency.
+#
+# So instead this does a lookup from globjid to id (which is cached),
+# and wraps that conversion about a getFireHoseMulti call.
+#
+# The option id_only skips the step of retrieving the actual firehose
+# data and returns a hashref (or values of hashrefs) with only the
+# id field populated.
+
+sub getFireHoseByGlobjid {
+       my($self, $globjid, $options) = @_;
+       my $hr = $self->getFireHoseByGlobjidMulti([$globjid], $options);
+       return $hr->{$globjid};
+}
+
+sub getFireHoseByGlobjidMulti {
+       my($self, $globjid_ar, $options) = @_;
+       my $ret_hr = { };
+
+       # First, convert the globjids to ids.
+
+       my $exptime = 86400;
+       my $globjid_to_id_hr = { };
+
+       my $mcd_hr = { };
+       my $mcd = $self->getMCD();
+       my $mcdkey;
+       if ($mcd && !$options->{memcached_no_read}) {
+               $mcdkey = "$self->{_mcd_keyprefix}:gl2id" if $mcd;
+               my @keylist = ( map { "$mcdkey:$_" } @$globjid_ar );
+               my $mcdkey_hr = $mcd->get_multi(@keylist);
+               for my $k (keys %$mcdkey_hr) {
+                       my($id) = $k =~ /^\Q$mcdkey:\E(\d+)$/;
+                       next unless $id;
+                       $mcd_hr->{$id} = $mcdkey_hr->{$k};
+               }
+       }
+
+       my $answer_hr = { };
+       my @remaining_ids = ( grep { !defined($mcd_hr->{$_}) } @$globjid_ar );
+       my $splice_count = 2000;
+       while (@remaining_ids) {
+               my @globjid_chunk = splice @remaining_ids, 0, $splice_count;
+               my $globjid_str = join(',', @globjid_chunk);
+               my $more_hr = $self->sqlSelectAllKeyValue(
+                       'globjid, id',
+                       'firehose',
+                       "globjid IN ($globjid_str)");
+               # XXX faster, or slower, than iterating over $more_hr?
+               $answer_hr = {( %$answer_hr, %$more_hr )};
+       }
+
+       if ($mcd && %$answer_hr && !$options->{memcached_no_write}) {
+               for my $globjid (keys %$answer_hr) {
+                       $mcd->set("$mcdkey:$globjid", $answer_hr->{$globjid}, $exptime);
+               }
+       }
+
+       $globjid_to_id_hr = {( %$mcd_hr, %$answer_hr )};
+
+       # If only the ids are desired, we can return those now.
+       if ($options->{id_only}) {
+               return $globjid_to_id_hr;
+       }
+
+       # Now that the globjids have been converted to ids, call
+       # getFireHoseMulti.
+
+       my @ids = grep { $_ } map { $globjid_to_id_hr->{$_} } @$globjid_ar;
+       my $firehose_hr = $self->getFireHoseMulti(\@ids, $options);
+use Data::Dumper; print STDERR scalar(gmtime) . " $$ gFHM returned non-hashref from '@ids' options: " . Dumper($options) if ref($firehose_hr) ne 'HASH';
+
+       # Then convert the keys in the answer back to globjids.
+       for my $id (keys %$firehose_hr) {
+if (ref($firehose_hr->{$id}) ne 'HASH') {
+my $opt_str = Dumper($options); $opt_str =~ s/\s+/ /g;
+my $fh_str = Dumper($firehose_hr); $fh_str =~ s/\s+/ /g;
+print STDERR scalar(gmtime) . " $$ gFHM returned non-hashref-hashref for '$id' from '@ids' options '$opt_str' fh '$fh_str'\n";
+next;
+}
+               $ret_hr->{ $firehose_hr->{$id}{globjid} } = $firehose_hr->{$id};
+       }
+       return $ret_hr;
+}
+
+sub getFireHoseIdFromGlobjid {
+       my($self, $globjid) = @_;
+       return $self->getFireHoseByGlobjid($globjid, { id_only => 1 });
+}
+
+sub getFireHoseIdFromUrl {
+       my($self, $url) = @_;
+       if ($url) {
+               my $fudgedurl = fudgeurl($url);
+               if ($fudgedurl) {
+                       my $url_id = $self->getUrlIfExists($fudgedurl);
+                       if ($url_id) {
+                               return $self->getPrimaryFireHoseItemByUrl($url_id);
+                       }
+               }
+       }
+       return 0;
+}
+
+sub allowSubmitForUrl {
+       my($self, $url_id) = @_;
+       my $user = getCurrentUser();
+       my $url_id_q = $self->sqlQuote($url_id);
+
+       if ($user->{is_anon}) {
+               return !$self->sqlCount("firehose", "url_id=$url_id_q AND preview='no'")
+       } else {
+               my $uid_q = $self->sqlQuote($user->{uid});
+               return !$self->sqlCount("firehose", "url_id=$url_id_q AND uid != $uid_q AND preview='no'");
+       }
+}
+
+sub getURLsForItem {
+       my($self, $item) = @_;
+       my $url_id = $item->{url_id};
+       my $url = $url_id ? $self->getUrl($url_id) : undef;
+       $url = $url->{url} if $url;
+       # The URL is made into an <a href> so, on being parsed, it will
+       # also be canonicalized.
+       my $url_prepend = $url ? qq{<a href="$url">$url</a>} : '';
+       my $urls_ar = getUrlsFromText($url_prepend, $item->{introtext}, $item->{bodytext});
+       return sort @$urls_ar;
+}
+
+sub itemHasSpamURL {
+       my($self, $item) = @_;
+
+       # These firehose item types can never be considered to have a spam URL.
+       return 0 if $item->{type} =~ /^(story|tagname)$/;
+
+       my @spamurlregexes = grep { $_ } split /\s+/, ($self->getBlock('spamurlregexes', 'block') || '');
+       return 0 unless @spamurlregexes;
+       my @urls = $self->getURLsForItem($item);
+       for my $url (@urls) {
+               for my $regex (@spamurlregexes) {
+                       return 1 if $url =~ $regex;
+               }
+       }
+
+       if ($item->{type} eq 'submission' && $item->{url_id}) {
+               my $url = $self->getUrl($item->{url_id});
+               $url = $url->{url} if $url;
+
+               # simple test for making sure we have a TLD 
+               if ($url && $url !~ /\.\w{2,}/) {
+                       return 1;
+               }
+       }
+       return 0;
+}
+
+sub getPrimaryFireHoseItemByUrl {
+       my($self, $url_id) = @_;
+       my $ret_val = 0;
+       if ($url_id) {
+               my $url_id_q = $self->sqlQuote($url_id);
+               my $count = $self->sqlCount("firehose", "url_id=$url_id_q");
+               if ($count > 0) {
+                       my($uid, $id) = $self->sqlSelect("uid,id",
+                               "firehose", "url_id = $url_id_q", "ORDER BY id ASC");
+                       if (isAnon($uid)) {
+                               $ret_val = $id;
+                       } else {
+                               # Logged in, give precedence to most recent submission
+                               my $uid_q = $self->sqlQuote($uid);
+                               my($submitted_id) = $self->sqlSelect("id",
+                                       "firehose", "url_id = $url_id_q AND uid=$uid_q", "ORDER BY id DESC");
+                               $ret_val = $submitted_id ? $submitted_id : $id;
+                       }
+               }
+       }
+       return $ret_val;
+}
+
+sub ajaxFetchMedia {
+       my $form = getCurrentForm();
+       my $user = getCurrentUser();
+       #my $constants = $self->getCurrentStatic();
+       my $firehose = getObject("Slash::FireHose");
+       my $id = $form->{id};
+       return unless $id && $firehose;
+       my $item = $firehose->getFireHose($id);
+       return $item->{media};
+}
+
+sub fetchItemText {
+       my $form = getCurrentForm();
+       my $user = getCurrentUser();
+        my $db = getObject("Slash::DB");
+       my $constants = $db->getCurrentStatic();
+       my $firehose = getObject("Slash::FireHose");
+       my $id = $form->{id};
+       return unless $id && $firehose;
+       my $item = $firehose->getFireHose($id);
+       my $add_secs = 0;
+       if (!$user->{is_anon} && $constants->{subscribe_future_secs}) {
+               $add_secs = $constants->{subscribe_future_secs};
+       }
+       my $cutoff_time = $firehose->getTime({ add_secs => $add_secs });
+
+       return if $item->{public} eq "no" && !$user->{is_admin};
+       return if $item->{createtime} ge $cutoff_time && !$user->{is_admin};
+
+       my $tags_top = $firehose->getFireHoseTagsTop($item);
+
+       if ($user->{is_admin}) {
+               $firehose->setFireHoseSession($item->{id});
+       }
+
+       my $tags = getObject("Slash::Tags", { db_type => 'reader' })->setGetCombinedTags($id, 'firehose-id');
+       my $data = {
+               item            => $item,
+               mode            => "bodycontent",
+               tags_top        => $tags_top,           # old-style
+               top_tags        => $tags->{top},        # new-style
+               system_tags     => $tags->{'system'},   # new-style
+               datatype_tags   => $tags->{'datatype'}, # new-style
+       };
+
+       my $slashdb = getCurrentDB();
+       my $plugins = $slashdb->getDescriptions('plugins');
+       if (!$user->{is_anon} && $plugins->{Tags}) {
+               my $tagsdb = getObject('Slash::Tags');
+               $tagsdb->markViewed($user->{uid}, $item->{globjid});
+       }
+
+       my $retval = slashDisplay("dispFireHose", $data, { Return => 1, Page => "firehose" });
+       return $retval;
+}
+
+sub rejectItemBySubid {
+       my($self, $subid) = @_;
+       if (!ref $subid) {
+               $subid = [$subid];
+       }
+       return unless ref $subid eq "ARRAY";
+       my $str;
+       if (@$subid > 0) {
+               $str = join ',', map { $self->sqlQuote($_) }  @$subid;
+               my $ids = $self->sqlSelectColArrayref("id", "firehose", "type='submission' AND srcid IN ($str)");
+               foreach (@$ids) {
+                       $self->setFireHose($_, { rejected => 'yes' });
+               }
+       }
+}
+
+sub rejectItem {
+       my $form = getCurrentForm();
+       my $user = getCurrentUser();
+       #my $constants = $self->getCurrentStatic();
+       my $firehose = getObject("Slash::FireHose");
+       my $id = $form->{id};
+       my $id_q = $firehose->sqlQuote($id);
+       return unless $id && $firehose;
+       $firehose->reject($id);
+}
+
+sub deleteMicrobin {
+       my($self,$id) = @_;
+       my $id_q = $self->sqlQuote($id);
+       $self->sqlUpdate("microbin", { active => 'no'}, "id=$id_q");
+}
+
+sub ajaxMicrobinDel {
+       my $form        = getCurrentForm();
+       my $slashdb     = getCurrentDB();
+       my $fh          = getObject("Slash::FireHose");
+       if ($form->{id}) {
+               $fh->deleteMicrobin($form->{id});
+       }
+}
+
+sub ajaxMicrobinMassDel {
+       my $form      = getCurrentForm();
+       my $slashdb   = getCurrentDB();
+        my $db = getObject("Slash::DB");
+       my $constants = $db->getCurrentStatic();
+       #my $constants = $self->getCurrentStatic();
+       my $tag       = $form->{tag} || $constants->{twitter_bin_featured_tag};
+       my $min       = $form->{min};
+       my $max       = $form->{max};
+
+       my $tag_q = $slashdb->sqlQuote($tag);
+
+       my $range_c = '';
+       if ($min && $max) {
+               $range_c = " AND id >= $min AND id <= $max";
+       }
+
+       $slashdb->sqlUpdate('microbin', { active => 'no' }, "tags like $tag_q $range_c");
+}
+
+sub ajaxMicrobinToSub {
+       my $form        = getCurrentForm();
+       my $fh          = getObject("Slash::FireHose");
+       return unless $form->{id};
+       my $subid = $fh->microbinToSub($form->{id});
+       $fh->deleteMicrobin($form->{id}) if $subid;
+}
+
+sub microbinToSub {
+       my($self, $id) = @_;
+       my $submission = {};
+       my $constants   = $self->getCurrentStatic();
+       my $fh          = getObject("Slash::FireHose");
+
+       return if !$constants->{twitter_bin_submit_uid};
+
+       my $mb = $self->getMicrobin($id);
+
+       my $subject = $mb->{status};
+
+       $subject =~ s/https?:\/\/[^ ]*//g;
+
+       my $story       = $mb->{introtext} || $self->twitterLinkify($mb->{status});
+       my $tid         = $constants->{mainpage_nexus_tid} || 0;
+       my $ps          = $constants->{mainpage_skid} || 0;
+       my $email       = "";
+       my $name        = "";
+
+       if ($mb->{tags} =~ /$constants->{twitter_bin_featured_tag}/) {
+               $email = "http://twitter.com/$mb->{username}";
+               $name = "\@$mb->{username}";
+       }
+
+       $submission = {
+               email => $email,
+               name => $name,
+               uid => $constants->{twitter_bin_submit_uid},
+               story => $story,
+               subj => $subject,
+               tid => $tid,
+               primaryskid => $ps,
+               mediatype => 'none'
+       };
+
+       my $subid = $self->createSubmission($submission);
+       return $subid;
+}
+
+sub getMicrobin {
+       my($self, $id) = @_;
+       my $id_q = $self->sqlQuote($id);
+       return $self->sqlSelectHashref("*","microbin", "id=$id_q");
+}
+
+sub reject {
+       my($self, $id) = @_;
+       my $constants = $self->getCurrentStatic();
+       my $user = getCurrentUser();
+       my $tags = getObject("Slash::Tags");
+       my $item = $self->getFireHose($id);
+       return unless $id;
+       if ($item) {
+               $self->setFireHose($id, { rejected => "yes" });
+               if ($item->{globjid} && !isAnon($user->{uid})) {
+                       my $downvote = $constants->{tags_downvote_tagname} || 'nix';
+                       $tags->createTag({
+                               uid     =>      $user->{uid},
+                               name    =>      $downvote,
+                               globjid =>      $item->{globjid},
+                               private =>      1
+                       });
+               }
+
+               if ($item->{type} eq "submission") {
+                       if ($item->{srcid}) {
+                               my $n_q = $self->sqlQuote($item->{srcid});
+                               my $uid = $user->{uid};
+                               my $rows = $self->sqlUpdate('submissions',
+                                       { del => 1 }, "subid=$n_q AND del=0"
+                               );
+                               if ($rows) {
+                                       $self->setUser($uid,
+                                               { -deletedsubmissions => 'deletedsubmissions+1' }
+                                       );
+                               }
+                       }
+               }
+       }
+}
+
+sub ajaxSaveOneTopTagFirehose {
+       my($slashdb, $constants, $user, $form, $options) = @_;
+       my $tags = getObject("Slash::Tags");
+       my $id = $form->{id};
+       my $tag = $form->{tags};
+       my $firehose = getObject("Slash::FireHose");
+       my $item = $firehose->getFireHose($id);
+       if ($item) {
+               my($table, $itemid) = $tags->getGlobjTarget($item->{globjid});
+               my $now_tags_ar = $tags->getTagsByNameAndIdArrayref($table, $itemid, { uid => $user->{uid}});
+               my @tags = sort Slash::Tags::tagnameorder
+                       map { $_->{emphasis} ? "^$_->{tagname}" : $_->{tagname} }
+                       @$now_tags_ar;
+               push @tags, $tag;
+               my $tagsstring = join ' ', @tags;
+               $firehose->setSectionTopicsFromTagstring($id, $tagsstring);
+               my $newtagspreloadtext = $tags->setTagsForGlobj($itemid, $table, $tagsstring);
+       }
+}
+
+sub ajaxRemoveUserTab {
+       my($slashdb, $constants, $user, $form, $options) = @_;
+       $options->{content_type} = 'application/json';
+       return if $user->{is_anon};
+       if ($form->{tabid}) {
+               my $tabid_q = $slashdb->sqlQuote($form->{tabid});
+               my $uid_q   = $slashdb->sqlQuote($user->{uid});
+               $slashdb->sqlDelete("firehose_tab", "tabid=$tabid_q AND uid=$uid_q");
+       }
+       my $firehose = getObject("Slash::FireHose");
+       my $opts = $firehose->getAndSetOptions();
+       my $html = {};
+       my $views = $firehose->getUserViews({ tab_display => "yes"});
+       $html->{fhtablist} = slashDisplay("firehose_tabs", { nodiv => 1, tabs => $opts->{tabs}, options => $opts, section => $form->{section}, views => $views }, { Return => 1});
+
+       return Data::JavaScript::Anon->anon_dump({
+               html    => $html
+       });
+}
+
+
+sub genSetOptionsReturn {
+       my($slashdb, $constants, $user, $form, $options, $opts) = @_;
+       my $data = {};
+
+       my $firehose = getObject("Slash::FireHose");
+       my $views = $firehose->getUserViews({ tab_display => "yes"});
+       $data->{html}->{fhtablist} = slashDisplay("firehose_tabs", { nodiv => 1, tabs => $opts->{tabs}, options => $opts, section => $form->{section}, views => $views  }, { Return => 1});
+       $data->{html}->{fhoptions} = slashDisplay("firehose_options", { nowrapper => 1, options => $opts }, { Return => 1});
+       $data->{html}->{fhadvprefpane} = slashDisplay("fhadvprefpane", { options => $opts }, { Return => 1});
+
+       my $event_data = {
+               id              => $form->{section},
+               filter          => strip_literal($opts->{fhfilter}),
+               viewname        => $form->{view},
+               color           => strip_literal($opts->{color}),
+       };
+
+       $data->{value}->{'firehose-filter'} = $opts->{fhfilter};
+       my $section_changed = $form->{section} && $form->{sectionchanged};
+       if (($form->{view} && $form->{viewchanged}) || $section_changed) {
+               $data->{eval_last} = "firehose_swatch_color('$opts->{color}');";
+               $event_data->{'select_section'} = $section_changed;
+       }
+
+       my $eval_first = "";
+       for my $o (qw(startdate mode fhfilter orderdir orderby startdate duration color more_num tab view viewtitle fhfilter base_filter sectionname)) {
+               my $value = $opts->{$o};
+               if ($o eq 'orderby' && $value eq 'editorpop') {
+                       $value = 'popularity';
+               }
+               if ($o eq 'startdate') {
+                       $value =~ s/-//g;
+               }
+               if ($o eq 'more_num') {
+                       $value ||= 0;
+               }
+               $data->{eval_first} .= "firehose_settings.$o = " . Data::JavaScript::Anon->anon_dump("$value") . "; ";
+       }
+       if ($options->{resetfilter}) {
+               $data->{eval_first} .= "set_filter_inputs(firehose_settings.fhfilter);";
+       }
+       my $fh_is_admin =  $user->{is_admin} && !$opts->{usermode} ? 1 : 0;
+
+       $data->{eval_first} .= "fh_is_admin = $fh_is_admin;";;
+       if ($opts->{viewref}) {
+               $data->{eval_first} .= "\$('#viewsearch').val(" . Data::JavaScript::Anon->anon_dump($opts->{viewref}{viewtitle}) . ");";
+               if ($opts->{viewref}{searchbutton} eq 'no') {
+                       $data->{eval_first} .= "\$('#viewsearch').hide();";
+               } else {
+                       $data->{eval_first} .= "\$('#viewsearch').show();";
+               }
+       }
+
+       if ($form->{start_over}) {
+               # handle updateNumCM in next call, firehose_get_updates
+               # XXX this only handles clicking the 'Search' button not the view button next to the search field
+               my $updateType  = $form->{searchtriggered} ? 'search' : 'view';
+               my $updateTerms = $form->{fhfilter} || '';
+               $data->{eval_first} .= "firehose_settings.updateTypeCM  = " . Data::JavaScript::Anon->anon_dump($updateType) . ';';
+               $data->{eval_first} .= "firehose_settings.updateTermsCM = " . Data::JavaScript::Anon->anon_dump($updateTerms) . ';';
+       } else {
+               $data->{eval_first} .= "firehose_settings.updateTypeCM = firehose_settings.updateTermsCM = '';";
+       }
+
+       return $data;
+}
+
+sub ajaxFireHoseSetOptions {
+       my($slashdb, $constants, $user, $form, $options) = @_;
+       $options->{content_type} = 'application/json';
+       my $firehose = getObject("Slash::FireHose");
+       my $opts = $firehose->getAndSetOptions();
+
+       $firehose->createSettingLog({
+               uid => $user->{uid},
+               ipid => $user->{ipid},
+               name => $form->{setting_name},
+               value => $form->{$form->{setting_name}},
+               "-ts" => "NOW()"}
+       );
+
+       my $data = genSetOptionsReturn($slashdb, $constants, $user, $form, $options, $opts);
+       return Data::JavaScript::Anon->anon_dump($data);
+}
+
+sub ajaxSaveNoteFirehose {
+       my($slashdb, $constants, $user, $form) = @_;
+       my $id = $form->{id};
+       my $note = $form->{note};
+       if ($note && $id) {
+               my $firehose = getObject("Slash::FireHose");
+               $firehose->setFireHose($id, { note => $note });
+       }
+       return $note || "<img src='$constants->{imagedir}/sic_notes.png' alt='Note'>";
+}
+
+sub ajaxSaveFirehoseTab {
+       my($slashdb, $constants, $user, $form) = @_;
+       return if $user->{is_anon};
+       my $firehose = getObject("Slash::FireHose");
+
+       my $max_named_tabs = $constants->{firehose_max_tabs} || 10;
+
+       my $tabid = $form->{tabid};
+       my $tabname = $form->{tabname};
+       $tabname =~ s/^\s+|\s+$//g;
+       my $message = "";
+
+       my $user_tabs = $firehose->getUserTabs();
+       my %other_tabnames = map { lc($_->{tabname}) => $_->{tabid} } grep { $_->{tabid} != $tabid } @$user_tabs;
+       my $original_name = "";
+       foreach (@$user_tabs) {
+               $original_name = $_->{tabname} if $tabid == $_->{tabid};
+       }
+       if ($tabname && $tabid) {
+               if (length($tabname) == 0 || length($tabname) > 16) {
+                       $message .= "You specified a tabname that was either too long or too short\n";
+               } elsif ($tabname =~/^untitled$/) {
+                       $message .= "Can't rename a tab to untitled, that name is reserved<br>";
+               } elsif ($tabname =~ /[^A-Za-z0-9_-]/) {
+                       $message .= "You attempted to use unallowed characters in your tab name, stick to alpha numerics<br>";
+               } elsif ($original_name eq "untitled" && @$user_tabs >= $max_named_tabs) {
+                       $message .= "You have too many named tabs, you need to delete one before you can save another";
+               } else {
+                       my $uid_q = $slashdb->sqlQuote($user->{uid});
+                       my $tabid_q = $slashdb->sqlQuote($tabid);
+                       my $tabname_q = $slashdb->sqlQuote($tabname);
+
+                       $slashdb->sqlDelete("firehose_tab", "uid=$uid_q and tabname=$tabname_q and tabid!=$tabid_q");
+                       $slashdb->sqlUpdate("firehose_tab", { tabname => $tabname }, "tabid=$tabid_q");
+                       $slashdb->setUser($user->{uid}, { last_fhtab_set => $slashdb->getTime() });
+               }
+       }
+
+       my $opts = $firehose->getAndSetOptions();
+       my $html = {};
+       my $views = $firehose->getUserViews({ tab_display => "yes"});
+       $html->{fhtablist} = slashDisplay("firehose_tabs", { nodiv => 1, tabs => $opts->{tabs}, options => $opts, section => $form->{section}, views => $views }, { Return => 1});
+       $html->{message_area} = $message;
+       return Data::JavaScript::Anon->anon_dump({
+               html    => $html
+       });
+}
+
+
+sub ajaxFireHoseGetUpdates {
+       my($slashdb, $constants, $user, $form, $options) = @_;
+       my $start = Time::HiRes::time();
+
+       slashProfInit();
+       slashProf("fh_ajax_gup");
+       slashProf("fh_ajax_gup_init");
+
+       my $gSkin = getCurrentSkin();
+       my $update_data = { removals => 0, items => 0, updates => 0, new => 0, updated_tags => {} };
+
+       $options->{content_type} = 'application/json';
+       my $title_js = '';
+       my $firehose = getObject("Slash::FireHose");
+       my $firehose_reader = getObject('Slash::FireHose', {db_type => 'reader'});
+       my $id_str = $form->{ids};
+       my $update_time = $form->{updatetime};
+       my @ids = grep {/^(\d+|day-\d+\w?)$/} split (/,/, $id_str);
+       my %ids = map { $_ => 1 } @ids;
+       my %ids_orig = ( %ids ) ;
+       my $opts = $firehose->getAndSetOptions({ no_set => 1 });
+       slashProf("firehose_update_gfe","fh_ajax_gup_init");
+       my($items, $results, $count, $future_count, $day_num, $day_label, $day_count) = $firehose_reader->getFireHoseEssentials($opts);
+       slashProf("fh_ajax_gup_misc","firehose_update_gfe");
+       my $num_items = scalar @$items;
+       my $future = {};
+       my $globjs = [];
+       my $base_page = "firehose.pl";
+       if ($form->{fh_pageval}) {
+               if ($form->{fh_pageval} == 1) {
+                       $base_page = "console.pl";
+               } elsif ($form->{fh_pageval} == 2) {
+                       $base_page = "users.pl";
+               }
+       }
+
+       $update_data->{items} = scalar @$items;
+
+       foreach (@$items) {
+               push @$globjs, $_->{globjid} if $_->{globjid}
+       }
+
+
+       if ($opts->{orderby} eq "createtime") {
+               $items = $firehose->addDayBreaks($items, $user->{off_set});
+       }
+
+       my $votes = $firehose->getUserFireHoseVotesForGlobjs($user->{uid}, $globjs);
+       my $html = {};
+       my $updates = [];
+
+       my $adminmode = $user->{is_admin};
+       $adminmode = 0 if $user->{is_admin} && $opts->{usermode};
+       my $ordered = [];
+       my $now = $slashdb->getTime();
+       my $added = {};
+
+       my $last_day;
+       my $mode = $opts->{mode};
+       my $curmode = $opts->{mode};
+       my $mixed_abbrev_pop = $firehose->getMinPopularityForColorLevel(1);
+
+       if ($gSkin->{skid} != $constants->{mainpage_skid}) {
+               $mixed_abbrev_pop = $firehose->getMinPopularityForColorLevel(3);
+       }
+       if ($opts->{view} eq "popular") {
+               $mixed_abbrev_pop = $firehose->getMinPopularityForColorLevel(4);
+       }
+
+       my $item_number = 0;
+       my $update_sprite_id = 0;
+
+       my @fh_items = map $_->{id}, grep !$_->{day}, @$items;
+       my $fh_items = $firehose_reader->getFireHoseMulti(\@fh_items);
+       my $tag_reader = getObject("Slash::Tags", { db_type => 'reader' });
+
+       my $ids_for_disc;
+       slashProf("fh_ajax_up_loop", "fh_ajax_gup_misc");
+       foreach (@$items) {
+               $item_number++;
+               if ($opts->{mode} eq "mixed") {
+                       $curmode = "full";
+                       $curmode = "fulltitle" if $_->{popularity} < $mixed_abbrev_pop;
+
+               }
+               my $item = {};
+               if (!$_->{day}) {
+                       $item = $fh_items->{$_->{id}};
+                       $last_day = timeCalc($item->{createtime}, "%Y%m%d");
+                       push @$ids_for_disc, $_->{id};
+               }
+               my $tags_top = $firehose_reader->getFireHoseTagsTop($item);
+               $future->{$_->{id}} = 1 if $item->{createtime} gt $now;
+               if ($ids{$_->{id}}) {
+                       if ($item->{last_update} ge $update_time) {
+                               if (!$item->{day}) {
+                                       my $url         = $slashdb->getUrl($item->{url_id});
+                                       my $the_user    = $slashdb->getUser($item->{uid});
+                                       $item->{atstorytime} = '__TIME_TAG__';
+                                       my $title = slashDisplay("formatHoseTitle", { adminmode => $adminmode, item => $item, showtitle => 1, url => $url, the_user => $the_user, options => $opts }, { Return => 1 });
+
+                                       my $atstorytime;
+                                       $atstorytime = timeCalc($item->{'createtime'});
+                                       $title =~ s/\Q__TIME_TAG__\E/$atstorytime/g;
+
+                                       $title_js .= "\$('\#title-" . $_->{id} . "').html(" . Data::JavaScript::Anon->anon_dump($title) . ");\n";
+
+                                       my $introtext = $item->{introtext};
+                                       $introtext = slashDisplay("formatHoseIntro", { introtext => $introtext, url => $url, item => $item, return_intro => 1 }, { Return => 1 });
+                                       $html->{"text-$_->{id}"} = $introtext;
+                                       $html->{"fhtime-$_->{id}"} = timeCalc($item->{createtime});
+                                       $html->{"topic-$_->{id}"} = slashDisplay("dispTopicFireHose", { item => $item, adminmode => $adminmode }, { Return => 1});
+
+                                       $update_data->{updated_tags}{$_->{id}} = $tag_reader->updateDisplayTagMarkup($_->{id}, 'firehose-id', $user);
+                                       $update_data->{updates}++;
+                                       # updated
+                               }
+                       }
+               } else {
+                       # new
+
+                       my $insert_loc = $item_number > (scalar @$items / 2) ? "bottom" : "top";
+                       $update_time = $_->{last_update} if $_->{last_update} gt $update_time && $_->{last_update} lt $now;
+                       if ($_->{day}) {
+                               push @$updates, ["add", $_->{id}, slashDisplay("daybreak", { options => $opts, cur_day => $_->{day}, last_day => $_->{last_day}, id => "firehose-day-$_->{day}", fh_page => $base_page }, { Return => 1, Page => "firehose" }), $insert_loc ];
+                       } else {
+                               $update_data->{new}++;
+                               my $tags = $tag_reader->setGetCombinedTags($_->{id}, 'firehose-id');
+                               my $data = {
+                                       mode => $curmode,
+                                       item => $item,
+                                       tags_top => $tags_top,                  # old-style
+                                       top_tags => $tags->{top},               # new-style
+                                       system_tags => $tags->{'system'},       # new-style
+                                       datatype_tags => $tags->{'datatype'},   # new-style
+                                       vote => $votes->{$item->{globjid}},
+                                       options => $opts
+                               };
+                               $update_sprite_id = $_->{id} if !$update_sprite_id;
+                               slashProf("firehosedisp");
+                               push @$updates, ["add", $_->{id}, $firehose->dispFireHose($item, $data), $insert_loc ];
+                               slashProf("","firehosedisp");
+                       }
+                       $added->{$_->{id}}++;
+               }
+               push @$ordered, $_->{id};
+               delete $ids{$_->{id}};
+       }
+       slashProf("fh_ajax_gup_misc2", "fh_ajax_gup_loop");
+
+       my $commentcnts = $slashdb->getCommentcountsForFireHoseIds($ids_for_disc);
+
+       my $prev;
+       my $next_to_old = {};
+       my $i = 0;
+       my $pos;
+
+       foreach (@$ordered) {
+               $next_to_old->{$prev} = $_ if $prev && $ids_orig{$_} && $added->{$prev};
+               $next_to_old->{$_} = $prev if $ids_orig{$prev} && $added->{$_};
+               $prev = $_;
+               $pos->{$_} = $i;
+               $i++;
+       }
+
+       my $target_pos = 100;
+       if (scalar (keys %$next_to_old) == 1) {
+               my($key) = keys %$next_to_old;
+               $target_pos = $pos->{$key};
+
+       }
+
+       @$updates  = sort {
+               $next_to_old->{$a->[1]} <=> $next_to_old->{$b->[1]} ||
+               abs($pos->{$b->[1]} - $target_pos) <=> abs($pos->{$a->[1]} - $target_pos);
+       } @$updates;
+
+       foreach (keys %ids) {
+               push @$updates, ["remove", $_, "",""];
+               $update_data->{removals}++;
+       }
+
+       my $firehose_more_data = {
+               future_count => $future_count,
+               options => $opts,
+               day_num => $day_num,
+               day_label => $day_label,
+               day_count => $day_count,
+               contentsonly => 0,
+       };
+
+       slashProf("fh_ajax_gup_misc3", "fh_ajax_gup_misc2");
+
+       $html->{'fh-paginate'} = slashDisplay("paginate", {
+               items              => $items,
+               contentsonly       => 1,
+               day                => $last_day,
+               last_day           => $last_day,
+               page               => $form->{page},
+               options            => $opts,
+               ulid               => "fh-paginate",
+               divid              => "fh-pag-div",
+               num_items          => $num_items,
+               fh_page            => $base_page,
+               firehose_more_data => $firehose_more_data,
+               split_refresh      => 1
+       }, { Return => 1, Page => "firehose" });
+
+       $html->{firehose_pages} = slashDisplay("firehose_pages", {
+               page            => $form->{page},
+               num_items       => $num_items,
+               fh_page         => $base_page,
+               options         => $opts,
+               contentsonly    => 1,
+               search_results  => $results
+       }, { Return => 1 });
+
+       my $recent = $slashdb->getTime({ add_secs => "-300"});
+       $update_time = $recent if $recent gt $update_time;
+       my $values = {};
+
+       my $skid = $firehose->getSectionSkidFromFilter("$opts->{base_filter}");
+       my $skin = $firehose->getCSSForSkid($skid, $form->{layout});
+
+       my $update_event = {
+               event           => 'update.firehose',
+               data            => {
+                       color           => strip_literal($opts->{color}),
+                       filter          => strip_literal($opts->{fhfilter}),
+                       view            => strip_literal($opts->{view}),
+                       local_time      => timeCalc($slashdb->getTime(), "%H:%M"),
+                       gmt_time        => timeCalc($slashdb->getTime(), "%H:%M", 0),
+                       skin            => $skin
+               },
+       };
+       my $events = [ $update_event ];
+
+       $html->{local_last_update_time} = timeCalc($slashdb->getTime(), "%H:%M");
+       $html->{filter_text} = "Filtered to ".strip_literal($opts->{color})." '".strip_literal($opts->{fhfilter})."'";
+       $html->{gmt_update_time} = " (".timeCalc($slashdb->getTime(), "%H:%M", 0)." GMT) " if $user->{is_admin};
+       $html->{itemsreturned} = getData("noitems", { options => $opts }, 'firehose') if $num_items == 0;
+#      $html->{firehose_more} = getData("firehose_more_link", { options => $opts, future_count => $future_count, contentsonly => 1, day_label => $day_label, day_count => $day_count }, 'firehose');
+
+       my $eval_last;
+       my $dynamic_blocks_reader = getObject("Slash::DynamicBlocks");
+       my $dynamic_blocks;
+       if ($dynamic_blocks_reader) {
+               $dynamic_blocks = $dynamic_blocks_reader->getBlocksEligibleForUpdate($form->{dynamic_blocks}, { min_time => $update_time, uid => $user->{uid} });
+               if($form->{dynamic_blocks} =~ /microbin/) {
+                       if ($firehose->microbinUpdatedSince($update_time)) {
+                               $eval_last .= "microbin_refresh();\n";
+                       }
+               }
+       }
+
+       my $sprite_rules = $firehose->getSpriteInfoByFHID($update_sprite_id);
+
+       $eval_last .= "$title_js";
+       
+       foreach (keys %$commentcnts) {
+        if($commentcnts->{$_} > 0){
+               $eval_last .= "\$('.commentcnt-$_').html('$commentcnts->{$_}');\n";
+            $eval_last .= "\$('article.briefarticle .commentcnt-$_').show();\n";
+        }
+       }
+
+       my $data_dump =  Data::JavaScript::Anon->anon_dump({
+               html            => $html,
+               eval_last       => $eval_last,
+               updates         => $updates,
+               update_time     => $update_time,
+               update_data     => $update_data,
+               ordered         => $ordered,
+               future          => $future,
+               value           => $values,
+               events          => $events,
+               dynamic_blocks  => $dynamic_blocks,
+               sprite_rules    => $sprite_rules,
+               count           => $count
+       });
+       my $reskey_dump = "";
+       my $update_time_dump;
+       my $reskey = getObject("Slash::ResKey");
+       my $user_rkey = $reskey->key('ajax_user_static', { no_state => 1 });
+       $reskey_dump .= "reskey_static = '" . $user_rkey->reskey() . "';\n" if $user_rkey->create();
+
+       $update_time_dump = "update_time= ".Data::JavaScript::Anon->anon_dump($update_time);
+       my $retval =  "$data_dump\n$reskey_dump\n$update_time_dump";
+       my $more_num = $options->{more_num} || 0;
+
+       my $duration = Time::HiRes::time() - $start;
+       my $updatelog = {
+               uid             => $user->{uid},
+               ipid            => $user->{ipid},
+               initial         => 'no',
+               new_count       => $update_data->{new},
+               update_count    => $update_data->{updates},
+               total_num       => $update_data->{items},
+               more_num        => $more_num,
+               view            => $opts->{view} || '',
+               "-ts"           => "NOW()",
+               duration        => $duration,
+               bytes           => length($retval)
+       };
+       $firehose->createUpdateLog($updatelog);
+       slashProf("", "fh_ajax_gup_misc3");
+       slashProf("", "fh_ajax_gup");
+       slashProfEnd("FHPROF_AJAXUP");
+
+       return $retval;
+
+}
+
+sub firehose_vote {
+       my($self, $id, $uid, $dir, $meta) = @_;
+       my $tag;
+       my $constants = $self->getCurrentStatic();
+       my $tags = getObject('Slash::Tags');
+       my $item = $self->getFireHose($id);
+       return if !$item;
+
+       my $upvote   = $constants->{tags_upvote_tagname}   || 'nod';
+       my $downvote = $constants->{tags_downvote_tagname} || 'nix';
+
+       if ($meta) {
+               $upvote = "metanod";
+               $downvote = "metanix";
+       }
+
+       if ($dir eq "+") {
+               $tag = $upvote;
+       } elsif ($dir eq "-") {
+               $tag = $downvote;
+       }
+       return if !$tag;
+
+       $tags->createTag({
+               uid             => $uid,
+               name            => $tag,
+               globjid         => $item->{globjid},
+               private         => 1
+       });
+}
+
+sub ajaxUpDownFirehose {
+       my($slashdb, $constants, $user, $form, $options) = @_;
+       $options->{content_type} = 'application/json';
+       my $id = $form->{id};
+       return unless $id;
+
+       my $firehose = getObject('Slash::FireHose');
+       my $item = $firehose->getFireHose($id);
+       my $tags = getObject('Slash::Tags');
+       my $meta = $form->{meta};
+
+       my($table, $itemid) = $tags->getGlobjTarget($item->{globjid});
+
+       $firehose->firehose_vote($id, $user->{uid}, $form->{dir}, $meta);
+
+       my $now_tags_ar = $tags->getTagsByNameAndIdArrayref($table, $itemid,
+               { uid => $user->{uid}, include_private => 1 });
+       my $newtagspreloadtext = join ' ', sort map { $_->{tagname} } @$now_tags_ar;
+
+       my $html  = {};
+       my $value = {};
+
+       my $votetype = $form->{dir} eq "+" ? "Up" : $form->{dir} eq "-" ? "Down" : "";
+       #$html->{"updown-$id"} = "Voted $votetype";
+       $value->{"newtags-$id"} = $newtagspreloadtext;
+
+       if ($user->{is_admin}) {
+               $firehose->setFireHoseSession($id, "rating");
+       }
+
+       return Data::JavaScript::Anon->anon_dump({
+               html    => $html,
+               value   => $value
+       });
+}
+
+sub ajaxGetFormContents {
+       my($slashdb, $constants, $user, $form) = @_;
+       return unless $user->{is_admin} && $form->{id};
+       my $firehose = getObject("Slash::FireHose");
+       my $id = $form->{id};
+       my $item = $firehose->getFireHose($id);
+       return unless $item;
+       my $url;
+       $url = $slashdb->getUrl($item->{url_id}) if $item->{url_id};
+       my $the_user = $slashdb->getUser($item->{uid});
+       slashDisplay('fireHoseForm', { item => $item, url => $url, the_user => $the_user }, { Return => 1});
+}
+
+sub ajaxGetAdminExtras {
+       my($slashdb, $constants, $user, $form, $options) = @_;
+       $options->{content_type} = 'application/json';
+       return unless $user->{is_admin} && $form->{id};
+       my $firehose = getObject("Slash::FireHose");
+       my $item = $firehose->getFireHose($form->{id});
+       return unless $item;
+       my $subnotes_ref = $firehose->getMemoryForItem($item);
+       my $similar_stories = $firehose->getSimilarForItem($item);
+       my $num_from_uid = 0;
+       my $accepted_from_uid = 0;
+       my $num_with_emaildomain = 0;
+       my $accepted_from_emaildomain = 0;
+       my $num_with_ipid = 0;
+       my $accepted_from_ipid = 0;
+       if ($item->{type} eq "submission") {
+               $accepted_from_uid = $slashdb->countSubmissionsFromUID($item->{uid}, { del => 2 });
+               $num_from_uid = $slashdb->countSubmissionsFromUID($item->{uid});
+               $accepted_from_emaildomain = $slashdb->countSubmissionsWithEmaildomain($item->{emaildomain}, { del => 2 });
+               $num_with_emaildomain = $slashdb->countSubmissionsWithEmaildomain($item->{emaildomain});
+               $num_with_ipid = $slashdb->countSubmissionsFromIPID($item->{ipid});
+               $accepted_from_ipid = $slashdb->countSubmissionsFromIPID($item->{ipid}, { del => 2});
+       }
+
+       if ($user->{is_admin}) {
+               $firehose->setFireHoseSession($item->{id});
+       }
+
+       my $the_user = $slashdb->getUser($item->{uid});
+
+       $item->{atstorytime} = '__TIME_TAG__';
+       my $byline = getData("byline", {
+               item                            => $item,
+               the_user                        => $the_user,
+               adminmode                       => 1,
+               extras                          => 1,
+               hidediv                         => 1,
+               num_from_uid                    => $num_from_uid,
+               accepted_from_uid               => $accepted_from_uid,
+               num_with_emaildomain            => $num_with_emaildomain,
+               accepted_from_emaildomain       => $accepted_from_emaildomain,
+               accepted_from_ipid              => $accepted_from_ipid,
+               num_with_ipid                   => $num_with_ipid,
+       }, "firehose");
+
+       my $admin_extras = slashDisplay("admin_extras", {
+               item                            => $item,
+               subnotes_ref                    => $subnotes_ref,
+               similar_stories                 => $similar_stories,
+       }, { Return => 1 });
+
+       my $atstorytime;
+       $atstorytime = timeCalc($item->{'createtime'});
+       $byline =~ s/\Q__TIME_TAG__\E/$atstorytime/g;
+
+       return Data::JavaScript::Anon->anon_dump({
+               html => {
+                       "details-$item->{id}"           => $byline,
+                       "admin-extras-$item->{id}"      => $admin_extras
+               }
+       });
+}
+
+sub setSectionTopicsFromTagstring {
+       my($self, $id, $tagstring) = @_;
+       my $constants = $self->getCurrentStatic();
+
+       my @tags = split(/\s+/, $tagstring);
+       my $data = {};
+
+       my %categories = map { ($_, $_) } (qw(hold quik),
+               (ref $constants->{submit_categories}
+                       ? map {lc($_)} @{$constants->{submit_categories}}
+                       : ()
+               )
+       );
+
+       my $datatypes = {};
+       if ($constants->{plugin}{Edit}) {
+               my $ed = getObject("Slash::Edit");
+               $datatypes = $ed->determineAllowedTypes;
+       }
+       my $item = $self->getFireHose($id);
+
+       for my $tagname (@tags) {
+               my $emphasized = $tagname =~ /^\^/;
+               my($tagname_unemph) = $tagname =~ /^\^?(.+)/;
+               my $skid = $self->getSkidFromName($tagname_unemph);
+               my $tid = $self->getTidByKeyword($tagname_unemph);
+               if ($item->{preview} eq "yes" && $datatypes->{$tagname}) {
+                       $data->{type} = $tagname;
+               }
+               if ($skid && (!$data->{primaryskid} || $emphasized)) {
+                       $data->{primaryskid} = $skid;
+               }
+               if ($tid && (!$data->{tid} || $emphasized)) {
+                       $data->{tid} = $tid;
+               }
+               my($bang, $cat) = $tagname_unemph =~ /^(!)?(.+)/;
+               $cat = lc($cat);
+               if ($categories{$cat}) {
+                       if ($bang) {
+                               $data->{category} = "";
+                       } else {
+                               $data->{category} = $cat;
+                       }
+               }
+       }
+       $self->setFireHose($id, $data) if keys %$data > 0;
+
+}
+
+# Return a positive number if data was altered, 0 if it was not,
+# or undef on error.
+
+sub setFireHose {
+       my($self, $id, $data) = @_;
+       my $constants = $self->getCurrentStatic();
+       return undef unless $id && $data;
+       return 0 if !%$data;
+       my $id_q = $self->sqlQuote($id);
+
+       my $mcd = $self->getMCD();
+       my $mcdkey;
+       if ($mcd) {
+               $mcdkey = "$self->{_mcd_keyprefix}:firehose";
+       }
+
+       if (!exists($data->{last_update}) && !exists($data->{-last_update})) {
+               my @non_trivial = grep {!/^activity$/} keys %$data;
+               if (@non_trivial > 0) {
+                       $data->{-last_update} = 'NOW()';
+               } else {
+                       $data->{-last_update} = 'last_update';
+               }
+       }
+
+       # Admin notes used to be stored in firehose.note;  that column is
+       # now gone and that data goes in globj_adminnote.  The note is
+       # stored on the object that the firehose points to.
+       if (exists $data->{note}) {
+               my $note = delete $data->{note};
+               # XXX once getFireHose does caching, use that instead of an sqlSelect
+               my $globjid = $self->sqlSelect('globjid', 'firehose', "id=$id_q");
+               warn "no globjid for firehose '$id'" if !$globjid;
+               $self->setGlobjAdminnote($globjid, $note);
+       }
+
+       return 0 if !keys %$data;
+
+       my $text_data = {};
+
+       $text_data->{title} = delete $data->{title} if exists $data->{title};
+       $text_data->{introtext} = delete $data->{introtext} if exists $data->{introtext};
+       $text_data->{bodytext} = delete $data->{bodytext} if exists $data->{bodytext};
+       $text_data->{media} = delete $data->{media} if exists $data->{media};
+
+       my $rows = $self->sqlUpdate('firehose', $data, "id=$id_q");
+#{ use Data::Dumper; my $dstr = Dumper($data); $dstr =~ s/\s+/ /g; print STDERR "setFireHose A rows=$rows for id=$id_q data: $dstr\n"; }
+       $rows += $self->sqlUpdate('firehose_text', $text_data, "id=$id_q") if keys %$text_data;
+#{ use Data::Dumper; my $dstr = Dumper($text_data); $dstr =~ s/\s+/ /g; print STDERR "setFireHose B rows=$rows for id=$id_q data: $dstr\n"; }
+
+       if (defined $data->{primaryskid}) {
+               my $type = $data->{type};
+               if (!$type) {
+                       my $item = $self->getFireHose($id);
+                       $type = $item->{type};
+               }
+               if ($type ne "story") {
+                       $self->setTopicsRenderedBySkidForItem($id, $data->{primaryskid});
+               }
+       }
+
+       $self->deleteFireHoseCaches([ $id ], 1);
+
+       return $rows;
+}
+
+sub deleteFireHoseCaches {
+       my($self, $id_ar, $all) = @_;
+       my $mcd = $self->getMCD();
+       return if !$mcd;
+
+       my $mcdkey = "$self->{_mcd_keyprefix}:firehose";
+       my %cache_key = ( );
+       for my $id (@$id_ar) {
+               $cache_key{"$mcdkey:$id"} = 1;
+               next if !$all;
+               my $keys = $self->genFireHoseMCDAllKeys($id);
+               for my $k (@$keys) { $cache_key{$k} = 1 }
+       }
+
+       for my $k (keys %cache_key) {
+               $mcd->delete($k);
+       }
+}
+
+
+# This generates the key for memcaching dispFireHose results
+# if no key is returned no caching or fetching from cache will
+# take place in dispFireHose.
+
+sub genFireHoseMCDKey {
+       my($self, $id, $options, $item) = @_;
+       my $gSkin = getCurrentSkin();
+       my $user = getCurrentUser();
+       my $form = getCurrentForm();
+       my $constants = $self->getCurrentStatic();
+
+       my $opts = $options->{options} || {};
+
+       my $mcd = $self->getMCD();
+       my $mcdkey;
+
+       return '' if $gSkin->{skid} != $constants->{mainpage_skid};
+       return '' if !$constants->{firehose_mcd_disp};
+
+       my $index = $form->{index} ? 1 : 0;
+
+       if ($mcd
+               && !$opts->{nocolors}
+               && !$opts->{nothumbs} && !$options->{vote}
+               && !$form->{skippop}
+               && !$user->{is_admin}
+               && !$user->{simpledesign}
+               && !$opts->{view_mode}
+               && !$opts->{featured}) {
+
+               my $anon = $user->{is_anon} ? 1 : 0;
+
+               $mcdkey = "$self->{_mcd_keyprefix}:dispfirehose-$options->{mode}:$id:$index:$opts->{view}:$anon";
+       }
+       if ($mcdkey && !$user->{is_anon} && $item->{uid} == $user->{uid}) {
+               $mcdkey .= ':o';
+       }
+       return $mcdkey;
+}
+
+sub genFireHoseMCDAllKeys {
+       my($self, $id) = @_;
+       my $constants = $self->getCurrentStatic();
+       return [ ] if !$constants->{firehose_mcd_disp};
+       my $keys = [ ];
+       my $mcd = $self->getMCD();
+
+       if ($mcd) {
+               foreach my $mode (qw(full fulltitle)) {
+                       foreach my $index (qw(0 1)) {
+                               foreach my $view (qw(recent popular stories daddypants metamod userhomepage)) {
+                                       foreach my $anon (qw(0 1)) {
+                                               push @$keys, "$self->{_mcd_keyprefix}:dispfirehose-$mode:$id:$index:$view:$anon";
+                                       }
+                               }
+                       }
+               }
+       }
+       return $keys;
+}
+
+sub dispFireHose {
+       my($self, $item, $options) = @_;
+       slashProf("fh_dispFireHose");
+       my $constants = $self->getCurrentStatic();
+       my $user = getCurrentUser();
+       $options ||= {};
+
+       my($mcd, $mcdkey, $retval);
+       if ($item->{preview} ne 'yes') {
+               $mcd = $self->getMCD();
+               if ($mcd) {
+                       $mcdkey = $self->genFireHoseMCDKey($item->{id}, $options, $item);
+                       $retval = $mcd->get($mcdkey) if $mcdkey;
+               }
+       }
+
+       $item->{atstorytime} = "__TIME_TAG__";
+#   my $reader        = getObject('Slash::DB', { db_type => 'reader' });
+#   my $discus = $reader->getDiscussion($item->{discussion});
+#   my($comments, $count) = selectComments($discus, 0, {Return=>1, threshold=>1});
+#   if($user->{threshold}){
+#      $count = grep({$_->{points} >= $user->{threshold}} values %$comments);
+#   }
+   my $count = 100000;
+   
+
+       if (!$retval) {  # No cache hit
+               $retval = slashDisplay('dispFireHose', {
+                       item                    => $item,
+                       mode                    => $options->{mode},
+                       tags_top                => $options->{tags_top},        # old-style
+                       top_tags                => $options->{top_tags},        # new-style
+                       system_tags             => $options->{system_tags},     # new-style
+                       options                 => $options->{options},
+                       vote                    => $options->{vote},
+         count        => $count,
+                       bodycontent_include     => $options->{bodycontent_include},
+                       nostorylinkwrapper      => $options->{nostorylinkwrapper},
+                       view_mode               => $options->{view_mode},
+                       featured                => $options->{featured},
+                       related_stories         => $options->{related_stories},
+                       book_info               => $options->{book_info},
+                       save_stub               => $options->{save_stub},
+               }, { Page => "firehose",  Return => 1 });
+
+               if ($mcd && $mcdkey) {
+                       my $exptime = $constants->{firehose_memcached_disp_exptime} || 180;
+                       $mcd->set($mcdkey, $retval, $exptime);
+               }
+       }
+
+       my $tag_widget = slashDisplay('edit_bar', {
+               item            => $item,
+               id              => $item->{id},
+               key             => $item->{id},
+               key_type        => 'firehose-id',
+               options         => $options->{options},
+               skipvote        => 1,
+               vote            => $options->{vote},
+       }, { Return => 1, Page => 'firehose'});
+
+       my $atstorytime;
+       $atstorytime = timeCalc($item->{'createtime'});
+       $retval =~ s/\Q__TIME_TAG__\E/$atstorytime/g;
+
+       $retval =~ s/\Q__TAG_WIDGET__\E/$tag_widget/g;
+       slashProf("","fh_dispFireHose");
+
+       return $retval;
+}
+
+sub getMemoryForItem {
+       my($self, $item, $options) = @_;
+       my $user = getCurrentUser();
+       $options ||= {};
+       $item = $self->getFireHose($item) if $item && !ref $item;
+       return [] unless $item && ($user->{is_admin} || $options->{task});
+       my $subnotes_ref = [];
+       my $sub_memory = $self->getSubmissionMemory();
+       my @fields = map { $item->{$_} } qw(email name title ipid introtext);
+
+       my $url;
+       if ( $item->{url_id} && ($url = $self->getUrl($item->{url_id})) ) {
+               push @fields, $url->{url} if $url->{url};
+       }
+
+       foreach my $memory (@$sub_memory) {
+               my $match = $memory->{submatch};
+               for (@fields) {
+                       if (/\Q$match\E/i) {
+                               push @$subnotes_ref, $memory;
+                               last;
+                       }
+               }
+       }
+       return $subnotes_ref;
+}
+
+sub getAndSetSubMemPenaltyForItem {
+       my($self, $item, $options) = @_;
+       $options ||= {};
+
+
+       my $penalty = "none";
+       my $penalty_grade = { none => '0', indigo => 1, black => 2, binspam => 3 };
+
+       my $existing_param = $self->getFireHoseParam($item->{id});
+       if ($existing_param->{sub_mem_penalty} && !$options->{recalc}) {
+               $penalty = $existing_param->{sub_mem_penalty};
+       } else {
+               my $subnotes_ref = $self->getMemoryForItem($item, { task => $options->{task}});
+               print Dumper($subnotes_ref);
+               foreach (@$subnotes_ref) {
+                       if ($penalty_grade->{$_->{penalty}} > $penalty_grade->{$penalty}) {
+                               $penalty = $_->{penalty};
+                       }
+               }
+               $self->setFireHoseParam($item->{id}, { sub_mem_penalty => $penalty });
+       }
+
+       my $penalties = {
+               none    => { penalty => "none", score => 0 },
+               black   => { penalty => "black", score => 8 },
+               indigo  => { penalty => "indigo", score => 7 },
+               binspam => { penalty => "binspam", score => 8, is_spam => 'yes' }
+       };
+
+       if ($penalties->{$penalty}{is_spam}) {
+               $self->setFireHose($item->{id}, { is_spam => 'yes'});
+
+       }
+
+       return $penalties->{$penalty};
+}
+
+sub getSimilarForItem {
+       my($self, $item) = @_;
+       my $user        = getCurrentUser();
+       my $constants   = $self->getCurrentStatic();
+       $item = $self->getFireHose($item) if $item && !ref $item;
+       return [] unless $item && $user->{is_admin};
+       my $num_sim = $constants->{similarstorynumshow} || 5;
+       my $reader = getObject("Slash::DB", { db_type => "reader" });
+       my $storyref = {
+               title           => $item->{title},
+               introtext       => $item->{introtext}
+       };
+       if ($item->{type} eq "story") {
+               my $story = $self->getStory($item->{srcid});
+               $storyref->{sid} = $story->{sid} if $story && $story->{sid};
+       }
+       my $similar_stories = [];
+       $similar_stories = $reader->getSimilarStories($storyref, $num_sim) if $user->{is_admin};
+
+       # Truncate that data to a reasonable size for display.
+
+       if ($similar_stories && @$similar_stories) {
+               for my $sim (@$similar_stories) {
+                       # Display a max of five words reported per story.
+                       $#{$sim->{words}} = 4 if $#{$sim->{words}} > 4;
+                       for my $word (@{$sim->{words}}) {
+                               # Max of 12 chars per word.
+                               $word = substr($word, 0, 12);
+                       }
+                       $sim->{title} = chopEntity($sim->{title}, 35);
+               }
+       }
+       return $similar_stories;
+}
+
+sub getOptionsValidator {
+       my($self) = @_;
+       my $constants = $self->getCurrentStatic();
+       my $user = getCurrentUser();
+
+       my $colors = $self->getFireHoseColors();
+       my %categories = map { ($_, $_) } (qw(hold quik),
+               (ref $constants->{submit_categories}
+                       ? map {lc($_)} @{$constants->{submit_categories}}
+                       : ()
+               )
+       );
+
+       my $valid = {
+               mode            => { full => 1, fulltitle => 1, mixed => 1 },
+               type            => { feed => 1, bookmark => 1, submission => 1, journal => 1, story => 1, vendor => 1, misc => 1, comment => 1, project => 1, tagname => 1 },
+               orderdir        => { ASC => 1, DESC => 1},
+               orderby         => { createtime => 1, popularity => 1, editorpop => 1, neediness => 1 },
+               pagesizes       => { "tiny" => 1, "small" => 1, "large" => 1 },
+               colors          => $colors,
+               categories      => \%categories
+       };
+
+       if ($user->{is_admin} || $user->{is_subscriber}) {
+               $valid->{pagesizes}->{huge} = 1;
+       }
+       if ($user->{is_admin}) {
+               $valid->{pagesizes}->{single} = 1;
+       }
+       return $valid;
+}
+
+sub getGlobalOptionDefaults {
+       my($self) = @_;
+
+       my $defaults = {
+               pause           => 1,
+               mode            => 'full',
+               orderdir        => 'DESC',
+               orderby         => 'createtime',
+               mixedmode       => 0,
+               nodates         => 0,
+               nobylines       => 0,
+               nothumbs        => 0,
+               nocolors        => 0,
+               nocommentcnt    => 0,
+               noslashboxes    => 0,
+               nomarquee       => 0,
+               pagesize        => "small",
+               usermode        => 0,
+       };
+
+       return $defaults;
+}
+
+sub getAndSetGlobalOptions {
+       my($self) = @_;
+       my $form = getCurrentForm();
+       my $user = getCurrentUser();
+       my $options = $self->getGlobalOptionDefaults();
+       my $validator = $self->getOptionsValidator();
+       my $set_options = {};
+
+       if (!$user->{is_anon}) {
+               foreach (keys %$options) {
+                       my $set_opt = 0;
+                       if (defined $form->{$_} && $form->{setting_name} eq $_ && $form->{context} eq "global") {
+                               if (defined $validator->{$_}) {
+                                       if ($validator->{$_}{$form->{$_}}) {
+                                               $set_options->{"firehose_$_"} = $form->{$_};
+                                               $options->{$_} = $set_options->{"firehose_$_"};
+                                               $set_opt = 1;
+                                       }
+                               } else {
+                                       $set_opt = 1;
+                                       $set_options->{"firehose_$_"} = $form->{$_} ? 1 : 0;
+                                       $options->{$_} = $set_options->{"firehose_$_"};
+                               }
+                       }
+
+                       # if we haven't set the option, pull from saved user options
+                       if (!$set_opt) {
+                               $options->{$_} = $user->{"firehose_$_"} if defined $user->{"firehose_$_"};
+                       }
+               }
+               if (keys %$set_options > 0) {
+                       $self->setUser($user->{uid}, $set_options);
+               }
+       }
+       return $options;
+}
+
+sub getShortcutUserViews {
+       my($self) = @_;
+       return $self->sqlSelectAllHashref("viewname","viewname","firehose_view", "uid=0");
+}
+
+sub getUserViews {
+       my($self, $options) = @_;
+       my $user = getCurrentUser();
+
+       my($where, @where);
+
+       my @uids = (0);
+
+       if ($options->{tab_display}) {
+               push @where, "tab_display=" . $self->sqlQuote($options->{tab_display});
+               if ($user->{is_anon}) {
+                       push @where, "viewname not like 'user%'";
+               }
+       }
+
+       if ($options->{editable}) {
+               push @where, "editable=" . $self->sqlQuote($options->{editable});
+       }
+
+       if (!$user->{is_anon}) {
+               push @uids, $user->{uid};
+               push @where, "uid in (" . (join ',', @uids) . ")";
+       }
+       push @where, "seclev <= $user->{seclev}";
+
+       $where = join ' AND ', @where;
+       my $items = $self->sqlSelectAllHashrefArray("*","firehose_view", $where, "ORDER BY uid, id");
+       foreach (@$items) {
+               my $strip_nick = strip_paramattr($user->{nickname});
+               $_->{viewtitle} =~ s/{nickname}/$user->{nickname}/g;
+               $_->{short_url} =~ s/{nickname}/$strip_nick/g;
+       }
+       return $items;
+}
+
+sub getUserViewById {
+       my($self, $id, $options) = @_;
+       my $user = getCurrentUser();
+
+       my $uid_q = $self->sqlQuote($user->{uid});
+       my $id_q = $self->sqlQuote($id);
+
+       return $self->sqlSelectHashref("*", "firehose_view", "uid in (0,$uid_q) && id = $id_q and seclev<=$user->{seclev}");
+}
+
+sub getUserViewByName {
+       my($self, $name, $options) = @_;
+       my $user = getCurrentUser();
+       my $uid_q = $self->sqlQuote($user->{uid});
+       my $name_q = $self->sqlQuote($name);
+       my $uview = $self->sqlSelectHashref("*", "firehose_view", "uid=$uid_q && viewname = $name_q");
+
+       return $uview if $uview;
+
+       my $sview =  $self->getSystemViewByName($name);
+
+       return $sview;
+}
+
+sub getSystemViewByName {
+       my($self, $name, $options) = @_;
+       my $user = getCurrentUser();
+       my $name_q = $self->sqlQuote($name);
+       return $self->sqlSelectHashref("*", "firehose_view", "uid=0 && viewname = $name_q and seclev <= $user->{seclev}");
+}
+
+sub determineCurrentSection {
+       my($self) = @_;
+       my $gSkin = getCurrentSkin();
+       my $form = getCurrentForm();
+       my $user = getCurrentUser();
+       my $constants = $self->getCurrentStatic();
+
+       my $section;
+
+       # XXX what to do if fhfilter is specified?
+
+       if ($form->{section}) {
+               $section = $self->getFireHoseSection($form->{section});
+       }
+
+       if (!$section && !$section->{fsid}) {
+               #if ($user->{firehose_default_section} && $gSkin->{skid}== $constants->{mainpage_skid}) {
+               #       $section = $self->getFireHoseSection($user->{firehose_default_section});
+               #} else {
+
+               # No longer make use of $user->{firehose_default_section}
+               $section = $self->getFireHoseSectionBySkid($gSkin->{skid});
+
+               #}
+       }
+
+       # No longer use user section prefs
+       # $section = $self->applyUserSectionPrefs($section);
+       return $section;
+}
+
+sub applyUserViewPrefs {
+       my($self, $view) = @_;
+       my $constants = $self->getCurrentStatic();
+       my $user_prefs = $self->getViewUserPrefs($view->{id});
+       if ($user_prefs) {
+               foreach (qw(mode datafilter usermode admin_unsigned orderby orderdir)) {
+                       $view->{$_} = $user_prefs->{$_}
+               }
+       }
+       return $view;
+}
+
+sub applyUserSectionPrefs {
+       my($self, $section) = @_;
+       my $constants = $self->getCurrentStatic();
+       if ($section->{uid} == 0) {
+               my $user_prefs = $self->getSectionUserPrefs($section->{fsid});
+               if ($user_prefs) {
+                       foreach (qw(section_name section_filter view_id section_color display)) {
+                               next if $_ eq "section_name" && $section->{skid} == $constants->{mainpage_skid};
+                               $section->{$_} = $user_prefs->{$_};
+                       }
+               }
+       }
+       return $section;
+}
+
+sub applyViewOptions {
+       my($self, $view, $options, $second) = @_;
+       my $gSkin = getCurrentSkin();
+       my $form = getCurrentForm();
+       my $user = getCurrentUser();
+
+       $view = $self->applyUserViewPrefs($view);
+       $options->{view} = $view->{viewname};
+       $options->{viewref} = $view;
+
+       my $viewfilter = "$view->{filter}";
+       $viewfilter .= " $view->{datafilter}" if $view->{datafilter};
+       $viewfilter .= " unsigned" if $user->{is_admin} && $view->{admin_unsigned} eq "yes"
+               && $options->{fhfilter} !~ /\bsigned\b/; # allow fhfilter to override
+
+       if ($viewfilter =~ /{nickname}/) {
+               my $the_user = $self->getUser($form->{user_view_uid}) || $user;
+               $viewfilter =~ s/{nickname}/$the_user->{nickname}/;
+       }
+
+       my $validator = $self->getOptionsValidator();
+
+       if ($view->{useparentfilter} eq "no") {
+               if (!$second || ($form->{viewchanged} || $form->{sectionchanged})) {
+                       $options->{fhfilter} = $viewfilter;
+                       $options->{view_filter} = $viewfilter;
+                       $options->{base_filter} = $viewfilter;
+               }
+               if ($form->{viewchanged} || $form->{sectionchanged}) {
+                       $options->{resetfilter} = 1;
+               }
+               $options->{tab} = "";
+               $options->{tab_ref} = "";
+       } else {
+               $options->{fhfilter} = "$options->{base_filter}";
+               $options->{view_filter} = $viewfilter;
+       }
+
+       if ($view->{use_exclusions} eq "yes") {
+               if ($user->{story_never_author}) {
+                       my $author_exclusions;
+                       foreach (split /,/, $user->{story_never_author}) {
+                               my $nick = $self->getUser($_, 'nickname');
+                               $author_exclusions .= " \"-author:$nick\" " if $nick;
+                       }
+                       $viewfilter .= $author_exclusions if $author_exclusions;
+               }
+               if ($user->{firehose_exclusions}) {
+                       my $base_ops = $self->splitOpsFromString($options->{base_filter});
+                       my %base_ops = map { $_ => 1 } @$base_ops;
+                       my $ops = $self->splitOpsFromString($user->{firehose_exclusions});
+                       my @fh_exclusions;
+
+                       my $skins = $self->getSkins();
+                       my %skin_nexus = map { $skins->{$_}{name} => $skins->{$_}{nexus} } keys %$skins;
+
+                       foreach (@$ops) {
+                               my($not, $op) = $_ =~/^(-?)(.*)$/;
+                               next if $base_ops{$op};
+
+                               if ($validator->{type}{$_}) {
+                                       push @fh_exclusions, "-$op";
+                               } elsif ($skin_nexus{$_}) {
+                                       push @fh_exclusions, "-$op";
+                               } else {
+                                       push @fh_exclusions, "-$op";
+                               }
+                       }
+                       if (@fh_exclusions) {
+                               $viewfilter .= " ". (join ' ', @fh_exclusions)
+                       }
+               }
+               $options->{view_filter} = $viewfilter;
+       }
+
+       foreach (qw(mode color duration orderby orderdir datafilter)) {
+               next if $_ eq "color" && $options->{color} && $view->{useparentfilter} eq "yes";
+               $options->{$_} = $view->{$_} if $view->{$_} ne "";
+       }
+
+       if (!$second) {
+               foreach (qw(pause)) {
+                       $options->{$_} = $view->{$_} if $view->{$_} ne "";
+               }
+       }
+
+       $options->{usermode} = 1;
+
+       if ($user->{is_admin}) {
+               foreach (qw(usermode admin_unsigned)) {
+                       $options->{$_} = $view->{$_} eq "yes" ? 1 : 0;
+               }
+       }
+       return $options;
+}
+
+sub genUntitledTab {
+       my($self, $user_tabs, $options) = @_;
+       my $user = getCurrentUser();
+       $options ||= {};
+
+       my $tab_compare = {
+               filter          => "fhfilter"
+       };
+
+       my $tab_match = 0;
+       foreach my $tab (@$user_tabs) {
+
+               my $this_tab_compare;
+               %$this_tab_compare = %$tab_compare;
+
+               my $equal = 1;
+
+               foreach (keys %$this_tab_compare) {
+                       $options->{$this_tab_compare->{$_}} ||= "";
+                       if ($tab->{$_} ne $options->{$this_tab_compare->{$_}}) {
+                               $equal = 0;
+                       }
+               }
+
+               if (defined($options->{tab}) && $options->{tab} eq $tab->{tabname}) {
+                       $tab->{active} = 1;
+               }
+
+               if ($equal) {
+                       $tab_match = 1;
+               }
+       }
+
+       if (!$tab_match) {
+               my $data = {};
+               foreach (keys %$tab_compare) {
+                       $data->{$_} = $options->{$tab_compare->{$_}} || '';
+               }
+               if (!$user->{is_anon}) {
+                       $self->createOrReplaceUserTab($user->{uid}, "untitled", $data);
+               }
+               $user_tabs = $self->getUserTabs();
+               foreach (@$user_tabs) {
+                       $_->{active} = 1 if $_->{tabname} eq "untitled";
+               }
+       }
+       return $user_tabs;
+}
+
+
+#{
+#my $stopwords;
+#sub sphinxFilterQuery {
+#      my($query, $mode) = @_;
+#      # query size is limited, so strip out stopwords
+#      unless (defined $stopwords) {
+#              my $sphinxdb = getObject('Slash::Sphinx', { db_type => 'sphinx' });
+#              my @stopwords = $sphinxdb->getSphinxStopwords;
+#              if (@stopwords) {
+#                      $stopwords = join '|', @stopwords;
+#                      $stopwords = qr{\b(?:$stopwords)\b};
+#              } else {
+#                      $stopwords = 0;
+#              }
+#      }
+#
+#      $mode ||= 'all';
+#      $mode = 'extended2' if $mode eq 'extended';
+#
+#      my $basic = 'a-zA-Z0-9_ ';
+#
+#      # SSS what about hyphenated words?
+#      my $extra = ':\'';  # : and ' are sometimes useful in plain text, like for Perl modules and contractions
+#
+#      my $boolb = '()&|'; # base boolean syntax
+#      my $booln = '!\-';  # boolean negation
+#      my $bool  = $boolb . $booln; # full boolean syntax
+#
+#      my $extb  = '@\[\],*"~/=';   # base extended syntax
+#      my $ext   = $bool . $extb;   # full extended syntax
+#
+#      my $chars = $basic;  # . $extra;  # not until we figure out indexer behavior -- pudge
+# SSS: when we do syntax checking later, we may allow those characters
+# to pass through -- pudge
+#      $chars .= $boolb if $mode eq 'boolean';
+#      $chars .= $booln if $mode eq 'boolean';
+#      $chars .= $ext   if $mode eq 'extended2';
+#      $chars .= '\\x{100}-\\x{2FFFF}' if $self->getCurrentStatic('utf8');
+
+       # keep only these characters
+#      $query =~ s/[^$chars]+/ /g;
+
+       # clean up spaces
+#      $query =~ s/ +/ /g; $query =~ s/^ //g; $query =~ s/ $//g;
+
+#      $query =~ s/$stopwords//gio if $stopwords;
+
+       # we may want to adjust the mode before returning it
+#      return($query, $mode);
+#}
+#}
+
+# this serialization code can be heavily modified to taste ... it doesn't
+# really matter what it spits out, as long as we can rely on it being
+# consistent and unique for a given query
+{
+my @options = (
+       # meta search parameters
+       qw(
+               limit more_num orderby orderdir offset
+               startdate startdateraw duration admin_filters
+       ),
+       # other search parameters
+       # don't need usermode, since !usermode == no cache
+       qw(
+               filter color category not_id not_uid public not_public
+               accepted not_accepted rejected not_rejected type not_type
+               primaryskid not_primaryskid signed unsigned nexus not_nexus
+               spritegen tagged_by_uid tagged_as offmainpage smalldevices
+               createtime_no_future createtime_subscriber_future
+               tagged_non_negative tagged_for_homepage uid
+       )
+);
+
+# from the user; ideally, we would calculate startdate etc.
+# and not use off_set, but in practice not much difference
+my @prefs = qw(
+       off_set
+);
+
+# some things can be arrays; sort numerically unless listed here
+my %stringsort = (map { $_ => 1 } qw(
+       type not_type
+));
+
+sub serializeOptions {
+       my($self, $options, $prefs) = @_;
+
+       # copy the data so we can massage it into place
+       my $data = {};
+       for my $opt (@options) {
+               next unless defined $options->{$opt};
+               my $ref = ref $options->{$opt};
+
+               if ($ref eq 'ARRAY') {
+                       # normalize sort
+                       if ($stringsort{$opt}) {
+                               $data->{$opt} = [ sort @{ $options->{$opt} } ];
+                       } else {
+                               $data->{$opt} = [ sort { $a <=> $b } @{ $options->{$opt} } ];
+                       }
+               } elsif ($ref) {
+                       errorLog("$opt is a $ref, don't know what to do!");
+               } else {
+                       $data->{$opt} = $options->{$opt};
+               }
+       }
+
+       # do prefs come before or after options?
+       for my $pref (@prefs) {
+               $data->{$pref} = $prefs->{$pref} if $prefs && defined $prefs->{$pref};
+       }
+
+       local $Data::Dumper::Sortkeys = 1;
+       local $Data::Dumper::Indent   = 0;
+       local $Data::Dumper::Terse    = 1;
+       return Dumper($data);
+} }
+
+sub getAndSetOptions {
+       my($self, $opts) = @_;
+       slashProf("fh_gASO");
+
+       my $user        = getCurrentUser();
+       my $constants   = $self->getCurrentStatic();
+       my $form        = getCurrentForm();
+       my $gSkin       = getCurrentSkin();
+
+       my $nick_user = $user;
+       if ($opts->{user_view} && $opts->{user_view}{uid}) {
+               $form->{user_view_uid} = $opts->{user_view}{uid};
+               $nick_user = $self->getUser($form->{user_view_uid}) || $user;
+       }
+
+       my $mainpage = 0;
+
+       my($f_change, $v_change, $t_change, $s_change, $search_trigger);
+
+       if (!$opts->{initial}) {
+               ($f_change, $v_change, $t_change, $s_change, $search_trigger) = ($form->{filterchanged}, $form->{viewchanged}, $form->{tabchanged}, $form->{sectionchanged}, $form->{searchtriggered});
+       }
+
+       my $validator = $self->getOptionsValidator();
+
+       $opts           ||= {};
+
+       my $global_opts = $self->getAndSetGlobalOptions();
+       my $options = {};
+
+       # Beginning of initial pageload handling
+       if ($opts->{initial}) {
+               # Start off with global options if initial load
+               %$options = %$global_opts;
+
+               if (defined $opts->{fhfilter} || defined $form->{fhfilter}) {
+                       my $fhfilter = defined $opts->{fhfilter} ? $opts->{fhfilter} : $form->{fhfilter};
+
+                       $options->{fhfilter} = $fhfilter;
+                       $options->{base_filter} = $fhfilter;
+
+                       $form->{tab} = '';
+                       $opts->{view} = '';
+                       $form->{view} ||= 'search';
+
+               } else {
+
+                       my $section = $self->determineCurrentSection();
+
+                       if ($section && $section->{fsid}) {
+                               $options->{sectionref} = $section;
+                               $options->{section} = $section->{fsid};
+                               $options->{base_filter} = $section->{section_filter};
+                       }
+
+                       if (!$form->{view} && !$opts->{view}) {
+                               $options->{color} = $section->{section_color};
+                       }
+
+
+                       # Jump to default view as necessary
+
+                       if (!$opts->{view} && !$form->{view}) {
+                               my $view;
+                               if ($section) {
+                                       $view = $self->getUserViewById($section->{view_id});
+                               }
+
+                               if ($view && $view->{id}) {
+                                       $opts->{view} = $view->{viewname};
+                               } else {
+                                       $opts->{view} = "stories";
+                               }
+                       }
+               }
+
+               my $view;
+
+               if ($opts->{view} || $form->{view}) {
+                       my $viewname = $opts->{view} || $form->{view};
+                       $view = $self->getUserViewByName($viewname);
+               }
+               if ($view) {
+                       $options = $self->applyViewOptions($view, $options);
+               }
+
+
+
+
+
+       } else {
+               my $view_applied = 0;
+               # set only global options
+               $options->{$_} = $global_opts->{$_} foreach qw(nocommentcnt nobylines nodates nothumbs nomarquee nocolors noslashboxes mixedmode pagesize);
+
+               # handle non-initial pageload
+               $options->{fhfilter} = $form->{fhfilter} if defined $form->{fhfilter};
+               $options->{base_filter} = $form->{fhfilter} if defined $form->{fhfilter};
+
+               if (($f_change || $search_trigger) && defined $form->{fhfilter}) {
+                       my $fhfilter = $form->{fhfilter};
+
+                       $options->{fhfilter} = $fhfilter;
+                       $options->{base_filter} = $fhfilter;
+
+                       if ($search_trigger && !$f_change) {
+                               $form->{view} = 'search';
+                       }
+
+               }
+
+               if ($s_change && defined $form->{section}) {
+                       my $section = $self->determineCurrentSection();
+                       $options->{color} = $section->{section_color};
+                       if ($section && $section->{fsid}) {
+                               $options->{section} = $section->{fsid};
+                               $options->{sectionref} = $section;
+                               $options->{base_filter} = $section->{section_filter};
+
+                               my $view = $self->getUserViewById($section->{view_id});
+
+                               if ($view && $view->{id}) {
+                                       $opts->{view} = $view->{viewname};
+                               } else {
+                                       $opts->{view} = "stories";
+                               }
+
+                               $options->{viewref} = $self->getUserViewByName($opts->{view});
+
+                               $options = $self->applyViewOptions($options->{viewref}, $options, 1);
+                               $view_applied = 1;
+                       }
+               } elsif ($form->{view}) {
+                       my $view = $self->getUserViewByName($form->{view});
+                       if ($view) {
+                               $options->{view} = $form->{view};
+                               $options->{viewref} = $view;
+                       }
+               }
+
+               $options = $self->applyViewOptions($options->{viewref}, $options, 1) if !$view_applied && $options->{viewref};
+               $options->{tab} = $form->{tab} if $form->{tab} && !$t_change;
+       }
+
+       $options->{global} = $global_opts;
+       if ($opts->{initial} && $form->{addfilter}) {
+               my $addfilter = $form->{addfilter};
+               $addfilter =~ s/[^a-zA-Z0-9]//g;
+               $options->{base_filter} .= " $addfilter" if $addfilter;
+       }
+       $options->{base_filter} =~ s/{nickname}/$user->{nickname}/;
+       $options->{fhfilter} = $options->{base_filter};
+
+       $options->{view_filter} ||= '';
+
+       my $fhfilter = $options->{base_filter} . ($options->{view_filter} ? (" " . $options->{view_filter}) : '');
+
+       my $no_saved = $form->{no_saved};
+       $opts->{no_set} ||= $no_saved;
+       $opts->{initial} ||= 0;
+
+       if (defined $form->{nocommentcnt} && $form->{setfield}) {
+               $options->{nocommentcnt} = $form->{nocommentcnt} ? 1 : 0;
+       }
+
+       my $mode = $options->{mode};
+
+       if (!$s_change && !$v_change && !$search_trigger) {
+               $mode = $form->{mode} || $options->{mode} || '';
+       }
+
+       my $pagesize;
+       $pagesize = $options->{pagesize} = $validator->{pagesizes}{$options->{pagesize}} ? $options->{pagesize} : "small";
+
+       if (!$s_change && !$v_change && !$search_trigger) {
+               $options->{mode} = $s_change ? $options->{mode} : $mode;
+       }
+
+       $form->{pause} = 1 if $no_saved;
+
+       my $firehose_page = $user->{state}{firehose_page} || '';
+
+       if (!$v_change && !$s_change && !$search_trigger) {
+               if (defined $form->{duration}) {
+                       if ($form->{duration} =~ /^-?\d+$/) {
+                               $options->{duration} = $form->{duration};
+                       }
+               }
+               $options->{duration} = "-1" if !$options->{duration};
+
+               if (defined $form->{startdate}) {
+                       if ($form->{startdate} =~ /^\d{8}$/) {
+                               my($y, $m, $d) = $form->{startdate} =~ /(\d{4})(\d{2})(\d{2})/;
+                               if ($y) {
+                                       $options->{startdate} = "$y-$m-$d";
+                               }
+                       } else {
+                               $options->{startdate} = $form->{startdate};
+                       }
+               }
+               $options->{startdate} = "" if !$options->{startdate};
+               if ($form->{issue}) {
+                       if ($form->{issue} =~ /^\d{8}$/) {
+                               my($y, $m, $d) = $form->{issue} =~ /(\d{4})(\d{2})(\d{2})/;
+                               $options->{startdate} = "$y-$m-$d";
+                               $options->{issue} = $form->{issue};
+                               $options->{duration} = 1;
+
+                       } else {
+                               $form->{issue} = "";
+                       }
+               }
+       }
+
+
+       my $colors = $self->getFireHoseColors();
+       if ($form->{color} && $validator->{colors}->{$form->{color}} && !$s_change && !$v_change) {
+               $options->{color} = $form->{color};
+       }
+
+       if ($form->{orderby}) {
+               if ($form->{orderby} eq "popularity") {
+                       if ($user->{is_admin} && !$options->{usermode}) {
+                               $options->{orderby} = 'editorpop';
+                       } else {
+                               $options->{orderby} = 'popularity';
+                       }
+               } elsif ($form->{orderby} eq 'neediness') {
+                       $options->{orderby} = 'neediness';
+               } else {
+                       $options->{orderby} = "createtime";
+               }
+
+       } else {
+               $options->{orderby} ||= 'createtime';
+       }
+
+       if ($form->{orderdir}) {
+               if (uc($form->{orderdir}) eq "ASC") {
+                       $options->{orderdir} = "ASC";
+               } else {
+                       $options->{orderdir} = "DESC";
+               }
+
+       } else {
+               $options->{orderdir} ||= 'DESC';
+       }
+
+       if ($opts->{initial}) {
+               if (!defined $form->{section}) {
+                       #$form->{section} = $gSkin->{skid} == $constants->{mainpage_skid} ? 0 : $gSkin->{skid};
+               }
+       }
+
+       my $the_skin = defined $form->{section} ? $self->getSkin($form->{section}) : $gSkin;
+
+
+       #my $skin_prefix="";
+       #if ($the_skin && $the_skin->{name} && $the_skin->{skid} != $constants->{mainpage_skid})  {
+       #       $skin_prefix = "$the_skin->{name} ";
+       #}
+
+       #$user_tabs = $self->genUntitledTab($user_tabs, $options);
+
+
+       foreach (qw(nodates nobylines nothumbs nocolors noslashboxes nomarquee)) {
+               if ($form->{setfield}) {
+                       if (defined $form->{$_}) {
+                               $options->{$_} = $form->{$_} ? 1 : 0;
+                       }
+               }
+       }
+
+       $options->{smalldevices} = 1 if $self->shouldForceSmall();
+       $options->{limit} = $self->getFireHoseLimitSize($options->{mode}, $pagesize, $options->{smalldevices}, $options);
+
+       my $page = $options->{page} = $form->{page} || 0;
+       if ($page) {
+               $options->{offset} = $page * $options->{limit};
+       }
+
+
+       $fhfilter =~ s/^\s+|\s+$//g;
+
+       $options->{user_view_uid} = $opts->{user_view}{uid} || $form->{user_view_uid};
+       if ($fhfilter =~ /\{nickname\}/) {
+               if (!$opts->{user_view}) {
+                       if ($form->{user_view_uid}) {
+                               $opts->{user_view} = $self->getUser($form->{user_view_uid}) || $user;
+                       } else {
+                               $opts->{user_view} = $user;
+                       }
+               }
+               my $the_nickname = $opts->{user_view}{nickname};
+
+               $fhfilter =~ s/\{nickname\}/$the_nickname/g;
+               $options->{fhfilter} =~ s/\{nickname\}/$the_nickname/g;
+               $options->{base_filter} =~ s/\{nickname\}/$the_nickname/g;
+       }
+
+       if ($fhfilter =~ /\{tag\}/) {
+               my $the_tag = $opts->{tag} || $form->{tagname};
+               $fhfilter =~ s/\{tag\}/$the_tag/g;
+               $options->{fhfilter} =~ s/\{tag\}/$the_tag/g;
+               $options->{base_filter} =~ s/\{tag\}/$the_tag/g;
+       }
+
+       my $fh_ops = $self->splitOpsFromString($fhfilter);
+
+       my $skins = $self->getSkins();
+       my %skin_nexus = map { $skins->{$_}{name} => $skins->{$_}{nexus} } keys %$skins;
+
+       my $fh_options = {};
+
+
+
+       foreach (@$fh_ops) {
+               my $not = "";
+               if (/^-/) {
+                       $not = "not_";
+                       $_ =~ s/^-//g;
+               }
+               if ($validator->{type}->{$_}) {
+                       push @{$fh_options->{$not."type"}}, $_;
+               } elsif ($user->{is_admin} && $validator->{categories}{$_} && !defined $fh_options->{category}) {
+                       $fh_options->{category} = $_;
+               } elsif ($skin_nexus{$_}) {
+                       push @{$fh_options->{$not."nexus"}}, $skin_nexus{$_};
+               } elsif ($user->{is_admin} && $_ eq "rejected") {
+                       $fh_options->{rejected} = "yes";
+               } elsif ($_ eq "accepted") {
+                       $fh_options->{accepted} = "yes";
+               } elsif ($user->{is_admin} && $_ eq "signed") {
+                       $fh_options->{signed} = 1;
+               } elsif ($user->{is_admin} && $_ eq "unsigned") {
+                       $fh_options->{unsigned} = 1;
+               } elsif (/^author:(.*)$/) {
+                       my $uid;
+                       my $nick = $1;
+                       if ($nick) {
+                               $uid = $self->getUserUID($nick);
+                               $uid ||= $user->{uid};
+                       }
+                       push @{$fh_options->{$not."uid"}}, $uid;
+               } elsif (/^authorfriend:(.*)$/ && $constants->{plugin}{Zoo}) {
+                       my $uid;
+                       my $nick = $1;
+                       if ($nick) {
+                               $uid = $self->getUserUID($nick);
+                               $uid ||= $user->{uid};
+                       }
+                       my $zoo = getObject("Slash::Zoo");
+                       my $friends = $zoo->getFriendsUIDs($uid);
+                       $friends = [-1], if @$friends < 1;   # No friends, pass a UID that won't match
+                       push @{$fh_options->{$not."uid"}}, @$friends;
+               } elsif (/^(user|home|hose):/) {
+                       my $type = $1;
+                       (my $nick = $_) =~ s/^$type://;
+                       my $uid;
+                       $uid = $self->getUserUID($nick) if $nick;
+                       $uid ||= $user->{uid};
+                       $fh_options->{tagged_by_uid} = [$uid];
+                       if ($type eq 'user') {
+                               $fh_options->{tagged_non_negative} = 1;
+#                              $fh_options->{ignore_nix} = 1;
+                       } elsif ($type eq 'home') {
+                               $fh_options->{tagged_for_homepage} = 1;
+                       } elsif ($type eq 'hose') {
+                               $fh_options->{tagged_non_negative} = 1;
+                               my $zoo = getObject("Slash::Zoo");
+                               my $friends = $zoo->getFriendsUIDs($uid);
+                               push @{$fh_options->{tagged_by_uid}}, @$friends;
+                       }
+               } elsif (/^tag:/) {
+                       my $tag = $_;
+                       $tag =~s/tag://g;
+                       $fh_options->{tagged_as} = $tag;
+               } elsif($_ eq 'fhbinspam') {
+                       $fh_options->{'is_spam'} = 'yes';
+               } elsif($_ eq 'fhbayes') {
+                       $fh_options->{'bayes_spam'} = 'yes';
+               } elsif($_ eq 'fhcollateral') {
+                       $fh_options->{'collateral_spam'} = 'yes';
+               } elsif (/^itemtype:(.*)$/) {
+                       if ($validator->{type}->{$1}) {
+                               push @{$fh_options->{$not."type"}}, $1;
+                       }
+               } elsif ($not) {
+                       $fh_options->{filter} .= '-' . $_ . ' ';
+                       $fh_options->{qfilter} .= '-' . $_ . ' ';
+               } else {
+                       $fh_options->{filter} .= $_ . ' ';
+                       $fh_options->{qfilter} .= $_ . ' ';
+               }
+       }
+
+       # push all necessary nexuses on if we want stories show as brief
+       if ($constants->{brief_sectional_mainpage} && $options->{viewref} && $options->{viewref}{viewname} eq 'stories') {
+               if (!$fh_options->{nexus}) {
+                       my $nexus_children = $self->getMainpageDisplayableNexuses();
+                       push @{$fh_options->{nexus}}, @$nexus_children, $constants->{mainpage_nexus_tid};
+
+                       $fh_options->{offmainpage} = "no";
+                       $fh_options->{stories_mainpage} = 1;
+               } else {
+                       $fh_options->{stories_sectional} = 1;
+               }
+       }
+       # Pull out any excluded nexuses we're explicitly asking for
+
+       if ($fh_options->{nexus} && $fh_options->{not_nexus}) {
+               my %not_nexus = map { $_ => 1 } @{$fh_options->{not_nexus}};
+               @{$fh_options->{nexus}} = grep { !$not_nexus{$_} } @{$fh_options->{nexus}};
+               delete $fh_options->{nexus} if @{$fh_options->{nexus}} == 0;
+       }
+
+       my $color = (defined $form->{color} && !$s_change && !$v_change) && $validator->{colors}->{$form->{color}} ? $form->{color} : "";
+       $color = defined $options->{color} && $validator->{colors}->{$options->{color}} ? $options->{color} : "" if !$color;
+
+       $fh_options->{color} = $color;
+
+
+       foreach (keys %$fh_options) {
+               $options->{$_} = $fh_options->{$_};
+       }
+
+       my $adminmode = 0;
+       $adminmode = 1 if $user->{is_admin};
+       if ($no_saved) {
+               $adminmode = 0;
+       } elsif (defined $options->{usermode}) {
+               $adminmode = 0 if $options->{usermode};
+       }
+
+       $options->{public} = "yes";
+
+       if (!$options->{usermode} && $user->{is_admin}) {
+               $options->{admin_filters} = 1;
+       }
+
+       if ($adminmode) {
+               # $options->{attention_needed} = "yes";
+               if ($options->{admin_filters}) {
+                       $options->{accepted} = "no" if !$options->{accepted};
+                       $options->{rejected} = "no" if !$options->{rejected};
+               }
+               $options->{duration} ||= -1;
+       } else  {
+               if ($firehose_page ne "user") {
+                       # $options->{accepted} = "no" if !$options->{accepted};
+               }
+
+               if (!$user->{is_anon} && (!$no_saved || $form->{index})) {
+                       $options->{createtime_subscriber_future} = 1;
+               } else {
+                       $options->{createtime_no_future} = 1;
+               }
+       }
+
+       if ($options->{issue}) {
+               $options->{duration} = 1;
+       }
+
+       $options->{not_id} = $opts->{not_id} if $opts->{not_id};
+       if ($form->{not_id} && $form->{not_id} =~ /^\d+$/) {
+               $options->{not_id} = $form->{not_id};
+       }
+
+
+       if ($v_change) {
+               $options->{section} = $form->{section};
+               if ($form->{section}) {
+                       $options->{sectionref} = $self->getFireHoseSection($form->{section});
+               }
+               $self->applyViewOptions($options->{viewref}, $options, 1);
+       }
+
+       if ($form->{index}) {
+               $options->{index} = 1;
+               $options->{skipmenu} = 1;
+
+               if ($options->{stories_mainpage}) {
+                       if (!$form->{issue}) {
+                               $options->{duration} = "-1";
+                               $options->{mode} = "mixed";
+                       }
+               }
+
+               if ($options->{stories_sectional}) {
+                       $options->{duration} = "-1";
+                       $options->{mode} = 'full';
+               }
+       }
+
+       if ($form->{more_num} && $form->{more_num} =~ /^\d+$/) {
+               $options->{more_num} = $form->{more_num};
+               if (!$user->{is_admin} && (($options->{limit} + $options->{more_num}) > 200)) {
+                       $options->{more_num} = 200 - $options->{limit} ;
+               }
+               if (!$user->{is_anon} && $options->{more_num} > $user->{firehose_max_more_num}) {
+                       $self->setUser($user->{uid}, { firehose_max_more_num => $options->{more_num}});
+               }
+       }
+
+       if ($options->{viewref} && $options->{viewref}{viewtitle}) {
+               if ($options->{viewref}{viewtitle} =~ /{nickname}/) {
+                       my $nick_user = $options->{user_view_uid} || $user->{uid};
+                       my $nick = $self->getUser($nick_user, 'nickname');
+                       $options->{viewref}{viewtitle} =~ s/\{nickname\}/$nick/;
+               }
+               $options->{viewtitle} = $options->{viewref}{viewtitle};
+       }
+       if ($options->{sectionref} && $options->{sectionref}{section_name}) {
+               $options->{sectionname} = $options->{sectionref}{section_name};
+       }
+
+       $options->{fhfilter} =~ s/{nickname}/$nick_user->{nickname}/g;
+       $options->{base_filter} =~ s/{nickname}/$nick_user->{nickname}/g;
+       $options->{view_filter} =~ s/{nickname}/$nick_user->{nickname}/g;
+
+#use Data::Dumper;
+#print STDERR Dumper($options);
+#print STDERR "TEST: BASE_FILTER $options->{base_filter}   FHFILTER: $options->{fhfilter} VIEW $options->{view} VFILTER: $options->{view_filter} TYPE: " . Dumper($options->{type}). "\n";
+#print STDERR "FHFILTER: $options->{fhfilter} NEXUS: " . Dumper($options->{nexus}) . "\n";
+#print STDERR "VIEW: $options->{view} MODE: $mode USERMODE: |$options->{usermode}  UNSIGNED: $options->{unsigned} PAUSE $options->{pause} FPAUSE: |$form->{pause}|\n";
+#print STDERR "DURATION $options->{duration} STARTDATE: $options->{startdate}\n";
+#print STDERR "VIEW: $options->{view} COLOR: $options->{color}\n";
+       slashProf("","fh_gASO");
+       return $options;
+}
+
+sub getFireHoseLimitSize {
+       my($self, $mode, $pagesize, $forcesmall, $options) = @_;
+       $pagesize ||= '';
+
+       my $user = getCurrentUser();
+       my $constants = $self->getCurrentStatic();
+       my $form = getCurrentForm();
+       my $limit;
+
+       my $pagesizes = {
+               single  => [1,1,1],
+               tiny    => [5,5,5],
+               small   => [10,10,20],
+               large   => [15,20,30],
+               huge    => [50,50,50],
+       };
+
+       if ($options->{view} && $options->{view} eq 'stories') {
+               $pagesizes->{small} = [15,15,20];
+       }
+
+       $pagesize ||= "small";
+       if ($forcesmall) {
+               $pagesize = "tiny";
+       }
+
+       my $mode_map = { full => 0, mixed => 1, fulltitle => 2 };
+
+       my $mode_id = $mode_map->{$mode} || 0;
+
+       $limit = $pagesizes->{$pagesize}[$mode_id];
+
+       $limit ||= 10;
+
+       $limit = 15 if defined $options->{view} && $options->{view} =~ /^user/ && $limit >= 15;
+       $limit = 10 if $form->{metamod};
+
+       return $limit;
+}
+
+sub shouldForceSmall {
+       my($self) = @_;
+       my $form = getCurrentForm();
+       my $user = getCurrentUser();
+       my $constants = $self->getCurrentStatic();
+
+       # the non-normal cases: a small device (e.g., iPhone) or an embedded use (e.g., Google Gadget)
+       my $force_smaller = $form->{embed} ? 1 : 0;
+       $force_smaller = 1 if $user->{state}{smalldevice};
+       return $force_smaller;
+}
+
+sub getInitTabtypeOptions {
+       my($self, $name) = @_;
+       my $gSkin = getCurrentSkin();
+       my $form = getCurrentForm();
+       my $constants = $self->getCurrentStatic();
+       my $vol = $self->getSkinVolume($gSkin->{skid});
+       my $day_specified = $form->{startdate} || $form->{issue};
+       my $set_option;
+
+       $vol ||= { story_vol => 0, other_vol => 0};
+
+       if ($name eq "tabsection") {
+               if ($gSkin->{skid} == $constants->{mainpage_skid}) {
+                       $set_option->{mixedmode} = "1";
+               }
+               $set_option->{mode} = "full";
+               if (!$day_specified) {
+                       if ($vol->{story_vol} > 25) {
+                               $set_option->{duration} = 7;
+                       } else {
+                               $set_option->{duration} = -1;
+                       }
+                       $set_option->{startdate} = "";
+               }
+       } elsif (($name eq "tabpopular" || $name eq "tabrecent") && !$day_specified) {
+               if ($vol->{story_vol} > 25) {
+                       $set_option->{duration} = 7;
+               } else {
+                       $set_option->{duration} = -1;
+               }
+               $set_option->{startdate} = "";
+               $set_option->{mixedmode} = "1";
+       } elsif ($name eq "metamod") {
+               $set_option->{duration} = 7;
+               $set_option->{startdate} = '';
+       }
+       return $set_option;
+}
+sub getFireHoseSystemTags {
+       my($self, $item) = @_;
+       my $constants = $self->getCurrentStatic();
+       my @system_tags;
+       push @system_tags, $item->{type};
+       if ($item->{primaryskid}) {
+               if ($item->{primaryskid} == $constants->{mainpage_skid}) {
+                       push @system_tags, "mainpage";
+               } else {
+                       my $the_skin = $self->getSkin($item->{primaryskid});
+                       push @system_tags, "$the_skin->{name}";
+               }
+       }
+       if ($item->{tid}) {
+               my $the_topic = $self->getTopic($item->{tid});
+               push @system_tags, "$the_topic->{keyword}";
+       }
+       return join ' ', @system_tags;
+}
+sub getFireHoseTagsTop {
+       my($self, $item) = @_;
+       my $user        = getCurrentUser();
+       my $constants   = $self->getCurrentStatic();
+       my $form = getCurrentForm();
+       my $tags_top     = [];
+
+       if ($user->{is_admin}) {
+               if ($item->{type} eq "story") {
+                       # 5 = add completer_handleNeverDisplay
+                       push @$tags_top, "$item->{type}:5";
+               } else {
+                       push @$tags_top, "$item->{type}:6";
+               }
+       } else {
+               push @$tags_top, $item->{type};
+       }
+
+       if ($item->{primaryskid}) {
+               if ($item->{primaryskid} == $constants->{mainpage_skid}) {
+                       push @$tags_top, "mainpage:2";
+               } else {
+                       my $the_skin = $self->getSkin($item->{primaryskid});
+                       push @$tags_top, "$the_skin->{name}:2";
+               }
+       }
+       if ($item->{tid}) {
+               my $the_topic = $self->getTopic($item->{tid});
+               push @$tags_top, "$the_topic->{keyword}:3";
+       }
+       my %seen_tags = map { $_ => 1 } @$tags_top;
+
+       # 0 = is a link, not a menu
+       my $user_tags_top = [];
+       push @$user_tags_top, map { "$_:0" } grep {!$seen_tags{$_}} split (/\s+/, $item->{toptags});
+
+       if ($constants->{smalldevices_ua_regex}) {
+               my $smalldev_re = qr($constants->{smalldevices_ua_regex});
+               if ($ENV{HTTP_USER_AGENT} =~ $smalldev_re) {
+                       $#$user_tags_top = 2;
+               }
+       }
+
+       if ($form->{embed}) {
+               $#$user_tags_top = 2;
+       }
+
+       push @$tags_top, @$user_tags_top;
+
+       return $tags_top;
+}
+
+sub getMinPopularityForColorLevel {
+       my($self, $level) = @_;
+       my $constants = $self->getCurrentStatic();
+       my $slicepoints = $constants->{firehose_slice_points};
+       my @levels = split / /, $slicepoints;
+       my $entry_min = $levels[$level-1];
+       my($entry, $min) = split /,/, $entry_min;
+       return $min;
+}
+
+sub getEntryPopularityForColorLevel {
+       my($self, $level) = @_;
+       my $constants = $self->getCurrentStatic();
+       my $slicepoints = $constants->{firehose_slice_points};
+       my @levels = split / /, $slicepoints;
+       my $entry_min = $levels[$level-1];
+       my($entry, $min) = split /,/, $entry_min;
+       return $entry;
+}
+
+sub getPopLevelForPopularity {
+       my($self, $pop) = @_;
+       my $constants = $self->getCurrentStatic();
+       my $slicepoints = $constants->{firehose_slice_points};
+       my @levels = split / /, $slicepoints;
+       for my $i (0..$#levels) {
+               my $entry_min = $levels[$i];
+               my($entry, $min) = split /,/, $entry_min;
+               return $i+1 if $pop >= $min;
+       }
+       # This should not happen, since the min value for the last slice
+       # is supposed to be very large negative.  If a score goes below
+       # it, though, return the next slice number.
+       return $#levels + 1;
+}
+
+sub listView {
+       my($self, $lv_opts) = @_;
+       my $start = Time::HiRes::time();
+
+       slashProfInit();
+       slashProf("fh_listview");
+       slashProf("fh_listview_init");
+
+       $lv_opts ||= {};
+       $lv_opts->{initial} = 1;
+
+       my $slashdb     = getCurrentDB();
+       my $user        = getCurrentUser();
+       my $gSkin       = getCurrentSkin();
+       my $form        = getCurrentForm();
+       my $constants   = $self->getCurrentStatic();
+
+       my $firehose_reader = getObject('Slash::FireHose', {db_type => 'reader'});
+       my $featured;
+
+       $lv_opts->{fh_page} ||= "firehose.pl";
+       my $base_page = $lv_opts->{fh_page};
+       my $options = $self->getAndSetOptions($lv_opts);
+
+       if ($featured && $featured->{id}) {
+               $options->{not_id} = $featured->{id};
+       }
+       slashProf("get_fhe", "fh_listview_init");
+       my($items, $results, $count, $future_count, $day_num, $day_label, $day_count) = $firehose_reader->getFireHoseEssentials($options);
+       slashProf("fh_listview_misc","get_fhe");
+
+       my $itemnum = scalar @$items;
+
+       my $globjs;
+
+       foreach (@$items) {
+               push @$globjs, $_->{globjid} if $_->{globjid}
+       }
+
+       if ($options->{orderby} eq "createtime" && !$options->{spritegen}) {
+               $items = $self->addDayBreaks($items, $user->{off_set});
+       }
+
+       my $votes = $self->getUserFireHoseVotesForGlobjs($user->{uid}, $globjs);
+
+       my $itemstext;
+       my $maxtime = $slashdb->getTime();
+       my $now = $slashdb->getTime();
+       my $colors = $self->getFireHoseColors(1);
+       my $colors_hash = $self->getFireHoseColors();
+
+       my $i=0;
+       my $last_day = 0;
+
+       my $mode = $options->{mode};
+       my $curmode = $options->{mode};
+       my $mixed_abbrev_pop = $self->getMinPopularityForColorLevel(1);
+       if ($gSkin->{skid} != $constants->{mainpage_skid}) {
+               $mixed_abbrev_pop = $self->getMinPopularityForColorLevel(3);
+       }
+
+       my @fh_items = map $_->{id}, grep !$_->{day}, @$items;
+       my $fh_items = $firehose_reader->getFireHoseMulti(\@fh_items);
+       my $tag_reader = getObject("Slash::Tags", { db_type => 'reader' });
+
+       slashProf("fh_listview_loop", "fh_listview_misc");
+       foreach (@$items) {
+               if ($options->{mode} eq "mixed") {
+                       $curmode = "full";
+                       $curmode = "fulltitle" if $_->{popularity} < $mixed_abbrev_pop;
+
+               }
+               $maxtime = $_->{createtime} if $_->{createtime} gt $maxtime && $_->{createtime} lt $now;
+               if ($_->{day}) {
+                       my $day = $_->{day};
+                       $day =~ s/ \d{2}:\d{2}:\d{2}$//;
+                       $itemstext .= slashDisplay("daybreak", { options => $options, cur_day => $day, last_day => $_->{last_day}, id => "firehose-day-$day", fh_page => $base_page }, { Return => 1, Page => "firehose" });
+               } else {
+                       my $item = $fh_items->{$_->{id}};
+                       my $tags_top = $firehose_reader->getFireHoseTagsTop($item);
+                       my $tags = $tag_reader->setGetCombinedTags($_->{id}, 'firehose-id');
+                       $last_day = timeCalc($item->{createtime}, "%Y%m%d");
+                       slashProf("firehosedisp");
+                       $itemstext .= $self->dispFireHose($item, {
+                               mode                    => $curmode,
+                               tags_top                => $tags_top,           # old-style
+                               top_tags                => $tags->{top},        # new-style
+                               system_tags             => $tags->{'system'},   # new-style
+                               datatype_tags           => $tags->{'datatype'}, # new-style
+                               options                 => $options,
+                               vote                    => $votes->{$item->{globjid}},
+                               bodycontent_include     => $user->{is_anon}
+                       });
+                       slashProf("","firehosedisp");
+               }
+               $i++;
+       }
+       slashProf("fh_listview_misc2", "fh_listview_loop");
+
+       my $Slashboxes = "";
+       if ($user->{state}{firehose_page} eq "console") {
+               my $console = getObject("Slash::Console");
+               $Slashboxes = $console->consoleBoxes();;
+       } else {
+               $Slashboxes = displaySlashboxes($gSkin);
+       }
+               my $refresh_options;
+       $refresh_options->{maxtime} = $maxtime;
+       if (uc($options->{orderdir}) eq "ASC") {
+               $refresh_options->{insert_new_at} = "bottom";
+       } else {
+               $refresh_options->{insert_new_at} = "top";
+       }
+
+       my $section = 0;
+       if ($gSkin->{skid} != $constants->{mainpage_skid}) {
+               $section = $gSkin->{skid};
+       }
+
+       my $firehose_more_data = {
+               future_count => $future_count,
+               options => $options,
+               day_num => $day_num,
+               day_label => $day_label,
+               day_count => $day_count
+       };
+
+       my $views = $self->getUserViews({ tab_display => "yes"});
+
+       my $sprite_rules = $self->js_anon_dump($self->getSpriteInfoByFHID($items->[0]->{id}));
+
+       slashProf("fh_listview_misc3", "fh_listview_misc2");
+       my $ret = slashDisplay("list", {
+               itemstext               => $itemstext,
+               itemnum                 => $itemnum,
+               page                    => $options->{page},
+               options                 => $options,
+               refresh_options         => $refresh_options,
+               votes                   => $votes,
+               colors                  => $colors,
+               colors_hash             => $colors_hash,
+               tabs                    => $options->{tabs},
+               slashboxes              => $Slashboxes,
+               last_day                => $last_day,
+               fh_page                 => $base_page,
+               search_results          => $results,
+               featured                => $featured,
+               section                 => $section,
+               firehose_more_data      => $firehose_more_data,
+               views                   => $views,
+               theupdatetime           => timeCalc($slashdb->getTime(), "%H:%M"),
+               sprite_rules            => $sprite_rules,
+       }, { Page => "firehose", Return => 1 });
+
+       my $duration = Time::HiRes::time() - $start;
+       my $updatelog = {
+               uid             => $user->{uid},
+               ipid            => $user->{ipid},
+               initial         => 'yes',
+               new_count       => $count,
+               update_count    => 0,
+               total_num       => $count,
+               more_num        => 0,
+               view            => $options->{view} || '',
+               "-ts"           => "NOW()",
+               duration        => $duration,
+               bytes           => length($ret)
+       };
+
+       $self->createUpdateLog($updatelog);
+
+       slashProf("", "fh_listview_misc3");
+       slashProf("", "fh_listview");
+       slashProfEnd("FH_LISTVIEW");
+       return $ret;
+
+}
+
+sub setFireHoseSession {
+       my($self, $id, $action) = @_;
+       my $user = getCurrentUser();
+       my $item = $self->getFireHose($id);
+
+       $action ||= "reviewing";
+
+       my $data = {};
+       $data->{lasttitle} = $item->{title};
+       if ($item->{type} eq "story") {
+               my $story = $self->getStory($item->{srcid});
+               $data->{last_sid} = $story->{sid} if $story && $story->{sid};
+       }
+
+       if (!$data->{last_sid}) {
+               $data->{last_fhid} = $item->{id};
+       }
+       $data->{last_subid} ||= '';
+       $data->{last_sid} ||= '';
+       $data->{last_action} = $action;
+       $self->setSession($user->{uid}, $data);
+}
+
+sub getUserTabs {
+       my($self, $options) = @_;
+       $options ||= {};
+       my $user = getCurrentUser();
+       my $uid_q = $self->sqlQuote($user->{uid});
+       my @where = ( );
+       push @where, "uid=$uid_q";
+       push @where, "tabname LIKE '$options->{prefix}%'" if $options->{prefix};
+       my $where = join ' AND ', @where;
+
+       my $tabs = $self->sqlSelectAllHashrefArray("*", "firehose_tab", $where, "ORDER BY tabname ASC");
+       @$tabs = sort {
+                       $b->{tabname} eq "untitled" ? -1 :
+                               $a->{tabname} eq "untitled" ? 1 : 0     ||
+                       $b->{tabname} eq "User" ? -1 :
+                               $a->{tabname} eq "User" ? 1 : 0 ||
+                       $a->{tabname} cmp $b->{tabname}
+       } @$tabs;
+       return $tabs;
+}
+
+sub getUserTabByName {
+       my($self, $name, $options) = @_;
+       $options ||= {};
+       my $user = getCurrentUser();
+       my $uid_q = $self->sqlQuote($user->{uid});
+       my $tabname_q = $self->sqlQuote($name);
+       return $self->sqlSelectHashref("*", "firehose_tab", "uid=$uid_q && tabname=$tabname_q");
+}
+
+sub getSystemDefaultTabs {
+       my($self) = @_;
+       return $self->sqlSelectAllHashrefArray("*", "firehose_tab", "uid='0'")
+}
+
+sub createUserTab {
+       my($self, $uid, $data) = @_;
+       $data->{uid} = $uid;
+       $self->sqlInsert("firehose_tab", $data);
+}
+
+sub createOrReplaceUserTab {
+       my($self, $uid, $name, $data) = @_;
+       return if !$uid;
+       $data ||= {};
+       $data->{uid} = $uid;
+       $data->{tabname} = $name;
+       $self->sqlReplace("firehose_tab", $data);
+}
+
+sub ajaxFirehoseListTabs {
+       my($slashdb, $constants, $user, $form) = @_;
+       my $firehose = getObject("Slash::FireHose");
+       my $tabs = $firehose->getUserTabs({ prefix => $form->{prefix}});
+       @$tabs = map { $_->{tabname}} grep { $_->{tabname} ne "untitled" } @$tabs;
+       return join "\n", @$tabs, "untit";
+}
+
+sub splitOpsFromString {
+       my($self, $str) = @_;
+       my @fh_ops_orig = map { lc($_) } split(/(\s+|")/, $str);
+       my @fh_ops;
+
+       my $in_quotes = 0;
+       my $cur_op = "";
+       foreach (@fh_ops_orig) {
+               if (!$in_quotes && $_ eq '"') {
+                       $in_quotes = 1;
+               } elsif ($in_quotes) {
+                       if ($_ eq '"') {
+                               push @fh_ops, $cur_op;
+                               $cur_op = "";
+                               $in_quotes = 0;
+                       } else {
+                               $cur_op .= $_;
+                       }
+               } elsif (/\S+/) {
+                       push @fh_ops, $_;
+               }
+       }
+       return \@fh_ops;
+}
+
+{
+my %last_levels;
+sub addDayBreaks {
+       my($self, $items, $offset, $options) = @_;
+       my $retitems = [];
+       my $breaks = 0;
+
+       my $level = $options->{level} || 0;
+       my $count = @$items;
+       my $break_ratio = 5;
+       my $max_breaks = ceil($count / $break_ratio);
+
+       my($db_levels, $db_order) = getDayBreakLevels();
+       my $fmt = $db_levels->{ $db_order->[$level] }{fmt};
+
+       my $last_level = $last_levels{$level} ||= timeCalc('1970-01-01 00:00:00', $fmt, 0);
+
+       foreach (@$items) {
+               my $cur_level = timeCalc($_->{createtime}, $fmt, $offset);
+               if ($cur_level ne $last_level) {
+                       if ($last_level ne $last_levels{$level}) {
+                               push @$retitems, { id => "day-$cur_level", day => $cur_level, last_day => $last_level };
+                               $breaks++;
+                       }
+               }
+
+               push @$retitems, $_;
+               $last_level = $cur_level;
+       }
+
+       if ($level < $#{$db_order}) {
+               my $newitems = addDayBreaks($self, $items, $offset,
+                       { level => $level+1 }
+               );
+#printf STDERR "daybreak levels: %s, breaks: %s, count: %s, maxbreaks: %s, existing: %s, new: %s\n",
+#      $db_order->[$level], $breaks, $count, $max_breaks,
+#      scalar(@$newitems), scalar(@$retitems);
+
+               $retitems = $newitems if (
+                       $breaks > $max_breaks
+                               ||
+                       @$newitems >= @$retitems
+               );
+
+       }
+
+       return $retitems;
+}}
+
+# deprecated, i think -- pudge 2009-02-17
+sub getOlderMonthsFromDay {
+       my($self, $day, $start, $end) = @_;
+       $day =~ s/-//g;
+       $day ||= $self->getDay(0);
+       my $cur_day = $self->getDay(0);
+
+       my($y, $m, $d) = $day =~/(\d{4})(\d{2})(\d{2})/;
+       my($cy, $cm, $cd) = $cur_day =~/(\d{4})(\d{2})(\d{2})/;
+
+       $d = "01";
+
+       my $days = [];
+
+       for ($start..$end) {
+               my($ny, $nm, $nd) = Add_Delta_YMD($y, $m, $d, 0, $_, 0);
+               $nm = "0$nm" if $nm < 10;
+               $nd = "0$nd" if $nd < 10;
+               my $the_day = "$ny$nm$nd";
+               if ($the_day le $cur_day || $_ == 0) {
+                       my $label = "";
+                       if ($ny == $cy) {
+                               $label = timeCalc($the_day, "%B", 0);
+                       } else {
+                               $label = timeCalc($the_day, "%B %Y", 0);
+                       }
+                       my $num_days = Days_in_Month($ny, $nm);
+                       my $active = $_ == 0;
+                       push @$days, [ $the_day, $label, $num_days, $active ];
+               }
+       }
+       return $days;
+}
+
+sub getFireHoseItemsByUrl {
+       my($self, $url_id) = @_;
+       my $url_id_q = $self->sqlQuote($url_id);
+       return $self->sqlSelectAllHashrefArray("*", "firehose, firehose_text", "firehose.id=firehose_text.id AND url_id = $url_id_q AND preview='no'");
+}
+
+sub ajaxFireHoseUsage {
+       my($slashdb, $constants, $user, $form) = @_;
+
+       my $tags_reader = getObject('Slash::Tags', { db_type => 'reader' });
+
+       my $downlabel = $constants->{tags_downvote_tagname} || 'nix';
+       my $down_id = $tags_reader->getTagnameidFromNameIfExists($downlabel);
+
+       my $uplabel = $constants->{tags_upvote_tagname} || 'nod';
+       my $up_id = $tags_reader->getTagnameidFromNameIfExists($uplabel);
+       my $data = {};
+
+#      $data->{fh_users} = $tags_reader->sqlSelect("COUNT(DISTINCT uid)", "tags",
+#              "tagnameid IN ($up_id, $down_id)");
+       my $d_clause = " AND created_at > DATE_SUB(NOW(), INTERVAL 1 DAY)";
+       my $h_clause = " AND created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)";
+       $data->{fh_users_day} = $tags_reader->sqlSelect("COUNT(DISTINCT uid)", "tags",
+               "tagnameid IN ($up_id, $down_id) $d_clause");
+       $data->{fh_users_hour} = $tags_reader->sqlSelect("COUNT(DISTINCT uid)", "tags",
+               "tagnameid IN ($up_id, $down_id) $h_clause");
+       $data->{tag_cnt_day} = $tags_reader->sqlSelect("COUNT(*)", "tags,users,firehose",
+               "firehose.globjid=tags.globjid AND tags.uid=users.uid AND users.seclev = 1 $d_clause");
+       $data->{tag_cnt_hour} = $tags_reader->sqlSelect("COUNT(*)", "tags,users,firehose",
+               "firehose.globjid=tags.globjid AND tags.uid=users.uid AND users.seclev = 1 $h_clause");
+       $data->{nod_cnt_day} = $tags_reader->sqlSelect("COUNT(*)", "tags,users",
+               "tags.uid=users.uid AND users.seclev = 1 AND tagnameid IN ($up_id) $d_clause");
+       $data->{nod_cnt_hour} = $tags_reader->sqlSelect("COUNT(*)", "tags,users",
+               "tags.uid=users.uid AND users.seclev = 1 AND tagnameid IN ($up_id) $h_clause");
+       $data->{nix_cnt_day} = $tags_reader->sqlSelect("COUNT(*)", "tags,users",
+               "tags.uid=users.uid AND users.seclev = 1 AND tagnameid IN ($down_id) $d_clause");
+       $data->{nix_cnt_hour} = $tags_reader->sqlSelect("COUNT(*)", "tags,users",
+               "tags.uid=users.uid AND users.seclev = 1 AND tagnameid IN ($down_id) $h_clause");
+       $data->{globjid_cnt_day} = $tags_reader->sqlSelect("COUNT(DISTINCT globjid)", "tags,users",
+               "tags.uid=users.uid AND users.seclev = 1 AND tagnameid IN ($up_id, $down_id) $d_clause");
+       $data->{globjid_cnt_hour} = $tags_reader->sqlSelect("COUNT(DISTINCT globjid)", "tags,users",
+               "tags.uid=users.uid AND users.seclev = 1 AND tagnameid IN ($up_id, $down_id) $h_clause");
+
+       slashDisplay("firehose_usage", $data, { Return => 1 });
+}
+
+sub getNextItemsForThumbnails {
+       my($self, $lastid, $limit) = @_;
+       $limit = " LIMIT $limit" if $limit;
+       $lastid = " AND firehose.id > $lastid" if defined $lastid;
+       return $self->sqlSelectAllHashrefArray("firehose.id,urls.url", "firehose,urls", "firehose.type='submission' AND firehose.url_id=urls.url_id AND mediatype='video' $lastid", "ORDER BY firehose.id ASC $limit");
+}
+
+sub createSectionSelect {
+       my($self, $default) = @_;
+       my $skins       = $self->getSkins();
+       my $constants   = $self->getCurrentStatic();
+       my $user =      getCurrentUser();
+       my $ordered     = [];
+       my $menu;
+
+       foreach my $skid (keys %$skins) {
+               if ($skins->{$skid}{skid} == $constants->{mainpage_skid}) {
+                       $menu->{0} = $constants->{mainpage_name} || $constants->{sitename};
+               } else {
+                       $menu->{$skid} = $skins->{$skid}{title};
+               }
+       }
+       my $onchange = $user->{is_anon}
+               ? "firehose_change_section_anon(this.options[this.selectedIndex].value)"
+               : "firehose_set_options('tabsection', this.options[this.selectedIndex].value)";
+
+       @$ordered = sort {$a == 0 ? -1 : $b == 0 ? 1 : 0 || $menu->{$a} cmp $menu->{$b} } keys %$menu;
+       return createSelect("section", $menu, { default => $default, return => 1, nsort => 0, ordered => $ordered, multiple => 0, onchange => $onchange });
+
+}
+
+sub linkFireHose {
+       my($self, $id_or_item, $options) = (@_);
+       my $gSkin       = getCurrentSkin();
+       my $constants   = $self->getCurrentStatic();
+       my $link_url;
+       $options ||= {};
+       my $item = ref($id_or_item) ? $id_or_item : $self->getFireHose($id_or_item);
+
+       my $linktitle = urlizeTitle($item->{title});
+       if ($options->{short_url}) {
+               $linktitle = '';
+       }
+
+       if ($item->{type} eq "story") {
+               my $story = $self->getStory($item->{srcid});
+               unless ($constants->{firehose_link_article2}) {
+                       my $story_link_ar = linkStory({
+                               sid     => $story->{sid},
+                               link    => $story->{title},
+                               tid     => $story->{tid},
+                               skin    => $story->{primaryskid} || $constants->{mainpage_skid},
+                       }, 0);
+                       $link_url = $story_link_ar->[0];
+               } else {
+                       my $story_skin = $self->getSkin($story->{primaryskid});
+                       $link_url = "$story_skin->{rootdir}/story/$story->{sid}/$linktitle";
+               }
+       } elsif ($item->{type} eq "journal") {
+               my $the_user = $self->getUser($item->{uid});
+               my $rootdir = $constants->{real_rootdir};
+#              if ($the_user->{shill_id}) {
+                       my $shill = $self->getShillInfo($the_user->{shill_id});
+                       if ($shill->{skid}) {
+                               my $shill_skin = $self->getSkin($shill->{skid});
+                               $rootdir = $shill_skin->{rootdir};
+                       }
+                       $link_url = $rootdir . "/~" . fixparam($the_user->{nickname}) . "/journal/$item->{srcid}/$linktitle";
+#              } else {
+#                      $link_url = $rootdir . "/~" . fixparam($the_user->{nickname}) . "/journal/$item->{srcid}";
+#              }
+
+       } elsif ($item->{type} eq "comment") {
+               my $com = $self->getComment($item->{srcid});
+#              $link_url = $gSkin->{real_rootdir} . "/comments.pl?sid=$com->{sid}&amp;cid=$com->{cid}";
+               $link_url = $gSkin->{real_rootdir} . "/comment/$com->{cid}";
+       } elsif ($item->{type} eq "submission") {
+               $link_url = $gSkin->{real_rootdir} . "/submission/$item->{srcid}/$linktitle";
+       } else {
+               $link_url = $gSkin->{real_rootdir} . '/firehose.pl?op=view&amp;id=' . $item->{id};
+       }
+
+}
+
+sub js_anon_dump {
+       my($self, $var) = @_;
+       return Data::JavaScript::Anon->anon_dump(strip_literal($var));
+}
+
+sub genFireHoseParams {
+       my($self, $options, $data) = @_;
+       my $user = getCurrentUser();
+       my $form = getCurrentForm();
+
+       $data ||= {};
+       my @params;
+
+       my $params = {
+               fhfilter        => 0,
+               issue           => 1,
+               startdate       => 1,
+               duration        => 1,
+               index           => 1,
+               view            => 1,
+               color           => 1
+       };
+       if ($user->{is_anon}) {
+               my($label, $value) = @_;
+               if ($options->{sel_tabtype} || $form->{tabtype}) {
+                       $label = "tabtype";
+                       $value = $form->{tabtype} || $options->{sel_tabtype};
+                       $value = strip_paramattr($value);
+                       push @params, "$label=$value";
+               }
+               $value = strip_paramattr($form->{section});
+               $label = "section";
+               push @params, "$label=$value";
+       }
+
+       my $skip_false = { startdate => 1, issue => 1 };
+
+       foreach my $label (keys %$params) {
+               next if $skip_false->{$label} && !$data->{$label} && !$options->{$label};
+               next if $user->{is_anon} && $params->{$label} == 0;
+               next if !defined $data->{$label} && !defined $options->{$label};
+               my $value = defined $data->{$label} ? $data->{$label} : $options->{$label};
+               if ($label eq "startdate") {
+                       $value =~s /-//g;
+               }
+               push @params, "$label=" . strip_paramattr($value);
+
+       }
+
+       my $str =  join('&amp;', @params);
+       return $str;
+}
+
+sub createUpdateLog {
+       my($self, $data) = @_;
+       return if !$self->getCurrentStatic("firehose_logging");
+       $data->{uid} ||= getCurrentUser('uid');
+       $self->sqlInsert("firehose_update_log", $data);
+}
+
+sub createSettingLog {
+       my($self, $data) = @_;
+       return if !$self->getCurrentStatic("firehose_logging");
+       return if !$data->{name};
+
+       $data->{value} ||= "";
+       $data->{uid} ||= getCurrentUser('uid');
+       $self->sqlInsert("firehose_setting_log", $data);
+}
+
+sub getSkinVolume {
+       my($self, $skid) = @_;
+       my $skid_q = $self->sqlQuote($skid);
+       return $self->sqlSelectHashref("*", "firehose_skin_volume", "skid=$skid_q");
+}
+
+sub genFireHoseWeeklyVolume {
+       my($self, $options) = @_;
+       $options ||= {};
+       my $colors = $self->getFireHoseColors();
+       my @where;
+
+       if ($options->{type}) {
+               push @where, "type=" . $self->sqlQuote($options->{type});
+       }
+       if ($options->{not_type}) {
+               push @where, "type!=" . $self->sqlQuote($options->{not_type});
+       }
+       if ($options->{color}) {
+               my $pop;
+               $pop = $self->getMinPopularityForColorLevel($colors->{$options->{color}});
+               push @where, "popularity >= " . $self->sqlQuote($pop);
+       }
+       if ($options->{primaryskid}) {
+               push @where, "primaryskid=" . $self->sqlQuote($options->{primaryskid});
+       }
+       push @where, "createtime >= DATE_SUB(NOW(), INTERVAL 7 DAY)";
+       my $where = join ' AND ', @where;
+       return $self->sqlCount("firehose", $where);
+}
+
+sub setSkinVolume {
+       my($self, $data) = @_;
+       $self->sqlReplace("firehose_skin_volume", $data);
+}
+
+sub getProjectsChangedSince {
+       my($self, $ts, $options) = @_;
+       my $ts_q = $self->sqlQuote($ts);
+       if ($ts =~ /^\d+$/) {
+               #convert from unixtime if necessary
+               $ts = $self->sqlSelect("from_unixtime($ts)");
+       }
+       $ts_q = $self->sqlQuote($ts);
+       my $max_num = defined($options->{max_num}) ? $options->{max_num} : 10;
+
+       my $hr_ar = $self->sqlSelectAllHashrefArray(
+               'firehose.id AS firehose_id, firehose.globjid, firehose.toptags, discussions.commentcount, GREATEST(firehose.last_update, discussions.last_update) AS last_update, unixname AS name',
+               'firehose, discussions, projects',
+               "firehose.type='project'
+                AND firehose.discussion = discussions.id
+                AND projects.id = firehose.srcid
+                AND (firehose.last_update >= $ts_q OR discussions.last_update >= $ts_q)",
+               "ORDER BY GREATEST(firehose.last_update, discussions.last_update) ASC
+                LIMIT $max_num");
+       $self->addGlobjEssentialsToHashrefArray($hr_ar);
+
+       return $hr_ar;
+}
+
+sub createSprite {
+       my($self, $fhid, $options) = @_;
+
+       my $constants = $self->getCurrentStatic();
+       my $convert = $constants->{imagemagick_convert};
+       my $convert_ops = ' -background none -mosaic -bordercolor none -border 0x0 -quality 100 -depth 8';
+       my $convert_image_ops = '';
+       my $border = 50;
+       my $x_offset = 0;
+       my $y_offset = 0;
+       my $output_file = $fhid . '.png';
+       my $image_ar = $self->getSpriteInfo($fhid);
+
+       # Build the param list to convert.
+       foreach my $image (@$image_ar) {
+               $convert_image_ops .= " -page +0+$y_offset " . $image->{file};
+               $image->{x_coord} = $x_offset;
+               $image->{y_coord} = $y_offset;
+               $image->{y_coord} = '-' . $image->{y_coord} if ($image->{y_coord});
+               #($image->{raw_filename}) = $image->{file} =~ m{^.+/(.+)\.\w{3}$};
+               $y_offset += ($border + $image->{height});
+       }
+
+       # Create the dest path.
+       my $bd = $constants->{basedir};
+       my ($numdir) = sprintf("%09d", $fhid);
+       my ($i, $j) = $numdir =~ /(\d\d\d)(\d\d\d)\d\d\d/;
+       my $path = catdir($bd, "images", "firehose", $i, $j);
+       mkpath($path, 0, 0775);
+
+       # Increment the version number of the sprite.
+       my $version = 1;
+       if (-s "$path/$output_file") {
+               my $sprite_info = $self->sqlSelect('sprite_info', 'firehose', "id = $fhid");
+               if ($sprite_info) {
+                       ($version) = $sprite_info =~ m{$output_file\?(\d)};
+                       ++$version;
+               }
+       }
+
+       # Format CSS
+       my $css =
+               slashDisplay('format_sprite_info', {
+                       fhid        => $fhid,
+                       path        => "firehose/$i/$j",
+                       sprite_name => $output_file,
+                       version     => $version,
+                       images      => $image_ar,
+               }, { Page => 'firehose', Return => 1 });
+
+       # Convert and UPDATE
+       my $cmd = $convert . $convert_image_ops . $convert_ops . " $path/$output_file";
+       system($cmd);
+
+       $self->sqlUpdate('firehose', { sprite => "$i/$j/$output_file", sprite_info => $css }, "id = $fhid");
+}
+
+sub getSpriteInfo {
+       my($self, $id) = @_;
+       my $constants = $self->getCurrentStatic();
+       my $item = $self->getFireHose($id);
+       my $opts = { initial => '1'};
+       my @images;
+       my $basepath = "$constants->{basedir}/images";
+       if ($item) {
+               if ($item->{type} eq "story") {
+                       $opts->{view} = "stories";
+               } else {
+                       $opts->{view} = "recent";
+               }
+               my $options = $self->getAndSetOptions($opts);
+               $options->{spritegen} =  1;
+               $options->{startdateraw} = $item->{createtime};
+               
+               # Don't filter out items in the future
+               $options->{createtime_no_future} = 0;
+               $options->{createtime_subscriber_future} = 0;
+
+               my ($items, $info) = $self->getFireHoseEssentials($options);
+
+                my $i = 0;
+                my $seen = {};
+                my $key;
+                foreach my $it (@$items) {
+                        if ($i == 0 && $it->{type} eq "story" && !$it->{thumb}) {
+                                my $topiclist = $self->getTopiclistForStory($item->{srcid});
+                                foreach (@$topiclist) {
+                                        $key = "tid-$_";
+                                        my $topic = $self->getTopic($_);
+                                        if ($topic->{image}) {
+                                                push @images, { label => $key, width => $topic->{width}, height => $topic->{height}, file => "$basepath/topics/$topic->{image}"} if !$seen->{$key};
+                                                $seen->{$key}++;
+                                        }
+                                }
+                        }
+                        if ($it->{thumb}) {
+                                $key = "thumb-$it->{thumb}";
+                                my $file = $self->getStaticFile($it->{thumb});
+                                push @images, { label => $key, width => $file->{width}, height => $file->{height},  file => "$basepath$file->{name}"} if !$seen->{$key};
+                                $seen->{$key}++;
+                        } else {
+                                $key = "tid-$it->{tid}";
+                                my $topic = $self->getTopic($it->{tid});
+                                if ($topic->{image}) {
+                                        push @images, { label => $key, width => $topic->{width}, height => $topic->{height}, file => "$basepath/topics/$topic->{image}"} if !$seen->{$key};
+                                        $seen->{$key}++;
+                                }
+
+                        }
+                        $i++;
+                }
+        }
+        return \@images;
+
+}
+
+sub getSpriteInfoByFHID {
+       my ($self, $fhid, $options) = @_;
+
+       my $constants = $self->getCurrentStatic();
+       my $sprite = {};
+       return {} if !$fhid;
+
+       my $fhid_q = $self->sqlQuote($fhid);
+       my $sprite_info = $self->sqlSelect('sprite_info', 'firehose', "id = $fhid_q");
+
+       $sprite_info =~ s/__IMAGEDIR__/$constants->{imagedir}/g;
+       foreach my $rule ($sprite_info =~ /(\..+?\{.+?\})/g) {
+               my ($topic) = $rule =~ /^\.(.+?)\s?\{.+$/;
+               $sprite->{$topic} = $rule;
+       }
+
+       return  $sprite;
+}
+
+sub getFirehoseScoreLogGraphUrl {
+       my($self, $id, $options) = @_;
+       $options ||= {};
+
+       my $width = $options->{width} || 350;
+       my $height = $options->{height} || 150;
+
+       my $id_q = $self->sqlQuote($id);
+       my $items = $self->sqlSelectAllHashrefArray("*","firehose_score_log", "id=$id_q", "order by ts asc");
+
+       my (@ed,@pop);
+
+       foreach(@$items) {
+               push @ed, int(($_->{editorpop}+50)/5);
+               push @pop, int(($_->{popularity}+50)/5);
+       }
+
+       my $pop = join ',', @pop;
+       my $ed = join ',', @ed;
+       return "http://chart.apis.google.com/chart?chs=$width" . "x$height&chd=t:$pop|$ed&cht=lc&chco=0000ff,FF0000,00ff00&chxt=y&chxr=0,-50,250" if @$items;
+}
+
+sub getMostCommentedItemsByTypeDays {
+       my($self, $type, $days, $limit) = @_;
+
+       $type ||= 'story';
+       $days ||= 1;
+       $limit ||= 3;
+
+       my $type_q = $self->sqlQuote($type);
+
+       my $ids =  $self->sqlSelectColArrayref(
+               "firehose.id",
+               "firehose, discussions",
+               "firehose.discussion=discussions.id AND createtime >= date_sub(now(), interval $days day) AND firehose.type=$type_q AND preview='no'",
+               "order by commentcount desc limit $limit"
+       );
+       my $items;
+       foreach (@$ids) {
+               push @$items, $self->getFireHose($_);
+       }
+       return $items;
+
+}
+
+sub twitterLinkify {
+       my($self, $text) = @_;
+
+       $text =~s|(https*://[^ ]*)|<a href=\"$1\"\>__LINK__\</a>|g;
+       my $link = $1;
+       if(length($1) > 40) {
+               $link = substr($link,0,40) . "...";
+       }
+       $text =~ s/__LINK__/$link/g;
+
+       return $text;
+}
+
+=head1 getOlderArchiveDate
+
+  returns date strings for link to archive page
+
+arguments:
+
+  day: base date, YYYYMMDD format. if 0, use today
+  count: number of items to return
+  options: options
+
+  options->{type}: (story|journal|comment)
+  options->{section}: section name
+
+SELECT TIMESTAMPDIFF(DAY, '2015-12-14 15:00:00', createtime) AS day
+  FROM firehose
+  WHERE createtime < '2015-12-14 15:00:00'
+    AND type = 'story'
+    AND primaryskid = 8
+  GROUP BY TIMESTAMPDIFF(DAY, '2015-12-14 15:00:00', createtime)
+  ORDER BY day DESC
+  LIMIT 3
+;
+
+=cut
+
+sub getOlderArchiveDate {
+    my ($self, $day, $count,  $opts) = @_;
+    my $slashdb = getCurrentDB();
+    my $constants = $self->getCurrentStatic();
+
+    my $db = getCurrentDB();
+    $day ||= $slashdb->getDay(0, $opts);
+    $opts ||= {};
+    my $days = [];
+    my $tzOffset = $opts->{offset} || $constants->{archive2_tz_offset} || -9;
+
+    if ($count < 1) {
+       return [];
+    }
+
+    my $mcd = $self->getMCD();
+    my $mcdExpire = $constants->{memcached_exptime_archive2} || 3600;
+    my $mcdkey = "";
+    my $retVal = {};
+    if ($mcd) {
+       $mcdkey = sprintf("$self->{_mcd_keyprefix}:Index2:footerDate:%s%+03d:%d:%d",
+                         $day, $tzOffset, $count, $opts->{skid});
+       $retVal = $mcd->get($mcdkey);
+       if ($retVal) {
+           return getFormatFromDays($retVal, $opts);
+       }
+    }
+
+
+    # $day is YYYYMMDD
+    my $dt = DateTime->new(
+       year => substr($day, 0, 4),
+       month => substr($day, 4, 2),
+       day => substr($day, 6, 2)
+       );
+    $dt->add( hours => $tzOffset);
+    my $startDate = $dt->strftime('%Y-%m-%d %H:%M:%S');
+
+    my $type = $opts->{story} || "story";
+
+    my $gSkin = getCurrentSkin();
+    my $skid_clause = "";
+
+    #warn "skid: $gSkin->{skid}";
+    if ($gSkin->{skid} != $constants->{mainpage_skid}) {
+       $skid_clause = "AND primaryskid = $gSkin->{skid}";
+    }
+    my $select = "TIMESTAMPDIFF(DAY, '$startDate', createtime) AS day";
+    my $from = "firehose";
+    my $where = qq{createtime < '$startDate'
+    AND type = '$type'
+    $skid_clause 
+};
+    my $other = qq{
+  GROUP BY TIMESTAMPDIFF(DAY, '$startDate', createtime)
+  ORDER BY day DESC
+  LIMIT $count
+  };  
+
+    my $a = $self->sqlSelectAllHashrefArray($select, $from, $where, $other);
+    foreach my $item (@$a) {
+       my $newDt = $dt->clone;
+       $newDt->add(days => $item->{day});
+       my $strDay = sprintf('%04d%02d%02d', $newDt->year, $newDt->month, $newDt->day);
+       push @$days, $strDay;
+    }    
+
+    if ($mcd) {
+       $mcd->set("$mcdkey", $days, $mcdExpire);
+    }
+    return getFormatFromDays($days, $opts);
+}
+
+
+1;
+
+__END__
+
+
+=head1 SEE ALSO
+
+Slash(3).
diff --git a/src/newslash_web/lib/Newslash/Model/SlashConstants.pm b/src/newslash_web/lib/Newslash/Model/SlashConstants.pm
new file mode 100755 (executable)
index 0000000..37a0672
--- /dev/null
@@ -0,0 +1,216 @@
+# This code is a part of Slash, and is released under the GPL.
+# Copyright 1997-2009 by Geeknet, Inc. See README
+# and COPYING for more information, or see http://slashcode.com/.
+
+package Newslash::Model::SlashConstants;
+
+=head1 NAME
+
+Slash::Constants - Constants for Slash
+
+=head1 SYNOPSIS
+
+       use Slash::Constants ':all';
+
+=head1 DESCRIPTION
+
+This module is for a single place to have all of our constants.
+Each constant is in one or more export tags.  All of the constants
+can be gotten via the "all" tag.  None are exported by default.
+
+=head1 CONSTANTS
+
+The constants below are grouped by tag.
+
+=cut
+
+use strict;
+use base 'Exporter';
+
+our $VERSION = '2.005001'; # v2.5.1
+our %CONSTANTS;
+
+constants();
+our @EXPORT            = qw();
+our @EXPORT_OK = map { keys %{$CONSTANTS{$_}} } keys %CONSTANTS;
+our %EXPORT_TAGS       = (
+       all     => [@EXPORT_OK],
+       map { ($_, [keys %{$CONSTANTS{$_}}]) } keys %CONSTANTS
+);
+
+
+sub constants {
+       my($group, @syms, @nums);
+
+       for (readline(DATA)) {
+               if (/^=head2 (\w+)$/ || /^__END__$/) {
+                       if ($group && @syms && @nums) {
+                               @{$CONSTANTS{$group}}{@syms} = @nums;
+                       }
+
+                       $group = $1;
+                       @syms = @nums = ();
+                       last if /^__END__$/;
+
+               } elsif (/^\t(\w+)$/) {
+                       push @syms, $1;
+
+               } elsif (/^# ([\d -]+)$/) {
+                       push @nums, split ' ', $1;
+               }
+       }
+
+       for my $g (keys %CONSTANTS) {
+               for my $s (keys %{$CONSTANTS{$g}}) {
+                       eval "use constant $s => $CONSTANTS{$g}{$s}";
+               }
+       }
+}
+
+1;
+
+# we dynamically assign the constants as formatted below.  the grouping
+# tag is after "=head2", the symbols are after "\t", and the numeric
+# values are listed after "#".  the format is very strict, including
+# leading and trailing whitespace, so add things carefully, and check
+# that you've got it right.  see the regexes in constants() if you
+# are not sure what's being matched.  -- pudge
+
+__DATA__
+
+=head2 messages
+
+These constants are for message delivery modes and message type codes.
+
+       MSG_MODE_NOCODE
+       MSG_MODE_NONE
+       MSG_MODE_EMAIL
+       MSG_MODE_WEB
+
+=cut
+
+# -2 -1 0 1
+
+=pod
+
+       MSG_CODE_REGISTRATION
+       MSG_CODE_UNKNOWN
+       MSG_CODE_NEWSLETTER
+       MSG_CODE_HEADLINES
+       MSG_CODE_M2
+       MSG_CODE_COMMENT_MODERATE
+       MSG_CODE_COMMENT_REPLY
+       MSG_CODE_JOURNAL_FRIEND
+       MSG_CODE_NEW_SUBMISSION
+       MSG_CODE_JOURNAL_REPLY
+       MSG_CODE_NEW_COMMENT
+       MSG_CODE_INTERUSER
+       MSG_CODE_ADMINMAIL
+       MSG_CODE_EMAILSTORY
+       MSG_CODE_ZOO_CHANGE
+       MSG_CODE_BADPASSWORD
+       MSG_CODE_MODSTATS
+       MSG_CODE_SUBSCRIPTION_LOW
+       MSG_CODE_SUBSCRIPTION_OUT
+       MSG_CODE_SCHEDULECHG
+       MSG_CODE_HTML_INVALID
+
+=cut
+
+# -2 -1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
+
+=pod
+
+       MSG_IUM_ANYONE
+       MSG_IUM_FRIENDS
+       MSG_IUM_NOFOES
+
+=cut
+
+# 1 2 3
+       
+=head2 web
+
+These constants are used for web programs, for the op hashes.
+
+       ALLOWED
+       FUNCTION
+       MINSECLEV
+
+=cut
+
+# 0 1 2
+
+=head2 reskey
+
+These constants are used for resource keys.
+
+       RESKEY_NOOP
+       RESKEY_SUCCESS
+       RESKEY_FAILURE
+       RESKEY_DEATH
+
+=cut
+
+# -1 0 1 2
+
+=head2 strip
+
+These constants are used to define the modes passed to stripByMode().  Only
+user-definable constants (for journals, comments) should be E<gt>= 1.  All
+else should be E<lt> 1.  If adding new user-definable modes, make sure to
+change Slash::Data::strip_mode() to allow the new value.
+
+       ANCHOR
+       NOTAGS
+       ATTRIBUTE
+       LITERAL
+       NOHTML
+       PLAINTEXT
+       HTML
+       EXTRANS
+       CODE
+       FULLHTML
+
+=cut
+
+# -4 -3 -2 -1 0 1 2 3 4 77
+
+=head2 people
+
+These constants are used to define different constants in the people system.
+
+       FRIEND
+       FREAK
+       FAN
+       FOE
+       FOF
+       EOF
+
+=cut
+
+# 1 2 3 4 5 6
+
+=head2 slashd
+
+These constants are used to define different constants in the people system.
+
+       SLASHD_LOG_NEXT_TASK
+       SLASHD_WAIT
+       SLASHD_NOWAIT
+
+=cut
+
+# -1 2 1
+
+__END__
+
+=head1 TODO
+
+Consider allowing some constants, like MSG_CODE_* constants,
+be defined dynamically.  Scary, though, with cross-dependencies
+in modules, etc.
+
+=head1 SEE ALSO
+
+Slash(3).
diff --git a/src/newslash_web/lib/Newslash/Model/SlashUtility.pm b/src/newslash_web/lib/Newslash/Model/SlashUtility.pm
new file mode 100644 (file)
index 0000000..4d45a88
--- /dev/null
@@ -0,0 +1,752 @@
+package Newslash::Model::SlashUtility;
+use Newslash::Model::Base -strict;
+use base 'Exporter';
+
+use Newslash::Model::SlashConstants qw(:strip);
+use HTML::Entities qw(:DEFAULT %char2entity %entity2char);
+
+## WORNING:
+## This is worst legacy from slash. deprecated, and you can use only backword-compatible.
+
+our @EXPORT  = qw(
+                     createSid
+                     countWords
+                );
+
+########################################################
+# If you change createSid() for your site, change regexSid() too.
+sub createSid {
+       my($bogus_sid) = @_;
+       # yes, this format is correct, don't change it :-)
+       my $sidformat = '%02d/%02d/%02d/%02d%0d2%02d';
+       # Create a sid based on the current time.
+       my @lt;
+       my $start_time = time;
+       if ($bogus_sid) {
+               # If we were called being told that there's at
+               # least one sid that is invalid (already taken),
+               # then look backwards in time until we find it,
+               # then go one second further.
+               my $loops = 1000;
+               while (--$loops) {
+                       $start_time--;
+                       @lt = localtime($start_time);
+                       $lt[5] %= 100; $lt[4]++; # year and month
+                       last if $bogus_sid eq sprintf($sidformat, @lt[reverse 0..5]);
+               }
+               if ($loops) {
+                       # Found the bogus sid by looking
+                       # backwards.  Go one second further.
+                       $start_time--;
+               } else {
+                       # Something's wrong.  Skip ahead in
+                       # time instead of back (not sure what
+                       # else to do).
+                       $start_time = time + 1;
+               }
+       }
+       @lt = localtime($start_time);
+       $lt[5] %= 100; $lt[4]++; # year and month
+       return sprintf($sidformat, @lt[reverse 0..5]);
+}
+
+
+#========================================================================
+
+=head2 fudgeurl(DATA)
+
+Prepares data to be a URL.  Such as:
+
+=over 4
+
+       my $url = fudgeurl($someurl);
+
+=item Parameters
+
+=over 4
+
+=item DATA
+
+The data to be escaped.
+
+=back
+
+=item Return value
+
+The escaped data.
+
+=back
+
+=cut
+
+sub fudgeurl {
+       my($url) = @_;
+
+       ### should we just escape spaces, quotes, apostrophes, and <> instead
+       ### of removing them? -- pudge
+
+       # Remove quotes and whitespace (we will expect some at beginning and end,
+       # probably)
+       $url =~ s/["\s]//g;
+       # any < or > char after the first char truncates the URL right there
+       # (we will expect a trailing ">" probably)
+       $url =~ s/^[<>]+//;
+       $url =~ s/[<>].*//;
+       # strip surrounding ' if exists
+       $url =~ s/^'(.+?)'$/$1/g;
+       # escape anything not allowed
+       $url = fixurl($url);
+       # run it through the grungy URL miscellaneous-"fixer"
+       $url = fixHref($url) || $url;
+
+       my $scheme_regex = _get_scheme_regex();
+
+       my $uri = new URI $url;
+       my $scheme = undef;
+       $scheme = $uri->scheme if $uri && $uri->can("scheme");
+
+       # modify scheme:/ to scheme:// for $schemes defined below
+       # need to recreate $uri after doing so to make userinfo
+       # clearing work for something like http:/foo.com...@bar.com
+       my $schemes_to_mod = { http => 1, https => 1, ftp => 1 };
+       if ($scheme && $schemes_to_mod->{$scheme}) {
+               $url = $uri->canonical->as_string;
+               $url =~ s|^$scheme:/([^/])|$scheme://$1|;
+               $uri = new URI $url;
+       }
+
+       if ($uri && !$scheme && $uri->can("authority") && $uri->authority) {
+               # The URI has an authority but no scheme, e.g. "//sitename.com/".
+               # URI.pm doesn't always handle this well.  E.g. host() returns
+               # undef.  So give it a scheme.
+               # XXX Rethink this -- it could probably be put lower down, in
+               # the "if" that handles stripping the userinfo.  We don't
+               # really need to add the scheme for most URLs. - Jamie
+
+               # and we should only add scheme if not a local site URL
+               my($from_site) = urlFromSite($uri->as_string);
+               $uri->scheme('http') unless $from_site;
+       }
+
+       if (!$uri) {
+
+               # Nothing we can do with it; manipulate the probably-bogus
+               # $url at the end of this function and return it.
+
+       } elsif ($scheme && $scheme !~ /^$scheme_regex$/) {
+
+               $url =~ s/^$scheme://i;
+               $url =~ tr/A-Za-z0-9-//cd; # allow only a few chars, for security
+               $url = "$scheme:$url";
+
+       } elsif ($uri) {
+
+               # Strip the authority, if any.
+               # This prevents annoying browser-display-exploits
+               # like "http://cnn.com%20%20%20...%20@baddomain.com".
+               # In future we may set up a package global or a field like
+               # getCurrentUser()->{state}{fixurlauth} that will allow
+               # this behavior to be turned off.
+
+               if ($uri->can('userinfo') && $uri->userinfo) {
+                       $uri->userinfo(undef);
+               }
+               if ($uri->can('host') && $uri->host) {
+                       # If this scheme has an authority (which means a
+                       # username and/or password and/or host and/or port)
+                       # then make sure the host and port are legit, and
+                       # zap the port if it's the default port.
+                       my $host = $uri->host;
+                       # Re the below line, see RFC 1035 and maybe 2396.
+                       # Underscore is not recommended and Slash has
+                       # disallowed it for some time, but allowing it
+                       # is really the right thing to do.
+                       $host =~ tr/A-Za-z0-9._-//cd;
+                       $uri->host($host);
+                       if ($uri->can('authority') && $uri->authority) {
+                               # We don't allow anything in the authority except
+                               # the host and optionally a port.  This shouldn't
+                               # matter since the userinfo portion was zapped
+                               # above.  But this is a bit of double security to
+                               # ensure nothing nasty in the authority.
+                               my $authority = $uri->host;
+                               if ($uri->can('host_port')
+                                       && $uri->port != $uri->default_port) {
+                                       $authority = $uri->host_port;
+                               }
+                               $uri->authority($authority);
+                       }
+               }
+
+               if ($scheme && $scheme eq 'mailto') {
+                       if (my $query = $uri->query) {
+                               $query =~ s/@/%40/g;
+                               $uri->query($query);
+                       }
+               }
+
+               $url = $uri->canonical->as_string;
+
+               if ($url =~ /#/) {
+                       my $token = ':::INSERT__23__HERE:::';
+                       # no # is OK, unless ...
+                       $url =~ s/#/$token/g;
+                       if ($url =~ m|^https?://|i || $url =~ m|^/| || $url =~ m|^$token|) {
+                               # HTTP, in which case the first # is OK
+                               $url =~ s/$token/#/;
+                       }
+                       $url =~ s/$token/%23/g;
+               }
+       }
+
+       # These entities can crash browsers and don't belong in URLs.
+       $url =~ s/&#(.+?);//g;
+       # we don't like SCRIPT at the beginning of a URL
+       my $decoded_url = decode_entities($url);
+       $decoded_url =~ s{ &(\#?[a-zA-Z0-9]+);? } { approveCharref($1) }gex;
+       return $decoded_url =~ /^[\s\w]*script\b/i ? undef : $url;
+}
+
+########################################################
+# Count words in a given scalar will strip HTML tags
+# before counts are made.
+sub countWords {
+       my($body) = @_;
+
+       # Sanity check.
+       $body = ${$body} if ref $body eq 'SCALAR';
+       return 0 if ref $body;
+
+       # Get rid of nasty print artifacts that may screw up counts.
+       $body = strip_nohtml($body);
+       $body =~ s/['`"~@#$%^()|\\\/!?.]//g;
+       $body =~ s/&(?:\w+|#(\d+));//g;
+       $body =~ s/[;\-,+=*&]/ /g;
+       $body =~ s/\s\s+/ /g;
+
+       # count words in $body.
+       my(@words) = ($body =~ /\b/g);
+
+       # Since we count boundaries, each word has two boundaries, so
+       # we divide by two to get our count. This results in values
+       # *close* to the return from a 'wc -w' on $body (within 1)
+       # so I think this is close enough. ;)
+       # - Cliff
+       return scalar @words / 2;
+}
+
+
+
+#========================================================================
+
+=head2 stripByMode(STRING [, MODE, NO_WHITESPACE_FIX])
+
+Private function.  Fixes up a string based on what the mode is.  This
+function is no longer exported, use the C<strip_*> functions instead.
+
+=over 4
+
+[ Should this be somewhat templatized, so they can customize
+the little HTML bits? Same goes with related functions. -- pudge ]
+
+=item Parameters
+
+=over 4
+
+=item STRING
+
+The string to be manipulated.
+
+=item MODE
+
+May be one of:
+
+=item nohtml
+
+The default.  Just strips out HTML.
+
+=item literal
+
+Prints the text verbatim into HTML, which
+means just converting < and > and & to their
+HTML entities.  Also turns on NO_WHITESPACE_FIX.
+
+=item extrans
+
+Similarly to 'literal', converts everything
+to its HTML entity, but then formatting is
+preserved by converting spaces to HTML
+space entities, and multiple newlines into BR
+tags.
+
+=item code
+
+Just like 'extrans' but wraps in CODE tags.
+
+=item attribute
+
+Attempts to format string to fit in as an HTML
+attribute, which means the same thing as 'literal',
+but " marks are also converted to their HTML entity.
+
+=item plaintext
+
+Similar to 'extrans', but does not translate < and >
+and & first (so C<stripBadHtml> is called first).
+
+=item anchor
+
+Removes ALL whitespace from inside the filter. It's
+is indented for use (but not limited to) the removal
+of white space from in side HREF anchor tags to 
+prevent nasty browser artifacts from showing up in
+the display. (Note: the value of NO_WHITESPACE_FIX 
+is ignored)
+
+=item html (or anything else)
+
+Just runs through C<stripBadHtml>.
+
+
+=item NO_WHITESPACE_FIX
+
+A boolean that, if true, disables fixing of whitespace
+problems.  A common exploit in these things is to
+run a lot of characters together so the page will
+stretch very wide.  If NO_WHITESPACE_FIX is false,
+then space is inserted to prevent this (see C<breakHtml>).
+
+=back
+
+=item Return value
+
+The manipulated string.
+
+
+=back
+
+=cut
+
+{ # closure for stripByMode
+
+my %ansi_to_ascii = (
+       131     => 'f',
+       133     => '...',
+       138     => 'S',
+       140     => 'OE',
+       142     => 'Z',
+       145     => '\'',
+       146     => '\'',
+       147     => '"',
+       148     => '"',
+       150     => '-',
+       151     => '--',
+       153     => '(TM)',
+       154     => 's',
+       156     => 'oe',
+       158     => 'z',
+       159     => 'Y',
+       166     => '|',
+       169     => '(C)',
+       174     => '(R)',
+       177     => '+/-',
+       188     => '1/4',
+       189     => '1/2',
+       190     => '3/4',
+       192     => 'A',
+       193     => 'A',
+       194     => 'A',
+       195     => 'A',
+       196     => 'A',
+       197     => 'A',
+       198     => 'AE',
+       199     => 'C',
+       200     => 'E',
+       201     => 'E',
+       202     => 'E',
+       203     => 'E',
+       204     => 'I',
+       205     => 'I',
+       206     => 'I',
+       207     => 'I',
+       208     => 'D',
+       209     => 'N',
+       210     => 'O',
+       211     => 'O',
+       212     => 'O',
+       213     => 'O',
+       214     => 'O',
+       215     => 'x',
+       216     => 'O',
+       217     => 'U',
+       218     => 'U',
+       219     => 'U',
+       220     => 'U',
+       221     => 'Y',
+       223     => 'B',
+       224     => 'a',
+       225     => 'a',
+       226     => 'a',
+       227     => 'a',
+       228     => 'a',
+       229     => 'a',
+       230     => 'ae',
+       231     => 'c',
+       232     => 'e',
+       233     => 'e',
+       234     => 'e',
+       235     => 'e',
+       236     => 'i',
+       237     => 'i',
+       238     => 'i',
+       239     => 'i',
+       240     => 'd',
+       241     => 'n',
+       242     => 'o',
+       243     => 'o',
+       244     => 'o',
+       245     => 'o',
+       246     => 'o',
+       247     => '/',
+       248     => 'o',
+       249     => 'u',
+       250     => 'u',
+       251     => 'u',
+       252     => 'u',
+       253     => 'y',
+       255     => 'y',
+);
+
+my %ansi_to_utf = (
+       128     => 8364,
+       129     => '',
+       130     => 8218,
+       131     => 402,
+       132     => 8222,
+       133     => 8230,
+       134     => 8224,
+       135     => 8225,
+       136     => 710,
+       137     => 8240,
+       138     => 352,
+       139     => 8249,
+       140     => 338,
+       141     => '',
+       142     => 381,
+       143     => '',
+       144     => '',
+       145     => 8216,
+       146     => 8217,
+       147     => 8220,
+       148     => 8221,
+       149     => 8226,
+       150     => 8211,
+       151     => 8212,
+       152     => 732,
+       153     => 8482,
+       154     => 353,
+       155     => 8250,
+       156     => 339,
+       157     => '',
+       158     => 382,
+       159     => 376,
+);
+
+# protect the hash by just returning it, for external use only
+sub _ansi_to_ascii { %ansi_to_ascii }
+sub _ansi_to_utf   { %ansi_to_utf }
+
+sub _charsetConvert {
+       my($char, $constants) = @_;
+       #$constants ||= getCurrentStatic();
+        $constants = { draconian_charset_convert => 0, };
+
+       my $str = '';
+       if ($constants->{draconian_charset_convert}) {
+               if ($constants->{draconian_charrefs}) {
+                       if ($constants->{good_numeric}{$char}) {
+                               $str = sprintf('&#%u;', $char);
+                       } else { # see if char is in %good_entity
+                               my $ent = $char2entity{chr $char};
+                               if ($ent) {
+                                       (my $data = $ent) =~ s/^&(\w+);$/$1/;
+                                       $str = $ent if $constants->{good_entity}{$data};
+                               }
+                       }
+               }
+               # fall back
+               $str ||= $ansi_to_ascii{$char};
+       }
+
+       # fall further back
+       # if the char is a special one we don't recognize in Latin-1,
+       # convert it here.  this does not prevent someone from manually
+       # entering &#147; or some such, if they feel they need to, it is
+       # to help catch it when browsers send non-Latin-1 data even though
+       # they shouldn't
+       $char = $ansi_to_utf{$char} if exists $ansi_to_utf{$char};
+       $str ||= sprintf('&#%u;', $char) if length $char;
+       return $str;
+}
+
+sub _fixupCharrefs {
+       my $constants = getCurrentStatic();
+
+       return if $constants->{bad_numeric};
+
+       # At the moment, unless the "draconian" rule is set, only
+       # entities that change the direction of text are forbidden.
+       # For more information, see
+       # <http://www.w3.org/TR/html4/struct/dirlang.html#bidirection>
+       # and <http://www.htmlhelp.com/reference/html40/special/bdo.html>.
+       $constants->{bad_numeric}  = { map { $_, 1 } @{$constants->{charrefs_bad_numeric}} };
+       $constants->{bad_entity}   = { map { $_, 1 } @{$constants->{charrefs_bad_entity}} };
+
+       $constants->{good_numeric} = { map { $_, 1 } @{$constants->{charrefs_good_numeric}},
+               grep { $_ < 128 || $_ > 159 } keys %ansi_to_ascii };
+       $constants->{good_entity}  = { map { $_, 1 } @{$constants->{charrefs_good_entity}}, qw(apos quot),
+               grep { s/^&(\w+);$/$1/ } map { $char2entity{chr $_} }
+               grep { $_ < 128 || $_ > 159 } keys %ansi_to_ascii };
+}
+
+my %action_data = ( );
+
+my %actions = (
+       newline_to_local => sub {
+                       ${$_[0]} =~ s/(?:\015?\012|\015)/\n/g;          },
+       trailing_whitespace => sub {
+                       ${$_[0]} =~ s/[\t ]+\n/\n/g;                    },
+       encode_html_amp => sub {
+                       ${$_[0]} =~ s/&/&amp;/g;                        },
+       encode_html_amp_ifnotent => sub {
+                       ${$_[0]} =~ s/&(?!#?[a-zA-Z0-9]+;)/&amp;/g;     },
+       encode_html_ltgt => sub {
+                       ${$_[0]} =~ s/</&lt;/g;
+                       ${$_[0]} =~ s/>/&gt;/g;                         },
+       encode_html_ltgt_stray => sub {
+                       1 while ${$_[0]} =~ s{
+                               ( (?: ^ | > ) [^<]* )
+                               >
+                       }{$1&gt;}gx;
+                       1 while ${$_[0]} =~ s{
+                               <
+                               ( [^>]* (?: < | $ ) )
+                               >
+                       }{&lt;$1}gx;                                    },
+       encode_html_quote => sub {
+                       ${$_[0]} =~ s/"/&#34;/g;                        },
+       breakHtml_ifwhitefix => sub {
+                       ${$_[0]} = breakHtml(${$_[0]})
+                               unless $action_data{no_white_fix};      },
+       processCustomTagsPre => sub {
+                       ${$_[0]} = processCustomTagsPre(${$_[0]});      },
+       processCustomTagsPost => sub {
+                       ${$_[0]} = processCustomTagsPost(${$_[0]});     },
+       approveTags => sub {
+                       ${$_[0]} =~ s/<(.*?)>/approveTag($1)/sge;       },
+       url2html => sub {
+                       ${$_[0]} = url2html(${$_[0]});                  },
+       approveCharrefs => sub {
+                       ${$_[0]} =~ s{
+                               &(\#?[a-zA-Z0-9]+);?
+                       }{approveCharref($1)}gex;                       },
+       space_between_tags => sub {
+                       ${$_[0]} =~ s/></> </g;                         },
+       whitespace_tagify => sub {
+                       ${$_[0]} =~ s/\n/<br>/gi;  # pp breaks
+                       ${$_[0]} =~ s/(?:<br>\s*){2,}<br>/<br><br>/gi;
+                       # Preserve leading indents / spaces
+                       # can mess up internal tabs, oh well
+                       ${$_[0]} =~ s/\t/    /g;                        },
+       paragraph_wrap => sub {
+                       # start off the text with a <p>!
+                       ${$_[0]} = '<p>' . ${$_[0]} unless ${$_[0]} =~ /^\s*<p>/s;
+                       # this doesn't assume there will be only two BRs,
+                       # but it does come after whitespace_tagify, so
+                       # chances are, will be only two BRs in a row
+                       ${$_[0]} =~ s/(?:<br>){2}/<p>/g;
+                       # make sure we don't end with a <br><p> or <br>
+                       ${$_[0]} =~ s/<br>(<p>|$)/$1/g;                 },
+       whitespace_and_tt => sub {
+                       ${$_[0]} =~ s{((?:  )+)(?: (\S))?} {
+                               ("&nbsp; " x (length($1)/2)) .
+                               (defined($2) ? "&nbsp;$2" : "")
+                       }eg;
+                       ${$_[0]} = "<tt>${$_[0]}</tt>";                 },
+       newline_indent => sub {
+                       ${$_[0]} =~ s{<br>\n?( +)} {
+                               "<br>\n" . ('&nbsp; ' x length($1))
+                       }ieg;                                           },
+       remove_tags => sub {
+                       ${$_[0]} =~ s/<.*?>//gs;                        },
+       remove_ltgt => sub {
+                       ${$_[0]} =~ s/<//g;
+                       ${$_[0]} =~ s/>//g;                             },
+       remove_trailing_lts => sub {
+                       ${$_[0]} =~ s/<(?!.*?>)//gs;                    },
+       remove_newlines => sub {
+                       ${$_[0]} =~ s/\n+//g;                           },
+       debugprint => sub {
+                       print STDERR "stripByMode debug ($_[1]) '${$_[0]}'\n";  },
+
+       encode_high_bits => sub {
+                       # !! assume Latin-1 !!
+                       #my $constants = getCurrentStatic();
+                       my $constants = { draconian_charset => 0, };
+                       if ($constants->{draconian_charset}) {
+                               # anything not CRLF tab space or ! to ~ in Latin-1
+                               # is converted to entities, where approveCharrefs or
+                               # encode_html_amp takes care of them later
+                               _fixupCharrefs();
+                               ${$_[0]} =~ s[([^\n\r\t !-~])][ _charsetConvert(ord($1), $constants)]ge;
+                       }                                               },
+);
+
+my %mode_actions = (
+        ANCHOR, [qw(
+                       newline_to_local
+                       remove_newlines                 )],
+       NOTAGS, [qw(
+                       newline_to_local
+                       encode_high_bits
+                       remove_tags
+                       remove_ltgt
+                       encode_html_amp_ifnotent
+                       approveCharrefs                 )],
+       ATTRIBUTE, [qw(
+                       newline_to_local
+                       encode_high_bits
+                       encode_html_amp
+                       encode_html_ltgt
+                       encode_html_quote               )],
+       LITERAL, [qw(
+                       newline_to_local
+                       encode_html_amp
+                       encode_html_ltgt
+                       breakHtml_ifwhitefix
+                       remove_trailing_lts
+                       approveTags
+                       space_between_tags
+                       encode_html_ltgt_stray          )],
+       NOHTML, [qw(
+                       newline_to_local
+                       trailing_whitespace
+                       encode_high_bits
+                       remove_tags
+                       remove_ltgt
+                       encode_html_amp                 )],
+       PLAINTEXT, [qw(
+                       newline_to_local
+                       trailing_whitespace
+                       encode_high_bits
+                       processCustomTagsPre
+                       remove_trailing_lts
+                       approveTags
+                       processCustomTagsPost
+                       space_between_tags
+                       encode_html_ltgt_stray
+                       encode_html_amp_ifnotent
+                       approveCharrefs
+                       breakHtml_ifwhitefix
+                       whitespace_tagify
+                       newline_indent
+                       paragraph_wrap                  )],
+       HTML, [qw(
+                       newline_to_local
+                       trailing_whitespace
+                       encode_high_bits
+                       processCustomTagsPre
+                       remove_trailing_lts
+                       approveTags
+                       processCustomTagsPost
+                       space_between_tags
+                       encode_html_ltgt_stray
+                       encode_html_amp_ifnotent
+                       approveCharrefs
+                       breakHtml_ifwhitefix            )],
+       CODE, [qw(
+                       newline_to_local
+                       trailing_whitespace
+                       encode_high_bits
+                       encode_html_amp
+                       encode_html_ltgt
+                       whitespace_tagify
+                       whitespace_and_tt
+                       breakHtml_ifwhitefix            )],
+       EXTRANS, [qw(
+                       newline_to_local
+                       trailing_whitespace
+                       encode_high_bits
+                       encode_html_amp
+                       encode_html_ltgt
+                       breakHtml_ifwhitefix
+                       whitespace_tagify
+                       newline_indent                  )],
+);
+
+sub stripByMode {
+       my($str, $fmode, $no_white_fix) = @_;
+       $str = '' if !defined($str);
+       $fmode ||= NOHTML;
+       $no_white_fix = 1 if !defined($no_white_fix) && $fmode == LITERAL;
+       $action_data{no_white_fix} = $no_white_fix || 0;
+
+       my @actions = @{$mode_actions{$fmode}};
+#my $c = 0; print STDERR "stripByMode:start:$c:[{ $str }]\n";
+       for my $action (@actions) {
+               $actions{$action}->(\$str, $fmode);
+#$c++; print STDERR "stripByMode:$action:$c:[{ $str }]\n";
+       }
+       return $str;
+}
+
+}
+
+#========================================================================
+
+=head2 strip_anchor(STRING [, NO_WHITESPACE_FIX])
+
+=head2 strip_attribute(STRING [, NO_WHITESPACE_FIX])
+
+=head2 strip_code(STRING [, NO_WHITESPACE_FIX])
+
+=head2 strip_extrans(STRING [, NO_WHITESPACE_FIX])
+
+=head2 strip_html(STRING [, NO_WHITESPACE_FIX])
+
+=head2 strip_literal(STRING [, NO_WHITESPACE_FIX])
+
+=head2 strip_nohtml(STRING [, NO_WHITESPACE_FIX])
+
+=head2 strip_notags(STRING [, NO_WHITESPACE_FIX])
+
+=head2 strip_plaintext(STRING [, NO_WHITESPACE_FIX])
+
+=head2 strip_mode(STRING [, MODE, NO_WHITESPACE_FIX])
+
+Wrapper for C<stripByMode>.  C<strip_mode> simply calls C<stripByMode>
+and has the same arguments, but C<strip_mode> will only allow modes
+with values greater than 0, that is, the user-supplied modes.  C<strip_mode>
+is only meant to be used for processing user-supplied modes, to prevent
+the user from accessing other mode types.  For using specific modes instead
+of user-supplied modes, use the function with that mode's name.
+
+See C<stripByMode> for details.
+
+=cut
+
+sub strip_mode {
+       my($string, $mode, @args) = @_;
+       return "" if !$mode || $mode < 1 || $mode > 4;  # user-supplied modes are 1-4
+       return stripByMode($string, $mode, @args);
+}
+
+sub strip_anchor       { stripByMode($_[0], ANCHOR,    @_[1 .. $#_]) }
+sub strip_attribute    { stripByMode($_[0], ATTRIBUTE, @_[1 .. $#_]) }
+sub strip_code         { stripByMode($_[0], CODE,      @_[1 .. $#_]) }
+sub strip_extrans      { stripByMode($_[0], EXTRANS,   @_[1 .. $#_]) }
+sub strip_html         { stripByMode($_[0], HTML,      @_[1 .. $#_]) }
+sub strip_literal      { stripByMode($_[0], LITERAL,   @_[1 .. $#_]) }
+sub strip_nohtml       { stripByMode($_[0], NOHTML,    @_[1 .. $#_]) }
+sub strip_notags       { stripByMode($_[0], NOTAGS,    @_[1 .. $#_]) }
+sub strip_plaintext    { stripByMode($_[0], PLAINTEXT, @_[1 .. $#_]) }