#!/usr/bin/perl # # DW::Controller::Media # # Displays media for a user. # # Authors: # Mark Smith # # Copyright (c) 2010-2018 by Dreamwidth Studios, LLC. # # This program is free software; you may redistribute it and/or modify it under # the same terms as Perl itself. For a copy of the license, please reference # 'perldoc perlartistic' or 'perldoc perlgpl'. # package DW::Controller::Media; use strict; use v5.10; use Log::Log4perl; my $log = Log::Log4perl->get_logger(__PACKAGE__); use DW::BlobStore; use DW::Routing; use DW::Request; use DW::Controller; use DW::External::Site; use POSIX; my %VALID_SIZES = ( map { $_ => $_ } ( 100, 320, 200, 640, 480, 1024, 768, 1280, 800, 600, 720, 1600, 1200 ) ); DW::Routing->register_regex( qr!^/file/(\d+)$!, \&media_handler, user => 1, formats => 1, ); DW::Routing->register_regex( qr!^/file/(\d+x\d+|full)(/\w:[\d\w]+)*/(\d+)$!, \&media_handler, user => 1, formats => 1, ); DW::Routing->register_string( '/file/list', \&media_manage_handler, app => 1 ); DW::Routing->register_string( '/file/edit', \&media_bulkedit_handler, app => 1 ); DW::Routing->register_string( '/file/new', \&media_new_handler, app => 1 ); DW::Routing->register_string( '/file', \&media_index_handler, app => 1 ); sub media_manage_handler { my ( $ok, $rv ) = controller(); return $rv unless $ok; my ( $remote, $r ) = ( $rv->{remote}, $rv->{r} ); return error_ml( 'error.openid', { sitename => $LJ::SITENAMESHORT, aopts => '/create' } ) if $remote->is_identity; my $u = $remote; my $adminmode = $u && $u->has_priv( 'canview', 'images' ); my $user = LJ::canonical_username( $r->get_args->{user} || $r->post_args->{user} ); if ( $adminmode && $user ) { $u = LJ::load_user($user); return error_ml('error.username_notfound') unless $u; return error_ml('error.purged.text') if $u->is_expunged; $user = undef if $remote->equals($u); } else { $user = undef; } # load all of a user's media. this is inefficient and won't be like this forever, # but it's simple for now... my @media = DW::Media->get_active_for_user( $u, width => 200, height => 200 ); $rv->{media} = \@media; $rv->{make_embed_url} = \&make_embed_url; $rv->{page} = $r->get_args->{page} || '1'; $rv->{view_type} = $r->get_args->{view} || ''; $rv->{maxpage} = POSIX::ceil( scalar @media / 20 ); $rv->{valid_sizes} = [%VALID_SIZES]; $rv->{convert_time} = \&LJ::mysql_time; $rv->{adminmode} = $adminmode ? 1 : 0; $rv->{user} = $user; my $media_usage = DW::Media->get_usage_for_user($u); my $media_quota = DW::Media->get_quota_for_user($u); $rv->{usage} = sprintf( "%0.3f MB", $media_usage / 1024 / 1024 ); $rv->{quota} = sprintf( "%0.1f MB", $media_quota / 1024 / 1024 ); $rv->{percentage} = sprintf( "%0.1f%%", $media_usage / $media_quota * 100 ); return DW::Template->render_template( 'media/index.tt', $rv ); } sub media_bulkedit_handler { my ( $ok, $rv ) = controller(); return $rv unless $ok; return error_ml( 'error.openid', { sitename => $LJ::SITENAMESHORT, aopts => '/create' } ) if $rv->{remote}->is_identity; my @security = ( { value => "public", text => LJ::Lang::ml('label.security.public2') }, { value => "usemask", text => LJ::Lang::ml('label.security.accesslist') }, { value => "private", text => LJ::Lang::ml('label.security.private2') }, ); $rv->{security} = \@security; my $r = DW::Request->get; if ( $r->did_post ) { my $post_args = $r->post_args; return error_ml('error.invalidauth') unless LJ::check_form_auth( $post_args->{lj_form_auth} ); if ( $post_args->{"action:edit"} ) { my %post = %{ $post_args->as_hashref || {} }; # transform our HTML field names to property names # and group by what media object they belong to # we don't care if the id or prop might not exist # right now, later steps will verify them my %props; while ( my ( $key, $val ) = each %post ) { next if $key eq "delete"; next unless $key =~ m/^(\w+)-(\d+)/; my $mediaid = $2 >> 8; if ( exists $props{$mediaid} ) { $props{$mediaid}{$1} = $val; } else { $props{$mediaid} = { $1 => $val }; } } # go through and try to fetch a media object from # each id, then try to set it's properties for my $media_key ( keys %props ) { my $media = DW::Media->new( user => $rv->{u}, mediaid => $media_key ); next unless $media; while ( my ( $key, $val ) = each %{ $props{$media_key} } ) { if ( $key eq 'security' ) { my $amask = $val eq "usemask" ? 1 : 0; $media->set_security( security => $val, allowmask => $amask ); } else { $media->prop( $key, $val ); } } } } elsif ( $post_args->{"action:delete"} ) { # FIXME: update with more efficient mass loader my @to_delete = $post_args->get_all("delete"); foreach my $id (@to_delete) { # FIXME: error messages my $mediaid = $id >> 8; my $media = DW::Media->new( user => $rv->{u}, mediaid => $mediaid ); next unless $media; $media->delete; } } } my @media = DW::Media->get_active_for_user( $rv->{remote}, width => 200, height => 200 ); my $media_usage = DW::Media->get_usage_for_user( $rv->{u} ); my $media_quota = DW::Media->get_quota_for_user( $rv->{u} ); $rv->{usage} = sprintf( "%0.3f MB", $media_usage / 1024 / 1024 ); $rv->{quota} = sprintf( "%0.1f MB", $media_quota / 1024 / 1024 ); $rv->{percentage} = sprintf( "%0.1f%%", $media_usage / $media_quota * 100 ); $rv->{ehtml} = \&LJ::ehtml; $rv->{media} = \@media; $rv->{page} = $r->get_args->{page} || '1'; $rv->{maxpage} = POSIX::ceil( scalar @media / 20 ); return DW::Template->render_template( 'media/edit.tt', $rv ); } sub media_handler { my ($opts) = @_; my ( $ok, $rv ) = controller( anonymous => 1 ); return $rv unless $ok; my $r = $rv->{r}; # Outputs an error message my $error_out = sub { my ( $code, $message ) = @_; $r->status($code); return $r->NOT_FOUND if $code == 404; # don't cache transient error responses $r->header_out( "Cache-Control" => "no-cache" ); $r->print($message); return $r->OK; }; # Old format or new format detection my ( $size, $extra, $id ) = @{ $opts->subpatterns }; my ( $width, $height ); if ( $size =~ /^(\d+)x(\d+)$/ ) { ( $width, $height ) = ( $1, $2 ); } elsif ( $size eq 'full' ) { # Do nothing, leave width/height undef } elsif ( $size =~ /^\d+$/ ) { # Should be old style format, so let's assume ( $id, $size, $extra ) = ( $size + 0, undef, undef ); } else { return $error_out->( 404, 'Not found' ); } # Ensure if a width or height are given, BOTH are given return $error_out->( 404, 'Not found' ) if defined $width xor defined $height; # Constrain widths and heights to certain valid sets if ( defined $width ) { return $error_out->( 404, 'Not found' ) unless exists $VALID_SIZES{$width} && exists $VALID_SIZES{$height}; } # Finalize id and extension checking my $ext = $opts->{format}; return $error_out->( 404, 'Not found' ) unless $id && $ext; my $anum = $id % 256; $id = ( $id - $anum ) / 256; # Load the account or error return $error_out->( 404, 'Need account name as user parameter' ) unless $opts->username; my $u = LJ::load_user_or_identity( $opts->username ) or return $error_out->( 404, 'Invalid account' ); # try to get the media object my $obj = DW::Media->new( user => $u, mediaid => $id, width => $width, height => $height ) or return $error_out->( 404, 'Not found' ); return $error_out->( 404, 'Not found' ) unless $obj->is_active && $obj->anum == $anum && $obj->ext eq $ext; # access control my $remote = $rv->{remote}; my $viewall = $r->get_args->{viewall} ? 1 : 0; # did they request it $viewall &&= defined $remote; # are they logged in $viewall &&= $remote->has_priv( 'canview', '*' ); # can they do it LJ::statushistory_add( $u->userid, $remote->userid, "viewall", $obj->url ) if $viewall; return $error_out->( 403, 'Not authorized' ) unless $viewall || $obj->visible_to($remote); # remote access, including crossposts my $refer_ok = sub { return 1 if LJ::check_referer(); my @xpost = map { $_->{domain} } DW::External::Site->get_xpost_sites; my ($ref_dom) = $r->header_in("Referer") =~ m!^https?://([^/]+)!; foreach my $domain (@xpost) { return 1 if $ref_dom eq $domain; # top level domain return 1 if $ref_dom =~ m!\.\Q$domain\E$!; # subdomain } return 0; }; return $error_out->( 403, 'Not authorized' ) unless $refer_ok->(); # limit offsite loading # load the data for this object my $dataref = DW::BlobStore->retrieve( media => $obj->mogkey ); return $error_out->( 500, 'Unexpected internal error locating file' ) unless defined $dataref && ref $dataref eq 'SCALAR'; # now we're done! $r->set_last_modified( $obj->{logtime} ) if $obj->{logtime}; $r->content_type( $obj->mimetype ); $r->print($$dataref); return $r->OK; } sub media_new_handler { my ( $ok, $rv ) = controller(); return $rv unless $ok; return error_ml( 'error.openid', { sitename => $LJ::SITENAMESHORT, aopts => '/create' } ) if $rv->{remote}->is_identity; $rv->{security} = [ { value => "public", text => LJ::Lang::ml('label.security.public2') }, { value => "usemask", text => LJ::Lang::ml('label.security.accesslist') }, { value => "private", text => LJ::Lang::ml('label.security.private2') }, ]; $rv->{default_security} = $rv->{remote}->newpost_minsecurity; $rv->{default_security} = 'usemask' if $rv->{default_security} eq 'friends'; return DW::Template->render_template( 'media/new.tt', $rv ); } sub media_index_handler { my ( $ok, $rv ) = controller(); return $rv unless $ok; my $media_usage = DW::Media->get_usage_for_user( $rv->{u} ); my $media_quota = DW::Media->get_quota_for_user( $rv->{u} ); $rv->{usage} = sprintf( "%0.3f MB", $media_usage / 1024 / 1024 ); $rv->{quota} = sprintf( "%0.1f MB", $media_quota / 1024 / 1024 ); $rv->{percentage} = sprintf( "%0.1f%%", $media_usage / $media_quota * 100 ); return DW::Template->render_template( 'media/home.tt', $rv ); } # a helper function to build the embed code, being sure to # clean user-entered fields before outputing them. sub make_embed_url { my ( $obj, %opts ) = @_; my $url = $obj->full_url; my $alt = $obj->prop('alttext') || ''; my $title = $obj->prop('title') || ''; my $embed; if ( defined $opts{type} && $opts{type} eq 'thumbnail' ) { my $thumb_url = $obj->url(); $embed = ""
            . LJ::ehtml($alt)
            . ""; } else { $embed = "" . LJ::ehtml($alt) . ""; } return $embed; } 1;