#
# execute PHP buildin functions
#
package PHP::Decode::Func;

use strict;
use warnings;
use List::Util qw(min max shuffle);
use MIME::Base64;
use Compress::Zlib;
use Digest::MD5 qw(md5 md5_hex);
eval "use Digest::SHA1 qw(sha1 sha1_hex); 1" or eval "use Digest::SHA qw(sha1 sha1_hex); 1" or die "Digest::SHA/SHA1 required";
use HTML::Entities;
use URI::Escape;
use File::Basename;
use PHP::Decode::Array qw(is_int_index);
use PHP::Decode::Op;
use PHP::Decode::Parser qw(:all);
use PHP::Decode::Transformer;

my $VERSION = '0.129';

# if block contains just a sequence of strings, then return joined string
#
sub _joinable_str {
	my ($parser, $s, $val) = @_;

	if (is_block($s)) {
		my ($type, $a) = @{$parser->{strmap}{$s}};
		foreach my $stmt (@$a) {
			$val = &_joinable_str($parser, $stmt, $val);
			last if (!defined $val);
		}
		return $val;
	}
	if (is_strval($s)) {
		return $val . $parser->{strmap}{$s};
	}
	if (is_null($s)) {
		return '';
	}
	return;
}

sub parsing_func {
	my ($ctx) = @_;

	if ($ctx->{infunction}) {
		if (!$ctx->{incall}) {
			return 0;
		}
		return 1;
	}
	return 0;
}

# see: https://gist.github.com/BaiGang/1321793
#
sub levenshtein {
	my ($str1, $str2) = @_;
	my ($len1, $len2) = (length $str1, length $str2);
	my %mat;
 
	if ($len1 == 0) {
		return $len2;
	}
	if ($len2 == 0) {
		return $len1;
	}
	for (my $i = 0; $i <= $len1; ++$i) {
		$mat{0}{$i} = $i;
		$mat{1}{$i} = 0;
	}
	my @ar1 = split //, $str1;
	my @ar2 = split //, $str2;
 
	for (my $j = 1; $j <= $len2; ++$j) {
		my $p = $j % 2;
		my $q = ($j + 1) % 2;
		$mat{$p}{0} = $j;
		for (my $i = 1; $i <= $len1; ++$i) {
			my $cost = 0;
			if ($ar1[$i-1] ne $ar2[$j-1]) {
				$cost = 1;
			}
			$mat{$p}{$i} = min($cost + $mat{$q}{$i-1},
			$mat{$p}{$i-1} + 1, $mat{$q}{$i} + 1);
		}
	}
	return $mat{$len2%2}{$len1};
} 

# see: Perl-only CRC32 function
# http://billauer.co.il/blog/2011/05/perl-crc32-crc-xs-module/
#
my $crc32a_table;
my $crc32b_table;

sub mycrc32a {
	my ($input, $init_value, $polynomial) = @_;

	# hash('crc32', $str);
	# == perl -I./Digest-CRC-0.22/lib -e 'use Digest::CRC;
	#    $ctx = Digest::CRC->new(width=>32, init=>0xffffffff, xorout=>0xffffffff, refout=>0, poly=>0x4C11DB7, refin=>0, cont=>0);
	#    $ctx->add("test");
	#    print $digest = $ctx->hexdigest . "\n";' --> (need to read bytes backwards)
	#
	$init_value = 0 unless (defined $init_value);
	$polynomial = 0x04c11db7 unless (defined $polynomial);

	unless (defined $crc32a_table) {
		$crc32a_table = [];

		for (my $i=0; $i<256; $i++) {
			my $x = $i << 24;
			for (my $j=0; $j<8; $j++) {
				if ($x & (1 << 31)) {
					$x = ($x << 1) ^ $polynomial;
				} else {
					$x = $x << 1;
				}
			}
			$x = $x & 0xffffffff;
			push @$crc32a_table, $x;
		}
	}
	my $crc = $init_value ^ 0xffffffff;

	foreach my $x (unpack ('C*', $input)) {
		$crc = (($crc << 8) & 0xffffff00) ^ $crc32a_table->[(($crc >> 24) & 0xff) ^ ($x & 0xff)];
	}
	$crc = $crc ^ 0xffffffff;

	# reverse byteorder
	$crc = ($crc << 24) & 0xff000000 | ($crc << 8) & 0xff0000 | ($crc >> 8) & 0xff00 | ($crc >> 24) & 0xff;
	return $crc;
}

sub mycrc32b {
	my ($input, $init_value, $polynomial) = @_;

	# hash('crc32b', $str);
	# == perl -I./Digest-CRC-0.22/lib -e 'use Digest::CRC;
	#    $ctx = Digest::CRC->new(width=>32, init=>0xffffffff, xorout=>0xffffffff, refout=>1, poly=>0x4C11DB7, refin=>1, cont=>0); 
	#    $ctx->add("test");
	#    print $digest = $ctx->hexdigest;'
	#
	$init_value = 0 unless (defined $init_value);
	#$polynomial = _reflect(0x04c11db7,32) unless (defined $polynomial);
	$polynomial = 0xedb88320 unless (defined $polynomial);

	unless (defined $crc32b_table) {
		$crc32b_table = [];
		for (my $i=0; $i<256; $i++) {
			my $x = $i;
			for (my $j=0; $j<8; $j++) {
				if ($x & 1) {
					$x = ($x >> 1) ^ $polynomial;
				} else {
					$x = $x >> 1;
				}
			}
			push @$crc32b_table, $x;
		}
	}
	my $crc = $init_value ^ 0xffffffff;

	foreach my $x (unpack ('C*', $input)) {
		$crc = (($crc >> 8) & 0xffffff) ^ $crc32b_table->[($crc ^ $x) & 0xff];
	}
	$crc = $crc ^ 0xffffffff;
	return $crc;
}

