###############################################################################
## 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::Capacities::Download;

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();
  }
}

use Apache::Ocsinventory::Server::System;
use Apache::Ocsinventory::Server::Communication;
use Apache::Ocsinventory::Server::Constants;
use Apache::Ocsinventory::Server::Capacities::Download::Inventory;

# Initialize option
push @{$Apache::Ocsinventory::OPTIONS_STRUCTURE},{
  'NAME' => 'DOWNLOAD',
  'HANDLER_PROLOG_READ' => undef,
  'HANDLER_PROLOG_RESP' => \&download_prolog_resp,
  'HANDLER_PRE_INVENTORY' => undef,
  'HANDLER_POST_INVENTORY' => \&download_post_inventory,
  'REQUEST_NAME' => 'DOWNLOAD',
  'HANDLER_REQUEST' => \&download_handler,
  'HANDLER_DUPLICATE' => \&download_duplicate,
  'TYPE' => OPTION_TYPE_ASYNC,
  'XML_PARSER_OPT' => {
      'ForceArray' => ['PACKAGE']
  }
};

sub download_prolog_resp{
  
  my $current_context = shift;
  my $resp = shift;

  my $dbh = $current_context->{'DBI_HANDLE'};
  my $groups = $current_context->{'MEMBER_OF'};
  my $hardware_id = $current_context->{'DATABASE_ID'};
  
  my $groupsParams = $current_context->{'PARAMS_G'};
  my ( $downloadSwitch, $cycleLatency, $fragLatency, $periodLatency, $periodLength, $timeout,$execTimeout);
  

  my($pack_sql, $hist_sql);
  my($pack_req, $hist_req);
  my($hist_row, $pack_row);
  my(@packages, @history, @forced_packages, @scheduled_packages, @postcmd_packages, @dont_repeat);
  my $blacklist;
  
  if($ENV{'OCS_OPT_DOWNLOAD'}){
    $downloadSwitch = 1;
    # Group's parameters
    for(keys(%$groupsParams)){

      $downloadSwitch = $$groupsParams{$_}->{'DOWNLOAD_SWITCH'}->{'IVALUE'}
        if exists( $$groupsParams{$_}->{'DOWNLOAD_SWITCH'}->{'IVALUE'} ) 
        and $$groupsParams{$_}->{'DOWNLOAD_SWITCH'}->{'IVALUE'} < $downloadSwitch;
      
      $cycleLatency = $$groupsParams{$_}->{'DOWNLOAD_CYCLE_LATENCY'}->{'IVALUE'} 
        if ( (exists($$groupsParams{$_}->{'DOWNLOAD_CYCLE_LATENCY'}->{'IVALUE'}) 
        and $$groupsParams{$_}->{'DOWNLOAD_CYCLE_LATENCY'}->{'IVALUE'} > $cycleLatency)
        or !$cycleLatency);
      
      $fragLatency = $$groupsParams{$_}->{'DOWNLOAD_FRAG_LATENCY'}->{'IVALUE'} 
        if ( (exists($$groupsParams{$_}->{'DOWNLOAD_FRAG_LATENCY'}->{'IVALUE'}) 
        and $$groupsParams{$_}->{'DOWNLOAD_FRAG_LATENCY'}->{'IVALUE'} > $fragLatency)
        or !$fragLatency);
      
      $periodLatency = $$groupsParams{$_}->{'DOWNLOAD_PERIOD_LATENCY'}->{'IVALUE'} 
        if ( (exists($$groupsParams{$_}->{'DOWNLOAD_PERIOD_LATENCY'}->{'IVALUE'}) 
        and $$groupsParams{$_}->{'DOWNLOAD_PERIOD_LATENCY'}->{'IVALUE'} > $periodLatency)
        or !$periodLatency);

      $periodLength = $$groupsParams{$_}->{'DOWNLOAD_PERIOD_LENGTH'}->{'IVALUE'}
        if ( (exists($$groupsParams{$_}->{'DOWNLOAD_PERIOD_LENGTH'}->{'IVALUE'}) 
        and $$groupsParams{$_}->{'DOWNLOAD_PERIOD_LENGTH'}->{'IVALUE'} < $periodLength) 
        or !$periodLength); 
      
      $timeout = $$groupsParams{$_}->{'DOWNLOAD_TIMEOUT'}->{'IVALUE'} 
        if ( (exists($$groupsParams{$_}->{'DOWNLOAD_TIMEOUT'}->{'IVALUE'}) 
        and $$groupsParams{$_}->{'DOWNLOAD_TIMEOUT'}->{'IVALUE'} < $timeout) 
        or !$timeout);

      $execTimeout = $$groupsParams{$_}->{'DOWNLOAD_EXECUTION_TIMEOUT'}->{'IVALUE'} 
        if ( (exists($$groupsParams{$_}->{'DOWNLOAD_EXECUTION_TIMEOUT'}->{'IVALUE'}) 
        and $$groupsParams{$_}->{'DOWNLOAD_EXECUTION_TIMEOUT'}->{'IVALUE'} < $execTimeout) 
        or !$execTimeout);
    }  
  }
  else{
    $downloadSwitch = 0;
  }
  
  $downloadSwitch = $current_context->{'PARAMS'}{'DOWNLOAD_SWITCH'}->{'IVALUE'} 
      if defined($current_context->{'PARAMS'}{'DOWNLOAD_SWITCH'}->{'IVALUE'}) and $downloadSwitch;
      
  $cycleLatency   = $ENV{'OCS_OPT_DOWNLOAD_CYCLE_LATENCY'} unless $cycleLatency;
  $fragLatency   = $ENV{'OCS_OPT_DOWNLOAD_FRAG_LATENCY'} unless $fragLatency;
  $periodLatency   = $ENV{'OCS_OPT_DOWNLOAD_PERIOD_LATENCY'} unless $periodLatency;
  $periodLength   = $ENV{'OCS_OPT_DOWNLOAD_PERIOD_LENGTH'} unless $periodLength;
  $timeout  = $ENV{'OCS_OPT_DOWNLOAD_TIMEOUT'} unless $timeout;
  $execTimeout  = $ENV{'OCS_OPT_DOWNLOAD_EXECUTION_TIMEOUT'} unless $execTimeout;
  
  push @packages,{
    'TYPE'       => 'CONF',
    'ON'       => $downloadSwitch,
    'CYCLE_LATENCY'   => $current_context->{'PARAMS'}{'DOWNLOAD_CYCLE_LATENCY'}->{'IVALUE'}   
            || $cycleLatency,
    'FRAG_LATENCY'     => $current_context->{'PARAMS'}{'DOWNLOAD_FRAG_LATENCY'}->{'IVALUE'}   
            || $fragLatency,
    'PERIOD_LATENCY'   => $current_context->{'PARAMS'}{'DOWNLOAD_PERIOD_LATENCY'}->{'IVALUE'} 
            || $periodLatency,
    'PERIOD_LENGTH'   => $current_context->{'PARAMS'}{'DOWNLOAD_PERIOD_LENGTH'}->{'IVALUE'}   
            || $periodLength,
    'TIMEOUT'     => $current_context->{'PARAMS'}{'DOWNLOAD_TIMEOUT'}->{'IVALUE'}
            || $timeout,
    'EXECUTION_TIMEOUT'     => $current_context->{'PARAMS'}{'DOWNLOAD_EXECUTION_TIMEOUT'}->{'IVALUE'}
            || $execTimeout
  };
  
  if($downloadSwitch){
  
    # If this option is set, we send only the needed package to the agent
    # Can be a performance issue
    # Agents prior to 4.0.3.0 do not send history data
    $hist_sql = q {
      SELECT PKG_ID
      FROM download_history
      WHERE HARDWARE_ID=?
    };
    $hist_req = $dbh->prepare( $hist_sql );
    $hist_req->execute( $hardware_id );
    
    while( $hist_row = $hist_req->fetchrow_hashref ){
      push @history, $hist_row->{'PKG_ID'};
    }

    #We get scheduled packages affected to the computer 
    &get_scheduled_packages($dbh, $hardware_id, \@scheduled_packages);

    #We get scheduled packages affected with a postcmd command 
    &get_postcmd_packages($dbh, $hardware_id, \@postcmd_packages);

    #We get packages marked as forced affeted to the computer
    &get_forced_packages($dbh, $hardware_id, \@forced_packages);

    #We get packages for groups that computer is member of 
    if( $current_context->{'EXIST_FL'} && $ENV{'OCS_OPT_ENABLE_GROUPS'} && @$groups ){
      $pack_sql =  q {
        SELECT IVALUE,FILEID,INFO_LOC,PACK_LOC,CERT_PATH,CERT_FILE
        FROM devices,download_enable
        WHERE HARDWARE_ID=? 
        AND devices.NAME='DOWNLOAD'
        AND download_enable.ID=devices.IVALUE
      };

      my $verif_affected = 'SELECT TVALUE FROM devices WHERE HARDWARE_ID=? AND IVALUE=? AND NAME="DOWNLOAD"';
      my $trace_event = 'INSERT INTO devices(HARDWARE_ID,NAME,IVALUE,TVALUE) VALUES(?,"DOWNLOAD",?,NULL)';

      $pack_req = $dbh->prepare( $pack_sql );
            
      for( @$groups ){
        $pack_req->execute( $_ );

        #We get scheduled packages affected to the group
        &get_scheduled_packages($dbh, $_, \@scheduled_packages);

        #We get packages affected to the group which contain postcmd command
        &get_postcmd_packages($dbh, $_, \@postcmd_packages);

        while( $pack_row = $pack_req->fetchrow_hashref ){
          my $fileid = $pack_row->{'FILEID'};
          my ($schedule, $scheduled_package, $postcmd, $postcmd_package);

          if( (grep /^$fileid$/, @history) or (grep /^$fileid$/, @dont_repeat)){
            next;
          }

          if( $ENV{'OCS_OPT_DOWNLOAD_GROUPS_TRACE_EVENTS'} ){
            # We verify if the package is already traced and not already in history
            my $verif_affected_sth = $dbh->prepare($verif_affected);
            $verif_affected_sth->execute($hardware_id, $pack_row->{'IVALUE'});
            if($verif_affected_sth->rows){
              my $verif_affected_row = $verif_affected_sth->fetchrow_hashref();
              if($verif_affected_row!~/NULL/ && $verif_affected_row!~/NOTIFIED/){
                $verif_affected_sth->finish();
                # We do not send package if the current state is fed
                next;
              }
            }
            else{
              $dbh->do($trace_event, {}, $hardware_id, $pack_row->{'IVALUE'})
            }
          }

          #We check if package is scheduled 
          for $scheduled_package (@scheduled_packages) {
            if ( $scheduled_package->{'FILEID'} =~ /^$fileid$/ ) {
	      $schedule = $scheduled_package->{'SCHEDULE'};
	      last;
            }
          }

          #We check if package is affected with a postcmd command
          for $postcmd_package (@postcmd_packages) {
            if ( $postcmd_package->{'FILEID'} =~ /^$fileid$/ ) {
	      $postcmd = $postcmd_package->{'POSTCMD'};
	      last;
            }
          }
          
          push @packages,{
            'TYPE'       => 'PACK',
            'ID'         => $pack_row->{'FILEID'},
            'INFO_LOC'   => $pack_row->{'INFO_LOC'},
            'PACK_LOC'   => $pack_row->{'PACK_LOC'},
            'CERT_PATH'  => $pack_row->{'CERT_PATH'}?$pack_row->{'CERT_PATH'}:'INSTALL_PATH',
            'CERT_FILE'  => $pack_row->{'CERT_FILE'}?$pack_row->{'CERT_FILE'}:'INSTALL_PATH',
            'SCHEDULE'   => $schedule?$schedule:'',
            'POSTCMD'    => $postcmd?$postcmd:'',
            'FORCE'      => 0 
          };

          push @dont_repeat, $fileid;
        }
      }
    }

    #We get packages for this computer 
    $pack_sql =  q {
      SELECT e.ID, e.FILEID, e.INFO_LOC, e.PACK_LOC, e.CERT_PATH, e.CERT_FILE, e.SERVER_ID
      FROM devices d, download_enable e 
      WHERE d.HARDWARE_ID=? 
      AND d.IVALUE=e.ID 
      AND d.NAME='DOWNLOAD'
      AND (d.TVALUE IS NULL OR d.TVALUE='NOTIFIED')
    };

      
    $pack_req = $dbh->prepare( $pack_sql );
    # Retrieving packages associated to the current device
    $pack_req->execute( $hardware_id );
    
    while($pack_row = $pack_req->fetchrow_hashref()){
      my $fileid = $pack_row->{'FILEID'};
      my $enable_id = $pack_row->{'ID'};
      my $pack_loc = $pack_row->{'PACK_LOC'};
      my ($forced, $schedule, $scheduled_package, $postcmd, $postcmd_package);

      #We check if package is scheduled 
      for $scheduled_package (@scheduled_packages) {
        if ( $scheduled_package->{'FILEID'} == $fileid ) {
	  $schedule = $scheduled_package->{'SCHEDULE'};
	  last;
        }
      }

      #We check if package is affected with a postcmd command
      for $postcmd_package (@postcmd_packages) {
        if ( $postcmd_package->{'FILEID'} == $fileid ) {
	  $postcmd = $postcmd_package->{'POSTCMD'};
	  last;
        }
      }

      #We check if package is marcked as forced
      if ( grep /^$fileid$/, @forced_packages ) {
         $forced = 1; 
      } else {
         $forced = 0;
      }

      # If the package is in history, the device will not be notified
      # We have to show this behaviour to user. We use the package events.
      if ($forced == 0){
        if ( grep /^$fileid$/, @history ){
          $dbh->do(q{ UPDATE devices SET TVALUE='SUCCESS_ALREADY_IN_HISTORY', COMMENTS=? WHERE NAME='DOWNLOAD' AND HARDWARE_ID=? AND IVALUE=? }, {},  scalar localtime(), $current_context->{'DATABASE_ID'}, $enable_id ) ;
          next ;
        }
      }

      if( grep /^$fileid$/, @dont_repeat){
        next;
      }
      
      # Substitude $IP$ with server ipaddress or $NAME with server name
      my %substitute = (
        '$IP$'   => {'table' => 'hardware', 'field' => 'IPADDR'},
        '$NAME$' => {'table' => 'hardware', 'field' => 'NAME'}
      );

      for my $motif (keys(%substitute)){
        if($pack_loc=~/\Q$motif\E/){
          
          my( $srvreq, $srvreq_sth, $srvreq_row);
          my $field = $substitute{$motif}->{field};
          my $table = $substitute{$motif}->{table};

          $srvreq = "select $field from $table where ID=?";
          $srvreq_sth = $dbh->prepare($srvreq);
          $srvreq_sth->execute($pack_row->{'SERVER_ID'});

          if($srvreq_row=$srvreq_sth->fetchrow_hashref()){
            my $template = $srvreq_row->{$field};
            $pack_loc =~ s/(.*)\Q$motif\E(.*)/${1}${template}${2}/g;
          }
          else{
            $pack_loc='';
          }
        }
      }

      next if $pack_loc eq '';

      push @packages,{
        'TYPE'       => 'PACK',
        'ID'         => $pack_row->{'FILEID'},
        'INFO_LOC'   => $pack_row->{'INFO_LOC'},
        'PACK_LOC'   => $pack_loc,
        'CERT_PATH'  => $pack_row->{'CERT_PATH'}?$pack_row->{'CERT_PATH'}:'INSTALL_PATH',
        'CERT_FILE'  => $pack_row->{'CERT_FILE'}?$pack_row->{'CERT_FILE'}:'INSTALL_PATH',
        'SCHEDULE'   => $schedule?$schedule:'',
        'POSTCMD'    => $postcmd?$postcmd:'',
        'FORCE'      => $forced
      };

      push @dont_repeat, $fileid;
    }
    $dbh->do(q{ UPDATE devices SET TVALUE='NOTIFIED', COMMENTS=? WHERE NAME='DOWNLOAD' AND HARDWARE_ID=? AND TVALUE IS NULL }
    ,{},  scalar localtime(), $current_context->{'DATABASE_ID'} ) if $pack_req->rows;
  }

  push @{ $resp->{'OPTION'} },{
    'NAME'   => ['DOWNLOAD'],
    'PARAM' => \@packages
  };
    
  return 0;
}

