diff --git a/CHANGELOG b/CHANGELOG index 9d17176..4ee30d8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,9 @@ Revision history for Lutim - Remove @framasky as default tweet_card_via setting - Add a message saying how many images there is in the gallery - Use ISO::639_1 for languages' native names + - Add CLI command to print informations about images + - Add CLI command to remove images + - Add CLI command to search images based on the uploader's IP address 0.10.4 2018-05-07 - Fix bug in cache system that would allow someone to view an image with an incorrect decryption key diff --git a/lib/Lutim.pm b/lib/Lutim.pm index f552f72..b5ca0c5 100644 --- a/lib/Lutim.pm +++ b/lib/Lutim.pm @@ -23,6 +23,8 @@ sub startup { $self->{wait_for_it} = {}; + push @{$self->commands->namespaces}, 'Lutim::Command'; + $self->plugin('DebugDumperHelper'); my $config = $self->plugin('Config', { diff --git a/lib/Lutim/Command/image.pm b/lib/Lutim/Command/image.pm new file mode 100644 index 0000000..d98e13c --- /dev/null +++ b/lib/Lutim/Command/image.pm @@ -0,0 +1,228 @@ +# vim:set sw=4 ts=4 sts=4 ft=perl expandtab: +package Lutim::Command::image; +use Mojo::Base 'Mojolicious::Command'; +use Mojo::Util qw(getopt); +use Mojo::Collection 'c'; +use Lutim::DB::Image; +use FindBin qw($Bin); +use File::Spec qw(catfile); + +has description => 'Manage stored images'; +has usage => sub { shift->extract_usage }; + +my $csv_header = 0; + +sub run { + my $c = shift; + my @args = @_; + + my $cfile = Mojo::File->new($Bin, '..' , 'lutim.conf'); + if (defined $ENV{MOJO_CONFIG}) { + $cfile = Mojo::File->new($ENV{MOJO_CONFIG}); + unless (-e $cfile->to_abs) { + $cfile = Mojo::File->new($Bin, '..', $ENV{MOJO_CONFIG}); + } + } + my $config = $c->app->plugin('Config', { + file => $cfile, + default => { + provisioning => 100, + provis_step => 5, + length => 8, + always_encrypt => 0, + anti_flood_delay => 5, + max_file_size => 10*1024*1024, + https => 0, + proposed_delays => '0,1,7,30,365', + default_delay => 0, + max_delay => 0, + token_length => 24, + crypto_key_length => 8, + thumbnail_size => 100, + theme => 'default', + dbtype => 'sqlite', + db_path => 'lutim.db', + max_files_in_zip => 15, + prefix => '/', + minion => { + enabled => 0, + dbtype => 'sqlite', + db_path => 'minion.db' + }, + cache_max_size => 0, + memcached_servers => [], + quiet_logs => 0, + disable_img_stats => 0, + x_frame_options => 'DENY', + x_content_type_options => 'nosniff', + x_xss_protection => '1; mode=block', + } + }); + + if (scalar(@{$config->{memcached_servers}})) { + $c->app->plugin(CHI => { + lutim_images_cache => { + driver => 'Memcached', + servers => $config->{memcached_servers}, + expires_in => '1 day', + expires_on_backend => 1, + } + }); + } + + getopt \@args, + 'i|info=s{1,}' => \my @info, + 'c|csv' => \my $csv, + 'r|remove=s{1,}' => \my @remove, + 'y|yes' => \my $yes, + 'q|quiet' => \my $quiet, + 's|search=s' => \my $ip + ; + + if (scalar @info) { + c(@info)->each( + sub { + my ($e, $num) = @_; + my $i = get_short($c, $e); + print_infos($i, $csv) if $i; + } + ); + } + if (scalar @remove) { + c(@remove)->each( + sub { + my ($e, $num) = @_; + my $i = get_short($c, $e); + if ($i) { + if ($i->enabled) { + print_infos($i, 0) unless $quiet; + delete_short($c, $i, $yes); + } else { + say sprintf('The image %s is already disabled', $e); + } + } + } + ); + if ($config->{cache_max_size} && !scalar(@{$config->{memcached_servers}})) { + say "\nPlease reload Lutim to be sure that the deleted images are not in the cache anymore."; + } + } + if ($ip) { + my $u = Lutim::DB::Image->new(app => $c->app)->search_created_by($ip); + my @shorts; + $u->each(sub { + my ($e, $num) = @_; + push @shorts, $e->short; + print_infos($e, $csv); + }); + say sprintf('%d matching URLs', $u->size); + say sprintf("If you want to delete those images, please do:\n carton exec script/lutim image --remove %s", join(' ', @shorts)) if @shorts; + } +} + +sub get_short { + my $c = shift; + my $short = shift; + + my $i = Lutim::DB::Image->new(app => $c->app, short => $short); + if ($i->path) { + return $i; + } else { + say sprintf('Sorry, unable to find an image with short = %s', $short); + return undef; + } +} + +sub print_infos { + my $i = shift; + my $csv = shift; + + my $msg; + + if ($i) { + if ($csv) { + if (!$csv_header) { + say 'short,path,footprint,enabled,mediatype,filename,counter,delete_at_first_view,delete_at_day,created_at,created_by,last_access_at,width,height'; + $csv_header = 1; + } + $msg = '"%s","%s","%s",%d,"%s","%s",%d,"%s",%d,"%s","%s","%s",%d,%d'; + } else { + $msg = <created_at); + my $created_at = sprintf('%d-%d-%d %d:%d:%d GMT', $year + 1900, ++$mon, $mday, $hour, $min, $sec); + + my $last_access_at = ''; + if ($i->last_access_at) { + ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime($i->last_access_at); + $last_access_at = sprintf('%d-%d-%d %d:%d:%d GMT', $year + 1900, ++$mon, $mday, $hour, $min, $sec); + } + say sprintf($msg, + $i->short, + $i->path, + $i->footprint, + $i->enabled, + $i->mediatype, + $i->filename, + $i->counter, + $i->delete_at_first_view, + $i->delete_at_day, + $created_at, + $i->created_by, + $last_access_at, + $i->width, + $i->height + ); + } +} + +sub delete_short { + my $c = shift; + my $i = shift; + my $y = shift; + + my $confirm = ($y) ? 'yes' : undef; + unless (defined $confirm) { + printf('Are you sure you want to remove this image (%s) ? [N/y] ', $i->short); + $confirm = ; + chomp $confirm; + } + if ($confirm =~ m/^y(es)?$/i) { + $c->app->delete_image($i); + } else { + say 'Answer was not "y" or "yes". Aborting deletion.'; + } +} + +=encoding utf8 + +=head1 NAME + +Lutim::Command::image - Manage URL in Lutim's database + +=head1 SYNOPSIS + + Usage: + carton exec script/lutim image --info [--csv] Print infos about the space-separated images (--csv creates a CSV output) + carton exec script/lutim image --remove [--yes] [--quiet] Delete the space-separated images (--yes disables confirmation, --quiet disables informations printing) + carton exec script/lutim image --search Print infos about the images uploaded by this IP (database LIKE, may include images uploaded by other IPs) + +=cut + +1; + diff --git a/lib/Lutim/DB/Image.pm b/lib/Lutim/DB/Image.pm index fd743d0..8f5a1f1 100644 --- a/lib/Lutim/DB/Image.pm +++ b/lib/Lutim/DB/Image.pm @@ -331,6 +331,20 @@ sub to_hash { =back +=head2 search_created_by + +=over 1 + +=item B : C<$c-Esearch_created_by($ip)> + +=item B : an IP address + +=item B : get enabled images that have been uploaded by this IP address (database query: LIKE '$ip%', results may include images uploaded by similar IP addresses) + +=item B : a Mojo::Collection object containing the matching images as Lutim::DB::Image objects + +=back + =cut 1; diff --git a/lib/Lutim/DB/Image/Pg.pm b/lib/Lutim/DB/Image/Pg.pm index e4d8671..9bf2057 100644 --- a/lib/Lutim/DB/Image/Pg.pm +++ b/lib/Lutim/DB/Image/Pg.pm @@ -189,6 +189,27 @@ sub disable { return $c; } +sub search_created_by { + my $c = shift; + my $ip = shift; + + my @images; + + my $records = $c->app->pg->db->select('lutim', undef, { enabled => 1, created_by => { -like => $ip.'%' } })->hashes; + + $records->each( + sub { + my ($e, $num) = @_; + my $i = Lutim::DB::Image->new(app => $c->app); + $i->_slurp($e); + + push @images, $i; + } + ); + + return c(@images); +} + sub _slurp { my $c = shift; my $r = shift; diff --git a/lib/Lutim/DB/Image/SQLite.pm b/lib/Lutim/DB/Image/SQLite.pm index 9915bf5..fd68ebc 100644 --- a/lib/Lutim/DB/Image/SQLite.pm +++ b/lib/Lutim/DB/Image/SQLite.pm @@ -190,6 +190,27 @@ sub disable { return $c; } +sub search_created_by { + my $c = shift; + my $ip = shift; + + my @images; + + my $records = $c->app->sqlite->db->select('lutim', undef, { enabled => 1, created_by => { -like => $ip.'%' } })->hashes; + + $records->each( + sub { + my ($e, $num) = @_; + my $i = Lutim::DB::Image->new(app => $c->app); + $i->_slurp($e); + + push @images, $i; + } + ); + + return c(@images); +} + sub _slurp { my $c = shift; my $r = shift; diff --git a/themes/default/lib/Lutim/I18N/lutim.pot b/themes/default/lib/Lutim/I18N/lutim.pot index 3a54eb2..f0dcc0f 100644 --- a/themes/default/lib/Lutim/I18N/lutim.pot +++ b/themes/default/lib/Lutim/I18N/lutim.pot @@ -32,11 +32,11 @@ msgstr "" msgid "-or-" msgstr "" -#: lib/Lutim.pm:219 lib/Lutim/Command/cron/stats.pm:151 lib/Lutim/Command/cron/stats.pm:165 lib/Lutim/Command/cron/stats.pm:182 themes/default/templates/index.html.ep:5 themes/default/templates/partial/raw.js.ep:25 themes/default/templates/partial/raw.js.ep:8 themes/default/templates/raw.html.ep:10 +#: lib/Lutim.pm:221 lib/Lutim/Command/cron/stats.pm:151 lib/Lutim/Command/cron/stats.pm:165 lib/Lutim/Command/cron/stats.pm:182 themes/default/templates/index.html.ep:5 themes/default/templates/partial/raw.js.ep:25 themes/default/templates/partial/raw.js.ep:8 themes/default/templates/raw.html.ep:10 msgid "1 year" msgstr "" -#: lib/Lutim.pm:218 lib/Lutim/Command/cron/stats.pm:148 lib/Lutim/Command/cron/stats.pm:162 lib/Lutim/Command/cron/stats.pm:179 themes/default/templates/index.html.ep:4 themes/default/templates/partial/for_my_delay.html.ep:13 themes/default/templates/partial/lutim.js.ep:149 themes/default/templates/partial/raw.js.ep:22 themes/default/templates/partial/raw.js.ep:5 themes/default/templates/raw.html.ep:7 +#: lib/Lutim.pm:220 lib/Lutim/Command/cron/stats.pm:148 lib/Lutim/Command/cron/stats.pm:162 lib/Lutim/Command/cron/stats.pm:179 themes/default/templates/index.html.ep:4 themes/default/templates/partial/for_my_delay.html.ep:13 themes/default/templates/partial/lutim.js.ep:149 themes/default/templates/partial/raw.js.ep:22 themes/default/templates/partial/raw.js.ep:5 themes/default/templates/raw.html.ep:7 msgid "24 hours" msgstr "" @@ -507,7 +507,7 @@ msgstr "" msgid "core developer" msgstr "" -#: lib/Lutim.pm:217 lib/Lutim/Command/cron/stats.pm:147 lib/Lutim/Command/cron/stats.pm:161 lib/Lutim/Command/cron/stats.pm:178 themes/default/templates/index.html.ep:3 themes/default/templates/partial/raw.js.ep:21 themes/default/templates/partial/raw.js.ep:4 themes/default/templates/raw.html.ep:6 +#: lib/Lutim.pm:219 lib/Lutim/Command/cron/stats.pm:147 lib/Lutim/Command/cron/stats.pm:161 lib/Lutim/Command/cron/stats.pm:178 themes/default/templates/index.html.ep:3 themes/default/templates/partial/raw.js.ep:21 themes/default/templates/partial/raw.js.ep:4 themes/default/templates/raw.html.ep:6 msgid "no time limit" msgstr ""