#!/usr/bin/perl # # This code was forked from the LiveJournal project owned and operated # by Live Journal, Inc. The code has been modified and expanded by # Dreamwidth Studios, LLC. These files were originally licensed under # the terms of the license supplied by Live Journal, Inc, which can # currently be found at: # # http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt # # In accordance with the original license, this code and all its # modifications are provided under the GNU General Public License. # A copy of that license can be found in the LICENSE file included as # part of this distribution. package LJ; use strict; use Carp; use POSIX; use Digest::MD5; use Digest::SHA1; use DW::Auth::Challenge; use DW::External::Site; use DW::Request; use DW::Formats; use LJ::Utils qw(rand_chars); use LJ::Global::Constants; use LJ::Event; use LJ::Subscription::Pending; use LJ::Directory::Search; use LJ::Directory::Constraint; use LJ::PageStats; use LJ::JSON; # # 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/LJ/Global/Img.pm but can be # overridden in cgi-bin/LJ/Local/Img.pm or etc/config.pl. # args: imagecode, type?, attrs? # des-imagecode: The unique string key to reference the image. Not a filename, # but the purpose or location of the image. # des-type: By default, the tag returned is an <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, $type, $attr ) = @_; $type //= ''; # Type is either "" or "input" my ( $attrs, $alt ) = ( '', '' ); if ($attr) { if ( ref $attr eq "HASH" ) { if ( exists $attr->{alt} ) { $alt = LJ::ehtml( $attr->{alt} ); delete $attr->{alt}; } $attrs .= " $_=\"" . LJ::ehtml( $attr->{$_} || '' ) . "\"" foreach keys %$attr; } else { $attrs = " id=\"$attr\""; } } my $i = $LJ::Img::img{$ic}; $alt ||= LJ::Lang::string_exists( $i->{alt} ) ? LJ::Lang::ml( $i->{alt} ) : $i->{alt}; if ( $type eq "" ) { return "{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 $base = $u->journal_base; 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->( $1 ); !ge if defined $str; return $str; } # return 1 if URL is a safe stylesheet that S1/S2/etc can pull in. # return 0 to reject the link tag # return a URL to rewrite the stylesheet URL # $href will always be present. $host and $path may not. sub valid_stylesheet_url { my ( $href, $host, $path ) = @_; unless ( $host && $path ) { return 0 unless $href =~ m!^https?://([^/]+?)(/.*)$!; ( $host, $path ) = ( $1, $2 ); } my $cleanit = sub { # allow tag, if we're doing no css cleaning return 1 unless LJ::is_enabled('css_cleaner'); # remove tag, if we have no CSSPROXY configured return 0 unless $LJ::CSSPROXY; # rewrite tag for CSS cleaning return "$LJ::CSSPROXY?u=" . LJ::eurl($href); }; return 1 if $LJ::TRUSTED_CSS_HOST{$host}; return $cleanit->() unless $host =~ /\Q$LJ::DOMAIN\E$/i; # let users use system stylesheets. return 1 if $host eq $LJ::DOMAIN || $host eq $LJ::DOMAIN_WEB || $href =~ /^\Q$LJ::STATPREFIX\E/; # S2 stylesheets: return 1 if $path =~ m!^(/~\w+|/users/\w+|/\w+)?/res/(\d+)/stylesheet(\?\d+)?$!; # unknown, reject. return $cleanit->(); } # # name: LJ::make_authas_select # des: Given a u object and some options, determines which users the given user # can switch to. If the list exists, returns a select list and a submit # button with labels. Otherwise returns a hidden element. # returns: string of HTML elements # args: u, opts? # des-opts: Optional. Valid keys are: # 'authas' - current user, gets selected in drop-down; # 'label' - label to go before form elements; # 'button' - button label for submit button; # 'type' - journaltype (affects label & list filtering) # 'selectonly' - 1 for just menu items, otherwise entire HTML block # 'foundation' - 1 for Foundation HTML, otherwise legacy HTML # others - arguments to pass to $u->get_authas_list. # sub make_authas_select { my ( $u, $opts ) = @_; # type, authas, label, button my $authas = $opts->{authas} || $u->user; my $button = $opts->{button} || $BML::ML{'web.authas.btn'}; my $label = $opts->{label} || $BML::ML{'web.authas.select.label'}; my $foundation = $opts->{foundation} || 0; my @list = $u->get_authas_list($opts); # only do most of form if there are options to select from if ( @list > 1 || $list[0] ne $u->user ) { my $menu = LJ::html_select( { name => 'authas', selected => $authas, class => 'hideable', id => 'authas', style => $foundation ? 'width: 100%' : '', }, map { $_, $_ } @list ); my $ret = ''; if ( $opts->{selectonly} ) { $ret = $menu; } else { $ret = $foundation ? q{
} . q{
} . q{
} . $menu . q{
} . q{
} . LJ::html_submit( undef, $button, { class => "secondary button" } ) . q{
} . q{
} . q{
} # else not foundation : "
" . LJ::Lang::ml( 'web.authas.select', { menu => $menu, username => LJ::ljuser($authas) } ) . " " . LJ::html_submit( undef, $button ) . "

\n"; } return $ret; } # no communities to choose from, give the caller a hidden return LJ::html_hidden( authas => $authas ); } # # name: LJ::help_icon # des: Returns BML to show a help link/icon given a help topic, or nothing # if the site hasn't defined a URL for that topic. Optional arguments # include HTML/BML to place before and after the link/icon, should it # be returned. # args: topic, pre?, post? # des-topic: Help topic key. # See etc/config-local.pl, or [special[helpurls]] for examples. # des-pre: HTML/BML to place before the help icon. # des-post: HTML/BML to place after the help icon. # 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 || ""; return "$pre" . LJ::img( 'help', '' ) . "$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"; 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 .= " 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 .= " warningbar?>"; return $ret; } # # 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 { LJ::page_change_getargs( page => $_[0] ) }; my $href_opts = $opts->{href_opts} || sub { '' }; my $nav = ''; return $nav unless $pages && $pages > 1; $nav .= "

"; $nav .= LJ::Lang::ml( 'ljlib.pageofpages', { page => $page, total => $pages } ); $nav .= "

