#/*
# *  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: App.pm 1057 2007-09-04 14:37:43Z hikarin $
#

package Zeromin2::App;

use strict;

BEGIN {
    my $pkg = __PACKAGE__;
    for my $mtd (qw(bbs key user kernel request)) {
        my $attr = '__' . $mtd;
        no strict 'refs';
        *{"${pkg}::${mtd}"} = sub { $_[0]->{$attr} }
    }
}

sub new {
    my ( $zClass, $iKernel, @args ) = @_;
    bless {
        __kernel  => $iKernel,
        __request => Img0ch::Request->new(@args),
    }, $zClass;
}

sub DESTROY {
    my ($zApp) = @_;
    if ( my $session = $zApp->{__session} ) {
        if ( !$session->is_expired() and !$session->is_empty() ) {
            $session->param( 'token', $zApp->{__session_token} );
            $session->expire( 'token', '+15m' );
            $session->flush();
        }
    }
    CGI::Session->name('CGISESSID');
    return;
}

sub run {
    my ($zApp)   = @_;
    my $iConfig  = $zApp->kernel()->get_config();
    my $iRequest = $zApp->{__request};
    my $content;

    $zApp->proxy( \$content, $iRequest );
    if ( $iConfig->get('ZerominUseGzip') ) {
        my $compress = sub {
            require Compress::Zlib;
            my ($content_ref) = @_;
            $$content_ref = Compress::Zlib::memGzip($$content_ref);
            return;
        };
        my $accept = $iRequest->get_header('accept-encoding');
        $iRequest->enable_compress();
        $compress->( \$content );
    }

    my $charset = $zApp->encoding();
    $iRequest->send_http_header( $zApp->{__content_type}, $charset );
    $iRequest->print( \$content );
}

sub proxy {
    my ( $zApp, $content, $iRequest ) = @_;
    $iRequest ||= $zApp->{__request};
    my $Qclass;
    my $param = {};

    $zApp->start_session($param);
    my $class  = $iRequest->param('class')  || $param->{class};
    my $method = $iRequest->param('method') || $param->{method};
    if ( $class and $method ) {
        $Qclass = 'Zeromin2::App::' . lc $class;
        $method =~ /\A[\w_]+\z/xms or die 'Invalid method', "\n";
    }
    else {
        $iRequest->init( $zApp->kernel()->get_config() );
        $zApp->render_as_html( $content, $iRequest, { Start => 1 } );
        return 1;
    }

    eval {
        my $mtd = $zApp->load( $Qclass, $method )
            or die "${Qclass}::${method}() not found\n";
        $iRequest->init( $zApp->kernel()->get_config() );

        my $iKernel       = $zApp->{__kernel};
        my $bbs_requested = $iRequest->bbs();
        my $bbs           = $bbs_requested || $param->{bbs};
        my $key           = $iRequest->param('key') || $param->{thread};
        my $bbs_dir       = '';
        if ($bbs) {
            require Img0ch::BBS;
            my $iBBS = Img0ch::BBS->new( $iKernel, { id => $bbs, } );
            $iBBS->get_id()
                or $iBBS = Img0ch::BBS->new( $iKernel, { bbs => $bbs, } );
            $zApp->{__bbs} = $iBBS;
            $zApp->{__key} = $key;
            $bbs_dir       = $iBBS->get_name();
        }

        my $ok = $zApp->validate( ( $zApp->{__bbs} || $iKernel ),
            $iRequest, $param );
        if ( $iRequest->param('js') ) {
            $zApp->{__content_type} = 'text/javascript';
            $$content = ${
                $iRequest->get_json(
                    {   ok      => 1,
                        loginOK => $ok,
                        data    => $mtd->($zApp),
                    }
                )
                };
        }
        else {
            my $result;
            if ($ok) {
                my $file;
                $result = $mtd->($zApp);
                if ( $zApp->is_success($result) ) {
                    $file = $zApp->_result(
                        $iRequest, $Qclass, $class,
                        $method,   $result, 'OK'
                    );
                }
                else {
                    $file = $zApp->_result(
                        $iRequest, $Qclass, $class,
                        $method,   $result, 'NG'
                    );
                }
                my $iConfig = $zApp->kernel()->get_config();
                my $code    = 'Code' . $result->{Code};
                my $done    = 'Done_' . $class . '_' . $method;
                my $sessnm  = $iConfig->get('zeromin2.session_name')
                    || 'zeromin_session';
                $zApp->render_as_html(
                    $content,
                    $iRequest,
                    {   ( map { %{$_} } @{ $zApp->{__template} } ),
                        %$result,
                        BBS         => $bbs,
                        BBSName     => $bbs_dir,
                        KEY         => $key,
                        LoginOK     => 1,
                        LoginUser   => ( $iRequest->param('user') || '' ),
                        LoginPass   => ( $iRequest->param('pass') || '' ),
                        Session     => $param->{session},
                        SessionName => $sessnm,
                        Token       => $param->{token},
                        $done       => 1,
                        $code       => 1
                    },
                    $zApp->template_directory(),
                    $file
                );
                $zApp->save_session( $bbs_requested, $key );
            }
            else {
                $result = { loginOK => 0 };
                $zApp->render_as_html( $content, $iRequest,
                    { LoginOK => 0 } );
            }
        }
        return 1;
    };
    if ( my $e = $@ ) {
        if ( $iRequest->param('js') ) {
            $zApp->{__content_type} = 'text/javascript';
            $$content
                = ${ $iRequest->get_json( { loginOK => 0, error => $e } ) };
        }
        else {
            $zApp->{__content_type} = 'text/html';
            $zApp->render_as_html( $content, $iRequest,
                { LoginOK => 1, Error => $e } );
        }
        return 0;
    }
    else {
        return 1;
    }
}

