#!/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::EmbedModule; use strict; use Carp qw (croak); use LJ::Auth; use HTML::TokeParser; use LJ::JSON; use DW::Task::EmbedWorker; # 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, MAX_WIDTH_PERCENT => 100, MAX_HEIGHT_PERCENT => 100, }; my %embeddable_tags = map { $_ => 1 } qw( object embed iframe ); # 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}; my $need_new_id = !defined $id; if ( defined $id ) { my $old_content = $class->module_content( moduleid => $id, journalid => LJ::want_userid($journal) )->{content} || ''; my $new_content = $contents; # old content is cleaned by module_content(); new is not LJ::CleanHTML::clean_embed( \$new_content ); $old_content =~ s/\s//sg; $new_content =~ s/\s//sg; $need_new_id = 1 unless $old_content eq $new_content; } # are we creating a new entry? if ($need_new_id) { $id = LJ::alloc_user_counter( $journal, 'D' ) or die "Could not allocate embed module ID"; } my $cmptext = 'C-' . LJ::text_compress($contents); # construct a direct link to the object if possible my $src_info = $class->extract_src_info( { contents => $contents, cmptext => $cmptext, journal => $journal, preview => $preview, id => $id, } ); ## embeds for journal entry pre-post 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, linktext, url) " . "VALUES (?, ?, ?, ?, ?)", undef, $journal->userid, $id, $cmptext, $src_info->{linktext}, $src_info->{url} ); die $journal->errstr if $journal->err; # save in memcache my $memkey = $class->memkey( $journal->userid, $id, $preview ); my $cref = { content => $cmptext, linktext => $src_info->{linktext}, url => $src_info->{url}, }; LJ::MemCache::set( $memkey, $cref ); return $id; } # changes
(((?!<\/div>).)*)<\/div>/$3<\/site-embed>/ig; $txt =~ s/(((?!<\/div>).)*)<\/div>/$3<\/site-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|site)\-embed[^>]+\/>)/$class->_expand_tag($journal, $1, $opts{edit}, %opts)/ge if $$entryref; } 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 site-embed, id is missing]' unless $attrs{id}; if ( $opts{expand_full} ) { return $class->module_content( moduleid => $attrs{id}, journalid => $journal->id ) ->{content}; } elsif ($edit) { return '" . $class->module_content( moduleid => $attrs{id}, journalid => $journal->id )->{content} . "<\/site-embed>"; } 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 unless LJ::is_enabled('embed_module'); # fast track out if we don't have to expand anything return unless $$postref =~ /(lj|site)\-embed|embed|object|iframe/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' || $tag eq 'site-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 ( $embeddable_tags{$tag} && $type eq 'S' ) { # or 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 or } . qq{$direct_link}; my $remote = LJ::get_remote(); return $iframe_tag unless $remote; return $iframe_tag if $opts{edit}; # show placeholder instead of iframe? my $placeholder_prop = $remote->prop('opt_embedplaceholders'); my $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 = DW::Request->get; my $view = $r ? $r->note("view") : ''; if ( !$placeholder_prop && $view && $view eq 'friends' ) { # show placeholder if this is not whitelisted video $do_placeholder = 1 if $no_whitelist; } return $iframe_tag unless $do_placeholder; # placeholder return LJ::placeholder_link( placeholder_html => $iframe_tag, link => $iframe_link, width => $width, width_unit => $width_unit, height => $height, height => $height_unit, img => "$LJ::IMGPREFIX/videoplaceholder.png", url => $url, linktext => $linktext, ); } 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"; return { content => '' } if $journal->is_expunged; my $preview = $opts{preview}; # are we displaying the content? (as opposed to processing the text for other reasons) my $display = $opts{display_as_content}; # try memcache my $memkey = $class->memkey( $journalid, $moduleid, $preview ); my ( $content, $linktext, $url ); # for direct linking my $cref = LJ::MemCache::get($memkey); $content = $cref->{content}; $linktext = $cref->{linktext}; $url = $cref->{url}; my ( $dbload, $dbid ); # module id from the database unless ( defined $content ) { my $table_name = ($preview) ? 'embedcontent_preview' : 'embedcontent'; ( $content, $dbid, $linktext, $url ) = $journal->selectrow_array( "SELECT " . "content, moduleid, linktext, url 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 LJ::CleanHTML::clean_embed( \$content, { display_as_content => $display } ); my $return_content; # if we got stuff out of database if ($dbload) { # if we didn't get a moduleid out of the database then this entry is not valid $return_content = { content => defined $dbid ? $content : "[Invalid lj-embed id $moduleid]", linktext => $linktext, url => $url, }; # save in memcache LJ::MemCache::set( $memkey, $return_content ); } else { # get rid of whitespace around the content $return_content = { content => LJ::trim($content) || '', linktext => $linktext, url => $url, }; } return $return_content; } sub memkey { my ( $class, $journalid, $moduleid, $preview ) = @_; my $pfx = $preview ? 'embedcontpreview2' : 'embedcont2'; 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: not the right way to do this. $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;