/* execlass.cc
 * This file belongs to Worker, a file manager for UN*X/X11.
 * Copyright (C) 2002-2019 Ralf Hoffmann.
 * You can contact me at: ralf@boomerangsworld.de
 *   or http://www.boomerangsworld.de/worker
 *
 * This program 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.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "execlass.h"
#include <stdarg.h>
#include "datei.h"
#include "aguix/lowlevelfunc.h"
#include "worker_locale.h"
#include "wconfig.h"
#include "worker.h"
#include "pdatei.h"
#include <fstream>
#include <sstream>

/* THREADSAFE */
ExeClass::ExeClass( Worker *w )
{
  worker = w;
  tmpfile = Datei::createTMPName();
  tmpoutput = Datei::createTMPName();
  tmperror = Datei::createTMPName();

  if ( tmpoutput != NULL ) {
    fp = worker_fopen( tmpoutput, "w" );
    if ( fp != NULL ) {
      /* be sure the output can only be changed be use */
      worker_fclose( fp );
      worker_chmod( tmpoutput, S_IRUSR | S_IWUSR | S_IXUSR );
    }
  }

  if ( tmperror != NULL ) {
    fp = worker_fopen( tmperror, "w" );
    if ( fp != NULL ) {
      /* be sure the output can only be changed be use */
      worker_fclose( fp );
      worker_chmod( tmperror, S_IRUSR | S_IWUSR );
    }
  }
  
  if ( tmpfile != NULL ) {
    fp = worker_fopen( tmpfile, "w" );
    if ( fp != NULL ) {
      worker_chmod( tmpfile, S_IRUSR | S_IWUSR | S_IXUSR );
      if ( fprintf( fp, "#! /bin/sh\n\n" ) < 0 ) {
        worker_fclose( fp );
        fp = NULL;
      }
    }
  }
  lasterror = 0;
}

ExeClass::~ExeClass()
{
  if ( fp != NULL ) {
    worker_fclose( fp );
  }

  if ( tmpfile != NULL ) {
    worker_unlink( tmpfile );
    _freesafe( tmpfile );
  }

  if ( tmpoutput != NULL ) {
    worker_unlink( tmpoutput );
    _freesafe( tmpoutput );
  }

  if ( tmperror != NULL ) {
    worker_unlink( tmperror );
    _freesafe( tmperror );
  }
}

/* this method adds a command to the shell-script
   to do this in a secure manner (when using special characters
   in filenames like: testfile`rm testfile`), I will first scan the command
   str for all words and build argvlist (same as for exec..).
   Then I rebuild the command str but all words including the command
   will be single quoted and print to file.
   This way I get the same security as using exec but I can execute more
   then one command and run them in a terminal or capturing the output
 */
/* THREADSAFE */
void ExeClass::addCommand( const char *format, ... )
{
  va_list va;
  int n, size = 1024;
  char *tstr;
  std::string string1;
  int error = 0;
  
  if ( ( fp != NULL ) && ( format != NULL ) ) {
    // first create command str
    while ( 1 ) {
      tstr = (char*)_allocsafe( size );
      va_start( va, format );
      n = vsnprintf( tstr, size, format, va );
      va_end( va );
      if ( ( n > -1 ) && ( n < size ) ) break;
      _freesafe( tstr );
      size *= 2;
    }
    string1 = Worker_secureCommandForShell( tstr );
    _freesafe( tstr );
    if ( string1.length() > 0 ) {
      if ( fprintf( fp, "%s", string1.c_str() ) < 0 ) error++;
      if ( fprintf( fp, "\n" ) < 0 ) error++;
    }
    if ( error != 0 ) {
      worker_fclose( fp );
      fp = NULL;
      //TODO show requests?
    }
  }
}

/* THREADSAFE if worker == NULL or worker THREADSAFE
              and if wconfig THREADSAFE */
