<?php
//========================================================================
// Author:  Pascal KISSIAN
// Resume:  http://pascal.kissian.net
//
// Copyright (c) 2015-2020 Pascal KISSIAN
//
// Published under the MIT License
//          Consider it as a proof of concept!
//          No warranty of any kind.
//          Use and abuse at your own risks.
//========================================================================

// case-sensitive:      variable names, constant name, array keys, class properties, labels
// case-insensitive:    function names, class names, class method names, namespaces, keywords and constructs
// classes, interfaces, and traits share the same internal naming_space! only a single Scrambler instance for all of them!


class Scrambler
{
  const SCRAMBLER_CONTEXT_VERSION = '1.1';

  private $t_first_chars = null;     // allowed first char of a generated identifier
  private $t_chars = null;     // allowed all except first char of a generated identifier
  private $l1 = null;     // length of $t_first_chars string
  private $l2 = null;     // length of $t_chars       string
  private $r = null;     // seed and salt for random char generation, modified at each iteration.
  private $scramble_type = null;     // type on which scrambling is done (i.e. variable, function, etc.)
  private $case_sensitive = null;     // self explanatory
  private $scramble_mode = null;     // allowed modes are 'identifier', 'hexa', 'numeric'
  private $scramble_length = null;     // current length of scrambled names
  private $scramble_length_min = null;     // min     length of scrambled names
  private $scramble_length_max = null;     // max     length of scrambled names
  private $t_ignore = null;     // array where keys are names to ignore.
  private $t_ignore_prefix = null;     // array where keys are prefix of names to ignore.
  private $t_scramble = null;     // array of scrambled items (key = source name , value = scrambled name)
  private $t_rscramble = null;     // array of reversed scrambled items (key = scrambled name, value = source name)
  private $context_directory = null;     // where to save/restore context
  private $silent = null;     // display or not Information level messages.
  private $label_counter = 0;     // internal label counter.

  private $t_reserved_variable_names = array('this', 'GLOBALS', '_SERVER', '_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_ENV', '_REQUEST',
    'php_errormsg', 'HTTP_RAW_POST_DATA', 'http_response_header', 'argc', 'argv'
  );
  private $t_reserved_function_names = array('__halt_compiler', '__autoload', 'abstract', 'and', 'array', 'as', 'bool', 'break', 'callable', 'case', 'catch',
    'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else',
    'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile',
    'eval', 'exit', 'extends', 'false', 'final', 'finally', 'float', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'fn',
    'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'int', 'interface', 'isset', 'list',
    'namespace', 'new', 'null', 'or', 'print', 'private', 'protected', 'public', 'require', 'require_once',
    'return', 'static', 'string', 'switch', 'throw', 'trait', 'true', 'try', 'unset', 'use', 'var', 'while', 'xor', 'yield',
    'apache_request_headers'                        // seems that it is not included in get_defined_functions ..
  );

  private $t_reserved_class_names = array('parent', 'self', 'static',                    // same reserved names for classes, interfaces  and traits...
    'int', 'float', 'bool', 'string', 'true', 'false', 'null', 'void', 'iterable', 'object', 'resource', 'scalar', 'mixed', 'numeric', 'fn'
  );

  private $t_reserved_method_names = array('__construct', '__destruct', '__call', '__callstatic', '__get', '__set', '__isset', '__unset', '__sleep', '__wakeup', '__tostring', '__invoke', '__set_state', '__clone', '__debuginfo');


