#/*
# *  Copyright 2007 hkrn <hikarin@users.sourceforge.jp>
# *
# *  Licensed under the Apache License, Version 2.0 (the "License");
# *  you may not use this file except in compliance with the License.
# *  You may obtain a copy of the License at
# *
# *      http://www.apache.org/licenses/LICENSE-2.0
# *
# *  Unless required by applicable law or agreed to in writing, software
# *  distributed under the License is distributed on an "AS IS" BASIS,
# *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# *  See the License for the specific language governing permissions and
# *  limitations under the License.
# */
#
# $Id: bbs.pm 1926 2010-01-17 14:41:16Z hikarin $
#

package Zeromin2::App::bbs;

use strict;
use Zeromin::BBS qw();
use Zeromin2::App qw();

BEGIN {
    Zeromin2::App->install_alias( __PACKAGE__,
        {   'repair_thread_upload_file_table' =>
                [ 'thread', 'repair_upload_file_table' ],
            'remove_thread'         => [ 'thread', 'remove' ],
            'move_thread'           => [ 'thread', 'move' ],
            'stop_thread'           => [ 'thread', 'stop' ],
            'restart_thread'        => [ 'thread', 'restart' ],
            'remove_res'            => [ 'thread', 'remove_res' ],
            'erase_res'             => [ 'thread', 'erase_res' ],
            'remove_file'           => [ 'thread', 'remove_file' ],
            'edit_res_form'         => [ 'thread', 'edit_res_form' ],
            'import_thread'         => [ 'thread', 'do_import' ],
            'create_special_thread' => [ 'thread', 'create_special' ],
            'update_removed_upload_files_as_json' =>
                [ 'upload', 'update_removed_upload_files_as_json' ],
            'freeze_file'        => [ 'upload',  'freeze_file' ],
            'release_file'       => [ 'upload',  'release_file' ],
            'pool_thread'        => [ 'pool',    'create' ],
            'restore_pool'       => [ 'pool',    'restore' ],
            'remove_pool'        => [ 'pool',    'remove' ],
            'create_archive'     => [ 'archive', 'create' ],
            'update_archive'     => [ 'archive', 'update' ],
            'remove_archive'     => [ 'archive', 'remove' ],
            'deny_ip_local'      => [ 'filter',  'deny_ip_local' ],
            'deny_ip_global'     => [ 'filter',  'deny_ip_global' ],
            'deny_serial_local'  => [ 'filter',  'deny_serial_local' ],
            'deny_serial_global' => [ 'filter',  'deny_serial_global' ],
        }
    );
}

sub load {
    my ($zApp) = @_;
    $zApp->privilege() or return $zApp->return_value(1);

    require Zeromin::Category;
    my $iKernel   = $zApp->kernel();
    my $zBBS      = Zeromin::BBS->new( $iKernel, { bbs => '' } );
    my $zCategory = Zeromin::Category->new($iKernel);
    my $bbs_range = $zApp->user()->get_bbs_range();
    my $page_name = 'bbs';
    my ( $unijp, $encoding ) = $zApp->unijp();
    my @ret;

    if (@$bbs_range) {
        @ret = $zBBS->gets_with_page( $bbs_range, $unijp, $zCategory,
            $zApp->page_param($page_name) );
    }
    else {
        @ret = $zBBS->all_with_page( $unijp, $zCategory,
            $zApp->page_param($page_name) );
    }

    return $zApp->return_value( 0, @ret );
}

