# vim:set sw=4 ts=4 sts=4 ft=perl expandtab:
package Lufi::Controller::Files;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::JSON qw(encode_json decode_json to_json true false);
use Mojo::Util qw(encode decode);
use Mojo::File;
use Lufi::DB::File;
use Lufi::DB::Slice;
use File::Spec::Functions;
use Number::Bytes::Human qw(format_bytes);
use Filesys::DfPortable;
use Crypt::SaltedHash;

sub files {
    my $c = shift;

    if ((!defined($c->config('ldap')) && !defined($c->config('htpasswd'))) || $c->is_user_authenticated) {
        $c->render(template => 'files');
    } else {
        $c->redirect_to('login');
    }
}

sub upload {
    my $c = shift;

    my $invitation;
    my $token = $c->session->{guest_token};
    $invitation = Lufi::DB::Invitation->new(app => $c->app)->from_token($token) if $token;
    if ((!defined($c->config('ldap')) && !defined($c->config('htpasswd'))) || $c->is_user_authenticated || $invitation) {
        $c->inactivity_timeout(30000000);

        $c->app->log->debug('Client connected');

        $c->on(
            message => sub {
                my ($ws, $text) = @_;

                my $invit = Lufi::DB::Invitation->new(app => $c->app)->from_token($token) if $token;

                my $begin = time;

                my ($json) = split('XXMOJOXX', $text, 2);
                $json = encode 'UTF-8', $json;
                $text =~ s/^.*?XXMOJOXX/${json}XXMOJOXX/;
                $json = decode_json $json;

                $c->app->log->debug('Got message');

                if (defined($json->{cancel}) && $json->{cancel}) {
                    my $f = Lufi::DB::File->new(app => $c->app)->from_short($json->{id});
                    if ($f && $f->mod_token && $f->mod_token eq $json->{mod_token}) {
                        $f = $f->delete();
                        return $ws->send(to_json(
                            {
                                action  => 'cancel',
                                success => $f->deleted ? true : false,
                                msg     => $f->deleted ? 'Lufi::DB::File->delete() was successfull' : 'Lufi::DB::File->delete() failed',
                                i       => $json->{i}
                            }
                        ));
                    } else {
                        return $ws->send(to_json(
                            {
                                action  => 'cancel',
                                success => false,
                                msg     => 'Lufi::DB::File not found or invalid mod_token',
                                i       => $json->{i}
                            }
                        ));
                    }
                }

                my $stop = 0;

                # Check if stop_upload file is present
                if ($c->stop_upload) {
                    $stop = 1;
                    return $ws->send(decode('UTF-8', encode_json(
                        {
                            success    => false,
                            msg        => $c->l('Sorry, uploading is disabled.'),
                            sent_delay => $json->{delay},
                            i          => $json->{i}
                        }
                    )));
                }
                # Check against max_size
                if (defined $c->config('max_file_size')) {
                    if ($json->{size} > $c->config('max_file_size')) {
                        $stop = 1;
                        return $ws->send(decode('UTF-8', encode_json(
                            {
                                success    => false,
                                msg        => $c->l('Your file is too big: %1 (maximum size allowed: %2)', format_bytes($json->{size}), format_bytes($c->config('max_file_size'))),
                                sent_delay => $json->{delay},
                                i          => $json->{i}
                            }
                        )));
                    }
                }
                # Check that we have enough space (multiplying by 2 since it's encrypted, it takes more place that the original file)
                # Only check if using filesystem, not Swift storage
                if (!defined($c->config('swift')) && $json->{part} == 0 && ($json->{size} * 2) >= dfportable($c->config('upload_dir'))->{bavail}) {
                    $stop = 1;
                    return $ws->send(decode('UTF-8', encode_json(
                        {
                            success    => false,
                            msg        => $c->l('No enough space available on the server for this file (size: %1).', format_bytes($json->{size})),
                            sent_delay => $json->{delay},
                            i          => $json->{i}
                        }
                    )));
                }
                # Check that the invitation is still valid, but only if it's the first chunk
                # (i.e. a new file, we don't want to stop a current uploading)
                if ($json->{part} == 0 && $invit && !$invit->is_valid()) {
                    $stop = 1;
                    $c->app->log->info(sprintf('Someone (%s) tried to use an expired or deleted invitation.', $invit->guest_mail));
                    $ws->send(decode('UTF-8', encode_json(
                        {
                            success    => false,
                            msg        => $c->l('Sorry, your invitation has expired or has been deleted. Please contact %1 to have another invitation.', $invit->ldap_user_mail),
                        }
                    )));
                }

                unless ($stop) {
                    my $f;
                    if (defined($json->{id})) {
                        $f = Lufi::DB::File->new(app => $c->app)->from_short($json->{id});
                    } else {
                        my $delay;
                        unless (defined $json->{delay}) {
                            $json->{delay} = $c->max_delay;
                        }

                        if (defined $c->config('delay_for_size')) {
                            # Choose delay according to config
                            my $delays   = $c->config('delay_for_size');
                            my @keys     = sort {$b <=> $a} keys %{$delays};
                            for my $key (@keys) {
                                if ($json->{size} >= $key) {
                                    $delay = ($json->{delay} < $delays->{$key}) ? $json->{delay} : $delays->{$key};
                                    last;
                                }
                            }
                        }
                        # If the file size is lower than the lowest configured size or if there is no delay_for_size setting, we choose the configured max delay
                        unless (defined $delay) {
                            $delay = (($json->{delay} > 0 && $json->{delay} <= $c->max_delay) || $c->max_delay == 0) ? $json->{delay} : $c->max_delay;
                        }
                        # If we have a password
                        my $salted_pwd;
                        if ($c->config('allow_pwd_on_files') && defined($json->{file_pwd}) && $json->{file_pwd} ne '') {
                            my $csh = Crypt::SaltedHash->new(algorithm => 'SHA-256', salt_len => 8);
                            $csh->add($json->{file_pwd});

                            $salted_pwd = $csh->generate();
                        }

                        my $creator = $c->ip;
                        # Authenticated user logging
                        if ((defined($c->config('ldap')) || defined($c->config('htpasswd'))) && !$invitation) {
                            $creator = sprintf('User: %s, IP: %s', $c->current_user->{username}, $creator);
                        }
                        # Guest user logging
                        if ($invitation) {
                            $creator = sprintf('User: %s, IP: %s', $invitation->guest_mail, $creator);
                        }

                        my $delete_at_first_view = ($json->{del_at_first_view}) ? 1 : 0;
                        $delete_at_first_view    = 1 if $c->app->config('force_burn_after_reading');
                        $f = Lufi::DB::File->new(app => $c->app)->get_empty()
                                ->created_by($creator)
                                ->delete_at_first_view($delete_at_first_view)
                                ->delete_at_day($delay)
                                ->mediatype($json->{type})
                                ->filename($json->{name})
                                ->filesize($json->{size})
                                ->nbslices($json->{total})
                                ->mod_token($c->shortener($c->config('token_length')))
                                ->passwd($salted_pwd)
                                ->zipped($json->{zipped})
                                ->write;
                    }

                    # This check is just in case we didn't succeed to find a corresponding record
                    # It normally can't happen
                    if (defined $f) {
                        # If we already have a part, it's a resend because the websocket has been broken
                        # In this case, we don't need to rewrite the file
                        unless ($f->slices->grep(sub { $_->j == $json->{part} })->size) {
                            # Create slice file
                            my $s    = Lufi::DB::Slice->new(
                                app   => $c->app,
                                short => $f->short,
                                j     => $json->{part}
                            )->store($text);
                            push @{$f->slices}, $s;
                            $s->write;

                            if (($json->{part} + 1) == $json->{total}) {
                                $f->complete(1);
                                $f->created_at(time);
                                $f->write;
                            }
                        }

                        my $result = {
                            success           => true,
                            i                 => $json->{i},
                            j                 => $json->{part},
                            parts             => $json->{total},
                            short             => $f->short,
                            name              => $f->filename,
                            size              => $f->filesize,
                            del_at_first_view => (($f->delete_at_first_view) ? true : false),
                            created_at        => $f->created_at,
                            delay             => $f->delete_at_day,
                            token             => $f->mod_token,
                            sent_delay        => $json->{delay},
                            duration          => time - $begin
                        };
                        $ws->send(to_json($result));
                    } else {
                        $ws->send(decode('UTF-8', encode_json(
                            {
                                success    => false,
                                msg        => $c->l('The server was unable to find the file record to add your file part to. Please, contact the administrator.'),
                                sent_delay => $json->{delay},
                                i          => $json->{i}
                            }
                        )));
                    }
                }
            }
        );
        $c->on(
            finish => sub {
                $c->app->log->debug('Client disconnected');
            }
        );
    } else {
        $c->on(
            message => sub {
                $c->app->log->info(sprintf('Someone unauthenticated tried to upload a file. IP: %s', $c->ip));
                $c->finish;
            }
        );
    }
}

