371 lines
12 KiB
Perl
371 lines
12 KiB
Perl
|
|
#!/usr/bin/perl
|
||
|
|
#
|
||
|
|
# DW::Controller::Media
|
||
|
|
#
|
||
|
|
# Displays media for a user.
|
||
|
|
#
|
||
|
|
# Authors:
|
||
|
|
# Mark Smith <mark@dreamwidth.org>
|
||
|
|
#
|
||
|
|
# 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 =
|
||
|
|
"<a href='$url'><img src='$thumb_url' alt='"
|
||
|
|
. LJ::ehtml($alt)
|
||
|
|
. "' title='"
|
||
|
|
. LJ::ehtml($title)
|
||
|
|
. "'/></a>";
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$embed =
|
||
|
|
"<img src='$url' alt='" . LJ::ehtml($alt) . "' title='" . LJ::ehtml($title) . "' />";
|
||
|
|
}
|
||
|
|
|
||
|
|
return $embed;
|
||
|
|
}
|
||
|
|
|
||
|
|
1;
|