sub download_post_inventory{
  my $current_context = shift;
  
  return if !$ENV{'OCS_OPT_DOWNLOAD'};
  
  my $dbh = $current_context->{'DBI_HANDLE'};
  my $hardwareId = $current_context->{'DATABASE_ID'};
  my $result = $current_context->{'XML_ENTRY'}; 
    
  my @fromXml = get_history_xml( $result );
  my @fromDb;
  
  if( $ENV{OCS_OPT_INVENTORY_WRITE_DIFF} ){
    @fromDb = get_history_db( $hardwareId, $dbh );
    &update_history_diff( $hardwareId, $dbh, \@fromXml, \@fromDb );
  }
  else{
    &update_history_full( $hardwareId, $dbh, \@fromXml );
  }
}

sub download_handler{
  # Initialize data
  my $current_context = shift;
  
  my $dbh    = $current_context->{'DBI_HANDLE'};
  my $result  = $current_context->{'XML_ENTRY'};
  my $r     = $current_context->{'APACHE_OBJECT'};
  my $hardware_id = $current_context->{'DATABASE_ID'};

  my $request;
  
  $request = $dbh->prepare('
    SELECT ID FROM download_enable 
    WHERE FILEID=? 
    AND ID IN (SELECT IVALUE FROM devices WHERE NAME="download" AND HARDWARE_ID=?)');
  $request->execute( $result->{'ID'}, $hardware_id);

  &_log(2001, 'download', "$result->{'ID'}(".($result->{'ERR'}?$result->{'ERR'}:'UNKNOWN_CODE').")");
  
  if(my $row = $request->fetchrow_hashref()){
    $dbh->do('UPDATE devices SET TVALUE=?, COMMENTS=?
      WHERE NAME="DOWNLOAD" 
      AND HARDWARE_ID=? 
      AND IVALUE=?',
    {}, $result->{'ERR'}?$result->{'ERR'}:'UNKNOWN_CODE', scalar localtime(), $hardware_id, $row->{'ID'} ) 
      or return(APACHE_SERVER_ERROR);
    &_set_http_header('content-length', 0, $r);
    &_send_http_headers($r);
    return(APACHE_OK);
  }else{
    &_log(2501, 'download', "$result->{'ID'}(".($result->{'ERR'}?$result->{'ERR'}:'UNKNOWN_CODE').")");
    &_set_http_header('content-length', 0, $r);
    &_send_http_headers($r);
    return(APACHE_OK);
  }
}

sub download_duplicate {
  my $current_context = shift;
  my $device = shift;
  
  my $dbh = $current_context->{'DBI_HANDLE'};

  # Handle deployment servers
  $dbh->do('UPDATE download_enable SET SERVER_ID=? WHERE SERVER_ID=?', {}, $current_context->{'DATABASE_ID'}, $device);
  $dbh->do('UPDATE download_servers SET HARDWARE_ID=? WHERE HARDWARE_ID=?', {}, $current_context->{'DATABASE_ID'}, $device);

  # If we encounter problems, it aborts whole replacement
  return $dbh->do('DELETE FROM download_history WHERE HARDWARE_ID=?', {}, $device);
}


# Subroutine to get packagesmarked as scheduled 
sub get_scheduled_packages {
  my ($dbh, $hardware_id, $scheduled_packages) = @_;

  my ($scheduled_sql, $scheduled_req, $scheduled_row, $package, @package_ids);

  $scheduled_sql =  q {
    SELECT FILEID,TVALUE 
    FROM devices,download_enable 
    WHERE HARDWARE_ID=? 
    AND devices.IVALUE=download_enable.ID 
    AND devices.NAME='DOWNLOAD_SCHEDULE'
    AND TVALUE IS NOT NULL
  };

  $scheduled_req = $dbh->prepare( $scheduled_sql );
  $scheduled_req->execute( $hardware_id );
 
  for $package (@$scheduled_packages) {
      push @package_ids, $package->{'FILEID'}; 
  }
  
  while( $scheduled_row = $scheduled_req->fetchrow_hashref ){
    # If package is not already in array (to prevent problems with groups) 
    unless ( grep /^$scheduled_row->{'FILEID'}$/, @$scheduled_packages ) {
      push @$scheduled_packages, {
        'FILEID'     => $scheduled_row->{'FILEID'},
        'SCHEDULE'   => $scheduled_row->{'TVALUE'}
      };
    }
  }
}

# Subroutine to get packagesmarked as forced 
sub get_forced_packages {
  my ($dbh, $hardware_id, $forced_packages) = @_;

  my ($forced_sql, $forced_req, $forced_row);

  $forced_sql =  q {
    SELECT FILEID 
    FROM devices,download_enable 
    WHERE HARDWARE_ID=? 
    AND devices.IVALUE=download_enable.ID 
    AND devices.NAME='DOWNLOAD_FORCE'
    AND TVALUE=1
  };

  $forced_req = $dbh->prepare( $forced_sql );
  $forced_req->execute( $hardware_id );
   
  while( $forced_row = $forced_req->fetchrow_hashref ){
    # If package is not already in array (to prevent problems with groups) 
    unless ( grep /^$forced_row->{'FILEID'}$/, @$forced_packages ) {
      push @$forced_packages, $forced_row->{'FILEID'};
    }
  }
}


# Subroutine to get packages affected with a postcmd command 
sub get_postcmd_packages {
  my ($dbh, $hardware_id, $postcmd_packages) = @_;

  my ($postcmd_sql, $postcmd_req, $postcmd_row, $package, @package_ids);

  $postcmd_sql =  q {
    SELECT FILEID,TVALUE 
    FROM devices,download_enable 
    WHERE HARDWARE_ID=? 
    AND devices.IVALUE=download_enable.ID 
    AND devices.NAME='DOWNLOAD_POSTCMD'
    AND TVALUE IS NOT NULL
  };

  $postcmd_req = $dbh->prepare( $postcmd_sql );
  $postcmd_req->execute( $hardware_id );
 
  for $package (@$postcmd_packages) {
      push @package_ids, $package->{'FILEID'}; 
  }
  
  while( $postcmd_row = $postcmd_req->fetchrow_hashref ){
    # If package is not already in array (to prevent problems with groups) 
    unless ( grep /^$postcmd_row->{'FILEID'}$/, @$postcmd_packages ) {
      push @$postcmd_packages, {
        'FILEID'     => $postcmd_row->{'FILEID'},
        'POSTCMD'   => $postcmd_row->{'TVALUE'}
      };
    }
  }
}


1;

