#!/usr/bin/perl
#
# This code was forked from the LiveJournal project owned and operated
# by Live Journal, Inc. The code has been modified and expanded by
# Dreamwidth Studios, LLC. These files were originally licensed under
# the terms of the license supplied by Live Journal, Inc, which can
# currently be found at:
#
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
#
# In accordance with the original license, this code and all its
# modifications are provided under the GNU General Public License.
# A copy of that license can be found in the LICENSE file included as
# part of this distribution.

package LJ;
use strict;

use Carp;
use POSIX;
use Digest::MD5;
use Digest::SHA1;

use DW::Auth::Challenge;
use DW::External::Site;
use DW::Request;
use DW::Formats;
use LJ::Utils qw(rand_chars);
use LJ::Global::Constants;
use LJ::Event;
use LJ::Subscription::Pending;
use LJ::Directory::Search;
use LJ::Directory::Constraint;
use LJ::PageStats;
use LJ::JSON;

# <LJFUNC>
# name: LJ::img
# des: Returns an HTML &lt;img&gt; or &lt;input&gt; tag to an named image
#      code, which each site may define with a different image file with
#      its own dimensions.  This prevents hard-coding filenames & sizes
#      into the source.  The real image data is stored in LJ::Img, which
#      has default values provided in cgi-bin/LJ/Global/Img.pm but can be
#      overridden in cgi-bin/LJ/Local/Img.pm or etc/config.pl.
# args: imagecode, type?, attrs?
# des-imagecode: The unique string key to reference the image.  Not a filename,
#                but the purpose or location of the image.
# des-type: By default, the tag returned is an &lt;img&gt; tag, but if 'type'
#           is "input", then an input tag is returned.
# des-attrs: Optional hashref of other attributes.  If this isn't a hashref,
#            then it's assumed to be a scalar for the 'name' attribute for
#            input controls.
# </LJFUNC>
sub img {
    my ( $ic, $type, $attr ) = @_;
    $type //= '';    # Type is either "" or "input"

    my ( $attrs, $alt ) = ( '', '' );
    if ($attr) {
        if ( ref $attr eq "HASH" ) {
            if ( exists $attr->{alt} ) {
                $alt = LJ::ehtml( $attr->{alt} );
                delete $attr->{alt};
            }
            $attrs .= " $_=\"" . LJ::ehtml( $attr->{$_} || '' ) . "\"" foreach keys %$attr;
        }
        else {
            $attrs = " id=\"$attr\"";
        }
    }

    my $i = $LJ::Img::img{$ic};
    $alt ||= LJ::Lang::string_exists( $i->{alt} ) ? LJ::Lang::ml( $i->{alt} ) : $i->{alt};
    if ( $type eq "" ) {
        return
              "<img src=\"$LJ::IMGPREFIX$i->{src}\" width=\"$i->{width}\" "
            . "height=\"$i->{height}\" alt=\"$alt\" title=\"$alt\" "
            . "border='0'$attrs />";
    }
    if ( $type eq "input" ) {
        return
              "<input type=\"image\" src=\"$LJ::IMGPREFIX$i->{'src'}\" "
            . "width=\"$i->{'width'}\" height=\"$i->{'height'}\" title=\"$alt\" "
            . "alt=\"$alt\" border='0'$attrs />";
    }
    return "<b>XXX</b>";
}

# <LJFUNC>
# name: LJ::date_to_view_links
# class: component
# des: Returns HTML of date with links to user's journal.
# args: u, date
# des-date: date in yyyy-mm-dd form.
# returns: HTML with yyyy, mm, and dd all links to respective views.
# </LJFUNC>
sub date_to_view_links {
    my ( $u, $date ) = @_;
    return unless $date =~ /^(\d\d\d\d)-(\d\d)-(\d\d)/;

    my ( $y, $m, $d ) = ( $1, $2, $3 );
    my $base = $u->journal_base;

    my $ret;
    $ret .= "<a href=\"$base/$y/\">$y</a>-";
    $ret .= "<a href=\"$base/$y/$m/\">$m</a>-";
    $ret .= "<a href=\"$base/$y/$m/$d/\">$d</a>";
    return $ret;
}

