#!/usr/bin/perl # # This code was forked from the LiveJournal project owned and operated # by Live Journal, Inc. The code has been modified and expanded by # Dreamwidth Studios, LLC. These files were originally licensed under # the terms of the license supplied by Live Journal, Inc, which can # currently be found at: # # http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt # # In accordance with the original license, this code and all its # modifications are provided under the GNU General Public License. # A copy of that license can be found in the LICENSE file included as # part of this distribution. package LJ::S2; use strict; use DW; use DW::Request; use lib DW->home . "/src/s2"; use S2; use S2::Color; use S2::Checker; use S2::Compiler; use HTMLCleaner; use LJ::CSS::Cleaner; 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; use LJ::S2::TagsPage; use LJ::S2::IconsPage; use Storable; use POSIX (); use DW::SiteScheme; use LJ::PageStats; # TEMP HACK sub get_s2_reader { return LJ::get_dbh( "s2slave", "slave", "master" ); } sub make_journal { my ( $u, $styleid, $view, $remote, $opts ) = @_; my $apache_r = $opts->{'r'}; my $ret; $LJ::S2::ret_ref = \$ret; my ( $entry, $page, $use_modtime ); if ( $view eq "res" ) { if ( $opts->{'pathextra'} =~ m!/(\d+)/stylesheet$! ) { $styleid = $1 unless $styleid && $styleid eq "sitefeeds"; $entry = [ qw( Page::print_contextual_stylesheet() Page::print_default_stylesheet() print_stylesheet() Page::print_theme_stylesheet() ) ]; $opts->{'contenttype'} = 'text/css'; $use_modtime = 1; } else { $opts->{'handler_return'} = $apache_r->NOT_FOUND; return; } } $u->{'_s2styleid'} = ( $styleid && $styleid =~ /^\d+$/ ) ? $styleid + 0 : 0; # try to get an S2 context my $ctx = s2_context( $styleid, use_modtime => $use_modtime, u => $u, style_u => $opts->{style_u} ); unless ($ctx) { $opts->{'handler_return'} = $apache_r->OK; return; } # see also Apache/LiveJournal.pm my $lang = $LJ::DEFAULT_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 siteviews version # instead. We may also have explicitly asked to use siteviews by the caller my $style_u = $opts->{style_u} || $u; if ( !LJ::S2::use_journalstyle_entry_page( $style_u, $ctx ) && ( $view eq "entry" || $view eq "reply" ) # reply / entry page || !LJ::S2::use_journalstyle_icons_page( $style_u, $ctx ) && ( $view eq "icons" ) # icons || ( ( $view eq "entry" || $view eq "reply" ) # make sure capability supports it && !LJ::get_cap( ( $opts->{'checkremote'} ? $remote : $u ), "s2view$view" ) ) ) { $styleid = "siteviews"; # we changed the styleid, so generate a new context $ctx = s2_context( "siteviews", use_modtime => $use_modtime, u => $u, style_u => $opts->{style_u} ); } if ( $styleid && $styleid eq "siteviews" ) { $apache_r->notes->{'no_control_strip'} = 1; ${ $opts->{'handle_with_siteviews_ref'} } = 1; $opts->{siteviews_extra_content} ||= {}; my $siteviews_class = { '_type' => "Siteviews", '_input_captures' => [], '_content' => $opts->{siteviews_extra_content}, }; $ctx->[S2::SCRATCH]->{siteviews_enabled} = 1; $ctx->[S2::PROPS]->{SITEVIEWS} = $siteviews_class; } # setup tags backwards compatibility unless ( $ctx->[S2::PROPS]->{'tags_aware'} ) { $opts->{enable_tags_compatibility} = 1; } $opts->{'ctx'} = $ctx; $LJ::S2::CURR_CTX = $ctx; foreach ( "name", "url", "urlname" ) { LJ::text_out( \$u->{$_} ); } $u->{'_journalbase'} = $u->journal_base( vhost => $opts->{'vhost'} ); my $view2class = { lastn => "RecentPage", archive => "YearPage", day => "DayPage", read => "FriendsPage", month => "MonthPage", reply => "ReplyPage", entry => "EntryPage", tag => "TagsPage", network => "FriendsPage", icons => "IconsPage", }; if ( my $class = $view2class->{$view} ) { $entry = "${class}::print()"; no strict 'refs'; # this will fail (bogus method), but in non-apache context will bring # in the right file because of Class::Autouse above eval { "LJ::S2::$class"->force_class_autouse; }; my $cv = *{"LJ::S2::$class"}{CODE}; die "No LJ::S2::$class function!" unless $cv; $page = $cv->( $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'; if ( !LJ::BetaFeatures->user_in_beta( $remote => "nos2foundation" ) ) { LJ::set_active_resource_group('foundation'); # Minimal Foundation component support if we're not in site scheme unless ( $ctx->[S2::SCRATCH]->{siteviews_enabled} ) { LJ::need_res( { priority => $LJ::LIB_RES_PRIORITY, group => 'foundation' }, "stc/css/foundation/foundation_minimal.css" ); } } else { LJ::set_active_resource_group('jquery'); } # Control strip if ( $page->{show_control_strip} ) { LJ::Hooks::run_hook('control_strip_stylesheet_link'); } LJ::need_res( { group => "all" }, qw( js/jquery/jquery.ui.core.js js/jquery/jquery.ui.widget.js js/jquery/jquery.ui.tooltip.js js/jquery/jquery.ui.button.js js/jquery/jquery.ui.dialog.js js/jquery/jquery.ui.position.js js/jquery.ajaxtip.js stc/jquery/jquery.ui.core.css stc/jquery/jquery.ui.tooltip.css stc/jquery/jquery.ui.button.css stc/jquery/jquery.ui.dialog.css stc/jquery/jquery.ui.theme.smoothness.css stc/canary.css ) ); # this will cause double-JS and likely cause issues if called during siteviews # as this is done once the page is out of S2's control. unless ( $ctx->[S2::SCRATCH]->{siteviews_enabled} || $view eq "res" ) { $page->{head_content} .= LJ::res_includes_head(); # CSS $page->{head_content} .= get_script_tags($page) if $LJ::ACTIVE_RES_GROUP eq "jquery"; # Foundation puts scripts at end of body } $page->{head_content} .= LJ::PageStats->new->render_head('journal'); s2_run( $apache_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); # If there's an entry for contenttype in the context 'scratch' # area, copy it into the "real" content type field. $opts->{contenttype} = $ctx->[S2::SCRATCH]->{contenttype} if defined $ctx->[S2::SCRATCH]->{contenttype}; $ret = $page->{'LJ_cmtinfo'} . $ret if $opts->{'need_cmtinfo'} and defined $page->{'LJ_cmtinfo'}; return $ret; } sub s2_run { my ( $apache_r, $ctx, $opts, $entry, $page ) = @_; $opts ||= {}; local $LJ::S2::CURR_CTX = $ctx; 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 $apache_r = eval { BML::get_request() }; if ( $apache_r && $apache_r->notes->{journalid} ) { my $journal = LJ::load_userid( $apache_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; $apache_r->status($status); $apache_r->content_type( $ctx->[S2::SCRATCH]->{'ctype'} || $ctype ); # FIXME: not necessary in ModPerl 2.0? #$apache_r->send_http_header(); }; my $need_flush; my $print_ctr = 0; # every 'n' prints we check the recursion depth my $out_straight = sub { # Hacky: forces text flush. see: # http://zilla.livejournal.org/906 if ($need_flush) { $cleaner->parse(""); $need_flush = 0; } my $output = $_[0]; $output = '' unless defined $output; $$LJ::S2::ret_ref .= $output; S2::check_depth() if ++$print_ctr % 8 == 0; }; my $out_clean = sub { my $text = shift; $text = '' unless defined $text; $cleaner->parse($text); $need_flush = 1; S2::check_depth() if ++$print_ctr % 8 == 0; }; 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 my $css_mode = $ctype eq "text/css"; S2::Builtin::LJ::start_css($ctx) if $css_mode; eval { if ( ref $entry ) { foreach (@$entry) { S2::run_code( $ctx, $_, $page ) if S2::function_exists( $ctx, $_ ); } } else { S2::run_code( $ctx, $entry, $page ); } }; S2::Builtin::LJ::end_css($ctx) if $css_mode; $LJ::S2::CURR_PAGE = 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; } # # name: LJ::S2::make_link # des: Takes a group of key=value pairs to append to a URL. # returns: The finished URL. # args: url, vars # des-url: A string with the URL to append to. The URL # should not have a question mark in it. # des-vars: A hashref of the key=value pairs to append with. # sub make_link { my $url = shift; my $vars = shift; my $append = "?"; foreach ( keys %$vars ) { next if ( $vars->{$_} eq "" ); $url .= "${append}${_}=$vars->{$_}"; $append = "&"; } return $url; } # Uses the res_includes system to return the appropriate JavaScript tags # for the current page. This should be called once per page at most, and never # called for siteviews pages. sub get_script_tags { my ($page) = @_; my $ret = LJ::res_includes_body(); $ret .= LJ::control_strip_js_inject( user => $page->{_u}->user ) if $page->{show_control_strip}; $ret .= '' if $LJ::ACTIVE_RES_GROUP eq "foundation"; return $ret; } # # 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
"; } # 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 = ( map { $_ => 1 } @lids ); # layerid => 1 # 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"} ) { delete $need{$lid}; $ret->{$lid} = $uid; } } # if we still need any from the database, get them now if (%need) { my $dbh = LJ::get_db_writer(); my $in = join( ',', keys %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 # if dev server, only cache layers for 1 second foreach my $lid (@lids) { if ( my $loaded = S2::layer_loaded( $lid, $LJ::IS_DEV_SERVER ? 1 : 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 foreach my $lid (@from_db) { next if S2::layer_loaded($lid); unless ( $us->{$lid} ) { next; } if ( $us->{$lid}->{userid} == $sysid ) { next; } LJ::MemCache::set( [ $lid, "s2c:$lid" ], [ time(), 0 ] ); } return $maxtime; } sub is_public_internal_layer { my $layerid = shift; my $pub = get_public_layers(); while ($layerid) { # doesn't exist, probably private return 0 unless defined $pub->{$layerid}; my $internal = $pub->{$layerid}->{is_internal}; return 1 if defined $internal && $internal; return 0 if defined $internal && !$internal; $layerid = $pub->{$layerid}->{b2lid}; } return 0; } # whether all layers in this style are public sub style_is_public { my $style = $_[0]; return 0 unless $style; my %lay_info; LJ::S2::load_layer_info( \%lay_info, [ $style->{layer}->{layout}, $style->{layer}->{theme}, $style->{layer}->{user}, $style->{layer}->{i18n}, $style->{layer}->{i18nc} ] ); my $pub = get_public_layers(); while ( my ( $layerid, $layerinfo ) = each %lay_info ) { return 0 unless $pub->{$layerid} || $layerinfo->{is_public}; } return 1; } # find existing re-distributed layers that are in the database # and their styleids. sub get_public_layers { my $opts = ref $_[0] eq 'HASH' ? shift : {}; my $sysid = shift; # optional system userid (usually not used) unless ( $opts->{force} ) { $LJ::CACHED_PUBLIC_LAYERS ||= LJ::MemCache::get("s2publayers"); return $LJ::CACHED_PUBLIC_LAYERS if $LJ::CACHED_PUBLIC_LAYERS; } $sysid ||= LJ::get_userid("system"); my $layers = get_layers_of_user( $sysid, "is_system", [qw(des note author author_name author_email is_internal)] ); $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, $infokeys ) = @_; my $subst_user = LJ::Hooks::run_hook( "substitute_s2_layers_user", $u ); if ( defined $subst_user && LJ::isu($subst_user) ) { $u = $subst_user; } 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', " : ""; $extrainfo .= join( ', ', map { $dbr->quote($_) } @$infokeys ) . ", " if $infokeys; 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 ( defined $layers{$_}->{uniq} && $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 # - 'force_layers' -- if force_layers, then the style's layers are loaded from the database sub get_style { my ( $arg, $opts ) = @_; my $verify = 0; my $force_layers = 0; my ( $styleid, $u ); if ( ref $opts eq "HASH" ) { $verify = $opts->{'verify'}; $u = $opts->{'u'}; $force_layers = $opts->{'force_layers'}; } elsif ($opts) { $verify = 1; die "Bogus second arg to LJ::S2::get_style" if ref $opts; } if ( ref $arg ) { $u = $arg; $styleid = $u->prop('s2_style'); } else { $styleid = ( $arg || 0 ) + 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, $force_layers ) : LJ::S2::get_style_layers( $styleid, $force_layers ); 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 ( $styleid, %opts ) = @_; # get arguments we'll use frequently my $r = DW::Request->get; my $u = $opts{u} || LJ::get_active_journal(); my $remote = $opts{remote} || LJ::get_remote(); my $style_u = $opts{style_u} || $u; my %style; if ( $styleid && $styleid eq "siteviews" ) { %style = siteviews_style( $u, $remote, $opts{mode} ); } elsif ( $styleid && $styleid eq "sitefeeds" ) { %style = sitefeeds_style(); } if ( ref($styleid) eq "CODE" ) { %style = $styleid->(); } # 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 return s2_context( 0, %opts ) if $styleid; # were we trying to load the default style? $r->content_type('text/html'); $r->print( 'Error preparing to run: One or more layers required to load the stock style have been deleted.' ); return undef; } # if we are supposed to use modtime checking (i.e. for stylesheets) then go # ahead and do that logic now if ( $opts{use_modtime} ) { my $mod_since = $r->header_in('If-Modified-Since') || ''; if ( $mod_since eq LJ::time_to_http($modtime) ) { # 304 return; unload non-public layers LJ::S2::cleanup_layers(@layers); $r->status_line('304 Not Modified'); return undef; } else { $r->set_last_modified($modtime); } } my $ctx; eval { $ctx = S2::make_context(@layers); }; if ($ctx) { # let's use the scratch field as a hashref $ctx->[S2::SCRATCH] ||= {}; LJ::S2::populate_system_props($ctx); LJ::S2::alias_renamed_props($ctx); LJ::S2::alias_overriding_props($ctx); S2::set_output( sub { } ); # printing suppressed S2::set_output_safe( sub { } ); eval { S2::run_code( $ctx, "prop_init()" ); }; eval { S2::run_code( $ctx, "modules_init()" ); }; escape_all_props( $ctx, \@layers ); return $ctx unless $@; } # failure to generate context; unload our non-public layers LJ::S2::cleanup_layers(@layers); $r->content_type('text/html'); $r->print( 'Error preparing to run: ' . $@ ); return undef; } sub escape_all_props { my ( $ctx, $lids ) = @_; foreach my $lid (@$lids) { foreach my $pname ( S2::get_property_names($lid) ) { next unless $ctx->[S2::PROPS]{$pname}; my $prop = S2::get_property( $lid, $pname ); my $mode = $prop->{string_mode} || "plain"; escape_prop_value( $ctx->[S2::PROPS]{$pname}, $mode ); } } } my $css_cleaner; sub _css_cleaner { return $css_cleaner ||= LJ::CSS::Cleaner->new; } sub escape_prop_value_ret { my $what = $_[0]; escape_prop_value( $what, $_[1] ); return $what; } sub escape_prop_value { my $mode = $_[1]; my $css_c = _css_cleaner(); # This function modifies its first parameter in place. if ( ref $_[0] eq 'ARRAY' ) { for ( my $i = 0 ; $i < scalar( @{ $_[0] } ) ; $i++ ) { escape_prop_value( $_[0][$i], $mode ); } } elsif ( ref $_[0] eq 'HASH' ) { foreach my $k ( keys %{ $_[0] } ) { escape_prop_value( $_[0]{$k}, $mode ); } } elsif ( !ref $_[0] ) { if ( $mode eq 'simple-html' || $mode eq 'simple-html-oneline' ) { LJ::CleanHTML::clean_subject( \$_[0] ); $_[0] =~ s!\n!
!g if $mode eq 'simple-html'; } elsif ( $mode eq 'html' || $mode eq 'html-oneline' ) { LJ::CleanHTML::clean_event( \$_[0] ); $_[0] =~ s!\n!
!g if defined $_[0] && $mode eq 'html'; } elsif ( $mode eq 'css' ) { my $clean = $css_c->clean( $_[0] ); LJ::Hooks::run_hook( 'css_cleaner_transform', \$clean ); $_[0] = $clean; } elsif ( $mode eq 'css-attrib' ) { if ( $_[0] =~ /[\{\}]/ ) { # If the string contains any { and } characters, it can't go in a style="" attrib $_[0] = "/* bad CSS: can't use braces in a style attribute */"; return; } my $clean = $css_c->clean_property( $_[0] ); $_[0] = $clean; } elsif ( defined $_[0] ) { # plain $_[0] =~ s//>/g; $_[0] =~ s!\n!
!g; } } else { $_[0] = undef; # Something's gone very wrong. Zzap the value completely. } } sub siteviews_style { my ( $u, $remote, $mode ) = @_; my %style; my $public = get_public_layers(); my $theme = "siteviews/default"; foreach my $candidate ( DW::SiteScheme->inheritance ) { if ( $public->{"siteviews/$candidate"} ) { $theme = "siteviews/$candidate"; last; } } %style = ( core => "core2", layout => "siteviews/layout", theme => $theme, ); # convert the value names to s2layerid while ( my ( $layer, $name ) = each %style ) { next unless $public->{$name}; my $id = $public->{$name}->{'s2lid'}; $style{$layer} = $id; } return %style; } sub sitefeeds_style { return unless %$LJ::DEFAULT_FEED_STYLE; my $public = get_public_layers(); my %style; # convert the value names to s2layerid while ( my ( $layer, $name ) = each %$LJ::DEFAULT_FEED_STYLE ) { next unless $public->{$name}; my $id = $public->{$name}->{'s2lid'}; $style{$layer} = $id; } return %style; } # 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 create_style { my ( $u, $name ) = @_; my $dbh = LJ::get_db_writer(); return 0 unless $dbh && $u->writer; my $uid = $u->{userid} + 0 or return 0; # 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; # in case we had an invalid / empty value from before LJ::MemCache::delete( [ $styleid, "s2s:$styleid" ] ); 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; $dbh->do( "DELETE FROM s2styles WHERE styleid=?", undef, $styleid ); $u->do( "DELETE FROM s2stylelayers2 WHERE userid=? AND styleid=?", undef, $u->{userid}, $styleid ); LJ::MemCache::delete( [ $styleid, "s2s:$styleid" ] ); return 1; } sub rename_user_style { my ( $u, $styleid, $name ) = @_; return 1 unless $styleid; my $dbh = LJ::get_db_writer(); return 0 unless $dbh; $dbh->do( "UPDATE s2styles SET name=? WHERE styleid=? AND userid=?", undef, $name, $styleid, $u->id ); LJ::MemCache::delete( [ $styleid, "s2s:$styleid" ] ); return 1; } sub load_style { my $db = ref $_[0] ? shift : undef; my $id = shift; return undef unless $id; my %opts = @_; my $memkey = [ $id, "s2s:$id" ]; my $style = LJ::MemCache::get($memkey); unless ( defined $style ) { $db ||= LJ::S2::get_s2_reader() or die "Unable to get S2 reader"; $style = $db->selectrow_hashref( "SELECT styleid, userid, name, modtime " . "FROM s2styles WHERE styleid=?", undef, $id ); die $db->errstr if $db->err; LJ::MemCache::add( $memkey, $style || {}, 3600 ); } return undef unless $style; unless ( $opts{skip_layer_load} ) { 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 s2stylelayers2 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 s2source_inno 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 ); $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 = $force ? undef : $LJ::S2::REQ_CACHE_STYLE_ID{$styleid}; return $stylay if $stylay; my $memkey = [ $styleid, "s2sl:$styleid" ]; $stylay = LJ::MemCache::get($memkey) unless $force; if ($stylay) { $LJ::S2::REQ_CACHE_STYLE_ID{$styleid} = $stylay; return $stylay; } # if an option $u was passed as the first arg, # we won't load the userid... otherwise we have to 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; }; $fetch->( $u, "SELECT type, s2lid FROM s2stylelayers2 " . "WHERE userid=? AND styleid=?", $u->userid, $styleid ); # set in memcache LJ::MemCache::set( $memkey, \%stylay ); $LJ::S2::REQ_CACHE_STYLE_ID{$styleid} = \%stylay; return \%stylay; } sub set_style_layers { 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->id, $styleid, $dbh->quote($_), $newlay{$_} // 0 ) } 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; my $layerid = $LJ::S2::REQ_CACHE_LAYER_ID{$lid}; return $layerid if $layerid; my $ret = $db->selectrow_hashref( "SELECT s2lid, b2lid, userid, type " . "FROM s2layers WHERE s2lid=?", undef, $lid ); die $db->errstr if $db->err; $LJ::S2::REQ_CACHE_LAYER_ID{$lid} = $ret; return $ret; } 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; $ctx->[S2::PROPS]->{'STYLES_IMGDIR'} = $LJ::IMGPREFIX . "/styles"; $ctx->[S2::PROPS]->{'STATDIR'} = $LJ::STATPREFIX; } # renamed some props from core1 => core2. Make sure that S2 still handles these variables correctly when working with a core1 layer sub alias_renamed_props { my $ctx = shift; $ctx->[S2::PROPS]->{num_items_recent} = $ctx->[S2::PROPS]->{page_recent_items} if exists $ctx->[S2::PROPS]->{page_recent_items}; $ctx->[S2::PROPS]->{num_items_reading} = $ctx->[S2::PROPS]->{page_friends_items} if exists $ctx->[S2::PROPS]->{page_friends_items}; $ctx->[S2::PROPS]->{reverse_sortorder_day} = $ctx->[S2::PROPS]->{page_day_sortorder} eq 'reverse' ? 1 : 0 if exists $ctx->[S2::PROPS]->{page_day_sortorder}; $ctx->[S2::PROPS]->{reverse_sortorder_year} = $ctx->[S2::PROPS]->{page_year_sortorder} eq 'reverse' ? 1 : 0 if exists $ctx->[S2::PROPS]->{page_year_sortorder}; # Not adding the new views to core1, force non-entry use_journalstyle_ to 0 for core1 if ( exists $ctx->[S2::PROPS]->{view_entry_disabled} ) { $ctx->[S2::PROPS]->{use_journalstyle_entry_page} = !$ctx->[S2::PROPS]->{view_entry_disabled}; $ctx->[S2::PROPS]->{use_journalstyle_icons_page} = 0; } } # use the "grouped_property_override" property to determine whether a custom property should override a default property. # one potential use: to customize which sections a module may show up in. sub alias_overriding_props { my $ctx = $_[0]; my %overrides = %{ $ctx->[S2::PROPS]->{grouped_property_override} || {} }; return unless %overrides; while ( my ( $original, $overriding ) = each %overrides ) { $ctx->[S2::PROPS]->{$original} = $ctx->[S2::PROPS]->{$overriding} if $ctx->[S2::PROPS]->{$overriding}; } } sub convert_prop_val { my ( $prop, $val ) = @_; $prop ||= {}; my $type = $prop->{type} || ''; return int($val) if $type eq "int"; return $val ? "true" : "false" if $type eq "bool"; # if not int or bool, treat property as text - use quotes, # use zero-width lookahead to insert a backslash where needed $val =~ s/(?=[\\\$\"])/\\/g; return qq{"$val"}; } 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 = LJ::Lang::ml('s2theme.autogenerated.warning'); $s2 .= "layerinfo \"type\" = \"user\";\n"; $s2 .= "layerinfo \"name\" = \"Auto-generated Customizations\";\n"; foreach my $name ( sort keys %$overrides ) { next if $name =~ /\W/; my $val = convert_prop_val( @{ $overrides->{$name} } ); $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 ); unless ($layer) { $$err_ref = "Unable to load layer"; return 0; } } unless ($lid) { $$err_ref = "No layer ID specified."; return 0; } # 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 = LJ::S2::load_layer_source($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 = !$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; } unless ($dbcm) { $$err_ref = "Unable to get database handle"; return 0; } 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'} ) { LJ::S2::set_layer_source( $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::freeze($checker); LJ::text_compress( \$chk_frz ); $dbh->do( "REPLACE INTO s2checker (s2lid, checker) VALUES (?,?)", undef, $lid, $chk_frz ) or die "replace into s2checker (lid = $lid)"; } # 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 "replace into s2info (values = $values)"; $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 "replace into s2compiled (lid = $lid)"; } 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 "replace into s2compiled2 (lid = $lid)"; } # 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; # check request cache my %layers_from_cache = (); foreach my $lid (@$listref) { my $layerinfo = $LJ::S2::REQ_CACHE_LAYER_INFO{$lid}; if ( keys %$layerinfo ) { $layers_from_cache{$lid} = 1; foreach my $k ( keys %$layerinfo ) { $outhash->{$lid}->{$k} = $layerinfo->{$k}; } } } # only return if we found all of the given layers in request cache if ( keys %$outhash && ( scalar @$listref == scalar keys %layers_from_cache ) ) { return 1; } # get all of the layers that weren't in request cache from the db my $in = join( ',', map { $_ + 0 } grep { !$layers_from_cache{$_} } @$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 ) { $LJ::S2::REQ_CACHE_LAYER_INFO{$id}->{$k} = $v; $outhash->{$id}->{$k} = $v; } return 1; } sub set_layer_source { my ( $s2lid, $source_ref ) = @_; my $dbh = LJ::get_db_writer(); my $rv = $dbh->do( "REPLACE INTO s2source_inno (s2lid, s2code) VALUES (?,?)", undef, $s2lid, $$source_ref ); die $dbh->errstr if $dbh->err; return $rv; } sub load_layer_source { my $s2lid = shift; my $dbh = LJ::get_db_writer(); return $dbh->selectrow_array( "SELECT s2code FROM s2source_inno WHERE s2lid=?", undef, $s2lid ); } sub load_layer_source_row { my $s2lid = shift; my $dbh = LJ::get_db_writer(); return $dbh->selectrow_hashref( "SELECT * FROM s2source_inno WHERE s2lid=?", undef, $s2lid ); } 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->{$_}; $v->{b2layer} = $src->{ $src->{$_}->{b2lid} }; # include layout information my $is_active = LJ::Hooks::run_hook( "layer_is_active", $v->{'uniq'} ); push @themes, $v if ( $v->{type} eq "theme" && $layid && $v->{b2lid} == $layid && ( !defined $is_active || $is_active ) ); } } return @themes; } # src, layid passed to get_layout_themes; u is optional sub get_layout_themes_select { my ( $src, $layid, $u ) = @_; my ( @sel, $last_uid, $text, $can_use_layer, $layout_allowed ); foreach my $t ( get_layout_themes( $src, $layid ) ) { # themes should be shown but disabled if you can't use the layout unless ( defined $layout_allowed ) { if ( defined $u && $t->{b2layer} && $t->{b2layer}->{uniq} ) { $layout_allowed = LJ::S2::can_use_layer( $u, $t->{b2layer}->{uniq} ); } else { # if no parent layer information, or no uniq (user style?), # then just assume it's allowed $layout_allowed = 1; } } $text = $t->{name}; $can_use_layer = $layout_allowed && ( !defined $u || LJ::S2::can_use_layer( $u, $t->{uniq} ) ) ; # if no u, accept theme; else check policy $text = "$text*" unless $can_use_layer; if ( $last_uid && $t->{userid} != $last_uid ) { push @sel, 0, '---'; # divider between system & user } $last_uid = $t->{userid}; # these are passed to LJ::html_select which can take hashrefs push @sel, { value => $t->{s2lid}, text => $text, disabled => !$can_use_layer, }; } 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/styles/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 $u->can_create_s2_styles; return 0 unless $uniq; return 1 if LJ::Hooks::run_hook( 's2_can_use_layer', { u => $u, uniq => $uniq, } ); my $pol = get_policy(); my $can = 0; my @try = ( $uniq =~ m!/layout$! ) ? ( '*', $uniq ) : # this is a layout ( '*/themes', $uniq ); # this is probably a theme foreach (@try) { 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 $u->can_create_s2_styles; return 1 if $u->can_create_s2_props; 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'}; return {} unless LJ::isu($u); my $counts = {}; my $remote = LJ::get_remote(); my $days = $u->get_daycounts($remote) or return {}; foreach my $day (@$days) { $counts->{ $day->[0] }->{ $day->[1] }->{ $day->[2] } = $day->[3]; } return $s2page->{'_day_counts'} = $counts; } sub use_journalstyle_entry_page { my ( $u, $ctx ) = @_; return 0 if !$u || $u->is_syndicated; # see sitefeeds/layout.s2 my $userprop = $u->prop('use_journalstyle_entry_page'); my $reparse_userprop = sub { # We can't use a regular boolean for this, because "false" # userprops are deleted, and it would always fall back to # the style's previous setting in the negative case. # So let's store this as 'Y' or 'N' and then reparse # it to return the expected boolean value. my $val = $userprop; return 1 if $val && $val eq 'Y'; return 0 if $val && $val eq 'N'; return undef; # unexpected or undefined value }; my $reparsed; $reparsed = $reparse_userprop->() if defined $userprop; return $reparsed if defined $reparsed; # if the userprop isn't defined, or we got an unexpected value # check the current style for the legacy S2 prop, and then # set the userprop going forward $ctx ||= LJ::S2::s2_context( $u->{s2_style} ) or return undef; my $ctxval = $ctx->[S2::PROPS]->{use_journalstyle_entry_page}; $userprop = $ctxval ? 'Y' : 'N'; $u->set_prop( 'use_journalstyle_entry_page', $userprop ); return $reparse_userprop->(); } sub tracking_popup_js { return LJ::is_enabled('esn_ajax') ? ( { group => 'all' }, qw( js/jquery/jquery.ui.core.js js/jquery/jquery.ui.widget.js js/jquery/jquery.ui.tooltip.js js/jquery.ajaxtip.js js/jquery/jquery.ui.position.js stc/jquery/jquery.ui.core.css stc/jquery/jquery.ui.tooltip.css js/jquery.esn.js ) ) : (); } sub use_journalstyle_icons_page { my ( $u, $ctx ) = @_; return 0 if !$u || $u->is_syndicated; # see sitefeeds/layout.s2 return 0 unless exists $ctx->[S2::CLASSES]->{IconsPage}; # core1 doesn't support IconsPage return $u->prop('use_journalstyle_icons_page') ? 1 : 0; } ## 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_tz { # timezone can be scalar timezone name, DateTime::TimeZone object, or LJ::User object my ( $epoch, $timezone ) = @_; return undef unless $timezone; if ( ref $timezone eq "LJ::User" ) { $timezone = $timezone->prop("timezone"); return undef unless $timezone; } my $dt = eval { DateTime->from_epoch( epoch => $epoch, time_zone => $timezone, ); }; return undef unless $dt; my $ret = { '_type' => 'DateTime' }; $ret->{'year'} = $dt->year; $ret->{'month'} = $dt->month; $ret->{'day'} = $dt->day; $ret->{'hour'} = $dt->hour; $ret->{'min'} = $dt->minute; $ret->{'sec'} = $dt->second; # DateTime.pm's dayofweek is 1-based/Mon-Sun, but S2's is 1-based/Sun-Sat, # so first we make DT's be 0-based/Sun-Sat, then shift it up to 1-based. $ret->{'_dayofweek'} = ( $dt->day_of_week % 7 ) + 1; return $ret; } sub DateTime_parts { my $datestr = defined $_[0] ? $_[0] : ''; my @parts = split /\s+/, $datestr; my $dt = { '_type' => 'DateTime' }; $dt->{year} = defined $parts[0] ? $parts[0] + 0 : 0; $dt->{month} = defined $parts[1] ? $parts[1] + 0 : 0; $dt->{day} = defined $parts[2] ? $parts[2] + 0 : 0; $dt->{hour} = defined $parts[3] ? $parts[3] + 0 : 0; $dt->{min} = defined $parts[4] ? $parts[4] + 0 : 0; $dt->{sec} = defined $parts[5] ? $parts[5] + 0 : 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 ( $u, $kwid, $kw ) = @_; return undef unless $u && $kwid && $kw; my $url = LJ::Tags::tag_url( $u, $kw ); my $t = { _type => 'Tag', _id => $kwid, name => LJ::ehtml($kw), url => $url, }; return $t; } sub TagDetail { my ( $u, $kwid, $tag ) = @_; return undef unless $u && $kwid && ref $tag eq 'HASH'; my $t = { _type => 'TagDetail', _id => $kwid, name => LJ::ehtml( $tag->{name} ), url => LJ::Tags::tag_url( $u, $tag->{name} ), visibility => $tag->{security_level}, }; # Work out how many uses of the tag the current remote (if any) # should be able to see. This is easy for public & protected # entries, but gets tricky with group filters because a post can # be visible to >1 of them. Instead of working it out accurately # every time, we give an approximation that will either be accurate # or an underestimate. my $count = 0; my $remote = LJ::get_remote(); if ( defined $remote && $remote->can_manage($u) ) { #own journal $count = $tag->{uses}; my $groupcount = $tag->{uses}; foreach (qw(public private protected)) { $t->{security_counts}->{$_} = $tag->{security}->{$_}; $groupcount -= $tag->{security}->{$_}; } $t->{security_counts}->{group} = $groupcount; } elsif ( defined $remote ) { #logged in, not own journal my $trusted = $u->trusts_or_has_member($remote); my $grpmask = $u->trustmask($remote); $count = $tag->{security}->{public}; $t->{security_counts}->{public} = $tag->{security}->{public}; if ($trusted) { $count += $tag->{security}->{protected}; $t->{security_counts}->{protected} = $tag->{security}->{protected}; } if ( $grpmask > 1 ) { # Find the greatest number of uses of this tag in any one group # that this remote is a member of, and add that number to the count my $maxgroupsize = 0; foreach ( LJ::bit_breakdown($grpmask) ) { $maxgroupsize = $tag->{security}->{groups}->{$_} if $tag->{security}->{groups}->{$_} && $tag->{security}->{groups}->{$_} > $maxgroupsize; } $count += $maxgroupsize; } } else { #logged out. $count = $tag->{security}->{public}; $t->{security_counts}->{public} = $tag->{security}->{public}; } $t->{use_count} = $count; return $t; } sub TagList { my ( $tags, $u, $jitemid, $opts, $taglist ) = @_; while ( my ( $kwid, $keyword ) = each %{ $tags || {} } ) { push @$taglist, Tag( $u, $kwid => $keyword ); } LJ::Hooks::run_hooks( 'augment_s2_tag_list', u => $u, jitemid => $jitemid, tag_list => $taglist ); @$taglist = sort { $a->{name} cmp $b->{name} } @$taglist; return "" if $opts->{no_entry_body}; return "" unless $opts->{enable_tags_compatibility} && @$taglist; return LJ::S2::get_tags_text( $opts->{ctx}, $taglist ); } sub Entry { my ( $u, $arg ) = @_; my $e = { '_type' => 'Entry', 'link_keyseq' => [ 'edit_entry', 'edit_tags' ], 'metadata' => {}, }; foreach ( qw( subject text journal poster new_day end_day comments userpic permalink_url itemid tags timeformat24 admin_post dom_id ) ) { $e->{$_} = $arg->{$_}; } my $pic = $e->{userpic}; if ( $pic->{url} ) { my $userpic_style = $arg->{userpic_style} || ""; if ( $userpic_style eq 'small' ) { $pic->{width} = $pic->{width} * 3 / 4; $pic->{height} = $pic->{height} * 3 / 4; } elsif ( $userpic_style eq "smaller" ) { $pic->{width} = $pic->{width} / 2; $pic->{height} = $pic->{height} / 2; } } my $remote = LJ::get_remote(); my $poster = $e->{poster}->{_u}; $e->{'tags'} ||= []; $e->{'time'} = DateTime_parts( $arg->{'dateparts'} ); $e->{'system_time'} = DateTime_parts( $arg->{'system_dateparts'} ); $e->{'depth'} = 0; # Entries are always depth 0. Comments are 1+. my $link_keyseq = $e->{'link_keyseq'}; push @$link_keyseq, 'mem_add' if LJ::is_enabled('memories'); push @$link_keyseq, 'tell_friend' if LJ::is_enabled('tellafriend'); push @$link_keyseq, 'watch_comments' if LJ::is_enabled('esn'); push @$link_keyseq, 'unwatch_comments' if LJ::is_enabled('esn'); # 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" ) { if ( $arg->{'allowmask'} == 0 ) { # custom security with no group -- essentially private $e->{'security'} = "private"; $e->{'security_icon'} = Image_std("security-private"); } elsif ( $arg->{'allowmask'} > 1 && $poster && $poster->equals($remote) ) { # custom group -- only show to journal owner $e->{'security'} = "custom"; $e->{'security_icon'} = Image_std("security-groups"); } else { # friends only or custom group showing to non journal owner $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"); } $e->{adult_content_level} = ""; if ( $arg->{adult_content_level} eq "explicit" ) { $e->{adult_content_level} = "18"; $e->{adult_content_icon} = Image_std("adult-18"); } elsif ( $arg->{adult_content_level} eq "concepts" ) { $e->{adult_content_level} = "NSFW"; $e->{adult_content_icon} = Image_std("adult-nsfw"); } else { # do nothing. } my $m_arg = $arg; # if moodthemeid not given, look up the user's if we have it $m_arg = $u if !defined $arg->{moodthemeid} && LJ::isu($u); my $p = $arg->{props}; my $img_arg; my %current = LJ::currents( $p, $m_arg, { s2imgref => \$img_arg } ); $e->{metadata}->{ lc $_ } = $current{$_} foreach keys %current; $e->{mood_icon} = Image(@$img_arg) if defined $img_arg; my $apache_r = BML::get_request(); # custom friend groups my $group_names = $arg->{group_names}; unless ($group_names) { my $entry = LJ::Entry->new( $e->{journal}->{_u}, ditemid => $e->{itemid} ); $group_names = $entry->group_names; } $e->{metadata}->{groups} = $group_names if $group_names; # TODO: Populate this field more intelligently later, but for now this will # hopefully disuade people from hardcoding logic like this into their S2 # layers when they do weird parsing/manipulation of the text member in # untrusted layers. $e->{text_must_print_trusted} = 1 if $e->{text} =~ m!<(script|object|applet|embed|iframe)\b!i; return $e; } #returns an S2 Entry from a user object and an entry object sub Entry_from_entryobj { my ( $u, $entry_obj, $opts ) = @_; my $remote = LJ::get_remote(); my $get = $opts->{getargs}; my $no_entry_body = $opts->{no_entry_body}; my $anum = $entry_obj->anum; my $jitemid = $entry_obj->jitemid; my $ditemid = $entry_obj->ditemid; # $journal: journal posted to my $journalid = $entry_obj->journalid; my $journal = LJ::load_userid($journalid); # is style=mine used? or if remote has it on and this entry is not part of # their journal. if either are yes, it needs to be added to comment links my %opt_stylemine = $remote && $remote->prop('opt_stylemine') && $remote->id != $journalid ? ( style => 'mine' ) : (); my $style_args = LJ::viewing_style_args( %$get, %opt_stylemine ); # Configuration for cleaning the entry text: cuts and such my $cut_disable = $opts->{cut_disable}; my $cleanhtml_opts = { cuturl => $entry_obj->url( style_opts => LJ::viewing_style_opts( %$get, %opt_stylemine ) ), ljcut_disable => $cut_disable, }; # reading pages might need to display image placeholders my $cleanhtml_extra = $opts->{cleanhtml_extra} || {}; foreach my $k ( keys %$cleanhtml_extra ) { $cleanhtml_opts->{$k} = $cleanhtml_extra->{$k}; } #load and prepare subject and text of entry my $subject = LJ::CleanHTML::quote_html( $entry_obj->subject_html, $get->{nohtml} ); my $text = $no_entry_body ? "" : LJ::CleanHTML::quote_html( $entry_obj->event_html($cleanhtml_opts), $get->{nohtml} ); unless ($no_entry_body) { LJ::expand_embedded( $journal, $jitemid, $remote, \$text ); $text = DW::Logic::AdultContent->transform_post( post => $text, journal => $journal, remote => $remote, entry => $entry_obj ); } # journal: posted to; poster: posted by my $posterid = $entry_obj->posterid; my $userlite_journal = UserLite($journal); my $poster = $journal; # except for communities, posterid and journalid should match, only load separate UserLite object if that is not the case my $userlite_poster = $userlite_journal; unless ( $posterid == $journalid ) { $poster = LJ::load_userid($posterid); $userlite_poster = UserLite($poster); } # loading S2 Userpic my $userpic; my $userpic_position = S2::get_property_value( $opts->{ctx}, 'userpics_position' ) || ""; my $userpic_style; unless ( $userpic_position eq "none" ) { # if the post was made in a community, use either the userpic it was posted with or the community pic depending on the style setting if ( $posterid == $journalid || !S2::get_property_value( $opts->{ctx}, 'use_shared_pic' ) ) { my ( $pic, $kw ) = $entry_obj->userpic; $userpic = Image_userpic( $poster, $pic->picid, $kw ) if $pic; } else { $userpic = Image_userpic( $journal, $journal->userpic->picid ) if $journal->userpic; } $userpic_style = S2::get_property_value( $opts->{ctx}, 'entry_userpic_style' ); } # override used moodtheme if necessary my $moodthemeid = $u->prop('opt_forcemoodtheme') eq 'Y' ? $u->moodtheme : $poster->moodtheme; # tags loading and sorting my $tags = LJ::Tags::get_logtags( $journal, $jitemid ); my $taglist = []; $text .= TagList( $tags->{$jitemid}, $journal, $jitemid, $opts, $taglist ); my $tagnav; # building the CommentInfo and Entry objects my $comments = CommentInfo( $entry_obj->comment_info( u => $u, remote => $remote, style_args => $style_args, journal => $journal ) ); my $entry = Entry( $u, { subject => $subject, text => $text, dateparts => LJ::alldatepart_s2( $entry_obj->{eventtime} ), system_dateparts => LJ::alldatepart_s2( $entry_obj->{logtime} ), security => $entry_obj->security, adult_content_level => $entry_obj->adult_content_calculated || $journal->adult_content_calculated, allowmask => $entry_obj->allowmask, props => $entry_obj->props, itemid => $ditemid, journal => $userlite_journal, poster => $userlite_poster, comments => $comments, new_day => 0, #if true, set later end_day => 0, #if true, set later userpic => $userpic, userpic_style => $userpic_style, tags => $taglist, tagnav => $tagnav, permalink_url => $entry_obj->url, moodthemeid => $moodthemeid, timeformat24 => $remote && $remote->use_24hour_time, admin_post => $entry_obj->admin_post, dom_id => "entry-" . $journal->user . "-$ditemid", } ); return $entry; } 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; } my $layoutname; my $themename; my $layouturl; $layouturl = ""; if ($styleid) { my $style = load_style($styleid); my $theme; if ( $style && $style->{layer}->{theme} ) { $theme = LJ::S2Theme->new( themeid => $style->{layer}->{theme}, user => $opts->{style_u} || $u, undef_if_missing => 1 ); } if ($theme) { $layoutname = $theme->layout_name; $themename = $theme->name; $layouturl = "$LJ::SITEROOT/customize/?layoutid=" . $theme->layoutid if $theme->is_system_layout; } else { $layoutname = S2::get_layer_info( $style->{layer}->{layout}, 'name' ); $themename = LJ::Lang::ml("s2theme.themename.notheme"); } } # get MAX(modtime of style layers) my $stylemodtime = S2::get_style_modtime( $opts->{'ctx'} ); if ($styleid) { my $style = load_style($styleid); $stylemodtime = $style->{'modtime'} if $style->{'modtime'} > $stylemodtime; } my $linkobj = LJ::Links::load_linkobj($u); my $linklist = [ map { UserLink($_) } @$linkobj ]; my $remote = LJ::get_remote(); my $tz_remote; if ($remote) { my $tz = $remote->prop("timezone"); $tz_remote = $tz ? eval { DateTime::TimeZone->new( name => $tz ); } : undef; } unless ( $u->prop('customtext_content') ) { $u->set_prop( 'customtext_content', $opts->{ctx}->[S2::PROPS]->{text_module_customtext_content} ); } unless ( $u->prop('customtext_url') ) { $u->set_prop( 'customtext_url', $opts->{ctx}->[S2::PROPS]->{text_module_customtext_url} ); } if ( !defined $u->prop('customtext_title') || $u->prop('customtext_title') eq '' || $u->prop('customtext_title') eq "Custom Text" ) { $u->set_prop( 'customtext_title', $opts->{ctx}->[S2::PROPS]->{text_module_customtext} ); } my $r = DW::Request->get; my $p = { '_type' => 'Page', '_u' => $u, 'view' => '', 'args' => \%args, 'journal' => User($u), 'journal_type' => $u->{'journaltype'}, 'layout_name' => $layoutname, 'theme_name' => $themename, 'layout_url' => $layouturl, 'time' => DateTime_unix(time), 'local_time' => $tz_remote ? DateTime_tz( time, $tz_remote ) : DateTime_unix(time), 'base_url' => $base_url, 'stylesheet_url' => "$base_url/res/$styleid/stylesheet?$stylemodtime", 'view_url' => { recent => LJ::create_url( "/", viewing_style => 1 ), userinfo => $u->profile_url, archive => LJ::create_url( "/archive", viewing_style => 1 ), read => LJ::create_url( "/read", viewing_style => 1 ), network => LJ::create_url( "/network", viewing_style => 1 ), tags => LJ::create_url( "/tag/", viewing_style => 1 ), memories => "$LJ::SITEROOT/tools/memories?user=$u->{user}", }, 'linklist' => $linklist, 'customtext_title' => escape_prop_value_ret( $u->prop('customtext_title'), 'plain' ), 'customtext_url' => escape_prop_value_ret( $u->prop('customtext_url'), 'plain' ), 'customtext_content' => escape_prop_value_ret( $u->prop('customtext_content'), 'html' ), 'views_order' => [ 'recent', 'archive', 'read', 'tags', 'memories', 'userinfo' ], 'global_title' => LJ::ehtml( $u->{'journaltitle'} || $u->{'name'} ), 'global_subtitle' => LJ::ehtml( $u->{'journalsubtitle'} ), 'show_control_strip' => LJ::Hooks::run_hook('show_control_strip'), 'head_content' => '', 'is_canary' => $LJ::IS_CANARY, 'data_link' => {}, 'data_links_order' => [], _styleopts => LJ::viewing_style_opts(%$get), timeformat24 => $remote && $remote->use_24hour_time, include_meta_viewport => $r->cookie('no_mobile') ? 0 : 1, session_msgs => $r->msgs }; $r->clear_msgs; if ( $opts && $opts->{'saycharset'} ) { $p->{'head_content'} .= '\n"; } if ( LJ::Hooks::are_hooks('s2_head_content_extra') ) { $p->{head_content} .= LJ::Hooks::run_hook( 's2_head_content_extra', $remote, $opts->{r} ); } my %meta_opts = $opts ? ( feeds => $opts->{addfeeds}, tags => $opts->{tags}, openid => $opts->{addopenid}, ) : (); $meta_opts{remote} = $remote; $p->{head_content} .= $u->meta_discovery_links(%meta_opts); # other useful link rels $p->{head_content} .= qq{\n}; $p->{head_content} .= qq{\n} if $LJ::APPLE_TOUCH_ICON; $p->{head_content} .= qq{\n} if $LJ::FACEBOOK_PREVIEW_ICON; $p->{head_content} .= qq{\n}; $p->{head_content} .= qq{\n}; # Identity (type I) accounts only have read views $p->{views_order} = [ 'read', 'userinfo' ] if $u->is_identity; # feed accounts only have recent entries views $p->{views_order} = [ 'recent', 'archive', 'userinfo' ] if $u->is_syndicated; $p->{views_order} = [ 'recent', 'archive', 'read', 'network', 'tags', 'memories', 'userinfo' ] if $u->can_use_network_page; $p->{has_activeentries} = 0; # don't need to load active entries if the user does not have the cap to display them if ( $u->can_use_active_entries ) { my @active = $u->active_entries; # array to hold the Entry objects my @activeentries; foreach my $itemid (@active) { my $entry_obj = LJ::Entry->new( $u, jitemid => $itemid ); # copy over $opts so that we don't inadvertently affect other things my $activeentry_opts = { %{ $opts || {} }, no_entry_body => 1 }; # only show the entries $remote has the permission to view if ( $entry_obj->visible_to($remote) ) { my $activeentry = Entry_from_entryobj( $u, $entry_obj, $activeentry_opts ); push @{ $p->{activeentries} }, $activeentry; # if at least one is accessible to $remote , show active entries module on journal page $p->{has_activeentries} = 1; } } } return $p; } sub Link { my ( $url, $caption, $icon, %extra ) = @_; my $lnk = { '_type' => 'Link', 'caption' => $caption, 'url' => $url, 'icon' => $icon, 'extra' => {%extra}, }; return $lnk; } sub Image { my ( $url, $w, $h, $alttext, %extra ) = @_; return { '_type' => 'Image', 'url' => $url, 'width' => $w, 'height' => $h, 'alttext' => $alttext, 'extra' => {%extra}, }; } sub Image_std { my $name = shift; my $ctx = $LJ::S2::CURR_CTX or die "No S2 context available "; my $imgprefix = $LJ::IMGPREFIX; $imgprefix =~ s/^https?://; unless ( $LJ::S2::RES_MADE++ ) { $LJ::S2::RES_CACHE = {}; my $textmap = { 'security-protected' => 'text_icon_alt_protected', 'security-private' => 'text_icon_alt_private', 'security-groups' => 'text_icon_alt_groups', 'adult-nsfw' => 'text_icon_alt_nsfw', 'adult-18' => 'text_icon_alt_18', 'sticky-entry' => 'text_icon_alt_sticky_entry', 'admin-post' => 'text_icon_alt_admin_post', }; foreach ( keys %$textmap ) { my $i = $LJ::Img::img{$_}; $LJ::S2::RES_CACHE->{$_} = Image( "$imgprefix$i->{src}", $i->{width}, $i->{height}, $ctx->[S2::PROPS]->{ $textmap->{$_} } ); } # additional icons from LJ::Img # with alt text from translation system my @ic = qw( btn_del btn_freeze btn_unfreeze btn_scr btn_unscr editcomment editentry edittags tellfriend memadd prev_entry next_entry track untrack atom rss ); foreach (@ic) { my $i = $LJ::Img::img{$_}; $LJ::S2::RES_CACHE->{$_} = Image( "$imgprefix$i->{src}", $i->{width}, $i->{height}, LJ::Lang::ml( $i->{alt} ) ); } } return $LJ::S2::RES_CACHE->{$name}; } sub Image_userpic { my ( $u, $picid, $kw, $width, $height ) = @_; $picid ||= $u->get_picid_from_keyword($kw) if LJ::isu($u); return Null("Image") unless $picid; # get the Userpic object my $p = LJ::Userpic->new( $u, $picid ); # load the dimensions, unless they have been passed in explicitly $width ||= $p->width; $height ||= $p->height; my $alttext = $p->alttext($kw); my $title = $p->titletext($kw); return { '_type' => "Image", 'url' => "$LJ::USERPIC_ROOT/$picid/$u->{'userid'}", 'width' => $width, 'height' => $height, 'alttext' => $alttext, 'extra' => { title => $title }, }; } 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_all'} = $opts->{'url_all'} unless $ir->{'all_subitems_displayed'}; $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 CommentNav { my $h = shift; $h->{'_type'} = "CommentNav"; return $h; } sub User { my ($u) = @_; my $o = UserLite($u); $o->{'_type'} = "User"; $o->{'default_pic'} = Image_userpic( $u, $u->{'defaultpicid'} ); $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'} ), 'hover' => LJ::ehtml( $link->{'hover'} ), 'children' => $link->{'children'} || [], # TODO: implement parent-child relationships }; } sub UserLite { my ($u) = @_; my $o; return $o unless $u; $o = { '_type' => 'UserLite', '_u' => $u, 'user' => LJ::ehtml( $u->user ), 'username' => LJ::ehtml( $u->display_name ), 'name' => LJ::ehtml( $u->{'name'} ), 'journal_type' => $u->{'journaltype'}, 'userpic_listing_url' => $u->allpics_base, 'link_keyseq' => [], }; my $lks = $o->{link_keyseq}; push @$lks, qw(manage_membership trust watch post_entry track message); push @$lks, 'tell_friend' if LJ::is_enabled('tellafriend'); # TODO: Figure out some way to use the userinfo_linkele hook here? return $o; } # Given an S2 Entry object, return if it's the first, second, third, etc. entry that we've seen sub nth_entry_seen { my $e = shift; my $key = "$e->{'journal'}->{'username'}-$e->{'itemid'}"; my $ref = $LJ::REQ_GLOBAL{'nth_entry_keys'}; if ( exists $ref->{$key} ) { return $ref->{$key}; } return $LJ::REQ_GLOBAL{'nth_entry_keys'}->{$key} = ++$LJ::REQ_GLOBAL{'nth_entry_ct'}; } sub sitescheme_secs_to_iso { my ( $secs, $opts ) = @_; my $remote = LJ::get_remote(); my @ret; # time format (12/24 hr) my $fmt_time = "%%hh%%:%%min%% %%a%%m"; # 12-hr default $fmt_time = "%%HH%%:%%min%%" if $remote && $remote->use_24hour_time; # convert date to S2 object my $s2_ctx = []; # fake S2 context object my $s2_datetime; my $has_tz = ''; # don't display timezone unless requested below # if opts has a true tz key, get the remote user's timezone if possible if ( $opts->{tz} ) { $s2_datetime = DateTime_tz( $secs, $remote ); $has_tz = defined $s2_datetime ? "(local)" : "UTC"; } # if timezone execution failed, use GMT $s2_datetime = DateTime_unix($secs) unless defined $s2_datetime; my @s2_args = ( $s2_ctx, $s2_datetime ); # reformat date and time display for user push @ret, S2::Builtin::LJ::Date__date_format( @s2_args, "iso" ); push @ret, S2::Builtin::LJ::DateTime__time_format( @s2_args, $fmt_time ); push @ret, $has_tz; return join ' ', @ret; } # adectomy sub current_box_type { } sub curr_page_supports_ebox { 0 } # Convenience method since it gets checked multiple times sub has_quickreply { my ($page) = @_; return 0 if $page->{_type} eq 'EntryPreviewPage'; my $view = $page->{view}; # Also needs adding to the list in core2.s2 return $view eq 'entry' || $view eq 'read' || $view eq 'day' || $view eq 'recent' || $view eq 'network'; } ############### package S2::Builtin::LJ; use strict; sub UserLite { my ( $ctx, $user ) = @_; my $u = LJ::load_user($user); return LJ::S2::UserLite($u); } sub start_css { my ($ctx) = @_; my $sc = $ctx->[S2::SCRATCH]; # Always increment, but only continue if it was 0 return if $sc->{_css_depth}++; $sc->{_start_css_pout} = S2::get_output(); $sc->{_start_css_pout_s} = S2::get_output_safe(); $sc->{_start_css_buffer} = ""; my $printer = sub { my $arg = shift; $sc->{_start_css_buffer} .= $arg if defined $arg; }; S2::set_output($printer); S2::set_output_safe($printer); } sub end_css { my ($ctx) = @_; my $sc = $ctx->[S2::SCRATCH]; # Only decrement _css_depth if it is non-zero, only continue if it becomes zero return unless $sc->{_css_depth} && ( --$sc->{_css_depth} == 0 ); # restore our printer/safe printer S2::set_output( $sc->{_start_css_pout} ); S2::set_output_safe( $sc->{_start_css_pout_s} ); # our CSS to clean: my $css = $sc->{_start_css_buffer}; my $cleaner = LJ::CSS::Cleaner->new; my $clean = $cleaner->clean($css); LJ::Hooks::run_hook( 'css_cleaner_transform', \$clean ); $sc->{_start_css_pout}->( "/* Cleaned CSS: */\n" . $clean . "\n" ); } sub alternate { my ( $ctx, $one, $two ) = @_; my $scratch = $ctx->[S2::SCRATCH]; $scratch->{alternate}{"$one\0$two"} = !$scratch->{alternate}{"$one\0$two"}; return $scratch->{alternate}{"$one\0$two"} ? $one : $two; } sub clean_css_classname { my ( $ctx, $classname ) = @_; my $clean_classname; if ( $classname =~ /eval/ ) { $clean_classname = $classname . " "; $classname =~ s/eval/ev-l/g; $clean_classname .= $classname; } else { $clean_classname = $classname; } return $clean_classname; } sub get_image { return LJ::S2::Image_std( $_[1] ); } sub set_content_type { my ( $ctx, $type ) = @_; die "set_content_type is not yet implemented"; $ctx->[S2::SCRATCH]->{contenttype} = $type; } sub striphtml { my ( $ctx, $s ) = @_; $s =~ s/<.*?>//g; return $s; } 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 ) = @_; $n = 0 unless defined $n; 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 $user; # now get data from one of two paths, depending on if we were given a UserLite # object or a string for the username, so make sure we have the username. if ( ref $obj eq 'HASH' ) { $user = $obj->{user}; } else { $user = $obj; } my $u = LJ::load_user($user); return "" unless $u; # construct URL to return $view = "profile" if $view eq "userinfo"; $view = "" if $view eq "recent"; my $base = $u->journal_base; return "$base/$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( CORE::rand( $high - $low + 1 ) ) + $low; } sub pageview_unique_string { my ($ctx) = @_; return LJ::pageview_unique_string(); } sub viewer_logged_in { my $remote = LJ::get_remote(); return defined $remote; } sub viewer_is_owner { my $remote = LJ::get_remote(); return 0 unless $remote; return 0 unless defined($LJ::S2::CURR_PAGE); return $remote->equals( $LJ::S2::CURR_PAGE->{_u} ); } # NOTE: this method is old and deprecated, but we still support it for people # who are importing styles from old sites. since we don't know if the style # is asking if the viewer is "watched" or if they're "trusted", we default to # returning true if they're trusted. since we believe that the majority of # trust relationships also include a watch relationship, this should be the # right behavior in 90%+ of cases. in the few that it is not, we humbly # suggest that people update their styles to use the DW core/functions. sub viewer_is_friend { return viewer_has_access(); } sub viewer_has_access { my $remote = LJ::get_remote(); return 0 unless $remote; return 0 unless defined($LJ::S2::CURR_PAGE); my $ju = $LJ::S2::CURR_PAGE->{_u}; return viewer_is_member() if $ju->is_community; return $ju->trusts($remote); } sub viewer_is_subscribed { my $remote = LJ::get_remote(); return 0 unless $remote; return 0 unless defined $LJ::S2::CURR_PAGE; my $ju = $LJ::S2::CURR_PAGE->{_u}; return $remote->watches($ju); } sub viewer_is_member { my $remote = LJ::get_remote(); return 0 unless $remote; return 0 unless defined($LJ::S2::CURR_PAGE); my $ju = $LJ::S2::CURR_PAGE->{_u}; return 0 unless $ju->is_community; return $remote->member_of($ju); } sub viewer_is_admin { my $remote = LJ::get_remote(); return 0 unless $remote; return 0 unless defined $LJ::S2::CURR_PAGE; my $ju = $LJ::S2::CURR_PAGE->{_u}; return 0 unless $ju->is_community; return $remote->can_manage($ju); } sub viewer_is_moderator { my $remote = LJ::get_remote(); return 0 unless $remote; return 0 unless defined $LJ::S2::CURR_PAGE; my $ju = $LJ::S2::CURR_PAGE->{_u}; return 0 unless $ju->is_community; return $remote->can_moderate($ju); } sub viewer_can_manage_tags { return 0 unless defined $LJ::S2::CURR_PAGE; my $ju = $LJ::S2::CURR_PAGE->{_u}; # use the same function as that used in /manage/tags return LJ::get_authas_user( $ju->user ) ? 1 : 0; } sub viewer_sees_control_strip { my $apache_r = BML::get_request(); return LJ::Hooks::run_hook('show_control_strip'); } # Returns true if the viewer can search this person's journal sub viewer_can_search { my $remote = LJ::get_remote(); return 0 unless $remote; return 0 unless defined $LJ::S2::CURR_PAGE; my $ju = $LJ::S2::CURR_PAGE->{_u}; # return based on this function return $ju->allow_search_by($remote); } # Returns a search form for this journal sub print_search_form { return "" unless defined($LJ::S2::CURR_PAGE); my $ju = $LJ::S2::CURR_PAGE->{_u}; my $search_form = '

'; $search_form .= '
'; $search_form .= LJ::form_auth(); $search_form .= ''; if ( $ju->allow_comments_indexed ) { $search_form .= ''; $search_form .= ''; } $search_form .= ''; $search_form .= '
'; S2::pout($search_form); } # maintained only for compatibility with core1, eventually these can be removed # when we've upgraded everybody. or we keep this cruft until the cows come home # as a stolid reminder to our past. sub viewer_sees_vbox { 0 } sub viewer_sees_hbox_top { 0 } sub viewer_sees_hbox_bottom { 0 } sub viewer_sees_ad_box { 0 } sub viewer_sees_ebox { 0 } sub viewer_sees_ads { 0 } sub _get_Entry_ebox_args { 0 } sub Entry__viewer_sees_ebox { 0 } sub control_strip_logged_out_userpic_css { my $apache_r = BML::get_request(); my $u = LJ::load_userid( $apache_r->notes->{journalid} ); return '' unless $u; return LJ::Hooks::run_hook( 'control_strip_userpic', $u ); } sub control_strip_logged_out_full_userpic_css { my $apache_r = BML::get_request(); my $u = LJ::load_userid( $apache_r->notes->{journalid} ); return '' unless $u; return LJ::Hooks::run_hook( 'control_strip_loggedout_userpic', $u ); } sub weekdays { my ($ctx) = @_; return S2::get_property_value( $ctx, 'reg_firstdayofweek' ) eq "monday" ? [ 2 .. 7, 1 ] : [ 1 .. 7 ]; } sub journal_current_datetime { my ($ctx) = @_; my $ret = { '_type' => 'DateTime' }; my $apache_r = BML::get_request(); my $u = LJ::load_userid( $apache_r->notes->{journalid} ); return $ret unless $u; # turn the timezone offset number into a four character string (plus '-' if negative) # e.g. -1000, 0700, 0430 my $timezone = $u->timezone; my $partial_hour = "00"; if ( $timezone =~ /(\.\d+)/ ) { $partial_hour = $1 * 60; } my $neg = $timezone =~ /-/ ? 1 : 0; my $hour = sprintf( "%02d", abs( int($timezone) ) ); # two character hour $hour = $neg ? "-$hour" : "$hour"; $timezone = $hour . $partial_hour; my $now = DateTime->now( time_zone => $timezone ); $ret->{year} = $now->year; $ret->{month} = $now->month; $ret->{day} = $now->day; $ret->{hour} = $now->hour; $ret->{min} = $now->minute; $ret->{sec} = $now->second; # DateTime.pm's dayofweek is 1-based/Mon-Sun, but S2's is 1-based/Sun-Sat, # so first we make DT's be 0-based/Sun-Sat, then shift it up to 1-based. $ret->{_dayofweek} = ( $now->day_of_week % 7 ) + 1; return $ret; } sub SubscriptionFilter { my ( $name, $sortorder, $public, $url ) = @_; return { '_type' => 'SubscriptionFilter', 'name' => $name, 'public' => $public ? 1 : 0, 'sortorder' => $sortorder, 'url' => $url, }; } sub journal_subscription_filters { my ($ctx) = @_; # only owners can see non-public filters my $public_only = not viewer_is_owner(); my @ret; # automatically gets the content filters in the right order my @filters = $LJ::S2::CURR_PAGE->{_u}->content_filters; foreach my $filter (@filters) { my $filterurl = $LJ::S2::CURR_PAGE->{_u}->journal_base() . "/read/$filter->{name}"; my $subfilter = SubscriptionFilter( $filter->name, $filter->sortorder, $filter->public, $filterurl ); push @ret, $subfilter if ( $filter->public || !$public_only ); } return \@ret; } sub style_is_active { my ($ctx) = @_; my $layoutid = $ctx->[S2::LAYERLIST]->[1]; my $themeid = $ctx->[S2::LAYERLIST]->[2]; my $pub = LJ::S2::get_public_layers(); my $layout_is_active = LJ::Hooks::run_hook( "layer_is_active", $pub->{$layoutid}->{uniq} ); return 0 unless !defined $layout_is_active || $layout_is_active; if ( defined $themeid ) { my $theme_is_active = LJ::Hooks::run_hook( "layer_is_active", $pub->{$themeid}->{uniq} ); return 0 unless !defined $theme_is_active || $theme_is_active; } return 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 int__compare { my ( $ctx, $this, $other ) = @_; return $other <=> $this; } 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 { '_type' => 'Color', as_string => "" } if $s eq ""; 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 ( $ctx, $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 ) = @_; _Color__update_hsl($this) unless $this->{_hslset}; if ( defined $h ) { $this->{_h} = $h % 256; _Color__update_rgb($this); } $this->{_h}; } sub Color__saturation { my ( $ctx, $this, $s ) = @_; _Color__update_hsl($this) unless $this->{_hslset}; if ( defined $s ) { $this->{_s} = $s % 256; _Color__update_rgb($this); } $this->{_s}; } sub Color__lightness { my ( $ctx, $this, $l ) = @_; _Color__update_hsl($this) unless $this->{_hslset}; if ( defined $l ) { $this->{_l} = $l % 256; _Color__update_rgb($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__blend { my ( $ctx, $this, $other, $value ) = @_; my $multiplier = $value / 100; my $new = { '_type' => 'Color', 'r' => int( $this->{'r'} - ( ( $this->{'r'} - $other->{'r'} ) * $multiplier ) + .5 ), 'g' => int( $this->{'g'} - ( ( $this->{'g'} - $other->{'g'} ) * $multiplier ) + .5 ), 'b' => int( $this->{'b'} - ( ( $this->{'b'} - $other->{'b'} ) * $multiplier ) + .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 ) = @_; my $page = get_page(); my $u = $page->{'_u'}; my $post_user = $page->{'entry'} ? $page->{'entry'}->{'poster'}->{'user'} : undef; my $com_user = $this->{'poster'} ? $this->{'poster'}->{'user'} : undef; my $remote = LJ::get_remote(); my $null_link = { '_type' => 'Link', '_isnull' => 1 }; my $dtalkid = $this->{talkid}; my $comment = LJ::Comment->new( $u, dtalkid => $dtalkid ); if ( $key eq "delete_comment" ) { return $null_link unless LJ::Talk::can_delete( $remote, $u, $post_user, $com_user ); return LJ::S2::Link( "$LJ::SITEROOT/delcomment?journal=$u->{'user'}&id=$this->{'talkid'}", $ctx->[S2::PROPS]->{"text_multiform_opt_delete"}, LJ::S2::Image_std('btn_del') ); } if ( $key eq "freeze_thread" ) { return $null_link if $this->{'frozen'}; return $null_link unless LJ::Talk::can_freeze( $remote, $u, $post_user, $com_user ); return LJ::S2::Link( "$LJ::SITEROOT/talkscreen?mode=freeze&journal=$u->{'user'}&talkid=$this->{'talkid'}", $ctx->[S2::PROPS]->{"text_multiform_opt_freeze"}, LJ::S2::Image_std('btn_freeze') ); } if ( $key eq "unfreeze_thread" ) { return $null_link unless $this->{'frozen'}; return $null_link unless LJ::Talk::can_unfreeze( $remote, $u, $post_user, $com_user ); return LJ::S2::Link( "$LJ::SITEROOT/talkscreen?mode=unfreeze&journal=$u->{'user'}&talkid=$this->{'talkid'}", $ctx->[S2::PROPS]->{"text_multiform_opt_unfreeze"}, LJ::S2::Image_std('btn_unfreeze') ); } if ( $key eq "screen_comment" ) { return $null_link if $this->{'screened'}; return $null_link unless LJ::Talk::can_screen( $remote, $u, $post_user, $com_user ); return LJ::S2::Link( "$LJ::SITEROOT/talkscreen?mode=screen&journal=$u->{'user'}&talkid=$this->{'talkid'}", $ctx->[S2::PROPS]->{"text_multiform_opt_screen"}, LJ::S2::Image_std('btn_scr') ); } if ( $key eq "unscreen_comment" ) { return $null_link unless $this->{'screened'}; return $null_link unless LJ::Talk::can_unscreen( $remote, $u, $post_user, $com_user ); return LJ::S2::Link( "$LJ::SITEROOT/talkscreen?mode=unscreen&journal=$u->{'user'}&talkid=$this->{'talkid'}", $ctx->[S2::PROPS]->{"text_multiform_opt_unscreen"}, LJ::S2::Image_std('btn_unscr') ); } # added new button if ( $key eq "unscreen_to_reply" ) { #return $null_link unless $this->{'screened'}; #return $null_link unless LJ::Talk::can_unscreen($remote, $u, $post_user, $com_user); return LJ::S2::Link( "$LJ::SITEROOT/talkscreen?mode=unscreen&journal=$u->{'user'}&talkid=$this->{'talkid'}", $ctx->[S2::PROPS]->{"text_multiform_opt_unscreen_to_reply"}, LJ::S2::Image_std('btn_unscr') ); } if ( $key eq "watch_thread" || $key eq "unwatch_thread" || $key eq "watching_parent" ) { return $null_link unless LJ::is_enabled('esn'); return $null_link unless $remote && $remote->can_use_esn && $remote->can_track_thread; if ( $key eq "unwatch_thread" ) { return $null_link unless $remote->has_subscription( journal => $u, event => "JournalNewComment", arg2 => $comment->jtalkid ); my @subs = $remote->has_subscription( journal => $comment->entry->journal, event => "JournalNewComment", arg2 => $comment->jtalkid ); my $subscr = $subs[0]; return $null_link unless $subscr; my $auth_token = $remote->ajax_auth_token( '/__rpc_esn_subs', subid => $subscr->id, action => 'delsub' ); my $etypeid = 'LJ::Event::JournalNewComment'->etypeid; return LJ::S2::Link( "$LJ::SITEROOT/manage/tracking/comments?journal=$u->{'user'}&talkid=" . $comment->dtalkid, $ctx->[S2::PROPS]->{"text_multiform_opt_untrack"}, LJ::S2::Image_std('untrack'), 'lj_etypeid' => $etypeid, 'lj_journalid' => $u->id, 'lj_subid' => $subscr->id, 'class' => 'TrackButton', 'id' => 'lj_track_btn_' . $dtalkid, 'lj_dtalkid' => $dtalkid, 'lj_arg2' => $comment->jtalkid, 'lj_auth_token' => $auth_token, 'js_swapname' => $ctx->[S2::PROPS]->{text_multiform_opt_track} ); } return $null_link if $remote->has_subscription( journal => $u, event => "JournalNewComment", arg2 => $comment->jtalkid ); # at this point, we know that the thread is either not being watched or its parent is being watched # in other words, the user is not subscribed to this particular comment # see if any parents are being watched my $watching_parent = $comment->thread_has_subscription( $remote, $u ); my $etypeid = 'LJ::Event::JournalNewComment'->etypeid; my %subparams = ( journalid => $comment->entry->journal->id, etypeid => $etypeid, arg2 => LJ::Comment->new( $comment->entry->journal, dtalkid => $dtalkid )->jtalkid, ); my $auth_token = $remote->ajax_auth_token( '/__rpc_esn_subs', action => 'addsub', %subparams ); my %btn_params = map { ( 'lj_' . $_, $subparams{$_} ) } keys %subparams; $btn_params{'class'} = 'TrackButton'; $btn_params{'lj_auth_token'} = $auth_token; $btn_params{'lj_subid'} = 0; $btn_params{'lj_dtalkid'} = $dtalkid; $btn_params{'id'} = "lj_track_btn_" . $dtalkid; $btn_params{'js_swapname'} = $ctx->[S2::PROPS]->{text_multiform_opt_untrack}; if ( $key eq "watch_thread" && !$watching_parent ) { return LJ::S2::Link( "$LJ::SITEROOT/manage/tracking/comments?journal=$u->{'user'}&talkid=$dtalkid", $ctx->[S2::PROPS]->{"text_multiform_opt_track"}, LJ::S2::Image_std('track'), %btn_params ); } if ( $key eq "watching_parent" && $watching_parent ) { return LJ::S2::Link( "$LJ::SITEROOT/manage/tracking/comments?journal=$u->{'user'}&talkid=$dtalkid", $ctx->[S2::PROPS]->{"text_multiform_opt_track"}, LJ::S2::Image_std('untrack'), %btn_params ); } return $null_link; } if ( $key eq "edit_comment" ) { return $null_link unless $comment->remote_can_edit; my $edit_url = $this->{edit_url} || $comment->edit_url; return LJ::S2::Link( $edit_url, $ctx->[S2::PROPS]->{"text_multiform_opt_edit"}, LJ::S2::Image_std('editcomment') ); } if ( $key eq "expand_comments" ) { return $null_link unless $u->show_thread_expander($remote); ## show "Expand" link only if ## 1) the comment is collapsed ## 2) any of comment's children are collapsed my $show_expand_link; if ( !$this->{full} and !$this->{deleted} ) { $show_expand_link = 1; } else { foreach my $c ( @{ $this->{replies} } ) { if ( !$c->{full} and !$c->{deleted} ) { $show_expand_link = 1; last; } } } return $null_link unless $show_expand_link; return LJ::S2::Link( "#", ## actual link is javascript: onclick='....' $ctx->[S2::PROPS]->{"text_comment_expand"} ); } if ( $key eq "hide_comments" ) { ## show "Hide/Show" link if the comment has any children # only show hide/show comments if using jquery if ( @{ $this->{replies} || [] } > 0 ) { return LJ::S2::Link( "#", ## actual link is javascript: onclick='....' $ctx->[S2::PROPS]->{"text_comment_hide"} ); } else { return $null_link; } } if ( $key eq "unhide_comments" ) { ## show "Hide/Unhide" link if the comment has any children # only show hide/show comments if using jquery if ( @{ $this->{replies} || [] } > 0 ) { return LJ::S2::Link( "#", ## actual link is javascript: onclick='....' $ctx->[S2::PROPS]->{"text_comment_unhide"} ); } else { return $null_link; } } } sub Comment__print_multiform_check { my ( $ctx, $this ) = @_; my $tid = $this->{'talkid'} >> 8; $S2::pout->( "" ); } sub Comment__print_reply_link { my ( $ctx, $this, $opts ) = @_; $opts ||= {}; my $basesubject = $this->{'subject'}; $opts->{'basesubject'} = $basesubject; $opts->{'target'} ||= $this->{'talkid'}; _print_quickreply_link( $ctx, $this, $opts ); } *EntryLite__print_reply_link = \&_print_quickreply_link; *Entry__print_reply_link = \&_print_quickreply_link; *Page__print_reply_link = \&_print_quickreply_link; *EntryPage__print_reply_link = \&_print_quickreply_link; sub _print_quickreply_link { my ( $ctx, $this, $opts ) = @_; $opts ||= {}; # one of these had better work my $replyurl = $opts->{'reply_url'} || $this->{'reply_url'} # entrypage comments || $this->{'entry'}->{'comments'}->{'post_url'} # entrypage entry || $this->{comments}->{post_url}; # readpage entry # clean up input: my $linktext = LJ::ehtml( $opts->{'linktext'} ) || ""; my $target = $opts->{target} || ''; return unless $target =~ /^[\w-]+$/; # if no target specified bail out my $opt_class = $opts->{class} || ''; undef $opt_class unless $opt_class =~ /^[\w\s-]+$/; my $opt_img = LJ::CleanHTML::canonical_url( $opts->{'img_url'} ); $replyurl = LJ::CleanHTML::canonical_url($replyurl); # if they want an image change the text link to the image, # and add the text after the image if they specified it as well if ($opt_img) { # hella robust img options. (width,height,align,alt,title) my $width = $opts->{'img_width'} + 0; my $height = $opts->{'img_height'} + 0; my $align = $opts->{'img_align'}; my $alt = LJ::ehtml( $opts->{'alt'} ); my $title = LJ::ehtml( $opts->{'title'} ); my $border = $opts->{'img_border'} + 0; $width = $width ? "width=$width" : ""; $height = $height ? "height=$height" : ""; $border = $border ne "" ? "border=$border" : ""; $alt = $alt ? "alt=\"$alt\"" : ""; $title = $title ? "title=\"$title\"" : ""; $align = $align =~ /^\w+$/ ? "align=\"$align\"" : ""; $linktext = "$linktext"; } my $basesubject = $opts->{basesubject} || ''; #cleaned later $opt_class = $opt_class ? "class=\"$opt_class\"" : ""; my $page = get_page(); my $remote = LJ::get_remote(); my $onclick = ""; unless ( $remote && $remote->prop("opt_no_quickreply") ) { my $pid = ( $target =~ /^\d+$/ && $page->{_type} eq 'EntryPage' ) ? int( $target / 256 ) : 0; $basesubject =~ s/^(Re:\s*)*//i; $basesubject = "Re: $basesubject" if $basesubject; $basesubject = LJ::ejs($basesubject); $onclick = "return function(that) {return quickreply(\"$target\", $pid, \"$basesubject\",that)}(this)"; $onclick = "onclick='$onclick'"; } $onclick = "" unless LJ::S2::has_quickreply($page); $onclick = "" unless LJ::is_enabled('s2quickreply'); # FIXME: I THINK the next line is incoherent on reading/network pages. -NF $onclick = "" if $page->{'_u'}->does_not_allow_comments_from_non_access($remote); $replyurl = LJ::ehtml($replyurl); $S2::pout->("$linktext"); } sub _print_reply_container { my ( $ctx, $this, $opts ) = @_; my $page = get_page(); return unless LJ::S2::has_quickreply($page); my $target = $opts->{target} || ''; undef $target unless $target =~ /^[\w-]+$/; my $class = $opts->{class} || ''; # set target to the dtalkid if no target specified (link will be same) my $dtalkid = $this->{'talkid'} || undef; $target ||= $dtalkid; return if !$target; undef $class unless $class =~ /^([\w\s]+)$/; $class = $class ? "class=\"$class\"" : ""; $S2::pout->( "
" ); # unless we've already inserted the big qrdiv ugliness, do it. unless ( $ctx->[S2::SCRATCH]->{'quickreply_printed_div'}++ ) { my $u = $page->{'_u'}; my $ditemid = $page->{'entry'}{'itemid'} || $this->{itemid} || 0; my $userpic = LJ::ehtml( $page->{'_picture_keyword'} ) || ""; my $thread = ""; $thread = $page->{_viewing_thread_id} + 0 if defined $page->{_viewing_thread_id}; $S2::pout->( LJ::create_qr_div( $u, $ditemid, style_opts => $page->{_styleopts}, userpic => $userpic, thread => $thread, minimal => $page->{view} ne "entry", ) ); } } *EntryLite__print_reply_container = \&_print_reply_container; *Entry__print_reply_container = \&_print_reply_container; *Comment__print_reply_container = \&_print_reply_container; *EntryPage__print_reply_container = \&_print_reply_container; *Page__print_reply_container = \&_print_reply_container; sub Comment__expand_link { my ( $ctx, $this, $opts ) = @_; $opts ||= {}; my $prop_text = LJ::ehtml( $ctx->[S2::PROPS]->{"text_comment_expand"} ); my $text = LJ::ehtml( $opts->{text} ); $text =~ s/&nbsp;/ /gi; # allow   in the text my $opt_img = LJ::CleanHTML::canonical_url( $opts->{img_url} ); # if they want an image change the text link to the image, # and add the text after the image if they specified it as well if ($opt_img) { my $width = $opts->{img_width}; my $height = $opts->{img_height}; my $border = $opts->{img_border}; my $align = LJ::ehtml( $opts->{img_align} ); my $alt = LJ::ehtml( $opts->{img_alt} ) || $prop_text; my $title = LJ::ehtml( $opts->{img_title} ) || $prop_text; $width = defined $width && $width =~ /^\d+$/ ? " width=\"$width\"" : ""; $height = defined $height && $height =~ /^\d+$/ ? " height=\"$height\"" : ""; $border = defined $border && $border =~ /^\d+$/ ? " border=\"$border\"" : ""; $align = $align =~ /^\w+$/ ? " align=\"$align\"" : ""; $alt = $alt ? " alt=\"$alt\"" : ""; $title = $title ? " title=\"$title\"" : ""; $text = "$text"; } elsif ( !$text ) { $text = $prop_text; } my $title = $opts->{title} ? " title='" . LJ::ehtml( $opts->{title} ) . "'" : ""; my $class = $opts->{class} ? " class='" . LJ::ehtml( $opts->{class} ) . "'" : ""; my $onclick = ""; # if we're in top-only mode, then we display the expand link as # the unhide ('show x comments') message if ( $this->{"hide_children"} ) { my $comment_count = $this->{'showable_children'}; $text = LJ::ehtml( get_plural_phrase( $ctx, $comment_count, "text_comment_unhide" ) ); my $remote = LJ::get_remote(); $onclick = " onClick=\"Expander.make(this,'$this->{js_expand_url}','$this->{talkid}', true); return false;\""; } else { $onclick = " onClick=\"Expander.make(this,'$this->{js_expand_url}','$this->{talkid}'); return false;\""; } return "$text"; } sub Comment__print_expand_link { $S2::pout->( Comment__expand_link(@_) ); } # creates the (javascript) link that hides comments under this comment. sub Comment__print_hide_link { my ( $ctx, $this, $opts ) = @_; $opts ||= {}; my $comment_count = $this->{'showable_children'}; my $prop_text = LJ::ehtml( get_plural_phrase( $ctx, $comment_count, "text_comment_hide" ) ); my $text = LJ::ehtml( $opts->{text} ); $text =~ s/&nbsp;/ /gi; # allow   in the text my $opt_img = LJ::CleanHTML::canonical_url( $opts->{img_url} ); # if they want an image change the text link to the image, # and add the text after the image if they specified it as well if ($opt_img) { my $width = $opts->{img_width}; my $height = $opts->{img_height}; my $border = $opts->{img_border}; my $align = LJ::ehtml( $opts->{img_align} ); my $alt = LJ::ehtml( $opts->{img_alt} ) || $prop_text; my $title = LJ::ehtml( $opts->{img_title} ) || $prop_text; $width = defined $width && $width =~ /^\d+$/ ? " width=\"$width\"" : ""; $height = defined $height && $height =~ /^\d+$/ ? " height=\"$height\"" : ""; $border = defined $border && $border =~ /^\d+$/ ? " border=\"$border\"" : ""; $align = $align =~ /^\w+$/ ? " align=\"$align\"" : ""; $alt = $alt ? " alt=\"$alt\"" : ""; $title = $title ? " title=\"$title\"" : ""; $text = "$text"; } elsif ( !$text ) { $text = $prop_text; } my $title = $opts->{title} ? " title='" . LJ::ehtml( $opts->{title} ) . "'" : ""; my $class = $opts->{class} ? " class='" . LJ::ehtml( $opts->{class} ) . "'" : ""; $S2::pout->( "{talkid}'); return false;\">$text" ); } # creates the (javascript) link that unhides comments under this comment. sub Comment__print_unhide_link { my ( $ctx, $this, $opts ) = @_; $opts ||= {}; my $comment_count = $this->{'showable_children'}; my $prop_text = LJ::ehtml( get_plural_phrase( $ctx, $comment_count, "text_comment_unhide" ) ); my $text = LJ::ehtml( $opts->{text} ); $text =~ s/&nbsp;/ /gi; # allow   in the text my $opt_img = LJ::CleanHTML::canonical_url( $opts->{img_url} ); # if they want an image change the text link to the image, # and add the text after the image if they specified it as well if ($opt_img) { my $width = $opts->{img_width}; my $height = $opts->{img_height}; my $border = $opts->{img_border}; my $align = LJ::ehtml( $opts->{img_align} ); my $alt = LJ::ehtml( $opts->{img_alt} ) || $prop_text; my $title = LJ::ehtml( $opts->{img_title} ) || $prop_text; $width = defined $width && $width =~ /^\d+$/ ? " width=\"$width\"" : ""; $height = defined $height && $height =~ /^\d+$/ ? " height=\"$height\"" : ""; $border = defined $border && $border =~ /^\d+$/ ? " border=\"$border\"" : ""; $align = $align =~ /^\w+$/ ? " align=\"$align\"" : ""; $alt = $alt ? " alt=\"$alt\"" : ""; $title = $title ? " title=\"$title\"" : ""; $text = "$text"; } elsif ( !$text ) { $text = $prop_text; } my $title = $opts->{title} ? " title='" . LJ::ehtml( $opts->{title} ) . "'" : ""; my $class = $opts->{class} ? " class='" . LJ::ehtml( $opts->{class} ) . "'" : ""; $S2::pout->( "{talkid}'); return false;\">$text" ); } sub Page__print_script_tags { my ( $ctx, $this ) = @_; if ( $LJ::ACTIVE_RES_GROUP && $LJ::ACTIVE_RES_GROUP eq "foundation" ) { $S2::pout->( LJ::S2::get_script_tags($this) ); } } sub Page__print_trusted { my ( $ctx, $this, $key ) = @_; # use 'username' so that we can put 'foo.site.com' in the hash instead of # having to look up their 'ext_nnnn' name my $username = $this->{journal}->{username}; my $fullkey = "$username-$key"; if ( $LJ::TRUSTED_S2_WHITELIST_USERNAMES{$username} ) { # more restrictive way: username-key $S2::pout->( LJ::conf_test( $LJ::TRUSTED_S2_WHITELIST{$fullkey} ) ) if exists $LJ::TRUSTED_S2_WHITELIST{$fullkey}; } else { # less restrictive way: key $S2::pout->( LJ::conf_test( $LJ::TRUSTED_S2_WHITELIST{$key} ) ) if exists $LJ::TRUSTED_S2_WHITELIST{$key}; } } # 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; sub Date__compare { my ( $ctx, $this, $other ) = @_; return $other->{year} <=> $this->{year} || $other->{month} <=> $this->{month} || $other->{day} <=> $this->{day} || $other->{hour} <=> $this->{hour} || $other->{min} <=> $this->{min} || $other->{sec} <=> $this->{sec}; } *DateTime__compare = \&Date__compare; 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 _dt_vars_html { my $datecode = shift; return qq{ "/",$dt_vars{yyyy}, "/", $dt_vars{mm}, "/", $dt_vars{dd}, "/" } if $datecode =~ /^(d|dd|dayord)$/; return qq{ "/",$dt_vars{yyyy}, "/", $dt_vars{mm}, "/" } if $datecode =~ /^(m|mm|mon|month)$/; return qq{ "/",$dt_vars{yyyy}, "/" } if $datecode =~ /^(yy|yyyy)$/; } sub Date__date_format { my ( $ctx, $this, $fmt, $as_link ) = @_; $fmt ||= "short"; $as_link ||= ""; # formatted as link is separate from format as not link my $c = \$ctx->[S2::SCRATCH]->{'_code_datefmt'}->{ $fmt . $as_link }; 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"}; } elsif ( $fmt eq "iso" ) { $realfmt = "%%yyyy%%-%%mm%%-%%dd%%"; } my @parts = split( /\%\%/, $realfmt ); my $code = "\$\$c = sub { my \$time = shift; return join('',"; my $i = 0; foreach (@parts) { if ( $i % 2 ) { # translate date %%variable%% to value my $link = _dt_vars_html($_); $code .= $as_link && $link ? qq{"", $dt_vars{$_},"",} : $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 UserLite__ljuser { my ( $ctx, $UserLite, $link_color ) = @_; my $link_color_string = $link_color ? $link_color->{as_string} : ""; return LJ::ljuser( $UserLite->{_u}, { link_color => $link_color_string } ); } sub UserLite__get_link { my ( $ctx, $this, $key ) = @_; my $linkbar = $this->{_u}->user_link_bar( LJ::get_remote() ); my $button = sub { my ( $link, $key ) = @_; return undef unless $link; my $caption = $ctx->[S2::PROPS]->{userlite_interaction_links} eq "text" ? $link->{text} : $link->{title}; return LJ::S2::Link( $link->{url}, $caption, LJ::S2::Image( $link->{image}, $link->{width} || 20, $link->{height} || 18 ) ); }; return $button->( $linkbar->manage_membership, $key ) if $key eq 'manage_membership'; return $button->( $linkbar->trust, $key ) if $key eq 'trust'; return $button->( $linkbar->watch, $key ) if $key eq 'watch'; return $button->( $linkbar->post, $key ) if $key eq 'post_entry'; return $button->( $linkbar->message, $key ) if $key eq 'message'; return $button->( $linkbar->track, $key ) if $key eq 'track'; return $button->( $linkbar->memories, $key ) if $key eq 'memories'; return $button->( $linkbar->tellafriend, $key ) if $key eq 'tell_friend'; # Else? return undef; } *User__get_link = \&UserLite__get_link; sub EntryLite__get_link { my ( $ctx, $this, $key ) = @_; my $null_link = { '_type' => 'Link', '_isnull' => 1 }; if ( $this->{_type} eq 'Entry' || $this->{_type} eq 'StickyEntry' ) { return _Entry__get_link( $ctx, $this, $key ); } elsif ( $this->{_type} eq 'Comment' ) { return _Comment__get_link( $ctx, $this, $key ); } else { return $null_link; } } *Entry__get_link = \&EntryLite__get_link; *Comment__get_link = \&EntryLite__get_link; # method for smart converting raw subject to html-link sub EntryLite__formatted_subject { my ( $ctx, $this, $opts ) = @_; my $subject = $this->{subject}; my $format = delete $opts->{format} || ""; my $force_text = $format eq "text" ? 1 : 0; # Figure out what subject to show. Even if the settings are configured # to show nothing for entries or comments without subjects, there should # always be at a minimum a hidden visibility subject line for screenreaders. my $set_subject = sub { my ( $all_subs, $always ) = @_; return if defined $subject and $subject ne ""; # no subject my $text_nosubject = $ctx->[S2::PROPS]->{text_nosubject}; if ( $text_nosubject ne "" ) { # if text_nosubject is set, use it as the subject if # all_entrysubjects/all_commentsubjects is true, # or if we're in the month view for entries, # or if we're in the collapsed view for comments $subject = $text_nosubject if $ctx->[S2::PROPS]->{$all_subs} || $always; } if ( $subject eq "" ) { # still no subject, so use hidden text_nosubject_screenreader $subject = $ctx->[S2::PROPS]->{text_nosubject_screenreader}; $opts->{class} .= " invisible"; } }; # Leave the subject as is if it exists. Otherwise, determine what to show. if ( $this->{_type} eq 'Entry' || $this->{_type} eq 'StickyEntry' ) { $set_subject->( 'all_entrysubjects', $LJ::S2::CURR_PAGE->{view} eq 'month' ); } elsif ( $this->{_type} eq "Comment" ) { $set_subject->( 'all_commentsubjects', !$this->{full} ); } my $class = $opts->{class} ? " class=\"" . LJ::ehtml( $opts->{class} ) . "\" " : ''; my $style = $opts->{style} ? " style=\"" . LJ::ehtml( $opts->{style} ) . "\" " : ''; # display subject as-is (cleaned but not wrapped in a link) # if we forced it to plain text # or subject has a link and we are on a full comment/single entry view and don't need to click through # TODO: how about other HTML tags? if ( $force_text || ( $subject =~ /href/ && ( $this->{full} || $LJ::S2::CURR_PAGE->{view} eq "reply" || $LJ::S2::CURR_PAGE->{view} eq "entry" ) ) ) { return "$subject"; } else { # we need to be able to click through this subject, so remove links LJ::CleanHTML::clean( \$subject, { noexpandembedded => 1, mode => "allow", remove => ["a"] } ); # additional cleaning for title attribute, necessary to enable # screenreaders to see the names of the invisible links my $title = $subject; LJ::CleanHTML::clean_subject_all( \$title ); return "{permalink_url}\"$class$style>$subject"; } } *Entry__formatted_subject = \&EntryLite__formatted_subject; *Comment__formatted_subject = \&EntryLite__formatted_subject; 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'}; my $subj = $this->{'subject'}; LJ::CleanHTML::clean_subject_all( \$subj ); return $this->{'_plainsubject'} = $subj; } *Entry__get_plain_subject = \&EntryLite__get_plain_subject; *Comment__get_plain_subject = \&EntryLite__get_plain_subject; sub _Entry__get_link { my ( $ctx, $this, $key ) = @_; my $journal = $this->{'journal'}->{'user'}; my $poster = $this->{'poster'}->{'user'}; my $remote = LJ::get_remote(); my $null_link = { '_type' => 'Link', '_isnull' => 1 }; my $journalu = LJ::load_user($journal); my $esnjournal = $journalu->is_community ? $journal : $poster; if ( $key eq "edit_entry" ) { return $null_link unless $remote && ( $remote->user eq $journal || $remote->user eq $poster || $remote->can_manage($journalu) ); return LJ::S2::Link( "$LJ::SITEROOT/editjournal?journal=$journal&itemid=$this->{'itemid'}", $ctx->[S2::PROPS]->{"text_edit_entry"}, LJ::S2::Image_std('editentry') ); } if ( $key eq "edit_tags" ) { my $entry = LJ::Entry->new( $journalu, ditemid => $this->{itemid} ); return $null_link unless $remote && LJ::Tags::can_add_entry_tags( $remote, $entry ); return LJ::S2::Link( "$LJ::SITEROOT/edittags?journal=$journal&itemid=$this->{'itemid'}", $ctx->[S2::PROPS]->{"text_edit_tags"}, LJ::S2::Image_std('edittags') ); } if ( $key eq "tell_friend" ) { return $null_link unless LJ::is_enabled('tellafriend'); my $entry = LJ::Entry->new( $journalu->userid, ditemid => $this->{itemid} ); return $null_link unless $entry->can_tellafriend($remote); return LJ::S2::Link( "$LJ::SITEROOT/tools/tellafriend?journal=$journal&itemid=$this->{'itemid'}", $ctx->[S2::PROPS]->{"text_tell_friend"}, LJ::S2::Image_std('tellfriend') ); } if ( $key eq "mem_add" ) { return $null_link unless LJ::is_enabled('memories'); return LJ::S2::Link( "$LJ::SITEROOT/tools/memadd?journal=$journal&itemid=$this->{'itemid'}", $ctx->[S2::PROPS]->{"text_mem_add"}, LJ::S2::Image_std('memadd') ); } if ( $key eq "nav_prev" ) { return LJ::S2::Link( LJ::create_url( "/go", host => $LJ::DOMAIN_WEB, viewing_style => 1, args => { journal => $journal, itemid => $this->{itemid}, dir => "prev", } ), $ctx->[S2::PROPS]->{"text_entry_prev"}, LJ::S2::Image_std('prev_entry') ); } if ( $key eq "nav_next" ) { return LJ::S2::Link( LJ::create_url( "/go", host => $LJ::DOMAIN_WEB, viewing_style => 1, args => { journal => $journal, itemid => $this->{itemid}, dir => "next", } ), $ctx->[S2::PROPS]->{"text_entry_next"}, LJ::S2::Image_std('next_entry') ); } if ( $key eq "nav_tag_prev" ) { return LJ::S2::Link( LJ::create_url( "/go", host => $LJ::DOMAIN_WEB, viewing_style => 1, args => { journal => $journal, itemid => $this->{itemid}, redir_key => $this->{tagnav}->{name}, dir => "prev", } ), $ctx->[S2::PROPS]->{"text_entry_prev"}, LJ::S2::Image_std('prev_entry') ); } if ( $key eq "nav_tag_next" ) { return LJ::S2::Link( LJ::create_url( "/go", host => $LJ::DOMAIN_WEB, viewing_style => 1, args => { journal => $journal, itemid => $this->{itemid}, redir_key => $this->{tagnav}->{name}, dir => "next", } ), $ctx->[S2::PROPS]->{"text_entry_next"}, LJ::S2::Image_std('next_entry') ); } my $etypeid = 'LJ::Event::JournalNewComment'->etypeid; my $newentry_etypeid = 'LJ::Event::JournalNewEntry'->etypeid; my ($newentry_sub) = $remote ? $remote->has_subscription( journalid => $journalu->id, event => "JournalNewEntry", require_active => 1, ) : undef; my $newentry_auth_token; if ($newentry_sub) { $newentry_auth_token = $remote->ajax_auth_token( '/__rpc_esn_subs', subid => $newentry_sub->id, action => 'delsub', ); } elsif ($remote) { $newentry_auth_token = $remote->ajax_auth_token( '/__rpc_esn_subs', journalid => $journalu->id, action => 'addsub', etypeid => $newentry_etypeid, ); } if ( $key eq "watch_comments" ) { return $null_link unless LJ::is_enabled('esn'); return $null_link unless $remote && $remote->can_use_esn; return $null_link if $remote->has_subscription( journal => LJ::load_user($journal), event => "JournalNewComment", arg1 => $this->{'itemid'}, arg2 => 0, require_active => 1, ); my $auth_token = $remote->ajax_auth_token( '/__rpc_esn_subs', journalid => $journalu->id, action => 'addsub', etypeid => $etypeid, arg1 => $this->{itemid}, ); return LJ::S2::Link( "$LJ::SITEROOT/manage/tracking/entry?journal=$journal&itemid=$this->{'itemid'}", $ctx->[S2::PROPS]->{"text_watch_comments"}, LJ::S2::Image_std('track'), 'lj_journalid' => $journalu->id, 'lj_etypeid' => $etypeid, 'lj_subid' => 0, 'lj_arg1' => $this->{itemid}, 'lj_auth_token' => $auth_token, 'lj_newentry_etypeid' => $newentry_etypeid, 'lj_newentry_token' => $newentry_auth_token, 'lj_newentry_subid' => $newentry_sub ? $newentry_sub->id : 0, 'class' => 'TrackButton', 'js_swapname' => $ctx->[S2::PROPS]->{text_unwatch_comments}, 'journal' => $esnjournal ); } if ( $key eq "unwatch_comments" ) { return $null_link unless LJ::is_enabled('esn'); return $null_link unless $remote && $remote->can_use_esn; my @subs = $remote->has_subscription( journal => LJ::load_user($journal), event => "JournalNewComment", arg1 => $this->{'itemid'}, arg2 => 0, require_active => 1, ); my $subscr = $subs[0]; return $null_link unless $subscr; my $auth_token = $remote->ajax_auth_token( '/__rpc_esn_subs', subid => $subscr->id, action => 'delsub' ); return LJ::S2::Link( "$LJ::SITEROOT/manage/tracking/entry?journal=$journal&itemid=$this->{'itemid'}", $ctx->[S2::PROPS]->{"text_unwatch_comments"}, LJ::S2::Image_std('untrack'), 'lj_journalid' => $journalu->id, 'lj_subid' => $subscr->id, 'lj_etypeid' => $etypeid, 'lj_arg1' => $this->{itemid}, 'lj_auth_token' => $auth_token, 'lj_newentry_etypeid' => $newentry_etypeid, 'lj_newentry_token' => $newentry_auth_token, 'lj_newentry_subid' => $newentry_sub ? $newentry_sub->id : 0, 'class' => 'TrackButton', 'js_swapname' => $ctx->[S2::PROPS]->{text_watch_comments}, 'journal' => $esnjournal ); } } 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]; my @actions = qw( unscreen screen delete ); push @actions, "deletespam" unless LJ::sysban_check( 'spamreport', $this->{entry}->{journal}->{username} ); $S2::pout->( LJ::labelfy( 'multiform_mode', $pr->{text_multiform_des} ) . "\n" . LJ::html_select( { name => 'mode', id => 'multiform_mode' }, "" => "", map { $_ => $pr->{"text_multiform_opt_$_"} } @actions ) . "\n" . LJ::html_submit( '', $pr->{'text_multiform_btn'}, { "id" => "multiform_submit", "onclick" => 'return ((document.multiform.mode.value != "delete" ' . '&& document.multiform.mode.value != "deletespam")) ' . "|| 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'}->{'user'} ) . "\n" ); } sub Page__print_control_strip { my ( $ctx, $this ) = @_; my $control_strip = LJ::control_strip( user => $LJ::S2::CURR_PAGE->{'journal'}->{'_u'}->{'user'} ); return "" unless $control_strip; $S2::pout->($control_strip); } *RecentPage__print_control_strip = \&Page__print_control_strip; *DayPage__print_control_strip = \&Page__print_control_strip; *MonthPage__print_control_strip = \&Page__print_control_strip; *YearPage__print_control_strip = \&Page__print_control_strip; *FriendsPage__print_control_strip = \&Page__print_control_strip; *EntryPage__print_control_strip = \&Page__print_control_strip; *ReplyPage__print_control_strip = \&Page__print_control_strip; *TagsPage__print_control_strip = \&Page__print_control_strip; # removed as part of generic ads removal sub Page__print_hbox_top { } sub Page__print_hbox_bottom { } sub Page__print_vbox { } sub Page__print_ad_box { } sub Entry__print_ebox { } sub Page__print_ad { } sub Page__visible_tag_list { my ( $ctx, $this, $limit ) = @_; $limit ||= ""; return $this->{'_visible_tag_list'} if defined $this->{'_visible_tag_list'} && !$limit; my $remote = LJ::get_remote(); my $u = $LJ::S2::CURR_PAGE->{'_u'}; return [] unless $u; # use the cached tag list, if we have it my @taglist = @{ $this->{'_visible_tag_list'} || [] }; unless (@taglist) { my $tags = LJ::Tags::get_usertags( $u, { remote => $remote } ); return [] unless $tags; foreach my $kwid ( keys %{$tags} ) { # only show tags for display next unless $tags->{$kwid}->{display}; # create tag object push @taglist, LJ::S2::TagDetail( $u, $kwid => $tags->{$kwid} ); } } if ($limit) { @taglist = sort { $b->{use_count} <=> $a->{use_count} } @taglist; @taglist = splice @taglist, 0, $limit; } @taglist = sort { $a->{name} cmp $b->{name} } @taglist; return $this->{'_visible_tag_list'} = \@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; *TagsPage__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); # defaults to current year/month my @now = gmtime(time); my ( $curyear, $curmonth ) = ( $now[5] + 1900, $now[4] + 1 ); my ( $year, $month ) = ( $curyear, $curmonth ); # only want to look at current years, not future-dated posts my @years = grep { $_ <= $curyear } sort { $a <=> $b } keys %$counts; if (@years) { # year/month of last post $year = $years[-1]; # we'll take any month of previous years, or anything up to the current month $month = ( grep { $year < $curyear || $_ <= $curmonth } sort { $a <=> $b } keys %{ $counts->{$year} } )[-1]; } return $this->{'_latest_month'} = LJ::S2::YearMonth( $this, { 'year' => $year, 'month' => $month, }, S2::get_property_value( $ctx, 'reg_firstdayofweek' ) eq "monday" ? 1 : 0 ); } *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}; return "#" unless $UserLite && $u; return $u->journal_base; } 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, $as_link ) = @_; $fmt ||= "long"; $as_link ||= ""; my $c = \$ctx->[S2::SCRATCH]->{'_code_monthfmt'}->{ $fmt . $as_link }; 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 ) { # translate date %%variable%% to value my $link = _dt_vars_html($_); $code .= $as_link && $link ? qq{"", $dt_vars{$_},"",} : $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__index { use utf8; my ( $ctx, $this, $substr, $position ) = @_; return index( $this, $substr, $position ); } sub string__substr { my ( $ctx, $this, $start, $length ) = @_; use Encode qw/decode_utf8 encode_utf8/; my $ustr = decode_utf8($this); my $result = substr( $ustr, $start, $length ); return encode_utf8($result); } 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__replace { use utf8; my ( $ctx, $this, $find, $replace ) = @_; $this =~ s/\Q$find\E/$replace/g; return $this; } sub string__split { use utf8; my ( $ctx, $this, $splitby ) = @_; my @result = split /\Q$splitby\E/, $this; return \@result; } 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; } sub string__compare { use utf8; # Does this actually make any difference here? my ( $ctx, $this, $other ) = @_; return $other cmp $this; } sub string__css_length_value { my ( $ctx, $this ) = @_; $this =~ s/^\s+//g; $this =~ s/\s+$//g; # Is it one of the acceptable keywords? my %allowed_keywords = map { $_ => 1 } qw(larger smaller xx-small x-small small medium large x-large xx-large auto inherit); return $this if $allowed_keywords{$this}; # Is it a number followed by an acceptable unit? my %allowed_units = map { $_ => 1 } qw(em ex px in cm mm pt pc %); return $this if $this =~ /^[\-\+]?(\d*\.)?\d+([a-z]+|\%)$/ && $allowed_units{$2}; # Is it zero? return "0" if $this =~ /^(0*\.)?0+$/; return ''; } sub string__css_multiply_length { my ( $ctx, $this, $multiplier ) = @_; my ( $length, $unit ) = $this =~ /(\d+)(.+)/; return string__css_length_value( $ctx, ( $length * $multiplier ) . $unit ); } sub string__css_divide_length { my ( $ctx, $this, $divisor ) = @_; my ( $length, $unit ) = $this =~ /(\d+)(.+)/; return string__css_length_value( $ctx, int( $length / $divisor ) . $unit ); } sub string__css_string { my ( $ctx, $this ) = @_; $this =~ s/\\/\\\\/g; $this =~ s/\"/\\\"/g; return '"' . $this . '"'; } sub string__css_url_value { my ( $ctx, $this ) = @_; return '' if $this !~ m!^https?://!; return '' if $this =~ /[^a-z0-9A-Z\.\@\$\-_\.\+\!\*'\(\),&=#;:\?\/\%~]/; return 'url(' . string__css_string( $ctx, $this ) . ')'; } sub string__css_keyword { my ( $ctx, $this, $allowed ) = @_; $this =~ s/^\s+//g; $this =~ s/\s+$//g; return '' if $this =~ /[^a-z\-]/i; if ($allowed) { # If we've got an arrayref, transform it into a hashref. $allowed = { map { $_ => 1 } @$allowed } if ref $allowed eq 'ARRAY'; return '' unless $allowed->{$this}; } return lc($this); } sub string__css_keyword_list { my ( $ctx, $this, $allowed ) = @_; $this =~ s/^\s+//g; $this =~ s/\s+$//g; my @in = split( /\s+/, $this ); my @out = (); # Do the transform of $allowed to a hash once here rather than once for each keyword $allowed = { map { $_ => 1 } @$allowed } if ref $allowed eq 'ARRAY'; foreach my $kw (@in) { $kw = string__css_keyword( $ctx, $kw, $allowed ); push @out, $kw if $kw; } return join( ' ', @out ); } sub Siteviews__need_res { my ( $ctx, $this, $opts, $res ) = @_; die "Siteviews doesn't work standalone" unless $ctx->[S2::SCRATCH]->{siteviews_enabled}; if ( ref($opts) ne 'HASH' ) { $res = $opts; LJ::need_res($res); } else { LJ::need_res( $opts, $res ); } } sub Siteviews__start_capture { my ( $ctx, $this ) = @_; die "Siteviews doesn't work standalone" unless $ctx->[S2::SCRATCH]->{siteviews_enabled}; # force flush S2::get_output()->(""); push @{ $this->{_input_captures} }, $LJ::S2::ret_ref; my $text = ""; $LJ::S2::ret_ref = \$text; } sub Siteviews__end_capture { my ( $ctx, $this ) = @_; die "Siteviews doesn't work standalone" unless $ctx->[S2::SCRATCH]->{siteviews_enabled}; return "" unless scalar( @{ $this->{_input_captures} } ); # force flush S2::get_output()->(""); my $text_ref = $LJ::S2::ret_ref; $LJ::S2::ret_ref = pop @{ $this->{_input_captures} }; return $$text_ref; } sub Siteviews__set_content { my ( $ctx, $this, $content, $text ) = @_; die "Siteviews doesn't work standalone" unless $ctx->[S2::SCRATCH]->{siteviews_enabled}; $this->{_content}->{$content} = $text; } sub keys_alpha { my ( $ctx, $ref ) = @_; return undef unless ref $ref eq 'HASH'; # return reference to array of sorted keys return [ sort { $a cmp $b } keys %$ref ]; } 1;