Your IP : 216.73.216.162


Current Path : /home/x/b/o/xbodynamge/namtation/wp-content/
Upload File :
Current File : /home/x/b/o/xbodynamge/namtation/wp-content/Api.tar

Api.php000066600000001351151134034550005773 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Api;

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

use AIOSEO\Plugin\Common\Api as CommonApi;

/**
 * Api class for the admin.
 *
 * @since 4.0.0
 */
class Api extends CommonApi\Api {
	/**
	 * The routes we use in the rest API.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $liteRoutes = [
		// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
		// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	];

	/**
	 * Get all the routes to register.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of routes.
	 */
	protected function getRoutes() {
		return array_merge_recursive( $this->routes, $this->liteRoutes );
	}
}Wizard.php000066600000002124151134034550006521 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Api;

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

use AIOSEO\Plugin\Common\Api as CommonApi;

/**
 * Route class for the API.
 *
 * @since 4.0.0
 */
class Wizard extends CommonApi\Wizard {
	/**
	 * Save the wizard information.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function saveWizard( $request ) {
		$response = parent::saveWizard( $request );
		$body     = $request->get_json_params();
		$section  = ! empty( $body['section'] ) ? sanitize_text_field( $body['section'] ) : null;
		$wizard   = ! empty( $body['wizard'] ) ? $body['wizard'] : null;

		// Save the smart recommendations section.
		if ( 'smartRecommendations' === $section && ! empty( $wizard['smartRecommendations'] ) ) {
			$smartRecommendations = $wizard['smartRecommendations'];
			if ( isset( $smartRecommendations['usageTracking'] ) ) {
				aioseo()->options->advanced->usageTracking = $smartRecommendations['usageTracking'];
			}
		}

		return $response;
	}
}Listener.php000066600000016145151146132220007052 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchStatistics\Api;

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

// phpcs:disable WordPress.Security.NonceVerification.Recommended
// phpcs:disable HM.Security.NonceVerification.Recommended

/**
 * Class that holds our listeners for the microservice.
 *
 * @since   4.3.0
 * @version 4.6.2 Moved from Pro to Common.
 */
class Listener {
	/**
	 * Class constructor.
	 *
	 * @since 4.3.0
	 */
	public function __construct() {
		add_action( 'admin_init', [ $this, 'listenForAuthentication' ] );
		add_action( 'admin_init', [ $this, 'listenForReauthentication' ] );
		add_action( 'admin_init', [ $this, 'listenForReturningBack' ] );

		add_action( 'wp_ajax_nopriv_aioseo_is_installed', [ $this, 'isInstalled' ] );
		add_action( 'wp_ajax_nopriv_aioseo_rauthenticate', [ $this, 'reauthenticate' ] );
	}

	/**
	 * Listens to the response from the microservice server.
	 *
	 * @since 4.3.0
	 *
	 * @return void
	 */
	public function listenForAuthentication() {
		if ( empty( $_REQUEST['aioseo-oauth-action'] ) || 'auth' !== $_REQUEST['aioseo-oauth-action'] ) {
			return;
		}

		if (
			! aioseo()->access->hasCapability( 'aioseo_search_statistics_settings' ) ||
			! aioseo()->access->hasCapability( 'aioseo_general_settings' ) ||
			! aioseo()->access->hasCapability( 'aioseo_setup_wizard' )
		) {
			return;
		}

		if ( empty( $_REQUEST['tt'] ) || empty( $_REQUEST['key'] ) || empty( $_REQUEST['token'] ) || empty( $_REQUEST['authedsite'] ) ) {
			return;
		}

		if ( ! aioseo()->searchStatistics->api->trustToken->validate( sanitize_text_field( wp_unslash( $_REQUEST['tt'] ) ) ) ) {
			return;
		}

		$profile = [
			'key'        => sanitize_text_field( wp_unslash( $_REQUEST['key'] ) ),
			'token'      => sanitize_text_field( wp_unslash( $_REQUEST['token'] ) ),
			'siteurl'    => site_url(),
			'authedsite' => esc_url_raw( wp_unslash( $this->getAuthenticatedDomain() ) )
		];

		$success = aioseo()->searchStatistics->api->auth->verify( $profile );
		if ( ! $success ) {
			return;
		}

		$this->saveAndRedirect( $profile );
	}

	/**
	 * Listens to for the reauthentication response from the microservice.
	 *
	 * @since 4.3.0
	 *
	 * @return void
	 */
	public function listenForReauthentication() {
		if ( empty( $_REQUEST['aioseo-oauth-action'] ) || 'reauth' !== $_REQUEST['aioseo-oauth-action'] ) {
			return;
		}

		if (
			! aioseo()->access->hasCapability( 'aioseo_search_statistics_settings' ) ||
			! aioseo()->access->hasCapability( 'aioseo_general_settings' ) ||
			! aioseo()->access->hasCapability( 'aioseo_setup_wizard' )
		) {
			return;
		}

		if ( empty( $_REQUEST['tt'] ) || empty( $_REQUEST['authedsite'] ) ) {
			return;
		}

		if ( ! aioseo()->searchStatistics->api->trustToken->validate( sanitize_text_field( wp_unslash( $_REQUEST['tt'] ) ) ) ) {
			return;
		}

		$existingProfile = aioseo()->searchStatistics->api->auth->getProfile( true );
		if ( empty( $existingProfile['key'] ) || empty( $existingProfile['token'] ) ) {
			return;
		}

		$profile = [
			'key'        => $existingProfile['key'],
			'token'      => $existingProfile['token'],
			'siteurl'    => site_url(),
			'authedsite' => esc_url_raw( wp_unslash( $this->getAuthenticatedDomain() ) )
		];

		$this->saveAndRedirect( $profile );
	}

	/**
	 * Listens for the response from the microservice when the user returns back.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	public function listenForReturningBack() {
		if ( empty( $_REQUEST['aioseo-oauth-action'] ) || 'back' !== $_REQUEST['aioseo-oauth-action'] ) {
			return;
		}

		if (
			! aioseo()->access->hasCapability( 'aioseo_search_statistics_settings' ) ||
			! aioseo()->access->hasCapability( 'aioseo_general_settings' ) ||
			! aioseo()->access->hasCapability( 'aioseo_setup_wizard' )
		) {
			return;
		}

		wp_safe_redirect( $this->getRedirectUrl() );
		exit;
	}

	/**
	 * Return a success status code indicating that the plugin is installed.
	 *
	 * @since 4.3.0
	 *
	 * @return void
	 */
	public function isInstalled() {
		wp_send_json_success( [
			'version' => aioseo()->version,
			'pro'     => aioseo()->pro
		] );
	}

	/**
	 * Validate the trust token and tells the microservice that we can reauthenticate.
	 *
	 * @since 4.3.0
	 *
	 * @return void
	 */
	public function reauthenticate() {
		foreach ( [ 'key', 'token', 'tt' ] as $arg ) {
			if ( empty( $_REQUEST[ $arg ] ) ) {
				wp_send_json_error( [
					'error'   => 'authenticate_missing_arg',
					'message' => 'Authentication request missing parameter: ' . $arg,
					'version' => aioseo()->version,
					'pro'     => aioseo()->pro
				] );
			}
		}

		$trustToken = ! empty( $_REQUEST['tt'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['tt'] ) ) : '';
		if ( ! aioseo()->searchStatistics->api->trustToken->validate( $trustToken ) ) {
			wp_send_json_error( [
				'error'   => 'authenticate_invalid_tt',
				'message' => 'Invalid TT sent',
				'version' => aioseo()->version,
				'pro'     => aioseo()->pro
			] );
		}

		// If the trust token is validated, send a success response to trigger the regular auth process.
		wp_send_json_success();
	}

	/**
	 * Saves the authenticated account, clear the existing data and redirect back to the settings page.
	 *
	 * @since 4.3.0
	 *
	 * @return void
	 */
	private function saveAndRedirect( $profile ) {
		// Reset the search statistics data.
		aioseo()->searchStatistics->reset();

		// Save the authenticated profile.
		aioseo()->searchStatistics->api->auth->setProfile( $profile );

		// Reset dismissed alerts.
		$dismissedAlerts = aioseo()->settings->dismissedAlerts;
		foreach ( $dismissedAlerts as $key => $alert ) {
			if ( in_array( $key, [ 'searchConsoleNotConnected', 'searchConsoleSitemapErrors' ], true ) ) {
				$dismissedAlerts[ $key ] = false;
			}
		}
		aioseo()->settings->dismissedAlerts = $dismissedAlerts;

		// Maybe verifies the site.
		aioseo()->searchStatistics->site->maybeVerify();

		// Redirects to the original page.
		wp_safe_redirect( $this->getRedirectUrl() );
		exit;
	}

	/**
	 * Returns the authenticated domain.
	 *
	 * @since 4.3.0
	 *
	 * @return string The authenticated domain.
	 */
	private function getAuthenticatedDomain() {
		if ( empty( $_REQUEST['authedsite'] ) ) {
			return '';
		}

		$authedSite = sanitize_text_field( wp_unslash( $_REQUEST['authedsite'] ) );
		if ( false !== aioseo()->helpers->stringIndex( $authedSite, 'sc-domain:' ) ) {
			$authedSite = str_replace( 'sc-domain:', '', $authedSite );
		}

		return $authedSite;
	}

	/**
	 * Gets the redirect URL.
	 *
	 * @since 4.6.2
	 *
	 * @return string The redirect URL.
	 */
	private function getRedirectUrl() {
		$returnTo    = ! empty( $_REQUEST['return-to'] ) ? sanitize_key( $_REQUEST['return-to'] ) : '';
		$redirectUrl = 'admin.php?page=aioseo';

		switch ( $returnTo ) {
			case 'webmaster-tools':
				$redirectUrl = 'admin.php?page=aioseo-settings#/webmaster-tools?activetool=googleSearchConsole';
				break;
			case 'setup-wizard':
				$redirectUrl = 'index.php?page=aioseo-setup-wizard#/' . aioseo()->standalone->setupWizard->getNextStage();
				break;
			case 'search-statistics':
				$redirectUrl = 'admin.php?page=aioseo-search-statistics/#search-statistics';
				break;
		}

		return admin_url( $redirectUrl );
	}
}TrustToken.php000066600000003001151146132220007372 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchStatistics\Api;

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

/**
 * Handles the trust token.
 *
 * @since   4.3.0
 * @version 4.6.2 Moved from Pro to Common.
 */
class TrustToken {
	/**
	 * Returns the trust token from the database or creates a new one & stores it.
	 *
	 * @since 4.3.0
	 *
	 * @return string The trust token.
	 */
	public function get() {
		$trustToken = aioseo()->internalOptions->internal->searchStatistics->trustToken;
		if ( empty( $trustToken ) ) {
			$trustToken = $this->generate();
			aioseo()->internalOptions->internal->searchStatistics->trustToken = $trustToken;
		}

		return $trustToken;
	}

	/**
	 * Rotates the trust token.
	 *
	 * @since 4.3.0
	 *
	 * @return void
	 */
	public function rotate() {
		$trustToken = $this->generate();
		aioseo()->internalOptions->internal->searchStatistics->trustToken = $trustToken;
	}

	/**
	 * Generates a new trust token.
	 *
	 * @since 4.3.0
	 *
	 * @return string The trust token.
	 */
	public function generate() {
		return hash( 'sha512', wp_generate_password( 128, true, true ) . uniqid( '', true ) );
	}

	/**
	 * Verifies whether the passed trust token is valid or not.
	 *
	 * @since 4.3.0
	 *
	 * @param  string $passedTrustToken The trust token to validate.
	 * @return bool                     Whether the trust token is valid or not.
	 */
	public function validate( $passedTrustToken = '' ) {
		$trustToken = $this->get();

		return hash_equals( $trustToken, $passedTrustToken );
	}
}Request.php000066600000022526151146132220006715 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchStatistics\Api;

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

/**
 * Constructs requests to our microservice.
 *
 * @since   4.3.0
 * @version 4.6.2 Moved from Pro to Common.
 */
class Request {
	/**
	 * The base API route.
	 *
	 * @since 4.3.0
	 *
	 * @var string
	 */
	private $base = '';

	/**
	 * The URL scheme.
	 *
	 * @since 4.3.0
	 *
	 * @var string
	 */
	private $scheme = 'https://';

	/**
	 * The current API route.
	 *
	 * @since 4.3.0
	 *
	 * @var string
	 */
	private $route = '';

	/**
	 * The full API URL endpoint.
	 *
	 * @since 4.3.0
	 *
	 * @var string
	 */
	private $url = '';

	/**
	 * The current API method.
	 *
	 * @since 4.3.0
	 *
	 * @var string
	 */
	private $method = '';

	/**
	 * The API token.
	 *
	 * @since 4.3.0
	 *
	 * @var string
	 */
	private $token = '';

	/**
	 * The API key.
	 *
	 * @since 4.3.0
	 *
	 * @var string
	 */
	private $key = '';

	/**
	 * The API trust token.
	 *
	 * @since 4.3.0
	 *
	 * @var string
	 */
	private $tt = '';

	/**
	 * Plugin slug.
	 *
	 * @since 4.3.0
	 *
	 * @var bool|string
	 */
	private $plugin = false;

	/**
	 * URL to test connection with.
	 *
	 * @since 4.3.0
	 *
	 * @var string
	 */
	private $testurl = '';

	/**
	 * The site URL.
	 *
	 * @since 4.3.0
	 *
	 * @var string
	 */
	private $siteurl = '';

	/**
	 * The plugin version.
	 *
	 * @since 4.3.0
	 *
	 * @var string
	 */
	private $version = '';

	/**
	 * The site identifier.
	 *
	 * @since 4.3.0
	 *
	 * @var string
	 */
	private $sitei = '';

	/**
	 * The request args.
	 *
	 * @since 4.3.0
	 *
	 * @var array
	 */
	private $args = [];

