LJSUP-17669: Login.bml form refactoring
[livejournal.git] / cgi-bin / weblib.pl
blob5aa3f6536c6effdfe497bac31c77d36e4323c62d
1 #!/usr/bin/perl
4 package LJ;
5 use strict;
7 use lib "$ENV{LJHOME}/cgi-bin";
9 # load the bread crumb hash
10 require "crumbs.pl";
12 use Carp;
13 use LJ::Auth::Challenge;
14 use LJ::Request;
15 use LJ::JSON;
16 use Class::Autouse qw(
17 LJ::Event
18 LJ::Subscription::Pending
19 LJ::M::ProfilePage
20 LJ::Directory::Search
21 LJ::Directory::Constraint
22 LJ::M::FriendsOf
24 use LJ::ControlStrip;
25 use LJ::SiteScheme;
26 use LJ::Pics::Album;
27 use LJ::URI::Shortener;
28 use Apache::WURFL;
29 use Encode;
30 use Digest::SHA qw/sha1_base64/;
32 # <LJFUNC>
33 # name: LJ::img
34 # des: Returns an HTML &lt;img&gt; or &lt;input&gt; tag to an named image
35 # code, which each site may define with a different image file with
36 # its own dimensions. This prevents hard-coding filenames & sizes
37 # into the source. The real image data is stored in LJ::Img, which
38 # has default values provided in cgi-bin/imageconf.pl but can be
39 # overridden in etc/ljconfig.pl.
40 # args: imagecode, type?, attrs?
41 # des-imagecode: The unique string key to reference the image. Not a filename,
42 # but the purpose or location of the image.
43 # des-type: By default, the tag returned is an &lt;img&gt; tag, but if 'type'
44 # is "input", then an input tag is returned.
45 # des-attrs: Optional hashref of other attributes. If this isn't a hashref,
46 # then it's assumed to be a scalar for the 'name' attribute for
47 # input controls.
48 # </LJFUNC>
49 sub img
51 my $ic = shift;
52 my $type = shift; # either "" or "input"
53 my $attr = shift;
55 my $attrs;
56 my $alt;
57 if ($attr) {
58 if (ref $attr eq "HASH") {
59 $alt = LJ::ehtml($attr->{alt}) if (exists $attr->{alt});
60 foreach (keys %$attr) {
61 $attrs .= " $_=\"" . LJ::ehtml($attr->{$_}) . "\""
62 unless ((lc $_) eq 'alt');
64 } else {
65 $attrs = " name=\"$attr\"";
69 my $i = $LJ::Img::img{$ic};
70 $alt ||= LJ::Lang::string_exists($i->{'alt'}) ? LJ::Lang::ml($i->{'alt'}) : $i->{'alt'};
71 if ($type eq "") {
72 return "<img src=\"$LJ::IMGPREFIX$i->{'src'}\" width=\"$i->{'width'}\" ".
73 "height=\"$i->{'height'}\" alt=\"$alt\" title=\"$alt\" ".
74 "border='0'$attrs />";
76 if ($type eq "input") {
77 return "<input type=\"image\" src=\"$LJ::IMGPREFIX$i->{'src'}\" ".
78 "width=\"$i->{'width'}\" height=\"$i->{'height'}\" title=\"$alt\" ".
79 "alt=\"$alt\" border='0'$attrs />";
81 return "<b>XXX</b>";
84 # <LJFUNC>
85 # name: LJ::date_to_view_links
86 # class: component
87 # des: Returns HTML of date with links to user's journal.
88 # args: u, date
89 # des-date: date in yyyy-mm-dd form.
90 # returns: HTML with yyyy, mm, and dd all links to respective views.
91 # </LJFUNC>
92 sub date_to_view_links
94 my ($u, $date) = @_;
95 return unless $date =~ /^(\d\d\d\d)-(\d\d)-(\d\d)/;
97 my ($y, $m, $d) = ($1, $2, $3);
98 my ($nm, $nd) = ($m+0, $d+0); # numeric, without leading zeros
99 my $user = $u->{'user'};
100 my $base = LJ::journal_base($u);
102 my $ret;
103 $ret .= "<a href=\"$base/$y/\">$y</a>-";
104 $ret .= "<a href=\"$base/$y/$m/\">$m</a>-";
105 $ret .= "<a href=\"$base/$y/$m/$d/\">$d</a>";
106 return $ret;
110 # <LJFUNC>
111 # name: LJ::auto_linkify
112 # des: Takes a plain-text string and changes URLs into <a href> tags (auto-linkification).
113 # args: str
114 # des-str: The string to perform auto-linkification on.
115 # returns: The auto-linkified text.
116 # </LJFUNC>
117 sub auto_linkify
119 my $str = shift;
120 my $match = sub {
121 my $str = shift;
122 if ($str =~ /^(.*?)(&(#39|quot|lt|gt)(;.*)?)$/) {
123 return "<a href='$1'>$1</a>$2";
124 } else {
125 return "<a href='$str'>$str</a>";
128 $str =~ s!(https?://[^\s\'\"\<\>]+[a-zA-Z0-9_/&=\-])! $match->($1); !ge;
129 return $str;
132 # return 1 if URL is a safe stylesheet that S1/S2/etc can pull in.
133 # return 0 to reject the link tag
134 # return a URL to rewrite the stylesheet URL
135 # $href will always be present. $host and $path may not.
136 sub valid_stylesheet_url {
137 my ($href, $host, $path) = @_;
138 unless ($host && $path) {
139 return 0 unless $href =~ m!^https?://([^/]+?)(/.*)$!;
140 ($host, $path) = ($1, $2);
143 my $cleanit = sub {
144 # allow tag, if we're doing no css cleaning
145 return 1 if $LJ::DISABLED{'css_cleaner'};
147 # remove tag, if we have no CSSPROXY configured
148 return 0 unless $LJ::CSSPROXY;
150 # rewrite tag for CSS cleaning
151 return "$LJ::CSSPROXY?u=" . LJ::eurl($href);
154 return 1 if $LJ::TRUSTED_CSS_HOST{$host};
155 return $cleanit->() unless $host =~ /\Q$LJ::DOMAIN\E$/i;
157 # let users use system stylesheets.
158 return 1 if $host eq $LJ::DOMAIN || $host eq $LJ::DOMAIN_WEB ||
159 $href =~ /^\Q$LJ::STATPREFIX\E/;
161 # S2 stylesheets:
162 return 1 if $path =~ m!^(/\w+)?/res/(\d+)/stylesheet(\?\d+)?$!;
164 # unknown, reject.
165 return $cleanit->();
169 # <LJFUNC>
170 # name: LJ::make_authas_select
171 # des: Given a u object and some options, determines which users the given user
172 # can switch to. If the list exists, returns a select list and a submit
173 # button with labels. Otherwise returns a hidden element.
174 # returns: string of HTML elements
175 # args: u, opts?
176 # des-opts: Optional. Valid keys are:
177 # 'authas' - current user, gets selected in drop-down;
178 # 'label' - label to go before form elements;
179 # 'button' - button label for submit button;
180 # others - arguments to pass to [func[LJ::get_authas_list]];
181 # check_paid - for each user in list will set parameter 'data-paid:0|1', used by JS
182 # </LJFUNC>
183 sub make_authas_select {
184 my ($u, $opts) = @_; # type, authas, label, button
186 die "make_authas_select called outside of web context"
187 unless LJ::is_web_context();
189 my @list = LJ::get_authas_list($u, $opts);
191 # only do most of form if there are options to select from
192 shift @list if @list && $opts->{'remove_self'};
193 if (@list > 1 || $opts->{'show_me'} || $list[0] ne $u->{'user'}) {
194 my $ret;
195 my $label = $BML::ML{'web.authas.label'};
196 $label = $BML::ML{'web.authas.label.comm'} if ($opts->{'type'} eq "C");
197 $ret = ($opts->{'label'} || $label) . " ";
198 my %select_id = $opts->{'id'} ? ( id => $opts->{'id'} ) : ();
199 $ret .= LJ::html_select({ 'name' => 'authas',
200 'selected' => $opts->{'authas'} || $u->{'user'},
201 'class' => 'hideable',
202 %select_id,
204 ## We loaded all users in LJ::get_authas_list(). Here we use their singletons.
205 (map {
206 my $u = LJ::load_user ($_);
207 my %is_paid = $opts->{'check_paid'}
208 ? ($u && ($u->get_cap('perm') || $u->get_cap('paid'))
209 ? ( js_data => " data-paid='1' " )
210 : ( js_data => " data-paid='0' " )
212 : undef;
214 text => $_,
215 value => $u->display_name,
216 %is_paid,
218 } @list), @{$opts->{'add_fields'}} ) . " ";
219 $ret .= $opts->{'button_tag'} . LJ::html_submit(undef, $opts->{'button'} || $BML::ML{'web.authas.btn'}) . $opts->{'button_close_tag'};
220 return $ret;
223 # no communities to choose from, give the caller a hidden
224 my $ret = LJ::html_hidden('authas', $opts->{'authas'} || $u->{'user'});
225 $ret .= $opts->{'nocomms'} if $opts->{'nocomms'};
226 return $ret;
229 # <LJFUNC>
230 # name: LJ::make_postto_select
231 # des: Given a u object and some options, determines which users the given user
232 # can post to. If the list exists, returns a select list and a submit
233 # button with labels. Otherwise returns a hidden element.
234 # returns: string of HTML elements
235 # args: u, opts?
236 # des-opts: Optional. Valid keys are:
237 # 'postto' - current user, gets selected in drop-down;
238 # 'label' - label to go before form elements;
239 # 'button' - button label for submit button;
240 # others - arguments to pass to [func[LJ::get_postto_list]].
241 # </LJFUNC>
242 sub make_postto_select {
243 my ($u, $opts) = @_; # type, authas, label, button
245 my @list = LJ::get_postto_list($u, $opts);
247 # only do most of form if there are options to select from
248 if (@list > 1) {
249 return ($opts->{'label'} || $BML::ML{'web.postto.label'}) . " " .
250 LJ::html_select({ 'name' => 'authas',
251 'selected' => $opts->{'authas'} || $u->{'user'}},
252 map { $_, $_ } @list) . " " .
253 LJ::html_submit(undef, $opts->{'button'} || $BML::ML{'web.postto.btn'});
256 # no communities to choose from, give the caller a hidden
257 return LJ::html_hidden('authas', $opts->{'authas'} || $u->{'user'});
260 # <LJFUNC>
261 # name: LJ::help_icon
262 # des: Returns BML to show a help link/icon given a help topic, or nothing
263 # if the site hasn't defined a URL for that topic. Optional arguments
264 # include HTML/BML to place before and after the link/icon, should it
265 # be returned.
266 # args: topic, pre?, post?
267 # des-topic: Help topic key.
268 # See doc/ljconfig.pl.txt, or [special[helpurls]] for examples.
269 # des-pre: HTML/BML to place before the help icon.
270 # des-post: HTML/BML to place after the help icon.
271 # </LJFUNC>
272 sub help_icon
274 my $topic = shift;
275 my $pre = shift;
276 my $post = shift;
277 return "" unless (defined $LJ::HELPURL{$topic});
278 return "$pre<?help $LJ::HELPURL{$topic} help?>$post";
281 # like help_icon, but no BML.
282 sub help_icon_html {
283 my $topic = shift;
284 my $url = $LJ::HELPURL{$topic} or return "";
285 my $pre = shift || "";
286 my $post = shift || "";
287 my $title = shift || "";
288 my $helplink_class = ($title) ? "b-helplink b-helplink-withtitle" : "b-helplink";
289 my $title_wrapper = ($title) ? "<span class=\"b-helplink-title\">$title</span>" : "";
290 # FIXME: use LJ::img() here, not hard-coding width/height
291 return "$pre<a href=\"$url\" class=\"$helplink_class\" target=\"_blank\" title=\"Help\"><span class=\"b-helplink-icon\"></span>$title_wrapper</a>$post";
294 # <LJFUNC>
295 # name: LJ::bad_input
296 # des: Returns common BML for reporting form validation errors in
297 # a bulleted list.
298 # returns: BML showing errors.
299 # args: error*
300 # des-error: A list of errors
301 # </LJFUNC>
302 sub bad_input
304 my @errors = @_;
305 my $ret = "";
306 $ret .= "<?badcontent?>\n<ul>\n";
307 foreach my $ei (@errors) {
308 my $err = LJ::errobj($ei) or next;
309 $err->log;
310 $ret .= $err->as_bullets;
312 $ret .= "</ul>\n";
313 return $ret;
317 # <LJFUNC>
318 # name: LJ::error_list
319 # des: Returns an error bar with bulleted list of errors.
320 # returns: BML showing errors.
321 # args: error*
322 # des-error: A list of errors
323 # </LJFUNC>
324 sub error_list
326 # FIXME: retrofit like bad_input above? merge? make aliases for each other?
327 my @errors = @_;
328 my $ret;
329 $ret .= '<div class="errorbar">';
330 $ret .= "<strong>";
331 $ret .= BML::ml('error.procrequest');
332 $ret .= "</strong><ul>";
334 foreach my $ei (@errors) {
335 my $err = LJ::errobj($ei) or next;
336 $err->log;
337 $ret .= $err->as_bullets;
339 $ret .= " </ul></div>";
340 return $ret;
344 # <LJFUNC>
345 # name: LJ::error_noremote
346 # des: Returns an error telling the user to log in.
347 # returns: Translation string "error.notloggedin"
348 # </LJFUNC>
349 sub error_noremote
351 return "<?needlogin?>";
355 # <LJFUNC>
356 # name: LJ::warning_list
357 # des: Returns a warning bar with bulleted list of warnings.
358 # returns: BML showing warnings
359 # args: warnings*
360 # des-warnings: A list of warnings
361 # </LJFUNC>
362 sub warning_list
364 my @warnings = @_;
365 my $ret;
367 $ret .= "<?warningbar ";
368 $ret .= "<strong>";
369 $ret .= BML::ml('label.warning');
370 $ret .= "</strong><ul>";
372 foreach (@warnings) {
373 $ret .= "<li>$_</li>";
375 $ret .= " </ul> warningbar?>";
376 return $ret;
379 sub tosagree_widget {
380 my ($checked, $errstr) = @_;
382 return
383 "<div class='formitemDesc'>" .
384 BML::ml('tos.mustread',
385 { aopts => "target='_new' href='$LJ::SITEROOT/legal/tos.bml'" }) .
386 "</div>" .
387 "<iframe width='684' height='300' src='/legal/tos-mini.bml' " .
388 "style='border: 1px solid gray;'></iframe>" .
389 "<div>" . LJ::html_check({ name => 'agree_tos', id => 'agree_tos',
390 value => '1', selected => $checked }) .
391 "<label for='agree_tos'>" . BML::ml('tos.haveread') . "</label></div>" .
392 ($errstr ? "<?inerr $errstr inerr?>" : '');
395 sub tosagree_html {
396 my $domain = shift;
398 my $ret = "<?h1 $LJ::REQUIRED_TOS{title} h1?>";
400 my $html_str = LJ::tosagree_str($domain => 'html');
401 $ret .= "<?p $html_str p?>" if $html_str;
403 $ret .= "<div style='margin-left: 40px; margin-bottom: 20px;'>";
404 $ret .= LJ::tosagree_widget(@_);
405 $ret .= "</div>";
407 return $ret;
410 sub tosagree_str {
411 my ($domain, $key) = @_;
413 return ref $LJ::REQUIRED_TOS{$domain} && $LJ::REQUIRED_TOS{$domain}->{$key} ?
414 $LJ::REQUIRED_TOS{$domain}->{$key} : $LJ::REQUIRED_TOS{$key};
417 # <LJFUNC>
418 # name: LJ::did_post
419 # des: Cookies should only show pages which make no action.
420 # When an action is being made, check the request coming
421 # from the remote user is a POST request.
422 # info: When web pages are using cookie authentication, you can't just trust that
423 # the remote user wants to do the action they're requesting. It's way too
424 # easy for people to force other people into making GET requests to
425 # a server. What if a user requested http://server/delete_all_journal.bml,
426 # and that URL checked the remote user and immediately deleted the whole
427 # journal? Now anybody has to do is embed that address in an image
428 # tag and a lot of people's journals will be deleted without them knowing.
429 # Cookies should only show pages which make no action. When an action is
430 # being made, check that it's a POST request.
431 # returns: true if REQUEST_METHOD == "POST"
432 # </LJFUNC>
433 sub did_post
435 return (BML::get_method() eq "POST");
438 # <LJFUNC>
439 # name: LJ::robot_meta_tags
440 # des: Returns meta tags to instruct a robot/crawler to not index or follow links.
441 # returns: A string with appropriate meta tags
442 # </LJFUNC>
443 sub robot_meta_tags
445 return "<meta name=\"robots\" content=\"noindex, nofollow, noarchive\" />\n" .
446 "<meta name=\"googlebot\" content=\"noindex, nofollow, noarchive, nosnippet\" />\n";
449 sub paging_bar
451 my ($page, $pages, $opts) = @_;
453 my $self_link = $opts->{'self_link'} ||
454 sub { BML::self_link({ 'page' => $_[0] }) };
456 my $href_opts = $opts->{'href_opts'} || sub { '' };
458 my $navcrap;
459 if ($pages > 1) {
460 $navcrap .= "<center><font face='Arial,Helvetica' size='-1'><b>";
461 $navcrap .= BML::ml('ljlib.pageofpages',{'page'=>$page, 'total'=>$pages}) . "<br />";
462 my $left = "<b>&lt;&lt;</b>";
463 if ($page > 1) { $left = "<a href='" . $self_link->($page-1) . "'" . $href_opts->($page-1) . ">$left</a>"; }
464 my $right = "<b>&gt;&gt;</b>";
465 if ($page < $pages) { $right = "<a href='" . $self_link->($page+1) . "'" . $href_opts->($page+1) . ">$right</a>"; }
466 $navcrap .= $left . " ";
467 for (my $i=1; $i<=$pages; $i++) {
468 my $link = "[$i]";
469 if ($i != $page) { $link = "<a href='" . $self_link->($i) . "'" . $href_opts->($i) . ">$link</a>"; }
470 else { $link = "<font size='+1'><b>$link</b></font>"; }
471 $navcrap .= "$link ";
473 $navcrap .= "$right";
474 $navcrap .= "</font></center>\n";
475 $navcrap = BML::fill_template("standout", { 'DATA' => $navcrap });
477 return $navcrap;
480 # <LJFUNC>
481 # class: web
482 # name: LJ::make_cookie
483 # des: Prepares cookie header lines.
484 # returns: An array of cookie lines.
485 # args: name, value, expires, path?, domain?
486 # des-name: The name of the cookie.
487 # des-value: The value to set the cookie to.
488 # des-expires: The time (in seconds) when the cookie is supposed to expire.
489 # Set this to 0 to expire when the browser closes. Set it to
490 # undef to delete the cookie.
491 # des-path: The directory path to bind the cookie to.
492 # des-domain: The domain (or domains) to bind the cookie to.
493 # </LJFUNC>
494 sub make_cookie
496 my ($name, $value, $expires, $path, $domain) = @_;
497 my $cookie = "";
498 my @cookies = ();
500 # let the domain argument be an array ref, so callers can set
501 # cookies in both .foo.com and foo.com, for some broken old browsers.
502 if ($domain && ref $domain eq "ARRAY") {
503 foreach (@$domain) {
504 push(@cookies, LJ::make_cookie($name, $value, $expires, $path, $_));
506 return;
509 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime($expires);
510 $year+=1900;
512 my @day = qw{Sunday Monday Tuesday Wednesday Thursday Friday Saturday};
513 my @month = qw{Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec};
515 $cookie = sprintf "%s=%s", LJ::eurl($name), LJ::eurl($value);
517 # this logic is confusing potentially
518 unless (defined $expires && $expires==0) {
519 $cookie .= sprintf "; expires=$day[$wday], %02d-$month[$mon]-%04d %02d:%02d:%02d GMT",
520 $mday, $year, $hour, $min, $sec;
523 $cookie .= "; path=$path" if $path;
524 $cookie .= "; domain=$domain" if $domain;
525 push(@cookies, $cookie);
526 return @cookies;
529 sub set_active_crumb
531 $LJ::ACTIVE_CRUMB = shift;
532 return undef;
535 sub set_dynamic_crumb
537 my ($title, $parent) = @_;
538 $LJ::ACTIVE_CRUMB = [ $title, $parent ];
541 sub get_parent_crumb
543 my $thiscrumb = LJ::get_crumb(LJ::get_active_crumb());
544 return LJ::get_crumb($thiscrumb->[2]);
547 sub get_active_crumb
549 return $LJ::ACTIVE_CRUMB;
552 sub get_crumb_path
554 my $cur = LJ::get_active_crumb();
555 my @list;
556 while ($cur) {
557 # get crumb, fix it up, and then put it on the list
558 if (ref $cur) {
559 # dynamic crumb
560 push @list, [ $cur->[0], '', $cur->[1], 'dynamic' ];
561 $cur = $cur->[1];
562 } else {
563 # just a regular crumb
564 my $crumb = LJ::get_crumb($cur);
565 last unless $crumb;
566 last if $cur eq $crumb->[2];
567 $crumb->[3] = $cur;
568 push @list, $crumb;
570 # now get the next one we're going after
571 $cur = $crumb->[2]; # parent of this crumb
574 return @list;
577 sub get_crumb
579 my $crumbkey = shift;
580 if (defined $LJ::CRUMBS_LOCAL{$crumbkey}) {
581 return $LJ::CRUMBS_LOCAL{$crumbkey};
582 } else {
583 return $LJ::CRUMBS{$crumbkey};
587 # <LJFUNC>
588 # name: LJ::check_referer
589 # class: web
590 # des: Checks if the user is coming from a given URI.
591 # args: uri?, referer?
592 # des-uri: string; the URI we want the user to come from.
593 # des-referer: string; the location the user is posting from.
594 # If not supplied, will be retrieved with BML::get_client_header.
595 # In general, you don't want to pass this yourself unless
596 # you already have it or know we can't get it from BML.
597 # returns: 1 if they're coming from that URI, else undef
598 # </LJFUNC>
599 sub check_referer {
600 my $uri = shift(@_) || '';
601 my $referer = shift(@_) || BML::get_client_header('Referer');
603 # get referer and check
604 return 1 unless $referer;
605 return 1 if $LJ::SITEROOT && $referer =~ m!^$LJ::SITEROOT$uri!;
606 return 1 if $LJ::DOMAIN && $referer =~ m!^http://$LJ::DOMAIN$uri!;
607 return 1 if $LJ::DOMAIN_WEB && $referer =~ m!^http://$LJ::DOMAIN_WEB$uri!;
608 return 1 if $LJ::USER_VHOSTS && $referer =~ m!^http://([A-Za-z0-9_\-]{1,15})\.$LJ::DOMAIN$uri!;
609 return 1 if $uri =~ m!^http://! && $referer eq $uri;
610 return undef;
613 # <LJFUNC>
614 # name: LJ::repost_auth
615 # class: web
616 # des: Creates an authentication token to be used later to verify that a form hidden field "repost"
617 # not modified by user
618 # args: type, username, url
619 # des-type: type of repost, see LJSUP-8061
620 # des-username: name of original poster
621 # des-url: url of original post
622 # returns: HTML hidden field to be inserted into the output of a page.
623 # </LJFUNC>
624 sub repost_auth {
625 my ($type, $username, $url, $subject, $raw) = @_;
626 my $str = join( ':', map ( LJ::eurl($_), $type, $username, $url, $subject));
627 my $auth = Digest::MD5::md5_hex( $str . $LJ::REPOST_SECRET );
629 return $auth if $raw;
631 $str .= ":$auth";
632 return LJ::html_hidden("repost_params", $str);
635 # <LJFUNC>
636 # name: LJ::form_auth
637 # class: web
638 # des: Creates an authentication token to be used later to verify that a form
639 # submission came from a particular user.
640 # args: raw?
641 # des-raw: boolean; If true, returns only the token (no HTML).
642 # returns: HTML hidden field to be inserted into the output of a page.
643 # </LJFUNC>
644 sub form_auth {
645 my $raw = shift;
646 my $chal = $LJ::REQ_GLOBAL{form_auth_chal};
648 unless ($chal) {
649 my $remote = LJ::get_remote();
650 my $id = $remote ? $remote->id : 0;
651 my $sess = $remote && $remote->session ? $remote->session->id : LJ::UniqCookie->current_uniq;
653 my $auth = join('-', LJ::rand_chars(10), $id, $sess);
654 $chal = LJ::Auth::Challenge->generate(86400, $auth);
655 $LJ::REQ_GLOBAL{form_auth_chal} = $chal;
658 return $raw ? $chal : LJ::html_hidden("lj_form_auth", $chal);
661 # <LJFUNC>
662 # name: LJ::check_form_auth
663 # class: web
664 # des: Verifies form authentication created with [func[LJ::form_auth]].
665 # returns: Boolean; true if the current data in %POST is a valid form, submitted
666 # by the user in $remote using the current session,
667 # or false if the user has changed, the challenge has expired,
668 # or the user has changed session (logged out and in again, or something).
669 # </LJFUNC>
670 sub check_form_auth {
671 my $opts = shift;
672 my $formauth = $BMLCodeBlock::POST{'lj_form_auth'};
673 if (ref $opts eq 'HASH') {
674 $formauth = $opts->{'lj_form_auth'} if defined $opts->{'lj_form_auth'};
675 } else {
676 $formauth = $opts if defined $opts;
677 $opts = {};
679 return 0 unless $formauth;
681 my $remote = LJ::get_remote();
682 my $id = $remote ? $remote->id : 0;
683 my $sess = $remote && $remote->session ? $remote->session->id : LJ::UniqCookie->current_uniq;
685 # check the attributes are as they should be
686 my $attr = LJ::get_challenge_attributes($formauth);
687 my ($randchars, $chal_id, $chal_sess) = split(/\-/, $attr);
689 return 0 unless $id == $chal_id;
690 return 0 unless $sess eq $chal_sess;
692 # check the signature is good and not expired
693 return LJ::Auth::Challenge->check($formauth, {
694 dont_check_count => !$opts->{'enable_check_count'},
695 } );
698 # <LJFUNC>
699 # name: LJ::create_qr_div
700 # class: web
701 # des: Creates the hidden div that stores the QuickReply form.
702 # returns: undef upon failure or HTML for the div upon success
703 # args: user, remote, ditemid, stylemine, userpic
704 # des-u: user object or userid for journal reply in.
705 # des-ditemid: ditemid for this comment.
706 # des-stylemine: if the user has specified style=mine for this page.
707 # des-userpic: alternate default userpic.
708 # </LJFUNC>
709 sub create_qr_div {
711 my ($user, $ditemid, $stylemine, $userpic, $viewing_thread, $text_hint) = @_;
712 my $u = LJ::want_user($user);
713 my $remote = LJ::get_remote();
714 return undef unless $u && $remote && $ditemid;
715 return undef if $remote->underage;
717 $stylemine ||= 0;
718 my $qrhtml;
720 LJ::load_user_props($remote, "opt_no_quickreply");
721 return undef if $remote->{'opt_no_quickreply'};
723 $qrhtml .= "<div id='qrformdiv'><form id='qrform' name='qrform' method='POST' action='$LJ::SITEROOT/talkpost_do.bml'>";
724 $qrhtml .= LJ::form_auth();
726 my $stylemineuri = $stylemine ? "style=mine&" : "";
727 my $basepath = LJ::journal_base($u) . "/$ditemid.html?${stylemineuri}";
728 my $usertype;
730 if ($remote->is_identity && $remote->is_trusted_identity) {
731 $usertype = lc($remote->identity->short_code) . '_cookie';
732 } else {
733 $usertype = 'cookieuser';
736 $qrhtml .= LJ::html_hidden({'name' => 'replyto', 'id' => 'replyto', 'value' => ''},
737 {'name' => 'parenttalkid', 'id' => 'parenttalkid', 'value' => ''},
738 {'name' => 'journal', 'id' => 'journal', 'value' => $u->{'user'}},
739 {'name' => 'itemid', 'id' => 'itemid', 'value' => $ditemid},
740 {'name' => 'usertype', 'id' => 'usertype', 'value' => $usertype },
741 {'name' => 'qr', 'id' => 'qr', 'value' => '1'},
742 {'name' => 'cookieuser', 'id' => 'cookieuser', 'value' => $remote->{'user'}},
743 {'name' => 'dtid', 'id' => 'dtid', 'value' => ''},
744 {'name' => 'basepath', 'id' => 'basepath', 'value' => $basepath},
745 {'name' => 'stylemine', 'id' => 'stylemine', 'value' => $stylemine},
746 {'name' => 'viewing_thread', 'id' => 'viewing_thread', 'value' => $viewing_thread},
749 # rate limiting challenge
751 $qrhtml .= LJ::html_hidden("chrp1", LJ::Talk::generate_chrp1($u->{userid}, $ditemid));
754 # Start making the div itself
755 $qrhtml .= "<table style='border: 1px solid black'>";
756 $qrhtml .= "<tr valign='center'>";
757 $qrhtml .= "<td align='right'><b>".BML::ml('/talkpost.bml.opt.from')."</b></td><td align='left'>";
758 $qrhtml .= LJ::ljuser($remote->{'user'});
759 $qrhtml .= "</td><td align='center'>";
761 my (%userpicmap, $defaultpicurl);
763 # Userpic selector
765 my %res;
766 LJ::do_request({ "mode" => "login",
767 "ver" => ($LJ::UNICODE ? "1" : "0"),
768 "user" => $remote->{'user'},
769 "getpickws" => 1,
770 'getpickwurls' => 1, },
771 \%res, { "noauth" => 1, "userid" => $remote->{'userid'}}
774 if ($res{'pickw_count'}) {
775 $qrhtml .= BML::ml('/talkpost.bml.label.picturetouse2',
777 'aopts'=>"href='$LJ::SITEROOT/allpics.bml?user=$remote->{'user'}'"});
778 my @pics;
779 for (my $i=1; $i<=$res{'pickw_count'}; $i++) {
780 push @pics, $res{"pickw_$i"};
782 @pics = sort { lc($a) cmp lc($b) } @pics;
783 $qrhtml .= LJ::html_select({'name' => 'prop_picture_keyword',
784 'selected' => $userpic, 'id' => 'prop_picture_keyword', 'tabindex' => '8' },
785 ("", BML::ml('/talkpost.bml.opt.defpic'), map { ($_, $_) } @pics));
787 # userpic browse button
788 $qrhtml .= qq {
789 <input type="button" id="lj_userpicselect" value="Browse" onclick="QuickReply.userpicSelect()" tabindex="9" />
790 } unless $LJ::DISABLED{userpicselect} || ! $remote->get_cap('userpicselect');
792 $qrhtml .= LJ::help_icon_html("userpics", " ");
794 foreach my $i (1 .. $res{'pickw_count'}) {
795 $userpicmap{$res{"pickw_$i"}} = $res{"pickwurl_$i"};
798 if (my $upi = $remote->userpic) {
799 $defaultpicurl = $upi->url;
804 $qrhtml .= "</td></tr>";
806 $qrhtml .= "<tr><td align='right' valign='top'>";
807 $qrhtml .= "<b>".BML::ml('/talkpost.bml.opt.subject')."</b></td>";
808 $qrhtml .= "<td colspan='2' align='left'>";
809 $qrhtml .= "<input class='textbox' type='text' size='50' maxlength='100' name='subject' id='subject' value='' tabindex='10' />";
811 $qrhtml .= "<div id=\"subjectCaptionText\">" . $text_hint . "</div>" if $text_hint;
813 $qrhtml .= "</td></tr>";
815 $qrhtml .= "<tr valign='top'>";
816 $qrhtml .= "<td align='right'><b>".BML::ml('/talkpost.bml.opt.message')."</b></td>";
817 $qrhtml .= "<td colspan='3' style='width: 90%'>";
819 $qrhtml .= "<textarea class='textbox' rows='10' cols='50' wrap='soft' name='body' id='body' style='width: 99%' tabindex='20'></textarea>";
820 $qrhtml .= "</td></tr>";
822 $qrhtml .= LJ::run_hook('extra_quickreply_rows', {
823 'user' => $user,
824 'ditemid' => $ditemid,
827 $qrhtml .= "<tr><td>&nbsp;</td>";
828 $qrhtml .= "<td colspan='3' align='left'>";
830 $qrhtml .= LJ::html_submit('submitpost', BML::ml('/talkread.bml.button.post'),
831 { 'id' => 'submitpost',
832 'raw' => q|onclick="if (QuickReply.check()){ QuickReply.submit() }" tabindex='30' |,
835 $qrhtml .= "&nbsp;" . LJ::html_submit('submitmoreopts', BML::ml('/talkread.bml.button.more'),
836 { 'id' => 'submitmoreopts', 'tabindex' => '31',
837 'raw' => 'onclick="if (QuickReply.more()){ QuickReply.submit() }"'
839 if ($LJ::SPELLER) {
840 $qrhtml .= "&nbsp;<input type='checkbox' name='do_spellcheck' value='1' id='do_spellcheck' tabindex='32' /> <label for='do_spellcheck'>";
841 $qrhtml .= BML::ml('/talkread.bml.qr.spellcheck');
842 $qrhtml .= "</label>";
845 LJ::load_user_props($u, 'opt_logcommentips');
846 if ($u->{'opt_logcommentips'} eq 'A') {
847 $qrhtml .= '<p class="b-bubble b-bubble-alert b-bubble-noarrow b-bubble-intext b-qrform-alert-logcommentips">';
848 $qrhtml .= LJ::deemp(BML::ml('/talkpost.bml.logyourip'));
849 $qrhtml .= LJ::help_icon_html("iplogging", " ");
850 $qrhtml .= "</p>";
853 $qrhtml .= "</td></tr></table>";
854 $qrhtml .= "</form></div>";
856 my $ret;
857 $ret = "<script type=\"text/javascript\">\n";
859 $qrhtml = LJ::ejs($qrhtml);
861 # here we create some separate fields for saving the quickreply entry
862 # because the browser will not save to a dynamically-created form.
864 my $qrsaveform .= LJ::ejs(LJ::html_hidden(
865 {'name' => 'saved_subject', 'id' => 'saved_subject'},
866 {'name' => 'saved_body', 'id' => 'saved_body'},
867 {'name' => 'saved_spell', 'id' => 'saved_spell'},
868 {'name' => 'saved_upic', 'id' => 'saved_upic'},
869 {'name' => 'saved_dtid', 'id' => 'saved_dtid'},
870 {'name' => 'saved_ptid', 'id' => 'saved_ptid'},
873 %userpicmap = map { (LJ::ehtml($_) => $userpicmap{$_}) } keys %userpicmap;
875 my $userpicmap = LJ::JSON->to_json(\%userpicmap);
876 $ret .= qq{
877 var userpicmap = $userpicmap;
878 var defaultpicurl = "$defaultpicurl";
879 document.write("$qrsaveform");
880 var de = document.createElement('div');
881 de.id = 'qrdiv';
882 de.innerHTML = "$qrhtml";
883 de.style.display = 'none';
884 document.body.insertBefore(de, document.body.firstChild);
887 $ret .= "</script>";
889 return $ret;
892 # <LJFUNC>
893 # name: LJ::make_qr_link
894 # class: web
895 # des: Creates the link to toggle the QR reply form or if
896 # JavaScript is not enabled, then forwards the user through
897 # to replyurl.
898 # returns: undef upon failure or HTML for the link
899 # args: dtid, basesubject, linktext, replyurl
900 # des-dtid: dtalkid for this comment
901 # des-basesubject: parent comment's subject
902 # des-linktext: text for the user to click
903 # des-replyurl: URL to forward user to if their browser
904 # does not support QR.
905 # </LJFUNC>
906 sub make_qr_link
908 my ($dtid, $basesubject, $linktext, $replyurl) = @_;
910 return undef unless defined $dtid && $linktext && $replyurl;
912 my $remote = LJ::get_remote();
913 LJ::load_user_props($remote, "opt_no_quickreply");
914 unless ($remote->{'opt_no_quickreply'}) {
915 my $pid = int($dtid / 256);
917 $basesubject =~ s/^(Re:\s*)*//i;
918 $basesubject = "Re: $basesubject" if $basesubject;
919 $basesubject = LJ::ehtml(LJ::ejs($basesubject));
920 my $onclick = "return QuickReply.reply('$dtid',$pid,'$basesubject')";
922 my $ju;
923 $ju = LJ::load_userid(LJ::Request->notes('journalid')) if LJ::Request->is_inited and LJ::Request->notes('journalid');
925 $onclick = "" if $ju->{'opt_whocanreply'} eq 'friends' and $remote and not LJ::is_friend($ju, $remote);
926 return "<a href=\"$replyurl\" onclick=\"$onclick\" rel='nofollow'>$linktext</a>";
927 } else { # QR Disabled
928 return "<a href=\"$replyurl\" rel='nofollow'>$linktext</a>";
932 # <LJFUNC>
933 # name: LJ::get_lastcomment
934 # class: web
935 # des: Looks up the last talkid and journal the remote user posted in.
936 # returns: talkid, jid
937 # args:
938 # </LJFUNC>
939 sub get_lastcomment {
940 my $remote = LJ::get_remote();
941 return (undef, undef) unless $remote;
943 # Figure out their last post
944 my $memkey = [$remote->{'userid'}, "lastcomm:$remote->{'userid'}"];
945 my $memval = LJ::MemCache::get($memkey);
946 my ($jid, $talkid);
947 ($jid, $talkid) = split(/:/, $memval) if $memval;
949 return ($talkid, $jid);
952 # <LJFUNC>
953 # name: LJ::make_qr_target
954 # class: web
955 # des: Returns a div usable for QuickReply boxes.
956 # returns: HTML for the div
957 # args:
958 # </LJFUNC>
959 sub make_qr_target {
960 my $name = shift;
962 return "<div id='ljqrt$name' name='ljqrt$name'></div>";
965 # <LJFUNC>
966 # name: LJ::set_lastcomment
967 # class: web
968 # des: Sets the lastcomm memcached key for this user's last comment.
969 # returns: undef on failure
970 # args: u, remote, dtalkid, life?
971 # des-u: Journal they just posted in, either u or userid
972 # des-remote: Remote user
973 # des-dtalkid: Talkid for the comment they just posted
974 # des-life: How long, in seconds, the memcached key should live.
975 # </LJFUNC>
976 sub set_lastcomment
978 my ($u, $remote, $dtalkid, $life) = @_;
980 my $userid = LJ::want_userid($u);
981 return undef unless $userid && $remote && $dtalkid;
983 # By default, this key lasts for 10 seconds.
984 $life ||= 10;
986 # Set memcache key for highlighting the comment
987 my $memkey = [$remote->{'userid'}, "lastcomm:$remote->{'userid'}"];
988 LJ::MemCache::set($memkey, "$userid:$dtalkid", time()+$life);
990 return;
993 sub deemp {
994 "<span class='de'>$_[0]</span>";
997 # <LJFUNC>
998 # name: LJ::entry_form
999 # class: web
1000 # des: Returns a properly formatted form for creating/editing entries.
1001 # args: head, onload, opts
1002 # des-head: string reference for the <head> section (JavaScript previews, etc).
1003 # des-onload: string reference for JavaScript functions to be called on page load
1004 # des-opts: hashref of keys/values:
1005 # mode: either "update" or "edit", depending on context;
1006 # datetime: date and time, formatted yyyy-mm-dd hh:mm;
1007 # remote: remote u object;
1008 # subject: entry subject;
1009 # event: entry text;
1010 # richtext: allow rich text formatting;
1011 # auth_as_remote: bool option to authenticate as remote user, pre-filling pic/friend groups/etc.
1012 # return: form to include in BML pages.
1013 # </LJFUNC>
1014 sub entry_form {
1015 my $widget = LJ::Widget::EntryForm->new;
1017 $widget->set_data(@_);
1018 return $widget->render;
1021 # entry form subject
1022 sub entry_form_subject_widget {
1023 my $class = shift;
1025 if ($class) {
1026 $class = qq { class="$class" };
1028 return qq { <input name="subject" $class/> };
1031 # entry form hidden date field
1032 sub entry_form_date_widget {
1033 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
1034 $year+=1900;
1035 $mon=sprintf("%02d", $mon+1);
1036 $mday=sprintf("%02d", $mday);
1037 $min=sprintf("%02d", $min);
1038 return LJ::html_hidden({'name' => 'date_ymd_yyyy', 'value' => $year, 'id' => 'update_year'},
1039 {'name' => 'date_ymd_dd', 'value' => $mday, 'id' => 'update_day'},
1040 {'name' => 'date_ymd_mm', 'value' => $mon, 'id' => 'update_mon'},
1041 {'name' => 'hour', 'value' => $hour, 'id' => 'update_hour'},
1042 {'name' => 'min', 'value' => $min, 'id' => 'update_min'});
1045 # entry form event text box
1046 sub entry_form_entry_widget {
1047 my $class = shift;
1049 if ($class) {
1050 $class = qq { class="$class" };
1053 return qq { <textarea cols=50 rows=10 name="event" $class></textarea> };
1057 # entry form "journals can post to" dropdown
1058 # NOTE!!! returns undef if no other journals user can post to
1059 sub entry_form_postto_widget {
1060 my $remote = shift;
1062 return undef unless LJ::isu($remote);
1064 my $ret;
1065 # log in to get journals can post to
1066 my $res;
1067 $res = LJ::Protocol::do_request("login", {
1068 "ver" => $LJ::PROTOCOL_VER,
1069 "username" => $remote->{'user'},
1070 }, undef, {
1071 "noauth" => 1,
1072 "u" => $remote,
1075 return undef unless $res;
1077 my @journals = map { $_, $_ } @{$res->{'usejournals'}};
1079 return undef unless @journals;
1081 push @journals, $remote->{'user'};
1082 push @journals, $remote->{'user'};
1083 @journals = sort @journals;
1084 $ret .= LJ::html_select({ 'name' => 'usejournal', 'selected' => $remote->{'user'}}, @journals) . "\n";
1085 return $ret;
1088 sub entry_form_security_widget {
1089 my $ret = '';
1091 my @secs = ("public", BML::ml('label.security.public'),
1092 "private", BML::ml('label.security.private'),
1093 "friends", BML::ml('label.security.friends'));
1095 $ret .= LJ::html_select({ 'name' => 'security'},
1096 @secs);
1098 return $ret;
1101 sub entry_form_tags_widget {
1102 my $ret = '';
1104 return '' if $LJ::DISABLED{tags};
1106 $ret .= LJ::html_text({
1107 'name' => 'prop_taglist',
1108 'size' => '35',
1109 'maxlength' => '255',
1111 $ret .= LJ::help_icon('addtags');
1113 return $ret;
1116 # <LJFUNC>
1117 # name: LJ::entry_form_decode
1118 # class: web
1119 # des: Decodes an entry_form into a protocol-compatible hash.
1120 # info: Generate form with [func[LJ::entry_form]].
1121 # args: req, post
1122 # des-req: protocol request hash to build.
1123 # des-post: entry_form POST contents.
1124 # returns: req
1125 # </LJFUNC>
1126 sub entry_form_decode
1128 my ($req, $POST) = @_;
1130 # find security
1131 my $sec = "public";
1132 my $amask = 0;
1133 if ($POST->{'security'} eq "private") {
1134 $sec = "private";
1135 } elsif ($POST->{'security'} eq "friends") {
1136 $sec = "usemask"; $amask = 1;
1137 } elsif ($POST->{'security'} eq "custom") {
1138 $sec = "usemask";
1139 foreach my $bit (0..30) {
1140 next unless $POST->{"custom_bit_$bit"};
1141 $amask |= (1 << $bit);
1144 $req->{'security'} = $sec;
1145 $req->{'allowmask'} = $amask;
1147 # date/time
1148 my $date = LJ::html_datetime_decode({ 'name' => "date_ymd", }, $POST);
1149 my ($year, $mon, $day) = split( /\D/, $date);
1151 my $time_value= $POST->{'time'}; # get value of hh::mm
1152 my ($hour, $min) = split ( /:/, $time_value);
1154 # TEMP: ease golive by using older way of determining differences
1155 my $date_old = LJ::html_datetime_decode({ 'name' => "date_ymd_old", }, $POST);
1156 my ($year_old, $mon_old, $day_old) = split( /\D/, $date_old);
1157 my ($hour_old, $min_old) = ($POST->{'hour_old'}, $POST->{'min_old'});
1159 my $different = $POST->{'min_old'} && (($year ne $year_old) || ($mon ne $mon_old)
1160 || ($day ne $day_old) || ($hour ne $hour_old) || ($min ne $min_old));
1162 # this value is set when the JS runs, which means that the user-provided
1163 # time is sync'd with their computer clock. otherwise, the JS didn't run,
1164 # so let's guess at their timezone.
1165 if ($POST->{'date_diff'} || $POST->{'date_diff_nojs'} || $different) {
1166 $req->{'year'} = $year;
1167 $req->{'mon'} = $mon;
1168 $req->{'day'} = $day;
1169 $req->{'hour'} = $hour;
1170 $req->{'min'} = $min;
1173 # copy some things from %POST
1174 foreach (qw(subject
1175 prop_picture_keyword prop_current_moodid
1176 prop_current_mood
1177 prop_opt_screening prop_opt_noemail
1178 prop_opt_preformatted prop_opt_nocomments prop_opt_lockcomments
1179 prop_current_location prop_current_coords
1180 prop_taglist prop_qotdid prop_give_features
1181 prop_ljart_event_town prop_ljart_event_location
1182 prop_ljart_event_paid prop_ljart_event_price
1183 prop_ljart_event_type prop_ljart_event_image
1184 prop_ljart_event_desc prop_ljart_event prop_ljart_event_users
1185 prop_ljart_portfolio_thumbnail prop_ljart_portfolio
1186 repost_budget paid_repost_on repost_limit_sc
1187 repost_targeting_age repost_targeting_gender
1188 repost_targeting_country repost_targeting_state
1189 )) {
1190 $req->{$_} = $POST->{$_};
1193 # optional opts
1194 foreach (qw( prop_discovery )) {
1195 $req->{$_} = $POST->{$_} if defined $POST->{$_};
1198 if ( length( $POST->{'prop_current_music'} ) > 197 ) {
1199 my $pos = index( $POST->{'prop_current_music'}, '|' );
1201 if ( $pos == -1 ) {
1202 $req->{'prop_current_music'} = substr( $POST->{'prop_current_music'}, 0, 197 ) . '...';
1204 else {
1205 $req->{'prop_current_music'} = substr( substr($POST->{'prop_current_music'}, 0, $pos), 0, 197 ) . '... ' . substr( $POST->{'prop_current_music'}, $pos );
1208 else {
1209 $req->{'prop_current_music'} = $POST->{'prop_current_music'};
1212 if ($POST->{"subject"} eq BML::ml('entryform.subject.hint2')) {
1213 $req->{"subject"} = "";
1216 $req->{"prop_opt_preformatted"} ||= $POST->{'switched_rte_on'} ? 1 :
1217 $POST->{'event_format'} eq "preformatted" ? 1 : 0;
1218 $req->{"prop_opt_nocomments"} ||= $POST->{'comment_settings'} eq "nocomments" ? 1 : 0;
1219 $req->{'prop_opt_lockcomments'} ||= $POST->{'comment_settings'} eq 'lockcomments' ? 1 : 0;
1220 $req->{"prop_opt_noemail"} ||= $POST->{'comment_settings'} eq "noemail" ? 1 : 0;
1221 $req->{'prop_opt_backdated'} = $POST->{'prop_opt_backdated'} ? 1 : 0;
1222 $req->{'prop_opt_norating'} = $POST->{'prop_opt_norating'} ? 1 : 0;
1224 my $ljart_date_from = $POST->{'prop_ljart_event_date_from'};
1225 my $ljart_date_to = $POST->{'prop_ljart_event_date_to'};
1226 my $ljart_time_from = $POST->{'prop_ljart_event_time_from'};
1227 my $ljart_time_to = $POST->{'prop_ljart_event_time_to'};
1229 $req->{'prop_ljart_event_date'} = $ljart_date_to ? "$ljart_date_from-$ljart_date_to" : $ljart_date_from;
1230 $req->{'prop_ljart_event_time'} = $ljart_time_to ? "$ljart_time_from-$ljart_time_to" : $ljart_time_from;
1232 $req->{'prop_copyright'} = $POST->{'prop_copyright'} ? 'P' : 'C' if LJ::is_enabled('default_copyright', LJ::get_remote())
1233 && $POST->{'defined_copyright'};
1234 $req->{'prop_poster_ip'} = LJ::get_remote_ip();
1236 my $uniq = LJ::UniqCookie->current_uniq();
1237 $req->{'prop_uniq'} = $uniq;
1239 if ( my $reposted_from = $POST->{'reposted_from'} ) {
1240 my $reposted_entry = LJ::Entry->new_from_url($reposted_from);
1242 #TODO: check visibility? it's not a security concern, but still
1243 if ($reposted_entry) {
1244 $req->{'prop_reposted_from'} = $reposted_entry->url;
1248 if (LJ::is_enabled("content_flag")) {
1249 $req->{prop_adult_content} = $POST->{prop_adult_content};
1250 $req->{prop_adult_content} = ""
1251 unless $req->{prop_adult_content} eq "none" || $req->{prop_adult_content} eq "concepts" || $req->{prop_adult_content} eq "explicit";
1254 # nuke taglists that are just blank
1255 $req->{'prop_taglist'} = "" unless defined $req->{'prop_taglist'} && $req->{'prop_taglist'} =~ /\S/;
1257 # Convert the rich text editor output back to parsable lj tags.
1258 my $event = $POST->{'event'};
1259 if ($POST->{'switched_rte_on'}) {
1260 $req->{"prop_used_rte"} = 1;
1262 # We want to see if we can hit the fast path for cleaning
1263 # if they did nothing but add line breaks.
1264 my $attempt = $event;
1265 $attempt =~ s!<br />!\n!g;
1267 if ($attempt !~ /<\w/) {
1268 $event = $attempt;
1270 # Make sure they actually typed something, and not just hit
1271 # enter a lot
1272 $attempt =~ s!(?:<p>(?:&nbsp;|\s)+</p>|&nbsp;)\s*?!!gm;
1273 $event = '' unless $attempt =~ /\S/;
1275 $req->{'prop_opt_preformatted'} = 0;
1276 } else {
1277 # Old methods, left in for compatibility during code push
1278 $event =~ s!<lj-cut class="ljcut">!<lj-cut>!gi;
1280 $event =~ s!<lj-raw class="ljraw">!<lj-raw>!gi;
1282 } else {
1283 $req->{"prop_used_rte"} = 0;
1286 $req->{'event'} = $event;
1288 ## see if an "other" mood they typed in has an equivalent moodid
1289 if ($POST->{'prop_current_mood'}) {
1290 if (my $id = LJ::mood_id($POST->{'prop_current_mood'})) {
1291 $req->{'prop_current_moodid'} = $id;
1292 } else {
1293 $req->{'prop_current_moodid'} = '';
1297 # process site-specific options
1298 LJ::run_hooks('decode_entry_form', $POST, $req);
1300 return $req;
1303 # returns exactly what was passed to it normally. but in developer mode,
1304 # it includes a link to a page that automatically grants the needed priv.
1305 sub no_access_error {
1306 my ($text, $priv, $privarg) = @_;
1307 if ($LJ::IS_DEV_SERVER) {
1308 my $remote = LJ::get_remote();
1309 return "$text <b>(DEVMODE: <a href='/admin/priv/?devmode=1&user=$remote->{user}&priv=$priv&arg=$privarg'>Grant $priv\[$privarg\]</a>)</b>";
1310 } else {
1311 return $text;
1315 # Data::Dumper for JavaScript
1316 sub js_dumper {
1317 my $obj = shift;
1318 if (ref $obj) {
1319 return LJ::JSON->to_json($obj);
1320 } else {
1321 return ($obj =~ /^[1-9]\d*$/) ? $obj : '"' . LJ::ejs($obj) . '"';
1326 my %stat_cache = (); # key -> {lastcheck, modtime}
1327 sub _file_modtime {
1328 my ($key, $now) = @_;
1329 if (my $ci = $stat_cache{$key}) {
1330 if ($ci->{lastcheck} > $now - 10) {
1331 return $ci->{modtime};
1335 my $file = "$LJ::HOME/htdocs/$key";
1336 my $mtime = (stat($file))[9];
1337 $stat_cache{$key} = { lastcheck => $now, modtime => $mtime };
1338 return $mtime;
1342 ## stc/0 is the empty file.
1343 ## its modtime is checked for all concatenated sources and it is updated
1344 ## for every release, so in the most cases this modtime
1345 ## as timestamp is used for "?v" param value.
1346 sub stc_0_modtime {
1347 my $now = shift;
1348 $now = time() unless defined $now;
1350 ## touch-ing it changes ?v= param for all included res.
1351 return _file_modtime("stc/0", $now);
1354 sub stat_src_to_url {
1355 my $url = shift;
1356 my $mtime = _file_modtime("/stc" . $url, time);
1357 return $LJ::STATPREFIX . $url . "?v=" . $mtime;
1360 sub need_res_group {
1361 my (@groupnames) = @_;
1362 foreach my $name (@groupnames){
1363 my $group = $LJ::RES_GROUP_DEPS{$name};
1364 next unless $group;
1365 next if $LJ::REQ_GLOBAL{__need_res_group}->{$group}++;
1367 ## js, css, templates
1368 foreach my $resource_class (qw/js css templates/){
1369 next unless exists $group->{$resource_class};
1370 foreach my $files ($group->{$resource_class}){
1371 foreach my $resource (@$files){
1372 if (ref $resource eq 'ARRAY'){
1373 ## need_res expects $cond as a first argument,
1374 ## but for config files other order is preferable:
1375 ## source file name at first place then condition at second
1377 ## support both
1378 my ($file, $cond) = ref($resource->[0])
1379 ? ($resource->[1], $resource->[0])
1380 : ($resource->[0], $resource->[1]);
1381 LJ::need_res($cond, $file);
1382 } else {
1383 LJ::need_res($resource);
1389 ## ml
1390 if (my $mls = $group->{ml}){
1391 LJ::need_string(@$mls);
1394 ## groups
1395 if (my $groups = $group->{groups}){
1396 LJ::need_res_group($_) for @$groups;
1401 sub need_journal_res {
1402 LJ::need_string(qw{
1403 paidrepost.button.title
1404 paidrepost.button.title.owner
1405 paidrepost.button.title.curr
1406 paidrepost.button.title.delete
1407 paidrepost.button.title.counter
1409 paidrepost.button.label
1410 paidrepost.button.label.done
1412 entry.reference.label.title
1413 entry.reference.label.reposted
1416 LJ::need_string(@LJ::REPOST_ML);
1418 LJ::need_res(@LJ::JOURNAL_RES_ALL);
1421 ## Support for conditional file inclusion:
1422 ## e.g. LJ::need_res( {condition => 'IE'}, 'ie.css', 'myie.css') will result in
1423 ## <!--[if IE]><link rel="stylesheet" type="text/css" href="$statprefix/..." /><![endif]-->
1424 ## Support 'args' option. Example: LJ::need_res( { args => 'media="screen"' }, 'stc/ljtimes/iframe.css' );
1425 ## Results in: <link rel="stylesheet" type="text/css" href="http://stat.lj-3-32.bulyon.local/ljtimes/iframe.css?v=1285833891" media="screen"/>
1426 ## LJ::need_res( {clean_list => 1} ) will suppress ALL previous resources and do NOTHING more!
1427 ## LJ::need_res( {insert_head => 1}, 'my.css' ) insert my.css to the head of the list of sources.
1428 sub need_res {
1429 my $opts = (ref $_[0]) ? shift : {};
1430 my @keys = @_;
1432 if ($opts->{clean_list}) {
1433 %LJ::NEEDED_RES = ();
1434 %LJ::NAMED_NEED_RES = ();
1435 @LJ::NEEDED_RES = ();
1436 @LJ::INCLUDE_TEMPLATE = ();
1437 return;
1440 my $insert_head = $opts->{insert_head} ? 1 : 0;
1442 my @reskeys = ();
1443 foreach my $key (@keys) {
1444 my $reskey = $key;
1445 my $resopts = $opts;
1446 if (ref $reskey eq 'ARRAY'){
1447 $reskey = $key->[1];
1448 $resopts = $key->[0];
1451 if ( $reskey =~ m!^templates/! ) {
1452 push @LJ::INCLUDE_TEMPLATE, $reskey;
1453 next;
1456 die "Bogus reskey $reskey" unless $reskey =~ m!^(js|stc)/!;
1458 if ($opts->{'separate_list'}) {
1459 unless (exists $LJ::NEEDED_RES_SEPARATE{$reskey}) {
1460 push @reskeys, $reskey;
1462 $LJ::NEEDED_RES_SEPARATE{$reskey} = $resopts;
1463 } else {
1464 unless (exists $LJ::NEEDED_RES{$reskey}) {
1465 push @reskeys, $reskey;
1467 $LJ::NEEDED_RES{$reskey} = $resopts;
1471 if ($opts->{'separate_list'}) {
1472 if ($insert_head) {
1473 unshift @LJ::NEEDED_RES_SEPARATE, @reskeys;
1474 } else {
1475 push @LJ::NEEDED_RES_SEPARATE, @reskeys;
1477 } else {
1478 if ($insert_head) {
1479 unshift @LJ::NEEDED_RES, @reskeys;
1480 } else {
1481 push @LJ::NEEDED_RES, @reskeys;
1485 return;
1488 sub include_raw {
1489 my $type = shift;
1490 my $code = shift;
1492 die "Bogus include type: $type"
1493 unless $type =~ m!^(?:js|css|js_link|css_link|html)$!;
1495 push @LJ::INCLUDE_RAW => [$type, $code];
1498 sub res_template_includes {
1499 my $ret = shift;
1500 my %loaded;
1501 if (LJ::is_enabled('templates_from_stat')) {
1502 my $time = time;
1503 my $lang = LJ::Lang::current_language();
1504 my $src = $LJ::IS_SSL? $LJ::SSLROOT : $LJ::STATPREFIX;
1505 $src .= '/tmpl/??';
1507 my $timestamp = int(time() / $LJ::TEMPLATES_UPDATE_TIME);
1508 foreach my $extension ('.tmpl', '.jqtmpl') {
1509 my $mtime = 0;
1511 my @files_list = map {
1512 local $_ = $_;
1513 s{^} {../};
1514 my $lmtime = _file_modtime($_, $time);
1515 $mtime = $lmtime if $lmtime > $mtime;
1516 s{^.*?templates/} {};
1518 } grep {
1519 -1 != index $_, $extension
1520 } @LJ::SITEWIDE_TEMPLATES, @LJ::INCLUDE_TEMPLATE;
1522 $ret .= @files_list
1523 ? join (join(',', @files_list),
1524 qq{<script type="text/javascript" src="$src},
1525 qq{?v=$mtime&tm=$timestamp;uselang=$lang"></script>\n})
1526 : '';
1528 } else {
1529 foreach my $template (@LJ::SITEWIDE_TEMPLATES, @LJ::INCLUDE_TEMPLATE) {
1530 my $path = [split m{(?<!\\)/}, $template];
1531 my $file = pop @$path;
1532 my ($type, $filter);
1534 shift @$path if $path->[0] eq 'templates';
1536 $path = join '/', $LJ::TEMPLATE_BASE, @$path;
1537 my $fpath = join '/', $path, $file;
1539 -f $fpath or warn 'Missing template '. $fpath and next;
1540 $loaded{lc $fpath}++ and next;
1542 for ($file) {
1543 m{\.jqtmpl$}i and do {
1544 $type = 'JQuery.tmpl';
1545 $filter = 'jqtmpl';
1548 m{\.tmpl$}i and do {
1549 $type = 'HTML::Template';
1550 $filter = $LJ::TEMPLATE_FILTER;
1554 $type or next;
1556 my $data = LJ::Response::CachedTemplate->new(
1557 file => $file,
1558 path => $path,
1559 type => $type,
1560 translate => $LJ::TEMPLATE_TRANSLATION,
1561 filter => $filter,
1564 # Create template id
1565 my $key = $template;
1566 $key =~ s{(?<!\\)/} {-}g;
1567 $key =~ s{\.(?:jq)?tmpl$} {}g;
1569 # TODO: </script> in template can ruin your day
1570 if ( $LJ::IS_DEV_SERVER ) {
1571 $ret .= sprintf q{<script type="text/plain"
1572 id="%s"
1573 data-path="%s"
1574 data-file="%s"
1575 data-type="%s"
1576 data-filter="%s"
1577 data-translation="%s">},
1578 $key, $path, $file, $type, $filter, $LJ::TEMPLATE_TRANSLATION;
1579 $ret .= $data->raw_output();
1580 $ret .= '</script>';
1581 } else {
1582 $ret .= sprintf q{<script type="text/plain" id="%s">}, $key;
1583 $ret .= $data->raw_output();
1584 $ret .= '</script>';
1587 # Let js know about template
1588 $ret .= sprintf q{<script>LJ.UI.registerTemplate('%s', '%s', '%s');</script>}, $key, $key, $LJ::TEMPLATE_TRANSLATION;
1592 return $ret;
1595 sub res_includes {
1596 my $opts = shift || {};
1597 my $only_needed = $opts->{only_needed}; # do not include defaults
1598 my $site_anyway = $opts->{site_anyway}; # Site anyway
1599 my $is_mobile = $opts->{is_mobile}; # m.livejournal.com
1601 my $no_sitewide_css = $opts->{no_sitewide_css}; # do not include @LJ::SITEWIDE_CSS
1603 # TODO: automatic dependencies from external map and/or content of files,
1604 # currently it's limited to dependencies on the order you call LJ::need_res();
1605 my $ret = "";
1606 my $ret_js = "";
1607 my $ret_css = "";
1608 my %libs = (); ## pseudo files.
1609 my $do_concat = $LJ::IS_SSL ? $LJ::CONCAT_RES_SSL : $LJ::CONCAT_RES;
1610 my $now = time();
1612 # all conditions must be complete here
1613 # example: cyr/non-cyr flag changed at settings page
1614 unless ($only_needed) {
1615 LJ::run_hooks('sitewide_resources', {
1616 is_mobile => $is_mobile,
1617 no_sitewide_css => $no_sitewide_css,
1621 # use correct root and prefixes for SSL pages
1622 my ($siteroot, $imgprefix, $statprefix, $jsprefix, $wstatprefix);
1623 if ($LJ::IS_SSL) {
1624 $siteroot = $LJ::SSLROOT;
1625 $imgprefix = $LJ::SSLIMGPREFIX;
1626 $statprefix = $LJ::SSLSTATPREFIX;
1627 $jsprefix = $LJ::SSLJSPREFIX;
1628 $wstatprefix = $LJ::SSLWSTATPREFIX;
1630 else {
1631 $siteroot = $LJ::SITEROOT;
1632 $imgprefix = $LJ::IMGPREFIX;
1633 $statprefix = $LJ::STATPREFIX;
1634 $jsprefix = $LJ::JSPREFIX;
1635 $wstatprefix = $LJ::WSTATPREFIX;
1638 # add jQuery.tmpl templates
1639 if ( $opts->{'only_tmpl'} ) {
1640 return res_template_includes;
1643 # include standard JS info
1644 unless ( $only_needed && !$site_anyway) {
1645 # find current journal
1646 my $journal_base = '';
1647 my $journal = '';
1648 my $ju;
1650 if (LJ::Request->is_inited) {
1651 my $journalid = LJ::Request->notes('journalid');
1653 $ju = LJ::load_userid($journalid) if $journalid;
1655 if ($ju) {
1656 $journal_base = $ju->journal_base;
1657 $journal = $ju->{user};
1661 my %journal_info;
1662 if (my $journalu = LJ::get_active_journal()) {
1663 %journal_info = $journalu->info_for_js;
1664 $journal_base ||= $journalu->journal_base;
1665 $journal ||= $journalu->username;
1668 my $remote = LJ::get_remote();
1669 my $hasremote = $remote ? 1 : 0;
1670 my $remote_is_suspended = $remote && $remote->is_suspended ? 1 : 0;
1672 # ctxpopup prop
1673 my $ctxpopup = 1;
1674 $ctxpopup = 0 if $remote and not $remote->prop("opt_ctxpopup");
1675 $ctxpopup = 0 if LJ::Request->get_param('ctxpp') eq 'no';
1677 # poll for esn inbox updates?
1678 my $inbox_update_poll = $LJ::DISABLED{inbox_update_poll} ? 0 : 1;
1680 # are media embeds enabled?
1681 my $embeds_enabled = $LJ::DISABLED{embed_module} ? 0 : 1;
1683 # esn ajax enabled?
1684 my $esn_async = LJ::conf_test($LJ::DISABLED{esn_ajax}) ? 0 : 1;
1686 # remote is maintainer in current journal
1687 my $remote_is_maintainer = ($remote && $remote->can_manage($ju)) ? 1 : 0;
1689 my $default_copyright = $remote ? ($remote->prop("default_copyright") || 'P') : 'P';
1690 my $locale = LJ::Lang::current_language();
1691 $locale = $locale eq 'debug'? $locale : LJ::lang_to_locale($locale);
1693 my %comm_access = ();
1694 if ($ju && $ju->is_community && $remote && $remote->is_validated) {
1695 my @c_acc = LJ::get_comm_settings ($ju);
1696 $comm_access{'membership'} = $c_acc[0] if scalar @c_acc;
1699 my (undef, $country) = LJ::GeoLocation->ip_class ();
1700 my $ljentry = LJ::Request->notes('ljentry') || ''; # url
1701 my %site = (
1702 imgprefix => "$imgprefix",
1703 siteroot => "$siteroot",
1704 statprefix => "$statprefix",
1705 jsonrpcprefix => "$LJ::JSON_RPC_PREFIX",
1706 logprefix => "$LJ::JSLOGPREFIX",
1707 picsUploadDomain => $LJ::PICS_UPLOAD_DOMAIN,
1708 currentJournalBase => "$journal_base",
1709 currentJournal => "$journal",
1710 currentEntry => $ljentry,
1711 has_remote => $hasremote,
1712 remote_can_track_threads => $remote && $remote->get_cap('track_thread'),
1713 remote_is_suspended => $remote_is_suspended,
1714 remote_is_maintainer => $remote_is_maintainer,
1715 remote_is_identity => $remote && $remote->is_identity,
1716 remote_is_sup => LJ::SUP->is_remote_sup()? 1 : 0,
1717 ctx_popup => $ctxpopup,
1718 inbox_update_poll => $inbox_update_poll,
1719 media_embed_enabled => $embeds_enabled,
1720 esn_async => $esn_async,
1721 server_time => int time(),
1722 templates_update_time => int $LJ::TEMPLATES_UPDATE_TIME || 600,
1723 remoteJournalBase => $remote && $remote->journal_base,
1724 remoteUser => $remote && $remote->user,
1725 remoteLocale => LJ::lang_to_locale( LJ::Lang::get_remote_lang() ),
1726 locale => $locale,
1727 pics_production => LJ::is_enabled('pics_production'),
1728 v => stc_0_modtime($now),
1729 country => $country,
1730 counterprefix => "$LJ::LJCOUNTER_URI_BASE",
1731 %comm_access,
1733 $site{default_copyright} = $default_copyright if LJ::is_enabled('default_copyright', $remote);
1734 $site{is_dev_server} = 1 if $LJ::IS_DEV_SERVER;
1735 $site{inbox_unread_count} = $remote->notification_inbox->unread_count if $remote and LJ::is_enabled('inbox_unread_count_in_head');
1737 LJ::run_hooks('add_to_site_js', \%site) unless ($only_needed);
1739 LJ::need_var(D => \%LJ::JS_D) unless exists $LJ::JSVAR{'D'};
1741 my $site_params = LJ::js_dumper(\%site);
1743 my $to_json = sub {
1744 return 'null' unless $_[0];
1746 $_[0] = LJ::JSON->to_json($_[0]);
1748 # LJSUP-12854: Fix escape for Site object
1749 $_[0] =~ s{(?<=</s)(?=cript)} {"+"}gi;
1751 return $_[0];
1754 my $journal_info_json = $to_json->(\%journal_info);
1755 my $jsml_out = $to_json->(\%LJ::JSML);
1756 my $jsvar_out = $to_json->(\%LJ::JSVAR);
1757 my $remote_info = $to_json->(get_remote_info());
1758 my $journal_info = $to_json->(get_journal_info());
1759 my $entry_info = $to_json->(get_entry_info());
1760 my $ljlive_info = $to_json->(get_ljlive_info());
1762 my $site_version = LJ::ejs($LJ::CURRENT_VERSION);
1764 $ret_js .= <<"";
1765 <script type="text/javascript">
1766 Site = window.Site || {};
1767 Site.ml_text = $jsml_out;
1768 Site.page = $jsvar_out;
1769 Site.page.template = {};
1770 Site.page.ljlive = $ljlive_info;
1771 Site.timer = +(new Date());
1772 Site.remote = $remote_info;
1773 Site.journal = $journal_info;
1774 Site.entry = $entry_info;
1775 (function(){
1776 var p = $site_params, i;
1777 for (i in p) Site[i] = p[i];
1778 })();
1779 Site.current_journal = $journal_info_json;
1780 Site.version = '$site_version';
1781 </script>
1784 } ## / unless $only_needed
1786 my $host = LJ::Request->header_in("Host");
1788 # foreign domain case
1789 if (not $host =~ /\.$LJ::DOMAIN(:\d+)?$/ and not $opts->{only_css}) {
1790 my $remote = LJ::get_remote();
1792 #first part of cross-domain auth
1793 if ( $remote ) {
1795 my $hash_userid = sha1_base64($remote->{_session}->{userid} . $LJ::DOMAIN_JOURNALS_SECRET_KEY);
1796 $ret_js .= qq|
1797 <script type="text/javascript">
1798 lj_user = '$hash_userid';
1799 </script>
1802 else {
1803 $ret_js .= qq|
1804 <script type="text/javascript">
1805 lj_user = 0;
1806 </script>
1810 $ret_js .= qq|
1811 <script src="$siteroot/misc/get_auth_js.bml"></script>
1814 my $curl = LJ::Session::_current_url();
1815 $curl =~ m|^https?://(.+?)/|i;
1817 my $domain = $1;
1819 my $sign_time = time;
1820 my $curl_sign = LJ::run_hook('sign_set_domain_session_redirect' => $curl, $sign_time);
1822 $curl = LJ::eurl($curl);
1824 $ret_js .= qq|
1825 <script type="text/javascript">
1826 if( lj_user !== 0 && lj_master_user === 0 ) {
1827 window.location = "http://$domain/misc/clear_domain_session.bml?return=$curl";
1828 } else if ( lj_master_user !== 0 && lj_master_user !== lj_user ) {
1829 window.location = "${LJ::SITEROOT}/misc/get_domain_session.bml?return=$curl&sign=$curl_sign&t=$sign_time";
1831 </script>\n|;
1834 my $minify_js_flag = LJ::Request->get_param("minify_js") eq 0;
1836 my %list; # type -> condition -> args -> [list of files];
1837 my %oldest; # type -> condition -> args -> $oldest
1838 my $add = sub {
1839 my ($type, $what, $modtime, $opts) = @_;
1841 $opts ||= {};
1842 my $condition = $opts->{condition};
1843 $condition ||= ''; ## by default, no condtion is present
1845 my $args = $opts->{args};
1846 $args ||= '';
1848 # in the concat-res case, we don't directly append the URL w/
1849 # the modtime, but rather do one global max modtime at the
1850 # end, which is done later in the tags function.
1851 unless ($do_concat){
1852 $what .= "?v=$modtime";
1853 $what .= "&minify_js=0" if $minify_js_flag and $type =~ /^js/;
1856 push @{$list{$type}{$condition}{$args} ||= []}, $what;
1857 $oldest{$type}{$condition}{$args} = $modtime if $modtime > $oldest{$type}{$condition}{$args};
1860 ## Replace sources with appropriate libraries
1861 unless ($only_needed){
1862 my %libs = ();
1863 @LJ::NEEDED_RES =
1864 grep { length }
1865 map {
1866 my $res = $_;
1867 ## is the key part of library/package
1868 my $library;
1869 if ($library = ($LJ::JS_SOURCE_MAP_REV{$_} || $LJ::CSS_SOURCE_MAP_REV{$_})){
1870 $res = $libs{$library}++ ? '' : $library;
1872 $res;
1873 } @LJ::NEEDED_RES;
1876 my $mtime0 = stc_0_modtime($now);
1878 foreach my $key (@LJ::NEEDED_RES) {
1879 my $path;
1880 my $mtime;
1881 my $library;
1883 ## for libraries check mtime of all files
1884 my $library_files;
1885 if ($library_files = ($LJ::JS_SOURCES_MAP{$key} || $LJ::CSS_SOURCES_MAP{$key})){
1886 $library = $key;
1887 $libs{$library} = 1;
1888 foreach my $file (@$library_files){
1889 my $lmtime = _file_modtime($key, $now);
1890 $mtime = $lmtime if $lmtime > $mtime;
1894 $mtime = $mtime0 unless defined $mtime;
1896 $path = $key;
1898 if ($path =~ m!^js/(.+)!) {
1899 $add->("js$library", $1, $mtime, $LJ::NEEDED_RES{$key} || {});
1901 elsif ($path =~ /\.css$/ && $path =~ m!^(w?)stc/(.+)!) {
1902 $add->("${1}stccss$library", $2, $mtime, $LJ::NEEDED_RES{$key});
1904 elsif ($path =~ /\.js$/ && $path =~ m!^(w?)stc/(.+)!) {
1905 $add->("${1}stcjs", $2, $mtime, $LJ::NEEDED_RES{$key});
1909 foreach my $key (@LJ::NEEDED_RES_SEPARATE) {
1910 my $path;
1911 my $mtime;
1912 my $library;
1914 $path = $key;
1916 if ($path =~ m!^js/(.+)!) {
1917 $add->("js2$library", $1, $mtime, $LJ::NEEDED_RES_SEPARATE{$key} || {});
1919 elsif ($path =~ /\.css$/ && $path =~ m!^(w?)stc/(.+)!) {
1920 $add->("${1}stccss$library", $2, $mtime, $LJ::NEEDED_RES_SEPARATE{$key});
1922 elsif ($path =~ /\.js$/ && $path =~ m!^(w?)stc/(.+)!) {
1923 $add->("${1}stcjs", $2, $mtime, $LJ::NEEDED_RES_SEPARATE{$key});
1927 my $tags = sub {
1928 my ($type, $template) = @_;
1929 return unless $list{$type};
1930 return if $opts->{only_css}
1931 and $template =~ /^<script/;
1932 return if $opts->{only_js}
1933 and $template =~ /^<link/;
1935 my $minify_js = ($minify_js_flag and $template =~ /^<script/)
1936 ? '&minify_js=0' : "";
1938 foreach my $cond (sort {length($a) <=> length($b)} keys %{ $list{$type} }) {
1939 foreach my $args (sort {length($a) <=> length($b)} keys %{ $list{$type}{$cond} }) {
1940 my $list = $list{$type}{$cond}{$args};
1941 my $start = ($cond) ? "<!--[if $cond]>" : "";
1942 my $end = ($cond) ? "<![endif]-->\n" : "\n";
1944 if ($do_concat) {
1945 my $csep = join(',', @$list);
1946 my $mtime = $oldest{$type}{$cond}{$args};
1948 ## shorten long (>20 symbols) links
1949 if (!$LJ::DISABLED{shorten_long_stat_links} and length ($csep) > 100 and not LJ::Request->get_param('fullstatlinks')) {
1950 my $short = LJ::URI::Shortener->uri_to_id($csep);
1951 $csep = "." . $short if $short;
1954 ## stc/0 is the empty file.
1955 ## touch-ing it changes ?v= param for all included res.
1956 my $mtime_base = stc_0_modtime($now);
1957 $mtime = $mtime_base if $mtime_base > $mtime;
1959 $csep .= "?v=" . $mtime;
1960 if ($minify_js){
1961 $csep .= $minify_js;
1963 my $inc = $template;
1964 $inc =~ s/__+/??$csep/;
1965 $inc =~ s/##/$args/;
1966 $ret .= $start . $inc . $end;
1968 else {
1969 foreach my $item (@$list) {
1970 my $inc = $template;
1971 $inc =~ s/__+/$item/;
1972 $inc =~ s/##/$args/;
1973 $ret .= $start . $inc . $end;
1981 ## To ensure CSS files are downloaded in parallel, always include external CSS before external JavaScript.
1982 ## (C) http://code.google.com/speed/page-speed/
1984 unless ($opts->{only_js}) {
1985 $ret .= $ret_css;
1986 foreach my $library (@LJ::CSS_SOURCES_ORDER){ ## add libraries in strict order
1987 next unless $libs{$library};
1988 $tags->("stccss$library", "<link rel=\"stylesheet\" type=\"text/css\" href=\"$statprefix/___\" ##>");
1990 $tags->("stccss", "<link rel=\"stylesheet\" type=\"text/css\" href=\"$statprefix/___\" ##>");
1991 $tags->("wstccss", "<link rel=\"stylesheet\" type=\"text/css\" href=\"$wstatprefix/___\" ##>");
1994 unless ($opts->{only_css}) {
1995 $ret .= $ret_js;
1996 foreach my $library (@LJ::JS_SOURCES_ORDER){ ## add libraries in strict order
1997 next unless $libs{$library};
1998 $tags->("js$library", "<script type=\"text/javascript\" src=\"$jsprefix/___\"></script>");
2001 $tags->("common_js", "<script type=\"text/javascript\" src=\"$jsprefix/___\"></script>");
2002 $tags->("js", "<script type=\"text/javascript\" src=\"$jsprefix/___\"></script>");
2003 $tags->("js2", "<script type=\"text/javascript\" src=\"$jsprefix/___\"></script>");
2004 $tags->("stcjs", "<script type=\"text/javascript\" src=\"$statprefix/___\"></script>");
2005 $tags->("wstcjs", "<script type=\"text/javascript\" src=\"$wstatprefix/___\"></script>");
2008 return $ret if $only_needed;
2010 # add raw js/css
2011 foreach my $inc (@LJ::INCLUDE_RAW) {
2012 my ( $type, $code ) = @$inc;
2014 if ($type eq 'js'){
2015 $ret .= qq|<script type="text/javascript">\r\n$code</script>\r\n| unless $opts->{only_css};
2017 elsif ($type eq 'css'){
2018 $ret .= qq|<style>\r\n$code</style>\n| unless $opts->{only_js};
2020 elsif ( $type eq 'js_link' ) {
2021 $ret .= qq{<script type="text/javascript" src="$code"></script>\r\n} unless $opts->{only_css};
2023 elsif ( $type eq 'css_link' ) {
2024 $ret .= qq{<link rel="stylesheet" type="text/css" href="$code" >} unless $opts->{only_js};
2026 elsif ( $type eq 'html' ) {
2027 $ret .= $code unless $opts->{only_css}; ## add raw html to js part
2032 return $ret;
2035 sub get_remote_info {
2036 my $remote = LJ::get_remote();
2037 my $journal = LJ::get_active_journal();
2039 return unless $remote;
2041 return {
2042 ($journal ? (
2043 alias => $remote->get_alias($journal),
2044 is_friend => LJ::JSON->to_boolean($remote->is_friend($remote)),
2045 is_subscribedon => LJ::JSON->to_boolean($remote->is_mysubscription($remote)),
2046 ) : ()),
2048 # Personal info
2049 id => $remote->id,
2050 username => $remote->username,
2051 profile_url => $remote->profile_url,
2052 journal_url => $remote->journal_url,
2053 userhead_url => $remote->userhead_url,
2054 journal_title => $remote->journal_title,
2055 display_username => $remote->display_username,
2057 # Flags
2058 is_sup => LJ::JSON->to_boolean($remote->is_sup),
2059 is_paid => LJ::JSON->to_boolean($remote->is_paid),
2060 is_personal => LJ::JSON->to_boolean($remote->is_personal),
2061 is_identity => LJ::JSON->to_boolean($remote->is_identity),
2062 is_suspended => LJ::JSON->to_boolean($remote->is_suspended),
2063 is_community => LJ::JSON->to_boolean($remote->is_community),
2067 sub get_journal_info {
2068 my $remote = LJ::get_remote();
2069 my $journal = LJ::get_active_journal();
2071 return unless $journal;
2073 return {
2074 ($remote ? (
2075 is_member => LJ::JSON->to_boolean($journal->is_member($remote)),
2076 is_friend => LJ::JSON->to_boolean($remote->is_friend($journal)),
2077 is_invite_sent => LJ::JSON->to_boolean($remote->is_invite_sent($journal)),
2078 is_subscribedon => LJ::JSON->to_boolean($remote->is_mysubscription($journal)),
2079 ) : ()),
2081 # Personal info
2082 id => $journal->id,
2083 username => $journal->username,
2084 profile_url => $journal->profile_url,
2085 journal_url => $journal->journal_url,
2086 userhead_url => $journal->userhead_url,
2087 journal_title => $journal->journal_title,
2088 display_username => $journal->display_username,
2090 # Flags
2091 is_paid => LJ::JSON->to_boolean($journal->is_paid),
2092 is_personal => LJ::JSON->to_boolean($journal->is_personal),
2093 is_identity => LJ::JSON->to_boolean($journal->is_identity),
2094 is_suspended => LJ::JSON->to_boolean($journal->is_suspended),
2095 is_community => LJ::JSON->to_boolean($journal->is_community),
2099 sub get_entry_info {
2100 my $entry = LJ::Entry->new_from_url(LJ::Request->current_page_url());
2101 return unless $entry && $entry->valid;
2103 my $journal = $entry->journal;
2104 return unless $journal;
2106 my $poster = $entry->poster;
2107 return unless $poster;
2109 return {
2110 ditemid => $entry->ditemid,
2111 title => $entry->subject_raw,
2112 poster => $poster->user,
2113 journal => $journal->user,
2117 # Discovery times branding
2118 sub get_ljlive_info {
2119 my $bodyref = '';
2121 LJ::run_hooks('ljtimes_rebranding' => {
2122 'location' => 'reskining_html',
2123 'bodyref' => \$bodyref,
2126 return {
2127 ($bodyref ? (branding_template => $bodyref) : ()),
2128 is_enabled => LJ::JSON->to_boolean(LJ::Discovery::Times->is_enabled),
2132 # Returns HTML of a dynamic tag could given passed in data
2133 # Requires hash-ref of tag => { url => url, value => value }
2134 sub tag_cloud {
2135 my ($tags, $opts) = @_;
2137 # find sizes of tags, sorted
2138 my @sizes = sort { $a <=> $b } map { $tags->{$_}->{'value'} } keys %$tags;
2140 # remove duplicates:
2141 my %sizes = map { $_, 1 } @sizes;
2142 @sizes = sort { $a <=> $b } keys %sizes;
2144 my @tag_names = sort keys %$tags;
2146 my $percentile = sub {
2147 my $n = shift;
2148 my $total = scalar @sizes;
2149 for (my $i = 0; $i < $total; $i++) {
2150 next if $n > $sizes[$i];
2151 return $i / $total;
2155 my $base_font_size = 8;
2156 my $font_size_range = $opts->{font_size_range} || 25;
2157 my $ret .= "<div id='tagcloud' class='tagcloud'>";
2158 my %tagdata = ();
2159 foreach my $tag (@tag_names) {
2160 my $tagurl = $tags->{$tag}->{'url'};
2161 my $ct = $tags->{$tag}->{'value'};
2162 my $pt = int($base_font_size + $percentile->($ct) * $font_size_range);
2163 $ret .= "<a ";
2164 $ret .= "id='taglink_$tag' " unless $opts->{ignore_ids};
2165 $ret .= "href='" . LJ::ehtml($tagurl) . "' style='font-size: ${pt}pt;'><span>";
2166 $ret .= LJ::ehtml($tag) . "</span></a>\n";
2168 # build hash of tagname => final point size for refresh
2169 $tagdata{$tag} = $pt;
2171 $ret .= "</div>";
2173 return $ret;
2176 sub get_next_ad_id {
2177 return ++$LJ::REQ_GLOBAL{'curr_ad_id'};
2181 ## Function LJ::check_page_ad_block. Return answer (true/false) to question:
2182 ## Should we show ad of this type on this page.
2183 ## Args: uri of the page and orient of the ad block (e.g. 'App-Confirm')
2185 sub check_page_ad_block {
2186 my $uri = shift;
2187 my $orient = shift;
2189 # The AD_MAPPING hash may contain code refs
2190 # This allows us to choose an ad based on some logic
2191 # Example: If LJ::did_post() show 'App-Confirm' type ad
2192 my $ad_mapping = LJ::run_hook('get_ad_uri_mapping', $uri) ||
2193 LJ::conf_test($LJ::AD_MAPPING{$uri});
2195 return 1 if $ad_mapping eq $orient;
2196 return 1 if ref($ad_mapping) eq 'HASH' && $ad_mapping->{$orient};
2197 return;
2200 # returns a hash with keys "layout" and "theme"
2201 # "theme" is empty for S1 users
2202 sub get_style_for_ads {
2203 my $u = shift;
2205 my %ret;
2206 $ret{layout} = "";
2207 $ret{theme} = "";
2209 # Values for custom layers, default themes, and S1 styles
2210 my $custom_layout = "custom_layout";
2211 my $custom_theme = "custom_theme";
2212 my $default_theme = "default_theme";
2213 my $s1_prefix = "s1_";
2215 if ($u->prop('stylesys') == 2) {
2216 my %style = LJ::S2::get_style($u);
2217 my $public = LJ::S2::get_public_layers();
2219 # get layout
2220 my $layout = $public->{$style{layout}}->{uniq}; # e.g. generator/layout
2221 $layout =~ s/\/\w+$//;
2223 # get theme
2224 # if the theme id == 0, then we have no theme for this layout (i.e. default theme)
2225 my $theme;
2226 if ($style{theme} == 0) {
2227 $theme = $default_theme;
2228 } else {
2229 $theme = $public->{$style{theme}}->{uniq}; # e.g. generator/mintchoc
2230 $theme =~ s/^\w+\///;
2233 $ret{layout} = $layout ? $layout : $custom_layout;
2234 $ret{theme} = $theme ? $theme : $custom_theme;
2235 } else {
2236 my $view = LJ::Request->notes->{view};
2237 $view = "lastn" if $view eq "";
2239 if ($view =~ /^(?:friends|day|calendar|lastn)$/) {
2240 my $pubstyles = LJ::S1::get_public_styles();
2241 my $styleid = $u->prop("s1_${view}_style");
2243 my $layout = "";
2244 if ($pubstyles->{$styleid}) {
2245 $layout = $pubstyles->{$styleid}->{styledes}; # e.g. Clean and Simple
2246 $layout =~ s/\W//g;
2247 $layout =~ s/\s//g;
2248 $layout = lc $layout;
2249 $layout = $s1_prefix . $layout;
2252 $ret{layout} = $layout ? $layout : $s1_prefix . $custom_layout;
2256 return %ret;
2259 sub get_search_term {
2260 my $uri = shift;
2261 my $search_arg = shift;
2263 my %search_pages = (
2264 '/interests.bml' => 1,
2265 '/directory.bml' => 1,
2266 '/multisearch.bml' => 1,
2269 return "" unless $search_pages{$uri};
2271 my $term = "";
2272 my $args = LJ::Request->args;
2273 if ($uri eq '/interests.bml') {
2274 if ($args =~ /int=([^&]+)/) {
2275 $term = $1;
2277 } elsif ($uri eq '/directory.bml') {
2278 if ($args =~ /int_like=([^&]+)/) {
2279 $term = $1;
2281 } elsif ($uri eq '/multisearch.bml') {
2282 $term = $search_arg;
2285 # change +'s to spaces
2286 $term =~ s/\+/ /;
2288 return $term;
2292 # this returns ad html given a search string
2293 sub search_ads {
2294 my %opts = @_;
2296 return '' if LJ::conf_test($LJ::DISABLED{content_ads});
2298 return '' unless $LJ::USE_JS_ADCALL_FOR_SEARCH;
2300 my $remote = LJ::get_remote();
2302 return '' unless LJ::run_hook('should_show_ad', {
2303 ctx => 'app',
2304 user => $remote,
2305 type => '',
2308 return '' unless LJ::run_hook('should_show_search_ad');
2310 my $query = delete $opts{query} or croak "No search query specified in call to search_ads";
2311 my $count = int(delete $opts{count} || 1);
2312 my $adcount = int(delete $opts{adcount} || 3);
2314 my $adid = get_next_ad_id();
2315 my $divid = "ad_$adid";
2317 my @divids = map { "ad_$_" } (1 .. $count);
2319 my %adcall = (
2320 u => join(',', map { $adcount } @divids), # how many ads to show in each
2321 r => rand(),
2322 q => $query,
2323 id => join(',', @divids),
2324 p => 'lj',
2325 add => 'lj_content_ad',
2326 remove => 'lj_inactive_ad',
2329 if ($remote) {
2330 $adcall{user} = $remote->id;
2333 my $adparams = LJ::encode_url_string(\%adcall,
2335 sort { length $adcall{$a} <=> length $adcall{$b} }
2336 grep { length $adcall{$_} }
2337 keys %adcall
2341 # allow 24 bytes for escaping overhead
2342 $adparams = substr($adparams, 0, 1_000);
2344 my $url = $LJ::ADSERVER . '/google/?' . $adparams;
2346 my $adhtml;
2348 my $adcall = '';
2349 if (++$LJ::REQ_GLOBAL{'curr_search_ad_id'} == $count) {
2350 $adcall .= qq { <script charset="utf-8" id="ad${adid}s" src="$url"></script>\n };
2351 $adcall .= qq { <script language="javascript" src="http://www.google.com/afsonline/show_afs_ads.js"></script> };
2354 $adhtml = qq {
2355 <div class="lj_inactive_ad" id="$divid" style="clear: left;">
2356 $adcall
2357 </div>
2358 <div class='lj_inactive_ad clear'>&nbsp;</div>
2361 return $adhtml;
2364 sub get_ads {
2365 LJ::run_hook('ADV_get_ad_html', @_);
2368 sub should_show_ad {
2369 LJ::run_hook('ADV_should_show_ad', @_);
2372 # modifies list of interests (appends tags of sponsored questions to the list)
2373 # sponsored question may be taken
2374 # 1. from argument of function: $opts = { extra => {qotd => ...} },
2375 # 2. from URL args of /update.bml page (/update.bml?qotd=123)
2376 # 3. from first displayed entry on the page
2377 sub modify_interests_for_adcall {
2378 my $opts = shift;
2379 my $list = shift;
2381 my $qotd;
2382 if (ref $opts->{extra} && $opts->{extra}->{qotd}) {
2383 $qotd = $opts->{extra}->{qotd};
2384 } elsif (LJ::Request->is_inited && LJ::Request->notes('codepath') eq 'bml.update' && $BMLCodeBlock::GET{qotd}) {
2385 $qotd = $BMLCodeBlock::GET{qotd};
2386 } elsif (@LJ::SUP_LJ_ENTRY_REQ) {
2387 my ($journalid, $posterid, $ditemid) = @{ $LJ::SUP_LJ_ENTRY_REQ[0] };
2388 my $entry = LJ::Entry->new(LJ::load_userid($journalid), ditemid => $ditemid);
2389 if ($entry && $entry->prop("qotdid")) {
2390 $qotd = $entry->prop("qotdid");
2394 if ($qotd) {
2395 $qotd = LJ::QotD->get_single_question($qotd) unless ref $qotd;
2396 my $tags = LJ::QotD->remove_default_tags($qotd->{tags});
2397 if ($tags && $qotd->{is_special} eq "Y") {
2398 unshift @$list, $tags;
2403 # this function will filter out blocked interests, as well filter out interests which
2404 # cause the
2405 sub interests_for_adcall {
2406 my $u = shift;
2407 my %opts = @_;
2409 # base ad call is 300-400 bytes, we'll allow interests to be around 600
2410 # which is unlikely to go over IE's 1k URL limit.
2411 my $max_len = $opts{max_len} || 600;
2413 my $int_len = 0;
2415 my @interest_list = $u ? $u->notable_interests(100) : ();
2417 modify_interests_for_adcall(\%opts, \@interest_list);
2419 return join(',',
2420 grep {
2422 # not a blocked interest
2423 ! defined $LJ::AD_BLOCKED_INTERESTS{$_} &&
2425 # and we've not already got over 768 bytes of interests
2426 # -- +1 is for comma
2427 ($int_len += length($_) + 1) <= $max_len;
2429 } @interest_list
2433 # for use when calling an ad from BML directly
2434 sub ad_display {
2435 my %opts = @_;
2437 # can specify whether the wrapper div on the ad is used or not
2438 my $use_wrapper = defined $opts{use_wrapper} ? $opts{use_wrapper} : 1;
2440 my $ret = LJ::ads(%opts);
2442 my $extra;
2443 if ($ret =~ /"ljad ljad(.+?)"/i) {
2444 # Add a badge ad above all skyscrapers
2445 # First, try to print a badge ad in journal context (e.g. S1 comment pages)
2446 # Then, if it doesn't print, print it in user context (e.g. normal app pages)
2447 if ($1 eq "skyscraper") {
2448 $extra = LJ::ads(type => $opts{'type'},
2449 orient => 'Journal-Badge',
2450 user => $opts{'user'},
2451 search_arg => $opts{'search_arg'},
2452 force => '1' );
2453 $extra = LJ::ads(type => $opts{'type'},
2454 orient => 'App-Extra',
2455 user => $opts{'user'},
2456 search_arg => $opts{'search_arg'},
2457 force => '1' )
2458 unless $extra;
2460 $ret = $extra . $ret
2463 my $pagetype = $opts{orient};
2464 $pagetype =~ s/^BML-//;
2465 $pagetype = lc $pagetype;
2467 $ret = $opts{below_ad} ? "$ret<br />$opts{below_ad}" : $ret;
2468 $ret = $ret && $use_wrapper ? "<div class='ljadwrapper-$pagetype'>$ret</div>" : $ret;
2470 return $ret;
2473 sub control_strip {
2474 my %opts = @_;
2476 return LJ::ControlStrip->render($opts{user});
2479 sub control_strip_js_inject {
2480 my %opts = @_;
2482 LJ::ControlStrip->need_res(%opts);
2485 sub journal_js_inject {
2486 LJ::need_journal_res();
2488 LJ::need_res(qw(
2489 js/s2.js
2490 js/esn.js
2491 js/jquery/jquery.lj.confirmbubble.js
2492 js/jquery/jquery.lj.ljcut.js
2495 LJ::run_hooks('extra_journal_js');
2498 # For the Rich Text Editor
2499 # Set JS variables for use by the RTE
2500 sub rte_js_vars {
2501 my ($remote) = @_;
2503 my $ret = '';
2504 # The JS var canmakepoll is used by fckplugin.js to change the behaviour
2505 # of the poll button in the RTE.
2506 # Also remove any RTE buttons that have been set to disabled.
2507 my $canmakepoll = "true";
2508 $canmakepoll = "false" if ($remote && !LJ::get_cap($remote, 'makepoll'));
2509 $ret .= "<script type='text/javascript'>\n";
2510 $ret .= " var RTEdisabled = new Array();\n";
2511 LJ::need_var(makepoll => $canmakepoll eq 'true'? 1 : 0);
2512 my $rte_disabled = $LJ::DISABLED{rte_buttons} || {};
2513 foreach my $key (keys %$rte_disabled) {
2514 $ret .= " RTEdisabled['$key'] = true;" if $rte_disabled->{$key};
2516 $ret .= qq^
2517 var canmakepoll = $canmakepoll;
2519 function removeDisabled(ToolbarSet) {
2520 for (var i=0; i<ToolbarSet.length; i++) {
2521 for (var j=0; j<ToolbarSet[i].length; j++) {
2522 if (RTEdisabled[ToolbarSet[i][j]] == true) ToolbarSet[i].splice(j,1);
2526 </script>^;
2528 return $ret;
2531 # returns a placeholder link
2532 sub placeholder_link {
2533 my (%opts) = @_;
2535 my $placeholder_html = LJ::ejs_all(delete $opts{placeholder_html} || '');
2536 my $width = delete $opts{width} || 100;
2537 my $height = delete $opts{height} || 100;
2538 my $link = delete $opts{link} || '';
2539 my $img = delete $opts{img} || "$LJ::IMGPREFIX/videoplaceholder.png?v=8209";
2541 $width -= 2;
2542 $height -= 2;
2544 return qq {
2545 <a href="$link" class="b-mediaplaceholder b-mediaplaceholder-video } . ( $opts{remove_video_sizes} ? '" ' : ' b-mediaplaceholder-good" style="width:' . $width . 'px;height:' . $height . 'px;"' ) . ( $width ? qq~ data-width="$width"~ : '' ) . ( $height ? qq~ data-height="$height"~: '' ) . qq{ onclick="return LiveJournal.placeholderClick(this, '$placeholder_html')">
2546 <span class="b-mediaplaceholder-outer">
2547 <span class="b-mediaplaceholder-inner">
2548 <i class="b-mediaplaceholder-pic"></i>
2549 <span class="b-mediaplaceholder-label b-mediaplaceholder-view">} . ($opts{no_encode} ? Encode::decode_utf8(LJ::Lang::ml("mediaplaceholder.viewvideo")) : Encode::encode_utf8(Encode::decode_utf8(LJ::Lang::ml("mediaplaceholder.viewvideo")))) . qq{</span>
2550 <span class="b-mediaplaceholder-label b-mediaplaceholder-loading">} . ($opts{no_encode} ? Encode::decode_utf8(LJ::Lang::ml("mediaplaceholder.loading")) : Encode::encode_utf8(Encode::decode_utf8(LJ::Lang::ml("mediaplaceholder.loading")))) . qq{</span>
2551 </span>
2552 </span>
2553 </a>
2555 } #"
2557 # Returns replacement for lj-replace tags
2558 sub lj_replace {
2559 my $key = shift;
2560 my $attr = shift;
2562 # Return hook if hook output not undef
2563 if (LJ::are_hooks("lj-replace_$key")) {
2564 my $replace = LJ::run_hook("lj-replace_$key");
2565 return $replace if defined $replace;
2568 # Return value of coderef if key defined
2569 my %valid_keys = ( 'first_post' => \&lj_replace_first_post );
2571 if (my $cb = $valid_keys{$key}) {
2572 die "$cb is not a valid coderef" unless ref $cb eq 'CODE';
2573 return $cb->($attr);
2576 return undef;
2579 # Replace for lj-replace name="first_post"
2580 sub lj_replace_first_post {
2581 return unless LJ::is_web_context();
2582 return BML::ml('web.lj-replace.first_post', {
2583 'update_link' => "href='$LJ::SITEROOT/update.bml'",
2587 # this returns the right max length for a VARCHAR(255) database
2588 # column. but in HTML, the maxlength is characters, not bytes, so we
2589 # have to assume 3-byte chars and return 80 instead of 255. (80*3 ==
2590 # 240, approximately 255). However, we special-case Russian where
2591 # they often need just a little bit more, and make that 100. because
2592 # their bytes are only 2, so 100 * 2 == 200. as long as russians
2593 # don't enter, say, 100 characters of japanese... but then it'd get
2594 # truncated or throw an error. we'll risk that and give them 20 more
2595 # characters.
2596 sub std_max_length {
2597 my $lang = eval { BML::get_language() };
2598 return 80 if !$lang || $lang =~ /^en/;
2599 return 100 if $lang =~ /\b(hy|az|be|et|ka|ky|kk|lt|lv|mo|ru|tg|tk|uk|uz)\b/i;
2600 return 80;
2603 # Common challenge/response JavaScript, needed by both login pages and comment pages alike.
2604 # Forms that use this should onclick='return sendForm()' in the submit button.
2605 # Returns true to let the submit continue.
2606 $LJ::COMMON_CODE{'chalresp_js'} = qq{
2607 <script language="JavaScript" type="text/javascript">
2608 <!--
2609 function sendForm (formid, checkuser)
2611 if (formid == null) formid = 'login';
2612 // 'checkuser' is the element id name of the username textfield.
2613 // only use it if you care to verify a username exists before hashing.
2615 if (! document.getElementById) return true;
2616 var loginform = document.getElementById(formid);
2617 if (! loginform) return true;
2618 if(document.getElementById('prop_current_location')){
2619 if(document.getElementById('prop_current_location').value=='detecting...') document.getElementById('prop_current_location').value='';
2621 // Avoid accessing the password field if there is no username.
2622 // This works around Opera < 7 complaints when commenting.
2623 if (checkuser) {
2624 var username = null;
2625 for (var i = 0; username == null && i < loginform.elements.length; i++) {
2626 if (loginform.elements[i].id == checkuser) username = loginform.elements[i];
2628 if (username != null && username.value == "") return true;
2631 if (! loginform.password || ! loginform.login_chal || ! loginform.login_response) return true;
2632 var pass = loginform.password.value;
2633 var chal = loginform.login_chal.value;
2634 var res = MD5(chal + MD5(pass));
2635 loginform.login_response.value = res;
2636 loginform.password.value = ""; // dont send clear-text password!
2637 return true;
2639 // -->
2640 </script>
2643 # Common JavaScript function for auto-checking radio buttons on form
2644 # input field data changes
2645 $LJ::COMMON_CODE{'autoradio_check'} = q{
2646 <script language="JavaScript" type="text/javascript">
2647 <!--
2648 /* If radioid exists, check the radio button. */
2649 function checkRadioButton(radioid) {
2650 if (!document.getElementById) return;
2651 var radio = document.getElementById(radioid);
2652 if (!radio) return;
2653 radio.checked = true;
2655 // -->
2656 </script>
2659 sub initial_body_html {
2660 my $after_body_open = '';
2661 LJ::run_hooks('insert_html_after_body_open', \$after_body_open);
2662 return $after_body_open;
2665 # returns HTML which should appear before </body>
2666 sub final_body_html {
2667 my $before_body_close = "";
2668 LJ::run_hooks('insert_html_before_body_close', \$before_body_close);
2670 if (LJ::Request->notes('codepath') eq "bml.talkread" || LJ::Request->notes('codepath') eq "bml.talkpost") {
2671 my $journalu = LJ::load_userid(LJ::Request->notes('journalid'));
2672 unless (LJ::Request->notes('bml_use_scheme') eq 'lynx') {
2673 my $graphicpreviews_obj = LJ::graphicpreviews_obj();
2674 $before_body_close .= $graphicpreviews_obj->render($journalu);
2678 return $before_body_close;
2681 # return a unique per pageview string based on the remote's unique cookie
2682 sub pageview_unique_string {
2683 my $cached_uniq = $LJ::REQ_GLOBAL{pageview_unique_string};
2684 return $cached_uniq if $cached_uniq;
2686 my $uniq = LJ::UniqCookie->current_uniq . time() . LJ::rand_chars(8);
2687 $uniq = Digest::SHA1::sha1_hex($uniq);
2689 $LJ::REQ_GLOBAL{pageview_unique_string} = $uniq;
2690 return $uniq;
2693 # <LJFUNC>
2694 # name: LJ::site_schemes
2695 # class: web
2696 # des: Returns a list of available BML schemes.
2697 # args: none
2698 # return: array
2699 # </LJFUNC>
2700 sub site_schemes {
2701 my @schemes = @LJ::SCHEMES;
2702 LJ::run_hooks('modify_scheme_list', \@schemes);
2703 @schemes = grep { !$_->{disabled} } @schemes;
2704 return @schemes;
2707 # returns a random value between 0 and $num_choices-1 for a particular uniq
2708 # if no uniq available, just returns a random value between 0 and $num_choices-1
2709 sub ab_testing_value {
2710 my %opts = @_;
2712 return $LJ::DEBUG{ab_testing_value} if defined $LJ::DEBUG{ab_testing_value};
2714 my $num_choices = $opts{num_choices} || 2;
2715 my $uniq = LJ::UniqCookie->current_uniq;
2717 my $val;
2718 if ($uniq) {
2719 $val = unpack("I", $uniq);
2720 $val %= $num_choices;
2721 } else {
2722 $val = int(rand($num_choices));
2725 return $val;
2728 # sets up appropriate js for journals that need a special statusvis message at the top
2729 # returns some js that must be added onto the journal page's head
2730 sub statusvis_message_js {
2731 my $u = shift;
2733 return "" unless $u;
2735 my $statusvis = $u->statusvis;
2736 return "" unless $statusvis =~ /^[LMO]$/;
2738 my $statusvis_full = "";
2739 $statusvis_full = "locked" if $statusvis eq "L";
2740 $statusvis_full = "memorial" if $statusvis eq "M";
2741 $statusvis_full = "readonly" if $statusvis eq "O";
2743 LJ::need_res("js/statusvis_message.js");
2745 LJ::need_var(StatusvisMessage => LJ::Lang::ml("statusvis_message.$statusvis_full"));
2748 sub needlogin_redirect_url {
2749 my $uri = LJ::Request->uri;
2750 if (my $qs = LJ::Request->args) {
2751 $uri .= "?" . $qs;
2753 $uri = LJ::eurl($uri);
2755 return "$LJ::SITEROOT/login.bml?returnto=$uri";
2758 sub needlogin_redirect {
2759 return LJ::Request->redirect( LJ::needlogin_redirect_url() );
2762 sub get_body_class_for_service_pages {
2763 my %opts = @_;
2765 my @classes;
2766 push @classes, @{ $opts{'classes'} } if $opts{'classes'};
2767 push @classes, (LJ::get_remote()) ? 'logged-in' : 'logged-out';
2768 push @classes, 'p-ssl' if $LJ::IS_SSL;
2770 my $uri = LJ::Request->uri;
2771 my $host = LJ::Request->header_in("Host");
2772 if ($uri =~ m!^/index\.bml$!) {
2773 push @classes, "index-page";
2774 } elsif ($uri =~ m{^/stats/latest\.bml$}) {
2775 push @classes, "p-lenta";
2776 } elsif ($uri =~ m!^/shop(/.*)?$!) {
2777 push @classes, "shop-page";
2778 } elsif ($uri =~ m!^/pics(/.*)?$!) {
2779 if (LJ::_is_pics_branding_active()) {
2780 my ($user) = $host =~ /([\w\-]{1,15})\.\Q$LJ::DOMAIN\E$/;
2781 $user = LJ::get_remote () || LJ::load_user ($user);
2782 if ($user && LJ::Pics::Album->list( 'userid' => $user->userid )) {
2783 ## photos exists
2784 push @classes, "b-foto-branding-view";
2785 } else {
2786 ## no photos
2787 push @classes, "b-foto-branding-promo";
2789 } else {
2790 ## branding is not active
2791 push @classes, "framework-page";
2793 } elsif ($uri =~ m!^/browse(/.*)?$!) {
2794 push @classes, "catalogue-page";
2795 } elsif (
2796 $uri =~ m!^/games(/.*)?$!
2797 || $host eq "$LJ::USERAPPS_SUBDOMAIN.$LJ::DOMAIN"
2798 || $uri =~ m!^/adv(/.*)?$!
2800 push @classes, 'framework-page';
2801 } elsif ($uri =~ m|^/friendstimes|
2802 or ($host =~ m!^(\w+)\.\Q$LJ::USER_DOMAIN\E$!
2803 and $LJ::IS_USER_DOMAIN->{$1}
2804 and $uri =~ m!/([\w-]+)/friendstimes/?!
2807 push @classes, "p-friendstimes";
2808 } elsif (LJ::Request->notes ("homepage_v2")) {
2809 push @classes, "p-home";
2811 if ($uri =~ m!^/(?:update|editjournal)\.bml!) {
2812 push @classes, "b-foto-branding"
2813 if LJ::_is_pics_branding_active();
2816 LJ::run_hooks( 'get_body_class_for_service_pages', \@classes );
2818 return join(" ", @classes);
2821 # Add some javascript language strings
2822 sub need_string {
2823 my @strings = @_;
2825 for my $item (@strings) {
2826 # When comes as a hash ref, should be treated as name => value
2827 if(ref $item eq 'HASH') {
2828 for my $key (keys %$item) {
2829 $LJ::JSML{$key} = $item->{$key};
2831 # When handling array ref, name the ml by the value of the second element
2832 } elsif(ref $item eq 'ARRAY') {
2833 $LJ::JSML{$$item[1]} = LJ::Lang::ml($$item[0]);
2834 # If scalar - use the ml named this way
2835 } else {
2836 $LJ::JSML{$item} = LJ::Lang::ml($item);
2841 # Add some javascript variables
2842 sub need_var {
2843 my %vars;
2845 # Our arguments are hash ref
2846 if (@_ == 1 and ref $_[0] and ref $_[0] eq 'HASH') {
2847 %vars = %{$_[0]};
2848 # List of key-value pairs otherwise
2849 } else {
2850 while (my ($k, $v) = splice @_, 0, 2) {
2851 $vars{$k} = $v;
2855 while (my ($k, $v) = each %vars) {
2856 warn 'JS Variable override: '. $k
2857 if $LJ::IS_DEV_SERVER and exists $LJ::JSVAR{$k};
2859 $LJ::JSVAR{$k} = $v;
2863 sub set_remote_language {
2864 my ($lang) = @_;
2866 my $l = LJ::Lang::get_lang($lang);
2867 my $remote = LJ::get_remote();
2869 my $exptime = 0;
2871 my $cval = $l->{'lncode'} . '/' . time();
2873 # if logged in, change userprop and make cookie expiration
2874 # the same as their login expiration
2875 if ($remote) {
2876 $remote->set_prop( 'browselang' => $l->{lncode} );
2878 if ( $remote->{'_session'}->{'exptype'} eq 'long' ) {
2879 $exptime = $remote->{'_session'}->{'timeexpire'};
2883 # set cookie
2884 LJ::Request->set_cookie( 'langpref' => $cval, 'expires' => $exptime );
2886 # set language through BML so it will apply immediately
2887 BML::set_language( $l->{'lncode'} );
2889 return;
2892 sub priv_for_page {
2893 my $url = shift;
2894 return undef unless $url;
2895 return undef unless $LJ::PAGE_PRIVILEGES{$url};
2896 my $priv = $LJ::PAGE_PRIVILEGES{$url}{'priv'};
2897 my $arg = $LJ::PAGE_PRIVILEGES{$url}{'arg'};
2898 return "$priv:$arg";
2901 # http://ogp.me/, https://dev.twitter.com/docs/cards
2902 sub metadata_html {
2903 my $meta = shift;
2904 return '' unless $meta;
2906 # https://dev.twitter.com/docs/cards/app-installs-and-deep-linking
2907 (my $iosScheme .= $meta->{'url'}) =~ s/^(http)/lj/;
2909 my %tags = (
2910 'og:title' => $meta->{'title'} || '(no title)',
2911 'og:description' => $meta->{'description'} || '',
2912 'og:image' => $meta->{'image'},
2913 'og:type' => 'website',
2914 'og:url' => $meta->{'url'} || $LJ::SITEROOT,
2915 'twitter:card' => 'summary',
2916 'twitter:site' => '@livejournal',
2918 'twitter:app:name:iphone' => 'LiveJournal',
2919 "twitter:app:id:iphone" => '383091547',
2920 "twitter:app:url:iphone" => $iosScheme,
2921 "twitter:app:name:ipad" => 'LiveJournal',
2922 "twitter:app:id:ipad" => '383091547',
2923 "twitter:app:url:ipad" => $iosScheme
2926 my $html = '';
2927 foreach my $k ( sort keys %tags ) {
2928 my $property_ehtml = LJ::ehtml($k);
2929 my $content_ehtml = LJ::ehtml( $tags{$k} );
2930 $html .=
2931 qq{<meta property="$property_ehtml" content="$content_ehtml" />};
2934 return $html;