  function __construct($type, $conf, $target_directory)
  {
    global $t_pre_defined_classes, $t_pre_defined_class_methods, $t_pre_defined_class_properties, $t_pre_defined_class_constants;
    global $t_pre_defined_class_methods_by_class, $t_pre_defined_class_properties_by_class, $t_pre_defined_class_constants_by_class;

    $this->scramble_type = $type;
    $this->t_first_chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    $this->t_chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_';
    $this->r = md5(microtime(true));     // random seed
    $this->t_scramble = array();
    $this->silent = $conf->silent;
    if (isset($conf->scramble_mode)) {
      switch ($conf->scramble_mode) {
        case 'numeric':
          $this->scramble_length_max = 32;
          $this->scramble_mode = $conf->scramble_mode;
          $this->t_first_chars = 'O';
          $this->t_chars = '0123456789';
          break;
        case 'hexa':
          $this->scramble_length_max = 32;
          $this->scramble_mode = $conf->scramble_mode;
          $this->t_first_chars = 'abcdefABCDEF';
          break;
        case 'identifier':
        default:
          $this->scramble_length_max = 16;
          $this->scramble_mode = 'identifier';
      }
    }
    $this->l1 = strlen($this->t_first_chars) - 1;
    $this->l2 = strlen($this->t_chars) - 1;
    $this->scramble_length_min = 2;
    $this->scramble_length = 5;
    if (isset($conf->scramble_length)) {
      $conf->scramble_length += 0;
      if (($conf->scramble_length >= $this->scramble_length_min) && ($conf->scramble_length <= $this->scramble_length_max)) {
        $this->scramble_length = $conf->scramble_length;
      }
    }
    switch ($type) {
      case 'constant':
        $this->case_sensitive = true;
        $this->t_ignore = array_flip($this->t_reserved_function_names);
        $this->t_ignore = array_merge($this->t_ignore, get_defined_constants(false));
        if (isset($conf->t_ignore_constants)) {
          $t = $conf->t_ignore_constants;
          $t = array_flip($t);
          $this->t_ignore = array_merge($this->t_ignore, $t);
        }
        if (isset($conf->t_ignore_constants_prefix)) {
          $t = $conf->t_ignore_constants_prefix;
          $t = array_flip($t);
          $this->t_ignore_prefix = $t;
        }
        break;
      case 'class_constant':
        $this->case_sensitive = true;
        $this->t_ignore = array_flip($this->t_reserved_function_names);
        $this->t_ignore = array_merge($this->t_ignore, get_defined_constants(false));
        if ($conf->t_ignore_pre_defined_classes != 'none') {
          if ($conf->t_ignore_pre_defined_classes == 'all') $this->t_ignore = array_merge($this->t_ignore, $t_pre_defined_class_constants);
          if (is_array($conf->t_ignore_pre_defined_classes)) {
            $t_class_names = array_map('strtolower', $conf->t_ignore_pre_defined_classes);
            foreach ($t_class_names as $class_name) if (isset($t_pre_defined_class_constants_by_class[$class_name])) $this->t_ignore = array_merge($this->t_ignore, $t_pre_defined_class_constants_by_class[$class_name]);
          }
        }
        if (isset($conf->t_ignore_class_constants)) {
          $t = $conf->t_ignore_class_constants;
          $t = array_flip($t);
          $this->t_ignore = array_merge($this->t_ignore, $t);
        }
        if (isset($conf->t_ignore_class_constants_prefix)) {
          $t = $conf->t_ignore_class_constants_prefix;
          $t = array_flip($t);
          $this->t_ignore_prefix = $t;
        }
        break;
      case 'variable':
        $this->case_sensitive = true;
        $this->t_ignore = array_flip($this->t_reserved_variable_names);
        if (isset($conf->t_ignore_variables)) {
          $t = $conf->t_ignore_variables;
          $t = array_flip($t);
          $this->t_ignore = array_merge($this->t_ignore, $t);
        }
        if (isset($conf->t_ignore_variables_prefix)) {
          $t = $conf->t_ignore_variables_prefix;
          $t = array_flip($t);
          $this->t_ignore_prefix = $t;
        }
        break;
      /*
   case 'function':
      $this->case_sensitive       = false;
      $this->t_ignore             = array_flip($this->t_reserved_function_names);
      $t                          = get_defined_functions();                  $t = array_map('strtolower',$t['internal']);    $t = array_flip($t);
      $this->t_ignore             = array_merge($this->t_ignore,$t);
      if (isset($conf->t_ignore_functions))
      {
          $t                      = $conf->t_ignore_functions;                $t = array_map('strtolower',$t);                $t = array_flip($t);
          $this->t_ignore         = array_merge($this->t_ignore,$t);
      }
      if (isset($conf->t_ignore_functions_prefix))
      {
          $t                      = $conf->t_ignore_functions_prefix;         $t = array_map('strtolower',$t);                $t = array_flip($t);
          $this->t_ignore_prefix  = $t;
      }
      break;
      */
      case 'property':
        $this->case_sensitive = true;
        $this->t_ignore = array_flip($this->t_reserved_variable_names);
        if ($conf->t_ignore_pre_defined_classes != 'none') {
          if ($conf->t_ignore_pre_defined_classes == 'all') $this->t_ignore = array_merge($this->t_ignore, $t_pre_defined_class_properties);
          if (is_array($conf->t_ignore_pre_defined_classes)) {
            $t_class_names = array_map('strtolower', $conf->t_ignore_pre_defined_classes);
            foreach ($t_class_names as $class_name) if (isset($t_pre_defined_class_properties_by_class[$class_name])) $this->t_ignore = array_merge($this->t_ignore, $t_pre_defined_class_properties_by_class[$class_name]);
          }
        }
        if (isset($conf->t_ignore_properties)) {
          $t = $conf->t_ignore_properties;
          $t = array_flip($t);
          $this->t_ignore = array_merge($this->t_ignore, $t);
        }
        if (isset($conf->t_ignore_properties_prefix)) {
          $t = $conf->t_ignore_properties_prefix;
          $t = array_flip($t);
          $this->t_ignore_prefix = $t;
        }
        break;
      case 'function_or_class':           // same instance is used for scrambling classes, interfaces, and traits.  and namespaces... and functions ...for aliasing
        $this->case_sensitive = false;
        $this->t_ignore = array_flip($this->t_reserved_function_names);
        $t = get_defined_functions();
        $t = array_map('strtolower', $t['internal']);
        $t = array_flip($t);
        $this->t_ignore = array_merge($this->t_ignore, $t);
        if (isset($conf->t_ignore_functions)) {
          $t = $conf->t_ignore_functions;
          $t = array_map('strtolower', $t);
          $t = array_flip($t);
          $this->t_ignore = array_merge($this->t_ignore, $t);
        }
        if (isset($conf->t_ignore_functions_prefix)) {
          $t = $conf->t_ignore_functions_prefix;
          $t = array_map('strtolower', $t);
          $t = array_flip($t);
          $this->t_ignore_prefix = $t;
        }

        $this->t_ignore = array_merge($this->t_ignore, array_flip($this->t_reserved_class_names));
        $this->t_ignore = array_merge($this->t_ignore, array_flip($this->t_reserved_variable_names));
        //                $this->t_ignore             = array_merge($this->t_ignore, array_flip($this->t_reserved_function_names));
        $t = get_defined_functions();
        $t = array_flip($t['internal']);
        $this->t_ignore = array_merge($this->t_ignore, $t);
        if ($conf->t_ignore_pre_defined_classes != 'none') {
          if ($conf->t_ignore_pre_defined_classes == 'all') $this->t_ignore = array_merge($this->t_ignore, $t_pre_defined_classes);
          if (is_array($conf->t_ignore_pre_defined_classes)) {
            $t_class_names = array_map('strtolower', $conf->t_ignore_pre_defined_classes);
            foreach ($t_class_names as $class_name) if (isset($t_pre_defined_classes[$class_name])) $this->t_ignore[$class_name] = 1;
          }
        }
        if (isset($conf->t_ignore_classes)) {
          $t = $conf->t_ignore_classes;
          $t = array_map('strtolower', $t);
          $t = array_flip($t);
          $this->t_ignore = array_merge($this->t_ignore, $t);
        }
        if (isset($conf->t_ignore_interfaces)) {
          $t = $conf->t_ignore_interfaces;
          $t = array_map('strtolower', $t);
          $t = array_flip($t);
          $this->t_ignore = array_merge($this->t_ignore, $t);
        }
        if (isset($conf->t_ignore_traits)) {
          $t = $conf->t_ignore_traits;
          $t = array_map('strtolower', $t);
          $t = array_flip($t);
          $this->t_ignore = array_merge($this->t_ignore, $t);
        }
        if (isset($conf->t_ignore_namespaces)) {
          $t = $conf->t_ignore_namespaces;
          $t = array_map('strtolower', $t);
          $t = array_flip($t);
          $this->t_ignore = array_merge($this->t_ignore, $t);
        }
        if (isset($conf->t_ignore_classes_prefix)) {
          $t = $conf->t_ignore_classes_prefix;
          $t = array_map('strtolower', $t);
          $t = array_flip($t);
          $this->t_ignore_prefix = array_merge($this->t_ignore_prefix, $t);
        }
        if (isset($conf->t_ignore_interfaces_prefix)) {
          $t = $conf->t_ignore_interfaces_prefix;
          $t = array_map('strtolower', $t);
          $t = array_flip($t);
          $this->t_ignore_prefix = array_merge($this->t_ignore_prefix, $t);
        }
        if (isset($conf->t_ignore_traits_prefix)) {
          $t = $conf->t_ignore_traits_prefix;
          $t = array_map('strtolower', $t);
          $t = array_flip($t);
          $this->t_ignore_prefix = array_merge($this->t_ignore_prefix, $t);
        }
        if (isset($conf->t_ignore_namespaces_prefix)) {
          $t = $conf->t_ignore_namespaces_prefix;
          $t = array_map('strtolower', $t);
          $t = array_flip($t);
          $this->t_ignore_prefix = array_merge($this->t_ignore_prefix, $t);
        }
        break;
      case 'method':
        $this->case_sensitive = false;
        if ($conf->parser_mode == 'ONLY_PHP7') $this->t_ignore = array();      // in php7 method names can be keywords
        else                                    $this->t_ignore = array_flip($this->t_reserved_function_names);

        $t = array_flip($this->t_reserved_method_names);
        $this->t_ignore = array_merge($this->t_ignore, $t);

        $t = get_defined_functions();
        $t = array_map('strtolower', $t['internal']);
        $t = array_flip($t);
        $this->t_ignore = array_merge($this->t_ignore, $t);
        if ($conf->t_ignore_pre_defined_classes != 'none') {
          if ($conf->t_ignore_pre_defined_classes == 'all') $this->t_ignore = array_merge($this->t_ignore, $t_pre_defined_class_methods);
          if (is_array($conf->t_ignore_pre_defined_classes)) {
            $t_class_names = array_map('strtolower', $conf->t_ignore_pre_defined_classes);
            foreach ($t_class_names as $class_name) if (isset($t_pre_defined_class_methods_by_class[$class_name])) $this->t_ignore = array_merge($this->t_ignore, $t_pre_defined_class_methods_by_class[$class_name]);
          }
        }
        if (isset($conf->t_ignore_methods)) {
          $t = $conf->t_ignore_methods;
          $t = array_map('strtolower', $t);
          $t = array_flip($t);
          $this->t_ignore = array_merge($this->t_ignore, $t);
        }
        if (isset($conf->t_ignore_methods_prefix)) {
          $t = $conf->t_ignore_methods_prefix;
          $t = array_map('strtolower', $t);
          $t = array_flip($t);
          $this->t_ignore_prefix = $t;
        }
        break;
      case 'label':
        $this->case_sensitive = true;
        $this->t_ignore = array_flip($this->t_reserved_function_names);
        if (isset($conf->t_ignore_labels)) {
          $t = $conf->t_ignore_labels;
          $t = array_flip($t);
          $this->t_ignore = array_merge($this->t_ignore, $t);
        }
        if (isset($conf->t_ignore_labels_prefix)) {
          $t = $conf->t_ignore_labels_prefix;
          $t = array_flip($t);
          $this->t_ignore_prefix = $t;
        }
        break;
    }
    if (isset($target_directory))                                   // the constructor will restore previous saved context if exists
    {
      $this->context_directory = get_context_directory($target_directory);
      if (file_exists("{$this->context_directory}/{$this->scramble_type}")) {
        $t = unserialize(file_get_contents("{$this->context_directory}/{$this->scramble_type}"));
        if ($t[0] !== self::SCRAMBLER_CONTEXT_VERSION) {
          fprintf(STDERR, "Error:\tContext format has changed! run with --clean option!" . PHP_EOL);
          $this->context_directory = null;        // do not overwrite incoherent values when exiting
          exit(1);
        }
        $this->t_scramble = $t[1];
        $this->t_rscramble = $t[2];
        $this->scramble_length = $t[3];
        $this->label_counter = $t[4];
      }
    }
  }

