<?php

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

/**
 * Plugin Name: LWD Automatic Updates
 * Description: Enables automatic updates for all plugins and themes, with filesystem, object-cache, and clean logging safeguards.
 * Version: 1.2.7
 * LWD Type: mu-plugin
 * LWD Install: existing-only
 * LWD File: lwd-automatic-updates.php
 * Requires PHP: 5.6.0
 * Requires at least: 5.0
 */

/**
 * Allow Elementor and Elementor Pro updates from their upstream providers.
 * Never accept the LWD repository as the download source for these plugins.
 */
function lwd_auto_update_plugin( $update, $item ) {
    $plugin = isset( $item->plugin ) ? (string) $item->plugin : '';
    if ( in_array( $plugin, array( 'elementor/elementor.php', 'elementor-pro/elementor-pro.php' ), true ) ) {
        $package = isset( $item->package ) ? (string) $item->package : '';
        $host = $package ? wp_parse_url( $package, PHP_URL_HOST ) : '';
        if ( is_string( $host ) && 'plugins.linkweb.ca' === strtolower( $host ) ) {
            error_log( '[LWD Automatic Updates] Rejected incorrect Elementor update source for ' . $plugin );
            return false;
        }
    }
    return true;
}
add_filter( 'auto_update_plugin', 'lwd_auto_update_plugin', 10, 2 );

function lwd_auto_update_guard_elementor_source( $reply, $package, $upgrader, $hook_extra ) {
    $plugin = is_array( $hook_extra ) && isset( $hook_extra['plugin'] ) ? (string) $hook_extra['plugin'] : '';
    if ( in_array( $plugin, array( 'elementor/elementor.php', 'elementor-pro/elementor-pro.php' ), true ) ) {
        $host = wp_parse_url( (string) $package, PHP_URL_HOST );
        if ( is_string( $host ) && 'plugins.linkweb.ca' === strtolower( $host ) ) {
            return new WP_Error( 'lwd_invalid_elementor_source', 'Elementor packages cannot be downloaded from the Link Web repository.' );
        }
    }
    return $reply;
}
add_filter( 'upgrader_pre_download', 'lwd_auto_update_guard_elementor_source', 10, 4 );

/**
 * Independent hourly event protects against the core updater event being
 * rescheduled too far in the future. WordPress core handles locking.
 */
function lwd_auto_update_ensure_schedule() {
    if ( ! wp_next_scheduled( 'lwd_auto_update_hourly' ) ) {
        wp_schedule_event( time() + 300, 'hourly', 'lwd_auto_update_hourly' );
    }
}
add_action( 'init', 'lwd_auto_update_ensure_schedule' );
function lwd_auto_update_hourly_callback() {
    do_action( 'wp_maybe_auto_update' );
}
add_action( 'lwd_auto_update_hourly', 'lwd_auto_update_hourly_callback' );

// Automatically update all themes.
add_filter( 'auto_update_theme', '__return_true' );

// Keep routine plugin/theme automatic-update notices internal to LWD.
add_filter( 'auto_plugin_update_send_email', '__return_false' );
add_filter( 'auto_theme_update_send_email', '__return_false' );

/**
 * Prefer direct filesystem access when the WordPress content
 * and plugin directories are writable.
 *
 * This avoids WordPress incorrectly falling back to FTP on
 * environments where direct filesystem access is available.
 *
 * @param string $method
 * @param array  $args
 * @param string $context
 * @param bool   $allow_relaxed_file_ownership
 *
 * @return string
 */
function lwd_filesystem_method( $method, $args, $context, $allow_relaxed_file_ownership ) {

    if (
        defined( 'WP_CONTENT_DIR' ) &&
        defined( 'WP_PLUGIN_DIR' ) &&
        is_dir( WP_CONTENT_DIR ) &&
        is_dir( WP_PLUGIN_DIR ) &&
        is_writable( WP_CONTENT_DIR ) &&
        is_writable( WP_PLUGIN_DIR )
    ) {
        return 'direct';
    }

    return $method;
}

add_filter( 'filesystem_method', 'lwd_filesystem_method', 10, 4 );

/**
 * Prepare WordPress for automatic updates.
 *
 * Clears stale persistent object-cache state and removes an expired
 * automatic-updater lock when necessary.
 */
function lwd_prepare_automatic_updates() {

    $using_object_cache = wp_using_ext_object_cache();

    if ( $using_object_cache ) {
        wp_cache_delete( 'notoptions', 'options' );
        wp_cache_delete( 'auto_updater.lock', 'options' );
    }

    $lock = get_option( 'auto_updater.lock' );

    if ( $lock && ( time() - (int) $lock ) > HOUR_IN_SECONDS ) {
        delete_option( 'auto_updater.lock' );

        if ( $using_object_cache ) {
            wp_cache_delete( 'auto_updater.lock', 'options' );
            wp_cache_delete( 'notoptions', 'options' );
        }
    }
}

add_action( 'wp_maybe_auto_update', 'lwd_prepare_automatic_updates', 1 );

/**
 * Determine whether a captured PHP error-log line is only routine successful
 * WordPress automatic-updater progress.
 *
 * @param string $line Captured error-log line.
 *
 * @return bool
 */