sub download {
    my $c     = shift;
    my $short = $c->param('short');

    $c->inactivity_timeout(300000);
    $c->app->log->debug('Client connected');

    my $ldfile = Lufi::DB::File->new(app => $c->app)->from_short($short);

    # Do we have a file?
    if (defined $ldfile) {
        # Is the file fully uploaded?
        if ($ldfile->deleted
            || (
                $ldfile->delete_at_day != 0
                && (
                    ($ldfile->created_at + $ldfile->delete_at_day * 86400) < time()
                )
            )
        ) {
            unless ($ldfile->deleted) {
                $ldfile->delete;
            }
            $c->on(
                message => sub {
                    my ($ws, $json) = @_;
                    $c->send(decode('UTF-8', encode_json(
                        {
                            success => false,
                            msg     => $c->l('Error: the file existed but was deleted.')
                        }
                    )));
                }
            );
        } elsif (defined($ldfile->abuse)) {
            my $abuse_msg = $c->l('This file has been deactivated by the admins. Contact them to know why.');
            $abuse_msg    = $c->app->config('abuse')->{$ldfile->abuse} if ($c->app->config('abuse') && $c->app->config('abuse')->{$ldfile->abuse});
            $c->on(
                message => sub {
                    my ($ws, $json) = @_;
                    $c->send(decode('UTF-8', encode_json(
                        {
                            success => false,
                            msg     => $abuse_msg
                        }
                    )));
                }
            );
        } elsif ($ldfile->complete) {
            my $f = $ldfile;

            $c->on(
                message => sub {
                    my ($ws, $json) = @_;
                    $json = decode_json $json;

                    # Do we need a password?
                    my $valid = 1;
                    if ($c->config('allow_pwd_on_files') && defined($f->{passwd})) {
                        my $pwd = $json->{file_pwd};
                        $valid = Crypt::SaltedHash->validate($f->{passwd}, $json->{file_pwd}, 8);
                    }

                    if ($valid) {
                        if (defined($json->{part})) {
                            # Make $num an integer instead of a string
                            my $num = $json->{part} + 0;

                            # Get the slice
                            my $e    = $f->slices->[$num];
                            my $text = $e->retrieve();

                            my ($json2) = split('XXMOJOXX', $text, 2);
                            $json2 = decode 'UTF-8', $json2;
                            $text =~ s/^.*?XXMOJOXX/${json2}XXMOJOXX/;

                            # Send the slice
                            $c->send($text);
                        } elsif (defined($json->{ended}) && $json->{ended}) {
                            $f->counter($f->counter + 1);
                            $f->last_access_at(time);

                            if ($f->delete_at_first_view) {
                                $f->delete;
                            } else {
                                $f->write;
                            }
                        }
                    } else {
                        $c->send(decode('UTF-8', encode_json(
                            {
                                msg => $c->l('Your password is not valid. Please refresh the page to retry.')
                            }
                        )));
                    }
                }
            );
            $c->on(
                finish => sub {
                    $c->app->log->debug('Client disconnected');
                }
            );
        } else {
            $c->on(
                message => sub {
                    my ($ws, $json) = @_;
                    $c->send(decode('UTF-8', encode_json(
                        {
                            success => false,
                            msg     => $c->l('Error: the file has not been sent entirely.')
                        }
                    )));
                }
            );
        }
    } else {
        $c->send(decode('UTF-8', encode_json(
            {
                success => false,
                msg     => $c->l('Error: unable to find the file. Are you sure of your URL?')
            }
        )));
    }
}