	/**
	 * Additional data to append to request body.
	 *
	 * @since 4.3.0
	 *
	 * @var array
	 */
	protected $additionalData = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.3.0
	 *
	 * @param string $route  The API route.
	 * @param array  $args   List of arguments.
	 * @param string $method The API method.
	 */
	public function __construct( $route, $args = [], $method = 'POST' ) {
		$this->base    = trailingslashit( aioseo()->searchStatistics->api->getApiUrl() ) . trailingslashit( aioseo()->searchStatistics->api->getApiVersion() );
		$this->route   = trailingslashit( $route );
		$this->url     = trailingslashit( $this->scheme . $this->base . $this->route );
		$this->method  = $method;
		$this->token   = ! empty( $args['token'] ) ? $args['token'] : aioseo()->searchStatistics->api->auth->getToken();
		$this->key     = ! empty( $args['key'] ) ? $args['key'] : aioseo()->searchStatistics->api->auth->getKey();
		$this->tt      = ! empty( $args['tt'] ) ? $args['tt'] : '';
		$this->args    = ! empty( $args ) ? $args : [];
		$this->siteurl = site_url();
		$this->plugin  = 'aioseo-' . strtolower( aioseo()->versionPath );
		$this->version = aioseo()->version;
		$this->sitei   = ! empty( $args['sitei'] ) ? $args['sitei'] : '';
		$this->testurl = ! empty( $args['testurl'] ) ? $args['testurl'] : '';
	}

	/**
	 * Sends and processes the API request.
	 *
	 * @since 4.3.0
	 *
	 * @return mixed The response.
	 */
	public function request() {
		// Make sure we're not blocked.
		$blocked = $this->isBlocked( $this->url );
		if ( is_wp_error( $blocked ) ) {
			return new \WP_Error(
				'api-error',
				sprintf(
					'The firewall of the server is blocking outbound calls. Please contact your hosting provider to fix this issue. %s',
					$blocked->get_error_message()
				)
			);
		}

		// 1. BUILD BODY
		$body = [];
		if ( ! empty( $this->args ) ) {
			foreach ( $this->args as $name => $value ) {
				$body[ $name ] = $value;
			}
		}

		foreach ( [ 'sitei', 'siteurl', 'version', 'key', 'token', 'tt' ] as $key ) {
			if ( ! empty( $this->{$key} ) ) {
				$body[ $key ] = $this->{$key};
			}
		}

		// If this is a plugin API request, add the data.
		if ( 'info' === $this->route || 'update' === $this->route ) {
			$body['aioseoapi-plugin'] = $this->plugin;
		}

		// Add in additional data if needed.
		if ( ! empty( $this->additionalData ) ) {
			$body['aioseoapi-data'] = maybe_serialize( $this->additionalData );
		}

		if ( 'GET' === $this->method ) {
			$body['time'] = time(); // Add a timestamp to avoid caching.
		}

		$body['timezone']        = gmdate( 'e' );
		$body['ip']              = ! empty( $_SERVER['SERVER_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_ADDR'] ) ) : '';

		// 2. SET HEADERS
		$headers = array_merge( [
			'Content-Type'      => 'application/json',
			'Cache-Control'     => 'no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0',
			'Pragma'            => 'no-cache',
			'Expires'           => 0,
			'AIOSEOAPI-Referer' => site_url(),
			'AIOSEOAPI-Sender'  => 'WordPress',
			'X-AIOSEO-Key'      => aioseo()->internalOptions->internal->siteAnalysis->connectToken,
		], aioseo()->helpers->getApiHeaders() );

		// 3. COMPILE REQUEST DATA
		$data = [
			'headers'    => $headers,
			'body'       => wp_json_encode( $body ),
			'timeout'    => 3000,
			'user-agent' => aioseo()->helpers->getApiUserAgent()
		];

		// 4. EXECUTE REQUEST
		if ( 'GET' === $this->method ) {
			$queryString = http_build_query( $body, '', '&' );

			unset( $data['body'] );

			$response = wp_remote_get( esc_url_raw( $this->url ) . '?' . $queryString, $data );
		} else {
			$response = wp_remote_post( esc_url_raw( $this->url ), $data );
		}

		// 5. VALIDATE RESPONSE
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$responseCode = wp_remote_retrieve_response_code( $response );
		$responseBody = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( is_wp_error( $responseBody ) ) {
			return false;
		}

		if ( 200 !== $responseCode ) {
			$type = ! empty( $responseBody['type'] ) ? $responseBody['type'] : 'api-error';

			if ( empty( $responseCode ) ) {
				return new \WP_Error(
					$type,
					'The API was unreachable.'
				);
			}

			if ( empty( $responseBody ) || ( empty( $responseBody['message'] ) && empty( $responseBody['error'] ) ) ) {
				return new \WP_Error(
					$type,
					sprintf(
						'The API returned a <strong>%s</strong> response',
						$responseCode
					)
				);
			}

			if ( ! empty( $responseBody['message'] ) ) {
				return new \WP_Error(
					$type,
					sprintf(
						'The API returned a <strong>%1$d</strong> response with this message: <strong>%2$s</strong>',
						$responseCode, stripslashes( $responseBody['message'] )
					)
				);
			}

			if ( ! empty( $responseBody['error'] ) ) {
				return new \WP_Error(
					$type,
					sprintf(
						'The API returned a <strong>%1$d</strong> response with this message: <strong>%2$s</strong>', $responseCode,
						stripslashes( $responseBody['error'] )
					)
				);
			}
		}

		// Check if the trust token is required.
		if (
			! empty( $this->tt ) &&
			( empty( $responseBody['tt'] ) || ! hash_equals( $this->tt, $responseBody['tt'] ) )
		) {
			return new \WP_Error( 'validation-error', 'Invalid API request.' );
		}

		return $responseBody;
	}

	/**
	 * Sets additional data for the request.
	 *
	 * @since 4.3.0
	 *
	 * @param  array $data The additional data.
	 * @return void
	 */
	public function setAdditionalData( array $data ) {
		$this->additionalData = array_merge( $this->additionalData, $data );
	}

	/**
	 * Checks if the given URL is blocked for a request.
	 *
	 * @since 4.3.0
	 *
	 * @param  string         $url The URL to test against.
	 * @return bool|\WP_Error      False or WP_Error if it is blocked.
	 */
	private function isBlocked( $url = '' ) {
		// The below page is a test HTML page used for firewall/router login detection
		// and for image linking purposes in Google Images. We use it to test outbound connections
		// It's on Google's main CDN so it loads in most countries in 0.07 seconds or less. Perfect for our
		// use case of testing outbound connections.
		$testurl = ! empty( $this->testurl ) ? $this->testurl : 'https://www.google.com/blank.html';
		if ( defined( 'WP_HTTP_BLOCK_EXTERNAL' ) && WP_HTTP_BLOCK_EXTERNAL ) {
			if ( defined( 'WP_ACCESSIBLE_HOSTS' ) ) {
				$wpHttp      = new \WP_Http();
				$onBlacklist = $wpHttp->block_request( $url );
				if ( $onBlacklist ) {
					return new \WP_Error( 'api-error', 'The API was unreachable because the API url is on the WP HTTP blocklist.' );
				} else {
					$params = [
						'sslverify'  => false,
						'timeout'    => 2,
						'user-agent' => aioseo()->helpers->getApiUserAgent(),
						'body'       => ''
					];

					$response = wp_remote_get( $testurl, $params );
					if ( ! is_wp_error( $response ) && $response['response']['code'] >= 200 && $response['response']['code'] < 300 ) {
						return false;
					} else {
						if ( is_wp_error( $response ) ) {
							return $response;
						} else {
							return new \WP_Error( 'api-error', 'The API was unreachable because the call to Google failed.' );
						}
					}
				}
			} else {
				return new \WP_Error( 'api-error', 'The API was unreachable because no external hosts are allowed on this site.' );
			}
		} else {
			$params = [
				'sslverify'  => false,
				'timeout'    => 2,
				'user-agent' => aioseo()->helpers->getApiUserAgent(),
				'body'       => ''
			];

			$response = wp_remote_get( $testurl, $params );
			if ( ! is_wp_error( $response ) && $response['response']['code'] >= 200 && $response['response']['code'] < 300 ) {
				return false;
			} else {
				if ( is_wp_error( $response ) ) {
					return $response;
				} else {
					return new \WP_Error( 'api-error', 'The API was unreachable because the call to Google failed.' );
				}
			}
		}
	}
}Auth.php000066600000010064151146132220006160 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchStatistics\Api;

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

/**
 * Handles the authentication/connection to our microservice.
 *
 * @since   4.3.0
 * @version 4.6.2 Moved from Pro to Common.
 */
class Auth {
	/**
	 * The authenticated profile data.
	 *
	 * @since 4.3.0
	 *
	 * @var array
	 */
	private $profile = [];

	/**
	 * The type of authentication.
	 *
	 * @since 4.6.2
	 *
	 * @var string
	 */
	public $type = 'lite';

	/**
	 * Class constructor.
	 *
	 * @since 4.3.0
	 */
	public function __construct() {
		$this->profile = $this->getProfile();

		if ( aioseo()->pro ) {
			$this->type = 'pro';
		}
	}

	/**
	 * Returns the authenticated profile.
	 *
	 * @since 4.3.0
	 *
	 * @param  bool  $force Busts the cache and forces an update of the profile data.
	 * @return array        The authenticated profile data.
	 */
	public function getProfile( $force = false ) {
		if ( ! empty( $this->profile ) && ! $force ) {
			return $this->profile;
		}

		$this->profile = aioseo()->internalOptions->internal->searchStatistics->profile;

		return $this->profile;
	}

	/**
	 * Returns the profile key.
	 *
	 * @since 4.3.0
	 *
	 * @return string The profile key.
	 */
	public function getKey() {
		return ! empty( $this->profile['key'] ) ? $this->profile['key'] : '';
	}

	/**
	 * Returns the profile token.
	 *
	 * @since 4.3.0
	 *
	 * @return string The profile token.
	 */
	public function getToken() {
		return ! empty( $this->profile['token'] ) ? $this->profile['token'] : '';
	}

	/**
	 * Returns the authenticated site.
	 *
	 * @since 4.3.0
	 *
	 * @return string The authenticated site.
	 */
	public function getAuthedSite() {
		return ! empty( $this->profile['authedsite'] ) ? $this->profile['authedsite'] : '';
	}

	/**
	 * Sets the profile data.
	 *
	 * @since 4.3.0
	 *
	 * @return void
	 */
	public function setProfile( $data = [] ) {
		$this->profile = $data;

		aioseo()->internalOptions->internal->searchStatistics->profile = $this->profile;
	}

	/**
	 * Deletes the profile data.
	 *
	 * @since 4.3.0
	 *
	 * @return void
	 */
	public function deleteProfile() {
		$this->setProfile( [] );
	}

	/**
	 * Check whether we are connected.
	 *
	 * @since 4.3.0
	 *
	 * @return bool Whether we are connected or not.
	 */
	public function isConnected() {
		return ! empty( $this->profile['key'] );
	}

	/**
	 * Verifies whether the authentication details are valid.
	 *
	 * @since 4.3.0
	 *
	 * @return bool Whether the data is valid or not.
	 */
	public function verify( $credentials = [] ) {
		$creds = ! empty( $credentials ) ? $credentials : aioseo()->internalOptions->internal->searchStatistics->profile;

		if ( empty( $creds['key'] ) ) {
			return new \WP_Error( 'validation-error', 'Authentication key is missing.' );
		}

		$request = new Request( "auth/verify/{$this->type}/", [
			'tt'      => aioseo()->searchStatistics->api->trustToken->get(),
			'key'     => $creds['key'],
			'token'   => $creds['token'],
			'testurl' => 'https://' . aioseo()->searchStatistics->api->getApiUrl() . '/v1/test/',
		] );
		$response = $request->request();

		aioseo()->searchStatistics->api->trustToken->rotate();

		return ! is_wp_error( $response );
	}

	/**
	 * Removes all authentication data.
	 *
	 * @since 4.3.0
	 *
	 * @return bool Whether the authentication data was deleted or not.
	 */
	public function delete() {
		if ( ! $this->isConnected() ) {
			return false;
		}

		$creds = aioseo()->searchStatistics->api->auth->getProfile( true );
		if ( empty( $creds['key'] ) ) {
			return false;
		}

		( new Request( "auth/delete/{$this->type}/", [
			'tt'      => aioseo()->searchStatistics->api->trustToken->get(),
			'key'     => $creds['key'],
			'token'   => $creds['token'],
			'testurl' => 'https://' . aioseo()->searchStatistics->api->getApiUrl() . '/v1/test/',
		] ) )->request();

		aioseo()->searchStatistics->api->trustToken->rotate();
		aioseo()->searchStatistics->api->auth->deleteProfile();
		aioseo()->searchStatistics->reset();

		// Resets the results for the Google meta tag.
		aioseo()->options->webmasterTools->google = '';

		return true;
	}
}Sitemaps.php000066600000011452151146252330007053 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Route class for the API.
 *
 * @since 4.0.0
 */