void ExeClass::execute()
{
  std::string exestr;
  int error = 0;
  
  if ( fp == NULL ) return;

  if ( worker != NULL ) worker->setWaitCursor();

  exestr = AGUIXUtils::replace_percent_s_quoted( "/bin/sh %s", tmpfile, NULL );

  if ( worker_fclose( fp ) != 0 ) error++;
  fp = NULL;

  if ( error == 0 ) {
      int tres __attribute__((unused)) = system( exestr.c_str() );
  } else {
    //TODO show requests?
  }

  if ( worker != NULL ) worker->unsetWaitCursor();
}

/* THREADSAFE if worker == NULL or worker THREADSAFE */
char *ExeClass::getOutput()
{
  return getOutput( NULL );
}

/* THREADSAFE if worker == NULL or worker THREADSAFE */
char *ExeClass::getOutput( int *return_error )
{
  char *erg = NULL;
  
  if ( getOutputAndRV( &erg, NULL, return_error ) != 0 ) return NULL;
  return erg;
}

/* THREADSAFE if worker == NULL or worker THREADSAFE */
int ExeClass::getReturnCode()
{
  return getReturnCode( NULL );
}

/* THREADSAFE if worker == NULL or worker THREADSAFE */
int ExeClass::getReturnCode( int *return_error )
{
  int erg;

  if ( getOutputAndRV( NULL, &erg, return_error ) != 0 ) return -1;
  return erg;
}

/* THREADSAFE if worker == NULL or worker THREADSAFE */
int ExeClass::getOutputAndRV( char **return_output, int *return_value, int *return_error )
{
  std::string exestr;
  Datei *d;
  char *erg = NULL;
  int rerg = -1;
  
  if ( fp == NULL ) return 1;
  
  if ( worker != NULL ) worker->setWaitCursor();

  if ( worker_fclose( fp ) != 0 ) {
    fp  = NULL; 
    if ( worker != NULL ) worker->unsetWaitCursor();
    return 1;
  }
  fp = NULL;

  exestr = "/bin/sh ";
  exestr += AGUIX_catTrustedAndUnTrusted2( "", tmpfile );
  exestr += " >";
  exestr += AGUIX_catTrustedAndUnTrusted2( "", tmpoutput );
  exestr += " 2>";
  exestr += AGUIX_catTrustedAndUnTrusted2( "", tmperror );
  rerg = system( exestr.c_str() );
  d = new Datei();
  if ( d->open( tmpoutput, "r" ) == 0 ) {
    erg = d->getLine();
    d->close();
  }
  delete d;
  if ( Datei::fileExistsExt( tmperror ) == Datei::D_FE_FILE ) {
    if ( Datei::fileSize( tmperror ) != 0 ) {
      // filesize has to be 0 if anything was okay
      lasterror = 2;
    } else {
      lasterror = 0;
    }
  } else {
    // error file not found => assume something gone wrong
    lasterror = 1;
  }
  if ( return_error != NULL ) *return_error = lasterror;

  if ( worker != NULL ) worker->unsetWaitCursor();

  if ( return_output != NULL )
    *return_output = erg;
  else if ( erg != NULL )
    _freesafe( erg );
  
  if ( return_value != NULL ) *return_value = rerg;
  
  return 0;
}

/* THREADSAFE */
void ExeClass::cdDir( const char *dir )
{
  char *tstr;
  int error = 0;
  
  if ( ( fp != NULL ) && ( dir != NULL ) ) {
    if ( worker_islocal( dir ) == 1 ) {
      tstr = AGUIX_prepareForSingleQuote( dir );
      if ( fprintf( fp, "cd '%s'\n", tstr ) < 0 ) error++;
      _freesafe( tstr );
    }

    if ( error != 0 ) {
      worker_fclose( fp );
      fp = NULL;
      //TODO show requests?
    }
  }
}

/* THREADSAFE */
int ExeClass::getLastError()
{
  return lasterror;
}

/***********************
 * non-class functions *
 ***********************/

/*
 * Worker_buildArgvList
 * create a argv vector from a string and return size of vector
 * without last NULL element
 * THREADSAFE
 */