sub r {
    my $c     = shift;
    my $short = $c->param('short');

    my $ldfile = Lufi::DB::File->new(app => $c->app)->from_short($short);

    if (defined $ldfile) {
        return $c->render(
            template => 'render',
            f        => $ldfile,
            file_pwd => ($c->config('allow_pwd_on_files') && defined($ldfile->passwd))
        );
    } else {
        return $c->render(
            template => 'render',
            msg      => $c->l('Could not find the file. Are you sure of the URL?')
        );
    }
}

sub get_counter {
    my $c     = shift;
    my $short = $c->param('short');
    my $token = $c->param('token');

    if ((!defined($c->config('ldap')) && !defined($c->config('htpasswd'))) || $c->is_user_authenticated) {
        my $ldfile = Lufi::DB::File->new(app => $c->app)->from_short($short);

        if (defined $ldfile) {
            if ($ldfile->mod_token eq $token) {
                return $c->render(
                    json => {
                        success => true,
                        short   => $short,
                        counter => $ldfile->counter,
                        deleted => ($ldfile->deleted) ? true : false
                    }
                );
            } else {
                return $c->render(
                    json => {
                        success => false,
                        missing => false,
                        short   => $short,
                        msg     => $c->l('Unable to get counter for %1. The token is invalid.', $short)
                    }
                );
            }
        } else {
            return $c->render(
                json => {
                    success => false,
                    missing => true,
                    short   => $short,
                    msg     => $c->l('Unable to get counter for %1. The file does not exists. It will be removed from your localStorage.', $short)
                }
            );
        }
    } else {
        return $c->render(
            json => {
                success => false,
                missing => false,
                short   => $short,
                msg     => $c->l('Unable to get counter for %1. You are not authenticated.', $short)
            }
        );
    }
}