class Sitemaps {
	/**
	 * Delete all static sitemap files.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function deleteStaticFiles() {
		require_once ABSPATH . 'wp-admin/includes/file.php';
		$files = list_files( get_home_path(), 1 );
		if ( ! count( $files ) ) {
			return;
		}

		$isGeneralSitemapStatic = aioseo()->options->sitemap->general->advancedSettings->enable &&
			in_array( 'staticSitemap', aioseo()->internalOptions->internal->deprecatedOptions, true ) &&
			! aioseo()->options->deprecated->sitemap->general->advancedSettings->dynamic;

		$detectedFiles = [];
		if ( ! $isGeneralSitemapStatic ) {
			foreach ( $files as $filename ) {
				if ( preg_match( '#.*sitemap.*#', (string) $filename ) ) {
					// We don't want to delete the video sitemap here at all.
					$isVideoSitemap = preg_match( '#.*video.*#', (string) $filename ) ? true : false;
					if ( ! $isVideoSitemap ) {
						$detectedFiles[] = $filename;
					}
				}
			}
		}

		if ( ! count( $detectedFiles ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No sitemap files found.'
			], 400 );
		}

		$fs = aioseo()->core->fs;
		if ( ! $fs->isWpfsValid() ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No access to filesystem.'
			], 400 );
		}

		foreach ( $detectedFiles as $file ) {
			$fs->fs->delete( $file, false, 'f' );
		}

		Models\Notification::deleteNotificationByName( 'sitemap-static-files' );

		return new \WP_REST_Response( [
			'success'       => true,
			'notifications' => Models\Notification::getNotifications()
		], 200 );
	}

	/**
	 * Deactivates conflicting plugins.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function deactivateConflictingPlugins() {
		$error = esc_html__( 'Deactivation failed. Please check permissions and try again.', 'all-in-one-seo-pack' );
		if ( ! current_user_can( 'install_plugins' ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => $error
			], 400 );
		}

		aioseo()->conflictingPlugins->deactivateConflictingPlugins( [ 'seo', 'sitemap' ] );

		Models\Notification::deleteNotificationByName( 'conflicting-plugins' );

		return new \WP_REST_Response( [
			'success'       => true,
			'notifications' => Models\Notification::getNotifications()
		], 200 );
	}

	/**
	* Check whether the slug for the HTML sitemap is not in use.
	*
	* @since 4.1.3
	*
	* @param  \WP_REST_Request   $request The REST Request
	* @return \WP_REST_Response           The response.
	*/
	public static function validateHtmlSitemapSlug( $request ) {
		$body = $request->get_json_params();

		$pageUrl = ! empty( $body['pageUrl'] ) ? sanitize_text_field( $body['pageUrl'] ) : '';
		if ( empty( $pageUrl ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No path was provided.'
			], 400 );
		}

		$parsedPageUrl = wp_parse_url( $pageUrl );
		if ( empty( $parsedPageUrl['path'] ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'The given path is invalid.'
			], 400 );
		}

		$isUrl         = aioseo()->helpers->isUrl( $pageUrl );
		$isInternalUrl = aioseo()->helpers->isInternalUrl( $pageUrl );
		if ( $isUrl && ! $isInternalUrl ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'The given URL is not a valid internal URL.'
			], 400 );
		}

		$pathExists = self::pathExists( $parsedPageUrl['path'], false );

		return new \WP_REST_Response( [
			'exists' => $pathExists
		], 200 );
	}

	/**
	 * Checks whether the given path is unique or not.
	 *
	 * @since   4.1.4
	 * @version 4.2.6
	 *
	 * @param  string  $path The path.
	 * @param  bool    $path Whether the given path is a URL.
	 * @return boolean       Whether the path exists.
	 */
	private static function pathExists( $path, $isUrl ) {
		$path = trim( aioseo()->helpers->excludeHomePath( $path ), '/' );
		$url  = $isUrl ? $path : trailingslashit( home_url() ) . $path;
		$url  = user_trailingslashit( $url );

		// Let's do another check here, just to be sure that the domain matches.
		if ( ! aioseo()->helpers->isInternalUrl( $url ) ) {
			return false;
		}

		$response = wp_safe_remote_head( $url );
		$status   = wp_remote_retrieve_response_code( $response );

		if ( ! $status ) {
			// If there is no status code, we might be in a local environment with CURL misconfigured.
			// In that case we can still check if a post exists for the path by quering the DB.
			$post = aioseo()->helpers->getPostbyPath(
				$path,
				OBJECT,
				aioseo()->helpers->getPublicPostTypes( true )
			);

			return is_object( $post );
		}

		return 200 === $status;
	}
}Migration.php000066600000003575151146252330007226 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

use AIOSEO\Plugin\Common\Migration as CommonMigration;
use AIOSEO\Plugin\Common\Models;

/**
 * Route class for the API.
 *
 * @since 4.0.6
 */
class Migration {
	/**
	 * Resets blank title formats and retriggers the post/term meta migration.
	 *
	 * @since 4.0.6
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function fixBlankFormats() {
		$oldOptions = ( new CommonMigration\OldOptions() )->oldOptions;
		if ( ! $oldOptions ) {
			return new \WP_REST_Response( [
				'success' => true,
				'message' => 'Could not load v3 options.'
			], 400 );
		}

		$postTypes  = aioseo()->helpers->getPublicPostTypes( true );
		$taxonomies = aioseo()->helpers->getPublicTaxonomies( true );
		foreach ( $oldOptions as $k => $v ) {
			if ( ! preg_match( '/^aiosp_([a-zA-Z]*)_title_format$/', (string) $k, $match ) || ! empty( $v ) ) {
				continue;
			}

			$objectName = $match[1];
			if ( in_array( $objectName, $postTypes, true ) && aioseo()->dynamicOptions->searchAppearance->postTypes->has( $objectName ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$objectName->title = '#post_title #separator_sa #site_title';
				continue;
			}

			if ( in_array( $objectName, $taxonomies, true ) && aioseo()->dynamicOptions->searchAppearance->taxonomies->has( $objectName ) ) {
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$objectName->title = '#taxonomy_title #separator_sa #site_title';
			}
		}

		aioseo()->migration->redoMetaMigration();

		Models\Notification::deleteNotificationByName( 'v3-migration-title-formats-blank' );

		return new \WP_REST_Response( [
			'success'       => true,
			'message'       => 'Title formats have been reset; post/term migration has been scheduled.',
			'notifications' => Models\Notification::getNotifications()
		], 200 );
	}
}Tags.php000066600000000604151146252330006161 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

/**
 * Route class for the API.
 *
 * @since 4.0.0
 */
class Tags {
	/**
	 * Get all Tags.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function getTags() {
		return new \WP_REST_Response( aioseo()->tags->all( true ), 200 );
	}
}Tools.php000066600000015430151146252330006366 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

use AIOSEO\Plugin\Common\Models;
use AIOSEO\Plugin\Common\Tools as CommonTools;

/**
 * Route class for the API.
 *
 * @since 4.0.0
 */
class Tools {
	/**
	 * Import contents from a robots.txt url, static file or pasted text.
	 *
	 * @since   4.0.0
	 * @version 4.4.2
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function importRobotsTxt( $request ) {
		$body   = $request->get_json_params();
		$blogId = ! empty( $body['blogId'] ) ? $body['blogId'] : 0;
		$source = ! empty( $body['source'] ) ? $body['source'] : '';
		$text   = ! empty( $body['text'] ) ? sanitize_textarea_field( $body['text'] ) : '';
		$url    = ! empty( $body['url'] ) ? sanitize_url( $body['url'], [ 'http', 'https' ] ) : '';

		try {
			if ( is_multisite() && 'network' !== $blogId ) {
				aioseo()->helpers->switchToBlog( $blogId );
			}

			switch ( $source ) {
				case 'url':
					aioseo()->robotsTxt->importRobotsTxtFromUrl( $url, $blogId );

					break;
				case 'text':
					aioseo()->robotsTxt->importRobotsTxtFromText( $text, $blogId );

					break;
				case 'static':
				default:
					aioseo()->robotsTxt->importPhysicalRobotsTxt( $blogId );
					aioseo()->robotsTxt->deletePhysicalRobotsTxt();

					$options = aioseo()->options;
					if ( 'network' === $blogId ) {
						$options = aioseo()->networkOptions;
					}

					$options->tools->robots->enable = true;

					break;
			}

			return new \WP_REST_Response( [
				'success'       => true,
				'notifications' => Models\Notification::getNotifications()
			], 200 );
		} catch ( \Exception $e ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => $e->getMessage()
			], 400 );
		}
	}

	/**
	 * Delete the static robots.txt file.
	 *
	 * @since   4.0.0
	 * @version 4.4.5
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function deleteRobotsTxt() {
		try {
			aioseo()->robotsTxt->deletePhysicalRobotsTxt();

			return new \WP_REST_Response( [
				'success'       => true,
				'notifications' => Models\Notification::getNotifications()
			], 200 );
		} catch ( \Exception $e ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => $e->getMessage()
			], 400 );
		}
	}

	/**
	 * Email debug info.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response The response.
	 */
	public static function emailDebugInfo( $request ) {
		$body  = $request->get_json_params();
		$email = ! empty( $body['email'] ) ? $body['email'] : null;

		if ( ! filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'invalid-email-address'
			], 400 );
		}

		require_once ABSPATH . 'wp-admin/includes/update.php';

		// Translators: 1 - The plugin name ("All in One SEO"), 2 - The Site URL.
		$html = sprintf( __( '%1$s Debug Info from %2$s', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_NAME, aioseo()->helpers->getSiteDomain() ) . "\r\n------------------\r\n\r\n";
		$info = CommonTools\SystemStatus::getSystemStatusInfo();
		foreach ( $info as $group ) {
			if ( empty( $group['results'] ) ) {
				continue;
			}

			$html .= "\r\n\r\n{$group['label']}\r\n";
			foreach ( $group['results'] as $data ) {
				$html .= "{$data['header']}: {$data['value']}\r\n";
			}
		}

		if ( ! wp_mail(
			$email,
			// Translators: 1 - The plugin name ("All in One SEO).
			sprintf( __( '%1$s Debug Info', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_NAME ),
			$html
		) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Unable to send debug email, please check your email send settings and try again.'
			], 400 );
		}

		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}

	/**
	 * Create a settings backup.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function createBackup( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		aioseo()->backup->create();

		return new \WP_REST_Response( [
			'success' => true,
			'backups' => array_reverse( aioseo()->backup->all() )
		], 200 );
	}

	/**
	 * Restore a settings backup.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function restoreBackup( $request ) {
		$body   = $request->get_json_params();
		$backup = ! empty( $body['backup'] ) ? (int) $body['backup'] : null;
		if ( empty( $backup ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'backups' => array_reverse( aioseo()->backup->all() )
			], 400 );
		}

		aioseo()->backup->restore( $backup );

		return new \WP_REST_Response( [
			'success'         => true,
			'backups'         => array_reverse( aioseo()->backup->all() ),
			'options'         => aioseo()->options->all(),
			'internalOptions' => aioseo()->internalOptions->all()
		], 200 );
	}

	/**
	 * Delete a settings backup.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function deleteBackup( $request ) {
		$body   = $request->get_json_params();
		$backup = ! empty( $body['backup'] ) ? (int) $body['backup'] : null;
		if ( empty( $backup ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'backups' => array_reverse( aioseo()->backup->all() )
			], 400 );
		}

		aioseo()->backup->delete( $backup );

		return new \WP_REST_Response( [
			'success' => true,
			'backups' => array_reverse( aioseo()->backup->all() )
		], 200 );
	}

	/**
	 * Save the .htaccess file.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function saveHtaccess( $request ) {
		$body     = $request->get_json_params();
		$htaccess = ! empty( $body['htaccess'] ) ? sanitize_textarea_field( $body['htaccess'] ) : '';

		if ( empty( $htaccess ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => __( '.htaccess file is empty.', 'all-in-one-seo-pack' )
			], 400 );
		}

		$htaccess     = aioseo()->helpers->decodeHtmlEntities( $htaccess );
		$saveHtaccess = (object) aioseo()->htaccess->saveContents( $htaccess );
		if ( ! $saveHtaccess->success ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => $saveHtaccess->message ? $saveHtaccess->message : __( 'An error occurred while trying to write to the .htaccess file. Please try again later.', 'all-in-one-seo-pack' ),
				'reason'  => $saveHtaccess->reason
			], 400 );
		}

		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}
}Connect.php000066600000004650151146252330006661 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

/**
 * Route class for the API.
 *
 * @since 4.0.0
 */
class Connect {
	/**
	 * Get the connect URL.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function getConnectUrl( $request ) {
		$body    = $request->get_json_params();
		$key     = ! empty( $body['licenseKey'] ) ? sanitize_text_field( $body['licenseKey'] ) : null;
		$wizard  = ! empty( $body['wizard'] ) ? (bool) $body['wizard'] : false;
		$success = true;
		$urlData = aioseo()->admin->connect->generateConnectUrl( $key, $wizard ? admin_url( 'index.php?page=aioseo-setup-wizard#/success' ) : null );
		$url     = '';
		$message = '';

		if ( ! empty( $urlData['error'] ) ) {
			$success = false;
			$message = $urlData['error'];
		}

		$url = $urlData['url'];

		return new \WP_REST_Response( [
			'success' => $success,
			'url'     => $url,
			'message' => $message,
			'popup'   => ! isset( $urlData['popup'] ) ? true : $urlData['popup']
		], 200 );
	}

	/**
	 * Process the connection.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function processConnect( $request ) {
		$body        = $request->get_json_params();
		$wizard      = ! empty( $body['wizard'] ) ? sanitize_text_field( $body['wizard'] ) : null;
		$success     = true;

		if ( $wizard ) {
			aioseo()->internalOptions->internal->wizard = $wizard;
		}

		$response = aioseo()->admin->connect->process();
		if ( ! empty( $response['error'] ) ) {
			$message = $response['error'];
		} else {
			$message = $response['success'];
		}

		return new \WP_REST_Response( [
			'success' => $success,
			'message' => $message
		], 200 );
	}

	/**
	 * Saves the connect token.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function saveConnectToken( $request ) {
		$body    = $request->get_json_params();
		$token   = ! empty( $body['token'] ) ? sanitize_text_field( $body['token'] ) : null;
		$success = true;
		$message = 'token-saved';

		aioseo()->internalOptions->internal->siteAnalysis->connectToken = $token;

		return new \WP_REST_Response( [
			'success' => $success,
			'message' => $message
		], 200 );
	}
}Network.php000066600000002547151146252330006724 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

/**
 * Route class for the API.
 *
 * @since 4.2.5
 */
class Network {
	/**
	 * Save network robots rules.
	 *
	 * @since 4.2.5
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response The response.
	 */
	public static function saveNetworkRobots( $request ) {
		$isNetwork        = 'network' === $request->get_param( 'siteId' );
		$siteId           = $isNetwork ? aioseo()->helpers->getNetworkId() : (int) $request->get_param( 'siteId' );
		$body             = $request->get_json_params();
		$rules            = ! empty( $body['rules'] ) ? array_map( 'sanitize_text_field', $body['rules'] ) : [];
		$enabled          = isset( $body['enabled'] ) ? boolval( $body['enabled'] ) : null;
		$searchAppearance = ! empty( $body['searchAppearance'] ) ? $body['searchAppearance'] : [];

		aioseo()->helpers->switchToBlog( $siteId );

		$options = $isNetwork ? aioseo()->networkOptions : aioseo()->options;
		$enabled = null === $enabled ? $options->tools->robots->enable : $enabled;

		$options->sanitizeAndSave( [
			'tools'            => [
				'robots' => [
					'enable' => $enabled,
					'rules'  => $rules
				]
			],
			'searchAppearance' => $searchAppearance
		] );

		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}
}Integrations/WpCode.php000066600000001656151146252330011122 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api\Integrations;

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

use AIOSEO\Plugin\Common\Integrations\WpCode as WpCodeIntegration;

/**
 * Route class for the API.
 *
 * @since 4.3.8
 */
class WpCode {
	/**
	 * Load the WPCode Snippets from the library, if available.
	 *
	 * @since 4.3.8
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function getSnippets( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return new \WP_REST_Response( [
			'success'           => true,
			'snippets'          => WpCodeIntegration::loadWpCodeSnippets(),
			'pluginInstalled'   => WpCodeIntegration::isPluginInstalled(),
			'pluginActive'      => WpCodeIntegration::isPluginActive(),
			'pluginNeedsUpdate' => WpCodeIntegration::pluginNeedsUpdate()
		], 200 );
	}
}Integrations/Semrush.php000066600000004331151146252330011360 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api\Integrations;

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

use AIOSEO\Plugin\Common\Integrations\Semrush as SemrushIntegration;

/**
 * Route class for the API.
 *
 * @since 4.0.16
 */
class Semrush {
	/**
	 * Fetches the additional keyphrases.
	 *
	 * @since 4.0.16
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function semrushGetKeyphrases( $request ) {
		$body       = $request->get_json_params();
		$keyphrases = SemrushIntegration::getKeyphrases( $body['keyphrase'], $body['database'] );
		if ( false === $keyphrases ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'You may have sent too many requests to Semrush. Please wait a few minutes and try again.'
			], 400 );
		}

		return new \WP_REST_Response( [
			'success'    => true,
			'keyphrases' => $keyphrases
		], 200 );
	}

	/**
	 * Authenticates with Semrush.
	 *
	 * @since 4.0.16
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function semrushAuthenticate( $request ) {
		$body = $request->get_json_params();

		if ( empty( $body['code'] ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Missing authorization code.'
			], 400 );
		}

		$success = SemrushIntegration::authenticate( $body['code'] );
		if ( ! $success ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Authentication failed.'
			], 400 );
		}

		return new \WP_REST_Response( [
			'success' => true,
			'semrush' => aioseo()->internalOptions->integrations->semrush->all()
		], 200 );
	}

	/**
	 * Refreshes the API tokens.
	 *
	 * @since 4.0.16
	 *
	 * @return \WP_REST_Response          The response.
	 */
	public static function semrushRefresh() {
		$success = SemrushIntegration::refreshTokens();
		if ( ! $success ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'API tokens could not be refreshed.'
			], 400 );
		}

