#!/usr/bin/perl
#

package LJ;
use strict;

# load the bread crumb hash
require "$ENV{'LJHOME'}/cgi-bin/crumbs.pl";

# <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/imageconf.pl but can be
#      overridden in cgi-bin/ljconfig.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 = shift;
    my $type = shift;  # either "" or "input"
    my $attr = shift;

    my $attrs;
    if ($attr) {
        if (ref $attr eq "HASH") {
            foreach (keys %$attr) {
                $attrs .= " $_=\"" . LJ::ehtml($attr->{$_}) . "\"";
            }
        } else {
            $attrs = " name=\"$attr\"";
        }
    }

    my $i = $LJ::Img::img{$ic};
    if ($type eq "") {
        return "<img src=\"$LJ::IMGPREFIX$i->{'src'}\" width=\"$i->{'width'}\" ".
            "height=\"$i->{'height'}\" alt=\"$i->{'alt'}\" title=\"$i->{'alt'}\" ".
            "border='0'$attrs />";
    }
    if ($type eq "input") {
        return "<input type=\"image\" src=\"$LJ::IMGPREFIX$i->{'src'}\" ".
            "width=\"$i->{'width'}\" height=\"$i->{'height'}\" title=\"$i->{'alt'}\" ".
            "alt=\"$i->{'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 yyy, 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 ($nm, $nd) = ($m+0, $d+0);   # numeric, without leading zeros
    my $user = $u->{'user'};
    my $base = LJ::journal_base($u);

    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
# arg-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->($&); !ge;
    return $str;
}


# <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
#           others - arguments to pass to LJ::get_authas_list
# </LJFUNC>
sub make_authas_select {
    my ($u, $opts) = @_; # type, authas, label, button

    my @list = LJ::get_authas_list($u, $opts);

    # only do most of form if there are options to select from
    if (@list > 1) {
        return ($opts->{'label'} || 'Work as user:') . " " . 
               LJ::html_select({ 'name' => 'authas',
                                 'selected' => $opts->{'authas'} || $u->{'user'}},
                                 map { $_, $_ } @list) . " " .
               LJ::html_submit(undef, $opts->{'button'} || 'Switch');
    }

    # no communities to choose from, give the caller a hidden
    return  LJ::html_hidden('authas', $opts->{'authas'} || $u->{'user'});
}

# <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 doc/ljconfig.pl.txt 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";
}