sub is_success {
    my ( $zApp, $data ) = @_;
    ( ref $data || '' ) eq 'HASH' or return 0;
    return $data->{Code} == 0 ? 1 : 0;
}

sub _result {
    my ( $zApp, $iRequest, $Qclass, $class, $method, $result, $param ) = @_;
    my $file;
    if ( my $method_param = $iRequest->param($param) ) {
        $method = $method_param;
        $method =~ /\A[\w_]+\z/xms or die 'Invalid method', "\n";
        my $mtd = $zApp->load( $Qclass, $method )
            or die "${Qclass}::${$method}() not found\n";
        my $code = $result->{Code};
        my $ret  = $mtd->($zApp);
        %$result = ( %$result, %{ $mtd->($zApp) } );
        $result->{Code} = $code;
        $file = join '_', $class, $method;
        $zApp->{__action_state} = [ $class, $method ];
    }
    else {
        $file = join '_', $class, $method;
        $zApp->{__action_state} = [ $class, $method ];
    }
    return $file;
}

sub null {undef}

sub logger {
    my ( $zApp, $success_or_fail, $reason, $params ) = @_;
    my $class = (caller)[0];
    my $zUser = $zApp->user();
    my $user  = $zUser ? $zUser->get_current_user->{id} : 0;
    my $ip    = $zApp->request()->ip_int();

    require Zeromin::Log::Action;
    my $zLogAct = Zeromin::Log::Action->new( $zApp->kernel() );
    $zLogAct->load();
    $zLogAct->add( $class, $user, $ip, $success_or_fail, $reason, $params );
    $zLogAct->save();
    1;
}

sub encoding {
    $_[1] and $_[0]->{__encoding} = $_[1];
    $_[0]->{__encoding} || 'UTF-8';
}

sub content_type {
    $_[1] and $_[0]->{__content_type} = $_[1];
    $_[0]->{__content_type} || 'text/html';
}

sub add_template_param {
    my ( $zApp, $tagset ) = @_;
    ( ref $tagset || '' ) eq 'HASH' or return;
    $zApp->{__template} ||= [];
    push @{ $zApp->{__template} }, $tagset;
    return;
}

sub template_directory {'zeromin2'}

sub template_file {'login'}

sub template_installer {'init_install'}

sub self {
    my ( $zApp, $iConfig ) = @_;
    $iConfig->get('zeromin2.url') || 'zeromin2.cgi';
}

sub _html {
    my ( $zApp, $content, $iRequest, $parameters, $t_dir, $t_file ) = @_;
    $parameters             ||= {};
    $zApp->{__content_type} ||= 'text/html';

    require Img0ch::Template;
    require Zeromin::User;
    my ( $dir, $file );
    my $zUser = Zeromin::User->new( $zApp->{__kernel} );
    $zUser->load();
    $zApp->{__user} = $zUser;

    if ( $zUser->is_initialized() ) {
        $dir  = $t_dir  || $zApp->template_directory();
        $file = $t_file || $zApp->template_file();
    }
    else {
        $dir  = $zApp->template_directory();
        $file = $zApp->template_installer();
    }

    my $iTemplate = Img0ch::Template->new(
        $zApp->{__kernel},
        {   dir     => $dir,
            file    => $file,
            request => $iRequest,
        }
    );
    my $iConfig = $zApp->kernel()->get_config();
    my $self    = $zApp->self($iConfig);
    $iTemplate->param( { %$parameters, ZerominScript => $self } );
    $$content = ${ $iTemplate->to_string() };
    return;
}