		return new \WP_REST_Response( [
			'success' => true,
			'semrush' => aioseo()->internalOptions->integrations->semrush->all()
		], 200 );
	}
}Plugins.php000066600000010533151146252330006706 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

/**
 * Route class for the API.
 *
 * @since 4.0.0
 */
class Plugins {
	/**
	 * Installs plugins from vue.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function installPlugins( $request ) {
		$error   = esc_html__( 'Installation failed. Please check permissions and try again.', 'all-in-one-seo-pack' );
		$body    = $request->get_json_params();
		$plugins = ! empty( $body['plugins'] ) ? $body['plugins'] : [];
		$network = ! empty( $body['network'] ) ? $body['network'] : false;

		if ( ! is_array( $plugins ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => $error
			], 400 );
		}

		if ( ! aioseo()->addons->canInstall() ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => $error
			], 400 );
		}

		$failed    = [];
		$completed = [];
		foreach ( $plugins as $plugin ) {
			if ( empty( $plugin['plugin'] ) ) {
				return new \WP_REST_Response( [
					'success' => false,
					'message' => $error
				], 400 );
			}

			$result = aioseo()->addons->installAddon( $plugin['plugin'], $network );
			if ( ! $result ) {
				$failed[] = $plugin['plugin'];
			} else {
				$completed[ $plugin['plugin'] ] = $result;
			}
		}

		return new \WP_REST_Response( [
			'success'   => true,
			'completed' => $completed,
			'failed'    => $failed
		], 200 );
	}

	/**
	 * Upgrade plugins from vue.
	 *
	 * @since 4.1.6
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function upgradePlugins( $request ) {
		$error   = esc_html__( 'Plugin update failed. Please check permissions and try again.', 'all-in-one-seo-pack' );
		$body    = $request->get_json_params();
		$plugins = ! empty( $body['plugins'] ) ? $body['plugins'] : [];
		$network = ! empty( $body['network'] ) ? $body['network'] : false;

		if ( ! is_array( $plugins ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => $error
			], 400 );
		}

		if ( ! aioseo()->addons->canUpdate() ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => $error
			], 400 );
		}

		$failed    = [];
		$completed = [];
		foreach ( $plugins as $plugin ) {
			if ( empty( $plugin['plugin'] ) ) {
				return new \WP_REST_Response( [
					'success' => false,
					'message' => $error
				], 400 );
			}

			$result = aioseo()->addons->upgradeAddon( $plugin['plugin'], $network );
			if ( ! $result ) {
				$failed[] = $plugin['plugin'];
			} else {
				$completed[ $plugin['plugin'] ] = aioseo()->addons->getAddon( $plugin['plugin'], true );
			}
		}

		return new \WP_REST_Response( [
			'success'   => true,
			'completed' => $completed,
			'failed'    => $failed
		], 200 );
	}

	/**
	 * Deactivates plugins from vue.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function deactivatePlugins( $request ) {
		$error   = esc_html__( 'Deactivation failed. Please check permissions and try again.', 'all-in-one-seo-pack' );
		$body    = $request->get_json_params();
		$plugins = ! empty( $body['plugins'] ) ? $body['plugins'] : [];
		$network = ! empty( $body['network'] ) ? $body['network'] : false;

		if ( ! is_array( $plugins ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => $error
			], 400 );
		}

		if ( ! current_user_can( 'install_plugins' ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => $error
			], 400 );
		}

		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		$failed    = [];
		$completed = [];
		foreach ( $plugins as $plugin ) {
			if ( empty( $plugin['plugin'] ) ) {
				return new \WP_REST_Response( [
					'success' => false,
					'message' => $error
				], 400 );
			}

			deactivate_plugins( $plugin['plugin'], false, $network );

			$stillActive = $network ? is_plugin_active_for_network( $plugin['plugin'] ) : is_plugin_active( $plugin['plugin'] );
			if ( $stillActive ) {
				$failed[] = $plugin['plugin'];
			}

			$completed[] = $plugin['plugin'];
		}

		return new \WP_REST_Response( [
			'success'   => true,
			'completed' => $completed,
			'failed'    => $failed
		], 200 );
	}
}User.php000066600000001402151146252330006176 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

/**
 * Handles user related API routes.
 *
 * @since 4.2.8
 */
class User {
	/**
	 * Get the user image.
	 *
	 * @since 4.2.8
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function getUserImage( $request ) {
		$args = $request->get_params();

		if ( empty( $args['userId'] ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No user ID was provided.'
			], 400 );
		}

		$url = get_avatar_url( $args['userId'] );

		return new \WP_REST_Response( [
			'success' => true,
			'url'     => is_array( $url ) ? $url[0] : $url,
		], 200 );
	}
}EmailSummary.php000066600000003027151146252330007672 0ustar00<?php

namespace AIOSEO\Plugin\Common\Api;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Email Summary related REST API endpoint callbacks.
 *
 * @since 4.7.2
 */
class EmailSummary {
	/**
	 * Sends a summary.
	 *
	 * @since 4.7.2
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function send( $request ) {
		try {
			$body = $request->get_json_params();

			$to        = $body['to'] ?? '';
			$frequency = $body['frequency'] ?? '';
			if ( $to && $frequency ) {
				aioseo()->emailReports->summary->run( [
					'recipient' => $to,
					'frequency' => $frequency,
				] );
			}

			return new \WP_REST_Response( [
				'success' => true,
			], 200 );
		} catch ( \Exception $e ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => $e->getMessage()
			], 200 );
		}
	}

	/**
	 * Enable email reports from notification.
	 *
	 * @since 4.7.7
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function enableEmailReports() {
		// Update option.
		aioseo()->options->advanced->emailSummary->enable = true;

		// Remove notification.
		$notification = Models\Notification::getNotificationByName( 'email-reports-enable-reminder' );
		if ( $notification->exists() ) {
			$notification->delete();
		}

		// Send a success response.
		return new \WP_REST_Response( [
			'success'       => true,
			'notifications' => Models\Notification::getNotifications()
		], 200 );
	}
}SearchStatistics.php000066600000013703151146252330010547 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

use AIOSEO\Plugin\Common\SearchStatistics\Api;

/**
 * Route class for the API.
 *
 * @since   4.3.0
 * @version 4.6.2 Moved from Pro to Common.
 */
class SearchStatistics {
	/**
	 * Get the authorize URL.
	 *
	 * @since 4.3.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function getAuthUrl( $request ) {
		$body = $request->get_params();

		if ( aioseo()->searchStatistics->api->auth->isConnected() ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Cannot authenticate. Please re-authenticate.'
			], 200 );
		}

		$returnTo = ! empty( $body['returnTo'] ) ? sanitize_key( $body['returnTo'] ) : '';
		$url      = add_query_arg( [
			'tt'      => aioseo()->searchStatistics->api->trustToken->get(),
			'sitei'   => aioseo()->searchStatistics->api->getSiteIdentifier(),
			'version' => aioseo()->version,
			'ajaxurl' => admin_url( 'admin-ajax.php' ),
			'siteurl' => site_url(),
			'return'  => urlencode( admin_url( 'admin.php?page=aioseo&return-to=' . $returnTo ) ),
			'testurl' => 'https://' . aioseo()->searchStatistics->api->getApiUrl() . '/v1/test/'
		], 'https://' . aioseo()->searchStatistics->api->getApiUrl() . '/v1/auth/new/' . aioseo()->searchStatistics->api->auth->type . '/' );

		$url = apply_filters( 'aioseo_search_statistics_auth_url', $url );

		return new \WP_REST_Response( [
			'success' => true,
			'url'     => $url,
		], 200 );
	}

	/**
	 * Get the reauthorize URL.
	 *
	 * @since 4.3.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function getReauthUrl( $request ) {
		$body = $request->get_params();

		if ( ! aioseo()->searchStatistics->api->auth->isConnected() ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Cannot re-authenticate. Please authenticate.',
			], 200 );
		}

		$returnTo = ! empty( $body['returnTo'] ) ? sanitize_key( $body['returnTo'] ) : '';
		$url      = add_query_arg( [
			'tt'      => aioseo()->searchStatistics->api->trustToken->get(),
			'sitei'   => aioseo()->searchStatistics->api->getSiteIdentifier(),
			'version' => aioseo()->version,
			'ajaxurl' => admin_url( 'admin-ajax.php' ),
			'siteurl' => site_url(),
			'key'     => aioseo()->searchStatistics->api->auth->getKey(),
			'token'   => aioseo()->searchStatistics->api->auth->getToken(),
			'return'  => urlencode( admin_url( 'admin.php?page=aioseo&return-to=' . $returnTo ) ),
			'testurl' => 'https://' . aioseo()->searchStatistics->api->getApiUrl() . '/v1/test/'
		], 'https://' . aioseo()->searchStatistics->api->getApiUrl() . '/v1/auth/reauth/' . aioseo()->searchStatistics->api->auth->type . '/' );

		$url = apply_filters( 'aioseo_search_statistics_reauth_url', $url );

		return new \WP_REST_Response( [
			'success' => true,
			'url'     => $url,
		], 200 );
	}

	/**
	 * Delete the authorization.
	 *
	 * @since 4.3.0
	 *
	 * @return \WP_REST_Response          The response.
	 */
	public static function deleteAuth() {
		if ( ! aioseo()->searchStatistics->api->auth->isConnected() ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Cannot deauthenticate. You are not currently authenticated.'
			], 200 );
		}

		aioseo()->searchStatistics->api->auth->delete();
		aioseo()->searchStatistics->cancelActions();

		return new \WP_REST_Response( [
			'success' => true,
			'message' => 'Successfully deauthenticated.'
		], 200 );
	}

