###############################################################################
## Copyright 2005-2016 OCSInventory-NG/OCSInventory-Server contributors.
## See the Contributors file for more details about them.
## 
## This file is part of OCSInventory-NG/OCSInventory-ocsreports.
##
## OCSInventory-NG/OCSInventory-Server is free software: you can redistribute
## it and/or modify it under the terms of the GNU General Public License as
## published by the Free Software Foundation, either version 2 of the License,
## or (at your option) any later version.
##
## OCSInventory-NG/OCSInventory-Server is distributed in the hope that it
## will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
## of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with OCSInventory-NG/OCSInventory-ocsreports. if not, write to the
## Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
## MA 02110-1301, USA.
################################################################################
package Apache::Ocsinventory::Server::System;

use Apache::Ocsinventory::Server::System::Config;

use strict;

BEGIN{
  if($ENV{'OCS_MODPERL_VERSION'} == 1){
    require Apache::Ocsinventory::Server::Modperl1;
    Apache::Ocsinventory::Server::Modperl1->import();
  }elsif($ENV{'OCS_MODPERL_VERSION'} == 2){
    require Apache::Ocsinventory::Server::Modperl2;
    Apache::Ocsinventory::Server::Modperl2->import();
  }
  &check_config();
}

require Exporter;

our @ISA = qw /Exporter/;

our @EXPORT = qw /
  _log
  _debug
  _lock
  _unlock
  _send_file
/;

our %EXPORT_TAGS = (
  'server' => [ 
    qw/
    _get_xml_parser_opt
    _get_sys_options
    _database_connect
    _end
    _check_deviceid
    _log
    _debug
    _lock
    _unlock
    _send_file
    _inflate
    _get_spec_params
    _get_spec_params_g
    /
  ]
);

our @EXPORT_OK = (
  qw /
  _log 
  _lock
  _modules_get_request_handler
  _modules_get_pre_inventory_options
  _modules_get_post_inventory_options
  _modules_get_prolog_readers
  _modules_get_prolog_writers
  _modules_get_duplicate_handlers
  /
);

Exporter::export_ok_tags('server');

use Apache::Ocsinventory::Server::Constants;

sub _get_sys_options{

  return 0 if $ENV{OCS_OPT_OPTIONS_NOT_OVERLOADED};

  # Which options enabled ?
  #############
  # We read the table config looking for the ivalues of these options
  my $dbh = $Apache::Ocsinventory::CURRENT_CONTEXT{'DBI_HANDLE'};
  my $row;
  my $request = $dbh->prepare('SELECT * FROM config');
  $request->execute;

  # read options defined in ocs GUI
  while($row=$request->fetchrow_hashref){
    for( keys %CONFIG ){
      if( $row->{'NAME'} eq $_){
          $ENV{'OCS_OPT_'.$_} = $row->{ $CONFIG{$_}->{type} };
      }
    }
  }
  $request->finish;
  0;
}

# Try other compress algorithm
sub _inflate{
  my @inflate_subs = (
    # gzip file content
    sub {  my $data_ref = shift; 
      if(my $result = Compress::Zlib::memGunzip( ${$data_ref})){
        $Apache::Ocsinventory::CURRENT_CONTEXT{'DEFLATE_SUB'} = \&Compress::Zlib::memGzip;
        &_log(321,'system', 'gzip') if $ENV{'OCS_OPT_LOGLEVEL'};
        return $result; 
      }
      undef;
    },
    sub {
      my $ref = shift;
      if($$ref =~ /^<\?xml/i){
        $Apache::Ocsinventory::CURRENT_CONTEXT{'DEFLATE_SUB'} = sub {return $_[0]};
        &_log(321,'system', 'not_deflated') if $ENV{'OCS_OPT_LOGLEVEL'};
        return $$ref;
      }
      undef;
        }
  );
  my $data_ref = shift;
  my $inflated_ref = shift;
  
  for( @inflate_subs ){
    last if( $$inflated_ref = &{$_}($data_ref) );
  };
  1;
}