sub load_index {
    my ($zApp) = @_;
    $zApp->privilege( undef, 1 ) or return $zApp->return_value(1);

    require Img0ch::App::BBS;
    my $iBBS     = $zApp->bbs();
    my $iKernel  = $zApp->kernel();
    my $iRequest = $zApp->request();
    my $iApp = Img0ch::App::BBS->new( $zApp->kernel(), $iRequest->request() );
    my ($param) = $iApp->update_index( $iBBS, 1, 1 );

    require Img0ch::Thread::Revision;
    require Img0ch::Upload::Poll;
    require Zeromin2::Util;
    my $encoding  = $iBBS->get_encoding();
    my $privilege = $zApp->privilege('can_view_thread_log');
    my $rewriter  = Zeromin2::Util::get_rewriter($zApp);

    for my $thread ( @{ $param->{Threads} } ) {
        my $key        = $thread->{key};
        my $iLog       = $iBBS->get_log_instance($key);
        my $iTRevision = Img0ch::Thread::Revision->new( $iBBS, $key );
        my $iUPoll     = Img0ch::Upload::Poll->new( $iBBS, $key );
        Zeromin2::Util::to_utf8( $iKernel, $encoding,
            [ \$thread->{subject} ] );
        for my $res ( @{ $thread->{Thread} } ) {
            my $resno = $res->{resno};
            if ($privilege) {
                my $info = $iLog->get($resno);
                my $ip   = $info->[1];
                $ip
                    = $ip
                    ? join '.', unpack( 'C*', $info->[1] )
                    : '(null)';
                $res->{ip}     = $ip;
                $res->{serial} = $info->[2] || '(null)';
                $res->{agent}  = $info->[3] || '(null)';
            }
            else {
                $res->{ip}     = '(null)';
                $res->{serial} = '(null)';
                $res->{agent}  = '(null)';
            }
            $res->{revision} = $iTRevision->get_latest_id();
            $res->{poll_bad_count} = $iUPoll->count( $resno, 'BAD' );
            Zeromin2::Util::to_utf8(
                $iKernel,
                $encoding,
                [   \$res->{name}, \$res->{mail},
                    \$res->{date}, \$res->{text},
                ]
            );
            $rewriter->( \$res->{text} );
        }
    }

    for my $thread ( @{ $param->{Stored} } ) {
        Zeromin2::Util::to_utf8( $iKernel, $encoding,
            [ \$thread->{subject} ] );
    }

    # redefine BBS_TITLE and BBS_SUBTITLE for using UTF-8
    my $iSetting      = $iApp->setting();
    my $bbs_title     = $iSetting->get('BBS_TITLE');
    my $bbs_sub_title = $iSetting->get('BBS_SUBTITLE');

    Zeromin2::Util::to_utf8(
        $iKernel,
        $encoding,
        [   \$param->{HEAD},      \$param->{FOOT}, \$param->{Banner},
            \$param->{SubBanner}, \$bbs_title,     \$bbs_sub_title,
        ]
    );
    $rewriter->( \$param->{HEAD} );
    $rewriter->( \$param->{FOOT} );
    $rewriter->( \$param->{Banner} );
    $rewriter->( \$param->{SubBanner} );

    $zApp->add_template_param(
        {   %{ $iSetting->get_hash() },
            %$param,
            BBS_TITLE    => $bbs_title,
            BBS_SUBTITLE => $bbs_sub_title,
            BBSName      => $iBBS->get_name(),
            IsTypeTemplate =>
                ( $iSetting->get('BBS_TEMPLATE') =~ /\Atype/xms ? 1 : 0 ),
        }
    );
    return $zApp->return_value(0);
}

sub load_threads {
    _load_threads( $_[0], 'Zeromin/Subject.pm', 'Zeromin::Subject', undef,
        'thread' );
}

sub load_special_threads {
    _load_threads( $_[0], 'Zeromin/Thread/Special.pm',
        'Zeromin::Thread::Special', undef, 'special_thread' );
}

sub load_pooled_threads {
    _load_threads( $_[0], 'Zeromin/Pool.pm', 'Zeromin::Pool',
        'can_enter_pool_section', 'pool' );
}

sub load_archived_threads {
    _load_threads( $_[0], 'Zeromin/Archive.pm', 'Zeromin::Archive',
        'can_enter_archive_section', 'archive' );
}