	/**
	 * Deletes a sitemap.
	 *
	 * @since 4.6.2
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function deleteSitemap( $request ) {
		$body    = $request->get_json_params();
		$sitemap = ! empty( $body['sitemap'] ) ? $body['sitemap'] : '';

		if ( empty( $sitemap ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No sitemap provided.'
			], 200 );
		}

		$args = [
			'sitemap' => $sitemap
		];

		$api      = new Api\Request( 'google-search-console/sitemap/delete/', $args, 'POST' );
		$response = $api->request();

		if ( is_wp_error( $response ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => $response['message']
			], 200 );
		}

		aioseo()->internalOptions->searchStatistics->sitemap->list      = $response['data'];
		aioseo()->internalOptions->searchStatistics->sitemap->lastFetch = time();

		return new \WP_REST_Response( [
			'success' => true,
			'data'    => [
				'internalOptions'    => aioseo()->internalOptions->searchStatistics->sitemap->all(),
				'sitemapsWithErrors' => aioseo()->searchStatistics->sitemap->getSitemapsWithErrors()
			]
		], 200 );
	}

	/**
	 * Ignores a sitemap.
	 *
	 * @since 4.6.2
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function ignoreSitemap( $request ) {
		$body    = $request->get_json_params();
		$sitemap = ! empty( $body['sitemap'] ) ? $body['sitemap'] : '';

		if ( empty( $sitemap ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No sitemap provided.'
			], 200 );
		}

		$ignoredSitemaps = aioseo()->internalOptions->searchStatistics->sitemap->ignored;
		if ( is_array( $sitemap ) ) {
			$ignoredSitemaps = array_merge( $ignoredSitemaps, $sitemap );
		} else {
			$ignoredSitemaps[] = $sitemap;
		}

		$ignoredSitemaps = array_unique( $ignoredSitemaps ); // Remove duplicates.
		$ignoredSitemaps = array_filter( $ignoredSitemaps ); // Remove empty values.

		aioseo()->internalOptions->searchStatistics->sitemap->ignored = $ignoredSitemaps;

		return new \WP_REST_Response( [
			'success' => true,
			'data'    => [
				'internalOptions'    => aioseo()->internalOptions->searchStatistics->sitemap->all(),
				'sitemapsWithErrors' => aioseo()->searchStatistics->sitemap->getSitemapsWithErrors()
			]
		], 200 );
	}
}WritingAssistant.php000066600000021163151146252330010603 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

use AIOSEO\Plugin\Common\Models;

/**
 * WritingAssistant class for the API.
 *
 * @since 4.7.4
 */
class WritingAssistant {
	/**
	 * Process the keyword.
	 *
	 * @since 4.7.4
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function processKeyword( $request ) {
		$body        = $request->get_json_params();
		$postId      = absint( $body['postId'] );
		$keywordText = sanitize_text_field( $body['keyword'] );
		$country     = sanitize_text_field( $body['country'] );
		$language    = sanitize_text_field( strtolower( $body['language'] ) );

		if ( empty( $keywordText ) || empty( $country ) || empty( $language ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'error'   => __( 'Missing data to generate a report', 'all-in-one-seo-pack' )
			] );
		}

		$keyword              = Models\WritingAssistantKeyword::getKeyword( $keywordText, $country, $language );
		$writingAssistantPost = Models\WritingAssistantPost::getPost( $postId );
		if ( $keyword->exists() ) {
			$writingAssistantPost->attachKeyword( $keyword->id );

			// Returning early will let the UI code start polling the keyword.
			return new \WP_REST_Response( [
				'success'  => true,
				'progress' => $keyword->progress
			], 200 );
		}

		// Start a new keyword process.
		$processResult = aioseo()->writingAssistant->seoBoost->service->processKeyword( $keywordText, $country, $language );
		if ( is_wp_error( $processResult ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'error'   => $processResult->get_error_message()
			] );
		}

		// Store the new keyword.
		$keyword->uuid     = $processResult['slug'];
		$keyword->progress = 0;
		$keyword->save();

		// Update the writing assistant post with the current keyword.
		$writingAssistantPost->attachKeyword( $keyword->id );

		return new \WP_REST_Response( [ 'success' => true ], 200 );
	}

	/**
	 * Get current keyword for a Post.
	 *
	 * @since 4.7.4
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function getPostKeyword( $request ) {
		$postId = $request->get_param( 'postId' );

		if ( empty( $postId ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => __( 'Empty Post ID', 'all-in-one-seo-pack' )
			], 404 );
		}

		$keyword = Models\WritingAssistantPost::getKeyword( $postId );
		if ( $keyword && 100 !== $keyword->progress ) {
			// Update progress.
			$newProgress = aioseo()->writingAssistant->seoBoost->service->getProgressAndResult( $keyword->uuid );
			if ( is_wp_error( $newProgress ) ) {
				return new \WP_REST_Response( [
					'success' => false,
					'error'   => $newProgress->get_error_message()
				], 200 );
			}

			if ( 'success' !== $newProgress['status'] ) {
				return new \WP_REST_Response( [
					'success' => false,
					'error'   => $newProgress['msg']
				], 200 );
			}

			$keyword->progress = ! empty( $newProgress['report']['progress'] ) ? $newProgress['report']['progress'] : $keyword->progress;

			if ( ! empty( $newProgress['report']['keywords'] ) ) {
				$keyword->keywords = $newProgress['report']['keywords'];
			}

			if ( ! empty( $newProgress['report']['competitors'] ) ) {
				$keyword->competitors = [
					'competitors' => $newProgress['report']['competitors'],
					'summary'     => $newProgress['report']['competitors_summary']
				];
			}

			$keyword->save();
		}

		// Return a refreshed keyword here because we need some parsed data.
		$keyword = Models\WritingAssistantPost::getKeyword( $postId );

		return new \WP_REST_Response( $keyword, 200 );
	}

	/**
	 * Get the content analysis for a post.
	 *
	 * @since 4.7.4
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function getContentAnalysis( $request ) {
		$title       = $request->get_param( 'title' );
		$description = $request->get_param( 'description' );
		$content     = apply_filters( 'the_content', $request->get_param( 'content' ) );
		$postId      = $request->get_param( 'postId' );
		if ( empty( $content ) || empty( $postId ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => __( 'Empty Content or Post ID', 'all-in-one-seo-pack' )
			], 200 );
		}

		$keyword = Models\WritingAssistantPost::getKeyword( $postId );
		if (
			! $keyword ||
			! $keyword->exists() ||
			100 !== $keyword->progress
		) {
			return new \WP_REST_Response( [
				'success' => false,
				'error'   => __( 'Keyword not found or not ready', 'all-in-one-seo-pack' )
			], 200 );
		}

		$writingAssistantPost = Models\WritingAssistantPost::getPost( $postId );

		// Make sure we're not analysing the same content again.
		$contentHash = sha1( $content );
		if (
			! empty( $writingAssistantPost->content_analysis ) &&
			$writingAssistantPost->content_analysis_hash === $contentHash
		) {
			return new \WP_REST_Response( $writingAssistantPost->content_analysis, 200 );
		}

		// Call SEOBoost service to get the content analysis.
		$contentAnalysis = aioseo()->writingAssistant->seoBoost->service->getContentAnalysis( $title, $description, $content, $keyword->uuid );
		if ( is_wp_error( $contentAnalysis ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'error'   => $contentAnalysis->get_error_message()
			], 200 );
		}

		if ( empty( $contentAnalysis['result'] ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'error'   => __( 'Empty response from service', 'all-in-one-seo-pack' )
			], 200 );
		}

		// Update the post with the content analysis.
		$writingAssistantPost->content_analysis      = $contentAnalysis['result'];
		$writingAssistantPost->content_analysis_hash = $contentHash;
		$writingAssistantPost->save();

		return new \WP_REST_Response( $contentAnalysis['result'], 200 );
	}

	/**
	 * Get the user info.
	 *
	 * @since 4.7.4
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function getUserInfo() {
		$userInfo = aioseo()->writingAssistant->seoBoost->service->getUserInfo();
		if ( is_wp_error( $userInfo ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'error'   => $userInfo->get_error_message()
			], 200 );
		}

		if ( empty( $userInfo['status'] ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'error'   => __( 'Empty response from service', 'all-in-one-seo-pack' )
			], 200 );
		}

		if ( 'success' !== $userInfo['status'] ) {
			return new \WP_REST_Response( [
				'success' => false,
				'error'   => $userInfo['msg']
			], 200 );
		}

		return new \WP_REST_Response( $userInfo, 200 );
	}

	/**
	 * Get the user info.
	 *
	 * @since 4.7.4
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function getUserOptions() {
		$userOptions = aioseo()->writingAssistant->seoBoost->getUserOptions();

		return new \WP_REST_Response( $userOptions, 200 );
	}

	/**
	 * Get the report history.
	 *
	 * @since 4.7.4
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function getReportHistory() {
		$reportHistory = aioseo()->writingAssistant->seoBoost->getReportHistory();

		if ( is_wp_error( $reportHistory ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'error'   => $reportHistory->get_error_message()
			], 200 );
		}

		return new \WP_REST_Response( $reportHistory, 200 );
	}

	/**
	 * Disconnect the user.
	 *
	 * @since 4.7.4
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function disconnect() {
		aioseo()->writingAssistant->seoBoost->setAccessToken( '' );

		return new \WP_REST_Response( [ 'success' => true ], 200 );
	}

	/**
	 * Save user options.
	 *
	 * @since 4.7.4
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function saveUserOptions( $request ) {
		$body = $request->get_json_params();

		$userOptions = [
			'country'  => $body['country'],
			'language' => $body['language'],
		];

		aioseo()->writingAssistant->seoBoost->setUserOptions( $userOptions );

		return new \WP_REST_Response( [ 'success' => true ], 200 );
	}

	/**
	 * Set the report progress.
	 *
	 * @since 4.7.4
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function setReportProgress( $request ) {
		$body              = $request->get_json_params();
		$keyword           = Models\WritingAssistantPost::getKeyword( (int) $body['postId'] );
		$keyword->progress = (int) $body['progress'];
		$keyword->save();

		return new \WP_REST_Response( [ 'success' => true ], 200 );
	}
}Settings.php000066600000057350151146252330007075 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

use AIOSEO\Plugin\Common\Models;
use AIOSEO\Plugin\Common\Migration;

/**
 * Route class for the API.
 *
 * @since 4.0.0
 */
class Settings {
	/**
	 * Contents to import.
	 *
	 * @since 4.7.2
	 *
	 * @var array
	 */
	public static $importFile = [];

	/**
	 * Update the settings.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function getOptions() {
		return new \WP_REST_Response( [
			'options'  => aioseo()->options->all(),
			'settings' => aioseo()->settings->all()
		], 200 );
	}

	/**
	 * Toggles a card in the settings.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function toggleCard( $request ) {
		$body  = $request->get_json_params();
		$card  = ! empty( $body['card'] ) ? sanitize_text_field( $body['card'] ) : null;
		$cards = aioseo()->settings->toggledCards;
		if ( array_key_exists( $card, $cards ) ) {
			$cards[ $card ] = ! $cards[ $card ];
			aioseo()->settings->toggledCards = $cards;
		}

		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}

	/**
	 * Toggles a radio in the settings.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function toggleRadio( $request ) {
		$body   = $request->get_json_params();
		$radio  = ! empty( $body['radio'] ) ? sanitize_text_field( $body['radio'] ) : null;
		$value  = ! empty( $body['value'] ) ? sanitize_text_field( $body['value'] ) : null;
		$radios = aioseo()->settings->toggledRadio;
		if ( array_key_exists( $radio, $radios ) ) {
			$radios[ $radio ] = $value;
			aioseo()->settings->toggledRadio = $radios;
		}

		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}

	/**
	 * Dismisses an alert.
	 *
	 * @since 4.3.6
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function dismissAlert( $request ) {
		$body   = $request->get_json_params();
		$alert  = ! empty( $body['alert'] ) ? sanitize_text_field( $body['alert'] ) : null;
		$alerts = aioseo()->settings->dismissedAlerts;
		if ( array_key_exists( $alert, $alerts ) ) {
			$alerts[ $alert ] = true;
			aioseo()->settings->dismissedAlerts = $alerts;
		}

		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}

	/**
	 * Toggles a table's items per page setting.
	 *
	 * @since 4.2.5
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function changeItemsPerPage( $request ) {
		$body   = $request->get_json_params();
		$table  = ! empty( $body['table'] ) ? sanitize_text_field( $body['table'] ) : null;
		$value  = ! empty( $body['value'] ) ? intval( $body['value'] ) : null;
		$tables = aioseo()->settings->tablePagination;
		if ( array_key_exists( $table, $tables ) ) {
			$tables[ $table ] = $value;
			aioseo()->settings->tablePagination = $tables;
		}

		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}

	/**
	 * Dismisses the upgrade bar.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function hideUpgradeBar() {
		aioseo()->settings->showUpgradeBar = false;

		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}

	/**
	 * Hides the Setup Wizard CTA.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function hideSetupWizard() {
		aioseo()->settings->showSetupWizard = false;

		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}

	/**
	 * Save options from the front end.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function saveChanges( $request ) {
		$body           = $request->get_json_params();
		$options        = ! empty( $body['options'] ) ? $body['options'] : [];
		$dynamicOptions = ! empty( $body['dynamicOptions'] ) ? $body['dynamicOptions'] : [];
		$network        = ! empty( $body['network'] ) ? (bool) $body['network'] : false;
		$networkOptions = ! empty( $body['networkOptions'] ) ? $body['networkOptions'] : [];

		// If this is the network admin, reset the options.
		if ( $network ) {
			aioseo()->networkOptions->sanitizeAndSave( $networkOptions );
		} else {
			aioseo()->options->sanitizeAndSave( $options );
			aioseo()->dynamicOptions->sanitizeAndSave( $dynamicOptions );
		}

		// Re-initialize notices.
		aioseo()->notices->init();

		return new \WP_REST_Response( [
			'success'       => true,
			'notifications' => Models\Notification::getNotifications()
		], 200 );
	}

	/**
	 * Reset settings.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function resetSettings( $request ) {
		$body     = $request->get_json_params();
		$settings = ! empty( $body['settings'] ) ? $body['settings'] : [];

		$notAllowedOptions = aioseo()->access->getNotAllowedOptions();

		foreach ( $settings as $setting ) {
			$optionAccess = in_array( $setting, [ 'robots', 'blocker' ], true ) ? 'tools' : $setting;

			if ( in_array( $optionAccess, $notAllowedOptions, true ) ) {
				continue;
			}

			switch ( $setting ) {
				case 'robots':
					aioseo()->options->tools->robots->reset();
					aioseo()->options->searchAppearance->advanced->unwantedBots->reset();
					aioseo()->options->searchAppearance->advanced->searchCleanup->settings->preventCrawling = false;
					break;
				default:
					if ( 'searchAppearance' === $setting ) {
						aioseo()->robotsTxt->resetSearchAppearanceRules();
					}

					if ( aioseo()->options->has( $setting ) ) {
						aioseo()->options->$setting->reset();
					}
					if ( aioseo()->dynamicOptions->has( $setting ) ) {
						aioseo()->dynamicOptions->$setting->reset();
					}
			}

			if ( 'access-control' === $setting ) {
				aioseo()->access->addCapabilities();
			}
		}

		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}

	/**
	 * Import settings from external file.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request.
	 * @return \WP_REST_Response          The response.
	 */
	public static function importSettings( $request ) {
		$file        = $request->get_file_params()['file'];
		$isJSONFile  = 'application/json' === $file['type'];
		$isCSVFile   = 'text/csv' === $file['type'];
		$isOctetFile = 'application/octet-stream' === $file['type'];
		if (
			empty( $file['tmp_name'] ) ||
			empty( $file['type'] ) ||
			(
				! $isJSONFile &&
				! $isCSVFile &&
				! $isOctetFile
			)
		) {
			return new \WP_REST_Response( [
				'success' => false
			], 400 );
		}

		$contents = aioseo()->core->fs->getContents( $file['tmp_name'] );
		if ( empty( $contents ) ) {
			return new \WP_REST_Response( [
				'success' => false
			], 400 );
		}

		if ( $isJSONFile ) {
			self::$importFile = json_decode( $contents, true );
		}

		if ( $isCSVFile ) {
			// Transform the CSV content into the original JSON array.
			self::$importFile = self::prepareCsvImport( $contents );
		}

		// If the file is invalid just return.
		if ( empty( self::$importFile ) ) {
			return new \WP_REST_Response( [
				'success' => false
			], 400 );
		}

		// Import settings.
		if ( ! empty( self::$importFile['settings'] ) ) {
			self::importSettingsFromFile( self::$importFile['settings'] );
		}

		// Import posts.
		if ( ! empty( self::$importFile['postOptions'] ) ) {
			self::importPostsFromFile( self::$importFile['postOptions'] );
		}

		// Import INI.
		if ( $isOctetFile ) {
			$response = aioseo()->importExport->importIniData( self::$importFile );
			if ( ! $response ) {
				return new \WP_REST_Response( [
					'success' => false
				], 400 );
			}
		}

		return new \WP_REST_Response( [
			'success' => true,
			'options' => aioseo()->options->all()
		], 200 );
	}