# Database connection
sub _database_connect{
  my $mode = shift;
  my %params;
  my ($host, $database, $port, $user, $password);
  
  my $env_mode_prefix = 'OCS_DB';

  if($mode eq 'write'){
    ($host, $database, $port, $user, $password) = ( $ENV{'OCS_DB_HOST'}, $ENV{'OCS_DB_NAME'}, $ENV{'OCS_DB_PORT'}, 
      $ENV{'OCS_DB_USER'}, $Apache::Ocsinventory::CURRENT_CONTEXT{'APACHE_OBJECT'}->dir_config('OCS_DB_PWD') );
  }
  # Local Mode
  elsif($mode eq 'local'){
    ($host, $port, $user, $password) = ( $ENV{'OCS_DB_HOST'}, $ENV{'OCS_DB_PORT'}, 
      $ENV{'OCS_DB_USER'}, $Apache::Ocsinventory::CURRENT_CONTEXT{'APACHE_OBJECT'}->dir_config('OCS_DB_PWD') );
    $database = $ENV{'OCS_DB_LOCAL'}||$ENV{'OCS_DB_NAME'};
  }
  # Slave mode
  elsif($mode eq 'read'){
    if($ENV{'OCS_DB_SL_HOST'}){
      $host = $ENV{'OCS_DB_SL_HOST'};
      $database = $ENV{'OCS_DB_SL_NAME'}||'ocsweb';
      $port = $ENV{'OCS_DB_SL_PORT'}||'3306';
      $user = $ENV{'OCS_DB_SL_USER'};
      $password  = $Apache::Ocsinventory::CURRENT_CONTEXT{'APACHE_OBJECT'}->dir_config('OCS_DB_SL_PWD');
      $env_mode_prefix .= '_SL';
    }
    else{
      $host = $ENV{'OCS_DB_HOST'};
      $database = $ENV{'OCS_DB_NAME'}||'ocsweb';
      $port = $ENV{'OCS_DB_PORT'}||'3306';
      $user = $ENV{'OCS_DB_USER'};
      $password  = $Apache::Ocsinventory::CURRENT_CONTEXT{'APACHE_OBJECT'}->dir_config('OCS_DB_PWD');
    }
  }
  else{
    &_log(521,'database_connect', 'invalid_mode') if $ENV{'OCS_OPT_LOGLEVEL'};
    return undef;
  }

  $params{'AutoCommit'} = 0;
  $params{'PrintError'} = $ENV{'OCS_OPT_DBI_PRINT_ERROR'};
  # Optionnaly a mysql socket different than the client's built in
  $params{'mysql_socket'} = $ENV{'OCS_OPT_DBI_MYSQL_SOCKET'} if $ENV{'OCS_OPT_DBI_MYSQL_SOCKET'};

  my $mysql_ssl_mode = '';
  if( defined($ENV{$env_mode_prefix.'_SSL_ENABLED'}) and $ENV{$env_mode_prefix.'_SSL_ENABLED'} == 1 )
  {
    if( defined($ENV{$env_mode_prefix.'_SSL_MODE'}) and $ENV{$env_mode_prefix.'_SSL_MODE'} eq 'SSL_MODE_PREFERRED' )
    {
        $mysql_ssl_mode = ';mysql_ssl=1;mysql_ssl_optional=1';
    }
    elsif( defined($ENV{$env_mode_prefix.'_SSL_MODE'}) and $ENV{$env_mode_prefix.'_SSL_MODE'} eq 'SSL_MODE_REQUIRED' )
    {
        $mysql_ssl_mode = ';mysql_ssl=1;mysql_ssl_verify_server_cert=0';
    }
    elsif( defined($ENV{$env_mode_prefix.'_SSL_MODE'}) and $ENV{$env_mode_prefix.'_SSL_MODE'} eq 'SSL_MODE_STRICT' )
    {
        $mysql_ssl_mode = ';mysql_ssl=1;mysql_ssl_verify_server_cert=1';
    }
    else
    {
        # SSL Is enabled but mode hasn't been provided. Let's put PREFERRED mode by default
        $mysql_ssl_mode = ';mysql_ssl=1;mysql_ssl_optional=1';
    }

    if( defined( $ENV{$env_mode_prefix.'_SSL_CLIENT_KEY'} ) and defined( $ENV{$env_mode_prefix.'_SSL_CLIENT_CERT'} ) and defined( $ENV{$env_mode_prefix.'_SSL_CA_CERT'} ) )
    {
        $mysql_ssl_mode .= ';mysql_ssl_client_key='.$ENV{$env_mode_prefix.'_SSL_CLIENT_KEY'}.';mysql_ssl_client_cert='.$ENV{$env_mode_prefix.'_SSL_CLIENT_CERT'}.';mysql_ssl_ca_file='.$ENV{$env_mode_prefix.'_SSL_CA_CERT'};
    }
  }

  # Connection...
  my $dbh = DBI->connect("DBI:mysql:database=$database;host=$host;port=$port".$mysql_ssl_mode, $user, $password, \%params);
  unless($dbh) {
    &_log(521, 'database_connect', DBI->errstr);
    return undef;
  }
  $dbh->do("SET NAMES 'utf8'") if($dbh && $ENV{'OCS_OPT_UNICODE_SUPPORT'});
  $dbh->do("SET sql_mode='NO_ENGINE_SUBSTITUTION'");
  return $dbh;
}

