Connect.php 13.3 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472
<?php
namespace AIOSEO\Plugin\Lite\Admin;

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

use AIOSEO\Plugin\Common\Utils;

/**
 * Connect to AIOSEO Pro Worker Service to connect with our Premium Services.
 *
 * @since 4.0.0
 */
class Connect {
	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'wp_ajax_nopriv_aioseo_connect_process', [ $this, 'process' ] );

		add_action( 'admin_menu', [ $this, 'addDashboardPage' ] );
		add_action( 'admin_init', [ $this, 'maybeLoadConnect' ] );

		// Create the plugin upgrader with our custom skin.
		$this->installer = new Utils\PluginUpgraderSilentAjax( new Utils\PluginUpgraderSkin() );
	}

	/**
	 * Adds a dashboard page for our setup wizard.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addDashboardPage() {
		add_dashboard_page( '', '', 'aioseo_manage_seo', 'aioseo-connect-pro', '' );
		remove_submenu_page( 'index.php', 'aioseo-connect-pro' );
		add_dashboard_page( '', '', 'aioseo_manage_seo', 'aioseo-connect', '' );
		remove_submenu_page( 'index.php', 'aioseo-connect' );
	}

	/**
	 * Checks to see if we should load the setup wizard.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function maybeLoadConnect() {
		// Don't load the interface if doing an ajax call.
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
			return;
		}

		// Check for connect-specific parameter
		// Allow plugins to disable the connect
		// Check if current user is allowed to save settings.
		if (
			! isset( $_GET['page'] ) ||
			( 'aioseo-connect-pro' !== $_GET['page'] && 'aioseo-connect' !== wp_unslash( $_GET['page'] ) ) || // phpcs:ignore HM.Security.ValidatedSanitizedInput.InputNotSanitized
			! current_user_can( 'aioseo_manage_seo' )
		) {
			return;
		}

		set_current_screen();

		// Remove an action in the Gutenberg plugin ( not core Gutenberg ) which throws an error.
		remove_action( 'admin_print_styles', 'gutenberg_block_editor_admin_print_styles' );

		if ( 'aioseo-connect-pro' === wp_unslash( $_GET['page'] ) ) { // phpcs:ignore HM.Security.ValidatedSanitizedInput.InputNotSanitized
			$this->loadConnectPro();

			return;
		}

		$this->loadConnect();
	}

	/**
	 * Load the Connect template.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function loadConnect() {
		$this->enqueueScripts();
		$this->connectHeader();
		$this->connectContent();
		$this->connectFooter();
		exit;
	}

	/**
	 * Load the Connect Pro template.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function loadConnectPro() {
		$this->enqueueScriptsPro();
		$this->connectHeader();
		$this->connectContent();
		$this->connectFooter( 'pro' );
		exit;
	}

	/**
	 * Enqueue's scripts for the setup wizard.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function enqueueScripts() {
		// We don't want any plugin adding notices to our screens. Let's clear them out here.
		remove_all_actions( 'admin_notices' );
		remove_all_actions( 'all_admin_notices' );

		// Scripts.
		aioseo()->helpers->enqueueScript(
			'aioseo-vendors',
			'js/chunk-vendors.js'
		);
		aioseo()->helpers->enqueueScript(
			'aioseo-common',
			'js/chunk-common.js'
		);
		aioseo()->helpers->enqueueScript(
			'aioseo-connect-script',
			'js/connect.js'
		);

		// Styles.
		$rtl = is_rtl() ? '.rtl' : '';
		aioseo()->helpers->enqueueStyle(
			'aioseo-vendors',
			"css/chunk-vendors$rtl.css"
		);
		aioseo()->helpers->enqueueStyle(
			'aioseo-common',
			"css/chunk-common$rtl.css"
		);
		// aioseo()->helpers->enqueueStyle(
		//  'aioseo-connect-style',
		//  "css/connect$rtl.css"
		// );
		// aioseo()->helpers->enqueueStyle(
		//  'aioseo-connect-vendors-style',
		//  "css/chunk-connect-vendors$rtl.css"
		// );

		wp_localize_script(
			'aioseo-connect-script',
			'aioseo',
			aioseo()->helpers->getVueData()
		);
	}

	/**
	 * Enqueue's scripts for the setup wizard.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function enqueueScriptsPro() {
		// We don't want any plugin adding notices to our screens. Let's clear them out here.
		remove_all_actions( 'admin_notices' );
		remove_all_actions( 'all_admin_notices' );

		// Scripts.
		aioseo()->helpers->enqueueScript(
			'aioseo-vendors',
			'js/chunk-vendors.js'
		);
		aioseo()->helpers->enqueueScript(
			'aioseo-common',
			'js/chunk-common.js'
		);
		aioseo()->helpers->enqueueScript(
			'aioseo-connect-pro-script',
			'js/connect-pro.js'
		);

		// Styles.
		$rtl = is_rtl() ? '.rtl' : '';
		aioseo()->helpers->enqueueStyle(
			'aioseo-vendors',
			"css/chunk-vendors$rtl.css"
		);
		aioseo()->helpers->enqueueStyle(
			'aioseo-common',
			"css/chunk-common$rtl.css"
		);
		// aioseo()->helpers->enqueueStyle(
		//  'aioseo-connect-pro-style',
		//  "css/connect-pro$rtl.css"
		// );
		// aioseo()->helpers->enqueueStyle(
		//  'aioseo-connect-pro-vendors-style',
		//  "css/chunk-connect-pro-vendors$rtl.css"
		// );

		wp_localize_script(
			'aioseo-connect-pro-script',
			'aioseo',
			aioseo()->helpers->getVueData()
		);
	}

	/**
	 * Outputs the simplified header used for the Onboarding Wizard.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function connectHeader() {
		?>
		<!DOCTYPE html>
		<html <?php language_attributes(); ?>>
		<head>
			<meta name="viewport" content="width=device-width"/>
			<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
			<title>
			<?php
				// Translators: 1 - The plugin name ("All in One SEO").
				echo sprintf( esc_html__( '%1$s &rsaquo; Connect', 'all-in-one-seo-pack' ), esc_html( AIOSEO_PLUGIN_NAME ) );
			?>
			</title>
			<?php do_action( 'admin_print_scripts' ); ?>
			<?php do_action( 'admin_print_styles' ); ?>
			<?php do_action( 'admin_head' ); ?>
		</head>
		<body class="aioseo-connect">
		<?php
	}

	/**
	 * Outputs the content of the current step.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function connectContent() {
		echo '<div id="aioseo-app"></div>';
	}

	/**
	 * Outputs the simplified footer used for the Onboarding Wizard.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function connectFooter( $pro = '' ) {
		?>
		<?php
		wp_print_scripts( 'aioseo-vendors' );
		wp_print_scripts( 'aioseo-common' );
		wp_print_scripts( "aioseo-connect-$pro-script" );
		?>
		</body>
		</html>
		<?php
	}

	/**
	 * Generates and returns the AIOSEO Connect URL.
	 *
	 * @since 4.0.0
	 *
	 * @return array The AIOSEO Connect URL or an error message inside an array.
	 */
	public function generateConnectUrl( $key, $redirect = null ) {
		// Check for permissions.
		if ( ! current_user_can( 'install_plugins' ) ) {
			return [
				'error' => esc_html__( 'You are not allowed to install plugins.', 'all-in-one-seo-pack' )
			];
		}

		if ( empty( $key ) ) {
			return [
				'error' => esc_html__( 'Please enter your license key to connect.', 'all-in-one-seo-pack' ),
			];
		}

		// Verify pro version is not installed.
		$active = activate_plugin( 'all-in-one-seo-pack-pro/all_in_one_seo_pack_pro', false, false, true );

		if ( ! is_wp_error( $active ) ) {
			// Deactivate plugin.
			deactivate_plugins( plugin_basename( AIOSEO_FILE ), false, false );

			return [
				'error' => esc_html__( 'Pro version is already installed.', 'all-in-one-seo-pack' )
			];
		}

		// Just check if network is set.
		$network = isset( $_POST['network'] ) ? (bool) wp_unslash( $_POST['network'] ) : false; // phpcs:ignore HM.Security.ValidatedSanitizedInput.InputNotSanitized, HM.Security.NonceVerification.Missing, Generic.Files.LineLength.MaxExceeded
		$network = ! empty( $network );

		// Redirect.
		$token = hash( 'sha512', wp_rand() );

		// Save the options.
		aioseo()->internalOptions->internal->connect->key     = $key;
		aioseo()->internalOptions->internal->connect->time    = time();
		aioseo()->internalOptions->internal->connect->network = $network;
		aioseo()->internalOptions->internal->connect->token   = $token;

		$url = add_query_arg( [
			'key'      => $key,
			'network'  => $network,
			'token'    => $token,
			'version'  => aioseo()->version,
			'siteurl'  => admin_url(),
			'homeurl'  => home_url(),
			'endpoint' => admin_url( 'admin-ajax.php' ),
			'php'      => PHP_VERSION,
			'wp'       => get_bloginfo( 'version' ),
			'redirect' => rawurldecode( base64_encode( $redirect ? $redirect : admin_url( 'admin.php?page=aioseo-settings' ) ) ),
			'v'        => 1,
		], defined( 'AIOSEO_UPGRADE_URL' ) ? AIOSEO_UPGRADE_URL : 'https://upgrade.aioseo.com' );

		// We're storing the ID of the user who is installing Pro so that we can add capabilties for him after upgrading.
		aioseo()->cache->update( 'connect_active_user', get_current_user_id(), 15 * MINUTE_IN_SECONDS );

		return [
			'url' => $url,
		];
	}

	/**
	 * Process AIOSEO Connect.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $downloadUrl The download URL.
	 * @param  string $postToken   The token to validate.
	 * @return array               An array containing a valid response or an error message.
	 */
	public function process() {
		// Verify params present (oth & download link).
		$postToken   = ! empty( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : ''; // phpcs:ignore HM.Security.NonceVerification.Missing
		$downloadUrl = ! empty( $_POST['file'] ) ? esc_url_raw( wp_unslash( $_POST['file'] ) ) : ''; // phpcs:ignore HM.Security.NonceVerification.Missing

		// Translators: 1 - The marketing site anchor name ("aioseo.com").
		$error   = sprintf( esc_html__( 'Could not install upgrade. Please download from %1$s and install manually.', 'all-in-one-seo-pack' ), esc_html( AIOSEO_MARKETING_DOMAIN ) );
		$success = esc_html__( 'Plugin installed & activated.', 'all-in-one-seo-pack' );

		// verify params present (token & download link).
		if ( empty( $downloadUrl ) || empty( $postToken ) ) {
			wp_send_json_error( $error );
		}

		// Verify token.
		$token = aioseo()->internalOptions->internal->connect->token;
		if ( empty( $token ) ) {
			wp_send_json_error( $error );
		}

		// This function has been included in WP Core since 3.9.2. @see: https://developer.wordpress.org/reference/functions/hash_equals/
		if ( ! hash_equals( $token, $postToken ) ) { // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.hash_equalsFound
			wp_send_json_error( $error );
		}

		// Delete connect token so we don't replay.
		aioseo()->internalOptions->internal->connect->token = null;

		// Verify pro not activated.
		if ( aioseo()->pro ) {
			wp_send_json_success( $success );
		}

		// Check license key.
		$licenseKey = aioseo()->internalOptions->internal->connect->key;
		if ( ! $licenseKey ) {
			wp_send_json_error( esc_html__( 'You are not licensed.', 'all-in-one-seo-pack' ) );
		}

		// Set the license key in a new option so we can get it when Pro is activated.
		aioseo()->internalOptions->internal->validLicenseKey = $licenseKey;

		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/class-wp-screen.php';
		require_once ABSPATH . 'wp-admin/includes/screen.php';

		// Set the current screen to avoid undefined notices.
		set_current_screen( 'toplevel_page_aioseo' );

		// Prepare variables.
		$url = esc_url_raw(
			add_query_arg(
				[
					'page' => 'aioseo-settings',
				],
				admin_url( 'admin.php' )
			)
		);

		// Verify pro not installed.
		$network = aioseo()->internalOptions->internal->connect->network;
		$active  = activate_plugin( 'all-in-one-seo-pack-pro/all_in_one_seo_pack.php', $url, $network, true );
		if ( ! is_wp_error( $active ) ) {
			aioseo()->internalOptions->internal->connect->reset();

			// Because the regular activation hooks won't run, we need to add capabilities for the installing user so that he doesn't run into an error on the first request.
			aioseo()->activate->addCapabilitiesOnUpgrade();

			deactivate_plugins( plugin_basename( AIOSEO_FILE ), false, $network );

			wp_send_json_success( $success );
		}

		$creds = request_filesystem_credentials( $url, '', false, false, null );
		// Check for file system permissions.
		if ( false === $creds ) {
			wp_send_json_error( $error );
		}
		if ( ! aioseo()->helpers->wpfs( $creds ) ) {
			wp_send_json_error( $error );
		}

		// Do not allow WordPress to search/download translations, as this will break JS output.
		remove_action( 'upgrader_process_complete', [ 'Language_Pack_Upgrader', 'async_upgrade' ], 20 );

		// Create the plugin upgrader with our custom skin.
		$installer = new Utils\PluginUpgraderSilentAjax( new Utils\PluginUpgraderSkin() );

		// Error check.
		if ( ! method_exists( $installer, 'install' ) ) {
			wp_send_json_error( $error );
		}

		$installer->install( $downloadUrl );

		// Flush the cache and return the newly installed plugin basename.
		wp_cache_flush();

		$pluginBasename = $installer->plugin_info();

		if ( ! $pluginBasename ) {
			wp_send_json_error( $error );
		}

		// Activate the plugin silently.
		$activated = activate_plugin( $pluginBasename, '', $network, true );
		if ( is_wp_error( $activated ) ) {
			wp_send_json_error( esc_html__( 'The Pro version installed correctly, but it needs to be activated from the Plugins page inside your WordPress admin.', 'all-in-one-seo-pack' ) );
		}

		aioseo()->internalOptions->internal->connect->reset();

		// Because the regular activation hooks won't run, we need to add capabilities for the installing user so that he doesn't run into an error on the first request.
		aioseo()->activate->addCapabilitiesOnUpgrade();

		deactivate_plugins( plugin_basename( AIOSEO_FILE ), false, $network );

		wp_send_json_success( $success );
	}
}