int Worker_buildArgvList( const char *str, char ***argvlist )
{
  int pos, quotemode, start, end = -1;
  bool found;
  int len, size;
  struct Worker_bal {
    char *arg;
    struct Worker_bal *next;
  } *head = NULL, *elem;
  
  if ( ( str == NULL ) || ( argvlist == NULL ) ) return -1;
  
  pos = 0;
  quotemode = 0;
  size = 0;

  len = strlen( str );
  for (; pos < len;) {
    // first skip whitespaces
    while ( isspace( str[pos] ) ) pos++;
    start = pos;
    for ( found = false; found == false;) {
      if ( str[pos] == '\0' ) {
        end = pos;
        found = true;
      } else {
        switch ( quotemode ) {
          case 1:
            if ( str[pos++] == '\'' ) quotemode = 0;
            break;
          case 2:
            if ( str[pos] == '\\' ) pos ++;
            else if ( str[pos] == '"' ) quotemode = 0;
            pos++;
            break;
          default:
            if ( str[pos] == '\'' ) quotemode = 1;
            else if ( str[pos] == '"' ) quotemode = 2;
            else if ( str[pos] == '\\' ) pos++;
            else if ( isspace( str[pos] ) ) {
              // found new real entry
              // from start to end-1
              end = pos;
              found = true;
            }
            pos++;
            break;
        }
      }
    }
    if ( start < end ) {
      elem = (struct Worker_bal*)_allocsafe( sizeof( struct Worker_bal ) );
      elem->next = head;
      elem->arg = (char*)_allocsafe( end - start + 1);
      strncpy( elem->arg, str + start, end - start );
      elem->arg[end - start] = '\0';
      head = elem;
      size++;
    }
  }
  *argvlist = (char**)_allocsafe( sizeof( char * ) * ( size + 1 ) );
  for ( int i = ( size - 1 ); i >= 0; i-- ) {
    elem = head;
    (*argvlist)[i] = elem->arg;
    head = elem->next;
    _freesafe( elem );
  }
  (*argvlist)[size] = NULL;
  return size;
}

/*
 * Worker_freeArgvList
 * THREADSAFE
 */
void Worker_freeArgvList( char **argvlist )
{
  int i;
  
  for ( i = 0;; i++ ) {
    if ( argvlist[i] != NULL ) _freesafe( argvlist[i] );
    else break;
  }
  _freesafe( argvlist );
}

typedef enum { WORKER_IS_NORMAL,
               WORKER_IS_INPUT,
               WORKER_IS_INPUT_WF,
               WORKER_IS_OUTPUT,
               WORKER_IS_OUTPUT_WF,
               WORKER_IS_AOUTPUT,
               WORKER_IS_AOUTPUT_WF,
               WORKER_IS_OUTPUT_1,
               WORKER_IS_OUTPUT_2 } redirectmode_t;

#if 0
/*
 * Worker_getRedirectMode
 * THREADSAFE
 */