sub _check_deviceid{
  my $DeviceID = shift;

  # If we do not find it
  unless(defined($DeviceID)){
    return(1);
  }

  # If it is not conform
  unless($DeviceID=~/^.+-\d{4}(?:-\d{2}){5}$/){
    return(1);
  }
  0;
}

sub _lock{
   my $device = shift;
   my $dbh = $Apache::Ocsinventory::CURRENT_CONTEXT{'DBI_HANDLE'} || shift;
  
  if($dbh->do('INSERT INTO locks(HARDWARE_ID, ID, SINCE) VALUES(?,?,NOW())', {} , $device, $$ )){
    $Apache::Ocsinventory::CURRENT_CONTEXT{'LOCK_FL'} = 1 
      if $device eq ${Apache::Ocsinventory::CURRENT_CONTEXT{'DATABASE_ID'}};
    return(0);
  }else{
    if( $ENV{'OCS_OPT_LOCK_REUSE_TIME'} ){
      if( $dbh->do( 'SELECT * FROM locks  WHERE HARDWARE_ID=? AND (UNIX_TIMESTAMP()-UNIX_TIMESTAMP(SINCE))>?', 
        {}, $device, $ENV{'OCS_OPT_LOCK_REUSE_TIME'} ) != '0E0' ) {                      
        &_log(516,'lock', 'reuse_lock') if $ENV{'OCS_OPT_LOGLEVEL'};
        if( $dbh->do('UPDATE locks SET ID=? WHERE HARDWARE_ID=?', {}, $$, $device) ){
          $Apache::Ocsinventory::CURRENT_CONTEXT{'LOCK_FL'} = 1;
          return 0;
        }
        else{
          &_log(516,'lock', 'failed') if $ENV{'OCS_OPT_LOGLEVEL'};
          return 1;
        }
      }
    }
    return 1;
  }
}


sub _unlock{
  my $device = shift;
  my $dbh = $Apache::Ocsinventory::CURRENT_CONTEXT{'DBI_HANDLE'} || shift;

  if($dbh->do('DELETE FROM locks WHERE HARDWARE_ID=? AND ID=?', {}, $device, $$)){
    $Apache::Ocsinventory::CURRENT_CONTEXT{'LOCK_FL'} = 0 if $device eq ${Apache::Ocsinventory::CURRENT_CONTEXT{'DATABASE_ID'}};
    return(0);
  }else{
    return(1);
  }
}