# <LJFUNC>
# name: LJ::bad_input
# des: Returns common BML for reporting form validation errors in
#      a bulletted list.
# returns: BML showing errors.
# args: error*
# des-error: A list of errors
# </LJFUNC>
sub bad_input
{
    # change return status from 200 to 403:
    BML::set_status(403); # 403 Forbidden

    my @errors = @_;
    my $ret = "";
    $ret .= "<?badcontent?>\n<ul>\n";
    foreach (@errors) {
        $ret .= "<li>$_</li>\n";
    }
    $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
{
    my @errors = @_;
    my $ret;
    $ret .= "<?errorbar ";
    $ret .= "<strong>";
    $ret .= BML::ml('error.procrequest');
    $ret .= "</strong><ul>";

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

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

sub tosagree_widget {
    my ($checked, $errstr) = @_;

    return 
        "<div class='formitemDesc'>" .
        BML::ml('tos.mustread', 
                { aopts => "target='_new' href='$LJ::SITEROOT".$LJ::TOSURL."'" }) . 
        "</div>" .
        "<iframe width='600' height='300' src='$LJ::TOSURLMINI' " .
        "style='border: 1px solid gray;'></iframe>" .
        "<div>" . LJ::html_check({ name => 'agree_tos', id => 'agree_tos',
                                   value => '1', selected =>  $checked }) .
        "<label for='agree_tos'>" . BML::ml('tos.haveread') . "</label></div>" .
        ($errstr ? "<?inerr $errstr inerr?>" : '');
}

sub tosagree_html {
    my $domain = shift;

    my $ret = "<?h1 $LJ::REQUIRED_TOS{title} h1?>";

    my $html_str = LJ::tosagree_str($domain => 'html');
    $ret .= "<?p $html_str p?>" if $html_str;

    $ret .= "<div style='margin-left: 40px; margin-bottom: 20px;'>";
    $ret .= LJ::tosagree_widget(@_);
    $ret .= "</div>";

    return $ret;
}

sub tosagree_str {
    my ($domain, $key) = @_;

    return ref $LJ::REQUIRED_TOS{$domain} && $LJ::REQUIRED_TOS{$domain}->{$key} ?
        $LJ::REQUIRED_TOS{$domain}->{$key} : $LJ::REQUIRED_TOS{$key};
}

# <LJFUNC>
# name: LJ::did_post
# des: When web pages 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 block a robot from indexing or following 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 { BML::self_link({ 'page' => $_[0] }) };

    my $navcrap;
    if ($pages > 1) {
        $navcrap .= "<center><font face='Arial,Helvetica' size='-1'><b>";
        $navcrap .= BML::ml('ljlib.pageofpages',{'page'=>$page, 'total'=>$pages}) . "<br />";
        my $left = "<b>&lt;&lt;</b>";
        if ($page > 1) { $left = "<a href='" . $self_link->($page-1) . "'>$left</a>"; }
        my $right = "<b>&gt;&gt;</b>";
        if ($page < $pages) { $right = "<a href='" . $self_link->($page+1) . "'>$right</a>"; }
        $navcrap .= $left . " ";
        for (my $i=1; $i<=$pages; $i++) {
            my $link = "[$i]";
            if ($i != $page) { $link = "<a href='" . $self_link->($i) . "'>$link</a>"; }
            else { $link = "<font size='+1'><b>$link</b></font>"; }
            $navcrap .= "$link ";
        }
        $navcrap .= "$right";
        $navcrap .= "</font></center>\n";
        $navcrap = BML::fill_template("standout", { 'DATA' => $navcrap });
    }
    return $navcrap;
}

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

sub set_active_crumb
{
    $LJ::ACTIVE_CRUMB = shift;
    return undef;
}

sub set_dynamic_crumb
{
    my ($title, $parent) = @_;
    $LJ::ACTIVE_CRUMB = [ $title, $parent ];
}

sub get_parent_crumb
{
    my $thiscrumb = LJ::get_crumb(LJ::get_active_crumb());
    return LJ::get_crumb($thiscrumb->[2]);
}

sub get_active_crumb
{
    return $LJ::ACTIVE_CRUMB;
}

sub get_crumb_path
{
    my $cur = LJ::get_active_crumb();
    my @list;
    while ($cur) {
        # get crumb, fix it up, and then put it on the list
        if (ref $cur) {
            # dynamic crumb
            push @list, [ $cur->[0], '', $cur->[1], 'dynamic' ];
            $cur = $cur->[1];
        } else {   
            # just a regular crumb
            my $crumb = LJ::get_crumb($cur);
            last unless $crumb;
            last if $cur eq $crumb->[2];
            $crumb->[3] = $cur;
            push @list, $crumb;

            # now get the next one we're going after
            $cur = $crumb->[2]; # parent of this crumb
        }
    }
    return @list;
}

sub get_crumb
{
    my $crumbkey = shift;
    if (defined $LJ::CRUMBS_LOCAL{$crumbkey}) {
        return $LJ::CRUMBS_LOCAL{$crumbkey};
    } else {
        return $LJ::CRUMBS{$crumbkey};
    }
}

# <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;
    return 1 if $LJ::SITEROOT   && $referer =~ m!^$LJ::SITEROOT$uri!;
    return 1 if $LJ::DOMAIN     && $referer =~ m!^http://$LJ::DOMAIN$uri!;
    return 1 if $LJ::DOMAIN_WEB && $referer =~ m!^http://$LJ::DOMAIN_WEB$uri!;
    return 1 if $uri =~ m!^http://! && $referer eq $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.
# returns: HTML hidden field to be inserted into the output of a page.
# </LJFUNC>
sub form_auth {
    my $remote = LJ::get_remote()    or return "";
    my $sess = $remote->{'_session'} or return "";
    my $auth = join('-',
                    LJ::rand_chars(10),
                    $remote->{userid},
                    $sess->{auth});
    return LJ::html_hidden("lj_form_auth", LJ::challenge_generate(86400, $auth));
}

# <LJFUNC>
# name: LJ::check_form_auth
# class: web
# des: Verifies form authentication created with 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, 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 $remote = LJ::get_remote()    or return 0;
    my $sess = $remote->{'_session'} or return 0;
    my $formauth = $BMLCodeBlock::POST{'lj_form_auth'} or return 0;

    # check the attributes are as they should be
    my $attr = LJ::get_challenge_attributes($formauth);
    my ($randchars, $userid, $sessauth) = split(/\-/, $attr);
    return 0 unless $userid == $remote->{userid} &&
        $sessauth eq $sess->{auth};

    # check the signature is good and not expired
    my $opts = { dont_check_count => 1 };  # in/out
    LJ::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 Quick Reply form
# returns: undef upon failure or HTML for the div upon success
# args: user, remote, ditemid, stylemine, userpic
# des-u: user object or userid for journal reply in
# des-ditemid: ditemid for this comment
# des-stylemine: if the user has specified style=mine for this page
# des-userpic: alternate default userpic
# </LJFUNC>
sub create_qr_div {

    my ($user, $ditemid, $stylemine, $userpic, $viewing_thread) = @_;
    my $u = LJ::want_user($user);
    my $remote = LJ::get_remote();
    return undef unless $u && $remote && $ditemid;
    return undef if $remote->underage;

    $stylemine ||= 0;
    my $qrhtml;

    LJ::load_user_props($remote, "opt_no_quickreply");
    return undef if $remote->{'opt_no_quickreply'};

    my $stylemineuri = $stylemine ? "style=mine&" : "";
    my $basepath = LJ::journal_base($u) . "/$ditemid.html?${stylemineuri}replyto=";
    $qrhtml .= LJ::html_hidden({'name' => 'replyto', 'id' => 'replyto', 'value' => ''},
                               {'name' => 'parenttalkid', 'id' => 'parenttalkid', 'value' => ''},
                               {'name' => 'itemid', 'id' => 'itemid', 'value' => $ditemid},
                               {'name' => 'usertype', 'id' => 'usertype', 'value' => 'cookieuser'},
                               {'name' => 'userpost', 'id' => 'userpost', 'value' => $remote->{'user'}},
                               {'name' => 'qr', 'id' => 'qr', 'value' => '1'},
                               {'name' => 'cookieuser', 'id' => 'cookieuser', 'value' => $remote->{'user'}},
                               {'name' => 'dtid', 'id' => 'dtid', 'value' => ''},
                               {'name' => 'basepath', 'id' => 'basepath', 'value' => $basepath},
                               {'name' => 'stylemine', 'id' => 'stylemine', 'value' => $stylemine},
                               {'name' => 'saved_subject', 'id' => 'saved_subject'},
                               {'name' => 'saved_body', 'id' => 'saved_body'},
                               {'name' => 'saved_spell', 'id' => 'saved_spell'},
                               {'name' => 'saved_upic', 'id' => 'saved_upic'},
                               {'name' => 'saved_dtid', 'id' => 'saved_dtid'},
                               {'name' => 'saved_ptid', 'id' => 'saved_ptid'},
                               {'name' => 'viewing_thread', 'id' => 'viewing_thread', 'value' => $viewing_thread},
                               );

    # 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);
        $qrhtml .= LJ::html_hidden("chrp1", "$chal-$res");
    }

    # Start making the div itself
    $qrhtml .= "<div id='qrdiv' name='qrdiv' style='display:none;'>";
    $qrhtml .= "<table style='border: 1px solid black'>";
    $qrhtml .= "<tr valign='center'>";
    $qrhtml .= "<td align='right'><b>".BML::ml('/talkpost.bml.opt.from')."</b></td><td align='left'>";
    $qrhtml .= LJ::ljuser($remote->{'user'});
    $qrhtml .= "</td><td align='center'>";

    # Userpic selector
    {
        my %res;
        LJ::do_request({ "mode" => "login",
                         "ver" => ($LJ::UNICODE ? "1" : "0"),
                         "user" => $remote->{'user'},
                         "getpickws" => 1, },
                       \%res, { "noauth" => 1, "userid" => $remote->{'userid'}}
                       );

        if ($res{'pickw_count'}) {
            $qrhtml .= BML::ml('/talkpost.bml.label.picturetouse2',
                               {
                                   'aopts'=>"href='$LJ::SITEROOT/allpics.bml?user=$remote->{'user'}'"});
            my @pics;
            for (my $i=1; $i<=$res{'pickw_count'}; $i++) {
                push @pics, $res{"pickw_$i"};
            }
            @pics = sort { lc($a) cmp lc($b) } @pics;
            $qrhtml .= LJ::html_select({'name' => 'prop_picture_keyword',
                                        'selected' => $userpic, 'id' => 'prop_picture_keyword' },
                                       ("", BML::ml('/talkpost.bml.opt.defpic'), map { ($_, $_) } @pics));

            $qrhtml .= ' ' . BML::fill_template('help', { 'DATA' => $LJ::HELPURL{'userpics'} } )
                if defined $LJ::HELPURL{'userpics'};
        }
    }

    $qrhtml .= "</td></tr>";
    
    if ($remote->openid_identity && !$remote->{'email'}) {
      $qrhtml .= "<tr><td valign='top' align='right'><strong>E-mail для ответов:</strong></td><td>";
      $qrhtml .= LJ::html_text({ 'name' => 'email', 'value' => '', 'size' => '40', 'maxlength' => '50' });
      $qrhtml .= "<font size=-1 color=red><br/>Пока вы не введете e-mail, вы не сможете получать ответы по e-mail.";
      $qrhtml .= "<br/>Внимание: на указанный e-mail будет выслано подтверждение.</font>";
      $qrhtml .= "</td><tr>";
    }

    $qrhtml .= "<tr><td align='right'>";
    $qrhtml .= "<b>".BML::ml('/talkpost.bml.opt.subject')."</b></td>";
    $qrhtml .= "<td colspan='2' align='left'>";
    $qrhtml .= "<input class='textbox' type='text' size='50' maxlength='100' name='subject' id='subject' value='' />";
    $qrhtml .= "</td></tr>";

    $qrhtml .= "<tr valign='top'>";
    $qrhtml .= "<td align='right'><b>".BML::ml('/talkpost.bml.opt.message')."</b></td>";
    $qrhtml .= "<td colspan='3' style='width: 90%'>";

    $qrhtml .= "<textarea class='textbox' rows='10' cols='50' wrap='soft' name='body' id='body' style='width: 99%'></textarea>";
    $qrhtml .= "</td></tr>";

    $qrhtml .= "<tr><td>&nbsp;</td>";
    $qrhtml .= "<td colspan='3' align='left'>";

    $qrhtml .= LJ::html_submit('submitpost', BML::ml('/talkread.bml.button.post'),
                               { "id" => "submitpost",
                                 "raw" => "onclick='if (checkLength()) {submitform();}'"
                                 });

    $qrhtml .= "&nbsp;" . LJ::html_submit('submitmoreopts', BML::ml('/talkread.bml.button.more'),
                                          { "id" => "submitmoreopts",
                                            "raw" => "onclick='if (moreopts()) {submitform();}'"
                                            });
    if ($LJ::SPELLER) {
        $qrhtml .= "&nbsp;<input type='checkbox' name='do_spellcheck' value='1' id='do_spellcheck' /> <label for='do_spellcheck'>";
        $qrhtml .= BML::ml('/talkread.bml.qr.spellcheck');
        $qrhtml .= "</label>";
    }

    LJ::load_user_props($u, 'opt_logcommentips');
    if ($u->{'opt_logcommentips'} eq 'A') {
        $qrhtml .= '<br />';
        $qrhtml .= BML::fill_template('de', { 'DATA' => BML::ml('/talkpost.bml.logyourip') } );
        $qrhtml .= ' ' . BML::fill_template('help', { 'DATA' => $LJ::HELPURL{'iplogging'} } )
            if defined $LJ::HELPURL{'iplogging'};
    }

    $qrhtml .= "</td></tr></table>";
    $qrhtml .= "</div>";

    my $ret;
    $ret = "<script language='JavaScript'>\n";
    $ret .= "<!--\n";
    $qrhtml = LJ::ejs($qrhtml);
    $ret .= "document.write('$qrhtml');\n";
    $ret .= "-->\n";
    $ret .= "</script>";
    return $ret;
}

