<?php

namespace WP_CLI\Core;

use Exception;
use WP_CLI;
use WP_CLI\Utils;
use WP_Error;
use Core_Upgrader as DefaultCoreUpgrader;
use WP_Filesystem_Base;

/**
 * A Core Upgrader class that caches the download, and uses cached if available.
 *
 * @package wp-cli
 */
class CoreUpgrader extends DefaultCoreUpgrader {

	/**
	 * Whether to retry without certificate validation on TLS handshake failure.
	 *
	 * @var bool
	 */
	private $insecure;

	/**
	 * CoreUpgrader constructor.
	 *
	 * @param \WP_Upgrader_Skin|null $skin
	 * @param bool                   $insecure
	 */
	public function __construct( $skin = null, $insecure = false ) {
		$this->insecure = $insecure;
		parent::__construct( $skin );
	}

	/**
	 * Caches the download, and uses cached if available.
	 *
	 * @access public
	 *
	 * @param string $package          The URI of the package. If this is the full path to an
	 *                                 existing local file, it will be returned untouched.
	 * @param bool   $check_signatures Whether to validate file signatures. Default false.
	 * @param array  $hook_extra       Extra arguments to pass to the filter hooks. Default empty array.
	 * @return string|WP_Error The full path to the downloaded package file, or a WP_Error object.
	 */
	public function download_package( $package, $check_signatures = false, $hook_extra = [] ) {

		/**
		 * Filter whether to return the package.
		 *
		 * @since 3.7.0
		 * @since 5.5.0 Added the `$hook_extra` parameter.
		 *
		 * @param bool         $reply      Whether to bail without returning the package. Default is false.
		 * @param string       $package    The package file name.
		 * @param \WP_Upgrader $upgrader   The WP_Upgrader instance.
		 * @param array        $hook_extra Extra arguments passed to hooked filters.
		 */
		$reply = apply_filters(
			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Override existing hook from Core.
			'upgrader_pre_download',
			false,
			$package,
			$this,
			$hook_extra
		);

		/**
		 * @var false|string|\WP_Error $reply
		 */

		if ( false !== $reply ) {
			return $reply;
		}

		// Check if package is a local or remote file. Bail if it's local.
		if ( ! preg_match( '!^(http|https|ftp)://!i', $package ) && file_exists( $package ) ) {
			return $package;
		}

		if ( empty( $package ) ) {
			return new WP_Error( 'no_package', $this->strings['no_package'] );
		}

		$filename  = pathinfo( $package, PATHINFO_FILENAME );
		$extension = pathinfo( $package, PATHINFO_EXTENSION );

		$temp = Utils\get_temp_dir() . uniqid( 'wp_' ) . ".{$extension}";
		register_shutdown_function(
			function () use ( $temp ) {
				if ( file_exists( $temp ) ) {
					unlink( $temp );
				}
			}
		);

		$cache     = WP_CLI::get_cache();
		$update    = $GLOBALS['wpcli_core_update_obj'];
		$cache_key = "core/{$filename}-{$update->locale}.{$extension}";

		/**
		 * @var false|string $cache_file
		 */
		$cache_file = $cache->has( $cache_key );

		if ( $cache_file && false === stripos( $package, 'https://wordpress.org/nightly-builds/' )
			&& false === stripos( $package, 'http://wordpress.org/nightly-builds/' ) ) {
			WP_CLI::log( "Using cached file '{$cache_file}'..." );
			copy( $cache_file, $temp );
			return $temp;
		}

		/*
		 * Download to a temporary file because piping from cURL to tar is flaky
		 * on MinGW (and probably in other environments too).
		 */
		$headers = [ 'Accept' => 'application/json' ];
		$options = [
			'timeout'       => 600,  // 10 minutes ought to be enough for everybody.
			'filename'      => $temp,
			'halt_on_error' => false,
			'insecure'      => $this->insecure,
		];

		$this->skin->feedback( 'downloading_package', $package );

		try {
			/** @var \WpOrg\Requests\Response $response */
			$response = Utils\http_request( 'GET', $package, null, $headers, $options );
		} catch ( Exception $e ) {
			return new WP_Error( 'download_failed', $e->getMessage() );
		}

		if ( ! is_null( $response ) && 200 !== (int) $response->status_code ) {
			return new WP_Error( 'download_failed', $this->strings['download_failed'] );
		}

		if ( false === stripos( $package, 'https://wordpress.org/nightly-builds/' ) ) {
			$cache->import( $cache_key, $temp );
		}

		return $temp;
	}

	/**
	 * Upgrade WordPress core.
	 *
	 * @access public
	 *
	 * @global WP_Filesystem_Base $wp_filesystem Subclass
	 * @global callable           $_wp_filesystem_direct_method
	 *
	 * @param object $current Response object for whether WordPress is current.
	 * @param array  $args {
	 *        Optional. Arguments for upgrading WordPress core. Default empty array.
	 *
	 *        @type bool $pre_check_md5    Whether to check the file checksums before
	 *                                     attempting the upgrade. Default true.
	 *        @type bool $attempt_rollback Whether to attempt to rollback the chances if
	 *                                     there is a problem. Default false.
	 *        @type bool $do_rollback      Whether to perform this "upgrade" as a rollback.
	 *                                     Default false.
	 * }
	 * @return string|false|WP_Error New WordPress version on success, false or WP_Error on failure.
	 */
	public function upgrade( $current, $args = [] ) {
		set_error_handler( [ __CLASS__, 'error_handler' ], E_USER_WARNING | E_USER_NOTICE );

		$result = parent::upgrade( $current, $args );

		restore_error_handler();

		return $result;
	}

	/**
	 * Error handler to ignore failures on accessing SSL "https://api.wordpress.org/core/checksums/1.0/" in `get_core_checksums()` which seem to occur intermittently.
	 */
	public static function error_handler( $errno, $errstr, $errfile, $errline, $errcontext = null ) {
		// If ignoring E_USER_WARNING | E_USER_NOTICE, default.
		if ( ! ( error_reporting() & $errno ) ) {
			return false;
		}
		// If not in "wp-admin/includes/update.php", default.
		$update_php = 'wp-admin/includes/update.php';
		if ( 0 !== substr_compare( $errfile, $update_php, -strlen( $update_php ) ) ) {
			return false;
		}
		// Else assume it's in `get_core_checksums()` and just ignore it.
		return true;
	}
}
