Commit da4dbda8 authored by Luc Didry's avatar Luc Didry

Introducing automatic ban system

parent efff3797
Pipeline #8496 failed with stage
in 5 minutes and 6 seconds
...@@ -53,6 +53,16 @@ sub startup { ...@@ -53,6 +53,16 @@ sub startup {
$self->secrets($config->{secret}); $self->secrets($config->{secret});
$self->helper(
ip => sub {
my $c = shift;
my $proxy = $c->req->headers->header('X-Forwarded-For');
my $ip = ($proxy) ? $proxy : $c->tx->remote_address;
return $ip;
}
);
$self->helper( $self->helper(
provisioning => sub { provisioning => sub {
my $c = shift; my $c = shift;
...@@ -133,6 +143,17 @@ sub startup { ...@@ -133,6 +143,17 @@ sub startup {
} }
); );
$self->helper(
cleaning => sub {
my $c = shift;
# Delete old sessions
LstuModel::Sessions->delete_where('until < ?', time);
# Delete old bans
LstuModel::Ban->delete_where('until < ?', time);
}
);
# Hooks # Hooks
$self->hook( $self->hook(
after_dispatch => sub { after_dispatch => sub {
...@@ -144,8 +165,6 @@ sub startup { ...@@ -144,8 +165,6 @@ sub startup {
before_dispatch => sub { before_dispatch => sub {
my $c = shift; my $c = shift;
# Delete old sessions
LstuModel::Sessions->delete_where('until < ?', time);
# API allowed domains # API allowed domains
if (defined($c->config('allowed_domains'))) { if (defined($c->config('allowed_domains'))) {
if ($c->config('allowed_domains')->[0] eq '*') { if ($c->config('allowed_domains')->[0] eq '*') {
......
...@@ -8,19 +8,55 @@ sub login { ...@@ -8,19 +8,55 @@ sub login {
my $pwd = $c->param('adminpwd'); my $pwd = $c->param('adminpwd');
my $act = $c->param('action'); my $act = $c->param('action');
if (defined($c->config('adminpwd')) && defined($pwd) && $pwd eq $c->config('adminpwd')) { $c->cleaning;
my $token = $c->shortener(32);
LstuModel::Sessions->create(token => $token, until => time + 3600); my $ip = $c->ip;
$c->session('token' => $token);
$c->redirect_to('stats'); my @banned = LstuModel::Ban->select('WHERE ip = ? AND until > ? AND strike >= 3', $ip, time);
} elsif (defined($act) && $act eq 'logout') { if (scalar @banned) {
LstuModel::Sessions->delete_where('token = ?', $c->session->{token}); my $penalty = 3600;
delete $c->session->{token}; if ($banned[0]->strike >= 6) {
$penalty = 3600 * 24 * 30; # 30 days of banishing
}
$banned[0]->update(
strike => $banned[0]->strike + 1,
until => time + $penalty
);
$c->flash('msg' => $c->l('Too many bad passwords. You\'re banned.'));
$c->flash('banned' => 1);
$c->redirect_to('stats'); $c->redirect_to('stats');
} else { } else {
$c->flash('msg' => $c->l('Bad password')); if (defined($c->config('adminpwd')) && defined($pwd) && $pwd eq $c->config('adminpwd')) {
$c->redirect_to('stats'); my $token = $c->shortener(32);
LstuModel::Sessions->create(
token => $token,
until => time + 3600
);
$c->session('token' => $token);
$c->redirect_to('stats');
} elsif (defined($act) && $act eq 'logout') {
LstuModel::Sessions->delete_where('token = ?', $c->session->{token});
delete $c->session->{token};
$c->redirect_to('stats');
} else {
my @bans = LstuModel::Ban->select('WHERE ip = ?', $ip);
if (scalar @bans) {
$bans[0]->update(
strike => $bans[0]->strike + 1,
until => time + 3600
);
} else {
LstuModel::Ban->create(
ip => $ip,
strike => 1,
until => time + 3600
);
}
$c->flash('msg' => $c->l('Bad password'));
$c->redirect_to('stats');
}
} }
} }
......
...@@ -7,97 +7,139 @@ use Mojo::JSON qw(to_json decode_json); ...@@ -7,97 +7,139 @@ use Mojo::JSON qw(to_json decode_json);
use Mojo::URL; use Mojo::URL;
sub add { sub add {
my $c = shift; my $c = shift;
my $url = Mojo::URL->new($c->param('lsturl'));
my $custom_url = $c->param('lsturl-custom');
my $format = $c->param('format');
$custom_url = undef if (defined($custom_url) && $custom_url eq '');
my ($msg, $short);
if (defined($custom_url) && ($custom_url =~ m/^a(pi)?$|^stats$/ || $custom_url =~ m/^d$/ ||
$custom_url =~ m/\.json$/ || $custom_url !~ m/^[-a-zA-Z0-9_]+$/))
{
$msg = $c->l('The shortened text can contain only numbers, letters and the - and _ character, can\'t be "a", "api", "d" or "stats" or end with ".json". Your URL to shorten: %1', $url);
} elsif (defined($custom_url) && LstuModel::Lstu->count('WHERE short = ?', $custom_url) > 0) {
$msg = $c->l('The shortened text (%1) is already used. Please choose another one.', $custom_url);
} elsif (is_http_uri($url->to_string) || is_https_uri($url->to_string)) {
my $res = $c->is_spam($url, 0);
if ($res->{is_spam}) {
$msg = $res->{msg};
} else {
my @records = LstuModel::Lstu->select('WHERE url = ?', $url);
if (scalar(@records) && !defined($custom_url)) { #$c->cleaning;
# Already got this URL
$short = $records[0]->short;
} else {
if(LstuModel->begin) {
if (defined($custom_url)) {
LstuModel::Lstu->create(
short => $custom_url,
url => $url,
counter => 0,
timestamp => time()
);
$short = $custom_url; my $ip = $c->ip;
} else {
@records = LstuModel::Lstu->select('WHERE url IS NULL LIMIT 1');
if (scalar(@records)) {
$records[0]->update(
url => $url,
counter => 0,
timestamp => time()
);
$short = $records[0]->short; my @banned = LstuModel::Ban->select('WHERE ip = ? AND until > ? AND strike >= 3', $ip, time);
} else { if (scalar @banned) {
# Houston, we have a problem my $penalty = 3600;
$msg = $c->l('No shortened URL available. Please retry or contact the administrator at %1. Your URL to shorten: [_2]', $c->config('contact'), $url); if ($banned[0]->strike >= 6) {
} $penalty = 3600 * 24 * 30; # 30 days of banishing
}
LstuModel->commit;
}
}
} }
} else { $banned[0]->update(
$msg = $c->l('%1 is not a valid URL.', $url); strike => $banned[0]->strike + 1,
} until => time + $penalty
if ($msg) { );
my $msg = $c->l('You asked to shorten too many URLs too quickly. You\'re banned for %1 hour(s).', $penalty/3600);
$c->respond_to( $c->respond_to(
json => { json => { success => Mojo::JSON->false, msg => $msg } }, json => { json => { success => Mojo::JSON->false, msg => $msg } },
any => sub { any => sub {
my $c = shift; my $c = shift;
$c->flash('msg' => $msg); $c->flash('msg' => $msg);
$c->flash('banned' => 1);
$c->redirect_to('index'); $c->redirect_to('index');
} }
); );
} else { } else {
# Get URLs from cookie my $url = Mojo::URL->new($c->param('lsturl'));
my $u = (defined($c->cookie('url'))) ? decode_json $c->cookie('url') : []; my $custom_url = $c->param('lsturl-custom');
# Add the new URL my $format = $c->param('format');
push @{$u}, $short;
# Make the array contain only unique URLs $custom_url = undef if (defined($custom_url) && $custom_url eq '');
my %k = map { $_, 1 } @{$u};
@{$u} = keys %k; my ($msg, $short);
# And set the cookie if (defined($custom_url) && ($custom_url =~ m/^a(pi)?$|^stats$/ || $custom_url =~ m/^d$/ ||
my $cookie = to_json($u); $custom_url =~ m/\.json$/ || $custom_url !~ m/^[-a-zA-Z0-9_]+$/))
$c->cookie('url' => $cookie, {expires => time + 142560000}); # expires in 10 years {
$msg = $c->l('The shortened text can contain only numbers, letters and the - and _ character, can\'t be "a", "api", "d" or "stats" or end with ".json". Your URL to shorten: %1', $url);
my $prefix = $c->prefix; } elsif (defined($custom_url) && LstuModel::Lstu->count('WHERE short = ?', $custom_url) > 0) {
$msg = $c->l('The shortened text (%1) is already used. Please choose another one.', $custom_url);
} elsif (is_http_uri($url->to_string) || is_https_uri($url->to_string)) {
my $res = $c->is_spam($url, 0);
if ($res->{is_spam}) {
$msg = $res->{msg};
} else {
my @records = LstuModel::Lstu->select('WHERE url = ?', $url);
if (scalar(@records) && !defined($custom_url)) {
# Already got this URL
$short = $records[0]->short;
} else {
my @bans = LstuModel::Ban->select('WHERE ip = ?', $ip);
if (scalar @bans) {
$bans[0]->update(
strike => $bans[0]->strike + 1,
until => time + 1
);
} else {
LstuModel::Ban->create(
ip => $ip,
strike => 1,
until => time + 1
);
}
$c->respond_to( if(LstuModel->begin) {
json => { json => { success => Mojo::JSON->true, url => $url, short => $prefix.$short } }, if (defined($custom_url)) {
any => sub { LstuModel::Lstu->create(
my $c = shift; short => $custom_url,
url => $url,
counter => 0,
timestamp => time()
);
$c->flash('url' => $url); $short = $custom_url;
$c->flash('short' => $prefix.$short); } else {
$c->redirect_to('index'); @records = LstuModel::Lstu->select('WHERE url IS NULL LIMIT 1');
if (scalar(@records)) {
$records[0]->update(
url => $url,
counter => 0,
timestamp => time()
);
$short = $records[0]->short;
} else {
# Houston, we have a problem
$msg = $c->l('No shortened URL available. Please retry or contact the administrator at %1. Your URL to shorten: [_2]', $c->config('contact'), $url);
}
}
LstuModel->commit;
}
}
} }
); } else {
$msg = $c->l('%1 is not a valid URL.', $url);
}
if ($msg) {
$c->respond_to(
json => { json => { success => Mojo::JSON->false, msg => $msg } },
any => sub {
my $c = shift;
$c->flash('msg' => $msg);
$c->redirect_to('index');
}
);
} else {
# Get URLs from cookie
my $u = (defined($c->cookie('url'))) ? decode_json $c->cookie('url') : [];
# Add the new URL
push @{$u}, $short;
# Make the array contain only unique URLs
my %k = map { $_, 1 } @{$u};
@{$u} = keys %k;
# And set the cookie
my $cookie = to_json($u);
$c->cookie('url' => $cookie, {expires => time + 142560000}); # expires in 10 years
my $prefix = $c->prefix;
$c->respond_to(
json => { json => { success => Mojo::JSON->true, url => $url, short => $prefix.$short } },
any => sub {
my $c = shift;
$c->flash('url' => $url);
$c->flash('short' => $prefix.$short);
$c->redirect_to('index');
}
);
}
} }
} }
......
...@@ -5,6 +5,13 @@ use Mojo::JSON qw(true false); ...@@ -5,6 +5,13 @@ use Mojo::JSON qw(true false);
use Test::More; use Test::More;
use Test::Mojo; use Test::Mojo;
use lib 'lib';
use LstuModel;
# Rotten syntax, but prevents "Static LstuModel->delete has been deprecated"
LstuModel::Lstu->delete_where('1 = 1');
LstuModel::Ban->delete_where('1 = 1');
my $t = Test::Mojo->new('Lstu'); my $t = Test::Mojo->new('Lstu');
$t->get_ok('/') $t->get_ok('/')
->status_is(200) ->status_is(200)
...@@ -21,6 +28,7 @@ $t->post_ok('/a' => form => { lsturl => 'truc', format => 'json' }) ...@@ -21,6 +28,7 @@ $t->post_ok('/a' => form => { lsturl => 'truc', format => 'json' })
->json_has('msg', 'success') ->json_has('msg', 'success')
->json_is({success => false, msg => 'truc is not a valid URL.'}); ->json_is({success => false, msg => 'truc is not a valid URL.'});
LstuModel::Ban->delete_where('1 = 1'); # Prevents banishing
my $a = $t->ua->post('/a' => form => { lsturl => 'https://lstu.fr', format => 'json' })->res->json('/short'); my $a = $t->ua->post('/a' => form => { lsturl => 'https://lstu.fr', format => 'json' })->res->json('/short');
$t->get_ok($a) $t->get_ok($a)
...@@ -66,4 +74,34 @@ $t->get_ok('/d/'.$a.'?format=json') ...@@ -66,4 +74,34 @@ $t->get_ok('/d/'.$a.'?format=json')
->status_is(200) ->status_is(200)
->json_is({success => true, deleted => 1}); ->json_is({success => true, deleted => 1});
$t->post_ok('/stats' => form => { adminpwd => 'toto', action => 'logout' })
->status_is(200);
# Test admin banishing
$t->post_ok('/stats' => form => { adminpwd => 'totoi' })
->status_is(200)
->content_like(qr/Bad password/);
$t->post_ok('/stats' => form => { adminpwd => 'totoi' })
->status_is(200)
->content_like(qr/Bad password/);
$t->post_ok('/stats' => form => { adminpwd => 'totoi' })
->status_is(200)
->content_like(qr/Too many bad passwords\./);
# Test user banishing
LstuModel::Ban->delete_where('1 = 1'); # Reset banishing
LstuModel::Lstu->delete_where('1 = 1');
$t->ua->post('/a' => form => { lsturl => 'https://lstu.fr', format => 'json' });
$t->ua->post('/a' => form => { lsturl => 'https://lut.im', format => 'json' });
$t->ua->post('/a' => form => { lsturl => 'https://erco.xyz', format => 'json' });
$t->ua->post('/a' => form => { lsturl => 'https://onsenfout.fr', format => 'json' });
$t->post_ok('/a' => form => { lsturl => 'https://lufi.io', format => 'json' })
->status_is(200)
->json_has('msg', 'success')
->json_is('/success' => false)
->json_like('/msg' => qr#You asked to shorten too many URLs too quickly\. You're banned for \d+ hour\(s\)\.#);
done_testing(); done_testing();
...@@ -20,7 +20,7 @@ msgstr "" ...@@ -20,7 +20,7 @@ msgstr ""
"X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-SourceCharset: UTF-8\n"
#. ($url) #. ($url)
#: lib/Lstu.pm:232 #: lib/Lstu/Controller/Lstu.pm:106
msgid "%1 is not a valid URL." msgid "%1 is not a valid URL."
msgstr "" msgstr ""
...@@ -28,11 +28,11 @@ msgstr "" ...@@ -28,11 +28,11 @@ msgstr ""
msgid "About" msgid "About"
msgstr "" msgstr ""
#: themes/default/templates/stats.html.ep:12 themes/default/templates/stats.html.ep:13 #: themes/default/templates/stats.html.ep:13 themes/default/templates/stats.html.ep:14
msgid "Admin password" msgid "Admin password"
msgstr "" msgstr ""
#: lib/Lstu.pm:347 lib/Lstu.pm:379 #: lib/Lstu/Controller/Admin.pm:57 lib/Lstu/Controller/Admin.pm:90
msgid "Bad password" msgid "Bad password"
msgstr "" msgstr ""
...@@ -40,11 +40,11 @@ msgstr "" ...@@ -40,11 +40,11 @@ msgstr ""
msgid "Copy to clipboard" msgid "Copy to clipboard"
msgstr "" msgstr ""
#: themes/default/templates/stats.html.ep:29 #: themes/default/templates/stats.html.ep:31
msgid "Counter" msgid "Counter"
msgstr "" msgstr ""
#: themes/default/templates/stats.html.ep:30 #: themes/default/templates/stats.html.ep:32
msgid "Created" msgid "Created"
msgstr "" msgstr ""
...@@ -52,7 +52,7 @@ msgstr "" ...@@ -52,7 +52,7 @@ msgstr ""
msgid "Custom shortened text" msgid "Custom shortened text"
msgstr "" msgstr ""
#: themes/default/templates/stats.html.ep:33 #: themes/default/templates/stats.html.ep:35
msgid "Delete" msgid "Delete"
msgstr "" msgstr ""
...@@ -60,7 +60,7 @@ msgstr "" ...@@ -60,7 +60,7 @@ msgstr ""
msgid "Failure reason" msgid "Failure reason"
msgstr "" msgstr ""
#: themes/default/templates/index.html.ep:12 themes/default/templates/stats.html.ep:16 #: themes/default/templates/index.html.ep:12 themes/default/templates/stats.html.ep:17
msgid "Go!" msgid "Go!"
msgstr "" msgstr ""
...@@ -92,16 +92,16 @@ msgstr "" ...@@ -92,16 +92,16 @@ msgstr ""
msgid "License:" msgid "License:"
msgstr "" msgstr ""
#: themes/default/templates/stats.html.ep:9 #: themes/default/templates/stats.html.ep:10
msgid "Logout from admin stats" msgid "Logout from admin stats"
msgstr "" msgstr ""
#: themes/default/templates/stats.html.ep:63 #: themes/default/templates/stats.html.ep:65
msgid "Next" msgid "Next"
msgstr "" msgstr ""
#. ($c->config('contact') #. ($c->config('contact')
#: lib/Lstu.pm:224 #: lib/Lstu/Controller/Lstu.pm:98
msgid "No shortened URL available. Please retry or contact the administrator at %1. Your URL to shorten: %2" msgid "No shortened URL available. Please retry or contact the administrator at %1. Your URL to shorten: %2"
msgstr "" msgstr ""
...@@ -109,7 +109,7 @@ msgstr "" ...@@ -109,7 +109,7 @@ msgstr ""
msgid "Parameters:" msgid "Parameters:"
msgstr "" msgstr ""
#: themes/default/templates/stats.html.ep:61 #: themes/default/templates/stats.html.ep:63
msgid "Previous" msgid "Previous"
msgstr "" msgstr ""
...@@ -121,7 +121,7 @@ msgstr "" ...@@ -121,7 +121,7 @@ msgstr ""
msgid "Response:" msgid "Response:"
msgstr "" msgstr ""
#: themes/default/templates/stats.html.ep:28 #: themes/default/templates/stats.html.ep:30
msgid "Shortened URL" msgid "Shortened URL"
msgstr "" msgstr ""
...@@ -130,31 +130,35 @@ msgid "Statistics" ...@@ -130,31 +130,35 @@ msgid "Statistics"
msgstr "" msgstr ""
#. ($url->host) #. ($url->host)
#: lib/Lstu.pm:114 #: lib/Lstu.pm:127
msgid "The URL host or one of its redirection(s) (%1) is blacklisted at Spamhaus. I refuse to shorten it." msgid "The URL host or one of its redirection(s) (%1) is blacklisted at Spamhaus. I refuse to shorten it."
msgstr "" msgstr ""
#. ($url->host) #. ($url->host)
#: lib/Lstu.pm:127 #: lib/Lstu.pm:140
msgid "The URL redirects 3 times or most. It's most likely a dangerous URL (spam, phishing, etc.). I refuse to shorten it." msgid "The URL redirects 3 times or most. It's most likely a dangerous URL (spam, phishing, etc.). I refuse to shorten it."
msgstr "" msgstr ""
#. ($c->url_for('/') #. ($c->url_for('/')
#: lib/Lstu.pm:368 lib/Lstu.pm:405 #: lib/Lstu/Controller/Admin.pm:79 lib/Lstu/Controller/Lstu.pm:221
msgid "The shortened URL %1 doesn't exist." msgid "The shortened URL %1 doesn't exist."
msgstr "" msgstr ""
#. ($custom_url) #. ($custom_url)
#: lib/Lstu.pm:190 #: lib/Lstu/Controller/Lstu.pm:50
msgid "The shortened text (%1) is already used. Please choose another one." msgid "The shortened text (%1) is already used. Please choose another one."
msgstr "" msgstr ""
#. ($url) #. ($url)
#: lib/Lstu.pm:188 #: lib/Lstu/Controller/Lstu.pm:48
msgid "The shortened text can contain only numbers, letters and the - and _ character, can't be \"a\", \"api\", \"d\" or \"stats\" or end with \".json\". Your URL to shorten: %1" msgid "The shortened text can contain only numbers, letters and the - and _ character, can't be \"a\", \"api\", \"d\" or \"stats\" or end with \".json\". Your URL to shorten: %1"
msgstr "" msgstr ""
#: themes/default/templates/stats.html.ep:27 #: lib/Lstu/Controller/Admin.pm:25
msgid "Too many bad passwords. You're banned."
msgstr ""
#: themes/default/templates/stats.html.ep:29
msgid "URL" msgid "URL"
msgstr "" msgstr ""
...@@ -162,6 +166,11 @@ msgstr "" ...@@ -162,6 +166,11 @@ msgstr ""
msgid "URL to shorten" msgid "URL to shorten"
msgstr "" msgstr ""
#. ($penalty/3600)
#: lib/Lstu/Controller/Lstu.pm:26
msgid "You asked to shorten too many URLs too quickly. You're banned for %1 hour(s)."
msgstr ""
#: themes/default/templates/api.html.ep:22 #: themes/default/templates/api.html.ep:22
msgid "You will be redirected to the Lstu interface with a message giving the shortened URL" msgid "You will be redirected to the Lstu interface with a message giving the shortened URL"
msgstr "" msgstr ""
......
...@@ -25,7 +25,7 @@ msgstr "" ...@@ -25,7 +25,7 @@ msgstr ""
"X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-SourceCharset: UTF-8\n"
#. ($url) #. ($url)
#: lib/Lstu.pm:232 #: lib/Lstu/Controller/Lstu.pm:106
msgid "%1 is not a valid URL." msgid "%1 is not a valid URL."
msgstr "%1 n’est pas une URL valide." msgstr "%1 n’est pas une URL valide."
...@@ -33,11 +33,11 @@ msgstr "%1 n’est pas une URL valide." ...@@ -33,11 +33,11 @@ msgstr "%1 n’est pas une URL valide."
msgid "About" msgid "About"
msgstr "À propos" msgstr "À propos"
#: themes/default/templates/stats.html.ep:12 themes/default/templates/stats.html.ep:13 #: themes/default/templates/stats.html.ep:13 themes/default/templates/stats.html.ep:14
msgid "Admin password" msgid "Admin password"
msgstr "Mot de passe administrateur" msgstr "Mot de passe administrateur"
#: lib/Lstu.pm:347 lib/Lstu.pm:379 #: lib/Lstu/Controller/Admin.pm:57 lib/Lstu/Controller/Admin.pm:90
msgid "Bad password" msgid "Bad password"
msgstr "Mot de passe incorrect" msgstr "Mot de passe incorrect"
...@@ -45,11 +45,11 @@ msgstr "Mot de passe incorrect" ...@@ -45,11 +45,11 @@ msgstr "Mot de passe incorrect"
msgid "Copy to clipboard" msgid "Copy to clipboard"
msgstr "Copier dans le presse-papier" msgstr "Copier dans le presse-papier"
#: themes/default/templates/stats.html.ep:29 #: themes/default/templates/stats.html.ep:31
msgid "Counter" msgid "Counter"
msgstr "Compteur" msgstr "Compteur"
#: themes/default/templates/stats.html.ep:30 #: themes/default/templates/stats.html.ep:32
msgid "Created" msgid "Created"
msgstr "Créé le" msgstr "Créé le"
...@@ -57,7 +57,7 @@ msgstr "Créé le" ...@@ -57,7 +57,7 @@ msgstr "Créé le"
msgid "Custom shortened text" msgid "Custom shortened text"
msgstr "Texte du raccourci personnalisé" msgstr "Texte du raccourci personnalisé"
#: themes/default/templates/stats.html.ep:33 #: themes/default/templates/stats.html.ep:35
msgid "Delete" msgid "Delete"
msgstr "Supprimer" msgstr "Supprimer"
...@@ -65,7 +65,7 @@ msgstr "Supprimer" ...@@ -65,7 +65,7 @@ msgstr "Supprimer"
msgid "Failure reason" msgid "Failure reason"
msgstr "Raison de l’échec" msgstr "Raison de l’échec"
#: themes/default/templates/index.html.ep:12 themes/default/templates/stats.html.ep:16 #: themes/default/templates/index.html.ep:12 themes/default/templates/stats.html.ep:17
msgid "Go!" msgid "Go!"
msgstr "Allons-y !" msgstr "Allons-y !"
...@@ -97,16 +97,16 @@ msgstr "JSON : succès" ...@@ -97,16 +97,16 @@ msgstr "JSON : succès"
msgid "License:" msgid "License:"
msgstr "Licence :" msgstr "Licence :"
#: themes/default/templates/stats.html.ep:9 #: themes/default/templates/stats.html.ep:10
msgid "Logout from admin stats" msgid "Logout from admin stats"
msgstr "Déconnexion des stats admin" msgstr "Déconnexion des stats admin"
#: themes/default/templates/stats.html.ep:63 #: themes/default/templates/stats.html.ep:65
msgid "Next" msgid "Next"
msgstr "Suivant" msgstr "Suivant"
#. ($c->config('contact') #. ($c->config('contact')
#: lib/Lstu.pm:224 #: lib/Lstu/Controller/Lstu.pm:98
msgid "No shortened URL available. Please retry or contact the administrator at %1. Your URL to shorten: %2" msgid "No shortened URL available. Please retry or contact the administrator at %1. Your URL to shorten: %2"
msgstr "Il n’y a plus d’URL raccourcie disponible. Veuillez réessayer ou contactez l’administrateur sur %1. Rappel de l’URL à raccourcir : %2"