\n"; my $linkify = sub { "( $_[0] ) . ">$_[1]\n"; }; my ( $left, $right ) = ( "<<", ">>" ); $left = $linkify->( $page - 1, $left ) if $page > 1; $right = $linkify->( $page + 1, $right ) if $page < $pages; my @pagelinks; for ( my $i = 1 ; $i <= $pages ; $i++ ) { my $link = "[$i]"; $link = ( $i != $page ) ? $linkify->( $i, $link ) : "$link"; push @pagelinks, "
" if $i > 10 && ( $i == 11 || $i % 10 == 0 ); push @pagelinks, $link; } $nav .= "$left   "; $nav .= ""; $nav .= join ' ', @pagelinks; $nav .= ""; $nav .= "   $right"; return "
$nav
\n"; } =head2 C<< LJ::page_change_getargs( %args ) >> Returns the current URL with a modified list of GET arguments. =cut sub page_change_getargs { my %args = @_; my %cu_opts = ( keep_args => 1, no_blank => 1 ); # specified args will override keep_args return LJ::create_url( undef, args => \%args, %cu_opts ); } =head2 C<< LJ::paging( $listref, $page, $pagesize ) >> Drop-in replacement for BML::paging in non-BML context. =cut sub paging { my ( $listref, $page, $pagesize ) = @_; $page = 1 unless $page && $page == int $page; return unless $pagesize; # let's not divide by zero my @items = @{$listref}; my %self; my $newurl = sub { # replaces BML::page_newurl return LJ::page_change_getargs( page => $_[0] ); }; $self{itemcount} = scalar @items; $self{pages} = $self{itemcount} / $pagesize; $self{pages} = int( $self{pages} ) + 1 if $self{pages} != int( $self{pages} ); # round up any fraction $page = 1 if $page < 1; $page = $self{pages} if $page > $self{pages}; $self{page} = $page; $self{itemfirst} = $pagesize * ( $page - 1 ) + 1; $self{itemlast} = $pagesize * $page; $self{itemlast} = $self{itemcount} if $self{pages} == $page; my @range = ( $self{itemfirst} - 1 ) .. ( $self{itemlast} - 1 ); $self{items} = [ @items[@range] ]; my ( $prev, $next ) = ( $newurl->( $page - 1 ), $newurl->( $page + 1 ) ); $self{backlink} = "<<<" unless $page == 1; $self{nextlink} = ">>>" unless $page == $self{pages}; return %self; } # # 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; } # # 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; my ( $origuri, $origreferer ) = ( $uri, $referer ); # escape any regex characters, like the '.' in '.bml' $uri = quotemeta($uri); # check that the end of the uri matches exactly (no extra characters or dir levels) # or else that the uri is followed immediately by additional parameters my $checkend = '(?:$|\\?)'; # allow us to properly check URIs without .bml extensions if ( $origuri =~ /\.bml($|\?)/ ) { $checkend = '' if $1 eq '?'; $uri =~ s/\\.bml($|\\\?)/$1$checkend/; $referer =~ s/\.bml($|\?)/$1/; } elsif ($uri) { $uri .= $checkend; } else { $uri = '(/|$)'; } return 1 if $LJ::SITEROOT && $referer =~ m!^\Q$LJ::SITEROOT\E$uri!; return 1 if $LJ::DOMAIN && $referer =~ m!^https?://\Q$LJ::DOMAIN\E$uri!; return 1 if $LJ::DOMAIN_WEB && $referer =~ m!^https?://\Q$LJ::DOMAIN_WEB\E$uri!; return 1 if $referer =~ m!^https?://([A-Za-z0-9_\-]{1,25})\.\Q$LJ::DOMAIN\E$uri!; return 1 if $origuri =~ m!^https?://! && $origreferer eq $origuri; # Dev container: $LJ::DOMAIN is empty, so match referer host against request host if ( $LJ::IS_DEV_SERVER && !$LJ::DOMAIN ) { my $r = eval { DW::Request->get }; if ($r) { my $host = $r->host; return 1 if $referer =~ m!^https?://\Q$host\E$uri!; } } return undef; } # # 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 = DW::Auth::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 = DW::Auth::Challenge->get_attributes($formauth); my ( $randchars, $chal_id, $chal_sess ) = split( /\-/, $attr ); return 0 unless $id == $chal_id; return 0 unless $sess eq $chal_sess; # check the signature is good and not expired my $opts = { dont_check_count => 1 }; # in/out DW::Auth::Challenge->check( $formauth, $opts ); return $opts->{valid} && !$opts->{expired}; } # # name: LJ::create_qr_div # class: web # des: Creates the hidden div that stores the QuickReply form. # returns: undef upon failure or HTML for the div upon success # args: user, remote, ditemid, style args, userpic, viewing thread # des-u: user object or userid for journal reply in. # des-ditemid: ditemid for this comment. # des-style_opts: the viewing style arguments on this page, as a hashref. # des-userpic: alternate default userpic. # sub create_qr_div { my ( $user, $ditemid, %opts ) = @_; my $u = LJ::want_user($user); my $remote = LJ::get_remote(); return undef unless $u && $remote && $ditemid; my $style_opts = $opts{style_opts} || {}; my $userpic_kw = $opts{userpic}; my $viewing_thread = $opts{thread}; return undef if $remote->prop("opt_no_quickreply"); my $e = LJ::Entry->new( $u, ditemid => $ditemid ); my $separator = %$style_opts ? "&" : "?"; my $basepath = $e->url( style_opts => LJ::viewing_style_opts(%$style_opts) ) . $separator; my $usertype = ( $remote->openid_identity && $remote->is_validated ) ? 'openid_cookie' : 'cookieuser'; my $hidden_form_elements .= LJ::html_hidden( { 'name' => 'replyto', 'id' => 'replyto', 'value' => '' }, { 'name' => 'parenttalkid', 'id' => 'parenttalkid', 'value' => '' }, # ^ these two inputs are duplicates, but oh well. { 'name' => 'journal', 'id' => 'journal', 'value' => $u->{'user'} }, { 'name' => 'itemid', 'id' => 'itemid', 'value' => $ditemid }, { 'name' => 'usertype', 'id' => 'usertype', 'value' => $usertype }, { 'name' => 'qr', 'id' => 'qr', 'value' => '1' }, { 'name' => 'cookieuser', 'id' => 'cookieuser', 'value' => $remote->{'user'} }, { 'name' => 'dtid', 'id' => 'dtid', 'value' => '' }, # ^ the "display" version of replyto/parenttalkid. Starts empty, set by # JS when QR is summoned, then used to build the "more options" URL. # Nothing uses this after the form is submitted, it's just for JS. { 'name' => 'basepath', 'id' => 'basepath', 'value' => $basepath }, { 'name' => 'viewing_thread', 'id' => 'viewing_thread', 'value' => $viewing_thread }, ); while ( my ( $key, $value ) = each %$style_opts ) { $hidden_form_elements .= LJ::html_hidden( { name => $key, id => $key, value => $value } ); } # rate limiting challenge { my ( $time, $secret ) = LJ::get_secret(); my $rchars = LJ::rand_chars(20); my $chal = $ditemid . "-$u->{userid}-$time-$rchars"; my $res = Digest::MD5::md5_hex( $secret . $chal ); $hidden_form_elements .= LJ::html_hidden( "chrp1", "$chal-$res" ); } # hashref with "selected" and "items" keys my $editors = DW::Formats::select_items( preferred => $remote->prop('comment_editor'), ); # FIXME: This is incoherent on reading/network pages. (But it's only # noticeable when viewing the reading/network page of someone who wouldn't # allow you to comment.) -NF my $post_disabled = $u->does_not_allow_comments_from($remote); return DW::Template->template_string( 'journal/quickreply.tt', { form_url => LJ::create_url( '/talkpost_do', host => $LJ::DOMAIN_WEB ), hidden_form_elements => $hidden_form_elements, post_disabled => $post_disabled, post_button_class => $post_disabled ? 'ui-state-disabled' : '', # Currently unused, but might come back. minimal => $opts{minimal} ? 1 : 0, current_icon_kw => $userpic_kw, current_icon => LJ::Userpic->new_from_keyword( $remote, $userpic_kw ), editors => $editors, foundation_beta => !LJ::BetaFeatures->user_in_beta( $remote => "nos2foundation" ), remote => $remote, journal => { is_iplogging => $u->opt_logcommentips eq 'A', is_linkstripped => !$remote || ( $remote && $remote->is_identity && !$u->trusts_or_has_member($remote) ), }, help => { icon => LJ::help_icon_html( "userpics", " " ), iplogging => LJ::help_icon_html( "iplogging", " " ), }, } ); } # # 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(); my ( $talkid, $jid ); # Figure out their last post if ($remote) { my $memkey = [ $remote->{'userid'}, "lastcomm:$remote->{'userid'}" ]; my $memval = LJ::MemCache::get($memkey); ( $jid, $talkid ) = split( /:/, $memval ) if $memval; } return ( $talkid, $jid ); } # # 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]"; } =head2 C<< LJ::determine_viewing_style( $args, $view, $u ) >> Takes a hashref of get args, and the current view, and an optional user. Returns "original", "mine", "site", or "light" as the style. =cut sub determine_viewing_style { my ( $args, $view, $u ) = @_; my $style = 'original'; # incorporate any user preferences $style = $u->viewing_style($view) if $u; # incorporate any style arguments my %style_getargs = %{ LJ::viewing_style_opts(%$args) }; $style = $style_getargs{'style'} if $style_getargs{'style'}; # keep format=light for backwards compatibility -- override and # assume that somebody using it really wants the light version $style = $style_getargs{'format'} if $style_getargs{'format'}; return $style; } =head2 C<< LJ::viewing_style_args( %arguments ) >> Takes a list of viewing styles arguments from a list, makes sure they are valid values, and returns them as a string that can be appended to the URL. Looks for "s2id", "format", "style" =cut sub viewing_style_args { #fixme - this should be modernised to take a hashref rather than a hash my (%args) = @_; %args = %{ LJ::viewing_style_opts(%args) }; my @valid_args; while ( my ( $key, $value ) = each %args ) { push @valid_args, "$key=$value"; } return join "&", @valid_args; } =head2 C<< LJ::viewing_style_opts( %arguments ) >> Takes a list of viewing styles arguments from a list, and returns a hashref of valid values =cut sub viewing_style_opts { #fixme - this should be modernised to take a hashref rather than a hash my (%args) = @_; return {} unless %args; my $valid_style_args = { style => { light => 1, site => 1, mine => 1, original => 1 }, format => { light => 1 }, fallback => { s2 => 1, bml => 1 }, }; my %ret; # only accept purely numerical s2ids $ret{s2id} = $args{s2id} if $args{s2id} && $args{s2id} =~ /^\d+$/; foreach my $key ( keys %{$valid_style_args} ) { $ret{$key} = $args{$key} if $args{$key} && $valid_style_args->{$key}->{ $args{$key} }; } return \%ret; } =head2 C<< LJ::create_url($path,%opts) >> If specified, path must begin with a / args being a list of arguments to create. opts can contain: proto -- specify a protocol host -- link to different domains args -- get arguments to add fragment -- add fragment identifier cur_args -- hashref of current GET arguments to the page keep_args -- arguments to keep keep_query_string -- keep the raw query string (ignores keep_args) no_blank -- remove keys with null values from GET args viewing_style -- include viewing style args =cut sub create_url { my ( $path, %opts ) = @_; my $r = DW::Request->get; my %out_args = %{ $opts{args} || {} }; my $host = lc( $opts{host} || $r->host ); $path ||= $r->uri; my $proto = $opts{proto} // $LJ::PROTOCOL; my $url = $proto . "://$host$path"; # TWO PATHS: if keep_query_string is used, we simply preserve that # with no further logic. If not, however, we perform arguments logic. my $args; if ( $opts{keep_query_string} ) { $args = $r->query_string; } else { my $orig_args = $opts{cur_args} || $r->get_args( preserve_case => 1 ); # Move over viewing style arguments if ( $opts{viewing_style} ) { my $vs_args = LJ::viewing_style_opts(%$orig_args); foreach my $k ( keys %$vs_args ) { $out_args{$k} = $vs_args->{$k} unless exists $out_args{$k}; } } $opts{keep_args} = [ keys %$orig_args ] if defined $opts{keep_args} and $opts{keep_args} == 1; $opts{keep_args} = [] if ref $opts{keep_args} ne 'ARRAY'; # Move over arguments that we need to keep foreach my $k ( @{ $opts{keep_args} } ) { $out_args{$k} = $orig_args->{$k} if exists $orig_args->{$k} && !exists $out_args{$k}; } foreach my $k ( keys %out_args ) { if ( !defined $out_args{$k} ) { delete $out_args{$k}; } elsif ( !length $out_args{$k} ) { delete $out_args{$k} if $opts{no_blank}; } } $args = LJ::encode_url_string( \%out_args, [ sort keys %out_args ] ); } $url .= "?$args" if $args; $url .= "#" . $opts{fragment} if $opts{fragment}; return $url; } # # 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 ( $moodlist, $moodpics ); # usejournal has no point if you're trying to use the account you're logged in as, # so disregard it so we can assume that if it exists, we're trying to post to an # account that isn't us if ( $remote && $opts->{usejournal} && $remote->{user} eq $opts->{usejournal} ) { delete $opts->{usejournal}; } # Temp fix for FF 2.0.0.17 my $rte_is_supported = LJ::is_enabled( 'rte_support', BML::get_client_header("User-Agent") ); $opts->{'richtext_default'} = 0 unless $rte_is_supported; $opts->{'richtext'} = $opts->{'richtext_default'}; my $tabnum = 10; #make allowance for username and password # Leave gaps for interpolated fields eg date/time my $tabindex = sub { return ( $tabnum += 10 ) - 10; }; $opts->{'event'} = LJ::durl( $opts->{'event'} ) if $opts->{'mode'} eq "edit"; $out .= "\n\n
\n"; $out .= LJ::error_list( $errors->{entry} ) if $errors->{entry}; ### Icon Selection my $pic = ''; # displays chosen/default pic my $picform = ''; # displays form drop-down LJ::Widget::UserpicSelector->render( picargs => [ $remote, \$$head, \$pic, \$picform ], prop_picture_keyword => $opts->{prop_picture_keyword}, no_auth => !$opts->{auth_as_remote}, onload => $onload, altlogin => $altlogin, entry_js => 1 ); # libs for userpicselect LJ::Talk::init_iconbrowser_js() if !$altlogin && $remote && $remote->can_use_userpic_select; $out .= $pic; ### Meta Information Column 1 { # do a login action to get usejournals, but only if using remote my $res; $res = LJ::Protocol::do_request( "login", { ver => $LJ::PROTOCOL_VER, username => $remote->user, }, undef, { noauth => 1, u => $remote, } ) if $opts->{auth_as_remote}; $out .= "
\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"; } elsif ( $res && ref $res->{'usejournals'} eq 'ARRAY' ) { my $submitprefix = BML::ml('entryform.update3'); $out .= "

\n"; $out .= "\n"; $out .= LJ::html_select( { 'name' => 'usejournal', 'id' => 'usejournal', 'selected' => $usejournal, 'tabindex' => $tabindex->(), 'class' => 'select', "onchange" => "changeSubmit('" . $submitprefix . "','" . $remote->{'user'} . "'); getUserTags('$remote->{user}'); changeSecurityOptions('$remote->{user}'); XPostAccount.updateXpostFromJournal('$remote->{user}');" }, "", $remote->{'user'}, map { $_, $_ } @{ $res->{'usejournals'} } ) . "\n"; $out .= "

\n"; } } # Authentication box $out .= "

{'auth'} inerr?>

\n" if $errors->{'auth'}; # Date / Time { my ( $year, $mon, $mday, $hour, $min ) = split( /\D/, $opts->{'datetime'} ); my $monthlong = LJ::Lang::month_long($mon); # date entry boxes / formatting note my $datetime = LJ::html_datetime( { name => 'date_ymd', notime => 1, default => "$year-$mon-$mday", tabindex => $tabindex->(), disabled => $opts->{'disabled_save'} } ); $datetime .= "  "; $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 .= "

\n"; $out .= "\n"; $out .= "$monthlong $mday, $year, $hour" . ":" . "$min " . BML::ml('entryform.date.edit') . "\n"; $out .= "$datetime
\n"; $out .= LJ::html_check( { 'type' => "check", 'id' => "prop_opt_backdated", 'name' => "prop_opt_backdated", "value" => 1, 'selected' => $opts->{'prop_opt_backdated'}, 'tabindex' => $tabindex->() } ); $out .= "\n"; $out .= LJ::help_icon_html( "backdate", "", "" ) . "\n"; $out .= "
\n"; $out .= "

\n"; $out .= "\n"; $$onload .= " defaultDate();"; } # User Picture { my $tab = $tabindex->(); $picform =~ s/~~TABINDEX~~/$tab/; $out .= $picform; } $out .= "
\n\n"; ### Other Posting Options { $out .= "
\n"; $out .= LJ::Hooks::run_hook( 'entryforminfo', $opts->{'usejournal'}, $opts->{'remote'} ); $out .= "
\n\n"; } ### Subject $out .= "
\n"; $out .= "\n"; $out .= LJ::html_text( { 'name' => 'subject', 'value' => $opts->{'subject'}, 'class' => 'text', 'id' => 'subject', 'size' => '43', 'maxlength' => '100', 'tabindex' => $tabindex->(), 'disabled' => $opts->{'disabled_save'} } ) . "\n"; $out .= ""; $out .= "
\n\n"; $$onload .= " showEntryTabs();"; } ### Display Spell Check Results: $out .= "
" . BML::ml('entryform.spellchecked') . "
$opts->{'spellcheck_html'}
\n" if $opts->{'spellcheck_html'}; ### Insert Object Toolbar: LJ::need_res( qw( js/6alib/core.js js/6alib/dom.js js/6alib/ippu.js js/lj_ippu.js ) ); $out .= "
\n"; $out .= "\n"; my $format_selected = ( $opts->{mode} eq "update" && $remote && $remote->disable_auto_formatting ) || $opts->{'prop_opt_preformatted'} || $opts->{'event_format'} ? "checked='checked'" : ""; $out .= " " . LJ::help_icon_html( "noautoformat", "", " " ) . "\n"; $out .= "
\n\n"; ### Draft Status Area $out .= "
\n"; $out .= LJ::html_textarea( { 'name' => 'event', 'value' => $opts->{'event'}, 'rows' => '20', 'cols' => '50', 'style' => '', 'tabindex' => $tabindex->(), 'wrap' => 'soft', 'disabled' => $opts->{'disabled_save'}, 'id' => 'draft' } ) . "\n"; $out .= "
\n\n"; $out .= "\n\n"; LJ::need_res( 'stc/fck/fckeditor.js', 'js/rte.js', 'stc/display_none.css' ); if ( !$opts->{'did_spellcheck'} ) { my $jnorich = LJ::ejs( LJ::deemp( BML::ml('entryform.htmlokay.norich2') ) ); $out .= < RTE $out .= ''; } $out .= LJ::html_hidden( { name => 'switched_rte_on', id => 'switched_rte_on', value => '0' } ); $out .= "
"; if ( !$opts->{'disabled_save'} ) { ### Options # Tag labeling if ( LJ::is_enabled('tags') ) { $out .= "

"; $out .= ""; $out .= LJ::html_text( { 'name' => 'prop_taglist', 'id' => 'prop_taglist', 'class' => 'text', 'size' => '35', 'value' => $opts->{'prop_taglist'}, 'tabindex' => $tabindex->(), 'raw' => "autocomplete='off'", } ); $out .= LJ::help_icon_html('addtags'); $out .= "

"; } $out .= "

\n"; $out .= "\n"; $out .= ""; # Current Mood { my @moodlist = ( '', BML::ml('entryform.mood.noneother') ); my $sel; my $moods = DW::Mood->get_moods; foreach ( sort { $moods->{$a}->{'name'} cmp $moods->{$b}->{'name'} } keys %$moods ) { push @moodlist, ( $_, $moods->{$_}->{'name'} ); if ( $opts->{prop_current_mood} && $opts->{prop_current_mood} eq $moods->{$_}->{name} || $opts->{prop_current_moodid} && $opts->{prop_current_moodid} == $_ ) { $sel = $_; } } if ($remote) { my $r_theme = DW::Mood->new( $remote->{'moodthemeid'} ); foreach my $mood ( keys %$moods ) { my $moodid = $moods->{$mood}->{id}; if ( $r_theme && $r_theme->get_picture( $moodid, \my %pic ) ) { $moodlist .= " moods[" . $moodid; $moodlist .= "] = \""; $moodlist .= $moods->{$mood}->{name} . "\";\n"; $moodpics .= " moodpics[" . $moodid; $moodpics .= "] = \""; $moodpics .= $pic{pic} . "\";\n"; } } $$onload .= " mood_preview();"; $$head .= < MOODS } my $moodpreviewoc; $moodpreviewoc = 'mood_preview()' if $remote; $out .= LJ::html_select( { 'name' => 'prop_current_moodid', 'id' => 'prop_current_moodid', 'selected' => $sel, 'onchange' => $moodpreviewoc, 'class' => 'select', 'tabindex' => $tabindex->() }, @moodlist ); $out .= " " . LJ::html_text( { 'name' => 'prop_current_mood', 'id' => 'prop_current_mood', 'class' => 'text', 'value' => $opts->{'prop_current_mood'}, 'onchange' => $moodpreviewoc, 'size' => '15', 'maxlength' => '30', 'tabindex' => $tabindex->() } ); } $out .= ""; $out .= "\n"; $out .= "\n"; $out .= "\n"; # Comment Settings my $comment_settings_selected = sub { return "noemail" if $opts->{'prop_opt_noemail'}; return "nocomments" if $opts->{prop_opt_nocomments} || $opts->{prop_opt_nocomments_maintainer}; return $opts->{'comment_settings'}; }; my $comment_settings_journaldefault = sub { return "Disabled" if $opts->{prop_opt_default_nocomments} && $opts->{prop_opt_default_nocomments} eq 'N'; return "No Email" if $opts->{prop_opt_default_noemail} && $opts->{prop_opt_default_noemail} eq 'N'; return "Enabled"; }; my $nocomments_display = $opts->{prop_opt_nocomments_maintainer} ? 'entryform.comment.settings.nocomments.admin' : 'entryform.comment.settings.nocomments'; my $comment_settings_default = BML::ml( 'entryform.comment.settings.default5', { 'aopts' => $comment_settings_journaldefault->() } ); $out .= LJ::html_select( { 'name' => "comment_settings", 'id' => 'comment_settings', 'class' => 'select', 'selected' => $comment_settings_selected->(), 'tabindex' => $tabindex->() }, "", $comment_settings_default, "nocomments", BML::ml( $nocomments_display, "noemail" ), "noemail", BML::ml('entryform.comment.settings.noemail') ); $out .= LJ::help_icon_html( "comment", "", " " ); $out .= "\n"; $out .= "\n"; $out .= "