sub _log{
  my $code = shift;
  my $phase = shift;
  my $comment = shift;
  my $DeviceID = $Apache::Ocsinventory::CURRENT_CONTEXT{'DEVICEID'}||'NA';
  my $ipaddress = $Apache::Ocsinventory::CURRENT_CONTEXT{'IPADDRESS'}||'??';
  our $LOG;
  
  if(!$LOG){
    open LOG, '>>'.$ENV{'OCS_OPT_LOGPATH'}.'/activity.log' or die "Failed to open log file : $! ($ENV{'OCS_OPT_LOGPATH'})\n";
    # We don't want buffer, so we always flush the handles
    select(LOG);
    $|=1;
    $LOG = \*LOG;
  }
    
  print $LOG localtime().";$$;$code;$DeviceID;$ipaddress;".&_get_http_header('User-agent',$Apache::Ocsinventory::CURRENT_CONTEXT{'APACHE_OBJECT'}).";$phase;".($comment?$comment:"")."\n";
}

# Subroutine called at the end of execution
sub _end{
  my $ret = shift;
  my $dbh = $Apache::Ocsinventory::CURRENT_CONTEXT{'DBI_HANDLE'};
  my $DeviceID = $Apache::Ocsinventory::CURRENT_CONTEXT{'DATABASE_ID'};

  #Non-transactionnal table
  &_unlock($DeviceID) if $Apache::Ocsinventory::CURRENT_CONTEXT{'LOCK_FL'};
  
  if( $ret==APACHE_SERVER_ERROR ){ 
    &_log(515,'end', 'error') if $ENV{'OCS_OPT_LOGLEVEL'};
    $dbh->rollback;
  }
  elsif( $ret eq APACHE_BAD_REQUEST ){
    &_log(515,'end', 'bad_request') if $ENV{'OCS_OPT_LOGLEVEL'};
  }
  else{
    $dbh->commit;
  }
  close(our $LOG) && undef $LOG if defined $LOG;
  $dbh->disconnect;
  return $ret;
}

# Retrieve option request handler
sub _modules_get_request_handler{
  my $request = shift;
  my %search = (
    'REQUEST_NAME' => $request
  );
  my @ret = &_modules_search(\%search, 'HANDLER_REQUEST');
  return($ret[0]);
}

# Retrieve options with preinventory handler
sub _modules_get_pre_inventory_options{
  return(&_modules_search(undef, 'HANDLER_PRE_INVENTORY'));
}

# Retrieve options with postinventory handler
sub _modules_get_post_inventory_options{
  return(&_modules_search(undef, 'HANDLER_POST_INVENTORY'));
}

# Retrieve options with prolog_read
sub _modules_get_prolog_readers{
  return(&_modules_search(undef, 'HANDLER_PROLOG_READ'));
}

# Retrieve options with prolog_resp
sub _modules_get_prolog_writers{
  return(&_modules_search(undef, 'HANDLER_PROLOG_RESP'));
}

# Retrieve duplicate handlers
sub _modules_get_duplicate_handlers{
  return(&_modules_search(undef, 'HANDLER_DUPLICATE'));
}

# Read options structures
sub _modules_search{
  # Take a hash ref and return an array
  # The hash indicate the desire handler is the second arg

  my $search = shift;
  my $handler = shift;

  my @ret;
  my $count;

  my $module;
  my $search_key;
  my $module_key;

  for $module (@{$Apache::Ocsinventory::OPTIONS_STRUCTURE}){
    $count = 0;
    if($search){
      for $search_key (keys(%$search)){

        if($search_key eq 'REQUEST_NAME'){

          $count ++ if defined($module->{$search_key}) and ($module->{$search_key} eq $search->{$search_key});

        }elsif($search_key eq 'TYPE'){

          $count ++ if defined($module->{$search_key}) and ($module->{$search_key} == $search->{$search_key});
        }

      }
      if($count == keys(%$search)){
        push @ret, $module->{$handler} if $module->{$handler};
        $count = 0;
      }
    }else{
      push @ret, $module->{$handler} if $module->{$handler};
    }
  }

  if(@ret){
    return(@ret);
  }else{
    return(0);
  }
}