*render_as_html = \&_html;

sub return_value {
    my ( $zApp, $code, $content, $page ) = @_;
    my $result;

    if ($page) {
        my $pages = [];
        map { push @$pages, { num => $_ } }
            ( $page->first_page() .. $page->last_page() );
        $result = {
            Entries => $content,
            Pages   => $pages,
            Total   => $page->total_entries(),
            Current => $page->current_page(),
            Prev    => $page->previous_page(),
            Next    => $page->next_page(),
            Item    => $page->entries_per_page()
        };
    }
    else {
        $result = { Entries => $content };
    }
    return { Code => $code, %{$result} };
}

sub page_param {
    my ( $zApp, $page_suffix_name ) = @_;
    my $iRequest = $zApp->request();
    my $session  = $zApp->{__session};
    my $offset   = $iRequest->param_int('offset');
    my $param    = $iRequest->param_int('item');

    $page_suffix_name ||= 'global';
    my $key = 'page_item_' . $page_suffix_name;
    my $item = $param || $session->param($key);

    if ($param) {
        $session->param( $key, $param );
        $session->flush();
    }

    return ( $item, $offset );
}

sub page_template {
    my ( $zApp, $page ) = @_;
    my $pages;

    map { push @$pages, { num => $_ } }
        ( $page->first_page() .. $page->last_page() );
    return {
        Pages   => $pages,
        Total   => $page->total_entries(),
        Current => $page->current_page(),
        Prev    => $page->previous_page(),
        Next    => $page->next_page(),
        Item    => $page->entries_per_page()
    };
}

sub start_session {
    my ( $zApp, $parameter ) = @_;
    my $iKernel          = $zApp->kernel();
    my $iRequest         = $zApp->request();
    my $iConfig          = $iKernel->get_config();
    my $session_save_dir = $iConfig->get('zeromin2.session_dir')
        || die '"zeromin2.session_dir" value is empty', "\n";

    my $dsn = 'driver:file;id:md5;';
    if ( eval 'use Storable qw(); 1' ) {
        $dsn .= ';serializer:storable';
    }
    else {
        require FreezeThaw;
        $dsn .= ';serializer:freezethaw';
    }

    require CGI::Session;
    CGI::Session->name( $iConfig->get('zeromin2.session_name')
            || 'zeromin_session' );
    my $session
        = CGI::Session->load( $dsn, $iRequest,
        { Directory => $session_save_dir } )
        or $iKernel->throw_exception( CGI::Session->errstr() );

    $zApp->{__session} = $session;
    if ( !$session->is_expired() and !$session->is_empty() ) {
        %{$parameter} = %{ $session->dataref() };
    }
    return;
}

sub save_session {
    my ( $zApp, $bbs, $key ) = @_;
    $zApp->{__prevent_save_session} and return;

    my $session = $zApp->{__session};
    my $action  = $zApp->{__action_state};
    if ( $zApp->{__user} ) {
        $session->param( 'class',  $action->[0] );
        $session->param( 'method', $action->[1] );
        $session->param( 'bbs',    $bbs );
        $session->param( 'thread', $key );
        $session->flush();
    }
    return;
}

sub prevent_saving_session {
    $_[0]->{__prevent_save_session} = 1;
    return;
}

