HEX
Server: Apache
System: Linux p102.lithium.hosting 4.18.0-553.141.1.el8_10.x86_64 #1 SMP Fri Jul 10 17:48:02 UTC 2026 x86_64
User: bvzmoamr (9955)
PHP: 8.1.34
Disabled: syslog
Upload Files
File: //proc/self/cwd/wp-content/plugins/mainwp-child/class/class-mainwp-child-patchstack.php
<?php
/**
 * MainWP Patchstack
 *
 * MainWP Patchstack extension handler.
 * Extension URL: https://mainwp.com/extension/patchstack/
 *
 * @package MainWP\Child
 */

namespace MainWP\Child;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// phpcs:disable PSR1.Classes.ClassDeclaration, WordPress.WP.AlternativeFunctions -- required to achieve desired results, pull request solutions appreciated.

/**
 * Class MainWP_Child_Patchstack
 *
 * MainWP Patchstack extension handler
 */
class MainWP_Child_Patchstack { //phpcs:ignore -- NOSONAR - multi methods.

    /**
     * Public static variable to hold the single instance of the class.
     *
     * @var mixed Default null
     */
    public static $instance = null;

    /**
     * Patchstack install status. True if the plugin is installed on the child site.
     *
     * @var bool If Patchstack plugin installed, return true, if not, return false.
     */
    protected $is_plugin_installed = false;

    /**
     * The plugin slug.
     *
     * @var string slug string.
     */
    protected $the_plugin_slug = 'patchstack/patchstack.php';

    /**
     * API URL of Patchstack to communicate with.
     *
     * @var   string API URL.
     */
    protected $api_url = 'https://api.patchstack.com/monitor';

    /**
     * Option name to hide the Patchstack Insights plugin.
     *
     * @var string Option name.
     */
    protected $option_hide_name = 'mainwp_patchstack_hide_plugin';

    /**
     * Whitelist of allowed sanitization functions.
     *
     * @var array $allowed_callbacks allowed sanitization functions.
     */
    protected static $allowed_callbacks = array(
        'sanitize_text_field',
        'sanitize_textarea_field',
        'sanitize_email',
        'sanitize_url',
        'intval',
        'absint',
        'wp_kses_post',
    );

    /**
     * Method instance()
     *
     * Create a public static instance.
     *
     * @return mixed Class instance.
     */
    public static function instance() {
        if ( null === static::$instance ) {
            static::$instance = new self();
        }

        return static::$instance;
    }

    /**
     * MainWP_Child_Patchstack constructor.
     *
     * Run any time class is called.
     */
    public function __construct() {
        $this->is_plugin_installed = is_plugin_active( $this->the_plugin_slug );
    }

    /**
     * Method init()
     *
     * Initiate action hooks.
     *
     * @return void
     */
    public function init() {
        add_filter( 'mainwp_site_sync_others_data', array( $this, 'sync_others_data' ), 10, 2 );
        if ( get_option( $this->option_hide_name ) === 'hide' ) {
            add_filter( 'all_plugins', array( $this, 'all_plugins' ) );
            add_action( 'admin_menu', array( $this, 'remove_menu' ) );
        }
    }

    /**
     * Method actions()
     *
     * Fire off certain Patchstack Insights plugin actions.
     *
     * @uses \MainWP\Child\MainWP_Child_Patchstack::save_settings() Save the plugin settings.
     * @uses \MainWP\Child\MainWP_Child_Patchstack::set_showhide() Hide or unhide the Patchstack Insights plugin.
     * @uses \MainWP\Child\MainWP_Child_Patchstack::install_plugin() Get the Patchstack Insights plugin data and store it in the sync request.
     * @uses \MainWP\Child\MainWP_Helper::write()
     */
    public function action() {
        $information = array();
        $mwp_action  = MainWP_System::instance()->validate_params( 'action' );
        if ( ! empty( $mwp_action ) ) {
            switch ( $mwp_action ) {
                case 'install_plugin':
                    $information = $this->install_plugin();
                    break;
                case 'sync_data':
                    $information = $this->sync_data();
                    break;
                case 'show_hide':
                    $information = $this->set_showhide();
                    break;
                case 'save_settings':
                default:
                    $information = $this->save_settings();
                    break;
            }
        }

        MainWP_Helper::write( $information );
    }

