ljr/local/cgi-bin/LJ/EmbedModule.pm

467 lines
16 KiB
Perl
Raw Permalink Normal View History

2019-02-05 21:49:12 +00:00
#!/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 <object>, <embed> or <lj-embed>
REGULAR => 1,
# inside <object> or <embed> tag
IMPLICIT => 2,
# inside explicit <lj-embed> 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 class="ljembed"... tags from the RTE into proper lj-embed tags
sub transform_rte_post {
my ($class, $txt) = @_;
return $txt unless $txt && $txt =~ /ljembed/i;
# ghetto... shouldn't use regexes to parse this
$txt =~ s/<div\s*class="ljembed"\s*(embedid="(\d+)")?\s*>(((?!<\/div>).)*)<\/div>/<lj-embed id="$2">$3<\/lj-embed>/ig;
$txt =~ s/<div\s*(embedid="(\d+)")?\s*class="ljembed"\s*>(((?!<\/div>).)*)<\/div>/<lj-embed id="$2">$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/(<lj\-embed[^>]+\/>)/$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 '<lj-embed ' . join(' ', map {"$_=\"$attrs{$_}\""} keys %attrs) . ">\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->{'/'}) {
# <lj-embed ...>, 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') {
# <object> or <embed>
# 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') {
# </object> or </embed>
# 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') {
# <object> or <embed>
# 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') {
# </lj-embed> - 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 .= "<lj-embed " . join(' ', map { exists $embed_attrs{$_} ? "$_=\"$embed_attrs{$_}\"" : () } qw / id width height /) . "/>";
$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 {<iframe src="$iframe_url" } .
qq{width="$width" height="$height" allowtransparency="true" frameborder="0" class="lj_embedcontent" $id></iframe>};
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 "</$tag>";
} else { # C, T, D or PI
return $tag;
}
}
1;