  function __destruct()
  {
    //print_r($this->t_scramble);
    if (!$this->silent) fprintf(STDERR, "Info:\t[%-17s] scrambled \t: %8d%s", $this->scramble_type, count($this->t_scramble), PHP_EOL);
    if (isset($this->context_directory))                            // the destructor will save the current context
    {
      $t = array();
      $t[0] = self::SCRAMBLER_CONTEXT_VERSION;
      $t[1] = $this->t_scramble;
      $t[2] = $this->t_rscramble;
      $t[3] = $this->scramble_length;
      $t[4] = $this->label_counter;
      file_put_contents("{$this->context_directory}/{$this->scramble_type}", serialize($t));
    }
  }

  private function str_scramble($s)                                   // scramble the string according parameters
  {
    $c1 = $this->t_first_chars[mt_rand(0, $this->l1)];      // first char of the identifier
    $c2 = $this->t_chars      [mt_rand(0, $this->l2)];      // prepending salt for md5
    $this->r = str_shuffle(md5($c2 . $s . md5($this->r)));           // 32 chars random hex number derived from $s and lot of pepper and salt

    $s = $c1;
    switch ($this->scramble_mode) {
      case 'numeric':
        for ($i = 0, $l = $this->scramble_length - 1; $i < $l; ++$i) $s .= $this->t_chars[base_convert(substr($this->r, $i, 2), 16, 10) % ($this->l2 + 1)];
        break;
      case 'hexa':
        for ($i = 0, $l = $this->scramble_length - 1; $i < $l; ++$i) $s .= substr($this->r, $i, 1);
        break;
      case 'identifier':
      default:
        for ($i = 0, $l = $this->scramble_length - 1; $i < $l; ++$i) $s .= $this->t_chars[base_convert(substr($this->r, 2 * $i, 2), 16, 10) % ($this->l2 + 1)];
    }
    return $s;
  }