    /**
     * Hide BackWPup Plugin from the WordPress Installed plugin list.
     *
     * @param array $plugins Installed plugins.
     * @return array $plugins Installed plugins without BackWPup Plugin on the list.
     */
    public function all_plugins( $plugins ) {
        foreach ( $plugins as $key => $value ) {
            $plugin_slug = basename( $key, '.php' );
            if ( 'patchstack' === $plugin_slug ) {
                unset( $plugins[ $key ] );
            }
        }

        return $plugins;
    }
    /**
     * Remove Patchstack Plugin from the WordPress Admin.
     */
    public function remove_menu() {
        // Remove patchstack from the admin menu.
        remove_menu_page( 'options-general.php?page=patchstack' );
        global $submenu;

        // Remove the WordPress Admin SubMenu.
        if ( isset( $submenu['patchstack'] ) ) {
            unset( $submenu['patchstack'] );
        }

        if ( isset( $submenu['options-general.php'] ) ) {
            foreach ( $submenu['options-general.php'] as $index => $item ) {
                if ( 'patchstack' === $item[2] ) {
                    unset( $submenu['options-general.php'][ $index ] );
                    break;
                }
            }
        }

        $pos = isset( $_SERVER['REQUEST_URI'] ) ? stripos( wp_unslash( $_SERVER['REQUEST_URI'] ), 'options-general.php?page=patchstack' ) : false;
        if ( false !== $pos ) {
            wp_safe_redirect( get_option( 'siteurl' ) . '/wp-admin/index.php' );
            exit();
        }
    }

    /**
     * Method sync_others_data()
     *
     * Sync the Patchstack plugin settings.
     *
     * @param  array $information Array containing the sync information.
     * @param  array $data        Array containing the Patchstack plugin data to be synced.
     *
     * @return array $information Array containing the sync information.
     */
    public function sync_others_data( $information, $data = array() ) {
        if ( isset( $data['sync_patchstack_data'] ) && ( 'yes' === $data['sync_patchstack_data'] ) ) {
            try {
                $data['plugin_hide_status']          = get_site_option( $this->option_hide_name, false );
                $information['sync_patchstack_data'] = $data;
            } catch ( MainWP_Exception $e ) {
                // ok!
            }
        }
        return $information;
    }