\n"; # Current Location $out .= "

"; if ( LJ::is_enabled('web_current_location') ) { $out .= ""; $out .= ""; $out .= LJ::html_text( { name => 'prop_current_location', value => $opts->{prop_current_location}, id => 'prop_current_location', class => 'text', size => '35', maxlength => LJ::std_max_length(), tabindex => $tabindex->() } ) . "\n"; $out .= ""; } # Comment Screening settings $out .= "\n"; $out .= "\n"; my $opt_default_screen = $opts->{prop_opt_default_screening} || ''; my $screening_levels_default = $opt_default_screen eq 'N' ? BML::ml('label.screening.none2') : $opt_default_screen eq 'R' ? BML::ml('label.screening.anonymous2') : $opt_default_screen eq 'F' ? BML::ml('label.screening.nonfriends2') : $opt_default_screen eq 'A' ? BML::ml('label.screening.all2') : BML::ml('label.screening.none2'); my @levels = ( '', BML::ml( 'label.screening.default4', { 'aopts' => $screening_levels_default } ), 'N', BML::ml('label.screening.none2'), 'R', BML::ml('label.screening.anonymous2'), 'F', BML::ml('label.screening.nonfriends2'), 'A', BML::ml('label.screening.all2') ); $out .= LJ::html_select( { 'name' => 'prop_opt_screening', 'id' => 'prop_opt_screening', 'class' => 'select', 'selected' => $opts->{'prop_opt_screening'}, 'tabindex' => $tabindex->() }, @levels ); $out .= LJ::help_icon_html( "screening", "", " " ); $out .= "\n"; $out .= "

\n"; # Current Music $out .= "

\n"; $out .= "\n"; $out .= "\n"; # BML::ml('entryform.music') $out .= LJ::html_text( { name => 'prop_current_music', value => $opts->{prop_current_music}, id => 'prop_current_music', class => 'text', size => '35', maxlength => LJ::std_max_length(), tabindex => $tabindex->() } ) . "\n"; $out .= "\n"; $out .= ""; # Content Flag if ( LJ::is_enabled('adult_content') ) { my @adult_content_menu = ( "" => BML::ml('entryform.adultcontent.default'), none => BML::ml('entryform.adultcontent.none'), concepts => BML::ml('entryform.adultcontent.concepts'), explicit => BML::ml('entryform.adultcontent.explicit'), ); $out .= "\n"; $out .= LJ::html_select( { name => 'prop_adult_content', id => 'prop_adult_content', class => 'select', selected => $opts->{prop_adult_content} || "", tabindex => $tabindex->(), }, @adult_content_menu ); $out .= LJ::help_icon_html( "adult_content", "", " " ); } $out .= "\n"; $out .= "

\n"; if ( LJ::is_enabled('adult_content') ) { $out .= "

"; $out .= ""; $out .= LJ::html_text( { 'name' => 'prop_adult_content_reason', 'id' => 'prop_adult_content_reason', 'class' => 'text', 'size' => '35', 'maxlength' => '255', 'value' => $opts->{'prop_adult_content_reason'}, 'tabindex' => $tabindex->(), } ); $out .= LJ::help_icon_html('adult_content_reason'); $out .= "

"; } if ( $remote && !$altlogin ) { # crosspost my @accounts = DW::External::Account->get_external_accounts($remote); # populate the per-account html first, so that we only have to # go through them once. my $accthtml = ""; my $xpostbydefault = 0; my $xpost_tabindex = $tabindex->(); my $did_spellcheck = $opts->{spellcheck_html} ? 1 : 0; if ( scalar @accounts ) { my $xpoststring = $opts->{prop_xpost}; my $xpost_selected = DW::External::Account->xpost_string_to_hash($xpoststring); foreach my $acct (@accounts) { # print the checkbox for each account my $acctid = $acct->acctid; my $acctname = $acct->displayname; my $selected; if ( $opts->{mode} eq 'edit' ) { $selected = $xpost_selected->{ $acct->acctid } ? "1" : "0"; } elsif ($did_spellcheck) { $selected = $opts->{"prop_xpost_$acctid"}; } else { $selected = $acct->xpostbydefault; } $accthtml .= "\n"; $accthtml .= "" . LJ::html_check( { 'type' => 'checkbox', 'name' => "prop_xpost_$acctid", 'id' => "prop_xpost_$acctid", 'class' => 'check xpost_acct_checkbox', 'value' => '1', 'selected' => $selected, 'tabindex' => $tabindex->(), 'onchange' => 'XPostAccount.xpostAcctUpdated();', } ) . "\n"; $xpostbydefault = 1 if $selected; $accthtml .= ""; unless ( $acct->password ) { # password field if no password $accthtml .= ""; $accthtml .= ""; $accthtml .= LJ::html_text( { 'name' => "prop_xpost_password_$acctid", 'id' => "prop_xpost_password_$acctid", 'value' => "", 'disabled' => 0, 'size' => 40, 'maxlength' => 80, 'type' => 'password', 'class' => 'xpost_pw' } ); $accthtml .= ""; $accthtml .= ""; $accthtml .= ""; $accthtml .= ""; } $accthtml .= "\n"; $accthtml .= "\n"; } } $out .= qq [ \n"; $out .= "
\n"; $out .= "