sub load_plugins {
    require Zeromin2::App::plugin;
    Zeromin2::App::plugin::load( $_[0], 1 );
}

sub save_plugins {
    require Zeromin2::App::plugin;
    Zeromin2::App::plugin::save( $_[0], 1 );
}

sub select_threads_to_move {
    my ($zApp) = @_;
    $zApp->privilege( 'can_stop_thread', 1 )
        or return $zApp->return_value( 1, [] );

    require Zeromin::Category;
    require Zeromin::Subject;

    my $iKernel = $zApp->kernel();
    my $zBBS
        = Zeromin::BBS->new( $iKernel, { bbs => $zApp->bbs()->get_name() } );
    my $zCategory = Zeromin::Category->new($iKernel);
    my $bbs_range = $zApp->user()->get_bbs_range();
    my ( $unijp, $encoding ) = $zApp->unijp();
    my @ret;

    if (@$bbs_range) {
        @ret = $zBBS->gets( $bbs_range, $unijp, $zCategory );
    }
    else {
        @ret = $zBBS->all( $unijp, $zCategory );
    }

    my $zSubject = Zeromin::Subject->new($zBBS);
    $zSubject->load();

    my $threads = [];
    my $keys    = $zSubject->to_array();
    for my $key (@$keys) {
        my $data = $zSubject->get_utf8($key);
        push @$threads,
            {
            key     => $key,
            subject => $data->[0],
            };
    }

    $zApp->add_template_param(
        {   BBSName => $zBBS->get_name(),
            BBSList => @ret,
            Threads => $threads,
        }
    );

    return $zApp->return_value(0);
}

sub _load_threads {
    my ( $zApp, $module_file, $class_name, $privilege, $page_name ) = @_;
    $zApp->privilege( $privilege, 1 ) or return $zApp->return_value( 1, [] );

    require $module_file;
    my $threads  = [];
    my $iBBS     = $zApp->bbs();
    my $zSubject = $class_name->new($iBBS);
    my ( $unijp, $encoding ) = $zApp->unijp();
    $zSubject->load();

    require Zeromin2::Util;
    my $log_privilege = $zApp->privilege('can_view_thread_log');
    my $iRequest      = $zApp->request();
    my $sort          = $iRequest->param('sort_by');
    my $now           = time();
    my ( $keys, $page );

    $sort
        ? $keys
        = $zSubject->to_array()
        : ( ( $keys, $page )
        = $zSubject->to_array_with_page( $zApp->page_param($page_name) ) );
    for my $key (@$keys) {
        my $agent  = '(null)';
        my $ip     = '(null)';
        my $data   = $zSubject->get_utf8($key);
        my $count  = $data->[1];
        my $elapse = $now - $key;
        my $speed  = $count / $elapse * 86400;
        $speed < 0.05 and $speed = 0.1;
        if ($log_privilege) {
            my $iLog = $iBBS->get_log_instance($key);
            my $info = $iLog->get(1);
            $agent = $info->[3];
            if ( my $serial = $info->[2] ) {
                $ip = $serial;
            }
            else {
                $ip = $info->[1];
                $ip = $ip ? join '.', unpack( 'C*', $ip ) : '(null)';
            }
        }
        push @$threads,
            {
            agent   => $agent,
            count   => $count,
            date    => Zeromin2::Util::format_date($key),
            key     => $key,
            ip      => $ip,
            speed   => sprintf( '%01.1f', $speed ),
            subject => $data->[0],
            };
    }

    my $is_descend = 0;
    if ($sort) {
        my ( $sort_type, $is_numeric ) = @{
            {   count   => [ 'count',   1 ],
                date    => [ 'date',    0 ],
                key     => [ 'key',     0 ],
                ip      => [ 'ip',      0 ],
                speed   => [ 'speed',   1 ],
                subject => [ 'subject', 0 ]
            }->{$sort}
                || [ 'count', 1 ]
            };
        $iRequest->param('descend') and $is_descend = 1;
        my $sort_function
            = $is_numeric
            ? (
            $is_descend
            ? sub { $b->{$sort_type} <=> $a->{$sort_type} }
            : sub { $a->{$sort_type} <=> $b->{$sort_type} }
            )
            : (
            $is_descend
            ? sub { $b->{$sort_type} cmp $a->{$sort_type} }
            : sub { $a->{$sort_type} cmp $b->{$sort_type} }
            );
        my @sorted = sort $sort_function @$threads;
        require Data::Page;
        $page = Data::Page->new( scalar @sorted,
            $zApp->page_param($page_name) );
        @$threads = $page->splice( \@sorted );
    }

    $zApp->add_template_param(
        {   BBSName => $iBBS->get_name(),
            Descend => $is_descend,
            SortBy  => $sort,
        }
    );
    return $zApp->return_value( 0, $threads, $page );
}

