<?php
/**
 * Core File Compare — verifies wp-admin/, wp-includes/ files against
 * the official wordpress.org MD5 manifest for the installed WP version.
 *
 * Catches: modified core files (backdoors), missing files (broken upload),
 * extra files in core dirs (web shells dropped into wp-admin/).
 *
 * Uses official API:
 *   https://api.wordpress.org/core/checksums/1.0/?version=X.Y.Z&locale=L
 * Returns: { checksums: { "<relative-path>": "<md5>", ... } }
 *
 * @package Brainwerk Security Suite
 */

if ( ! defined( 'ABSPATH' ) ) exit;

class Shieldforge_Core_Compare {

    const CACHE_TTL    = DAY_IN_SECONDS;
    const CACHE_PREFIX = 'shieldforge_core_manifest_';

    public static function register( Shieldforge_Loader $loader ) {
        // Kein Auto-Cron — User-getriggert über Admin-Button (CPU-Intensive).
        $loader->add_action( 'wp_ajax_shieldforge_core_compare', __CLASS__, 'ajax_run' );
    }

    public static function ajax_run() {
        if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error( array( 'message' => 'forbidden' ), 403 );
        if ( ! check_ajax_referer( 'shieldforge_admin', 'nonce', false ) ) wp_send_json_error( array( 'message' => 'bad nonce' ), 403 );
        $report = self::run();
        wp_send_json( $report );
    }

    /**
     * Runs the comparison. Returns:
     *   { ok: bool, summary: {modified, missing, extra, total_checked},
     *     details: { modified: [], missing: [], extra: [] }, version, locale }
     */
    public static function run() {
        global $wp_version;
        $locale = get_locale();
        $manifest = self::fetch_manifest( $wp_version, $locale );
        if ( is_wp_error( $manifest ) ) {
            return array( 'ok' => false, 'error' => $manifest->get_error_message() );
        }

        $modified = array(); $missing = array(); $extra = array();
        $checked  = 0;

        foreach ( $manifest as $rel => $expected_md5 ) {
            $checked++;
            $full = ABSPATH . $rel;
            if ( ! file_exists( $full ) ) {
                // Skip the langs/ tree — WP-Languages werden dynamisch nachgeladen
                if ( false !== strpos( $rel, 'wp-content/languages/' ) ) continue;
                $missing[] = $rel;
                continue;
            }
            $actual = @md5_file( $full ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- file passed file_exists() above but may still be unreadable (race/permissions); failure yields false, handled by the !== comparison below.
            if ( $actual !== $expected_md5 ) {
                $modified[] = array( 'path' => $rel, 'expected' => $expected_md5, 'actual' => $actual );
            }
        }

        // Extra-Files scannen — alles in wp-admin/ und wp-includes/ das NICHT im manifest steht
        foreach ( array( 'wp-admin', 'wp-includes' ) as $dir ) {
            $base = ABSPATH . $dir;
            if ( ! is_dir( $base ) ) continue;
            $extra = array_merge( $extra, self::find_extras( $base, ABSPATH, $manifest ) );
        }

        $ok = ( empty( $modified ) && empty( $missing ) && empty( $extra ) );
        return array(
            'ok'      => $ok,
            'version' => $wp_version,
            'locale'  => $locale,
            'summary' => array(
                'modified'      => count( $modified ),
                'missing'       => count( $missing ),
                'extra'         => count( $extra ),
                'total_checked' => $checked,
            ),
            'details' => array(
                'modified' => $modified,
                'missing'  => $missing,
                'extra'    => $extra,
            ),
            'checked_at' => gmdate( 'c' ),
        );
    }

    private static function fetch_manifest( $version, $locale ) {
        $key = self::CACHE_PREFIX . md5( $version . '|' . $locale );
        $cached = get_transient( $key );
        if ( false !== $cached ) return $cached;
        $url = add_query_arg( array( 'version' => $version, 'locale' => $locale ), 'https://api.wordpress.org/core/checksums/1.0/' );
        $r = wp_remote_get( $url, array( 'timeout' => 15 ) );
        if ( is_wp_error( $r ) ) return $r;
        if ( (int) wp_remote_retrieve_response_code( $r ) !== 200 ) return new WP_Error( 'http', 'API returned ' . wp_remote_retrieve_response_code( $r ) );
        $body = json_decode( (string) wp_remote_retrieve_body( $r ), true );
        if ( empty( $body['checksums'] ) || ! is_array( $body['checksums'] ) ) return new WP_Error( 'parse', 'invalid response' );
        set_transient( $key, $body['checksums'], self::CACHE_TTL );
        return $body['checksums'];
    }

    private static function find_extras( $dir, $abspath, $manifest ) {
        $extras = array();
        $iter = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ) );
        foreach ( $iter as $file ) {
            if ( ! $file->isFile() ) continue;
            $rel = ltrim( str_replace( $abspath, '', $file->getPathname() ), '/\\' );
            $rel = str_replace( '\\', '/', $rel );
            // Nur die offiziellen Core-Extensions prüfen (sonst zu viele False-Positives durch user-uploads)
            $ext = strtolower( pathinfo( $rel, PATHINFO_EXTENSION ) );
            if ( ! in_array( $ext, array( 'php', 'js', 'css', 'html', 'htm', 'json' ), true ) ) continue;
            if ( ! isset( $manifest[ $rel ] ) ) {
                $extras[] = $rel;
            }
        }
        return $extras;
    }
}