"; $out .= LJ::html_check( { 'type' => 'checkbox', 'name' => 'prop_xpost_check', 'id' => 'prop_xpost_check', 'class' => 'check', 'value' => '1', 'selected' => $xpostbydefault, 'disabled' => ( scalar @accounts ) ? '0' : '1', 'tabindex' => $xpost_tabindex, 'onchange' => 'XPostAccount.xpostButtonUpdated();', } ); $out .= LJ::help_icon_html('prop_xpost_check'); $out .= "" . BML::ml('entryform.xpost.manage') . ""; $out .= "

\n"; $out .= $accthtml; $out .= "
\n"; $out .= "
\n"; $out .= qq [

]; } ### Other Posting Options $out .= LJ::Hooks::run_hook( 'add_extra_entryform_fields', { opts => $opts, tabindex => $tabindex } ) || ''; $out .= ""; # extra submit button so make sure it posts the form when person presses enter key if ( $opts->{'mode'} eq "edit" ) { $out .= ""; } if ( $opts->{'mode'} eq "update" ) { $out .= ""; } # submit_value field to emulate the submit button selected if we # have to submit with javascript $out .= ""; my $preview; $preview = ""; if ( !$opts->{'disabled_save'} ) { $out .= < PREVIEW } if ( $LJ::SPELLER && !$opts->{'disabled_save'} ) { $out .= LJ::html_submit( 'action:spellcheck', BML::ml('entryform.spellcheck'), { onclick => 'XPostAccount.doSpellcheck()', tabindex => $tabindex->() } ) . " "; } # Update posting date/time $out .= ""; $out .= "\n"; $out .= "

\n"; } ### Community maintainer bar if ( $opts->{'maintainer_mode'} ) { $out .= "

\n"; $out .= "" . BML::ml('entryform.maintainer') . "\n"; $out .= "

\n"; # adult content settings if ( LJ::is_enabled('adult_content') ) { $out .= "

\n"; my %poster_adult_content_menu = ( "" => BML::ml('entryform.adultcontent.default'), none => BML::ml('entryform.adultcontent.none'), concepts => BML::ml('entryform.adultcontent.concepts'), explicit => BML::ml('entryform.adultcontent.explicit'), ); my @adult_content_menu = ( "" => BML::ml( 'entryform.adultcontent.poster', { setting => $poster_adult_content_menu{ $opts->{prop_adult_content} } } ), none => BML::ml('entryform.adultcontent.none'), concepts => BML::ml('entryform.adultcontent.concepts'), explicit => BML::ml('entryform.adultcontent.explicit'), ); $out .= "\n"; $out .= LJ::html_select( { name => 'prop_adult_content_maintainer', id => 'prop_adult_content_maintainer', class => 'select', selected => $opts->{prop_adult_content_maintainer} || "", tabindex => $tabindex->(), }, @adult_content_menu ); $out .= LJ::help_icon_html( "adult_content", "", " " ); $out .= "

\n"; $out .= "

"; $out .= ""; $out .= LJ::html_text( { 'name' => 'prop_adult_content_maintainer_reason', 'id' => 'prop_adult_content_maintainer_reason', 'class' => 'text', 'size' => '35', 'maxlength' => '255', 'value' => $opts->{'prop_adult_content_maintainer_reason'}, 'tabindex' => $tabindex->(), } ); $out .= LJ::help_icon_html('adult_content_reason'); $out .= "

"; } # comment disabling/enabling # only possible if comments weren't disabled by poster unless ( $opts->{prop_opt_nocomments} ) { $out .= "

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

"; } } $out .= "
\n\n"; ### Submit Bar { $out .= "
\n\n"; # Security my $secbar = 0; if ( $opts->{'mode'} eq "update" || !$opts->{'disabled_save'} ) { my $usejournalu = LJ::load_user( $opts->{usejournal} ); my $is_comm = $usejournalu && $usejournalu->is_comm ? 1 : 0; my $string_public = LJ::ejs( BML::ml('label.security.public2') ); my $string_friends = LJ::ejs( BML::ml('label.security.accesslist') ); my $string_friends_comm = LJ::ejs( BML::ml('label.security.members') ); my $string_private = LJ::ejs( BML::ml('label.security.private2') ); my $string_admin = LJ::ejs( BML::ml('label.security.maintainers') ); my $string_custom = LJ::ejs( BML::ml('label.security.custom') ); $out .= qq{ }; $$onload .= " setColumns();" if $remote; my @secs = ( "public", $string_public, "friends", $is_comm ? $string_friends_comm : $string_friends ); push @secs, ( "private", $string_private ) unless $is_comm; push @secs, ( "private", $string_admin ) if $is_comm && $remote && $remote->can_manage($usejournalu); my ( @secopts, @trust_groups ); @trust_groups = $remote->trust_groups if $remote; if ( scalar @trust_groups && !$is_comm ) { push @secs, ( "custom", $string_custom ); push @secopts, ( "onchange" => "customboxes()" ); } if (@secs) { $secbar = 1; $out .= "
\n"; $out .= "\n"; } $out .= LJ::html_select( { 'id' => "security", 'name' => 'security', 'include_ids' => 1, 'class' => 'select', 'selected' => $opts->{'security'}, 'tabindex' => $tabindex->(), @secopts }, @secs ) . "\n"; # if custom security groups available, show them in a hideable div if ( scalar @trust_groups ) { my $display = $opts->{security} && $opts->{security} eq "custom" ? "block" : "none"; $out .= LJ::help_icon( "security", "\n", "\n\n" ); $out .= "
\n"; $out .= "
    "; foreach my $group (@trust_groups) { my $fg = $group->{groupnum}; $out .= "
  • "; $out .= LJ::html_check( { 'name' => "custom_bit_$fg", 'id' => "custom_bit_$fg", 'selected' => $opts->{"custom_bit_$fg"} || ( $opts->{security_mask} ? $opts->{security_mask} + 0 : 0 ) & 1 << $fg } ) . " "; $out .= "\n"; $out .= "
  • "; } $out .= "