sub create {
    my ($zApp) = @_;
    $zApp->check_csrf() or return $zApp->return_value(1);
    $zApp->privilege('can_create_bbs') or return $zApp->return_value(1);

    my $iKernel  = $zApp->kernel();
    my $iRequest = $zApp->request();
    my $error    = {};
    my $param    = {};
    _validate_bbs_create_param( $zApp, $iKernel, $iRequest, $param, $error )
        or return $zApp->return_value( $error->{code} );

    my $bbs     = $param->{bbs_dir};
    my $name    = $param->{bbs_name};
    my $subname = $param->{bbs_sub_name};

    require Zeromin::BBS;
    my $zBBS = Zeromin::BBS->new( $zApp->kernel(), { bbs => $bbs } );
    $zBBS->create(
        {   category   => $param->{bbs_category},
            request    => $iRequest,
            sub_title  => $subname,
            title      => $name,
            uploadable => $param->{bbs_uploadable},
            inherit    => $param->{bbs_inherit},
            utf8       => $param->{bbs_utf8},
        }
    ) or return $zApp->return_value(7);
    $zApp->logger( 1, 'CREATED BBS: %s', [$bbs] );

    $zApp->add_template_param( { Done_bbs_create => 1 } );
    return $zApp->return_value( 0, $zBBS->get_id() );
}

sub create_form {
    my ($zApp) = @_;
    $zApp->privilege('can_create_bbs') or return $zApp->return_value(1);

    require Zeromin::Category;
    my $iKernel   = $zApp->kernel();
    my $zCategory = Zeromin::Category->new($iKernel);

    my ( $unijp, $encoding ) = $zApp->unijp();
    my $zBBS = Zeromin::BBS->new( $iKernel, { bbs => '' } );
    my $param = {
        BBSList      => $zBBS->all( $unijp, $zCategory ),
        CategoryList => $zCategory->all()
    };

    $zApp->add_template_param($param);
    return $zApp->return_value( 0, $param );
}

sub change_category {
    my ($zApp) = @_;
    $zApp->check_csrf() or return $zApp->return_value(1);
    $zApp->privilege('can_create_bbs') or return $zApp->return_value(1);

    my $iRequest = $zApp->request();
    my @param    = $iRequest->param('board');
    my $category = $iRequest->param('category');
    my $status   = {};

    for my $id (@param) {
        $id = Img0ch::Kernel::intval($id) || next;
        my $zBBS = Zeromin::BBS->new( $zApp->kernel(), { id => $id } );
        if ( $zBBS->change_category($category) ) {
            $zApp->logger(
                1,
                'CHANGED CATEGORY TO %s: %s',
                [ $category, $id ]
            );
            $status->{$id} = $category;
        }
        else {
            $zApp->logger(
                1,
                'TRIED CHANGING INEXIST CATEGORY (%s): %s',
                [ $category, $id ],
            );
            $status->{$id} = 0;
        }
    }

    return $zApp->return_value( 0, $status );
}