	/**
	 * Import settings from a file.
	 *
	 * @since 4.7.2
	 *
	 * @param array $settings The data to import.
	 */
	private static function importSettingsFromFile( $settings ) {
		// Clean up the array removing options the user should not manage.
		$notAllowedOptions = aioseo()->access->getNotAllowedOptions();
		$settings          = array_diff_key( $settings, $notAllowedOptions );
		if ( ! empty( $settings['deprecated'] ) ) {
			$settings['deprecated'] = array_diff_key( $settings['deprecated'], $notAllowedOptions );
		}

		// Remove any dynamic options and save them separately since this has been refactored.
		$commonDynamic = [
			'sitemap',
			'searchAppearance',
			'breadcrumbs',
			'accessControl'
		];

		foreach ( $commonDynamic as $cd ) {
			if ( ! empty( $settings[ $cd ]['dynamic'] ) ) {
				$settings['dynamic'][ $cd ] = $settings[ $cd ]['dynamic'];
				unset( $settings[ $cd ]['dynamic'] );
			}
		}

		// These options have a very different structure so we'll do them separately.
		if ( ! empty( $settings['social']['facebook']['general']['dynamic'] ) ) {
			$settings['dynamic']['social']['facebook']['general'] = $settings['social']['facebook']['general']['dynamic'];
			unset( $settings['social']['facebook']['general']['dynamic'] );
		}

		if ( ! empty( $settings['dynamic'] ) ) {
			aioseo()->dynamicOptions->sanitizeAndSave( $settings['dynamic'] );
			unset( $settings['dynamic'] );
		}

		if ( ! empty( $settings['tools']['robots']['rules'] ) ) {
			$settings['tools']['robots']['rules'] = array_merge( aioseo()->robotsTxt->extractSearchAppearanceRules(), $settings['tools']['robots']['rules'] );
		}

		aioseo()->options->sanitizeAndSave( $settings );
	}

	/**
	 * Import posts from a file.
	 *
	 * @since 4.7.2
	 *
	 * @param array $postOptions The data to import.
	 */
	private static function importPostsFromFile( $postOptions ) {
		$notAllowedFields = aioseo()->access->getNotAllowedPageFields();

		foreach ( $postOptions as $postData ) {
			if ( ! empty( $postData['posts'] ) ) {
				foreach ( $postData['posts'] as $post ) {
					unset( $post['id'] );
					// Clean up the array removing fields the user should not manage.
					$post    = array_diff_key( $post, $notAllowedFields );
					$thePost = Models\Post::getPost( $post['post_id'] );

					// Remove primary term if the term is not attached to the post anymore.
					if ( ! empty( $post['primary_term'] ) && aioseo()->helpers->isJsonString( $post['primary_term'] ) ) {
						$primaryTerms = json_decode( $post['primary_term'], true );

						foreach ( $primaryTerms as $tax => $termId ) {
							$terms = wp_get_post_terms( $post['post_id'], $tax, [
								'fields' => 'ids'
							] );

							if ( is_array( $terms ) && ! in_array( $termId, $terms, true ) ) {
								unset( $primaryTerms[ $tax ] );
							}
						}

						$post['primary_term'] = empty( $primaryTerms ) ? null : wp_json_encode( $primaryTerms );
					}

					// Remove FAQ Block schema if the block is not present in the post anymore.
					if ( ! empty( $post['schema'] ) && aioseo()->helpers->isJsonString( $post['schema'] ) ) {
						$schemas = json_decode( $post['schema'], true );

						foreach ( $schemas['blockGraphs'] as $index => $block ) {
							if ( 'aioseo/faq' !== $block['type'] ) {
								continue;
							}

							$postBlocks   = parse_blocks( get_the_content( null, false, $post['post_id'] ) );
							$postFaqBlock = array_filter( $postBlocks, function( $block ) {
								return 'aioseo/faq' === $block['blockName'];
							} );

							if ( empty( $postFaqBlock ) ) {
								unset( $schemas['blockGraphs'][ $index ] );
							}
						}

						$post['schema'] = wp_json_encode( $schemas );
					}

					$thePost->set( $post );
					$thePost->save();
				}
			}
		}
	}

	/**
	 * Prepare the content from CSV to the original JSON array to import.
	 *
	 * @since 4.7.2
	 *
	 * @param  string $fileContent The Data to import.
	 * @return array               The content.
	 */
	public static function prepareCSVImport( $fileContent ) {
		$content    = [];
		$newContent = [
			'postOptions' => null
		];

		$rows = str_getcsv( $fileContent, "\n" );

		// Get the first row to check if the file has post_id or term_id.
		$header = str_getcsv( $rows[0], ',' );
		$header = aioseo()->helpers->sanitizeOption( $header );

		// Check if the file has post_id or term_id.
		$type = in_array( 'post_id', $header, true ) ? 'posts' : null;
		$type = in_array( 'term_id', $header, true ) ? 'terms' : $type;

		if ( ! $type ) {
			return false;
		}

		// Remove header row.
		unset( $rows[0] );

		$jsonFields = [
			'keywords',
			'keyphrases',
			'page_analysis',
			'primary_term',
			'og_article_tags',
			'schema',
			'options',
			'open_ai',
			'videos'
		];

		foreach ( $rows as $row ) {
			$row = str_replace( '\\""', '\\"', $row );
			$row = str_getcsv( $row, ',' );

			foreach ( $row as $key => $value ) {
				$key = aioseo()->helpers->sanitizeOption( $key );

				if ( ! empty( $value ) && in_array( $header[ $key ], $jsonFields, true ) && ! aioseo()->helpers->isJsonString( $value ) ) {
					continue;
				} elseif ( '' === trim( $value ) ) {
					$value = null;
				}

				$content[ $header [ $key ] ] = $value;
			}
			$newContent['postOptions']['content'][ $type ][] = $content;
		}

		return $newContent;
	}

	/**
	 * Export settings.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function exportSettings( $request ) {
		$body        = $request->get_json_params();
		$settings    = ! empty( $body['settings'] ) ? $body['settings'] : [];
		$allSettings = [
			'settings' => []
		];

		if ( empty( $settings ) ) {
			return new \WP_REST_Response( [
				'success' => false
			], 400 );
		}

		$options           = aioseo()->options->noConflict();
		$dynamicOptions    = aioseo()->dynamicOptions->noConflict();
		$notAllowedOptions = aioseo()->access->getNotAllowedOptions();
		foreach ( $settings as $setting ) {
			$optionAccess = in_array( $setting, [ 'robots', 'blocker' ], true ) ? 'tools' : $setting;

			if ( in_array( $optionAccess, $notAllowedOptions, true ) ) {
				continue;
			}

			switch ( $setting ) {
				case 'robots':
					$allSettings['settings']['tools']['robots'] = $options->tools->robots->all();
					// Search Appearance settings that are also found in the robots settings.
					if ( empty( $allSettings['settings']['searchAppearance']['advanced'] ) ) {
						$allSettings['settings']['searchAppearance']['advanced'] = [
							'unwantedBots'  => $options->searchAppearance->advanced->unwantedBots->all(),
							'searchCleanup' => [
								'settings' => [
									'preventCrawling' => $options->searchAppearance->advanced->searchCleanup->settings->preventCrawling
								]
							]
						];
					}
					break;
				default:
					if ( $options->has( $setting ) ) {
						$allSettings['settings'][ $setting ] = $options->$setting->all();
					}

					// If there are related dynamic settings, let's include them.
					if ( $dynamicOptions->has( $setting ) ) {
						$allSettings['settings']['dynamic'][ $setting ] = $dynamicOptions->$setting->all();
					}

					// It there is a related deprecated $setting, include it.
					if ( $options->deprecated->has( $setting ) ) {
						$allSettings['settings']['deprecated'][ $setting ] = $options->deprecated->$setting->all();
					}
					break;
			}
		}

		return new \WP_REST_Response( [
			'success'  => true,
			'settings' => $allSettings
		], 200 );
	}

	/**
	 * Export post data.
	 *
	 * @since 4.7.2
	 *
	 * @param  \WP_REST_Request  $request The REST Request.
	 * @return \WP_REST_Response          The response.
	 */
	public static function exportContent( $request ) {
		$body            = $request->get_json_params();
		$postOptions     = $body['postOptions'] ?? [];
		$typeFile        = $body['typeFile'] ?? false;
		$siteId          = (int) ( $body['siteId'] ?? get_current_blog_id() );
		$contentPostType = null;
		$return          = true;

		try {
			aioseo()->helpers->switchToBlog( $siteId );

			// Get settings from post types selected.
			if ( ! empty( $postOptions ) ) {
				$fieldsToExclude = [
					'seo_score'                  => '',
					'schema_type'                => '',
					'schema_type_options'        => '',
					'images'                     => '',
					'image_scan_date'            => '',
					'videos'                     => '',
					'video_thumbnail'            => '',
					'video_scan_date'            => '',
					'link_scan_date'             => '',
					'link_suggestions_scan_date' => '',
					'local_seo'                  => '',
					'options'                    => '',
					'open_ai'                    => ''
				];

				$notAllowed = array_merge( aioseo()->access->getNotAllowedPageFields(), $fieldsToExclude );
				$posts      = self::getPostTypesData( $postOptions, $notAllowed );

				// Generate content to CSV or JSON.
				if ( ! empty( $posts ) ) {
					// Change the order of keys so the post_title shows up at the beginning.
					$data = [];
					foreach ( $posts as $p ) {
						$item = [
							'id'         => '',
							'post_id'    => '',
							'post_title' => '',
							'title'      => ''
						];

						$p['title']      = aioseo()->helpers->decodeHtmlEntities( $p['title'] );
						$p['post_title'] = aioseo()->helpers->decodeHtmlEntities( $p['post_title'] );

						$data[] = array_merge( $item, $p );
					}

					if ( 'csv' === $typeFile ) {
						$contentPostType = self::dataToCsv( $data );
					}

					if ( 'json' === $typeFile ) {
						$contentPostType['postOptions']['content']['posts'] = $data;
					}
				}
			}
		} catch ( \Throwable $th ) {
			$return = false;
		}

		return new \WP_REST_Response( [
			'success'      => $return,
			'postTypeData' => $contentPostType
		], 200 );
	}

	/**
	 * Returns the posts of specific post types.
	 *
	 * @since 4.7.2
	 *
	 * @param  array $postOptions      The post types to get data from.
	 * @param  array $notAllowedFields An array of fields not allowed to be returned.
	 * @return array                   The posts.
	 */
	private static function getPostTypesData( $postOptions, $notAllowedFields = [] ) {
		$posts = aioseo()->core->db->start( 'aioseo_posts as ap' )
			->select( 'ap.*, p.post_title' )
			->join( 'posts as p', 'ap.post_id = p.ID' )
			->whereIn( 'p.post_type', $postOptions )
			->orderBy( 'ap.id' )
			->run()
			->result();

		if ( ! empty( $notAllowedFields ) ) {
			foreach ( $posts as $key => &$p ) {
				$p = array_diff_key( (array) $p, $notAllowedFields );
				if ( count( $p ) <= 2 ) {
					unset( $posts[ $key ] );
				}
			}
		}

		return $posts;
	}

	/**
	 * Returns a CSV string.
	 *
	 * @since 4.7.2
	 *
	 * @param  array $data An array of data to transform into a CSV.
	 * @return string      The CSV string.
	 */
	public static function dataToCsv( $data ) {
		// Get the header row.
		$csvString = implode( ',', array_keys( (array) $data[0] ) ) . "\r\n";

		// Get the content rows.
		foreach ( $data as $row ) {
			$row = (array) $row;
			foreach ( $row as &$value ) {
				if ( aioseo()->helpers->isJsonString( $value ) ) {
					$value = '"' . str_replace( '"', '""', $value ) . '"';
				} elseif ( false !== strpos( (string) $value, ',' ) ) {
					$value = '"' . $value . '"';
				}
			}

			$csvString .= implode( ',', $row ) . "\r\n";
		}

		return $csvString;
	}