static int Worker_getRedirectMode( const char *str1, redirectmode_t *return_mode, int *return_filedescr, int *return_offset )
{
  redirectmode_t redirectmode;
  int j, mode, ch;

  if ( ( str1 == NULL ) ||
       ( return_mode == NULL ) ||
       ( return_filedescr == NULL ) ||
       ( return_offset == NULL ) ) {
    return -1;
  }

  redirectmode = WORKER_IS_NORMAL;
  *return_filedescr = -1;
  for ( j = 0, mode = 0; mode != -1; j++ ) {
    ch = str1[j];
    switch ( mode ) {
      case 1:
        // input candidate
        if ( ch == '<' ) { 
          mode = -1;  // << not allowed
        } else if ( ch == '\0' ) {
          redirectmode = WORKER_IS_INPUT;
          mode = -1;
        } else {
          redirectmode = WORKER_IS_INPUT_WF;
          *return_offset = 1;
          mode = -1;
        }
        break;
      case 10: // "1" or "2"
        if ( ch == '>' ) {
          mode = 11;
        } else mode = -1;
        break;
      case 11: // ">" or "1>" or "2>"
        if ( ch == '\0' ) {
          redirectmode = WORKER_IS_OUTPUT;
          mode = -1;
        } else if ( ch == '>' ) {
          mode = 12;
        } else if ( ch == '&' ) {
          mode = 20;
        } else {
          redirectmode = WORKER_IS_OUTPUT_WF;
          *return_offset = ( (*return_filedescr) == 0 ) ? 1 : 2;
          mode = -1;
        }
        break;
      case 12: // ">>" or "1>>" or "2>>"
        if ( ch == '\0' ) {
          redirectmode = WORKER_IS_AOUTPUT;
          mode = -1;
        } else {
          redirectmode = WORKER_IS_AOUTPUT_WF;
          *return_offset = ( (*return_filedescr) == 0 ) ? 2 : 3;
          mode = -1;
        }
        break;
      case 20: // ">&" or "1>&" or "2>&"
        if ( ch == '1' ) {
          mode = 21;
        } else if ( ch == '2' ) {
          mode = 22;
        } else {
          mode = -1;
        }
        break;
      case 21:
        if ( ch == '\0' ) {
          redirectmode = WORKER_IS_OUTPUT_1;
          mode = -1;
        } else {
          mode = -1;
        }
        break;
      case 22:
        if ( ch == '\0' ) {
          redirectmode = WORKER_IS_OUTPUT_2;
          mode = -1;
        } else {
          mode = -1;
        }
        break;
      default:
        if ( ch == '<' ) {
          mode = 1;
        } else if ( ch == '1' ) {
          *return_filedescr = 1;
          mode = 10;
        } else if ( ch == '2' ) {
          *return_filedescr = 2;
          mode = 10;
        } else if ( ch == '>' ) {
          *return_filedescr = 0;
          mode = 11;
        } else mode = -1;
        break;
    }
    if ( ch == '\0' ) break;
  }
  *return_mode = redirectmode;
  return 0;
}
#endif

#if 0 //Code fuer Redirection, erlaubt im Gegensatz
      // zum aktuell benutzten Code auch Dinge wie 2>/dev/null
      // (muss derzeit als 2> /dev/null geschrieben werden)
      // ist aber nicht ganz so konsequent
  redirectmode_t redirectmode;
  int filedescr, offset;
        // check for redirection
        filedescr = -1;
        offset = -1;
        redirectmode = WORKER_IS_NORMAL;
        if ( Worker_getRedirectMode( argvlist[i], &redirectmode, &filedescr, &offset ) == 0 ) {
        } else redirectmode = WORKER_IS_NORMAL;
        
        if ( ( redirectmode != WORKER_IS_NORMAL ) && ( i > 0 ) ) {
          if ( ( redirectmode == WORKER_IS_OUTPUT_1 ) ||
               ( redirectmode == WORKER_IS_OUTPUT_2 ) ||
               ( redirectmode == WORKER_IS_INPUT ) ||
               ( redirectmode == WORKER_IS_OUTPUT ) ||
               ( redirectmode == WORKER_IS_AOUTPUT ) ) {
            // whole word is a redirection statement
            // no need for special work
            retstr += " ";
            retstr += argvlist[i];
          } else if ( redirectmode == WORKER_IS_INPUT_WF ) {
            if ( offset >= 0 ) {
              retstr += " <";
              tstr1 = AGUIX_unquoteString( argvlist[i] + offset );
              tstr2 = AGUIX_prepareForSingleQuote( tstr1 );
              _freesafe( tstr1 );
              retstr += "'";
              retstr += tstr2;
              retstr += "'";
              _freesafe( tstr2 );
            }
          } else if ( ( redirectmode == WORKER_IS_OUTPUT_WF ) ||
                      ( redirectmode == WORKER_IS_AOUTPUT_WF ) ) {
            if ( ( filedescr != -1 ) && ( offset != -1 ) ) {
              switch( filedescr ) {
                case 1:
                  retstr += " 1>";
                  break;
                case 2:
                  retstr += " 2>";
                  break;
                default:
                  retstr += " >";
                  break;
              }
              if ( redirectmode == WORKER_IS_AOUTPUT_WF ) {
                retstr += ">";
              }
              tstr1 = AGUIX_unquoteString( argvlist[i] + offset );
              tstr2 = AGUIX_prepareForSingleQuote( tstr1 );
              _freesafe( tstr1 );
              retstr += "'";
              retstr += tstr2;
              retstr += "'";
              _freesafe( tstr2 );
            }
          }
        } else 