function lwd_auto_update_is_routine_log_line( $line ) {
    $line = trim( (string) $line );
    $line = preg_replace( '/^\[[^\]]+\]\s*/', '', $line );

    if ( '' === $line || "'" === $line ) {
        return true;
    }

    $exact = array(
        'Automatic updates starting...',
        'Automatic updates complete.',
        'Automatic plugin updates starting...',
        'Automatic plugin updates complete.',
        'Automatic theme updates starting...',
        'Automatic theme updates complete.',
        'Automatic translation updates starting...',
        'Automatic translation updates complete.',
        'Scraping home page...',
        'true',
    );

    if ( in_array( trim( $line ), $exact, true ) ) {
        return true;
    }

    if ( preg_match( "/^Upgrading (?:plugin|theme|translation) '.+'\.\.\.$/", $line ) ) {
        return true;
    }

    if ( preg_match( "/^(?:Plugin|Theme|Translation) '.+' has been upgraded\.$/", $line ) ) {
        return true;
    }

    if ( preg_match( "/^The update for '.+' has no fatal errors\.$/", $line ) ) {
        return true;
    }

    return false;
}

/**
 * Restore the original PHP error log and replay only meaningful updater-time
 * entries. Real PHP warnings/errors/fatals and unknown messages are preserved.
 */
function lwd_finish_automatic_update_logging() {
    if ( empty( $GLOBALS['lwd_auto_update_log_state'] ) || ! is_array( $GLOBALS['lwd_auto_update_log_state'] ) ) {
        return;
    }

    $state = $GLOBALS['lwd_auto_update_log_state'];

    if ( empty( $state['active'] ) ) {
        return;
    }

    $GLOBALS['lwd_auto_update_log_state']['active'] = false;

    if ( isset( $state['original_error_log'] ) ) {
        @ini_set( 'error_log', (string) $state['original_error_log'] );
    } else {
        @ini_restore( 'error_log' );
    }

    $temp_log = isset( $state['temp_log'] ) ? (string) $state['temp_log'] : '';

    if ( '' === $temp_log || ! is_file( $temp_log ) || ! is_readable( $temp_log ) ) {
        return;
    }

    $lines = @file( $temp_log, FILE_IGNORE_NEW_LINES );
    @unlink( $temp_log );

    if ( ! is_array( $lines ) || empty( $lines ) ) {
        return;
    }

    $inside_scrape       = false;
    $scrape_lines        = array();
    $skip_closing_quote  = false;

    foreach ( $lines as $line ) {
        $plain = preg_replace( '/^\[[^\]]+\]\s*/', '', trim( (string) $line ) );

        if ( false !== strpos( $plain, '###### wp_scraping_result_start:' ) ) {
            $inside_scrape = true;
            $scrape_lines  = array();
            continue;
        }

        if ( $inside_scrape ) {
            if ( false !== strpos( $plain, '###### wp_scraping_result_end:' ) ) {
                $inside_scrape = false;
                $skip_closing_quote = true;

                /*
                 * WordPress logs "true" when the post-update loopback found no
                 * fatal error. Suppress only that successful result. If the
                 * scrape payload contains anything else, replay it because it
                 * may be the fatal-error details we need to investigate.
                 */
                $meaningful_scrape = array();
                foreach ( $scrape_lines as $scrape_line ) {
                    $scrape_line = trim( (string) $scrape_line );
                    if ( '' !== $scrape_line && "'" !== $scrape_line ) {
                        $meaningful_scrape[] = $scrape_line;
                    }
                }

                if ( 1 !== count( $meaningful_scrape ) || 'true' !== $meaningful_scrape[0] ) {
                    foreach ( $meaningful_scrape as $scrape_line ) {
                        error_log( $scrape_line );
                    }
                }

                $scrape_lines = array();
                continue;
            }

            $scrape_lines[] = $plain;
            continue;
        }

        if ( $skip_closing_quote && "'" === trim( $plain ) ) {
            $skip_closing_quote = false;
            continue;
        }

        $skip_closing_quote = false;

        if ( lwd_auto_update_is_routine_log_line( $plain ) ) {
            continue;
        }

        error_log( $plain );
    }

    /*
     * A malformed/truncated scrape block should never be silently discarded.
     */
    if ( $inside_scrape && ! empty( $scrape_lines ) ) {
        foreach ( $scrape_lines as $scrape_line ) {
            $scrape_line = trim( (string) $scrape_line );
            if ( '' !== $scrape_line && "'" !== $scrape_line && 'true' !== $scrape_line ) {
                error_log( $scrape_line );
            }
        }
    }
}

/**
 * Run WordPress's normal automatic updater while isolating only its error-log
 * output. WordPress core emits successful progress through error_log() when
 * WP_DEBUG and WP_DEBUG_LOG are enabled; those routine entries should not
 * trigger the server PHP error-alert workflow.
 */
function lwd_run_automatic_updates_with_clean_logging() {
    $original_error_log = ini_get( 'error_log' );
    $temp_dir = function_exists( 'get_temp_dir' ) ? get_temp_dir() : WP_CONTENT_DIR . '/';
    $temp_log = trailingslashit( $temp_dir ) . 'lwd-auto-update-' . getmypid() . '-' . wp_generate_password( 8, false, false ) . '.log';

    $GLOBALS['lwd_auto_update_log_state'] = array(
        'active'             => false,
        'original_error_log' => $original_error_log,
        'temp_log'           => $temp_log,
    );

    if ( false === @ini_set( 'error_log', $temp_log ) ) {
        wp_maybe_auto_update();
        return;
    }

    $GLOBALS['lwd_auto_update_log_state']['active'] = true;
    register_shutdown_function( 'lwd_finish_automatic_update_logging' );

    wp_maybe_auto_update();

    lwd_finish_automatic_update_logging();
}

/*
 * Replace only WordPress's automatic-updater callback. The actual updater
 * implementation remains WordPress core and all normal updater hooks still run.
 */
remove_action( 'wp_maybe_auto_update', 'wp_maybe_auto_update' );
add_action( 'wp_maybe_auto_update', 'lwd_run_automatic_updates_with_clean_logging', 10 );