    /**
     * Method install_plugin()
     *
     * Get the Patchstack Insights plugin data and store it in the sync request.
     *
     * @return array $information Array containing the sync information.
     */
    /**
     * Install (overwrite if exists) and activate the Patchstack plugin.
     *
     * @return array|\WP_Error
     */
    private function install_plugin() {  // phpcs:ignore -- NOSONAR -- complexity
        // Read settings.
        $raw_settings = $this->sanitized_post( 'settings' );
        $settings     = json_decode( $raw_settings, true );

        if ( json_last_error() !== JSON_ERROR_NONE ) {
            return new \WP_Error( 'json_decode_error', 'Invalid JSON in settings: ' . json_last_error_msg() );
        }

        if ( empty( $settings['ps_id'] ) || empty( $settings['token'] ) ) {
            return new \WP_Error( 'bad_params', 'Missing ps_id or token.' );
        }

        // Call endpoint returns FILE (ZIP), not JSON.
        $url    = '/download/wordpress/' . intval( $settings['ps_id'] );
        $binary = $this->send_request( $url, $settings['token'], 'GET', array(), false );

        if ( is_wp_error( $binary ) ) {
            return $binary;
        }
        if ( is_array( $binary ) ) {
            // API should return file; if it returns JSON then it is a business error.
            return new \WP_Error( 'api_error', 'Unexpected JSON for download endpoint.', $binary );
        }
        if ( ! is_string( $binary ) || '' === $binary ) {
            return new \WP_Error( 'empty_file', 'Empty plugin file.' );
        }

        require_once ABSPATH . 'wp-admin/includes/plugin.php';  // phpcs:ignore -- NOSONAR
        require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';  // phpcs:ignore -- NOSONAR
        require_once ABSPATH . 'wp-admin/includes/file.php';  // phpcs:ignore -- NOSONAR
        require_once ABSPATH . 'wp-admin/includes/misc.php';  // phpcs:ignore -- NOSONAR

        // Write ZIP to temporary file.
        $tmp = wp_tempnam( 'patchstack.zip' );
        if ( ! $tmp ) {
            return new \WP_Error( 'tmp_fail', 'Failed to create temp file.' );
        }

        $bytes = @file_put_contents( $tmp, $binary, LOCK_EX ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
        if ( false === $bytes || 0 === $bytes ) {
            @unlink( $tmp ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
            return new \WP_Error( 'write_fail', 'Failed to write plugin ZIP to temp file.' );
        }

        // Check slug.
        if ( empty( $this->the_plugin_slug ) || strpos( $this->the_plugin_slug, '/' ) === false ) {
            return new \WP_Error( 'bad_slug', 'Invalid plugin slug (expected "patchstack/patchstack.php").' );
        }

        // Deactivate if active.
        $was_active = function_exists( 'is_plugin_active' ) ? is_plugin_active( $this->the_plugin_slug ) : false;
        if ( $was_active ) {
            deactivate_plugins( $this->the_plugin_slug, true );
            if ( function_exists( 'is_plugin_active' ) && is_plugin_active( $this->the_plugin_slug ) ) {
                return new \WP_Error( 'deactivate_failed', 'Failed to deactivate existing plugin.' );
            }
        }

        // Delete plugin if exists (file or folder).
        $plugin_dir = WP_PLUGIN_DIR . '/' . dirname( $this->the_plugin_slug );
        if ( file_exists( WP_PLUGIN_DIR . '/' . $this->the_plugin_slug ) || is_dir( $plugin_dir ) ) {
            // delete_plugins() will try to deactivate if needed and remove files.
            $deleted = delete_plugins( array( $this->the_plugin_slug ) );
            if ( is_wp_error( $deleted ) ) {
                return new \WP_Error(
                    'delete_failed',
                    'Failed to delete existing plugin folder.',
                    array( 'error' => $deleted->get_error_message() )
                );
            }
            // In some envs a residual folder may remain; hard-delete with WP_Filesystem.
            if ( is_dir( $plugin_dir ) ) {
                global $wp_filesystem;
                if ( ! $wp_filesystem ) {
                    WP_Filesystem();
                }
                if ( $wp_filesystem && $wp_filesystem->is_dir( $plugin_dir ) ) {
                    $wp_filesystem->delete( $plugin_dir, true );
                }
                if ( is_dir( $plugin_dir ) ) {
                    return new \WP_Error( 'delete_residual_failed', 'Plugin directory still exists after deletion.' );
                }
            }
        }

        // Install/overwrite with Plugin_Upgrader (WordPress core standard).
        $skin     = new \Automatic_Upgrader_Skin();
        $upgrader = new \Plugin_Upgrader( $skin );

        $options_filter = static function ( $options ) {
            $options['clear_destination']           = true;
            $options['abort_if_destination_exists'] = false;
            return $options;
        };
        add_filter( 'upgrader_package_options', $options_filter );

        try {
            $installed = $upgrader->install( $tmp );
        } finally {
            remove_filter( 'upgrader_package_options', $options_filter );
            @unlink( $tmp ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
        }

        if ( is_wp_error( $installed ) ) {
            return $installed;
        }
        if ( ! $installed ) {
            return new \WP_Error( 'install_failed', 'Plugin installation failed.' );
        }

        // Check plugin file.
        $plugin_file = '';
        if ( is_array( $upgrader->result ?? null ) ) {
            if ( ! empty( $upgrader->result['plugin'] ) ) {
                $plugin_file = $upgrader->result['plugin'];
            } elseif ( ! empty( $upgrader->result['destination'] ) ) {
                $dest_dir = trailingslashit( $upgrader->result['destination'] );
                $all      = get_plugins();
                foreach ( $all as $rel => $headers ) {
                    if ( strpos( WP_PLUGIN_DIR . '/' . $rel, $dest_dir ) === 0 ) {
                        $plugin_file = $rel;
                        break;
                    }
                }
            }
        }

        if ( ! $plugin_file && ! empty( $this->the_plugin_slug ) && file_exists( WP_PLUGIN_DIR . '/' . $this->the_plugin_slug ) ) {
            $plugin_file = $this->the_plugin_slug;
        }

        if ( ! $plugin_file ) {
            return new \WP_Error( 'main_file_missing', 'Installed but plugin main file not found.' );
        }

        return array(
            'success'     => 1,
            'is_active'   => is_plugin_active( $this->the_plugin_slug ),
            'plugin_file' => $this->the_plugin_slug,
            'message'     => $message,
        );
    }

    /**
     * Method set_showhide()
     *
     * Hide or unhide the Patchstack Insights plugin.
     *
     * @return array $information Array containing the sync information.
     */
    private function set_showhide() {
        $raw  = $this->sanitized_post( 'show_hide' );
        $hide = ( 'hide' === sanitize_text_field( $raw ) ) ? 'hide' : '';
        update_site_option( $this->option_hide_name, $hide );

        return array( 'success' => 1 );
    }

    /**
     * Summary of sync_data
     *
     * @return array{data: mixed, success: int|array{error: string}}`
     */
    private function sync_data() {
        $oauth = $this->sanitized_post( 'oauth' );
        if ( empty( $oauth ) ) {
            return array( 'error' => 'Missing oauth data.' );
        }
        $response = wp_remote_post(
            admin_url( 'admin-ajax.php' ),
            array(
                'timeout'   => 30,
                'sslverify' => false,
                'body'      => array(
                    'action' => 'patchstack_activate_license',
                    'key'    => $oauth,
                ),
            )
        );

        $body = wp_remote_retrieve_body( $response );
        $data = json_decode( $body, true );
        return array(
            'success' => 1,
            'data'    => $data,
        );
    }

    /**
     * Method save_settings()
     *
     * Save the Patchstack Insights plugin settings.
     *
     * @return array $information Array containing the sync information.
     */
    private function save_settings() {
        return array( 'success' => 1 );
    }

    /**
     * Send an HTTP request to the API and return JSON (array) or raw string (binary).
     *
     * @param string            $url         API endpoint (e.g. '/site/plugin/resync/123').
     * @param string            $token       API token.
     * @param string            $method      HTTP method. Default 'GET'.
     * @param array|string|null $data        Data for non-GET methods (auto JSON-encoded if array).
     * @param bool              $expect_json Expect JSON response (true) or raw/binary (false).
     *
     * @return array|string|\WP_Error JSON array, raw string, or WP_Error on failure.
     */
    private function send_request( $url, $token, $method = 'GET', $data = array(), $expect_json = true ) {  // phpcs:ignore -- NOSONAR
        if ( empty( $token ) ) {
            return new \WP_Error( 'no_token', 'Missing API token.' );
        }

        $method = strtoupper( $method );
        $args   = array(
            'method'      => $method,
            'timeout'     => 90,
            'redirection' => 5,
            'blocking'    => true,
            'headers'     => array(
                'UserToken' => $token,
                'Accept'    => $expect_json ? 'application/json' : '*/*', // NOSONAR.
            ),
        );

        // Only send body when not GET/HEAD.
        if ( ! empty( $data ) && ! in_array( $method, array( 'GET', 'HEAD' ), true ) ) {
            if ( is_array( $data ) ) {
                $args['headers']['Content-Type'] = 'application/json';
                $args['body']                    = wp_json_encode( $data );
            } else {
                $args['body'] = (string) $data;
            }
        }

        $response = wp_remote_request( trailingslashit( $this->api_url ) . ltrim( $url, '/' ), $args );
        if ( is_wp_error( $response ) ) {
            return $response;
        }

        $code  = (int) wp_remote_retrieve_response_code( $response );
        $body  = wp_remote_retrieve_body( $response );
        $ctype = (string) wp_remote_retrieve_header( $response, 'content-type' );

        if ( 200 !== $code ) {
            // If the server returns an error JSON → try to parse it to get specific information.
            $message = "HTTP $code";
            if ( stripos( $ctype, 'application/json' ) !== false ) {
                $json = json_decode( $body, true );
                if ( is_array( $json ) ) {
                    $message = $json['error'] ?? $json['message'] ?? $message;
                }
            }
            return new \WP_Error(
                'http_error',
                $message,
                array(
                    'status' => $code,
                    'body'   => $body,
                    'ctype'  => $ctype,
                )
            );
        }

        if ( $expect_json || stripos( $ctype, 'application/json' ) !== false ) {
            if ( '' === $body ) {
                return array();
            }
            $decoded = json_decode( $body, true );
            if ( json_last_error() !== JSON_ERROR_NONE ) {
                return new \WP_Error(
                    'json_decode',
                    'Invalid JSON: ' . json_last_error_msg(),
                    array(
                        'body' => $body,
                    )
                );
            }
            return $decoded;
        }

        return $body;
    }

    /**
     * Method sanitized_post()
     *
     * Sanitized post field.
     *
     * @param string $key key to get from POST.
     * @param string $callback cleaning method.
     * @param mixed  $default_value Default return value.
     *
     * @return mixed data value.
     */
    private static function sanitized_post( $key, $callback = 'sanitize_text_field', $default_value = '' ) {
        if ( ! in_array( $callback, self::$allowed_callbacks, true ) ) {
            $callback = 'sanitize_text_field';
        }

        return isset( $_POST[ $key ] ) ? $callback( wp_unslash( $_POST[ $key ] ) ) : $default_value; // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
    }
}