#endif

/*
 * Worker_secureCommandForShell
 * THREADSAFE
 */
std::string Worker_secureCommandForShell( const char *str )
{
  int i, n;
  char *tstr1, *tstr2;
  char **argvlist;
  std::string retstr = "";
  const char *redirectlist[] = { "<",
                                 ">",
                                 ">&2",
                                 "1>",
                                 "1>&2",
                                 "2>",
                                 "2>&1",
                                 ">>",
                                 "1>>",
                                 "2>>" };
  int j, s;
  
  s = sizeof( redirectlist ) / sizeof( redirectlist[0] );
  if ( str != NULL ) {
    // build argvlist
    n = Worker_buildArgvList( str, &argvlist );
    
    if ( n > 0 ) {
      // there is a real command available
      // now just print all entries in single quotes
      // BUT: I have to unquote first 
      for ( i = 0; i < n; i++ ) {
        // check for special permitted chars for the shell
        // NOTE:I only allow this characters when not in quotes!
        // Allowed: |, ;, & (at the end), &&, ||
        
        // check for redirection
        for ( j = 0; j < s; j++ ) {
          if ( strcmp( argvlist[i], redirectlist[j] ) == 0 ) break;
        }

        if ( ( j < s ) && ( i > 0 ) ) {
          retstr += " ";
          retstr += redirectlist[j];
        } else if ( ( strcmp( argvlist[i], "&&" ) == 0 ) && ( i > 0 ) ) {
          retstr += " &&";
        } else if ( ( strcmp( argvlist[i], "||" ) == 0 ) && ( i > 0 ) ) {
          retstr += " ||";
        } else if ( ( strcmp( argvlist[i], "|" ) == 0 ) && ( i > 0 ) ) {
          retstr += " |";
        } else if ( strcmp( argvlist[i], ";" ) == 0 ) {
          retstr += " ;";
        } else if ( ( strcmp( argvlist[i], "&" ) == 0 ) && ( i == ( n - 1 ) ) ) {
          retstr += " &";
        } else {
          tstr1 = AGUIX_unquoteString( argvlist[i] );
          tstr2 = AGUIX_prepareForSingleQuote( tstr1 );
          _freesafe( tstr1 );
          if ( i > 0 ) {
            retstr += " '";
            retstr += tstr2;
            retstr += "'";
          } else {
            retstr += "'";
            retstr += tstr2;
            retstr += "'";
          }
          _freesafe( tstr2 );
        }
      }
    }

    Worker_freeArgvList( argvlist );
  }
  return retstr;
}

#define REPLACEENV \
{ \
  quotestart = i + 1; \
  quoteend = quotestart; \
  if ( str[quotestart] == '{' ) { \
    /* stop at first nonalphanum or _ \
       I don't care if this is the closing } */ \
    quoteend++;  /* skip { */ \
    while ( str[quoteend] != '\0' ) { \
      if ( ! ( isalnum( str[quoteend] ) || ( str[quoteend] == '_' ) ) ) break; \
      /* if ( str[quoteend] == '}' ) break; */ \
      quoteend++; \
    } \
  } else { \
    /* search to first non alphanum or _ */ \
    while ( str[quoteend] != '\0' ) { \
      if ( ! ( isalnum( str[quoteend] ) || ( str[quoteend] == '_' ) ) ) break; \
      quoteend++; \
    } \
  } \
  if ( quoteend > quotestart ) { \
    envname = (char*)_allocsafe( quoteend - quotestart + 1 ); \
    if ( str[quotestart] == '{' ) { \
      strncpy( envname, str + quotestart + 1, quoteend - quotestart - 1 ); \
      envname[quoteend - quotestart - 1] = '\0'; \
    } else { \
      strncpy( envname, str + quotestart, quoteend - quotestart ); \
      envname[quoteend - quotestart] = '\0'; \
    } \
    envval = getenv( envname ); \
    _freesafe( envname ); \
    if ( envval != NULL ) { \
/*#if 0 */\
/*      envlen = strlen( envval ); \
      newlen = curlen + envlen + 1; \
      newstr = (char*)_allocsafe( newlen ); \
      strncpy( newstr, curstr, curpos ); \
      strncpy( newstr + curpos, envval, envlen ); \
      _freesafe( curstr ); \
      curstr = newstr; \
      curlen = newlen; \
      curpos = curpos + envlen;*/ \
/*#else */\
      curstr[curpos] = '\0'; \
      tstr = AGUIX_catQuotedAndUnQuoted( curstr, envval ); \
      /* we need space for the rest of str \
         curlen will be curlen + length of envval \
         but because special chars are protected now the length \
         can be greater the strlen( envval ) \
         but: envlen = strlen( tstr ) - curpos */ \
      newlen = curlen + ( strlen( tstr ) - curpos ) + 1; \
      newstr = (char*)_allocsafe( newlen ); \
      strncpy( newstr, tstr, newlen - 1 ); \
      _freesafe( curstr ); \
      curstr = newstr; \
      curlen = newlen; \
      curpos = strlen( tstr ); \
      _freesafe( tstr ); \
/*#endif */\
    } \
  } \
  i = quoteend; \
  if ( str[quotestart] != '{' ) i--; \
}