# <LJFUNC>
# name: LJ::auto_linkify
# des: Takes a plain-text string and changes URLs into <a href> tags (auto-linkification).
# args: str
# des-str: The string to perform auto-linkification on.
# returns: The auto-linkified text.
# </LJFUNC>
sub auto_linkify {
    my $str   = shift;
    my $match = sub {
        my $str = shift;
        if ( $str =~ /^(.*?)(&(#39|quot|lt|gt)(;.*)?)$/ ) {
            return "<a href='$1'>$1</a>$2";
        }
        else {
            return "<a href='$str'>$str</a>";
        }
    };
    $str =~ s!(https?://[^\s\'\"\<\>]+[a-zA-Z0-9_/&=\-])! $match->( $1 ); !ge
        if defined $str;

    return $str;
}

# return 1 if URL is a safe stylesheet that S1/S2/etc can pull in.
# return 0 to reject the link tag
# return a URL to rewrite the stylesheet URL
# $href will always be present.  $host and $path may not.
sub valid_stylesheet_url {
    my ( $href, $host, $path ) = @_;
    unless ( $host && $path ) {
        return 0 unless $href =~ m!^https?://([^/]+?)(/.*)$!;
        ( $host, $path ) = ( $1, $2 );
    }

    my $cleanit = sub {

        # allow tag, if we're doing no css cleaning
        return 1 unless LJ::is_enabled('css_cleaner');

        # remove tag, if we have no CSSPROXY configured
        return 0 unless $LJ::CSSPROXY;

        # rewrite tag for CSS cleaning
        return "$LJ::CSSPROXY?u=" . LJ::eurl($href);
    };

    return 1 if $LJ::TRUSTED_CSS_HOST{$host};
    return $cleanit->() unless $host =~ /\Q$LJ::DOMAIN\E$/i;

    # let users use system stylesheets.
    return 1
        if $host eq $LJ::DOMAIN
        || $host eq $LJ::DOMAIN_WEB
        || $href =~ /^\Q$LJ::STATPREFIX\E/;

    # S2 stylesheets:
    return 1 if $path =~ m!^(/~\w+|/users/\w+|/\w+)?/res/(\d+)/stylesheet(\?\d+)?$!;

    # unknown, reject.
    return $cleanit->();
}

# <LJFUNC>
# name: LJ::make_authas_select
# des: Given a u object and some options, determines which users the given user
#      can switch to.  If the list exists, returns a select list and a submit
#      button with labels.  Otherwise returns a hidden element.
# returns: string of HTML elements
# args: u, opts?
# des-opts: Optional.  Valid keys are:
#           'authas' - current user, gets selected in drop-down;
#           'label' - label to go before form elements;
#           'button' - button label for submit button;
#           'type' - journaltype (affects label & list filtering)
#           'selectonly' - 1 for just menu items, otherwise entire HTML block
#           'foundation' - 1 for Foundation HTML, otherwise legacy HTML
#           others - arguments to pass to $u->get_authas_list.
# </LJFUNC>
sub make_authas_select {
    my ( $u, $opts ) = @_;    # type, authas, label, button

    my $authas = $opts->{authas} || $u->user;
    my $button = $opts->{button} || $BML::ML{'web.authas.btn'};
    my $label  = $opts->{label}  || $BML::ML{'web.authas.select.label'};

    my $foundation = $opts->{foundation} || 0;

    my @list = $u->get_authas_list($opts);

    # only do most of form if there are options to select from
    if ( @list > 1 || $list[0] ne $u->user ) {
        my $menu = LJ::html_select(
            {
                name     => 'authas',
                selected => $authas,
                class    => 'hideable',
                id       => 'authas',
                style    => $foundation ? 'width: 100%' : '',
            },
            map { $_, $_ } @list
        );

        my $ret = '';
        if ( $opts->{selectonly} ) {
            $ret = $menu;
        }
        else {
            $ret = $foundation
                ? q{<div class='row collapse'><div class='columns small-3 medium-1'><label class='inline'>}
                . $label
                . q{</label></div>}
                . q{<div class='columns small-9 medium-11'><div class='row'>}
                . q{<div class="columns small-8 medium-4">}
                . $menu
                . q{</div>}
                . q{<div class='columns small-4 medium-2 end'>}
                . LJ::html_submit( undef, $button, { class => "secondary button" } )
                . q{</div>}
                . q{</div></div>}
                . q{</div>}

                # else not foundation
                : "<br/>"
                . LJ::Lang::ml( 'web.authas.select',
                { menu => $menu, username => LJ::ljuser($authas) } )
                . " "
                . LJ::html_submit( undef, $button )
                . "<br/><br/>\n";
        }

        return $ret;
    }

    # no communities to choose from, give the caller a hidden
    return LJ::html_hidden( authas => $authas );
}

# <LJFUNC>
# name: LJ::help_icon
# des: Returns BML to show a help link/icon given a help topic, or nothing
#      if the site hasn't defined a URL for that topic.  Optional arguments
#      include HTML/BML to place before and after the link/icon, should it
#      be returned.
# args: topic, pre?, post?
# des-topic: Help topic key.
#            See etc/config-local.pl, or [special[helpurls]] for examples.
# des-pre: HTML/BML to place before the help icon.
# des-post: HTML/BML to place after the help icon.
# </LJFUNC>
sub help_icon {
    my $topic = shift;
    my $pre   = shift;
    my $post  = shift;
    return "" unless ( defined $LJ::HELPURL{$topic} );
    return "$pre<?help $LJ::HELPURL{$topic} help?>$post";
}

# like help_icon, but no BML.
sub help_icon_html {
    my $topic = shift;
    my $url   = $LJ::HELPURL{$topic} or return "";
    my $pre   = shift || "";
    my $post  = shift || "";
    return
          "$pre<a href=\"$url\" class=\"helplink\" target=\"_blank\">"
        . LJ::img( 'help', '' )
        . "</a>$post";
}

# <LJFUNC>
# name: LJ::bad_input
# des: Returns common BML for reporting form validation errors in
#      a bulleted list.
# returns: BML showing errors.
# args: error*
# des-error: A list of errors
# </LJFUNC>
sub bad_input {
    my @errors = @_;
    my $ret    = "";
    $ret .= "<?badcontent?>\n<ul>\n";
    foreach my $ei (@errors) {
        my $err = LJ::errobj($ei) or next;
        $ret .= $err->as_bullets;
    }
    $ret .= "</ul>\n";
    return $ret;
}

# <LJFUNC>
# name: LJ::error_list
# des: Returns an error bar with bulleted list of errors.
# returns: BML showing errors.
# args: error*
# des-error: A list of errors
# </LJFUNC>
sub error_list {

    # FIXME: retrofit like bad_input above?  merge?  make aliases for each other?
    my @errors = @_;
    my $ret;
    $ret .= "<?errorbar ";
    $ret .= "<strong>";
    $ret .= BML::ml('error.procrequest');
    $ret .= "</strong><ul>";

    foreach my $ei (@errors) {
        my $err = LJ::errobj($ei) or next;
        $ret .= $err->as_bullets;
    }
    $ret .= " </ul> errorbar?>";
    return $ret;
}

# <LJFUNC>
# name: LJ::error_noremote
# des: Returns an error telling the user to log in.
# returns: Translation string "error.notloggedin"
# </LJFUNC>
sub error_noremote {
    return "<?needlogin?>";
}

# <LJFUNC>
# name: LJ::warning_list
# des: Returns a warning bar with bulleted list of warnings.
# returns: BML showing warnings
# args: warnings*
# des-warnings: A list of warnings
# </LJFUNC>
sub warning_list {
    my @warnings = @_;
    my $ret;

    $ret .= "<?warningbar ";
    $ret .= "<strong>";
    $ret .= BML::ml('label.warning');
    $ret .= "</strong><ul>";

    foreach (@warnings) {
        $ret .= "<li>$_</li>";
    }
    $ret .= " </ul> warningbar?>";
    return $ret;
}

# <LJFUNC>
# name: LJ::did_post
# des: Cookies should only show pages which make no action.
#      When an action is being made, check the request coming
#      from the remote user is a POST request.
# info: When web pages are using cookie authentication, you can't just trust that
#       the remote user wants to do the action they're requesting.  It's way too
#       easy for people to force other people into making GET requests to
#       a server.  What if a user requested http://server/delete_all_journal.bml,
#       and that URL checked the remote user and immediately deleted the whole
#       journal?  Now anybody has to do is embed that address in an image
#       tag and a lot of people's journals will be deleted without them knowing.
#       Cookies should only show pages which make no action.  When an action is
#       being made, check that it's a POST request.
# returns: true if REQUEST_METHOD == "POST"
# </LJFUNC>
sub did_post {
    return ( BML::get_method() eq "POST" );
}

# <LJFUNC>
# name: LJ::robot_meta_tags
# des: Returns meta tags to instruct a robot/crawler to not index or follow links.
# returns: A string with appropriate meta tags
# </LJFUNC>
sub robot_meta_tags {
    return "<meta name=\"robots\" content=\"noindex, nofollow, noarchive\" />\n"
        . "<meta name=\"googlebot\" content=\"noindex, nofollow, noarchive, nosnippet\" />\n";
}

sub paging_bar {
    my ( $page, $pages, $opts ) = @_;

    my $self_link = $opts->{self_link}
        || sub { LJ::page_change_getargs( page => $_[0] ) };

    my $href_opts = $opts->{href_opts} || sub { '' };

    my $nav = '';
    return $nav unless $pages && $pages > 1;

    $nav .= "<p style='font-weight: bolder; margin: 0 0 .5em 0'>";
    $nav .= LJ::Lang::ml( 'ljlib.pageofpages', { page => $page, total => $pages } );
    $nav .= "</p>\n";

    my $linkify = sub {
        "<a href='" . $self_link->( $_[0] ) . "'" . $href_opts->( $_[0] ) . ">$_[1]</a>\n";
    };

    my ( $left, $right ) = ( "<b>&lt;&lt;</b>", "<b>&gt;&gt;</b>" );
    $left  = $linkify->( $page - 1, $left )  if $page > 1;
    $right = $linkify->( $page + 1, $right ) if $page < $pages;

    my @pagelinks;

    for ( my $i = 1 ; $i <= $pages ; $i++ ) {
        my $link = "[$i]";
        $link = ( $i != $page ) ? $linkify->( $i, $link ) : "<b>$link</b>";
        push @pagelinks, "<br />" if $i > 10 && ( $i == 11 || $i % 10 == 0 );
        push @pagelinks, $link;
    }

    $nav .= "$left &nbsp; ";
    $nav .= "<span style='text-align: center'>";
    $nav .= join ' ', @pagelinks;
    $nav .= "</span>";
    $nav .= " &nbsp; $right";

    return
"<div class='action-box'><div class='inner'>$nav</div></div><div class='clear-floats'></div>\n";
}

=head2 C<< LJ::page_change_getargs( %args ) >>
Returns the current URL with a modified list of GET arguments.
=cut

sub page_change_getargs {
    my %args    = @_;
    my %cu_opts = ( keep_args => 1, no_blank => 1 );

    # specified args will override keep_args
    return LJ::create_url( undef, args => \%args, %cu_opts );
}

=head2 C<< LJ::paging( $listref, $page, $pagesize ) >>
Drop-in replacement for BML::paging in non-BML context.
=cut

sub paging {
    my ( $listref, $page, $pagesize ) = @_;
    $page = 1 unless $page && $page == int $page;
    return unless $pagesize;    # let's not divide by zero
    my @items = @{$listref};
    my %self;

    my $newurl = sub {

        # replaces BML::page_newurl
        return LJ::page_change_getargs( page => $_[0] );
    };

    $self{itemcount} = scalar @items;

    $self{pages} = $self{itemcount} / $pagesize;
    $self{pages} = int( $self{pages} ) + 1
        if $self{pages} != int( $self{pages} );    # round up any fraction

    $page = 1            if $page < 1;
    $page = $self{pages} if $page > $self{pages};
    $self{page} = $page;

    $self{itemfirst} = $pagesize * ( $page - 1 ) + 1;
    $self{itemlast}  = $pagesize * $page;
    $self{itemlast}  = $self{itemcount} if $self{pages} == $page;

    my @range = ( $self{itemfirst} - 1 ) .. ( $self{itemlast} - 1 );
    $self{items} = [ @items[@range] ];

    my ( $prev, $next ) = ( $newurl->( $page - 1 ), $newurl->( $page + 1 ) );
    $self{backlink} = "<a href=\"$prev\">&lt;&lt;&lt;</a>" unless $page == 1;
    $self{nextlink} = "<a href=\"$next\">&gt;&gt;&gt;</a>" unless $page == $self{pages};

    return %self;
}

# <LJFUNC>
# class: web
# name: LJ::make_cookie
# des: Prepares cookie header lines.
# returns: An array of cookie lines.
# args: name, value, expires, path?, domain?
# des-name: The name of the cookie.
# des-value: The value to set the cookie to.
# des-expires: The time (in seconds) when the cookie is supposed to expire.
#              Set this to 0 to expire when the browser closes. Set it to
#              undef to delete the cookie.
# des-path: The directory path to bind the cookie to.
# des-domain: The domain (or domains) to bind the cookie to.
# </LJFUNC>
sub make_cookie {
    my ( $name, $value, $expires, $path, $domain ) = @_;
    my $cookie  = "";
    my @cookies = ();

    # let the domain argument be an array ref, so callers can set
    # cookies in both .foo.com and foo.com, for some broken old browsers.
    if ( $domain && ref $domain eq "ARRAY" ) {
        foreach (@$domain) {
            push( @cookies, LJ::make_cookie( $name, $value, $expires, $path, $_ ) );
        }
        return;
    }

    my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = gmtime($expires);
    $year += 1900;

    my @day   = qw{Sunday Monday Tuesday Wednesday Thursday Friday Saturday};
    my @month = qw{Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec};

    $cookie = sprintf "%s=%s", LJ::eurl($name), LJ::eurl($value);

    # this logic is confusing potentially
    unless ( defined $expires && $expires == 0 ) {
        $cookie .= sprintf "; expires=$day[$wday], %02d-$month[$mon]-%04d %02d:%02d:%02d GMT",
            $mday, $year, $hour, $min, $sec;
    }

    $cookie .= "; path=$path"     if $path;
    $cookie .= "; domain=$domain" if $domain;
    push( @cookies, $cookie );
    return @cookies;
}

# <LJFUNC>
# name: LJ::check_referer
# class: web
# des: Checks if the user is coming from a given URI.
# args: uri?, referer?
# des-uri: string; the URI we want the user to come from.
# des-referer: string; the location the user is posting from.
#              If not supplied, will be retrieved with BML::get_client_header.
#              In general, you don't want to pass this yourself unless
#              you already have it or know we can't get it from BML.
# returns: 1 if they're coming from that URI, else undef
# </LJFUNC>
sub check_referer {
    my $uri     = shift(@_) || '';
    my $referer = shift(@_) || BML::get_client_header('Referer');

    # get referer and check
    return 1 unless $referer;

    my ( $origuri, $origreferer ) = ( $uri, $referer );

    # escape any regex characters, like the '.' in '.bml'
    $uri = quotemeta($uri);

    # check that the end of the uri matches exactly (no extra characters or dir levels)
    # or else that the uri is followed immediately by additional parameters
    my $checkend = '(?:$|\\?)';

    # allow us to properly check URIs without .bml extensions
    if ( $origuri =~ /\.bml($|\?)/ ) {
        $checkend = '' if $1 eq '?';
        $uri     =~ s/\\.bml($|\\\?)/$1$checkend/;
        $referer =~ s/\.bml($|\?)/$1/;
    }
    elsif ($uri) {
        $uri .= $checkend;
    }
    else {
        $uri = '(/|$)';
    }

    return 1 if $LJ::SITEROOT   && $referer =~ m!^\Q$LJ::SITEROOT\E$uri!;
    return 1 if $LJ::DOMAIN     && $referer =~ m!^https?://\Q$LJ::DOMAIN\E$uri!;
    return 1 if $LJ::DOMAIN_WEB && $referer =~ m!^https?://\Q$LJ::DOMAIN_WEB\E$uri!;
    return 1
        if $referer =~ m!^https?://([A-Za-z0-9_\-]{1,25})\.\Q$LJ::DOMAIN\E$uri!;
    return 1 if $origuri =~ m!^https?://! && $origreferer eq $origuri;

    # Dev container: $LJ::DOMAIN is empty, so match referer host against request host
    if ( $LJ::IS_DEV_SERVER && !$LJ::DOMAIN ) {
        my $r = eval { DW::Request->get };
        if ($r) {
            my $host = $r->host;
            return 1 if $referer =~ m!^https?://\Q$host\E$uri!;
        }
    }

    return undef;
}

# <LJFUNC>
# name: LJ::form_auth
# class: web
# des: Creates an authentication token to be used later to verify that a form
#      submission came from a particular user.
# args: raw?
# des-raw: boolean; If true, returns only the token (no HTML).
# returns: HTML hidden field to be inserted into the output of a page.
# </LJFUNC>
sub form_auth {
    my $raw  = shift;
    my $chal = $LJ::REQ_GLOBAL{form_auth_chal};

    unless ($chal) {
        my $remote = LJ::get_remote();
        my $id     = $remote ? $remote->id : 0;
        my $sess =
            $remote && $remote->session ? $remote->session->id : LJ::UniqCookie->current_uniq;

        my $auth = join( '-', LJ::rand_chars(10), $id, $sess );
        $chal = DW::Auth::Challenge->generate( 86400, $auth );
        $LJ::REQ_GLOBAL{form_auth_chal} = $chal;
    }

    return $raw ? $chal : LJ::html_hidden( "lj_form_auth", $chal );
}

# <LJFUNC>
# name: LJ::check_form_auth
# class: web
# des: Verifies form authentication created with [func[LJ::form_auth]].
# returns: Boolean; true if the current data in %POST is a valid form, submitted
#          by the user in $remote using the current session,
#          or false if the user has changed, the challenge has expired,
#          or the user has changed session (logged out and in again, or something).
# </LJFUNC>
sub check_form_auth {
    my $formauth = shift || $BMLCodeBlock::POST{'lj_form_auth'};
    return 0 unless $formauth;

    my $remote = LJ::get_remote();
    my $id     = $remote ? $remote->id : 0;
    my $sess   = $remote && $remote->session ? $remote->session->id : LJ::UniqCookie->current_uniq;

    # check the attributes are as they should be
    my $attr = DW::Auth::Challenge->get_attributes($formauth);
    my ( $randchars, $chal_id, $chal_sess ) = split( /\-/, $attr );

    return 0 unless $id == $chal_id;
    return 0 unless $sess eq $chal_sess;

    # check the signature is good and not expired
    my $opts = { dont_check_count => 1 };    # in/out
    DW::Auth::Challenge->check( $formauth, $opts );
    return $opts->{valid} && !$opts->{expired};
}

# <LJFUNC>
# name: LJ::create_qr_div
# class: web
# des: Creates the hidden div that stores the QuickReply form.
# returns: undef upon failure or HTML for the div upon success
# args: user, remote, ditemid, style args, userpic, viewing thread
# des-u: user object or userid for journal reply in.
# des-ditemid: ditemid for this comment.
# des-style_opts: the viewing style arguments on this page, as a hashref.
# des-userpic: alternate default userpic.
# </LJFUNC>
sub create_qr_div {

    my ( $user, $ditemid, %opts ) = @_;
    my $u      = LJ::want_user($user);
    my $remote = LJ::get_remote();
    return undef unless $u && $remote && $ditemid;

    my $style_opts     = $opts{style_opts} || {};
    my $userpic_kw     = $opts{userpic};
    my $viewing_thread = $opts{thread};

    return undef if $remote->prop("opt_no_quickreply");

    my $e         = LJ::Entry->new( $u, ditemid => $ditemid );
    my $separator = %$style_opts ? "&" : "?";
    my $basepath  = $e->url( style_opts => LJ::viewing_style_opts(%$style_opts) ) . $separator;

    my $usertype =
        ( $remote->openid_identity && $remote->is_validated ) ? 'openid_cookie' : 'cookieuser';
    my $hidden_form_elements .= LJ::html_hidden(
        { 'name' => 'replyto',      'id' => 'replyto',      'value' => '' },
        { 'name' => 'parenttalkid', 'id' => 'parenttalkid', 'value' => '' },

        # ^ these two inputs are duplicates, but oh well.
        { 'name' => 'journal',    'id' => 'journal',    'value' => $u->{'user'} },
        { 'name' => 'itemid',     'id' => 'itemid',     'value' => $ditemid },
        { 'name' => 'usertype',   'id' => 'usertype',   'value' => $usertype },
        { 'name' => 'qr',         'id' => 'qr',         'value' => '1' },
        { 'name' => 'cookieuser', 'id' => 'cookieuser', 'value' => $remote->{'user'} },
        { 'name' => 'dtid',       'id' => 'dtid',       'value' => '' },

        # ^ the "display" version of replyto/parenttalkid. Starts empty, set by
        # JS when QR is summoned, then used to build the "more options" URL.
        # Nothing uses this after the form is submitted, it's just for JS.
        { 'name' => 'basepath',       'id' => 'basepath',       'value' => $basepath },
        { 'name' => 'viewing_thread', 'id' => 'viewing_thread', 'value' => $viewing_thread },
    );

    while ( my ( $key, $value ) = each %$style_opts ) {
        $hidden_form_elements .= LJ::html_hidden( { name => $key, id => $key, value => $value } );
    }

    # rate limiting challenge
    {
        my ( $time, $secret ) = LJ::get_secret();
        my $rchars = LJ::rand_chars(20);
        my $chal   = $ditemid . "-$u->{userid}-$time-$rchars";
        my $res    = Digest::MD5::md5_hex( $secret . $chal );
        $hidden_form_elements .= LJ::html_hidden( "chrp1", "$chal-$res" );
    }

    # hashref with "selected" and "items" keys
    my $editors = DW::Formats::select_items( preferred => $remote->prop('comment_editor'), );

    # FIXME: This is incoherent on reading/network pages. (But it's only
    # noticeable when viewing the reading/network page of someone who wouldn't
    # allow you to comment.) -NF
    my $post_disabled = $u->does_not_allow_comments_from($remote);

    return DW::Template->template_string(
        'journal/quickreply.tt',
        {
            form_url             => LJ::create_url( '/talkpost_do', host => $LJ::DOMAIN_WEB ),
            hidden_form_elements => $hidden_form_elements,
            post_disabled        => $post_disabled,
            post_button_class    => $post_disabled ? 'ui-state-disabled' : '',

            # Currently unused, but might come back.
            minimal => $opts{minimal} ? 1 : 0,

            current_icon_kw => $userpic_kw,
            current_icon    => LJ::Userpic->new_from_keyword( $remote, $userpic_kw ),

            editors => $editors,

            foundation_beta => !LJ::BetaFeatures->user_in_beta( $remote => "nos2foundation" ),

            remote => $remote,

            journal => {
                is_iplogging    => $u->opt_logcommentips eq 'A',
                is_linkstripped => !$remote
                    || ( $remote && $remote->is_identity && !$u->trusts_or_has_member($remote) ),
            },

            help => {
                icon      => LJ::help_icon_html( "userpics",  " " ),
                iplogging => LJ::help_icon_html( "iplogging", " " ),
            },
        }
    );
}

# <LJFUNC>
# name: LJ::get_lastcomment
# class: web
# des: Looks up the last talkid and journal the remote user posted in.
# returns: talkid, jid
# args:
# </LJFUNC>
sub get_lastcomment {
    my $remote = LJ::get_remote();
    my ( $talkid, $jid );

    # Figure out their last post
    if ($remote) {
        my $memkey = [ $remote->{'userid'}, "lastcomm:$remote->{'userid'}" ];
        my $memval = LJ::MemCache::get($memkey);
        ( $jid, $talkid ) = split( /:/, $memval ) if $memval;
    }

    return ( $talkid, $jid );
}

# <LJFUNC>
# name: LJ::make_qr_target
# class: web
# des: Returns a div usable for QuickReply boxes.
# returns: HTML for the div
# args:
# </LJFUNC>
sub make_qr_target {
    my $name = shift;

    return "<div id='ljqrt$name' name='ljqrt$name'></div>";
}

# <LJFUNC>
# name: LJ::set_lastcomment
# class: web
# des: Sets the lastcomm memcached key for this user's last comment.
# returns: undef on failure
# args: u, remote, dtalkid, life?
# des-u: Journal they just posted in, either u or userid
# des-remote: Remote user
# des-dtalkid: Talkid for the comment they just posted
# des-life: How long, in seconds, the memcached key should live.
# </LJFUNC>
sub set_lastcomment {
    my ( $u, $remote, $dtalkid, $life ) = @_;

    my $userid = LJ::want_userid($u);
    return undef unless $userid && $remote && $dtalkid;

    # By default, this key lasts for 10 seconds.
    $life ||= 10;

    # Set memcache key for highlighting the comment
    my $memkey = [ $remote->{'userid'}, "lastcomm:$remote->{'userid'}" ];
    LJ::MemCache::set( $memkey, "$userid:$dtalkid", time() + $life );

    return;
}

sub deemp {
    "<span class='de'>$_[0]</span>";
}

=head2 C<< LJ::determine_viewing_style( $args, $view, $u ) >>
Takes a hashref of get args, and the current view, and an optional user.
Returns "original", "mine", "site", or "light" as the style.
=cut

sub determine_viewing_style {
    my ( $args, $view, $u ) = @_;

    my $style = 'original';

    # incorporate any user preferences
    $style = $u->viewing_style($view) if $u;

    # incorporate any style arguments
    my %style_getargs = %{ LJ::viewing_style_opts(%$args) };
    $style = $style_getargs{'style'} if $style_getargs{'style'};

    # keep format=light for backwards compatibility -- override and
    # assume that somebody using it really wants the light version
    $style = $style_getargs{'format'} if $style_getargs{'format'};

    return $style;
}

=head2 C<< LJ::viewing_style_args( %arguments ) >>
Takes a list of viewing styles arguments from a list, makes sure they are valid values,
and returns them as a string that can be appended to the URL. Looks for "s2id", "format", "style"
=cut

sub viewing_style_args {

    #fixme - this should be modernised to take a hashref rather than a hash
    my (%args) = @_;

    %args = %{ LJ::viewing_style_opts(%args) };

    my @valid_args;
    while ( my ( $key, $value ) = each %args ) {
        push @valid_args, "$key=$value";
    }

    return join "&", @valid_args;
}

=head2 C<< LJ::viewing_style_opts( %arguments ) >>
Takes a list of viewing styles arguments from a list, and returns a hashref of valid values
=cut

sub viewing_style_opts {

    #fixme - this should be modernised to take a hashref rather than a hash
    my (%args) = @_;
    return {} unless %args;

    my $valid_style_args = {
        style    => { light => 1, site => 1, mine => 1, original => 1 },
        format   => { light => 1 },
        fallback => { s2    => 1, bml => 1 },
    };

    my %ret;

    # only accept purely numerical s2ids
    $ret{s2id} = $args{s2id} if $args{s2id} && $args{s2id} =~ /^\d+$/;

    foreach my $key ( keys %{$valid_style_args} ) {
        $ret{$key} = $args{$key}
            if $args{$key} && $valid_style_args->{$key}->{ $args{$key} };
    }

    return \%ret;
}

=head2 C<< LJ::create_url($path,%opts) >>
If specified, path must begin with a /

args being a list of arguments to create.
opts can contain:
proto -- specify a protocol
host -- link to different domains
args -- get arguments to add
fragment -- add fragment identifier
cur_args -- hashref of current GET arguments to the page
keep_args -- arguments to keep
keep_query_string -- keep the raw query string (ignores keep_args)
no_blank -- remove keys with null values from GET args
viewing_style -- include viewing style args
=cut

sub create_url {
    my ( $path, %opts ) = @_;

    my $r        = DW::Request->get;
    my %out_args = %{ $opts{args} || {} };

    my $host = lc( $opts{host} || $r->host );
    $path ||= $r->uri;

    my $proto = $opts{proto} // $LJ::PROTOCOL;
    my $url   = $proto . "://$host$path";

    # TWO PATHS: if keep_query_string is used, we simply preserve that
    # with no further logic. If not, however, we perform arguments logic.
    my $args;
    if ( $opts{keep_query_string} ) {
        $args = $r->query_string;

    }
    else {
        my $orig_args = $opts{cur_args} || $r->get_args( preserve_case => 1 );

        # Move over viewing style arguments
        if ( $opts{viewing_style} ) {
            my $vs_args = LJ::viewing_style_opts(%$orig_args);
            foreach my $k ( keys %$vs_args ) {
                $out_args{$k} = $vs_args->{$k} unless exists $out_args{$k};
            }
        }

        $opts{keep_args} = [ keys %$orig_args ]
            if defined $opts{keep_args} and $opts{keep_args} == 1;
        $opts{keep_args} = [] if ref $opts{keep_args} ne 'ARRAY';

        # Move over arguments that we need to keep
        foreach my $k ( @{ $opts{keep_args} } ) {
            $out_args{$k} = $orig_args->{$k}
                if exists $orig_args->{$k} && !exists $out_args{$k};
        }

        foreach my $k ( keys %out_args ) {
            if ( !defined $out_args{$k} ) {
                delete $out_args{$k};
            }
            elsif ( !length $out_args{$k} ) {
                delete $out_args{$k} if $opts{no_blank};
            }
        }

        $args = LJ::encode_url_string( \%out_args, [ sort keys %out_args ] );
    }

    $url .= "?$args" if $args;
    $url .= "#" . $opts{fragment} if $opts{fragment};

    return $url;
}

# <LJFUNC>
# name: LJ::entry_form
# class: web
# des: Returns a properly formatted form for creating/editing entries.
# args: head, onload, opts
# des-head: string reference for the <head> section (JavaScript previews, etc).
# des-onload: string reference for JavaScript functions to be called on page load
# des-opts: hashref of keys/values:
#           mode: either "update" or "edit", depending on context;
#           datetime: date and time, formatted yyyy-mm-dd hh:mm;
#           remote: remote u object;
#           subject: entry subject;
#           event: entry text;
#           richtext: allow rich text formatting;
#           auth_as_remote: bool option to authenticate as remote user, pre-filling pic/friend groups/etc.
# return: form to include in BML pages.
# </LJFUNC>
sub entry_form {
    my ( $opts, $head, $onload, $errors ) = @_;

    my $out      = "";
    my $remote   = $opts->{remote};
    my $altlogin = $opts->{altlogin};
    my ( $moodlist, $moodpics );

    # usejournal has no point if you're trying to use the account you're logged in as,
    # so disregard it so we can assume that if it exists, we're trying to post to an
    # account that isn't us
    if ( $remote && $opts->{usejournal} && $remote->{user} eq $opts->{usejournal} ) {
        delete $opts->{usejournal};
    }

    # Temp fix for FF 2.0.0.17
    my $rte_is_supported = LJ::is_enabled( 'rte_support', BML::get_client_header("User-Agent") );
    $opts->{'richtext_default'} = 0 unless $rte_is_supported;

    $opts->{'richtext'} = $opts->{'richtext_default'};
    my $tabnum = 10;    #make allowance for username and password
                        # Leave gaps for interpolated fields eg date/time
    my $tabindex = sub { return ( $tabnum += 10 ) - 10; };
    $opts->{'event'} = LJ::durl( $opts->{'event'} ) if $opts->{'mode'} eq "edit";

    $out .= "\n\n<div id='entry-form-wrapper'>\n";
    $out .= LJ::error_list( $errors->{entry} ) if $errors->{entry};

    ### Icon Selection

    my $pic     = '';    # displays chosen/default pic
    my $picform = '';    # displays form drop-down

    LJ::Widget::UserpicSelector->render(
        picargs              => [ $remote, \$$head, \$pic, \$picform ],
        prop_picture_keyword => $opts->{prop_picture_keyword},
        no_auth              => !$opts->{auth_as_remote},
        onload               => $onload,
        altlogin             => $altlogin,
        entry_js             => 1
    );

    # libs for userpicselect
    LJ::Talk::init_iconbrowser_js()
        if !$altlogin && $remote && $remote->can_use_userpic_select;

    $out .= $pic;

    ### Meta Information Column 1
    {
        # do a login action to get usejournals, but only if using remote
        my $res;
        $res = LJ::Protocol::do_request(
            "login",
            {
                ver      => $LJ::PROTOCOL_VER,
                username => $remote->user,
            },
            undef,
            {
                noauth => 1,
                u      => $remote,
            }
        ) if $opts->{auth_as_remote};

        $out .= "<div id='metainfo'>\n\n";

        # login info
        $out .= $opts->{'auth'};
        if ( $opts->{'mode'} eq "update" ) {

            # communities the user can post in
            my $usejournal = $opts->{'usejournal'};
            if ($usejournal) {
                $out .= "<p id='usejournal_single' class='pkg'>\n";
                $out .=
                      "<label for='usejournal' class='left'>"
                    . BML::ml('entryform.postto')
                    . "</label>\n";
                $out .= LJ::ljuser($usejournal);
                $out .= LJ::html_hidden(
                    { name => 'usejournal', value => $usejournal, id => 'usejournal_username' } );
                $out .= LJ::html_hidden( usejournal_set => 'true' );
                $out .= "</p>\n";
            }
            elsif ( $res && ref $res->{'usejournals'} eq 'ARRAY' ) {
                my $submitprefix = BML::ml('entryform.update3');
                $out .= "<p id='usejournal_list' class='pkg'>\n";
                $out .=
                      "<label for='usejournal' class='left'>"
                    . BML::ml('entryform.postto')
                    . "</label>\n";
                $out .= LJ::html_select(
                    {
                        'name'     => 'usejournal',
                        'id'       => 'usejournal',
                        'selected' => $usejournal,
                        'tabindex' => $tabindex->(),
                        'class'    => 'select',
                        "onchange" => "changeSubmit('"
                            . $submitprefix . "','"
                            . $remote->{'user'}
                            . "'); getUserTags('$remote->{user}'); changeSecurityOptions('$remote->{user}'); XPostAccount.updateXpostFromJournal('$remote->{user}');"
                    },
                    "",
                    $remote->{'user'},
                    map { $_, $_ } @{ $res->{'usejournals'} }
                ) . "\n";
                $out .= "</p>\n";
            }
        }

        # Authentication box
        $out .= "<p class='update-errors'><?inerr $errors->{'auth'} inerr?></p>\n"
            if $errors->{'auth'};

        # Date / Time
        {
            my ( $year, $mon, $mday, $hour, $min ) = split( /\D/, $opts->{'datetime'} );
            my $monthlong = LJ::Lang::month_long($mon);

            # date entry boxes / formatting note
            my $datetime = LJ::html_datetime(
                {
                    name     => 'date_ymd',
                    notime   => 1,
                    default  => "$year-$mon-$mday",
                    tabindex => $tabindex->(),
                    disabled => $opts->{'disabled_save'}
                }
            );
            $datetime .= "<span class='float-left'>&nbsp;&nbsp;</span>";
            $datetime .= LJ::html_text(
                {
                    size      => 2,
                    class     => 'text',
                    maxlength => 2,
                    value     => $hour,
                    name      => "hour",
                    tabindex  => $tabindex->(),
                    disabled  => $opts->{'disabled_save'}
                }
            ) . "<span class='float-left'>:</span>";
            $datetime .= LJ::html_text(
                {
                    size      => 2,
                    class     => 'text',
                    maxlength => 2,
                    value     => $min,
                    name      => "min",
                    tabindex  => $tabindex->(),
                    disabled  => $opts->{'disabled_save'}
                }
            );

            # JavaScript sets this value, so we know that the time we get is correct
            # but always trust the time if we've been through the form already
            my $date_diff = ( $opts->{'mode'} eq "edit" || $opts->{'spellcheck_html'} ) ? 1 : 0;
            $datetime .= LJ::html_hidden( "date_diff", $date_diff );

            # but if we don't have JS, give a signal to trust the given time
            $datetime .= "<noscript>" . LJ::html_hidden( "date_diff_nojs", "1" ) . "</noscript>";

            $out .= "<p class='pkg'>\n";
            $out .=
                "<label for='modifydate' class='left'>" . BML::ml('entryform.date') . "</label>\n";
            $out .=
"<span id='currentdate' class='float-left'><span id='currentdate-date'>$monthlong $mday, $year, $hour"
                . ":"
                . "$min</span> <a href='javascript:void(0)' onclick='editdate();' id='currentdate-edit'>"
                . BML::ml('entryform.date.edit')
                . "</a></span>\n";
            $out .=
                  "<span id='modifydate'>$datetime <?de "
                . BML::ml('entryform.date.24hournote')
                . " de?><br />\n";
            $out .= LJ::html_check(
                {
                    'type'     => "check",
                    'id'       => "prop_opt_backdated",
                    'name'     => "prop_opt_backdated",
                    "value"    => 1,
                    'selected' => $opts->{'prop_opt_backdated'},
                    'tabindex' => $tabindex->()
                }
            );
            $out .=
                  "<label for='prop_opt_backdated' class='right'>"
                . BML::ml('entryform.backdated4')
                . "</label>\n";
            $out .= LJ::help_icon_html( "backdate", "", "" ) . "\n";
            $out .= "</span><!-- end #modifydate -->\n";
            $out .= "</p>\n";
            $out .=
                  "<noscript><p id='time-correct' class='small'>"
                . BML::ml('entryform.nojstime.note')
                . "</p></noscript>\n";
            $$onload .= " defaultDate();";
        }

        # User Picture
        {
            my $tab = $tabindex->();
            $picform =~ s/~~TABINDEX~~/$tab/;
            $out .= $picform;
        }

        $out .= "</div><!-- end #metainfo -->\n\n";

        ### Other Posting Options
        {
            $out .= "<div id='infobox'>\n";
            $out .=
                LJ::Hooks::run_hook( 'entryforminfo', $opts->{'usejournal'}, $opts->{'remote'} );
            $out .= "</div><!-- end #infobox -->\n\n";
        }

        ### Subject
        $out .= "<div id='compose-entry' class='pkg'>\n";

        $out .= "<label class='left' for='subject'>" . BML::ml('entryform.subject') . "</label>\n";
        $out .= LJ::html_text(
            {
                'name'      => 'subject',
                'value'     => $opts->{'subject'},
                'class'     => 'text',
                'id'        => 'subject',
                'size'      => '43',
                'maxlength' => '100',
                'tabindex'  => $tabindex->(),
                'disabled'  => $opts->{'disabled_save'}
            }
        ) . "\n";
        $out .= "<ul id='entry-tabs' style='display: none;'>\n";
        $out .= "<li id='jrich'>"
            . BML::ml(
            "entryform.htmlokay.rich4",
            {
                'opts' => 'href="javascript:void(0);" onclick="return useRichText(\'draft\', \''
                    . $LJ::WSTATPREFIX . '\');"'
            }
            )
            . "</li>\n"
            if $rte_is_supported;
        $out .= "<li id='jplain' class='on'>"
            . BML::ml( "entryform.plainswitch2",
            { 'aopts' => 'href="javascript:void(0);" onclick="return usePlainText(\'draft\');"' } )
            . "</li>\n";
        $out     .= "</ul>";
        $out     .= "</div><!-- end #entry -->\n\n";
        $$onload .= " showEntryTabs();";
    }

    ### Display Spell Check Results:
    $out .=
          "<div id='spellcheck-results'><strong>"
        . BML::ml('entryform.spellchecked')
        . "</strong><br />$opts->{'spellcheck_html'}</div>\n"
        if $opts->{'spellcheck_html'};

    ### Insert Object Toolbar:
    LJ::need_res(
        qw(
            js/6alib/core.js
            js/6alib/dom.js
            js/6alib/ippu.js
            js/lj_ippu.js
            )
    );
    $out .= "<div id='htmltools' class='pkg'>\n";
    $out .= "<ul class='pkg'>\n";
    $out .=
"<li class='image'><a href='javascript:void(0);' onclick='InOb.handleInsertImage();' title='"
        . BML::ml('fckland.ljimage') . "'>"
        . BML::ml('entryform.insert.image2')
        . "</a></li>\n";
    $out .=
"<li class='media'><a href='javascript:void(0);' onclick='InOb.handleInsertEmbed();' title='"
        . BML::ml('fcklang.ljvideo2') . "'>"
        . BML::ml('fcklang.ljvideo2')
        . "</a></li>\n"
        if LJ::is_enabled('embed_module');
    $out .= "</ul>\n";
    my $format_selected =
           ( $opts->{mode} eq "update" && $remote && $remote->disable_auto_formatting )
        || $opts->{'prop_opt_preformatted'}
        || $opts->{'event_format'} ? "checked='checked'" : "";
    $out .=
"<span id='linebreaks'><input type='checkbox' class='check' value='preformatted' name='event_format' id='event_format' $format_selected  />
            <label for='event_format'>"
        . BML::ml('entryform.format3')
        . "</label>"
        . LJ::help_icon_html( "noautoformat", "", " " )
        . "</span>\n";
    $out .= "</div>\n\n";

    ### Draft Status Area
    $out .= "<div id='draft-container' class='pkg'>\n";
    $out .= LJ::html_textarea(
        {
            'name'     => 'event',
            'value'    => $opts->{'event'},
            'rows'     => '20',
            'cols'     => '50',
            'style'    => '',
            'tabindex' => $tabindex->(),
            'wrap'     => 'soft',
            'disabled' => $opts->{'disabled_save'},
            'id'       => 'draft'
        }
    ) . "\n";
    $out .= "</div><!-- end #draft-container -->\n\n";
    $out .= "<input type='text' disabled='disabled' name='draftstatus' id='draftstatus' />\n\n";
    LJ::need_res( 'stc/fck/fckeditor.js', 'js/rte.js', 'stc/display_none.css' );
    if ( !$opts->{'did_spellcheck'} ) {

        my $jnorich = LJ::ejs( LJ::deemp( BML::ml('entryform.htmlokay.norich2') ) );

        $out .= <<RTE;
        <script language='JavaScript' type='text/javascript'>
            <!--

        // Check if this browser supports FCKeditor
        var rte = new FCKeditor();
        var t = rte._IsCompatibleBrowser();
        if (t) {
RTE

        my @sites = DW::External::Site->get_sites;
        my @sitevalues;
        foreach my $site ( sort { $a->{sitename} cmp $b->{sitename} } @sites ) {
            push @sitevalues, { domain => $site->{domain}, sitename => $site->{sitename} };
        }

        $out .= "var FCKLang;\n";
        $out .= "if (!FCKLang) FCKLang = {};\n";
        $out .= "FCKLang.UserPrompt = \"" . LJ::ejs( BML::ml('fcklang.userprompt') ) . "\";\n";
        $out .= "FCKLang.UserPrompt_User = \""
            . LJ::ejs( BML::ml('fcklang.userprompt.user') ) . "\";\n";
        $out .= "FCKLang.UserPrompt_Site = \""
            . LJ::ejs( BML::ml('fcklang.userprompt.site') ) . "\";\n";
        $out .= "FCKLang.UserPrompt_SiteList =" . to_json( \@sitevalues ) . ";\n";
        $out .= "FCKLang.InvalidChars = \"" . LJ::ejs( BML::ml('fcklang.invalidchars') ) . "\";\n";
        $out .= "FCKLang.LJUser = \"" . LJ::ejs( BML::ml('fcklang.ljuser') ) . "\";\n";
        $out .= "FCKLang.LJVideo = \"" . LJ::ejs( BML::ml('fcklang.ljvideo2') ) . "\";\n";
        $out .=
            "FCKLang.EmbedContents = \"" . LJ::ejs( BML::ml('fcklang.embedcontents') ) . "\";\n";
        $out .= "FCKLang.EmbedPrompt = \"" . LJ::ejs( BML::ml('fcklang.embedprompt') ) . "\";\n";
        $out .= "FCKLang.CutPrompt = \"" . LJ::ejs( BML::ml('fcklang.cutprompt') ) . "\";\n";
        $out .= "FCKLang.ReadMore = \"" . LJ::ejs( BML::ml('fcklang.readmore') ) . "\";\n";
        $out .= "FCKLang.CutContents = \"" . LJ::ejs( BML::ml('fcklang.cutcontents') ) . "\";\n";
        $out .= "FCKLang.LJCut = \"" . LJ::ejs( BML::ml('fcklang.ljcut') ) . "\";\n";

        if ( $opts->{'richtext_default'} ) {
            $$onload .= 'useRichText("draft", "' . LJ::ejs($LJ::WSTATPREFIX) . '");';
        }

        {
            my $jrich = LJ::ejs(
                LJ::deemp(
                    BML::ml(
                        "entryform.htmlokay.rich2",
                        {
                            'opts' =>
'href="javascript:void(0);" onclick="return useRichText(\'draft\', \''
                                . LJ::ejs($LJ::WSTATPREFIX) . '\');"'
                        }
                    )
                )
            );

            my $jplain = LJ::ejs(
                LJ::deemp(
                    BML::ml(
                        "entryform.plainswitch",
                        {
                            'aopts' =>
'href="javascript:void(0);" onclick="return usePlainText(\'draft\');"'
                        }
                    )
                )
            );
        }

        $out .= <<RTE;
        } else {
            document.getElementById('entry-tabs').style.visibility = 'hidden';
            document.getElementById('htmltools').style.display = 'block';
            document.write("$jnorich");
            usePlainText('draft');
        }
        //-->
            </script>
RTE

        $out .=
            '<noscript><?de ' . BML::ml('entryform.htmlokay.norich2') . ' de?><br /></noscript>';
    }
    $out .= LJ::html_hidden( { name => 'switched_rte_on', id => 'switched_rte_on', value => '0' } );

    $out .= "<div id='options' class='pkg'>";
    if ( !$opts->{'disabled_save'} ) {
        ### Options

        # Tag labeling
        if ( LJ::is_enabled('tags') ) {
            $out .= "<p class='pkg'>";
            $out .=
                  "<label for='prop_taglist' class='left options'>"
                . BML::ml('entryform.tags')
                . "</label>";
            $out .= LJ::html_text(
                {
                    'name'     => 'prop_taglist',
                    'id'       => 'prop_taglist',
                    'class'    => 'text',
                    'size'     => '35',
                    'value'    => $opts->{'prop_taglist'},
                    'tabindex' => $tabindex->(),
                    'raw'      => "autocomplete='off'",
                }
            );
            $out .= LJ::help_icon_html('addtags');
            $out .= "</p>";
        }

        $out .= "<p class='pkg'>\n";
        $out .= "<span id='prop_mood_wrapper' class='inputgroup-left'>\n";
        $out .=
              "<label for='prop_current_moodid' class='left options'>"
            . BML::ml('entryform.mood')
            . "</label>";

        # Current Mood
        {
            my @moodlist = ( '', BML::ml('entryform.mood.noneother') );
            my $sel;

            my $moods = DW::Mood->get_moods;

            foreach ( sort { $moods->{$a}->{'name'} cmp $moods->{$b}->{'name'} } keys %$moods ) {
                push @moodlist, ( $_, $moods->{$_}->{'name'} );

                if (   $opts->{prop_current_mood}
                    && $opts->{prop_current_mood} eq $moods->{$_}->{name}
                    || $opts->{prop_current_moodid} && $opts->{prop_current_moodid} == $_ )
                {
                    $sel = $_;
                }
            }

            if ($remote) {
                my $r_theme = DW::Mood->new( $remote->{'moodthemeid'} );
                foreach my $mood ( keys %$moods ) {
                    my $moodid = $moods->{$mood}->{id};
                    if ( $r_theme && $r_theme->get_picture( $moodid, \my %pic ) ) {
                        $moodlist .= "    moods[" . $moodid;
                        $moodlist .= "] = \"";
                        $moodlist .= $moods->{$mood}->{name} . "\";\n";
                        $moodpics .= "    moodpics[" . $moodid;
                        $moodpics .= "] = \"";
                        $moodpics .= $pic{pic} . "\";\n";
                    }
                }
                $$onload .= " mood_preview();";
                $$head   .= <<MOODS;
<script type="text/javascript" language="JavaScript"><!--
if (document.getElementById) {
    var moodpics = new Array();
    $moodpics
    var moods    = new Array();
    $moodlist
}
//--></script>
MOODS
            }
            my $moodpreviewoc;
            $moodpreviewoc = 'mood_preview()' if $remote;
            $out .= LJ::html_select(
                {
                    'name'     => 'prop_current_moodid',
                    'id'       => 'prop_current_moodid',
                    'selected' => $sel,
                    'onchange' => $moodpreviewoc,
                    'class'    => 'select',
                    'tabindex' => $tabindex->()
                },
                @moodlist
            );
            $out .= " "
                . LJ::html_text(
                {
                    'name'      => 'prop_current_mood',
                    'id'        => 'prop_current_mood',
                    'class'     => 'text',
                    'value'     => $opts->{'prop_current_mood'},
                    'onchange'  => $moodpreviewoc,
                    'size'      => '15',
                    'maxlength' => '30',
                    'tabindex'  => $tabindex->()
                }
                );
        }
        $out .= "<span id='mood_preview'></span>";
        $out .= "</span>\n";
        $out .= "<span class='inputgroup-right'>\n";
        $out .=
              "<label for='comment_settings' class='left options'>"
            . BML::ml('entryform.comment.settings2')
            . "</label>\n";

        # Comment Settings
        my $comment_settings_selected = sub {
            return "noemail" if $opts->{'prop_opt_noemail'};
            return "nocomments"
                if $opts->{prop_opt_nocomments} || $opts->{prop_opt_nocomments_maintainer};
            return $opts->{'comment_settings'};
        };

        my $comment_settings_journaldefault = sub {
            return "Disabled"
                if $opts->{prop_opt_default_nocomments}
                && $opts->{prop_opt_default_nocomments} eq 'N';
            return "No Email"
                if $opts->{prop_opt_default_noemail} && $opts->{prop_opt_default_noemail} eq 'N';
            return "Enabled";
        };

        my $nocomments_display =
            $opts->{prop_opt_nocomments_maintainer}
            ? 'entryform.comment.settings.nocomments.admin'
            : 'entryform.comment.settings.nocomments';

        my $comment_settings_default = BML::ml( 'entryform.comment.settings.default5',
            { 'aopts' => $comment_settings_journaldefault->() } );
        $out .= LJ::html_select(
            {
                'name'     => "comment_settings",
                'id'       => 'comment_settings',
                'class'    => 'select',
                'selected' => $comment_settings_selected->(),
                'tabindex' => $tabindex->()
            },
            "",
            $comment_settings_default,
            "nocomments",
            BML::ml( $nocomments_display, "noemail" ),
            "noemail",
            BML::ml('entryform.comment.settings.noemail')
        );
        $out .= LJ::help_icon_html( "comment", "", " " );
        $out .= "\n";
        $out .= "</span>\n";
        $out .= "</p>\n";

        # Current Location
        $out .= "<p class='pkg'>";
        if ( LJ::is_enabled('web_current_location') ) {
            $out .= "<span class='inputgroup-left'>";
            $out .=
                  "<label for='prop_current_location' class='left options'>"
                . BML::ml('entryform.location')
                . "</label>";
            $out .= LJ::html_text(
                {
                    name      => 'prop_current_location',
                    value     => $opts->{prop_current_location},
                    id        => 'prop_current_location',
                    class     => 'text',
                    size      => '35',
                    maxlength => LJ::std_max_length(),
                    tabindex  => $tabindex->()
                }
            ) . "\n";
            $out .= "</span>";
        }

        # Comment Screening settings
        $out .= "<span class='inputgroup-right'>\n";
        $out .=
              "<label for='prop_opt_screening' class='left options'>"
            . BML::ml('entryform.comment.screening2')
            . "</label>\n";
        my $opt_default_screen = $opts->{prop_opt_default_screening} || '';
        my $screening_levels_default =
              $opt_default_screen eq 'N' ? BML::ml('label.screening.none2')
            : $opt_default_screen eq 'R' ? BML::ml('label.screening.anonymous2')
            : $opt_default_screen eq 'F' ? BML::ml('label.screening.nonfriends2')
            : $opt_default_screen eq 'A' ? BML::ml('label.screening.all2')
            :                              BML::ml('label.screening.none2');
        my @levels = (
            '',  BML::ml( 'label.screening.default4', { 'aopts' => $screening_levels_default } ),
            'N', BML::ml('label.screening.none2'),
            'R', BML::ml('label.screening.anonymous2'),
            'F', BML::ml('label.screening.nonfriends2'),
            'A', BML::ml('label.screening.all2')
        );
        $out .= LJ::html_select(
            {
                'name'     => 'prop_opt_screening',
                'id'       => 'prop_opt_screening',
                'class'    => 'select',
                'selected' => $opts->{'prop_opt_screening'},
                'tabindex' => $tabindex->()
            },
            @levels
        );
        $out .= LJ::help_icon_html( "screening", "", " " );
        $out .= "</span>\n";
        $out .= "</p>\n";

        # Current Music
        $out .= "<p class='pkg'>\n";
        $out .= "<span class='inputgroup-left'>\n";
        $out .=
              "<label for='prop_current_music' class='left options'>"
            . BML::ml('entryform.music')
            . "</label>\n";

        # BML::ml('entryform.music')
        $out .= LJ::html_text(
            {
                name      => 'prop_current_music',
                value     => $opts->{prop_current_music},
                id        => 'prop_current_music',
                class     => 'text',
                size      => '35',
                maxlength => LJ::std_max_length(),
                tabindex  => $tabindex->()
            }
        ) . "\n";
        $out .= "</span>\n";
        $out .= "<span class='inputgroup-right'>";

        # Content Flag
        if ( LJ::is_enabled('adult_content') ) {
            my @adult_content_menu = (
                ""       => BML::ml('entryform.adultcontent.default'),
                none     => BML::ml('entryform.adultcontent.none'),
                concepts => BML::ml('entryform.adultcontent.concepts'),
                explicit => BML::ml('entryform.adultcontent.explicit'),
            );

            $out .=
                  "<label for='prop_adult_content' class='left options'>"
                . BML::ml('entryform.adultcontent')
                . "</label>\n";
            $out .= LJ::html_select(
                {
                    name     => 'prop_adult_content',
                    id       => 'prop_adult_content',
                    class    => 'select',
                    selected => $opts->{prop_adult_content} || "",
                    tabindex => $tabindex->(),
                },
                @adult_content_menu
            );
            $out .= LJ::help_icon_html( "adult_content", "", " " );
        }
        $out .= "</span>\n";
        $out .= "</p>\n";

        if ( LJ::is_enabled('adult_content') ) {
            $out .= "<p class='pkg'>";
            $out .=
                  "<label for='prop_adult_content_reason' class='left options'>"
                . BML::ml('entryform.adultcontentreason')
                . "</label>";
            $out .= LJ::html_text(
                {
                    'name'      => 'prop_adult_content_reason',
                    'id'        => 'prop_adult_content_reason',
                    'class'     => 'text',
                    'size'      => '35',
                    'maxlength' => '255',
                    'value'     => $opts->{'prop_adult_content_reason'},
                    'tabindex'  => $tabindex->(),
                }
            );
            $out .= LJ::help_icon_html('adult_content_reason');
            $out .= "</p>";
        }

        if ( $remote && !$altlogin ) {

            # crosspost
            my @accounts = DW::External::Account->get_external_accounts($remote);

            # populate the per-account html first, so that we only have to
            # go through them once.
            my $accthtml       = "";
            my $xpostbydefault = 0;
            my $xpost_tabindex = $tabindex->();
            my $did_spellcheck = $opts->{spellcheck_html} ? 1 : 0;
            if ( scalar @accounts ) {
                my $xpoststring    = $opts->{prop_xpost};
                my $xpost_selected = DW::External::Account->xpost_string_to_hash($xpoststring);
                foreach my $acct (@accounts) {

                    # print the checkbox for each account
                    my $acctid   = $acct->acctid;
                    my $acctname = $acct->displayname;
                    my $selected;
                    if ( $opts->{mode} eq 'edit' ) {
                        $selected = $xpost_selected->{ $acct->acctid } ? "1" : "0";
                    }
                    elsif ($did_spellcheck) {
                        $selected = $opts->{"prop_xpost_$acctid"};
                    }
                    else {
                        $selected = $acct->xpostbydefault;
                    }
                    $accthtml .=
"<tr><td><label for='prop_xpost_$acctid' class='left options'>$acctname</label></td>\n";
                    $accthtml .= "<td>"
                        . LJ::html_check(
                        {
                            'type'     => 'checkbox',
                            'name'     => "prop_xpost_$acctid",
                            'id'       => "prop_xpost_$acctid",
                            'class'    => 'check xpost_acct_checkbox',
                            'value'    => '1',
                            'selected' => $selected,
                            'tabindex' => $tabindex->(),
                            'onchange' => 'XPostAccount.xpostAcctUpdated();',
                        }
                        ) . "</td>\n";
                    $xpostbydefault = 1 if $selected;

                    $accthtml .= "<td>";
                    unless ( $acct->password ) {

                        # password field if no password
                        $accthtml .= "<span id='prop_xpost_pwspan_$acctid'>";
                        $accthtml .=
                              "<label for='prop_xpost_password_$acctid'>"
                            . BML::ml('xpost.password')
                            . "</label>";
                        $accthtml .= LJ::html_text(
                            {
                                'name'      => "prop_xpost_password_$acctid",
                                'id'        => "prop_xpost_password_$acctid",
                                'value'     => "",
                                'disabled'  => 0,
                                'size'      => 40,
                                'maxlength' => 80,
                                'type'      => 'password',
                                'class'     => 'xpost_pw'
                            }
                        );
                        $accthtml .=
                            "<span class='xpost_pwstatus' id='prop_xpost_pwstatus_$acctid'></span>";
                        $accthtml .=
"<input type='hidden' name='prop_xpost_chal_$acctid' id='prop_xpost_chal_$acctid' class='xpost_chal' />";
                        $accthtml .=
"<input type='hidden' name='prop_xpost_resp_$acctid' id='prop_xpost_resp_$acctid'/>";
                        $accthtml .= "</span>";
                    }
                    $accthtml .= "</td>\n";

                    $accthtml .= "</tr>\n";
                }
            }
            $out .= qq [
                    <script type="text/javascript" language="JavaScript">
                      // xpost messages
                      var xpostUser = '$remote->{user}';
                ];
            $out .= "var xpostCheckingMessage = '" . BML::ml('xpost.nopw.checking') . "';\n";
            $out .= "var xpostCancelLabel =  '" . BML::ml('xpost.nopw.cancel') . "';\n";
            $out .= "var xpostPwRequired = '" . BML::ml('xpost.nopw.required') . "';\n";
            $out .= "</script>\n";
            $out .= "<div id='xpostdiv'>\n";
            $out .=
                  "<p><label for='prop_xpost_check' class='left options'>"
                . BML::ml('entryform.xpost')
                . "</label>";
            $out .= LJ::html_check(
                {
                    'type'     => 'checkbox',
                    'name'     => 'prop_xpost_check',
                    'id'       => 'prop_xpost_check',
                    'class'    => 'check',
                    'value'    => '1',
                    'selected' => $xpostbydefault,
                    'disabled' => ( scalar @accounts ) ? '0' : '1',
                    'tabindex' => $xpost_tabindex,
                    'onchange' => 'XPostAccount.xpostButtonUpdated();',
                }
            );
            $out .= LJ::help_icon_html('prop_xpost_check');
            $out .= "<a href = '/manage/settings/?cat=othersites'>"
                . BML::ml('entryform.xpost.manage') . "</a>";
            $out .= "</p>\n<table summary=''>";
            $out .= $accthtml;
            $out .= "</table>\n";

            $out .= "</div>\n";
            $out .= qq [
              <p class='pkg'>
              <span class='inputgroup-left'></span>
                       ];
        }

        ### Other Posting Options
        $out .=
            LJ::Hooks::run_hook( 'add_extra_entryform_fields',
            { opts => $opts, tabindex => $tabindex } )
            || '';

        $out .= "<span class='inputgroup-right'>";

        # extra submit button so make sure it posts the form when person presses enter key
        if ( $opts->{'mode'} eq "edit" ) {
            $out .= "<input type='submit' name='action:save' class='hidden_submit xpost_submit' />";
        }
        if ( $opts->{'mode'} eq "update" ) {
            $out .=
                "<input type='submit' name='action:update' class='hidden_submit xpost_submit' />";
        }

        # submit_value field to emulate the submit button selected if we
        # have to submit with javascript
        $out .= "<input type='hidden' name='submit_value' />";

        my $preview;
        $preview =
              "<input type='button' value='"
            . BML::ml('entryform.preview')
            . "' onclick='entryPreview(this.form)' tabindex='"
            . $tabindex->() . "' />";
        if ( !$opts->{'disabled_save'} ) {
            $out .= <<PREVIEW;
<script type="text/javascript" language="JavaScript">
<!--
if (document.getElementById) {
    document.write("$preview ");
}
//-->
</script>
PREVIEW
        }
        if ( $LJ::SPELLER && !$opts->{'disabled_save'} ) {
            $out .= LJ::html_submit(
                'action:spellcheck',
                BML::ml('entryform.spellcheck'),
                { onclick => 'XPostAccount.doSpellcheck()', tabindex => $tabindex->() }
            ) . "&nbsp;";
        }

        # Update posting date/time
        $out .=
              "<input type='button' value='"
            . BML::ml('entryform.updatedate')
            . "' onclick='settime(\""
            . LJ::ejs( BML::ml('entryform.dateupdated') )
            . "\", this);' tabindex='"
            . $tabindex->() . "' />";
        $out .= "</span>\n";
        $out .= "</p>\n";
    }

    ### Community maintainer bar

    if ( $opts->{'maintainer_mode'} ) {
        $out .= "<p class='pkg'>\n";
        $out .= "<em>" . BML::ml('entryform.maintainer') . "</em>\n";
        $out .= "</p>\n";

        # adult content settings
        if ( LJ::is_enabled('adult_content') ) {
            $out .= "<p class='pkg'>\n";
            my %poster_adult_content_menu = (
                ""       => BML::ml('entryform.adultcontent.default'),
                none     => BML::ml('entryform.adultcontent.none'),
                concepts => BML::ml('entryform.adultcontent.concepts'),
                explicit => BML::ml('entryform.adultcontent.explicit'),
            );

            my @adult_content_menu = (
                "" => BML::ml(
                    'entryform.adultcontent.poster',
                    { setting => $poster_adult_content_menu{ $opts->{prop_adult_content} } }
                ),
                none     => BML::ml('entryform.adultcontent.none'),
                concepts => BML::ml('entryform.adultcontent.concepts'),
                explicit => BML::ml('entryform.adultcontent.explicit'),
            );

            $out .=
                  "<label for='prop_adult_content_maintainer' class='left options'>"
                . BML::ml('entryform.adultcontent.maintainer')
                . "</label>\n";
            $out .= LJ::html_select(
                {
                    name     => 'prop_adult_content_maintainer',
                    id       => 'prop_adult_content_maintainer',
                    class    => 'select',
                    selected => $opts->{prop_adult_content_maintainer} || "",
                    tabindex => $tabindex->(),
                },
                @adult_content_menu
            );
            $out .= LJ::help_icon_html( "adult_content", "", " " );
            $out .= "</p>\n";

            $out .= "<p class='pkg'>";
            $out .=
                  "<label for='prop_adult_content_maintainer_reason' class='left options'>"
                . BML::ml('entryform.adultcontentreason.maintainer')
                . "</label>";
            $out .= LJ::html_text(
                {
                    'name'      => 'prop_adult_content_maintainer_reason',
                    'id'        => 'prop_adult_content_maintainer_reason',
                    'class'     => 'text',
                    'size'      => '35',
                    'maxlength' => '255',
                    'value'     => $opts->{'prop_adult_content_maintainer_reason'},
                    'tabindex'  => $tabindex->(),
                }
            );
            $out .= LJ::help_icon_html('adult_content_reason');
            $out .= "</p>";
        }

        # comment disabling/enabling
        # only possible if comments weren't disabled by poster
        unless ( $opts->{prop_opt_nocomments} ) {
            $out .= "<p class='pkg'>";
            $out .=
                  "<label for='prop_opt_nocomments_maintainer' class='left options'>"
                . BML::ml('entryform.comment.disable')
                . "</label>";

            # comment disabling is done via a checkbox as it has only two settings
            # if we got this far, this is always set to the maintainer setting
            my $selected = $opts->{prop_opt_nocomments_maintainer};
            $out .= LJ::html_check(
                {
                    type     => 'checkbox',
                    name     => "prop_opt_nocomments_maintainer",
                    id       => "prop_opt_nocomments_maintainer",
                    class    => 'check',
                    value    => '1',
                    selected => $selected,
                    tabindex => $tabindex->(),
                }
            );
            $out .= "</p>";
        }
    }

    $out .= "</div><!-- end #options -->\n\n";

    ### Submit Bar
    {
        $out .= "<div id='submitbar' class='pkg'>\n\n";

        # Security
        my $secbar = 0;
        if ( $opts->{'mode'} eq "update" || !$opts->{'disabled_save'} ) {
            my $usejournalu = LJ::load_user( $opts->{usejournal} );
            my $is_comm     = $usejournalu && $usejournalu->is_comm ? 1 : 0;

            my $string_public       = LJ::ejs( BML::ml('label.security.public2') );
            my $string_friends      = LJ::ejs( BML::ml('label.security.accesslist') );
            my $string_friends_comm = LJ::ejs( BML::ml('label.security.members') );
            my $string_private      = LJ::ejs( BML::ml('label.security.private2') );
            my $string_admin        = LJ::ejs( BML::ml('label.security.maintainers') );
            my $string_custom       = LJ::ejs( BML::ml('label.security.custom') );

            $out .= qq{
                    <script>var UpdateFormStrings = new Object();
                    UpdateFormStrings.public = "$string_public";
                    UpdateFormStrings.friends = "$string_friends";
                    UpdateFormStrings.friends_comm = "$string_friends_comm";
                    UpdateFormStrings.private = "$string_private";
                    UpdateFormStrings.custom = "$string_custom";
                    UpdateFormStrings.admin = "$string_admin";</script>
                };

            $$onload .= " setColumns();" if $remote;
            my @secs = (
                "public", $string_public, "friends",
                $is_comm ? $string_friends_comm : $string_friends
            );
            push @secs, ( "private", $string_private ) unless $is_comm;
            push @secs, ( "private", $string_admin )
                if $is_comm && $remote && $remote->can_manage($usejournalu);

            my ( @secopts, @trust_groups );
            @trust_groups = $remote->trust_groups if $remote;
            if ( scalar @trust_groups && !$is_comm ) {
                push @secs, ( "custom", $string_custom );
                push @secopts, ( "onchange" => "customboxes()" );
            }

            if (@secs) {
                $secbar = 1;
                $out .= "<div id='security_container'>\n";
                $out .= "<label for='security'>" . BML::ml('entryform.security2') . " </label>\n";
            }

            $out .= LJ::html_select(
                {
                    'id'          => "security",
                    'name'        => 'security',
                    'include_ids' => 1,
                    'class'       => 'select',
                    'selected'    => $opts->{'security'},
                    'tabindex'    => $tabindex->(),
                    @secopts
                },
                @secs
            ) . "\n";

            # if custom security groups available, show them in a hideable div
            if ( scalar @trust_groups ) {
                my $display = $opts->{security} && $opts->{security} eq "custom" ? "block" : "none";
                $out .= LJ::help_icon( "security", "<span id='security-help'>\n", "\n</span>\n" );
                $out .= "<div id='custom_boxes' class='pkg' style='display: $display;'>\n";
                $out .= "<ul id='custom_boxes_list'>";
                foreach my $group (@trust_groups) {
                    my $fg = $group->{groupnum};
                    $out .= "<li>";
                    $out .= LJ::html_check(
                        {
                            'name'     => "custom_bit_$fg",
                            'id'       => "custom_bit_$fg",
                            'selected' => $opts->{"custom_bit_$fg"}
                                || ( $opts->{security_mask} ? $opts->{security_mask} + 0 : 0 ) &
                                1 << $fg
                        }
                    ) . " ";
                    $out .=
                          "<label for='custom_bit_$fg'>"
                        . LJ::ehtml( $group->{groupname} )
                        . "</label>\n";
                    $out .= "</li>";
                }
                $out .= "</ul>";
                $out .= "</div><!-- end #custom_boxes -->\n";
            }
        }

        if ( $opts->{'mode'} eq "update" ) {
            my $onclick = "";

            my $defaultjournal;
            my $not_a_journal = 0;
            if ( $opts->{'usejournal'} ) {
                $defaultjournal = $opts->{'usejournal'};
            }
            elsif ( $remote && $opts->{auth_as_remote} ) {
                $defaultjournal = $remote->user;
            }
            else {
                $defaultjournal = "Journal";
                $not_a_journal  = 1;
            }

            $$onload .= " changeSubmit('" . BML::ml('entryform.update3') . "', '$defaultjournal');";
            $$onload .= " getUserTags('$defaultjournal');" unless $not_a_journal;
            $$onload .= " changeSecurityOptions('$defaultjournal');" unless $opts->{'security'};

            $out .= LJ::html_submit(
                'action:update',
                BML::ml('entryform.update4'),
                {
                    'onclick'  => $onclick,
                    'class'    => 'update_submit xpost_submit',
                    'id'       => 'formsubmit',
                    'tabindex' => $tabindex->()
                }
            ) . "&nbsp;\n";
        }

        if ( $opts->{'mode'} eq "edit" ) {
            my $onclick = "";

            if ( !$opts->{'disabled_save'} ) {
                $out .= LJ::html_submit(
                    'action:save',
                    BML::ml('entryform.save'),
                    {
                        'onclick'  => $onclick,
                        'disabled' => $opts->{'disabled_save'},
                        'class'    => 'xpost_submit',
                        'tabindex' => $tabindex->()
                    }
                ) . "&nbsp;\n";
            }
            elsif ( $opts->{maintainer_mode} ) {
                $out .= LJ::html_submit(
                    'action:savemaintainer',
                    BML::ml('entryform.save.maintainer'),
                    {
                        'onclick'  => $onclick,
                        'disabled' => !$opts->{'maintainer_mode'},
                        'class'    => 'xpost_submit',
                        'tabindex' => $tabindex->()
                    }
                ) . "&nbsp;\n";
            }

            # do a double-confirm on delete if we have crossposts that
            # would also get removed
            my $delete_onclick =
                  "return XPostAccount.confirmDelete('"
                . LJ::ejs( BML::ml('entryform.delete.confirm') ) . "', '"
                . LJ::ejs( BML::ml('entryform.delete.xposts.confirm') ) . "')";
            $out .= LJ::html_submit(
                'action:delete',
                BML::ml('entryform.delete'),
                {
                    'disabled' => $opts->{'disabled_delete'},
                    'class'    => 'xpost_submit',
                    'tabindex' => $tabindex->(),
                    'onclick'  => $delete_onclick
                }
            ) . "&nbsp;\n";

            if ( !$opts->{'disabled_spamdelete'} ) {
                $out .= LJ::html_submit(
                    'action:deletespam',
                    BML::ml('entryform.deletespam'),
                    {
                        'onclick' => "return confirm('"
                            . LJ::ejs( BML::ml('entryform.deletespam.confirm') ) . "')",
                        'class'    => 'xpost_submit',
                        'tabindex' => $tabindex->()
                    }
                ) . "\n";
            }
        }

        $out .= "</div><!-- end #security_container -->\n\n" if $secbar;
        $out .= "</div><!-- end #submitbar -->\n\n";
        $out .= "</div><!-- end #entry-form-wrapper -->\n\n";

        $out .= "<script  type='text/javascript'>\n";
        $out .= "// <![CDATA[ \n ";
        $out .= "init_update_bml() \n";
        $out .= "// ]]>\n";
        $out .= "</script>\n";
    }
    return $out;
}

# <LJFUNC>
# name: LJ::entry_form_decode
# class: web
# des: Decodes an entry_form into a protocol-compatible hash.
# info: Generate form with [func[LJ::entry_form]].
# args: req, post
# des-req: protocol request hash to build.
# des-post: entry_form POST contents.
# returns: req
# </LJFUNC>
sub entry_form_decode {
    my ( $req, $POST ) = @_;

    # find security
    my $sec   = "public";
    my $amask = 0;
    if ( $POST->{'security'} eq "private" ) {
        $sec = "private";
    }
    elsif ( $POST->{'security'} eq "friends" ) {
        $sec   = "usemask";
        $amask = 1;
    }
    elsif ( $POST->{'security'} eq "custom" ) {
        $sec = "usemask";
        foreach my $bit ( 1 .. 60 ) {
            next unless $POST->{"custom_bit_$bit"};
            $amask |= ( 1 << $bit );
        }
    }
    $req->{'security'}  = $sec;
    $req->{'allowmask'} = $amask;

    # date/time
    my $date = LJ::html_datetime_decode( { 'name' => "date_ymd", }, $POST );
    my ( $year, $mon, $day ) = split( /\D/, $date );
    my ( $hour, $min ) = ( $POST->{'hour'}, $POST->{'min'} );

    # TEMP: ease golive by using older way of determining differences
    my $date_old = LJ::html_datetime_decode( { 'name' => "date_ymd_old", }, $POST );
    my ( $year_old, $mon_old, $day_old ) = split( /\D/, $date_old );
    my ( $hour_old, $min_old ) = ( $POST->{'hour_old'}, $POST->{'min_old'} );

    my $different = $POST->{'min_old'}
        && ( ( $year ne $year_old )
        || ( $mon ne $mon_old )
        || ( $day ne $day_old )
        || ( $hour ne $hour_old )
        || ( $min ne $min_old ) );

    # this value is set when the JS runs, which means that the user-provided
    # time is sync'd with their computer clock. otherwise, the JS didn't run,
    # so let's guess at their timezone.
    if ( $POST->{'date_diff'} || $POST->{'date_diff_nojs'} || $different ) {
        delete $req->{'tz'};
        $req->{'year'} = $year;
        $req->{'mon'}  = $mon;
        $req->{'day'}  = $day;
        $req->{'hour'} = $hour;
        $req->{'min'}  = $min;
    }

    # copy some things from %POST
    foreach (
        qw(subject
        prop_picture_keyword prop_current_moodid
        prop_current_mood prop_current_music
        prop_opt_screening prop_opt_noemail
        prop_opt_preformatted prop_opt_nocomments
        prop_current_location prop_current_coords
        prop_taglist )
        )
    {
        $req->{$_} = $POST->{$_};
    }

    if ( $POST->{"subject"} && ( $POST->{"subject"} eq BML::ml('entryform.subject.hint2') ) ) {
        $req->{"subject"} = "";
    }

    $req->{"prop_opt_preformatted"} ||=
          $POST->{'switched_rte_on'} ? 1
        : $POST->{event_format} && $POST->{event_format} eq "preformatted" ? 1
        :                                                                    0;
    $req->{"prop_opt_nocomments"} ||=
        $POST->{comment_settings} && $POST->{comment_settings} eq "nocomments" ? 1 : 0;
    $req->{"prop_opt_noemail"} ||=
        $POST->{comment_settings} && $POST->{comment_settings} eq "noemail" ? 1 : 0;
    $req->{'prop_opt_backdated'} = $POST->{'prop_opt_backdated'} ? 1 : 0;

    if ( LJ::is_enabled('adult_content') ) {
        $req->{prop_adult_content} = $POST->{prop_adult_content} || '';
        $req->{prop_adult_content} = ""
            unless $req->{prop_adult_content} eq "none"
            || $req->{prop_adult_content} eq "concepts"
            || $req->{prop_adult_content} eq "explicit";

        $req->{prop_adult_content_reason} = $POST->{prop_adult_content_reason} || "";
    }

    # nuke taglists that are just blank
    $req->{'prop_taglist'} = "" unless $req->{'prop_taglist'} && $req->{'prop_taglist'} =~ /\S/;

    # Convert the rich text editor output back to parsable lj tags.
    my $event = $POST->{'event'};
    if ( $POST->{'switched_rte_on'} ) {
        $req->{"prop_used_rte"} = 1;

        # We want to see if we can hit the fast path for cleaning
        # if they did nothing but add line breaks.
        my $attempt = $event;
        $attempt =~ s!<br />!\n!g;

        if ( $attempt !~ /<\w/ ) {
            $event = $attempt;

            # Make sure they actually typed something, and not just hit
            # enter a lot
            $attempt =~ s!(?:<p>(?:&nbsp;|\s)+</p>|&nbsp;)\s*?!!gm;
            $event = '' unless $attempt =~ /\S/;

            $req->{'prop_opt_preformatted'} = 0;
        }
        else {
            # Old methods, left in for compatibility during code push
            $event =~ s!<lj-cut class="ljcut">!<lj-cut>!gi;

            $event =~ s!<lj-raw class="ljraw">!<lj-raw>!gi;
        }
    }
    else {
        $req->{"prop_used_rte"} = 0;
    }

    $req->{'event'} = $event;

    ## see if an "other" mood they typed in has an equivalent moodid
    if ( $POST->{'prop_current_mood'} ) {
        if ( my $id = DW::Mood->mood_id( $POST->{'prop_current_mood'} ) ) {
            $req->{'prop_current_moodid'} = $id;
            delete $req->{'prop_current_mood'};
        }
    }

    # process site-specific options
    LJ::Hooks::run_hooks( 'decode_entry_form', $POST, $req );

    return $req;
}

{
    my %stat_cache = ();    # key -> {lastcheck, modtime}

    sub _file_modtime {
        my ( $key, $now ) = @_;
        if ( my $ci = $stat_cache{$key} ) {
            if ( $ci->{lastcheck} > $now - 10 ) {
                return $ci->{modtime};
            }
        }

        my $set = sub {
            my $mtime = shift;
            $stat_cache{$key} = { lastcheck => $now, modtime => $mtime };
            return $mtime;
        };

        my $file;

        # Prefer the compiled version, but fall back to source
        if ( defined $LJ::STATDOCS ) {
            $file = $LJ::STATDOCS . '/' . $key;
        }
        else {
            $file = LJ::resolve_file("htdocs/$key");
        }

        my $mtime = defined $file ? ( stat($file) )[9] : undef;
        return $set->($mtime);
    }
}

# INTERLUDE: What's up w/ need_res, res_includes, and set_active_resource_group?
#
#     GOOD QUESTION, WONDER-CAT. This is a system for lazy resolution and
# deduplication of required CSS/JS files. Any component can declare its own
# dependencies without caring where it's being called from, which theoretically
# makes things slightly more self-contained and maintainable.
#     Later (I think?), someone extended it to enable incremental code
# modernization. A component can declare multiple implementations of a frontend
# feature, specifying a "resource group" for each implementation. When it's
# finally time to build a page, the controller sets its preferred resource group
# and pulls in only frontend code that's compatible with that group.
#     As for how to use these, well, hmm. They follow a classic LJ design
# pattern that I like to call "launch some garbage into the void / reach
# into the void and grab some garbage." Infinite flexibility, zero handrails or
# validation. So here's the conventions I've been able to puzzle out.
#
# * `need_res`: Declares one or more JS/CSS files that need to be loaded for
# the current page. need_res([<opts hashref>], <path>, [<path>...])
#     FILE PATHS: Use paths like "js/jquery.replyforms.js" or
#         "stc/components/quick-reply.css", relative to the top-level "htdocs"
#         directory. Paths can ONLY go into the "js" (javascript) or "stc" (CSS)
#         subdirectories. Note that on a live instance of the app, "stc" also
#         has the compiled contents of the "scss" directory (minus any files
#         whose names begin with an underscore [_]).
#     PRIORITY: The opts hashref can have a "priority" key, whose value is
#         an integer. This affects the order in which files are added to the
#         page; otherwise the order is effectively random. Files with bigger
#         priority values get included first. Existing code only seems to
#         use two values: 0 (normal) and 3 (prerequisites). 0 is never
#         referenced explicitly, just defaulted to. 3 is always referenced
#         indirectly, via the $LJ::LIB_RES_PRIORITY variable.
#     RESOURCE GROUP: The opts hashref can have a "group" key, whose value
#         is a string. See "KNOWN RESOURCE GROUPS" below for more details. If
#         you don't specify a group, need_res puts CSS files in the "all" group
#         and JS files in the "default" (actually legacy) group.
#
# * `set_active_resource_group`: Sets the value of a global variable called
# $LJ::ACTIVE_RES_GROUP, which then affects the behavior of res_includes. It's
# supposed to be called pretty late in the process of building a page. You can
# check that variable to see what will _probably_ be happening, but be careful;
# there aren't a lot of assurances about how accurate it is at any given time.
#
# * `res_includes`: Builds the actual HTML tags for the JS/CSS in the current
# resource group (plus the special "all" group), and returns them as a string.
# Whatever calls res_includes is responsible for putting the result somewhere
# useful.
#     Conceptually, res_includes is only meant to be called once per page, but
# since Foundation expects JS to go at the bottom of the <body>, it's more like
# "call half of it twice," via the wrapper functions `res_includes_head`
# (for CSS and really basic inline JS that everything else relies on) and
# `res_includes_body` (JS loaded from files).
#
# KNOWN RESOURCE GROUPS: There isn't any validation on these, but these are the
# values that actually get used by something.
#     - "foundation" - newer pages.
#     - "jquery" - pages whose JS was modernized in the early '10s.
#     - "default" - legacy LJ pages that have basically never been modernized.
#     - "all" - always included, regardless of the current resource group.
#     - "fragment" - no idea, tbh. Something involving the template system.
#
# LIMITATIONS: Lazy resolution doesn't work reliably with non-"siteviews" S2
# pages (they can sometimes do JS, but not CSS), so they should know all of
# their CSS/JS files BEFORE we start rendering markup. In particular, watch out
# for this when using .tt components that call need_res.
#     Why? To lazy resolve, you need to call res_includes AFTER the inner
# content is fully built, which means building the outer frame of the page last.
# That's easy for site pages, but since journal styles can override the entire
# HTML document, there's not really any protected "outer" part of the page that
# core2 can defer.
# -NF

# optional first argument: hashref with options
# other arguments: resources to include
sub need_res {
    my %opts;
    if ( ref $_[0] eq 'HASH' ) {
        %opts = %{ shift() };
    }

    my $group = $opts{group};

    # higher priority means it comes first in the ordering
    my $priority = $opts{priority} || 0;

    foreach my $reskey (@_) {
        die "Bogus reskey $reskey" unless $reskey =~ m!^(js|stc)/!;

        # we put javascript in the 'default' group and CSS in the 'all' group
        # since we need CSS everywhere and we are switching JS groups
        my $lgroup = $group || ( $reskey =~ /^js/ ? 'default' : 'all' );
        unless ( $LJ::NEEDED_RES{"$lgroup-$reskey"}++ ) {
            $LJ::NEEDED_RES[$priority] ||= [];

            push @{ $LJ::NEEDED_RES[$priority] }, [ $lgroup, $reskey ];
        }
    }
}

sub res_includes {
    my (%opts) = @_;

    my $include_js          = !$opts{nojs};
    my $include_libs        = !$opts{nolib};
    my $include_stylesheets = !$opts{no_stylesheets};
    my $include_script_tags = !$opts{no_scripttags};
    my $include_links       = $include_stylesheets || $include_script_tags;

    # TODO: automatic dependencies from external map and/or content of files,
    # currently it's limited to dependencies on the order you call LJ::need_res();
    my $ret = "";

    # use correct root and prefixes for SSL pages
    my ( $siteroot, $imgprefix, $statprefix, $jsprefix, $wstatprefix, $iconprefix );
    $siteroot    = $LJ::SITEROOT;
    $imgprefix   = $LJ::IMGPREFIX;
    $statprefix  = $LJ::STATPREFIX;
    $jsprefix    = $LJ::JSPREFIX;
    $wstatprefix = $LJ::WSTATPREFIX;
    $iconprefix  = $LJ::USERPIC_ROOT;

    if ($include_js) {

        # find current journal
        my $r            = DW::Request->get;
        my $journal_base = '';
        my $journal      = '';
        if ($r) {
            my $journalid = $r->note('journalid');

            my $ju;
            $ju = LJ::load_userid($journalid) if $journalid;

            if ($ju) {
                $journal_base = $ju->journal_base;
                $journal      = $ju->{user};
            }
        }

        my $remote    = LJ::get_remote();
        my $hasremote = $remote ? 1 : 0;

        # ctxpopup prop
        my $ctxpopup_icons    = 1;
        my $ctxpopup_userhead = 1;
        $ctxpopup_icons    = 0 if $remote && !$remote->opt_ctxpopup_icons;
        $ctxpopup_userhead = 0 if $remote && !$remote->opt_ctxpopup_userhead;

        # poll for esn inbox updates?
        my $inbox_update_poll = LJ::is_enabled('inbox_update_poll');

        # are media embeds enabled?
        my $embeds_enabled = LJ::is_enabled('embed_module');

        # esn ajax enabled?
        my $esn_async = LJ::is_enabled('esn_ajax');

        my %site = (
            imgprefix           => "$imgprefix",
            siteroot            => "$siteroot",
            statprefix          => "$statprefix",
            iconprefix          => "$iconprefix",
            currentJournalBase  => "$journal_base",
            currentJournal      => "$journal",
            has_remote          => $hasremote,
            ctx_popup           => ( $ctxpopup_icons || $ctxpopup_userhead ),
            ctx_popup_icons     => $ctxpopup_icons,
            ctx_popup_userhead  => $ctxpopup_userhead,
            inbox_update_poll   => $inbox_update_poll,
            media_embed_enabled => $embeds_enabled,
            esn_async           => $esn_async,
            user_domain         => $LJ::USER_DOMAIN,
            cmax_comment        => LJ::CMAX_COMMENT,
        );

        my $site_params = to_json( \%site );

        # include standard JS info
        $ret .= qq {
            <script type="text/javascript">
                var Site;
                if (!Site)
                    Site = {};

                Site = Object.assign(Site, $site_params);
           </script>
        };
    }

    if ($include_links) {
        my $now = time();
        my %list;      # type -> [];
        my %oldest;    # type -> $oldest
        my %included = ();
        my $add      = sub {
            my ( $type, $what, $modtime, $order ) = @_;

            # the same file may have been included twice
            # if it was in two different groups and not JS
            # so add another check here
            return if $included{$what};
            $included{$what} = 1;

            # in the concat-res case, we don't directly append the URL w/
            # the modtime, but rather do one global max modtime at the
            # end, which is done later in the tags function.
            $modtime = '' unless defined $modtime;

            $list{$type} ||= [];
            push @{ $list{$type}[$order] ||= [] }, $what;
            $oldest{$type} ||= [];
            $oldest{$type}[$order] = $modtime
                if $modtime && $modtime > ( $oldest{$type}[$order] || 0 );
        };

 # we may not want to pull in the libraries again, say if we're pulling in elements via an ajax load
        delete $LJ::NEEDED_RES[$LJ::LIB_RES_PRIORITY] unless $include_libs;

        my $order = 0;
        foreach my $by_priority ( reverse @LJ::NEEDED_RES ) {
            next unless $by_priority;
            $order++;

            foreach my $resrow (@$by_priority) {

                # determine if this resource is part of the resource group that is active;
                # or 'default' if no group explicitly active
                my ( $group, $key ) = @$resrow;
                next
                    if $group ne 'all'
                    && ( ( defined $LJ::ACTIVE_RES_GROUP && $group ne $LJ::ACTIVE_RES_GROUP )
                    || ( !defined $LJ::ACTIVE_RES_GROUP && $group ne 'default' ) );

                my $path;
                my $mtime = _file_modtime( $key, $now );
                if ( $key =~ m!^stc/fck/! || $LJ::FORCE_WSTAT{$key} ) {
                    $path = "w$key";    # wstc/ instead of stc/
                }
                else {
                    $path = $key;
                }

                # if we want to also include a local version of this file, include that too
                if (@LJ::USE_LOCAL_RES) {
                    if ( grep { lc $_ eq lc $key } @LJ::USE_LOCAL_RES ) {
                        my $inc = $key;
                        $inc =~ s/(\w+)\.(\w+)$/$1-local.$2/;
                        LJ::need_res($inc);
                    }
                }

                if ( $path =~ m!^js/(.+)! ) {
                    $add->( "js", $1, $mtime, $order );
                }
                elsif ( $path =~ /\.css$/ && $path =~ m!^(w?)stc/(.+)! ) {
                    $add->( "${1}stccss", $2, $mtime, $order );
                }
                elsif ( $path =~ /\.js$/ && $path =~ m!^(w?)stc/(.+)! ) {
                    $add->( "${1}stcjs", $2, $mtime, $order );
                }
            }
        }

        my $tags = sub {
            my ( $type, $template ) = @_;
            for my $o ( 0 ... $order ) {
                my $list;
                my $template_order = $template;
                next unless $list = $list{$type}[$o];

                my $csep = join( ',', @$list );
                $csep .= "?v=" . $oldest{$type}[$o];
                $template_order =~ s/__+/??$csep/;
                $ret .= $template_order;
            }
        };

        if ($include_stylesheets) {
            $tags->(
                "stccss", "<link rel=\"stylesheet\" type=\"text/css\" href=\"$statprefix/___\" />\n"
            );
            $tags->(
                "wstccss",
                "<link rel=\"stylesheet\" type=\"text/css\" href=\"$wstatprefix/___\" />\n"
            );
        }

        if ($include_script_tags) {
            $tags->( "js", "<script type=\"text/javascript\" src=\"$jsprefix/___\"></script>\n" );
            $tags->(
                "stcjs", "<script type=\"text/javascript\" src=\"$statprefix/___\"></script>\n"
            );
            $tags->(
                "wstcjs", "<script type=\"text/javascript\" src=\"$wstatprefix/___\"></script>\n"
            );
        }
    }

    return $ret;
}

sub res_includes_head {
    return LJ::res_includes( no_scripttags => 1 );
}

sub res_includes_body {
    return LJ::res_includes( nojs => 1, no_stylesheets => 1 );
}

# called to set the active resource group
sub set_active_resource_group {
    $LJ::ACTIVE_RES_GROUP = $_[0];
}

# Returns HTML of a dynamic tag could given passed in data
# Requires hash-ref of tag => { url => url, value => value }
sub tag_cloud {
    my ( $tags, $opts ) = @_;

    # find sizes of tags, sorted
    my @sizes = sort { $a <=> $b } map { $tags->{$_}->{'value'} } keys %$tags;

    # remove duplicates:
    my %sizes = map { $_, 1 } @sizes;
    @sizes = sort { $a <=> $b } keys %sizes;

    my @tag_names = sort keys %$tags;

    my $percentile = sub {
        my $n     = shift;
        my $total = scalar @sizes;
        for ( my $i = 0 ; $i < $total ; $i++ ) {
            next if $n > $sizes[$i];
            return $i / $total;
        }
    };

    my $base_font_size  = 8;
    my $font_size_range = $opts->{font_size_range} || 25;
    my $ret .= "<div id='tagcloud' class='tagcloud'>";
    my %tagdata = ();
    foreach my $tag (@tag_names) {
        my $tagurl = $tags->{$tag}->{'url'};
        my $ct     = $tags->{$tag}->{'value'};
        my $pt     = int( $base_font_size + $percentile->($ct) * $font_size_range );
        $ret .= "<a ";
        $ret .= "id='taglink_$tag' " unless $opts->{ignore_ids};
        $ret .=
            "href='" . LJ::ehtml($tagurl) . "' style='font-size: ${pt}pt; text-decoration: none'>";
        $ret .= LJ::ehtml($tag) . "</a>\n";

        # build hash of tagname => final point size for refresh
        $tagdata{$tag} = $pt;
    }
    $ret .= "</div>";

    return $ret;
}

sub control_strip {
    my %opts = @_;
    my $user = delete $opts{user};

    my $journal    = LJ::load_user($user);
    my $show_strip = 1;
    $show_strip = LJ::Hooks::run_hook("show_control_strip")
        if ( LJ::Hooks::are_hooks("show_control_strip") );

    return "" unless $show_strip;

    my $remote = LJ::get_remote();

    my $r                  = DW::Request->get;
    my $passed_in_location = $opts{host} && $opts{uri} ? 1 : 0;
    my $host               = delete $opts{host} || $r->host;
    my $uri                = delete $opts{uri} || $r->uri;

    my $args;
    my $argshash = {};

    # we need to pass in location explicitly when creating a control strip using JS
    if ($passed_in_location) {
        $args = delete $opts{args};
        LJ::decode_url_string( $args, $argshash );
    }
    else {
        $args     = $r->query_string;
        $argshash = $r->get_args;
    }

    my $view    = delete $opts{view} || $r->note('view');
    my $view_is = sub { defined $view && $view eq $_[0] };

    my $baseuri = "$LJ::PROTOCOL://$host$uri";

    $baseuri .= $args ? "?$args" : "";
    my $euri        = LJ::eurl($baseuri);
    my $create_link = LJ::Hooks::run_hook( "override_create_link_on_navstrip", $journal )
        || "<a href='$LJ::SITEROOT/create'>"
        . BML::ml( 'web.controlstrip.links.create', { 'sitename' => $LJ::SITENAMESHORT } ) . "</a>";

    # Build up some common links
    my %links = (
        'login' =>
            "<a href='$LJ::SITEROOT/?returnto=$euri'>$BML::ML{'web.controlstrip.links.login'}</a>",
        'post_journal' =>
            "<a href='$LJ::SITEROOT/update'>$BML::ML{'web.controlstrip.links.post2'}</a>",
        'home' => "<a href='$LJ::SITEROOT/'>" . $BML::ML{'web.controlstrip.links.home'} . "</a>",
        'recent_comments' =>
"<a href='$LJ::SITEROOT/comments/recent'>$BML::ML{'web.controlstrip.links.recentcomments'}</a>",
        'manage_friends' =>
"<a href='$LJ::SITEROOT/manage/circle/'>$BML::ML{'web.controlstrip.links.managecircle'}</a>",
        'manage_entries' =>
"<a href='$LJ::SITEROOT/editjournal'>$BML::ML{'web.controlstrip.links.manageentries'}</a>",
        'invite_friends' =>
"<a href='$LJ::SITEROOT/manage/circle/invite'>$BML::ML{'web.controlstrip.links.invitefriends'}</a>",
        'create_account' => $create_link,
        'syndicated_list' =>
            "<a href='$LJ::SITEROOT/feeds/list'>$BML::ML{'web.controlstrip.links.popfeeds'}</a>",
        'learn_more' => LJ::Hooks::run_hook('control_strip_learnmore_link')
            || "<a href='$LJ::SITEROOT/'>$BML::ML{'web.controlstrip.links.learnmore'}</a>",
        'explore' => "<a href='$LJ::SITEROOT/explore/'>"
            . BML::ml( 'web.controlstrip.links.explore', { sitenameabbrev => $LJ::SITENAMEABBREV } )
            . "</a>",
        'confirm' =>
            "<a href='$LJ::SITEROOT/register'>$BML::ML{'web.controlstrip.links.confirm'}</a>",
    );

    if ($remote) {
        my $unread = $remote->notification_inbox->unread_count;
        $links{inbox} .= "<a href='$LJ::SITEROOT/inbox/'>$BML::ML{'web.controlstrip.links.inbox'}";
        $links{inbox} .= " ($unread)" if $unread;
        $links{inbox} .= "</a>";

        $links{settings} =
"<a href='$LJ::SITEROOT/manage/settings/'>$BML::ML{'web.controlstrip.links.settings'}</a>";
        $links{'view_friends_page'} =
              "<a href='"
            . $remote->journal_base
            . "/read'>$BML::ML{'web.controlstrip.links.viewreadingpage'}</a>";
        $links{'add_friend'} =
"<a href='$LJ::SITEROOT/circle/$journal->{user}/edit'>$BML::ML{'web.controlstrip.links.addtocircle'}</a>";
        $links{'edit_friend'} =
"<a href='$LJ::SITEROOT/circle/$journal->{user}/edit'>$BML::ML{'web.controlstrip.links.modifycircle'}</a>";
        $links{'track_user'} =
"<a href='$LJ::SITEROOT/manage/tracking/user?journal=$journal->{user}'>$BML::ML{'web.controlstrip.links.trackuser'}</a>";

        if ( $journal->is_syndicated ) {
            $links{'add_friend'} =
"<a href='$LJ::SITEROOT/circle/$journal->{user}/edit?action=subscribe'>$BML::ML{'web.controlstrip.links.addfeed'}</a>";
            $links{'remove_friend'} =
"<a href='$LJ::SITEROOT/circle/$journal->{user}/edit?action=remove'>$BML::ML{'web.controlstrip.links.removefeed'}</a>";
        }
        if ( $journal->is_community ) {
            $links{'join_community'} =
"<a href='$LJ::SITEROOT/circle/$journal->{user}/edit'>$BML::ML{'web.controlstrip.links.joincomm'}</a>"
                unless $journal->is_closed_membership;
            $links{'leave_community'} =
"<a href='$LJ::SITEROOT/circle/$journal->{user}/edit'>$BML::ML{'web.controlstrip.links.leavecomm'}</a>";
            $links{'watch_community'} =
"<a href='$LJ::SITEROOT/circle/$journal->{user}/edit?action=subscribe'>$BML::ML{'web.controlstrip.links.watchcomm'}</a>";
            $links{'unwatch_community'} =
"<a href='$LJ::SITEROOT/circle/$journal->{user}/edit'>$BML::ML{'web.controlstrip.links.removecomm'}</a>";
            $links{'post_to_community'} =
"<a href='$LJ::SITEROOT/update?usejournal=$journal->{user}'>$BML::ML{'web.controlstrip.links.postcomm'}</a>";
            $links{'edit_community_profile'} =
"<a href='$LJ::SITEROOT/manage/profile/?authas=$journal->{user}'>$BML::ML{'web.controlstrip.links.editcommprofile'}</a>";
            $links{'edit_community_invites'} =
                  "<a href='"
                . $journal->community_invite_members_url
                . "'>$BML::ML{'web.controlstrip.links.managecomminvites'}</a>";
            $links{'edit_community_members'} =
                  "<a href='"
                . $journal->community_manage_members_url
                . "'>$BML::ML{'web.controlstrip.links.editcommmembers'}</a>";
            $links{'track_community'} =
"<a href='$LJ::SITEROOT/manage/tracking/user?journal=$journal->{user}'>$BML::ML{'web.controlstrip.links.trackcomm'}</a>";
            $links{'queue'} =
                  "<a href='"
                . $journal->moderation_queue_url
                . "'>$BML::ML{'web.controlstrip.links.queue'}</a>";
        }
    }
    my $journal_display = $journal->ljuser_display;
    my %statustext      = (
        'yourjournal'            => $BML::ML{'web.controlstrip.status.yourjournal'},
        'yourfriendspage'        => $BML::ML{'web.controlstrip.status.yourreadingpage'},
        'yourfriendsfriendspage' => $BML::ML{'web.controlstrip.status.yournetworkpage'},
        'personal' => BML::ml( 'web.controlstrip.status.personal', { 'user' => $journal_display } ),
        'personalfriendspage' => BML::ml(
            'web.controlstrip.status.personalreadingpage', { 'user' => $journal_display }
        ),
        'personalfriendsfriendspage' => BML::ml(
            'web.controlstrip.status.personalnetworkpage', { 'user' => $journal_display }
        ),
        'community' =>
            BML::ml( 'web.controlstrip.status.community', { 'user' => $journal_display } ),
        'syn'   => BML::ml( 'web.controlstrip.status.syn',   { 'user' => $journal_display } ),
        'other' => BML::ml( 'web.controlstrip.status.other', { 'user' => $journal_display } ),
        'mutualtrust' =>
            BML::ml( 'web.controlstrip.status.mutualtrust', { 'user' => $journal_display } ),
        'mutualtrust_mutualwatch' => BML::ml(
            'web.controlstrip.status.mutualtrust_mutualwatch',
            { 'user' => $journal_display }
        ),
        'mutualtrust_watch' =>
            BML::ml( 'web.controlstrip.status.mutualtrust_watch', { 'user' => $journal_display } ),
        'mutualtrust_watchedby' => BML::ml(
            'web.controlstrip.status.mutualtrust_watchedby',
            { 'user' => $journal_display }
        ),
        'mutualwatch' =>
            BML::ml( 'web.controlstrip.status.mutualwatch', { 'user' => $journal_display } ),
        'trust_mutualwatch' =>
            BML::ml( 'web.controlstrip.status.trust_mutualwatch', { 'user' => $journal_display } ),
        'trust_watch' =>
            BML::ml( 'web.controlstrip.status.trust_watch', { 'user' => $journal_display } ),
        'trust_watchedby' =>
            BML::ml( 'web.controlstrip.status.trust_watchedby', { 'user' => $journal_display } ),
        'trustedby_mutualwatch' => BML::ml(
            'web.controlstrip.status.trustedby_mutualwatch',
            { 'user' => $journal_display }
        ),
        'trustedby_watch' =>
            BML::ml( 'web.controlstrip.status.trustedby_watch', { 'user' => $journal_display } ),
        'trustedby_watchedby' => BML::ml(
            'web.controlstrip.status.trustedby_watchedby', { 'user' => $journal_display }
        ),
        'maintainer' =>
            BML::ml( 'web.controlstrip.status.maintainer', { 'user' => $journal_display } ),
        'memberwatcher' =>
            BML::ml( 'web.controlstrip.status.memberwatcher', { 'user' => $journal_display } ),
        'watcher' => BML::ml( 'web.controlstrip.status.watcher', { 'user' => $journal_display } ),
        'member'  => BML::ml( 'web.controlstrip.status.member',  { 'user' => $journal_display } ),
        'trusted' => BML::ml( 'web.controlstrip.status.trusted', { 'user' => $journal_display } ),
        'watched' => BML::ml( 'web.controlstrip.status.watched', { 'user' => $journal_display } ),
        'trusted_by' =>
            BML::ml( 'web.controlstrip.status.trustedby', { 'user' => $journal_display } ),
        'watched_by' =>
            BML::ml( 'web.controlstrip.status.watchedby', { 'user' => $journal_display } ),
    );

    # Vars for controlstrip.tt
    my $template_args = {
        'view'         => $view,
        'userpic_html' => '',
        'logo_html'    => ( LJ::Hooks::run_hook( 'control_strip_logo', $remote, $journal ) || '' ),
        'show_login_form' => 0,
        'links'           => \%links,
        'statustext'      => '',

        # remote # only set if logged in
        #     .user => "plainname"
        #     .sessid => integer
        #     .display => "<span class="ljuser">..."
        #     .is_validated => bool
        #     .is_identity => bool
        'actionlinks' => [],

        # filters # only set if viewing reading or network page
        #     .all => []
        #     .selected => ""
        'viewoptions' => [],
        'search_html' => LJ::Widget::Search->render,

        # url of the rendered page, for the login/logout form to redirect back to
        'returnto' => $baseuri,
    };

    # Shortcuts for the two nested array refs that get repeatedly dereferenced later
    my $actionlinks = $template_args->{'actionlinks'};
    my $viewoptions = $template_args->{'viewoptions'};

    if ($remote) {
        my $userpic = $remote->userpic;
        $template_args->{'remote'} = {
            'sessid'       => $remote->session->id || 0,
            'user'         => $remote->user,
            'display'      => $remote->ljuser_display,
            'is_validated' => $remote->is_validated,
            'is_identity'  => $remote->is_identity,
        };
        if ($userpic) {
            my $wh = $userpic->img_fixedsize( width => 43, height => 43 );
            $template_args->{'userpic_html'} =
                  "<a href='$LJ::SITEROOT/manage/icons'><img src='"
                . $userpic->url
                . "' alt=\"$BML::ML{'web.controlstrip.userpic.alt'}\" title=\"$BML::ML{'web.controlstrip.userpic.title'}\" $wh /></a>";
        }
        else {
            my $tinted_nouserpic_img = "";

            if ( $journal->prop('stylesys') == 2 ) {
                my $ctx = $LJ::S2::CURR_CTX;
                my $custom_nav_strip =
                    S2::get_property_value( $ctx, "custom_control_strip_colors" );

                if ( $custom_nav_strip ne "off" ) {
                    my $linkcolor = S2::get_property_value( $ctx, "control_strip_linkcolor" );

                    if ( $linkcolor ne "" ) {
                        $tinted_nouserpic_img =
                            S2::Builtin::LJ::palimg_modify( $ctx, "controlstrip/nouserpic.gif",
                            [ S2::Builtin::LJ::PalItem( $ctx, 0, $linkcolor ) ] );
                    }
                }
            }
            if ( $tinted_nouserpic_img eq "" ) {
                $tinted_nouserpic_img = "$LJ::IMGPREFIX/controlstrip/nouserpic.gif";
            }
            $template_args->{'userpic_html'} =
"<a href='$LJ::SITEROOT/manage/icons'><img src='$tinted_nouserpic_img' alt=\"$BML::ML{'web.controlstrip.nouserpic.alt'}\" title=\"$BML::ML{'web.controlstrip.nouserpic.title'}\" height='43' width='43' /></a>";
        }

        if ( $remote->equals($journal) ) {
            if ( $view_is->("read") ) {
                $template_args->{'statustext'} = $statustext{'yourfriendspage'};
            }
            elsif ( $view_is->("network") ) {
                $template_args->{'statustext'} = $statustext{'yourfriendsfriendspage'};
            }
            else {
                $template_args->{'statustext'} = $statustext{'yourjournal'};
            }

            if ( $view_is->("read") || $view_is->("network") ) {
                my @filters = (
                    "all",             $BML::ML{'web.controlstrip.select.friends.all'},
                    "showpeople",      $BML::ML{'web.controlstrip.select.friends.journals'},
                    "showcommunities", $BML::ML{'web.controlstrip.select.friends.communities'},
                    "showsyndicated",  $BML::ML{'web.controlstrip.select.friends.feeds'}
                );

# content_filters returns an array of content filters this user had, sorted by sortorder
# since this is only shown if $remote->equals( $journal ) , we don't have to care whether a filter is public or not
                my @custom_filters = $journal->content_filters;

                # Making as few changes to existing behaviour
                my $default_filter = "default view";
                foreach my $f (@custom_filters) {

                    # Both 'default' and 'default view' are default filters
                    $default_filter = "default" if lc( $f->name ) eq "default";
                    push @filters, "filter:" . lc( $f->name ), $f->name;
                }

                my $selected = "all";

                # first, change the selection state to reflect any filter in use;
                # if we have no default filter or if the named filter somehow
                # fails to exist, this will effectively select nothing
                if ( $r->uri =~ /^\/read\/?(.+)?/i ) {
                    my $filter = $1 || $default_filter;
                    $selected = "filter:" . LJ::durl( lc($filter) );

                    # but don't select the filter if the query string contains filter=0
                    # (fun fact: named filter + filter=0 returns a 404 error)
                    $selected = "all" if $r->query_string && $r->query_string =~ /\bfilter=0\b/;
                }

                # next, change the selection state to reflect showtypes from getargs;
                # note this will override the implicit default filter or filter=0 selection
                # if a match is found, but not a filter explicitly named in the URL.
                # (of course you can use both! we're just competing for the
                #  state of the pop-up menu in the control strip here)
                if (   ( $r->uri eq "/read" || $r->uri eq "/network" )
                    && $r->query_string
                    && $r->query_string ne "" )
                {
                    $selected = "showpeople"      if $r->query_string =~ /\bshow=P\b/;
                    $selected = "showcommunities" if $r->query_string =~ /\bshow=C\b/;
                    $selected = "showsyndicated"  if $r->query_string =~ /\bshow=F\b/;
                }

                push( @$actionlinks, $links{'manage_friends'} );

                # Data for the reading list filter drop-down:
                $template_args->{'filters'} = {
                    'all'      => \@filters,
                    'selected' => $selected,
                };

            }
            else {
                push( @$actionlinks,
                    $links{'recent_comments'},
                    $links{'manage_entries'},
                    $links{'invite_friends'} );
            }
        }
        elsif ( $journal->is_personal || $journal->is_identity ) {
            my $trusted      = $remote->trusts($journal);
            my $trusted_by   = $journal->trusts($remote);
            my $mutual_trust = $trusted && $trusted_by ? 1 : 0;
            my $watched      = $remote->watches($journal);
            my $watched_by   = $journal->watches($remote);
            my $mutual_watch = $watched && $watched_by ? 1 : 0;

            if ( $mutual_trust && $mutual_watch ) {
                $template_args->{'statustext'} = $statustext{mutualtrust_mutualwatch};
                push( @$actionlinks, $links{edit_friend} );
            }
            elsif ( $mutual_trust && $watched ) {
                $template_args->{'statustext'} = $statustext{mutualtrust_watch};
                push( @$actionlinks, $links{edit_friend} );
            }
            elsif ( $mutual_trust && $watched_by ) {
                $template_args->{'statustext'} = $statustext{mutualtrust_watchedby};
                push( @$actionlinks, $links{edit_friend} );
            }
            elsif ( $trusted && $mutual_watch ) {
                $template_args->{'statustext'} = $statustext{trust_mutualwatch};
                push( @$actionlinks, $links{edit_friend} );
            }
            elsif ( $trusted_by && $mutual_watch ) {
                $template_args->{'statustext'} = $statustext{trustedby_mutualwatch};
                push( @$actionlinks, $links{edit_friend} );
            }
            elsif ($mutual_trust) {
                $template_args->{'statustext'} = $statustext{mutualtrust};
                push( @$actionlinks, $links{edit_friend} );
            }
            elsif ($mutual_watch) {
                $template_args->{'statustext'} = $statustext{mutualwatch};
                push( @$actionlinks, $links{edit_friend} );
            }
            elsif ( $trusted && $watched ) {
                $template_args->{'statustext'} = $statustext{trust_watch};
                push( @$actionlinks, $links{edit_friend} );
            }
            elsif ( $trusted && $watched_by ) {
                $template_args->{'statustext'} = $statustext{trust_watchedby};
                push( @$actionlinks, $links{edit_friend} );
            }
            elsif ( $trusted_by && $watched ) {
                $template_args->{'statustext'} = $statustext{trustedby_watch};
                push( @$actionlinks, $links{edit_friend} );
            }
            elsif ( $trusted_by && $watched_by ) {
                $template_args->{'statustext'} = $statustext{trustedby_watchedby};
                push( @$actionlinks, $links{add_friend} );
            }
            elsif ($trusted) {
                $template_args->{'statustext'} = $statustext{trusted};
                push( @$actionlinks, $links{edit_friend} );
            }
            elsif ($trusted_by) {
                $template_args->{'statustext'} = $statustext{trusted_by};
                push( @$actionlinks, $links{add_friend} );
            }
            elsif ($watched) {
                $template_args->{'statustext'} = $statustext{watched};
                push( @$actionlinks, $links{edit_friend} );
            }
            elsif ($watched_by) {
                $template_args->{'statustext'} = $statustext{watched_by};
                push( @$actionlinks, $links{add_friend} );
            }
            else {
                if ( $view_is->("read") ) {
                    $template_args->{'statustext'} = $statustext{'personalfriendspage'};
                }
                elsif ( $view_is->("network") ) {
                    $template_args->{'statustext'} = $statustext{'personalfriendsfriendspage'};
                }
                else {
                    $template_args->{'statustext'} = $statustext{'personal'};
                }
                push( @$actionlinks, $links{'add_friend'} );
            }
            push( @$actionlinks, $links{'track_user'} );
        }
        elsif ( $journal->is_community ) {
            my $watching          = $remote->watches($journal);
            my $memberof          = $remote->member_of($journal);
            my $haspostingaccess  = $remote->can_post_to($journal);
            my $isclosedcommunity = $journal->is_closed_membership;

            if ( $remote->can_manage_other($journal) ) {
                $template_args->{'statustext'} = "$statustext{maintainer}";
                push( @$actionlinks, $links{post_to_community} )
                    if $haspostingaccess;

                if ( $journal->prop('moderated') ) {
                    push( @$actionlinks, "$links{queue} [" . $journal->get_mod_queue_count . "]" );
                }
                else {
                    push( @$actionlinks, $links{edit_community_profile} );
                }

                push( @$actionlinks,
                    $links{edit_community_invites},
                    $links{edit_community_members} );

            }
            elsif ( $watching && $memberof ) {
                $template_args->{'statustext'} = $statustext{memberwatcher};
                push( @$actionlinks, $links{post_to_community} )
                    if $haspostingaccess;
                push( @$actionlinks, $links{leave_community} );
                push( @$actionlinks, $links{track_community} );

            }
            elsif ($watching) {
                $template_args->{'statustext'} = $statustext{watcher};
                push( @$actionlinks, $links{post_to_community} )
                    if $haspostingaccess;
                push( @$actionlinks,
                    $isclosedcommunity
                    ? "This is a closed community"
                    : $links{join_community} );
                push( @$actionlinks, $links{unwatch_community} );
                push( @$actionlinks, $links{track_community} );

            }
            elsif ($memberof) {
                $template_args->{'statustext'} = $statustext{member};
                push( @$actionlinks, $links{post_to_community} )
                    if $haspostingaccess;
                push( @$actionlinks,
                    $links{watch_community}, $links{'leave_community'},
                    $links{track_community} );

            }
            else {
                $template_args->{'statustext'} = $statustext{community};
                push( @$actionlinks, $links{post_to_community} )
                    if $haspostingaccess;
                push( @$actionlinks,
                    $isclosedcommunity
                    ? "This is a closed community"
                    : $links{join_community} );
                push( @$actionlinks, $links{watch_community}, $links{track_community} );
            }
        }
        elsif ( $journal->is_syndicated ) {
            $template_args->{'statustext'} = $statustext{syn};
            if ( $remote && !$remote->watches($journal) ) {
                push( @$actionlinks, $links{add_friend} );
            }
            elsif ( $remote && $remote->watches($journal) ) {
                push( @$actionlinks, $links{remove_friend} );
            }
            push( @$actionlinks, $links{syndicated_list} );
        }
        else {
            $template_args->{'statustext'} = $statustext{other};
        }

    }
    else {
        $template_args->{'userpic_html'} =
            LJ::Hooks::run_hook( 'control_strip_loggedout_userpic_contents', $euri ) || "";

        my $show_login_form = LJ::Hooks::run_hook( "show_control_strip_login_form", $journal );
        $show_login_form = 1 if !defined $show_login_form;

        $template_args->{'show_login_form'} = $show_login_form;

        if ( $journal->is_personal || $journal->is_identity ) {
            if ( $view_is->("read") ) {
                $template_args->{'statustext'} = $statustext{'personalfriendspage'};
            }
            elsif ( $view_is->("network") ) {
                $template_args->{'statustext'} = $statustext{'personalfriendsfriendspage'};
            }
            else {
                $template_args->{'statustext'} = $statustext{'personal'};
            }
        }
        elsif ( $journal->is_community ) {
            $template_args->{'statustext'} = $statustext{'community'};
        }
        elsif ( $journal->is_syndicated ) {
            $template_args->{'statustext'} = $statustext{'syn'};
        }
        else {
            $template_args->{'statustext'} = $statustext{'other'};
        }

        push( @$actionlinks, $links{'login'} ) unless $show_login_form;
        push( @$actionlinks, $links{'create_account'}, $links{'learn_more'} );
    }

    # search box and ?style=mine/?style=light/?style=original/?style=site options
    # determine whether style is "mine", and define new uri variable to manipulate
    # note: all expressions case-insensitive
    my $current_style = determine_viewing_style( $r->get_args, $view, $remote );

    # a quick little routine to use when cycling through the options
    # to create the style links for the nav bar
    my $make_style_link = sub {
        return LJ::ehtml(
            create_url(
                $uri,
                host     => $host,
                cur_args => $argshash,

                # change the style arg
                'args' => { 'style' => $_[0] },

                # keep any other existing arguments
                'keep_args' => 1,
            )
        );
    };

    # cycle through all possibilities, add the valid ones
    foreach my $view_type (qw( mine site light original )) {

        # only want to offer this option if user is logged in and it's not their own journal, since
        # original will take care of that
        if (    $view_type eq "mine"
            and $current_style ne $view_type
            and $remote
            and not $remote->equals($journal) )
        {
            push @$viewoptions,
                  "<a href='"
                . $make_style_link->($view_type) . "'>"
                . LJ::Lang::ml('web.controlstrip.reloadpage.mystyle2') . "</a>";
        }
        elsif (
                $view_type eq "site"
            and $current_style ne $view_type
            and defined $view
            and {
                entry => 1,
                reply => 1,
                icons => 1,
            }->{$view}
            )
        {
            push @$viewoptions,
                  "<a href='"
                . $make_style_link->($view_type) . "'>"
                . LJ::Lang::ml('web.controlstrip.reloadpage.sitestyle') . "</a>";
        }
        elsif ( $view_type eq "light" and $current_style ne $view_type ) {
            push @$viewoptions,
                  "<a href='"
                . $make_style_link->($view_type) . "'>"
                . LJ::Lang::ml('web.controlstrip.reloadpage.lightstyle2') . "</a>";
        }
        elsif ( $view_type eq "original" and $current_style ne $view_type ) {
            push @$viewoptions,
                  "<a href='"
                . $make_style_link->($view_type) . "'>"
                . LJ::Lang::ml('web.controlstrip.reloadpage.origstyle2') . "</a>";
        }
    }

    return DW::Template->template_string( 'journal/controlstrip.tt', $template_args );
}

sub control_strip_js_inject {
    my %opts = @_;
    my $user = delete $opts{user} || '';

    my $ret;

    my $r    = DW::Request->get;
    my $host = $r->host;
    my $uri  = $r->uri;
    my $args = LJ::eurl( $r->query_string ) || '';
    my $view = $r->note('view') || '';

    $ret .= qq{
<script type='text/javascript'>
jQuery(function(jQ){
    if (jQ("#lj_controlstrip").length == 0) {
        jQ.getJSON("/$user/__rpc_controlstrip?user=$user&host=$host&uri=$uri&args=$args&view=$view", {},
            function(data) {
                jQ("<div></div>").html(data.control_strip).prependTo("body");
            }
        );
    }
})
</script>};

    return $ret;
}

# For the Rich Text Editor
# Set JS variables for use by the RTE
sub rte_js_vars {
    my ($remote) = @_;

    my $ret = '';

    # The JS var canmakepoll is used by fckplugin.js to change the behaviour
    # of the poll button in the RTE.
    # Also remove any RTE buttons that have been set to disabled.
    my $canmakepoll = "true";
    $canmakepoll = "false" if ( $remote && !$remote->can_create_polls );
    $ret .= "<script type='text/javascript'>\n";
    $ret .= "    var RTEdisabled = new Array();\n";
    my $rte_disabled = $LJ::DISABLED{rte_buttons} || {};
    foreach my $key ( keys %$rte_disabled ) {
        $ret .= "    RTEdisabled['$key'] = true;" if $rte_disabled->{$key};
    }

    $ret .= qq^
        var canmakepoll = $canmakepoll;

        function removeDisabled(ToolbarSet) {
            for (var i=0; i<ToolbarSet.length; i++) {
                for (var j=0; j<ToolbarSet[i].length; j++) {
                    if (RTEdisabled[ToolbarSet[i][j]] == true) ToolbarSet[i].splice(j,1);
                }
            }
        }

        var SiteConfig = new Object();
    </script>^;

    return $ret;
}

# prints out UI for subscribing to some events
sub subscribe_interface {
    my ( $u, %opts ) = @_;

    croak "subscribe_interface wants a \$u" unless LJ::isu($u);

    my $categories    = delete $opts{categories}                     || [];
    my $journalu      = delete $opts{journal}                        || LJ::get_remote();
    my $def_notes     = delete $opts{default_selected_notifications} || [];
    my $num_per_page  = delete $opts{num_per_page}                   || 250;
    my $page          = delete $opts{page}                           || 1;
    my $settings_page = delete $opts{settings_page}                  || 0;

    croak "Invalid user object passed to subscribe_interface" unless LJ::isu($journalu);

    croak "Invalid options passed to subscribe_interface" if ( scalar keys %opts );

    my @notify_classes = LJ::NotificationMethod->all_classes;
    return "No notification methods" unless @notify_classes;

    my $page_vars = { settings_page => $settings_page };
    $page_vars->{ntypeids} = join ',', map { $_->ntypeid } @notify_classes;

    # skip the inbox type; it's always on
    @notify_classes = grep { $_ ne 'LJ::NotificationMethod::Inbox' } @notify_classes;
    $page_vars->{notify_classes} = \@notify_classes;

    my @catids;
    my $catid = 0;
    my @catdata;    # this is eventually passed to the display template
    my @prev_cats;

    my %num_subs_by_type = ();    # for the subscription_stats hook

    # pagination variables
    my $displayed_tracking_start = ( $page - 1 ) * $num_per_page;
    my $displayed_tracking_end   = $displayed_tracking_start + $num_per_page - 1;
    my $displayed_tracking_count = 0;

    foreach my $cat_hash (@$categories) {
        my ( $category, $cat_events ) = %$cat_hash;

        # is this category the tracking category?
        my $is_tracking_category = $category eq "Subscription Tracking";

        my $cat_data = { catid => $catid };
        push @catids, $catid;
        my $cat_empty = 1;

        my $cat_title_key = lc($category);
        $cat_title_key =~ s/ /-/g;
        $cat_data->{title_key} = $cat_title_key;

        # add notifytype headings
        $cat_data->{notify_headers} = [];

        foreach my $notify_class (@notify_classes) {
            my $title   = eval { $notify_class->title($u) } or next;
            my $ntypeid = $notify_class->ntypeid            or next;

            # create the checkall box for this event type.
            my $disabled = !$notify_class->configured_for_user($u);

            if ( $notify_class->disabled_url && $disabled ) {
                $title = "<a href='" . $notify_class->disabled_url . "'>$title</a>";
            }
            elsif ( $notify_class->url ) {
                $title = "<a href='" . $notify_class->url . "'>$title</a>";
            }
            $title .= " " . LJ::help_icon( $notify_class->help_url ) if $notify_class->help_url;

            push @{ $cat_data->{notify_headers} },
                { ntypeid => $ntypeid, title => $title, disabled => $disabled };
        }

        # build list of subscriptions to show the user
        $cat_data->{pending_subs} = [];

        my @pending_subscriptions =
              $is_tracking_category
            ? @$cat_events
            : $u->subscription_event_filter( $cat_events, $journalu, $settings_page );

        # inbox method
        my $special_subs     = 0;
        my $unavailable_subs = 0;
        my $sub_count        = 0;
        foreach my $pending_sub (@pending_subscriptions) {
            my $sub_data = $u->pending_sub_data($pending_sub);
            next unless $sub_data;

            $sub_data->{altrow_class}   = $sub_count % 2 == 1   ? "odd"      : "even";
            $sub_data->{inactive_class} = $sub_data->{inactive} ? "inactive" : "";
            $sub_data->{disabled_class} = $sub_data->{disabled} ? "disabled" : "";

            $sub_data->{hidden_style} = $sub_data->{hidden} ? " style='visibility: hidden;'" : "";

            if ( $sub_data->{special_sub} ) {
                $special_subs++;

                push @{ $cat_data->{pending_subs} }, $sub_data;
                $sub_count++;
                next;
            }

            $unavailable_subs++ if $sub_data->{disabled};

            my $evt_class = $pending_sub->event_class or next;
            next if !$evt_class->is_visible && $settings_page;

            # override disabled below if always_checked (can't uncheck)
            my $always_checked = eval { "$evt_class"->always_checked; };
            $sub_data->{disabled} = 1 if $always_checked;

            if ($is_tracking_category) {
                next if $u->tracked_event_exclude( $pending_sub, \@prev_cats );

                my $do_show = 1;
                $do_show = 0
                    unless $displayed_tracking_count >= $displayed_tracking_start
                    && $displayed_tracking_count <= $displayed_tracking_end;
                $displayed_tracking_count++;

                if ( $do_show && $sub_data->{subscribed} ) {
                    my $subid = $pending_sub->id;

                    $sub_data->{subid}      = $subid;
                    $sub_data->{auth_token} = $u->ajax_auth_token(
                        "/__rpc_esn_subs",
                        subid  => $subid,
                        action => 'delsub'
                    );
                }

                $sub_data->{do_show} = $do_show;
            }
            else {
                $sub_data->{do_show} = 1;

                next unless eval { $evt_class->subscription_applicable($pending_sub) };
                next
                    if $u->equals($journalu)
                    && $pending_sub->journalid
                    && $pending_sub->journalid != $u->{userid};
            }

            $cat_empty = 0;    # no more nexts

            # this hook also populates %num_subs_by_type
            $sub_data->{notif_options} = LJ::Hooks::run_hook(
                'subscription_notif_options',
                u                    => $u,
                sub_data             => $sub_data,
                pending_sub          => $pending_sub,
                def_notes            => $def_notes,
                is_tracking_category => $is_tracking_category,
                num_subs_by_type     => \%num_subs_by_type,
                notify_classes       => \@notify_classes,
            );

            push @{ $cat_data->{pending_subs} }, $sub_data;
            $sub_count++;
        }

        # show blurb if not tracking anything
        $cat_data->{empty_blurb}       = $cat_empty && $is_tracking_category;
        $cat_data->{special_subs}      = $special_subs;
        $cat_data->{show_upgrade_note} = !$u->is_paid && ( $special_subs || $unavailable_subs );

        push @catdata,   $cat_data;
        push @prev_cats, $cat_hash;

        $catid++;
    }

    $page_vars->{catdata} = \@catdata;
    $page_vars->{catids}  = join ',', @catids;

    # show how many subscriptions we have active / inactive
    $page_vars->{subscription_stats} =
        LJ::Hooks::run_hook( 'subscription_stats', $u, \%num_subs_by_type );

    $page_vars->{pagination} = LJ::paging_bar(
        $page,
        ceil( $displayed_tracking_count / $num_per_page ),
        {
            self_link => sub {
                return LJ::create_url(
                    undef,
                    args      => { page => $_[0] },
                    keep_args => 1,
                    no_blank  => 1,
                    fragment  => "category-subscription-tracking"
                );
            }
        }
    );

    return DW::Template->template_string( 'tracking/subscribe-interface.tt', $page_vars );
}

# returns a placeholder link
sub placeholder_link {
    my (%opts) = @_;

    my $placeholder_html = LJ::ejs_all( delete $opts{placeholder_html} || '' );
    my $width            = delete $opts{width} || 100;
    my $height           = delete $opts{height} || 100;
    my $width_unit       = delete $opts{width_unit} || "px";
    my $height_unit      = delete $opts{height_unit} || "px";
    my $link             = delete $opts{link} || '';
    my $url              = delete $opts{url} || '';
    my $linktext         = delete $opts{linktext} || '';
    my $img              = delete $opts{img} || "$LJ::IMGPREFIX/videoplaceholder.png";

    my $direct_link =
        defined $url
        ? '<div class="embed_link"><a href="' . $url . '">' . $linktext . '</a></div>'
        : '';
    return qq {
            <div class="LJ_Placeholder_Container" style="width: ${width}${width_unit}; height: ${height}${height_unit};">
                <div class="LJ_Placeholder_HTML" style="display: none;">$placeholder_html</div>
                <div class="LJ_Container"></div>
                <a href="$link">
                    <img src="$img" class="LJ_Placeholder" title="Click to show embedded content" />
                </a>
            </div>
            $direct_link
        };
}

# this returns the right max length for a VARCHAR(255) database
# column.  but in HTML, the maxlength is characters, not bytes, so we
# have to assume 3-byte chars and return 80 instead of 255.  (80*3 ==
# 240, approximately 255).  However, we special-case Russian where
# they often need just a little bit more, and make that 100.  because
# their bytes are only 2, so 100 * 2 == 200.  as long as russians
# don't enter, say, 100 characters of japanese... but then it'd get
# truncated or throw an error.  we'll risk that and give them 20 more
# characters.
sub std_max_length {
    my $lang = eval { BML::get_language() };
    return 80  if !$lang || $lang =~ /^en/;
    return 100 if $lang =~ /\b(hy|az|be|et|ka|ky|kk|lt|lv|mo|ru|tg|tk|uk|uz)\b/i;
    return 80;
}

sub final_head_html {
    my $ret = "";

    if ( my $pagestats_obj = LJ::PageStats->new ) {
        $ret .= $pagestats_obj->render_head;
    }

    return $ret;
}

# returns HTML which should appear before </body>
sub final_body_html {
    my $before_body_close = "";
    LJ::Hooks::run_hooks( 'insert_html_before_body_close', \$before_body_close );

    if ( my $pagestats_obj = LJ::PageStats->new ) {
        $before_body_close .= $pagestats_obj->render;
    }

    return $before_body_close;
}

# return a unique per pageview string based on the remote's unique cookie
sub pageview_unique_string {
    my $cached_uniq = $LJ::REQ_GLOBAL{pageview_unique_string};
    return $cached_uniq if $cached_uniq;

    my $uniq = LJ::UniqCookie->current_uniq . time() . LJ::rand_chars(8);
    $uniq = Digest::SHA1::sha1_hex($uniq);

    $LJ::REQ_GLOBAL{pageview_unique_string} = $uniq;
    return $uniq;
}

# returns canonical link for use in header of journal pages
sub canonical_link {
    my ( $url, $tid ) = @_;
    if ( $tid += 0 ) {    # sanitize input
        $url .= "?thread=$tid" . LJ::Talk::comment_anchor($tid);
    }
    return qq{<link rel="canonical" href="$url" />\n};

}

# Takes a string as input and returns a canonicalized slug. This is used in
# the logslugs table for URL generation.
sub canonicalize_slug {
    return undef unless defined $_[0];

    # If you change this, please update htdocs/stc/js/jquery.postform.js
    # to keep the regular expressions in the toSlug function in sync.
    my $str = LJ::trim( lc shift );
    $str =~ s/\s+/-/g;
    $str =~ s/[^a-z0-9_-]//gi;
    $str =~ s/-+/-/g;
    $str =~ s/^-|-$//g;
    $str = LJ::text_trim( $str, 255, 100 );

    return $str;
}

1;