# see: https://metacpan.org/dist/PHP-Strings/source/Strings.pm
# todo: might use more functions from PHP::Strings module
#
sub stripcslashes {
    my ($str) = @_;

    $str =~ s{
            \\([abfnrtv\\?'"])
            |
            \\(\d\d\d)
            |
            \\(x[[:xdigit:]]{2})
            |
            \\(x[[:xdigit:]])
    }{
        if ( $+ eq 'v' ) {
            "\013";
        } elsif (length $+ == 1) {
            eval qq{qq/\\$+/};
        } else {
            chr oct "0$+";
        }
    }exg ;
    return $str;
}

sub dyn_replace {
	my ($replace) = @_;
	my @groups;
	{
		no strict 'refs';
		$groups[$_] = $$_ for 1 .. $#-;      # the size of @- tells us the number of capturing groups
	}
	# For the e modifier preg_replace escapes [' " \ NULL] in the replacement string.
	# see: https://www.php.net/manual/en/function.preg-replace.php
	#
	for (my $i=1; $i < scalar @groups; $i++) {
		#print ">> dyn_replace $i: $groups[$i]\n";
		$groups[$i] =~ s/(["'\\\0])/\\$1/g;
	}
	$replace =~ s/\$(\d+)/$groups[$1]/g;
	return $replace;
}

sub dyn_replace_eval {
	my ($ctx, $replace) = @_;
	my $parser = $ctx->{parser};
	my $res = dyn_replace($replace);

	my $code = $parser->setstr($res);
	my $parser2 = $parser->subparser();
	my $ctx2 = $ctx->subctx(parser => $parser2);
	my $block = $ctx2->parse_eval($code);
	my $k = $ctx2->exec_eval($block);
	my $str = _joinable_str($parser, $k, '');
	unless (defined $str) {
		# mark non resolvable code with {{{#stmtX}}} pattern
		#
		$str = '{{{' . $k . '}}}';
	}
	$ctx->{log}->($ctx, 'replace', $replace, "$res -> eval($code) -> $str ($k)") if $ctx->{log};
	return $str;
}

sub dyn_result {
	my ($str, $parser) = @_;
	my @out;
	my $k;
	my $res;

	# when the result contains non resolved statements,
	# then create a chain of concatted expresions.
	#
	while ($str =~ /^(.*?)\{\{\{(#\w+\d+)\}\}\}(.*)$/) {
		if ($1 ne '') {
			$k = $parser->setstr($1);
			push(@out, $k);
		}
		push(@out, $2);
		$str = $3;
	}
	if (($str eq '') && (scalar @out > 0)) {
		$res = pop(@out);
	} else {
		$res = $parser->setstr($str);
	}
	while (scalar @out > 0) {
		my $op1 = pop(@out);
		$res = $parser->setexpr('.', $op1, $res);
	}
	return $res;
}

sub preg_replace {
	my ($ctx, $_pattern, $_replacement, $str, $mode, $limit) = @_;
	my $parser = $ctx->{parser};
	my $pattern = $parser->get_strval($_pattern);
	my $replacement = $parser->get_strval($_replacement);
	my $cnt = 0;
	my $res;
	my $k;

	if ($str eq '#null') {
		$res = '';
	} else {
		$res = $parser->get_strval($str);
	}
	unless (defined $pattern && defined $res) {
		return;
	}
	my $m = '';
	my $modifier;
	if ($mode eq 'preg') {
		my ($delim) = $pattern =~ /^(.)/;
		my $delim2;

		# allow bracket delimiters
		# https://php.net/regexp.reference.delimiters
		#
		if ($delim eq '[') {
			$delim2 = ']';
		} elsif ($delim eq '(') {
			$delim2 = ')';
		} elsif ($delim eq '{') {
			$delim2 = '}';
		} elsif ($delim eq '<') {
			$delim2 = '>';
		} else {
			$delim2 = $delim;;
		}
		# TODO: need to handle escapes in pattern?
		#       https://php.net/manual/en/function.preg-quote.php
		#
		if ($pattern =~ m|^\Q$delim\E([^$delim2]*)\Q$delim2\E(\w*)$|) {
			$pattern = $1;
			$modifier = $2;
			$m .= 'i' if ($modifier =~ /i/);
			$m .= 'm' if ($modifier =~ /m/);
			$m .= 's' if ($modifier =~ /s/);
			$m .= 'x' if ($modifier =~ /x/);
		}
	}

	# don't escape pattern here - it might contain regexp rules
	# don't escape replacement here - special-chars should not get excaped
	#
	if ($limit == 0) {
		# delete pattern
		if ($mode eq 'preg') {
			$res =~ s/$pattern//g;
		} else {
			$res =~ s/\Q$pattern\E//g;
		}
		$k = $parser->setstr($res);
	} else {
		unless (defined $replacement) {
			return;
		}

		# the replacement might contain backreferences in the
		# form '\1'. Convert them to '$1' for perl call.
		#
		if ($mode eq 'preg') {
			$replacement =~ s/(\\([0-9]))/\$$2/g;
		}

		# When the replacement contains backreferences and
		# it comes from data, it needs to get evaled first.
		# (eval as string by /ee does not help in this case).
		#
		# http://stackoverflow.com/questions/1908159/perl-can-i-store-backreferences-not-their-values-in-variables
		#
		if (defined $modifier && $modifier =~ /e/) {
			# eval-modifier deprecated since php-5.5
			# https://php.net/manual/en/reference.pcre.pattern.modifiers.php#reference.pcre.pattern.modifiers.eval
			#
			eval { $res =~ s/$pattern/dyn_replace_eval($ctx, $replacement)/eg; };
			if ($@) {
				$ctx->{warn}->($ctx, 'replace', $str, "bad preg/e $pattern");
				return;
			}
			if (scalar @- > 0) {
				$cnt++;
			}
			$k = dyn_result($res, $parser);
		} else {
			if ($limit != -1) {
				while ($cnt < $limit) {
					if ($mode eq 'preg') {
						eval { $res =~ s/$pattern/dyn_replace($replacement)/e; };
					} else {
						eval { $res =~ s/\Q$pattern\E/$replacement/; };
					}
					if ($@) {
						$ctx->{warn}->($ctx, 'replace', $str, "bad preg $pattern");
						return;
					}
					if (scalar @- == 0) {
						# no match found in substitution
						last;
					}
					$cnt++;
				}
			} else {
				if ($mode eq 'preg') {
					eval { $res =~ s/$pattern/dyn_replace($replacement)/eg; };
				} else {
					# TODO: use quotemeta($replacement) here?
					#
					eval { $res =~ s/\Q$pattern\E/$replacement/g; };
				}
				if ($@) {
					$ctx->{warn}->($ctx, 'replace', $str, "bad preg $pattern");
					return;
				}
				if (scalar @- > 0) {
					$cnt++;
				}
			}
			$k = $parser->setstr($res);
		}
	}
	$ctx->{log}->($ctx, 'replace', $str, "[%s]->[%s] %s -> %s", $parser->shortstr($pattern,40), $parser->shortstr($replacement,40), $parser->shortstr($str,40), $parser->shortstr($res,40)) if $ctx->{log};
	return ($k, $cnt);
}

sub preg_replace_subject {
	my ($ctx, $pattern, $replacement, $str, $mode, $limit) = @_;
	my $parser = $ctx->{parser};

	if (is_array($pattern)) {
		my $arr = $parser->{strmap}{$pattern};
		my $keys = $arr->get_keys();
		my $rep = $replacement;
		my $r_arr;
		my $r_keys;
		my $cnt = 0;

		if (is_array($replacement)) {
			$r_arr = $parser->{strmap}{$replacement};
			$r_keys = $r_arr->get_keys();
		}
		foreach my $k (@$keys) {
			my $val = $arr->val($k);
			if (is_array($replacement)) {
				my $rk = shift(@$r_keys);
				$rep = $r_arr->val($rk);
			}
			#$ctx->{log}->($ctx, 'replace', $str, "preg $val -> $rep [$str]") if $ctx->{log};
			my ($r, $n) = preg_replace($ctx, $val, $rep, $str, $mode, $limit);
			if ($limit != -1) {
				$limit -= $cnt;
			}
			$cnt += $n;
			$str = $r;
		}
		return ($str, $cnt);
	} elsif (is_strval($pattern)) {
		my ($r, $cnt) = preg_replace($ctx, $pattern, $replacement, $str, $mode, $limit);
		return ($r, $cnt);
	}
	return;
}

use constant R_VOID   => 0x0001;
use constant R_INT    => 0x0002;
use constant R_FLOAT  => 0x0004;
use constant R_BOOL   => 0x0008;
use constant R_STR    => 0x0010;
use constant R_ARRAY  => 0x0020;
use constant R_OBJECT => 0x0040;
use constant R_CALL   => 0x0080;
use constant R_MIXED  => 0x0100;
use constant R_FIX    => 0x0200; # string not usable by eval
use constant R_FIXSTR => (R_FIX|R_STR);

# https://php.net/manual/en/function.get-defined-functions.php
# list all:           php -r '$x = get_defined_functions(); print_r($x);'  | less
# list standard:      php -r '$f=get_extension_funcs("standard"); print_r($f);'  | less
# list per extension: php -r '$x = get_loaded_extensions(); foreach ($x as $e) { print ("$e: \n"); $f=get_extension_funcs($e); print_r($f); }'  | less
#
my %php_funcs_core = (
	zend_version => { ret => R_FIXSTR },
	func_num_args => { ret => R_INT },
	func_get_arg => { ret => R_MIXED },
	func_get_args => { ret => R_ARRAY },
	strlen => { ret => R_INT },
	strcmp => { ret => R_INT },
	strncmp => { ret => R_INT },
	strcasecmp => { ret => R_INT },
	strncasecmp => { ret => R_INT },
	each => { param => ['#ref0'], ret => R_ARRAY },
	error_reporting => { ret => R_INT },
	define => { ret => R_BOOL },
	defined => { ret => R_BOOL },
	get_class => { ret => R_STR },
	get_called_class => { ret => R_STR },
	get_parent_class => { ret => R_STR },
	method_exists => { ret => R_BOOL },
	property_exists => { ret => R_BOOL },
	class_exists => { ret => R_BOOL },
	interface_exists => { ret => R_BOOL },
	function_exists => { ret => R_BOOL },
	class_alias => { ret => R_BOOL },
	get_included_files => { ret => R_ARRAY },
	get_required_files => { ret => R_ARRAY },
	is_subclass_of => { ret => R_BOOL },
	is_a => { ret => R_BOOL },
	get_class_vars => { ret => R_ARRAY },
	get_object_vars => { ret => R_ARRAY },
	get_class_methods => { ret => R_ARRAY },
	trigger_error => { ret => R_BOOL },
	user_error => { ret => R_BOOL },
	set_error_handler => { ret => R_CALL },
	restore_error_handler => { ret => R_BOOL },
	set_exception_handler => { ret => R_CALL },
	restore_exception_handler => { ret => R_BOOL },
	get_declared_classes => { ret => R_ARRAY },
	get_declared_interfaces => { ret => R_ARRAY },
	get_defined_functions => { ret => R_ARRAY },
	get_defined_vars => { ret => R_ARRAY },
	create_function => { ret => R_STR, callable => 1 },
	get_resource_type => { ret => R_FIXSTR },
	get_loaded_extensions => { ret => R_ARRAY },
	extension_loaded => { ret => R_BOOL },
	get_extension_funcs => { ret => R_ARRAY },
	get_defined_constants => { ret => R_ARRAY },
	debug_backtrace => { ret => R_ARRAY },
	debug_print_backtrace => { ret => R_VOID },
	gc_collect_cycles => { ret => R_INT },
	gc_enabled => { ret => R_BOOL },
	gc_enable => { ret => R_VOID },
	gc_disable => { ret => R_VOID },
	gc_status => { ret => R_ARRAY },
);

my %php_funcs_standard = (
	exit => { ret => R_VOID }, # is language construct
	die => { ret => R_VOID }, # is language construct
	unset => { ret => R_VOID }, # is language construct
	constant => { ret => R_MIXED },
	bin2hex => { ret => R_FIXSTR },
	sleep => { ret => R_INT },
	usleep => { ret => R_VOID },
	time_nanosleep => { ret => R_ARRAY|R_BOOL },
	time_sleep_until => { ret => R_BOOL },
	strptime => { ret => R_STR },
	flush => { ret => R_VOID },
	wordwrap => { ret => R_STR },
	htmlspecialchars => { ret => R_STR },
	htmlentities => { ret => R_STR },
	html_entity_decode => { ret => R_STR },
	htmlspecialchars_decode => { ret => R_STR },
	get_html_translation_table => { ret => R_ARRAY },
	sha1 => { ret => R_FIXSTR },
	sha1_file => { ret => R_FIXSTR|R_BOOL },
	md5 => { ret => R_FIXSTR },
	md5_file => { ret => R_FIXSTR|R_BOOL },
	crc32 => { ret => R_INT },
	iptcparse => {},
	iptcembed => {},
	getimagesize => {},
	image_type_to_mime_type => {},
	image_type_to_extension => {},
	phpinfo => {},
	phpversion => {},
	phpcredits => {},
	php_logo_guid => {},
	php_real_logo_guid => {},
	php_egg_logo_guid => {},
	zend_logo_guid => {},
	php_sapi_name => {},
	php_uname => { ret => R_STR },
	php_ini_scanned_files => {},
	php_ini_loaded_file => {},
	strnatcmp => {},
	strnatcasecmp => {},
	substr_count => { ret => R_INT },
	strspn => {},
	strcspn => {},
	strtok => {},
	strtoupper => {},
	strtolower => {},
	strpos => {},
	stripos => {},
	strrpos => {},
	strripos => {},
	strrev => { ret => R_STR },
	hebrev => {},
	hebrevc => {},
	nl2br => {},
	basename => {},
	dirname => {},
	pathinfo => {},
	stripslashes => {},
	stripcslashes => {},
	strstr => {},
	stristr => {},
	strrchr => {},
	str_shuffle => {},
	str_word_count => {},
	str_split => { ret => R_ARRAY },
	strpbrk => {},
	substr_compare => {},
	strcoll => {},
	money_format => {},
	substr => {},
	substr_replace => {},
	quotemeta => {},
	ucfirst => {},
	lcfirst => {},
	ucwords => {},
	strtr => {},
	addslashes => {},
	addcslashes => {},
	rtrim => { ret => R_STR },
	str_replace => {},
	str_ireplace => {},
	str_repeat => {},
	count_chars => {},
	chunk_split => {},
	trim => { ret => R_STR },
	ltrim => { ret => R_STR },
	strip_tags => {},
	similar_text => {},
	explode => {},
	implode => {},
	join => {},
	setlocale => {},
	localeconv => {},
	nl_langinfo => {},
	soundex => {},
	levenshtein => {},
	chr => {},
	ord => {},
	parse_str => {},
	str_getcsv => {},
	str_pad => {},
	chop => {},
	strchr => {},
	sprintf => {},
	printf => {},
	vprintf => {},
	vsprintf => {},
	fprintf => {},
	vfprintf => {},
	sscanf => {},
	fscanf => {},
	parse_url => {},
	urlencode => { ret => R_STR },
	urldecode => { ret => R_STR },
	rawurlencode => { ret => R_STR },
	rawurldecode => { ret => R_STR },
	http_build_query => {},
	readlink => {},
	linkinfo => {},
	symlink => {},
	link => {},
	unlink => {},
	exec => { param => ['#str0', '#ref0', '#ref0'], ret => R_STR|R_BOOL },
	system => {},
	escapeshellcmd => {},
	escapeshellarg => {},
	passthru => {},
	shell_exec => {},
	proc_open => {},
	proc_close => {},
	proc_terminate => {},
	proc_get_status => {},
	proc_nice => {},
	rand => {},
	srand => {},
	getrandmax => {},
	mt_rand => {},
	mt_srand => {},
	mt_getrandmax => {},
	getservbyname => {},
	getservbyport => {},
	getprotobyname => {},
	getprotobynumber => {},
	getmyuid => {},
	getmygid => {},
	getmypid => {},
	getmyinode => {},
	getlastmod => {},
	base64_decode => { cmd => \&cmd_base64_decode, ret => R_STR },
	base64_encode => { cmd => \&cmd_base64_encode, ret => R_STR },
	convert_uuencode => {},
	convert_uudecode => {},
	abs => {},
	ceil => {},
	floor => {},
	round => {},
	sin => {},
	cos => {},
	tan => {},
	asin => {},
	acos => {},
	atan => {},
	atanh => {},
	atan2 => {},
	sinh => {},
	cosh => {},
	tanh => {},
	asinh => {},
	acosh => {},
	expm1 => {},
	log1p => {},
	pi => {},
	is_finite => {},
	is_nan => {},
	is_infinite => {},
	pow => { ret => R_INT|R_FLOAT },
	exp => {},
	log => {},
	log10 => {},
	sqrt => {},
	hypot => {},
	deg2rad => {},
	rad2deg => {},
	bindec => {},
	hexdec => {},
	octdec => {},
	decbin => {},
	decoct => {},
	dechex => {},
	base_convert => {},
	number_format => {},
	fmod => {},
	inet_ntop => {},
	inet_pton => {},
	ip2long => {},
	long2ip => {},
	getenv => { cmd => \&PHP::Decode::Transformer::cmd_getenv, ret => R_STR },
	putenv => {},
	getopt => {},
	sys_getloadavg => {},
	microtime => {},
	gettimeofday => {},
	getrusage => {},
	uniqid => {},
	quoted_printable_decode => {},
	quoted_printable_encode => {},
	convert_cyr_string => {},
	get_current_user => {},
	set_time_limit => {},
	get_cfg_var => {},
	magic_quotes_runtime => {},
	set_magic_quotes_runtime => {},
	get_magic_quotes_gpc => {},
	get_magic_quotes_runtime => {},
	import_request_variables => {},
	error_log => {},
	error_get_last => {},
	call_user_func => { callable => 1 },
	call_user_func_array => { callable => 1 },
	call_user_method => { callable => 1 },
	call_user_method_array => { callable => 1 },
	forward_static_call => { callable => 1 },
	forward_static_call_array => { callable => 1 },
	serialize => {},
	unserialize => {},
	var_dump => {},
	var_export => {},
	debug_zval_dump => {},
	print_r => {},
	memory_get_usage => {},
	memory_get_peak_usage => {},
	register_shutdown_function => { callable => 1 },
	register_tick_function => { callable => 1 },
	unregister_tick_function => {},
	highlight_file => {},
	show_source => {},
	highlight_string => {},
	php_strip_whitespace => {},
	ini_get => {},
	ini_get_all => {},
	ini_set => {},
	ini_alter => {},
	ini_restore => {},
	get_include_path => {},
	set_include_path => {},
	restore_include_path => {},
	setcookie => {},
	setrawcookie => {},
	header => {},
	header_remove => {},
	headers_sent => {},
	headers_list => {},
	connection_aborted => {},
	connection_status => {},
	ignore_user_abort => {},
	parse_ini_file => {},
	parse_ini_string => {},
	is_uploaded_file => {},
	move_uploaded_file => {},
	gethostbyaddr => {},
	gethostbyname => {},
	gethostbynamel => {},
	gethostname => {},
	dns_check_record => {},
	checkdnsrr => {},
	dns_get_mx => {},
	getmxrr => {},
	dns_get_record => {},
	intval => { ret => R_INT },
	floatval => { ret => R_FLOAT },
	doubleval => { ret => R_FLOAT },
	strval => { ret => R_STR },
	gettype => {},
	settype => {},
	empty => { ret => R_BOOL },
	isset => { ret => R_BOOL },
	is_null => { ret => R_BOOL },
	is_resource => { ret => R_BOOL },
	is_bool => { ret => R_BOOL },
	is_long => { ret => R_BOOL },
	is_float => { ret => R_BOOL },
	is_int => { ret => R_BOOL },
	is_integer => { ret => R_BOOL },
	is_double => { ret => R_BOOL },
	is_real => { ret => R_BOOL },
	is_numeric => { ret => R_BOOL },
	is_string => { ret => R_BOOL },
	is_array => { ret => R_BOOL },
	is_object => { ret => R_BOOL },
	is_scalar => { ret => R_BOOL },
	is_callable => { ret => R_BOOL },
	pclose => {},
	popen => {},
	readfile => {},
	rewind => {},
	rmdir => {},
	umask => {},
	fclose => {},
	feof => {},
	fgetc => {},
	fgets => {},
	fgetss => {},
	fread => {},
	fopen => {},
	fpassthru => {},
	ftruncate => {},
	fstat => {},
	fseek => {},
	ftell => {},
	fflush => {},
	fwrite => {},
	fputs => {},
	mkdir => {},
	rename => {},
	copy => {},
	tempnam => {},
	tmpfile => {},
	file => {},
	file_get_contents => {},
	file_put_contents => {},
	stream_select => {},
	stream_context_create => {},
	stream_context_set_params => {},
	stream_context_get_params => {},
	stream_context_set_option => {},
	stream_context_get_options => {},
	stream_context_get_default => {},
	stream_context_set_default => {},
	stream_filter_prepend => {},
	stream_filter_append => {},
	stream_filter_remove => {},
	stream_socket_client => {},
	stream_socket_server => {},
	stream_socket_accept => {},
	stream_socket_get_name => {},
	stream_socket_recvfrom => {},
	stream_socket_sendto => {},
	stream_socket_enable_crypto => {},
	stream_socket_shutdown => {},
	stream_socket_pair => {},
	stream_copy_to_stream => {},
	stream_get_contents => {},
	stream_supports_lock => {},
	fgetcsv => {},
	fputcsv => {},
	flock => {},
	get_meta_tags => {},
	stream_set_read_buffer => {},
	stream_set_write_buffer => {},
	set_file_buffer => {},
	set_socket_blocking => {},
	stream_set_blocking => {},
	socket_set_blocking => {},
	stream_get_meta_data => {},
	stream_get_line => {},
	stream_wrapper_register => {},
	stream_register_wrapper => {},
	stream_wrapper_unregister => {},
	stream_wrapper_restore => {},
	stream_get_wrappers => {},
	stream_get_transports => {},
	stream_resolve_include_path => {},
	stream_is_local => {},
	get_headers => {},
	stream_set_timeout => {},
	socket_set_timeout => {},
	socket_get_status => {},
	realpath => {},
	fnmatch => {},
	fsockopen => {},
	pfsockopen => {},
	pack => {},
	unpack => {},
	get_browser => {},
	crypt => {},
	opendir => {},
	closedir => {},
	chdir => {},
	chroot => {},
	getcwd => {},
	rewinddir => {},
	readdir => {},
	dir => {},
	scandir => {},
	glob => {},
	fileatime => {},
	filectime => {},
	filegroup => {},
	fileinode => {},
	filemtime => {},
	fileowner => {},
	fileperms => {},
	filesize => {},
	filetype => {},
	file_exists => {},
	is_writable => {},
	is_writeable => {},
	is_readable => {},
	is_executable => {},
	is_file => {},
	is_dir => {},
	is_link => {},
	stat => {},
	lstat => {},
	chown => {},
	chgrp => {},
	lchown => {},
	lchgrp => {},
	chmod => {},
	touch => {},
	clearstatcache => {},
	disk_total_space => {},
	disk_free_space => {},
	diskfreespace => {},
	realpath_cache_size => {},
	realpath_cache_get => {},
	mail => {},
	ezmlm_hash => {},
	openlog => {},
	syslog => {},
	closelog => {},
	define_syslog_variables => {},
	lcg_value => {},
	metaphone => {},
	ob_start => { cmd => \&PHP::Decode::Transformer::cmd_ob_start, ret => R_BOOL, callable => 1 },
	ob_flush => {},
	ob_clean => {},
	ob_end_flush => { cmd => \&PHP::Decode::Transformer::cmd_ob_end_flush, ret => R_BOOL, callable => 1 },
	ob_end_clean => { cmd => \&PHP::Decode::Transformer::cmd_ob_end_clean, ret => R_BOOL },
	ob_get_flush => {},
	ob_get_clean => {},
	ob_get_length => {},
	ob_get_level => {},
	ob_get_status => {},
	ob_get_contents => {},
	ob_implicit_flush => {},
	ob_list_handlers => {},
	ksort => { param => ['#ref0'], ret => R_BOOL },
	krsort => { param => ['#ref0'], ret => R_BOOL },
	natsort => { param => ['#ref0'], ret => R_BOOL },
	natcasesort => { param => ['#ref0'], ret => R_BOOL },
	asort => { param => ['#ref0'], ret => R_BOOL },
	arsort => { param => ['#ref0'], ret => R_BOOL },
	sort => { param => ['#ref0'], ret => R_BOOL },
	rsort => { param => ['#ref0'], ret => R_BOOL },
	usort => { param => ['#ref0'], ret => R_BOOL, callable => 1 },
	uasort => { param => ['#ref0'], ret => R_BOOL, callable => 1 },
	uksort => { param => ['#ref0'], ret => R_BOOL, callable => 1 },
	shuffle => { param => ['#ref0'], ret => R_BOOL },
	array_walk => { param => ['#ref0'], ret => R_BOOL, callable => 1 },
	array_walk_recursive => { param => ['#ref0'], ret => R_BOOL, callable => 1 },
	count => {},
	end => { param => ['#ref0'], ret => R_MIXED },
	prev => { param => ['#ref0'], ret => R_MIXED },
	next => { param => ['#ref0'], ret => R_MIXED },
	reset => { param => ['#ref0'], ret => R_MIXED },
	current => {},
	key => {},
	min => {},
	max => {},
	in_array => {},
	array_search => {},
	extract => { param => ['#ref0'], ret => R_INT },
	compact => {},
	array_fill => {},
	array_fill_keys => {},
	range => { ret => R_ARRAY },
	array_multisort => { param => ['#ref0'], ret => R_BOOL },
	array_push => { param => ['#ref0'], ret => R_INT },
	array_pop => { param => ['#ref0'], ret => R_MIXED },
	array_shift => { param => ['#ref0'], ret => R_MIXED },
	array_unshift => { param => ['#ref0'], ret => R_INT },
	array_splice => { param => ['#ref0'], ret => R_ARRAY },
	array_slice => {},
	array_merge => {},
	array_merge_recursive => {},
	array_replace => {},
	array_replace_recursive => {},
	array_keys => {},
	array_values => {},
	array_count_values => {},
	array_reverse => {},
	array_reduce => {},
	array_pad => {},
	array_flip => {},
	array_change_key_case => {},
	array_rand => {},
	array_unique => {},
	array_intersect => {},
	array_intersect_key => {},
	array_intersect_ukey => { callable => 1 },
	array_uintersect => { callable => 1 },
	array_intersect_assoc => {},
	array_uintersect_assoc => { callable => 1 },
	array_intersect_uassoc => { callable => 1 },
	array_uintersect_uassoc => { callable => 1 },
	array_diff => {},
	array_diff_key => {},
	array_diff_ukey => { callable => 1 },
	array_udiff => { callable => 1 },
	array_diff_assoc => {},
	array_udiff_assoc => { callable => 1 },
	array_diff_uassoc => { callable => 1 },
	array_udiff_uassoc => { callable => 1 },
	array_sum => {},
	array_product => {},
	array_filter => { callable => 1 },
	array_map => { callable => 1, ret => R_ARRAY },
	array_chunk => {},
	array_combine => {},
	array_key_exists => {},
	pos => {},
	sizeof => {},
	key_exists => {},
	assert => {},
	assert_options => {},
	version_compare => {},
	ftok => {},
	str_rot13 => {},
	stream_get_filters => {},
	stream_filter_register => {},
	stream_bucket_make_writeable => {},
	stream_bucket_prepend => {},
	stream_bucket_append => {},
	stream_bucket_new => {},
	output_add_rewrite_var => {},
	output_reset_rewrite_vars => {},
	sys_get_temp_dir => {},
);

my %php_funcs_date = (
        strtotime => {},
        date => {},
        idate => {},
        gmdate => {},
        mktime => {},
        gmmktime => {},
        checkdate => {},
        strftime => {},
        gmstrftime => {},
        time => {},
        localtime => {},
        getdate => {},
        date_create => {},
        date_create_from_format => {},
        date_parse => {},
        date_parse_from_format => {},
        date_get_last_errors => {},
        date_format => {},
        date_modify => {},
        date_add => {},
        date_sub => {},
        date_timezone_get => {},
        date_timezone_set => {},
        date_offset_get => {},
        date_diff => {},
        date_time_set => {},
        date_date_set => {},
        date_isodate_set => {},
        date_timestamp_set => {},
        date_timestamp_get => {},
        timezone_open => {},
        timezone_name_get => {},
        timezone_name_from_abbr => {},
        timezone_offset_get => {},
        timezone_transitions_get => {},
        timezone_location_get => {},
        timezone_identifiers_list => {},
        timezone_abbreviations_list => {},
        timezone_version_get => {},
        date_interval_create_from_date_string => {},
        date_interval_format => {},
        date_default_timezone_set => {},
        date_default_timezone_get => {},
        date_sunrise => {},
        date_sunset => {},
        date_sun_info => {},
);

my %php_funcs_pcre = (
        preg_match => {},
        preg_match_all => {},
        preg_replace => { ret => R_STR|R_ARRAY },
        preg_replace_callback => { callable => 1, ret => R_STR|R_ARRAY },
        preg_replace_callback_array => { callable => 1, ret => R_STR|R_ARRAY },
        preg_filter => {},
        preg_split => { ret => R_ARRAY },
        preg_quote => {},
        preg_grep => {},
        preg_last_error => {},
);

my %php_funcs_posix = (
        posix_kill => {},
        posix_getpid => {},
        posix_getppid => {},
        posix_getuid => {},
        posix_setuid => {},
        posix_geteuid => {},
        posix_seteuid => {},
        posix_getgid => {},
        posix_setgid => {},
        posix_getegid => {},
        posix_setegid => {},
        posix_getgroups => {},
        posix_getlogin => {},
        posix_getpgrp => {},
        posix_setsid => {},
        posix_setpgid => {},
        posix_getpgid => {},
        posix_getsid => {},
        posix_uname => {},
        posix_times => {},
        posix_ctermid => {},
        posix_ttyname => {},
        posix_isatty => {},
        posix_getcwd => {},
        posix_mkfifo => {},
        posix_mknod => {},
        posix_access => {},
        posix_getgrnam => {},
        posix_getgrgid => {},
        posix_getpwnam => {},
        posix_getpwuid => {},
        posix_getrlimit => {},
        posix_get_last_error => {},
        posix_errno => {},
        posix_strerror => {},
        posix_initgroups => {},
);

my %php_funcs_curl = (
        curl_init => {},
        curl_copy_handle => {},
        curl_version => {},
        curl_setopt => {},
        curl_setopt_array => {},
        curl_exec => {},
        curl_getinfo => {},
        curl_error => {},
        curl_errno => {},
        curl_close => {},
        curl_multi_init => {},
        curl_multi_add_handle => {},
        curl_multi_remove_handle => {},
        curl_multi_select => {},
        curl_multi_exec => {},
        curl_multi_getcontent => {},
        curl_multi_info_read => {},
        curl_multi_close => {},
);

my %php_funcs_zlib = (
        readgzfile => {},
        gzrewind => {},
        gzclose => {},
        gzeof => {},
        gzgetc => {},
        gzgets => {},
        gzgetss => {},
        gzread => {},
        gzopen => {},
        gzpassthru => {},
        gzseek => {},
        gztell => {},
        gzwrite => {},
        gzputs => {},
        gzfile => {},
        gzcompress => {},
        gzuncompress => {},
        gzdeflate => {},
        gzinflate => {},
        gzdecode => {},
        gzencode => {},
        ob_gzhandler => {},
        zlib_get_coding_type => {},
);

my %php_funcs_mysql = (
        mysql_connect => {},
        mysql_pconnect => {},
        mysql_close => {},
        mysql_select_db => {},
        mysql_query => {},
        mysql_unbuffered_query => {},
        mysql_db_query => {},
        mysql_list_dbs => {},
        mysql_list_tables => {},
        mysql_list_fields => {},
        mysql_list_processes => {},
        mysql_error => {},
        mysql_errno => {},
        mysql_affected_rows => {},
        mysql_insert_id => {},
        mysql_result => {},
        mysql_num_rows => {},
        mysql_num_fields => {},
        mysql_fetch_row => {},
        mysql_fetch_array => {},
        mysql_fetch_assoc => {},
        mysql_fetch_object => {},
        mysql_data_seek => {},
        mysql_fetch_lengths => {},
        mysql_fetch_field => {},
        mysql_field_seek => {},
        mysql_free_result => {},
        mysql_field_name => {},
        mysql_field_table => {},
        mysql_field_len => {},
        mysql_field_type => {},
        mysql_field_flags => {},
        mysql_escape_string => {},
        mysql_real_escape_string => {},
        mysql_stat => {},
        mysql_thread_id => {},
        mysql_client_encoding => {},
        mysql_ping => {},
        mysql_get_client_info => {},
        mysql_get_host_info => {},
        mysql_get_proto_info => {},
        mysql_get_server_info => {},
        mysql_info => {},
        mysql_set_charset => {},
        mysql => {},
        mysql_fieldname => {},
        mysql_fieldtable => {},
        mysql_fieldlen => {},
        mysql_fieldtype => {},
        mysql_fieldflags => {},
        mysql_selectdb => {},
        mysql_freeresult => {},
        mysql_numfields => {},
        mysql_numrows => {},
        mysql_listdbs => {},
        mysql_listtables => {},
        mysql_listfields => {},
        mysql_db_name => {},
        mysql_dbname => {},
        mysql_tablename => {},
        mysql_table_name => {},
);

my %php_funcs_hash = (
        hash => { ret => R_STR },
	hash_file => {},
	hash_hmac => {},
	hash_hmac_file => {},
	hash_init => {},
	hash_update => {},
	hash_update_stream => {},
	hash_update_file => {},
	hash_final => {},
	hash_copy => {},
	hash_algos => {},
	hash_hmac_algos => {},
	hash_pbkdf2 => {},
	hash_equals => {},
	hash_hkdf => {},
	mhash_get_block_size => {},
	mhash_get_hash_name => {},
	mhash_keygen_s2k => {},
	mhash_count => {},
	mhash => {},
);

my %php_funcs_libxml = (
	libxml_set_streams_context => {},
	libxml_use_internal_errors => {},
	libxml_get_last_error => {},
	libxml_get_errors => {},
	libxml_clear_errors => {},
	libxml_disable_entity_loader => {},
	libxml_set_external_entity_loader => {},
);

my %php_funcs_filter = (
	filter_has_var => { ret => R_BOOL },
	filter_input => { ret => R_MIXED },
	filter_var => { ret => R_MIXED },
	filter_input_array => { ret => R_MIXED },
	filter_var_array => { ret => R_ARRAY|R_BOOL },
	filter_list => { ret => R_ARRAY },
	filter_id => { ret => R_INT|R_BOOL },
);

my %php_funcs_json = (
	json_encode => { ret => R_STR },
	json_decode => { ret => R_MIXED },
	json_last_error => { ret => R_INT },
	json_last_error_msg => { ret => R_STR },
);

my %php_funcs_spl = (
	class_implements => {},
	class_parents => {},
	class_uses => {},
	spl_autoload => {},
	spl_autoload_call => {},
	spl_autoload_extensions => {},
	spl_autoload_functions => {},
	spl_autoload_register => {},
	spl_autoload_unregister => {},
	spl_classes => {},
	spl_object_hash => {},
	spl_object_id => {},
	iterator_apply => {},
	iterator_count => {},
	iterator_to_array => {},
);

my %php_funcs_session = (
	session_name => { ret => R_STR },
	session_module_name => { ret => R_STR },
	session_save_path => { ret => R_STR },
	session_id => { ret => R_STR },
	session_create_id => { ret => R_STR },
	session_regenerate_id => { ret => R_BOOL },
	session_decode => { ret => R_BOOL },
	session_encode => { ret => R_STR },
	session_destroy => { ret => R_BOOL },
	session_unset => { ret => R_BOOL },
	session_gc => { ret => R_INT },
	session_get_cookie_params => { ret => R_BOOL },
	session_write_close => { ret => R_BOOL },
	session_abort => { ret => R_BOOL },
	session_reset => { ret => R_BOOL },
	session_status => { ret => R_INT },
	session_register_shutdown => { ret => R_VOID },
	session_commit => { ret => R_BOOL },
	session_set_save_handler => { ret => R_BOOL },
	session_cache_limiter => { ret => R_STR },
	session_cache_expire => { ret => R_INT },
	session_set_cookie_params => { ret => R_ARRAY },
	session_start => { ret => R_BOOL },
);

my %php_funcs_pdo = (
	pdo_drivers => {},
);

my %php_funcs_xml = (
	xml_parser_create => {},
	xml_parser_create_ns => {},
	xml_set_object => {},
	xml_set_element_handler => {},
	xml_set_character_data_handler => {},
	xml_set_processing_instruction_handler => {},
	xml_set_default_handler => {},
	xml_set_unparsed_entity_decl_handler => {},
	xml_set_notation_decl_handler => {},
	xml_set_external_entity_ref_handler => {},
	xml_set_start_namespace_decl_handler => {},
	xml_set_end_namespace_decl_handler => {},
	xml_parse => {},
	xml_parse_into_struct => {},
	xml_get_error_code => {},
	xml_error_string => {},
	xml_get_current_line_number => {},
	xml_get_current_column_number => {},
	xml_get_current_byte_index => {},
	xml_parser_free => {},
	xml_parser_set_option => {},
	xml_parser_get_option => {},
);

my %php_funcs_calendar = (
	cal_days_in_month => {},
	cal_from_jd => {},
	cal_info => {},
	cal_to_jd => {},
	easter_date => {},
	easter_days => {},
	frenchtojd => {},
	gregoriantojd => {},
	jddayofweek => {},
	jdmonthname => {},
	jdtofrench => {},
	jdtogregorian => {},
	jdtojewish => {},
	jdtojulian => {},
	jdtounix => {},
	jewishtojd => {},
	juliantojd => {},
	unixtojd => {},
);

my %php_funcs_ctype = (
	ctype_alnum => { ret => R_BOOL },
	ctype_alpha => { ret => R_BOOL },
	ctype_cntrl => { ret => R_BOOL },
	ctype_digit => { ret => R_BOOL },
	ctype_lower => { ret => R_BOOL },
	ctype_graph => { ret => R_BOOL },
	ctype_print => { ret => R_BOOL },
	ctype_punct => { ret => R_BOOL },
	ctype_space => { ret => R_BOOL },
	ctype_upper => { ret => R_BOOL },
	ctype_xdigit => { ret => R_BOOL },
);

my %php_funcs_gettext = (
	textdomain => { ret => R_STR },
	gettext => { ret => R_STR },
	'_' => { ret => R_STR },
	dgettext => { ret => R_STR },
	dcgettext => { ret => R_STR },
	bindtextdomain => { ret => R_STR },
	ngettext => { ret => R_STR},
	dngettext => { ret => R_STR },
	dcngettext => { ret => R_STR },
	bind_textdomain_codeset => { ret => R_STR },
);

my %php_funcs_iconv = (
	iconv_strlen => { ret => R_INT },
	iconv_substr => { ret => R_STR },
	iconv_strpos => { ret => R_INT },
	iconv_strrpos => { ret => R_INT },
	iconv_mime_encode => { ret => R_STR },
	iconv_mime_decode => { ret => R_STR },
	iconv_mime_decode_headers => { ret => R_STR|R_ARRAY },
	iconv => { ret => R_STR },
	iconv_set_encoding => { ret => R_BOOL },
	iconv_get_encoding => { ret => R_STR|R_ARRAY },
	ob_iconv_handler => { callable => 1, ret => R_STR },
);

my %php_funcs_socket = (
	socket_select => {},
	socket_create_listen => {},
	socket_accept => {},
	socket_set_nonblock => {},
	socket_set_block => {},
	socket_listen => {},
	socket_close => {},
	socket_write => {},
	socket_read => { ret => R_STR },
	socket_getsockname => {},
	socket_getpeername => {},
	socket_create => {},
	socket_connect => {},
	socket_strerror => {},
	socket_bind => {},
	socket_recv => {},
	socket_send => {},
	socket_recvfrom => {},
	socket_sendto => {},
	socket_get_option => {},
	socket_getopt => {},
	socket_set_option => {},
	socket_setopt => {},
	socket_create_pair => {},
	socket_shutdown => {},
	socket_last_error => {},
	socket_clear_error => {},
	socket_import_stream => {},
	socket_export_stream => {},
	socket_sendmsg => {},
	socket_recvmsg => {},
	socket_cmsg_space => {},
	socket_addrinfo_lookup => {},
	socket_addrinfo_connect => {},
	socket_addrinfo_bind => {},
	socket_addrinfo_explain => {},
);

my %php_funcs_tokenizer = (
	token_get_all => {},
	token_name => {},
);

# https://developer.wordpress.org/reference/functions/
#
my %php_funcs_wordpress = (
	get_option => { ret => R_MIXED },
	update_option => { ret => R_BOOL },
	add_post_meta => { ret => R_INT },
	get_categories => { ret => R_ARRAY },
	wp_add_post_tags => { ret => R_ARRAY },
	wp_create_category => { ret => R_INT },
	get_posts => { ret => R_MIXED },
	get_post => { ret => R_MIXED },
	wp_insert_post => { ret => R_INT },
	wp_update_post => { ret => R_INT },
	wp_delete_post => { ret => R_MIXED },
	wp_set_post_tags => { ret => R_ARRAY },
	get_post_meta => { ret => R_MIXED },
	add_post_meta => { ret => R_INT },
	update_post_meta => { ret => R_INT },
	delete_post_meta => { ret => R_BOOL },
	get_users => { ret => R_ARRAY },
	get_user_by => { ret => R_MIXED },
	get_userdata => { ret => R_MIXED },
	wp_insert_user => { ret => R_INT },
	wp_update_user => { ret => R_INT },
	wp_delete_user => { ret => R_BOOL },
	get_user_meta => { ret => R_MIXED },
	add_user_meta => { ret => R_INT },
	update_user_meta => { ret => R_INT },
	delete_user_meta => { ret => R_BOOL },
	get_user_setting => { ret => R_MIXED },
	delete_user_setting => { ret => R_BOOL },
	wp_schedule_single_event => { ret => R_BOOL },
	wp_clear_scheduled_hook => { ret => R_INT },
	wp_unschedule_event => { ret => R_BOOL },
	wp_upload_dir => { ret => R_ARRAY },
);

my %php_funcs = (
	Core     => \%php_funcs_core,
	standard => \%php_funcs_standard,
	date     => \%php_funcs_date,
	pcre     => \%php_funcs_pcre,
	posix    => \%php_funcs_posix,
	curl     => \%php_funcs_curl,
	zlib     => \%php_funcs_zlib,
	mysql    => \%php_funcs_mysql,
	hash     => \%php_funcs_hash,
	libxml   => \%php_funcs_libxml,
	filter   => \%php_funcs_filter,
	json     => \%php_funcs_json,
	spl      => \%php_funcs_spl,
	session  => \%php_funcs_session,
	pdo      => \%php_funcs_pdo,
	xml      => \%php_funcs_xml,
	calendar => \%php_funcs_calendar,
	ctype    => \%php_funcs_ctype,
	gettext  => \%php_funcs_gettext,
	iconv    => \%php_funcs_iconv,
	socket   => \%php_funcs_socket,
	tokenizer=> \%php_funcs_tokenizer,
	#wordpress=> \%php_funcs_wordpress,
);

sub get_php_func {
	my ($cmd) = @_;

	$cmd = lc($cmd);

	foreach my $e (keys %php_funcs) {
		if (exists $php_funcs{$e}{$cmd}) {
			my $f = $php_funcs{$e}{$cmd};
			return $f;
		}
	}
	return
}

sub func_may_call_callbacks {
	my ($cmd) = @_;

	$cmd = lc($cmd);

	if ($cmd =~ /^(eval|include|include_once|require|require_once)$/) {
		return 1;
	}
	my $f = get_php_func($cmd);
	if (defined $f) {
		if (exists $f->{callable}) {
			return 1;
		}
		return 0;
	}
	return 1;
}

sub func_may_return_string {
	my ($cmd) = @_;

	$cmd = lc($cmd);

	if ($cmd =~ /^(include|include_once|require|require_once)$/) {
		return 0;
	}
	my $f = get_php_func($cmd);
	if (defined $f) {
		if (exists $f->{ret}) {
			unless ($f->{ret} & (R_STR|R_MIXED)) {
				return 0;
			}
			if ($f->{ret} & R_FIX) {
				return 0;
			}
		}
		return 1;
	}
	return 1;
}

sub cmd_base64_decode {
	my ($ctx, $cmd, $args) = @_;
	my $parser = $ctx->{parser};

	if (scalar @$args >= 1) {
		my $s = $parser->get_strval($$args[0]);
		my $php50_compat;
		my $strict = 0;
		if (scalar @$args > 1) {
			$strict = $parser->get_strval($$args[1]);
		}
		if (defined $s) {
			# older php-versions (< php5.1) treated blanks in base64 string as '+':
			# https://php.net/manual/en/function.base64-decode.php
			#
			# perl ignores any character not part of the 65-character base64 subset.
			# https://perldoc.perl.org/MIME::Base64
			#
			if ($php50_compat) {
				$s =~ s/ /+/g; # incompatible with ignore-rule
			}
			if ($strict && !($s =~ /^[A-Za-z0-9\/+]*=*$/)) {
				$ctx->{warn}->($ctx, 'cmd', $cmd, "$$args[0] contains non-strict chars");
			}
			my $decoded;
			{
				# suppress decode_base64() warnings (premature end of data, etc.)
				local $^W = 0;
				$decoded = decode_base64($s);
			}
			if (defined $decoded) {
				#$decoded = decode_input($decoded); # might convert wide strings back to internal perl utf-8 encoding
				#$ctx->{log}->($ctx, 'cmd', $cmd, "%d:[%s]->%d:[%s]", length($s), $parser->shortstr($s,40), length($decoded), $parser->shortstr($decoded,40)) if $ctx->{log};
				return $parser->setstr($decoded);
			}
		}
	}
	return;
}

sub cmd_base64_encode {
	my ($ctx, $cmd, $args) = @_;
	my $parser = $ctx->{parser};

	if (scalar @$args == 1) {
		my $s = $parser->get_strval($$args[0]);
		if (defined $s) {
			# might convert from internal perl wide string representation to utf-8 stream
			# see: https://perldoc.perl.org/MIME::Base64
			#
			#my $bytestr = encode('utf-8', $s);
			#my $encoded = encode_base64($bytestr,'');
			my $encoded = encode_base64($s,'');
			if (defined $encoded) {
				$ctx->{log}->($ctx, 'cmd', $cmd, "%d:[%s]->%d:[%s]", length($s), $parser->shortstr($s,40), length($encoded), $parser->shortstr($encoded,40)) if $ctx->{log};
				return $parser->setstr($encoded);
			}
		}
	}
	return;
}

sub get_refvar_val {
	my ($ctx, $var) = @_;
	my $parser = $ctx->{parser};

	unless ($ctx->{infunction} && !$ctx->{incall}) {
		if (is_variable($var)) {
			my $val = $ctx->getvar($var, 1);
			return (1, $val); # valid
		} elsif ($var =~ /^(\#elem\d+)$/) {
			my ($v, $i) = @{$parser->{strmap}{$var}};
			my ($basevar, $has_index, $idxstr) = $ctx->resolve_variable($var, 0);
			if ($has_index) {
				my $basestr = $ctx->exec_statement($basevar, 0);
				if (defined $basestr && is_array($basestr) && defined $idxstr) {
					$idxstr = $parser->setstr('') if is_null($idxstr); # null maps to '' array index
					my $arr = $parser->{strmap}{$basestr};
					my $val = $arr->get($idxstr);
					return (1, $val); # valid
				}
			}
		}
	}
	return;
}

sub set_refvar_val {
	my ($ctx, $var, $val) = @_;
	my $parser = $ctx->{parser};

	if (is_variable($var)) {
		$ctx->setvar($var, $val, 1);
	} elsif ($var =~ /^(\#elem\d+)$/) {
		my $sub = $parser->setexpr('=', $var, $val);
		my $had_assigns = $ctx->have_assignments();
		my $k = $ctx->exec_statement($sub);
		if (!$had_assigns) {
			# remove any pending assignments, to avoid variable
			# insertion for the 'sub'-assignmenmt.
			# The full array assignment is inserted after the loop
			#
			$ctx->discard_pending_assignments();
		}
	}
	return;
}

sub exec_cmd {
	my ($ctx, $cmd, $args) = @_;
	my $parser = $ctx->{parser};
	my $res;
	my $to_num = 0;

	# some dynamic function names are created from mixed-style strings
	# see: https://www.php.net/manual/en/functions.user-defined.php
	#      function names are case-insensitive for the ASCII characters A to Z.
	#
	$cmd = lc($cmd);

	$ctx->{log}->($ctx, 'cmd', $cmd, "args (%s)", join(', ', @$args)) if $ctx->{log};

	my $f = get_php_func($cmd);

	if (defined $f && exists $f->{cmd}) {
		my $k = &{$f->{cmd}}(@_);
		return $k;
	}

	if ($cmd eq 'unescape') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = $s;
				$res =~ s/%([0-9a-fA-F]{2})/chr(hex($1))/ge;
			}
		}
	} elsif ($cmd eq 'str_rot13') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = $s;
				$res =~ tr/A-Za-z/N-ZA-Mn-za-m/;
			}
		}
	} elsif ($cmd eq 'strrev') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = reverse $s;
			}
		}
	} elsif ($cmd eq 'strtoupper') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = uc($s);
			}
		}
	} elsif ($cmd eq 'strtolower') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = lc($s);
			}
		}
	} elsif ($cmd eq 'ucfirst') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = ucfirst($s);
			}
		}
	} elsif ($cmd eq 'lcfirst') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = lcfirst($s);
			}
		}
	} elsif ($cmd eq 'dirname') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = dirname($s);
			}
		}
	} elsif ($cmd eq 'basename') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = basename($s);
			}
		}
	} elsif ($cmd eq 'strlen') {
		if (scalar @$args == 1) {
			my $val = $$args[0];
			if (is_array($val)) {
				$ctx->{warn}->($ctx, 'cmd', $cmd, "len of array $val taken -> 0");
				$res = 0;
				$to_num = 1;
			} elsif (is_null($val)) {
				$res = 0;
				$to_num = 1;
			} elsif (is_strval($val)) {
				my $s = $parser->get_strval($val);
				if (defined $s) {
					$res = length($s);
					$to_num = 1;
				}
			}
		}
	} elsif ($cmd eq 'chr') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = chr(int($s) & 0xff);
			}
		}
	} elsif ($cmd eq 'ord') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = ord($s);
				$to_num = 1;
			}
		}
	} elsif ($cmd eq 'hexdec') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = hex($s);
				$to_num = 1;
			}
		}
	} elsif ($cmd eq 'octdec') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = oct($s);
				$to_num = 1;
			}
		}
	} elsif ($cmd eq 'bindec') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = unpack('N', pack('B32', substr('0' x 32 . $s, -32)));
				$to_num = 1;
			}
		}
	} elsif ($cmd eq 'dechex') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = sprintf("%x", $s);
			}
		}
	} elsif ($cmd eq 'octhex') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = sprintf("%o", $s);
			}
		}
	} elsif ($cmd eq 'binhex') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = sprintf("%b", $s);
			}
		}
	} elsif ($cmd eq 'bin2hex') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = unpack('H*', $s);
			}
		}
	} elsif ($cmd eq 'hex2bin') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = pack('H*', $s);
			}
		}
	} elsif ($cmd eq 'floor') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$s = 0 if ($s eq '');
				$res = int($s);
				$to_num = 1;
			}
		}
	} elsif ($cmd eq 'ceil') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$s = 0 if ($s eq '');
				$res = int($s + 0.99);
				$to_num = 1;
			}
		}
	} elsif ($cmd eq 'intval') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$s = 0 if ($s eq '');
				$res = int(PHP::Decode::Op::to_num($s));
				$to_num = 1;
			}
		}
	} elsif ($cmd eq 'strval') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = "$s";
			}
		}
	} elsif ($cmd eq 'boolval') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$s = 0 if ($s eq '');
				if ($s eq '0') {
					$res = 0;
				} else {
					$res = 1;
				}
				$to_num = 1;
			}
		}
	} elsif ($cmd eq 'floatval') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$s = 0 if ($s eq '');
				$res = $s + 0;
				$to_num = 1;
			}
		}
	} elsif ($cmd eq 'pow') {
		if (scalar @$args == 2) {
			my $s = $parser->get_strval($$args[0]);
			my $e = $parser->get_strval($$args[1]);
			if (defined $s && defined $e) {
				$s = 0 if ($s eq '');
				$e = 0 if ($e eq '');
				$res = $s ** $e;
				$to_num = 1;
			}
		}
	} elsif ($cmd eq 'gzinflate') {
		if (scalar @$args >= 1) {
			my $s = $parser->get_strval($$args[0]);
			my $maxlen = '0';
			if (scalar @$args > 1) {
				$maxlen = $parser->get_strval($$args[1]);
				$maxlen = 0 if ($maxlen eq '');
			}
			if (defined $s && (defined $maxlen && ($maxlen eq '0'))) {
				my ($i, $istatus) = Compress::Zlib::inflateInit(-WindowBits => -(MAX_WBITS));
				my ($output, $ostatus) = $i->inflate($s);
				if (defined $output) {
					$res = $output;
				}
			}
		}
	} elsif ($cmd eq 'gzdecode') {
		if (scalar @$args >= 1) {
			my $s = $parser->get_strval($$args[0]);
			my $maxlen = '0';
			if (scalar @$args > 1) {
				$maxlen = $parser->get_strval($$args[1]);
				$maxlen = 0 if ($maxlen eq '');
			}
			if (defined $s && (defined $maxlen && ($maxlen eq '0'))) {
				# http://www.faqs.org/rfcs/rfc1952.html
				#
				if (substr($s, 0, 3) eq "\x1f\x8b\x08") {
					my $off = 10;
					my $flag = ord(substr($s, 3, 1));
					if ($flag > 0 ) {
						if ($flag & 4) {
							my ($xlen) = unpack('v', substr($s, $off, 2));
							$off = $off + 2 + $xlen;
						}
						if ($flag & 8) {
							$off = index($s, "\0", $off) + 1;
						}
						if ($flag & 16) {
							$off = index($s, "\0", $off) + 1;
						}
						if ($flag & 2) {
							$off = $off + 2;
						}
					}
					my $b = substr($s, $off, -8);

					my ($i, $istatus) = Compress::Zlib::inflateInit(-WindowBits => -(MAX_WBITS));
					my ($output, $ostatus) = $i->inflate($b);
					if (defined $output) {
						$res = $output;
					}
				}
			}
		}
	} elsif ($cmd eq 'gzuncompress') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = Compress::Zlib::uncompress($s);
			}
		}
	} elsif ($cmd eq 'crc32') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = mycrc32b($s);
				$to_num = 1;
			}
		}
	} elsif ($cmd eq 'hash') {
		if (scalar @$args >= 2) {
			my $a = $parser->get_strval($$args[0]);
			my $s = $parser->get_strval($$args[1]);
			my $raw = '0';
			if (scalar @$args > 2) {
				$raw = $parser->get_strval($$args[2]);
			}
			if (defined $a && defined $s && defined $raw) {
				my $output;
				if ($a eq 'crc32') {
					$output = pack('N', mycrc32a($s));
				} elsif ($a eq 'crc32b') {
					$output = pack('N', mycrc32b($s));
				} elsif ($a eq 'sha1') {
					$output = sha1($s);
				} elsif ($a eq 'md5') {
					$output = md5($s);
				}
				if (defined $output) {
					if ($raw eq '0') {
						#$res = sprintf("%x", $output);
						$res = unpack('H*', ''.$output);
					} else {
						$res = $output;
					}
				}
			}
		}
	} elsif ($cmd eq 'rawurldecode') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				# no '+'->' ' conversion here
				$res = URI::Escape::uri_unescape($s);
			}
		}
	} elsif ($cmd eq 'rawurlencode') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				# no ' '->'+' conversion here
				# default safe RFC3986 characters are: "A-Za-z0-9\-\._~"
				$res = URI::Escape::uri_escape($s);
			}
		}
	} elsif ($cmd eq 'urldecode') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$s =~ s/\+/ /g;
				$res = URI::Escape::uri_unescape($s);
			}
		}
	} elsif ($cmd eq 'urlencode') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				# default safe characters are here: "A-Za-z0-9\-\._"
				# see: php/ext/standard/url.c
				$res = URI::Escape::uri_escape($s, "^A-Za-z0-9\-\._ ");
				$res =~ s/ /\+/g;
			}
		}
	} elsif ($cmd eq 'trim') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = $s;
				$res =~ s/^\s+|\s+$//g;
			}
		}
	} elsif ($cmd eq 'ltrim') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = $s;
				$res =~ s/^\s+//;
			}
		}
	} elsif (($cmd eq 'rtrim') || ($cmd eq 'chop')) {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = $s;
				$res =~ s/\s+$//;
			}
		}
	} elsif ($cmd eq 'stripslashes') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = $s;
				$res =~ s/\\(\'|\"|\\)/$1/g;
			}
		}
	} elsif ($cmd eq 'stripcslashes') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = stripcslashes($s);
			}
		}
	} elsif ($cmd eq 'strpos') {
		if (scalar @$args >= 2) {
			my $s = $parser->get_strval($$args[0]);
			my $p = $parser->get_strval($$args[1]);
			my $off = 0;
			if (scalar @$args == 3) {
				$off = $parser->get_strval($$args[2]);
				$off = 0 if ($off eq '');
			}
			if (defined $s && defined $p) {
				$res = index($s, $p, $off);
				$to_num = 1;
			}
		}
	} elsif ($cmd eq 'substr') {
		if (scalar @$args >= 2) {
			my $s = $parser->get_strval($$args[0]);
			my $off = $parser->get_strval($$args[1]);
			if (defined $s && defined $off) {
				$off = 0 if ($off eq '');
				if (scalar @$args == 3) {
					my $len = $parser->get_strval($$args[2]);
					if (defined $len) {
						$len = 0 if ($len eq '');
						$res = substr($s, $off, $len);
					}
				} else {
					$res = substr($s, $off);
				}
			}
		}
	} elsif ($cmd eq 'substr_count') {
		if (scalar @$args >= 2) {
			my $s = $parser->get_strval($$args[0]);
			my $p = $parser->get_strval($$args[1]);
			if (defined $s) {
				my $sub = $s;
				if (scalar @$args > 2) {
					my $off = $parser->get_strval($$args[2]);
					if (scalar @$args > 3) {
						my $len = $parser->get_strval($$args[3]);
						$sub = substr($s, $off, $len);
					} else {
						$sub = substr($s, $off);
					}
				}
				$res = () = $sub =~ /\Q$p\E/g;
				$to_num=1;
			}
		}
	} elsif ($cmd eq 'strstr') {
		if (scalar @$args >= 2) {
			my $s = $parser->get_strval($$args[0]);
			my $p = $parser->get_strval($$args[1]);
			if (defined $s && defined $p) {
				my $off = index($s, $p);
				if (defined $off) {
					my $before = 0;
					if (scalar @$args > 2) {
						$before = $parser->get_strval($$args[2]);
					}
					if ($before) {
						$res = substr($s, 0, $off);
					} else {
						$res = substr($s, $off);
					}
				}
			}
		}
	} elsif ($cmd eq 'strtr') {
		# https://www.php.net/manual/en/function.strtr.php
		#
		# if from and to have different lengths, the extra characters in
		# the longer of the two are ignored. The length of string will be
		# the same as the return value's.
		#
		if (scalar @$args == 3) {
			my $str = $parser->get_strval($$args[0]);
			my $from = $parser->get_strval($$args[1]);
			my $to = $parser->get_strval($$args[2]);
			my $org = $str;

			if (defined $str && defined $from && defined $to) {
				# for from > to perl would skip characters missing in 'to',
				# for to > from perl would substitue the last char with the full suffix.
				#
				if (length($from) > length($to)) {
					$to .= substr($from, length($from)-1);
				} elsif (length($to) > length($from)) {
					$from .= substr($to, length($to)-1);
				}
				# tr needs eval to use variables
				#
				eval "\$str =~ tr/\Q$from\E/\Q$to\E/;";
				unless ($@) {
					$ctx->{log}->($ctx, 'cmd', $cmd, "[%s]->[%s] [%s]->[%s]", $parser->shortstr($from,40), $parser->shortstr($to,40), $parser->shortstr($org,40), $parser->shortstr($str,40)) if $ctx->{log};
 					$res = $str;
				}
			}
		}
	} elsif ($cmd eq 'strcmp') {
		# https://php.net/manual/en/function.strcmp.php
		# TODO: perl cmp does just return -1,0,1
		#
		if (scalar @$args == 2) {
			my $s = $parser->get_strval($$args[0]);
			my $d = $parser->get_strval($$args[1]);
			if (defined $s && defined $d) {
				$res = $s cmp $d;
				$to_num = 1;
			}
		}
	} elsif ($cmd eq 'strncmp') {
		# https://php.net/manual/en/function.strncmp.php
		# TODO: perl cmp does just return -1,0,1
		#
		if (scalar @$args == 3) {
			my $s = $parser->get_strval($$args[0]);
			my $d = $parser->get_strval($$args[1]);
			my $len = $parser->get_strval($$args[2]);
			if (defined $s && defined $d && defined $len) {
				$len = 0 if ($len eq '');
				$res = substr($s,0,$len) cmp substr($d,0,$len);
				$to_num = 1;
			}
		}
	} elsif ($cmd eq 'strcasecmp') {
		# https://php.net/manual/en/function.strcasecmp.php
		# TODO: perl cmp does just return -1,0,1
		#
		if (scalar @$args == 2) {
			my $s = $parser->get_strval($$args[0]);
			my $d = $parser->get_strval($$args[1]);
			if (defined $s && defined $d) {
				$res = lc($s) cmp lc($d);
				$to_num = 1;
			}
		}
	} elsif ($cmd eq 'strncasecmp') {
		# https://php.net/manual/en/function.strncasecmp.php
		# TODO: perl cmp does just return -1,0,1
		#
		if (scalar @$args == 3) {
			my $s = $parser->get_strval($$args[0]);
			my $d = $parser->get_strval($$args[1]);
			my $len = $parser->get_strval($$args[2]);
			if (defined $s && defined $d && defined $len) {
				$len = 0 if ($len eq '');
				$res = lc(substr($s,0,$len)) cmp lc(substr($d,0,$len));
				$to_num = 1;
			}
		}
	} elsif ($cmd eq 'str_repeat') {
		# https://php.net/manual/en/function.str-repeat.php
		#
		if (scalar @$args == 2) {
			my $s = $parser->get_strval($$args[0]);
			my $m = $parser->get_strval($$args[1]);

			if (defined $s && defined $m) {
				$m = 0 if ($m eq '');
				if ($m > $ctx->{max_repeat}) { # limit maxsize
					$ctx->{warn}->($ctx, 'cmd', $cmd, "$s x $m memory exhaution");
				} else {
					$res = $s x $m;
				}
			}
		}
	} elsif ($cmd eq 'str_replace') {
		if (scalar @$args >= 3) {
			my $pattern = $$args[0];
			my $replacement = $$args[1];
			my $subject = $$args[2];
			my $limit = -1;
			if (scalar @$args == 4) {
				$limit = $parser->get_strval($$args[3]);
				$limit = 0 if ($limit eq '');
			}
			if (defined $pattern && defined $replacement && defined $subject) {
				if (is_array($subject)) {
					my $arr = $parser->{strmap}{$subject};
					my $keys = $arr->get_keys();
					my $newarr = $parser->newarr();
	
					foreach my $k (@$keys) {
						my $val = $arr->val($k);
						my ($r, $cnt) = preg_replace_subject($ctx, $pattern, $replacement, $val, 'str', $limit);
						$newarr->set(undef, $r);
						if ($limit != -1) {
							$limit -= $cnt;
						}
					}
					return $newarr->{name};
				} elsif (is_strval($subject)) {
					my ($r, $cnt) = preg_replace_subject($ctx, $pattern, $replacement, $subject, 'str', $limit);
					return $r;
				} elsif ($subject eq '#null') {
					my ($r, $cnt) = preg_replace_subject($ctx, $pattern, $replacement, $subject, 'str', $limit);
					return $r;
				}
			}
		}
	} elsif ($cmd eq 'ereg_replace') {
		if (scalar @$args == 3) {
			my $pattern = $parser->get_strval($$args[0]);
			my $replacement = $parser->get_strval($$args[1]);
			my $str = $parser->get_strval($$args[2]);

			# todo: use posix regex here
			#
			if (defined $pattern && defined $replacement && defined $str) {
				$res = $str;
				$res =~ s/\Q$pattern\E/$replacement/g;
				$ctx->{log}->($ctx, 'cmd', $cmd, "[%s]->[%s] %s -> %s", $parser->shortstr($pattern,40), $parser->shortstr($replacement,40), $parser->shortstr($str,40), $parser->shortstr($res,40)) if $ctx->{log};
			}
		}
	} elsif ($cmd eq 'preg_replace') {
		if (scalar @$args >= 3) {
			my $pattern = $$args[0];
			my $replacement = $$args[1];
			my $subject = $$args[2];
			my $limit = -1;
			if (scalar @$args == 4) {
				$limit = $parser->get_strval($$args[3]);
				$limit = 0 if ($limit eq '');
			}

			# preg_replace uses pattern '/xxx/' instead of 'xxx'
			#
			if (defined $pattern && defined $replacement && defined $subject) {
				if (is_array($subject)) {
					my $arr = $parser->{strmap}{$subject};
					my $keys = $arr->get_keys();
					my $newarr = $parser->newarr();
	
					foreach my $k (@$keys) {
						my $val = $arr->val($k);
						my ($r, $cnt) = preg_replace_subject($ctx, $pattern, $replacement, $val, 'preg', $limit);
						$newarr->set(undef, $r);
						if ($limit != -1) {
							$limit -= $cnt;
						}
					}
					return $newarr->{name};
				} elsif (is_strval($subject)) {
					my ($r, $cnt) = preg_replace_subject($ctx, $pattern, $replacement, $subject, 'preg', $limit);
					return $r;
				} elsif ($subject eq '#null') {
					my ($r, $cnt) = preg_replace_subject($ctx, $pattern, $replacement, $subject, 'preg', $limit);
					return $r;
				}
			}
		}
	} elsif ($cmd eq 'preg_quote') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				# escape: . \ + * ? [ ^ ] $ ( ) { } = ! < > | : -
				$s =~ s/%([\.\\\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:\-])/\\$1/ge;
				$res = $s;
			}
		}
	} elsif ($cmd eq 'md5') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = md5_hex($s);
			}
		}
	} elsif ($cmd eq 'sha1') {
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = sha1_hex($s);
			}
		}
	} elsif ($cmd eq 'levenshtein') {
		if (scalar @$args == 2) {
			my $s1 = $parser->get_strval($$args[0]);
			my $s2 = $parser->get_strval($$args[1]);
			if (defined $s1 && defined $s2) {
				$res = levenshtein($s1, $2);
			}
		}
	} elsif ($cmd eq 'str_split') {
		if (scalar @$args >= 1) {
			my $s1 = $parser->get_strval($$args[0]);
			my $s2 = '1';
			if (scalar @$args == 2) {
				$s2 = $parser->get_strval($$args[1]);
			}
			if (defined $s1 && defined $s2) {
				my $arr = $parser->newarr();
				# don't include delimter in split result
				my @parts = $s1 =~ /.{1,$s2}/g;

				foreach my $p (@parts) {
					$ctx->{log}->($ctx, 'cmd', $cmd, "[$s2] part: $p") if $ctx->{log};
					my $k = $parser->setstr($p);
					$arr->set(undef, $k);
				}
				return $arr->{name};
			}
		}
	} elsif ($cmd eq 'preg_split') {
		if (scalar @$args >= 2) {
			my $s1 = $parser->get_strval($$args[0]);
			my $s2 = $parser->get_strval($$args[1]);
			my $limit = '-1';
			my $flags = 0;
			if (scalar @$args > 2) {
				$limit = $parser->get_strval($$args[2]);
				$limit = 0 if ($limit eq '');
			}
			if (scalar @$args > 3) {
				$flags = $parser->get_strval($$args[3]);
			}
			if (defined $s1 && defined $s2 && ($flags == 0)) {
				my $arr = $parser->newarr();
				my ($pattern) = $s1 =~ /^\/(.*)\/$/;
				my @parts = split /$pattern/, $s2, $limit;

				foreach my $p (@parts) {
					$ctx->{log}->($ctx, 'cmd', $cmd, "[$s2] '$s1' part: $p") if $ctx->{log};
					my $k = $parser->setstr($p);
					$arr->set(undef, $k);
				}
				return $arr->{name};
			}
		}
	} elsif ($cmd eq 'explode') {
		if (scalar @$args >= 2) {
			my $s1 = $parser->get_strval($$args[0]);
			my $s2 = $parser->get_strval($$args[1]);
			my $limit = '-1';
			if (scalar @$args == 3) {
				$limit = $parser->get_strval($$args[2]);
			}
			if (defined $s1 && defined $s2) {
				my $arr = $parser->newarr();
				my @parts = split /\Q$s1\E/, $s2, $limit;

				foreach my $p (@parts) {
					$ctx->{log}->($ctx, 'cmd', $cmd, "[$s2] '$s1': part: $p") if $ctx->{log};
					my $k = $parser->setstr($p);
					$arr->set(undef, $k);
				}
				return $arr->{name};
			}
		}
	} elsif (($cmd eq 'implode') || ($cmd eq 'join')) {
		# https://php.net/manual/en/function.implode.php
		# 'join' is an alias for implode()
		#
		if (scalar @$args >= 1) {
			my $basestr = $$args[0];
			my $glue = '';

			if (scalar @$args == 2) {
				if (is_array($$args[0])) {
					$ctx->{warn}->($ctx, 'cmd', $cmd, "reversed arguments $$args[0] $$args[1]");
					$basestr = $$args[0];
					$glue = $parser->get_strval($$args[1]);
				} else {
					$glue = $parser->get_strval($$args[0]);
					$basestr = $$args[1];
				}
			}
			if (defined $glue && defined $basestr) {
				if (is_array($basestr)) {
					my $arr = $parser->{strmap}{$basestr};
					my $keys = $arr->get_keys();

					my @vals = ();
					my $failed = 0;
					foreach my $k (@$keys) {
						my $val = $arr->val($k);
						if (is_strval($val)) {
							push (@vals, $parser->{strmap}->{$val});
						} else {
							my $v = $ctx->exec_statement($val);
							if (is_strval($v)) {
								push (@vals, $parser->{strmap}->{$v});
							} else {
								$ctx->{warn}->($ctx, 'cmd', $cmd, "$basestr: key $k not str ($val -> $v)");
								$failed = 1;
								last;
							}
						}
					}
					$res = join($glue, @vals) unless ($failed);
				}
			}
		}
	} elsif ($cmd eq 'range') {
		# https://www.php.net/manual/en/function.range.php
		#
		if (scalar @$args >= 2) {
			my $s1 = $parser->get_strval($$args[0]);
			my $s2 = $parser->get_strval($$args[1]);
			my $step = '1';
			if (scalar @$args == 3) {
				$step = $parser->get_strval($$args[2]);
				$step = 0 if ($step eq '');
			}
			if (defined $s1 && defined $s2) {
				my $arr = $parser->newarr();
				my @parts;

				# https://perldoc.perl.org/perlop#Range-Operators
				# If the initial value specified isn't part of a magical increment sequence
				# (that is, a non-empty string matching /^[a-zA-Z]*[0-9]*\z/), only the
				# initial value will be returned.
				if (($s1 =~ /^[a-zA-Z0-9]*$/) && ($s2 =~ /^[a-zA-Z0-9]*$/)) {
					@parts = ($s1 .. $s2);
				} else {
					@parts = map { chr } (ord($s1) .. ord($s2));
				}

				foreach my $p (@parts) {
					$ctx->{log}->($ctx, 'cmd', $cmd, "['$s1', '$s2']: part: $p") if $ctx->{log};
					my $k = $parser->setstr($p);
					$arr->set(undef, $k);
				}
				return $arr->{name};
			}
		}
	} elsif (($cmd eq 'count') || ($cmd eq 'sizeof')) {
		# https://php.net/manual/en/function.count.php
		# todo: support arg2: COUNT_NORMAL/COUNT_RECURSIVE
		#
		if (scalar @$args == 1) {
			my $basestr = $$args[0];
			if (defined $basestr) {
				if (is_array($basestr)) {
					my $arr = $parser->{strmap}{$basestr};
					my $keys = $arr->get_keys();
					$res = scalar @$keys;
					$to_num = 1;
				}
			}
		}
	} elsif (($cmd eq 'htmlspecialchars') || ($cmd eq 'htmlentities')) {
		if (scalar @$args >= 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = HTML::Entities::encode_entities($s);
			}
		}
	} elsif (($cmd eq 'htmlspecialchars_decode') || ($cmd eq 'html_entity_decode')) {
		# just support 1 argument version without flags & charset for now
		# 
		if (scalar @$args == 1) {
			my $s = $parser->get_strval($$args[0]);
			if (defined $s) {
				$res = HTML::Entities::decode_entities($s);
			}
		}
	} elsif ($cmd eq 'pack') {
		if (scalar @$args >= 1) {
			my $s = $parser->get_strval($$args[0]);
			my @param = ();
			my $i;

			if (defined $s) {
				for ($i=1; $i < scalar @$args; $i++) {
					my $p = $parser->get_strval($args->[$i]);
					last unless (defined $p);
					push(@param, $p);
				}
				if ($i == scalar @$args) {
					eval { $res = pack($s, @param); };
					if ($@) {
						$ctx->{warn}->($ctx, 'cmd', $cmd, "bad param $s");
					}
				}
			}
		}
	} elsif ($cmd eq 'unpack') {
		if (scalar @$args == 2) {
			my $s = $parser->get_strval($$args[0]);
			my $d = $parser->get_strval($$args[1]);

			if (defined $s && defined $d) {
				my @values;
				eval { @values = unpack($s, $d); };
				if ($@) {
					$ctx->{warn}->($ctx, 'cmd', $cmd, "bad param $s");
				}

				my $arr = $parser->newarr();

				foreach my $p (@values) {
					$ctx->{log}->($ctx, 'cmd', $cmd, "[$s]: $p") if $ctx->{log};
					my $k = $parser->setstr($p);
					$arr->set(undef, $k);
				}
				return $arr->{name};
			}
		}
	} elsif ($cmd eq 'sprintf') {
		if (scalar @$args >= 1) {
			my $s = $parser->get_strval($$args[0]);
			my @param = ();
			my $i;

			if (defined $s) {
				for ($i=1; $i < scalar @$args; $i++) {
					my $p = $parser->get_strval($args->[$i]);
					last unless (defined $p);
					push(@param, $p);
				}
				if ($i == scalar @$args) {
					eval { $res = sprintf($s, @param); };
					if ($@) {
						$ctx->{warn}->($ctx, 'cmd', $cmd, "bad param $s");
					}
				}
			}
		}
	} elsif ($cmd eq 'array_push') {
		# https://php.net/manual/en/function.array-push.php
		#
		if (scalar @$args >= 1) {
			my $var = $$args[0]; # ref var
			my ($is_valid, $val) = get_refvar_val($ctx, $var);

			if ($is_valid) {
				if (!defined $val || is_null($val) || is_array($val)) {
					my $arr;
					if (defined $val && is_array($val)) {
						$arr = $parser->{strmap}{$val};
						$arr = $arr->copy(); # recursive copy
					} else {
						$arr = $parser->newarr();
					}
					for (my $i=1; $i < scalar @$args; $i++) {
						$arr->set(undef, $$args[$i]);
					}
					set_refvar_val($ctx, $var, $arr->{name});
					my $keys = $arr->get_keys();

					$res = scalar keys @$keys; # returns elem count
					$to_num = 1;
				}
			}
		}
	} elsif ($cmd eq 'array_pop') {
		# https://php.net/manual/en/function.array-pop.php
		#
		if (scalar @$args == 1) {
			my $var = $$args[0]; # ref var
			my ($is_valid, $val) = get_refvar_val($ctx, $var);

			if ($is_valid) {
				if (!defined $val || is_array($val)) {
					if (defined $val && is_array($val)) {
						my $arr = $parser->{strmap}{$val};
						my $keys = $arr->get_keys();
						if (scalar @$keys == 0) {
							$res = '#null';
						} else {
							my $k = pop @$keys;
							$res = $arr->val($k);
							$arr = $arr->copy($keys);
							set_refvar_val($ctx, $var, $arr->{name});
						}
					} else {
						$res = '#null';
					}
					return $res;
				}
			}
		}
	} elsif ($cmd eq 'array_unshift') {
		# https://php.net/manual/en/function.array-unshift.php
		#
		if (scalar @$args >= 1) {
			my $var = $$args[0]; # ref var
			my ($is_valid, $val) = get_refvar_val($ctx, $var);

			if ($is_valid) {
				if (!defined $val || is_null($val) || is_array($val)) {
					my $newarr = $parser->newarr();

					for (my $i=1; $i < scalar @$args; $i++) {
						$newarr->set(undef, $$args[$i]); # prepend new elems
					}
					if (defined $val && is_array($val)) {
						my $arr = $parser->{strmap}{$val};
						my $keys = $arr->get_keys();

						foreach my $k (@$keys) {
							my $oldval = $arr->val($k);
							if (is_int_index($k)) {
								$newarr->set(undef, $oldval); # renumber int key
							} else {
								$newarr->set($k, $oldval);
							}
						}
					}
					set_refvar_val($ctx, $var, $newarr->{name});

					my $keys = $newarr->get_keys();
					$res = scalar keys @$keys; # returns elem count
					$to_num = 1;
				}
			}
		}
	} elsif ($cmd eq 'array_shift') {
		# https://php.net/manual/en/function.array-shift.php
		#
		if (scalar @$args == 1) {
			my $var = $$args[0]; # ref var
			my ($is_valid, $val) = get_refvar_val($ctx, $var);

			if ($is_valid) {
				if (!defined $val || is_array($val)) {
					if (defined $val && is_array($val)) {
						my $arr = $parser->{strmap}{$val};
						my $keys = $arr->get_keys();
						if (scalar @$keys == 0) {
							$res = '#null';
						} else {
							my $k = shift @$keys;
							$res = $arr->val($k);

							my $newarr = $parser->newarr();
							foreach my $k (@$keys) {
								my $oldval = $arr->val($k);
								if (is_int_index($k)) {
									$newarr->set(undef, $oldval); # renumber int key
								} else {
									$newarr->set($k, $oldval);
								}
							}
							set_refvar_val($ctx, $var, $newarr->{name});
						}
					} else {
					}
				} else {
					$res = '#null';
				}
				return $res;
			}
		}
	} elsif ($cmd eq 'each') {
		# https://php.net/manual/en/function.each.php
		#
		if (scalar @$args == 1) {
			my $var = $$args[0]; # ref var
			my ($is_valid, $val) = get_refvar_val($ctx, $var);

			if ($is_valid) {
				if (defined $val && is_array($val)) {
					my $arr = $parser->{strmap}{$val};
					my $pos = $arr->get_pos();
					my $keys = $arr->get_keys();
					if ($pos >= scalar @$keys) {
						$res = 0;
						$to_num = 1;
					} else {
						my $k = $keys->[$pos];
						my $v = $arr->val($k);
						my $newarr = $parser->newarr();
						if (is_int_index($k)) {
							$k = $parser->setnum($k);
						}
						$newarr->set(undef, $k);
						$newarr->set(undef, $v);
						$arr->set_pos($pos+1);
						return $newarr->{name};
					}
				} else {
					$res = 0;
					$to_num = 1;
				}
			}
		}
	} elsif ($cmd eq 'reset') {
		# https://php.net/manual/en/function.reset.php
		#
		if (scalar @$args == 1) {
			my $var = $$args[0]; # ref var
			my ($is_valid, $val) = get_refvar_val($ctx, $var);

			if ($is_valid) {
				if (defined $val && is_array($val)) {
					my $arr = $parser->{strmap}{$val};
					my $keys = $arr->get_keys();
					$arr->set_pos(0);
					if (scalar @$keys == 0) {
						$res = 0;
						$to_num = 1;
					} else {
						my $k = shift @$keys;
						$res = $arr->val($k);
						return $res;
					}
				} else {
					$res = 0;
					$to_num = 1;
				}
			}
		}
	} elsif ($cmd eq 'next') {
		# https://php.net/manual/en/function.next.php
		#
		if (scalar @$args == 1) {
			my $var = $$args[0]; # ref var
			my ($is_valid, $val) = get_refvar_val($ctx, $var);

			if ($is_valid) {
				if (defined $val && is_array($val)) {
					my $arr = $parser->{strmap}{$val};
					my $pos = $arr->get_pos();
					my $keys = $arr->get_keys();
					if (($pos+1) >= scalar @$keys) {
						$res = 0;
						$to_num = 1;
					} else {
						my $k = $keys->[$pos+1];
						$res = $arr->val($k);
						$arr->set_pos($pos+1);
						return $res;
					}
				} else {
					$res = 0;
					$to_num = 1;
				}
			}
		}
	} elsif ($cmd eq 'prev') {
		# https://php.net/manual/en/function.prev.php
		#
		if (scalar @$args == 1) {
			my $var = $$args[0]; # ref var
			my ($is_valid, $val) = get_refvar_val($ctx, $var);

			if ($is_valid) {
				if (defined $val && is_array($val)) {
					my $arr = $parser->{strmap}{$val};
					my $pos = $arr->get_pos();
					my $keys = $arr->get_keys();
					if (($pos == 0) || (scalar @$keys == 0)) {
						$res = 0;
						$to_num = 1;
					} else {
						my $k = $keys->[$pos-1];
						$res = $arr->val($k);
						$arr->set_pos($pos-1);
						return $res;
					}
				} else {
					$res = 0;
					$to_num = 1;
				}
			}
		}
	} elsif ($cmd eq 'end') {
		# https://php.net/manual/en/function.end.php
		#
		if (scalar @$args == 1) {
			my $var = $$args[0]; # ref var
			my ($is_valid, $val) = get_refvar_val($ctx, $var);

			if ($is_valid) {
				if (defined $val && is_array($val)) {
					my $arr = $parser->{strmap}{$val};
					my $keys = $arr->get_keys();
					if (scalar @$keys == 0) {
						$res = 0;
						$to_num = 1;
					} else {
						my $pos = scalar @$keys - 1;
						my $k = $keys->[$pos];
						$res = $arr->val($k);
						$arr->set_pos($pos);
						return $res;
					}
				} else {
					$res = 0;
					$to_num = 1;
				}
			}
		}
	} elsif (($cmd eq 'current') || ($cmd eq 'pos')) {
		# https://php.net/manual/en/function.current.php
		#
		if (scalar @$args == 1) {
			my $var = $$args[0];

			unless ($ctx->{infunction} && !$ctx->{incall}) {
				if (is_array($var)) {
					my $arr = $parser->{strmap}{$var};
					my $pos = $arr->get_pos();
					my $keys = $arr->get_keys();
					if ($pos >= scalar @$keys) {
						$res = 0;
						$to_num = 1;
					} else {
						my $k = $keys->[$pos];
						$res = $arr->val($k);
						return $res;
					}
				} else {
					$res = 0;
					$to_num = 1;
				}
			}
		}
	} elsif ($cmd eq 'key') {
		# https://php.net/manual/en/function.key.php
		#
		if (scalar @$args == 1) {
			my $var = $$args[0];

			unless ($ctx->{infunction} && !$ctx->{incall}) {
				if (is_array($var)) {
					my $arr = $parser->{strmap}{$var};
					my $pos = $arr->get_pos();
					my $keys = $arr->get_keys();
					if ($pos >= scalar @$keys) {
						$res = '#null';
						return $res;
					} else {
						my $k = $keys->[$pos];
						if (is_int_index($k)) {
							$k = $parser->setnum($k);
						}
						return $k;
					}
				} else {
					$res = '#null';
					return $res;
				}
			}
		}
	} elsif ($cmd eq 'array_map') {
		# https://php.net/manual/en/function.array-map.php
		#
		if (scalar @$args == 2) {
			my $name = $$args[0];
			my $param = $$args[1];
			if ((is_strval($name) || is_variable($name)) && is_array($param)) {
				my $arr = $parser->{strmap}{$param};
				my $keys = $arr->get_keys();

				if (is_strval($name)) {
					$name = $parser->{strmap}{$name};
				}
				if (defined $keys) {
					my $newarr = $parser->newarr();
					foreach my $k (@$keys) {
						my $val = $arr->val($k);
						my $c = $parser->setcall($name, [$val]);
						my $v = $ctx->exec_statement($c);
						$newarr->set($k, $v);
					}
					return $newarr->{name};
				}
			}
		}
	} elsif ($cmd eq 'array_walk') {
		# https://php.net/manual/en/function.array-walk.php
		#
		if (scalar @$args >= 2) {
			my $var = $$args[0]; # ref var
			my $name = $$args[1]; # callable
			my $arg;
			if (is_strval($name)) {
				$name = $parser->{strmap}->{$name};
			}
			if (scalar @$args > 2) {
				$arg = $$args[2];
			}
			my ($is_valid, $val) = get_refvar_val($ctx, $var);

			if ($is_valid) {
				if (defined $val && is_array($val)) {
					# todo: need only to create new array if handler has ref-param
					#
					my $arr = $parser->{strmap}{$val};
					my $keys = $arr->get_keys();
					my $newarr = $parser->newarr();
					foreach my $k (@$keys) {
						my $oldval = $arr->val($k);
						my $k2 = $k;
						if (is_int_index($k)) {
							$k2 = $parser->setnum($k);
						}
						my $v = $var.'__walktmp';
						$ctx->setvar($v, $oldval, 1);
						my $c = $parser->setcall($name, defined $arg ? [$v, $k2, $arg] : [$v, $k2]);
						my $r = $ctx->exec_statement($c, 1);
						my $n = $ctx->getvar($v, 1);
						$newarr->set($k, $n);
					}
					set_refvar_val($ctx, $var, $newarr->{name});
				}
				$res = 1;
				$to_num = 1;
			}
		}
       } elsif ($cmd eq 'array_rand') {
		# https://php.net/manual/en/function.array-rand.php
		#
		if (scalar @$args >= 1) {
			my $param = $$args[0];
			my $num = 1;
			if (scalar @$args == 2) {
				$num = $parser->get_strval($$args[1]);
			}
			unless ($ctx->{infunction} && !$ctx->{incall}) {
			    # don't precalc rand() in function body
			    #
			    if (is_array($param)) {
				my $arr = $parser->{strmap}{$param};
				my $keys = $arr->get_keys();
				if (!defined $keys || ($num > scalar @$keys)) {
					$res = 0;
					$to_num = 1;
				} elsif ($num == 1) {
					my $i = int(rand(scalar @$keys));
					my $k = $keys->[$i]; # return key
					if (is_int_index($k)) {
						$res = $k;
						$to_num = 1;
					} else {
						$res = $parser->get_strval($k);
					}
				} else {
					# keep random keys uniq & in order
					my @randidx = shuffle(0 .. $#$keys);
					@randidx = sort { $a <=> $b } @randidx[0..$num-1];

					my $newarr = $parser->newarr();
					foreach (my $i=0; $i < $num; $i++) {
						my $k = $keys->[$randidx[$i]];
						if (is_int_index($k)) {
							$k = $parser->setnum($k);
						}
						$newarr->set(undef, $k); # append key
					}
					return $newarr->{name};
				}
			    }
			}
		}
	} elsif ($cmd eq 'round') {
		if (scalar @$args >= 1) {
			my $s = $parser->get_strval($$args[0]);
			my $precision = 0;
			if (scalar @$args == 2) {
				$precision = $parser->get_strval($$args[1]);
			}
			if (defined $s && defined $precision) {
				if ($precision == 0) {
					$res = int($s + 0.5);
				} else {
					$res = sprintf "%0.*f", $precision, $s;
				}
				$to_num = 1;
			}
		}
	} elsif (($cmd eq 'rand') || ($cmd eq 'mt_rand')) {
		my $min = 0;
		my $max = 32767;
		if (scalar @$args == 2) {
			$min = $parser->get_strval($$args[0]);
			$max = $parser->get_strval($$args[1]);
		}
		if (defined $min && defined $max) {
		    # don't precalc rand() in function body
		    #
		    unless ($ctx->{infunction} && !$ctx->{incall}) {
			my $range = int($max) - int($min);
			$res = int(rand($range)) + int($min);
			$to_num = 1;
		    }
		}
	} elsif ($cmd eq 'empty') {
		# https://php.net/manual/en/function.empty.php
		# (https://php.net/manual/en/types.comparisons.php)
		#
		if (scalar @$args >= 1) {
			my $val = $$args[0];
			if (is_array($val)) {
				my $arr = $parser->{strmap}{$val};
				$res = $arr->empty() ? 1 : 0;
				$to_num = 1;
			} elsif (is_null($val)) {
				$res = 1;
				$to_num = 1;
			} elsif (is_strval($val)) {
				my $s = $parser->get_strval($val);
				if (!defined $s) {
					$res = 1;
				} elsif ($s eq '0') {
					$res = 1;
				} elsif (($val =~ /^(\#str\d+)$/) && ($s eq '')) {
					$res = 1;
				} else {
					$res = 0;
				}
				$to_num = 1;
			} elsif (is_variable($val)) {
				my $v = $ctx->getvar($val);
				#$ctx->{log}->($ctx, 'cmd', $cmd, "$val -> $v: (tainted: %s)", $ctx->{tainted}) if $ctx->{log};
				if (!defined $v) {
		                    unless (&parsing_func($ctx) || $ctx->{tainted}) {
					unless ($ctx->{skipundef}) {
						$res = 1;
						$to_num = 1;
					}
				    }
				} elsif ($v eq '#unresolved') {
					$ctx->{warn}->($ctx, 'cmd', $cmd, "$val is #unresolved");
				}
			} elsif ($val =~ /^(\#elem\d+)$/) {
				my ($v, $i) = @{$parser->{strmap}->{$val}};
				my ($basevar, $has_index, $idxstr) = $ctx->resolve_variable($val, 0);
				if (defined $basevar) {
				     if ($has_index) {
					my $basestr = $ctx->exec_statement($basevar, 0);
					if (defined $basestr && is_array($basestr) && defined $idxstr) {
						$idxstr = $parser->setstr('') if is_null($idxstr); # null maps to '' array index
						my $arr = $parser->{strmap}{$basestr};
						my $arrval = $arr->get($idxstr);
						if (defined $arrval) {
							my $s = $parser->get_strval($arrval);
							if (!defined $s) {
								$res = 1;
							} elsif ($s eq '0') {
								$res = 1;
							} elsif (($arrval =~ /^(\#str\d+)$/) && ($s eq '')) {
								$res = 1;
							} else {
								$res = 0;
							}
						} elsif (!$ctx->is_superglobal($basevar)) {
							$res = 1;
						}
						$to_num = 1;
					} elsif (!defined $basestr && (!$ctx->is_superglobal($basevar) || ($basevar =~ /^\$GLOBALS$/))) {
						unless ($ctx->{tainted}) {
							$ctx->{warn}->($ctx, 'cmd', $cmd, "$val %s[%s] is #unresolved", $basevar, defined $i ? $i : '');
						}
					}
				    } else {
					my $v = $ctx->getvar($basevar);
					if (!defined $v) {
				             unless (&parsing_func($ctx) || $ctx->{tainted}) {
						unless ($ctx->{skipundef}) {
							$res = 1;
							$to_num = 1;
						}
					    }
					} elsif ($v eq '#unresolved') {
						$ctx->{warn}->($ctx, 'cmd', $cmd, "$val is #unresolved");
					}
				    }
				}
			}
		}
	} elsif ($cmd eq 'is_array') {
		# https://www.php.net/manual/en/function.is-array.php
		#
		if (scalar @$args == 1) {
			my $val = $$args[0];
			if (is_array($val)) {
				$res = 1;
				$to_num = 1;
			} elsif ($ctx->is_superglobal($val)) {
				$res = 1;
				$to_num = 1;
			} elsif (is_null($val)) {
				$res = 0;
				$to_num = 1;
			} elsif (is_strval($val)) {
				$res = 0;
				$to_num = 1;
			}
		}
	} elsif ($cmd eq 'is_null') {
		# https://php.net/manual/en/function.is-null.php (similar to $a === null)
		# (https://php.net/manual/en/types.comparisons.php)
		#
		if (scalar @$args >= 1) {
			my $val = $$args[0];
			if (is_array($val)) {
				$res = 0;
				$to_num = 1;
			} elsif (is_null($val)) {
				$res = 1;
				$to_num = 1;
			} elsif (is_strval($val)) {
				my $s = $parser->get_strval($val);
				if (!defined $s) {
					$res = 1;
				} else {
					$res = 0;
				}
				$to_num = 1;
			} elsif (is_variable($val)) {
				my $v = $ctx->getvar($val);
				if (!defined $v) {
		                    unless (&parsing_func($ctx) || $ctx->{tainted}) {
					unless ($ctx->{skipundef}) {
						if ($ctx->is_superglobal($val)) {
							# superglobals always exist as empty array,
							# Since it is unknown which fields exist in a real environment,
							# don't create superglobal array on the fly here.
							#
							$res = 0;
						} else {
							$res = 1;
						}
						$to_num = 1;
					}
				    }
				} elsif ($v eq '#unresolved') {
					$ctx->{warn}->($ctx, 'cmd', $cmd, "$val is #unresolved");
				}
			} elsif ($val =~ /^(\#elem\d+)$/) {
				my ($v, $i) = @{$parser->{strmap}->{$val}};
				my ($basevar, $has_index, $idxstr) = $ctx->resolve_variable($val, 0);
				if (defined $basevar) {
					my $basestr = $ctx->exec_statement($basevar, 0);
					if (defined $basestr && is_array($basestr) && defined $idxstr) {
						$idxstr = $parser->setstr('') if is_null($idxstr); # null maps to '' array index
						my $arr = $parser->{strmap}{$basestr};
						my $arrval = $arr->get($idxstr);
						if (defined $arrval) {
							$res = 0;
						} elsif (!$ctx->is_superglobal($basevar)) {
							$res = 1;
						}
						$to_num = 1;
					} elsif (!defined $basestr && (!$ctx->is_superglobal($basevar) || ($basevar =~ /^\$GLOBALS$/))) {
						unless ($ctx->{tainted}) {
							$ctx->{warn}->($ctx, 'cmd', $cmd, "$val %s[%s] is #unresolved", $basevar, defined $i ? $i : '');
						}
					}
				}
			}
		}
	} elsif ($cmd eq 'is_null_weak') {
		# https://php.net/manual/en/types.comparisons.php (similar to $a == null)
		#
		if (scalar @$args >= 1) {
			my $val = $$args[0];
			if (is_array($val)) {
				$res = 1;
				$to_num = 1;
			} elsif (is_null($val)) {
				$res = 1;
				$to_num = 1;
			} elsif (is_strval($val)) {
				my $s = $parser->get_strval($val);
				if (!defined $s) {
					$res = 1;
				} elsif (($val =~ /^(\#num\d+)$/) && ($s == 0)) {
					$res = 1;
				} elsif ($s eq '0') {
					$res = 0;
				} elsif (($val =~ /^(\#str\d+)$/) && ($s eq '')) {
					$res = 1;
				} elsif ($val =~ /^(\#null)$/) {
					$res = 1;
				} else {
					$res = 0;
				}
				$to_num = 1;
			} elsif (is_variable($val)) {
				my $v = $ctx->getvar($val);
				if (!defined $v) {
		            	    unless (&parsing_func($ctx) || $ctx->{tainted}) {
					unless ($ctx->{skipundef}) {
						if ($ctx->is_superglobal($val)) {
							# superglobals always exist as empty array,
							# Since it is unknown which fields exist in a real environment,
							# don't create superglobal array on the fly here.
							#
							$res = 0;
						} else {
							$res = 1;
						}
						$to_num = 1;
					}
				    }
				} elsif ($v eq '#unresolved') {
					$ctx->{warn}->($ctx, 'cmd', $cmd, "$val is #unresolved");
				}
			} elsif ($val =~ /^(\#elem\d+)$/) {
				my ($v, $i) = @{$parser->{strmap}->{$val}};
				my ($basevar, $has_index, $idxstr) = $ctx->resolve_variable($val, 0);
				if (defined $basevar) {
					my $basestr = $ctx->exec_statement($basevar, 0);
					if (defined $basestr && is_array($basestr) && defined $idxstr) {
						$idxstr = $parser->setstr('') if is_null($idxstr); # null maps to '' array index
						my $arr = $parser->{strmap}{$basestr};
						my $arrval = $arr->get($idxstr);
						if (defined $arrval) {
							my $s = $parser->get_strval($arrval);
							if (!defined $s) {
								$res = 1;
							} elsif (($arrval =~ /^(\#num\d+)$/) && ($s == 0)) {
								$res = 1;
							} elsif ($arrval eq '0') {
								$res = 1;
							} elsif (($arrval =~ /^(\#str\d+)$/) && ($s eq '')) {
								$res = 1;
							} elsif ($arrval =~ /^(\#null)$/) {
								$res = 1;
							} else {
								$res = 0;
							}
						} elsif (!$ctx->is_superglobal($basevar)) {
							$res = 1;
						}
						$to_num = 1;
					} elsif (!defined $basestr && (!$ctx->is_superglobal($basevar) || ($basevar =~ /^\$GLOBALS$/))) {
						unless ($ctx->{tainted}) {
							$ctx->{warn}->($ctx, 'cmd', $cmd, "$val %s[%s] is #unresolved", $basevar, defined $i ? $i : '');
						}
					}
				}
			}
		}
	} elsif ($cmd eq 'isset') {
		my $inv = &exec_cmd($ctx, 'is_null', $args);
		if (defined $inv && is_strval($inv)) {
			if ($parser->{strmap}{$inv} eq '0') {
				$res = 1;
			} else {
				$res = 0;
			}
			$to_num = 1;
		}
	} elsif ($cmd eq 'define') {
		if (scalar @$args == 2) {
			my $key = $parser->get_strval($$args[0]);
			my $val = $parser->get_strval($$args[1]);
			if (defined $key && defined $val) {
				if (defined $ctx->{defines}) {
					$ctx->{defines}{$key} = $$args[1];
					$ctx->{log}->($ctx, 'cmd', $cmd, "$key = $val") if $ctx->{log};
					# no result here
					return '#noreturn';
				}
			}
		}
	} elsif ($cmd eq 'function_exists') {
		# https://php.net/manual/en/function.function-exists.php
		#
		if (scalar @$args == 1) {
			my $fn = $parser->get_strval($$args[0]);
			if (defined $fn) {
				my $lfn = lc($fn);

				# php functions are always global
				# - so a user defined func is valid also in tainted mode
				#
				unless ($ctx->{skipundef}) {
					if ($ctx->getfun($fn)) {
						$res = 1;
					}
				}
				unless (defined $res) {
					if (get_php_func($lfn)) {
						$res = 1;
					}
				}
				unless (defined $res) {
					if ($ctx->{tainted}) {
						$ctx->{warn}->($ctx, 'cmd', $cmd, "$fn not found but ctx tainted -> skip execution");
						return;
					} else {
						$res = 0;
					}
				}
				$to_num = 1;
			}
		}
	} elsif ($cmd eq 'create_function') {
		# https://php.net/manual/en/function.create-function.php
		# (see also: anonymous functions)
		#
		# This is basically what PHP does internally:
		# function create_function($args, $code) {
		#     // create a random $functionName
		#     eval('function ' . $functionName . '($args){$code}');
		#     return $functionName;
		# }
		#
		# It's why you can do something like this (which works on the same principle as SQL injection):
		# $code = 'print "I print repeatedly.\n"; } print "I print once.\n"; if (false) {';
		# $function = create_function('', $code);
		# call_user_func($function);
		# call_user_func($function);
		# 
		# // I print once.
		# // I print repeatedly.
		# // I print repeatedly.
		#
		if (scalar @$args == 2) {
			my $params = $parser->get_strval($$args[0]);
			my $code   = $parser->get_strval($$args[1]);
			if (defined $args && defined $code) {
				my @arglist;
				foreach my $p (split(/,/, $params)) {
					my $stmt = $parser->read_statement([$p], undef);
					push(@arglist, $stmt);
				}
				# eval with new parser and without global & local varmap
				#
				my $codestr = $parser->setstr('function ('.$params.') {'.$code.'}');
				my $parser2 = $parser->subparser();
				my $ctx2 = $ctx->subctx(globals => {}, varmap => {}, parser => $parser2, tainted => 1, varhist => {});
				my $block = $ctx2->parse_eval($codestr);
				my $stmt = $ctx2->exec_eval($block);
				$stmt = $ctx->exec_statement($stmt);
				return $stmt;
			}
		}
	} elsif ($cmd eq 'call_user_func') {
		# https://php.net/manual/en/function.call-user-func.php
		#
		if (scalar @$args >= 1) {
			my $name = $$args[0];
			if (is_strval($name) || is_variable($name)) {
				if (is_strval($name)) {
					$name = $parser->{strmap}->{$name};
				}
				my @arglist = @$args[1..@$args-1];
				my $call = $parser->setcall($name, \@arglist);
				return $call;
			}
		}
	} elsif ($cmd eq 'call_user_func_array') {
		# https://php.net/manual/en/function.call-user-func-array.php
		#
		if (scalar @$args == 2) {
			my $name = $$args[0];
			my $param = $$args[1];
			if ((is_strval($name) || is_variable($name)) && is_array($param)) {
				my $arr = $parser->{strmap}{$param};
				my $keys = $arr->get_keys();
				if (is_strval($name)) {
					$name = $parser->{strmap}->{$name};
				}
				if (@$keys) {
					my @arglist = ();
					foreach my $k (@$keys) {
						my $val = $arr->val($k);
						my $v = $ctx->exec_statement($val);
						push(@arglist, $v);
					}
					my $call = $parser->setcall($name, \@arglist);
					return $call;
				}
			}
		}
	} elsif ($cmd eq 'file') {
		# https://php.net/manual/en/function.file.php
		# - TODO: also URL possible
		#
		if (scalar @$args >= 1) {
			my $filename = $parser->get_strval($$args[0]);
			my $flags = 0;

			if (scalar @$args > 1) {
				$flags = $parser->get_strval($$args[1]);
			}
			if (defined $filename) {
			    my ($fh) = $parser->newfh($filename, 'r');
			    if (defined $fh) {
				my $buf = $parser->{strmap}->{$fh}{buf};
				$parser->{strmap}->{$fh}{buf} = '';
				# split in lines and keep trailing newlines
				#
				my @lines;
				if ($flags =~ /FILE_IGNORE_NEW_LINES/) {
					@lines = split(/\n/, $buf);
				} else {
					@lines = split(/^/, $buf);
				}
				my $arr = $parser->newarr();

				foreach my $p (@lines) {
					$ctx->{log}->($ctx, 'cmd', $cmd, "[$filename] line: $p") if $ctx->{log};
					my $k = $parser->setstr($p);
					$arr->set(undef, $k);
				}
				return $arr->{name};
			    }
			}
		}
	} elsif ($cmd eq 'file_get_contents') {
		# https://php.net/manual/en/function.file-get-contents.php
		#
		if (scalar @$args == 1) {
			my $filename = $parser->get_strval($$args[0]);

			if (defined $filename) {
			    my ($fh) = $parser->newfh($filename, 'r');
			    if (defined $fh) {
				my $buf = $parser->{strmap}->{$fh}{buf};
				$parser->{strmap}->{$fh}{buf} = '';
				$res = $buf;
			    }
			}
		}
	} elsif ($cmd eq 'reset') {
		# https://php.net/manual/en/function.reset.php
		# reset next()-pointer & return first array element
		#
		if (scalar @$args == 1) {
			my $basestr = $$args[0];

			if (is_array($basestr)) {
				my $arr = $parser->{strmap}{$basestr};
				my $keys = $arr->get_keys();

				#my @vals = map { $arr->val($_) } @$keys;
				$res = $arr->val($keys->[0]);
			}
		}
	} elsif ($cmd eq 'fopen') {
		if (scalar @$args >= 2) {
			my $filename = $parser->get_strval($$args[0]);
			my $mode = $parser->get_strval($$args[1]);
			if (defined $filename && defined $mode) {
				my ($fh) = $parser->newfh($filename, $mode);
				return $fh;
			}
		}
	} elsif ($cmd eq 'fclose') {
		if (scalar @$args == 1) {
			my $fh = $$args[0];
			if ($fh =~ /^(\#fh\d+)$/) {
				$parser->{strmap}->{$fh}{pos} = 0;
				$parser->{strmap}->{$fh}{buf} = '';
			}
		}
	} elsif ($cmd eq 'fread') {
		if (scalar @$args == 2) {
			my $fh = $$args[0];
			my $cnt = $parser->get_strval($$args[1]);
			if (($fh =~ /^(\#fh\d+)$/) && defined $cnt) {
				my $pos = $parser->{strmap}->{$fh}{pos};
				my $buf = $parser->{strmap}->{$fh}{buf};
				if ($cnt > length($buf) - $pos) {
					$cnt = length($buf) - $pos;
				}
				$res = substr($buf, $pos, $cnt);
				$pos += $cnt;
				$parser->{strmap}->{$fh}{pos} = $pos;
			}
		}
	} elsif ($cmd eq 'fgets') {
		if (scalar @$args >= 1) {
			my $fh = $$args[0];
			my $cnt;
			if (scalar @$args == 2) {
				$cnt = $parser->get_strval($$args[1]);
			}
			if ($fh =~ /^(\#fh\d+)$/) {
				my $pos = $parser->{strmap}->{$fh}{pos};
				my $buf = $parser->{strmap}->{$fh}{buf};
				$res = '';

				while ($pos < length($buf)) {
					my $ch = substr($buf, $pos, 1);
					if ($ch eq "\n") {
						$pos++;
						last;
					}
					last if (defined $cnt && (length($res) >= $cnt));
					$res .= $ch;
					$pos++;
				}
				$parser->{strmap}->{$fh}{pos} = $pos;
			}
		}
	}

        if (defined $res) {
		my $k;
		if ($to_num) {
			$k = $parser->setnum($res);
		} else {
			$k = $parser->setstr($res);
		}
		return $k;
	}
	return;
}

1;

__END__

=head1 NAME

PHP::Decode::Func

=head1 SYNOPSIS

  # Creating an instance

  my %strmap;
  my $parser = PHP::Decode::Parser->new(strmap => \%strmap);
  my $ctx = PHP::Decode::Transformer->new(parser => $parser);

  # Exec func

  my $str = $parser->setstr('test');
  my $res = PHP::Decode::Func::exec_cmd($ctx, 'strlen', [$str]);

  if (defined $res) {
      my $code = $parser->format_stmt($res);
      print $code;
  }

=head1 DESCRIPTION

The PHP::Decode::Func Module contains implementations of php builtin-functions
without external side-effects.

=head1 METHODS

=head2 exec_cmd

Execute a php buildin-function.

    $res = PHP::Decode::Func::exec_cmd($ctx, $cmd, $args);

=head2 Dependencies

Requires the PHP::Decode::Parser, PHP::Decode::Transformer and PHP::Decode::Array Modules.

Some other Modules are required to implement the php functions:
List::Util, Compress::Zlib, Digest::MD5, Digest::SHA1, HTML::Entities, URI::Escape, File::Basename.

=head1 AUTHORS

Barnim Dzwillo @ Strato AG

=cut