sub validate {
    my ( $zApp, $iObject, $iRequest, $parameter ) = @_;
    my $iKernel = $zApp->kernel();
    my $iConfig = $iKernel->get_config();
    my $session = $zApp->{__session};
    my ( $id, $ret, $token );

    require Zeromin::User;
    my $zUser = Zeromin::User->new($iObject);
    $zUser->load();
    $zUser->is_initialized() or return 1;
    $zApp->{__user} = $zUser;

    if ( $session->is_expired() ) {
        $session->close();
        $session->delete();
        my $user = $iRequest->param('user');
        my $pass = $iRequest->param('pass');
        $ret = $zUser->is_valid( $user, $pass );
        $ret ? $zApp->logger( 1, 'LOGIN' ) : $zApp->logger(
            0,
            'FAILED TO LOGIN FROM %s WITH %s',
            [ $user, $pass ]
        );
    }
    elsif ( $session->is_empty() ) {
        my $user = $iRequest->param('user');
        my $pass = $iRequest->param('pass');
        if ( $ret = $zUser->is_valid( $pass, $user ) ) {
            $session = $session->new()
                or $iKernel->throw_exception( $session->errstr() );
            $id = $session->id();
            $token = _generate_token( $iConfig, $iRequest, $id );
            $session->param( 'user',  $user );
            $session->param( 'pass',  $pass );
            $session->param( 'token', $token );
            $session->expire('+1h');
            $session->flush();
            my $cookie_path = $iConfig->get('zeromin2.cookie_path');
            my $cookie_sec  = $iConfig->get('zeromin2.secured_cookie');

            if ( !$cookie_path ) {
                require File::Basename;
                $cookie_path
                    = File::Basename::dirname( $iRequest->request_uri() );
            }
            $iRequest->cookie(
                $session->name(),
                {   value  => $id,
                    path   => $cookie_path,
                    secure => $cookie_sec
                }
            );
            $zApp->logger( 1, 'LOGIN' );
        }
        else {
            $zApp->logger(
                0,
                'FAILED TO LOGIN FROM %s WITH %s',
                [ $user, $pass ]
            );
        }
    }
    else {
        my $user = $session->param('user');
        my $pass = $session->param('pass');
        if ( !( $ret = $zUser->is_valid( $pass, $user ) ) ) {
            $session->close();
            $session->delete();
            $zApp->logger(
                0,
                'FAILED TO LOGIN FROM %s WITH %s',
                [ $user, $pass ]
            );
        }
        else {
            $id = $session->id();
            $token = _generate_token( $iConfig, $iRequest, $id );
        }
    }

    $parameter->{token}      = $token;
    $parameter->{session}    = $id;
    $zApp->{__session_token} = $token;
    return $ret;
}

sub privilege {
    my ( $zApp, $privilege, $require_bbs_level ) = @_;
    my $zUser = $zApp->user() || return 0;
    if ($privilege) {
        $zUser->$privilege or return 0;
    }

    my $iBBS = $zApp->bbs();
    ( $require_bbs_level and !$iBBS ) and return 0;
    if ( my $range = $zUser->get_current_user()->{bbs} ) {
        $iBBS->get_id() == $range or return 0;
    }
    return 1;
}

sub check_csrf {
    my ($zApp)   = @_;
    my $iRequest = $zApp->request();
    my $session  = $zApp->{__session};
    $iRequest->request_by_post() or return 0;

    my $rtoken = $iRequest->param('token') || return 0;
    my $stoken = $session->param('token')  || return 0;
    return $rtoken eq $stoken ? 1 : 0;
}

sub session { $_[0]->{__session} }

sub install_alias {
    my ( $zClass, $class, $methods ) = @_;

    while ( my ( $method, $argument ) = each %$methods ) {
        my ( $module_name, $invoker ) = @$argument;
        my $module_file = 'Zeromin2/App/' . $module_name . '.pm';
        my $module_ns   = 'Zeromin2::App::' . $module_name;
        no strict 'refs';
        *{"${class}::${method}"} = sub {
            require $module_file;
            $module_ns->can($invoker)->(@_);
        };
    }
    return;
}

sub _generate_token {
    my ( $iConfig, $iRequest, $id ) = @_;
    my $seed  = $iConfig->get('Seed');
    my $tdir  = $iConfig->get('TemplateCacheRoot');
    my $bpath = $iConfig->get('BBSPath');
    my $rpath = $iConfig->get('RepositoryRoot');
    my $time  = $iRequest->now();
    return Digest::MD5::md5_base64( $id, $seed, $tdir, $bpath, $rpath, $$,
        $time, rand($time) );
}

sub load {
    my ( $zApp, $class, $method ) = @_;
    my $file = $class . '.pm';
    $file =~ s{::}{/}g;
    require $file;
    index( $method, '_' ) == 0 and return 0;
    return $class->can($method);
}

sub unijp {
    my ($zApp) = @_;
    require Unicode::Japanese;
    my $unijp    = Unicode::Japanese->new();
    my $encoding = $zApp->kernel()->get_encoding(1);
    return ( $unijp, $encoding );
}

1;
__END__