# <LJFUNC>
# name: LJ::make_qr_link
# class: web
# des: Creates the link to toggle the QR reply form or if
# JavaScript is not enabled, then forwards the user through
# to replyurl.
# returns: undef upon failure or HTML for the link
# args: dtid, basesubject, linktext, replyurl
# des-dtid: dtalkid for this comment
# des-basesubject: parent comment's subject
# des-linktext: text for the user to click
# des-replyurl: URL to forward user to if their browser
# does not support QR
# </LJFUNC>
sub make_qr_link
{
    my ($dtid, $basesubject, $linktext, $replyurl) = @_;

    return undef unless defined $dtid && $linktext && $replyurl;

    my $remote = LJ::get_remote();
    LJ::load_user_props($remote, "opt_no_quickreply");
    unless ($remote->{'opt_no_quickreply'}) {
        my $pid = int($dtid / 256);

        $basesubject =~ s/^(Re:\s*)*//i;
        $basesubject = "Re: $basesubject" if $basesubject;
        $basesubject = LJ::ejs($basesubject);
        my $onclick = "return quickreply('$dtid', $pid, '$basesubject')";
        $onclick = LJ::ehtml($onclick);
        return "<a onclick='$onclick' href='$replyurl' >$linktext</a>";
    } else { # QR Disabled
        return "<a href='$replyurl' >$linktext</a>";
    }
}

# <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;
    return (undef, undef) unless $remote;

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

    return ($talkid, $jid);
}

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

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

# <LJFUNC>
# name: LJ::set_lastcomment
# class: web
# des: Sets the lastcomm Memcache 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 Memcache 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;
}