	/**
	 * Import other plugin settings.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function importPlugins( $request ) {
		$body    = $request->get_json_params();
		$plugins = ! empty( $body['plugins'] ) ? $body['plugins'] : [];

		foreach ( $plugins as $plugin ) {
			aioseo()->importExport->startImport( $plugin['plugin'], $plugin['settings'] );
		}

		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}

	/**
	 * Executes a given administrative task.
	 *
	 * @since 4.1.2
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function doTask( $request ) {
		$body          = $request->get_json_params();
		$action        = ! empty( $body['action'] ) ? $body['action'] : '';
		$data          = ! empty( $body['data'] ) ? $body['data'] : [];
		$network       = ! empty( $body['network'] ) ? boolval( $body['network'] ) : false;
		$siteId        = ! empty( $body['siteId'] ) ? intval( $body['siteId'] ) : false;
		$siteOrNetwork = empty( $siteId ) ? aioseo()->helpers->getNetworkId() : $siteId; // If we don't have a siteId, we will use the networkId.

		// When on network admin page and no siteId, it is supposed to perform on network level.
		if ( $network && 'clear-cache' === $action && empty( $siteId ) ) {
			aioseo()->core->networkCache->clear();

			return new \WP_REST_Response( [
				'success' => true
			], 200 );
		}

		// Switch to the right blog before processing any task.
		aioseo()->helpers->switchToBlog( $siteOrNetwork );

		switch ( $action ) {
			// General
			case 'clear-cache':
				aioseo()->core->cache->clear();
				break;
			case 'clear-plugin-updates-transient':
				delete_site_transient( 'update_plugins' );
				break;
			case 'readd-capabilities':
				aioseo()->access->addCapabilities();
				break;
			case 'reset-data':
				aioseo()->uninstall->dropData( true );
				aioseo()->internalOptions->database->installedTables = '';
				aioseo()->internalOptions->internal->lastActiveVersion = '4.0.0';
				aioseo()->internalOptions->save( true );
				aioseo()->updates->addInitialCustomTablesForV4();
				break;
			// Sitemap
			case 'clear-image-data':
				aioseo()->sitemap->query->resetImages();
				break;
			// Migrations
			case 'rerun-migrations':
				aioseo()->internalOptions->database->installedTables   = '';
				aioseo()->internalOptions->internal->lastActiveVersion = '4.0.0';
				aioseo()->internalOptions->save( true );
				break;
			case 'rerun-addon-migrations':
				aioseo()->internalOptions->database->installedTables = '';

				foreach ( $data as $sku ) {
					$convertedSku = aioseo()->helpers->dashesToCamelCase( $sku );
					if (
						function_exists( $convertedSku ) &&
						isset( $convertedSku()->internalOptions )
					) {
						$convertedSku()->internalOptions->internal->lastActiveVersion = '0.0';
					}
				}
				break;
			case 'restart-v3-migration':
				Migration\Helpers::redoMigration();
				break;
			// Old Issues
			case 'remove-duplicates':
				aioseo()->updates->removeDuplicateRecords();
				break;
			case 'unescape-data':
				aioseo()->admin->scheduleUnescapeData();
				break;
			// Deprecated Options
			case 'deprecated-options':
				// Check if the user is forcefully wanting to add a deprecated option.
				$allDeprecatedOptions = aioseo()->internalOptions->getAllDeprecatedOptions() ?: [];
				$enableOptions        = array_keys( array_filter( $data ) );
				$enabledDeprecated    = array_intersect( $allDeprecatedOptions, $enableOptions );

				aioseo()->internalOptions->internal->deprecatedOptions = array_values( $enabledDeprecated );
				aioseo()->internalOptions->save( true );
				break;
			case 'aioseo-reset-seoboost-logins':
				aioseo()->writingAssistant->seoBoost->resetLogins();
				break;
			default:
				aioseo()->helpers->restoreCurrentBlog();

				return new \WP_REST_Response( [
					'success' => true,
					'error'   => 'The given action isn\'t defined.'
				], 400 );
		}

		// Revert back to the current blog after processing to avoid conflict with other actions.
		aioseo()->helpers->restoreCurrentBlog();

		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}

	/**
	 * Change Sem Rush Focus Keyphrase default country.
	 *
	 * @since 4.7.5
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function changeSemrushCountry( $request ) {
		$body     = $request->get_json_params();
		$country  = ! empty( $body['value'] ) ? sanitize_text_field( $body['value'] ) : 'US';

		aioseo()->settings->semrushCountry = $country;

		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}
}Notifications.php000066600000010563151146252330010101 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Route class for the API.
 *
 * @since 4.0.0
 */
class Notifications {
	/**
	 * Extend the start date of a notice.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function blogVisibilityReminder() {
		return self::reminder( 'blog-visibility' );
	}

	/**
	 * Extend the start date of a notice.
	 *
	 * @since 4.0.5
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function descriptionFormatReminder() {
		return self::reminder( 'description-format' );
	}

	/**
	 * Extend the start date of a notice.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function installMiReminder() {
		return self::reminder( 'install-mi' );
	}

	/**
	 * Extend the start date of a notice.
	 *
	 * @since 4.2.1
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function installOmReminder() {
		return self::reminder( 'install-om' );
	}

	/**
	 * Extend the start date of a notice.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function installAddonsReminder() {
		return self::reminder( 'install-addons' );
	}

	/**
	 * Extend the start date of a notice.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function installImageSeoReminder() {
		return self::reminder( 'install-aioseo-image-seo' );
	}

	/**
	 * Extend the start date of a notice.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function installLocalBusinessReminder() {
		return self::reminder( 'install-aioseo-local-business' );
	}

	/**
	 * Extend the start date of a notice.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function installNewsSitemapReminder() {
		return self::reminder( 'install-aioseo-news-sitemap' );
	}

	/**
	 * Extend the start date of a notice.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function installVideoSitemapReminder() {
		return self::reminder( 'install-aioseo-video-sitemap' );
	}

	/**
	 * Extend the start date of a notice.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function conflictingPluginsReminder() {
		return self::reminder( 'conflicting-plugins' );
	}

	/**
	 * Extend the start date of a notice.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function migrationCustomFieldReminder() {
		return self::reminder( 'v3-migration-custom-field' );
	}

	/**
	 * Extend the start date of a notice.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function migrationSchemaNumberReminder() {
		return self::reminder( 'v3-migration-schema-number' );
	}

	/**
	 * This allows us to not repeat code over and over.
	 *
	 * @since 4.0.0
	 *
	 * @param  string            $slug The slug of the reminder.
	 * @return \WP_REST_Response       The response.
	 */
	protected static function reminder( $slug ) {
		aioseo()->notices->remindMeLater( $slug );

		return new \WP_REST_Response( [
			'success'       => true,
			'notifications' => Models\Notification::getNotifications()
		], 200 );
	}

	/**
	 * Dismiss notifications.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function dismissNotifications( $request ) {
		$slugs = $request->get_json_params();

		$notifications = aioseo()->core->db
			->start( 'aioseo_notifications' )
			->whereIn( 'slug', $slugs )
			->run()
			->models( 'AIOSEO\\Plugin\\Common\\Models\\Notification' );

		foreach ( $notifications as $notification ) {
			$notification->dismissed = 1;
			$notification->save();
		}

		// Dismiss static notifications.
		if ( in_array( 'notification-review', $slugs, true ) ) {
			update_user_meta( get_current_user_id(), '_aioseo_notification_plugin_review_dismissed', '3' );
		}

		if ( in_array( 'notification-review-delay', $slugs, true ) ) {
			update_user_meta( get_current_user_id(), '_aioseo_notification_plugin_review_dismissed', strtotime( '+1 week' ) );
		}

		return new \WP_REST_Response( [
			'success'       => true,
			'notifications' => Models\Notification::getNotifications()
		], 200 );
	}
}Analyze.php000066600000015146151146252330006675 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

use AIOSEO\Plugin\Common\Models\SeoAnalyzerResult;

/**
 * Route class for the API.
 *
 * @since 4.0.0
 */
class Analyze {
	/**
	 * Analyzes the site for SEO.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function analyzeSite( $request ) {
		$body             = $request->get_json_params();
		$analyzeUrl       = ! empty( $body['url'] ) ? esc_url_raw( urldecode( $body['url'] ) ) : null;
		$refreshResults   = ! empty( $body['refresh'] ) ? (bool) $body['refresh'] : false;
		$analyzeOrHomeUrl = ! empty( $analyzeUrl ) ? $analyzeUrl : home_url();
		$responseCode     = null === aioseo()->core->cache->get( 'analyze_site_code' ) ? [] : aioseo()->core->cache->get( 'analyze_site_code' );
		$responseBody     = null === aioseo()->core->cache->get( 'analyze_site_body' ) ? [] : aioseo()->core->cache->get( 'analyze_site_body' );
		if (
			empty( $responseCode ) ||
			empty( $responseCode[ $analyzeOrHomeUrl ] ) ||
			empty( $responseBody ) ||
			empty( $responseBody[ $analyzeOrHomeUrl ] ) ||
			$refreshResults
		) {
			$token      = aioseo()->internalOptions->internal->siteAnalysis->connectToken;
			$url        = defined( 'AIOSEO_ANALYZE_URL' ) ? AIOSEO_ANALYZE_URL : 'https://analyze.aioseo.com';
			$response   = aioseo()->helpers->wpRemotePost( $url . '/v3/analyze/', [
				'timeout' => 60,
				'headers' => [
					'X-AIOSEO-Key' => $token,
					'Content-Type' => 'application/json'
				],
				'body'    => wp_json_encode( [
					'url' => $analyzeOrHomeUrl
				] ),
			] );

			$responseCode[ $analyzeOrHomeUrl ] = wp_remote_retrieve_response_code( $response );
			$responseBody[ $analyzeOrHomeUrl ] = json_decode( wp_remote_retrieve_body( $response ), true );

			aioseo()->core->cache->update( 'analyze_site_code', $responseCode, 10 * MINUTE_IN_SECONDS );
			aioseo()->core->cache->update( 'analyze_site_body', $responseBody, 10 * MINUTE_IN_SECONDS );
		}

		if ( 200 !== $responseCode[ $analyzeOrHomeUrl ] || empty( $responseBody[ $analyzeOrHomeUrl ]['success'] ) || ! empty( $responseBody[ $analyzeOrHomeUrl ]['error'] ) ) {
			if ( ! empty( $responseBody[ $analyzeOrHomeUrl ]['error'] ) && 'invalid-token' === $responseBody[ $analyzeOrHomeUrl ]['error'] ) {
				aioseo()->internalOptions->internal->siteAnalysis->reset();
			}

			return new \WP_REST_Response( [
				'success'  => false,
				'response' => $responseBody[ $analyzeOrHomeUrl ]
			], 400 );
		}

		if ( $analyzeUrl ) {
			$results = $responseBody[ $analyzeOrHomeUrl ]['results'];
			SeoAnalyzerResult::addResults( [
				'results' => $results,
				'score'   => $responseBody[ $analyzeOrHomeUrl ]['score']
			], $analyzeUrl );

			$result = SeoAnalyzerResult::getCompetitorsResults();

			return new \WP_REST_Response( $result, 200 );
		}

		$results = $responseBody[ $analyzeOrHomeUrl ]['results'];
		SeoAnalyzerResult::addResults( [
			'results' => $results,
			'score'   => $responseBody[ $analyzeOrHomeUrl ]['score']
		] );

		return new \WP_REST_Response( $responseBody[ $analyzeOrHomeUrl ], 200 );
	}

	/**
	 * Deletes the analyzed site for SEO.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function deleteSite( $request ) {
		$body       = $request->get_json_params();
		$analyzeUrl = ! empty( $body['url'] ) ? esc_url_raw( urldecode( $body['url'] ) ) : null;

		SeoAnalyzerResult::deleteByUrl( $analyzeUrl );

		$competitors = aioseo()->internalOptions->internal->siteAnalysis->competitors;

		unset( $competitors[ $analyzeUrl ] );

		// Reset the competitors.
		aioseo()->internalOptions->internal->siteAnalysis->competitors = $competitors;

		return new \WP_REST_Response( $competitors, 200 );
	}

	/**
	 * Analyzes the title for SEO.
	 *
	 * @since 4.1.2
	 *
	 * @param  \WP_REST_Request  $request The REST Request.
	 * @return \WP_REST_Response          The response.
	 */
	public static function analyzeHeadline( $request ) {
		$body                = $request->get_json_params();
		$headline            = ! empty( $body['headline'] ) ? sanitize_text_field( $body['headline'] ) : '';
		$shouldStoreHeadline = ! empty( $body['shouldStoreHeadline'] ) ? rest_sanitize_boolean( $body['shouldStoreHeadline'] ) : false;

		if ( empty( $headline ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => __( 'Please enter a valid headline.', 'all-in-one-seo-pack' )
			], 400 );
		}

		$result = aioseo()->standalone->headlineAnalyzer->getResult( $headline );

		if ( ! $result['analysed'] ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => $result['result']->msg
			], 400 );
		}

		$headlines = aioseo()->internalOptions->internal->headlineAnalysis->headlines;
		$headlines = array_reverse( $headlines, true );

		// Remove a headline from the list if it already exists.
		// This will ensure the new analysis is the first and open/highlighted.
		if ( array_key_exists( $headline, $headlines ) ) {
			unset( $headlines[ $headline ] );
		}

		$headlines[ $headline ] = wp_json_encode( $result );

		$headlines = array_reverse( $headlines, true );

		// Store the headlines with the latest one.
		if ( $shouldStoreHeadline ) {
			aioseo()->internalOptions->internal->headlineAnalysis->headlines = $headlines;
		}

		return new \WP_REST_Response( $headlines, 200 );
	}

	/**
	 * Deletes the analyzed Headline for SEO.
	 *
	 * @since 4.1.6
	 *
	 * @param  \WP_REST_Request  $request The REST Request.
	 * @return \WP_REST_Response          The response.
	 */
	public static function deleteHeadline( $request ) {
		$body     = $request->get_json_params();
		$headline = sanitize_text_field( $body['headline'] );

		$headlines = aioseo()->internalOptions->internal->headlineAnalysis->headlines;

		unset( $headlines[ $headline ] );

		// Reset the headlines.
		aioseo()->internalOptions->internal->headlineAnalysis->headlines = $headlines;

		return new \WP_REST_Response( $headlines, 200 );
	}