sub delete {
    my $c     = shift;
    my $short = $c->param('short');
    my $token = $c->param('token');

    if ((!defined($c->config('ldap')) && !defined($c->config('htpasswd'))) || $c->is_user_authenticated) {
        my $ldfile = Lufi::DB::File->new(app => $c->app)->from_short($short);

        $ldfile = undef unless (defined($ldfile) && $ldfile->mod_token eq $token);

        if (defined $ldfile) {
            my $msg;
            if ($ldfile->deleted) {
                $msg = $c->l('The file has already been deleted');
            } else {
                $ldfile->delete;
                $msg = $c->l('File deleted');
            }
            return $c->respond_to(
                json => {
                    json => {
                        success => true,
                        msg     => $msg
                    }
                },
                any => sub {
                    $c->render(
                        template => 'msg',
                        f        => $ldfile,
                        msg      => $msg
                    );
                }
            );
        } else {
            my $msg = $c->l('Could not find the file. Are you sure of the URL and the token?');
            return $c->respond_to(
                json => {
                    json => {
                        success => false,
                        msg     => $msg
                    }
                },
                any => sub {
                    $c->render(
                        template => 'msg',
                        f        => undef,
                        msg      => $msg
                    );
                }
            );
        }
    } else {
        my $msg = $c->l('Could not delete the file. You are not authenticated.');
        return $c->respond_to(
            json => {
                json => {
                    success => false,
                    msg     => $msg
                }
            },
            any => sub {
                $c->render(
                    template => 'msg',
                    f        => undef,
                    msg      => $msg
                );
            }
        );
    }
}

1;