# <LJFUNC>
# name: LJ::entry_form
# class: web
# des: Returns a properly formatted form for creating/editing entries
# args: opts, head
# 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
#   richtext_on: rich text formatting has been turned on
#   auth_as_remote: bool option to authenticate as remote user, prefilling 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 ($moodlist, $moodpics, $userpics);

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

    my $tabnum = 1;
    my $tabindex = sub { return $tabnum++; };
    $opts->{'event'} = LJ::durl($opts->{'event'}) if $opts->{'mode'} eq "edit";
    
    # 15 minute auth token, should be adequate
    my $chal = LJ::challenge_generate(900);
    $out .= "<input type='hidden' name='chal' id='login_chal' value='$chal' />";
    $out .= "<input type='hidden' name='response' id='login_response' value='' />";

    $out .= "<div id='EntryForm'>";
    $out .= "<table id='MetaInfo' cellpadding='0' cellspacing='0'>\n<tr valign='top'>";
    ### Meta Information Column 1
    {
        $out .= "<td style='width: 50%;'><table cellspacing='0' cellpadding='2' style='width: 100%; text-align: left' border='0'>";

        # Authentication box
        $out .= $opts->{'auth'};
        $out .= "<tr><th></th><td style='font-size: 0.85em;'><?inerr $errors->{'auth'} inerr?></td></tr>\n" if $errors->{'auth'};
        # Date / Time
        {
            my ($year, $mon, $mday, $hour, $min) = split( /\D/, $opts->{'datetime'});
            # date entry boxes / formatting note
            my $datetime = LJ::html_datetime({ 'name' => "date_ymd", 'notime' => 1, 'default' => "$year-$mon-$mday", 'disabled' => $opts->{'disabled_save'}}) . "&nbsp;" x 5;
            $datetime .=   LJ::html_text({ size => 2, maxlength => 2, value => $hour, name => "hour", tabindex => $tabindex->(), disabled => $opts->{'disabled_save'} }) . ":"; 
            $datetime .=   LJ::html_text({ size => 2, maxlength => 2, value => $min, name => "min", tabindex => $tabindex->(), disabled => $opts->{'disabled_save'} });
            
            $out .= "<tr valign='top'><th>" . BML::ml('entryform.date') . "</th>";
            $out .= "<td id='datetime_box' nowrap='nowrap'>$datetime <?de " . BML::ml('entryform.date.24hournote') . " de?>";
            $out .= "<noscript><br /><span style='font-size: 0.85em;'>" . BML::ml('entryform.nojstime.note') . "</span></noscript></td></tr>\n";
        }
    
        ### Subject
        $out .= "<tr valign='top'><th>" . BML::ml('entryform.subject') . "</th><td>";
        $out .= LJ::html_text({ 'name' => 'subject', 'value' => $opts->{'subject'},
                                'size' => '60', 'maxlength' => '100', 'tabindex' => $tabindex->(), 'disabled' => $opts->{'disabled_save'} }) . "\n";
        $out .= "</td></tr>";
        $out .= "</table></td>";
    }
    ### Meta Information Column 2


    {    
        $out .= "<td id='infobox'>";
        $out .= LJ::run_hook('entryforminfo');
        $out .= "</td>";
    }
    
    $out .= "</tr></table>\n";

    ### Display Spell Check Results:
    $out .= "<p><b>" . BML::ml('entryform.spellchecked') . "</b><br />$opts->{'spellcheck_html'}</p>" 
        if $opts->{'spellcheck_html'};
    $out .= "<p><?inerr " . BML::ml('Error') . " inerr?><br />$errors->{'entry'}</p>" 
        if $errors->{'entry'};

    ### Event Text Area:
    $out .= "<p><b>" . BML::ml('entryform.entry') . "</b></p>" unless $opts->{'richtext_on'};
    if ($opts->{'richtext_on'}) {
        my $jevent = $opts->{'event'};
        
        # manually typed tags
        $jevent =~ s/<lj user=['"]?(\w{1,15})['\"]?\s?\/?>/&lt;lj user="$1" \/&gt;/ig;
        $jevent =~ s/<(\/)?lj-cut(.*?)(?: \/)?>/&lt;$1lj-cut$2&gt;/ig;
        
        $jevent = LJ::ejs($jevent);
        my $rte_nosupport = LJ::ejs(BML::fill_template("de", { DATA => BML::ml('entryform.htmlokay.rte_nosupport') }));
        
        $out .= LJ::html_hidden('richtext', '1') . "\n";
        $out .= LJ::html_hidden('saved_entry', '') . "\n";
        
        $out .= <<RTE;
        <iframe id="testFrame" style="position: absolute; visibility: hidden; width: 0px; height: 0px;"></iframe>
            <script language="JavaScript" type="text/javascript" src="$LJ::JSPREFIX/browserdetect.js"></script>
            <script language='JavaScript' type='text/javascript'>
            <!--
            var siteroot = "$LJ::SITEROOT";
        //-->
            </script>
            <script language="JavaScript" type="text/javascript" src="$LJ::JSPREFIX/richtext.js"></script>
            <script language='JavaScript' type='text/javascript'>
            <!--
            writeRichText('rte', 'event', '$jevent', '99%', 300, true, "Entry:");
        if (isRichText == false) {
            document.write("$rte_nosupport");
        }
        //-->
            </script>
            <noscript>
RTE
    }
    
    $out .= LJ::html_textarea({ 'name' => 'event', 'value' => $opts->{'event'},
                                'rows' => '20', 'cols' => '50', 'style' => 'width: 100%', 
                                'wrap' => 'soft', 'tabindex' => $tabindex->(),
                                'disabled' => $opts->{'disabled_save'}});

    $out .= '</noscript>' if $opts->{'richtext_on'};
    $out .= LJ::html_hidden('prop_opt_preformatted', '1') if $opts->{'richtext_on'};

    my $jrich = LJ::ejs(BML::fill_template("de", {
        DATA => BML::ml("entryform.htmlokay.rich", { 'opts' => 'href="#" onClick="enable_rte()"' })
    }));
    my $jnorich = LJ::ejs(BML::fill_template("de", { DATA => BML::ml('entryform.htmlokay.norich') }));

    unless ( $opts->{'richtext_on'}   ||
             $opts->{'disabled_save'} ||
             ! $opts->{'richtext'}    ||
             ( $opts->{'did_spellcheck'} && $opts->{'richtext_on'} )
           ) {
        $out .= <<RTE;
        <script language='JavaScript' type='text/javascript'>
            <!--
            var t = document.getElementById;
        if (t) {
            document.write('<input type="hidden" name="switched_rte_on" value="" />');
            document.write("$jrich");
        } else {
            document.write("$jnorich");
        }
        //-->
            </script>
RTE
            $out .= '<noscript><?de ' . BML::ml('entryform.htmlokay.norich') . ' de?></noscript>';
    }
    $out .= '<br />';

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

    if (!$opts->{'disabled_save'}) {
        ### Options
        $out .= "<b>" . BML::ml('entryform.options') . "</b><br />";
        $out .= "<table style='width: 100%' id='Options'><tr valign='top'>";
        
        ### Options Column 1
        {
            $out .= "<td style='border-right: 1px dashed #999; text-align: center; width: 50%' id='column_one_td'>";
                $out .= "<table style='text-align: left' id='column_one_table'>";
            
            # Security
            {
                my @secs = ("public", BML::ml('label.security.public'), "private", BML::ml('label.security.private'),
                            "friends", BML::ml('label.security.friends'));
                
                my @secopts;
                if ($res && ref $res->{'friendgroups'} eq 'ARRAY' && scalar @{$res->{'friendgroups'}} && !$opts->{'usejournal'}) {
                    push @secs, ("custom", BML::ml('label.security.custom'));
                    push @secopts, ("onchange" => "customboxes()");
                }
                
                $out .= "<tr valign='top'><th>" . LJ::help_icon("security", "", " ") . BML::ml('entryform.security') . "</th><td>";
                $out .= LJ::html_select({ 'id' => "Security", 'name' => 'security', 
                                          
                                          'selected' => $opts->{'security'}, 
                                          'tabindex' => $tabindex->(), @secopts }, @secs);
                
                # if custom security groups available, show them in a hideable div
                if ($res && ref $res->{'friendgroups'} eq 'ARRAY' && scalar @{$res->{'friendgroups'}}) {
                    my $display = $opts->{'security'} eq "custom" ? "block" : "none";
                    $out .= "<div id='custom_boxes' style='display: $display;'>";
                    foreach my $fg (@{$res->{'friendgroups'}}) {
                        $out .= LJ::html_check({ 'name' => "custom_bit_$fg->{'id'}",
                                                 'id' => "custom_bit_$fg->{'id'}",
                                                 'selected' => $opts->{"custom_bit_$fg->{'id'}"} || $opts->{'security_mask'}+0 & 1 << $fg->{'id'} }) . " ";
                        $out .= "<label for='custom_bit_$fg->{'id'}'>" . LJ::ehtml($fg->{'name'}) . "</label><br />";
                    }
                    $out .= "</div>";
                }
                $out .= "</td></tr>";
            }
            # Text Formatting
            unless ($opts->{'richtext_on'}) {
                my $format_selected = $opts->{'prop_opt_preformatted'} ? "preformatted" : "";
                $format_selected ||= $opts->{'event_format'};
                
                $out .= "<tr valign='top'><th><label for='event_format'>" . BML::ml('entryform.format') . "</label></th><td>";
                $out .= LJ::html_select({ 'name' => "event_format", 'id' => "event_format", 
                                          'selected' => $format_selected, 'tabindex' => $tabindex->() },
                                        "auto", BML::ml('entryform.format.auto'), "preformatted", BML::ml('entryform.format.preformatted'));
                $out .= "</td></tr>";
            }
            # Current Music
            $out .= "<tr><th>" . BML::ml('entryform.music') . "</th><td>";
            $out .= LJ::html_text({ 'name' => 'prop_current_music', 'value' => $opts->{'prop_current_music'},
                                    'size' => '35', 'maxlength' => '120', 'tabindex' => $tabindex->() }) . "</td></tr>\n";
            
            # Current Mood
            {
                my @moodlist = ('', BML::ml('entryform.noneother'));
                my $sel;
                
                my $moods = LJ::get_moods();
                
                foreach (sort { $moods->{$a}->{'name'} cmp $moods->{$b}->{'name'} } keys %$moods) {
                    push @moodlist, ($_, $moods->{$_}->{'name'});
                    
                    if ($opts->{'prop_current_mood'} eq $moods->{$_}->{'name'} ||
                        $opts->{'prop_current_moodid'} == $_) {
                        $sel = $_;
                    }
                }
                
                if ($remote) {
#                    LJ::load_mood_theme($remote->{'moodthemeid'});
#return $out;    
#                      foreach my $mood (keys %$moods) {
#                          if (LJ::get_mood_picture($remote->{'moodthemeid'}, $moods->{$mood}->{id}, \ my %pic)) {
#                              $moodlist .= "    moods[" . $moods->{$mood}->{id} . "] = \"" . $moods->{$mood}->{name} . "\";\n";
#                              $moodpics .= "    moodpics[" . $moods->{$mood}->{id} . "] = \"" . $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
#    function mood_preview() {
#        if (! document.getElementById) return false;
#        var mood_preview = document.getElementById('mood_preview');
#        
#        var mood_list  = document.getElementById('prop_current_moodid');
#        var moodid = mood_list[mood_list.selectedIndex].value;
#        if (moodid == "") {
#            mood_preview.style.display = "none";
#        } else {
#            mood_preview.style.display = "block";
#            var mood_image_preview = document.getElementById('mood_image_preview');
#            mood_image_preview.src = moodpics[moodid];
#
#            var mood_text_preview = document.getElementById('mood_text_preview');
#            var mood_custom_text  = document.getElementById('prop_current_mood').value;
#            mood_text_preview.innerHTML = mood_custom_text == "" ? moods[moodid] : mood_custom_text;
#        }
#    }
#}
#//--></script>
#MOODS
                }
                
                $out .= "<tr valign='top'><th>" . BML::ml('entryform.mood') . "</th><td>";
                $out .= LJ::html_select({ 'name' => 'prop_current_moodid', 'id' => 'prop_current_moodid',
                                          'selected' => $sel, 'onchange' => "mood_preview()", 
                                          'tabindex' => $tabindex->() }, @moodlist);
                $out .= " " . LJ::html_text({ 'name' => 'prop_current_mood', 'id' => 'prop_current_mood',
                                              'value' => $opts->{'prop_current_mood'}, 'onchange' => "mood_preview()",
                                              'size' => '15', 'maxlength' => '30',
                                              'tabindex' => $tabindex->() });
                my $mood_preview = LJ::ejs("<p id='mood_preview'><img src='javascript:true' alt='' id='mood_image_preview' /> <span id='mood_text_preview'></span></p>");
                $out .= "<script type='text/javascript' language='JavaScript'>\n<!--\ndocument.write(\"$mood_preview\");\n//-->\n</script>" if $remote;
                $out .= "</td></tr>";
            }

            # Tag labeling
            unless ($LJ::DISABLED{tags}) {
                $out .= "<tr><th>" . BML::ml('entryform.tags') . "</th><td>";
                $out .= LJ::html_text(
                    {
                        'name'      => 'prop_taglist',
                        'size'      => '35',
                        'value'     => $opts->{'prop_taglist'},
                        'maxlength' => '255',
                        'tabindex'  => $tabindex->()
                    }
                );
                $out .= LJ::help_icon('addtags');
                $out .= "</td></tr>";
            }

            $out .= "</table></td>";
        }
        ### Options Column 2
        {
            $out .= "<td style='text-align: center' id='column_two_td'>";
            $out .= "<table style='text-align: left;' id='column_two_table'>";
            
            # Backdate Entry
            $out .= "<tr id='backdate_row'><th><label for='prop_opt_backdated'>" . BML::ml('entryform.backdated') . "</label></th><td>";
            $out .= LJ::html_check({ 'type' => "check", 'id' => "prop_opt_backdated", 
                                     'name' => "prop_opt_backdated", "value" => 1, 
                                     'selected' => $opts->{'prop_opt_backdated'},
                                     'tabindex' => $tabindex->() });
            $out .= "</td></tr>";
            
            # Comment Settings
            my $comment_settings_selected = $opts->{'prop_opt_noemail'} ? "noemail" : 
                $opts->{'prop_opt_nocomments'} ? "nocomments" : "";
            $comment_settings_selected  ||= $opts->{'comment_settings'};
            
            $out .= "<tr valign='top' id='comment_settings_row'><th style='white-space: nowrap'>" . BML::ml('entryform.comment.settings') . "</th><td>";
        $out .= LJ::html_select({ 'name' => "comment_settings", 'selected' => $comment_settings_selected,
                                  'tabindex' => $tabindex->() },
                                "", BML::ml('entryform.comment.settings.default'), "noemail", BML::ml('entryform.comment.settings.noemail'), "nocomments", BML::ml('entryform.comment.settings.nocomments'));
            $out .= "</td></tr>";
            
            # Comment Screening settings
            $out .= "<tr id='comment_screen_settings_row'><th>" . LJ::help_icon("screening", "", " ") . BML::ml('entryform.comment.screening') . "</th><td>";
            my @levels = ('', BML::ml('label.screening.default'), 'N', BML::ml('label.screening.none'),
                          'R', BML::ml('label.screening.anonymous'), 'F', BML::ml('label.screening.nonfriends'),
                          'A', BML::ml('label.screening.all'));
            $out .= LJ::html_select({ 'name' => 'prop_opt_screening', 'selected' => $opts->{'prop_opt_screening'},
                                      'tabindex' => $tabindex->() }, @levels);
            $out .= "</td></tr>";
            
            my $userpic_preview = "";
            # User Picture
            if ($res && ref $res->{'pickws'} eq 'ARRAY' && scalar @{$res->{'pickws'}} > 0) {
                my @pickws = map { ($_, $_) } @{$res->{'pickws'}};
                my $num = 0;
                $userpics .= "    userpics[$num] = \"$res->{'defaultpicurl'}\";\n";
                foreach (@{$res->{'pickwurls'}}) {
                    $num++; 
                    $userpics .= "    userpics[$num] = \"$_\";\n";
                }
                $$onload .= " userpic_preview();";
                $$head .= <<USERPICS;
<script type="text/javascript" language="JavaScript"><!--
if (document.getElementById) {
    var userpics = new Array();
    $userpics
    function userpic_preview() {
        if (! document.getElementById) return false;
        var userpic_select          = document.getElementById('prop_picture_keyword');
        var userpic_preview         = document.getElementById('userpic_preview');
        var userpic_preview_image   = document.getElementById('userpic_preview_image');

        if (userpics[userpic_select.selectedIndex] != "") {
            userpic_preview.style.display = "block";
            userpic_preview_image.src = userpics[userpic_select.selectedIndex];
        }
    }
}
//--></script>
USERPICS
                $out .= "<tr id='userpic_list_row' valign='top'>";
                $out .= "<th>" . LJ::help_icon("userpics", "", " ") . BML::ml('entryform.userpics') . "</th><td>";
                $out .= LJ::html_select({'name' => 'prop_picture_keyword', 'id' => 'prop_picture_keyword',
                                         'selected' => $opts->{'prop_picture_keyword'}, 'onchange' => "userpic_preview()",
                                         'tabindex' => $tabindex->() },
                                        "", BML::ml('entryform.opt.defpic'),
                                        @pickws);
                $out .= "</td></tr>\n";
                
                $userpic_preview = "<script type='text/javascript' language='JavaScript'>\n<!--\ndocument.write(\"" . 
                    LJ::ejs("<p id='userpic_preview' style='display: none'>" .
                            "<img src='' alt='selected userpic' id='userpic_preview_image' /></p>") .
                            "\");\n//-->\n</script>" if $remote;
            }
            $out .= "</table></td>";
            if ($userpic_preview ne "") { $out .= "<td style='width: 104px; text-align: left'>$userpic_preview</td>"; }
        }
        
        $out .= "</tr></table>";
    }
    ### Submit Bar
    {
        $out .= "<p style='text-align: center'><div id='SubmitBar'>";
        if ($opts->{'mode'} eq "update") {
            # communities the user can post in
            my $usejournal = $opts->{'usejournal'};
            if ($usejournal) {
                $out .= "<b>" . BML::ml('entryform.postto') . "</b> ";
                $out .= LJ::ljuser($usejournal);
                $out .= LJ::html_hidden('usejournal' => $usejournal, 'usejournal_set' => 'true');
            } elsif ($res && ref $res->{'usejournals'} eq 'ARRAY') {
                $out .= "<span id='usejournal_list'><b>" . BML::ml('entryform.postto') . "</b> ";
                $out .= LJ::html_select({ 'name' => 'usejournal', 'selected' => $usejournal,
                                          'tabindex' => $tabindex->() },
                                        "", $remote->{'user'},
                                        map { $_, $_ } @{$res->{'usejournals'}}) . "</span>\n";
            }
        }

        if ($opts->{'mode'} eq "update") {
            my $onclick = "";
            if ($opts->{'richtext_on'} || ! $LJ::IS_SSL) {
                $onclick .= "updateRTE('rte'); " if $opts->{'richtext_on'};
                $onclick .= "return sendForm('updateForm');" if ! $LJ::IS_SSL;
            }
            $out .= LJ::html_submit('action:update', BML::ml('entryform.update'), { 'onclick' => $onclick, 
                                                                                    'tabindex' => $tabindex->() }) . "&nbsp;";
        }

        if ($opts->{'mode'} eq "edit") {
            $out .= LJ::html_submit('action:save', BML::ml('entryform.save'),
                                    { 'disabled' => $opts->{'disabled_save'},
                                      'tabindex' => $tabindex->() }) . "&nbsp;";
            $out .= LJ::html_submit('action:delete', BML::ml('entryform.delete'), {
                'disabled' => $opts->{'disabled_delete'},
                'tabindex' => $tabindex->(),
                'onclick' => "return confirm('" . LJ::ejs(BML::ml('entryform.delete.confirm')) . "')" }) . "&nbsp;";
            

            if (!$opts->{'disabled_spamdelete'}) {
                $out .= LJ::html_submit('action:deletespam', BML::ml('entryform.deletespam'), {
                    'onclick' => "return confirm('" . LJ::ejs(BML::ml('entryform.deletespam.confirm')) . "')",
                    'tabindex' => $tabindex->() });
            }
        }
        if ($LJ::SPELLER && !$opts->{'disabled_save'}) {
            my $onclick = "updateRTE('rte');" if $opts->{'richtext_on'};
            $out .= LJ::html_submit('action:spellcheck', BML::ml('entryform.spellcheck'), { 'onclick' => $onclick,
                                                                                            'tabindex' => $tabindex->() }) . "&nbsp;";
        }

        my $preview = "var f=this.form; var action=f.action; f.action='/preview/entry.bml'; f.target='preview'; ";
        $preview   .= "window.open('','preview','width=760,height=600,resizable=yes,status=yes,toolbar=no,location=no,menubar=no,scrollbars=yes'); ";
        $preview   .= "updateRTE('rte'); " if $opts->{'richtext_on'};
        $preview   .= "f.submit(); f.action=action; f.target='_self'; return false; ";
        $preview    = LJ::ejs(LJ::html_submit('action:preview', BML::ml('entryform.preview'), { 'onclick' => $preview,
                                                                                                'tabindex' => $tabindex->() }));
        if(!$opts->{'disabled_save'}) {
            $out .= <<PREVIEW;
<script type="text/javascript" language="JavaScript">
<!--
if (document.getElementById) {
    document.write("$preview ");
}
//-->
</script>
PREVIEW
        }
        $out .= "</div></p>";
    }
    $out .= "</div>";
    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[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..30) {
            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);
    $req->{'year'} = $year; $req->{'mon'} = $mon; $req->{'day'} = $day;

    foreach ( "year", "mon", "day" ) {
        $req->{$_} = $POST->{$_} if $POST->{$_} ne "";
    }

    # copy some things from %POST
    foreach (qw(subject hour min
                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_taglist)) {
        $req->{$_} = $POST->{$_};
    }

    $req->{"prop_opt_preformatted"} ||= $POST->{'event_format'} eq "preformatted" ? 1 : 0;
    $req->{"prop_opt_nocomments"}   ||= $POST->{'comment_settings'} eq "nocomments" ? 1 : 0;
    $req->{"prop_opt_noemail"}      ||= $POST->{'comment_settings'} eq "noemail" ? 1 : 0;
    $req->{'prop_opt_backdated'}      = $POST->{'prop_opt_backdated'} ? 1 : 0;
    
    # Convert the rich text editor output back to parsable lj tags.
    my $event = $POST->{'event'};
    if ($POST->{'richtext'}) {
        # check for blank entry
        (my $event_tmp = $event) =~ s!(?:<br>|<P>(?:&nbsp;|\s)+</P>|&nbsp;)\s*?!!gm;
        if ($event_tmp =~ /\w/) { # ok, we still have content
            $event =~ s/&lt;(\/)?lj-cut(.*?)(?: \/)?&gt;/<$1lj-cut$2>/ig;
            $event =~ s/&lt;lj user=['"]?(\w{1,15})['"]?\s?\/?&gt;/<lj user="$1" \/>/ig; # manually typed tags
            $event =~ s/<span class="?ljuser"?.*?userinfo\.bml\?user=(.+?)".*?<\/b><\/a>(?:<\/span>)?/<lj user="$1" \/>/ig;
        } else { # RTE blanks (just <br>, newlines, &nbsp; - no real content)
            $event = undef; # force protocol error
        }
    }
    $req->{'event'} = $event;

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

# returns exactly what was passed to it normally.  but in developer mode,
# it includes a link to a page that automatically grants the needed priv.
sub no_access_error {
    my ($text, $priv, $privarg) = @_;
    if ($LJ::IS_DEV_SERVER) {
        my $remote = LJ::get_remote();
        return "$text <b>(DEVMODE: <a href='/admin/priv/?devmode=1&user=$remote->{user}&priv=$priv&arg=$privarg'>Grant $priv\[$privarg\]</a>)</b>";
    } else {
        return $text;
    }
}

# Data::Dumper for JavaScript
# sort for stable result, e.g. MD5()
sub js_dumper {
    my $obj = shift;
    if (ref $obj eq "HASH") {
        my $ret = "{";
        foreach my $k (sort keys %$obj) {
            $ret .= "$k: " . js_dumper($obj->{$k}) . ",";
        }
        chop $ret;
        $ret .= "}";
        return $ret;
    } elsif (ref $obj eq "ARRAY") {
        my $ret = "[" . join(", ", map { js_dumper($_) } sort @$obj) . "]";
        return $ret;
    } else {
        return $obj if $obj =~ /^\d+$/;
        return "\"" . LJ::ejs($obj) . "\"";
    }
}

# Common challenge/response javascript, needed by both login pages and comment pages alike.
# Forms that use this should onclick='return sendForm()' in the submit button.
# Returns true to let the submit continue.
$LJ::COMMON_CODE{'chalresp_js'} = qq{
<script type="text/javascript" src="$LJ::JSPREFIX/md5.js"></script>
<script language="JavaScript" type="text/javascript">
    <!--
function sendForm (formid, checkuser)
{
    if (formid == null) formid = 'login';
    // 'checkuser' is the element id name of the username textfield.
    // only use it if you care to verify a username exists before hashing.

    if (! document.getElementById) return true;
    var loginform = document.getElementById(formid);
    if (! loginform) return true;
    
    // Avoid accessing the password field if there is no username.
    // This works around Opera < 7 complaints when commenting.
    if (checkuser) {
        var username = null;
        for (var i = 0; username == null && i < loginform.elements.length; i++) {
            if (loginform.elements[i].id == checkuser) username = loginform.elements[i];
        }
        if (username != null && username.value == "") return true;
    }

    if (! loginform.password || ! loginform.login_chal || ! loginform.login_response) return true;
    var pass = loginform.password.value;
    var chal = loginform.login_chal.value;
    var res = MD5(chal + MD5(pass));
    loginform.login_response.value = res;
    loginform.password.value = "";  // dont send clear-text password!
    return true;
}
// -->
</script>
};

# Common Javascript function for auto-checking radio buttons on form
# input field data changes
$LJ::COMMON_CODE{'autoradio_check'} = q{
<script language="JavaScript" type="text/javascript">
    <!--
    /* If radioid exists, check the radio button. */
    function checkRadioButton(radioid) {
        if (!document.getElementById) return;
        var radio = document.getElementById(radioid);
        if (!radio) return;
        radio.checked = true;
    }
// -->
</script>
};

# Common Javascript functions for Quick Reply
$LJ::COMMON_CODE{'quickreply'} = q{
    <script language="JavaScript" type="text/javascript" src="/js/x_core.js"></script>
    <script language='Javascript' type='text/javascript'>
    <!--
    var lastDiv;
    lastDiv = 'qrdiv';

    function quickreply(dtid, pid, newsubject) {
        var ev = window.event;

        // on IE, cancel the bubble of the event up to the page. other
        // browsers don't seem to bubble events up registered this way.
        if (ev) {
            if (ev.stopPropagation)
               ev.stopPropagation();
            if ("cancelBubble" in ev)
                ev.cancelBubble = true;
        }

        // Mac IE 5.x does not like dealing with
        // nextSibling since it does not support it
        if (xIE4Up && xMac) { return true;}

        var ptalkid = xGetElementById('parenttalkid');
        ptalkid.value = pid;

        var rto = xGetElementById('replyto');
        rto.value = pid;

        var dtid_field = xGetElementById('dtid');
        dtid_field.value = dtid;

        var qr_div = xGetElementById('qrdiv');
        var cur_div = xGetElementById(dtid);

        if (lastDiv == 'qrdiv') {
            if (! showQRdiv(qr_div)) {
               return true;
            }

            // Only one swap
            if (! swapnodes(qr_div, cur_div)) {
                return true;
            }
        } else if (lastDiv != dtid) {
            var last_div = xGetElementById(lastDiv);

            // Two swaps
            if (! (swapnodes(last_div, cur_div) && swapnodes(qr_div, last_div))) {
                return true;
            }
        }

        lastDiv = dtid;

        var subject = xGetElementById('subject');
        subject.value = newsubject;

        var multi_form = xGetElementById('multiform');
        multi_form.action = '/talkpost_do.bml';

        // So it does not follow the link
        return false;
    }

    function moreopts()
    {
        var multi_form = xGetElementById('multiform');
        var basepath = xGetElementById('basepath');
        var dtid = xGetElementById('dtid');

        multi_form.action = basepath.value + dtid.value;
        return true;
    }

   function submitform()
   {
        var submit = xGetElementById('submitpost');
        submit.disabled = true;

        var submitmore = xGetElementById('submitmoreopts');
        submitmore.disabled = true;

        // New top-level comments
        var dtid = xGetElementById('dtid');
        if (dtid.value == 'top' || dtid.value == 'bottom') {
            dtid.value = 0;
        }

        var multi_form = xGetElementById('multiform');
        multi_form.submit();
   }

   function swapnodes (orig, to_swap) {
        var orig_pn = xParent(orig, true);
        var next_sibling = orig.nextSibling;
        var to_swap_pn = xParent(to_swap, true);
        if (! to_swap_pn) {
            return false;
        }

        to_swap_pn.replaceChild(orig, to_swap);
        orig_pn.insertBefore(to_swap, next_sibling);
        return true;
   }

   function checkLength() {
        var textbox = xGetElementById('body');
        if (!textbox) return true;
        if (textbox.value.length > 14300) {
             alert('Sorry, but your comment of ' + textbox.value.length + ' characters exceeds the maximum character length of 14300.  Please try shortening it and then post again.');
             return false;
        }
        return true;
   }

    // Maintain entry through browser navigations.
    function save_entry() {
        var qr_body = xGetElementById('body');
        var qr_subject = xGetElementById('subject');
        var do_spellcheck = xGetElementById('do_spellcheck');
        var qr_dtid = xGetElementById('dtid');
        var qr_ptid = xGetElementById('parenttalkid');
        var qr_upic = xGetElementById('prop_picture_keyword');

        document.multiform.saved_body.value = qr_body.value;
        document.multiform.saved_subject.value = qr_subject.value;
        document.multiform.saved_spell.value = do_spellcheck.checked;
        document.multiform.saved_dtid.value = qr_dtid.value;
        document.multiform.saved_ptid.value = qr_ptid.value;

        if (qr_upic) { // if it was in the form
            document.multiform.saved_upic.value = qr_upic.selectedIndex;
        }

        return false;
    }

    // Restore saved_entry text across platforms.
    function restore_entry() {
        var saved_body = xGetElementById('saved_body');
        if (saved_body.value == "") return false;

        setTimeout(
            function () {

                var dtid = xGetElementById('saved_dtid');
                if (! dtid) return false;
                var ptid = xGetElementById('saved_ptid');
                if (! ptid) return false;

                quickreply(dtid.value, ptid.value, document.multiform.saved_subject.value);

                var body = xGetElementById('body');
                if (! body) return false;
                body.value = saved_body.value;

                // Some browsers require we explicitly set this after the div has moved
                // and is now no longer hidden
                var subject = xGetElementById('subject');
                if (! subject) return false;
                subject.value = document.multiform.saved_subject.value

                var prop_picture_keyword = xGetElementById('prop_picture_keyword');
                if (prop_picture_keyword) { // if it was in the form
                    prop_picture_keyword.selectedIndex = document.multiform.saved_upic.value;
                }

                var spell_check = xGetElementById('do_spellcheck');
                if (! spell_check) return false;
                if (document.multiform.saved_spell.value == 'true') {
                    spell_check.checked = true;
                } else {
                    spell_check.checked = false;
                }

            }, 100);
        return false;
    }

    function showQRdiv(qr_div) {
        if (! qr_div) {
            qr_div = xGetElementById('qr_div');
            if (! qr_div) {
                return false;
            }
        } else if (qr_div.style && xDef(qr_div.style.display)) {
            qr_div.style.display='inline';
            return true;
        } else {
            return false;
        }
    }

    //  -->
    </script>
};

# <LJFUNC>
# name: LJ::set_interests
# des: Change a user's interests
# args: dbarg?, u, old, new
# arg-old: hashref of old interests (hashing being interest => intid)
# arg-new: listref of new interests
# returns: 1 on success, undef on failure
# </LJFUNC>
sub set_interests
{
    my ($u, $old, $new) = @_;

    $u = LJ::want_user($u);
    my $userid = $u->{'userid'};
    return undef unless $userid;

    return undef unless ref $old eq 'HASH';
    return undef unless ref $new eq 'ARRAY';

    my $dbh = LJ::get_db_writer();
    my %int_new = ();
    my %int_del = %$old;  # assume deleting everything, unless in @$new

    # user interests go in a different table than user interests,
    # though the schemas are the same so we can run the same queries on them
    my $uitable = $u->{'journaltype'} eq 'C' ? 'comminterests' : 'userinterests';

    # track if we made changes to refresh memcache later.
    my $did_mod = 0;

    foreach my $int (@$new)
    {
        $int = lc($int);       # FIXME: use utf8?
        $int =~ s/^i like //;  # *sigh*
        next unless $int;
        next if $int =~ / .+ .+ .+ /;  # prevent sentences
        next if $int =~ /[\<\>]/;
        my ($bl, $cl) = LJ::text_length($int);
        next if $bl > LJ::BMAX_INTEREST or $cl > LJ::CMAX_INTEREST;
        $int_new{$int} = 1 unless $old->{$int};
        delete $int_del{$int};
    }

    ### were interests removed?
    if (%int_del)
    {
        ## easy, we know their IDs, so delete them en masse
        my $intid_in = join(", ", values %int_del);
        $dbh->do("DELETE FROM $uitable WHERE userid=$userid AND intid IN ($intid_in)");
        $dbh->do("UPDATE interests SET intcount=intcount-1 WHERE intid IN ($intid_in)");
        $did_mod = 1;
    }

    ### do we have new interests to add?
    if (%int_new)
    {
        $did_mod = 1;

        ## difficult, have to find intids of interests, and create new ints for interests
        ## that nobody has ever entered before
        my $int_in = join(", ", map { $dbh->quote($_); } keys %int_new);
        my %int_exist;
        my @new_intids = ();  ## existing IDs we'll add for this user

        ## find existing IDs
        my $sth = $dbh->prepare("SELECT interest, intid FROM interests WHERE interest IN ($int_in)");
        $sth->execute;
        while (my ($intr, $intid) = $sth->fetchrow_array) {
            push @new_intids, $intid;       # - we'll add this later.
            delete $int_new{$intr};         # - so we don't have to make a new intid for
                                            #   this next pass.
        }

        if (@new_intids) {
            my $sql = "";
            foreach my $newid (@new_intids) {
                if ($sql) { $sql .= ", "; }
                else { $sql = "REPLACE INTO $uitable (userid, intid) VALUES "; }
                $sql .= "($userid, $newid)";
            }
            $dbh->do($sql);

            my $intid_in = join(", ", @new_intids);
            $dbh->do("UPDATE interests SET intcount=intcount+1 WHERE intid IN ($intid_in)");
        }
    }

    ### do we STILL have interests to add?  (must make new intids)
    if (%int_new)
    {
        foreach my $int (keys %int_new)
        {
            my $intid;
            my $qint = $dbh->quote($int);

            $dbh->do("INSERT INTO interests (intid, intcount, interest) ".
                     "VALUES (NULL, 1, $qint)");
            if ($dbh->err) {
                # somebody beat us to creating it.  find its id.
                $intid = $dbh->selectrow_array("SELECT intid FROM interests WHERE interest=$qint");
                $dbh->do("UPDATE interests SET intcount=intcount+1 WHERE intid=$intid");
            } else {
                # newly created
                $intid = $dbh->{'mysql_insertid'};
            }
            if ($intid) {
                ## now we can actually insert it into the userinterests table:
                $dbh->do("INSERT INTO $uitable (userid, intid) ".
                         "VALUES ($userid, $intid)");
            }
        }
    }

    ### if journaltype is community, clean their old userinterests from 'userinterests'
    if ($u->{'journaltype'} eq 'C') {
        $dbh->do("DELETE FROM userinterests WHERE userid=?", undef, $u->{'userid'});
    }

    LJ::memcache_kill($u, "intids") if $did_mod;
    return 1;
}

# $opts is optional, with keys:
#    forceids => 1   : don't use memcache for loading the intids
#    forceints => 1   : don't use memcache for loading the interest rows
#    justids => 1 : return arrayref of intids only, not names/counts
# returns otherwise an arrayref of interest rows, sorted by interest name
sub get_interests
{
    my ($u, $opts) = @_;
    $opts ||= {};
    return undef unless $u;
    my $uid = $u->{userid};
    my $uitable = $u->{'journaltype'} eq 'C' ? 'comminterests' : 'userinterests';

    # load the ids
    my $ids;
    my $mk_ids = [$uid, "intids:$uid"];
    $ids = LJ::MemCache::get($mk_ids) unless $opts->{'forceids'};
    unless ($ids && ref $ids eq "ARRAY") {
        $ids = [];
        my $dbh = LJ::get_db_writer();
        my $sth = $dbh->prepare("SELECT intid FROM $uitable WHERE userid=?");
        $sth->execute($uid);
        push @$ids, $_ while ($_) = $sth->fetchrow_array;
        LJ::MemCache::add($mk_ids, $ids, 3600*12);
    }
    return $ids if $opts->{'justids'};

    # load interest rows
    my %need;
    $need{$_} = 1 foreach @$ids;
    my @ret;

    unless ($opts->{'forceints'}) {
        if (my $mc = LJ::MemCache::get_multi(map { [$_, "introw:$_"] } @$ids)) {
            while (my ($k, $v) = each %$mc) {
                next unless $k =~ /^introw:(\d+)/;
                delete $need{$1};
                push @ret, $v;
            }
        }
    }

    if (%need) {
        my $ids = join(",", map { $_+0 } keys %need);
        my $dbr = LJ::get_db_reader();
        my $sth = $dbr->prepare("SELECT intid, interest, intcount FROM interests ".
                                "WHERE intid IN ($ids)");
        $sth->execute;
        my $memc_store = 0;
        while (my ($intid, $int, $count) = $sth->fetchrow_array) {
            # minimize latency... only store 25 into memcache at a time
            # (too bad we don't have set_multi.... hmmmm)
            my $aref = [$intid, $int, $count];
            if ($memc_store++ < 25) {
                # if the count is fairly high, keep item in memcache longer,
                # since count's not so important.
                my $expire = $count < 10 ? 3600*12 : 3600*48;
                LJ::MemCache::add([$intid, "introw:$intid"], $aref, $expire);
            }
            push @ret, $aref;
        }
    }

    @ret = sort { $a->[1] cmp $b->[1] } @ret;
    return \@ret;
}

# 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 $link   = delete $opts{link}   || delete $opts{placeholder_link} || '';
    my $img    = delete $opts{img}    || "$LJ::IMGPREFIX/videoplaceholder.png";
    my $img_title = delete $opts{img_title} || "Click to show embedded content";
    # A really stupid hack, so that the images won't be too big
    $height = ($height>150) ? $height-100  : 50 ;    
    $width = ($width>150) ? $width-100 : 50 ;

    return qq {
            <div class="LJ_Placeholder_Container" style="width: ${width}px; height: ${height}px;">
                <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="$img_title" width=$width height=$height/><br><font size=-4>Click to show embedded content</font>
                </a>
            </div>
        };
}

# Returns replacement for lj-replace tags
sub lj_replace {
    my $key = shift;
    my $attr = shift;

    # Return hook if hook output not undef
    if (LJ::are_hooks("lj-replace_$key")) {
        my $replace = LJ::run_hook("lj-replace_$key");
        return $replace if defined $replace;
    }

    # Return value of coderef if key defined
    my %valid_keys = ( 'first_post' => \&lj_replace_first_post );

    if (my $cb = $valid_keys{$key}) {
        die "$cb is not a valid coderef" unless ref $cb eq 'CODE';
        return $cb->($attr);
    }

    return undef;
}

# Replace for lj-replace name="first_post"
sub lj_replace_first_post {
    return unless LJ::is_web_context();
    return BML::ml('web.lj-replace.first_post', {
                   'update_link' => "href='$LJ::SITEROOT/update.bml'",
                   });
}

# 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 if $LJ::DISABLED{'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+)?/res/(\d+)/stylesheet(\?\d+)?$!;

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


1;