sub remove {
    my ($zApp) = @_;
    $zApp->check_csrf() or return $zApp->return_value(1);
    $zApp->privilege('can_remove_bbs') or return $zApp->return_value(1);

    my $zUser    = $zApp->user();
    my $iKernel  = $zApp->kernel();
    my $iRequest = $zApp->request();
    my @param    = $iRequest->param('board');
    scalar @param or return $zApp->return_value(2);

    my $status = {};
    for my $id (@param) {
        $id = Img0ch::Kernel::intval($id) || next;
        $zUser->have_privilege($id) or next;
        my $zBBS = Zeromin::BBS->new( $iKernel, { id => $id } );
        my $name = $zBBS->get_name();
        if ( $zBBS->remove() ) {
            $zApp->logger( 1, 'REMOVED BBS: %s', [$name] );
            $status->{$name} = 1;
        }
        else {
            $zApp->logger( 0, ('TRIED REMOVING BBS: %s'), [$name] );
            $status->{$name} = 0;
        }
    }

    return $zApp->return_value( 0, $status );
}

sub update_index {
    _task(
        shift, 'can_update_bbs', 'update',
        'UPDATED INDEX ON BBS',
        'TRIED UPDATING INDEX ON BBS',
    );
}

sub update_subject {
    _task(
        shift, 'can_update_subject', 'update_subject',
        'UPDATED SUBJECT ON BBS',
        'TRIED UPDATING SUBJECT ON BBS',
    );
}

sub recreate_subject {
    _task(
        shift, 'can_recreate_subject', 'recreate_subject',
        'RECREATED SUBJECT ON BBS',
        'TRIED RECREATING SUBJECT ON BBS',
    );
}

sub repair_upload_file_table {
    _task(
        shift,
        'can_create_bbs',
        'repair_upload_file_table',
        'REPAIRED UPLOADED FILE TABLE ON BBS',
        'TRIED REPAIRING UPLOADED FILE TABLE ON BBS',
    );
}

sub repair_tag_table {
    _task(
        shift,
        'can_recreate_subject',
        'repair_tag_table',
        'REPAIRED UPLOADED FILE TAG TABLE ON BBS',
        'TRIED REPAIRING UPLOADED FILE TAG TABLE ON BBS',
    );
}

sub stop {
    _task( shift, 'can_remove_bbs', 'stop', 'STOPPED BBS',
        'TRIED STOPPING BBS',
    );
}

sub update_bbsmenu {
    _task2( shift, 'can_update_bbslist', 'update_bbsmenu', 'UPDATED BBSMENU',
    );
}

sub repair_bbs_table {
    _task2( shift, 'can_create_bbs', 'repair_bbs_table', 'REPAIRED BBS TABLE',
    );
}

sub lock {
    _task3( shift, 'can_change_main_setting', { 'BBS_READONLY' => 'on' },
        'LOCKED BBS' );
}

sub unlock {
    _task3( shift, 'can_change_main_setting', { 'BBS_READONLY' => 'none' },
        'UNLOCKED BBS' );
}

sub _task {
    my ( $zApp, $priv_meth, $task_meth, $success, $fail ) = @_;
    $zApp->check_csrf() or return $zApp->return_value(1);
    $zApp->privilege($priv_meth) or return $zApp->return_value(1);

    my $zUser    = $zApp->user();
    my $iKernel  = $zApp->kernel();
    my $iRequest = $zApp->request();
    my @param    = $iRequest->param('board');
    scalar @param or return $zApp->return_value(2);

    my $status = {};
    for my $id (@param) {
        $id = Img0ch::Kernel::intval($id) || next;
        $zUser->have_privilege($id) or next;
        my $zBBS = Zeromin::BBS->new( $iKernel, { id => $id } );
        my $name = $zBBS->get_name();
        if ( $zBBS->$task_meth( $id, $iRequest ) ) {
            $zApp->logger( 1, ( $success . ': %s' ), [$name] );
            $status->{$name} = 1;
        }
        else {
            $zApp->logger( 0, ( $fail . ': %s' ), [$name] );
            $status->{$name} = 0;
        }
    }

    return $zApp->return_value( 0, $status );
}