/*
 * Worker_replaceEnv
 * THREADSAFE
 */
char *Worker_replaceEnv( const char *str )
{
  int quotemode = 0; /* 0 means no quote
			1 means in single quotes
			2 means in double quotes */
  int i, len;
  char *curstr, *newstr, *envname, *envval, *tstr;
  int curlen, newlen, curpos, quotestart, quoteend;

  if ( str == NULL ) return NULL;

  len = strlen( str );
  curlen = len;
  curstr = (char*)_allocsafe( curlen + 1);
  curpos = 0;

  for ( i = 0; i < len; i++ ) {
    switch ( quotemode ) {
    case 1:
      curstr[curpos++] = str[i];
      if ( str[i] == '\'' ) {
	quotemode = 0;
      }
      break;
    case 2:
      if ( str[i] == '\\' ) {
        // either the backslashed char is one of
        // the special characters or just a normal char
        // in any case just wrote both chars (the backslash
        // and the char) to the destination because
        // at this point I do not unquote anything but just
        // do env-replacing
        curstr[curpos++] = str[i++];
        curstr[curpos++] = str[i];
      } else if ( str[i] == '$' ) {
        REPLACEENV;
      } else if ( str[i] == '"' ) {
        curstr[curpos++] = str[i];
	quotemode = 0;
      } else {
        curstr[curpos++] = str[i];
      }
      break;
    default:
      if ( str[i] == '\\' ) {
	// not in quotes => ignore next
        curstr[curpos++] = str[i++];
        curstr[curpos++] = str[i];
      } else if ( str[i] == '"' ) {
	// double quote begin
        curstr[curpos++] = str[i];
	quotemode = 2;
      } else if ( str[i] == '$' ) {
        REPLACEENV;
      } else if ( str[i] == '\'' ) {
	// single quote begin
        curstr[curpos++] = str[i];
	quotemode = 1;
      } else {
        curstr[curpos++] = str[i];
      }
      break;
    }
  }
  curstr[curpos] = '\0';
  return curstr;
}

int ExeClass::readErrorOutput( std::string &return_str, int maxsize )
{
  PDatei d;
  char buf[128];
  int l, r;
  
  if ( maxsize < 1 )
    maxsize = 1;

  if ( tmperror != NULL && d.open( tmperror ) == 0 ) {
    while ( maxsize > 0 ) {
      l = a_min( (int)sizeof( buf ) - 1, maxsize );
      r = d.read( buf, l );
      
      if ( r < 1 )
        break;

      buf[r] = '\0';
      return_str += buf;
      
      maxsize -= r;
    }
    d.close();
  }
  return 0;
}

int ExeClass::readOutput( std::string &return_str )
{
    if ( ! tmpoutput ) return -EINVAL;

    std::ifstream ifile( tmpoutput );

    if ( ! ifile.is_open() ) return -ENOENT;

    std::stringstream buffer;

    buffer << ifile.rdbuf();

    return_str = buffer.str();

    return 0;
}
