#!/usr/bin/perl # use strict; use lib "$ENV{'LJHOME'}/src/s2"; use S2; use S2::Checker; use S2::Color; use S2::Compiler; use Storable; use Apache::Constants (); use HTMLCleaner; use POSIX (); use LJ::S2::RecentPage; use LJ::S2::YearPage; use LJ::S2::DayPage; use LJ::S2::FriendsPage; use LJ::S2::MonthPage; use LJ::S2::EntryPage; use LJ::S2::ReplyPage; package LJ::S2; # TEMP HACK sub get_s2_reader { return LJ::get_dbh("s2slave", "slave", "master"); } sub make_journal { my ($u, $styleid, $view, $remote, $opts) = @_; my $r = $opts->{'r'}; my $ret; $LJ::S2::ret_ref = \$ret; my ($entry, $page); my $con_opts = {}; if ($view eq "res") { if ($opts->{'pathextra'} =~ m!/(\d+)/stylesheet$!) { $styleid = $1; $entry = "print_stylesheet()"; $opts->{'contenttype'} = 'text/css'; $con_opts->{'use_modtime'} = 1; } else { $opts->{'handler_return'} = 404; return; } } $u->{'_s2styleid'} = $styleid + 0; $con_opts->{'u'} = $u; $con_opts->{'style_u'} = $opts->{'style_u'}; my $ctx = s2_context($r, $styleid, $con_opts); unless ($ctx) { $opts->{'handler_return'} = Apache::Constants::OK(); return; } my $lang = 'en'; LJ::run_hook('set_s2bml_lang', $ctx, \$lang); # note that's it's very important to pass LJ::Lang::get_text here explicitly # rather than relying on BML::set_language's fallback mechanism, which won't # work in this context since BML::cur_req won't be loaded if no BML requests # have been served from this Apache process yet BML::set_language($lang, \&LJ::Lang::get_text); # let layouts disable EntryPage / ReplyPage, using the BML version # instead. if ($ctx->[S2::PROPS]->{'view_entry_disabled'} && ($view eq "entry" || $view eq "reply")) { ${$opts->{'handle_with_bml_ref'}} = 1; return; } # make sure capability supports it if (($view eq "entry" || $view eq "reply") && ! LJ::get_cap(($opts->{'checkremote'} ? $remote : $u), "s2view$view")) { ${$opts->{'handle_with_bml_ref'}} = 1; return; } # setup tags backwards compatibility unless ($ctx->[S2::PROPS]->{'tags_aware'}) { $opts->{enable_tags_compatibility} = 1; } escape_context_props($ctx->[S2::PROPS]); $opts->{'ctx'} = $ctx; $LJ::S2::CURR_CTX = $ctx; foreach ("name", "url", "urlname") { LJ::text_out(\$u->{$_}); } $u->{'_journalbase'} = LJ::journal_base($u->{'user'}, $opts->{'vhost'}); if ($view eq "lastn") { $entry = "RecentPage::print()"; $page = RecentPage($u, $remote, $opts); } elsif ($view eq "calendar") { $entry = "YearPage::print()"; $page = YearPage($u, $remote, $opts); } elsif ($view eq "day") { $entry = "DayPage::print()"; $page = DayPage($u, $remote, $opts); } elsif ($view eq "friends" || $view eq "friendsfriends") { $entry = "FriendsPage::print()"; $page = FriendsPage($u, $remote, $opts); } elsif ($view eq "month") { $entry = "MonthPage::print()"; $page = MonthPage($u, $remote, $opts); } elsif ($view eq "entry") { $entry = "EntryPage::print()"; $page = EntryPage($u, $remote, $opts); } elsif ($view eq "reply") { $entry = "ReplyPage::print()"; $page = ReplyPage($u, $remote, $opts); } return if $opts->{'suspendeduser'}; return if $opts->{'handler_return'}; # the friends mode=live returns raw HTML in $page, in which case there's # nothing to "run" with s2_run. so $page isn't runnable, return it now. # but we have to make sure it's defined at all first, otherwise things # like print_stylesheet() won't run, which don't have an method invocant return $page if $page && ref $page ne 'HASH'; s2_run($r, $ctx, $opts, $entry, $page); if (ref $opts->{'errors'} eq "ARRAY" && @{$opts->{'errors'}}) { return join('', "Errors occurred processing this page:"); } # unload layers that aren't public LJ::S2::cleanup_layers($ctx); # see if we can save some bandwidth (though we already killed a bunch of CPU) my $etag = Digest::MD5::md5_hex(pack('C*', unpack('C*', $ret))); $etag = '"' . $etag . '"'; my $ifnonematch = $r->header_in("If-None-Match"); if (defined $ifnonematch && $etag eq $ifnonematch) { $opts->{'notmodified'} = 1; return undef; } else { $r->header_out("ETag", $etag); } return $ret; } sub s2_run { my ($r, $ctx, $opts, $entry, $page) = @_; my $ctype = $opts->{'contenttype'} || "text/html"; my $cleaner; my $cleaner_output = sub { my $text = shift; # expand lj-embed tags if ($text =~ /lj\-embed/i) { # find out what journal we're looking at my $r = eval { Apache->request }; if ($r && $r->notes("journalid")) { my $journal = LJ::load_userid($r->notes("journalid")); # expand tags LJ::EmbedModule->expand_entry($journal, \$text) if $journal; } } $$LJ::S2::ret_ref .= $text; }; if ($ctype =~ m!^text/html!) { $cleaner = HTMLCleaner->new( 'output' => $cleaner_output, 'valid_stylesheet' => \&LJ::valid_stylesheet_url, ); } my $send_header = sub { my $status = $ctx->[S2::SCRATCH]->{'status'} || 200; $r->status($status); $r->content_type($ctx->[S2::SCRATCH]->{'ctype'} || $ctype); $r->send_http_header(); }; my $need_flush; my $out_straight = sub { # Hacky: forces text flush. see: # http://zilla.livejournal.org/906 if ($need_flush) { $cleaner->parse(""); $need_flush = 0; } $$LJ::S2::ret_ref .= $_[0]; }; my $out_clean = sub { $cleaner->parse($_[0]); $need_flush = 1; }; S2::set_output($out_straight); S2::set_output_safe($cleaner ? $out_clean : $out_straight); $LJ::S2::CURR_PAGE = $page; $LJ::S2::RES_MADE = 0; # standard resources (Image objects) made yet eval { S2::run_code($ctx, $entry, $page); }; $LJ::S2::CURR_PAGE = undef; $LJ::S2::CURR_CTX = undef; if ($@) { my $error = $@; $error =~ s/\n/
\n/g; S2::pout("Error running style: $error"); return 0; } $cleaner->eof if $cleaner; # flush any remaining text/tag not yet spit out return 1; } # returns hashref { lid => $u }; undef on error sub get_layer_owners { my @lids = map { $_ + 0 } @_; return {} unless @lids; my $ret = {}; # lid => uid/$u my @need; # see what we can get out of memcache first my @keys; push @keys, [ $_, "s2lo:$_" ] foreach @lids; my $memc = LJ::MemCache::get_multi(@keys); foreach my $lid (@lids) { if (my $uid = $memc->{"s2lo:$lid"}) { $ret->{$lid} = $uid; } else { push @need, $lid; } } # if we still need any from the database, get them now if (@need) { my $dbh = LJ::get_db_writer(); my $in = join(',', @need); my $res = $dbh->selectall_arrayref("SELECT s2lid, userid FROM s2layers WHERE s2lid IN ($in)"); die "Database error in LJ::S2::get_layer_owners: " . $dbh->errstr . "\n" if $dbh->err; foreach my $row (@$res) { # save info and add to memcache $ret->{$row->[0]} = $row->[1]; LJ::MemCache::add([ $row->[0], "s2lo:$row->[0]" ], $row->[1]); } } # now load these users; they're likely process cached anyway, so it should # be pretty fast my $us = LJ::load_userids(values %$ret); foreach my $lid (keys %$ret) { $ret->{$lid} = $us->{$ret->{$lid}} } return $ret; } # returns max comptime of all lids requested to be loaded sub load_layers { my @lids = map { $_ + 0 } @_; return 0 unless @lids; my $maxtime = 0; # to be returned # figure out what is process cached...that goes to DB always # if it's not in process cache, hit memcache first my @from_db; # lid, lid, lid, ... my @need_memc; # lid, lid, lid, ... # initial sweep, anything loaded for less than 60 seconds is golden foreach my $lid (@lids) { if (my $loaded = S2::layer_loaded($lid, 60)) { # it's loaded and not more than 60 seconds load, so we just go # with it and assume it's good... if it's been recompiled, we'll # figure it out within the next 60 seconds $maxtime = $loaded if $loaded > $maxtime; } else { push @need_memc, $lid; } } # attempt to get things in @need_memc from memcache my $memc = LJ::MemCache::get_multi(map { [ $_, "s2c:$_"] } @need_memc); foreach my $lid (@need_memc) { if (my $row = $memc->{"s2c:$lid"}) { # load the layer from memcache; memcache data should always be correct my ($updtime, $data) = @$row; if ($data) { $maxtime = $updtime if $updtime > $maxtime; S2::load_layer($lid, $data, $updtime); } } else { # make it exist, but mark it 0 push @from_db, $lid; } } # it's possible we don't need to hit the database for anything return $maxtime unless @from_db; # figure out who owns what we need my $us = LJ::S2::get_layer_owners(@from_db); my $sysid = LJ::get_userid('system'); # break it down by cluster my %bycluster; # cluster => [ lid, lid, ... ] foreach my $lid (@from_db) { next unless $us->{$lid}; if ($us->{$lid}->{userid} == $sysid) { push @{$bycluster{0} ||= []}, $lid; } else { push @{$bycluster{$us->{$lid}->{clusterid}} ||= []}, $lid; } } # big loop by cluster foreach my $cid (keys %bycluster) { # if we're talking about cluster 0, the global, pass it off to the old # function which already knows how to handle that unless ($cid) { my $dbr = LJ::S2::get_s2_reader(); S2::load_layers_from_db($dbr, @{$bycluster{$cid}}); next; } my $db = LJ::get_cluster_master($cid); die "Unable to obtain handle to cluster $cid for LJ::S2::load_layers\n" unless $db; # create SQL to load the layers we want my $where = join(' OR ', map { "(userid=$us->{$_}->{userid} AND s2lid=$_)" } @{$bycluster{$cid}}); my $sth = $db->prepare("SELECT s2lid, compdata, comptime FROM s2compiled2 WHERE $where"); $sth->execute; # iterate over data, memcaching as we go while (my ($id, $comp, $comptime) = $sth->fetchrow_array) { LJ::text_uncompress(\$comp); LJ::MemCache::set([ $id, "s2c:$id" ], [ $comptime, $comp ]) if length $comp <= $LJ::MAX_S2COMPILED_CACHE_SIZE; S2::load_layer($id, $comp, $comptime); $maxtime = $comptime if $comptime > $maxtime; } } # now we have to go through everything again and verify they're all loaded and # otherwise do a fallback to the global my @to_load; foreach my $lid (@from_db) { next if S2::layer_loaded($lid); unless ($us->{$lid}) { print STDERR "Style $lid has no available owner.\n"; next; } if ($us->{$lid}->{userid} == $sysid) { print STDERR "Style $lid is owned by system but failed load from global.\n"; next; } if ($LJ::S2COMPILED_MIGRATION_DONE) { LJ::MemCache::set([ $lid, "s2c:$lid" ], [ time(), 0 ]); next; } push @to_load, $lid; } return $maxtime unless @to_load; # get the dbh and start loading these my $dbr = LJ::S2::get_s2_reader(); die "Failure getting S2 database handle in LJ::S2::load_layers\n" unless $dbr; my $where = join(' OR ', map { "s2lid=$_" } @to_load); my $sth = $dbr->prepare("SELECT s2lid, compdata, comptime FROM s2compiled WHERE $where"); $sth->execute; while (my ($id, $comp, $comptime) = $sth->fetchrow_array) { S2::load_layer($id, $comp, $comptime); $maxtime = $comptime if $comptime > $maxtime; } return $maxtime; } # find existing re-distributed layers that are in the database # and their styleids. sub get_public_layers { my $sysid = shift; # optional system userid (usually not used) $LJ::CACHED_PUBLIC_LAYERS ||= LJ::MemCache::get("s2publayers") unless $LJ::LESS_CACHING; return $LJ::CACHED_PUBLIC_LAYERS if $LJ::CACHED_PUBLIC_LAYERS; $sysid ||= LJ::get_userid("system"); my $layers = get_layers_of_user($sysid, "is_system"); return $layers if $LJ::LESS_CACHING; $LJ::CACHED_PUBLIC_LAYERS = $layers if $layers; LJ::MemCache::set("s2publayers", $layers, 60*10) if $layers; return $LJ::CACHED_PUBLIC_LAYERS; } # update layers whose b2lids have been remapped to new s2lids sub b2lid_remap { my ($uuserid, $s2lid, $b2lid) = @_; my $b2lid_new = $LJ::S2LID_REMAP{$b2lid}; return undef unless $uuserid && $s2lid && $b2lid && $b2lid_new; my $sysid = LJ::get_userid("system"); return undef unless $sysid; LJ::statushistory_add($uuserid, $sysid, 'b2lid_remap', "$s2lid: $b2lid=>$b2lid_new"); my $dbh = LJ::get_db_writer(); return $dbh->do("UPDATE s2layers SET b2lid=? WHERE s2lid=?", undef, $b2lid_new, $s2lid); } sub get_layers_of_user { my ($u, $is_system) = @_; my $userid = LJ::want_userid($u); return undef unless $userid; undef $u unless LJ::isu($u); return $u->{'_s2layers'} if $u && $u->{'_s2layers'}; my %layers; # id -> {hashref}, uniq -> {same hashref} my $dbr = LJ::S2::get_s2_reader(); my $extrainfo = $is_system ? "'redist_uniq', " : ""; my $sth = $dbr->prepare("SELECT i.infokey, i.value, l.s2lid, l.b2lid, l.type ". "FROM s2layers l, s2info i ". "WHERE l.userid=? AND l.s2lid=i.s2lid AND ". "i.infokey IN ($extrainfo 'type', 'name', 'langcode', ". "'majorversion', '_previews')"); $sth->execute($userid); die $dbr->errstr if $dbr->err; while (my ($key, $val, $id, $bid, $type) = $sth->fetchrow_array) { $layers{$id}->{'b2lid'} = $bid; $layers{$id}->{'s2lid'} = $id; $layers{$id}->{'type'} = $type; $key = "uniq" if $key eq "redist_uniq"; $layers{$id}->{$key} = $val; } foreach (keys %layers) { # setup uniq alias. if ($layers{$_}->{'uniq'} ne "") { $layers{$layers{$_}->{'uniq'}} = $layers{$_}; } # setup children keys my $bid = $layers{$_}->{b2lid}; next unless $layers{$_}->{'b2lid'}; # has the b2lid for this layer been remapped? # if so update this layer's specified b2lid if ($bid && $LJ::S2LID_REMAP{$bid}) { my $s2lid = $layers{$_}->{s2lid}; b2lid_remap($userid, $s2lid, $bid); $layers{$_}->{b2lid} = $LJ::S2LID_REMAP{$bid}; } if ($is_system) { my $bid = $layers{$_}->{'b2lid'}; unless ($layers{$bid}) { delete $layers{$layers{$_}->{'uniq'}}; delete $layers{$_}; next; } push @{$layers{$bid}->{'children'}}, $_; } } if ($u) { $u->{'_s2layers'} = \%layers; } return \%layers; } # get_style: # # many calling conventions: # get_style($styleid, $verify) # get_style($u, $verify) # get_style($styleid, $opts) # get_style($u, $opts) # # opts may contain keys: # - 'u' -- $u object # - 'verify' -- if verify, the $u->{'s2_style'} key is deleted if style isn't found sub get_style { my ($arg, $opts) = @_; my $verify = 0; my ($styleid, $u); if (ref $opts eq "HASH") { $verify = $opts->{'verify'}; $u = $opts->{'u'}; } elsif ($opts) { $verify = 1; die "Bogus second arg to LJ::S2::get_style" if ref $opts; } if (ref $arg) { $u = $arg; $styleid = $u->{'s2_style'} + 0; } else { $styleid = $arg + 0; } my %style; my $have_style = 0; if ($verify && $styleid) { my $dbr = LJ::S2::get_s2_reader(); my $style = $dbr->selectrow_hashref("SELECT * FROM s2styles WHERE styleid=$styleid"); if (! $style && $u) { delete $u->{'s2_style'}; $styleid = 0; } } if ($styleid) { my $stylay = $u ? LJ::S2::get_style_layers($u, $styleid) : LJ::S2::get_style_layers($styleid); while (my ($t, $id) = each %$stylay) { $style{$t} = $id; } $have_style = scalar %style; } # this is a hack to add remapping support for s2lids # - if a layerid is loaded above but it has a remapping # defined in ljconfig, use the remap id instead and # also save to database using set_style_layers if (%LJ::S2LID_REMAP) { my @remaps = (); # all system layer types (no user layers) foreach (qw(core i18nc i18n layout theme)) { my $lid = $style{$_}; if (exists $LJ::S2LID_REMAP{$lid}) { $style{$_} = $LJ::S2LID_REMAP{$lid}; push @remaps, "$lid=>$style{$_}"; } } if (@remaps) { my $sysid = LJ::get_userid("system"); LJ::statushistory_add($u, $sysid, 's2lid_remap', join(", ", @remaps)); LJ::S2::set_style_layers($u, $styleid, %style); } } unless ($have_style) { my $public = get_public_layers(); while (my ($layer, $name) = each %$LJ::DEFAULT_STYLE) { next unless $name ne ""; next unless $public->{$name}; my $id = $public->{$name}->{'s2lid'}; $style{$layer} = $id if $id; } } return %style; } sub s2_context { my $r = shift; my $styleid = shift; my $opts = shift || {}; my $u = $opts->{u}; my $style_u = $opts->{style_u} || $u; # but it doesn't matter if we're using the minimal style ... my %style; eval { my $r = Apache->request; if ($r->notes('use_minimal_scheme')) { my $public = get_public_layers(); while (my ($layer, $name) = each %LJ::MINIMAL_STYLE) { next unless $name ne ""; next unless $public->{$name}; my $id = $public->{$name}->{'s2lid'}; $style{$layer} = $id if $id; } } }; # fall back to the standard call to get a user's styles unless (%style) { %style = $u ? get_style($styleid, { 'u' => $style_u }) : get_style($styleid); } my @layers; foreach (qw(core i18nc layout i18n theme user)) { push @layers, $style{$_} if $style{$_}; } # TODO: memcache this. only make core S2 (which uses the DB) load # when we can't get all the s2compiled stuff from memcache. # compare s2styles.modtime with s2compiled.comptime to see if memcache # version is accurate or not. my $dbr = LJ::S2::get_s2_reader(); my $modtime = LJ::S2::load_layers(@layers); # check that all critical layers loaded okay from the database, otherwise # fall back to default style. if i18n/theme/user were deleted, just proceed. my $okay = 1; foreach (qw(core layout)) { next unless $style{$_}; $okay = 0 unless S2::layer_loaded($style{$_}); } unless ($okay) { # load the default style instead, if we just tried to load a real one and failed if ($styleid) { return s2_context($r, 0, $opts); } # were we trying to load the default style? $r->content_type("text/html"); $r->send_http_header(); $r->print("Error preparing to run: One or more layers required to load the stock style have been deleted."); return undef; } if ($opts->{'use_modtime'}) { my $ims = $r->header_in("If-Modified-Since"); my $ourtime = LJ::time_to_http($modtime); if ($ims eq $ourtime) { # 304 return; unload non-public layers LJ::S2::cleanup_layers(@layers); $r->status_line("304 Not Modified"); $r->send_http_header(); return undef; } else { $r->header_out("Last-Modified", $ourtime); } } my $ctx; eval { $ctx = S2::make_context(@layers); }; if ($ctx) { LJ::S2::populate_system_props($ctx); S2::set_output(sub {}); # printing suppressed S2::set_output_safe(sub {}); eval { S2::run_code($ctx, "prop_init()"); }; return $ctx unless $@; } # failure to generate context; unload our non-public layers LJ::S2::cleanup_layers(@layers); my $err = $@; $r->content_type("text/html"); $r->send_http_header(); $r->print("Error preparing to run: $err"); return undef; } # parameter is either a single context, or just a bunch of layerids # will then unregister the non-public layers sub cleanup_layers { my $pub = get_public_layers(); my @unload = ref $_[0] ? S2::get_layers($_[0]) : @_; S2::unregister_layer($_) foreach grep { ! $pub->{$_} } @unload; } sub clone_layer { die "LJ::S2::clone_layer() has not been ported to use s2compiled2, but this function is not currently in use anywhere; if you use this function, please update it to use s2compiled2.\n"; my $id = shift; return 0 unless $id; my $dbh = LJ::get_db_writer(); my $r; $r = $dbh->selectrow_hashref("SELECT * FROM s2layers WHERE s2lid=?", undef, $id); return 0 unless $r; $dbh->do("INSERT INTO s2layers (b2lid, userid, type) VALUES (?,?,?)", undef, $r->{'b2lid'}, $r->{'userid'}, $r->{'type'}); my $newid = $dbh->{'mysql_insertid'}; return 0 unless $newid; foreach my $t (qw(s2compiled s2info s2source)) { $r = $dbh->selectrow_hashref("SELECT * FROM $t WHERE s2lid=?", undef, $id); next unless $r; $r->{'s2lid'} = $newid; # kinda hacky: we have to update the layer id if ($t eq "s2compiled") { $r->{'compdata'} =~ s/\$_LID = (\d+)/\$_LID = $newid/; } $dbh->do("INSERT INTO $t (" . join(',', keys %$r) . ") VALUES (". join(',', map { $dbh->quote($_) } values %$r) . ")"); } return $newid; } sub create_style { my ($u, $name, $cloneid) = @_; my $dbh = LJ::get_db_writer(); return 0 unless $dbh && $u->writer; my $uid = $u->{userid} + 0 or return 0; my $clone; $clone = load_style($cloneid) if $cloneid; # can't clone somebody else's style return 0 if $clone && $clone->{'userid'} != $uid; # can't create name-less style return 0 unless $name =~ /\S/; $dbh->do("INSERT INTO s2styles (userid, name, modtime) VALUES (?,?, UNIX_TIMESTAMP())", undef, $u->{'userid'}, $name); my $styleid = $dbh->{'mysql_insertid'}; return 0 unless $styleid; if ($clone) { $clone->{'layer'}->{'user'} = LJ::clone_layer($clone->{'layer'}->{'user'}); my $values; foreach my $ly ('core','i18nc','layout','theme','i18n','user') { next unless $clone->{'layer'}->{$ly}; $values .= "," if $values; $values .= "($uid, $styleid, '$ly', $clone->{'layer'}->{$ly})"; } $u->do("REPLACE INTO s2stylelayers2 (userid, styleid, type, s2lid) ". "VALUES $values") if $values; } return $styleid; } sub load_user_styles { my $u = shift; my $opts = shift; return undef unless $u; my $dbr = LJ::S2::get_s2_reader(); my %styles; my $load_using = sub { my $db = shift; my $sth = $db->prepare("SELECT styleid, name FROM s2styles WHERE userid=?"); $sth->execute($u->{'userid'}); while (my ($id, $name) = $sth->fetchrow_array) { $styles{$id} = $name; } }; $load_using->($dbr); return \%styles if scalar(%styles) || ! $opts->{'create_default'}; # create a new default one for them, but first check to see if they # have one on the master. my $dbh = LJ::get_db_writer(); $load_using->($dbh); return \%styles if %styles; $dbh->do("INSERT INTO s2styles (userid, name, modtime) VALUES (?,?, UNIX_TIMESTAMP())", undef, $u->{'userid'}, $u->{'user'}); my $styleid = $dbh->{'mysql_insertid'}; return { $styleid => $u->{'user'} }; } sub delete_user_style { my ($u, $styleid) = @_; return 1 unless $styleid; my $dbh = LJ::get_db_writer(); return 0 unless $dbh && $u->writer; my $style = load_style($dbh, $styleid); delete_layer($style->{'layer'}->{'user'}); foreach my $t (qw(s2styles s2stylelayers)) { $dbh->do("DELETE FROM $t WHERE styleid=?", undef, $styleid) } $u->do("DELETE FROM s2stylelayers2 WHERE userid=? AND styleid=?", undef, $u->{userid}, $styleid); return 1; } sub load_style { my $db = ref $_[0] ? shift : undef; my $id = shift; return undef unless $id; my $memkey = [$id, "s2s:$id"]; my $style = LJ::MemCache::get($memkey); unless ($style) { $db ||= LJ::S2::get_s2_reader(); $style = $db->selectrow_hashref("SELECT styleid, userid, name, modtime ". "FROM s2styles WHERE styleid=?", undef, $id); LJ::MemCache::add($memkey, $style, 3600); } return undef unless $style; my $u = LJ::load_userid($style->{userid}) or return undef; $style->{'layer'} = LJ::S2::get_style_layers($u, $id) || {}; return $style; } sub create_layer { my ($userid, $b2lid, $type) = @_; $userid = LJ::want_userid($userid); return 0 unless $b2lid; # caller should ensure b2lid exists and is of right type return 0 unless $type eq "user" || $type eq "i18n" || $type eq "theme" || $type eq "layout" || $type eq "i18nc" || $type eq "core"; my $dbh = LJ::get_db_writer(); return 0 unless $dbh; $dbh->do("INSERT INTO s2layers (b2lid, userid, type) ". "VALUES (?,?,?)", undef, $b2lid, $userid, $type); return $dbh->{'mysql_insertid'}; } # takes optional $u as first argument... if user argument is specified, will # look through s2stylelayers and delete all mappings that this user has to # this particular layer. sub delete_layer { my $u = LJ::isu($_[0]) ? shift : undef; my $lid = shift; return 1 unless $lid; my $dbh = LJ::get_db_writer(); foreach my $t (qw(s2layers s2compiled s2info s2source s2checker)) { $dbh->do("DELETE FROM $t WHERE s2lid=?", undef, $lid); } # make sure we have a user object if possible unless ($u) { my $us = LJ::S2::get_layer_owners($lid); $u = $us->{$lid} if $us->{$lid}; } # delete s2compiled2 if this is a layer owned by someone other than system if ($u && $u->{user} ne 'system') { $u->do("DELETE FROM s2compiled2 WHERE userid = ? AND s2lid = ?", undef, $u->{userid}, $lid); } # now clear memcache of the compiled data LJ::MemCache::delete([ $lid, "s2c:$lid" ]); # now delete the mappings for this particular layer if ($u) { my $styles = LJ::S2::load_user_styles($u); my @ids = keys %{$styles || {}}; if (@ids) { # map in the ids we got from the user's styles and clear layers referencing # this particular layer id my $in = join(',', map { $_ + 0 } @ids); $dbh->do("DELETE FROM s2stylelayers WHERE styleid IN ($in) AND s2lid = ?", undef, $lid); $u->do("DELETE FROM s2stylelayers2 WHERE userid=? AND styleid IN ($in) AND s2lid = ?", undef, $u->{userid}, $lid); # now clean memcache so this change is immediately visible LJ::MemCache::delete([ $_, "s2sl:$_" ]) foreach @ids; } } return 1; } sub get_style_layers { my $u = LJ::isu($_[0]) ? shift : undef; my ($styleid, $force) = @_; return undef unless $styleid; # check memcache unless $force my $stylay = undef; my $memkey = [$styleid, "s2sl:$styleid"]; $stylay = LJ::MemCache::get($memkey) unless $force; return $stylay if $stylay; unless ($u) { my $sty = LJ::S2::load_style($styleid) or die "couldn't load styleid $styleid"; $u = LJ::load_userid($sty->{userid}) or die "couldn't load userid $sty->{userid} for styleid $styleid"; } my %stylay; my $fetch = sub { my ($db, $qry, @args) = @_; my $sth = $db->prepare($qry); $sth->execute(@args); die "ERROR: " . $sth->errstr if $sth->err; while (my ($type, $s2lid) = $sth->fetchrow_array) { $stylay{$type} = $s2lid; } return 0 unless %stylay; return 1; }; unless ($fetch->($u, "SELECT type, s2lid FROM s2stylelayers2 " . "WHERE userid=? AND styleid=?", $u->{userid}, $styleid)) { my $dbh = LJ::get_db_writer(); if ($fetch->($dbh, "SELECT type, s2lid FROM s2stylelayers WHERE styleid=?", $styleid)) { LJ::S2::set_style_layers_raw($u, $styleid, %stylay); } } # set in memcache LJ::MemCache::set($memkey, \%stylay); return \%stylay; } # the old interfaces. handles merging with global database data if necessary. sub set_style_layers { my ($u, $styleid, %newlay) = @_; my $dbh = LJ::get_db_writer(); return 0 unless $dbh && $u->writer; my @lay = ('core','i18nc','layout','theme','i18n','user'); my %need = map { $_, 1 } @lay; delete $need{$_} foreach keys %newlay; if (%need) { # see if the needed layers are already on the user cluster my ($sth, $t, $lid); $sth = $u->prepare("SELECT type FROM s2stylelayers2 WHERE userid=? AND styleid=?"); $sth->execute($u->{'userid'}, $styleid); while (($t) = $sth->fetchrow_array) { delete $need{$t}; } # if we still don't have everything, see if they exist on the # global cluster, and we'll merge them into the %newlay being # posted, so they end up on the user cluster if (%need) { $sth = $dbh->prepare("SELECT type, s2lid FROM s2stylelayers WHERE styleid=?"); $sth->execute($styleid); while (($t, $lid) = $sth->fetchrow_array) { $newlay{$t} = $lid; } } } set_style_layers_raw($u, $styleid, %newlay); } # just set in user cluster, not merging with global sub set_style_layers_raw { my ($u, $styleid, %newlay) = @_; my $dbh = LJ::get_db_writer(); return 0 unless $dbh && $u->writer; $u->do("REPLACE INTO s2stylelayers2 (userid,styleid,type,s2lid) VALUES ". join(",", map { sprintf("(%d,%d,%s,%d)", $u->{userid}, $styleid, $dbh->quote($_), $newlay{$_}) } keys %newlay)); return 0 if $u->err; $dbh->do("UPDATE s2styles SET modtime=UNIX_TIMESTAMP() WHERE styleid=?", undef, $styleid); # delete memcache key LJ::MemCache::delete([$styleid, "s2sl:$styleid"]); LJ::MemCache::delete([$styleid, "s2s:$styleid"]); return 1; } sub load_layer { my $db = ref $_[0] ? shift : LJ::S2::get_s2_reader(); my $lid = shift; return $db->selectrow_hashref("SELECT s2lid, b2lid, userid, type ". "FROM s2layers WHERE s2lid=?", undef, $lid); } sub escape_context_props { my $obj = shift; if (ref $obj eq "HASH") { while (my ($k, $v) = each %{$obj}) { if (ref $v) { escape_context_props($v); } else { $obj->{$k} =~ s/{$k} =~ s/>/>/g; $obj->{$k} =~ s!\n!
!g; } } } elsif (ref $obj eq "ARRAY") { foreach (@$obj) { if (ref) { escape_context_props($_); } else { s//>/g; s!\n!
!g; } } } } sub populate_system_props { my $ctx = shift; $ctx->[S2::PROPS]->{'SITEROOT'} = $LJ::SITEROOT; $ctx->[S2::PROPS]->{'PALIMGROOT'} = $LJ::PALIMGROOT; $ctx->[S2::PROPS]->{'SITENAME'} = $LJ::SITENAME; $ctx->[S2::PROPS]->{'SITENAMESHORT'} = $LJ::SITENAMESHORT; $ctx->[S2::PROPS]->{'SITENAMEABBREV'} = $LJ::SITENAMEABBREV; $ctx->[S2::PROPS]->{'IMGDIR'} = $LJ::IMGPREFIX; } sub layer_compile_user { my ($layer, $overrides) = @_; my $dbh = LJ::get_db_writer(); return 0 unless ref $layer; return 0 unless $layer->{'s2lid'}; return 1 unless ref $overrides; my $id = $layer->{'s2lid'}; my $s2 = "layerinfo \"type\" = \"user\";\n"; foreach my $name (keys %$overrides) { next if $name =~ /\W/; my $prop = $overrides->{$name}->[0]; my $val = $overrides->{$name}->[1]; if ($prop->{'type'} eq "int") { $val = int($val); } elsif ($prop->{'type'} eq "bool") { $val = $val ? "true" : "false"; } else { $val =~ s/[\\\$\"]/\\$&/g; $val = "\"$val\""; } $s2 .= "set $name = $val;\n"; } my $error; return 1 if LJ::S2::layer_compile($layer, \$error, { 's2ref' => \$s2 }); return LJ::error($error); } sub layer_compile { my ($layer, $err_ref, $opts) = @_; my $dbh = LJ::get_db_writer(); my $lid; if (ref $layer eq "HASH") { $lid = $layer->{'s2lid'}+0; } else { $lid = $layer+0; $layer = LJ::S2::load_layer($dbh, $lid) or return 0; } return 0 unless $lid; # get checker (cached, or via compiling) for parent layer my $checker = get_layer_checker($layer); unless ($checker) { $$err_ref = "Error compiling parent layer."; return undef; } # do our compile (quickly, since we probably have the cached checker) my $s2ref = $opts->{'s2ref'}; unless ($s2ref) { my $s2 = $dbh->selectrow_array("SELECT s2code FROM s2source WHERE s2lid=?", undef, $lid); unless ($s2) { $$err_ref = "No source code to compile."; return undef; } $s2ref = \$s2; } my $is_system = $layer->{'userid'} == LJ::get_userid("system"); my $untrusted = ! $LJ::S2_TRUSTED{$layer->{'userid'}} && ! $is_system; # system writes go to global. otherwise to user clusters. my $dbcm; if ($is_system) { $dbcm = $dbh; } else { my $u = LJ::load_userid($layer->{'userid'}); $dbcm = $u; } return 0 unless $dbcm; my $compiled; my $cplr = S2::Compiler->new({ 'checker' => $checker }); eval { $cplr->compile_source({ 'type' => $layer->{'type'}, 'source' => $s2ref, 'output' => \$compiled, 'layerid' => $lid, 'untrusted' => $untrusted, 'builtinPackage' => "S2::Builtin::LJ", }); }; if ($@) { $$err_ref = "Compile error: $@"; return undef; } # save the source, since it at least compiles if ($opts->{'s2ref'}) { $dbh->do("REPLACE INTO s2source (s2lid, s2code) VALUES (?,?)", undef, $lid, ${$opts->{'s2ref'}}) or return 0; } # save the checker object for later if ($layer->{'type'} eq "core" || $layer->{'type'} eq "layout") { $checker->cleanForFreeze(); my $chk_frz = Storable::nfreeze($checker); LJ::text_compress(\$chk_frz); $dbh->do("REPLACE INTO s2checker (s2lid, checker) VALUES (?,?)", undef, $lid, $chk_frz) or die; } # load the compiled layer to test it loads and then get layerinfo/etc from it S2::unregister_layer($lid); eval $compiled; if ($@) { $$err_ref = "Post-compilation error: $@"; return undef; } if ($opts->{'redist_uniq'}) { # used by update-db loader: my $redist_uniq = S2::get_layer_info($lid, "redist_uniq"); die "redist_uniq value of '$redist_uniq' doesn't match $opts->{'redist_uniq'}\n" unless $redist_uniq eq $opts->{'redist_uniq'}; } # put layerinfo into s2info my %info = S2::get_layer_info($lid); my $values; my $notin; foreach (keys %info) { $values .= "," if $values; $values .= sprintf("(%d, %s, %s)", $lid, $dbh->quote($_), $dbh->quote($info{$_})); $notin .= "," if $notin; $notin .= $dbh->quote($_); } if ($values) { $dbh->do("REPLACE INTO s2info (s2lid, infokey, value) VALUES $values") or die; $dbh->do("DELETE FROM s2info WHERE s2lid=? AND infokey NOT IN ($notin)", undef, $lid); } if ($opts->{'layerinfo'}) { ${$opts->{'layerinfo'}} = \%info; } # put compiled into database, with its ID number if ($is_system) { $dbh->do("REPLACE INTO s2compiled (s2lid, comptime, compdata) ". "VALUES (?, UNIX_TIMESTAMP(), ?)", undef, $lid, $compiled) or die; } else { my $gzipped = LJ::text_compress($compiled); $dbcm->do("REPLACE INTO s2compiled2 (userid, s2lid, comptime, compdata) ". "VALUES (?, ?, UNIX_TIMESTAMP(), ?)", undef, $layer->{'userid'}, $lid, $gzipped) or die; # delete from memcache; we can't store since we don't know the exact comptime LJ::MemCache::delete([ $lid, "s2c:$lid" ]); } # caller might want the compiled source if (ref $opts->{'compiledref'} eq "SCALAR") { ${$opts->{'compiledref'}} = $compiled; } S2::unregister_layer($lid); return 1; } sub get_layer_checker { my $lay = shift; my $err_ref = shift; return undef unless ref $lay eq "HASH"; return S2::Checker->new() if $lay->{'type'} eq "core"; my $parid = $lay->{'b2lid'}+0 or return undef; my $dbh = LJ::get_db_writer(); my $get_cached = sub { my $frz = $dbh->selectrow_array("SELECT checker FROM s2checker WHERE s2lid=?", undef, $parid) or return undef; LJ::text_uncompress(\$frz); return Storable::thaw($frz); # can be undef, on failure }; # the good path my $checker = $get_cached->(); return $checker if $checker; # no cached checker (or bogus), so we have to [re]compile to get it my $parlay = LJ::S2::load_layer($dbh, $parid); return undef unless LJ::S2::layer_compile($parlay); return $get_cached->(); } sub load_layer_info { my ($outhash, $listref) = @_; return 0 unless ref $listref eq "ARRAY"; return 1 unless @$listref; my $in = join(',', map { $_+0 } @$listref); my $dbr = LJ::S2::get_s2_reader(); my $sth = $dbr->prepare("SELECT s2lid, infokey, value FROM s2info WHERE ". "s2lid IN ($in)"); $sth->execute; while (my ($id, $k, $v) = $sth->fetchrow_array) { $outhash->{$id}->{$k} = $v; } return 1; } sub get_layout_langs { my $src = shift; my $layid = shift; my %lang; foreach (keys %$src) { next unless /^\d+$/; my $v = $src->{$_}; next unless $v->{'langcode'}; $lang{$v->{'langcode'}} = $src->{$_} if ($v->{'type'} eq "i18nc" || ($v->{'type'} eq "i18n" && $layid && $v->{'b2lid'} == $layid)); } return map { $_, $lang{$_}->{'name'} } sort keys %lang; } # returns array of hashrefs sub get_layout_themes { my $src = shift; $src = [ $src ] unless ref $src eq "ARRAY"; my $layid = shift; my @themes; foreach my $src (@$src) { foreach (sort { $src->{$a}->{'name'} cmp $src->{$b}->{'name'} } keys %$src) { next unless /^\d+$/; my $v = $src->{$_}; push @themes, $v if ($v->{'type'} eq "theme" && $layid && $v->{'b2lid'} == $layid); } } return @themes; } sub get_layout_themes_select { my @sel; my $last_uid; foreach my $t (get_layout_themes(@_)) { if ($last_uid && $t->{'userid'} != $last_uid) { push @sel, 0, '---'; # divider between system & user } $last_uid = $t->{'userid'}; push @sel, $t->{'s2lid'}, $t->{'name'}; } return @sel; } sub get_policy { return $LJ::S2::CACHE_POLICY if $LJ::S2::CACHE_POLICY; my $policy = {}; # localize $_ so that the while (

) below doesn't clobber it and cause problems # in anybody that happens to be calling us local $_; foreach my $infix ("", "-local") { my $file = "$LJ::HOME/bin/upgrading/s2layers/policy${infix}.dat"; my $layer = undef; open (P, $file) or next; while (

) { s/\#.*//; next unless /\S/; if (/^\s*layer\s*:\s*(\S+)\s*$/) { $layer = $1; next; } next unless $layer; s/^\s+//; s/\s+$//; my @words = split(/\s+/, $_); next unless $words[-1] eq "allow" || $words[-1] eq "deny"; my $allow = $words[-1] eq "allow" ? 1 : 0; if ($words[0] eq "use" && @words == 2) { $policy->{$layer}->{'use'} = $allow; } if ($words[0] eq "props" && @words == 2) { $policy->{$layer}->{'props'} = $allow; } if ($words[0] eq "prop" && @words == 3) { $policy->{$layer}->{'prop'}->{$words[1]} = $allow; } } } return $LJ::S2::CACHE_POLICY = $policy; } sub can_use_layer { my ($u, $uniq) = @_; # $uniq = redist_uniq value return 1 if LJ::get_cap($u, "s2styles"); my $pol = get_policy(); my $can = 0; foreach ('*', $uniq) { next unless defined $pol->{$_}; next unless defined $pol->{$_}->{'use'}; $can = $pol->{$_}->{'use'}; } return $can; } sub can_use_prop { my ($u, $uniq, $prop) = @_; # $uniq = redist_uniq value return 1 if LJ::get_cap($u, "s2styles"); my $pol = get_policy(); my $can = 0; my @layers = ('*'); my $pub = get_public_layers(); if ($pub->{$uniq} && $pub->{$uniq}->{'type'} eq "layout") { my $cid = $pub->{$uniq}->{'b2lid'}; push @layers, $pub->{$cid}->{'uniq'} if $pub->{$cid}; } push @layers, $uniq; foreach my $lay (@layers) { foreach my $it ('props', 'prop') { if ($it eq "props" && defined $pol->{$lay}->{'props'}) { $can = $pol->{$lay}->{'props'}; } if ($it eq "prop" && defined $pol->{$lay}->{'prop'}->{$prop}) { $can = $pol->{$lay}->{'prop'}->{$prop}; } } } return $can; } sub get_journal_day_counts { my ($s2page) = @_; return $s2page->{'_day_counts'} if defined $s2page->{'_day_counts'}; my $u = $s2page->{'_u'}; my $counts = {}; my $remote = LJ::get_remote(); my $days = LJ::get_daycounts($u, $remote) or return {}; foreach my $day (@$days) { $counts->{$day->[0]}->{$day->[1]}->{$day->[2]} = $day->[3]; } return $s2page->{'_day_counts'} = $counts; } ## S2 object constructors sub CommentInfo { my $opts = shift; $opts->{'_type'} = "CommentInfo"; $opts->{'count'} += 0; return $opts; } sub Date { my @parts = @_; my $dt = { '_type' => 'Date' }; $dt->{'year'} = $parts[0]+0; $dt->{'month'} = $parts[1]+0; $dt->{'day'} = $parts[2]+0; $dt->{'_dayofweek'} = $parts[3]; die "S2 Builtin Date() takes day of week 1-7, not 0-6" if defined $parts[3] && $parts[3] == 0; return $dt; } sub DateTime_unix { my $time = shift; my @gmtime = gmtime($time); my $dt = { '_type' => 'DateTime' }; $dt->{'year'} = $gmtime[5]+1900; $dt->{'month'} = $gmtime[4]+1; $dt->{'day'} = $gmtime[3]; $dt->{'hour'} = $gmtime[2]; $dt->{'min'} = $gmtime[1]; $dt->{'sec'} = $gmtime[0]; $dt->{'_dayofweek'} = $gmtime[6] + 1; return $dt; } sub DateTime_parts { my @parts = split(/\s+/, shift); my $dt = { '_type' => 'DateTime' }; $dt->{'year'} = $parts[0]+0; $dt->{'month'} = $parts[1]+0; $dt->{'day'} = $parts[2]+0; $dt->{'hour'} = $parts[3]+0; $dt->{'min'} = $parts[4]+0; $dt->{'sec'} = $parts[5]+0; # the parts string comes from MySQL which has range 0-6, # but internally and to S2 we use 1-7. $dt->{'_dayofweek'} = $parts[6] + 1 if defined $parts[6]; return $dt; } sub Tag { my ($prefix, $kw) = @_; my $e = { _type => 'Tag', #_id => $kwid, #seems unused? name => LJ::ehtml($kw), url => $prefix . LJ::eurl($kw), }; return $e; } # # name: LJ::S2::get_tags_text # class: s2 # des: Gets text for display in entry for tags compatibility. # args: ctx, taglistref # des-ctx: Current S2 context # des-taglistref: Arrayref containing "Tag" S2 objects # returns: String; can be appended to entry... undef on error (no context, no taglistref) # sub get_tags_text { my ($ctx, $taglist) = @_; return undef unless $ctx && $taglist; return "" unless @$taglist; # now get the customized tag text and insert the tag list and append to body my $tags = join(', ', map { "" } @$taglist); my $tagtext = S2::get_property_value($ctx, 'text_tags'); $tagtext =~ s/#/$tags/; return "

$tagtext
"; } sub Entry { my ($u, $arg) = @_; my $e = { '_type' => 'Entry', 'link_keyseq' => [ 'edit_entry', 'edit_tags' ], 'metadata' => {}, }; foreach (qw(subject _rawsubject text journal poster new_day end_day comments userpic permalink_url itemid)) { $e->{$_} = $arg->{$_}; } $e->{'time'} = DateTime_parts($arg->{'dateparts'}); $e->{'depth'} = 0; # Entries are always depth 0. Comments are 1+. my $link_keyseq = $e->{'link_keyseq'}; push @$link_keyseq, 'mem_add' unless $LJ::DISABLED{'memories'}; push @$link_keyseq, 'tell_friend' unless $LJ::DISABLED{'tellafriend'}; # Note: nav_prev and nav_next are not included in the keyseq anticipating # that their placement relative to the others will vary depending on # layout. if ($arg->{'security'} eq "public") { # do nothing. } elsif ($arg->{'security'} eq "usemask") { $e->{'security'} = "protected"; $e->{'security_icon'} = Image_std("security-protected"); } elsif ($arg->{'security'} eq "private") { $e->{'security'} = "private"; $e->{'security_icon'} = Image_std("security-private"); } my $p = $arg->{'props'}; if ($p->{'current_music'}) { $e->{'metadata'}->{'music'} = $p->{'current_music'}; LJ::CleanHTML::clean_subject(\$e->{'metadata'}->{'music'}); } if (my $mid = $p->{'current_moodid'}) { my $theme = defined $arg->{'moodthemeid'} ? $arg->{'moodthemeid'} : $u->{'moodthemeid'}; my %pic; $e->{'mood_icon'} = Image($pic{'pic'}, $pic{'w'}, $pic{'h'}) if LJ::get_mood_picture($theme, $mid, \%pic); if (my $mood = LJ::mood_name($mid)) { $e->{'metadata'}->{'mood'} = $mood; } } if ($p->{'current_mood'}) { $e->{'metadata'}->{'mood'} = $p->{'current_mood'}; LJ::CleanHTML::clean_subject(\$e->{'metadata'}->{'mood'}); } $e->{'tags'} = []; if ($p->{'tags'}) { my $prefix = $arg->{'base_url'} ? $arg->{'base_url'} . '/tag/' : LJ::journal_base($u) . '/tag/'; my @taglist; foreach (@{$p->{'tags'}}) { push @taglist, Tag($prefix, $_); } my $t = $arg->{enable_tags_compatibility}; #[bool, ctx] if ($t && $t->[0]) { $e->{'text'} .= LJ::S2::get_tags_text($t->[1], \@taglist); } $e->{'tags'} = \@taglist; } return $e; } sub Friend { my ($u) = @_; my $o = UserLite($u); $o->{'_type'} = "Friend"; $o->{'bgcolor'} = S2::Builtin::LJ::Color__Color($u->{'bgcolor'}); $o->{'fgcolor'} = S2::Builtin::LJ::Color__Color($u->{'fgcolor'}); return $o; } sub Null { my $type = shift; return { '_type' => $type, '_isnull' => 1, }; } sub Page { my ($u, $opts) = @_; my $styleid = $u->{'_s2styleid'} + 0; my $base_url = $u->{'_journalbase'}; my $get = $opts->{'getargs'}; my %args; foreach my $k (keys %$get) { my $v = $get->{$k}; next unless $k =~ s/^\.//; $args{$k} = $v; } # get MAX(modtime of style layers) my $stylemodtime = S2::get_style_modtime($opts->{'ctx'}); my $style = load_style($u->{'s2_style'}); $stylemodtime = $style->{'modtime'} if $style->{'modtime'} > $stylemodtime; my $linkobj = LJ::Links::load_linkobj($u); my $linklist = [ map { UserLink($_) } @$linkobj ]; my $p = { '_type' => 'Page', '_u' => $u, 'view' => '', 'args' => \%args, 'journal' => User($u), 'journal_type' => $u->{'journaltype'}, 'time' => DateTime_unix(time), 'base_url' => $base_url, 'stylesheet_url' => "$base_url/res/$styleid/stylesheet?$stylemodtime", 'view_url' => { 'recent' => "$base_url/", 'userinfo' => "$LJ::SITEROOT/userinfo.bml?user=$u->{'user'}", 'archive' => "$base_url/calendar", 'friends' => "$base_url/friends", }, 'linklist' => $linklist, 'views_order' => [ 'recent', 'archive', 'friends', 'userinfo' ], 'global_title' => LJ::ehtml($u->{'journaltitle'} || $u->{'name'}), 'global_subtitle' => LJ::ehtml($u->{'journalsubtitle'}), 'head_content' => '', }; if (LJ::are_hooks('s2_head_content_extra')) { my $remote = LJ::get_remote(); $p->{head_content} .= LJ::run_hook('s2_head_content_extra', $remote, $opts->{r}); } if ($LJ::UNICODE && $opts && $opts->{'saycharset'}) { $p->{'head_content'} .= '\n"; } # Automatic Discovery of RSS/Atom $p->{'head_content'} .= qq{\n}; $p->{'head_content'} .= qq{\n}; $p->{'head_content'} .= qq{\n}; $p->{'head_content'} .= qq{\n}; # FOAF autodiscovery my $real_user = 1; foreach (@LJ::PROTECTED_USERNAMES) { $real_user = 0 if ($u->{'user'} =~ $_); } if ($real_user) { my $foafurl = $u->{external_foaf_url} ? LJ::eurl($u->{external_foaf_url}) : "$p->{base_url}/data/foaf"; my $digest = Digest::SHA1::sha1_hex('mailto:' . $u->{email}); $p->{head_content} .= qq{\n}; $p->{head_content} .= qq{\n}; } return $p; } sub Image { my ($url, $w, $h, $alttext) = @_; return { '_type' => 'Image', 'url' => $url, 'width' => $w, 'height' => $h, 'alttext' => $alttext, }; } sub Image_std { my $name = shift; my $ctx = $LJ::S2::CURR_CTX or die "No S2 context available "; unless ($LJ::S2::RES_MADE++) { $LJ::S2::RES_CACHE = { 'security-protected' => Image("$LJ::IMGPREFIX/icon_protected.gif", 14, 15, $ctx->[S2::PROPS]->{'text_icon_alt_protected'}), 'security-private' => Image("$LJ::IMGPREFIX/icon_private.gif", 16, 16, $ctx->[S2::PROPS]->{'text_icon_alt_private'}), }; } return $LJ::S2::RES_CACHE->{$name}; } sub Image_userpic { my ($u, $picid, $kw) = @_; $picid ||= LJ::get_picid_from_keyword($u, $kw); my $pi = LJ::get_userpic_info($u); my $p = $pi->{'pic'}->{$picid}; return Null("Image") unless $p; return { '_type' => "Image", 'url' => "$LJ::USERPIC_ROOT/$picid/$u->{'userid'}", 'width' => $p->{'width'}, 'height' => $p->{'height'}, 'alttext' => "", }; } sub ItemRange_fromopts { my $opts = shift; my $ir = {}; my $items = $opts->{'items'}; my $page_size = ($opts->{'pagesize'}+0) || 25; my $page = $opts->{'page'}+0 || 1; my $num_items = scalar @$items; my $pages = POSIX::ceil($num_items / $page_size) || 1; if ($page > $pages) { $page = $pages; } splice(@$items, 0, ($page-1)*$page_size) if $page > 1; splice(@$items, $page_size) if @$items > $page_size; $ir->{'current'} = $page; $ir->{'total'} = $pages; $ir->{'total_subitems'} = $num_items; $ir->{'from_subitem'} = ($page-1) * $page_size + 1; $ir->{'num_subitems_displayed'} = @$items; $ir->{'to_subitem'} = $ir->{'from_subitem'} + $ir->{'num_subitems_displayed'} - 1; $ir->{'all_subitems_displayed'} = ($pages == 1); $ir->{'_url_of'} = $opts->{'url_of'}; return ItemRange($ir); } sub ItemRange { my $h = shift; # _url_of = sub($n) $h->{'_type'} = "ItemRange"; my $url_of = ref $h->{'_url_of'} eq "CODE" ? $h->{'_url_of'} : sub {"";}; $h->{'url_next'} = $url_of->($h->{'current'} + 1) unless $h->{'current'} >= $h->{'total'}; $h->{'url_prev'} = $url_of->($h->{'current'} - 1) unless $h->{'current'} <= 1; $h->{'url_first'} = $url_of->(1) unless $h->{'current'} == 1; $h->{'url_last'} = $url_of->($h->{'total'}) unless $h->{'current'} == $h->{'total'}; return $h; } sub User { my ($u) = @_; my $o = UserLite($u); $o->{'_type'} = "User"; $o->{'default_pic'} = Image_userpic($u, $u->{'defaultpicid'}); $o->{'userpic_listing_url'} = "$LJ::SITEROOT/allpics.bml?user=".$u->{'user'}; $o->{'website_url'} = LJ::ehtml($u->{'url'}); $o->{'website_name'} = LJ::ehtml($u->{'urlname'}); return $o; } sub UserLink { my $link = shift; # hashref # a dash means pass to s2 as blank so it will just insert a blank line $link->{'title'} = '' if $link->{'title'} eq "-"; return { '_type' => 'UserLink', 'is_heading' => $link->{'url'} ? 0 : 1, 'url' => LJ::ehtml($link->{'url'}), 'title' => LJ::ehtml($link->{'title'}), 'children' => $link->{'children'} || [], # TODO: implement parent-child relationships }; } sub UserLite { my ($u) = @_; my $display_name; if (eval { $u->display_name } ) { $display_name = $u->display_name; } else { $display_name = LJ::ehtml($u->{'name'}); # print STDERR "User " . $u->{'name'} . " does not have a display name.\n"; } my $o = { '_type' => 'UserLite', '_u' => $u, 'username' => $display_name, 'name' => LJ::ehtml($u->{'name'}), 'journal_type' => $u->{'journaltype'}, }; return $o; } ############### package S2::Builtin::LJ; use strict; sub AUTOLOAD { no strict; if ($AUTOLOAD =~ /::(\w+)$/) { my $real = \&{"S2::Builtin::$1"}; *{$AUTOLOAD} = $real; return $real->(@_); } die "No such builtin: $AUTOLOAD"; } sub ehtml { my ($ctx, $text) = @_; return LJ::ehtml($text); } sub eurl { my ($ctx, $text) = @_; return LJ::eurl($text); } # escape tags only sub etags { my ($ctx, $text) = @_; $text =~ s//>/g; return $text; } # sanitize URLs sub clean_url { my ($ctx, $text) = @_; unless ($text =~ m!^https?://[^\'\"\\]*$!) { $text = ""; } return $text; } sub get_page { return $LJ::S2::CURR_PAGE; } sub get_plural_phrase { my ($ctx, $n, $prop) = @_; my $form = S2::run_function($ctx, "lang_map_plural(int)", $n); my $a = $ctx->[S2::PROPS]->{"_plurals_$prop"}; unless (ref $a eq "ARRAY") { $a = $ctx->[S2::PROPS]->{"_plurals_$prop"} = [ split(m!\s*//\s*!, $ctx->[S2::PROPS]->{$prop}) ]; } my $text = $a->[$form]; # this fixes missing plural forms for russians (who have 2 plural forms) # using languages like english with 1 plural form $text = $a->[-1] unless defined $text; $text =~ s/\#/$n/; return LJ::ehtml($text); } sub get_url { my ($ctx, $obj, $view) = @_; my $dir = "users"; my $journal_type; my $user; # now get data from one of two paths, depending on if we were given a UserLite # object or a string for the username if (ref $obj eq 'HASH') { $journal_type = $obj->{journal_type}; $user = $obj->{username}; } else { my $u = LJ::load_user($obj); $journal_type = $u ? $u->{journaltype} : 'P'; $user = $u ? $u->{user} : $obj; } # construct URL to return $dir = 'community' if $journal_type eq 'C'; $view = "info" if $view eq "userinfo"; $view = "calendar" if $view eq "archive"; $view = "" if $view eq "recent"; return "$LJ::SITEROOT/$dir/$user/$view"; } sub htmlattr { my ($ctx, $name, $value) = @_; return "" if $value eq ""; $name = lc($name); return "" if $name =~ /[^a-z]/; return " $name=\"" . LJ::ehtml($value) . "\""; } sub rand { my ($ctx, $aa, $bb) = @_; my ($low, $high); if (ref $aa eq "ARRAY") { ($low, $high) = (0, @$aa - 1); } elsif (! defined $bb) { ($low, $high) = (1, $aa); } else { ($low, $high) = ($aa, $bb); } return int(rand($high - $low + 1)) + $low; } sub viewer_logged_in { my ($ctx) = @_; my $remote = LJ::get_remote(); return defined $remote; } sub viewer_is_owner { my ($ctx) = @_; my $remote = LJ::get_remote(); return 0 unless $remote; return $remote->{'userid'} == $LJ::S2::CURR_PAGE->{'_u'}->{'userid'}; } sub weekdays { my ($ctx) = @_; return [ 1..7 ]; # FIXME: make this conditionally monday first: [ 2..7, 1 ] } sub set_handler { my ($ctx, $hook, $stmts) = @_; my $p = $LJ::S2::CURR_PAGE; return unless $hook =~ /^\w+\#?$/; $hook =~ s/\#$/ARG/; $S2::pout->("\n"); } sub zeropad { my ($ctx, $num, $digits) = @_; $num += 0; $digits += 0; return sprintf("%0${digits}d", $num); } *int__zeropad = \&zeropad; sub Color__update_hsl { my ($this, $force) = @_; return if $this->{'_hslset'}++; ($this->{'_h'}, $this->{'_s'}, $this->{'_l'}) = S2::Color::rgb_to_hsl($this->{'r'}, $this->{'g'}, $this->{'b'}); $this->{$_} = int($this->{$_} * 255 + 0.5) foreach qw(_h _s _l); } sub Color__update_rgb { my ($this) = @_; ($this->{'r'}, $this->{'g'}, $this->{'b'}) = S2::Color::hsl_to_rgb( map { $this->{$_} / 255 } qw(_h _s _l) ); Color__make_string($this); } sub Color__make_string { my ($this) = @_; $this->{'as_string'} = sprintf("\#%02x%02x%02x", $this->{'r'}, $this->{'g'}, $this->{'b'}); } # public functions sub Color__Color { my ($s) = @_; $s =~ s/^\#//; $s =~ s/^(\w)(\w)(\w)$/$1$1$2$2$3$3/s; # 'c30' => 'cc3300' return if $s =~ /[^a-fA-F0-9]/ || length($s) != 6; my $this = { '_type' => 'Color' }; $this->{'r'} = hex(substr($s, 0, 2)); $this->{'g'} = hex(substr($s, 2, 2)); $this->{'b'} = hex(substr($s, 4, 2)); $this->{$_} = $this->{$_} % 256 foreach qw(r g b); Color__make_string($this); return $this; } sub Color__clone { my ($ctx, $this) = @_; return { %$this }; } sub Color__set_hsl { my ($this, $h, $s, $l) = @_; $this->{'_h'} = $h % 256; $this->{'_s'} = $s % 256; $this->{'_l'} = $l % 256; $this->{'_hslset'} = 1; Color__update_rgb($this); } sub Color__red { my ($ctx, $this, $r) = @_; if (defined $r) { $this->{'r'} = $r % 256; delete $this->{'_hslset'}; Color__make_string($this); } $this->{'r'}; } sub Color__green { my ($ctx, $this, $g) = @_; if (defined $g) { $this->{'g'} = $g % 256; delete $this->{'_hslset'}; Color__make_string($this); } $this->{'g'}; } sub Color__blue { my ($ctx, $this, $b) = @_; if (defined $b) { $this->{'b'} = $b % 256; delete $this->{'_hslset'}; Color__make_string($this); } $this->{'b'}; } sub Color__hue { my ($ctx, $this, $h) = @_; if (defined $h) { $this->{'_h'} = $h % 256; $this->{'_hslset'} = 1; Color__update_rgb($this); } elsif (! $this->{'_hslset'}) { Color__update_hsl($this); } $this->{'_h'}; } sub Color__saturation { my ($ctx, $this, $s) = @_; if (defined $s) { $this->{'_s'} = $s % 256; $this->{'_hslset'} = 1; Color__update_rgb($this); } elsif (! $this->{'_hslset'}) { Color__update_hsl($this); } $this->{'_s'}; } sub Color__lightness { my ($ctx, $this, $l) = @_; if (defined $l) { $this->{'_l'} = $l % 256; $this->{'_hslset'} = 1; Color__update_rgb($this); } elsif (! $this->{'_hslset'}) { Color__update_hsl($this); } $this->{'_l'}; } sub Color__inverse { my ($ctx, $this) = @_; my $new = { '_type' => 'Color', 'r' => 255 - $this->{'r'}, 'g' => 255 - $this->{'g'}, 'b' => 255 - $this->{'b'}, }; Color__make_string($new); return $new; } sub Color__average { my ($ctx, $this, $other) = @_; my $new = { '_type' => 'Color', 'r' => int(($this->{'r'} + $other->{'r'}) / 2 + .5), 'g' => int(($this->{'g'} + $other->{'g'}) / 2 + .5), 'b' => int(($this->{'b'} + $other->{'b'}) / 2 + .5), }; Color__make_string($new); return $new; } sub Color__lighter { my ($ctx, $this, $amt) = @_; $amt = defined $amt ? $amt : 30; Color__update_hsl($this); my $new = { '_type' => 'Color', '_hslset' => 1, '_h' => $this->{'_h'}, '_s' => $this->{'_s'}, '_l' => ($this->{'_l'} + $amt > 255 ? 255 : $this->{'_l'} + $amt), }; Color__update_rgb($new); return $new; } sub Color__darker { my ($ctx, $this, $amt) = @_; $amt = defined $amt ? $amt : 30; Color__update_hsl($this); my $new = { '_type' => 'Color', '_hslset' => 1, '_h' => $this->{'_h'}, '_s' => $this->{'_s'}, '_l' => ($this->{'_l'} - $amt < 0 ? 0 : $this->{'_l'} - $amt), }; Color__update_rgb($new); return $new; } sub Comment__get_link { my ($ctx, $this, $key) = @_; if ($key eq "delete_comment" || $key eq "unscreen_comment" || $key eq "screen_comment" || $key eq "freeze_thread" || $key eq "unfreeze_thread") { my $page = get_page(); my $u = $page->{'_u'}; my $post_user = $page->{'entry'} ? $page->{'entry'}->{'poster'}->{'username'} : undef; my $com_user = $this->{'poster'} ? $this->{'poster'}->{'username'} : undef; my $remote = LJ::get_remote(); if ($key eq "delete_comment") { return undef unless LJ::Talk::can_delete($remote, $u, $post_user, $com_user); return { '_type' => "Link", 'url' => "$LJ::SITEROOT/delcomment.bml?journal=$u->{'user'}&id=$this->{'talkid'}", 'caption' => $ctx->[S2::PROPS]->{"text_multiform_opt_delete"}, 'icon' => LJ::S2::Image("$LJ::IMGPREFIX/btn_del.gif", 22, 20), }; } if ($key eq "freeze_thread") { return undef if $this->{'frozen'}; return undef unless LJ::Talk::can_freeze($remote, $u, $post_user, $com_user); return { '_type' => "Link", 'url' => "$LJ::SITEROOT/talkscreen.bml?mode=freeze&journal=$u->{'user'}&talkid=$this->{'talkid'}", 'caption' => $ctx->[S2::PROPS]->{"text_multiform_opt_freeze"}, 'icon' => LJ::S2::Image("$LJ::IMGPREFIX/btn_freeze.gif", 22, 20), }; } if ($key eq "unfreeze_thread") { return undef unless $this->{'frozen'}; return undef unless LJ::Talk::can_unfreeze($remote, $u, $post_user, $com_user); return { '_type' => "Link", 'url' => "$LJ::SITEROOT/talkscreen.bml?mode=unfreeze&journal=$u->{'user'}&talkid=$this->{'talkid'}", 'caption' => $ctx->[S2::PROPS]->{"text_multiform_opt_unfreeze"}, 'icon' => LJ::S2::Image("$LJ::IMGPREFIX/btn_unfreeze.gif", 22, 20), }; } if ($key eq "screen_comment") { return undef if $this->{'screened'}; return undef unless LJ::Talk::can_screen($remote, $u, $post_user, $com_user); return { '_type' => "Link", 'url' => "$LJ::SITEROOT/talkscreen.bml?mode=screen&journal=$u->{'user'}&talkid=$this->{'talkid'}", 'caption' => $ctx->[S2::PROPS]->{"text_multiform_opt_screen"}, 'icon' => LJ::S2::Image("$LJ::IMGPREFIX/btn_scr.gif", 22, 20), }; } if ($key eq "unscreen_comment") { return undef unless $this->{'screened'}; return undef unless LJ::Talk::can_unscreen($remote, $u, $post_user, $com_user); return { '_type' => "Link", 'url' => "$LJ::SITEROOT/talkscreen.bml?mode=unscreen&journal=$u->{'user'}&talkid=$this->{'talkid'}", 'caption' => $ctx->[S2::PROPS]->{"text_multiform_opt_unscreen"}, 'icon' => LJ::S2::Image("$LJ::IMGPREFIX/btn_unscr.gif", 22, 20), }; } } } sub Comment__print_multiform_check { my ($ctx, $this) = @_; my $tid = $this->{'talkid'} >> 8; $S2::pout->(""); } # class 'date' sub Date__day_of_week { my ($ctx, $dt) = @_; return $dt->{'_dayofweek'} if defined $dt->{'_dayofweek'}; return $dt->{'_dayofweek'} = LJ::day_of_week($dt->{'year'}, $dt->{'month'}, $dt->{'day'}) + 1; } *DateTime__day_of_week = \&Date__day_of_week; my %dt_vars = ( 'm' => "\$time->{month}", 'mm' => "sprintf('%02d', \$time->{month})", 'd' => "\$time->{day}", 'dd' => "sprintf('%02d', \$time->{day})", 'yy' => "sprintf('%02d', \$time->{year} % 100)", 'yyyy' => "\$time->{year}", 'mon' => "\$ctx->[S2::PROPS]->{lang_monthname_short}->[\$time->{month}]", 'month' => "\$ctx->[S2::PROPS]->{lang_monthname_long}->[\$time->{month}]", 'da' => "\$ctx->[S2::PROPS]->{lang_dayname_short}->[Date__day_of_week(\$ctx, \$time)]", 'day' => "\$ctx->[S2::PROPS]->{lang_dayname_long}->[Date__day_of_week(\$ctx, \$time)]", 'dayord' => "S2::run_function(\$ctx, \"lang_ordinal(int)\", \$time->{day})", 'H' => "\$time->{hour}", 'HH' => "sprintf('%02d', \$time->{hour})", 'h' => "(\$time->{hour} % 12 || 12)", 'hh' => "sprintf('%02d', (\$time->{hour} % 12 || 12))", 'min' => "sprintf('%02d', \$time->{min})", 'sec' => "sprintf('%02d', \$time->{sec})", 'a' => "(\$time->{hour} < 12 ? 'a' : 'p')", 'A' => "(\$time->{hour} < 12 ? 'A' : 'P')", ); sub Date__date_format { my ($ctx, $this, $fmt) = @_; $fmt ||= "short"; my $c = \$ctx->[S2::SCRATCH]->{'_code_datefmt'}->{$fmt}; return $$c->($this) if ref $$c eq "CODE"; if (++$ctx->[S2::SCRATCH]->{'_code_datefmt_count'} > 15) { return "[too_many_fmts]"; } my $realfmt = $fmt; if (defined $ctx->[S2::PROPS]->{"lang_fmt_date_$fmt"}) { $realfmt = $ctx->[S2::PROPS]->{"lang_fmt_date_$fmt"}; } my @parts = split(/\%\%/, $realfmt); my $code = "\$\$c = sub { my \$time = shift; return join('',"; my $i = 0; foreach (@parts) { if ($i % 2) { $code .= $dt_vars{$_} . ","; } else { $_ = LJ::ehtml($_); $code .= "\$parts[$i],"; } $i++; } $code .= "); };"; eval $code; return $$c->($this); } *DateTime__date_format = \&Date__date_format; sub DateTime__time_format { my ($ctx, $this, $fmt) = @_; $fmt ||= "short"; my $c = \$ctx->[S2::SCRATCH]->{'_code_timefmt'}->{$fmt}; return $$c->($this) if ref $$c eq "CODE"; if (++$ctx->[S2::SCRATCH]->{'_code_timefmt_count'} > 15) { return "[too_many_fmts]"; } my $realfmt = $fmt; if (defined $ctx->[S2::PROPS]->{"lang_fmt_time_$fmt"}) { $realfmt = $ctx->[S2::PROPS]->{"lang_fmt_time_$fmt"}; } my @parts = split(/\%\%/, $realfmt); my $code = "\$\$c = sub { my \$time = shift; return join('',"; my $i = 0; foreach (@parts) { if ($i % 2) { $code .= $dt_vars{$_} . ","; } else { $_ = LJ::ehtml($_); $code .= "\$parts[$i],"; } $i++; } $code .= "); };"; eval $code; return $$c->($this); } sub EntryLite__get_link { my ($ctx, $this, $key) = @_; return undef; } sub EntryLite__get_tags_text { my ($ctx, $this) = @_; return LJ::S2::get_tags_text($ctx, $this->{tags}) || ""; } *Entry__get_tags_text = \&EntryLite__get_tags_text; sub EntryLite__get_plain_subject { my ($ctx, $this) = @_; return $this->{'_plainsubject'} if $this->{'_plainsubject'}; return $this->{'subject'} unless $this->{'_rawsubject'}; my $subj = $this->{'subject'}; LJ::CleanHTML::clean_subject_all(\$subj); return $this->{'_plainsubject'} = $subj; } *Entry__get_plain_subject = \&EntryLite__get_plain_subject; sub Entry__get_link { my ($ctx, $this, $key) = @_; if ($key eq "nav_prev" || $key eq "edit_entry" || $key eq "mem_add" || $key eq "tell_friend" || $key eq "nav_next" || $key eq "edit_tags") { my $journal = $this->{'journal'}->{'username'}; my $poster = $this->{'poster'}->{'username'}; my $remote = LJ::get_remote(); if ($key eq "edit_entry") { return undef unless $remote && ($remote->{'user'} eq $journal || $remote->{'user'} eq $poster || LJ::can_manage($remote, LJ::load_user($journal))); return { '_type' => "Link", 'url' => "$LJ::SITEROOT/editjournal.bml?journal=$journal&itemid=$this->{'itemid'}", 'caption' => "Edit Entry", 'icon' => LJ::S2::Image("$LJ::IMGPREFIX/btn_edit.gif", 22, 20), } } if ($key eq "edit_tags") { return undef unless $remote && LJ::Tags::can_add_tags(LJ::load_user($journal), $remote); return { '_type' => "Link", 'url' => "$LJ::SITEROOT/edittags.bml?journal=$journal&itemid=$this->{'itemid'}", 'caption' => 'Edit Tags', 'icon' => LJ::S2::Image("$LJ::IMGPREFIX/btn_edittags.gif", 22, 20), }; } if ($key eq "tell_friend") { return undef if $LJ::DISABLED{'tellafriend'}; return { '_type' => "Link", 'url' => "$LJ::SITEROOT/tools/tellafriend.bml?journal=$journal&itemid=$this->{'itemid'}", 'caption' => "Tell A Friend", 'icon' => LJ::S2::Image("$LJ::IMGPREFIX/btn_tellfriend.gif", 22, 20), }; } if ($key eq "mem_add") { return undef if $LJ::DISABLED{'memories'}; return { '_type' => "Link", 'url' => "$LJ::SITEROOT/tools/memadd.bml?journal=$journal&itemid=$this->{'itemid'}", 'caption' => "Add to Memories", 'icon' => LJ::S2::Image("$LJ::IMGPREFIX/btn_memories.gif", 22, 20), }; } if ($key eq "nav_prev") { return { '_type' => "Link", 'url' => "$LJ::SITEROOT/go.bml?journal=$journal&itemid=$this->{'itemid'}&dir=prev", 'caption' => "Previous Entry", 'icon' => LJ::S2::Image("$LJ::IMGPREFIX/btn_prev.gif", 22, 20), }; } if ($key eq "nav_next") { return { '_type' => "Link", 'url' => "$LJ::SITEROOT/go.bml?journal=$journal&itemid=$this->{'itemid'}&dir=next", 'caption' => "Next Entry", 'icon' => LJ::S2::Image("$LJ::IMGPREFIX/btn_next.gif", 22, 20), }; } } } sub Entry__plain_subject { my ($ctx, $this) = @_; return $this->{'_subject_plain'} if defined $this->{'_subject_plain'}; $this->{'_subject_plain'} = $this->{'subject'}; LJ::CleanHTML::clean_subject_all(\$this->{'_subject_plain'}); return $this->{'_subject_plain'}; } sub EntryPage__print_multiform_actionline { my ($ctx, $this) = @_; return unless $this->{'multiform_on'}; my $pr = $ctx->[S2::PROPS]; $S2::pout->($pr->{'text_multiform_des'} . "\n" . LJ::html_select({'name' => 'mode' }, "" => "", map { $_ => $pr->{"text_multiform_opt_$_"} } qw(unscreen screen delete)) . "\n" . LJ::html_submit('', $pr->{'text_multiform_btn'}, { "onclick" => "return (document.multiform.mode.value != \"delete\") " . "|| confirm(\"" . LJ::ejs($pr->{'text_multiform_conf_delete'}) . "\");" })); } sub EntryPage__print_multiform_end { my ($ctx, $this) = @_; return unless $this->{'multiform_on'}; $S2::pout->(""); } sub EntryPage__print_multiform_start { my ($ctx, $this) = @_; return unless $this->{'multiform_on'}; $S2::pout->("
\n" . LJ::html_hidden("ditemid", $this->{'entry'}->{'itemid'}, "journal", $this->{'entry'}->{'journal'}->{'username'}) . "\n"); } sub Page__visible_tag_list { my $ctx = shift; my $remote = LJ::get_remote(); my $u = $LJ::S2::CURR_PAGE->{'_u'}; return [] unless $u; my $tags = LJ::Tags::get_usertags($u, { remote => $remote }); return [] unless $tags; my $prefix = LJ::journal_base($u) . '/tag/'; my @taglist; foreach my $kwid (keys %{$tags}) { # only show tags for display next unless $tags->{$kwid}->{display}; # create tag object push @taglist, LJ::S2::Tag($prefix, $tags->{$kwid}->{name}); } @taglist = sort { $a->{name} cmp $b->{name} } @taglist; return \@taglist; } *RecentPage__visible_tag_list = \&Page__visible_tag_list; *DayPage__visible_tag_list = \&Page__visible_tag_list; *MonthPage__visible_tag_list = \&Page__visible_tag_list; *YearPage__visible_tag_list = \&Page__visible_tag_list; *FriendsPage__visible_tag_list = \&Page__visible_tag_list; *EntryPage__visible_tag_list = \&Page__visible_tag_list; *ReplyPage__visible_tag_list = \&Page__visible_tag_list; sub Page__get_latest_month { my ($ctx, $this) = @_; return $this->{'_latest_month'} if defined $this->{'_latest_month'}; my $counts = LJ::S2::get_journal_day_counts($this); my ($year, $month); my @years = sort { $a <=> $b } keys %$counts; if (@years) { # year/month of last post $year = $years[-1]; $month = (sort { $a <=> $b } keys %{$counts->{$year}})[-1]; } else { # year/month of current date, if no posts my @now = gmtime(time); ($year, $month) = ($now[5]+1900, $now[4]+1); } return $this->{'_latest_month'} = LJ::S2::YearMonth($this, { 'year' => $year, 'month' => $month, }); } *RecentPage__get_latest_month = \&Page__get_latest_month; *DayPage__get_latest_month = \&Page__get_latest_month; *MonthPage__get_latest_month = \&Page__get_latest_month; *YearPage__get_latest_month = \&Page__get_latest_month; *FriendsPage__get_latest_month = \&Page__get_latest_month; *EntryPage__get_latest_month = \&Page__get_latest_month; *ReplyPage__get_latest_month = \&Page__get_latest_month; sub palimg_modify { my ($ctx, $filename, $items) = @_; return undef unless $filename =~ /^\w[\w\/\-]*\.(gif|png)$/; my $url = "$LJ::PALIMGROOT/$filename"; return $url unless $items && @$items; return undef if @$items > 7; $url .= "/p"; foreach my $pi (@$items) { die "Can't modify a palette index greater than 15 with palimg_modify\n" if $pi->{'index'} > 15; $url .= sprintf("%1x%02x%02x%02x", $pi->{'index'}, $pi->{'color'}->{'r'}, $pi->{'color'}->{'g'}, $pi->{'color'}->{'b'}); } return $url; } sub palimg_tint { my ($ctx, $filename, $bcol, $dcol) = @_; # bright color, dark color [opt] return undef unless $filename =~ /^\w[\w\/\-]*\.(gif|png)$/; my $url = "$LJ::PALIMGROOT/$filename"; $url .= "/pt"; foreach my $col ($bcol, $dcol) { next unless $col; $url .= sprintf("%02x%02x%02x", $col->{'r'}, $col->{'g'}, $col->{'b'}); } return $url; } sub palimg_gradient { my ($ctx, $filename, $start, $end) = @_; return undef unless $filename =~ /^\w[\w\/\-]*\.(gif|png)$/; my $url = "$LJ::PALIMGROOT/$filename"; $url .= "/pg"; foreach my $pi ($start, $end) { next unless $pi; $url .= sprintf("%02x%02x%02x%02x", $pi->{'index'}, $pi->{'color'}->{'r'}, $pi->{'color'}->{'g'}, $pi->{'color'}->{'b'}); } return $url; } sub userlite_base_url { my ($ctx, $UserLite) = @_; my $u = $UserLite->{_u}; if ($u->{journaltype} eq "P") { return "$LJ::SITEROOT/users/$u->{user}"; } elsif ($u->{journaltype} eq "C") { return "$LJ::SITEROOT/community/$u->{user}"; } elsif ($u->{journaltype} eq "I") { return $u->url; } } sub userlite_as_string { my ($ctx, $UserLite) = @_; return LJ::ljuser($UserLite->{'_u'}); } sub PalItem { my ($ctx, $idx, $color) = @_; return undef unless $color && $color->{'_type'} eq "Color"; return undef unless $idx >= 0 && $idx <= 255; return { '_type' => 'PalItem', 'color' => $color, 'index' => $idx+0, }; } sub YearMonth__month_format { my ($ctx, $this, $fmt) = @_; $fmt ||= "long"; my $c = \$ctx->[S2::SCRATCH]->{'_code_monthfmt'}->{$fmt}; return $$c->($this) if ref $$c eq "CODE"; if (++$ctx->[S2::SCRATCH]->{'_code_timefmt_count'} > 15) { return "[too_many_fmts]"; } my $realfmt = $fmt; if (defined $ctx->[S2::PROPS]->{"lang_fmt_month_$fmt"}) { $realfmt = $ctx->[S2::PROPS]->{"lang_fmt_month_$fmt"}; } my @parts = split(/\%\%/, $realfmt); my $code = "\$\$c = sub { my \$time = shift; return join('',"; my $i = 0; foreach (@parts) { if ($i % 2) { $code .= $dt_vars{$_} . ","; } else { $_ = LJ::ehtml($_); $code .= "\$parts[$i],"; } $i++; } $code .= "); };"; eval $code; return $$c->($this); } sub Image__set_url { my ($ctx, $img, $newurl) = @_; $img->{'url'} = LJ::eurl($newurl); } sub ItemRange__url_of { my ($ctx, $this, $n) = @_; return "" unless ref $this->{'_url_of'} eq "CODE"; return $this->{'_url_of'}->($n+0); } sub UserLite__equals { return $_[1]->{'_u'}{'userid'} == $_[2]->{'_u'}{'userid'}; } *User__equals = \&UserLite__equals; *Friend__equals = \&UserLite__equals; sub string__substr { my ($ctx, $this, $start, $length) = @_; use utf8; return substr($this, $start, $length); } sub string__length { use utf8; my ($ctx, $this) = @_; return length($this); } sub string__lower { use utf8; my ($ctx, $this) = @_; return lc($this); } sub string__upper { use utf8; my ($ctx, $this) = @_; return uc($this); } sub string__upperfirst { use utf8; my ($ctx, $this) = @_; return ucfirst($this); } sub string__starts_with { use utf8; my ($ctx, $this, $str) = @_; return $this =~ /^\Q$str\E/; } sub string__ends_with { use utf8; my ($ctx, $this, $str) = @_; return $this =~ /\Q$str\E$/; } sub string__contains { use utf8; my ($ctx, $this, $str) = @_; return $this =~ /\Q$str\E/; } sub string__repeat { use utf8; my ($ctx, $this, $num) = @_; $num += 0; my $size = length($this) * $num; return "[too large]" if $size > 5000; return $this x $num; } 1;