#!/usr/bin/perl package LJ::EmbedModule; use strict; use Carp qw (croak); use Class::Autouse qw ( LJ::Auth HTML::TokeParser ); # states for a finite-state machine we use in parse() use constant { # reading plain html without , or REGULAR => 1, # inside or tag IMPLICIT => 2, # inside explicit tag EXPLICIT => 3, # maximum embed width and height MAX_WIDTH => 800, MAX_HEIGHT => 800, }; # can optionally pass in an id of a module to change its contents # returns module id sub save_module { my ($class, %opts) = @_; my $contents = $opts{contents} || ''; my $id = $opts{id}; my $journal = $opts{journal} or croak "No journal passed to LJ::EmbedModule::save_module"; my $preview = $opts{preview}; # are we creating a new entry? unless (defined $id) { $id = LJ::alloc_user_counter($journal, 'D') or die "Could not allocate embed module ID"; } my $cmptext = 'C-' . LJ::text_compress($contents); ## embeds for preview are stored in a special table, ## where new items overwrites old ones my $table_name = ($preview) ? 'embedcontent_preview' : 'embedcontent'; $journal->do("REPLACE INTO $table_name (userid, moduleid, content) VALUES ". "(?, ?, ?)", undef, $journal->{'userid'}, $id, $cmptext); die $journal->errstr if $journal->err; # save in memcache my $memkey = $class->memkey($journal->{'userid'}, $id, $preview); LJ::MemCache::set($memkey, $contents); return $id; } # changes
(((?!<\/div>).)*)<\/div>/$3<\/lj-embed>/ig; $txt =~ s/(((?!<\/div>).)*)<\/div>/$3<\/lj-embed>/ig; return $txt; } # takes a scalarref to entry text and expands lj-embed tags # REPLACE sub expand_entry { my ($class, $journal, $entryref, %opts) = @_; $$entryref =~ s/(]+\/>)/$class->_expand_tag($journal, $1, $opts{edit}, %opts)/ge; } sub _expand_tag { my $class = shift; my $journal = shift; my $tag = shift; my $edit = shift; my %opts = @_; my %attrs = $tag =~ /(\w+)="?(\-?\d+)"?/g; return '[invalid lj-embed, id is missing]' unless $attrs{id}; if ($edit) { return '\n" . $class->module_content(moduleid => $attrs{id}, journalid => $journal->id) . "\n<\/lj-embed>"; } elsif ($opts{'content_only'}) { return $class->module_content(moduleid => $attrs{id}, journalid => $journal->{'userid'}); } else { @opts{qw /width height/} = @attrs{qw/width height/}; return $class->module_iframe_tag($journal, $attrs{id}, %opts) } }; # take a scalarref to a post, parses any lj-embed tags, saves the contents # of the tags and replaces them with a module tag with the id. # REPLACE sub parse_module_embed { my ($class, $journal, $postref, %opts) = @_; return unless $postref && $$postref; return if LJ::conf_test($LJ::DISABLED{embed_module}); # fast track out if we don't have to expand anything return unless $$postref =~ /lj\-embed|embed|object/i; # do we want to replace with the lj-embed tags or iframes? my $expand = $opts{expand}; # if this is editing mode, then we want to expand embed tags for editing my $edit = $opts{edit}; # previews are a special case (don't want to permanantly save to db) my $preview = $opts{preview}; # deal with old-fashion calls if (($edit || $expand) && ! $preview) { return $class->expand_entry($journal, $postref, %opts); } # ok, we can safely parse post text # machine state my $state = REGULAR; my $p = HTML::TokeParser->new($postref); my $newtxt = ''; my %embed_attrs = (); # ($eid, $ewidth, $eheight); my $embed = ''; my @stack = (); my $next_preview_id = 1; while (my $token = $p->get_token) { my ($type, $tag, $attr) = @$token; $tag = lc $tag; my $newstate = undef; my $reconstructed = $class->reconstruct($token); if ($state == REGULAR) { if ($tag eq 'lj-embed' && $type eq 'S' && ! $attr->{'/'}) { # , not self-closed # switch to EXPLICIT state $newstate = EXPLICIT; # save embed id, width and height if they do exist in attributes $embed_attrs{id} = $attr->{id} if $attr->{id}; $embed_attrs{width} = ($attr->{width} > MAX_WIDTH ? MAX_WIDTH : $attr->{width}) if $attr->{width}; $embed_attrs{height} = ($attr->{height} > MAX_HEIGHT ? MAX_HEIGHT : $attr->{height}) if $attr->{height}; } elsif (($tag eq 'object' || $tag eq 'embed') && $type eq 'S') { # or # switch to IMPLICIT state unless it is a self-closed tag unless ($attr->{'/'}) { $newstate = IMPLICIT; # tag balance push @stack, $tag; } # append the tag contents to new embed buffer, so we can convert in to lj-embed later $embed .= $reconstructed; } else { # otherwise stay in REGULAR $newtxt .= $reconstructed; } } elsif ($state == IMPLICIT) { if ($tag eq 'object' || $tag eq 'embed') { if ($type eq 'E') { # or # update tag balance, but only if we have a valid balance up to this moment pop @stack if $stack[-1] eq $tag; # switch to REGULAR if tags are balanced (stack is empty), stay in IMPLICIT otherwise $newstate = REGULAR unless @stack; } elsif ($type eq 'S') { # or # mind the tag balance, do not update it in case of a self-closed tag push @stack, $tag unless $attr->{'/'}; } } # append to embed buffer $embed .= $reconstructed; } elsif ($state == EXPLICIT) { if ($tag eq 'lj-embed' && $type eq 'E') { # - that's the end of explicit embed block, switch to REGULAR $newstate = REGULAR; } else { # continue appending contents to embed buffer $embed .= $reconstructed; } } else { # let's be paranoid die "Invalid state: '$state'"; } # we decided to switch back to REGULAR and have something in embed buffer # so let's save buffer as an embed module and start all over again if (defined($newstate) && $newstate == REGULAR && $embed) { $embed_attrs{id} = $class->save_module( id => ($preview ? $next_preview_id++ : $embed_attrs{id}), contents => $embed, journal => $journal, preview => $preview, ); $newtxt .= ""; $embed = ''; %embed_attrs = (); } # switch the state if we have a new one $state = $newstate if defined $newstate; } # update passed text $$postref = $newtxt; } sub module_iframe_tag { my ($class, $u, $moduleid, %opts) = @_; return '' if $LJ::DISABLED{embed_module}; my $journalid = $u->{'userid'}; $moduleid += 0; # parse the contents of the module and try to come up with a guess at the width and height of the content my $content = $class->module_content(moduleid => $moduleid, journalid => $journalid); my $preview = $opts{preview}; my $width = 0; my $height = 0; my $p = HTML::TokeParser->new(\$content); my $embedcodes; # if the content only contains a whitelisted embedded video # then we can skip the placeholders (in some cases) my $no_whitelist = 0; my $found_embed = 0; # we don't need to estimate the dimensions if they are provided in tag attributes unless ($opts{width} && $opts{height}) { while (my $token = $p->get_token) { my $type = $token->[0]; my $tag = $token->[1] ? lc $token->[1] : ''; my $attr = $token->[2]; # hashref if ($type eq "S") { my ($elewidth, $eleheight); if ($attr->{width}) { $elewidth = $attr->{width}+0; $width = $elewidth if $elewidth > $width; } if ($attr->{height}) { $eleheight = $attr->{height}+0; $height = $eleheight if $eleheight > $height; } my $flashvars = $attr->{flashvars}; if ($tag eq 'object' || $tag eq 'embed') { my $src; next unless $src = $attr->{src}; # we have an object/embed tag with src, make a fake lj-template object my @tags = ( ['S', 'lj-template', { name => 'video', (defined $elewidth ? ( width => $width ) : ()), (defined $eleheight ? ( height => $height ) : ()), (defined $flashvars ? ( flashvars => $flashvars ) : ()), }], [ 'T', $src, {}], ['E', 'lj-template', {}], ); $embedcodes = LJ::run_hook('expand_template_video', \@tags); $found_embed = 1 if $embedcodes; $found_embed &&= $embedcodes !~ /Invalid video/i; $no_whitelist = !$found_embed; } elsif ($tag ne 'param') { $no_whitelist = 1; } } } # add padding $width += 50 if $width; $height += 50 if $height; } # use explicit values if we have them $width = $opts{width} if $opts{width}; $height = $opts{height} if $opts{height}; $width ||= 240; $height ||= 200; # some dimension min/maxing $width = 50 if $width < 50; $width = MAX_WIDTH if $width > MAX_WIDTH; $height = 50 if $height < 50; $height = MAX_HEIGHT if $height > MAX_HEIGHT; # safari caches state of sub-resources aggressively, so give # each iframe a unique 'name' attribute my $id = qq(name="embed_${journalid}_$moduleid"); my $auth_token = LJ::eurl(LJ::Auth->sessionless_auth_token('embedcontent', moduleid => $moduleid, journalid => $journalid, preview => $preview,)); my $iframe_url = qq {http://$LJ::EMBED_MODULE_DOMAIN/tools/embedcontent.bml?journalid=$journalid&moduleid=$moduleid&preview=$preview&auth_token=$auth_token}; my $iframe_tag = qq {}; my $remote = LJ::get_remote(); my $do_placeholder; if ($remote) { return $iframe_tag if $opts{edit}; # show placeholder instead of iframe? LJ::load_user_props($remote, "opt_embedplaceholders"); my $placeholder_prop = $remote->prop('opt_embedplaceholders'); $do_placeholder = $placeholder_prop && $placeholder_prop ne 'N'; # if placeholder_prop is not set, then show placeholder on a friends # page view UNLESS the embedded content is only one embed/object # tag and it's whitelisted video. my $r = eval { Apache->request }; my $view = $r ? $r->notes("view") : ''; if (! $placeholder_prop && $view eq 'friends') { # show placeholder if this is not whitelisted video $do_placeholder = 1 if $no_whitelist; } } else { $do_placeholder = $BML::COOKIE{'flashpref'}; $do_placeholder = 0 unless $do_placeholder eq "0" || $do_placeholder eq "1"; } return $iframe_tag unless $do_placeholder; my $tmpcontent = $class->module_content( journalid => $journalid, moduleid => $moduleid, 0 ); $tmpcontent =~ s/.+param\s+name\s*?=\s*?"?movie"?\s*value\s*?=\s*?"?//sg; #" $tmpcontent =~ s/("|\s).+//sg; #" $tmpcontent = LJ::ehtml($tmpcontent); # placeholder return LJ::placeholder_link( placeholder_html => $iframe_tag, placeholder_link => $iframe_url, width => $width, height => $height, img => "$LJ::IMGPREFIX/videoplaceholder.png", img_title => $tmpcontent, ); } sub module_content { my ($class, %opts) = @_; my $moduleid = $opts{moduleid}; croak "No moduleid" unless defined $moduleid; $moduleid += 0; my $journalid = $opts{journalid}+0 or croak "No journalid"; my $journal = LJ::load_userid($journalid) or die "Invalid userid $journalid"; my $preview = $opts{preview}; # try memcache my $memkey = $class->memkey($journalid, $moduleid, $preview); my $content = LJ::MemCache::get($memkey); my ($dbload, $dbid); # module id from the database unless (defined $content) { my $table_name = ($preview) ? 'embedcontent_preview' : 'embedcontent'; ($content, $dbid) = $journal->selectrow_array("SELECT content, moduleid FROM $table_name WHERE " . "moduleid=? AND userid=?", undef, $moduleid, $journalid); die $journal->errstr if $journal->err; $dbload = 1; } $content ||= ''; LJ::text_uncompress(\$content) if $content =~ s/^C-//; # clean js out of content unless ($LJ::DISABLED{'embedmodule-cleancontent'}) { LJ::CleanHTML::clean(\$content, { addbreaks => 0, tablecheck => 0, mode => 'allow', allow => [qw(object embed)], deny => [qw(script iframe)], remove => [qw(script iframe)], ljcut_disable => 1, cleancss => 0, extractlinks => 0, noautolinks => 1, extractimages => 0, noexpandembedded => 1, transform_embed_nocheck => 1, }); } # if we got stuff out of database if ($dbload) { # save in memcache LJ::MemCache::set($memkey, $content); # if we didn't get a moduleid out of the database then this entry is not valid return defined $dbid ? $content : "[Invalid lj-embed id $moduleid]"; } # get rid of whitespace around the content return LJ::trim($content) || ''; } sub memkey { my ($class, $journalid, $moduleid, $preview) = @_; my $pfx = $preview ? 'embedcontpreview' : 'embedcont'; return [$journalid, "$pfx:$journalid:$moduleid"]; } # create a tag string from HTML::TokeParser token sub reconstruct { my $class = shift; my $token = shift; my ($type, $tag, $attr, $attord) = @$token; if ($type eq 'S') { my $txt = "<$tag"; my $selfclose; # preserve order of attributes. the original order is # in element 4 of $token foreach my $name (@$attord) { if ($name eq '/') { $selfclose = 1; next; } # FIXME: ultra ghetto. $attr->{$name} = LJ::no_utf8_flag($attr->{$name}); $txt .= " $name=\"" . LJ::ehtml($attr->{$name}) . "\""; } $txt .= $selfclose ? " />" : ">"; } elsif ($type eq 'E') { return ""; } else { # C, T, D or PI return $tag; } } 1;