sub _get_xml_parser_opt{
  my $hash_ref = shift; 
  $hash_ref->{'ParserOpts'} = [ NoLWP => 1 ]; # Prevent XXE (see CVE-2018-14473)
  $hash_ref->{'SuppressEmpty'} = 1;
  $hash_ref->{'ForceArray'} = [];
  @{$hash_ref->{'ForceArray'}} = &_get_xml_parser_opt_force_array();
}

sub _get_xml_parser_opt_force_array{
  my @ret;
# Core
  push @ret, @{$Apache::Ocsinventory::Server::Inventory::XML_PARSER_OPT{'ForceArray'}};
  
# Options
  push @ret, @Apache::Ocsinventory::XMLParseOptForceArray;
  for my $module (@{$Apache::Ocsinventory::OPTIONS_STRUCTURE}){
    push @ret, @{$module->{'XML_PARSER_OPT'}->{'ForceArray'}} if $module->{'XML_PARSER_OPT'}->{'ForceArray'}=~/ARRAY/;
  }
  return @ret;
}

#
sub _send_file{

  # We want to know if the file is available
  my $context = shift;
  my $request;
  my $row;
  my $r = $Apache::Ocsinventory::CURRENT_CONTEXT{'APACHE_OBJECT'};
  my $dbh = $Apache::Ocsinventory::CURRENT_CONTEXT{'DBI_HANDLE'};

  if($context eq 'deploy'){
    my $file = shift;
    $request=$dbh->prepare('SELECT CONTENT FROM deploy WHERE NAME=?');
    $request->execute($file);

    # If not, we return a bad request and log the event
    unless($request->rows){
      &_log(511,'deploy','no_file') if $ENV{'OCS_OPT_LOGLEVEL'};
      return APACHE_BAD_REQUEST;
    }else{
      # We extract the file and send it
      $row = $request->fetchrow_hashref();
      # We force this content type to avoid the direct interpretation of, for example, a plain text file
      &_set_http_header('Cache-control' => $ENV{'OCS_OPT_PROXY_REVALIDATE_DELAY'},$r);
      &_set_http_header('Content-length' => length($row->{'CONTENT'}),$r);
      &_set_http_content_type('Application/octet-stream',$r);
      &_send_http_headers($r);
      $r->print($row->{'CONTENT'});

      # We log it
      &_log(302,'deploy','file_transmitted') if $ENV{'OCS_OPT_LOGLEVEL'};
      return APACHE_OK;
    }

  }
}

sub _get_spec_params{
  my $hardwareId = $Apache::Ocsinventory::CURRENT_CONTEXT{'DATABASE_ID'};
  return &_params_from_devices($hardwareId);
}

sub _get_spec_params_g{
  my $groups = $Apache::Ocsinventory::CURRENT_CONTEXT{'MEMBER_OF'};
  my %result;
  for(@$groups){
    $result{$_} = {&_params_from_devices($_)} ;
  }
  return %result;
}

sub _params_from_devices{
  my $hardwareId = shift;
  my $dbh = $Apache::Ocsinventory::CURRENT_CONTEXT{'DBI_HANDLE'};
  my $sth;
  my $row;
  my %result;
  
  $sth = $dbh->prepare('SELECT NAME,IVALUE,TVALUE FROM devices WHERE HARDWARE_ID=?');
  $sth->execute($hardwareId);
  while( $row = $sth->fetchrow_hashref()){
    if(exists($result{ $row->{'NAME'} })){
      if($result{ $row->{'NAME'} } =~ /ARRAY/){
        push @{$result{$row->{'NAME'}}}, 
          { 'IVALUE' => $row->{'IVALUE'}, 'TVALUE' => $row->{'TVALUE'} };
      }
      else{
        my $temp = $result{ $row->{'NAME'} };
        $result{ $row->{'NAME'} } = [];
        push @{$result{$row->{'NAME'}}}, $temp;
      }
    }
    else{
      $result{ $row->{'NAME'} } = { 
        'IVALUE' => $row->{'IVALUE'},
        'TVALUE' => $row->{'TVALUE'}
      };
    }
  }
  return %result;
}
1;