sub _task2 {
    my ( $zApp, $priv_meth, $task_meth, $log ) = @_;
    $zApp->check_csrf() or return $zApp->return_value(1);
    $zApp->privilege($priv_meth) or return $zApp->return_value(1);

    my $zBBS = Zeromin::BBS->new( $zApp->kernel(), { id => 0 } );
    my $done = $zBBS->$task_meth();
    $zApp->logger( 1, $log, [] );

    return $zApp->return_value( 0, $done );
}

sub _task3 {
    my ( $zApp, $priv_meth, $settings, $message ) = @_;
    $zApp->check_csrf() or return $zApp->return_value(1);
    $zApp->privilege($priv_meth) or return $zApp->return_value(1);

    my $zUser   = $zApp->user();
    my $iKernel = $zApp->kernel();
    my @param   = $zApp->request()->param('board');
    scalar @param or return $zApp->return_value(2);

    require Zeromin::Setting;
    my $status = {};

    for my $id (@param) {
        $id = Img0ch::Kernel::intval($id) || next;
        $zUser->have_privilege($id) or next;
        my $zBBS     = Zeromin::BBS->new( $iKernel, { id => $id } );
        my $zSetting = Zeromin::Setting->new($zBBS);
        my $name     = $zBBS->get_name();

        while ( my ( $key, $value ) = each %$settings ) {
            $zSetting->set( $key, $value );
        }
        $zSetting->save();

        $zApp->logger( 1, ( $message . ': %s' ), [$name] );
        $status->{ $zBBS->get_name() } = 1;
    }

    return $zApp->return_value( 0, $status );
}

sub _validate_bbs_create_param {
    my ( $zApp, $iKernel, $iRequest, $param, $error ) = @_;
    my $category = $iRequest->param_int('category') || 1;
    my $dir      = $iRequest->param('dir');
    my $inherit  = $iRequest->param('inherit');
    my $name     = $iRequest->param('BBS_TITLE');
    my $subname  = $iRequest->param('BBS_SUBTITLE');

    if ( $dir =~ /\A\s*\z/xms ) {
        $zApp->logger( 0, 'NO BBS DIRECTORY WAS SET' );
        %{$error} = ( code => 2 );
        return 0;
    }
    elsif ( $name =~ /\A\s*\z/xms ) {
        $zApp->logger( 0, 'NO BBS NAME WAS SET' );
        %{$error} = ( code => 3 );
        return 0;
    }
    elsif ( $subname =~ /\A\s*\z/xms ) {
        $zApp->logger( 0, 'NO BBS SUB NAME WAS SET' );
        %{$error} = ( code => 4 );
        return 0;
    }
    elsif ( $dir !~ /\A[\w\-\.]+\z/xms ) {
        $zApp->logger( 0, 'INVALID BBS DIRECTORY WAS SET' );
        %{$error} = ( code => 5 );
        return 0;
    }
    elsif ($dir eq 'test'
        or $dir eq '_system'
        or $dir eq '_template'
        or $dir eq 'static' )
    {
        $zApp->logger( 0, 'RESERVED WORD (BBS DIRECTORY) WAS SET' );
        %{$error} = ( code => 6 );
        return 0;
    }

    my $iBBS = Img0ch::BBS->new( $iKernel, { id => $inherit } );
    $inherit = $iBBS->get_id() || '';

    $param->{bbs_category}   = $category;
    $param->{bbs_dir}        = $dir;
    $param->{bbs_inherit}    = $inherit;
    $param->{bbs_name}       = $name;
    $param->{bbs_sub_name}   = $subname;
    $param->{bbs_uploadable} = $iRequest->param('BBS_MODE') eq 'picture';
    $param->{bbs_utf8}       = $iRequest->param('utf8') eq 'true' ? 1 : 0;
    1;
}

1;
__END__
