[dw-free] Add basic media uploading
[commit: http://hg.dwscoalition.org/dw-free/rev/6fd99d890be1]
Add basic media uploading
This gives us the ability to upload media (images for now). This initial
patch only allows us to submit photos via mobile. More to come. Very soon.
Patch by
mark.
Files modified:
Add basic media uploading
This gives us the ability to upload media (images for now). This initial
patch only allows us to submit photos via mobile. More to come. Very soon.
Patch by
![[staff profile]](https://www.dreamwidth.org/img/silk/identity/user_staff.png)
Files modified:
- bin/checkconfig.pl
- bin/incoming-mail-inject.pl
- bin/upgrading/update-db-general.pl
- cgi-bin/DW/Collection.pm
- cgi-bin/DW/Collection/Item.pm
- cgi-bin/DW/Controller/Media.pm
- cgi-bin/DW/Media.pm
- cgi-bin/DW/Media/Base.pm
- cgi-bin/DW/Media/Photo.pm
- cgi-bin/LJ/DB.pm
- cgi-bin/LJ/Emailpost.pm
- cgi-bin/LJ/Global/Defaults.pm
- cgi-bin/ljlib.pl
- htdocs/js/media/bulkedit.js
- htdocs/stc/media.css
- views/media/edit.tt
- views/media/edit.tt.text
- views/media/index.tt
- views/media/index.tt.text
-------------------------------------------------------------------------------- diff -r 267bb682c94f -r 6fd99d890be1 bin/checkconfig.pl --- a/bin/checkconfig.pl Fri Jul 20 19:39:16 2012 -0700 +++ b/bin/checkconfig.pl Fri Jul 20 21:16:56 2012 -0700 @@ -244,6 +244,10 @@ deb => 'libtext-markdown-perl', opt => 'Required to allow using Markdown in entries.', }, + "File::Type" => { + deb => 'libfile-type-perl', + opt => 'For media storage', + } ); diff -r 267bb682c94f -r 6fd99d890be1 bin/incoming-mail-inject.pl --- a/bin/incoming-mail-inject.pl Fri Jul 20 19:39:16 2012 -0700 +++ b/bin/incoming-mail-inject.pl Fri Jul 20 21:16:56 2012 -0700 @@ -19,6 +19,7 @@ $ENV{LJHOME} ||= "/home/lj"; } use lib "$ENV{LJHOME}/cgi-bin"; +use lib "$ENV{LJHOME}/extlib/lib/perl5"; require "$ENV{LJHOME}/cgi-bin/ljlib.pl"; use Class::Autouse qw( LJ::IncomingEmailHandle diff -r 267bb682c94f -r 6fd99d890be1 bin/upgrading/update-db-general.pl --- a/bin/upgrading/update-db-general.pl Fri Jul 20 19:39:16 2012 -0700 +++ b/bin/upgrading/update-db-general.pl Fri Jul 20 21:16:56 2012 -0700 @@ -3131,6 +3131,56 @@ ) EOC +# FIXME: add alt text, etc. mediaprops? +register_tablecreate("media", <<'EOC'); +CREATE TABLE `media` ( + `userid` int(10) unsigned NOT NULL, + `mediaid` int(10) unsigned NOT NULL, + `anum` tinyint(3) unsigned NOT NULL, + `ext` varchar(10) NOT NULL, + `state` char(1) NOT NULL DEFAULT 'A', + `mediatype` tinyint(3) unsigned NOT NULL, + `security` enum('public','private','usemask') NOT NULL DEFAULT 'public', + `allowmask` bigint(20) unsigned NOT NULL DEFAULT '0', + `logtime` int(10) unsigned NOT NULL, + `mimetype` varchar(60) NOT NULL, + `filesize` int(10) unsigned NOT NULL, + PRIMARY KEY (`userid`,`mediaid`) +) +EOC + +register_tablecreate("collections", <<'EOC'); +CREATE TABLE `collections` ( + `userid` int(10) unsigned NOT NULL, + `colid` int(10) unsigned NOT NULL, + `paruserid` int(10) unsigned NOT NULL, + `parcolid` int(10) unsigned NOT NULL, + `anum` tinyint(3) unsigned NOT NULL, + `state` char(1) NOT NULL DEFAULT 'A', + `security` enum('public','private','usemask') NOT NULL DEFAULT 'public', + `allowmask` bigint(20) unsigned NOT NULL DEFAULT '0', + `logtime` int(10) unsigned NOT NULL, + PRIMARY KEY (`userid`,`colid`), + INDEX (`paruserid`,`parcolid`) +) +EOC + +# FIXME: the indexes here are totally whack +register_tablecreate("collection_items", <<'EOC'); +CREATE TABLE `collection_items` ( + `userid` int(10) unsigned NOT NULL, + `colitemid` int(10) unsigned NOT NULL, + `colid` int(10) unsigned NOT NULL, + `itemtype` tinyint(3) unsigned NOT NULL, + `itemownerid` int(10) unsigned NOT NULL, + `itemid` int(10) unsigned NOT NULL, + `logtime` int(10) unsigned NOT NULL, + PRIMARY KEY (`userid`,`colid`,`colitemid`), + UNIQUE (`userid`,`colid`,`itemtype`,`itemownerid`,`itemid`), + INDEX (`itemtype`,`itemownerid`,`itemid`) +) +EOC + # NOTE: new table declarations go ABOVE here ;) ### changes diff -r 267bb682c94f -r 6fd99d890be1 cgi-bin/DW/Collection.pm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cgi-bin/DW/Collection.pm Fri Jul 20 21:16:56 2012 -0700 @@ -0,0 +1,128 @@ +#!/usr/bin/perl +# +# DW::Collection +# +# This represents a collection -- aka, a gallery of various items that you +# have collected together into a category. +# +# Authors: +# Mark Smith <mark@dreamwidth.org> +# +# Copyright (c) 2012 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'. +# +# This module allows you to organize a group of things that exist on the site +# for viewing and group commenting. Think of it as a gallery organizer that +# lets you put together various things and do stuff. Yeah, isn't that vague? +# + + +package DW::Collection; + +use strict; +use Carp qw/ croak confess /; + +use DW::Collection::Item; + +# Load a collection for a user, this is not how you create one +sub new { + my ( $class, %opts ) = @_; + confess 'Need a user and colid key' + unless $opts{user} && LJ::isu( $opts{user} ) && $opts{colid}; + + my $hr = $opts{user}->selectrow_hashref( + q{SELECT userid, colid, anum, state, security, allowmask, logtime, + paruserid, parcolid + FROM collections WHERE userid = ? AND colid = ?}, + undef, $opts{user}->id, $opts{colid} + ); + return if $opts{user}->err || ! $hr; + + return bless $hr, $class; +} + +# accessors for our internal data +sub u { $_[0]->{_u} ||= LJ::load_userid( $_[0]->{userid} ) } +sub userid { $_[0]->{userid} } +sub id { $_[0]->{colid} } +sub parent_userid { $_[0]->{paruserid} } +sub parent_id { $_[0]->{parcolid} } +sub anum { $_[0]->{anum} } +sub displayid { $_[0]->{colid} * 256 + $_[0]->{anum} } +sub state { $_[0]->{state} } +sub security { $_[0]->{security} } +sub allowmask { $_[0]->{allowmask} } +sub logtime { $_[0]->{logtime} } + +# instantiate and load our parent collection +sub parent { + my $self = $_[0]; + return undef unless $self->{paruserid}; + + my $paru = LJ::load_userid( $self->{paruserid} ); + return DW::Collection->new( user => $paru, colid => $self->{parcolid} ); +} + +# helper state subs +sub is_active { $_[0]->state eq 'A' } + +# load items for the collection +sub items { + my $self = $_[0]; + return wantarray ? @{$self->{_items}} : $self->{_items} + if exists $self->{_items}; + + my $u = $self->u; + my $hr = $u->selectall_hashref( + q{SELECT userid, colitemid, colid, itemtype, itemownerid, itemid, logtime + FROM collection_items WHERE userid = ? AND colid = ?}, + 'colitemid', undef, $u->id, $self->id + ); + croak $u->errstr if $u->err; + return () unless $hr; + + my @res; + foreach my $colitemid (keys %$hr) { + my $item = $hr->{$colitemid}; + push @res, DW::Collection::Item->new_from_row( %$item ); + } + $self->{_items} = \@res; + + return wantarray ? @res : \@res; +} + +# if user can see this +# FIXME: move this out to a general function? +sub visible_to { + my ( $self, $other_u ) = @_; + return 0 unless $other_u; + + # test that the user that owns this item is still visible, that we're still active, + # and return a true if we're public. + my $u = $self->u; + return 0 unless $self->is_active && $u->is_visible; + return 1 if $self->security eq 'public'; + + # at this point, if we don't have a remote user, fail + return 0 unless LJ::isu( $other_u ); + + # private check. if it's us, allow, else fail. + return 1 if $u->equals( $other_u ); + return 0 if $self->security eq 'private'; + + # simple usemask checking... + if ( $self->security eq 'usemask' ) { + my $gmask = $u->trustmask( $other_u ); + + my $allowed = int $gmask & int $self->allowmask; + return $allowed ? 1 : 0; + } + + # totally failed. + return 0; +} + +1; diff -r 267bb682c94f -r 6fd99d890be1 cgi-bin/DW/Collection/Item.pm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cgi-bin/DW/Collection/Item.pm Fri Jul 20 21:16:56 2012 -0700 @@ -0,0 +1,59 @@ +#!/usr/bin/perl +# +# DW::Collection::Item +# +# This is the base module to represent items in a collection. +# +# Authors: +# Mark Smith <mark@dreamwidth.org> +# +# Copyright (c) 2012 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::Collection::Item; + +use strict; +use Carp qw/ croak confess /; + +use constant TYPE_MEDIA => 1; +use constant TYPE_ENTRY => 2; +use constant TYPE_COMMENT => 3; + +sub new_from_row { + my ( $class, %opts ) = @_; + + # simply bless and return then, we don't do anything smart here yet + return bless \%opts, $class; +} + +sub u { $_[0]->{_u} ||= LJ::load_userid( $_[0]->{userid} ) } +sub userid { $_[0]->{userid} } +sub id { $_[0]->{colitemid} } +sub colid { $_[0]->{colid} } +sub itemtype { $_[0]->{itemtype} } +sub itemownerid { $_[0]->{itemownerid} } +sub itemid { $_[0]->{itemid} } +sub logtime { $_[0]->{logtime} } + +# this returns an object for the thing we represent +sub resolve { + my $self = $_[0]; + + my $owneru = LJ::load_userid( $self->{itemownerid} ); + if ( $self->{itemtype} == TYPE_MEDIA ) { + return DW::Media->new( user => $owneru, mediaid => $self->{itemid} ); + + } elsif ( $self->{itemtype} == TYPE_ENTRY ) { + + } elsif ( $self->{itemtype} == TYPE_COMMENT ) { + + } + + croak 'Invalid type in resolution.'; +} + +1; diff -r 267bb682c94f -r 6fd99d890be1 cgi-bin/DW/Controller/Media.pm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cgi-bin/DW/Controller/Media.pm Fri Jul 20 21:16:56 2012 -0700 @@ -0,0 +1,138 @@ +#!/usr/bin/perl +# +# DW::Controller::Media +# +# Displays media for a user. +# +# Authors: +# Mark Smith <mark@dreamwidth.org> +# +# Copyright (c) 2010 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 warnings; +use DW::Routing; +use DW::Request; + +use DW::Controller; + +DW::Routing->register_regex( qr|^/file/(\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 ); + +sub media_manage_handler { + my ( $ok, $rv ) = controller(); + return $rv unless $ok; + + # load all of a user's media. this is inefficient and won't be like this forever, + # but it's simple for now... + $rv->{media} = [ DW::Media->get_active_for_user( $rv->{remote} ) ]; + + return DW::Template->render_template( 'media/index.tt', $rv ); +} + +sub media_bulkedit_handler { + my ( $ok, $rv ) = controller(); + return $rv unless $ok; + + 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||{}}; + while ( my ($key, $secval) = each %post ) { + next unless $key =~ m/^security-(\d+)/; + my $mediaid = $1 >> 8; + my $media = DW::Media->new( user => $rv->{u}, mediaid => $mediaid ); + next unless $media; + + my $amask = $secval eq "usemask" ? 1 : 0; + $media->set_security( security => $secval, allowmask => $amask ); + } + } 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; + } + } + } + + $rv->{media} = [ DW::Media->get_active_for_user( $rv->{remote} ) ]; + + return DW::Template->render_template( 'media/edit.tt', $rv ); +} + +sub media_handler { + my $opts = shift; + my $r = DW::Request->get; + + # Outputs an error message + my $error_out = sub { + my ( $code, $message ) = @_; + $r->status( $code ); + return $r->NOT_FOUND if $code == 404; + + $r->print( $message ); + return $r->OK; + }; + + # get the media id + my ( $id, $ext ) = ( $opts->subpatterns->[0], $opts->{format} ); + $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 ) + 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 +# FIXME: support viewall + return $error_out->( 403, 'Not authorized' ) + unless $obj->visible_to( LJ::get_remote() ); + + # load the data for this object +# FIXME: support X-REPROXY headers here + my $dataref = LJ::mogclient()->get_file_data( $obj->mogkey ); + return $error_out->( 500, 'Unexpected internal error locating file' ) + unless defined $dataref && ref $dataref eq 'SCALAR'; + + # now we're done! + $r->content_type( $obj->mimetype ); + $r->print( $$dataref ); + return $r->OK; +} + +1; diff -r 267bb682c94f -r 6fd99d890be1 cgi-bin/DW/Media.pm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cgi-bin/DW/Media.pm Fri Jul 20 21:16:56 2012 -0700 @@ -0,0 +1,143 @@ +#!/usr/bin/perl +# +# DW::Media +# +# Base module for handling media storage and retrieval. Media is defined as +# some item (document, photo, video, audio, etc) that is owned by a user, +# may be tagged, sorted, and secured. +# +# This is the base/generic media class, there are other classes. +# +# Authors: +# Mark Smith <mark@dreamwidth.org> +# +# Copyright (c) 2010 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::Media; + +use strict; +use Carp qw/ croak confess /; +use File::Type; + +use DW::Media::Photo; + +use constant TYPE_PHOTO => 1; + +sub new { + my ( $class, %opts ) = @_; + confess 'Need a user and mediaid key' + unless $opts{user} && LJ::isu( $opts{user} ) && $opts{mediaid}; + + my $hr = $opts{user}->selectrow_hashref( + q{SELECT userid, mediaid, anum, ext, state, mediatype, security, allowmask, + logtime, mimetype, filesize + FROM media WHERE userid = ? AND mediaid = ?}, + undef, $opts{user}->id, $opts{mediaid} + ); + return if $opts{user}->err || ! $hr; + + return DW::Media::Photo->new_from_row( %$hr ) + if $hr->{mediatype} == TYPE_PHOTO; + + croak 'Got an invalid row, or a type we do not support yet.'; +} + +sub upload_media { + my ( $class, %opts ) = @_; + confess 'Need a user key' + unless $opts{user} && LJ::isu( $opts{user} ); + confess 'Need a file key or data key' + unless $opts{file} && -e $opts{file} || $opts{data}; + + # we need a mogilefs client or we can't store media + my $mog = LJ::mogclient() + or croak 'Sorry, MogileFS is not currently available.'; + + # okay, we know who it's for and what it is, that's all we really need. + if ( $opts{file} ) { + open FILE, "<$opts{file}" + or croak "Unable to load file to store."; + { local $/ = undef; $opts{data} = <FILE>; } + close FILE; + } + + # if no data then die + croak 'Found no data to store.' unless $opts{data}; + + # get type of file + my $mime = File::Type->new->mime_type( $opts{data} ) + or croak 'Unable to get MIME-type for uploaded file.'; + + # now get what type this is, from allowed mime types + my ( $type, $ext ) = DW::Media->get_upload_type( $mime ); + croak 'Sorry, that file type is not currently allowed.' + unless $type && $ext; + + # set the security + my $sec = $opts{security} || 'public'; + croak 'Invalid security for uploaded file.' + unless $sec =~ /^(?:public|private|usemask)$/; + $opts{allowmask} = 0 unless defined $opts{allowmask} && $sec eq 'usemask'; + + # now we can cook -- allocate an id and upload + my $id = LJ::alloc_user_counter( $opts{user}, 'A' ) + or croak 'Unable to allocate user counter for uploaded file.'; + $opts{user}->do( + q{INSERT INTO media (userid, mediaid, anum, ext, state, mediatype, security, allowmask, + logtime, mimetype, filesize) VALUES (?, ?, ?, ?, 'A', ?, ?, ?, UNIX_TIMESTAMP(), ?, ?)}, + undef, $opts{user}->id, $id, int(rand(256)), $ext, $type, $sec, $opts{allowmask}, + $mime, length $opts{data} + ); + croak "Failed to insert media row: " . $opts{user}->errstr . "." + if $opts{user}->err; + + # now get this back as an object + my $obj = DW::Media->new( user => $opts{user}, mediaid => $id ); + + # now we have to stick this in MogileFS + # FIXME: have different MogileFS classes for different media types + my $fh = $mog->new_file( $obj->mogkey, 'media' ) + or croak 'Unable to instantiate file in MogileFS.'; # FIXME: nuke the row! + $fh->print( $opts{data} ); + $fh->close + or croak 'Unable to save file to MogileFS.'; # FIXME: nuke the row! + + # uploaded, so return an object for this item + return $obj; +} + +sub get_upload_type { + my ( $class, $mime ) = @_; + + # FIXME: This may not cover everything. :-) + return (TYPE_PHOTO, 'jpg') if $mime eq 'image/jpeg'; + return (TYPE_PHOTO, 'gif') if $mime eq 'image/gif'; + return (TYPE_PHOTO, 'png') if $mime eq 'image/png' || $mime eq 'image/x-png'; + + return (undef, undef); +} + +sub get_active_for_user { + my ( $class, $u ) = @_; + confess 'Invalid user' unless LJ::isu( $u ); + + # get all active rows for this user + my $rows = $u->selectcol_arrayref( + q{SELECT mediaid FROM media WHERE userid = ? AND state = 'A'}, + undef, $u->id + ); + croak 'Failed to select rows: ' . $u->errstr . '.' if $u->err; + return () unless $rows && ref $rows eq 'ARRAY'; + + # construct media objects for each of the items and return that + return sort { $b->logtime <=> $a->logtime } + map { DW::Media->new( user => $u, mediaid => $_ ) } @$rows; +} + + +1; diff -r 267bb682c94f -r 6fd99d890be1 cgi-bin/DW/Media/Base.pm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cgi-bin/DW/Media/Base.pm Fri Jul 20 21:16:56 2012 -0700 @@ -0,0 +1,146 @@ +#!/usr/bin/perl +# +# DW::Media::Base +# +# This is the base module to represent media items. You should never instantiate +# this class directly... +# +# Authors: +# Mark Smith <mark@dreamwidth.org> +# +# Copyright (c) 2010 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::Media::Base; + +use strict; +use Carp qw/ croak confess /; + +sub new_from_row { + my ( $class, %opts ) = @_; + + # if the class is base, intuit something... + confess 'Please do not build the base class.' + if $class eq 'DW::Media::Base'; + + # simply bless and return then, we don't do anything smart here yet + return bless \%opts, $class; +} + +# accessors for our internal data +sub u { LJ::load_userid( $_[0]->{userid} ) } +sub userid { $_[0]->{userid} } +sub id { $_[0]->{mediaid} } +sub anum { $_[0]->{anum} } +sub displayid { $_[0]->{mediaid} * 256 + $_[0]->{anum} } +sub state { $_[0]->{state} } +sub mediatype { $_[0]->{mediatype} } +sub security { $_[0]->{security} } +sub allowmask { $_[0]->{allowmask} } +sub logtime { $_[0]->{logtime} } +sub mimetype { $_[0]->{mimetype} } +sub size { $_[0]->{filesize} } +sub mogkey { "media:$_[0]->{userid}:$_[0]->{mediaid}" } +sub ext { $_[0]->{ext} } + +# helper state subs +sub is_active { $_[0]->state eq 'A' } +sub is_deleted { $_[0]->state eq 'D' } + +# construct a URL for this resource +sub url { + my ( $self, $extra ) = ( $_[0], '' ); + if ( $_[1] && ref $_[1] eq 'HASH' ) { + # If either width or height is specified, add the extra output + my ( $w, $h ) = ( $_[1]->{width}||'', $_[1]->{height}||'' ); + $extra = $w . 'x' . $h . '/' + if $w || $h; + } + return $self->u->journal_base . '/file/' . $extra . $self->displayid . '.' . $self->ext; +} + +# if user can see this +sub visible_to { + my ( $self, $other_u ) = @_; + + # test that the user that owns this item is still visible, that we're still active, + # and return a true if we're public. + my $u = $self->u; + return 0 unless $self->is_active && $u->is_visible; + return 1 if $self->security eq 'public'; + + # at this point, if we don't have a remote user, fail + return 0 unless LJ::isu( $other_u ); + + # private check. if it's us, allow, else fail. + return 1 if $u->equals( $other_u ); + return 0 if $self->security eq 'private'; + + # simple usemask checking... + if ( $self->security eq 'usemask' ) { + my $gmask = $u->trustmask( $other_u ); + + my $allowed = int $gmask & int $self->allowmask; + return $allowed ? 1 : 0; + } + + # totally failed. + return 0; +} + +# we delete the actual file +# but we keep the metadata around for record-keeping purpose +# returns 1/0 on success or failure +sub delete { + my $self = $_[0]; + return 0 if $self->is_deleted; + + # we need a mogilefs client or we can't edit media + my $mog = LJ::mogclient() + or croak 'Sorry, MogileFS is not currently available.'; + my $u = $self->u + or croak 'Sorry, unable to load the user.'; + + $u->do( q{UPDATE media SET state = 'D' WHERE userid = ? AND mediaid = ?}, + undef, $u->id, $self->id ); + confess $u->errstr if $u->err; + + $self->{state} = 'D'; + + LJ::mogclient()->delete( $self->mogkey ); + + return 1; +} + +# change the security of this item. returns 0/1 for successfulness. +sub set_security { + my ( $self, %opts ) = @_; + return 0 if $self->is_deleted; + + my $security = $opts{security}; + confess 'Invalid security type passed to set_security.' + unless $security =~ /^(?:private|public|usemask)$/; + + my $mask = 0; + if ( $security eq 'usemask' ) { + $mask = int $opts{allowmask}; + } + + my $u = $self->u + or croak 'Sorry, unable to load the user.'; + $u->do( q{UPDATE media SET security = ?, allowmask = ? WHERE userid = ? AND mediaid = ?}, + undef, $security, $mask, $u->id, $self->id ); + confess $u->errstr if $u->err; + + $self->{security} = $security; + $self->{allowmask} = $mask; + + return 1; +} + + +1; diff -r 267bb682c94f -r 6fd99d890be1 cgi-bin/DW/Media/Photo.pm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cgi-bin/DW/Media/Photo.pm Fri Jul 20 21:16:56 2012 -0700 @@ -0,0 +1,26 @@ +#!/usr/bin/perl +# +# DW::Media::Photo +# +# Special module for photos for the DW media system. +# +# Authors: +# Mark Smith <mark@dreamwidth.org> +# +# Copyright (c) 2010 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::Media::Photo; + +use strict; +use Carp qw/ croak confess /; + +use DW::Media::Base; +use base 'DW::Media::Base'; + + +1; diff -r 267bb682c94f -r 6fd99d890be1 cgi-bin/LJ/DB.pm --- a/cgi-bin/LJ/DB.pm Fri Jul 20 19:39:16 2012 -0700 +++ b/cgi-bin/LJ/DB.pm Fri Jul 20 21:16:56 2012 -0700 @@ -62,6 +62,7 @@ "notifyarchive", "notifybookmarks", "pollprop2", "embedcontent_preview", "logprop_history", "import_status", "externalaccount", "content_filters", "content_filter_data", "userpicmap3", + "media", "collections", "collection_items", ); # keep track of what db locks we have out @@ -727,6 +728,8 @@ # 'D' == 'moDule embed contents', 'I' == Import data block # 'Z' == import status item, 'X' == eXternal account # 'F' == filter id, 'Y' = pic/keYword mapping id +# 'A' == mediA item id, 'O' == cOllection id, +# 'N' == collectioN item id # sub alloc_user_counter { @@ -735,7 +738,7 @@ ################################################################## # IF YOU UPDATE THIS MAKE SURE YOU ADD INITIALIZATION CODE BELOW # - return undef unless $dom =~ /^[LTMPSRKCOVEQGDIZXFY]$/; # + return undef unless $dom =~ /^[LTMPSRKCOVEQGDIZXFYA]$/; # ################################################################## my $dbh = LJ::get_db_writer(); @@ -852,7 +855,16 @@ undef, $uid); } elsif ($dom eq "Y") { $newmax = $u->selectrow_array("SELECT MAX(mapid) FROM userpicmap3 WHERE userid=?", - undef, $uid); + undef, $uid); + } elsif ($dom eq "A") { + $newmax = $u->selectrow_array("SELECT MAX(mediaid) FROM media WHERE userid = ?", + undef, $uid); + } elsif ($dom eq "O") { + $newmax = $u->selectrow_array("SELECT MAX(colid) FROM collections WHERE userid = ?", + undef, $uid); + } elsif ($dom eq "N") { + $newmax = $u->selectrow_array("SELECT MAX(colitemid) FROM collection_items WHERE userid = ?", + undef, $uid); } else { die "No user counter initializer defined for area '$dom'.\n"; } diff -r 267bb682c94f -r 6fd99d890be1 cgi-bin/LJ/Emailpost.pm --- a/cgi-bin/LJ/Emailpost.pm Fri Jul 20 19:39:16 2012 -0700 +++ b/cgi-bin/LJ/Emailpost.pm Fri Jul 20 21:16:56 2012 -0700 @@ -58,7 +58,7 @@ $format, $tent, # pict upload vars -# $fb_upload, $fb_upload_errstr, + $fb_upload, $fb_upload_errstr, ); $head = $entity->head; @@ -129,7 +129,7 @@ $body =~ s/\s+$//; # Snag charset and do utf-8 conversion - my $content_type = $tent->head->get('Content-type:'); + my $content_type = $tent ? $tent->head->get('Content-type:') : ''; $charset = $1 if $content_type =~ /\bcharset=['\"]?(\S+?)['\"]?[\s\;]/i; $format = $1 if $content_type =~ /\bformat=['\"]?(\S+?)['\"]?[\s\;]/i; my $delsp; @@ -470,48 +470,50 @@ $post_headers{'imgsecurity'} = lc($post_headers{'imgsecurity'}) || $u->{'emailpost_imgsecurity'} || 'public'; $post_headers{'imgsecurity'} = 'private' - unless $post_headers{'imgsecurity'} =~ /^(private|regusers|friends|public)$/; + unless $post_headers{'imgsecurity'} =~ /^(private|access|public)$/; + + # FIXME: translate security into usemask/allowmask combo # upload picture attachments to fotobilder. # undef return value? retry posting for later. -# $fb_upload = upload_images( -# $entity, $u, -# \$fb_upload_errstr, -# { -# imgsec => $post_headers{'imgsecurity'}, -# galname => $post_headers{'gallery'} || $u->{'emailpost_gallery'} -# } -# ) || return $err->( $fb_upload_errstr, { retry => 1 } ); -# -# # if we found and successfully uploaded some images... -# if (ref $fb_upload eq 'ARRAY') { -# my $fb_html = LJ::FBUpload::make_html( $u, $fb_upload, \%post_headers ); -# ## -# ## A problem was here: -# ## $body is utf-8 text without utf-8 flag (see Unicode::MapUTF8::to_utf8), -# ## $fb_html is ASCII with utf-8 flag on (because uploaded image description -# ## is parsed by XML::Simple, see cgi-bin/fbupload.pl, line 153). -# ## When 2 strings are concatenated, $body is auto-converted (incorrectly) -# ## from Latin-1 to UTF-8. -# ## -# $fb_html = Encode::encode("utf8", $fb_html) if Encode::is_utf8($fb_html); -# $body .= $fb_html; -# } -# -# # at this point, there are either no images in the message ($fb_upload == 1) -# # or we had some error during upload that we may or may not want to retry -# # from. $fb_upload contains the http error code. -# if ( $fb_upload == 400 # bad http request -# || $fb_upload == 1401 # user has exceeded the fb quota -# || $fb_upload == 1402 # user has exceeded the fb quota -# ) { -# # don't retry these errors, go ahead and post the body -# # to the journal, postfixed with the remote error. -# $body .= "\n"; -# $body .= "(Your picture was not posted: $fb_upload_errstr)"; -# } -# -# # Fotobilder server error. Retry. + $fb_upload = upload_images( + $entity, $u, + \$fb_upload_errstr, + { + security => $post_headers{'imgsecurity'}, + } + ) || return $err->( $fb_upload_errstr, { retry => 1 } ); + + # if we found and successfully uploaded some images... + if (ref $fb_upload eq 'ARRAY') { + my $fb_html = join( '<br />', map { '<img src="' . $_->url . '" />' } @$fb_upload ); + + ## + ## A problem was here: + ## $body is utf-8 text without utf-8 flag (see Unicode::MapUTF8::to_utf8), + ## $fb_html is ASCII with utf-8 flag on (because uploaded image description + ## is parsed by XML::Simple, see cgi-bin/fbupload.pl, line 153). + ## When 2 strings are concatenated, $body is auto-converted (incorrectly) + ## from Latin-1 to UTF-8. + ## + $fb_html = Encode::encode("utf8", $fb_html) if Encode::is_utf8($fb_html); + $body .= $fb_html; + } + + # at this point, there are either no images in the message ($fb_upload == 1) + # or we had some error during upload that we may or may not want to retry + # from. $fb_upload contains the http error code. + if ( $fb_upload == 400 # bad http request + || $fb_upload == 1401 # user has exceeded the fb quota + || $fb_upload == 1402 # user has exceeded the fb quota + ) { + # don't retry these errors, go ahead and post the body + # to the journal, postfixed with the remote error. + $body .= "\n"; + $body .= "(Your picture was not posted: $fb_upload_errstr)"; + } + + # Fotobilder server error. Retry. # return $err->( $fb_upload_errstr, { retry => 1 } ) if $fb_upload == 500; # build lj entry @@ -707,59 +709,24 @@ # undef - failure during upload # http_code - failure during upload w/ code # hashref - { title => url } for each image uploaded -# sub upload_images -# { -# my ($entity, $u, $rv, $opts) = @_; +sub upload_images { + my ( $entity, $u, $rv, $opts ) = @_; + +# FIXME: check if user can do this # return 1 unless LJ::get_cap($u, 'fb_can_upload') && $LJ::FB_SITEROOT; -# -# my @imgs = get_entity($entity, 'image'); -# return 1 unless scalar @imgs; -# -# my @images; -# foreach my $img_entity (@imgs) { -# my $img = $img_entity->bodyhandle; -# my $path = $img->path; -# -# my $result = LJ::FBUpload::do_upload( -# $u, $rv, -# { -# path => $path, -# rawdata => \$img->as_string, -# imgsec => $opts->{'imgsec'}, -# galname => $opts->{'galname'}, -# } -# ); -# -# # do upload() returned undef? This is a posting error -# # that should most likely be retried, due to something -# # wrong on our side of things. -# return if ! defined $result && $$rv; -# -# # http error during upload attempt -# # decide retry based on error type in caller -# return $result unless ref $result; -# -# # examine $result for errors -# if ($result->{Error}->{code}) { -# $$rv = $result->{Error}->{content}; -# -# # add 1000 to error code, so we can easily tell the -# # difference between fb protocol error and -# # http error when checking results. -# return $result->{Error}->{code} + 1000; -# } -# -# push @images, { -# url => $result->{URL}, -# width => $result->{Width}, -# height => $result->{Height}, -# title => $result->{Title}, -# }; -# } -# -# return \@images if scalar @images; -# return; -# } + + my @imgs = get_entity( $entity, 'image' ); + return 1 unless scalar @imgs; + + my @images; + foreach my $img_entity ( @imgs ) { + my $obj = DW::Media->upload_media( user => $u, data => $img_entity->bodyhandle->as_string, %$opts ); + push @images, $obj if $obj; + } + + return unless scalar @images; + return \@images; +} sub dblog { diff -r 267bb682c94f -r 6fd99d890be1 cgi-bin/LJ/Global/Defaults.pm --- a/cgi-bin/LJ/Global/Defaults.pm Fri Jul 20 19:39:16 2012 -0700 +++ b/cgi-bin/LJ/Global/Defaults.pm Fri Jul 20 21:16:56 2012 -0700 @@ -180,7 +180,6 @@ # defined a MOGILEFS_CONFIG hash in etc/config.pl and you explicitly set # at least the hosts key to be an arrayref of ip:port combinations # indicating where to reach your local MogileFS server. - %MOGILEFS_CONFIG = () unless defined %MOGILEFS_CONFIG; $MOGILEFS_CONFIG{domain} ||= 'livejournal'; $MOGILEFS_CONFIG{timeout} ||= 3; @@ -188,6 +187,7 @@ $MOGILEFS_CONFIG{classes}->{temp} ||= 2; $MOGILEFS_CONFIG{classes}->{userpics} ||= 3; $MOGILEFS_CONFIG{classes}->{vgifts} ||= 3; + $MOGILEFS_CONFIG{classes}->{media} ||= 3; # Default to allow all reproxying. %REPROXY_DISABLE = () unless %REPROXY_DISABLE; @@ -357,20 +357,20 @@ "js/hoverIntent.js" => "js/hoverIntent.minified.js", "js/tooltip.js" => "js/tooltip.min.js", - ) unless defined %LJ::MINIFY; + ) unless %LJ::MINIFY; # mapping of captcha type to specific desired implementation %CAPTCHA_TYPES = ( "T" => "textcaptcha", # "T" is for text "I" => "recaptcha", # "I" is for image - ) unless defined %CAPTCHA_TYPES; + ) unless %CAPTCHA_TYPES; $DEFAULT_CAPTCHA_TYPE ||= "T"; # default location of community posting guidelines $DEFAULT_POSTING_GUIDELINES_LOC ||= "N"; # Secrets - %SECRETS = () unless defined %SECRETS; + %SECRETS = () unless %SECRETS; # Userpic maximum. No user can have more than this. $USERPIC_MAXIMUM ||= 500; diff -r 267bb682c94f -r 6fd99d890be1 cgi-bin/ljlib.pl --- a/cgi-bin/ljlib.pl Fri Jul 20 19:39:16 2012 -0700 +++ b/cgi-bin/ljlib.pl Fri Jul 20 21:16:56 2012 -0700 @@ -89,6 +89,14 @@ use DW::LatestFeed; use LJ::Keywords; use LJ::Procnotify; +use LJ::DB; +use LJ::Tags; +use LJ::TextUtil; +use LJ::Time; +use LJ::Capabilities; +use DW::Mood; +use LJ::Global::Img; # defines LJ::Img +use DW::Media; # make Unicode::MapUTF8 autoload: sub Unicode::MapUTF8::AUTOLOAD { diff -r 267bb682c94f -r 6fd99d890be1 htdocs/js/media/bulkedit.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/htdocs/js/media/bulkedit.js Fri Jul 20 21:16:56 2012 -0700 @@ -0,0 +1,23 @@ +jQuery(function($) { + function selectForDelete() { + var $this = $(this); + $this.closest(".inner").toggleClass("ui-state-highlight", $this.is(":checked") ); + } + + function doDelete(e) { + var form = this.form; + e.preventDefault(); + var response = confirm( "Are you sure you want to delete these files?" ); + + if ( response ) { + var data = $(form).serializeArray(); + data.push({ name: "action:delete", value: true}); + $.post( form.action, data, function() { + $("#media-manage input[name=delete]:checked").closest(".media-item").fadeOut().remove(); + } ); + } + } + + $("#media-manage input[name=delete]").change(selectForDelete); + $("#media-manage input[name='action:delete']").click(doDelete); +}) \ No newline at end of file diff -r 267bb682c94f -r 6fd99d890be1 htdocs/stc/media.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/htdocs/stc/media.css Fri Jul 20 21:16:56 2012 -0700 @@ -0,0 +1,101 @@ +#media-grid .media-item { + list-style-type: none; + display: inline-block; + vertical-align: top; + margin: 2em 1em; + outline: 1px #666 solid; + padding: 1em; + min-width: 50px; + -webkit-box-shadow: 1px 2px 3px 1px rgba(200, 200, 200, 1); + box-shadow: 1px 2px 3px 1px rgba(200, 200, 200, 1); +} + +#media-grid .media-item img { + max-height: 300px; + outline: 1px #eee solid; + margin: 0 auto 1em auto; + display: block; +} + +#media-grid .media-item p { + margin: 0; padding: 0; + text-align: center; +} + +#media-grid .media-item .name { + font-weight: bold; +} + +#media-grid .media-item .filesize { + font-size: smaller; + font-style: italic; +} + +#media-list .media-item { + list-style-type: none; + display: block; +} + +#media-list .media-item .inner { + padding: 1em 0; +} + +#media-list .media-item p, #media-list .media-item select, #media-list .media-item img { + display: inline-block; + margin: 0 1em; +} + +#media-list .media-item .media { + width: 200px; + padding: 0 1em 0 0; +} + +#media-list .media-item img { + max-width: 200px; + max-height: 200px; +} + +#media-list .details { + width: 10em; +} +#media-list .filesize { + font-size: smaller; + font-style: italic; + display: none; +} + +#media-list .name { + display: none; +} + +fieldset.submit { + text-align: right; +} +fieldset.submit input { + margin-top: 2em; + border-width: 1px; + border-style: solid; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + font-weight: bold; + line-height: 1; + padding: 3px 8px 4px; + text-align: center; + + font-size: 1.2em; + line-height: 1.5em; +} + +fieldset.destructive input { + border: 0; + background: transparent; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; + + font-size: 0.9em; + text-decoration: underline; + margin-right: 2em; + float: left; +} diff -r 267bb682c94f -r 6fd99d890be1 views/media/edit.tt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/views/media/edit.tt Fri Jul 20 21:16:56 2012 -0700 @@ -0,0 +1,36 @@ +[% sections.title = '.title' | ml %] + +[% CALL dw.active_resource_group( "jquery") %] +[% CALL dw.need_res( { group => "jquery" }, + "js/media/bulkedit.js" + "stc/media.css" +) %] + +<p>[% ".intro" | ml %]</p> +[% IF media.size %] +<form id="media-manage" method="POST" action="[%site.root%]/file/edit"> +[%- dw.form_auth -%] +<fieldset id="media-list"> +<ul> + [%- FOREACH obj IN media -%] + <li class='media-item [% loop.count % 2 ? 'even' : 'odd' %]'> + <div class='inner'> + <p class='media'><img src="[% obj.url %]" /></p> + <p class='details'><span class='name '>[%obj.displayid%]</span><br /><span class='filesize'>[% obj.size / 1000 %] kb</span></p> + [%- form.select( name="security-$obj.displayid" + items = security + selected = obj.security ) -%] + <p><label for="delete_[% obj.displayid %]">Delete?</label> <input type="checkbox" name="delete" id="delete_[% obj.displayid %]" value="[% obj.displayid %]" /></p> + </div> + </li> + [%- END -%] +</ul> +</fieldset> +<fieldset class="submit"> + <input type="submit" value="Change Security" name="action:edit" /> +</fieldset> +<fieldset class="submit destructive"> + <input type="submit" value="Delete Selected" name="action:delete" /> +</fieldset> +</form> +[% END %] diff -r 267bb682c94f -r 6fd99d890be1 views/media/edit.tt.text --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/views/media/edit.tt.text Fri Jul 20 21:16:56 2012 -0700 @@ -0,0 +1,4 @@ +.intro=Here are all the files you've uploaded from mobile. + +.title=Bulk Edit Files + diff -r 267bb682c94f -r 6fd99d890be1 views/media/index.tt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/views/media/index.tt Fri Jul 20 21:16:56 2012 -0700 @@ -0,0 +1,21 @@ +[% sections.title = '.title' | ml %] + +[% dw.need_res( + "stc/media.css" +) %] + +<p>[% ".intro" | ml %] <a href="[%site.root%]/file/edit">[% '.intro.edit' | ml %]</a></p> + +[% IF media.size %] +<ul id="media-grid"> + [%- FOREACH obj IN media -%] + <li class='media-item'> + <div class='inner'> + <p class='media'><img src="[% obj.url %]" /></p> + <p class='details'><span class='name'>[% obj.displayid %]. [% obj.ext %]</span><br /><span class='filesize'>[% obj.size / 1000 %] kb</span></p> + </div> + </li> + [%- END -%] +</ul> + +[% END %] diff -r 267bb682c94f -r 6fd99d890be1 views/media/index.tt.text --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/views/media/index.tt.text Fri Jul 20 21:16:56 2012 -0700 @@ -0,0 +1,7 @@ +;; -*- coding: utf-8 -*- + +.intro=Below are the files you have uploaded to the site. + +.intro.edit=Manage your files + +.title=Your Files --------------------------------------------------------------------------------