"; $out .= "
\n"; } } if ( $opts->{'mode'} eq "update" ) { my $onclick = ""; my $defaultjournal; my $not_a_journal = 0; if ( $opts->{'usejournal'} ) { $defaultjournal = $opts->{'usejournal'}; } elsif ( $remote && $opts->{auth_as_remote} ) { $defaultjournal = $remote->user; } else { $defaultjournal = "Journal"; $not_a_journal = 1; } $$onload .= " changeSubmit('" . BML::ml('entryform.update3') . "', '$defaultjournal');"; $$onload .= " getUserTags('$defaultjournal');" unless $not_a_journal; $$onload .= " changeSecurityOptions('$defaultjournal');" unless $opts->{'security'}; $out .= LJ::html_submit( 'action:update', BML::ml('entryform.update4'), { 'onclick' => $onclick, 'class' => 'update_submit xpost_submit', 'id' => 'formsubmit', 'tabindex' => $tabindex->() } ) . " \n"; } if ( $opts->{'mode'} eq "edit" ) { my $onclick = ""; if ( !$opts->{'disabled_save'} ) { $out .= LJ::html_submit( 'action:save', BML::ml('entryform.save'), { 'onclick' => $onclick, 'disabled' => $opts->{'disabled_save'}, 'class' => 'xpost_submit', 'tabindex' => $tabindex->() } ) . " \n"; } elsif ( $opts->{maintainer_mode} ) { $out .= LJ::html_submit( 'action:savemaintainer', BML::ml('entryform.save.maintainer'), { 'onclick' => $onclick, 'disabled' => !$opts->{'maintainer_mode'}, 'class' => 'xpost_submit', 'tabindex' => $tabindex->() } ) . " \n"; } # do a double-confirm on delete if we have crossposts that # would also get removed my $delete_onclick = "return XPostAccount.confirmDelete('" . LJ::ejs( BML::ml('entryform.delete.confirm') ) . "', '" . LJ::ejs( BML::ml('entryform.delete.xposts.confirm') ) . "')"; $out .= LJ::html_submit( 'action:delete', BML::ml('entryform.delete'), { 'disabled' => $opts->{'disabled_delete'}, 'class' => 'xpost_submit', 'tabindex' => $tabindex->(), 'onclick' => $delete_onclick } ) . " \n"; if ( !$opts->{'disabled_spamdelete'} ) { $out .= LJ::html_submit( 'action:deletespam', BML::ml('entryform.deletespam'), { 'onclick' => "return confirm('" . LJ::ejs( BML::ml('entryform.deletespam.confirm') ) . "')", 'class' => 'xpost_submit', 'tabindex' => $tabindex->() } ) . "\n"; } } $out .= "
\n\n" if $secbar; $out .= "
\n\n"; $out .= "
\n\n"; $out .= "\n"; } return $out; } # # name: LJ::entry_form_decode # class: web # des: Decodes an entry_form into a protocol-compatible hash. # info: Generate form with [func[LJ::entry_form]]. # args: req, post # des-req: protocol request hash to build. # des-post: entry_form POST contents. # returns: req # sub entry_form_decode { my ( $req, $POST ) = @_; # find security my $sec = "public"; my $amask = 0; if ( $POST->{'security'} eq "private" ) { $sec = "private"; } elsif ( $POST->{'security'} eq "friends" ) { $sec = "usemask"; $amask = 1; } elsif ( $POST->{'security'} eq "custom" ) { $sec = "usemask"; foreach my $bit ( 1 .. 60 ) { next unless $POST->{"custom_bit_$bit"}; $amask |= ( 1 << $bit ); } } $req->{'security'} = $sec; $req->{'allowmask'} = $amask; # date/time my $date = LJ::html_datetime_decode( { 'name' => "date_ymd", }, $POST ); my ( $year, $mon, $day ) = split( /\D/, $date ); my ( $hour, $min ) = ( $POST->{'hour'}, $POST->{'min'} ); # TEMP: ease golive by using older way of determining differences my $date_old = LJ::html_datetime_decode( { 'name' => "date_ymd_old", }, $POST ); my ( $year_old, $mon_old, $day_old ) = split( /\D/, $date_old ); my ( $hour_old, $min_old ) = ( $POST->{'hour_old'}, $POST->{'min_old'} ); my $different = $POST->{'min_old'} && ( ( $year ne $year_old ) || ( $mon ne $mon_old ) || ( $day ne $day_old ) || ( $hour ne $hour_old ) || ( $min ne $min_old ) ); # this value is set when the JS runs, which means that the user-provided # time is sync'd with their computer clock. otherwise, the JS didn't run, # so let's guess at their timezone. if ( $POST->{'date_diff'} || $POST->{'date_diff_nojs'} || $different ) { delete $req->{'tz'}; $req->{'year'} = $year; $req->{'mon'} = $mon; $req->{'day'} = $day; $req->{'hour'} = $hour; $req->{'min'} = $min; } # copy some things from %POST foreach ( qw(subject prop_picture_keyword prop_current_moodid prop_current_mood prop_current_music prop_opt_screening prop_opt_noemail prop_opt_preformatted prop_opt_nocomments prop_current_location prop_current_coords prop_taglist ) ) { $req->{$_} = $POST->{$_}; } if ( $POST->{"subject"} && ( $POST->{"subject"} eq BML::ml('entryform.subject.hint2') ) ) { $req->{"subject"} = ""; } $req->{"prop_opt_preformatted"} ||= $POST->{'switched_rte_on'} ? 1 : $POST->{event_format} && $POST->{event_format} eq "preformatted" ? 1 : 0; $req->{"prop_opt_nocomments"} ||= $POST->{comment_settings} && $POST->{comment_settings} eq "nocomments" ? 1 : 0; $req->{"prop_opt_noemail"} ||= $POST->{comment_settings} && $POST->{comment_settings} eq "noemail" ? 1 : 0; $req->{'prop_opt_backdated'} = $POST->{'prop_opt_backdated'} ? 1 : 0; if ( LJ::is_enabled('adult_content') ) { $req->{prop_adult_content} = $POST->{prop_adult_content} || ''; $req->{prop_adult_content} = "" unless $req->{prop_adult_content} eq "none" || $req->{prop_adult_content} eq "concepts" || $req->{prop_adult_content} eq "explicit"; $req->{prop_adult_content_reason} = $POST->{prop_adult_content_reason} || ""; } # nuke taglists that are just blank $req->{'prop_taglist'} = "" unless $req->{'prop_taglist'} && $req->{'prop_taglist'} =~ /\S/; # Convert the rich text editor output back to parsable lj tags. my $event = $POST->{'event'}; if ( $POST->{'switched_rte_on'} ) { $req->{"prop_used_rte"} = 1; # We want to see if we can hit the fast path for cleaning # if they did nothing but add line breaks. my $attempt = $event; $attempt =~ s!
!\n!g; if ( $attempt !~ /<\w/ ) { $event = $attempt; # Make sure they actually typed something, and not just hit # enter a lot $attempt =~ s!(?:

(?: |\s)+

| )\s*?!!gm; $event = '' unless $attempt =~ /\S/; $req->{'prop_opt_preformatted'} = 0; } else { # Old methods, left in for compatibility during code push $event =~ s!!!gi; $event =~ s!!!gi; } } else { $req->{"prop_used_rte"} = 0; } $req->{'event'} = $event; ## see if an "other" mood they typed in has an equivalent moodid if ( $POST->{'prop_current_mood'} ) { if ( my $id = DW::Mood->mood_id( $POST->{'prop_current_mood'} ) ) { $req->{'prop_current_moodid'} = $id; delete $req->{'prop_current_mood'}; } } # process site-specific options LJ::Hooks::run_hooks( 'decode_entry_form', $POST, $req ); return $req; } { my %stat_cache = (); # key -> {lastcheck, modtime} sub _file_modtime { my ( $key, $now ) = @_; if ( my $ci = $stat_cache{$key} ) { if ( $ci->{lastcheck} > $now - 10 ) { return $ci->{modtime}; } } my $set = sub { my $mtime = shift; $stat_cache{$key} = { lastcheck => $now, modtime => $mtime }; return $mtime; }; my $file; # Prefer the compiled version, but fall back to source if ( defined $LJ::STATDOCS ) { $file = $LJ::STATDOCS . '/' . $key; } else { $file = LJ::resolve_file("htdocs/$key"); } my $mtime = defined $file ? ( stat($file) )[9] : undef; return $set->($mtime); } } # INTERLUDE: What's up w/ need_res, res_includes, and set_active_resource_group? # # GOOD QUESTION, WONDER-CAT. This is a system for lazy resolution and # deduplication of required CSS/JS files. Any component can declare its own # dependencies without caring where it's being called from, which theoretically # makes things slightly more self-contained and maintainable. # Later (I think?), someone extended it to enable incremental code # modernization. A component can declare multiple implementations of a frontend # feature, specifying a "resource group" for each implementation. When it's # finally time to build a page, the controller sets its preferred resource group # and pulls in only frontend code that's compatible with that group. # As for how to use these, well, hmm. They follow a classic LJ design # pattern that I like to call "launch some garbage into the void / reach # into the void and grab some garbage." Infinite flexibility, zero handrails or # validation. So here's the conventions I've been able to puzzle out. # # * `need_res`: Declares one or more JS/CSS files that need to be loaded for # the current page. need_res([], , [...]) # FILE PATHS: Use paths like "js/jquery.replyforms.js" or # "stc/components/quick-reply.css", relative to the top-level "htdocs" # directory. Paths can ONLY go into the "js" (javascript) or "stc" (CSS) # subdirectories. Note that on a live instance of the app, "stc" also # has the compiled contents of the "scss" directory (minus any files # whose names begin with an underscore [_]). # PRIORITY: The opts hashref can have a "priority" key, whose value is # an integer. This affects the order in which files are added to the # page; otherwise the order is effectively random. Files with bigger # priority values get included first. Existing code only seems to # use two values: 0 (normal) and 3 (prerequisites). 0 is never # referenced explicitly, just defaulted to. 3 is always referenced # indirectly, via the $LJ::LIB_RES_PRIORITY variable. # RESOURCE GROUP: The opts hashref can have a "group" key, whose value # is a string. See "KNOWN RESOURCE GROUPS" below for more details. If # you don't specify a group, need_res puts CSS files in the "all" group # and JS files in the "default" (actually legacy) group. # # * `set_active_resource_group`: Sets the value of a global variable called # $LJ::ACTIVE_RES_GROUP, which then affects the behavior of res_includes. It's # supposed to be called pretty late in the process of building a page. You can # check that variable to see what will _probably_ be happening, but be careful; # there aren't a lot of assurances about how accurate it is at any given time. # # * `res_includes`: Builds the actual HTML tags for the JS/CSS in the current # resource group (plus the special "all" group), and returns them as a string. # Whatever calls res_includes is responsible for putting the result somewhere # useful. # Conceptually, res_includes is only meant to be called once per page, but # since Foundation expects JS to go at the bottom of the , it's more like # "call half of it twice," via the wrapper functions `res_includes_head` # (for CSS and really basic inline JS that everything else relies on) and # `res_includes_body` (JS loaded from files). # # KNOWN RESOURCE GROUPS: There isn't any validation on these, but these are the # values that actually get used by something. # - "foundation" - newer pages. # - "jquery" - pages whose JS was modernized in the early '10s. # - "default" - legacy LJ pages that have basically never been modernized. # - "all" - always included, regardless of the current resource group. # - "fragment" - no idea, tbh. Something involving the template system. # # LIMITATIONS: Lazy resolution doesn't work reliably with non-"siteviews" S2 # pages (they can sometimes do JS, but not CSS), so they should know all of # their CSS/JS files BEFORE we start rendering markup. In particular, watch out # for this when using .tt components that call need_res. # Why? To lazy resolve, you need to call res_includes AFTER the inner # content is fully built, which means building the outer frame of the page last. # That's easy for site pages, but since journal styles can override the entire # HTML document, there's not really any protected "outer" part of the page that # core2 can defer. # -NF # optional first argument: hashref with options # other arguments: resources to include sub need_res { my %opts; if ( ref $_[0] eq 'HASH' ) { %opts = %{ shift() }; } my $group = $opts{group}; # higher priority means it comes first in the ordering my $priority = $opts{priority} || 0; foreach my $reskey (@_) { die "Bogus reskey $reskey" unless $reskey =~ m!^(js|stc)/!; # we put javascript in the 'default' group and CSS in the 'all' group # since we need CSS everywhere and we are switching JS groups my $lgroup = $group || ( $reskey =~ /^js/ ? 'default' : 'all' ); unless ( $LJ::NEEDED_RES{"$lgroup-$reskey"}++ ) { $LJ::NEEDED_RES[$priority] ||= []; push @{ $LJ::NEEDED_RES[$priority] }, [ $lgroup, $reskey ]; } } } sub res_includes { my (%opts) = @_; my $include_js = !$opts{nojs}; my $include_libs = !$opts{nolib}; my $include_stylesheets = !$opts{no_stylesheets}; my $include_script_tags = !$opts{no_scripttags}; my $include_links = $include_stylesheets || $include_script_tags; # TODO: automatic dependencies from external map and/or content of files, # currently it's limited to dependencies on the order you call LJ::need_res(); my $ret = ""; # use correct root and prefixes for SSL pages my ( $siteroot, $imgprefix, $statprefix, $jsprefix, $wstatprefix, $iconprefix ); $siteroot = $LJ::SITEROOT; $imgprefix = $LJ::IMGPREFIX; $statprefix = $LJ::STATPREFIX; $jsprefix = $LJ::JSPREFIX; $wstatprefix = $LJ::WSTATPREFIX; $iconprefix = $LJ::USERPIC_ROOT; if ($include_js) { # find current journal my $r = DW::Request->get; my $journal_base = ''; my $journal = ''; if ($r) { my $journalid = $r->note('journalid'); my $ju; $ju = LJ::load_userid($journalid) if $journalid; if ($ju) { $journal_base = $ju->journal_base; $journal = $ju->{user}; } } my $remote = LJ::get_remote(); my $hasremote = $remote ? 1 : 0; # ctxpopup prop my $ctxpopup_icons = 1; my $ctxpopup_userhead = 1; $ctxpopup_icons = 0 if $remote && !$remote->opt_ctxpopup_icons; $ctxpopup_userhead = 0 if $remote && !$remote->opt_ctxpopup_userhead; # poll for esn inbox updates? my $inbox_update_poll = LJ::is_enabled('inbox_update_poll'); # are media embeds enabled? my $embeds_enabled = LJ::is_enabled('embed_module'); # esn ajax enabled? my $esn_async = LJ::is_enabled('esn_ajax'); my %site = ( imgprefix => "$imgprefix", siteroot => "$siteroot", statprefix => "$statprefix", iconprefix => "$iconprefix", currentJournalBase => "$journal_base", currentJournal => "$journal", has_remote => $hasremote, ctx_popup => ( $ctxpopup_icons || $ctxpopup_userhead ), ctx_popup_icons => $ctxpopup_icons, ctx_popup_userhead => $ctxpopup_userhead, inbox_update_poll => $inbox_update_poll, media_embed_enabled => $embeds_enabled, esn_async => $esn_async, user_domain => $LJ::USER_DOMAIN, cmax_comment => LJ::CMAX_COMMENT, ); my $site_params = to_json( \%site ); # include standard JS info $ret .= qq { }; } if ($include_links) { my $now = time(); my %list; # type -> []; my %oldest; # type -> $oldest my %included = (); my $add = sub { my ( $type, $what, $modtime, $order ) = @_; # the same file may have been included twice # if it was in two different groups and not JS # so add another check here return if $included{$what}; $included{$what} = 1; # in the concat-res case, we don't directly append the URL w/ # the modtime, but rather do one global max modtime at the # end, which is done later in the tags function. $modtime = '' unless defined $modtime; $list{$type} ||= []; push @{ $list{$type}[$order] ||= [] }, $what; $oldest{$type} ||= []; $oldest{$type}[$order] = $modtime if $modtime && $modtime > ( $oldest{$type}[$order] || 0 ); }; # we may not want to pull in the libraries again, say if we're pulling in elements via an ajax load delete $LJ::NEEDED_RES[$LJ::LIB_RES_PRIORITY] unless $include_libs; my $order = 0; foreach my $by_priority ( reverse @LJ::NEEDED_RES ) { next unless $by_priority; $order++; foreach my $resrow (@$by_priority) { # determine if this resource is part of the resource group that is active; # or 'default' if no group explicitly active my ( $group, $key ) = @$resrow; next if $group ne 'all' && ( ( defined $LJ::ACTIVE_RES_GROUP && $group ne $LJ::ACTIVE_RES_GROUP ) || ( !defined $LJ::ACTIVE_RES_GROUP && $group ne 'default' ) ); my $path; my $mtime = _file_modtime( $key, $now ); if ( $key =~ m!^stc/fck/! || $LJ::FORCE_WSTAT{$key} ) { $path = "w$key"; # wstc/ instead of stc/ } else { $path = $key; } # if we want to also include a local version of this file, include that too if (@LJ::USE_LOCAL_RES) { if ( grep { lc $_ eq lc $key } @LJ::USE_LOCAL_RES ) { my $inc = $key; $inc =~ s/(\w+)\.(\w+)$/$1-local.$2/; LJ::need_res($inc); } } if ( $path =~ m!^js/(.+)! ) { $add->( "js", $1, $mtime, $order ); } elsif ( $path =~ /\.css$/ && $path =~ m!^(w?)stc/(.+)! ) { $add->( "${1}stccss", $2, $mtime, $order ); } elsif ( $path =~ /\.js$/ && $path =~ m!^(w?)stc/(.+)! ) { $add->( "${1}stcjs", $2, $mtime, $order ); } } } my $tags = sub { my ( $type, $template ) = @_; for my $o ( 0 ... $order ) { my $list; my $template_order = $template; next unless $list = $list{$type}[$o]; my $csep = join( ',', @$list ); $csep .= "?v=" . $oldest{$type}[$o]; $template_order =~ s/__+/??$csep/; $ret .= $template_order; } }; if ($include_stylesheets) { $tags->( "stccss", "\n" ); $tags->( "wstccss", "\n" ); } if ($include_script_tags) { $tags->( "js", "\n" ); $tags->( "stcjs", "\n" ); $tags->( "wstcjs", "\n" ); } } return $ret; } sub res_includes_head { return LJ::res_includes( no_scripttags => 1 ); } sub res_includes_body { return LJ::res_includes( nojs => 1, no_stylesheets => 1 ); } # called to set the active resource group sub set_active_resource_group { $LJ::ACTIVE_RES_GROUP = $_[0]; } # Returns HTML of a dynamic tag could given passed in data # Requires hash-ref of tag => { url => url, value => value } sub tag_cloud { my ( $tags, $opts ) = @_; # find sizes of tags, sorted my @sizes = sort { $a <=> $b } map { $tags->{$_}->{'value'} } keys %$tags; # remove duplicates: my %sizes = map { $_, 1 } @sizes; @sizes = sort { $a <=> $b } keys %sizes; my @tag_names = sort keys %$tags; my $percentile = sub { my $n = shift; my $total = scalar @sizes; for ( my $i = 0 ; $i < $total ; $i++ ) { next if $n > $sizes[$i]; return $i / $total; } }; my $base_font_size = 8; my $font_size_range = $opts->{font_size_range} || 25; my $ret .= "
"; my %tagdata = (); foreach my $tag (@tag_names) { my $tagurl = $tags->{$tag}->{'url'}; my $ct = $tags->{$tag}->{'value'}; my $pt = int( $base_font_size + $percentile->($ct) * $font_size_range ); $ret .= "{ignore_ids}; $ret .= "href='" . LJ::ehtml($tagurl) . "' style='font-size: ${pt}pt; text-decoration: none'>"; $ret .= LJ::ehtml($tag) . "\n"; # build hash of tagname => final point size for refresh $tagdata{$tag} = $pt; } $ret .= "
"; return $ret; } sub control_strip { my %opts = @_; my $user = delete $opts{user}; my $journal = LJ::load_user($user); my $show_strip = 1; $show_strip = LJ::Hooks::run_hook("show_control_strip") if ( LJ::Hooks::are_hooks("show_control_strip") ); return "" unless $show_strip; my $remote = LJ::get_remote(); my $r = DW::Request->get; my $passed_in_location = $opts{host} && $opts{uri} ? 1 : 0; my $host = delete $opts{host} || $r->host; my $uri = delete $opts{uri} || $r->uri; my $args; my $argshash = {}; # we need to pass in location explicitly when creating a control strip using JS if ($passed_in_location) { $args = delete $opts{args}; LJ::decode_url_string( $args, $argshash ); } else { $args = $r->query_string; $argshash = $r->get_args; } my $view = delete $opts{view} || $r->note('view'); my $view_is = sub { defined $view && $view eq $_[0] }; my $baseuri = "$LJ::PROTOCOL://$host$uri"; $baseuri .= $args ? "?$args" : ""; my $euri = LJ::eurl($baseuri); my $create_link = LJ::Hooks::run_hook( "override_create_link_on_navstrip", $journal ) || "" . BML::ml( 'web.controlstrip.links.create', { 'sitename' => $LJ::SITENAMESHORT } ) . ""; # Build up some common links my %links = ( 'login' => "$BML::ML{'web.controlstrip.links.login'}", 'post_journal' => "$BML::ML{'web.controlstrip.links.post2'}", 'home' => "" . $BML::ML{'web.controlstrip.links.home'} . "", 'recent_comments' => "$BML::ML{'web.controlstrip.links.recentcomments'}", 'manage_friends' => "$BML::ML{'web.controlstrip.links.managecircle'}", 'manage_entries' => "$BML::ML{'web.controlstrip.links.manageentries'}", 'invite_friends' => "$BML::ML{'web.controlstrip.links.invitefriends'}", 'create_account' => $create_link, 'syndicated_list' => "$BML::ML{'web.controlstrip.links.popfeeds'}", 'learn_more' => LJ::Hooks::run_hook('control_strip_learnmore_link') || "$BML::ML{'web.controlstrip.links.learnmore'}", 'explore' => "" . BML::ml( 'web.controlstrip.links.explore', { sitenameabbrev => $LJ::SITENAMEABBREV } ) . "", 'confirm' => "$BML::ML{'web.controlstrip.links.confirm'}", ); if ($remote) { my $unread = $remote->notification_inbox->unread_count; $links{inbox} .= "$BML::ML{'web.controlstrip.links.inbox'}"; $links{inbox} .= " ($unread)" if $unread; $links{inbox} .= ""; $links{settings} = "$BML::ML{'web.controlstrip.links.settings'}"; $links{'view_friends_page'} = "$BML::ML{'web.controlstrip.links.viewreadingpage'}"; $links{'add_friend'} = "$BML::ML{'web.controlstrip.links.addtocircle'}"; $links{'edit_friend'} = "$BML::ML{'web.controlstrip.links.modifycircle'}"; $links{'track_user'} = "$BML::ML{'web.controlstrip.links.trackuser'}"; if ( $journal->is_syndicated ) { $links{'add_friend'} = "$BML::ML{'web.controlstrip.links.addfeed'}"; $links{'remove_friend'} = "$BML::ML{'web.controlstrip.links.removefeed'}"; } if ( $journal->is_community ) { $links{'join_community'} = "$BML::ML{'web.controlstrip.links.joincomm'}" unless $journal->is_closed_membership; $links{'leave_community'} = "$BML::ML{'web.controlstrip.links.leavecomm'}"; $links{'watch_community'} = "$BML::ML{'web.controlstrip.links.watchcomm'}"; $links{'unwatch_community'} = "$BML::ML{'web.controlstrip.links.removecomm'}"; $links{'post_to_community'} = "$BML::ML{'web.controlstrip.links.postcomm'}"; $links{'edit_community_profile'} = "$BML::ML{'web.controlstrip.links.editcommprofile'}"; $links{'edit_community_invites'} = "$BML::ML{'web.controlstrip.links.managecomminvites'}"; $links{'edit_community_members'} = "$BML::ML{'web.controlstrip.links.editcommmembers'}"; $links{'track_community'} = "$BML::ML{'web.controlstrip.links.trackcomm'}"; $links{'queue'} = "$BML::ML{'web.controlstrip.links.queue'}"; } } my $journal_display = $journal->ljuser_display; my %statustext = ( 'yourjournal' => $BML::ML{'web.controlstrip.status.yourjournal'}, 'yourfriendspage' => $BML::ML{'web.controlstrip.status.yourreadingpage'}, 'yourfriendsfriendspage' => $BML::ML{'web.controlstrip.status.yournetworkpage'}, 'personal' => BML::ml( 'web.controlstrip.status.personal', { 'user' => $journal_display } ), 'personalfriendspage' => BML::ml( 'web.controlstrip.status.personalreadingpage', { 'user' => $journal_display } ), 'personalfriendsfriendspage' => BML::ml( 'web.controlstrip.status.personalnetworkpage', { 'user' => $journal_display } ), 'community' => BML::ml( 'web.controlstrip.status.community', { 'user' => $journal_display } ), 'syn' => BML::ml( 'web.controlstrip.status.syn', { 'user' => $journal_display } ), 'other' => BML::ml( 'web.controlstrip.status.other', { 'user' => $journal_display } ), 'mutualtrust' => BML::ml( 'web.controlstrip.status.mutualtrust', { 'user' => $journal_display } ), 'mutualtrust_mutualwatch' => BML::ml( 'web.controlstrip.status.mutualtrust_mutualwatch', { 'user' => $journal_display } ), 'mutualtrust_watch' => BML::ml( 'web.controlstrip.status.mutualtrust_watch', { 'user' => $journal_display } ), 'mutualtrust_watchedby' => BML::ml( 'web.controlstrip.status.mutualtrust_watchedby', { 'user' => $journal_display } ), 'mutualwatch' => BML::ml( 'web.controlstrip.status.mutualwatch', { 'user' => $journal_display } ), 'trust_mutualwatch' => BML::ml( 'web.controlstrip.status.trust_mutualwatch', { 'user' => $journal_display } ), 'trust_watch' => BML::ml( 'web.controlstrip.status.trust_watch', { 'user' => $journal_display } ), 'trust_watchedby' => BML::ml( 'web.controlstrip.status.trust_watchedby', { 'user' => $journal_display } ), 'trustedby_mutualwatch' => BML::ml( 'web.controlstrip.status.trustedby_mutualwatch', { 'user' => $journal_display } ), 'trustedby_watch' => BML::ml( 'web.controlstrip.status.trustedby_watch', { 'user' => $journal_display } ), 'trustedby_watchedby' => BML::ml( 'web.controlstrip.status.trustedby_watchedby', { 'user' => $journal_display } ), 'maintainer' => BML::ml( 'web.controlstrip.status.maintainer', { 'user' => $journal_display } ), 'memberwatcher' => BML::ml( 'web.controlstrip.status.memberwatcher', { 'user' => $journal_display } ), 'watcher' => BML::ml( 'web.controlstrip.status.watcher', { 'user' => $journal_display } ), 'member' => BML::ml( 'web.controlstrip.status.member', { 'user' => $journal_display } ), 'trusted' => BML::ml( 'web.controlstrip.status.trusted', { 'user' => $journal_display } ), 'watched' => BML::ml( 'web.controlstrip.status.watched', { 'user' => $journal_display } ), 'trusted_by' => BML::ml( 'web.controlstrip.status.trustedby', { 'user' => $journal_display } ), 'watched_by' => BML::ml( 'web.controlstrip.status.watchedby', { 'user' => $journal_display } ), ); # Vars for controlstrip.tt my $template_args = { 'view' => $view, 'userpic_html' => '', 'logo_html' => ( LJ::Hooks::run_hook( 'control_strip_logo', $remote, $journal ) || '' ), 'show_login_form' => 0, 'links' => \%links, 'statustext' => '', # remote # only set if logged in # .user => "plainname" # .sessid => integer # .display => "..." # .is_validated => bool # .is_identity => bool 'actionlinks' => [], # filters # only set if viewing reading or network page # .all => [] # .selected => "" 'viewoptions' => [], 'search_html' => LJ::Widget::Search->render, # url of the rendered page, for the login/logout form to redirect back to 'returnto' => $baseuri, }; # Shortcuts for the two nested array refs that get repeatedly dereferenced later my $actionlinks = $template_args->{'actionlinks'}; my $viewoptions = $template_args->{'viewoptions'}; if ($remote) { my $userpic = $remote->userpic; $template_args->{'remote'} = { 'sessid' => $remote->session->id || 0, 'user' => $remote->user, 'display' => $remote->ljuser_display, 'is_validated' => $remote->is_validated, 'is_identity' => $remote->is_identity, }; if ($userpic) { my $wh = $userpic->img_fixedsize( width => 43, height => 43 ); $template_args->{'userpic_html'} = "\"$BML::ML{'web.controlstrip.userpic.alt'}\""; } else { my $tinted_nouserpic_img = ""; if ( $journal->prop('stylesys') == 2 ) { my $ctx = $LJ::S2::CURR_CTX; my $custom_nav_strip = S2::get_property_value( $ctx, "custom_control_strip_colors" ); if ( $custom_nav_strip ne "off" ) { my $linkcolor = S2::get_property_value( $ctx, "control_strip_linkcolor" ); if ( $linkcolor ne "" ) { $tinted_nouserpic_img = S2::Builtin::LJ::palimg_modify( $ctx, "controlstrip/nouserpic.gif", [ S2::Builtin::LJ::PalItem( $ctx, 0, $linkcolor ) ] ); } } } if ( $tinted_nouserpic_img eq "" ) { $tinted_nouserpic_img = "$LJ::IMGPREFIX/controlstrip/nouserpic.gif"; } $template_args->{'userpic_html'} = "\"$BML::ML{'web.controlstrip.nouserpic.alt'}\""; } if ( $remote->equals($journal) ) { if ( $view_is->("read") ) { $template_args->{'statustext'} = $statustext{'yourfriendspage'}; } elsif ( $view_is->("network") ) { $template_args->{'statustext'} = $statustext{'yourfriendsfriendspage'}; } else { $template_args->{'statustext'} = $statustext{'yourjournal'}; } if ( $view_is->("read") || $view_is->("network") ) { my @filters = ( "all", $BML::ML{'web.controlstrip.select.friends.all'}, "showpeople", $BML::ML{'web.controlstrip.select.friends.journals'}, "showcommunities", $BML::ML{'web.controlstrip.select.friends.communities'}, "showsyndicated", $BML::ML{'web.controlstrip.select.friends.feeds'} ); # content_filters returns an array of content filters this user had, sorted by sortorder # since this is only shown if $remote->equals( $journal ) , we don't have to care whether a filter is public or not my @custom_filters = $journal->content_filters; # Making as few changes to existing behaviour my $default_filter = "default view"; foreach my $f (@custom_filters) { # Both 'default' and 'default view' are default filters $default_filter = "default" if lc( $f->name ) eq "default"; push @filters, "filter:" . lc( $f->name ), $f->name; } my $selected = "all"; # first, change the selection state to reflect any filter in use; # if we have no default filter or if the named filter somehow # fails to exist, this will effectively select nothing if ( $r->uri =~ /^\/read\/?(.+)?/i ) { my $filter = $1 || $default_filter; $selected = "filter:" . LJ::durl( lc($filter) ); # but don't select the filter if the query string contains filter=0 # (fun fact: named filter + filter=0 returns a 404 error) $selected = "all" if $r->query_string && $r->query_string =~ /\bfilter=0\b/; } # next, change the selection state to reflect showtypes from getargs; # note this will override the implicit default filter or filter=0 selection # if a match is found, but not a filter explicitly named in the URL. # (of course you can use both! we're just competing for the # state of the pop-up menu in the control strip here) if ( ( $r->uri eq "/read" || $r->uri eq "/network" ) && $r->query_string && $r->query_string ne "" ) { $selected = "showpeople" if $r->query_string =~ /\bshow=P\b/; $selected = "showcommunities" if $r->query_string =~ /\bshow=C\b/; $selected = "showsyndicated" if $r->query_string =~ /\bshow=F\b/; } push( @$actionlinks, $links{'manage_friends'} ); # Data for the reading list filter drop-down: $template_args->{'filters'} = { 'all' => \@filters, 'selected' => $selected, }; } else { push( @$actionlinks, $links{'recent_comments'}, $links{'manage_entries'}, $links{'invite_friends'} ); } } elsif ( $journal->is_personal || $journal->is_identity ) { my $trusted = $remote->trusts($journal); my $trusted_by = $journal->trusts($remote); my $mutual_trust = $trusted && $trusted_by ? 1 : 0; my $watched = $remote->watches($journal); my $watched_by = $journal->watches($remote); my $mutual_watch = $watched && $watched_by ? 1 : 0; if ( $mutual_trust && $mutual_watch ) { $template_args->{'statustext'} = $statustext{mutualtrust_mutualwatch}; push( @$actionlinks, $links{edit_friend} ); } elsif ( $mutual_trust && $watched ) { $template_args->{'statustext'} = $statustext{mutualtrust_watch}; push( @$actionlinks, $links{edit_friend} ); } elsif ( $mutual_trust && $watched_by ) { $template_args->{'statustext'} = $statustext{mutualtrust_watchedby}; push( @$actionlinks, $links{edit_friend} ); } elsif ( $trusted && $mutual_watch ) { $template_args->{'statustext'} = $statustext{trust_mutualwatch}; push( @$actionlinks, $links{edit_friend} ); } elsif ( $trusted_by && $mutual_watch ) { $template_args->{'statustext'} = $statustext{trustedby_mutualwatch}; push( @$actionlinks, $links{edit_friend} ); } elsif ($mutual_trust) { $template_args->{'statustext'} = $statustext{mutualtrust}; push( @$actionlinks, $links{edit_friend} ); } elsif ($mutual_watch) { $template_args->{'statustext'} = $statustext{mutualwatch}; push( @$actionlinks, $links{edit_friend} ); } elsif ( $trusted && $watched ) { $template_args->{'statustext'} = $statustext{trust_watch}; push( @$actionlinks, $links{edit_friend} ); } elsif ( $trusted && $watched_by ) { $template_args->{'statustext'} = $statustext{trust_watchedby}; push( @$actionlinks, $links{edit_friend} ); } elsif ( $trusted_by && $watched ) { $template_args->{'statustext'} = $statustext{trustedby_watch}; push( @$actionlinks, $links{edit_friend} ); } elsif ( $trusted_by && $watched_by ) { $template_args->{'statustext'} = $statustext{trustedby_watchedby}; push( @$actionlinks, $links{add_friend} ); } elsif ($trusted) { $template_args->{'statustext'} = $statustext{trusted}; push( @$actionlinks, $links{edit_friend} ); } elsif ($trusted_by) { $template_args->{'statustext'} = $statustext{trusted_by}; push( @$actionlinks, $links{add_friend} ); } elsif ($watched) { $template_args->{'statustext'} = $statustext{watched}; push( @$actionlinks, $links{edit_friend} ); } elsif ($watched_by) { $template_args->{'statustext'} = $statustext{watched_by}; push( @$actionlinks, $links{add_friend} ); } else { if ( $view_is->("read") ) { $template_args->{'statustext'} = $statustext{'personalfriendspage'}; } elsif ( $view_is->("network") ) { $template_args->{'statustext'} = $statustext{'personalfriendsfriendspage'}; } else { $template_args->{'statustext'} = $statustext{'personal'}; } push( @$actionlinks, $links{'add_friend'} ); } push( @$actionlinks, $links{'track_user'} ); } elsif ( $journal->is_community ) { my $watching = $remote->watches($journal); my $memberof = $remote->member_of($journal); my $haspostingaccess = $remote->can_post_to($journal); my $isclosedcommunity = $journal->is_closed_membership; if ( $remote->can_manage_other($journal) ) { $template_args->{'statustext'} = "$statustext{maintainer}"; push( @$actionlinks, $links{post_to_community} ) if $haspostingaccess; if ( $journal->prop('moderated') ) { push( @$actionlinks, "$links{queue} [" . $journal->get_mod_queue_count . "]" ); } else { push( @$actionlinks, $links{edit_community_profile} ); } push( @$actionlinks, $links{edit_community_invites}, $links{edit_community_members} ); } elsif ( $watching && $memberof ) { $template_args->{'statustext'} = $statustext{memberwatcher}; push( @$actionlinks, $links{post_to_community} ) if $haspostingaccess; push( @$actionlinks, $links{leave_community} ); push( @$actionlinks, $links{track_community} ); } elsif ($watching) { $template_args->{'statustext'} = $statustext{watcher}; push( @$actionlinks, $links{post_to_community} ) if $haspostingaccess; push( @$actionlinks, $isclosedcommunity ? "This is a closed community" : $links{join_community} ); push( @$actionlinks, $links{unwatch_community} ); push( @$actionlinks, $links{track_community} ); } elsif ($memberof) { $template_args->{'statustext'} = $statustext{member}; push( @$actionlinks, $links{post_to_community} ) if $haspostingaccess; push( @$actionlinks, $links{watch_community}, $links{'leave_community'}, $links{track_community} ); } else { $template_args->{'statustext'} = $statustext{community}; push( @$actionlinks, $links{post_to_community} ) if $haspostingaccess; push( @$actionlinks, $isclosedcommunity ? "This is a closed community" : $links{join_community} ); push( @$actionlinks, $links{watch_community}, $links{track_community} ); } } elsif ( $journal->is_syndicated ) { $template_args->{'statustext'} = $statustext{syn}; if ( $remote && !$remote->watches($journal) ) { push( @$actionlinks, $links{add_friend} ); } elsif ( $remote && $remote->watches($journal) ) { push( @$actionlinks, $links{remove_friend} ); } push( @$actionlinks, $links{syndicated_list} ); } else { $template_args->{'statustext'} = $statustext{other}; } } else { $template_args->{'userpic_html'} = LJ::Hooks::run_hook( 'control_strip_loggedout_userpic_contents', $euri ) || ""; my $show_login_form = LJ::Hooks::run_hook( "show_control_strip_login_form", $journal ); $show_login_form = 1 if !defined $show_login_form; $template_args->{'show_login_form'} = $show_login_form; if ( $journal->is_personal || $journal->is_identity ) { if ( $view_is->("read") ) { $template_args->{'statustext'} = $statustext{'personalfriendspage'}; } elsif ( $view_is->("network") ) { $template_args->{'statustext'} = $statustext{'personalfriendsfriendspage'}; } else { $template_args->{'statustext'} = $statustext{'personal'}; } } elsif ( $journal->is_community ) { $template_args->{'statustext'} = $statustext{'community'}; } elsif ( $journal->is_syndicated ) { $template_args->{'statustext'} = $statustext{'syn'}; } else { $template_args->{'statustext'} = $statustext{'other'}; } push( @$actionlinks, $links{'login'} ) unless $show_login_form; push( @$actionlinks, $links{'create_account'}, $links{'learn_more'} ); } # search box and ?style=mine/?style=light/?style=original/?style=site options # determine whether style is "mine", and define new uri variable to manipulate # note: all expressions case-insensitive my $current_style = determine_viewing_style( $r->get_args, $view, $remote ); # a quick little routine to use when cycling through the options # to create the style links for the nav bar my $make_style_link = sub { return LJ::ehtml( create_url( $uri, host => $host, cur_args => $argshash, # change the style arg 'args' => { 'style' => $_[0] }, # keep any other existing arguments 'keep_args' => 1, ) ); }; # cycle through all possibilities, add the valid ones foreach my $view_type (qw( mine site light original )) { # only want to offer this option if user is logged in and it's not their own journal, since # original will take care of that if ( $view_type eq "mine" and $current_style ne $view_type and $remote and not $remote->equals($journal) ) { push @$viewoptions, "" . LJ::Lang::ml('web.controlstrip.reloadpage.mystyle2') . ""; } elsif ( $view_type eq "site" and $current_style ne $view_type and defined $view and { entry => 1, reply => 1, icons => 1, }->{$view} ) { push @$viewoptions, "" . LJ::Lang::ml('web.controlstrip.reloadpage.sitestyle') . ""; } elsif ( $view_type eq "light" and $current_style ne $view_type ) { push @$viewoptions, "" . LJ::Lang::ml('web.controlstrip.reloadpage.lightstyle2') . ""; } elsif ( $view_type eq "original" and $current_style ne $view_type ) { push @$viewoptions, "" . LJ::Lang::ml('web.controlstrip.reloadpage.origstyle2') . ""; } } return DW::Template->template_string( 'journal/controlstrip.tt', $template_args ); } sub control_strip_js_inject { my %opts = @_; my $user = delete $opts{user} || ''; my $ret; my $r = DW::Request->get; my $host = $r->host; my $uri = $r->uri; my $args = LJ::eurl( $r->query_string ) || ''; my $view = $r->note('view') || ''; $ret .= qq{ }; return $ret; } # For the Rich Text Editor # Set JS variables for use by the RTE sub rte_js_vars { my ($remote) = @_; my $ret = ''; # The JS var canmakepoll is used by fckplugin.js to change the behaviour # of the poll button in the RTE. # Also remove any RTE buttons that have been set to disabled. my $canmakepoll = "true"; $canmakepoll = "false" if ( $remote && !$remote->can_create_polls ); $ret .= "^; return $ret; } # prints out UI for subscribing to some events sub subscribe_interface { my ( $u, %opts ) = @_; croak "subscribe_interface wants a \$u" unless LJ::isu($u); my $categories = delete $opts{categories} || []; my $journalu = delete $opts{journal} || LJ::get_remote(); my $def_notes = delete $opts{default_selected_notifications} || []; my $num_per_page = delete $opts{num_per_page} || 250; my $page = delete $opts{page} || 1; my $settings_page = delete $opts{settings_page} || 0; croak "Invalid user object passed to subscribe_interface" unless LJ::isu($journalu); croak "Invalid options passed to subscribe_interface" if ( scalar keys %opts ); my @notify_classes = LJ::NotificationMethod->all_classes; return "No notification methods" unless @notify_classes; my $page_vars = { settings_page => $settings_page }; $page_vars->{ntypeids} = join ',', map { $_->ntypeid } @notify_classes; # skip the inbox type; it's always on @notify_classes = grep { $_ ne 'LJ::NotificationMethod::Inbox' } @notify_classes; $page_vars->{notify_classes} = \@notify_classes; my @catids; my $catid = 0; my @catdata; # this is eventually passed to the display template my @prev_cats; my %num_subs_by_type = (); # for the subscription_stats hook # pagination variables my $displayed_tracking_start = ( $page - 1 ) * $num_per_page; my $displayed_tracking_end = $displayed_tracking_start + $num_per_page - 1; my $displayed_tracking_count = 0; foreach my $cat_hash (@$categories) { my ( $category, $cat_events ) = %$cat_hash; # is this category the tracking category? my $is_tracking_category = $category eq "Subscription Tracking"; my $cat_data = { catid => $catid }; push @catids, $catid; my $cat_empty = 1; my $cat_title_key = lc($category); $cat_title_key =~ s/ /-/g; $cat_data->{title_key} = $cat_title_key; # add notifytype headings $cat_data->{notify_headers} = []; foreach my $notify_class (@notify_classes) { my $title = eval { $notify_class->title($u) } or next; my $ntypeid = $notify_class->ntypeid or next; # create the checkall box for this event type. my $disabled = !$notify_class->configured_for_user($u); if ( $notify_class->disabled_url && $disabled ) { $title = "$title"; } elsif ( $notify_class->url ) { $title = "$title"; } $title .= " " . LJ::help_icon( $notify_class->help_url ) if $notify_class->help_url; push @{ $cat_data->{notify_headers} }, { ntypeid => $ntypeid, title => $title, disabled => $disabled }; } # build list of subscriptions to show the user $cat_data->{pending_subs} = []; my @pending_subscriptions = $is_tracking_category ? @$cat_events : $u->subscription_event_filter( $cat_events, $journalu, $settings_page ); # inbox method my $special_subs = 0; my $unavailable_subs = 0; my $sub_count = 0; foreach my $pending_sub (@pending_subscriptions) { my $sub_data = $u->pending_sub_data($pending_sub); next unless $sub_data; $sub_data->{altrow_class} = $sub_count % 2 == 1 ? "odd" : "even"; $sub_data->{inactive_class} = $sub_data->{inactive} ? "inactive" : ""; $sub_data->{disabled_class} = $sub_data->{disabled} ? "disabled" : ""; $sub_data->{hidden_style} = $sub_data->{hidden} ? " style='visibility: hidden;'" : ""; if ( $sub_data->{special_sub} ) { $special_subs++; push @{ $cat_data->{pending_subs} }, $sub_data; $sub_count++; next; } $unavailable_subs++ if $sub_data->{disabled}; my $evt_class = $pending_sub->event_class or next; next if !$evt_class->is_visible && $settings_page; # override disabled below if always_checked (can't uncheck) my $always_checked = eval { "$evt_class"->always_checked; }; $sub_data->{disabled} = 1 if $always_checked; if ($is_tracking_category) { next if $u->tracked_event_exclude( $pending_sub, \@prev_cats ); my $do_show = 1; $do_show = 0 unless $displayed_tracking_count >= $displayed_tracking_start && $displayed_tracking_count <= $displayed_tracking_end; $displayed_tracking_count++; if ( $do_show && $sub_data->{subscribed} ) { my $subid = $pending_sub->id; $sub_data->{subid} = $subid; $sub_data->{auth_token} = $u->ajax_auth_token( "/__rpc_esn_subs", subid => $subid, action => 'delsub' ); } $sub_data->{do_show} = $do_show; } else { $sub_data->{do_show} = 1; next unless eval { $evt_class->subscription_applicable($pending_sub) }; next if $u->equals($journalu) && $pending_sub->journalid && $pending_sub->journalid != $u->{userid}; } $cat_empty = 0; # no more nexts # this hook also populates %num_subs_by_type $sub_data->{notif_options} = LJ::Hooks::run_hook( 'subscription_notif_options', u => $u, sub_data => $sub_data, pending_sub => $pending_sub, def_notes => $def_notes, is_tracking_category => $is_tracking_category, num_subs_by_type => \%num_subs_by_type, notify_classes => \@notify_classes, ); push @{ $cat_data->{pending_subs} }, $sub_data; $sub_count++; } # show blurb if not tracking anything $cat_data->{empty_blurb} = $cat_empty && $is_tracking_category; $cat_data->{special_subs} = $special_subs; $cat_data->{show_upgrade_note} = !$u->is_paid && ( $special_subs || $unavailable_subs ); push @catdata, $cat_data; push @prev_cats, $cat_hash; $catid++; } $page_vars->{catdata} = \@catdata; $page_vars->{catids} = join ',', @catids; # show how many subscriptions we have active / inactive $page_vars->{subscription_stats} = LJ::Hooks::run_hook( 'subscription_stats', $u, \%num_subs_by_type ); $page_vars->{pagination} = LJ::paging_bar( $page, ceil( $displayed_tracking_count / $num_per_page ), { self_link => sub { return LJ::create_url( undef, args => { page => $_[0] }, keep_args => 1, no_blank => 1, fragment => "category-subscription-tracking" ); } } ); return DW::Template->template_string( 'tracking/subscribe-interface.tt', $page_vars ); } # returns a placeholder link sub placeholder_link { my (%opts) = @_; my $placeholder_html = LJ::ejs_all( delete $opts{placeholder_html} || '' ); my $width = delete $opts{width} || 100; my $height = delete $opts{height} || 100; my $width_unit = delete $opts{width_unit} || "px"; my $height_unit = delete $opts{height_unit} || "px"; my $link = delete $opts{link} || ''; my $url = delete $opts{url} || ''; my $linktext = delete $opts{linktext} || ''; my $img = delete $opts{img} || "$LJ::IMGPREFIX/videoplaceholder.png"; my $direct_link = defined $url ? '' : ''; return qq {
$direct_link }; } # this returns the right max length for a VARCHAR(255) database # column. but in HTML, the maxlength is characters, not bytes, so we # have to assume 3-byte chars and return 80 instead of 255. (80*3 == # 240, approximately 255). However, we special-case Russian where # they often need just a little bit more, and make that 100. because # their bytes are only 2, so 100 * 2 == 200. as long as russians # don't enter, say, 100 characters of japanese... but then it'd get # truncated or throw an error. we'll risk that and give them 20 more # characters. sub std_max_length { my $lang = eval { BML::get_language() }; return 80 if !$lang || $lang =~ /^en/; return 100 if $lang =~ /\b(hy|az|be|et|ka|ky|kk|lt|lv|mo|ru|tg|tk|uk|uz)\b/i; return 80; } sub final_head_html { my $ret = ""; if ( my $pagestats_obj = LJ::PageStats->new ) { $ret .= $pagestats_obj->render_head; } return $ret; } # returns HTML which should appear before sub final_body_html { my $before_body_close = ""; LJ::Hooks::run_hooks( 'insert_html_before_body_close', \$before_body_close ); if ( my $pagestats_obj = LJ::PageStats->new ) { $before_body_close .= $pagestats_obj->render; } return $before_body_close; } # return a unique per pageview string based on the remote's unique cookie sub pageview_unique_string { my $cached_uniq = $LJ::REQ_GLOBAL{pageview_unique_string}; return $cached_uniq if $cached_uniq; my $uniq = LJ::UniqCookie->current_uniq . time() . LJ::rand_chars(8); $uniq = Digest::SHA1::sha1_hex($uniq); $LJ::REQ_GLOBAL{pageview_unique_string} = $uniq; return $uniq; } # returns canonical link for use in header of journal pages sub canonical_link { my ( $url, $tid ) = @_; if ( $tid += 0 ) { # sanitize input $url .= "?thread=$tid" . LJ::Talk::comment_anchor($tid); } return qq{\n}; } # Takes a string as input and returns a canonicalized slug. This is used in # the logslugs table for URL generation. sub canonicalize_slug { return undef unless defined $_[0]; # If you change this, please update htdocs/stc/js/jquery.postform.js # to keep the regular expressions in the toSlug function in sync. my $str = LJ::trim( lc shift ); $str =~ s/\s+/-/g; $str =~ s/[^a-z0-9_-]//gi; $str =~ s/-+/-/g; $str =~ s/^-|-$//g; $str = LJ::text_trim( $str, 255, 100 ); return $str; } 1;