#!/usr/bin/perl
#
package LJ;
use strict;
use lib "$ENV{LJHOME}/cgi-bin";
# load the bread crumb hash
require "crumbs.pl";
use Carp;
use Class::Autouse qw(
LJ::Event
LJ::Subscription::Pending
LJ::M::ProfilePage
LJ::Directory::Search
LJ::Directory::Constraint
LJ::M::FriendsOf
);
#
# name: LJ::img
# des: Returns an HTML <img> or <input> 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 etc/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 <img> 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.
#
sub img
{
my $ic = shift;
my $type = shift; # either "" or "input"
my $attr = shift;
my $attrs;
my $alt;
if ($attr) {
if (ref $attr eq "HASH") {
$alt = LJ::ehtml($attr->{alt}) if (exists $attr->{alt});
foreach (keys %$attr) {
$attrs .= " $_=\"" . LJ::ehtml($attr->{$_}) . "\""
unless ((lc $_) eq 'alt');
}
} else {
$attrs = " name=\"$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 "{'src'}\" width=\"$i->{'width'}\" ".
"height=\"$i->{'height'}\" alt=\"$alt\" title=\"$alt\" ".
"border='0'$attrs />";
}
if ($type eq "input") {
return "{'src'}\" ".
"width=\"$i->{'width'}\" height=\"$i->{'height'}\" title=\"$alt\" ".
"alt=\"$alt\" border='0'$attrs />";
}
return "XXX";
}
#
# 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.
#
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 .= "$y-";
$ret .= "$m-";
$ret .= "$d";
return $ret;
}
#
# name: LJ::auto_linkify
# des: Takes a plain-text string and changes URLs into tags (auto-linkification).
# args: str
# des-str: The string to perform auto-linkification on.
# returns: The auto-linkified text.
#
sub auto_linkify
{
my $str = shift;
my $match = sub {
my $str = shift;
if ($str =~ /^(.*?)(&(#39|quot|lt|gt)(;.*)?)$/) {
return "$1$2";
} else {
return "$str";
}
};
$str =~ s!https?://[^\s\'\"\<\>]+[a-zA-Z0-9_/&=\-]! $match->($&); !ge;
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 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->();
}
#
# 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 [func[LJ::get_authas_list]].
#
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 || $list[0] ne $u->{'user'}) {
my $ret;
my $label = $BML::ML{'web.authas.label'};
$label = $BML::ML{'web.authas.label.comm'} if ($opts->{'type'} eq "C");
$ret = ($opts->{'label'} || $label) . " ";
$ret .= LJ::html_select({ 'name' => 'authas',
'selected' => $opts->{'authas'} || $u->{'user'},
'class' => 'hideable',
},
map { $_, $_ } @list) . " ";
$ret .= LJ::html_submit(undef, $opts->{'button'} || $BML::ML{'web.authas.btn'});
return $ret;
}
# no communities to choose from, give the caller a hidden
return LJ::html_hidden('authas', $opts->{'authas'} || $u->{'user'});
}
#
# name: LJ::make_postto_select
# des: Given a u object and some options, determines which users the given user
# can post 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:
# 'postto' - 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 [func[LJ::get_postto_list]].
#
sub make_postto_select {
my ($u, $opts) = @_; # type, authas, label, button
my @list = LJ::get_postto_list($u, $opts);
# only do most of form if there are options to select from
if (@list > 1) {
return ($opts->{'label'} || $BML::ML{'web.postto.label'}) . " " .
LJ::html_select({ 'name' => 'authas',
'selected' => $opts->{'authas'} || $u->{'user'}},
map { $_, $_ } @list) . " " .
LJ::html_submit(undef, $opts->{'button'} || $BML::ML{'web.postto.btn'});
}
# no communities to choose from, give the caller a hidden
return LJ::html_hidden('authas', $opts->{'authas'} || $u->{'user'});
}
#
# 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, 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.
#
sub help_icon
{
my $topic = shift;
my $pre = shift;
my $post = shift;
return "" unless (defined $LJ::HELPURL{$topic});
return "$pre$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 || "";
# FIXME: use LJ::img() here, not hard-coding width/height
return "$pre$post";
}
#
# 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
#
sub bad_input
{
my @errors = @_;
my $ret = "";
$ret .= "\n
\n";
foreach my $ei (@errors) {
my $err = LJ::errobj($ei) or next;
$err->log;
$ret .= $err->as_bullets;
}
$ret .= "
\n";
return $ret;
}
#
# 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
#
sub error_list
{
# FIXME: retrofit like bad_input above? merge? make aliases for each other?
my @errors = @_;
my $ret;
$ret .= "";
$ret .= BML::ml('error.procrequest');
$ret .= "
";
foreach my $ei (@errors) {
my $err = LJ::errobj($ei) or next;
$err->log;
$ret .= $err->as_bullets;
}
$ret .= "
errorbar?>";
return $ret;
}
#
# name: LJ::error_noremote
# des: Returns an error telling the user to log in.
# returns: Translation string "error.notloggedin"
#
sub error_noremote
{
return "";
}
#
# 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
#
sub warning_list
{
my @warnings = @_;
my $ret;
$ret .= "";
$ret .= BML::ml('label.warning');
$ret .= "
";
foreach (@warnings) {
$ret .= "
$_
";
}
$ret .= "
warningbar?>";
return $ret;
}
sub tosagree_widget {
my ($checked, $errstr) = @_;
return
"
" . LJ::html_check({ name => 'agree_tos', id => 'agree_tos',
value => '1', selected => $checked }) .
"
" .
($errstr ? "" : '');
}
sub tosagree_html {
my $domain = shift;
my $ret = "";
my $html_str = LJ::tosagree_str($domain => 'html');
$ret .= "" if $html_str;
$ret .= "
";
$ret .= LJ::tosagree_widget(@_);
$ret .= "
";
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};
}
#
# 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"
#
sub did_post
{
return (BML::get_method() eq "POST");
}
#
# 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
#
sub robot_meta_tags
{
return "\n" .
"\n";
}
sub paging_bar
{
my ($page, $pages, $opts) = @_;
my $self_link = $opts->{'self_link'} ||
sub { BML::self_link({ 'page' => $_[0] }) };
my $href_opts = $opts->{'href_opts'} || sub { '' };
my $navcrap;
if ($pages > 1) {
$navcrap .= "
\n";
$navcrap = BML::fill_template("standout", { 'DATA' => $navcrap });
}
return $navcrap;
}
#
# 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.
#
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};
}
}
#
# 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
#
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 $LJ::USER_VHOSTS && $referer =~ m!^http://([A-Za-z0-9_\-]{1,15})\.$LJ::DOMAIN$uri!;
return 1 if $uri =~ m!^http://! && $referer eq $uri;
return undef;
}
#
# 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.
#
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 = LJ::challenge_generate(86400, $auth);
$LJ::REQ_GLOBAL{form_auth_chal} = $chal;
}
return $raw ? $chal : LJ::html_hidden("lj_form_auth", $chal);
}
#
# 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).
#
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 = LJ::get_challenge_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
LJ::challenge_check($formauth, $opts);
return $opts->{valid} && ! $opts->{expired};
}
#
# 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, 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.
#
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'};
$qrhtml .= "";
my $ret;
$ret = "";
$ret .= qq {
} unless $LJ::DISABLED{userpicselect} || ! $remote->get_cap('userpicselect');
return $ret;
}
#
# 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.
#
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::ehtml(LJ::ejs($basesubject));
my $onclick = "return quickreply(\"$dtid\", $pid, \"$basesubject\")";
return "$linktext";
} else { # QR Disabled
return "$linktext";
}
}
#
# name: LJ::get_lastcomment
# class: web
# des: Looks up the last talkid and journal the remote user posted in.
# returns: talkid, jid
# args:
#
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);
}
#
# name: LJ::make_qr_target
# class: web
# des: Returns a div usable for QuickReply boxes.
# returns: HTML for the div
# args:
#
sub make_qr_target {
my $name = shift;
return "";
}
#
# 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.
#
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 {
"$_[0]";
}
#
# 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 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.
#
sub entry_form {
my ($opts, $head, $onload, $errors) = @_;
my $out = "";
my $remote = $opts->{'remote'};
my $altlogin = $opts->{'altlogin'};
my $userpic_display = $altlogin ? 'none' : 'block';
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};
}
# Temp fix for FF 2.0.0.17
my $rte_not_supported = LJ::conf_test($LJ::DISABLED{'rte_support'}, BML::get_client_header("User-Agent"));
$opts->{'richtext_default'} = 0 if ($rte_not_supported);
$opts->{'richtext'} = $opts->{'richtext_default'};
my $tabnum = 10; #make allowance for username and password
my $tabindex = sub { return $tabnum++; };
$opts->{'event'} = LJ::durl($opts->{'event'}) if $opts->{'mode'} eq "edit";
# 1 hour auth token, should be adequate
my $chal = LJ::challenge_generate(3600);
$out .= "\n\n
";
$out .= "\n\n";
$out .= "\n\n";
$out .= LJ::error_list($errors->{entry}) if $errors->{entry};
# 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,
});
}
### Userpic
my $userpic_preview = "";
# User Picture
if (!$altlogin && ($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();";
my $userpic_link_text;
$userpic_link_text = BML::ml('entryform.userpic.choose') if $remote;
$$head .= qq {
};
$$head .= qq {
} unless $LJ::DISABLED{userpicselect} || ! $remote->get_cap('userpicselect');
# libs for userpicselect
LJ::need_res(qw(
js/core.js
js/dom.js
js/json.js
js/template.js
js/ippu.js
js/lj_ippu.js
js/userpicselect.js
js/httpreq.js
js/hourglass.js
js/inputcomplete.js
stc/ups.css
js/datasource.js
js/selectable_table.js
)) if ! $LJ::DISABLED{userpicselect} && $remote->get_cap('userpicselect');
$out .= "
\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 .= "
\n";
$out .= "\n";
$out .= LJ::ljuser($usejournal);
$out .= LJ::html_hidden({ name => 'usejournal', value => $usejournal, id => 'usejournal_username' });
$out .= LJ::html_hidden( usejournal_set => 'true' );
$out .= "
\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", 'disabled' => $opts->{'disabled_save'}});
$datetime .= "";
$datetime .= LJ::html_text({ size => 2, class => 'text', maxlength => 2, value => $hour, name => "hour", tabindex => $tabindex->(), disabled => $opts->{'disabled_save'} }) . ":";
$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 .= "";
$out .= "
";
return $ret;
}
sub get_next_ad_id {
return ++$LJ::REQ_GLOBAL{'curr_ad_id'};
}
##
## Function LJ::check_page_ad_block. Return answer (true/false) to question:
## Should we show ad of this type on this page.
## Args: uri of the page and orient of the ad block (e.g. 'App-Confirm')
##
sub check_page_ad_block {
my $uri = shift;
my $orient = shift;
# The AD_MAPPING hash may contain code refs
# This allows us to choose an ad based on some logic
# Example: If LJ::did_post() show 'App-Confirm' type ad
my $ad_mapping = LJ::run_hook('get_ad_uri_mapping', $uri) ||
LJ::conf_test($LJ::AD_MAPPING{$uri});
return 1 if $ad_mapping eq $orient;
return 1 if ref($ad_mapping) eq 'HASH' && $ad_mapping->{$orient};
return;
}
# returns a hash with keys "layout" and "theme"
# "theme" is empty for S1 users
sub get_style_for_ads {
my $u = shift;
my %ret;
$ret{layout} = "";
$ret{theme} = "";
# Values for custom layers, default themes, and S1 styles
my $custom_layout = "custom_layout";
my $custom_theme = "custom_theme";
my $default_theme = "default_theme";
my $s1_prefix = "s1_";
if ($u->prop('stylesys') == 2) {
my %style = LJ::S2::get_style($u);
my $public = LJ::S2::get_public_layers();
# get layout
my $layout = $public->{$style{layout}}->{uniq}; # e.g. generator/layout
$layout =~ s/\/\w+$//;
# get theme
# if the theme id == 0, then we have no theme for this layout (i.e. default theme)
my $theme;
if ($style{theme} == 0) {
$theme = $default_theme;
} else {
$theme = $public->{$style{theme}}->{uniq}; # e.g. generator/mintchoc
$theme =~ s/^\w+\///;
}
$ret{layout} = $layout ? $layout : $custom_layout;
$ret{theme} = $theme ? $theme : $custom_theme;
} else {
my $view = Apache->request->notes->{view};
$view = "lastn" if $view eq "";
if ($view =~ /^(?:friends|day|calendar|lastn)$/) {
my $pubstyles = LJ::S1::get_public_styles();
my $styleid = $u->prop("s1_${view}_style");
my $layout = "";
if ($pubstyles->{$styleid}) {
$layout = $pubstyles->{$styleid}->{styledes}; # e.g. Clean and Simple
$layout =~ s/\W//g;
$layout =~ s/\s//g;
$layout = lc $layout;
$layout = $s1_prefix . $layout;
}
$ret{layout} = $layout ? $layout : $s1_prefix . $custom_layout;
}
}
return %ret;
}
sub get_search_term {
my $uri = shift;
my $search_arg = shift;
my %search_pages = (
'/interests.bml' => 1,
'/directory.bml' => 1,
'/multisearch.bml' => 1,
);
return "" unless $search_pages{$uri};
my $term = "";
my $args = Apache->request->args;
if ($uri eq '/interests.bml') {
if ($args =~ /int=([^&]+)/) {
$term = $1;
}
} elsif ($uri eq '/directory.bml') {
if ($args =~ /int_like=([^&]+)/) {
$term = $1;
}
} elsif ($uri eq '/multisearch.bml') {
$term = $search_arg;
}
# change +'s to spaces
$term =~ s/\+/ /;
return $term;
}
sub email_ads {
my %opts = @_;
my $channel = $opts{channel};
my $from_email = $opts{from_email};
my $to_u = $opts{to_u};
return '' unless LJ::run_hook('should_show_ad', {
ctx => 'app',
user => $to_u,
remote => $to_u,
type => $channel,
});
return '' unless LJ::run_hook('should_show_email_ad', $to_u);
my $adid = get_next_ad_id();
my $divid = "ad_$adid";
my %adcall = (
r => rand(),
p => 'lj',
channel => $channel,
type => 'user',
user => $to_u->id,
gender => $to_u->gender_for_adcall,
hR => Digest::MD5::md5_hex(lc($from_email)),
hS => Digest::MD5::md5_hex(lc($to_u->email_raw)),
site => "lj.email",
);
my $age = $to_u->age_for_adcall;
$adcall{age} = $age if $age;
my $adparams = LJ::encode_url_string(\%adcall,
[ sort { length $adcall{$a} <=> length $adcall{$b} }
grep { length $adcall{$_} }
keys %adcall ] );
# allow 24 bytes for escaping overhead
$adparams = substr($adparams, 0, 1_000);
my $image_url = $LJ::ADSERVER . '/image/?' . $adparams;
my $click_url = $LJ::ADSERVER . '/click/?' . $adparams;
my $adhtml = "";
return $adhtml;
}
# this returns ad html given a search string
sub search_ads {
my %opts = @_;
return '' if LJ::conf_test($LJ::DISABLED{content_ads});
return '' unless $LJ::USE_JS_ADCALL_FOR_SEARCH;
my $remote = LJ::get_remote();
return '' unless LJ::run_hook('should_show_ad', {
ctx => 'app',
user => $remote,
type => '',
});
return '' unless LJ::run_hook('should_show_search_ad');
my $query = delete $opts{query} or croak "No search query specified in call to search_ads";
my $count = int(delete $opts{count} || 1);
my $adcount = int(delete $opts{adcount} || 3);
my $adid = get_next_ad_id();
my $divid = "ad_$adid";
my @divids = map { "ad_$_" } (1 .. $count);
my %adcall = (
u => join(',', map { $adcount } @divids), # how many ads to show in each
r => rand(),
q => $query,
id => join(',', @divids),
p => 'lj',
add => 'lj_content_ad',
remove => 'lj_inactive_ad',
);
if ($remote) {
$adcall{user} = $remote->id;
}
my $adparams = LJ::encode_url_string(\%adcall,
[ sort { length $adcall{$a} <=> length $adcall{$b} }
grep { length $adcall{$_} }
keys %adcall ] );
# allow 24 bytes for escaping overhead
$adparams = substr($adparams, 0, 1_000);
my $url = $LJ::ADSERVER . '/google/?' . $adparams;
my $adhtml;
my $adcall = '';
if (++$LJ::REQ_GLOBAL{'curr_search_ad_id'} == $count) {
$adcall .= qq { \n };
$adcall .= qq { };
}
$adhtml = qq {
$adcall
};
return $adhtml;
}
sub get_ads {
LJ::run_hook('ADV_get_ad_html', @_);
}
sub should_show_ad {
LJ::run_hook('ADV_should_show_ad', @_);
}
sub ads {
my %opts = @_;
return "" unless $LJ::USE_ADS;
my $adcall_url = LJ::run_hook('construct_adcall', %opts);
my $hook_did_adurl = $adcall_url ? 1 : 0;
# WARNING: $ctx is terribly named and not an S2 context
my $ctx = $opts{'type'};
my $orient = $opts{'orient'};
my $user = $opts{'user'};
my $pubtext = $opts{'pubtext'};
my $tags = $opts{'tags'};
my $colors = $opts{'colors'};
my $position = $opts{'position'};
my $search_arg = $opts{'search_arg'};
my $interests_extra = $opts{'interests_extra'};
my $vertical = $opts{'vertical'};
my $page = $opts{'page'};
##
## Some BML files contains calls to LJ::ads inside them.
## When LJ::ads is called from BML files, special prefix 'BML-' is used.
## $pagetype is essentially $orient without leading 'BML-' prefix.
## E.g. $orient = 'BML-App-Confirm', $pagetype = 'App-Confirm'
## Prefix 'BML-' is also used in the ad config file (%LJ::AD_MAPPING).
##
my $pagetype = $orient;
$pagetype =~ s/^BML-//;
# first 500 words
$pubtext =~ s/<.+?>//g;
$pubtext = text_trim($pubtext, 1000);
my @words = grep { $_ } split(/\s+/, $pubtext);
my $max_words = 500;
@words = @words[0..$max_words-1] if @words > $max_words;
$pubtext = join(' ', @words);
# first 15 tags
my $max_tags = 15;
my @tag_names;
if ($tags) {
@tag_names = scalar @$tags > $max_tags ? @$tags[0..$max_tags-1] : @$tags;
}
my $tag_list = join(',', @tag_names);
my $debug = $LJ::DEBUG{'ads'};
# are we constructing JavaScript adcalls?
my $use_js_adcall = LJ::conf_test($LJ::USE_JS_ADCALL) ? 1 : 0;
# Don't show an ad unless we're in debug mode, or our hook says so.
return '' unless $debug || LJ::run_hook('should_show_ad', {
ctx => $ctx,
user => $user,
type => $pagetype,
});
# If we don't know about this page type, can't do much of anything
my $ad_page_mapping = LJ::run_hook('get_page_mapping', $pagetype) || $LJ::AD_PAGE_MAPPING{$pagetype};
unless ($ad_page_mapping) {
warn "No mapping for page type $pagetype"
if $LJ::IS_DEV_SERVER && $LJ::AD_DEBUG;
return '';
}
my $r = Apache->request;
my %adcall = ();
# Make sure this mapping is correct for app ads, journal ads only call this function
# once when they directly want a specific type of ads. App ads on the other hand
# are called via the site scheme, so this function may be called half a dozen times
# on each page creation.
if ($ctx eq "app") {
my $uri = BML::get_uri();
$uri = $uri =~ /\/$/ ? "$uri/index.bml" : $uri;
# Try making the uri from request notes if it doesn't match
# and uri ends in .html
if (!LJ::check_page_ad_block($uri,$orient) && $r->header_in('Host') ne $LJ::DOMAIN_WEB) {
if ($uri = $r->notes('bml_filename')) {
$uri =~ s!$LJ::HOME/(?:ssldocs|htdocs)!!;
$uri = $uri =~ /\/$/ ? "$uri/index.bml" : $uri;
}
}
# Make sure that the page type passed in is what the config says this
# page actually is.
return '' unless LJ::check_page_ad_block($uri,$orient) || $opts{'force'};
# If it was a search provide the query to the targeting engine
# for more relevant results
$adcall{search_term} = LJ::get_search_term($uri, $search_arg);
# Special case talkpost.bml and talkpost_do.bml as user pages
if ($uri =~ /^\/talkpost(?:_do)?\.bml$/) {
$adcall{type} = 'user';
}
}
$adcall{adunit} = $ad_page_mapping->{adunit}; # ie skyscraper, FIXME: this is ignored by adserver now
my $addetails = LJ::run_hook('get_ad_details', $adcall{adunit}) || $LJ::AD_TYPE{$adcall{adunit}}; # hashref of meta-data or scalar to directly serve
return '' unless $addetails;
$adcall{channel} = $pagetype;
$adcall{type} = $adcall{type} || $ad_page_mapping->{target}; # user|content
$adcall{url} = 'http://' . $r->header_in('Host') . $r->uri;
$adcall{contents} = $pubtext;
$adcall{tags} = $tag_list;
$adcall{pos} = $position;
$adcall{cbg} = $colors->{bgcolor};
$adcall{ctext} = $colors->{fgcolor};
$adcall{cborder} = $colors->{bordercolor};
$adcall{clink} = $colors->{linkcolor};
$adcall{curl} = $colors->{linkcolor};
unless (ref $addetails eq "HASH") {
LJ::run_hooks('notify_ad_block', $addetails);
$LJ::ADV_PER_PAGE++;
return $addetails;
}
# addetails is a hashref now:
$adcall{width} = $addetails->{width};
$adcall{height} = $addetails->{height};
$adcall{vc} = $vertical;
$adcall{pn} = $page;
my $remote = LJ::get_remote();
if ($remote) {
# pass userid
$adcall{user} = $remote->id;
# Pass age to targeting engine
my $age = $remote->age_for_adcall;
$adcall{age} = $age if $age;
# Pass country to targeting engine if user shares this information
if ($remote->can_show_location) {
$adcall{country} = $remote->prop('country');
}
# Pass gender to targeting engine
$adcall{gender} = $remote->gender_for_adcall;
# User selected ad content categories
$adcall{categories} = $remote->prop('ad_categories');
# User's notable interests
$adcall{interests} = LJ::interests_for_adcall($remote, extra => $interests_extra);
# Pass email address domain
my $email = $remote->email_raw;
$email =~ /\.([^\.]+)$/;
$adcall{ed} = $1;
}
$adcall{gender} ||= "unknown"; # for logged-out users
if ($ctx eq 'journal') {
my $u = $opts{user} ? LJ::load_user($opts{user}) : LJ::load_userid($r->notes("journalid"));
$opts{entry} = LJ::Entry->new_from_url($adcall{url});
if ($u) {
if (!$adcall{categories} && !$adcall{interests}) {
# If we have neither categories or interests, load the content author's
$adcall{categories} = $u->prop('ad_categories');
$adcall{interests} = LJ::interests_for_adcall($u, extra => $interests_extra);
}
# set the country to the content author's
# if it's not set, and default the language to the author's language
$adcall{country} ||= $u->prop('country') if $u->can_show_location;
$adcall{language} = $u->prop('browselang');
# pass style info
my %GET = Apache->request->args;
my $styleu = $GET{style} eq "mine" && $remote ? $remote : $u;
my %style = LJ::get_style_for_ads($styleu);
$adcall{layout} = defined $style{layout} ? $style{layout} : "";
$adcall{theme} = defined $style{theme} ? $style{theme} : "";
# pass ad placement info
$adcall{adplacement} = LJ::S2::current_box_type($u);
}
}
# Language this page is displayed in
# set the language to the current user's preferences if they are logged in
$adcall{language} = $r->notes('langpref') if $remote;
$adcall{language} =~ s/_LJ//; # Trim _LJ postfixJ
# What type of account level do they have?
if ($remote) {
# Logged in? This is either a plus user or a basic user.
$adcall{accttype} = $remote->in_class('plus') ? 'ADS' : 'FREE';
} else {
# aNONymous user
$adcall{accttype} = 'NON';
}
$adcall{accttype} = $remote ?
$remote->in_class('plus') ? 'ADS' : 'FREE' : # Ads or Free if logged in
'NON'; # Not logged in
# incremental Ad ID within this page
my $adid = get_next_ad_id();
# specific params for SixApart adserver
$adcall{p} = 'lj';
if ($use_js_adcall) {
$adcall{f} = 'AdEngine.insertAdResponse';
$adcall{id} = "ad$adid";
}
# cache busting and unique-ad logic
my $pageview_uniq = LJ::pageview_unique_string();
$adcall{r} = "$pageview_uniq:$adid"; # cache buster
$adcall{pagenum} = $pageview_uniq; # unique ads
LJ::run_hook("transform_adcall", \%opts, \%adcall);
# Build up escaped query string of adcall parameters
my $adparams = LJ::encode_url_string(\%adcall,
[ sort { length $adcall{$a} <=> length $adcall{$b} }
grep { length $adcall{$_} }
keys %adcall ] );
# ghetto, but allow 24 bytes for escaping overhead
$adparams = substr($adparams, 0, 1_000);
my $adhtml;
# use adcall_url from hook if it specified one
$adcall_url ||= $LJ::ADSERVER;
# final adcall urls for iframe/js serving types
my $iframe_url = $hook_did_adurl ? $adcall_url : "$adcall_url/show?$adparams";
my $js_url = $hook_did_adurl ? $adcall_url : "$adcall_url/js/?$adparams";
if ($debug) {
my $ehpub = LJ::ehtml($pubtext) || "[no text targeting]";
$adhtml = "
$ehpub
\n";
} else {
# Iframe with call to ad targeting server
my $width = LJ::ehtml($adcall{width});
my $height = LJ::ehtml($adcall{height});
$width .= 'px' if $width =~ /\d$/;
$height .= 'px' if $height =~ /\d$/;
my $dim_style = "width: $width; height: $height";
# Call ad via JavaScript or iframe
if ($use_js_adcall && ! $hook_did_adurl) {
# TODO: Makes sure these ad calls don't get cached
$adhtml = "
";
$adhtml .= "";
$adhtml .= "
";
} else {
my $frameborder = (defined $opts{frameborder}) ? $opts{frameborder} : 0;
$adhtml = "";
}
}
$adhtml = LJ::ads_wrap($adhtml, \%opts, \%adcall, $iframe_url) unless $opts{nowrap};
LJ::run_hooks('notify_ad_block', $adhtml);
$LJ::ADV_PER_PAGE++;
return $adhtml;
}
sub ads_wrap {
my $inner_block = shift;
my $opts = shift;
my $adcall = shift;
my $iframe_url = shift;
my $remote = LJ::get_remote();
my $adhtml = "\n
{adunit}\" id=\"\">\n";
$adhtml .= LJ::run_hook('add_advertising_link');
# For leaderboards, entryboxes, and box ads, show links on the top right
if ($adcall->{adunit} =~ /^leaderboard/ || $adcall->{adunit} =~ /^entrybox/ || $adcall->{adunit} =~ /^medrect/) {
$adhtml .= "
\n";
if ($LJ::IS_DEV_SERVER || exists $LJ::DEBUG{'ad_url_markers'}) {
my $marker = $LJ::DEBUG{'ad_url_markers'} || '#';
# This is so while working on ad related problems I can easily open the iframe in a new window
$adhtml .= "$marker | \n";
}
$adhtml .= "" . LJ::Lang::ml('web.ads.customize') . "\n";
$adhtml .= "
\n";
}
$adhtml .= "";
$adhtml .= $inner_block;
# For non-leaderboards, non-entryboxes, and non-box ads, show links on the bottom right
unless ($adcall->{adunit} =~ /^leaderboard/ || $adcall->{adunit} =~ /^entrybox/ || $adcall->{adunit} =~ /^medrect/) {
$adhtml .= "
\n";
if ($LJ::IS_DEV_SERVER || exists $LJ::DEBUG{'ad_url_markers'}) {
my $marker = $LJ::DEBUG{'ad_url_markers'} || '#';
# This is so while working on ad related problems I can easily open the iframe in a new window
$adhtml .= "$marker | \n";
}
$adhtml .= "" . LJ::Lang::ml('web.ads.customize') . "\n";
$adhtml .= "
\n";
}
$adhtml .= "
\n";
return $adhtml;
}
# this function will filter out blocked interests, as well filter out interests which
# cause the
sub interests_for_adcall {
my $u = shift;
my %opts = @_;
# base ad call is 300-400 bytes, we'll allow interests to be around 600
# which is unlikely to go over IE's 1k URL limit.
my $max_len = $opts{max_len} || 600;
my $int_len = 0;
my @interest_list = $u->notable_interests(100) if $u;
# pass in tag list if this ad relates to a special QotD
if (ref $opts{extra} && $opts{extra}->{qotd}) {
my $qotd = ref $opts{extra}->{qotd} ? $opts{extra}->{qotd} : LJ::QotD->get_single_question($opts{extra}->{qotd});
my $tags = LJ::QotD->remove_default_tags($qotd->{tags});
if ($tags && $qotd->{is_special} eq "Y" && $qotd->{time_start} <= time() && $qotd->{time_end} >= time() && $qotd->{active} eq "Y") {
unshift @interest_list, $tags;
}
}
return join(',',
grep {
# not a blocked interest
! defined $LJ::AD_BLOCKED_INTERESTS{$_} &&
# and we've not already got over 768 bytes of interests
# -- +1 is for comma
($int_len += length($_) + 1) <= $max_len;
} @interest_list
);
}
# for use when calling an ad from BML directly
sub ad_display {
my %opts = @_;
# can specify whether the wrapper div on the ad is used or not
my $use_wrapper = defined $opts{use_wrapper} ? $opts{use_wrapper} : 1;
my $ret = LJ::ads(%opts);
my $extra;
if ($ret =~ /"ljad ljad(.+?)"/i) {
# Add a badge ad above all skyscrapers
# First, try to print a badge ad in journal context (e.g. S1 comment pages)
# Then, if it doesn't print, print it in user context (e.g. normal app pages)
if ($1 eq "skyscraper") {
$extra = LJ::ads(type => $opts{'type'},
orient => 'Journal-Badge',
user => $opts{'user'},
search_arg => $opts{'search_arg'},
force => '1' );
$extra = LJ::ads(type => $opts{'type'},
orient => 'App-Extra',
user => $opts{'user'},
search_arg => $opts{'search_arg'},
force => '1' )
unless $extra;
}
$ret = $extra . $ret
}
my $pagetype = $opts{orient};
$pagetype =~ s/^BML-//;
$pagetype = lc $pagetype;
$ret = $opts{below_ad} ? "$ret $opts{below_ad}" : $ret;
$ret = $ret && $use_wrapper ? "
";
}
sub control_strip_js_inject
{
my %opts = @_;
my $user = delete $opts{user};
my $ret;
$ret .= "\n";
$ret .= "\n";
$ret .= "\n";
LJ::need_res(qw(
js/livejournal.js
js/md5.js
js/login.js
));
$ret .= qq{
};
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 && !LJ::get_cap($remote, 'makepoll'));
$ret .= "^;
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 $catref = delete $opts{'categories'};
my $journalu = delete $opts{'journal'} || LJ::get_remote();
my $formauth = delete $opts{'formauth'} || LJ::form_auth();
my $showtracking = delete $opts{'showtracking'} || 0;
my $getextra = delete $opts{'getextra'} || '';
my $ret_url = delete $opts{ret_url} || '';
my $def_notes = delete $opts{'default_selected_notifications'} || [];
my $settings_page= delete $opts{'settings_page'} || 0;
my $post_to_settings_page = delete $opts{'post_to_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);
LJ::need_res('stc/esn.css');
LJ::need_res('js/core.js');
LJ::need_res('js/dom.js');
LJ::need_res('js/checkallbutton.js');
LJ::need_res('js/esn.js');
my @categories = $catref ? @$catref : ();
my $ui_inbox = BML::ml('subscribe_interface.inbox');
my $ui_manage = BML::ml('subscribe_interface.manage_settings');
my $ui_notify = BML::ml('subscribe_interface.notify_me2', { sitenameabbrev => $LJ::SITENAMEABBREV });
my $ui_by = BML::ml('subscribe_interface.by2');
my $ret = "