  private function case_shuffle($s)   // this function is used to even more obfuscate insensitive names: on each acces to the name, a different randomized case of each letter is used.
  {
    for ($i = 0; $i < strlen($s); ++$i) $s[$i] = mt_rand(0, 1) ? strtoupper($s[$i]) : strtolower($s[$i]);
    return $s;
  }

  public function scramble($s)
  {
    $r = $this->case_sensitive ? $s : strtolower($s);
    if (array_key_exists($r, $this->t_ignore)) return $s;

    if (isset($this->t_ignore_prefix)) {
      foreach ($this->t_ignore_prefix as $key => $dummy) if (substr($r, 0, strlen($key)) === $key) return $s;
    }

    if (!isset($this->t_scramble[$r]))      // if not already scrambled:
    {
      for ($i = 0; $i < 50; ++$i)                // try at max 50 times if the random generated scrambled string has already beeen generated!
      {
        $x = $this->str_scramble($s);
        $z = strtolower($x);
        $y = $this->case_sensitive ? $x : $z;
        if (isset($this->t_rscramble[$y]) || isset($this->t_ignore[$z]))           // this random value is either already used or a reserved name
        {
          if (($i == 5) && ($this->scramble_length < $this->scramble_length_max)) ++$this->scramble_length;    // if not found after 5 attempts, increase the length...
          continue;                                                                                           // the next attempt will always be successfull, unless we already are maxlength
        }
        $this->t_scramble [$r] = $y;
        $this->t_rscramble[$y] = $r;
        break;
      }
      if (!isset($this->t_scramble[$r])) {
        fprintf(STDERR, "Scramble Error: Identifier not found after 50 iterations!%sAborting...%s", PHP_EOL, PHP_EOL); // should statistically never occur!
        exit(2);
      }
    }
    return $this->case_sensitive ? $this->t_scramble[$r] : $this->case_shuffle($this->t_scramble[$r]);
  }

  public function unscramble($s)
  {
    if (!$this->case_sensitive) $s = strtolower($s);
    return isset($this->t_rscramble[$s]) ? $this->t_rscramble[$s] : '';
  }

  public function generate_label_name($prefix = "!label")
  {
    return $prefix . ($this->label_counter++);
  }
}

?>