	/**
	 * Get Homepage results.
	 *
	 * @since 4.8.3
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function getHomeResults() {
		$results = SeoAnalyzerResult::getResults();

		return new \WP_REST_Response( [
			'success' => true,
			'result'  => $results,
		], 200 );
	}

	/**
	 * Get competitors results.
	 *
	 * @since 4.8.3
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function getCompetitorsResults() {
		$results = SeoAnalyzerResult::getCompetitorsResults();

		return new \WP_REST_Response( [
			'success' => true,
			'result'  => $results,
		], 200 );
	}
}Ping.php000066600000000631151146252330006160 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

/**
 * Route class for the API.
 *
 * @since 4.0.0
 */
class Ping {
	/**
	 * Returns a success if the API is alive.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function ping() {
		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}
}PostsTerms.php000066600000034521151146252330007413 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Route class for the API.
 *
 * @since 4.0.0
 */
class PostsTerms {
	/**
	 * Searches for posts or terms by ID/name.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function searchForObjects( $request ) {
		$body = $request->get_json_params();

		if ( empty( $body['query'] ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No search term was provided.'
			], 400 );
		}
		if ( empty( $body['type'] ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No type was provided.'
			], 400 );
		}

		$searchQuery = esc_sql( aioseo()->core->db->db->esc_like( $body['query'] ) );

		$objects        = [];
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		if ( 'posts' === $body['type'] ) {

			$postTypes = aioseo()->helpers->getPublicPostTypes( true );
			foreach ( $postTypes as $postType ) {
				// Check if post type isn't noindexed.
				if ( $dynamicOptions->searchAppearance->postTypes->has( $postType ) && ! $dynamicOptions->searchAppearance->postTypes->$postType->show ) {
					$postTypes = aioseo()->helpers->unsetValue( $postTypes, $postType );
				}
			}

			$objects = aioseo()->core->db
				->start( 'posts' )
				->select( 'ID, post_type, post_title, post_name' )
				->whereRaw( "( post_title LIKE '%{$searchQuery}%' OR post_name LIKE '%{$searchQuery}%' OR ID = '{$searchQuery}' )" )
				->whereIn( 'post_type', $postTypes )
				->whereIn( 'post_status', [ 'publish', 'draft', 'future', 'pending' ] )
				->orderBy( 'post_title' )
				->limit( 10 )
				->run()
				->result();

		} elseif ( 'terms' === $body['type'] ) {

			$taxonomies = aioseo()->helpers->getPublicTaxonomies( true );
			foreach ( $taxonomies as $taxonomy ) {
				// Check if taxonomy isn't noindexed.
				if ( $dynamicOptions->searchAppearance->taxonomies->has( $taxonomy ) && ! $dynamicOptions->searchAppearance->taxonomies->$taxonomy->show ) {
					$taxonomies = aioseo()->helpers->unsetValue( $taxonomies, $taxonomy );
				}
			}

			$objects = aioseo()->core->db
				->start( 'terms as t' )
				->select( 't.term_id as term_id, t.slug as slug, t.name as name, tt.taxonomy as taxonomy' )
				->join( 'term_taxonomy as tt', 't.term_id = tt.term_id', 'INNER' )
				->whereRaw( "( t.name LIKE '%{$searchQuery}%' OR t.slug LIKE '%{$searchQuery}%' OR t.term_id = '{$searchQuery}' )" )
				->whereIn( 'tt.taxonomy', $taxonomies )
				->orderBy( 't.name' )
				->limit( 10 )
				->run()
				->result();
		}

		if ( empty( $objects ) ) {
			return new \WP_REST_Response( [
				'success' => true,
				'objects' => []
			], 200 );
		}

		$parsed = [];
		foreach ( $objects as $object ) {
			if ( 'posts' === $body['type'] ) {
				$parsed[] = [
					'type'  => $object->post_type,
					'value' => (int) $object->ID,
					'slug'  => $object->post_name,
					'label' => $object->post_title,
					'link'  => get_permalink( $object->ID )
				];
			} elseif ( 'terms' === $body['type'] ) {
				$parsed[] = [
					'type'  => $object->taxonomy,
					'value' => (int) $object->term_id,
					'slug'  => $object->slug,
					'label' => $object->name,
					'link'  => get_term_link( $object->term_id )
				];
			}
		}

		return new \WP_REST_Response( [
			'success' => true,
			'objects' => $parsed
		], 200 );
	}

	/**
	 * Get post data for fetch requests
	 *
	 * @since   4.0.0
	 * @version 4.8.3 Changes the return value to include only the Vue data.
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function getPostData( $request ) {
		$args = $request->get_query_params();

		if ( empty( $args['postId'] ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No post ID was provided.'
			], 400 );
		}

		return new \WP_REST_Response( [
			'success' => true,
			'data'    => aioseo()->helpers->getVueData( 'post', $args['postId'], $args['integrationSlug'] ?? null )
		], 200 );
	}

	/**
	 * Get the first attached image for a post.
	 *
	 * @since 4.1.8
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function getFirstAttachedImage( $request ) {
		$args = $request->get_params();

		if ( empty( $args['postId'] ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No post ID was provided.'
			], 400 );
		}

		// Disable the cache.
		aioseo()->social->image->useCache = false;

		$post = aioseo()->helpers->getPost( $args['postId'] );
		$url  = aioseo()->social->image->getImage( 'facebook', 'attach', $post );

		// Reset the cache property.
		aioseo()->social->image->useCache = true;

		return new \WP_REST_Response( [
			'success' => true,
			'url'     => is_array( $url ) ? $url[0] : $url,
		], 200 );
	}

	/**
	 * Returns the posts custom fields.
	 *
	 * @since 4.0.6
	 *
	 * @param  \WP_Post|int $post The post.
	 * @return string             The custom field content.
	 */
	private static function getAnalysisContent( $post = null ) {
		$analysisContent = apply_filters( 'aioseo_analysis_content', aioseo()->helpers->getPostContent( $post ) );

		return sanitize_post_field( 'post_content', $analysisContent, $post->ID, 'display' );
	}

	/**
	 * Update post settings.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function updatePosts( $request ) {
		$body   = $request->get_json_params();
		$postId = ! empty( $body['id'] ) ? intval( $body['id'] ) : null;

		if ( ! $postId ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Post ID is missing.'
			], 400 );
		}

		$body['id']                  = $postId;
		$body['title']               = ! empty( $body['title'] ) ? sanitize_text_field( $body['title'] ) : null;
		$body['description']         = ! empty( $body['description'] ) ? sanitize_text_field( $body['description'] ) : null;
		$body['keywords']            = ! empty( $body['keywords'] ) ? aioseo()->helpers->sanitize( $body['keywords'] ) : null;
		$body['og_title']            = ! empty( $body['og_title'] ) ? sanitize_text_field( $body['og_title'] ) : null;
		$body['og_description']      = ! empty( $body['og_description'] ) ? sanitize_text_field( $body['og_description'] ) : null;
		$body['og_article_section']  = ! empty( $body['og_article_section'] ) ? sanitize_text_field( $body['og_article_section'] ) : null;
		$body['og_article_tags']     = ! empty( $body['og_article_tags'] ) ? aioseo()->helpers->sanitize( $body['og_article_tags'] ) : null;
		$body['twitter_title']       = ! empty( $body['twitter_title'] ) ? sanitize_text_field( $body['twitter_title'] ) : null;
		$body['twitter_description'] = ! empty( $body['twitter_description'] ) ? sanitize_text_field( $body['twitter_description'] ) : null;

		$error = Models\Post::savePost( $postId, $body );

		if ( ! empty( $error ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Failed update query: ' . $error
			], 401 );
		}

		return new \WP_REST_Response( [
			'success' => true,
			'posts'   => $postId
		], 200 );
	}

	/**
	 * Load post settings from Post screen.
	 *
	 * @since 4.5.5
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function loadPostDetailsColumn( $request ) {
		$body = $request->get_json_params();
		$ids  = ! empty( $body['ids'] ) ? (array) $body['ids'] : [];

		if ( ! $ids ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Post IDs are missing.'
			], 400 );
		}

		$posts = [];
		foreach ( $ids as $postId ) {
			$postTitle      = get_the_title( $postId );
			$headline       = ! empty( $postTitle ) ? sanitize_text_field( $postTitle ) : ''; // We need this to achieve consistency for the score when using special characters in titles
			$headlineResult = aioseo()->standalone->headlineAnalyzer->getResult( $headline );

			$posts[] = [
				'id'                => $postId,
				'titleParsed'       => aioseo()->meta->title->getPostTitle( $postId ),
				'descriptionParsed' => aioseo()->meta->description->getPostDescription( $postId ),
				'headlineScore'     => ! empty( $headlineResult['score'] ) ? (int) $headlineResult['score'] : 0,
			];
		}

		return new \WP_REST_Response( [
			'success' => true,
			'posts'   => $posts
		], 200 );
	}

	/**
	 * Update post settings from Post screen.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function updatePostDetailsColumn( $request ) {
		$body    = $request->get_json_params();
		$postId  = ! empty( $body['postId'] ) ? intval( $body['postId'] ) : null;
		$isMedia = isset( $body['isMedia'] ) ? true : false;

		if ( ! $postId ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Post ID is missing.'
			], 400 );
		}

		$aioseoPost = Models\Post::getPost( $postId );
		$aioseoData = json_decode( wp_json_encode( $aioseoPost ), true );

		if ( $isMedia ) {
			wp_update_post(
				[
					'ID'         => $postId,
					'post_title' => sanitize_text_field( $body['imageTitle'] ),
				]
			);
			update_post_meta( $postId, '_wp_attachment_image_alt', sanitize_text_field( $body['imageAltTag'] ) );
		}

		Models\Post::savePost( $postId, array_replace( $aioseoData, $body ) );

		$lastError = aioseo()->core->db->lastError();
		if ( ! empty( $lastError ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Failed update query: ' . $lastError
			], 401 );
		}

		return new \WP_REST_Response( [
			'success'     => true,
			'posts'       => $postId,
			'title'       => aioseo()->meta->title->getPostTitle( $postId ),
			'description' => aioseo()->meta->description->getPostDescription( $postId )
		], 200 );
	}

	/**
	 * Update post keyphrases.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function updatePostKeyphrases( $request ) {
		$body   = $request->get_json_params();
		$postId = ! empty( $body['postId'] ) ? intval( $body['postId'] ) : null;

		if ( ! $postId ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Post ID is missing.'
			], 400 );
		}

		$thePost = Models\Post::getPost( $postId );

		$thePost->post_id = $postId;
		if ( ! empty( $body['keyphrases'] ) ) {
			$thePost->keyphrases = wp_json_encode( $body['keyphrases'] );
		}
		if ( ! empty( $body['page_analysis'] ) ) {
			$thePost->page_analysis = wp_json_encode( $body['page_analysis'] );
		}
		if ( ! empty( $body['seo_score'] ) ) {
			$thePost->seo_score = sanitize_text_field( $body['seo_score'] );
		}
		$thePost->updated = gmdate( 'Y-m-d H:i:s' );
		$thePost->save();

		$lastError = aioseo()->core->db->lastError();
		if ( ! empty( $lastError ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Failed update query: ' . $lastError
			], 401 );
		}

		return new \WP_REST_Response( [
			'success' => true,
			'post'    => $postId
		], 200 );
	}

	/**
	 * Disable the Primary Term education.
	 *
	 * @since 4.3.6
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function disablePrimaryTermEducation( $request ) {
		$args = $request->get_params();

		if ( empty( $args['postId'] ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No post ID was provided.'
			], 400 );
		}

		$thePost = Models\Post::getPost( $args['postId'] );
		$thePost->options->primaryTerm->productEducationDismissed = true;
		$thePost->save();

		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}

	/**
	 * Disable the link format education.
	 *
	 * @since 4.2.2
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function disableLinkFormatEducation( $request ) {
		$args = $request->get_params();

		if ( empty( $args['postId'] ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No post ID was provided.'
			], 400 );
		}

		$thePost = Models\Post::getPost( $args['postId'] );
		$thePost->options->linkFormat->linkAssistantDismissed = true;
		$thePost->save();

		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}

	/**
	 * Increment the internal link count.
	 *
	 * @since 4.2.2
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function updateInternalLinkCount( $request ) {
		$args  = $request->get_params();
		$body  = $request->get_json_params();
		$count = ! empty( $body['count'] ) ? intval( $body['count'] ) : null;

		if ( empty( $args['postId'] ) || null === $count ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No post ID or count was provided.'
			], 400 );
		}

		$thePost = Models\Post::getPost( $args['postId'] );
		$thePost->options->linkFormat->internalLinkCount = $count;
		$thePost->save();

		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}

	/**
	 * Get the processed content by the given raw content.
	 *
	 * @since 4.5.2
	 *
	 * @param  \WP_REST_Request  $request The REST Request.
	 * @return \WP_REST_Response          The response.
	 */
	public static function processContent( $request ) {
		$args = $request->get_params();
		$body = $request->get_json_params();

		if ( empty( $args['postId'] ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No post ID was provided.'
			], 400 );
		}

		// Check if we can process it using a page builder integration.
		$pageBuilder = aioseo()->helpers->getPostPageBuilderName( $args['postId'] );
		if ( ! empty( $pageBuilder ) ) {
			return new \WP_REST_Response( [
				'success' => true,
				'content' => aioseo()->standalone->pageBuilderIntegrations[ $pageBuilder ]->processContent( $args['postId'], $body['content'] ),
			], 200 );
		}

		// Check if the content was passed, otherwise get it from the post.
		$content = $body['content'] ?? aioseo()->helpers->getPostContent( $args['postId'] );

		return new \WP_REST_Response( [
			'success' => true,
			'content' => apply_filters( 'the_content', $content ),
		], 200 );
	}
}