HEX
Server: LiteSpeed
System: Linux eko108.isimtescil.net 4.18.0-477.21.1.lve.1.el8.x86_64 #1 SMP Tue Sep 5 23:08:35 UTC 2023 x86_64
User: uyarreklamcomtr (11202)
PHP: 7.4.33
Disabled: opcache_get_status
Upload Files
File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/app.tar
Admin/Admin.php000064400000030364151536237000007345 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Admin;

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

/**
 * Handles all general admin code.
 *
 * @since 1.0.0
 */
class Admin {
	/**
	 * The main page slug.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private $pageSlug = 'broken-link-checker';

	/**
	 * The current page.
	 * This gets set as soon as we've identified that we're on a Broken Link Checker page.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private $currentPage = '';

	/**
	 * A list of asset slugs to use.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private $assetSlugs = [
		'pages' => 'src/vue/pages/{page}/main.js'
	];

	/**
	 * The plugin basename.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	public $plugin = '';

	/**
	 * The list of pages.
	 *
	 * @since 1.2.0
	 *
	 * @var array
	 */
	private $pages = [];

	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		if ( ! is_admin() ) {
			return;
		}

		add_action( 'admin_menu', [ $this, 'registerMenu' ] );
		add_action( 'admin_menu', [ $this, 'hideScheduledActionsMenu' ], 999 );
		add_filter( 'language_attributes', [ $this, 'addDirAttribute' ], 3000 );

		add_filter( 'plugin_row_meta', [ $this, 'registerRowMeta' ], 10, 2 );
		add_filter( 'plugin_action_links_' . AIOSEO_BROKEN_LINK_CHECKER_PLUGIN_BASENAME, [ $this, 'registerActionLinks' ], 10, 2 );

		add_action( 'admin_footer', [ $this, 'addAioseoModalPortal' ] );
	}

	/**
	 * Checks whether the current page is a Broken Link Checker page.
	 *
	 * @since 1.0.0
	 *
	 * @return bool Whether the current page is a Broken Link Checker page.
	 */
	public function isBrokenLinkCheckerPage() {
		return ! empty( $this->currentPage );
	}

	/**
	 * Add the dir attribute to the HTML tag.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $output The HTML language attribute.
	 * @return string         The modified HTML language attribute.
	 */
	public function addDirAttribute( $output ) {
		if ( is_rtl() || preg_match( '/dir=[\'"](ltr|rtl|auto)[\'"]/i', (string) $output ) ) {
			return $output;
		}

		return 'dir="ltr" ' . $output;
	}

	/**
	 * Registers the menu.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function registerMenu() {
		$hook = add_menu_page(
			__( 'Broken Links', 'aioseo-broken-link-checker' ),
			__( 'Broken Links', 'aioseo-broken-link-checker' ),
			'aioseo_blc_broken_links_page',
			$this->pageSlug,
			[ $this, 'renderMenuPage' ],
			'data:image/svg+xml;base64,' . base64_encode( aioseoBrokenLinkChecker()->helpers->icon() )
		);

		add_action( "load-{$hook}", [ $this, 'checkCurrentPage' ] );

		$this->registerMenuPages();
	}

	/**
	 * Renders the element that we mount our Vue UI on.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function renderMenuPage() {
		echo '<div id="aioseo-blc-app"></div>';
	}

	/**
	 * Registers our menu pages.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function registerMenuPages() {
		$hook = add_submenu_page(
			$this->pageSlug,
			__( 'SEO Settings', 'aioseo-broken-link-checker' ),
			__( 'SEO Settings', 'aioseo-broken-link-checker' ),
			'aioseo_blc_about_us_page',
			$this->pageSlug . '-seo-settings',
			[ $this, 'renderMenuPage' ]
		);

		$this->pages[] = $this->pageSlug . '-seo-settings';

		add_action( "load-{$hook}", [ $this, 'redirectSeoSettings' ] );

		$hook = add_submenu_page(
			$this->pageSlug,
			__( 'About Us', 'aioseo-broken-link-checker' ),
			__( 'About Us', 'aioseo-broken-link-checker' ),
			'aioseo_blc_about_us_page',
			$this->pageSlug . '-about',
			[ $this, 'renderMenuPage' ]
		);

		$this->pages[] = $this->pageSlug . '-about';

		add_action( "load-{$hook}", [ $this, 'checkCurrentPage' ] );
	}

	/**
	 * Checks if the current page is a Broken Link Checker page and if so, starts enqueing the relevant assets.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function checkCurrentPage() {
		global $admin_page_hooks; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$currentScreen = function_exists( 'get_current_screen' ) ? get_current_screen() : false;

		if ( empty( $currentScreen->id ) || empty( $admin_page_hooks ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			return;
		}

		$pages = [
			'about',
			'links',
			'seo-settings'
		];

		foreach ( $pages as $page ) {
			$addScripts = false;

			if ( 'toplevel_page_broken-link-checker' === $currentScreen->id ) {
				$page       = 'links';
				$addScripts = true;
			}

			if ( strpos( $currentScreen->id, 'broken-link-checker-' . $page ) !== false ) {
				$addScripts = true;
			}

			if ( ! $addScripts ) {
				continue;
			}

			// We don't want other plugins adding notices to our screens. Let's clear them out here.
			remove_all_actions( 'admin_notices' );
			remove_all_actions( 'all_admin_notices' );

			$this->currentPage = $page;
			add_action( 'admin_enqueue_scripts', [ $this, 'enqueueMenuAssets' ], 11 );
			add_filter( 'admin_footer_text', [ $this, 'addFooterText' ] );

			break;
		}
	}

	/**
	 * Enqueues our menu assets, based on the current page.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function enqueueMenuAssets() {
		if ( ! $this->currentPage ) {
			return;
		}

		$scriptHandle = str_replace( '{page}', $this->currentPage, $this->assetSlugs['pages'] );
		aioseoBrokenLinkChecker()->core->assets->load( $scriptHandle, [], aioseoBrokenLinkChecker()->helpers->getVueData( $this->currentPage ) );
	}

	/**
	 * Redirects the SEO Settings menu item to the General Settings in AIOSEO if it is installed and active.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function redirectSeoSettings() {
		if ( function_exists( 'aioseo' ) ) {
			wp_safe_redirect( admin_url( 'admin.php?page=aioseo-settings' ) );
			exit;
		}

		// If AIOSEO isn't active, proceed with loading the menu assets.
		$this->checkCurrentPage();
	}

	/**
	 * Hides the Scheduled Actions menu.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function hideScheduledActionsMenu() {
		// Don't hide it for developers when the main plugin isn't active.
		if ( defined( 'AIOSEO_BROKEN_LINK_CHECKER_DEV' ) && ! function_exists( 'aioseo' ) ) {
			return;
		}

		global $submenu;
		if ( ! isset( $submenu['tools.php'] ) ) {
			return;
		}

		foreach ( $submenu['tools.php'] as $index => $props ) {
			if ( ! empty( $props[2] ) && 'action-scheduler' === $props[2] ) {
				unset( $submenu['tools.php'][ $index ] );

				return;
			}
		}
	}

	/**
	 * Registers our row meta for the plugins page.
	 *
	 * @since 1.0.0
	 *
	 * @param  array  $actions    List of existing actions.
	 * @param  string $pluginFile The plugin file.
	 * @return array              List of action links.
	 */
	public function registerRowMeta( $actions, $pluginFile ) {
		$reviewLabel = str_repeat( '<span class="dashicons dashicons-star-filled" style="font-size: 18px; width:16px; height: 16px; color: #ffb900;"></span>', 5 );

		$actionLinks = [
			'suggest-feature' => [
				// Translators: This is an action link users can click to open a feature request.
				'label' => __( 'Suggest a Feature', 'aioseo-broken-link-checker' ),
				'url'   => aioseoBrokenLinkChecker()->helpers->utmUrl( AIOSEO_BROKEN_LINK_CHECKER_MARKETING_URL . 'blc-feature-suggestion/', 'plugin-row-meta', 'feature' ),
			],
			'review'          => [
				'label' => $reviewLabel,
				'url'   => aioseoBrokenLinkChecker()->helpers->utmUrl( AIOSEO_BROKEN_LINK_CHECKER_MARKETING_URL . 'review-blc', 'plugin-row-meta', 'review' ),
				'title' => sprintf(
					// Translators: 1 - The plugin name ("Broken Link Checker").
					__( 'Rate %1$s', 'aioseo-broken-link-checker' ),
					'Broken Link Checker'
				)
			]
		];

		return $this->parseActionLinks( $actions, $pluginFile, $actionLinks );
	}

	/**
	 * Registers our action links for the plugins page.
	 *
	 * @since 1.0.0
	 *
	 * @param  array  $actions    List of existing actions.
	 * @param  string $pluginFile The plugin file.
	 * @return array              List of action links.
	 */
	public function registerActionLinks( $actions, $pluginFile ) {
		$actionLinks = [
			'support' => [
				// Translators: This is an action link users can click to open our support.
				'label' => __( 'Support', 'aioseo-broken-link-checker' ),
				'url'   => aioseoBrokenLinkChecker()->helpers->utmUrl( AIOSEO_BROKEN_LINK_CHECKER_MARKETING_URL . 'plugin/blc-support', 'plugin-action-links', 'Documentation' ),
			],
			'docs'    => [
				// Translators: This is an action link users can click to open our documentation page.
				'label' => __( 'Documentation', 'aioseo-broken-link-checker' ),
				'url'   => aioseoBrokenLinkChecker()->helpers->utmUrl( AIOSEO_BROKEN_LINK_CHECKER_MARKETING_URL . 'doc-categories/broken-link-checker/', 'plugin-action-links', 'Documentation' ),
			]
		];

		if ( isset( $actions['edit'] ) ) {
			unset( $actions['edit'] );
		}

		return $this->parseActionLinks( $actions, $pluginFile, $actionLinks, 'before' );
	}

	/**
	 * Parses the action links.
	 *
	 * @since 1.0.0
	 *
	 * @param  array  $actions     The actions.
	 * @param  string $pluginFile  The plugin file.
	 * @param  array  $actionLinks The action links.
	 * @param  string $position    The position.
	 * @return array               The parsed actions.
	 */
	private function parseActionLinks( $actions, $pluginFile, $actionLinks = [], $position = 'after' ) {
		if ( empty( $this->plugin ) ) {
			$this->plugin = AIOSEO_BROKEN_LINK_CHECKER_PLUGIN_BASENAME;
		}

		if ( $this->plugin === $pluginFile && ! empty( $actionLinks ) ) {
			foreach ( $actionLinks as $key => $value ) {

				$link = [
					$key => sprintf(
						'<a href="%1$s" %2$s target="_blank">%3$s</a>',
						esc_url( $value['url'] ),
						isset( $value['title'] ) ? 'title="' . esc_attr( $value['title'] ) . '"' : '',
						$value['label']
					)
				];

				$actions = 'after' === $position ? array_merge( $actions, $link ) : array_merge( $link, $actions );
			}
		}

		return $actions;
	}

	/**
	 * Add the div for the modal portal.
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	public function addAioseoModalPortal() {
		if ( ! function_exists( 'aioseo' ) ) {
			echo '<div id="aioseo-modal-portal"></div>';
		}
	}

	/**
	 * Checks whether the current page is a Broken Link Checker menu page.
	 *
	 * @since 1.2.0
	 *
	 * @return bool Whether the current page is a Broken Link Checker menu page.
	 */
	public function isBlcScreen() {
		$currentScreen = aioseoBrokenLinkChecker()->helpers->getCurrentScreen();
		if ( empty( $currentScreen->id ) ) {
			return false;
		}

		$adminPages = array_keys( $this->pages );
		$adminPages = array_map( function( $slug ) {
			if ( 'aioseo' === $slug ) {
				return 'toplevel_page_broken-link-checker';
			}

			return 'broken-link-checker_page_' . $slug;
		}, $adminPages );

		return in_array( $currentScreen->id, $adminPages, true );
	}

	/**
	 * Add footer text to the WordPress admin screens.
	 *
	 * @since 1.2.0
	 *
	 * @return string The footer text.
	 */
	public function addFooterText() {
		$linkText = esc_html__( 'Give us a 5-star rating!', 'aioseo-broken-link-checker' );
		$href     = 'https://wordpress.org/support/plugin/broken-link-checker-seo/reviews/?filter=5#new-post';

		$link1 = sprintf(
			'<a href="%1$s" target="_blank" title="%2$s">&#9733;&#9733;&#9733;&#9733;&#9733;</a>',
			$href,
			$linkText
		);

		$link2 = sprintf(
			'<a href="%1$s" target="_blank" title="%2$s">WordPress.org</a>',
			$href,
			$linkText
		);

		printf(
			// Translators: 1 - The plugin name ("Broken Link Checker"), - 2 - This placeholder will be replaced with star icons, - 3 - "WordPress.org" - 4 - The plugin name ("Broken Link Checker").
			esc_html__( 'Please rate %1$s %2$s on %3$s to help us spread the word. Thank you!', 'aioseo-broken-link-checker' ),
			sprintf( '<strong>%1$s</strong>', esc_html( AIOSEO_BROKEN_LINK_CHECKER_PLUGIN_NAME ) ),
			wp_kses_post( $link1 ),
			wp_kses_post( $link2 )
		);

		// Stop WP Core from outputting its version number and instead add both theirs & ours.
		global $wp_version; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		printf(
			wp_kses_post( '<p class="alignright">%1$s</p>' ),
			sprintf(
				// Translators: 1 - WP Core version number, 2 - BLC version number.
				esc_html__( 'WordPress %1$s | BLC %2$s', 'aioseo-broken-link-checker' ),
				esc_html( $wp_version ), // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				esc_html( AIOSEO_BROKEN_LINK_CHECKER_VERSION )
			)
		);

		remove_filter( 'update_footer', 'core_update_footer' );

		return '';
	}
}Admin/License.php000064400000027104151536237000007675 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Admin;

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Handles license update/removal and related notices.
 *
 * @since 1.0.0
 */
class License {
	/**
	 * The base URL for the licensing API.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private $baseUrl = 'https://blc-licensing.aioseo.com/v1/';

	/**
	 * Options class instance.
	 *
	 * @since 1.0.0
	 *
	 * @var \AIOSEO\BrokenLinkChecker\Options\Options
	 */
	protected $options = null;

	/**
	 * InternalOptions class instance.
	 *
	 * @since 1.0.0
	 *t
	 * @var \AIOSEO\BrokenLinkChecker\Options\InternalOptions
	 */
	protected $internalOptions = null;

	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		$this->internalOptions = aioseoBrokenLinkChecker()->internalOptions;

		add_action( 'init', [ $this, 'checkIfNeedsValidation' ] );
	}

	/**
	 * Checks if we should validate the license key or not.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function checkIfNeedsValidation() {
		if ( ! $this->internalOptions->internal->license->licenseKey ) {
			if ( $this->needsReset() ) {
				$this->internalOptions->internal->license->reset(
					[
						'expires',
						'expired',
						'invalid',
						'disabled',
						'activationsError',
						'connectionError',
						'requestError',
						'level'
					]
				);
			}

			return;
		}

		// Validate the license key every 12 hours.
		$timestamp = $this->internalOptions->internal->license->lastChecked;
		if ( time() < $timestamp ) {
			return;
		}

		$success = $this->activate();
		if ( $success || aioseoBrokenLinkChecker()->core->cache->get( 'failed_update' ) ) {
			aioseoBrokenLinkChecker()->core->cache->delete( 'failed_update' );
			$this->internalOptions->internal->license->lastChecked = strtotime( '+12 hours' );

			return;
		}

		// If update failed, check again after one hour. If the second check fails too, we'll wait 12 hours.
		aioseoBrokenLinkChecker()->core->cache->update( 'failed_update', time() );
		$this->internalOptions->internal->license->lastChecked = strtotime( '+1 hour' );
	}

	/**
	 * Validate the license key.
	 *
	 * @since 1.0.0
	 *
	 * @return bool Whether or not it was activated.
	 */
	public function activate() {
		$this->internalOptions->internal->license->reset(
			[
				'expires',
				'expired',
				'invalid',
				'disabled',
				'activationsError',
				'connectionError',
				'requestError',
				'level'
			]
		);

		$licenseKey = $this->internalOptions->internal->license->licenseKey;
		if ( empty( $licenseKey ) ) {
			return false;
		}

		$site    = aioseoBrokenLinkChecker()->helpers->getSite();
		$domains = [
			'domain' => $site->domain,
			'path'   => $site->path
		];

		$response = $this->sendLicenseRequest( 'activate', $licenseKey, [ $domains ] );

		if ( empty( $response ) ) {
			// Something bad happened, error unknown.
			$this->internalOptions->internal->license->connectionError = true;

			return false;
		}

		if ( ! empty( $response->error ) ) {
			if ( 'missing-key-or-domain' === $response->error ) {
				$this->internalOptions->internal->license->requestError = true;

				return false;
			}

			if ( 'missing-license' === $response->error ) {
				$this->internalOptions->internal->license->invalid = true;

				return false;
			}

			if ( 'disabled' === $response->error ) {
				$this->internalOptions->internal->license->disabled = true;

				return false;
			}

			if ( 'activations' === $response->error ) {
				$this->internalOptions->internal->license->activationsError = true;

				return false;
			}

			if ( 'expired' === $response->error ) {
				$this->internalOptions->internal->license->expires = strtotime( $response->expires );
				$this->internalOptions->internal->license->expired = true;

				return false;
			}
		}

		// Something bad happened, error unknown.
		if ( empty( $response->success ) || empty( $response->level ) || empty( $response->broken_links_count ) ) {
			return false;
		}

		$oldQuota = $this->internalOptions->internal->license->quota;

		$this->internalOptions->internal->license->level   = $response->level;
		$this->internalOptions->internal->license->expires = strtotime( $response->expires );
		$this->internalOptions->internal->license->quota   = intval( $response->broken_links_count );

		// Set the remaining quota if it's never been set or if the user's plan has changed.
		if (
			! $this->internalOptions->internal->license->quotaRemaining ||
			( intval( $response->broken_links_count ) !== (int) $oldQuota )
		) {
			$this->internalOptions->internal->license->quotaRemaining = intval( $response->broken_links_count );
		}

		// Cancel all Link Status scans. The next request will fire off a new one.
		if ( function_exists( 'as_unschedule_all_actions' ) ) {
			as_unschedule_all_actions( aioseoBrokenLinkChecker()->main->linkStatus->actionName );
		}

		return true;
	}

	/**
	 * Deactivate the license key.
	 *
	 * @since 1.0.0
	 *
	 * @return bool Whether or not it was deactivated.
	 */
	public function deactivate() {
		$licenseKey = $this->internalOptions->internal->license->licenseKey;
		if ( empty( $licenseKey ) ) {
			return false;
		}

		$site    = aioseoBrokenLinkChecker()->helpers->getSite();
		$domains = [
			'domain' => $site->domain,
			'path'   => $site->path
		];

		$response = $this->sendLicenseRequest( 'deactivate', $licenseKey, [ $domains ] );

		if ( empty( $response ) ) {
			// Something bad happened, error unknown.
			$this->internalOptions->internal->license->connectionError = true;

			return false;
		}

		if ( ! empty( $response->error ) ) {
			if ( 'missing-key-or-domain' === $response->error || 'not-activated' === $response->error ) {
				$this->internalOptions->internal->license->requestError = true;

				return false;
			}

			if ( 'missing-license' === $response->error ) {
				$this->internalOptions->internal->license->invalid = true;

				return false;
			}

			if ( 'disabled' === $response->error ) {
				$this->internalOptions->internal->license->disabled = true;

				return false;
			}
		}

		$this->internalOptions->internal->license->reset(
			[
				'expires',
				'expired',
				'invalid',
				'disabled',
				'activationsError',
				'connectionError',
				'requestError',
				'level'
			]
		);

		// Cancel all Link Status scans.
		as_unschedule_all_actions( aioseoBrokenLinkChecker()->main->linkStatus->actionName );

		return true;
	}

	/**
	 * Returns the URL to check licenses.
	 *
	 * @since 1.0.0
	 *
	 * @return string The URL.
	 */
	public function getUrl() {
		if ( defined( 'AIOSEO_BROKEN_LINK_CHECKER_LICENSING_URL' ) ) {
			return AIOSEO_BROKEN_LINK_CHECKER_LICENSING_URL;
		}

		return $this->baseUrl;
	}

	/**
	 * Checks to see if the current license is expired.
	 *
	 * @since 1.0.0
	 *
	 * @return bool Whether the license is expired.
	 */
	public function isExpired() {
		$networkIsExpired = false;
		$licenseKey       = $this->internalOptions->internal->license->licenseKey;
		if ( empty( $licenseKey ) ) {
			return $networkIsExpired;
		}

		$expired = $this->internalOptions->internal->license->expired || $this->internalOptions->internal->license->expires < time();
		if ( $expired ) {
			$didActivationAttempt = $this->maybeReactivateExpiredLicense();

			// If we tried to activate the license again, start over. Otherwise, return true.
			return $didActivationAttempt ? $this->isExpired() : true;
		}

		$expires = $this->internalOptions->internal->license->expires;

		return 0 !== $expires && $expires < time();
	}

	/**
	 * Checks to see if the current license is disabled.
	 *
	 * @since 1.0.0
	 *
	 * @return bool Whether the license is disabled.
	 */
	public function isDisabled() {
		$networkIsDisabled = false;
		$licenseKey        = $this->internalOptions->internal->license->licenseKey;
		if ( empty( $licenseKey ) ) {
			return $networkIsDisabled;
		}

		return $this->internalOptions->internal->license->disabled;
	}

	/**
	 * Checks to see if the current license is invalid.
	 *
	 * @since 1.0.0
	 *
	 * @return bool Whether the license is invalid.
	 */
	public function isInvalid() {
		$networkIsInvalid = false;
		$licenseKey       = $this->internalOptions->internal->license->licenseKey;
		if ( empty( $licenseKey ) ) {
			return $networkIsInvalid;
		}

		return $this->internalOptions->internal->license->invalid;
	}

	/**
	 * Checks to see if the current license is active.
	 *
	 * @since 1.0.0
	 *
	 * @return bool Whether the license is active.
	 */
	public function isActive() {
		$networkIsActive = false;
		$licenseKey      = $this->internalOptions->internal->license->licenseKey;
		if ( empty( $licenseKey ) ) {
			return $networkIsActive;
		}

		return ! $this->isExpired() && ! $this->isDisabled() && ! $this->isInvalid();
	}

	/**
	 * Get the license level for the activated license.
	 *
	 * @since 1.0.0
	 *
	 * @return string The license level.
	 */
	public function getLicenseLevel() {
		return $this->internalOptions->internal->license->level;
	}

	/**
	 * Checks if the license data needs to be reset.
	 *
	 * @since 1.0.0
	 *
	 * @return bool Whether the license data needs to be reet.
	 */
	private function needsReset() {
		if ( ! empty( $this->internalOptions->internal->license->licenseKey ) ) {
			return false;
		}

		if ( $this->internalOptions->internal->license->level ) {
			return true;
		}

		if ( $this->internalOptions->internal->license->invalid ) {
			return true;
		}

		if ( $this->internalOptions->internal->license->disabled ) {
			return true;
		}

		$expired = $this->internalOptions->internal->license->expired;
		if ( $expired ) {
			return true;
		}

		$expires = $this->internalOptions->internal->license->expires;

		return 0 !== $expires;
	}

	/**
	 * Sends the license request.
	 *
	 * @since 1.0.0
	 *
	 * @param  string      $type       The type of request, either activate or deactivate.
	 * @param  string      $licenseKey The license key we are using for this request.
	 * @param  array       $domains    List of domains to activate or deactivate.
	 * @return Object|null             The JSON response as an object.
	 */
	public function sendLicenseRequest( $type, $licenseKey, $domains ) {
		$payload = [
			'sku'         => 'aioseo-broken-link-checker',
			'version'     => AIOSEO_BROKEN_LINK_CHECKER_VERSION,
			'php_version' => PHP_VERSION,
			'license'     => $licenseKey,
			'domains'     => $domains,
			'wp_version'  => get_bloginfo( 'version' )
		];

		return aioseoBrokenLinkChecker()->helpers->sendRequest( $this->getUrl() . $type . '/', $payload );
	}

	/**
	 * Checks if the current site is licensed at the network level.
	 *
	 * @since 1.0.0
	 *
	 * @return bool Whether the site is licensed at the network level.
	 */
	public function isNetworkLicensed() {
		if ( ! property_exists( aioseoBrokenLinkChecker(), 'networkLicense' ) ) {
			return false;
		}

		return aioseoBrokenLinkChecker()->networkLicense->isActive();
	}

	/**
	 * Whether the current license plan is the free plan.
	 *
	 * @since 1.0.0
	 *
	 * @return bool
	 */
	public function isFree() {
		return 'free' === strtolower( (string) $this->getLicenseLevel() );
	}

	/**
	 * Checks if the license is expired and attempts to activate it again.
	 *
	 * @since 1.1.0
	 *
	 * @return bool True if an attempt was made to activate the license, false if not.
	 */
	private function maybeReactivateExpiredLicense() {
		// If the license is expired, send out a request to check if it's still expired.
		// We cache this for a few hours so we don't spam the server.
		$transientName = 'expired_license_check';
		if ( aioseoBrokenLinkChecker()->core->cache->get( $transientName ) ) {
			return false;
		}

		$this->activate();
		aioseoBrokenLinkChecker()->core->cache->update( $transientName, true, 4 * HOUR_IN_SECONDS );

		return true;
	}
}Admin/Notices/NotConnected.php000064400000007140151536237000012300 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Admin\Notices;

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

/**
 * Not Connected notice.
 *
 * @since 1.2.1
 */
class NotConnected {
	/**
	 * Class constructor.
	 *
	 * @since 1.2.1
	 */
	public function __construct() {
		add_action( 'wp_ajax_aioseo-blc-dismiss-not-connected', [ $this, 'dismissNotice' ] );
	}

	/**
	 * Go through all the checks to see if we should show the notice.
	 *
	 * @since 1.2.1
	 *
	 * @return void
	 */
	public function maybeShowNotice() {
		// Don't show to users that cannot interact with the plugin.
		if ( ! current_user_can( 'edit_posts' ) || ! current_user_can( 'manage_options' ) ) {
			return;
		}

		if ( aioseoBrokenLinkChecker()->admin->isBlcScreen() ) {
			return;
		}

		// Make sure the user is not connected/licensed.
		if ( aioseoBrokenLinkChecker()->license->isActive() ) {
			return;
		}

		$dismissed = get_user_meta( get_current_user_id(), '_aioseo_blc_not_connected', true );
		if ( ! empty( $dismissed ) && $dismissed > time() ) {
			return;
		}

		$this->showNotice();

		add_action( 'admin_footer', [ $this, 'printScript' ] );
	}

	/**
	 * Actually show the review plugin 2.0.
	 *
	 * @since 1.2.1
	 *
	 * @return void
	 */
	public function showNotice() {
		$string = sprintf(
			// Translators: 1 - The plugin name ("Broken Link Checker").
			__( 'Your site is not connected with %1$s. %2$sConnect now%3$s to start scanning for broken links and fix them to improve your SEO.', 'aioseo-broken-link-checker' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'<strong>' . esc_html( AIOSEO_BROKEN_LINK_CHECKER_PLUGIN_NAME ) . '</strong>',
			'<a href="' . esc_url( admin_url( 'admin.php?page=broken-link-checker#/settings' ) ) . '">',
			'</a>'
		);

		?>
		<div class="notice notice-error aioseo-blc-not-connected is-dismissible">
			<div class="step-3">
				<p><?php echo $string; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p>
			</div>
		</div>
		<?php
	}

	/**
	 * Dismiss the notice.
	 *
	 * @since 1.2.1
	 *
	 * @return void
	 */
	public function dismissNotice() {
		if ( ! isset( $_POST['action'] ) || 'aioseo-blc-dismiss-not-connected' !== $_POST['action'] ) {
			return;
		}

		check_ajax_referer( 'aioseo-blc-dismiss-not-connected', 'nonce' );
		update_user_meta( get_current_user_id(), '_aioseo_blc_not_connected', strtotime( '+1 week' ) );

		wp_send_json_success();
	}

	/**
	 * Print the script for dismissing the notice.
	 *
	 * @since 1.2.1
	 *
	 * @return void
	 */
	public function printScript() {
		// Create a nonce.
		$nonce = wp_create_nonce( 'aioseo-blc-dismiss-not-connected' );
		?>
		<style>
			@keyframes dismissBtnVisible {
				from { opacity: 0.99; }
				to { opacity: 1; }
			}
			.aioseo-blc-not-connected button.notice-dismiss {
				animation-duration: 0.001s;
				animation-name: dismissBtnVisible;
			}
		</style>
		<script>
			window.addEventListener('load', function () {
				dismissNotice = function (dismissBtn) {
					dismissBtn.addEventListener('click', function (event) {
						var httpRequest = new XMLHttpRequest(),
							postData    = ''

						// Build the data to send in our request.
						postData += '&action=aioseo-blc-dismiss-not-connected'
						postData += '&nonce=<?php echo esc_html( $nonce ); ?>'

						httpRequest.open('POST', '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>')
						httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
						httpRequest.send(postData)
					})
				}

				dismissBtn = document.querySelector('.aioseo-blc-not-connected .notice-dismiss')
				dismissNotice(dismissBtn)
			});
		</script>
		<?php
	}
}Admin/Notices/Review.php000064400000016405151536237000011162 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Admin\Notices;

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

/**
 * Review plugin notice.
 *
 * @since 1.2.0
 */
class Review {
	/**
	 * Class constructor.
	 *
	 * @since 1.2.0
	 */
	public function __construct() {
		add_action( 'wp_ajax_aioseo-blc-dismiss-review-plugin-cta', [ $this, 'dismissNotice' ] );
	}

	/**
	 * Go through all the checks to see if we should show the notice.
	 *
	 * @since 1.2.0
	 *
	 * @return void
	 */
	public function maybeShowNotice() {
		// Don't show to users that cannot interact with the plugin.
		if ( ! current_user_can( 'edit_posts' ) ) {
			return;
		}

		if ( aioseoBrokenLinkChecker()->admin->isBlcScreen() ) {
			return;
		}

		// Make sure the user has connected/is licensed.
		if ( ! aioseoBrokenLinkChecker()->license->isActive() ) {
			return;
		}

		$dismissed = get_user_meta( get_current_user_id(), '_aioseo_blc_plugin_review_dismissed', true );
		if ( '3' === $dismissed || '4' === $dismissed ) {
			return;
		}

		if ( ! empty( $dismissed ) && $dismissed > time() ) {
			return;
		}

		// Show once plugin has been active for 2 weeks.
		if ( ! aioseoBrokenLinkChecker()->internalOptions->internal->firstActivated ) {
			aioseoBrokenLinkChecker()->internalOptions->internal->firstActivated = time();
		}

		$activated = aioseoBrokenLinkChecker()->internalOptions->internal->firstActivated( time() );
		if ( $activated > strtotime( '-2 weeks' ) ) {
			return;
		}

		$this->showNotice();

		// Print the script to the footer.
		add_action( 'admin_footer', [ $this, 'printScript' ] );
	}

	/**
	 * Actually show the review plugin 2.0.
	 *
	 * @since 1.2.0
	 *
	 * @return void
	 */
	public function showNotice() {
		$string1 = sprintf(
			// Translators: 1 - The plugin name ("Broken Link Checker").
			__( 'Hey, we noticed you have been using %1$s for some time - that’s awesome! Could you please do us a BIG favor and give it a 5-star rating on WordPress to help us spread the word and boost our motivation?', 'aioseo-broken-link-checker' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'<strong>' . esc_html( AIOSEO_BROKEN_LINK_CHECKER_PLUGIN_NAME ) . '</strong>'
		);

		// Translators: 1 - The plugin name ("Broken Link Checker").
		$string9  = __( 'Ok, you deserve it', 'aioseo-broken-link-checker' );
		$string10 = __( 'Nope, maybe later', 'aioseo-broken-link-checker' );
		$string11 = __( 'I already did', 'aioseo-broken-link-checker' );

		?>
		<div class="notice notice-info aioseo-blc-review-plugin-cta is-dismissible">
			<div class="step-3">
				<p><?php echo $string1; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p>
				<p>
					<?php // phpcs:ignore Generic.Files.LineLength.MaxExceeded ?>
					<a href="https://wordpress.org/support/plugin/broken-link-checker-seo/reviews/?filter=5#new-post" class="aioseo-blc-dismiss-review-notice" target="_blank" rel="noopener noreferrer">
						<?php echo esc_html( $string9 ); ?>
					</a>&nbsp;&bull;&nbsp;
					<a href="#" class="aioseo-blc-dismiss-review-notice-delay" target="_blank" rel="noopener noreferrer">
						<?php echo esc_html( $string10 ); ?>
					</a>&nbsp;&bull;&nbsp;
					<a href="#" class="aioseo-blc-dismiss-review-notice" target="_blank" rel="noopener noreferrer">
						<?php echo esc_html( $string11 ); ?>
					</a>
				</p>
			</div>
		</div>
		<?php
	}

	/**
	 * Print the script for dismissing the notice.
	 *
	 * @since 1.2.0
	 *
	 * @return void
	 */
	public function printScript() {
		// Create a nonce.
		$nonce = wp_create_nonce( 'aioseo-blc-dismiss-review' );
		?>
		<style>
			@keyframes dismissBtnVisible {
				from { opacity: 0.99; }
				to { opacity: 1; }
			}
			.aioseo-blc-review-plugin-cta button.notice-dismiss {
				animation-duration: 0.001s;
				animation-name: dismissBtnVisible;
			}
		</style>
		<script>
			window.addEventListener('load', function () {
				var aioseoBlcSetupButton,
					dismissBtn,
					interval

				aioseoBlcSetupButton = function (dismissBtn) {
					var notice      = document.querySelector('.notice.aioseo-blc-review-plugin-cta'),
						delay       = false,
						relay       = true,
						stepOne     = notice.querySelector('.step-1'),
						stepTwo     = notice.querySelector('.step-2'),
						stepThree   = notice.querySelector('.step-3')

					// Add an event listener to the dismiss button.
					dismissBtn.addEventListener('click', function (event) {
						var httpRequest = new XMLHttpRequest(),
							postData    = ''

						// Build the data to send in our request.
						postData += '&delay=' + delay
						postData += '&relay=' + relay
						postData += '&action=aioseo-blc-dismiss-review-plugin-cta'
						postData += '&nonce=<?php echo esc_html( $nonce ); ?>'

						httpRequest.open('POST', '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>')
						httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
						httpRequest.send(postData)
					})

					notice.addEventListener('click', function (event) {
						if (event.target.matches('.aioseo-blc-review-switch-step-3')) {
							event.preventDefault()
							stepOne.style.display   = 'none'
							stepTwo.style.display   = 'none'
							stepThree.style.display = 'block'
						}
						if (event.target.matches('.aioseo-blc-review-switch-step-2')) {
							event.preventDefault()
							stepOne.style.display   = 'none'
							stepThree.style.display = 'none'
							stepTwo.style.display   = 'block'
						}
						if (event.target.matches('.aioseo-blc-dismiss-review-notice-delay')) {
							event.preventDefault()
							delay = true
							relay = false
							dismissBtn.click()
						}
						if (event.target.matches('.aioseo-blc-dismiss-review-notice')) {
							if ('#' === event.target.getAttribute('href')) {
								event.preventDefault()
							}
							relay = false
							dismissBtn.click()
						}
					})
				}

				dismissBtn = document.querySelector('.aioseo-blc-review-plugin-cta .notice-dismiss')
				if (!dismissBtn) {
					document.addEventListener('animationstart', function (event) {
						if (event.animationName == 'dismissBtnVisible') {
							dismissBtn = document.querySelector('.aioseo-blc-review-plugin-cta .notice-dismiss')
							if (dismissBtn) {
								aioseoBlcSetupButton(dismissBtn)
							}
						}
					}, false)

				} else {
					aioseoBlcSetupButton(dismissBtn)
				}
			});
		</script>
		<?php
	}

	/**
	 * Dismiss the review plugin CTA.
	 *
	 * @since 1.2.0
	 *
	 * @return void
	 */
	public function dismissNotice() {
		// Early exit if we're not on a aioseo-blc-dismiss-review-plugin-cta action.
		if ( ! isset( $_POST['action'] ) || 'aioseo-blc-dismiss-review-plugin-cta' !== $_POST['action'] ) {
			return;
		}

		check_ajax_referer( 'aioseo-blc-dismiss-review', 'nonce' );
		$delay = isset( $_POST['delay'] ) ? 'true' === wp_unslash( $_POST['delay'] ) : false; // phpcs:ignore HM.Security.ValidatedSanitizedInput.InputNotSanitized
		$relay = isset( $_POST['relay'] ) ? 'true' === wp_unslash( $_POST['relay'] ) : false; // phpcs:ignore HM.Security.ValidatedSanitizedInput.InputNotSanitized

		if ( ! $delay ) {
			update_user_meta( get_current_user_id(), '_aioseo_blc_plugin_review_dismissed', $relay ? '4' : '3' );

			wp_send_json_success();

			return;
		}

		update_user_meta( get_current_user_id(), '_aioseo_blc_plugin_review_dismissed', strtotime( '+1 week' ) );

		wp_send_json_success();
	}
}Admin/Notifications.php000064400000023303151536237000011121 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Admin;

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Handles our notifications.
 *
 * @since 1.0.0
 */
class Notifications {
	/**
	 * The URL of the notifications endpoint.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private $url = 'https://blc-plugin-cdn.aioseo.com/wp-content/notifications.json';

	/**
	 * The review notice class instance.
	 *
	 * @since 1.2.0
	 *
	 * @var Notices\Review
	 */
	private $reviewNotice;

	/**
	 * The Not Connected notice class instance.
	 *
	 * @since 1.2.1
	 *
	 * @var Notices\NotConnected
	 */
	private $notConnectedNotice;

	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		add_action( 'aioseo_blc_admin_notifications_update', [ $this, 'update' ] );

		if ( ! is_admin() ) {
			return;
		}

		add_action( 'init', [ $this, 'init' ], 2 );
		add_action( 'admin_notices', [ $this, 'renderNotices' ] );
	}

	/**
	 * Initialize notifications.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function init() {
		// If our tables do not exist, create them now.
		if ( ! aioseoBrokenLinkChecker()->core->db->tableExists( 'aioseo_blc_notifications' ) ) {
			aioseoBrokenLinkChecker()->updates->addInitialTables();

			return;
		}

		$this->checkForUpdates();

		$this->notConnectedNotice = new Notices\NotConnected();
		$this->reviewNotice       = new Notices\Review();
	}

	/**
	 * Renders the notices.
	 *
	 * @since 1.2.0
	 *
	 * @return void
	 */
	public function renderNotices() {
		if ( ! is_admin() ) {
			return;
		}

		$this->notConnectedNotice->maybeShowNotice();
		$this->reviewNotice->maybeShowNotice();
	}

	/**
	 * Checks if we should update our notifications.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	private function checkForUpdates() {
		$nextRun = aioseoBrokenLinkChecker()->core->cache->get( 'admin_notifications_update' );
		if ( null !== $nextRun && time() < $nextRun ) {
			return;
		}

		aioseoBrokenLinkChecker()->actionScheduler->scheduleAsync( 'aioseo_blc_admin_notifications_update' );
		aioseoBrokenLinkChecker()->core->cache->update( 'admin_notifications_update', time() + DAY_IN_SECONDS );
	}

	/**
	 * Pulls in the notifications from our remote endpoint and stores them in the DB.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function update() {
		$notifications = $this->fetch();
		if ( empty( $notifications ) ) {
			return;
		}

		foreach ( $notifications as $notification ) {
			// First, let's check to see if the notification exists. If so, we want to override it.
			$aioseoNotification = aioseoBrokenLinkChecker()->core->db
				->start( 'aioseo_blc_notifications' )
				->where( 'notification_id', $notification->id )
				->run()
				->model( 'AIOSEO\\BrokenLinkChecker\\Models\\Notification' );

			$buttons = [
				'button1' => [
					'label' => ! empty( $notification->btns->main->text ) ? sanitize_text_field( $notification->btns->main->text ) : null,
					'url'   => ! empty( $notification->btns->main->url ) ? esc_url_raw( $notification->btns->main->url ) : null
				],
				'button2' => [
					'label' => ! empty( $notification->btns->alt->text ) ? sanitize_text_field( $notification->btns->alt->text ) : null,
					'url'   => ! empty( $notification->btns->alt->url ) ? esc_url_raw( $notification->btns->alt->url ) : null
				]
			];

			if ( ! $aioseoNotification->exists() ) {
				$aioseoNotification            = new Models\Notification();
				$aioseoNotification->slug      = uniqid();
				$aioseoNotification->dismissed = 0;
			}

			$aioseoNotification->notification_id = $notification->id;
			$aioseoNotification->title           = sanitize_text_field( $notification->title );
			$aioseoNotification->content         = sanitize_text_field( $notification->content );
			$aioseoNotification->type            = ! empty( $notification->notification_type ) ? sanitize_text_field( $notification->notification_type ) : 'info';
			$aioseoNotification->level           = $notification->type;
			$aioseoNotification->start           = ! empty( $notification->start ) ? sanitize_text_field( $notification->start ) : null;
			$aioseoNotification->end             = ! empty( $notification->end ) ? sanitize_text_field( $notification->end ) : null;
			$aioseoNotification->button1_label   = $buttons['button1']['label'];
			$aioseoNotification->button1_action  = $buttons['button1']['url'];
			$aioseoNotification->button2_label   = $buttons['button2']['label'];
			$aioseoNotification->button2_action  = $buttons['button2']['url'];

			$aioseoNotification->save();

			// Trigger the drawer to open.
			aioseoBrokenLinkChecker()->core->cache->update( 'show_notifications_drawer', true );
		}
	}

	/**
	 * Pulls in the notifications from the remote feed.
	 *
	 * @since 1.0.0
	 *
	 * @return array A list of notifications.
	 */
	private function fetch() {
		$response = aioseoBrokenLinkChecker()->helpers->wpRemoteGet( $this->getUrl() );
		if ( is_wp_error( $response ) ) {
			return [];
		}

		$body = wp_remote_retrieve_body( $response );
		if ( empty( $body ) ) {
			return [];
		}

		$notifications = json_decode( $body );
		if ( empty( $notifications ) ) {
			return [];
		}

		return $this->verify( $notifications );
	}

	/**
	 * Verifies a notification to see if it's valid before it is stored.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $notifications List of notifications items to verify.
	 * @return array                List of verified notifications.
	 */
	private function verify( $notifications ) {
		if ( ! is_array( $notifications ) || empty( $notifications ) ) {
			return [];
		}

		$data = [];
		foreach ( $notifications as $notification ) {
			// The content and type should never be empty. If they are, ignore the notification.
			if ( empty( $notification->content ) || empty( $notification->type ) ) {
				continue;
			}

			if ( ! is_array( $notification->type ) ) {
				$notification->type = [ $notification->type ];
			}

			foreach ( $notification->type as $type ) {
				$type = sanitize_text_field( $type );

				// Ignore the notification if not a single type matches.
				if ( ! $this->validateType( $type ) ) {
					continue 2;
				}
			}

			// Ignore the notification if it already expired.
			if ( ! empty( $notification->end ) && time() > strtotime( $notification->end ) ) {
				continue;
			}

			// Ignore the notification if it existed before installing Broken Link Checker.
			// Prevents spamming the user with notifications after activation.
			$activated = aioseoBrokenLinkChecker()->internalOptions->internal->firstActivated( time() );
			if ( ! empty( $notification->start ) && $activated > strtotime( $notification->start ) ) {
				continue;
			}

			$data[] = $notification;
		}

		return $data;
	}

	/**
	 * Validates the notification type.
	 *
	 * @since 1.0.0
	 *
	 * @param  string  $type The notification type we are targeting.
	 * @return bool          Whether the notification is valid.
	 */
	public function validateType( $type ) {
		if ( 'all' === $type ) {
			return true;
		}

		// If we are targeting unlicensed users.
		if ( 'free' === $type && ! aioseoBrokenLinkChecker()->license->isActive() ) {
			return true;
		}

		// If we are targeting licensed users.
		if ( 'licensed' === $type && aioseoBrokenLinkChecker()->license->isActive() ) {
			return true;
		}

		// Store notice if version matches.
		if ( $this->versionMatch( aioseoBrokenLinkChecker()->version, $type ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Checks whether two versions are equal.
	 *
	 * @since 1.0.0
	 *
	 * @param  string       $currentVersion The current version being used.
	 * @param  string|array $compareVersion The version to compare with.
	 * @return bool                         Whether it is a match.
	 */
	private function versionMatch( $currentVersion, $compareVersion ) {
		if ( is_array( $compareVersion ) ) {
			foreach ( $compareVersion as $compare_single ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				$recursiveResult = $this->versionMatch( $currentVersion, $compare_single ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				if ( $recursiveResult ) {
					return true;
				}
			}

			return false;
		}

		$currentParse = explode( '.', $currentVersion );
		if ( strpos( $compareVersion, '-' ) ) {
			$compareParse = explode( '-', $compareVersion );
		} elseif ( strpos( $compareVersion, '.' ) ) {
			$compareParse = explode( '.', $compareVersion );
		} else {
			return false;
		}

		$currentCount = count( $currentParse );
		$compareCount = count( $compareParse );
		for ( $i = 0; $i < $currentCount || $i < $compareCount; $i++ ) {
			if ( isset( $compareParse[ $i ] ) && 'x' === strtolower( $compareParse[ $i ] ) ) {
				unset( $compareParse[ $i ] );
			}

			if ( ! isset( $currentParse[ $i ] ) ) {
				unset( $compareParse[ $i ] );
			} elseif ( ! isset( $compareParse[ $i ] ) ) {
				unset( $currentParse[ $i ] );
			}
		}

		foreach ( $compareParse as $index => $subNumber ) {
			if ( $currentParse[ $index ] !== $subNumber ) {
				return false;
			}
		}

		return true;
	}


	/**
	 * Returns the URL for the notifications endpoint.
	 *
	 * @since 1.0.0
	 *
	 * @return string The URL.
	 */
	private function getUrl() {
		if ( defined( 'AIOSEO_BROKEN_LINK_CHECKER_NOTIFICATIONS_URL' ) ) {
			return AIOSEO_BROKEN_LINK_CHECKER_NOTIFICATIONS_URL;
		}

		return $this->url;
	}

	/**
	 * Extends a notice by a (default) 1 week start date.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $notice The notice name.
	 * @param  string $start  How long to extend the notice.
	 * @return void
	 */
	public function remindMeLater( $notice, $start = '+1 week' ) {
		$notification = Models\Notification::getNotificationByName( $notice );
		if ( ! $notification->exists() ) {
			return;
		}

		$notification->start = gmdate( 'Y-m-d H:i:s', strtotime( $start ) );
		$notification->save();
	}
}Api/Api.php000064400000021006151536237000006500 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Api;

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

/**
 * Handles the registration, validation and authorization of API routes.
 *
 * @since 1.0.0
 */
class Api {
	/**
	 * The REST API namespace.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public $namespace = 'aioseoBrokenLinkChecker/v1';

	/**
	 * The routes we use in the rest API.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private $routes = [
		// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
		'GET'    => [
			'options' => [ 'callback' => [ 'VueSettings', 'getOptions' ], 'access' => 'everyone' ],
			'ping'    => [ 'callback' => [ 'Ping', 'ping' ], 'access' => 'everyone' ]
		],
		'POST'   => [
			'broken-links/scan'            => [ 'callback' => [ 'BrokenLinks', 'getScanPercent' ], 'access' => [ 'aioseo_blc_broken_links_page' ] ],
			'edit-row'                     => [ 'callback' => [ 'EditRow', 'update' ], 'access' => [ 'aioseo_blc_broken_links_page' ] ],
			'license/activate'             => [ 'callback' => [ 'License', 'activate' ], 'access' => [ 'aioseo_blc_broken_links_page' ] ],
			'license/deactivate'           => [ 'callback' => [ 'License', 'deactivate' ], 'access' => [ 'aioseo_blc_broken_links_page' ] ],
			'link-status-table'            => [ 'callback' => [ 'LinkStatusTable', 'fetchData' ], 'access' => [ 'aioseo_blc_broken_links_page' ] ],
			'link-status-table/bulk'       => [ 'callback' => [ 'LinkStatusTable', 'bulk' ], 'access' => [ 'aioseo_blc_broken_links_page' ] ],
			'link-status-table/deletePost' => [ 'callback' => [ 'LinkStatusTable', 'deletePost' ], 'access' => [ 'aioseo_blc_broken_links_page' ] ],
			'link-status-table/dismiss'    => [ 'callback' => [ 'LinkStatusTable', 'dismiss' ], 'access' => [ 'aioseo_blc_broken_links_page' ] ],
			'link-status-table/recheck'    => [ 'callback' => [ 'LinkStatusTable', 'recheck' ], 'access' => [ 'aioseo_blc_broken_links_page' ] ],
			'link-status-table/undismiss'  => [ 'callback' => [ 'LinkStatusTable', 'undismiss' ], 'access' => [ 'aioseo_blc_broken_links_page' ] ],
			'link-status-table/unlink'     => [ 'callback' => [ 'LinkStatusTable', 'unlink' ], 'access' => [ 'aioseo_blc_broken_links_page' ] ],
			'links-table'                  => [ 'callback' => [ 'LinksTable', 'fetchData' ], 'access' => [ 'aioseo_blc_broken_links_page' ] ],
			'links-table/bulk'             => [ 'callback' => [ 'LinksTable', 'bulk' ], 'access' => [ 'aioseo_blc_broken_links_page' ] ],
			'links-table/unlink'           => [ 'callback' => [ 'LinksTable', 'unlink' ], 'access' => [ 'aioseo_blc_broken_links_page' ] ],
			'link-status-detail'           => [ 'callback' => [ 'LinkStatusDetail', 'getLinkStatusData' ], 'access' => [ 'aioseo_blc_broken_links_page' ] ],
			'notifications/dismiss'        => [ 'callback' => [ 'Notifications', 'dismissNotifications' ], 'access' => 'any' ],
			'objects'                      => [ 'callback' => [ 'PostsTerms', 'searchForObjects' ], 'access' => [ 'aioseo_blc_broken_links_page' ] ],
			'options'                      => [ 'callback' => [ 'VueSettings', 'saveChanges' ], 'access' => 'aioseo_blc_broken_links_page' ],
			'plugins/deactivate'           => [ 'callback' => [ 'Plugins', 'deactivatePlugins' ], 'access' => 'install_plugins' ],
			'plugins/install'              => [ 'callback' => [ 'Plugins', 'installPlugins' ], 'access' => 'install_plugins' ],
			'redirects/url'                => [ 'callback' => [ 'Redirects', 'getRedirectUrl' ], 'access' => [ 'aioseo_blc_broken_links_page' ] ],
			'settings/toggle-card'         => [ 'callback' => [ 'VueSettings', 'toggleCard' ], 'access' => 'aioseo_blc_broken_links_page' ],
			'settings/toggle-radio'        => [ 'callback' => [ 'VueSettings', 'toggleRadio' ], 'access' => 'aioseo_blc_broken_links_page' ],
			'settings/items-per-page'      => [ 'callback' => [ 'VueSettings', 'changeItemsPerPage' ], 'access' => 'aioseo_blc_broken_links_page' ]
		],
		'DELETE' => [
			'post' => [ 'callback' => [ 'Post', 'deletePost' ], 'access' => [ 'aioseo_blc_broken_links_page' ] ]
		]
		// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	];

	/**
	 * Class contructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		add_filter( 'rest_allowed_cors_headers', [ $this, 'allowedHeaders' ] );
		add_action( 'rest_api_init', [ $this, 'registerRoutes' ] );
	}

	/**
	 * Registers the API routes.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function registerRoutes() {
		$class = new \ReflectionClass( get_called_class() );
		foreach ( $this->routes as $method => $data ) {
			foreach ( $data as $route => $options ) {
				register_rest_route(
					$this->namespace,
					$route,
					[
						'methods'             => $method,
						'permission_callback' => empty( $options['permissions'] ) ? [ $this, 'validRequest' ] : [ $this, $options['permissions'] ],
						'callback'            => is_array( $options['callback'] )
							? [
								(
									! empty( $options['callback'][2] )
										? $options['callback'][2] . '\\' . $options['callback'][0]
										: (
											class_exists( $class->getNamespaceName() . '\\' . $options['callback'][0] )
												? $class->getNamespaceName() . '\\' . $options['callback'][0]
												: __NAMESPACE__ . '\\' . $options['callback'][0]
										)
								),
								$options['callback'][1]
							]
							: [ $this, $options['callback'] ]
					]
				);
			}
		}
	}

	/**
	 * Sets headers that are allowed for our API routes.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $allowHeaders The allowed request headers.
	 * @return array               The allowed request headers.
	 */
	public function allowedHeaders( $allowHeaders ) {
		if ( ! array_search( 'X-WP-Nonce', $allowHeaders, true ) ) {
			$allowHeaders[] = 'X-WP-Nonce';
		}

		return $allowHeaders;
	}

	/**
	 * Determine if the user is logged in and has the proper permissions.
	 *
	 * @since 1.0.0
	 *
	 * @param  \WP_REST_Request $request The REST Request.
	 * @return bool                      Whether the user is allowed access to the route.
	 */
	public function validRequest( $request ) {
		return is_user_logged_in() && $this->validateAccess( $request );
	}

	/**
	 * Validates access for the routes.
	 *
	 * @since 1.0.0
	 *
	 * @param  \WP_REST_Request $request The REST Request.
	 * @return bool                      Whether the user is allowed access to the route.
	 */
	private function validateAccess( $request ) {
		$routeData = $this->getRouteData( $request );
		if ( empty( $routeData ) || empty( $routeData['access'] ) ) {
			return false;
		}

		// Admins users always have access.
		if ( aioseoBrokenLinkChecker()->access->isAdmin() ) {
			return true;
		}

		switch ( $routeData['access'] ) {
			case 'everyone':
				// All users are able to access the route.
				return true;
			case 'any':
				// Users with any Broken Link Checker permission can access the route.
				$user = wp_get_current_user();
				foreach ( $user->get_role_caps() as $capability => $enabled ) {
					if ( $enabled && preg_match( '/^aioseo_blc_/', (string) $capability ) ) {
						return true;
					}
				}

				return false;
			default:
				// The user has access if he has any of the required capabilities.
				if ( ! is_array( $routeData['access'] ) ) {
					$routeData['access'] = [ $routeData['access'] ];
				}

				foreach ( $routeData['access'] as $access ) {
					if ( current_user_can( $access ) ) {
						return true;
					}
				}

				return false;
		}
	}

	/**
	 * Returns the data for the route that is being accessed.
	 *
	 * @since 1.0.0
	 *
	 * @param  \WP_REST_Request $request The REST Request.
	 * @return array                     The route data.
	 */
	private function getRouteData( $request ) {
		// NOTE: Since WordPress uses case-insensitive patterns to match routes,
		// we are forcing everything to lower case to ensure we have the proper route.
		// This prevents users with lower privileges from accessing routes they shouldn't.
		$route     = aioseoBrokenLinkChecker()->helpers->toLowercase( $request->get_route() );
		$route     = untrailingslashit( str_replace( '/' . $this->namespace . '/', '', $route ) );
		$routeData = isset( $this->routes[ $request->get_method() ][ $route ] ) ? $this->routes[ $request->get_method() ][ $route ] : [];

		// No direct route name, let's try the regexes.
		if ( empty( $routeData ) ) {
			foreach ( $this->routes[ $request->get_method() ] as $routeRegex => $routeInfo ) {
				$routeRegex = str_replace( '@', '\@', $routeRegex );
				if ( preg_match( "@{$routeRegex}@", (string) $route ) ) {
					$routeData = $routeInfo;
					break;
				}
			}
		}

		return $routeData;
	}
}Api/BrokenLinks.php000064400000002214151536237000010210 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Api;

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Handles all general Broken Links report related routes.
 *
 * @since 1.1.0
 */
class BrokenLinks {
	/**
	 * Returns the scan percent completed.
	 *
	 * @since 1.1.0
	 *
	 * @param  \WP_REST_Request  $request The request
	 * @return \WP_REST_Response          The response.
	 */
	public static function getScanPercent( $request ) {
		$body   = $request->get_json_params();
		$scan = ! empty( $body['scan'] ) ? sanitize_text_field( $body['scan'] ) : '';
		if ( empty( $scan ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No scan name given.'
			], 400 );
		}

		$percentage = 0;
		switch ( $scan ) {
			case 'links':
				$percentage = aioseoBrokenLinkChecker()->main->links->data->getScanPercentage();
				break;
			case 'linkStatuses':
				$percentage = aioseoBrokenLinkChecker()->main->linkStatus->data->getScanPercentage();
				break;
			default:
				break;
		}

		return new \WP_REST_Response( [
			'success' => true,
			'percent' => $percentage
		], 200 );
	}
}Api/CommonTableActions.php000064400000022736151536237000011523 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Api;

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Handles all common table action handlers.
 *
 * @since 1.1.0
 */
abstract class CommonTableActions {
	/**
	 * Unlinks the given link.
	 *
	 * @since 1.1.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function unlink( $request ) {
		$body         = $request->get_json_params();
		$linkStatusId = ! empty( $body['linkStatusId'] ) ? intval( $body['linkStatusId'] ) : null;
		$linkId       = ! empty( $body['linkId'] ) ? intval( $body['linkId'] ) : null;
		if ( empty( $linkStatusId ) && empty( $linkId ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No link status ID or link ID given.'
			], 400 );
		}

		if ( ! empty( $linkStatusId ) ) {
			$links = Models\Link::getByLinkStatusId( $linkStatusId );
			foreach ( $links as $link ) {
				self::removeLink( $link->id );
			}

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

		$success = self::removeLink( $linkId );
		if ( empty( $success ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Link could not be removed.'
			], 400 );
		}

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

	/**
	 * Rechecks the given links.
	 *
	 * @since   1.0.0
	 * @version 1.1.0 Moved from BrokenLinks to TableActions and add support for bulk-checking rows.
	 *
	 * @param  array       $linkStatusRows The Link Status rows.
	 * @return object|bool                 The response or false if the links could not be checked.
	 */
	protected static function recheckLinks( $linkStatusRows ) {
		$linkStatusIds = array_map( function( $linkStatusRow ) {
			return $linkStatusRow['id'];
		}, $linkStatusRows );

		$linkStatuses = Models\LinkStatus::getByIds( $linkStatusIds );
		if ( empty( $linkStatuses ) ) {
			return false;
		}

		$rows = [];
		foreach ( $linkStatuses as $linkStatus ) {
			$rows[ $linkStatus->id ] = $linkStatus->url;
		}

		$requestBody = array_merge(
			aioseoBrokenLinkChecker()->main->linkStatus->data->getBaseData(),
			[ 'rows' => $rows ]
		);

		$response     = aioseoBrokenLinkChecker()->main->linkStatus->doPostRequest( 'recheck-bulk', $requestBody );
		$responseCode = (int) wp_remote_retrieve_response_code( $response );
		$responseBody = json_decode( wp_remote_retrieve_body( $response ) );
		if ( is_wp_error( $response ) && 200 !== $responseCode || empty( $responseBody->success ) || empty( $responseBody->rows ) ) {
			return false;
		}

		foreach ( $responseBody->rows as $row ) {
			// Parse the data into a useable format and then save the updated results.
			aioseoBrokenLinkChecker()->main->linkStatus->parseResultsHelper( $row );
		}

		return $responseBody;
	}

	/**
	 * Updates a given link with a new anchor and/or URL.
	 *
	 * @since 1.1.0
	 *
	 * @param  int    $linkId    The Link ID.
	 * @param  string $newAnchor The new anchor.
	 * @param  string $newUrl    The new URL.
	 * @return bool              Whether the Link was updated.
	 */
	protected static function updateLink( $linkId, $newAnchor = '', $newUrl = '' ) {
		$link = Models\Link::getById( $linkId );
		if ( ! $link->exists() ) {
			return false;
		}

		$post = get_post( $link->post_id );
		if ( ! is_a( $post, 'WP_Post' ) ) {
			return false;
		}

		if ( empty( $newAnchor ) && empty( $newUrl ) ) {
			return false;
		}

		// First, update the link in the phrase.
		$oldAnchor     = aioseoBrokenLinkChecker()->helpers->escapeRegex( $link->anchor );
		$oldUrl        = aioseoBrokenLinkChecker()->helpers->escapeRegex( $link->url );
		$escapedAnchor = aioseoBrokenLinkChecker()->helpers->escapeRegexReplacement( $newAnchor ?: $link->anchor );
		$escapedUrl    = aioseoBrokenLinkChecker()->helpers->escapeRegexReplacement( $newUrl ?: $link->url );

		$newPhraseHtml = preg_replace( "/(<a.*?href=\")($oldUrl)(\".*?>[\s\w]*?)(<[^>]+>)?($oldAnchor)(<\/[^>]+>)?([\s\w]*?<\/a>)/is", "$1$escapedUrl$3$4$escapedAnchor$6$7", $link->phrase_html );

		$success = self::updateLinkInContent( $post, $link, $newPhraseHtml );
		if ( ! $success ) {
			// It's possible that the update failed because the original/old URL is relative in the phrase HTML.
			// In that case, make the old URL relative to match it.
			// This is needed because we make URLs absolute before storing them in the DB.
			$relativeUrl = self::makeUrlRelative( $link->url );
			if ( $relativeUrl !== $link->url ) {
				$oldUrl        = aioseoBrokenLinkChecker()->helpers->escapeRegex( $relativeUrl );
				$newPhraseHtml = preg_replace( "/(<a.*?href=\")($oldUrl)(\".*?>[\s\w]*?)(<[^>]+>)?($oldAnchor)(<\/[^>]+>)?([\s\w]*?<\/a>)/is", "$1$escapedUrl$3$4$escapedAnchor$6$7", $link->phrase_html ); // phpcs:ignore Generic.Files.LineLength.MaxExceeded

				$success = self::updateLinkInContent( $post, $link, $newPhraseHtml );
			}
		}

		return $success;
	}

	/**
	 * Removes a given link.
	 *
	 * @since   1.0.0
	 * @version 1.1.0 Moved from BrokenLinks to TableActions.
	 *
	 * @param  int  $linkId The Link ID.
	 * @return bool         Whether the Link was unlinked.
	 */
	protected static function removeLink( $linkId ) {
		$link = Models\Link::getById( $linkId );
		if ( ! $link->exists() ) {
			return false;
		}

		$post = get_post( $link->post_id );
		if ( ! is_a( $post, 'WP_Post' ) ) {
			return false;
		}

		// First, remove the link in the phrase.
		$escapedAnchor = aioseoBrokenLinkChecker()->helpers->escapeRegex( $link->anchor );
		$newPhraseHtml = preg_replace( "/<a.*?>([\s\w<>]*?{$escapedAnchor}[\s\w<>\/]*?)<\/a>/is", '$1', (string) aioseoBrokenLinkChecker()->helpers->escapeRegexReplacement( $link->phrase_html ) );

		if ( self::checkIsRelativeUrl( $link->url ) ) {
			$escapedUrl    = aioseoBrokenLinkChecker()->helpers->escapeRegex( $link->url );
			$newPhraseHtml = preg_replace( "/<a.*?href=\"{$escapedUrl}\".*?>[\s\w<>]*?{$escapedAnchor}[\s\w<>\/]*?<\/a>/is", $escapedAnchor, $newPhraseHtml );
		}

		return self::updateLinkInContent( $post, $link, $newPhraseHtml, true );
	}

	/**
	 * Adds, updates or removes a link in the content.
	 *
	 * @since 1.2.3
	 *
	 * @param  \WP_Post $post          The post object.
	 * @param  object   $link          The link object.
	 * @param  string   $newPhraseHtml The new phrase HTML.
	 * @param  bool     $isDeletion    Whether the link is being deleted.
	 * @return bool                    Whether the link was updated/deleted.
	 */
	private static function updateLinkInContent( $post, $link, $newPhraseHtml, $isDeletion = false ) {
		$postContent   = str_replace( '&nbsp;', ' ', (string) $post->post_content );
		$oldPhraseHtml = aioseoBrokenLinkChecker()->helpers->escapeRegex( $link->phrase_html );
		$pattern       = "/$oldPhraseHtml/i";

		$postContent = preg_replace( $pattern, $newPhraseHtml, (string) $postContent );

		// If the phrase is still there and we're deleting, attempt to remove it without the phrase if it occurs just once.
		if ( $isDeletion && preg_match( $pattern, $postContent ) ) {
			// Check if the post has just one occurence of this link.
			$escapedAnchor = aioseoBrokenLinkChecker()->helpers->escapeRegex( $link->anchor );
			$escapedUrl    = aioseoBrokenLinkChecker()->helpers->escapeRegex( $link->url );
			$pattern2      = "/<a.*?href=\"{$escapedUrl}\".*?>[\s\w<>]*?{$escapedAnchor}[\s\w<>\/]*?<\/a>/is";
			preg_match_all( $pattern2, $postContent, $matches );

			// If there's just one match, remove it without the phrase.
			if ( isset( $matches[0] ) && 1 === count( $matches[0] ) ) {
				$escapedAnchorReplacement = aioseoBrokenLinkChecker()->helpers->escapeRegexReplacement( $link->anchor );
				$postContent              = preg_replace( $pattern2, $escapedAnchorReplacement, $postContent );
			}
		}

		// Check again. If the phrase is still the same, bail.
		if ( preg_match( $pattern, $postContent ) ) {
			return false;
		}

		// Reset modified date when the post is updated if the option is enabled.
		$limitModifiedDate = aioseoBrokenLinkChecker()->options->general->linkTweaks->limitModifiedDate;
		if ( $limitModifiedDate ) {
			add_filter( 'wp_insert_post_data', function ( $data ) use ( $post ) {
				$data['post_modified']     = $post->post_modified;
				$data['post_modified_gmt'] = $post->post_modified_gmt;

				return $data;
			}, 99999, 1 );
		}

		// Now, update the post with the modified post content.
		$error = wp_update_post( [
			'ID'           => $link->post_id,
			'post_content' => $postContent
		], true );

		if ( 0 === $error || is_a( $error, 'WP_Error' ) ) {
			return false;
		}

		// Indicate that the post needs to be rescanned.
		aioseoBrokenLinkChecker()->main->links->postsToRescan[] = $link->post_id;

		// The "save_post" callback will trigger a rescan of the post, so we can delete the existing Link record.
		$link->delete();

		return true;
	}

	/**
	 * Checks if the given URL is relative.
	 *
	 * @since 1.2.3
	 *
	 * @param  string $url The URL to check.
	 * @return bool        Whether the URL is relative.
	 */
	private static function checkIsRelativeUrl( $url ) {
		$parsedUrl = wp_parse_url( $url );
		if ( ! $parsedUrl ) {
			return false;
		}

		return empty( $parsedUrl['scheme'] ) && empty( $parsedUrl['host'] );
	}

	/**
	 * Makes the given URL relative.
	 *
	 * @since 1.2.3
	 *
	 * @param  string $url The URL to make relative.
	 * @return string      The relative URL.
	 */
	private static function makeUrlRelative( $url ) {
		$parsedUrl = wp_parse_url( $url );
		if ( ! $parsedUrl ) {
			return $url;
		}

		return ! empty( $parsedUrl['path'] ) ? $parsedUrl['path'] : $url;
	}
}Api/EditRow.php000064400000003251151536237000007346 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Api;

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Handles link status/link row edit updates.
 *
 * @since 1.1.0
 */
class EditRow extends CommonTableActions {
	/**
	 * Edits the given link status/link row.
	 *
	 * @since 1.1.0
	 *
	 * @param  \WP_REST_Request  $request The request
	 * @return \WP_REST_Response          The response.
	 */
	public static function update( $request ) {
		$body         = $request->get_json_params();
		$linkStatusId = ! empty( $body['linkStatusId'] ) ? intval( $body['linkStatusId'] ) : null;
		$linkId       = ! empty( $body['linkId'] ) ? intval( $body['linkId'] ) : null;
		if ( empty( $linkStatusId ) && empty( $linkId ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No link status or link ID ID was provided.'
			], 400 );
		}

		$newUrl    = ! empty( $body['url'] ) ? sanitize_text_field( $body['url'] ) : '';
		$newAnchor = ! empty( $body['anchor'] ) ? sanitize_text_field( $body['anchor'] ) : '';
		if ( empty( $newUrl ) && empty( $newAnchor ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No new URL or anchor was provided.'
			], 400 );
		}

		// If a link status ID was provided, then we need to update the URL for each link related to this link status.
		if ( $linkStatusId ) {
			$links = Models\Link::getByLinkStatusId( $linkStatusId );
			foreach ( $links as $link ) {
				self::updateLink( $link->id, '', $newUrl );
			}
		}

		if ( $linkId ) {
			self::updateLink( $linkId, $newAnchor, $newUrl );
		}

		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}
}Api/License.php000064400000006471151536237000007362 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Api;

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Handles license update/removal.
 *
 * @since 1.0.0
 */
class License {
	/**
	 * Activates the license key.
	 *
	 * @since 1.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function activate( $request ) {
		$body       = $request->get_json_params();
		$network    = is_multisite() && ! empty( $body['network'] ) ? (bool) $body['network'] : false;
		$licenseKey = ! empty( $body['licenseKey'] ) ? sanitize_text_field( $body['licenseKey'] ) : null;
		if ( empty( $licenseKey ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No license key given.'
			], 400 );
		}

		$internalOptions = aioseoBrokenLinkChecker()->internalOptions;
		$license         = aioseoBrokenLinkChecker()->license;
		if ( $network ) {
			$internalOptions = aioseoBrokenLinkChecker()->internalNetworkOptions;
			$license         = aioseoBrokenLinkChecker()->networkLicense;
		}

		$internalOptions->internal->license->licenseKey = $licenseKey;
		$activated                                      = $license->activate();

		if ( $activated ) {
			// Force WordPress to check for updates.
			delete_site_transient( 'update_plugins' );

			// Scan for some posts to fill the report.
			aioseoBrokenLinkChecker()->actionScheduler->scheduleAsync( 'aioseo_blc_links_scan' );
		} else {
			$internalOptions->internal->license->licenseKey = null;

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

		aioseoBrokenLinkChecker()->notifications->init();

		return new \WP_REST_Response( [
			'success'       => true,
			'licenseData'   => $internalOptions->internal->license->all(),
			'notifications' => Models\Notification::getNotifications()
		], 200 );
	}

	/**
	 * Deactivates the license key.
	 *
	 * @since 1.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function deactivate( $request ) {
		$body    = $request->get_json_params();
		$network = is_multisite() && ! empty( $body['network'] ) ? (bool) $body['network'] : false;

		$internalOptions = aioseoBrokenLinkChecker()->internalOptions;
		$license         = aioseoBrokenLinkChecker()->license;
		if ( $network ) {
			$internalOptions = aioseoBrokenLinkChecker()->internalNetworkOptions;
			$license         = aioseoBrokenLinkChecker()->networkLicense;
		}

		$deactivated                                    = $license->deactivate();
		$internalOptions->internal->license->licenseKey = null;

		if ( $deactivated ) {
			// Force WordPress to check for updates.
			delete_site_transient( 'update_plugins' );

			$internalOptions->internal->license->reset(
				[
					'expires',
					'expired',
					'invalid',
					'disabled',
					'activationsError',
					'connectionError',
					'requestError',
					'level'
				]
			);
		} else {
			return new \WP_REST_Response( [
				'success' => false
			], 400 );
		}

		aioseoBrokenLinkChecker()->notifications->init();

		return new \WP_REST_Response( [
			'success'       => true,
			'licenseData'   => $internalOptions->internal->license->all(),
			'notifications' => Models\Notification::getNotifications()
		], 200 );
	}
}Api/LinkStatusDetail.php000064400000001667151536237000011226 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Api;

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Handles link status detail page routes.
 *
 * @since 1.1.0
 */
class LinkStatusDetail {
	/**
	 * Returns the Link Status data for the Link Detail page.
	 *
	 * @since 1.1.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function getLinkStatusData( $request ) {
		$body         = $request->get_json_params();
		$linkStatusId = ! empty( $body['linkStatusId'] ) ? intval( $body['linkStatusId'] ) : null;
		if ( empty( $linkStatusId ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No link status ID was provided.'
			], 400 );
		}

		return new \WP_REST_Response( [
			'success'    => true,
			'linkStatus' => Models\LinkStatus::getById( $linkStatusId )
		], 200 );
	}
}Api/LinkStatusTable.php000064400000015134151536237000011045 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Api;

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Handles link related routes.
 *
 * @since 1.1.0
 */
class LinkStatusTable extends CommonTableActions {
	/**
	 * Returns the data for the Broken Links Report.
	 *
	 * @since 1.1.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function fetchData( $request ) {
		$body       = $request->get_json_params();
		$limit      = ! empty( $body['limit'] ) ? intval( $body['limit'] ) : 20;
		$offset     = ! empty( $body['offset'] ) ? intval( $body['offset'] ) : 0;
		$searchTerm = ! empty( $body['searchTerm'] ) ? sanitize_text_field( $body['searchTerm'] ) : null;
		$filter     = ! empty( $body['filter'] ) ? sanitize_text_field( $body['filter'] ) : 'all';
		$orderBy    = ! empty( $body['orderBy'] ) ? sanitize_text_field( $body['orderBy'] ) : 'id';
		$orderDir   = ! empty( $body['orderDir'] ) && ! empty( $body['orderBy'] ) ? strtoupper( sanitize_text_field( $body['orderDir'] ) ) : 'DESC';

		return new \WP_REST_Response( [
			'success'      => true,
			'linkStatuses' => aioseoBrokenLinkChecker()->helpers->getLinkStatusesData( $limit, $offset, $searchTerm, $filter, $orderBy, $orderDir )
		], 200 );
	}

	/**
	 * Executes the given bulk action on the given rows.
	 *
	 * @since 1.1.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function bulk( $request ) {
		$body   = $request->get_json_params();
		$action = ! empty( $body['action'] ) ? sanitize_text_field( $body['action'] ) : null;
		$rows   = ! empty( $body['rows'] ) ? $body['rows'] : null;
		if ( empty( $action ) || empty( $rows ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No action or rows given.'
			], 400 );
		}

		switch ( $action ) {
			case 'recheck':
				$responseBody = self::recheckLinks( $rows );

				if ( null !== $responseBody ) {
					aioseoBrokenLinkChecker()->internalOptions->internal->license->quotaRemaining = $responseBody->quotaRemaining;

					// If the quota changed, reactivate the license to pull in the latest date from the marketing site.
					if ( aioseoBrokenLinkChecker()->internalOptions->internal->license->quota !== $responseBody->quota ) {
						aioseoBrokenLinkChecker()->internalOptions->internal->license->quota = $responseBody->quota;
						aioseoBrokenLinkChecker()->license->activate();
					}
				}
				break;
			case 'dismiss':
				foreach ( $rows as $row ) {
					self::setLinkStatusDismissed( $row['id'] );
				}
				break;
			case 'undismiss':
				foreach ( $rows as $row ) {
					self::setLinkStatusDismissed( $row['id'], false );
				}
				break;
			case 'unlink':
				foreach ( $rows as $row ) {
					$links = Models\Link::getByLinkStatusId( $row['id'] );
					foreach ( $links as $link ) {
						$link = (array) $link;
						self::removeLink( $link['id'] );
					}
				}
				break;
			default:
				break;
		}

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

	/**
	 * Rechecks the given link.
	 *
	 * @since 1.1.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function recheck( $request ) {
		$body         = $request->get_json_params();
		$linkStatusId = ! empty( $body['linkStatusId'] ) ? intval( $body['linkStatusId'] ) : null;
		if ( empty( $linkStatusId ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No link status ID given.'
			], 400 );
		}

		// Construct a list with a single row so that we can pass it into the bulk recheck handler.
		$linkStatusRows = [
			[
				'id' => $linkStatusId,
			]
		];

		$response = self::recheckLinks( $linkStatusRows );
		if ( ! $response ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Link could not be checked.'
			], 400 );
		}

		aioseoBrokenLinkChecker()->internalOptions->internal->license->quotaRemaining = $response->quotaRemaining;
		if ( aioseoBrokenLinkChecker()->internalOptions->internal->license->quota !== $response->quota ) {
			// If the quota changed, reactivate the license to pull in the latest date from the marketing site.
			aioseoBrokenLinkChecker()->internalOptions->internal->license->quota = $response->quota;
			aioseoBrokenLinkChecker()->license->activate();
		}

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

	/**
	 * Dismisses the given link.
	 *
	 * @since 1.1.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function dismiss( $request ) {
		$body         = $request->get_json_params();
		$linkStatusId = ! empty( $body['linkStatusId'] ) ? intval( $body['linkStatusId'] ) : null;
		if ( empty( $linkStatusId ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No link status ID given.'
			], 400 );
		}

		$success = self::setLinkStatusDismissed( $linkStatusId );
		if ( ! $success ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No link status found.'
			], 400 );
		}

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

	/**
	 * Undismisses the given link.
	 *
	 * @since 1.1.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function undismiss( $request ) {
		$body         = $request->get_json_params();
		$linkStatusId = ! empty( $body['linkStatusId'] ) ? intval( $body['linkStatusId'] ) : null;
		if ( empty( $linkStatusId ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No link status ID given.'
			], 400 );
		}

		$success = self::setLinkStatusDismissed( $linkStatusId, false );
		if ( ! $success ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No link status found.'
			], 400 );
		}

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

	/**
	 * Sets the dismissed value for a given Link Status object.
	 *
	 * @since   1.0.0
	 * @version 1.1.0 Moved from BrokenLinks to TableActions.
	 *
	 * @param  int  $linkStatusId The Link Status ID.
	 * @param  bool $value        The new value.
	 * @return bool               Whether the record was updated.
	 */
	protected static function setLinkStatusDismissed( $linkStatusId, $value = true ) {
		$linkStatus = Models\LinkStatus::getById( $linkStatusId );
		if ( ! $linkStatus->exists() ) {
			return false;
		}

		$linkStatus->dismissed = $value;
		$linkStatus->save();

		return true;
	}
}Api/LinksTable.php000064400000004526151536237000010027 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Api;

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Handles links table related routes.
 *
 * @since 1.1.0
 */
class LinksTable extends CommonTableActions {
	/**
	 * Returns the data for the links table.
	 *
	 * @since 1.1.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function fetchData( $request ) {
		$body         = $request->get_json_params();
		$linkStatusId = ! empty( $body['linkStatusId'] ) ? intval( $body['linkStatusId'] ) : null;
		$limit        = ! empty( $body['limit'] ) ? intval( $body['limit'] ) : 20;
		$offset       = ! empty( $body['offset'] ) ? intval( $body['offset'] ) : 0;
		$searchTerm   = ! empty( $body['searchTerm'] ) ? sanitize_text_field( $body['searchTerm'] ) : null;
		$whereClause  = Models\Link::getLinkWhereClause( $searchTerm );

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

		$totalRows = Models\Link::rowQueryCount( $linkStatusId, $whereClause );
		$page      = 0 === $offset ? 1 : ( $offset / $limit ) + 1;

		return new \WP_REST_Response( [
			'success' => true,
			'links'   => [
				'rows'   => Models\Link::rowQuery( $linkStatusId, $limit, $offset, $whereClause ),
				'totals' => [
					'page'  => $page,
					'pages' => ceil( $totalRows / $limit ),
					'total' => $totalRows
				]
			]
		], 200 );
	}

	/**
	 * Executes the given bulk action on the given rows.
	 *
	 * @since 1.1.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function bulk( $request ) {
		$body   = $request->get_json_params();
		$action = ! empty( $body['action'] ) ? sanitize_text_field( $body['action'] ) : null;
		$rows   = ! empty( $body['rows'] ) ? $body['rows'] : null;
		if ( empty( $action ) || empty( $rows ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No action or rows given.'
			], 400 );
		}

		switch ( $action ) {
			case 'unlink':
				foreach ( $rows as $row ) {
					self::removeLink( $row['id'] );
				}
				break;
			default:
				break;
		}

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

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Handles notification related routes.
 *
 * @since 1.0.0
 */
class Notifications {
	/**
	 * This allows us to not repeat code over and over.
	 *
	 * @since 1.0.0
	 *
	 * @param  string            $slug The slug of the reminder.
	 * @return \WP_REST_Response       The response.
	 */
	public static function reminder( $slug ) {
		aioseoBrokenLinkChecker()->notifications->remindMeLater( $slug );

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

	/**
	 * Dismiss notifications.
	 *
	 * @since 1.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 = aioseoBrokenLinkChecker()->core->db
			->start( 'aioseo_blc_notifications' )
			->whereIn( 'slug', $slugs )
			->run()
			->models( 'AIOSEO\\BrokenLinkChecker\\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_blc_notification_plugin_review_dismissed', '3' );
		}

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

		return new \WP_REST_Response( [
			'success'       => true,
			'notifications' => Models\Notification::getNotifications()
		], 200 );
	}
}Api/Plugins.php000064400000005763151536237000007424 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Api;

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

/**
 * Handles plugin install/deinstall.
 *
 * @since 1.0.0
 */
class Plugins {
	/**
	 * Installs plugins.
	 *
	 * @since 1.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function installPlugins( $request ) {
		$body    = $request->get_json_params();
		$plugins = ! empty( $body['plugins'] ) ? $body['plugins'] : [];
		$network = ! empty( $body['network'] ) ? $body['network'] : false;
		$error   = esc_html__( 'Installation failed. Please check permissions and try again.', 'aioseo-broken-link-checker' );

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

		if ( ! aioseoBrokenLinkChecker()->helpers->canInstall() ) {
			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 );
			}

			$result = aioseoBrokenLinkChecker()->helpers->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 );
	}

	/**
	 * Deactivates plugins.
	 *
	 * @since 1.0.0
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function deactivatePlugins( $request ) {
		$body    = $request->get_json_params();
		$plugins = ! empty( $body['plugins'] ) ? $body['plugins'] : [];
		$network = ! empty( $body['network'] ) ? $body['network'] : false;
		$error   = esc_html__( 'Deactivation failed. Please check permissions and try again.', 'aioseo-broken-link-checker' );

		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 );
			}

			$deactivated = deactivate_plugins( $plugin['plugin'], false, $network );
			if ( is_wp_error( $deactivated ) ) {
				$failed[] = $plugin['plugin'];
			}

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

		return new \WP_REST_Response( [
			'success'   => true,
			'completed' => $completed,
			'failed'    => $failed
		], 200 );
	}
}Api/Post.php000064400000001746151536237000006725 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Api;

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Handles all post related routes.
 *
 * @since 1.1.0
 */
class Post {
	/**
	 * Deletes the given post.
	 *
	 * @since 1.1.0
	 *
	 * @param  \WP_REST_Request  $request The request
	 * @return \WP_REST_Response          The response.
	 */
	public static function deletePost( $request ) {
		$body   = $request->get_json_params();
		$postId = ! empty( $body['postId'] ) ? intval( $body['postId'] ) : null;
		if ( empty( $postId ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No post ID given.'
			], 400 );
		}

		$success = wp_trash_post( $postId );
		if ( ! $success ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Failed to delete post.'
			], 500 );
		}

		Models\Link::deleteLinks( $postId );

		return new \WP_REST_Response( [
			'success' => true
		], 200 );
	}
}Api/PostsTerms.php000064400000004115151536237000010114 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Api;

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Handles post/term lookups.
 *
 * @since 1.0.0
 */
class PostsTerms {
	/**
	 * Returns posts by ID/name.
	 *
	 * @since 1.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();
		$searchTerm = ! empty( $body['query'] ) ? sanitize_text_field( $body['query'] ) : null;
		$type       = ! empty( $body['type'] ) ? sanitize_text_field( $body['type'] ) : null;
		if ( empty( $searchTerm ) || empty( $type ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'No search term or object type was provided.'
			], 400 );
		}

		$escapedSearchTerm = esc_sql( aioseoBrokenLinkChecker()->core->db->db->esc_like( $searchTerm ) );

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

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

		$parsedObjects = [];
		foreach ( $objects as $object ) {
			if ( 'posts' === $type ) {
				$parsedObjects[] = [
					'value' => (int) $object->ID,
					'slug'  => $object->post_name,
					'label' => $object->post_title,
					'type'  => $object->post_type,
					'link'  => get_permalink( $object->ID )
				];
			}
		}

		return new \WP_REST_Response( [
			'success' => true,
			'objects' => $parsedObjects
		], 200 );
	}
}Api/Redirects.php000064400000002525151536237000007720 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Api;

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

use \AIOSEO\Plugin\Addon\Redirects\Utils as RedirectUtils;

/**
 * Handles all redirect related routes.
 *
 * @since 1.1.0
 */
class Redirects {
	/**
	 * Returns the hash for a redirect added through the redirect monitor.
	 *
	 * @since 1.1.0
	 *
	 * @param  \WP_REST_Request  $request The request
	 * @return \WP_REST_Response          The response.
	 */
	public static function getRedirectUrl( $request ) {
		if ( ! function_exists( 'aioseoRedirects' ) ) {
			return new \WP_REST_Response( [
				'success' => false
			], 400 );
		}

		$body          = $request->get_json_params();
		$linkStatusUrl = ! empty( $body['linkStatusUrl'] ) ? sanitize_text_field( $body['linkStatusUrl'] ) : '';
		if ( empty( $linkStatusUrl ) ) {
			return new \WP_REST_Response( [
				'success' => false
			], 400 );
		}

		$urls = [
			[
				'url' => RedirectUtils\WpUri::excludeHomeUrl( $linkStatusUrl )
			]
		];

		$hash = md5( wp_json_encode( $urls ) );
		aioseoRedirects()->cache->update( 'manual-urls-' . $hash, $urls, HOUR_IN_SECONDS );

		$redirectUrl = add_query_arg( 'aioseo-manual-urls', $hash, admin_url( 'admin.php?page=aioseo-redirects' ) );

		return new \WP_REST_Response( [
			'success'     => true,
			'redirectUrl' => $redirectUrl
		], 200 );
	}
}Api/VueSettings.php000064400000007165151536237000010261 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Api;

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Handles the user Vue settings (toggled cards, etc.).
 *
 * @since 1.0.0
 */
class VueSettings {
	/**
	 * Returns the settings.
	 *
	 * @since 1.0.0
	 *
	 * @return \WP_REST_Response The response.
	 */
	public static function getOptions() {
		return new \WP_REST_Response( [
			'success'         => true,
			'options'         => aioseoBrokenLinkChecker()->options->all(),
			'internalOptions' => aioseoBrokenLinkChecker()->internalOptions->all(),
			'settings'        => aioseoBrokenLinkChecker()->vueSettings->all()
		], 200 );
	}

	/**
	 * Toggles a card in the settings.
	 *
	 * @since 1.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 = aioseoBrokenLinkChecker()->vueSettings->toggledCards;
		if ( ! array_key_exists( $card, $cards ) ) {
			return new \WP_REST_Response( [
				'success' => false
			], 400 );
		}

		$cards[ $card ] = ! $cards[ $card ];
		aioseoBrokenLinkChecker()->vueSettings->toggledCards = $cards;

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

	/**
	 * Toggles a radio in the settings.
	 *
	 * @since 1.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 = aioseoBrokenLinkChecker()->vueSettings->toggledRadio;
		if ( ! array_key_exists( $radio, $radios ) ) {
			return new \WP_REST_Response( [
				'success' => false
			], 400 );
		}

		$radios[ $radio ] = $value;
		aioseoBrokenLinkChecker()->vueSettings->toggledRadio = $radios;

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

	/**
	 * Toggles a table's items per page setting.
	 *
	 * @since 1.0.0
	 *
	 * @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 = aioseoBrokenLinkChecker()->vueSettings->tablePagination;
		if ( ! array_key_exists( $table, $tables ) ) {
			return new \WP_REST_Response( [
				'success' => false
			], 400 );
		}

		$tables[ $table ] = $value;
		aioseoBrokenLinkChecker()->vueSettings->tablePagination = $tables;

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

	/**
	 * Save options from the frontend.
	 *
	 * @since 1.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'] : []; // The options class will sanitize them.

		aioseoBrokenLinkChecker()->options->sanitizeAndSave( $options );

		// Re-initialize the notices.
		aioseoBrokenLinkChecker()->notifications->init();

		return new \WP_REST_Response( [
			'success'       => true,
			'notifications' => Models\Notification::getNotifications()
		], 200 );
	}
}BrokenLinkChecker.php000064400000016134151536237000010607 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker {
	// Exit if accessed directly.
	if ( ! defined( 'ABSPATH' ) ) {
		exit;
	}

	/**
	 * The main BrokenLinkChecker class.
	 *
	 * @since 1.0.0
	 */
	final class BrokenLinkChecker {
		/**
		 * Holds the instance of the plugin currently in use.
		 *
		 * @since 1.0.0
		 *
		 * @var BrokenLinkChecker
		 */
		private static $instance;

		/**
		 * Plugin version for enqueueing, etc.
		 * The value is retrieved from the AIOSEO_BROKEN_LINK_CHECKER_BROKEN_LINK_CHECKER_VERSION constant.
		 *
		 * @since 1.0.0
		 *
		 * @var string
		 */
		public $version = '';

		/**
		 * Whether we're in a dev environment.
		 *
		 * @since 1.0.0
		 *
		 * @var bool
		 */
		public $isDev = false;

		/**
		 * Core class instance.
		 *
		 * @since 1.0.0
		 *
		 * @var Core\Core
		 */
		public $core;

		/**
		 * InternalOptions class instance.
		 *
		 * @since 1.0.0
		 *
		 * @var Options\InternalOptions
		 */
		public $internalOptions;

		/**
		 * Pre updates class instance.
		 *
		 * @since 1.0.0
		 *
		 * @var Main\PreUpdates
		 */
		public $preUpdates;

		/**
		 * Helpers class instance.
		 *
		 * @since 1.0.0
		 *
		 * @var Utils\Helpers
		 */
		public $helpers;

		/**
		 * Options class instance.
		 *
		 * @since 1.0.0
		 *
		 * @var Options\Options
		 */
		public $options;

		/**
		 * Updates class instance.
		 *
		 * @since 1.0.0
		 *
		 * @var Main\Updates
		 */
		public $updates;

		/**
		 * Action scheduler class.
		 *
		 * @since 1.0.0
		 *
		 * @var Utils\ActionScheduler
		 */
		public $actionScheduler;

		/**
		 * License class.
		 *
		 * @since 1.0.0
		 *
		 * @var Admin\License
		 */
		public $license;

		/**
		 * Access class.
		 *
		 * @since 1.0.0
		 *
		 * @var Utils\Access
		 */
		public $access;

		/**
		 * Main class instance.
		 *
		 * @since 1.0.0
		 *
		 * @var Main\Main
		 */
		public $main;

		/**
		 * API class instance.
		 *
		 * @since 1.0.0
		 *
		 * @var Api\Api
		 */
		public $api;

		/**
		 * Standalone class instance.
		 *
		 * @since 1.0.0
		 *
		 * @var Standalone\Standalone
		 */
		public $standalone;

		/**
		 * Notifications class instance.
		 *
		 * @since 1.0.0
		 *
		 * @var Admin\Notifications
		 */
		public $notifications;

		/**
		 * VueSettings class instance.
		 *
		 * @since 1.1.0
		 *
		 * @var Utils\VueSettings
		 */
		public $vueSettings;

		/**
		 * Admin class instance.
		 *
		 * @since 1.2.0
		 *
		 * @var Admin\Admin
		 */
		public $admin;

		/**
		 * The main BrokenLinkChecker Instance.
		 *
		 * Insures that only one instance of BrokenLinkChecker exists in memory at any one
		 * time. Also prevents needing to define globals all over the place.
		 *
		 * @since 1.0.0
		 *
		 * @return BrokenLinkChecker The broken link checker instance.
		 */
		public static function instance() {
			if ( null === self::$instance || ! self::$instance instanceof self ) {
				self::$instance = new self();

				self::$instance->init();
			}

			return self::$instance;
		}

		/**
		 * Initialize Broken Link Checker!
		 *
		 * @since 1.0.0
		 *
		 * @return void
		 */
		private function init() {
			$this->constants();
			$this->includes();
			$this->preLoad();
			if ( ! $this->helpers->isUninstalling() ) {
				$this->load();
			}
		}

		/**
		 * Setup plugin constants.
		 * All the path/URL related constants are defined in main plugin file.
		 *
		 * @since 1.0.0
		 *
		 * @return void
		 */
		private function constants() {
			$defaultHeaders = [
				'name'    => 'Plugin Name',
				'version' => 'Version',
			];

			$pluginData = get_file_data( AIOSEO_BROKEN_LINK_CHECKER_FILE, $defaultHeaders );

			$constants = [
				'AIOSEO_BROKEN_LINK_CHECKER_PLUGIN_BASENAME'  => plugin_basename( AIOSEO_BROKEN_LINK_CHECKER_FILE ),
				'AIOSEO_BROKEN_LINK_CHECKER_PLUGIN_NAME'      => 'Broken Link Checker',
				'AIOSEO_BROKEN_LINK_CHECKER_PLUGIN_URL'       => plugin_dir_url( AIOSEO_BROKEN_LINK_CHECKER_FILE ),
				'AIOSEO_BROKEN_LINK_CHECKER_VERSION'          => $pluginData['version'],
				'AIOSEO_BROKEN_LINK_CHECKER_MARKETING_URL'    => 'https://aioseo.com/',
				'AIOSEO_BROKEN_LINK_CHECKER_MARKETING_DOMAIN' => 'aioseo.com'
			];

			foreach ( $constants as $constant => $value ) {
				if ( ! defined( $constant ) ) {
					define( $constant, $value );
				}
			}

			$this->version = AIOSEO_BROKEN_LINK_CHECKER_VERSION;
		}

		/**
		 * Including the new files with PHP 5.3 style.
		 *
		 * @since 1.0.0
		 *
		 * @return void
		 */
		private function includes() {
			$dependencies = [
				'/vendor/autoload.php',
				'/vendor/woocommerce/action-scheduler/action-scheduler.php'
			];

			foreach ( $dependencies as $path ) {
				if ( ! file_exists( AIOSEO_BROKEN_LINK_CHECKER_DIR . $path ) ) {
					// Something is not right.
					status_header( 500 );
					wp_die( esc_html__( 'Plugin is missing required dependencies. Please contact support for more information.', 'aioseo-broken-link-checker' ) );
				}
				require_once AIOSEO_BROKEN_LINK_CHECKER_DIR . $path;
			}

			$this->loadVersion();
		}

		/**
		 * Load the version of the plugin we are currently using.
		 *
		 * @since 1.0.0
		 *
		 * @return void
		 */
		private function loadVersion() {
			if (
				! class_exists( '\Dotenv\Dotenv' ) ||
				! file_exists( AIOSEO_BROKEN_LINK_CHECKER_DIR . '/build/.env' )
			) {
				return;
			}

			$dotenv = \Dotenv\Dotenv::createUnsafeImmutable( AIOSEO_BROKEN_LINK_CHECKER_DIR, '/build/.env' );
			$dotenv->load();

			$devPort = strtolower( getenv( 'VITE_AIOSEO_BROKEN_LINK_CHECKER_DEV_PORT' ) );
			if ( ! empty( $devPort ) ) {
				$this->isDev = true;

				// Fix SSL certificate invalid in our local environments.
				add_filter( 'https_ssl_verify', '__return_false' );
			}
		}

		/**
		 * Runs before we load the plugin.
		 *
		 * @since 1.0.0
		 *
		 * @return void
		 */
		private function preLoad() {
			$this->core            = new Core\Core();
			$this->internalOptions = new Options\InternalOptions();
			$this->preUpdates      = new Main\PreUpdates();
			$this->helpers         = new Utils\Helpers();
			$this->options         = new Options\Options();
		}

		/**
		 * Load our classes.
		 *
		 * @since 1.0.0
		 *
		 * @return void
		 */
		public function load() {
			$this->updates         = new Main\Updates();
			$this->actionScheduler = new Utils\ActionScheduler();
			$this->license         = new Admin\License();
			$this->access          = new Utils\Access();
			$this->main            = new Main\Main();
			$this->api             = new Api\Api();
			$this->standalone      = new Standalone\Standalone();
			$this->notifications   = new Admin\Notifications();
			$this->admin           = new Admin\Admin();

			add_action( 'init', [ $this, 'loadInit' ], 999 );
		}

		/**
		 * Things that need to load after init.
		 *
		 * @since 1.0.0
		 *
		 * @return void
		 */
		public function loadInit() {
			$this->vueSettings = new Utils\VueSettings( '_aioseo_blc_settings' );
		}
	}
}

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

	/**
	 * The function which returns the one AIOSEO instance.
	 *
	 * @since 1.0.0
	 *
	 * @return AIOSEO\BrokenLinkChecker\BrokenLinkChecker The instance.
	 */
	function aioseoBrokenLinkChecker() {
		return AIOSEO\BrokenLinkChecker\BrokenLinkChecker::instance();
	}
}Core/Assets.php000064400000036663151536237000007427 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Core;

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

/**
 * Handles the loading of our file assets.
 *
 * @since 1.0.0
 */
class Assets {
	/**
	 * Whether we should load dev scripts.
	 *
	 * @since 1.0.0
	 *
	 * @var boolean|null
	 */
	private $shouldLoadDevScripts = null;

	/**
	 * The script handle to use for asset enqueuing.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private $scriptHandle = 'aioseo-broken-link-checker';

	/**
	 * Holds the location of the manifest file.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private $manifestFile = '';

	/**
	 * True if we are in a dev environment. This mirrors the global isDev.
	 *
	 * @since 1.0.0
	 *
	 * @var bool
	 */
	private $isDev = false;

	/**
	 * Asset handles that should load as regular JS and not as modern JS module.
	 *
	 * @since 1.0.0
	 *
	 * @var array An array of handles.
	 */
	private $noModuleTag = [];

	/**
	 * Core class instance.
	 *
	 * @since 1.1.0
	 *
	 * @var Core
	 */
	public $core = null;

	/**
	 * The plugin version.
	 *
	 * @since 1.1.0
	 *
	 * @var int
	 */
	public $version = 0;

	/**
	 * The domain.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	public $domain = '';

	/**
	 * The port.
	 *
	 * @since 1.1.0
	 *
	 * @var int
	 */
	public $port = 0;

	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 *
	 * @param Core $core The AIOSEO Core class.
	 */
	public function __construct( $core ) {
		$this->core         = $core;
		$this->version      = aioseoBrokenLinkChecker()->version;
		$this->manifestFile = AIOSEO_BROKEN_LINK_CHECKER_DIR . '/dist/manifest.php';
		$this->isDev        = aioseoBrokenLinkChecker()->isDev;

		if ( $this->isDev ) {
			$this->domain = getenv( 'VITE_AIOSEO_BROKEN_LINK_CHECKER_DOMAIN' );
			$this->port   = getenv( 'VITE_AIOSEO_BROKEN_LINK_CHECKER_DEV_PORT' );
		}

		add_filter( 'script_loader_tag', [ $this, 'scriptLoaderTag' ], 10, 3 );
	}

	/**
	 * The asset to load.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $asset        The asset to load.
	 * @param  array  $dependencies An array of dependencies.
	 * @param  mixed  $data         Any data to be localized.
	 * @param  string $objectName   The object name to use when localizing.
	 * @return void
	 */
	public function load( $asset, $dependencies = [], $data = null, $objectName = 'aioseoBrokenLinkChecker' ) {
		$this->jsPreloadImports( $asset );
		$this->loadCss( $asset );
		$this->enqueueJs( $asset, $dependencies, $data, $objectName );
	}

	/**
	 * Filter the script loader tag if this is our script.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $tag    The tag that is going to be output.
	 * @param  string $handle The handle for the script.
	 * @return string         The modified tag.
	 */
	public function scriptLoaderTag( $tag, $handle, $src ) {
		if ( $this->skipModuleTag( $handle ) ) {
			return $tag;
		}

		$tag = str_replace( $src, $this->normalizeAssetsHost( $src ), $tag );

		// Remove the type and re-add it as module.
		$tag = preg_replace( '/type=[\'"].*?[\'"]/', '', (string) $tag );
		$tag = preg_replace( '/<script/', '<script type="module"', (string) $tag );

		return $tag;
	}

	/**
	 * Preload JS imports.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $asset The asset to load imports for.
	 * @return void
	 */
	private function jsPreloadImports( $asset ) {
		static $urls = []; // Prevent script from being loaded multiple times.

		$res = '';
		foreach ( $this->importsUrls( $asset ) as $url ) {
			if ( isset( $urls[ $url ] ) ) {
				continue;
			}

			$urls[ $url ] = true;

			$res .= '<link rel="modulepreload" href="' . esc_attr( $url ) . "\">\n";
		}

		$allowedHtml = [
			'link' => [
				'rel'  => [],
				'href' => []
			]
		];

		if ( ! empty( $res ) ) {
			if ( ! function_exists( 'wp_enqueue_script_module' ) ) {
				add_action( 'admin_head', function () use ( &$res, $allowedHtml ) {
					echo wp_kses( $res, $allowedHtml );
				} );
				add_action( 'wp_head', function () use ( &$res, $allowedHtml ) {
					echo wp_kses( $res, $allowedHtml );
				} );
			} else {
				add_action( 'admin_print_footer_scripts', function () use ( &$res, $allowedHtml ) {
					echo wp_kses( $res, $allowedHtml );
				}, 1000 );
			}
		}
	}

	/**
	 * Loads CSS for an asset from the manifest file.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $asset The script to load CSS for.
	 * @return void
	 */
	private function loadCss( $asset ) {
		if ( $this->shouldLoadDev() ) {
			return;
		}

		foreach ( $this->getCssUrls( $asset ) as $file => $url ) {
			wp_enqueue_style( $this->cssHandle( $file ), $url, [], $this->version );
		}
	}

	/**
	 * Register a CSS asset.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $asset        The script to load CSS for.
	 * @param  array  $dependencies An array of dependencies.
	 * @param  string $devPath      The file's dev path.
	 * @return void
	 */
	public function registerCss( $asset, $dependencies = [], $devPath = '' ) {
		$handle = $this->cssHandle( $asset );
		if ( wp_style_is( $handle, 'registered' ) ) {
			return;
		}

		$devPath = $devPath ?: $asset;

		$url = $this->shouldLoadDev()
			? $this->getDevUrl() . ltrim( $devPath, '/' )
			: $this->assetUrl( $asset );

		if ( ! $url ) {
			return;
		}

		wp_register_style( $handle, $url, $dependencies, $this->version );
	}

	/**
	 * Enqueue css.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $asset        The css to load.
	 * @param  string $devPath      The file's dev path.
	 * @param  array  $dependencies An array of dependencies.
	 * @return void
	 */
	public function enqueueCss( $asset, $dependencies = [], $devPath = '' ) {
		$this->registerCss( $asset, $dependencies, $devPath );

		$handle = $this->cssHandle( $asset );
		if ( wp_style_is( $handle, 'enqueued' ) ) {
			return;
		}

		wp_enqueue_style( $handle );
	}

	/**
	 * Register the JS to enqueue.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $asset        The script to load.
	 * @param  array  $dependencies An array of dependencies.
	 * @param  mixed  $data         Any data to be localized.
	 * @param  string $objectName   The object name to use when localizing.
	 * @return void
	 */
	public function registerJs( $asset, $dependencies = [], $data = null, $objectName = 'aioseoBrokenLinkChecker' ) {
		$handle = $this->jsHandle( $asset );
		if ( wp_script_is( $handle, 'registered' ) ) {
			return;
		}

		$url = $this->shouldLoadDev()
			? $this->getDevUrl() . ltrim( $asset, '/' )
			: $this->jsUrl( $asset );

		if ( ! $url ) {
			return;
		}

		wp_register_script( $handle, $url, $dependencies, $this->version, true );

		if ( empty( $data ) ) {
			return;
		}

		wp_localize_script(
			$handle,
			$objectName,
			$data
		);
	}

	/**
	 * Register the JS to enqueue.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $asset        The script to load.
	 * @param  array  $dependencies An array of dependencies.
	 * @param  mixed  $data         Any data to be localized.
	 * @param  string $objectName   The object name to use when localizing.
	 * @return void
	 */
	public function enqueueJs( $asset, $dependencies = [], $data = null, $objectName = 'aioseoBrokenLinkChecker' ) {
		$this->registerJs( $asset, $dependencies, $data, $objectName );

		$handle = $this->jsHandle( $asset );
		if ( wp_script_is( $handle, 'enqueued' ) ) {
			return;
		}

		wp_enqueue_script( $handle );
	}

	/**
	 * Return the dev URL.
	 *
	 * @since 1.0.0
	 *
	 * @return string The dev URL.
	 */
	private function getDevUrl() {
		$protocol = is_ssl() ? 'https://' : 'http://';

		return $protocol . $this->domain . ':' . $this->port . '/';
	}

	/**
	 * Get the asset URL.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $asset The asset to find the URL for.
	 * @return string        The URL for the asset.
	 */
	private function assetUrl( $asset ) {
		$assetManifest = $this->getAssetManifestItem( $asset );

		return ! empty( $assetManifest['file'] )
			? $this->basePath() . $assetManifest['file']
			: $this->basePath() . ltrim( $asset, '/' );
	}

	/**
	 * Get the JS URL.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $asset The asset to find the URL for.
	 * @return string        The URL for the asset.
	 */
	public function jsUrl( $asset ) {
		$manifestAsset = $this->getManifestItem( $asset );

		return ! empty( $manifestAsset['file'] )
			? $this->basePath() . $manifestAsset['file']
			: $this->basePath() . ltrim( $asset, '/' );
	}

	/**
	 * Get an item from the manifest.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $asset The asset to find.
	 * @return string        Manifest object.
	 */
	private function getManifestItem( $asset ) {
		$manifest = $this->getManifest();

		$asset = ltrim( $asset, '/' );

		return isset( $manifest[ $asset ] ) ? $manifest[ $asset ] : null;
	}

	/**
	 * Get the CSS asset handle.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $asset The asset to find the handle for.
	 * @return string        The asset handle.
	 */
	public function cssHandle( $asset ) {
		return "{$this->scriptHandle}/css/$asset";
	}

	/**
	 * Get the JS asset handle.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $asset The asset to find the handle for.
	 * @return string        The asset handle.
	 */
	public function jsHandle( $asset = '' ) {
		return "{$this->scriptHandle}/js/$asset";
	}

	/**
	 * Get the manifest to load assets from.
	 *
	 * @since 1.0.0
	 *
	 * @return array An array of files.
	 */
	private function getManifest() {
		static $file = null;
		if ( $file ) {
			return $file;
		}

		$manifestJson = ''; // This is set in the view.
		if ( file_exists( $this->manifestFile ) ) {
			require_once $this->manifestFile;
		}

		$file = json_decode( $manifestJson, true );

		return $file;
	}

	/**
	 * Get an item from the asset manifest.
	 *
	 * @since 1.0.0
	 *
	 * @param  string      $item An item to retrieve.
	 * @return string|null       The asset item.
	 */
	private function getAssetManifestItem( $item ) {
		$assetManifest = $this->getManifest();

		return ! empty( $assetManifest[ $item ] ) ? $assetManifest[ $item ] : null;
	}

	/**
	 * Get an asset's array of URLs to import.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $asset The asset to find imports for.
	 * @return array         An array of imports.
	 */
	private function importsUrls( $asset ) {
		$urls          = [];
		$manifestAsset = $this->getManifestItem( $asset );
		if ( ! empty( $manifestAsset['imports'] ) ) {
			foreach ( $manifestAsset['imports'] as $import ) {
				$importAsset = $this->getManifestItem( $import );
				if ( ! empty( $importAsset['file'] ) ) {
					$urls[] = $this->getPublicUrlBase() . $importAsset['file'];

					// Load the import's CSS if any.
					$this->loadCss( $import );
				}
			}
		}

		return $urls;
	}

	/**
	 * Returns an asset's CSS urls.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $asset The asset to find CSS URLs for.
	 * @return array         An array of CSS URLs to load.
	 */
	private function getCssUrls( $asset ) {
		$urls          = [];
		$manifestAsset = $this->getManifestItem( $asset );

		if ( ! empty( $manifestAsset['css'] ) ) {
			foreach ( $manifestAsset['css'] as $file ) {
				$urls[ $file ] = $this->getPublicUrlBase() . $file;
			}
		}

		return $urls;
	}

	/**
	 * Check if we should load the dev watcher scripts.
	 *
	 * @since 1.0.0
	 *
	 * @return boolean True if we should load the dev watcher scripts.
	 */
	private function shouldLoadDev() {
		if ( null !== $this->shouldLoadDevScripts ) {
			return $this->shouldLoadDevScripts;
		}

		if (
			! $this->isDev ||
			(
				defined( 'AIOSEO_BROKEN_LINK_CHECKER_LOAD_DEV_SCRIPTS' ) &&
				false === AIOSEO_BROKEN_LINK_CHECKER_LOAD_DEV_SCRIPTS
			)
		) {
			$this->shouldLoadDevScripts = false;

			return $this->shouldLoadDevScripts;
		}

		if ( ! $this->domain && ! $this->port ) {
			$this->shouldLoadDevScripts = false;

			return $this->shouldLoadDevScripts;
		}

		set_error_handler( function() {} ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
		$connection = fsockopen( $this->domain, $this->port ); // phpcs:ignore WordPress.WP.AlternativeFunctions
		restore_error_handler();

		if ( ! $connection ) {
			$this->shouldLoadDevScripts = false;

			return $this->shouldLoadDevScripts;
		}

		$this->shouldLoadDevScripts = true;

		return $this->shouldLoadDevScripts;
	}

	/**
	 * Get the path for the assets.
	 *
	 * @since 1.0.0
	 *
	 * @param  bool   $maybeDev Whether to try and load dev scripts.
	 * @return string           The path for the assets.
	 */
	public function getAssetsPath( $maybeDev = true ) {
		return $maybeDev && $this->shouldLoadDev()
			? $this->getDevUrl()
			: $this->basePath();
	}

	/**
	 * Finds out if a handle should be loaded as regular JS and not as modern JS module.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $handle The script handle.
	 * @return bool           Should the module tag be skipped.
	 */
	public function skipModuleTag( $handle ) {
		if ( ! aioseoBrokenLinkChecker()->helpers->stringContains( $handle, $this->jsHandle( '' ) ) ) {
			return true;
		}

		foreach ( $this->noModuleTag as $tag ) {
			if ( aioseoBrokenLinkChecker()->helpers->stringContains( $handle, $tag ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Normalize the assets host. Some sites manually set the WP_PLUGINS_URL
	 * and if that domain has www. and the site_url does not, then it will fail to load
	 * our assets. This doesn't fix the issue 100% because it will still fail on
	 * sub-domains that don't have the proper CORS headers. Those sites will need
	 * manual fixes.
	 *
	 * 4.1.10
	 *
	 * @param  string $path The path to normalize.
	 * @return string       The normalized path.
	 */
	public function normalizeAssetsHost( $path ) {
		static $paths = [];
		if ( isset( $paths[ $path ] ) ) {
			return apply_filters( 'aioseo_blc_normalize_assets_host', $paths[ $path ] );
		}

		// We need to verify the domain on the $path attribute matches
		// what's in site_url() for our assets or they won't load.
		$siteUrl        = site_url();
		$siteUrlEscaped = aioseoBrokenLinkChecker()->helpers->escapeRegex( $siteUrl );
		if ( preg_match( "/^$siteUrlEscaped/i", (string) $path ) ) {
			$paths[ $path ] = $path;

			return apply_filters( 'aioseo_blc_normalize_assets_host', $paths[ $path ] );
		}

		// We now know that the path doesn't contain the site_url().
		$newPath        = $path;
		$siteUrlParsed  = wp_parse_url( $siteUrl );
		$host           = aioseoBrokenLinkChecker()->helpers->escapeRegex( str_replace( 'www.', '', $siteUrlParsed['host'] ) );
		$scheme         = aioseoBrokenLinkChecker()->helpers->escapeRegex( $siteUrlParsed['scheme'] );

		$siteUrlHasWww = preg_match( "/^{$scheme}:\/\/www\.$host/", (string) $siteUrl );
		$pathHasWww    = preg_match( "/^{$scheme}:\/\/www\.$host/", (string) $path );

		// Check if the path contains www.
		if ( $pathHasWww && ! $siteUrlHasWww ) {
			// If the path contains www., we want to strip it out.
			$newPath = preg_replace( "/^({$scheme}:\/\/)(www\.)($host)/", '$1$3', (string) $path );
		}

		// Check if the site_url contains www.
		if ( $siteUrlHasWww && ! $pathHasWww ) {
			// If the site_url contains www., we want to add it in to the path.
			$newPath = preg_replace( "/^({$scheme}:\/\/)($host)/", '$1www.$2', (string) $path );
		}

		$paths[ $path ] = $newPath;

		return apply_filters( 'aioseo_blc_normalize_assets_host', $paths[ $path ] );
	}

	/**
	 * Returns the public URL base.
	 *
	 * @since 1.0.0
	 *
	 * @return string The URL base.
	 */
	private function getPublicUrlBase() {
		return $this->shouldLoadDev() ? $this->getDevUrl() . 'dist/assets/' : $this->basePath();
	}

	/**
	 * Returns the base path URL.
	 *
	 * @since 1.0.0
	 *
	 * @return string The base path URL.
	 */
	private function basePath() {
		return $this->normalizeAssetsHost( plugins_url( 'dist/assets/', AIOSEO_BROKEN_LINK_CHECKER_FILE ) );
	}
}Core/Cache.php000064400000014221151536237000007152 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Core;

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

/**
 * Handles our cache.
 *
 * @since 1.0.0
 */
class Cache {
	/**
	 * The name of our cache table.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private $table = 'aioseo_blc_cache';

	/**
	 * Our cache.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private static $cache = [];

	/**
	 * Prefix for this cache.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	protected $prefix = 'aioseo_blc_';

	/**
	 * Class constructor.
	 *
	 * @since 4.7.8
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'checkIfTableExists' ] ); // This needs to run on init because the DB
		// class gets instantiated along with the cache class.
	}

	/**
	 * Checks if the cache table exists and creates it if it doesn't.
	 *
	 * @since 4.7.8
	 *
	 * @return void
	 */
	public function checkIfTableExists() {
		if ( ! aioseoBrokenLinkChecker()->core->db->tableExists( $this->table ) ) {
			aioseoBrokenLinkChecker()->preUpdates->createCacheTable();
		}
	}

	/**
	 * Returns the cache value if it exists and isn't expired.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $key The key name. Use a '%' for a LIKE query.
	 * @return mixed       The value or null if the cache does not exist.
	 */
	public function get( $key ) {
		$key = $this->prepareKey( $key );
		if ( isset( self::$cache[ $key ] ) ) {
			return self::$cache[ $key ];
		}

		// Check if we're supposed to do a LIKE get.
		$isLikeGet = preg_match( '/%/', (string) $key );

		$result = aioseoBrokenLinkChecker()->core->db
			->start( $this->table )
			->select( '`key`, `value`' )
			->whereRaw( '( `expiration` IS NULL OR `expiration` > \'' . aioseoBrokenLinkChecker()->helpers->timeToMysql( time() ) . '\' )' );

		$isLikeGet ?
			$result->whereRaw( '`key` LIKE \'' . $key . '\'' ) :
			$result->where( 'key', $key );

		$result->output( ARRAY_A )->run();

		// If we have nothing in the cache, let's return null.
		$values = $result->nullSet() ? null : $result->result();

		// If we have something, let's normalize it.
		if ( $values ) {
			foreach ( $values as &$value ) {
				$value['value'] = aioseoBrokenLinkChecker()->helpers->maybeUnserialize( $value['value'] );
			}
			// Return only the single cache value.
			if ( ! $isLikeGet ) {
				$values = $values[0]['value'];
			}
		}

		// Return values without a static cache.
		// This is here because clearing the LIKE cache is not simple.
		if ( $isLikeGet ) {
			return $values;
		}

		self::$cache[ $key ] = $values;

		return self::$cache[ $key ];
	}

	/**
	 * Updates the given cache or creates it if it doesn't exist.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $key        The key name.
	 * @param  mixed  $value      The value.
	 * @param  int    $expiration The expiration time in seconds. Defaults to 24 hours. 0 to no expiration.
	 * @return void
	 */
	public function update( $key, $value, $expiration = DAY_IN_SECONDS ) {
		// If the value is null we'll convert it and give it a shorter expiration.
		if ( null === $value ) {
			$value      = false;
			$expiration = 10 * MINUTE_IN_SECONDS;
		}

		$value      = serialize( $value );
		$expiration = 0 < $expiration ? aioseoBrokenLinkChecker()->helpers->timeToMysql( time() + $expiration ) : null;

		aioseoBrokenLinkChecker()->core->db->insert( $this->table )
			->set( [
				'key'        => $this->prepareKey( $key ),
				'value'      => $value,
				'expiration' => $expiration,
				'created'    => aioseoBrokenLinkChecker()->helpers->timeToMysql( time() ),
				'updated'    => aioseoBrokenLinkChecker()->helpers->timeToMysql( time() )
			] )->onDuplicate( [
				'value'      => $value,
				'expiration' => $expiration,
				'updated'    => aioseoBrokenLinkChecker()->helpers->timeToMysql( time() )
			] )
			->run();

		$this->clearStatic( $key );
	}

	/**
	 * Deletes the cache record with the given key.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $key The key.
	 * @return void
	 */
	public function delete( $key ) {
		$key = $this->prepareKey( $key );

		aioseoBrokenLinkChecker()->core->db->delete( $this->table )
			->where( 'key', $key )
			->run();

		$this->clearStatic( $key );
	}

	/**
	 * Prepares the key before using the cache.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $key The key to prepare.
	 * @return string      The prepared key.
	 */
	private function prepareKey( $key ) {
		$key = trim( $key );
		$key = $this->prefix && 0 !== strpos( $key, $this->prefix ) ? $this->prefix . $key : $key;

		if ( aioseoBrokenLinkChecker()->helpers->isDev() && 80 < mb_strlen( $key, 'UTF-8' ) ) {
			throw new \Exception( 'You are using a cache key that is too large, shorten your key and try again: [' . esc_html( $key ) . ']' );
		}

		return $key;
	}

	/**
	 * Clears all of our cache.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function clear() {
		// Bust the tableExists and columnExists cache.
		aioseoBrokenLinkChecker()->internalOptions->database->installedTables = '';

		if ( $this->prefix ) {
			$this->clearPrefix( '' );

			return;
		}

		aioseoBrokenLinkChecker()->core->db->truncate( $this->table )->run();

		$this->clearStatic();
	}

	/**
	 * Clears all of our cache under a certain prefix.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $prefix A prefix to clear or empty to clear everything.
	 * @return void
	 */
	public function clearPrefix( $prefix ) {
		$prefix = $this->prepareKey( $prefix );

		aioseoBrokenLinkChecker()->core->db->delete( $this->table )
			->whereRaw( "`key` LIKE '$prefix%'" )
			->run();

		$this->clearStaticPrefix( $prefix );
	}

	/**
	 * Clears all of our static in-memory cache of a prefix.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $prefix The prefix to clear.
	 * @return void
	 */
	private function clearStaticPrefix( $prefix ) {
		$prefix = $this->prepareKey( $prefix );
		foreach ( array_keys( self::$cache ) as $key ) {
			if ( 0 === strpos( $key, $prefix ) ) {
				unset( self::$cache[ $key ] );
			}
		}
	}

	/**
	 * Clears all of our static in-memory cache.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $key The key to clear.
	 * @return void
	 */
	private function clearStatic( $key = null ) {
		if ( empty( $key ) ) {
			self::$cache = [];

			return;
		}

		unset( self::$cache[ $this->prepareKey( $key ) ] );
	}
}Core/Core.php000064400000002637151536237000007047 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Core;

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

use AIOSEO\BrokenLinkChecker\Options;
use AIOSEO\BrokenLinkChecker\Utils;

/**
 * Loads core classes.
 *
 * @since 1.0.0
 */
class Core {
	/**
	 * DB class instance.
	 *
	 * @since 1.1.0
	 *
	 * @var Database
	 */
	public $db = null;

	/**
	 * Filesystem class instance.
	 *
	 * @since 1.1.0
	 *
	 * @var Filesystem
	 */
	public $fs = null;

	/**
	 * Assets class instance.
	 *
	 * @since 1.1.0
	 *
	 * @var Assets
	 */
	public $assets = null;

	/**
	 * Cache class instance.
	 *
	 * @since 1.1.0
	 *
	 * @var Cache
	 */
	public $cache = null;

	/**
	 * NetworkCache class instance.
	 *
	 * @since 1.1.0
	 *
	 * @var NetworkCache
	 */
	public $networkCache = null;

	/**
	 * Options Cache class instance.
	 *
	 * @since 1.1.0
	 *
	 * @var \AIOSEO\BrokenLinkChecker\Options\Cache
	 */
	public $optionsCache = null;

	/**
	 * Uninstall class instance.
	 *
	 * @since 1.1.0
	 *
	 * @var Uninstall
	 */
	public $uninstall = null;

	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		$this->db           = new Database();
		$this->fs           = new Filesystem( $this );
		$this->assets       = new Assets( $this );
		$this->cache        = new Cache();
		$this->networkCache = new NetworkCache();
		$this->optionsCache = new Options\Cache();
		$this->uninstall    = new Uninstall();
	}
}Core/Database.php000064400000117531151536237000007663 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Core;

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

/**
 * Builds queries for the database and returns results.
 *
 * @since 1.0.0
 */
class Database {
	/**
	 * List of custom tables we support.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	public $customTables = [
		'aioseo_blc_cache',
		'aioseo_blc_links',
		'aioseo_blc_link_status',
		'aioseo_blc_notifications',
		'aioseo_blc_posts'
	];

	/**
	 * Holds the global $wpdb instance.
	 *
	 * @since 1.0.0
	 *
	 * @var \wpdb
	 */
	public $db = null;

	/**
	 * Holds the $wpdb prefix.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public $prefix = '';

	/**
	 * The database table in use by this query.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public $table = '';

	/**
	 * The sql statement (SELECT, INSERT, UPDATE, DELETE, etc.).
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private $statement = '';

	/**
	 * The limit clause for the sql query.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private $limit = '';

	/**
	 * The group clause for the sql query.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private $group = [];

	/**
	 * The order by clause for the sql query.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private $order = [];

	/**
	 * The select clause for the sql query.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private $select = [];

	/**
	 * The set clause for the sql query.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private $set = [];

	/**
	 * Duplicate clause for the INSERT query.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private $onDuplicate = [];

	/**
	 * Ignore clause for the INSERT query.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private $ignore = false;

	/**
	 * The where clause for the sql query.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private $where = [];

	/**
	 * The union clause for the sql query.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private $union = [];

	/**
	 * The JOIN clause for the SQL query.
	 *
	 * @since 1.1.0
	 *
	 * @var array
	 */
	private $join = [];

	/**
	 * Determines whether the select statement should be distinct.
	 *
	 * @since 1.0.0
	 *
	 * @var bool
	 */
	private $distinct = false;

	/**
	 * The order by direction for the query.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private $orderDirection = 'ASC';

	/**
	 * The query string is populated after the __toString function is run.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private $query = '';

	/**
	 * The sql query results are stored here.
	 *
	 * @since 1.0.0
	 *
	 * @var mixed
	 */
	private $result = [];

	/**
	 * The method in which $wpdb will output results.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private $output = 'OBJECT';

	/**
	 * Whether or not to strip tags.
	 *
	 * @since 1.0.0
	 *
	 * @var boolean
	 */
	private $stripTags = false;

	/**
	 * Set which option to use to escape the sql query.
	 *
	 * @since 1.0.0
	 *
	 * @var integer
	 */
	protected $escapeOptions = 0;

	/**
	 * A cache of all queries and their results.
	 *
	 * @var array
	 */
	private $cache = [];

	/**
	 * Whether or not to reset the cached results.
	 *
	 * @var boolean
	 */
	private $shouldResetCache = false;

	/**
	 * Constant for escape options.
	 *
	 * @since 1.0.0
	 *
	 * @var integer
	 */
	const ESCAPE_FORCE = 2;

	/**
	 * Constant for escape options.
	 *
	 * @since 1.0.0
	 *
	 * @var integer
	 */
	const ESCAPE_STRIP_HTML = 4;

	/**
	 * Constant for escape options.
	 *
	 * @since 1.0.0
	 *
	 * @var integer
	 */
	const ESCAPE_QUOTE = 8;

	/**
	 * List of model class instances.
	 *
	 * @since 1.1.0
	 *
	 * @var array
	 */
	private $models = [];

	/**
	 * The last query that ran, stringified.
	 *
	 * @since 1.1.0
	 */
	public $lastQuery = '';

	/**
	 * Prepares the database class for use.
	 *
	 * @since 1.0.0
	 *
	 * @global object $wpdb The WordPress database object.
	 */
	public function __construct( $escape = null ) {
		global $wpdb;
		$this->db            = $wpdb;
		$this->prefix        = $wpdb->prefix;
		$this->escapeOptions = is_null( $escape ) ? self::ESCAPE_STRIP_HTML | self::ESCAPE_QUOTE : $escape;
	}

	/**
	 * If this is a clone, lets reset all the data.
	 *
	 * @since 1.0.0
	 */
	public function __clone() {
		// We need to reset the result separetely as well since it is not in the default array.
		$this->reset( [ 'result' ] );
		$this->reset();
	}

	/**
	 * Gets all AIO installed tables.
	 *
	 * @since 1.0.0
	 *
	 * @return array An array of custom AIO tables.
	 */
	public function getInstalledTables() {
		$results = $this->db->get_results( 'SHOW TABLES', 'ARRAY_N' );

		return ! empty( $results ) ? wp_list_pluck( $results, 0 ) : [];
	}

	/**
	 * Gets all columns from a table.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $table The name of the table to lookup columns for.
	 * @return array         An array of custom AIO tables.
	 */
	public function getColumns( $table ) {
		$installedTables = json_decode( aioseoBrokenLinkChecker()->internalOptions->database->installedTables, true );
		$table           = $this->prefix . $table;
		if ( isset( $installedTables[ $table ] ) ) {
			if ( empty( $installedTables[ $table ] ) ) {
				$installedTables[ $table ] = $this->db->get_col( 'SHOW COLUMNS FROM `' . $table . '`' );
				aioseoBrokenLinkChecker()->internalOptions->database->installedTables = wp_json_encode( $installedTables );
			}

			return $installedTables[ $table ];
		}

		return [];
	}

	/**
	 * Checks if a table exists.
	 *
	 * @since 1.0.0
	 *
	 * @param  string  $table The name of the table.
	 * @return boolean        Whether or not the table exists.
	 */
	public function tableExists( $table ) {
		$table           = $this->prefix . $table;
		$installedTables = json_decode( aioseoBrokenLinkChecker()->internalOptions->database->installedTables, true ) ?: [];
		if ( isset( $installedTables[ $table ] ) ) {
			return true;
		}

		$results = $this->db->get_results( "SHOW TABLES LIKE '" . $table . "'" );
		if ( ! empty( $results ) ) {
			$installedTables[ $table ] = [];
			aioseoBrokenLinkChecker()->internalOptions->database->installedTables = wp_json_encode( $installedTables );

			return true;
		}

		return false;
	}

	/**
	 * Checks if a column exists on a given table.
	 *
	 * @since 1.0.0
	 *
	 * @param  string   $table  The name of the table.
	 * @param  string   $column The name of the column.
	 * @return boolean          Whether or not the column exists.
	 */
	public function columnExists( $table, $column ) {
		if ( ! $this->tableExists( $table ) ) {
			return false;
		}

		$columns = $this->getColumns( $table );

		if ( ! in_array( $column, $columns, true ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Gets the size of a table in bytes.
	 *
	 * @since 1.0.0
	 *
	 * @param  string  $table The table to check.
	 * @return integer        The size of the table in bytes.
	 */
	public function getTableSize( $table ) {
		$this->db->query( 'ANALYZE TABLE ' . $this->prefix . $table );
		$results = $this->db->get_results( '
			SELECT
				TABLE_NAME AS `table`,
				ROUND(SUM(DATA_LENGTH + INDEX_LENGTH)) AS `size`
			FROM information_schema.TABLES
			WHERE TABLE_SCHEMA = "' . $this->db->dbname . '"
			AND TABLE_NAME = "' . $this->prefix . $table . '"
			ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC;
		' );

		return empty( $results ) ? 0 : $results[0]->size;
	}

	/**
	 * The query string in all its glory.
	 *
	 * @since 1.0.0
	 *
	 * @return string The actual query string.
	 */
	public function __toString() {
		switch ( strtoupper( $this->statement ) ) {
			case 'INSERT':
				$insert = 'INSERT ';
				if ( $this->ignore ) {
					$insert .= 'IGNORE ';
				}
				$insert .= 'INTO ' . $this->table;
				$clauses   = [];
				$clauses[] = $insert;
				$clauses[] = 'SET ' . implode( ', ', $this->set );
				if ( ! empty( $this->onDuplicate ) ) {
					$clauses[] = 'ON DUPLICATE KEY UPDATE ' . implode( ', ', $this->onDuplicate );
				}

				break;
			case 'REPLACE':
				$clauses   = [];
				$clauses[] = "REPLACE INTO $this->table";
				$clauses[] = 'SET ' . implode( ', ', $this->set );

				break;
			case 'UPDATE':
				$clauses   = [];
				$clauses[] = "UPDATE $this->table";

				if ( count( $this->join ) > 0 ) {
					foreach ( (array) $this->join as $join ) {
						if ( is_array( $join[1] ) ) {
							$join_on = []; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
							foreach ( (array) $join[1] as $left => $right ) {
								$join_on[] = "$this->table.`$left` = `{$join[0]}`.`$right`"; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
							}
							// phpcs:disable Squiz.NamingConventions.ValidVariableName
							$clauses[] = "\t" . ( ( 'LEFT' === $join[2] || 'RIGHT' === $join[2] ) ? $join[2] . ' JOIN ' : 'JOIN ' ) . $join[0] . ' ON ' . implode( ' AND ', $join_on );
							// phpcs:enable Squiz.NamingConventions.ValidVariableName
						} else {
							$clauses[] = "\t" . ( ( 'LEFT' === $join[2] || 'RIGHT' === $join[2] ) ? $join[2] . ' JOIN ' : 'JOIN ' ) . "{$join[0]} ON {$join[1]}";
						}
					}
				}

				$clauses[] = 'SET ' . implode( ', ', $this->set );

				if ( count( $this->where ) > 0 ) {
					$clauses[] = "WHERE 1 = 1 AND\n\t" . implode( "\n\tAND ", $this->where );
				}

				if ( count( $this->order ) > 0 ) {
					$clauses[] = 'ORDER BY ' . implode( ', ', $this->order );
				}

				if ( $this->limit ) {
					$clauses[] = 'LIMIT ' . $this->limit;
				}

				break;

			case 'TRUNCATE':
				$clauses   = [];
				$clauses[] = "TRUNCATE TABLE $this->table";
				break;

			case 'DELETE':
				$clauses   = [];
				$clauses[] = "DELETE FROM $this->table";

				if ( count( $this->where ) > 0 ) {
					$clauses[] = "WHERE 1 = 1 AND\n\t" . implode( "\n\tAND ", $this->where );
				}

				if ( count( $this->order ) > 0 ) {
					$clauses[] = 'ORDER BY ' . implode( ', ', $this->order );
				}

				if ( $this->limit ) {
					$clauses[] = 'LIMIT ' . $this->limit;
				}

				break;
			case 'SELECT':
			case 'SELECT DISTINCT':
			default:
				// Select fields.
				$clauses   = [];
				$distinct  = ( $this->distinct || stripos( $this->statement, 'DISTINCT' ) !== false ) ? 'DISTINCT ' : '';
				$select    = ( count( $this->select ) > 0 ) ? implode( ",\n\t", $this->select ) : '*';
				$clauses[] = "SELECT {$distinct}\n\t{$select}";

				// Select table.
				$clauses[] = "FROM $this->table";

				// Select joins.
				if ( ! empty( $this->join ) && count( $this->join ) > 0 ) {
					foreach ( (array) $this->join as $join ) {
						if ( is_array( $join[1] ) ) {
							$join_on = []; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
							foreach ( (array) $join[1] as $left => $right ) {
								$join_on[] = "$this->table.`$left` = `{$join[0]}`.`$right`"; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
							}
							// phpcs:disable Squiz.NamingConventions.ValidVariableName
							$clauses[] = "\t" . ( ( 'LEFT' === $join[2] || 'RIGHT' === $join[2] ) ? $join[2] . ' JOIN ' : 'JOIN ' ) . $join[0] . ' ON ' . implode( ' AND ', $join_on );
							// phpcs:enable Squiz.NamingConventions.ValidVariableName
						} else {
							$clauses[] = "\t" . ( ( 'LEFT' === $join[2] || 'RIGHT' === $join[2] ) ? $join[2] . ' JOIN ' : 'JOIN ' ) . "{$join[0]} ON {$join[1]}";
						}
					}
				}

				// Select conditions.
				if ( count( $this->where ) > 0 ) {
					$clauses[] = "WHERE 1 = 1 AND\n\t" . implode( "\n\tAND ", $this->where );
				}

				// Union queries.
				if ( count( $this->union ) > 0 ) {
					foreach ( $this->union as $union ) {
						$keyword   = ( $union[1] ) ? 'UNION' : 'UNION ALL';
						$clauses[] = "\n$keyword\n\n$union[0]";
					}

					$clauses[] = '';
				}

				// Select groups.
				if ( count( $this->group ) > 0 ) {
					$clauses[] = 'GROUP BY ' . implode( ', ', $this->escapeColNames( $this->group ) );
				}

				// Select order.
				if ( count( $this->order ) > 0 ) {
					$orderFragments = [];
					foreach ( $this->escapeColNames( $this->order ) as $col ) {
						$orderFragments[] = ( preg_match( '/ (ASC|DESC|RAND\(\))$/i', $col ) ) ? $col : "$col $this->orderDirection";
					}

					$clauses[] = 'ORDER BY ' . implode( ', ', $orderFragments );
				}

				// Select limit.
				if ( $this->limit ) {
					$clauses[] = 'LIMIT ' . $this->limit;
				}

				break;
		}

		// @HACK for wpdb::prepare.
		$clauses[] = '/* %d = %d */';

		$this->query = str_replace( '%%d = %%d', '%d = %d', str_replace( '%', '%%', implode( "\n", $clauses ) ) );

		$this->lastQuery = $this->query;

		return $this->query;
	}

	/**
	 * Shortcut method to return the query string.
	 *
	 * @since 1.0.0
	 *
	 * @return string The query string.
	 */
	public function query() {
		return $this->__toString();
	}

	/**
	 * Start a new Database Query.
	 *
	 * @since 1.0.0
	 *
	 * @param  string  $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  boolean $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @param  string  $statement      The MySQL statement for the query.
	 * @return Database                Returns the Database class which can then be method chained for building the query.
	 */
	public function start( $table = null, $includesPrefix = false, $statement = 'SELECT' ) {
		// Always reset everything when starting a new query.
		$this->reset();
		$this->table = $includesPrefix ? $table : $this->prefix . $table;
		$this->statement = $statement;

		return $this;
	}

	/**
	 * Shortcut method for start with INSERT as the statement.
	 *
	 * @since 1.0.0
	 *
	 * @param  string  $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  boolean $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                Returns the Database class which can then be method chained for building the query.
	 */
	public function insert( $table = null, $includesPrefix = false ) {
		return $this->start( $table, $includesPrefix, 'INSERT' );
	}

	/**
	 * Shortcut method for start with INSERT IGNORE as the statement.
	 *
	 * @since 1.0.0
	 *
	 * @param  string  $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  boolean $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                Returns the Database class which can then be method chained for building the query.
	 */
	public function insertIgnore( $table = null, $includesPrefix = false ) {
		$this->ignore = true;

		return $this->start( $table, $includesPrefix, 'INSERT' );
	}

	/**
	 * Shortcut method for start with UPDATE as the statement.
	 *
	 * @since 1.0.0
	 *
	 * @param  string  $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  boolean $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                Returns the Database class which can then be method chained for building the query.
	 */
	public function update( $table = null, $includesPrefix = false ) {
		return $this->start( $table, $includesPrefix, 'UPDATE' );
	}

	/**
	 * Shortcut method for start with REPLACE as the statement.
	 *
	 * @since 1.0.0
	 *
	 * @param  string  $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  boolean $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                Returns the Database class which can then be method chained for building the query.
	 */
	public function replace( $table = null, $includesPrefix = false ) {
		return $this->start( $table, $includesPrefix, 'REPLACE' );
	}

	/**
	 * Shortcut method for start with TRUNCATE as the statement.
	 *
	 * @since 1.0.0
	 *
	 * @param  string  $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  boolean $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                Returns the Database class which can then be method chained for building the query.
	 */
	public function truncate( $table = null, $includesPrefix = false ) {
		return $this->start( $table, $includesPrefix, 'TRUNCATE' );
	}

	/**
	 * Shortcut method for start with DELETE as the statement.
	 *
	 * @since 1.0.0
	 *
	 * @param  string  $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  boolean $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                Returns the Database class which can then be method chained for building the query.
	 */
	public function delete( $table = null, $includesPrefix = false ) {
		return $this->start( $table, $includesPrefix, 'DELETE' );
	}

	/**
	 * Adds a SELECT clause.
	 *
	 * @since 1.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function select() {
		$args = (array) func_get_args();
		if ( count( $args ) === 1 && is_array( $args[0] ) ) {
			$args = $args[0];
		}

		$this->select = array_merge( $this->select, $this->escapeColNames( $args ) );

		return $this;
	}

	/**
	 * Adds a WHERE clause.
	 *
	 * @since 1.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function where() {
		$criteria = $this->prepArgs( func_get_args() );

		foreach ( (array) $criteria as $field => $value ) {
			if ( ! preg_match( '/[\(\)<=>!]+/', $field ) && false === stripos( $field, ' IS ' ) ) {
				$operator = ( is_null( $value ) ) ? 'IS' : '=';
				$escaped  = $this->escapeColNames( $field );
				$field    = array_pop( $escaped ) . ' ' . $operator;
			}

			if ( is_null( $value ) && false !== stripos( $field, ' IS ' ) ) {
				// WHERE `field` IS NOT NULL.
				$this->where[] = "$field NULL";
			} elseif ( is_null( $value ) ) {
				// WHERE `field` IS NULL.
				$this->where[] = "$field NULL";
			} elseif ( is_array( $value ) ) {
				$wheres = [];
				foreach ( (array) $value as $val ) {
					$wheres[] = sprintf( "$field %s", $this->escape( $val, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
				}

				$this->where[] = '(' . implode( ' OR ', $wheres ) . ')';
			} else {
				$this->where[] = sprintf( "$field %s", $this->escape( $value, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
			}
		}

		return $this;
	}

	/**
	 * Adds a complex WHERE clause.
	 *
	 * @since 1.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function whereRaw() {
		$criteria = $this->prepArgs( func_get_args() );

		foreach ( (array) $criteria as $clause ) {
			$this->where[] = $clause;
		}

		return $this;
	}

	/**
	 * Adds a WHERE clause with all arguments sent separated by OR instead of AND inside a subclause.
	 * @example [ 'a' => 1, 'b' => 2 ] becomes "AND (a = 1 OR b = 2)"
	 *
	 * @since 1.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function whereOr() {
		$criteria = $this->prepArgs( func_get_args() );

		$or = [];
		foreach ( (array) $criteria as $field => $value ) {
			if ( ! preg_match( '/[\(\)<=>!]+/', $field ) && false === stripos( $field, ' IS ' ) ) {
				$operator = ( is_null( $value ) ) ? 'IS' : '=';
				$field    = $this->escapeColNames( $field );
				$field    = array_pop( $field ) . ' ' . $operator;
			}

			if ( is_null( $value ) && false !== stripos( $field, ' IS ' ) ) {
				// WHERE `field` IS NOT NULL.
				$or[] = "$field NULL";
			} elseif ( is_null( $value ) ) {
				// WHERE `field` IS NULL.
				$or[] = "$field NULL";
			} else {
				$or[] = sprintf( "$field %s", $this->escape( $value, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
			}
		}

		// Create our subclause, and add it to the WHERE array.
		$this->where[] = '(' . implode( ' OR ', $or ) . ')';

		return $this;
	}

	/**
	 * Adds a WHERE IN() clause.
	 *
	 * @since 1.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function whereIn() {
		$criteria = $this->prepArgs( func_get_args() );

		foreach ( (array) $criteria as $field => $values ) {
			if ( ! is_array( $values ) ) {
				$values = [ $values ];
			} elseif ( count( $values ) === 0 ) {
				continue;
			}

			foreach ( $values as &$value ) {
				// Note: We can no longer check for `is_numeric` because a value like `61021e6242255` returns true and breaks the query.
				if ( is_integer( $value ) || is_float( $value ) ) {
					// No change.
				} elseif ( is_null( $value ) || false !== stristr( $value, 'NULL' ) ) {
					// Change to a true NULL value.
					$value = null;
				} else {
					$value = sprintf( '%s', $this->escape( $value, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
				}
			}

			$values = implode( ',', $values );
			$this->whereRaw( "$field IN($values)" );
		}

		return $this;
	}

	/**
	 * Adds a WHERE NOT IN() clause.
	 *
	 * @since 1.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function whereNotIn() {
		$criteria = $this->prepArgs( func_get_args() );

		foreach ( (array) $criteria as $field => $values ) {
			if ( ! is_array( $values ) ) {
				$values = [ $values ];
			} elseif ( count( $values ) === 0 ) {
				continue;
			}

			foreach ( $values as &$value ) {
				if ( is_numeric( $value ) ) {
					// No change.
				} elseif ( is_null( $value ) || false !== stristr( $value, 'NULL' ) ) {
					// Change to a true NULL value.
					$value = null;
				} else {
					$value = sprintf( '%s', $this->escape( $value, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
				}
			}

			$values = implode( ',', $values );
			$this->whereRaw( "$field NOT IN($values)" );
		}

		return $this;
	}

	/**
	 * Adds a LEFT JOIN clause.
	 *
	 * @since 1.0.0
	 *
	 * @param  string       $table          The name of the table to join to this query.
	 * @param  string|array $conditions     The conditions of the join clause.
	 * @param  boolean      $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                     Returns the Database class which can be method chained for more query building.
	 */
	public function leftJoin( $table, $conditions, $includesPrefix = false ) {
		return $this->join( $table, $conditions, 'LEFT', $includesPrefix );
	}

	/**
	 * Adds a JOIN clause.
	 *
	 * @since 1.0.0
	 *
	 * @param  string       $table          The name of the table to join to this query.
	 * @param  string|array $conditions     The conditions of the join clause.
	 * @param  string       $direction      This can take 'LEFT' or 'RIGHT' as arguments.
	 * @param  boolean      $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                     Returns the Database class which can be method chained for more query building.
	 */
	public function join( $table, $conditions, $direction = '', $includesPrefix = false ) {
		$this->join[] = [ $includesPrefix ? $table : $this->prefix . $table, $conditions, $direction ];

		return $this;
	}

	/**
	 * Add a UNION query.
	 *
	 * @since 1.0.0
	 *
	 * @param  Database|string $query    The query (Database object or query string) to be joined with.
	 * @param  Bool            $distinct Set whether this union should be distinct or not.
	 * @return Database                  Returns the Database class which can be method chained for more query building.
	 */
	public function union( $query, $distinct = true ) {
		$this->union[] = [ $query, $distinct ];

		return $this;
	}

	/**
	 * Adds a GROUP BY clause.
	 *
	 * @since 1.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function groupBy() {
		$args = (array) func_get_args();
		if ( count( $args ) === 1 && is_array( $args[0] ) ) {
			$args = $args[0];
		}

		$this->group = array_merge( $this->group, $args );

		return $this;
	}


	/**
	 * Adds a ORDER BY clause.
	 *
	 * @since   1.0.0
	 * @version 1.2.4 Hardened against SQL injection.
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function orderBy() {
		// Normalize arguments.
		$args = (array) func_get_args();
		if ( count( $args ) === 1 && is_array( $args[0] ) ) {
			$args = $args[0];
		}

		$orderBy = [];
		// Separate commas to account for multiple orders.
		foreach ( $args as $argComma ) {
			$orderBy = array_map( 'trim', array_merge( $orderBy, explode( ',', $argComma ) ) );
		}

		// Validate and sanitize column names and sort directions.]
		$sanitizedOrderBy = [];
		foreach ( $orderBy as $ordBy ) {
			$parts     = explode( ' ', $ordBy );
			$column    = str_replace( '`', '', $parts[0] ); // Strip existing ticks first.
			$column    = preg_replace( '/[^a-zA-Z0-9_.]/', '', $column ); // Strip invalid characters from the column name.
			$column    = $this->escapeColNames( $column )[0];
			$direction = isset( $parts[1] ) ? strtoupper( $parts[1] ) : 'ASC';

			// Validate the order direction.
			if ( ! in_array( $direction, [ 'ASC', 'DESC' ], true ) ) {
				$direction = 'ASC';
			}

			$sanitizedOrderBy[] = "$column $direction";
		}

		if ( ! empty( $sanitizedOrderBy ) ) {
			if ( ! empty( $args[0] ) && true !== $args[0] ) {
				$this->order = array_merge( $this->order, $sanitizedOrderBy );
			} else {
				// This allows for overwriting a preexisting order-by setting.
				array_shift( $sanitizedOrderBy );
				$this->order = $sanitizedOrderBy;
			}
		}

		return $this;
	}

	/**
	 * Adds a raw ORDER BY clause.
	 *
	 * @since 1.2.4
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function orderByRaw() {
		$args = (array) func_get_args();
		if ( count( $args ) === 1 && is_array( $args[0] ) ) {
			$args = $args[0];
		}

		$this->order = array_merge( $this->order, $args );

		return $this;
	}

	/**
	 * Sets the sort direction for ORDER BY clauses.
	 *
	 * @since 1.0.0
	 *
	 * @param string    $direction This sets the direction of the order by clause, default is 'ASC'.
	 * @return Database            Returns the Database class which can be method chained for more query building.
	 */
	public function orderDirection( $direction = 'ASC' ) {
		$this->orderDirection = $direction;

		return $this;
	}

	/**
	 * Adds a LIMIT clause.
	 *
	 * @since 1.0.0
	 *
	 * @param  int      $limit  The limit for the limit clause.
	 * @param  int      $offset The offset for the limit clause.
	 * @return Database         Returns the Database class which can be method chained for more query building.
	 */
	public function limit( $limit, $offset = -1 ) {
		if ( ! is_numeric( $limit ) || $limit <= 0 ) {
			return $this;
		}

		if ( ! is_numeric( $offset ) ) {
			$offset = -1;
		}

		$this->limit = ( -1 === $offset )
			? intval( $limit )
			: intval( $offset ) . ', ' . intval( $limit );

		return $this;
	}

	/**
	 * Converts associative arrays to a SET argument.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $args        The arguments.
	 * @return array $preparedSet The prepared arguments.
	 */
	private function prepareSet( $args ) {
		$args = $this->prepArgs( $args );

		$preparedSet = [];
		foreach ( (array) $args as $field => $value ) {
			if ( is_null( $value ) ) {
				$preparedSet[] = "`$field` = NULL";
			} elseif ( is_array( $value ) ) {
				throw new \Exception( 'Cannot save an unserialized array in the database. Data passed was: ' . wp_json_encode( $value ) );
			} elseif ( is_object( $value ) ) {
				throw new \Exception( 'Cannot save an unserialized object in the database. Data passed was: ' . esc_html( $value ) );
			} else {
				$preparedSet[] = sprintf( "`$field` = %s", $this->escape( $value, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
			}
		}

		return $preparedSet;
	}

	/**
	 * Adds a SET clause.
	 *
	 * @since 1.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function set() {
		$this->set = array_merge( $this->set, $this->prepareSet( func_get_args() ) );

		return $this;
	}

	/**
	 * Adds an ON DUPLICATE clause.
	 *
	 * @since 1.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function onDuplicate() {
		$this->onDuplicate = array_merge( $this->onDuplicate, $this->prepareSet( func_get_args() ) );

		return $this;
	}

	/**
	 * Set the output for the query.
	 *
	 * @since 1.0.0
	 *
	 * @param  string   $output  This can be one of the following: ARRAY_A | ARRAY_N | OBJECT | OBJECT_K.
	 * @return Database          Returns the Database class which can be method chained for more query building.
	 */
	public function output( $output ) {
		$this->output = $output;

		return $this;
	}

	/**
	 * Reset the cache so we make sure the query gets to the DB.
	 *
	 * @since 1.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function resetCache() {
		$this->shouldResetCache = true;

		return $this;
	}

	/**
	 * Run this query.
	 *
	 * @since 1.0.0
	 *
	 * @param  boolean  $reset  Whether to reset the results/query.
	 * @param  string   $return Determine which method to call on the $wpdb object
	 * @param  array    $params Optional extra parameters to pass to the db method call
	 * @return Database         Database query results.
	 */
	public function run( $reset = true, $return = 'results', $params = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		if ( ! in_array( $return, [ 'results', 'col', 'var' ], true ) ) {
			$return = 'results';
		}

		$prepare        = $this->db->prepare( $this->query(), 1, 1 );
		$queryHash      = sha1( $this->query() );
		$cacheTableName = $this->getCacheTableName();

		// Pull the result from the in-memory cache if everything checks out.
		if (
			! $this->shouldResetCache &&
			isset( $this->cache[ $cacheTableName ][ $queryHash ][ $return ] ) &&
			empty( $this->join )
		) {
			$this->result = $this->cache[ $cacheTableName ][ $queryHash ][ $return ];

			return $this;
		}

		switch ( $return ) {
			case 'col':
				$this->result = $this->db->get_col( $prepare );
				break;

			case 'var':
				$this->result = $this->db->get_var( $prepare );
				break;

			default:
				$this->result = $this->db->get_results( $prepare, $this->output );
		}

		if ( $reset ) {
			$this->reset();
		}

		$this->cache[ $cacheTableName ][ $queryHash ][ $return ] = $this->result;

		// Reset the cache trigger for the next run.
		$this->shouldResetCache = false;

		return $this;
	}

	/**
	 * Inject a count select statement and return the result.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $countColumn The column to count with. Defaults to '*' all.
	 * @return int                 The count total.
	 */
	public function count( $countColumn = '*' ) {
		$usingGroup = ! empty( $this->group );
		$results    = $this->select( 'count(' . $countColumn . ') as count' )
			->run()
			->result();

		return 1 === $this->numRows() && ! $usingGroup ? (int) $results[0]->count : $this->numRows();
	}

	/**
	 * Returns the query results based on the output.
	 *
	 * @since 1.0.0
	 *
	 * @return mixed This could be an array or an object based on the original output method.
	 */
	public function result() {
		return $this->result;
	}

	/**
	 * Return a model model from a row.
	 *
	 * @since 1.0.0
	 *
	 * @param string  $class The class to call.
	 * @return object        The class object.
	 */
	public function model( $class ) {
		$result = $this->result();

		return ! empty( $result ) ? ( is_array( $result ) ? new $class( (array) current( $result ) ) : $result ) : new $class();
	}

	/**
	 * Return an array of model models from the result
	 *
	 * @since 1.0.0
	 *
	 * @param  string $class  The class to call.
	 * @param  string $id     The id of the index to use.
	 * @param  bool $toJson Whether to convert to json.
	 * @return array         An array of class models.
	 */
	public function models( $class, $id = null, $toJson = false ) {
		if ( empty( $this->models ) ) {
			$i      = 0;
			$models = [];
			foreach ( $this->result() as $row ) {
				$var   = ( null === $id ) ? $row : $row[ $id ];
				$class = new $class( $var );
				// Lets add the class to the array using the class ID.
				$models[ $class->id ] = $toJson ? $class->jsonSerialize() : $class;
				$i++;
			}

			$this->models = $models;
		}

		return $this->models;
	}

	/**
	 * Returns the last error reported by MySQL.
	 *
	 * @since 1.0.0
	 *
	 * @return string The last error.
	 */
	public function lastError() {
		return $this->db->last_error;
	}

	/**
	 * Return the $wpdb insert_id from the last query.
	 *
	 * @since 1.0.0
	 *
	 * @return integer The id of the most recent INSERT query.
	 */
	public function insertId() {
		return $this->db->insert_id;
	}

	/**
	 * Return the $wpdb rows_affected from the last query.
	 *
	 * @since 1.0.0
	 *
	 * @return integer The number of rows affected.
	 */
	public function rowsAffected() {
		return $this->db->rows_affected;
	}

	/**
	 * Return the $wpdb num_rows from the last query.
	 *
	 * @since 1.0.0
	 *
	 * @return integer The count for the number of rows in the last query.
	 */
	public function numRows() {
		return $this->db->num_rows;
	}

	/**
	 * Check if the last query had any rows.
	 *
	 * @since 1.0.0
	 *
	 * @return bool Whether there were any rows retrived by the last query.
	 */
	public function nullSet() {
		return ( $this->numRows() < 1 );
	}

	/**
	 * This will start a MySQL transaction. Be sure to commit or rollback!
	 *
	 * @since 1.0.0
	 */
	public function startTransaction() {
		$this->db->query( 'START TRANSACTION' );
	}

	/**
	 * This will commit a MySQL transaction. Used in conjunction with startTransaction.
	 *
	 * @since 1.0.0
	 */
	public function commit() {
		$this->db->query( 'COMMIT' );
	}

	/**
	 * This will rollback a MySQL transaction. Used in conjunction with startTransaction.
	 *
	 * @since 1.0.0
	 */
	public function rollback() {
		$this->db->query( 'ROLLBACK' );
	}

	/**
	 * Fast way to execute queries.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $sql The sql query to execute.
	 * @return mixed       Could be an array or object depending on the result set.
	 */
	public function execute( $sql, $results = false ) {
		$this->lastQuery = $sql;

		if ( $results ) {
			$this->result = $this->db->get_results( $sql );

			return $this;
		}

		return $this->db->query( $sql );
	}

	/**
	 * Escape a value for safe use in SQL queries.
	 *
	 * @param string  $value   The value to be escaped.
	 * @param boolean $options Escape options.
	 * @return string          The escaped SQL value.
	 */
	public function escape( $value, $options = null ) {
		if ( is_array( $value ) ) {
			foreach ( $value as &$val ) {
				$val = $this->escape( $val, $options );
			}

			return $value;
		} else {
			$options = ( is_null( $options ) ) ? $this->getEscapeOptions() : $options;
			if ( ( $options & self::ESCAPE_STRIP_HTML ) !== 0 && isset( $this->stripTags ) && true === $this->stripTags ) {
				$value = wp_strip_all_tags( $value );
			}

			if (
				( ( $options & self::ESCAPE_FORCE ) !== 0 || php_sapi_name() === 'cli' ) ||
				( ( $options & self::ESCAPE_QUOTE ) !== 0 && ! is_integer( $value ) )
			) {
				$value = esc_sql( $value );
				if ( ! is_integer( $value ) ) {
					$value = "'$value'";
				}
			}

			return $value;
		}
	}

	/**
	 * Get the current escape options.
	 *
	 * @since 1.0.0
	 *
	 * @return integer The current escape options.
	 */
	public function getEscapeOptions() {
		return $this->escapeOptions;
	}


	/**
	 * Set the current escape options.
	 *
	 * @since 1.0.0
	 *
	 * @param integer $options
	 */
	public function setEscapeOptions( $options ) {
		$this->escapeOptions = $options;
	}

	/**
	 * Backtick-escapes an array of column and/or table names.
	 *
	 * @since 1.0.0
	 *
	 * @param array  $cols An array of column names to be escaped.
	 * @return array       An array of escaped column names.
	 */
	private function escapeColNames( $cols ) {
		if ( ! is_array( $cols ) ) {
			$cols = [ $cols ];
		}

		foreach ( $cols as &$col ) {
			if ( false === stripos( $col, '(' ) && false === stripos( $col, ' ' ) && false === stripos( $col, '*' ) ) {
				if ( stripos( $col, '.' ) ) {
					list( $table, $c ) = explode( '.', $col );
					$col = "`$table`.`$c`";
					continue;
				}

				$col = "`$col`";
			}
		}

		return $cols;
	}

	/**
	 * Gets a variable list of function arguments and reformats them as needed for many of the functions of this class.
	 *
	 * @since 1.0.0
	 *
	 * @param  mixed $values This could be anything, but if used properly its usually a string or an array.
	 * @return array         If the preparation is correct it will return an array of arguments.
	 */
	private function prepArgs( $values ) {
		$values = (array) $values;
		if ( ! is_array( $values[0] ) && count( $values ) === 2 ) {
			$values = [ $values[0] => $values[1] ];
		} elseif ( is_array( $values[0] ) && count( $values ) === 1 ) {
			$values = $values[0];
		}

		return $values;
	}

	/**
	 * Resets all the variables that make up the query.
	 *
	 * @since 1.0.0
	 *
	 * @param array     $what Set which items you want to reset, all are selected by default.
	 * @return Database       Returns the Database object.
	 */
	public function reset(
		$what = [
			'table',
			'statement',
			'limit',
			'group',
			'order',
			'select',
			'set',
			'onDuplicate',
			'ignore',
			'where',
			'union',
			'distinct',
			'orderDirection',
			'query',
			'output',
			'stripTags',
			'models',
			'join'
		]
	) {
		// If we are not running a select query, let's bust the cache for this table.
		$selectStatements = [ 'SELECT', 'SELECT DISTINCT' ];
		if (
			! empty( $this->statement ) &&
			! in_array( $this->statement, $selectStatements, true )
		) {
			$this->bustCache( $this->getCacheTableName() );
		}

		foreach ( (array) $what as $var ) {
			switch ( $var ) {
				case 'group':
				case 'order':
				case 'select':
				case 'set':
				case 'onDuplicate':
				case 'where':
				case 'union':
				case 'join':
					$this->$var = [];
					break;
				case 'orderDirection':
					$this->$var = 'ASC';
					break;
				case 'ignore':
				case 'stripTags':
					$this->$var = false;
					break;
				case 'output':
					$this->$var = 'OBJECT';
					break;
				default:
					if ( isset( $this->$var ) ) {
						$this->$var = null;
					}
					break;
			}
		}

		return $this;
	}

	/**
	 * Get the current value of one or more query properties. If only one property is specified, returns the value;
	 * if an array of values is specified, then returns an array of values.
	 *
	 * @since 1.0.0
	 *
	 * @param string|array  $what You can pass in an array of options to retrieve. By default it selects all if them.
	 * @return string|array       Returns the value of whichever variables are passed in.
	 */
	public function getQueryProperty(
		$what = [
			'table',
			'statement',
			'limit',
			'group',
			'order',
			'select',
			'set',
			'onDuplicate',
			'where',
			'union',
			'distinct',
			'orderDirection',
			'query',
			'output',
			'result'
		]
	) {
		if ( is_array( $what ) ) {
			$return = [];
			foreach ( (array) $what as $which ) {
				$return[ $which ] = $this->$which;
			}

			return $return;
		} else {
			return $this->$what;
		}
	}

	/**
	 * Get a table name for the cache key.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $cacheTableName The table name to check against.
	 * @return string                 The cache key table name.
	 */
	private function getCacheTableName( $cacheTableName = null ) {
		$cacheTableName = empty( $cacheTableName ) ? $this->table : $cacheTableName;

		foreach ( $this->customTables as $tableName ) {
			if ( false !== stripos( $cacheTableName, $this->prefix . $tableName ) ) {
				$cacheTableName = $tableName;
				break;
			}
		}

		return $cacheTableName;
	}

	/**
	 * Busts the cache for the given table name.
	 *
	 * @since 1.0.0
	 *
	 * @param  string|null $tableName The table name.
	 * @return void
	 */
	public function bustCache( $tableName = null ) {
		if ( ! $tableName ) {
			// Bust all the cache.
			$this->cache = [];

			return;
		}

		unset( $this->cache[ $tableName ] );
	}

	/**
	 * In order to not have a conflict, we need to return a clone.
	 *
	 * @since 1.0.0
	 *
	 * @return Database The cloned Database object.
	 */
	public function noConflict() {
		return clone $this;
	}
}Core/Filesystem.php000064400000013735151536237000010304 0ustar00<?php
// phpcs:disable WordPress.WP.AlternativeFunctions
namespace AIOSEO\BrokenLinkChecker\Core;

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

/**
 * Interface for the filesystem.
 *
 * @since 1.0.0
 */
class Filesystem {
	/**
	 * Holds the WP filesystem instance.
	 *
	 * @since 1.0.0
	 *
	 * @var \WP_Filesystem_Base
	 */
	public $fs = null;

	/**
	 * Core class instance.
	 *
	 * @since 1.1.0
	 *
	 * @var Core
	 */
	public $core = null;

	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 *
	 * @param Core  $core The Core class.
	 * @param array $args Optional arguments needed to construct the class with.
	 */
	public function __construct( $core, $args = [] ) {
		$this->core = $core;
		$this->init( $args );
	}

	/**
	 * Initialize the filesystem.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $args List of arguments for the WP_Filesystem class.
	 * @return void
	 */
	public function init( $args = [] ) {
		require_once ABSPATH . 'wp-admin/includes/file.php';

		WP_Filesystem( $args );
		// phpcs:disable Squiz.NamingConventions.ValidVariableName
		global $wp_filesystem;
		if ( is_object( $wp_filesystem ) ) {
			$this->fs = $wp_filesystem;
		}
		// phpcs:enable Squiz.NamingConventions.ValidVariableName
	}

	/**
	 * Wrapper method to check if a file exists.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $filename The filename to check if it exists.
	 * @return bool             Returns true if the file or dir specified exists; false otherwise.
	 */
	public function exists( $filename ) {
		if ( ! $this->isWpfsValid() ) {
			return @file_exists( $filename );
		}

		return $this->fs->exists( $filename );
	}

	/**
	 * Retrieve the contents of a file.
	 *
	 * @since 1.0.0
	 *
	 * @param  string      $filename The filename to get the contents for.
	 * @return string|bool           The function returns the read data or false on failure.
	 */
	public function getContents( $filename ) {
		if ( ! $this->exists( $filename ) ) {
			return false;
		}

		if ( ! $this->isWpfsValid() ) {
			return @file_get_contents( $filename );
		}

		return $this->fs->get_contents( $filename );
	}

	/**
	 * Reads entire file into an array.
	 *
	 * @since 1.0.0
	 *
	 * @param  string     $file Path to the file.
	 * @return array|bool       File contents in an array on success, false on failure.
	 */
	public function getContentsArray( $file ) {
		if ( ! $this->exists( $file ) ) {
			return false;
		}

		if ( ! $this->isWpfsValid() ) {
			return @file( $file );
		}

		return $this->fs->get_contents_array( $file );
	}

	/**
	 * Sets the access and modification times of a file.
	 * Note: If $file doesn't exist, it will be created.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $file  Path to file.
	 * @param  int    $time  Optional. Modified time to set for file. Default 0.
	 * @param  int    $atime Optional. Access time to set for file. Default 0.
	 * @return bool          True on success, false on failure.
	 */
	public function touch( $file, $time = 0, $atime = 0 ) {
		if ( 0 === $time ) {
			$time = time();
		}

		if ( 0 === $atime ) {
			$atime = time();
		}

		if ( ! $this->isWpfsValid() ) {
			return @touch( $file, $time, $atime );
		}

		return $this->fs->touch( $file, $time, $atime );
	}

	/**
	 * Writes a string to a file.
	 *
	 * @since 1.0.0
	 *
	 * @param  string    $file     Remote path to the file where to write the data.
	 * @param  string    $contents The data to write.
	 * @param  int|false $mode     Optional. The file permissions as octal number, usually 0644. Default false.
	 * @return int|bool            True on success, false on failure.
	 */
	public function putContents( $file, $contents, $mode = false ) {
		if ( ! $this->isWpfsValid() ) {
			return @file_put_contents( $file, $contents );
		}

		return $this->fs->put_contents( $file, $contents, $mode );
	}

	/**
	 * Checks if a file or dir is writable.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $file Path to file or dir.
	 * @return bool         Whether $file is writable.
	 */
	public function isWritable( $file ) {
		if ( ! $this->isWpfsValid() ) {
			return @is_writable( $file );
		}

		return $this->fs->is_writable( $file );
	}

	/**
	 * Checks if a file is readable.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $file Path to file.
	 * @return bool         Whether $file is readable.
	 */
	public function isReadable( $file ) {
		if ( ! $this->isWpfsValid() ) {
			return @is_readable( $file );
		}

		return $this->fs->is_readable( $file );
	}

	/**
	 * Gets the file size (in bytes).
	 *
	 * @since 1.0.0
	 *
	 * @param  string   $file Path to file.
	 * @return int|bool       Size of the file in bytes on success, false on failure.
	 */
	public function size( $file ) {
		if ( ! $this->isWpfsValid() ) {
			return @filesize( $file );
		}

		return $this->fs->size( $file );
	}

	/**
	 * Checks if resource is a file.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $file File path.
	 * @return bool         Whether $file is a file.
	 */
	public function isFile( $file ) {
		if ( ! $this->isWpfsValid() ) {
			return @is_file( $file );
		}

		return $this->fs->is_file( $file );
	}

	/**
	 * Checks if resource is a directory.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $path Directory path.
	 * @return bool         Whether $path is a directory.
	 */
	public function isDir( $path ) {
		if ( ! $this->isWpfsValid() ) {
			return @is_dir( $path );
		}

		return $this->fs->is_dir( $path );
	}

	/**
	 * A simple check to ensure that the WP_Filesystem is valid.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if valid, false if not.
	 */
	public function isWpfsValid() {
		if (
			! is_a( $this->fs, 'WP_Filesystem_Base' ) ||
			(
				// Errors is a WP_Error object.
				! empty( $this->fs->errors ) &&
				// We check if the errors array is empty for compatibility with WP < 5.1.
				! empty( $this->fs->errors->errors )
			)
		) {
			return false;
		}

		return true;
	}

	/**
	 * In order to prevent conflicts, we need to return a clone.
	 *
	 * @since 1.0.0
	 *
	 * @return Filesystem The cloned Filesystem object.
	 */
	public function noConflict() {
		return clone $this;
	}
}Core/NetworkCache.php000064400000005314151536237000010527 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Core;

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

/**
 * Handles our network cache.
 *
 * @since 1.0.0
 */
class NetworkCache extends Cache {
	/**
	 * Returns the cache value for a key if it exists and is not expired.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $key The cache key name. Use a '%' for a like query.
	 * @return mixed       The value or null if the cache does not exist.
	 */
	public function get( $key ) {
		if ( ! is_multisite() ) {
			return parent::get( $key );
		}

		aioseoBrokenLinkChecker()->helpers->switchToBlog( aioseoBrokenLinkChecker()->helpers->getNetworkId() );
		$value = parent::get( $key );
		aioseoBrokenLinkChecker()->helpers->restoreCurrentBlog();

		return $value;
	}

	/**
	 * Updates the given cache or creates it if it doesn't exist.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $key        The cache key name.
	 * @param  mixed  $value      The value.
	 * @param  int    $expiration The expiration time in seconds. Defaults to 24 hours. 0 to no expiration.
	 * @return void
	 */
	public function update( $key, $value, $expiration = DAY_IN_SECONDS ) {
		if ( ! is_multisite() ) {
			parent::update( $key, $value, $expiration );

			return;
		}

		aioseoBrokenLinkChecker()->helpers->switchToBlog( aioseoBrokenLinkChecker()->helpers->getNetworkId() );
		parent::update( $key, $value, $expiration );
		aioseoBrokenLinkChecker()->helpers->restoreCurrentBlog();
	}

	/**
	 * Deletes the given cache key.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $key The cache key.
	 * @return void
	 */
	public function delete( $key ) {
		if ( ! is_multisite() ) {
			parent::delete( $key );

			return;
		}

		aioseoBrokenLinkChecker()->helpers->switchToBlog( aioseoBrokenLinkChecker()->helpers->getNetworkId() );
		parent::delete( $key );
		aioseoBrokenLinkChecker()->helpers->restoreCurrentBlog();
	}

	/**
	 * Clears all of our cache.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function clear() {
		if ( ! is_multisite() ) {
			parent::clear();

			return;
		}

		aioseoBrokenLinkChecker()->helpers->switchToBlog( aioseoBrokenLinkChecker()->helpers->getNetworkId() );
		parent::clear();
		aioseoBrokenLinkChecker()->helpers->restoreCurrentBlog();
	}

	/**
	 * Clears all of our cache under a certain prefix.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $prefix A prefix to clear or empty to clear everything.
	 * @return void
	 */
	public function clearPrefix( $prefix ) {
		if ( ! is_multisite() ) {
			parent::clearPrefix( $prefix );

			return;
		}

		aioseoBrokenLinkChecker()->helpers->switchToBlog( aioseoBrokenLinkChecker()->helpers->getNetworkId() );
		parent::clearPrefix( $prefix );
		aioseoBrokenLinkChecker()->helpers->restoreCurrentBlog();
	}
}Core/Uninstall.php000064400000004675151536237000010134 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Core;

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

use AIOSEO\BrokenLinkChecker\Utils;

/**
 * Handles plugin deinstallation.
 *
 * @since 1.0.0
 */
class Uninstall {
	/**
	 * Removes all our tables and options.
	 *
	 * @since 1.0.0
	 *
	 * @param  bool $force Whether we should ignore the uninstall option or not. We ignore it when we reset all data via the Debug Panel.
	 * @return void
	 */
	public function dropData( $force = false ) {
		// Confirm that user has decided to remove all data, otherwise stop.
		if (
			! $force &&
			( ! aioseoBrokenLinkChecker()->options->advanced->enable || ! aioseoBrokenLinkChecker()->options->advanced->uninstall )
		) {
			return;
		}

		// Delete all our custom tables.
		global $wpdb;
		foreach ( $this->getDbTables() as $tableName ) {
			$wpdb->query( 'DROP TABLE IF EXISTS ' . $tableName ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		}

		// Delete all the plugin settings.
		$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'aioseo\_blc\_%'" );

		// Remove any transients we've left behind.
		$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '\_aioseo\_blc\_%'" );

		// Delete all entries from the action scheduler table.
		$wpdb->query( "DELETE FROM {$wpdb->prefix}actionscheduler_actions WHERE hook LIKE 'aioseo\_blc\_%'" );
		$wpdb->query( "DELETE FROM {$wpdb->prefix}actionscheduler_groups WHERE slug = 'aioseo\_blc'" );

		// Delete all our custom capabilities.
		$this->uninstallCapabilities();
	}

	/**
	 * Returns all the DB tables with their prefix.
	 *
	 * @since 1.0.0
	 *
	 * @return array List of tables.
	 */
	private function getDbTables() {
		global $wpdb;

		$tables = [];
		foreach ( aioseoBrokenLinkChecker()->core->db->customTables as $tableName ) {
			$tables[] = $wpdb->prefix . $tableName;
		}

		return $tables;
	}

	/**
	 * Removes all our custom capabilities.
	 *
	 * @since 1.2.4
	 *
	 * @return void
	 */
	private function uninstallCapabilities() {
		$access             = new Utils\Access();
		$customCapabilities = $access->getCapabilityList() ?? [];
		$roles              = aioseoBrokenLinkChecker()->helpers->getUserRoles();

		// Loop through roles and remove custom capabilities.
		foreach ( $roles as $roleName => $roleInfo ) {
			$role = get_role( $roleName );

			if ( $role ) {
				foreach ( $customCapabilities as $capability ) {
					$role->remove_cap( $capability );
				}
			}
		}
	}
}LinkStatus/Data.php000064400000010774151536237000010242 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\LinkStatus;

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Handles fetching of data required for Link Status scan requests.
 *
 * @since 1.0.0
 */
class Data {
	/**
	 * Returns the base data we need to include in our requests to the server.
	 *
	 * @since 1.0.0
	 *
	 * @return array The base data.
	 */
	public function getBaseData() {
		return [
			'domain'          => aioseoBrokenLinkChecker()->helpers->getSiteDomain(),
			'internalOptions' => aioseoBrokenLinkChecker()->internalOptions->all(),
			'indexedLinks'    => aioseoBrokenLinkChecker()->core->db->start( 'aioseo_blc_link_status' )->count(),
			'isSsl'           => is_ssl(),
			'options'         => aioseoBrokenLinkChecker()->options->all(),
			'version'         => AIOSEO_BROKEN_LINK_CHECKER_VERSION
		];
	}

	/**
	 * Returns links that still need to be checked.
	 *
	 * @since 1.0.0
	 *
	 * @param  bool      $countOnly          Whether to return the count instead of all the rows.
	 * @param  bool      $ignoreStaleResults Whether to ignore stale results.
	 * @return array|int                     The links to check the status for.
	 */
	public function getLinksToCheck( $countOnly = false, $ignoreStaleResults = false ) {
		static $linksToScan = null;
		if ( null !== $linksToScan ) {
			return $linksToScan;
		}

		$linksPerScan         = 200;
		$includedPostTypes    = aioseoBrokenLinkChecker()->helpers->getIncludedPostTypes();
		$includedPostStatuses = aioseoBrokenLinkChecker()->helpers->getIncludedPostStatuses();
		$excludedPostIds      = aioseoBrokenLinkChecker()->helpers->getExcludedPostIds();
		$time                 = aioseoBrokenLinkChecker()->helpers->timeToMysql( strtotime( '-7 days' ) );

		$query = aioseoBrokenLinkChecker()->core->db->start( 'aioseo_blc_link_status as als' )
			->join( 'aioseo_blc_links al', 'al.blc_link_status_id = als.id' )
			->join( 'posts as p', 'p.ID = al.post_id' )
			->where( 'als.dismissed', 0 )
			->groupBy( 'als.id' );

		if ( $ignoreStaleResults ) {
			$query->where( 'als.last_scan_date', null );
		} else {
			$query->whereRaw( "(
				als.last_scan_date IS NULL
				OR als.last_scan_date < '$time'
			)" );
		}

		$excludedDomains = aioseoBrokenLinkChecker()->helpers->getExcludedDomains();
		if ( ! empty( $excludedDomains ) ) {
			$query->whereNotIn( 'al.hostname', $excludedDomains );
		}

		if ( aioseoBrokenLinkChecker()->license->isFree() ) {
			$query->where( 'al.external', 0 );
		}

		if ( ! empty( $includedPostStatuses ) ) {
			$includedPostStatuses = aioseoBrokenLinkChecker()->helpers->implodeWhereIn( $includedPostStatuses, true );
			$query->whereRaw( "p.post_status IN ( $includedPostStatuses )" );
		}

		if ( ! empty( $includedPostTypes ) ) {
			$includedPostTypes = aioseoBrokenLinkChecker()->helpers->implodeWhereIn( $includedPostTypes, true );
			$query->whereRaw( "p.post_type IN ( $includedPostTypes )" );
		}

		if ( ! empty( $excludedPostIds ) ) {
			$excludedPostIds = aioseoBrokenLinkChecker()->helpers->implodeWhereIn( $excludedPostIds, true );
			$query->whereRaw( "p.ID NOT IN ( $excludedPostIds )" );
		}

		if ( $countOnly ) {
			return $query->count();
		}

		$linksToScan = $query->select( 'als.id, als.url' )
			->limit( $linksPerScan )
			->run()
			->result();

		return $linksToScan;
	}

	/**
	 * Returns the total number of indexed links.
	 *
	 * @since 1.1.0
	 *
	 * @return int The total number of indexed links.
	 */
	private function getTotalLinks() {
		$query = aioseoBrokenLinkChecker()->core->db->start( 'aioseo_blc_link_status as als' )
			->select( 'als.id' )
			->join( 'aioseo_blc_links al', 'al.blc_link_status_id = als.id' )
			->where( 'als.dismissed', 0 )
			->groupBy( 'als.id' );

		$excludedDomains = aioseoBrokenLinkChecker()->helpers->getExcludedDomains();
		if ( ! empty( $excludedDomains ) ) {
			$query->whereNotIn( 'al.hostname', $excludedDomains );
		}

		if ( aioseoBrokenLinkChecker()->license->isFree() ) {
			$query->where( 'al.external', 0 );
		}

		return $query->count();
	}

	/**
	 * Returns the scan percentage.
	 *
	 * @since 1.1.0
	 *
	 * @return int The scan percentage.
	 */
	public function getScanPercentage() {
		$linksToCheck = $this->getLinksToCheck( true, true );
		$totalLinks   = $this->getTotalLinks();
		if (
			( 0 === $linksToCheck || 0 === $totalLinks ) ||
			// If there's just a few posts to scan, then we don't want to show the scan percentage bubble.
			5 >= (int) $linksToCheck
		) {
			return 100;
		}

		return ceil( 100 - ( ( $linksToCheck / $totalLinks ) * 100 ) );
	}
}LinkStatus/LinkStatus.php000064400000024347151536237000011473 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\LinkStatus;

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Handles the Link Status scan.
 *
 * @since 1.0.0
 */
class LinkStatus {
	/**
	 * The base URL for the broken link checker server.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private $baseUrl = 'https://check-links.aioseo.com/v1/';

	/**
	 * The action name of the scan.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public $actionName = 'aioseo_blc_link_status_scan';

	/**
	 * Data class instance.
	 *
	 * @since 1.1.0
	 *
	 * @var Data
	 */
	public $data = null;

	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		$this->data = new Data();

		add_action( $this->actionName, [ $this, 'checkLinkStatuses' ], 11, 1 );
		if ( ! is_admin() ) {
			return;
		}

		add_action( 'init', [ $this, 'scheduleScan' ], 3003 );
	}

	/**
	 * Schedules the link status scan.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function scheduleScan() {
		if ( ! aioseoBrokenLinkChecker()->license->isActive() ) {
			return;
		}

		// If there is no action at all, schedule one.
		if ( ! aioseoBrokenLinkChecker()->actionScheduler->isScheduled( $this->actionName ) ) {
			aioseoBrokenLinkChecker()->actionScheduler->scheduleAsync( $this->actionName );
		}
	}

	/**
	 * Sends links to the server to check their status.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function checkLinkStatuses() {
		if ( ! aioseoBrokenLinkChecker()->license->isActive() ) {
			return;
		}

		$scanId = aioseoBrokenLinkChecker()->internalOptions->internal->scanId;
		if ( ! empty( $scanId ) ) {
			// If we have a scan ID, check if the results are ready.
			$this->checkForScanResults();

			return;
		}

		// If we don't have a scan ID, first check if there are links that need to be checked.
		$linksToCheck = $this->data->getlinksToCheck();
		if ( empty( $linksToCheck ) ) {
			// If there are no links to check, wait 15 minutes.
			aioseoBrokenLinkChecker()->actionScheduler->scheduleSingle( $this->actionName, 15 * MINUTE_IN_SECONDS );

			return;
		}

		// If there are links to check, start a new scan.
		$this->startScan();
	}

	/**
	 * Start a scan and store the scan ID.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	private function startScan() {
		$requestBody = array_merge(
			$this->data->getBaseData(),
			[
				'links' => $this->data->getlinksToCheck()
			]
		);

		$response     = $this->doPostRequest( 'scan/start/', $requestBody );
		$responseCode = (int) wp_remote_retrieve_response_code( $response );

		if ( 401 === $responseCode ) {
			aioseoBrokenLinkChecker()->actionScheduler->scheduleSingle( $this->actionName, DAY_IN_SECONDS + wp_rand( 60, 600 ) );

			return;
		}

		if ( 418 === $responseCode ) {
			aioseoBrokenLinkChecker()->actionScheduler->scheduleSingle( $this->actionName, HOUR_IN_SECONDS + wp_rand( 60, 600 ) );

			return;
		}

		$responseBody = json_decode( wp_remote_retrieve_body( $response ) );
		if (
			is_wp_error( $response ) ||
			200 !== $responseCode ||
			empty( $responseBody->success ) ||
			empty( $responseBody->scanId ) ||
			! isset( $responseBody->quotaRemaining )
		) {
			aioseoBrokenLinkChecker()->actionScheduler->scheduleSingle( $this->actionName, MINUTE_IN_SECONDS );

			return;
		}

		aioseoBrokenLinkChecker()->internalOptions->internal->scanId                  = $responseBody->scanId;
		aioseoBrokenLinkChecker()->internalOptions->internal->license->quotaRemaining = $responseBody->quotaRemaining;
		if ( aioseoBrokenLinkChecker()->internalOptions->internal->license->quota !== $responseBody->quota ) {
			// If the quota changed, reactivate the license to pull in the latest date from the marketing site.
			aioseoBrokenLinkChecker()->internalOptions->internal->license->quota = $responseBody->quota;
			aioseoBrokenLinkChecker()->license->activate();
		}

		aioseoBrokenLinkChecker()->actionScheduler->scheduleSingle( $this->actionName, MINUTE_IN_SECONDS );
	}

	/**
	 * Checks if the scan has been completed. If so, parses and stores the results.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	private function checkForScanResults() {
		$scanId = aioseoBrokenLinkChecker()->internalOptions->internal->scanId;
		if ( empty( $scanId ) ) {
			return;
		}

		$response     = $this->doPostRequest( "scan/{$scanId}/" );
		$responseCode = (int) wp_remote_retrieve_response_code( $response );

		if ( 401 === $responseCode ) {
			aioseoBrokenLinkChecker()->actionScheduler->scheduleSingle( $this->actionName, DAY_IN_SECONDS + wp_rand( 60, 600 ) );

			return;
		}

		if ( 418 === $responseCode ) {
			aioseoBrokenLinkChecker()->actionScheduler->scheduleSingle( $this->actionName, HOUR_IN_SECONDS + wp_rand( 60, 600 ) );

			return;
		}

		$responseBody = json_decode( wp_remote_retrieve_body( $response ) );
		if ( is_wp_error( $response ) && 200 !== $responseCode || empty( $responseBody->success ) ) {
			// If the scan data cannot be found on the server, wipe the scan ID so the scan restarts.
			if ( ! empty( $responseBody->error ) && 'missing-scan-data' === strtolower( $responseBody->error ) ) {
				aioseoBrokenLinkChecker()->internalOptions->internal->scanId = '';
			}

			aioseoBrokenLinkChecker()->actionScheduler->scheduleSingle( $this->actionName, MINUTE_IN_SECONDS );

			return;
		}

		$this->parseResults( $responseBody );

		aioseoBrokenLinkChecker()->internalOptions->internal->license->quotaRemaining = $responseBody->quotaRemaining;
		if ( aioseoBrokenLinkChecker()->internalOptions->internal->license->quota !== $responseBody->quota ) {
			// If the quota changed, reactivate the license to pull in the latest date from the marketing site.
			aioseoBrokenLinkChecker()->internalOptions->internal->license->quota = $responseBody->quota;
			aioseoBrokenLinkChecker()->license->activate();
		}

		// Once the request is successful, we know the scan has been completed and we can go ahead and reset it.
		$this->doDeleteRequest( "scan/{$scanId}/" );
		aioseoBrokenLinkChecker()->internalOptions->internal->scanId = '';
	}

	/**
	 * Parse the results that came back from the server.
	 *
	 * @since 1.0.0
	 *
	 * @param  Object $responseBody The response body object.
	 * @return void
	 */
	private function parseResults( $responseBody ) {
		$scanData = json_decode( $responseBody->scanData );
		if ( empty( $scanData ) || empty( $scanData->urls ) ) {
			return;
		}

		foreach ( $scanData->urls as $url ) {
			$this->parseResultsHelper( $url );
		}
	}

	/**
	 * Helper function for parseResults().
	 *
	 * @since 1.0.0
	 *
	 * @param  Object $url The URL object.
	 * @return void
	 */
	public function parseResultsHelper( $url ) {
		$linkStatus = Models\LinkStatus::getByUrl( $url->url );
		if ( ! $linkStatus->exists() || empty( $url->data ) ) {
			return;
		}

		if ( empty( $url->data->status ) ) {
			$linkStatus->scanning         = false;
			$linkStatus->broken           = true;
			$linkStatus->http_status_code = null;
			$linkStatus->request_duration = 0;
			$linkStatus->final_url        = '';
			$linkStatus->scan_count       = $linkStatus->scan_count + 1;
			$linkStatus->last_scan_date   = aioseoBrokenLinkChecker()->helpers->timeToMysql( time() );
			$linkStatus->log              = [
				'error'   => ! empty( $url->data->error ) ? $url->data->error : '',
				'headers' => ! empty( $url->data->headers ) ? $url->data->headers : ''
			];

			if ( ! $linkStatus->first_failure ) {
				$linkStatus->first_failure = aioseoBrokenLinkChecker()->helpers->timeToMysql( time() );
			}

			$linkStatus->save();

			return;
		}

		$success       = (int) $url->data->status < 400;
		$redirectCount = count( $url->data->redirects );
		$finalUrl      = $redirectCount ? $url->data->redirects[ $redirectCount - 1 ] : '';

		$linkStatus->scanning         = false;
		$linkStatus->broken           = ! $success;
		$linkStatus->http_status_code = (int) $url->data->status;
		$linkStatus->redirect_count   = $redirectCount;
		$linkStatus->final_url        = $finalUrl;
		$linkStatus->request_duration = ! empty( $url->data->stats->loadTime ) ? abs( $url->data->stats->loadTime ) : 0;
		$linkStatus->scan_count       = $linkStatus->scan_count + 1;
		$linkStatus->last_scan_date   = aioseoBrokenLinkChecker()->helpers->timeToMysql( time() );
		$linkStatus->log              = [
			'error'   => ! empty( $url->data->error ) ? $url->data->error : '',
			'headers' => ! empty( $url->data->headers ) ? $url->data->headers : ''
		];

		if ( $success ) {
			$linkStatus->last_success  = aioseoBrokenLinkChecker()->helpers->timeToMysql( time() );
			$linkStatus->first_failure = null;
		} elseif ( ! $linkStatus->first_failure ) {
			$linkStatus->first_failure = aioseoBrokenLinkChecker()->helpers->timeToMysql( time() );
		}

		$linkStatus->save();
	}

	/**
	 * Returns the URL for the Broken Link Checker server.
	 *
	 * @since 1.0.0
	 *
	 * @return string The URL.
	 */
	public function getUrl() {
		if ( defined( 'AIOSEO_BROKEN_LINK_CHECKER_SCAN_URL' ) ) {
			return AIOSEO_BROKEN_LINK_CHECKER_SCAN_URL;
		}

		return $this->baseUrl;
	}

	/**
	 * Sends a POST request to the server.
	 *
	 * @since 1.0.0
	 *
	 * @param  string          $path        The path.
	 * @param  array           $requestBody The request body.
	 * @return array|\WP_Error              The response or WP_Error on failure.
	 */
	public function doPostRequest( $path, $requestBody = [] ) {
		$requestData = [
			'headers' => [
				'X-AIOSEO-BLC-License' => aioseoBrokenLinkChecker()->internalOptions->internal->license->licenseKey,
				'Content-Type'         => 'application/json'
			],
			'timeout' => 60
		];

		if ( ! empty( $requestBody ) ) {
			$requestData['body'] = wp_json_encode( $requestBody );
		}

		$baseUrl  = $this->getUrl();
		$response = wp_remote_post( $baseUrl . $path, $requestData );

		return $response;
	}

	/**
	 * Sends a DELETE request to the server.
	 *
	 * @since 1.0.0
	 *
	 * @param  string          $path The path.
	 * @return array|\WP_Error       The response or WP_Error on failure.
	 */
	public function doDeleteRequest( $path ) {
		$requestData = [
			'method'  => 'DELETE',
			'headers' => [
				'X-AIOSEO-BLC-License' => aioseoBrokenLinkChecker()->internalOptions->internal->license->licenseKey,
				'Content-Type'         => 'application/json'
			],
			'timeout' => 60
		];

		$baseUrl  = $this->getUrl();
		$response = wp_remote_request( $baseUrl . $path, $requestData );

		return $response;
	}
}Links/Data.php000064400000026501151536237000007214 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Links;

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Handles the extraction, parsing and storage of links for the links scan.
 *
 * @since 1.0.0
 */
class Data {
	/**
	 * The ignored extensions.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private $ignoredExtensions = [];

	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		$this->setIgnoredExtensions();
	}

	/**
	 * Indexes the links in the given post.
	 *
	 * @since 1.0.0
	 *
	 * @param  int  $postId The post ID.
	 * @return void
	 */
	public function indexLinks( $postId ) {
		$post = get_post( $postId );
		if ( ! is_a( $post, 'WP_Post' ) ) {
			return;
		}

		// Delete all links first. We have to do this in order to remove old links that no longer exist.
		Models\Link::deleteLinks( $postId );

		$links = $this->extractLinks( $postId, $post->post_content );
		if ( empty( $links ) ) {
			return;
		}

		$this->storeLinks( $links );
	}

	/**
	 * Stores the given links to the DB.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $links The links.
	 * @return void
	 */
	private function storeLinks( $links ) {
		$urls         = [];
		$insertValues = [];
		$currentDate  = gmdate( 'Y-m-d H:i:s' );
		foreach ( $links as $linkData ) {
			$data = Models\Link::sanitizeLink( $linkData );
			if ( empty( $data ) ) {
				continue;
			}

			if ( ! Models\Link::validateLink( $data ) ) {
				continue;
			}

			$urls[ $data['url_hash'] ] = $data['url'];

			$blcLinkStatusId = '%d';
			if ( empty( $data['blc_link_status_id'] ) ) {
				$blcLinkStatusId           = '%s';
				$data['blc_link_status_id'] = 'null';
			}

			$insertValues[] = vsprintf(
				"(%d, $blcLinkStatusId, '%s', '%s', '%s', '%s', %d, '%s', '%s', '%s', '%s', '%s', '$currentDate', '$currentDate')",
				$data
			);
		}

		$implodedInsertValues = implode( ',', $insertValues );

		$tableName = aioseoBrokenLinkChecker()->core->db->prefix . 'aioseo_blc_links';
		aioseoBrokenLinkChecker()->core->db->execute(
			"INSERT INTO $tableName
			(`post_id`, `blc_link_status_id`, `url`, `url_hash`, `hostname`, `hostname_url`, `external`, `anchor`, `phrase`, `phrase_html`, `paragraph`, `paragraph_html`, `created`, `updated`)
			VALUES $implodedInsertValues"
		);

		$existing = aioseoBrokenLinkChecker()->core->db->start( 'aioseo_blc_link_status' )
			->select( 'url_hash' )
			->whereIn( 'url_hash', array_keys( $urls ) )
			->run()
			->result();

		foreach ( $existing as $row ) {
			unset( $urls[ $row->url_hash ] );
		}

		if ( empty( $urls ) ) {
			return;
		}

		foreach ( $urls as $hash => $url ) {
			$statusId = aioseoBrokenLinkChecker()->core->db->insert( 'aioseo_blc_link_status' )
				->set( [
					'url'      => $url,
					'url_hash' => $hash,
					'created'  => aioseoBrokenLinkChecker()->helpers->timeToMysql( time() ),
					'updated'  => aioseoBrokenLinkChecker()->helpers->timeToMysql( time() )
				] )
				->run()
				->insertId();

			aioseoBrokenLinkChecker()->core->db->update( 'aioseo_blc_links' )
				->where( 'url', $url )
				->set( [
					'blc_link_status_id' => $statusId
				] )
				->run();
		}
	}

	/**
	 * Returns the links that are in the post content.
	 *
	 * @since 1.0.0
	 *
	 * @param  int    $postId      The post ID.
	 * @param  string $postContent The post content.
	 * @return array               The links.
	 */
	private function extractLinks( $postId, $postContent ) {
		$postContent = aioseoBrokenLinkChecker()->helpers->decodeHtmlEntities( $postContent );

		// Strip data URIs to prevent catastrophic backtracking.
		$postContent = preg_replace( '/data:[^;]+;base64,[^"]+/', '', (string) $postContent );

		/**
		 * Regex pattern divided into groups:
		 * 0  - Full phrase with link tag.
		 * 2  - Start of the phrase, before the anchor.
		 * 4  - The URL.
		 * 6  - The anchor.
		 * 9  - The end of the phrase, after the anchor.
		 * 10 - The ending punctuation mark.
		 */
		preg_match_all(
			'/(([^\r\n.?!]*)<t?a[^>]*?href=(\"|\')(?!tel:|mailto:)([^\"\']*?)(\"|\')[^>]*?>([\s\w\W]*?)<\/t?a>|<!-- wp:core-embed\/wordpress {"url":"([^"]*?)"[^}]*?"} -->|(?:>|&nbsp;|\s)((?:(?:http|ftp|https)\:\/\/)(?:[\w_-]+(?:(?:\.[\w_-]+)+))(?:[\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-]))(?:<|&nbsp;|\s))([^<>.?!\r\n]*)([.?!]?)/i', // phpcs:disable Generic.Files.LineLength.MaxExceeded
			(string) $postContent,
			$matches
		);

		if ( empty( $matches[0] ) ) {
			return [];
		}

		$links = [];
		foreach ( $matches[0] as $k => $v ) {
			if ( empty( $matches[4][ $k ] ) || empty( $matches[6][ $k ] ) ) {
				continue;
			}

			$parsedUrl = $this->parseUrl( $matches[4][ $k ] );
			if ( empty( $parsedUrl['host'] ) ) {
				continue;
			}

			if (
				! empty( $parsedUrl['path'] ) &&
				preg_match( '/\.(.*?)$/i', $parsedUrl['path'], $extension ) &&
				! empty( $extension[1] ) &&
				in_array( $extension[1], $this->ignoredExtensions, true )
			) {
				continue;
			}

			// NOTE: We need to check this here before we strip off the "www" part.
			// Otherwise we will not be able to detect internal links on sites running on "www".
			$isInternal = $parsedUrl['host'] === $this->getHostname();

			$hostname = aioseoBrokenLinkChecker()->helpers->pregReplace( '/www\./i', '', $parsedUrl['host'] );
			$anchor   = wp_strip_all_tags( $matches[6][ $k ] );
			// Remove trailing URL tags. The regex isn't sufficient for this.
			$phrase = wp_strip_all_tags( $matches[0][ $k ] );
			$phrase = trim( preg_replace( '/(.*)(<t?a[^<>].*$)/', '', (string) $phrase ) );

			// Don't continue if the anchor or phrase are empty, e.g. blank link tag.
			if ( ! $anchor || ! $phrase ) {
				continue;
			}

			$phraseHtml = aioseoBrokenLinkChecker()->helpers->stripIncompleteHtmlTags( $matches[0][ $k ] );
			$phraseHtml = aioseoBrokenLinkChecker()->helpers->stripScriptTags( $phraseHtml );
			$phraseHtml = aioseoBrokenLinkChecker()->helpers->trimParagraphTags( $phraseHtml );

			if ( empty( $phraseHtml ) ) {
				continue;
			}

			$paragraph     = aioseoBrokenLinkChecker()->main->paragraph->get( $postId, $postContent, $phrase );
			$paragraphHtml = aioseoBrokenLinkChecker()->main->paragraph->getHtml( $anchor, $paragraph, $postContent );

			// Reformat the URL to get rid of params and fragments.
			$url = $this->geturlWithoutParamsAndFragment( $parsedUrl );

			// We need to sanitize the URL here so the hash is calculated based on the escaped version.
			$url = trim( sanitize_url( $url ) );

			$linkData = [
				'post_id'            => (int) $postId,
				'blc_link_status_id' => $this->getLinkStatusId( $url ),
				'url'                => $url,
				'url_hash'           => sha1( $url ),
				'hostname'           => $hostname,
				'hostname_url'       => sha1( $hostname ),
				'external'           => ! $isInternal,
				'anchor'             => $anchor,
				'phrase'             => $phrase,
				'phrase_html'        => $phraseHtml,
				'paragraph'          => $paragraph,
				'paragraph_html'     => $paragraphHtml
			];

			$links[] = $linkData;
		}

		return $links;
	}

	/**
	 * Return the link status ID.
	 *
	 * @since 1.0.0
	 *
	 * @param  string   $url The URL to look up.
	 * @return int|null      The link status ID.
	 */
	private function getLinkStatusId( $url ) {
		static $linkStatusId = [];

		$hash = sha1( $url );
		if ( isset( $linkStatusId[ $hash ] ) ) {
			return $linkStatusId[ $hash ];
		}

		$possibleLinkStatusId = aioseoBrokenLinkChecker()->core->db->start( 'aioseo_blc_link_status' )
			->where( 'url_hash', $hash )
			->run()
			->result();

		$linkStatusId[ $hash ] = ! empty( $possibleLinkStatusId ) ? $possibleLinkStatusId[0]->id : null;

		return $linkStatusId[ $hash ];
	}

	/**
	 * Returns the site's hostname.
	 *
	 * @since 1.0.0
	 *
	 * @return string The hostname.
	 */
	private function getHostname() {
		static $siteUrl = null;
		if ( null === $siteUrl ) {
			$siteUrl = wp_parse_url( get_site_url(), PHP_URL_HOST );
		}

		return $siteUrl;
	}

	/**
	 * Returns the parsed URL.
	 *
	 * @since 1.0.0
	 * @since 1.1.1 Renamed method.
	 *
	 * @param  string $url The URL.
	 * @return array       The parsed URL.
	 */
	private function parseUrl( $url ) {
		$parsedUrl = wp_parse_url( $url );
		if ( empty( $parsedUrl ) ) {
			return [];
		}

		// If the URL is relative, add the hostname of the site.
		if ( empty( $parsedUrl['host'] ) ) {
			$parsedUrl['host']   = $this->getHostname();
			$parsedUrl['scheme'] = wp_parse_url( get_site_url(), PHP_URL_SCHEME );
		}

		return $parsedUrl;
	}

	/**
	 * Returns the URL without params and fragments.
	 *
	 * @since 1.1.1
	 *
	 * @param  array  $parsedUrl The parsed URL.
	 * @return string            The URL without params and fragments.
	 */
	private function geturlWithoutParamsAndFragment( $parsedUrl ) {
		$url = '';
		if ( ! empty( $parsedUrl['scheme'] ) ) {
			$url .= $parsedUrl['scheme'] . '://';
		}

		$url .= $parsedUrl['host'];

		if ( ! empty( $parsedUrl['path'] ) ) {
			$url .= $parsedUrl['path'];
		}

		return $url;
	}

	/**
	 * Returns the posts to scan.
	 *
	 * @since 1.0.0
	 *
	 * @param  bool      $countOnly Whether to return only the count.
	 * @return array|int            The posts to scan or a count.
	 */
	public function getPostsToScan( $countOnly = false ) {
		$postsPerScan        = apply_filters( 'aioseo_blc_links_posts_per_scan', 50 );
		$postTypes           = aioseoBrokenLinkChecker()->helpers->getScannablePostTypes();
		$postStatuses        = aioseoBrokenLinkChecker()->helpers->getPublicPostStatuses( true );
		$minimumLinkScanDate = aioseoBrokenLinkChecker()->internalOptions->internal->minimumLinkScanDate ?: date( 'Y-m-d H:i:s' );

		$query = aioseoBrokenLinkChecker()->core->db->start( 'posts as p' )
			->leftJoin( 'aioseo_blc_posts as abp', 'p.ID = abp.post_id' )
			->whereIn( 'p.post_status', $postStatuses )
			->whereIn( 'p.post_type', $postTypes )
			->whereRaw( "(
				abp.post_id IS NULL OR
				abp.link_scan_date < p.post_modified_gmt OR
				abp.link_scan_date IS NULL OR
				abp.link_scan_date < '$minimumLinkScanDate'
			)" );

		if ( $countOnly ) {
			return $query->count();
		}

		$postsToScan = $query
			->select( 'DISTINCT p.ID, p.post_content, p.post_type, p.post_status' )
			->limit( $postsPerScan )
			->run()
			->result();

		return $postsToScan;
	}

	/**
	 * Returns the total number of scannable posts.
	 *
	 * @since 1.0.0
	 *
	 * @return int The total number of scannable posts.
	 */
	private function getTotalScannablePosts() {
		$postTypes    = aioseoBrokenLinkChecker()->helpers->getScannablePostTypes();
		$postStatuses = aioseoBrokenLinkChecker()->helpers->getPublicPostStatuses( true );

		$query = aioseoBrokenLinkChecker()->core->db->start( 'posts as p' )
			->whereIn( 'p.post_status', $postStatuses )
			->whereIn( 'p.post_type', $postTypes );

		return $query->count();
	}

	/**
	 * Returns the scan percentage.
	 *
	 * @since 1.0.0
	 *
	 * @return int The scan percentage.
	 */
	public function getScanPercentage() {
		$postsToScan         = $this->getPostsToScan( true );
		$totalScannablePosts = $this->getTotalScannablePosts();
		if ( 0 === $postsToScan || 0 === $totalScannablePosts ) {
			return 100;
		}

		return ceil( 100 - ( ( $postsToScan / $totalScannablePosts ) * 100 ) );
	}

	/**
	 * Sets the ignored extensions.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	private function setIgnoredExtensions() {
		$this->ignoredExtensions = apply_filters( 'aioseo_blc_ignored_extensions', [
			// Executable files
			'apk',
			'bat',
			'bin',
			'cgi',
			'com',
			'exe',
			'gadget',
			'jar',
			'py',
			'wsf',
		] );
	}
}Links/Links.php000064400000007723151536237000007430 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Links;

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Handles the Links scan.
 *
 * @since 1.0.0
 */
class Links {
	/**
	 * The action name of the scan.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private $scanActionName = 'aioseo_blc_links_scan';

	/**
	 * Data class instance.
	 *
	 * @since 1.1.0
	 *
	 * @var Data
	 */
	public $data = null;

	/**
	 * Holds the IDs of posts that need to be rescanned.
	 * We have to rescan these on shutdown instead of through the "save_post" hook since that hook is triggered right after a post is updated.
	 * That in turn can cause subsequent link updatss/deletions during REST API requests to fail because all links are deleted in the callback.
	 *
	 * @since 1.1.0
	 *
	 * @var array
	 */
	public $postsToRescan = [];

	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		$this->data = new Data();

		add_action( $this->scanActionName, [ $this, 'scanPosts' ], 11, 1 );
		add_action( 'save_post', [ $this, 'scanPost' ], 21, 1 );
		add_action( 'shutdown', [ $this, 'rescanPosts' ] );

		if ( ! is_admin() ) {
			return;
		}

		add_action( 'init', [ $this, 'scheduleScan' ], 3003 );
	}

	/**
	 * Schedules the initial links scan.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function scheduleScan() {
		// If there is no action at all, schedule one.
		if ( ! aioseoBrokenLinkChecker()->actionScheduler->isScheduled( $this->scanActionName ) ) {
			aioseoBrokenLinkChecker()->actionScheduler->scheduleAsync( $this->scanActionName );
		}
	}

	/**
	 * Scans posts for links and stores them in the DB.
	 *
	 * @since 1.0.0
	 *
	 * @param  bool $scheduleNewAction Whether to schedule a new action.
	 * @return void
	 */
	public function scanPosts( $scheduleNewAction = true ) {
		static $iterations = 0;
		$iterations++;

		aioseoBrokenLinkChecker()->helpers->timeElapsed();

		$postsToScan = $this->data->getPostsToScan();

		if ( empty( $postsToScan ) ) {
			if ( $scheduleNewAction ) {
				aioseoBrokenLinkChecker()->actionScheduler->scheduleSingle( $this->scanActionName, 15 * MINUTE_IN_SECONDS );
			}

			return;
		}

		foreach ( $postsToScan as $postToScan ) {
			$this->scanPost( $postToScan );
		}

		$timeElapsed = aioseoBrokenLinkChecker()->helpers->timeElapsed();
		if ( 10 > $timeElapsed && 200 > $iterations ) {
			// If we still have time, do another scan.
			$this->scanPosts();

			return;
		}

		if ( $scheduleNewAction ) {
			aioseoBrokenLinkChecker()->actionScheduler->scheduleSingle( $this->scanActionName, MINUTE_IN_SECONDS );
		}
	}

	/**
	 * Scans the given individual post for links.
	 *
	 * @since 1.0.0
	 *
	 * @param  Object|int $post The post object or ID (if called on "save_post").
	 * @return void
	 */
	public function scanPost( $post ) {
		if ( doing_action( 'save_post' ) && ! empty( $this->postsToRescan ) ) {
			// If posts need to be reindexed manually, bail.
			return;
		}

		if ( ! is_object( $post ) ) {
			$post = get_post( $post );
		}

		// Check if we didn't scan this post in the last 3 seconds. This is to prevent a second, subsequent request from scanning the same post.
		if ( aioseoBrokenLinkChecker()->core->cache->get( 'aioseo_blc_scan_post_' . $post->ID ) ) {
			return;
		}

		if ( ! aioseoBrokenLinkChecker()->helpers->isScannablePost( $post ) ) {
			return;
		}

		$this->data->indexLinks( $post->ID );

		$aioseoPost                 = Models\Post::getPost( $post->ID );
		$aioseoPost->link_scan_date = gmdate( 'Y-m-d H:i:s' );
		$aioseoPost->save();

		// Set a transient to prevent scanning the same post again in the next 3 seconds.
		aioseoBrokenLinkChecker()->core->cache->update( 'aioseo_blc_scan_post_' . $post->ID, true, 3 );
	}

	/**
	 * Reindexes posts on shutdown.
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	public function rescanPosts() {
		if ( empty( $this->postsToRescan ) ) {
			return;
		}

		foreach ( $this->postsToRescan as $postId ) {
			$this->scanPost( $postId );
		}
	}
}Main/Activate.php000064400000003362151536237000007707 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Main;

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

/**
 * Handles plugin (de)activation.
 *
 * @since 1.0.0
 */
class Activate {
	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		register_activation_hook( AIOSEO_BROKEN_LINK_CHECKER_FILE, [ $this, 'activate' ] );
		register_deactivation_hook( AIOSEO_BROKEN_LINK_CHECKER_FILE, [ $this, 'deactivate' ] );
	}

	/**
	 * Runs on activation.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function activate() {
		aioseoBrokenLinkChecker()->access->addCapabilities();

		// Set the activation timestamps.
		$time = time();
		aioseoBrokenLinkChecker()->internalOptions->internal->activated = $time;

		if ( ! aioseoBrokenLinkChecker()->internalOptions->internal->firstActivated ) {
			$this->showSetupWizard();

			aioseoBrokenLinkChecker()->internalOptions->internal->firstActivated = $time;
		}

		aioseoBrokenLinkChecker()->core->cache->clear();
	}

	/**
	 * Show the setup wizard if this is the first time the user activates the plugin.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function showSetupWizard() {
		if ( aioseoBrokenLinkChecker()->internalOptions->internal->firstActivated ) {
			return;
		}

		if ( is_network_admin() ) {
			return;
		}

		if ( isset( $_GET['activate-multi'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended
			return;
		}

		// Sets 30 second transient for welcome screen redirect on activation.
		aioseoBrokenLinkChecker()->core->cache->update( 'activation_redirect', true, 30 );
	}

	/**
	 * Runs on deactivation.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function deactivate() {
		aioseoBrokenLinkChecker()->access->removeCapabilities();
	}
}Main/Main.php000064400000012145151536237000007032 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Main;

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

use AIOSEO\BrokenLinkChecker\Highlighter;
use AIOSEO\BrokenLinkChecker\Links;
use AIOSEO\BrokenLinkChecker\LinkStatus;
use AIOSEO\BrokenLinkChecker\Models;

/**
 * Main class where core features are handled/registered.
 *
 * @since 1.0.0
 */
class Main {
	/**
	 * Paragraph class.
	 *
	 * @since 1.0.0
	 *
	 * @var Paragraph
	 */
	public $paragraph = null;

	/**
	 * Links class.
	 *
	 * @since 1.0.0
	 *
	 * @var Links\Links
	 */
	public $links = null;

	/**
	 * LinkStatus class.
	 *
	 * @since 1.0.0
	 *
	 * @var LinkStatus\LinkStatus
	 */
	public $linkStatus = null;

	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		if (
			! aioseoBrokenLinkChecker()->core->db->tableExists( 'aioseo_blc_links' ) ||
			! aioseoBrokenLinkChecker()->core->db->tableExists( 'aioseo_blc_link_status' ) ||
			! aioseoBrokenLinkChecker()->core->db->tableExists( 'aioseo_blc_cache' )
		) {
			aioseoBrokenLinkChecker()->updates->addInitialTables();

			// Don't return here; otherwise the Setup Wizard won't show on the first activation, but on the second.
		}

		new Activate();

		$this->paragraph  = new Paragraph();
		$this->links      = new Links\Links();
		$this->linkStatus = new LinkStatus\LinkStatus();

		add_filter( 'the_content', [ $this, 'filterLinks' ], 999 ); // High prio to make sure other plugins get a chance to render their content, parse their blocks, etc..
	}

	/**
	 * Filters links in the post content.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $postContent The post content.
	 * @return string              The post content.
	 */
	public function filterLinks( $postContent ) {
		if ( aioseoBrokenLinkChecker()->options->general->linkTweaks->nofollowBroken ) {
			$postContent = $this->nofollowBrokenLinks( $postContent );
		}

		return $postContent;
	}

	/**
	 * Adds rel="nofollow" to links in the post content that we know are broken.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $postContent The post content.
	 * @return string              The post content.
	 */
	private function nofollowBrokenLinks( $postContent ) {
		// First, capture all link tags.
		preg_match_all( '/<a.*href="(.*?").*>(.*?)<\/a>/i', (string) $postContent, $linkTags );

		if ( empty( $linkTags[0] ) ) {
			return $postContent;
		}

		foreach ( $linkTags[0] as $linkTag ) {
			preg_match( '/href="(.*?)"/i', (string) $linkTag, $url );
			if ( empty( $url[1] ) ) {
				continue;
			}

			// Now check if we've indexed the link. If so, check if it's broken and act accordingly.
			$linkStatus = Models\LinkStatus::getByUrl( $url[1] );
			if ( ! $linkStatus->exists() || ! $linkStatus->broken ) {
				continue;
			}

			preg_match( '/rel="(.*?)"/i', (string) $linkTag, $relAttributes );
			if ( ! empty( $relAttributes[0] ) ) {
				$relAttributes = explode( ' ', $relAttributes[1] );
				if ( ! in_array( 'nofollow', $relAttributes, true ) ) {
					$relAttributes[] = 'nofollow';
				}
				$relAttributes = implode( ' ', $relAttributes );
			} else {
				$relAttributes = 'nofollow';
			}

			$newLinkTag = $this->insertAttribute( $linkTag, 'rel', $relAttributes );

			$oldLinkTag = aioseoBrokenLinkChecker()->helpers->escapeRegex( $linkTag );
			$newLinkTag = aioseoBrokenLinkChecker()->helpers->escapeRegexReplacement( $newLinkTag );

			$postContent = preg_replace( "/{$oldLinkTag}/i", $newLinkTag, (string) $postContent );
		}

		return $postContent;
	}

	/**
	 * Inserts a given value for a given image attribute.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $linkTag       The HTML tag.
	 * @param  string $attributeName The attribute name.
	 * @param  string $value         The attribute value.
	 * @return array                 The modified attributes.
	 */
	private function insertAttribute( $linkTag, $attributeName, $value ) {
		if ( empty( $value ) ) {
			return $linkTag;
		}

		$value   = esc_attr( $value );
		$linkTag = preg_replace( $this->attributeRegex( $attributeName, true ), '${1}' . $value . '${2}', (string) $linkTag, 1, $count );

		// Attribute does not exist. Let's append it at the beginning of the tag.
		if ( ! $count ) {
			$linkTag = preg_replace( '/<a /', '<a ' . $this->attributeToHtml( $attributeName, $value ) . ' ', (string) $linkTag );
		}

		return $linkTag;
	}

	/**
	 * Returns a regex string to match an attribute.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $attributeName      The attribute name.
	 * @param  bool   $groupReplaceValue  Regex groupings without the value.
	 * @return string                     The regex string.
	 */
	private function attributeRegex( $attributeName, $groupReplaceValue = false ) {
		$regex = $groupReplaceValue ? "/(\s%s=['\"]).*?(['\"])/" : "/\s%s=['\"](.*?)['\"]/";

		return sprintf( $regex, trim( $attributeName ) );
	}

	/**
	 * Returns an attribute as HTML.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $attributeName The attribute name.
	 * @param  string $value         The attribute value.
	 * @return string                The HTML formatted attribute.
	 */
	private function attributeToHtml( $attributeName, $value ) {
		return sprintf( '%s="%s"', $attributeName, esc_attr( $value ) );
	}
}Main/Paragraph.php000064400000016130151536237000010051 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Main;

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

/**
 * Handles the extraction of the context paragraph from the post content.
 *
 * @since 1.0.0
 */
class Paragraph {
	/**
	 * Returns the context paragraph for the given phrase.
	 *
	 * @since 1.0.0
	 *
	 * @param  int    $postId      The post ID.
	 * @param  string $postContent The post content.
	 * @param  string $phrase      The phrase.
	 * @return string              The context paragraph.
	 */
	public function get( $postId, $postContent, $phrase ) {
		static $cachedPhrases = [];
		if ( ! isset( $cachedPhrases[ $postId ] ) ) {
			$postContent              = wp_strip_all_tags( $postContent );
			$cachedPhrases[ $postId ] = array_values( preg_split( '#([\.?!][\r\n\s]+|\r|\n|\s{2,})#u', (string) $postContent, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ) );
		}
		$phrases = $cachedPhrases[ $postId ];

		// Locate phrase in list of phrases and use preceding/consecutive phrase for context.
		$paragraph = $phrase;
		for ( $i = 0; $i < count( $phrases ); $i++ ) {
			$escapedPhrase = aioseoBrokenLinkChecker()->helpers->escapeRegex( $phrase );
			if (
				! preg_match( "/{$escapedPhrase}/i", $phrases[ $i ] ) &&
				// Do another check and include the delimiter.
				( ! isset( $phrases[ $i + 1 ] ) || 1 < str_word_count( $phrases[ $i + 1 ] ) || ! preg_match( "/{$escapedPhrase}/i", $phrases[ $i ] . $phrases[ $i + 1 ] ) )
			) {
				continue;
			}

			// Now we'll use preceding/consecutive phrases, relative to the phrase, to get the context.
			// The odd indexes are the delimiters (punctuation).
			// We need to validate each phrase part to prevent us from including line breaks.
			// When constructing the paragraph, we cannot use the phrase we passed in because it might have punctuation at the end.

			// If phrase is the first phrase of the content, add two consecutive phrases.
			if ( 0 === $i ) {
				if (
					isset( $phrases[1] ) && $this->isValidPhrase( $phrases[1] ) &&
					isset( $phrases[2] ) && $this->isValidPhrase( $phrases[2] )
				) {
					$paragraph = $phrases[ $i ] . $phrases[1] . $phrases[2];
					if ( isset( $phrases[3] ) ) {
						$paragraph .= $phrases[3];
					}

					if (
						isset( $phrases[4] ) && $this->isValidPhrase( $phrases[4] ) &&
						isset( $phrases[5] ) && $this->isValidPhrase( $phrases[5] )
					) {
						$paragraph .= $phrases[4] . $phrases[5];
					} elseif ( isset( $phrases[4] ) ) {
						// If we find a line break, we still want to add the delimiter.
						$paragraph .= $phrases[4];
					}
				} elseif ( isset( $phrases[1] ) ) {
					// If we find a line break, we still want to add the delimiter.
					$paragraph = $phrases[ $i ] . $phrases[1];
				}
				break;
			}

			// If phrase is the last phrase of the content, add two preceding phrases.
			if ( ( count( $phrases ) - 1 ) === $i ) {
				if (
					isset( $phrases[ $i - 1 ] ) && $this->isValidPhrase( $phrases[ $i - 1 ] ) &&
					isset( $phrases[ $i - 2 ] ) && $this->isValidPhrase( $phrases[ $i - 2 ] )
				) {
					$paragraph = $phrases[ $i - 2 ] . $phrases[ $i - 1 ] . $phrases[ $i ];

					if (
						isset( $phrases[ $i - 3 ] ) && $this->isValidPhrase( $phrases[ $i - 3 ] ) &&
						isset( $phrases[ $i - 4 ] ) && $this->isValidPhrase( $phrases[ $i - 4 ] )
					) {
						$paragraph = $phrases[ $i - 4 ] . $phrases[ $i - 3 ] . $paragraph;
					}
				}
				break;
			}

			$addedPrecedingSentence = false;
			if (
				isset( $phrases[ $i - 1 ] ) && $this->isValidPhrase( $phrases[ $i - 1 ] ) &&
				isset( $phrases[ $i - 2 ] ) && $this->isValidPhrase( $phrases[ $i - 2 ] )
				) {
				$addedPrecedingSentence = true;
				$paragraph = $phrases[ $i - 2 ] . $phrases[ $i - 1 ] . $phrases[ $i ];
			}

			if (
				isset( $phrases[ $i + 1 ] ) && $this->isValidPhrase( $phrases[ $i + 1 ] ) &&
				isset( $phrases[ $i + 2 ] ) && $this->isValidPhrase( $phrases[ $i + 2 ] )
			) {
				$paragraph = $addedPrecedingSentence ? $paragraph : $phrases[ $i ];
				$paragraph = $paragraph . $phrases[ $i + 1 ] . $phrases[ $i + 2 ];
				if ( isset( $phrases[ $i + 3 ] ) ) {
					$paragraph .= $phrases[ $i + 3 ];
				}
			} elseif ( isset( $phrases[ $i + 1 ] ) ) {
				// If we find a line break, we still want to add the delimiter.
				if ( ! $addedPrecedingSentence ) {
					$paragraph = $phrases[ $i ];
				}
				$paragraph .= $phrases[ $i + 1 ];
			}
			break;
		}

		return trim( $paragraph );
	}

	/**
	 * Returns the paragraph with its inner HTML contents and preceding/trailing tags.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $anchor       The anchor.
	 * @param  string $paragraph    The paragraph.
	 * @param  string $postContent  The post content.
	 * @param  bool   $isSuggestion Whether we're getting the HTML paragraph for a suggestion.
	 * @return string               The paragraph with its inner HTML contents.
	 */
	public function getHtml( $anchor, $paragraph, $postContent, $isSuggestion = false ) {
		$words = preg_split( '/\s|\p{P}/', (string) $paragraph, -1, PREG_SPLIT_NO_EMPTY );
		if ( ! isset( $words[0] ) ) {
			return $paragraph;
		}

		$firstWord = aioseoBrokenLinkChecker()->helpers->escapeRegex( $words[0] );
		$lastWord  = aioseoBrokenLinkChecker()->helpers->escapeRegex( $words[ count( $words ) - 1 ] );

		// We must check if the first/last word isn't part of the anchor. Otherwise we'll mess up the pattern below by including the word twice.
		$firstWord = ! preg_match( "/^{$firstWord}/i", (string) $anchor ) ? $firstWord : '';
		$lastWord  = ! preg_match( "/{$lastWord}$/i", (string) $anchor ) ? $lastWord : '';
		$anchor    = aioseoBrokenLinkChecker()->helpers->escapeRegex( $anchor );
		$pattern   = $isSuggestion
			? "/{$firstWord}.*{$anchor}.*{$lastWord}/i"
			: "/{$firstWord}.*<a[^<>]*>.*{$anchor}.*<\/a>.*{$lastWord}/i";

		preg_match( $pattern, (string) $postContent, $match );
		if ( ! isset( $match[0] ) ) {
			return $paragraph;
		}

		$paragraphWithInnerHtml        = $match[0];
		$escapedParagraphWithInnerHtml = aioseoBrokenLinkChecker()->helpers->escapeRegex( $paragraphWithInnerHtml );

		$precedingTags = '';
		preg_match( "/(<[a-z]* .*>|<[a-z]*>)+$escapedParagraphWithInnerHtml/i", (string) $postContent, $match );
		if ( ! empty( $match[0] ) ) {
			$precedingTags = preg_replace( "/$escapedParagraphWithInnerHtml/", '', $match[0] );
		}

		$trailingTags = '';
		preg_match( "/{$escapedParagraphWithInnerHtml}[.?!]?(<\/[a-z]*>)?/i", $postContent, $match );
		if ( ! empty( $match[0] ) ) {
			$trailingTags = preg_replace( "/$escapedParagraphWithInnerHtml/", '', $match[0] );
		}

		$paragraphHtml = $precedingTags . $paragraphWithInnerHtml . $trailingTags;

		$paragraphHtml = aioseoBrokenLinkChecker()->helpers->stripScriptTags( $paragraphHtml );
		$paragraphHtml = aioseoBrokenLinkChecker()->helpers->trimParagraphTags( $paragraphHtml );

		return $paragraphHtml;
	}

	/**
	 * Checks whether the phrase is valid. It cannot contain line breaks.
	 * We do this so that we can prevent phrases being added to the context paragraph that aren't part of the phrase's text block.
	 *
	 * @since 1.0.0
	 *
	 * @return bool Whether the phrase is valid.
	 */
	private function isValidPhrase( $phrase ) {
		return preg_match( '/(\r\n|\r|\n)/', $phrase );
	}
}Main/PreUpdates.php000064400000003467151536237000010231 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Main;

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

/**
 * This class contains pre-updates necessary for the main Updates class to run.
 *
 * @since 1.0.0
 */
class PreUpdates {
	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		$lastActiveVersion = aioseoBrokenLinkChecker()->internalOptions->internal->lastActiveVersion;
		if ( aioseoBrokenLinkChecker()->version !== $lastActiveVersion ) {
			// Bust the table/columns cache so that we can start the update migrations with a fresh slate.
			aioseoBrokenLinkChecker()->internalOptions->database->installedTables = '';
		}

		if ( version_compare( $lastActiveVersion, '1.0.0', '<' ) ) {
			$this->createCacheTable();
		}
	}

	/**
	 * Creates the cache table.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function createCacheTable() {
		$db             = aioseoBrokenLinkChecker()->core->db->db;
		$charsetCollate = '';

		if ( ! empty( $db->charset ) ) {
			$charsetCollate .= "DEFAULT CHARACTER SET {$db->charset}";
		}
		if ( ! empty( $db->collate ) ) {
			$charsetCollate .= " COLLATE {$db->collate}";
		}

		if ( ! aioseoBrokenLinkChecker()->core->db->tableExists( 'aioseo_blc_cache' ) ) {
			$tableName = $db->prefix . 'aioseo_blc_cache';

			aioseoBrokenLinkChecker()->core->db->execute(
				"CREATE TABLE {$tableName} (
					`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					`key` varchar(80) NOT NULL,
					`value` longtext NOT NULL,
					`expiration` datetime NULL,
					`created` datetime NOT NULL,
					`updated` datetime NOT NULL,
					PRIMARY KEY (`id`),
					UNIQUE KEY ndx_aioseo_blc_cache_key (`key`),
					KEY ndx_aioseo_blc_cache_expiration (`expiration`)
				) {$charsetCollate};"
			);
		}
	}
}Main/Updates.php000064400000015674151536237000007565 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Main;

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Handles update migrations.
 *
 * @since 1.0.0
 */
class Updates {
	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		add_action( 'init', [ $this, 'runUpdates' ], 1002 );
		add_action( 'init', [ $this, 'updateLatestVersion' ], 3000 );
	}

	/**
	 * Runs our migrations.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function runUpdates() {
		$lastActiveVersion = aioseoBrokenLinkChecker()->internalOptions->internal->lastActiveVersion;
		if ( version_compare( $lastActiveVersion, '1.0.0', '<' ) ) {
			$this->addInitialTables();

			aioseoBrokenLinkChecker()->internalOptions->internal->minimumLinkScanDate = date( 'Y-m-d H:i:s', time() );
		}

		if ( version_compare( $lastActiveVersion, '1.2.0', '<' ) ) {
			$this->dropInvalidMediaLinks();
			$this->dropLinksWithInvalidHash();
		}
	}

	/**
	 * Updates the latest version after all migrations and updates have run.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function updateLatestVersion() {
		if ( aioseoBrokenLinkChecker()->internalOptions->internal->lastActiveVersion === aioseoBrokenLinkChecker()->version ) {
			return;
		}

		aioseoBrokenLinkChecker()->internalOptions->internal->lastActiveVersion = aioseoBrokenLinkChecker()->version;

		aioseoBrokenLinkChecker()->core->db->bustCache();
		aioseoBrokenLinkChecker()->internalOptions->database->installedTables = '';
	}

	/**
	 * Adds our custom tables.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function addInitialTables() {
		$db             = aioseoBrokenLinkChecker()->core->db->db;
		$charsetCollate = '';

		if ( ! empty( $db->charset ) ) {
			$charsetCollate .= "DEFAULT CHARACTER SET {$db->charset}";
		}
		if ( ! empty( $db->collate ) ) {
			$charsetCollate .= " COLLATE {$db->collate}";
		}

		if ( ! aioseoBrokenLinkChecker()->core->db->tableExists( 'aioseo_blc_link_status' ) ) {
			$tableName = $db->prefix . 'aioseo_blc_link_status';

			aioseoBrokenLinkChecker()->core->db->execute(
				"CREATE TABLE {$tableName} (
					`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					`url` text NOT NULL,
					`url_hash` varchar(40) NOT NULL,
					`http_status_code` smallint(6) DEFAULT NULL,
					`broken` tinyint(1) unsigned DEFAULT 0 NOT NULL,
					`dismissed` tinyint(1) DEFAULT 0 NOT NULL,
					`request_duration` float DEFAULT NULL,
					`scan_count` int(4) unsigned DEFAULT 0 NOT NULL,
					`redirect_count` smallint(5) unsigned DEFAULT 0 NOT NULL,
					`final_url` text DEFAULT NULL,
					`first_failure` datetime DEFAULT NULL,
					`log` text DEFAULT NULL,
					`last_scan_date` datetime DEFAULT NULL,
					`created` datetime NOT NULL,
					`updated` datetime NOT NULL,
					PRIMARY KEY (id),
					UNIQUE KEY ndx_aioseo_blc_link_status_url_hash (url_hash)
				) {$charsetCollate};"
			);
		}

		if ( ! aioseoBrokenLinkChecker()->core->db->tableExists( 'aioseo_blc_links' ) ) {
			$tableName = $db->prefix . 'aioseo_blc_links';

			aioseoBrokenLinkChecker()->core->db->execute(
				"CREATE TABLE {$tableName} (
					`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					`post_id` bigint(20) unsigned NOT NULL,
					`blc_link_status_id` bigint(20) unsigned DEFAULT NULL,
					`url` text NOT NULL,
					`url_hash` varchar(40) NOT NULL,
					`hostname` text NOT NULL,
					`hostname_url` varchar(40) NOT NULL,
					`external` tinyint(1) DEFAULT 0 NOT NULL,
					`anchor` text NOT NULL,
					`phrase` text NOT NULL,
					`phrase_html` text NOT NULL,
					`paragraph` text NOT NULL,
					`paragraph_html` text NOT NULL,
					`created` datetime NOT NULL,
					`updated` datetime NOT NULL,
					PRIMARY KEY (id),
					KEY ndx_aioseo_blc_links_post_id (post_id),
					KEY ndx_aioseo_blc_links_hostname (hostname(10))
				) {$charsetCollate};"
			);
		}

		if ( ! aioseoBrokenLinkChecker()->core->db->tableExists( 'aioseo_blc_notifications' ) ) {
			$tableName = $db->prefix . 'aioseo_blc_notifications';

			aioseoBrokenLinkChecker()->core->db->execute(
				"CREATE TABLE {$tableName} (
					`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					`notification_id` bigint(20) unsigned DEFAULT NULL,
					`notification_name` varchar(255) DEFAULT NULL,
					`slug` varchar(13) NOT NULL,
					`title` text NOT NULL,
					`content` longtext NOT NULL,
					`type` varchar(64) NOT NULL,
					`level` text NOT NULL,
					`start` datetime DEFAULT NULL,
					`end` datetime DEFAULT NULL,
					`button1_label` varchar(255) DEFAULT NULL,
					`button1_action` varchar(255) DEFAULT NULL,
					`button2_label` varchar(255) DEFAULT NULL,
					`button2_action` varchar(255) DEFAULT NULL,
					`dismissed` tinyint(1) NOT NULL DEFAULT 0,
					`new` tinyint(1) NOT NULL DEFAULT 1,
					`created` datetime NOT NULL,
					`updated` datetime NOT NULL,
					PRIMARY KEY (id),
					UNIQUE KEY ndx__aioseo_blc_notifications_slug (slug),
					KEY ndx__aioseo_blc_notifications_dates (start, end),
					KEY ndx__aioseo_blc_notifications_type (type),
					KEY ndx__aioseo_blc_notifications_dismissed (dismissed)
				) {$charsetCollate};"
			);
		}

		if ( ! aioseoBrokenLinkChecker()->core->db->tableExists( 'aioseo_blc_posts' ) ) {
			$tableName = $db->prefix . 'aioseo_blc_posts';

			aioseoBrokenLinkChecker()->core->db->execute(
				"CREATE TABLE {$tableName} (
					id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					`post_id` bigint(20) unsigned NOT NULL,
					link_scan_date datetime DEFAULT NULL,
					created datetime NOT NULL,
					updated datetime NOT NULL,
					PRIMARY KEY (id),
					KEY ndx_aioseo_blc_posts_post_id (post_id)
				) {$charsetCollate};"
			);
		}
	}

	/**
	 * Removes all mailto and tel links from the database.
	 *
	 * @since 1.0.5
	 *
	 * @return void
	 */
	private function dropInvalidMediaLinks() {
		$tableName = aioseoBrokenLinkChecker()->core->db->prefix . 'aioseo_blc_links';

		aioseoBrokenLinkChecker()->core->db->execute(
			"DELETE FROM {$tableName} WHERE url LIKE 'mailto:%' OR url LIKE 'tel:%'"
		);

		$tableName = aioseoBrokenLinkChecker()->core->db->prefix . 'aioseo_blc_link_status';

		aioseoBrokenLinkChecker()->core->db->execute(
			"DELETE FROM {$tableName} WHERE url LIKE 'mailto:%' OR url LIKE 'tel:%'"
		);
	}

	/**
	 * Removes all links with percentage signs from the database as these had invalid hashes.
	 *
	 * @since 1.2.0
	 *
	 * @return void
	 */
	private function dropLinksWithInvalidHash() {
		$blcPosts = aioseoBrokenLinkChecker()->core->db->prefix . 'aioseo_blc_posts';
		$blcLinks = aioseoBrokenLinkChecker()->core->db->prefix . 'aioseo_blc_links';

		aioseoBrokenLinkChecker()->core->db->execute(
			"UPDATE {$blcPosts} SET link_scan_date = NULL
			WHERE post_id IN (
				SELECT post_id FROM {$blcLinks} WHERE url LIKE '%\\%%'
			)"
		);

		$blcLinkStatus = aioseoBrokenLinkChecker()->core->db->prefix . 'aioseo_blc_link_status';

		aioseoBrokenLinkChecker()->core->db->execute(
			"DELETE FROM {$blcLinkStatus} WHERE url LIKE '%\\%%'"
		);
	}
}Models/Link.php000064400000020033151536237000007375 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Models;

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

use AIOSEO\BrokenLinkChecker\Core\Database;

/**
 * The Link DB model class.
 *
 * @since 1.0.0
 */
class Link extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	protected $table = 'aioseo_blc_links';

	/**
	 * Fields that should be numeric values.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $integerFields = [ 'id', 'post_id', 'blc_link_status_id' ];

	/**
	 * Fields that are nullable.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $nullFields = [ 'blc_link_status_id' ];

	/**
	 * Fields that are booleans.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $booleanFields = [ 'external' ];

	/**
	 * Appended as an extra column, but not stored in the DB.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $appends = [ 'context' ];

	/**
	 * Returns the Link with the given ID.
	 *
	 * @since 1.0.0
	 *
	 * @param  int  $linkId The Link ID.
	 * @return Link         The Link.
	 */
	public static function getById( $linkId ) {
		return aioseoBrokenLinkChecker()->core->db->start( 'aioseo_blc_links' )
			->where( 'id', $linkId )
			->run()
			->model( 'AIOSEO\\BrokenLinkChecker\\Models\\Link' );
	}

	/**
	 * Returns the Links with the given Link Status ID.
	 *
	 * @since 1.1.0
	 *
	 * @param  int   $linkStatusId The Link Status ID.
	 * @return array               The Links.
	 */
	public static function getByLinkStatusId( $linkStatusId ) {
		return aioseoBrokenLinkChecker()->core->db->start( 'aioseo_blc_links' )
			->where( 'blc_link_status_id', $linkStatusId )
			->run()
			->models( 'AIOSEO\\BrokenLinkChecker\\Models\\Link' );
	}

	/**
	 * Deletes all Links for the given post.
	 *
	 * @since 1.0.0
	 *
	 * @param  int  $postId The post ID.
	 * @return void
	 */
	public static function deleteLinks( $postId ) {
		aioseoBrokenLinkChecker()->core->db->delete( 'aioseo_blc_links' )
			->where( 'post_id', $postId )
			->run();
	}

	/**
	 * Sanitizes the link object.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $link The link data.
	 * @return array       The sanitized link data.
	 */
	public static function sanitizeLink( $link ) {
		$instance = new self();

		$sanitizedLink = [];
		foreach ( $link as $k => $v ) {
			switch ( $k ) {
				case 'post_id':
				case 'blc_link_status_id':
					if ( null === $v && in_array( $k, $instance->nullFields, true ) ) {
						break;
					}
					$v = intval( $v );
					break;
				case 'external':
					$v = rest_sanitize_boolean( $v );
					break;
				case 'url':
					$v = sanitize_url( $v );
					break;
				case 'url_hash':
				case 'hostname':
				case 'hostname_hash':
				case 'anchor':
				case 'phrase':
				case 'paragraph':
					$v = sanitize_text_field( $v );
					break;
				case 'phrase_html':
				case 'paragraph_html':
					$v = aioseoBrokenLinkChecker()->helpers->wpKsesPhrase( $v );
					break;
				default:
					break;
			}

			if (
				empty( $v ) &&
				! in_array( $k, $instance->booleanFields, true ) &&
				! in_array( $k, $instance->nullFields, true )
			) {
				return [];
			}

			$sanitizedLink[ $k ] = esc_sql( $v );
		}

		return $sanitizedLink;
	}

	/**
	 * Checks whether the given link object is a valid one in the context of Broken Link Checker.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $link The link data.
	 * @return bool        Whether the link is valid or not.
	 */
	public static function validateLink( $link ) {
		$propsToCheck = [
			'url',
			'hostname',
			'anchor',
			'phrase',
			'phrase_html',
			'paragraph',
			'paragraph_html'
		];

		foreach ( $propsToCheck as $prop ) {
			$value = wp_strip_all_tags( $link[ $prop ] );
			if ( empty( $value ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Returns link row results based on the given arguments.
	 *
	 * @since 1.1.0
	 *
	 * @param  int    $limit       The limit.
	 * @param  int    $offset      The offset.
	 * @param  string $whereClause The WHERE clause.
	 * @return array               List of Link instances.
	 */
	public static function rowQuery( $linkStatusId, $limit = 5, $offset = 0, $whereClause = '' ) {
		$linkRows = self::baseQuery( $linkStatusId, $whereClause )
			->select( 'al.id, al.post_id, p.post_type, al.external, al.anchor, al.phrase' )
			->limit( $limit, $offset )
			->run()
			->result();

		if ( empty( $linkRows ) ) {
			return [];
		}

		$rowsWithData = [];
		foreach ( $linkRows as $linkRow ) {
			$linkRow->context = [
				'permalink' => get_permalink( $linkRow->post_id ),
				'postTitle' => aioseoBrokenLinkChecker()->helpers->getPostTitle( $linkRow->post_id ),
				'editLink'  => get_edit_post_link( $linkRow->post_id, '' ),
				'postType'  => $linkRow->post_type
			];

			$rowsWithData[] = $linkRow;
		}

		return $rowsWithData;
	}

	/**
	 * Returns link row count based on the given arguments.
	 *
	 * @since 1.0.0
	 *
	 * @param  int    $linkStatusId The link status ID.
	 * @param  string $whereClause  The WHERE clause.
	 * @return int                  The row count.
	 */
	public static function rowQueryCount( $linkStatusId, $whereClause = '' ) {
		$query = self::baseQuery( $linkStatusId, $whereClause )
			->count();

		return $query;
	}

	/**
	 * Returns the base query for the rowQuery() and rowCountQuery() methods.
	 *
	 * @since 1.0.0
	 *
	 * @param  int      $linkStatusId The link status ID.
	 * @param  string   $whereClause  The WHERE clause.
	 * @return Database               The query.
	 */
	public static function baseQuery( $linkStatusId, $whereClause = '' ) {
		$includedPostTypes    = aioseoBrokenLinkChecker()->helpers->getIncludedPostTypes();
		$includedPostStatuses = aioseoBrokenLinkChecker()->helpers->getIncludedPostStatuses();
		$excludedPostIds      = aioseoBrokenLinkChecker()->helpers->getExcludedPostIds();
		$excludedDomains      = aioseoBrokenLinkChecker()->helpers->getExcludedDomains();

		$query = aioseoBrokenLinkChecker()->core->db->start( 'aioseo_blc_links as al' )
			->join( 'posts as p', 'p.ID = al.post_id', 'RIGHT' )
			->where( 'al.blc_link_status_id', $linkStatusId );

		if ( ! empty( $whereClause ) ) {
			$query->whereRaw( $whereClause );
		}

		if ( ! empty( $includedPostStatuses ) ) {
			$query->whereIn( 'p.post_status', $includedPostStatuses );
		}

		if ( ! empty( $includedPostTypes ) ) {
			$query->whereIn( 'p.post_type', $includedPostTypes );
		}

		if ( ! empty( $excludedPostIds ) ) {
			$query->whereNotIn( 'p.ID', $excludedPostIds );
		}

		if ( ! empty( $excludedDomains ) ) {
			$query->whereNotIn( 'al.hostname', $excludedDomains );
		}

		$excludedDomains = aioseoBrokenLinkChecker()->helpers->getExcludedDomains();
		if ( ! empty( $excludedDomains ) ) {
			$query->whereNotIn( 'al.hostname', $excludedDomains );
		}

		return $query;
	}

	/**
	 * Get a WHERE clause for the Broken Links report search term.
	 *
	 * @since   1.0.0
	 * @version 1.1.0 Moved from Vue.php to Link model.
	 *
	 * @param  string $searchTerm The search term.
	 * @return string             The search where clause.
	 */
	public static function getLinkWhereClause( $searchTerm ) {
		if ( ! $searchTerm || 'null' === $searchTerm ) {
			return '';
		}

		$searchTerm = esc_sql( $searchTerm );
		if ( ! $searchTerm ) {
			return '';
		}

		$where = '';
		if ( intval( $searchTerm ) ) {
			$where .= '
				p.ID = ' . (int) $searchTerm . ' OR
			';
		}

		$where .= "
			al.url LIKE '%" . $searchTerm . "%' OR
			al.anchor LIKE '%" . $searchTerm . "%' OR
			p.post_title LIKE '%" . $searchTerm . "%' OR
			p.post_name LIKE '%" . $searchTerm . "%'
		";

		return "( $where )";
	}

	/**
	 * Extract the keys from the result and add them to the model.
	 *
	 * @since 1.2.1
	 *
	 * @param  array $keys The list of keys and values to add to the model.
	 * @return void
	 */
	protected function applyKeys( $keys ) {
		try {
			parent::applyKeys( $keys );

			foreach ( (array) $keys as $key => $value ) {
				if ( ! property_exists( $this, $key ) ) {
					continue;
				}

				if ( 'url' === $key && is_string( $this->$key ) ) {
					$this->$key = rawurldecode( $this->$key );
				}
			}
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}
}Models/LinkStatus.php000064400000017775151536237000010624 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Models;

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

use AIOSEO\BrokenLinkChecker\Core\Database;

/**
 * The LinkStatus DB model class.
 *
 * @since 1.0.0
 */
class LinkStatus extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	protected $table = 'aioseo_blc_link_status';

	/**
	 * Fields that should be numeric values.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $integerFields = [ 'id', 'broken', 'dismissed', 'scan_count', 'redirect_count', 'http_status_code' ];

	/**
	 * Fields that are nullable.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $nullFields = [ 'last_scan_date', 'final_url' ];

	/**
	 * Fields that should be boolean values.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $booleanFields = [
		'broken',
		'dismissed'
	];

	/**
	 * Fields that contain a JSON string.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $jsonFields = [ 'log' ];

	/**
	 * Returns the Link Status with the given ID.
	 *
	 * @since 1.0.0
	 *
	 * @param  int        $linkStatusId The Link Status ID.
	 * @return LinkStatus               The Link Status instance.
	 */
	public static function getById( $linkStatusId ) {
		return aioseoBrokenLinkChecker()->core->db->start( 'aioseo_blc_link_status' )
			->where( 'id', $linkStatusId )
			->run()
			->model( 'AIOSEO\\BrokenLinkChecker\\Models\\LinkStatus' );
	}

	/**
	 * Returns a list of Link Status rows with the given IDs.
	 *
	 * @since 1.1.0
	 *
	 * @param  array $linkStatusIds List of Link Status IDs.
	 * @return array                List of Link Status instances.
	 */
	public static function getByIds( $linkStatusIds ) {
		return aioseoBrokenLinkChecker()->core->db->start( 'aioseo_blc_link_status' )
			->whereIn( 'id', $linkStatusIds )
			->run()
			->models( 'AIOSEO\\BrokenLinkChecker\\Models\\LinkStatus' );
	}

	/**
	 * Returns the Link Status with the given URL.
	 *
	 * @since 1.0.0
	 *
	 * @param  string     $url The URL (unhashed!).
	 * @return LinkStatus      The Link Status instance.
	 */
	public static function getByUrl( $url ) {
		$hash = sha1( $url );

		$linkStatus = aioseoBrokenLinkChecker()->core->db->start( 'aioseo_blc_link_status' )
			->where( 'url_hash', $hash )
			->run()
			->model( 'AIOSEO\\BrokenLinkChecker\\Models\\LinkStatus' );

		if ( ! $linkStatus->exists() ) {
			// Updates to the plugin can cause hash mismatches. Let's do another attempt using the URL.
			// We do a join to improve performance since the URL isn't indexed.
			$hostname = wp_parse_url( $url, PHP_URL_HOST );
			$result   = aioseoBrokenLinkChecker()->core->db->start( 'aioseo_blc_link_status as abls' )
				->select( 'abls.id' )
				->join( 'aioseo_blc_links as abl', 'abls.id = abl.blc_link_status_id' )
				->where( 'abl.hostname', $hostname )
				->where( 'abls.url', $url )
				->groupBy( 'abls.id' )
				->limit( 1 )
				->run()
				->result();

			if ( ! empty( $result[0]->id ) ) {
				$linkStatus = self::getById( $result[0]->id );

				if ( $linkStatus->exists() ) {
					// Reset the URL hash to prevent future mismatches.
					$linkStatus->url_hash = $hash;
					$linkStatus->save();
				}
			}
		}

		return $linkStatus;
	}

	/**
	 * Returns all broken links for a given post ID.
	 *
	 * @since 1.2.0
	 *
	 * @param int    $postId The post ID.
	 * @return array         The list of broken links.
	 */
	public static function getBrokenByPostId( $postId ) {
		$query = aioseoBrokenLinkChecker()->core->db->start( 'aioseo_blc_link_status as als' )
			->join( 'aioseo_blc_links as al', 'als.id = al.blc_link_status_id' )
			->where( 'al.post_id', $postId )
			->where( 'als.broken', true )
			->where( 'als.dismissed', false );

		return $query->run()
			->result();
	}

	/**
	 * Returns link status row results based on the given arguments.
	 * This is basically a wrapper/query builder that we use to fetch all the data we need for the Broken Links Report.
	 *
	 * @since   1.0.0
	 * @version 1.1.0 Moved from Links model to Link Status model.
	 *
	 * @param  string $filter      The active filter.
	 * @param  int    $limit       The limit.
	 * @param  int    $offset      The offset.
	 * @param  string $whereClause The WHERE clause.
	 * @param  string $orderBy     The order by.
	 * @param  string $orderDir    The order direction.
	 * @return array               List of Link Status rows with related Link rows embedded.
	 */
	public static function rowQuery( $filter = 'all', $limit = 20, $offset = 0, $whereClause = '', $orderBy = '', $orderDir = 'DESC' ) {
		$query = self::baseQuery( $filter, $whereClause )
			->select( 'als.*, al.external' )
			->limit( $limit, $offset );

		if ( $orderBy && $orderDir ) {
			$query->orderBy( "$orderBy $orderDir" );
		} else {
			$query->orderBy( 'als.id DESC' );
		}

		$linkStatusRows = $query->run()
			->result();

		if ( empty( $linkStatusRows ) ) {
			return [];
		}

		$rowsWithData = [];
		foreach ( $linkStatusRows as $linkStatusRow ) {
			$linkStatusRow->totalLinks = Link::rowQueryCount( $linkStatusRow->id );
			if ( $linkStatusRow->totalLinks > 1 ) {
				$rowsWithData[] = $linkStatusRow;
				continue;
			}

			// If this link status has just one link, then we'll get it here.
			// Otherwise we'll get them when the links table loads.
			$linkRows            = Link::rowQuery( $linkStatusRow->id, 1 );
			$linkStatusRow->link = reset( $linkRows );
			$rowsWithData[]      = $linkStatusRow;
		}

		return $rowsWithData;
	}

	/**
	 * Returns link status row count based on the given arguments.
	 * This is basically a wrapper/query builder that we use to fetch all the counts we need for the Broken Links Report.
	 *
	 * @since   1.0.0
	 * @version 1.1.0 Moved from Links model to Link Status model.
	 *
	 * @param  string $filter      The active filter.
	 * @param  string $whereClause The WHERE clause.
	 * @return int                 The row count.
	 */
	public static function rowCountQuery( $filter = 'all', $whereClause = '' ) {
		$query = self::baseQuery( $filter, $whereClause );

		return $query->count();
	}

	/**
	 * Returns the base query for the rowQuery() and rowCountQuery() methods.
	 *
	 * @since   1.0.0
	 * @version 1.1.0 Moved from Links model to Link Status model.
	 *
	 * @param  string   $filter      The active filter.
	 * @param  string   $whereClause The WHERE clause.
	 * @return Database              The query.
	 */
	private static function baseQuery( $filter = 'all', $whereClause = '' ) {
		$includedPostTypes    = aioseoBrokenLinkChecker()->helpers->getIncludedPostTypes();
		$includedPostStatuses = aioseoBrokenLinkChecker()->helpers->getIncludedPostStatuses();
		$excludedPostIds      = aioseoBrokenLinkChecker()->helpers->getExcludedPostIds();
		$excludedDomains      = aioseoBrokenLinkChecker()->helpers->getExcludedDomains();

		$query = aioseoBrokenLinkChecker()->core->db->start( 'aioseo_blc_link_status as als' )
			->join( 'aioseo_blc_links as al', 'als.id = al.blc_link_status_id' )
			->join( 'posts as p', 'al.post_id = p.ID' )
			->groupBy( 'al.url' );

		if ( ! empty( $whereClause ) ) {
			$query->whereRaw( $whereClause );
		}

		if ( ! empty( $includedPostStatuses ) ) {
			$query->whereIn( 'p.post_status', $includedPostStatuses );
		}

		if ( ! empty( $includedPostTypes ) ) {
			$query->whereIn( 'p.post_type', $includedPostTypes );
		}

		if ( ! empty( $excludedPostIds ) ) {
			$query->whereNotIn( 'p.ID', $excludedPostIds );
		}

		if ( ! empty( $excludedDomains ) ) {
			$query->whereNotIn( 'al.hostname', $excludedDomains );
		}

		if ( ! empty( $filter ) ) {
			switch ( $filter ) {
				case 'broken':
					$query->where( 'als.broken', true );
					$query->where( 'als.dismissed', false );
					break;
				case 'redirects':
					$query->where( 'als.redirect_count >', 0 );
					$query->where( 'als.dismissed', false );
					break;
				case 'dismissed':
					$query->where( 'als.dismissed', true );
					break;
				case 'not-checked':
					$query->where( 'als.last_scan_date', null );
					break;
				case 'all':
				default:
					$query->where( 'als.dismissed', false );
					break;
			}
		}

		return $query;
	}
}Models/Model.php000064400000023447151536237000007554 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Models;

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

/**
 * The base Model class.
 *
 * @since 1.0.0
 */
#[\AllowDynamicProperties]
class Model implements \JsonSerializable {
	/**
	 * Fields that should be null when saving to the database.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $nullFields = [];

	/**
	 * Fields that should be encoded/decoded on save/get.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $jsonFields = [];

	/**
	 * Fields that should be boolean values.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $booleanFields = [];

	/**
	 * Fields that should be numeric values.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $integerFields = [];

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $hidden = [];

	/**
	 * The table used in the SQL query.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	protected $table = '';

	/**
	 * The primary key retrieved from the table.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	protected $pk = 'id';

	/**
	 * An array of columns from the DB that we can use.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private static $columns;

	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $var This can be the primary key of the resource, or it could be an array of data to manufacture a resource without a database query.
	 */
	public function __construct( $var = null ) {
		$fields = [];
		$skip   = [ 'id', 'created', 'updated' ];
		foreach ( $this->getColumns() as $column => $value ) {
			if ( ! in_array( $column, $skip, true ) ) {
				$fields[ $column ] = $value;
			}
		}

		$this->applyKeys( $fields );

		// Process straight through if we were given a valid array.
		if ( is_array( $var ) || is_object( $var ) ) {
			// Apply keys to object.
			$this->applyKeys( $var );

			if ( $this->exists() ) {
				return true;
			}

			return false;
		}

		return $this->loadData( $var );
	}

	/**
	 * Load the data from the database!
	 *
	 * @since 1.0.0
	 *
	 * @param  mixed $var The primary key to load up the model from the DB.
	 * @return Model      Returns the current object.
	 */
	protected function loadData( $var = null ) {
		if ( null === $var ) {
			return false;
		}

		$query = aioseoBrokenLinkChecker()->core->db
			->start( $this->table )
			->where( $this->pk, $var )
			->limit( 1 )
			->output( 'ARRAY_A' );

		$result = $query->run();
		if ( ! $result || $result->nullSet() ) {
			return $this;
		}

		$this->applyKeys( $result->result()[0] );

		return $this;
	}

	/**
	 * Take the keys from the result array and add them to the Model.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $array The array of keys and values to add to the Model.
	 * @return void
	 */
	protected function applyKeys( $array ) {
		if ( ! is_object( $array ) && ! is_array( $array ) ) {
			throw new \Exception( '$array must either be an object or an array.' );
		}

		foreach ( (array) $array as $key => $value ) {
			$key        = trim( $key );
			$this->$key = $value;

			if ( null === $value && in_array( $key, $this->nullFields, true ) ) {
				continue;
			}

			if ( in_array( $key, $this->jsonFields, true ) ) {
				$this->$key = json_decode( (string) $value );
				continue;
			}

			if ( in_array( $key, $this->booleanFields, true ) ) {
				$this->$key = (bool) $value;
				continue;
			}

			if ( in_array( $key, $this->integerFields, true ) ) {
				$this->$key = (int) $value;
			}
		}
	}

	/**
	 * Let's filter out any properties we cannot save to the database.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $key The table column.
	 * @return array       The array of valid columns for the database query.
	 */
	protected function filter( $key ) {
		$fields  = [];
		$skip    = [ 'created', 'updated' ];
		$table   = aioseoBrokenLinkChecker()->core->db->prefix . $this->table;
		$results = aioseoBrokenLinkChecker()->core->db->execute( 'SHOW COLUMNS FROM `' . $table . '`', true );
		$columns = $results->result();

		foreach ( $columns as $col ) {
			if ( ! in_array( $col->Field, $skip, true ) && array_key_exists( $col->Field, $key ) ) {
				$fields[ $col->Field ] = $key[ $col->Field ];
			}
		}

		return $fields;
	}

	/**
	 * Transforms the data to be null if it exists in the nullFields variables.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $data The data array to transform.
	 * @return array       The transformed data.
	 */
	protected function transform( $data, $set = false ) {
		foreach ( $this->nullFields as $field ) {
			if ( isset( $data[ $field ] ) && empty( $data[ $field ] ) ) {
				$data[ $field ] = null;
			}
		}

		foreach ( $this->booleanFields as $field ) {
			if ( isset( $data[ $field ] ) && '' === $data[ $field ] ) {
				unset( $data[ $field ] );
			} elseif ( isset( $data[ $field ] ) ) {
				$data[ $field ] = (bool) $data[ $field ] ? 1 : 0;
			}
		}

		if ( $set ) {
			return $data;
		}

		foreach ( $this->integerFields as $field ) {
			if ( isset( $data[ $field ] ) ) {
				$data[ $field ] = (int) $data[ $field ];
			}
		}

		foreach ( $this->jsonFields as $field ) {
			if ( isset( $data[ $field ] ) && ! aioseoBrokenLinkChecker()->helpers->isJsonString( $data[ $field ] ) ) {
				if ( is_array( $data[ $field ] ) && aioseoBrokenLinkChecker()->helpers->isArrayNumeric( $data[ $field ] ) ) {
					$data[ $field ] = array_values( $data[ $field ] );
				}
				$data[ $field ] = wp_json_encode( $data[ $field ] );
			}
		}

		return $data;
	}

	/**
	 * Sets a piece of data to the requested resource.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function set() {
		$args  = func_get_args();
		$count = func_num_args();

		if ( ! is_array( $args[0] ) && $count < 2 ) {
			throw new \Exception( 'The set method must contain at least 2 arguments: key and value. Or an array of data. Only one argument was passed and it was not an array.' );
		}

		$key   = $args[0];
		$value = ! empty( $args[1] ) ? $args[1] : null;

		if ( false === $key ) {
			return false;
		}

		if ( ! is_array( $key ) ) {
			$key = [ $key => $value ];
		}

		$key = $this->transform( $key, true );
		foreach ( $key as $k => $v ) {
			if ( ! empty( $k ) ) {
				$this->$k = $v;
			}
		}
	}

	/**
	 * Delete the row in the DB.
	 *
	 * @since 1.0.0
	 *
	 * @return null
	 */
	public function delete() {
		aioseoBrokenLinkChecker()->core->db
			->delete( $this->table )
			->where( $this->pk, $this->id )
			->run();

		return null;
	}

	/**
	 * Saves data to the requested resource.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function save() {
		$fields = $this->transform( $this->filter( (array) get_object_vars( $this ) ) );

		$id   = null;
		$date = gmdate( 'Y-m-d H:i:s' );
		if ( ! empty( $fields ) ) {
			$pk = $this->pk;

			if ( isset( $this->$pk ) && '' !== $this->$pk ) {
				// PK specified.
				$pkv   = $this->$pk;
				$query = aioseoBrokenLinkChecker()->core->db
					->start( $this->table )
					->where( [ $pk => $pkv ] )
					->run();

				if ( ! $query->nullSet() ) {
					// Row exists in database.
					$fields['updated'] = $date;
					aioseoBrokenLinkChecker()->core->db
						->update( $this->table )
						->set( $fields )
						->where( [ $pk => $pkv ] )
						->run();
					$id = $this->$pk;
				} else {
					// Row does not exist.
					$fields[ $pk ]     = $pkv;
					$fields['created'] = $date;
					$fields['updated'] = $date;

					$id = aioseoBrokenLinkChecker()->core->db
						->insert( $this->table )
						->set( $fields )
						->run()
						->insertId();

					if ( $id ) {
						$this->$pk = $id;
					}
				}
			} else {
				$fields['created'] = $date;
				$fields['updated'] = $date;

				$id = aioseoBrokenLinkChecker()->core->db
					->insert( $this->table )
					->set( $fields )
					->run()
					->insertId();

				if ( $id ) {
					$this->$pk = $id;
				}
			}
		}

		$this->reset( $id );
	}

	/**
	 * Check if the model exists in the database.
	 *
	 * @since 1.0.0
	 *
	 * @return bool Whether the model exists.
	 */
	public function exists() {
		return ( ! empty( $this->{$this->pk} ) ) ? true : false;
	}

	/**
	 * Resets a resource by forcing internal updates to be applied.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $id The resource ID.
	 * @return void
	 */
	public function reset( $id = null ) {
		$id = ! empty( $id ) ? $id : $this->{$this->pk};
		$this->__construct( $id );
	}

	/**
	 * Helper function to remove data we don't want serialized.
	 *
	 * @since 1.0.0
	 *
	 * @return array An array of data that we are OK with serializing.
	 */
	#[\ReturnTypeWillChange]
	// The attribute above omits a deprecation notice from PHP 8.1 that is thrown because the return type of jsonSerialize() isn't "mixed".
	// Once PHP 7.x is our minimum supported version, this can be removed in favour of overriding the return type in the method signature like this -
	// public function jsonSerialize() : array
	public function jsonSerialize() {
		$array = [];

		foreach ( $this->getColumns() as $column => $value ) {
			if ( in_array( $column, $this->hidden, true ) ) {
				continue;
			}

			$array[ $column ] = isset( $this->$column ) ? $this->$column : null;
		}

		return $array;
	}

	/**
	 * Retrieves the columns for the model.
	 *
	 * @since 1.0.0
	 *
	 * @return array An array of columns.
	 */
	public function getColumns() {
		if ( empty( self::$columns[ get_called_class() ] ) ) {
			self::$columns[ get_called_class() ] = [];

			// Let's set the columns that are available by default.
			$table   = aioseoBrokenLinkChecker()->core->db->prefix . $this->table;
			$results = aioseoBrokenLinkChecker()->core->db->execute( 'SHOW COLUMNS FROM `' . $table . '`', true );

			foreach ( $results->result() as $col ) {
				self::$columns[ get_called_class() ][ $col->Field ] = $col->Default;
			}

			if ( ! empty( $this->appends ) ) {
				foreach ( $this->appends as $append ) {
					self::$columns[ get_called_class() ][ $append ] = null;
				}
			}
		}

		return self::$columns[ get_called_class() ];
	}
}Models/Notification.php000064400000020101151536237000011122 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Models;

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

/**
 * The Notification DB model class.
 *
 * @since 1.0.0
 */
class Notification extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	protected $table = 'aioseo_blc_notifications';

	/**
	 * An array of fields to set to null if already empty when saving to the database.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $nullFields = [
		'start',
		'end',
		'notification_id',
		'notification_name',
		'button1_label',
		'button1_action',
		'button2_label',
		'button2_action'
	];

	/**
	 * Fields that should be json encoded on save and decoded on get.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $jsonFields = [ 'level' ];

	/**
	 * Fields that should be boolean values.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $booleanFields = [ 'dismissed' ];

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @var array
	 */
	protected $hidden = [ 'id' ];

	/**
	 * An array of fields attached to this resource.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $columns = [
		'id',
		'slug',
		'notification_id',
		'notification_name',
		'title',
		'content',
		'type',
		'level',
		'start',
		'end',
		'button1_label',
		'button1_action',
		'button2_label',
		'button2_action',
		'dismissed',
		'new',
		'created',
		'updated'
	];

	/**
	 * Returns all notifications.
	 *
	 * @since 1.0.0
	 *
	 * @param  bool  $reset Whether or not to reset the new notifications.
	 * @return array        List of notifications.
	 */
	public static function getNotifications( $reset = true ) {
		return [
			'active'    => self::getAllActiveNotifications(),
			'new'       => self::getNewNotifications( $reset ),
			'dismissed' => self::getAllDismissedNotifications()
		];
	}

	/**
	 * Returns the active notifications.
	 *
	 * @since 1.0.0
	 *
	 * @return array List of active notifications.
	 */
	public static function getAllActiveNotifications() {
		$staticNotifications = self::getStaticNotifications();
		$notifications       = array_values( json_decode( wp_json_encode( self::getActiveNotifications() ), true ) );

		return ! empty( $staticNotifications ) ? array_merge( $staticNotifications, $notifications ) : $notifications;
	}

	/**
	 * Returns all static notifications.
	 *
	 * @since 1.2.0
	 *
	 * @return array List of static notifications.
	 */
	public static function getStaticNotifications() {
		$notifications       = [ 'review' ];
		$staticNotifications = [];
		foreach ( $notifications as $notification ) {
			switch ( $notification ) {
				case 'review':
					// If dismissed, don't show again.
					$originalDismissed = get_user_meta( get_current_user_id(), '_aioseo_blc_plugin_review_dismissed', true );
					if ( '4' !== $originalDismissed ) {
						break;
					}

					$dismissed = get_user_meta( get_current_user_id(), '_aioseo_blc_notification_plugin_review_dismissed', true );
					if ( '3' === $dismissed ) {
						break;
					}

					if ( ! empty( $dismissed ) && $dismissed > time() ) {
						break;
					}

					$activated = aioseoBrokenLinkChecker()->internalOptions->internal->firstActivated( time() );
					if ( $activated > strtotime( '-20 days' ) ) {
						break;
					}

					$staticNotifications[] = [
						'slug'      => 'notification-' . $notification,
						'component' => 'notifications-' . $notification . '2',
					];
					break;
				default:
					break;
			}
		}

		return $staticNotifications;
	}

	/**
	 * Retrieve active notifications.
	 *
	 * @since 1.0.0
	 *
	 * @return array List of active notifications or empty.
	 */
	public static function getActiveNotifications() {
		return self::filterNotifications(
			aioseoBrokenLinkChecker()->core->db
				->start( 'aioseo_blc_notifications' )
				->where( 'dismissed', 0 )
				->whereRaw( "(start <= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR start IS NULL)" )
				->whereRaw( "(end >= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR end IS NULL)" )
				->orderBy( 'start DESC' )
				->orderBy( 'created DESC' )
				->run()
				->models( 'AIOSEO\\BrokenLinkChecker\\Models\\Notification' )
		);
	}

	/**
	 * Returns all new notifications. After retrieving them, this will reset them.
	 * This means that calling this method twice will result in no results
	 * the second time. The only exception is to pass false as a reset variable to prevent it.
	 *
	 * @since 1.0.0
	 *
	 * @param  boolean $reset Whether or not to reset the new notifications.
	 * @return array          List of new notifications if any exist.
	 */
	public static function getNewNotifications( $reset = true ) {
		$notifications = self::filterNotifications(
			aioseoBrokenLinkChecker()->core->db
				->start( 'aioseo_blc_notifications' )
				->where( 'dismissed', 0 )
				->where( 'new', 1 )
				->whereRaw( "(start <= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR start IS NULL)" )
				->whereRaw( "(end >= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR end IS NULL)" )
				->orderBy( 'start DESC' )
				->orderBy( 'created DESC' )
				->run()
				->models( 'AIOSEO\\BrokenLinkChecker\\Models\\Notification' )
		);

		if ( $reset ) {
			self::resetNewNotifications();
		}

		return $notifications;
	}

	/**
	 * Resets all new notifications.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public static function resetNewNotifications() {
		aioseoBrokenLinkChecker()->core->db
			->update( 'aioseo_blc_notifications' )
			->where( 'new', 1 )
			->set( 'new', 0 )
			->run();
	}

	/**
	 * Returns a list of dismissed notifications.
	 *
	 * @since 1.0.0
	 *
	 * @return array List of dismissed notifications.
	 */
	public static function getAllDismissedNotifications() {
		return array_values( json_decode( wp_json_encode( self::getDismissedNotifications() ), true ) );
	}

	/**
	 * Retrieve dismissed notifications.
	 *
	 * @since 1.0.0
	 *
	 * @return array List of dismissed notifications or empty.
	 */
	public static function getDismissedNotifications() {
		return self::filterNotifications(
			aioseoBrokenLinkChecker()->core->db
				->start( 'aioseo_blc_notifications' )
				->where( 'dismissed', 1 )
				->orderBy( 'updated DESC' )
				->run()
				->models( 'AIOSEO\\BrokenLinkChecker\\Models\\Notification' )
		);
	}

	/**
	 * Returns a notification by its name.
	 *
	 * @since 1.0.0
	 *
	 * @param  string       $name The notification name.
	 * @return Notification       The notification.
	 */
	public static function getNotificationByName( $name ) {
		return aioseoBrokenLinkChecker()->core->db
			->start( 'aioseo_blc_notifications' )
			->where( 'notification_name', $name )
			->run()
			->model( 'AIOSEO\\BrokenLinkChecker\\Models\\Notification' );
	}

	/**
	 * Stores a new notification in the DB.
	 *
	 * @since 1.0.0
	 *
	 * @param  array        $fields       The fields.
	 * @return Notification $notification The notification.
	 */
	public static function addNotification( $fields ) {
		// Set the dismissed status to false.
		$fields['dismissed'] = 0;

		$notification = new self();
		$notification->set( $fields );
		$notification->save();

		return $notification;
	}

	/**
	 * Deletes a notification by its name.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $name The notification name.
	 * @return void
	 */
	public static function deleteNotificationByName( $name ) {
		aioseoBrokenLinkChecker()->core->db
			->delete( 'aioseo_blc_notifications' )
			->where( 'notification_name', $name )
			->run();
	}

	/**
	 * Filters the notifications based on the targeted plan levels.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $notifications          The notifications
	 * @return array $remainingNotifications The remaining notifications.
	 */
	public static function filterNotifications( $notifications ) {
		$remainingNotifications = [];
		foreach ( $notifications as $notification ) {
			$levels = $notification->level;
			if ( ! is_array( $levels ) ) {
				$levels = empty( $notification->level ) ? [ 'all' ] : [ $notification->level ];
			}

			foreach ( $levels as $level ) {
				if ( ! aioseoBrokenLinkChecker()->notifications->validateType( $level ) ) {
					continue 2;
				}
			}

			$remainingNotifications[] = $notification;
		}

		return $remainingNotifications;
	}
}Models/Post.php000064400000002324151536237000007430 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Models;

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

/**
 * The Post DB model class.
 *
 * @since 1.0.0
 */
class Post extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	protected $table = 'aioseo_blc_posts';

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $hidden = [ 'id' ];

	/**
	 * Fields that are nullable.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $nullFields = [ 'last_scan_date', 'final_url' ];

	/**
	 * Fields that should be json encoded on save and decoded on get.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $jsonFields = [];


	/**
	 * Returns a Post with a given ID.
	 *
	 * @since 1.0.0
	 *
	 * @param  int  $postId The Post ID.
	 * @return Post         The Post object.
	 */
	public static function getPost( $postId ) {
		$post = aioseoBrokenLinkChecker()->core->db->start( 'aioseo_blc_posts' )
			->where( 'post_id', $postId )
			->run()
			->model( 'AIOSEO\\BrokenLinkChecker\\Models\\Post' );

		if ( ! $post->exists() ) {
			$post->post_id = $postId;
		}

		return $post;
	}
}Options/Cache.php000064400000003100151536237000007707 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Options;

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

/**
 * Class that holds all the cache for the options.
 *
 * @since 1.0.0
 */
class Cache {
	/**
	 * The DB options cache.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private static $db = [];

	/**
	 * The options cache.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private static $options = [];

	/**
	 * Sets the cache for the DB option.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $name  The cache name.
	 * @param  array  $value The value.
	 * @return void
	 */
	public function setDb( $name, $value ) {
		self::$db[ $name ] = $value;
	}

	/**
	 * Gets the cache for the DB option.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $name The cache name.
	 * @return array        The data from the cache.
	 */
	public function getDb( $name ) {
		return ! empty( self::$db[ $name ] ) ? self::$db[ $name ] : [];
	}

	/**
	 * Sets the cache for the options.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $name  The cache name.
	 * @param  array  $value The value.
	 * @return void
	 */
	public function setOptions( $name, $value ) {
		self::$options[ $name ] = $value;
	}

	/**
	 * Gets the cache for the options.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $name The cache name.
	 * @return array        The data from the cache.
	 */
	public function getOptions( $name ) {
		return ! empty( self::$options[ $name ] ) ? self::$options[ $name ] : [];
	}

	/**
	 * Resets the DB cache.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function resetDb() {
		self::$db = [];
	}
}Options/InternalOptions.php000064400000007223151536237000012046 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Options;

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

use AIOSEO\BrokenLinkChecker\Traits;

/**
 * Class that holds all internal options for Broken Link Checker.
 *
 * @since 1.0.0
 */
class InternalOptions {
	use Traits\Options;

	/**
	 * Holds a list of all the possible deprecated options.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $allDeprecatedOptions = [];

	/**
	 * All the default options.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $defaults = [
		// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
		'internal' => [
			'firstActivated'      => [ 'type' => 'number', 'default' => 0 ],
			'lastActiveVersion'   => [ 'type' => 'string', 'default' => '0.0' ],
			'scanId'              => [ 'type' => 'string', 'default' => null ],
			'minimumLinkScanDate' => [ 'type' => 'string', 'default' => null ],
			'license'             => [
				'expires'          => [ 'type' => 'number', 'default' => 0 ],
				'expired'          => [ 'type' => 'boolean', 'default' => false ],
				'invalid'          => [ 'type' => 'boolean', 'default' => false ],
				'disabled'         => [ 'type' => 'boolean', 'default' => false ],
				'connectionError'  => [ 'type' => 'boolean', 'default' => false ],
				'activationsError' => [ 'type' => 'boolean', 'default' => false ],
				'requestError'     => [ 'type' => 'boolean', 'default' => false ],
				'lastChecked'      => [ 'type' => 'number', 'default' => 0 ],
				'level'            => [ 'type' => 'string' ],
				'licenseKey'       => [ 'type' => 'string', 'default' => '' ],
				'quota'            => [ 'type' => 'number', 'default' => 0 ],
				'quotaRemaining'   => [ 'type' => 'number', 'default' => 0 ]
			]
		],
		'database' => [
			'installedTables' => [ 'type' => 'string' ]
		]
		// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	];

	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 *
	 * @param string $optionsName The options name.
	 */
	public function __construct( $optionsName = 'aioseo_blc_options_internal' ) {
		$this->optionsName = is_network_admin() ? $optionsName . '_network' : $optionsName;

		$this->init();

		add_action( 'shutdown', [ $this, 'save' ] );
	}

	/**
	 * Initializes the options.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	protected function init() {
		// Options from the DB.
		$dbOptions = $this->getDbOptions( $this->optionsName );

		// Refactor options.
		$this->defaultsMerged = array_replace_recursive( $this->defaults, $this->defaultsMerged );

		$options = array_replace_recursive(
			$this->defaultsMerged,
			$this->addValueToValuesArray( $this->defaultsMerged, $dbOptions )
		);

		aioseoBrokenLinkChecker()->core->optionsCache->setOptions( $this->optionsName, apply_filters( 'aioseo_blc_get_options_internal', $options ) );
	}

	/**
	 * Get all the deprecated options.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function getAllDeprecatedOptions() {
		return $this->allDeprecatedOptions;
	}

	/**
	 * Sanitizes, then saves the options to the database.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $options An array of options to sanitize, then save.
	 * @return void
	 */
	public function sanitizeAndSave( $options ) {
		if ( ! is_array( $options ) ) {
			return;
		}

		// Refactor options.
		$cachedOptions = aioseoBrokenLinkChecker()->core->optionsCache->getOptions( $this->optionsName );
		$dbOptions     = array_replace_recursive(
			$cachedOptions,
			$this->addValueToValuesArray( $cachedOptions, $options, [], true )
		);

		aioseoBrokenLinkChecker()->core->optionsCache->setOptions( $this->optionsName, $dbOptions );

		// Update values.
		$this->save( true );
	}
}Options/Options.php000064400000010153151536237000010345 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Options;

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

use AIOSEO\BrokenLinkChecker\Traits;

/**
 * Handles the main options.
 *
 * @since 1.0.0
 */
class Options {
	use Traits\Options;

	/**
	 * All the default options.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $defaults = [
		// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
		'general'  => [
			'linkTweaks'           => [
				'nofollowBroken'    => [ 'type' => 'boolean', 'default' => false ],
				'limitModifiedDate' => [ 'type' => 'boolean', 'default' => false ]
			],
			'highlightBrokenLinks' => [ 'type' => 'boolean', 'default' => false ]
		],
		'advanced' => [
			'enable'         => [ 'type' => 'boolean', 'default' => false ],
			'postTypes'      => [
				'all'      => [ 'type' => 'boolean', 'default' => true ],
				'included' => [ 'type' => 'array', 'default' => [ 'post', 'page' ] ]
			],
			'postStatuses'   => [
				'all'      => [ 'type' => 'boolean', 'default' => false ],
				'included' => [ 'type' => 'array', 'default' => [ 'publish', 'draft', 'pending', 'future', 'private' ] ]
			],
			'excludePosts'   => [ 'type' => 'array', 'default' => [] ],
			'excludeDomains' => [ 'type' => 'html', 'default' => '' ],
			'uninstall'      => [ 'type' => 'boolean', 'default' => false ]
		]
		// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	];

	/**
	 * The Construct method.
	 *
	 * @since 1.0.0
	 *
	 * @param string $optionsName An array of options.
	 */
	public function __construct( $optionsName = 'aioseo_blc_options' ) {
		$this->optionsName = $optionsName;

		$this->init();

		add_action( 'shutdown', [ $this, 'save' ] );
	}

	/**
	 * Initializes the options.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	protected function init() {
		$options = $this->getBrokenLinkCheckerDbOptions();

		aioseoBrokenLinkChecker()->core->optionsCache->setOptions( $this->optionsName, apply_filters( 'aioseo_blc_get_options', $options ) );
	}

	/**
	 * Get the DB options.
	 *
	 * @since 1.0.0
	 *
	 * @return array An array of options.
	 */
	public function getBrokenLinkCheckerDbOptions() {
		// Options from the DB.
		$dbOptions = $this->getDbOptions( $this->optionsName );

		// Refactor options.
		$this->defaultsMerged = array_replace_recursive( $this->defaults, $this->defaultsMerged );

		return array_replace_recursive(
			$this->defaultsMerged,
			$this->addValueToValuesArray( $this->defaultsMerged, $dbOptions )
		);
	}

	/**
	 * Sanitizes, then saves the options to the database.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $newOptions An array of options to sanitize, then save.
	 * @return void
	 */
	public function sanitizeAndSave( $newOptions ) {
		$this->init();

		if ( ! is_array( $newOptions ) ) {
			return;
		}

		// First, recursively replace the new options into the cached state.
		// It's important we use the helper method since we want to replace populated arrays with empty ones if needed (when a setting was cleared out).
		$cachedOptions = aioseoBrokenLinkChecker()->core->optionsCache->getOptions( $this->optionsName );
		$dbOptions     = aioseoBrokenLinkChecker()->helpers->arrayReplaceRecursive(
			$cachedOptions,
			$this->addValueToValuesArray( $cachedOptions, $newOptions, [], true )
		);

		// Now, we must also intersect both arrays to delete any individual keys that were unset.
		// We must do this because, while arrayReplaceRecursive will update the values for keys or empty them out,
		// it will keys that aren't present in the replacement array unaffected in the target array.
		$dbOptions = aioseoBrokenLinkChecker()->helpers->arrayIntersectRecursive(
			$dbOptions,
			$this->addValueToValuesArray( $cachedOptions, $newOptions, [], true ),
			'value'
		);

		if ( isset( $newOptions['advanced']['excludeDomains'] ) ) {
			$dbOptions['advanced']['excludeDomains'] = preg_replace( '/\h/', "\n", (string) $newOptions['advanced']['excludeDomains'] );
		}

		// Update the cache state.
		aioseoBrokenLinkChecker()->core->optionsCache->setOptions( $this->optionsName, $dbOptions );

		// Finally, save the new values to the DB.
		$this->save( true );
	}
}Standalone/Highlighter.php000064400000002016151536237000011604 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Standalone;

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

/**
 * Handles highlighting broken links.
 *
 * @since 1.2.0
 */
class Highlighter {
	/**
	 * Class constructor.
	 *
	 * @since 1.2.0
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'init' ] );
	}

	/**
	 * Initializes the class.
	 *
	 * @since 1.2.0
	 *
	 * @return void
	 */
	public function init() {
		if (
			is_admin() ||
			! is_user_logged_in() ||
			! current_user_can( 'edit_posts' )
		) {
			return;
		}

		if ( ! aioseoBrokenLinkChecker()->options->general->highlightBrokenLinks ) {
			return;
		}

		add_action( 'wp_enqueue_scripts', [ $this, 'enqueueScript' ] );
	}

	/**
	 * Enqueues the script.
	 *
	 * @since 1.2.0
	 *
	 * @return void
	 */
	public function enqueueScript() {
		$scriptHandle = 'src/vue/standalone/highlighter/main.js';
		aioseoBrokenLinkChecker()->core->assets->load( $scriptHandle, [], aioseoBrokenLinkChecker()->helpers->getVueData( 'highlighter' ) );
	}
}Standalone/SetupWizard.php000064400000011561151536237000011634 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Standalone;

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

/**
 * Class that holds our setup wizard.
 *
 * @since 1.0.0
 */
class SetupWizard {
	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		if ( ! is_admin() || wp_doing_cron() || wp_doing_ajax() ) {
			return;
		}

		add_action( 'admin_init', [ $this, 'loadSetupWizard' ] );
		add_action( 'admin_init', [ $this, 'redirect' ], 9999 );
		add_action( 'admin_menu', [ $this, 'addDashboardPage' ] );
		add_action( 'admin_head', [ $this, 'hideDashboardPageFromMenu' ] );
	}

	/**
	 * Redirects the user to the setup wizard.
	 * This method checks if a new install or update has just occurred. If so, then we redirect the user to the appropriate page.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function redirect() {
		if ( ! aioseoBrokenLinkChecker()->core->cache->get( 'activation_redirect' ) ) {
			return;
		}

		// If we are redirecting, clear the transient so it just happens once.
		aioseoBrokenLinkChecker()->core->cache->delete( 'activation_redirect' );

		if ( isset( $_GET['activate-multi'] ) || is_network_admin() ) { // phpcs:ignore HM.Security.NonceVerification.Recommended
			return;
		}

		wp_safe_redirect( admin_url( 'index.php?page=broken-link-checker-setup-wizard' ) );
		exit;
	}

	/**
	 * Adds a dashboard page for our setup wizard.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function addDashboardPage() {
		add_dashboard_page(
			__( 'Setup Wizard', 'aioseo-broken-link-checker' ),
			__( 'Setup Wizard', 'aioseo-broken-link-checker' ),
			'aioseo_blc_setup_wizard_page',
			'broken-link-checker-setup-wizard',
			''
		);
	}

	/**
	 * Hide the dashboard page from the menu.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function hideDashboardPageFromMenu() {
		remove_submenu_page( 'index.php', 'broken-link-checker-setup-wizard' );
	}

	/**
	 * Checks to see if we should load the setup wizard.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function loadSetupWizard() {
		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		// phpcs:disable HM.Security.ValidatedSanitizedInput.InputNotSanitized, HM.Security.NonceVerification.Recommended
		if (
			! isset( $_GET['page'] ) ||
			'broken-link-checker-setup-wizard' !== wp_unslash( $_GET['page'] ) ||
			! current_user_can( 'aioseo_blc_setup_wizard_page' )
		) {
			return;
		}
		// phpcs:enable

		set_current_screen();

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

		// If we are redirecting, clear the transient so it just happens once.
		aioseoBrokenLinkChecker()->core->cache->delete( 'activation_redirect' );

		$this->loadSetupWizardAssets();
	}

	/**
	 * Load the assets for the setup wizard.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function loadSetupWizardAssets() {
		$this->enqueueScripts();
		$this->setupWizardHeader();
		$this->setupWizardContent();
		$this->setupWizardFooter();
		exit;
	}

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

		$scriptHandle = 'src/vue/standalone/setup-wizard/main.js';
		aioseoBrokenLinkChecker()->core->assets->load( $scriptHandle, [], aioseoBrokenLinkChecker()->helpers->getVueData( 'setup-wizard' ) );

		wp_enqueue_style( 'common' );
	}

	/**
	 * Outputs the simplified header used for the Setup Wizard.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function setupWizardHeader() {
		?>
		<!DOCTYPE html>
		<html <?php language_attributes(); ?>>
		<head>
			<meta name="viewport" content="width=device-width"/>
			<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
			<title>
			<?php
				// Translators: 1 - The plugin name ("Broken Link Checker").
				echo sprintf( esc_html__( '%1$s &rsaquo; Setup Wizard', 'aioseo-broken-link-checker' ), esc_html( AIOSEO_BROKEN_LINK_CHECKER_PLUGIN_NAME ) );
			?>
			</title>
		</head>
		<body class="aioseo-blc-setup-wizard">
		<?php
	}

	/**
	 * Outputs the div to mount on.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function setupWizardContent() {
		echo '<div id="aioseo-blc-app">';
		// TODO: Add JavaScript error page here.
		echo '</div>';
	}

	/**
	 * Outputs the simplified footer used for the Setup Wizard.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function setupWizardFooter() {
		?>
		<?php
		wp_print_scripts( 'aioseo-vendors' );
		wp_print_scripts( 'aioseo-common' );
		wp_print_scripts( 'aioseo-setup-wizard-script' );
		do_action( 'admin_footer', '' );
		do_action( 'admin_print_footer_scripts' );
		?>
		</body>
		</html>
		<?php
	}
}Standalone/Standalone.php000064400000000531151536237000011436 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Standalone;

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

/**
 * Registers the standalone components.
 *
 * @since 1.0.0
 */
class Standalone {
	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		new Highlighter();
		new SetupWizard();
	}
}Traits/Helpers/Api.php000064400000006136151536237000010646 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Traits\Helpers;

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

/**
 * Contains API specific helper methods.
 *
 * @since 1.0.0
 */
trait Api {
	/**
	 * Request the remote URL via wp_remote_post and return a json decoded response.
	 *
	 * @since 1.0.0
	 *
	 * @param  array       $body    The content to retrieve from the remote URL.
	 * @param  array       $headers The headers to send to the remote URL.
	 * @return string|null          JSON decoded response on success, false on failure.
	 */
	public function sendRequest( $url, $body = [], $headers = [] ) {
		$body = wp_json_encode( $body );

		// Build the headers of the request.
		$headers = wp_parse_args(
			$headers,
			[
				'Content-Type' => 'application/json'
			]
		);

		// Setup variable for wp_remote_post.
		$requestArgs = [
			'headers' => $headers,
			'body'    => $body,
			'timeout' => 20
		];

		// Perform the query and retrieve the response.
		$response     = $this->wpRemotePost( $url, $requestArgs );
		$responseBody = wp_remote_retrieve_body( $response );

		// Bail out early if there are any errors.
		if ( ! $responseBody ) {
			return null;
		}

		// Return the json decoded content.
		return json_decode( $responseBody );
	}

	/**
	 * Sends a request using wp_remote_post.
	 *
	 * @since 1.0.0
	 *
	 * @param  string          $url  The URL to send the request to.
	 * @param  array           $args The args to use in the request.
	 * @return array|\WP_Error       The response as an array or WP_Error on failure.
	 */
	public function wpRemotePost( $url, $args = [] ) {
		return wp_remote_post( $url, array_replace_recursive( $this->getWpApiRequestDefaults(), $args ) );
	}

	/**
	 * Sends a request using wp_remote_get.
	 *
	 * @since 1.0.0
	 *
	 * @param  string          $url  The URL to send the request to.
	 * @param  array           $args The args to use in the request.
	 * @return array|\WP_Error       The response as an array or WP_Error on failure.
	 */
	public function wpRemoteGet( $url, $args = [] ) {
		return wp_remote_get( $url, array_replace_recursive( $this->getWpApiRequestDefaults(), $args ) );
	}

	/**
	 * Default arguments for wp_remote_get and wp_remote_post.
	 *
	 * @since 1.0.0
	 *
	 * @return array An array of default arguments for the request.
	 */
	private function getWpApiRequestDefaults() {
		return [
			'timeout'    => 10,
			'headers'    => aioseoBrokenLinkChecker()->helpers->getApiHeaders(),
			'user-agent' => aioseoBrokenLinkChecker()->helpers->getApiUserAgent()
		];
	}

	/**
	 * Returns the headers for internal API requests.
	 *
	 * @since 1.0.0
	 *
	 * @return array An array of headers.
	 */
	private function getApiHeaders() {
		return [
			'X-AIOSEO-BLC-License' => aioseoBrokenLinkChecker()->internalOptions->internal->license->licenseKey
		];
	}

	/**
	 * Returns the User Agent for internal API requests.
	 *
	 * @since 1.0.0
	 *
	 * @return string The User Agent.
	 */
	private function getApiUserAgent() {
		return 'WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' ) . '; AIOSEO/BrokenLinkChecker/' . AIOSEO_BROKEN_LINK_CHECKER_VERSION;
	}
}Traits/Helpers/Arrays.php000064400000006657151536237000011406 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Traits\Helpers;

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

/**
 * Contains array specific helper methods.
 *
 * @since 1.0.0
 */
trait Arrays {
	/**
	 * Checks whether the given array is associative.
	 * Arrays that only have consecutive, sequential numeric keys are numeric.
	 * Otherwise they are associative.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $array The array.
	 * @return bool         Whether the array is associative.
	 */
	public function isArrayAssociative( $array ) {
		return 0 < count( array_filter( array_keys( $array ), 'is_string' ) );
	}

	/**
	 * Checks whether the given array is numeric.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $array The array.
	 * @return bool         Whether the array is numeric.
	 */
	public function isArrayNumeric( $array ) {
		return ! $this->isArrayAssociative( $array );
	}

	/**
	 * Recursively replaces the values from one array with the ones from another.
	 * This function should act identical to the built-in array_replace_recursive(), with the exception that it also replaces array values with empty arrays.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $targetArray      The target array
	 * @param  array $replacementArray The array with values to replace in the target array.
	 * @return array                   The modified array.
	 */
	public function arrayReplaceRecursive( $targetArray, $replacementArray ) {
		// In some cases the target array isn't an array yet (due to e.g. race conditions in InternalOptions), so in that case we can just return the replacement array.
		if ( ! is_array( $targetArray ) ) {
			return $replacementArray;
		}

		foreach ( $replacementArray as $k => $v ) {
			// If the key does not exist yet on the target array, add it.
			if ( ! isset( $targetArray[ $k ] ) ) {
				$targetArray[ $k ] = $replacementArray[ $k ];
				continue;
			}

			// If the value is an array, only try to recursively replace it if the value isn't empty.
			// Otherwise empty arrays will be ignored and won't override the existing value of the target array.
			if ( is_array( $v ) && ! empty( $v ) ) {
				$targetArray[ $k ] = $this->arrayReplaceRecursive( $targetArray[ $k ], $v );
				continue;
			}

			// Replace with non-array value or empty array.
			$targetArray[ $k ] = $v;
		}

		return $targetArray;
	}

	/**
	 * Recursively intersects the two given arrays.
	 * You can pass in an optional argument (allowedKey) to restrict the intersect to arrays with a specific key.
	 * This is needed when we are e.g. sanitizing array values before setting/saving them to an option.
	 *
	 * @since 1.0.0
	 *
	 * @param  array  $array1     The first array.
	 * @param  array  $array2     The second array.
	 * @param  string $allowedKey The only key the method should run for (optional).
	 * @param  string $parentKey  The parent key.
	 * @return array              The intersected array.
	 */
	public function arrayIntersectRecursive( $array1, $array2, $allowedKey = '', $parentKey = '' ) {
		if ( ! $allowedKey || $allowedKey === $parentKey ) {
			$array1 = array_intersect_assoc( $array1, $array2 );
		}

		if ( empty( $array1 ) ) {
			return [];
		}

		foreach ( $array1 as $k => $v ) {
			if ( is_array( $v ) && isset( $array2[ $k ] ) ) {
				$array1[ $k ] = $this->arrayIntersectRecursive( $array1[ $k ], $array2[ $k ], $allowedKey, $k );
			}
		}

		if ( $this->isArrayNumeric( $array1 ) ) {
			$array1 = array_values( $array1 );
		}

		return $array1;
	}
}Traits/Helpers/Constants.php000064400000015636151536237000012116 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Traits\Helpers;

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

/**
 * Contains constant specific helper methods.
 *
 * @since 1.0.0
 */
trait Constants {
	/**
	 * Returns the plugin menu icon.
	 *
	 * @since 1.0.0
	 *
	 * @return string The icon as a string.
	 */
	public function icon() {
		return '<svg width="32" height="32" viewBox="0 0 32 32" fill="white" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_1215_174351)"><path d="M28.064 5.4904C27.692 5.8624 27.32 6.2344 26.948 6.6056C25.9736 7.58 24.9976 8.5544 24.024 9.5304C23.7968 9.7576 23.528 9.892 23.2056 9.888C22.7728 9.8824 22.4408 9.6832 22.24 9.3016C22.0352 8.9128 22.0712 8.5256 22.3168 8.1616C22.3624 8.0944 22.4192 8.0328 22.4768 7.9752C23.8216 6.6296 25.1672 5.284 26.512 3.9384C23.7008 1.4856 20.024 0 16 0C7.1632 0 0 7.1632 0 16C0 20.024 1.4864 23.7008 3.9384 26.5128C5.2824 25.1688 6.6264 23.8248 7.9696 22.48C8.1912 22.2576 8.4504 22.1112 8.7664 22.1096C9.192 22.1072 9.5256 22.2896 9.7456 22.66C9.8408 22.82 9.8896 22.996 9.8888 23.0928C9.8944 23.5208 9.7584 23.796 9.5232 24.0304C8.3272 25.2248 7.132 26.42 5.9368 27.6152C5.7872 27.7648 5.6384 27.9136 5.4888 28.0632C8.3 30.5144 11.976 31.9992 15.9984 31.9992C24.8352 31.9992 31.9984 24.836 31.9984 15.9992C31.9984 11.976 30.5136 8.3008 28.0624 5.4896L28.064 5.4904ZM7.5328 20.5952C7.132 20.9952 6.8544 21.4672 6.7456 22.0256C6.7256 22.1264 6.712 22.2264 6.7024 22.3256L4.7872 24.2408C4.7024 24 4.6344 23.7512 4.5864 23.4928C4.3728 22.348 4.5448 21.2568 5.0928 20.2272C5.3048 19.8288 5.5672 19.4664 5.8864 19.1464C6.4912 18.5408 7.0952 17.9352 7.7008 17.3304C7.8328 18.1624 8.0864 18.9536 8.4432 19.6856C8.1392 19.9888 7.836 20.292 7.5328 20.5952ZM19.1136 5.9192C19.7904 5.2456 20.6024 4.8136 21.5336 4.6128C22.4504 4.4152 23.3552 4.4704 24.236 4.792L22.3256 6.7024C22.0464 6.7288 21.7696 6.7968 21.496 6.912C21.1584 7.0544 20.8664 7.2624 20.608 7.52C20.3 7.8272 19.9928 8.1352 19.6848 8.4424C18.9536 8.0856 18.1624 7.832 17.3312 7.7C17.9248 7.1056 18.5176 6.5112 19.1128 5.9184L19.1136 5.9192ZM12.8072 26.156C12.1008 26.8424 11.2448 27.2592 10.272 27.4264C9.4128 27.5744 8.5736 27.5016 7.7608 27.2096L9.6744 25.296C9.7152 25.292 9.756 25.2912 9.7968 25.2856C10.2968 25.2192 10.7416 25.0216 11.1368 24.7112C11.2312 24.6368 11.3192 24.5544 11.404 24.4696C11.708 24.1656 12.012 23.8616 12.316 23.5568C13.0496 23.9144 13.8424 24.1688 14.676 24.3C14.0568 24.9224 13.4368 25.544 12.8072 26.156ZM23.1096 17.3384C23.0832 17.4856 23.052 17.6336 23.016 17.7808C22.98 17.928 22.94 18.0736 22.8968 18.2168C22.8616 18.3328 22.7904 18.4272 22.7 18.496C22.6096 18.5392 22.508 18.5616 22.4032 18.5568L21.4584 18.5136C21.2744 18.5056 21.1016 18.5792 20.9736 18.704C20.9096 18.756 20.8536 18.82 20.8104 18.8944C20.7192 19.052 20.6192 19.2032 20.5144 19.3496C20.4056 19.4912 20.292 19.628 20.1704 19.7584C20.1136 19.8192 20.0696 19.8896 20.0384 19.964C19.9528 20.124 19.9312 20.3136 19.9936 20.4888L20.3104 21.38C20.3464 21.4808 20.3536 21.5864 20.3376 21.6872C20.2976 21.7912 20.2288 21.8848 20.1304 21.9512C19.8808 22.12 19.62 22.2736 19.3512 22.4112C19.1152 22.532 18.8296 22.472 18.652 22.2792L18.0184 21.5928C17.8552 21.416 17.6016 21.3544 17.3656 21.4144C16.9928 21.5088 16.6072 21.5672 16.216 21.5856C15.9728 21.5968 15.748 21.7304 15.6432 21.9464L15.2376 22.7864C15.124 23.0224 14.868 23.1632 14.6064 23.116C14.4592 23.0896 14.3112 23.0584 14.164 23.0224C14.0168 22.9864 13.8712 22.9464 13.728 22.9032C13.68 22.8888 13.6368 22.8664 13.596 22.8408C13.4912 22.7256 13.4336 22.5672 13.4408 22.4024L13.484 21.4576C13.4952 21.2144 13.3624 20.9896 13.1568 20.8648C12.8256 20.664 12.5184 20.4304 12.2392 20.1704C12.1264 20.0656 11.9848 20.0016 11.8384 19.9904C11.7144 19.9568 11.5816 19.9576 11.4568 20.0016L10.5656 20.3184C10.4968 20.3432 10.4248 20.3536 10.3544 20.3536C10.2328 20.3184 10.1232 20.2432 10.0472 20.1304C9.8784 19.8808 9.7248 19.62 9.5872 19.3512C9.4664 19.1152 9.5264 18.8296 9.7192 18.652L10.4056 18.0184C10.5824 17.8552 10.644 17.6016 10.584 17.3656C10.4896 16.9928 10.4312 16.6072 10.4128 16.216C10.4016 15.9728 10.268 15.748 10.052 15.6432L9.212 15.2376C8.976 15.124 8.8352 14.868 8.8824 14.6064C8.9088 14.4592 8.94 14.3112 8.976 14.164C9.012 14.0168 9.052 13.8712 9.0952 13.728C9.1176 13.6544 9.156 13.5896 9.2032 13.5336C9.3032 13.4744 9.4208 13.4432 9.544 13.4488L10.4888 13.492C10.7104 13.5024 10.9168 13.392 11.0456 13.2168C11.0984 13.1696 11.1448 13.1144 11.1816 13.0504C11.2664 12.9032 11.3608 12.7632 11.4576 12.6264C11.5592 12.496 11.664 12.368 11.7768 12.2472C11.8256 12.1944 11.8648 12.1352 11.8944 12.072C12.0264 11.8968 12.0728 11.6664 11.9976 11.456L11.6808 10.5648C11.6392 10.448 11.6352 10.3248 11.6632 10.2112C11.7024 10.1512 11.7528 10.0976 11.816 10.0544C12.0656 9.8856 12.3264 9.732 12.5952 9.5944C12.8312 9.4736 13.1168 9.5336 13.2944 9.7264L13.928 10.4128C14.0912 10.5896 14.3448 10.6512 14.5808 10.5912C14.9536 10.4968 15.3392 10.4384 15.7304 10.42C15.9736 10.4088 16.1984 10.2752 16.3032 10.0592L16.7088 9.2192C16.8224 8.9832 17.0784 8.8424 17.34 8.8896C17.4872 8.916 17.6352 8.9472 17.7824 8.9832C17.9296 9.0192 18.0752 9.0592 18.2184 9.1024C18.3504 9.1432 18.4552 9.228 18.5248 9.3368C18.544 9.4024 18.5536 9.472 18.5504 9.5424L18.5072 10.4872C18.5008 10.6192 18.5384 10.7448 18.6056 10.8536C18.6592 10.9912 18.7616 11.1104 18.896 11.188C19.2328 11.3832 19.544 11.6104 19.8272 11.864C20.0064 12.0248 20.2608 12.0856 20.4896 12.004L21.3808 11.6872C21.5384 11.6312 21.708 11.6416 21.8496 11.7096C21.8848 11.74 21.9168 11.7744 21.944 11.8144C22.1128 12.064 22.2664 12.3248 22.404 12.5936C22.5248 12.8296 22.4648 13.1152 22.272 13.2928L21.5856 13.9264C21.4088 14.0896 21.3472 14.3432 21.4072 14.5792C21.5016 14.952 21.56 15.3376 21.5784 15.7288C21.5896 15.972 21.7232 16.1968 21.9392 16.3016L22.7792 16.7072C23.0152 16.8208 23.156 17.0768 23.1088 17.3384H23.1096ZM27.4624 10.028C27.3464 10.96 26.996 11.7936 26.4072 12.5248C26.3008 12.6568 26.1872 12.7832 26.068 12.9032C25.4792 13.4936 24.8904 14.0848 24.3 14.6736C24.1688 13.8408 23.9152 13.048 23.5576 12.316C23.868 12.0064 24.1784 11.696 24.4888 11.3856C24.8832 10.9904 25.1496 10.5208 25.2552 9.9712C25.2744 9.8704 25.288 9.7712 25.2976 9.672L27.2096 7.76C27.4776 8.488 27.5592 9.2472 27.4624 10.0272V10.028Z" fill="white"/><path d="M18.8921 13.1658C18.6385 13.1658 18.4193 13.2562 18.2409 13.4354L15.0513 16.625L13.7745 15.349C13.5969 15.1858 13.3857 15.1034 13.1473 15.1034C12.9089 15.1034 12.6913 15.1922 12.4961 15.373C12.3057 15.5626 12.2097 15.7818 12.2097 16.0242C12.2097 16.2666 12.3105 16.4834 12.5025 16.649L14.4169 18.5794C14.5961 18.7586 14.8097 18.849 15.0521 18.849C15.2945 18.849 15.5081 18.7586 15.6881 18.5786L19.5369 14.6962C19.7201 14.5282 19.8137 14.309 19.8057 14.0626C19.7985 13.8234 19.7049 13.6122 19.5353 13.4426C19.3689 13.261 19.1473 13.165 18.8929 13.165L18.8921 13.1658Z" fill="white"/></g><defs><clipPath id="clip0_1215_174351"><rect width="32" height="32" fill="white"/></clipPath></defs></svg>'; // phpcs:ignore Generic.Files.LineLength.MaxExceeded
	}
}Traits/Helpers/DateTime.php000064400000001057151536237000011626 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Traits\Helpers;

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

/**
 * Contains date/time specific helper methods.
 *
 * @since 1.0.0
 */
trait DateTime {
	/**
	 * Returns a MySQL formatted date.
	 *
	 * @since 1.0.0
	 *
	 * @param  int|string   $time Any format accepted by strtotime.
	 * @return false|string       The MySQL formatted string.
	 */
	public function timeToMysql( $time ) {
		$time = is_string( $time ) ? strtotime( $time ) : $time;

		return date( 'Y-m-d H:i:s', $time );
	}
}Traits/Helpers/Strings.php000064400000017636151536237000011575 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Traits\Helpers;

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

/**
 * Contains string specific helper methods.
 *
 * @since 1.0.0
 */
trait Strings {
	/**
	 * Escapes special regex characters.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $string    The string.
	 * @param  string $delimiter The delimiter character.
	 * @return string            The escaped string.
	 */
	public function escapeRegex( $string, $delimiter = '/' ) {
		static $escapeRegex = [];
		if ( isset( $escapeRegex[ $string ] ) ) {
			return $escapeRegex[ $string ];
		}
		$escapeRegex[ $string ] = preg_quote( (string) $string, $delimiter );

		return $escapeRegex[ $string ];
	}

	/**
	 * Escapes special regex characters inside the replacement string.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $string The string.
	 * @return string         The escaped string.
	 */
	public function escapeRegexReplacement( $string ) {
		static $escapeRegexReplacement = [];
		if ( isset( $escapeRegexReplacement[ $string ] ) ) {
			return $escapeRegexReplacement[ $string ];
		}

		$escapeRegexReplacement[ $string ] = str_replace( '$', '\$', $string );

		return $escapeRegexReplacement[ $string ];
	}

	/**
	 * preg_replace but with the replacement escaped.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $pattern     The pattern to search for.
	 * @param  string $replacement The replacement string.
	 * @param  string $subject     The subject to search in.
	 * @return string              The subject with matches replaced.
	 */
	public function pregReplace( $pattern, $replacement, $subject ) {
		$key = $pattern . $replacement . $subject;

		static $pregReplace = [];
		if ( isset( $pregReplace[ $key ] ) ) {
			return $pregReplace[ $key ];
		}

		// We can use the following pattern for this - (?<!\\)([\/.^$*+?|()[{}\]]{1})
		// The pattern above will only escape special characters if they're not escaped yet, which makes it compatible with all our patterns that are already escaped.
		// The caveat is that we'd need to first trim off slash delimiters and add them back later - otherwise they'd be escaped as well.

		$replacement         = $this->escapeRegexReplacement( $replacement );
		$pregReplace[ $key ] = preg_replace( $pattern, $replacement, (string) $subject );

		return $pregReplace[ $key ];
	}

	/**
	 * Returns the index of a substring in a string.
	 *
	 * @since 1.0.0
	 *
	 * @param  string   $stack  The stack.
	 * @param  string   $needle The needle.
	 * @param  int      $offset The offset.
	 * @return int|bool         The index where the string starts or false if it does not exist.
	 */
	public function stringIndex( $stack, $needle, $offset = 0 ) {
		$key = $stack . $needle . $offset;

		static $stringIndex = [];
		if ( isset( $stringIndex[ $key ] ) ) {
			return $stringIndex[ $key ];
		}

		$stringIndex[ $key ] = function_exists( 'mb_strpos' ) ? mb_strpos( $stack, $needle, $offset, get_option( 'blog_charset' ) ) : strpos( $stack, $needle, $offset );

		return $stringIndex[ $key ];
	}

	/**
	 * Checks if the given string contains the given substring.
	 *
	 * @since 1.0.0
	 *
	 * @param  string   $stack  The stack.
	 * @param  string   $needle The needle.
	 * @param  int      $offset The offset.
	 * @return bool             Whether the substring occurs in the main string.
	 */
	public function stringContains( $stack, $needle, $offset = 0 ) {
		$key = $stack . $needle . $offset;

		static $stringContains = [];
		if ( isset( $stringContains[ $key ] ) ) {
			return $stringContains[ $key ];
		}

		$stringContains[ $key ] = false !== $this->stringIndex( $stack, $needle, $offset );

		return $stringContains[ $key ];
	}

	/**
	 * Check if a string is JSON encoded or not.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $string The string to check.
	 * @return bool           True if it is JSON or false if not.
	 */
	public function isJsonString( $string ) {
		if ( ! is_string( $string ) ) {
			return false;
		}

		json_decode( $string );

		// Return a boolean whether or not the last error matches.
		return json_last_error() === JSON_ERROR_NONE;
	}

	/**
	 * Returns the string after all HTML entities have been decoded.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $string The string to decode.
	 * @return string         The decoded string.
	 */
	public function decodeHtmlEntities( $string ) {
		static $decodeHtmlEntities = [];
		if ( isset( $decodeHtmlEntities[ $string ] ) ) {
			return $decodeHtmlEntities[ $string ];
		}

		// We must manually decode non-breaking spaces since html_entity_decode doesn't do this.
		$string                        = $this->pregReplace( '/&nbsp;/', ' ', $string );
		$decodeHtmlEntities[ $string ] = html_entity_decode( (string) $string, ENT_QUOTES );

		return $decodeHtmlEntities[ $string ];
	}

	/**
	 * Returns the string with script tags stripped.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $string The string.
	 * @return string         The modified string.
	 */
	public function stripScriptTags( $string ) {
		static $stripScriptTags = [];
		if ( isset( $stripScriptTags[ $string ] ) ) {
			return $stripScriptTags[ $string ];
		}

		$stripScriptTags[ $string ] = $this->pregReplace( '/<script(.*?)>(.*?)<\/script>/is', '', $string );

		return $stripScriptTags[ $string ];
	}

	/**
	 * Returns the string with incomplete HTML tags stripped.
	 * Incomplete tags are not unopened/unclosed pairs but rather single tags that aren't properly formed.
	 * e.g. <a href='something'
	 * e.g. href='something' >
	 *
	 * @since 1.0.0
	 *
	 * @param  string $string The string.
	 * @return string         The modified string.
	 */
	public function stripIncompleteHtmlTags( $string ) {
		static $stripIncompleteHtmlTags = [];
		if ( isset( $stripIncompleteHtmlTags[ $string ] ) ) {
			return $stripIncompleteHtmlTags[ $string ];
		}

		$stripIncompleteHtmlTags[ $string ] = $this->pregReplace( '/(^(?!<).*?(\/>)|<[^>]*?(?!\/>)$)/is', '', $string );

		return $stripIncompleteHtmlTags[ $string ];
	}

	/**
	 * Trims HTML paragraph from the start/end of the given string.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $string The string.
	 * @return string         The modified string.
	 */
	public function trimParagraphTags( $string ) {
		$string = preg_replace( '/^<p[^>]*>/', '', (string) $string );
		$string = preg_replace( '/<\/p>/', '', (string) $string );

		return trim( $string );
	}

	/**
	 * Implodes an array into a WHEREIN clause useable string.
	 *
	 * @since 1.0.0
	 *
	 * @param  array  $array       The array.
	 * @param  bool   $outerQuotes Whether outer quotes should be added.
	 * @return string              The imploded array.
	 */
	public function implodeWhereIn( $array, $outerQuotes = false ) {
		// Reset the keys first in case there is no 0 index.
		$array = array_values( $array );

		if ( ! isset( $array[0] ) ) {
			return '';
		}

		if ( is_numeric( $array[0] ) ) {
			return implode( ', ', $array );
		}

		return $outerQuotes ? "'" . implode( "', '", $array ) . "'" : implode( "', '", $array );
	}

	/**
	 * Returns string after converting it to lowercase.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $string The original string.
	 * @return string         The string converted to lowercase.
	 */
	public function toLowerCase( $string ) {
		static $lowerCased = [];
		if ( isset( $lowerCased[ $string ] ) ) {
			return $lowerCased[ $string ];
		}
		$lowerCased[ $string ] = function_exists( 'mb_strtolower' ) ? mb_strtolower( $string, $this->getCharset() ) : strtolower( $string );

		return $lowerCased[ $string ];
	}

	/**
	 * Convert to camelCase.
	 *
	 * @since 1.1.0
	 *
	 * @param  string $string     The string to convert.
	 * @param  bool   $capitalize Whether to capitalize the first letter.
	 * @return string             The converted string.
	 */
	public function toCamelCase( $string, $capitalize = false ) {
		$string[0] = strtolower( $string[0] );
		if ( $capitalize ) {
			$string[0] = strtoupper( $string[0] );
		}

		return preg_replace_callback( '/_([a-z0-9])/', function ( $value ) {
			return strtoupper( $value[1] );
		}, $string );
	}
}Traits/Helpers/ThirdParty.php000064400000002243151536237000012222 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Traits\Helpers;

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

/**
 * Contains helper methods for other plugins/themes.
 *
 * @since 1.0.0
 */
trait ThirdParty {
	/**
	 * Checks whether WooCommerce is active.
	 *
	 * @since 1.0.0
	 *
	 * @return boolean Whether WooCommerce is active.
	 */
	public function isWooCommerceActive() {
		return class_exists( 'woocommerce' );
	}

	/**
	 * Checks whether the queried object is the WooCommerce shop page.
	 *
	 * @since 1.0.0
	 *
	 * @param  int  $id The post ID to check against (optional).
	 * @return bool     Whether the current page is the WooCommerce shop page.
	 */
	public function isWooCommerceShopPage( $id = 0 ) {
		if ( ! $this->isWooCommerceActive() ) {
			return false;
		}

		if ( ! is_admin() && ! aioseoBrokenLinkChecker()->helpers->isAjaxCronRestRequest() && function_exists( 'is_shop' ) ) {
			return is_shop();
		}

		$id = ! $id && ! empty( $_GET['post'] ) ? (int) wp_unslash( $_GET['post'] ) : (int) $id; // phpcs:ignore HM.Security.ValidatedSanitizedInput, HM.Security.NonceVerification.Recommended

		return $id && wc_get_page_id( 'shop' ) === $id;
	}
}Traits/Helpers/Vue.php000064400000022661151536237000010675 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Traits\Helpers;

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

use AIOSEO\BrokenLinkChecker\Models;

/**
 * Generates the data we need for Vue.
 *
 * @since 1.0.0
 */
trait Vue {
	/**
	 * The data to pass to Vue.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private $vueData = [];

	/**
	 * Returns the data for Vue.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $currentPage The current page.
	 * @return array               The data.
	 */
	public function getVueData( $currentPage = null ) {
		global $wp_version; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		static $showNotificationsDrawer = null;
		if ( null === $showNotificationsDrawer ) {
			$showNotificationsDrawer = aioseoBrokenLinkChecker()->core->cache->get( 'show_notifications_drawer' ) ? true : false;

			// IF this is set to true, let's disable it now so it doesn't pop up again.
			if ( $showNotificationsDrawer ) {
				aioseoBrokenLinkChecker()->core->cache->delete( 'show_notifications_drawer' );
			}
		}

		$this->vueData = [
			// The following data is needed on all screens.
			'wpVersion'           => $wp_version, // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			'page'                => $currentPage,
			'screen'              => aioseoBrokenLinkChecker()->helpers->getCurrentScreen(),
			'internalOptions'     => aioseoBrokenLinkChecker()->internalOptions->all(),
			'options'             => aioseoBrokenLinkChecker()->options->all(),
			'settings'            => aioseoBrokenLinkChecker()->vueSettings->all(),
			'notifications'       => array_merge( Models\Notification::getNotifications( false ), [ 'force' => $showNotificationsDrawer ] ),
			'helpPanel'           => [],
			'urls'                => [
				'domain'        => $this->getSiteDomain(),
				'mainSiteUrl'   => $this->getSiteUrl(),
				'home'          => home_url(),
				'restUrl'       => rest_url(),
				'editScreen'    => admin_url( 'edit.php' ),
				'publicPath'    => aioseoBrokenLinkChecker()->core->assets->normalizeAssetsHost( plugin_dir_url( AIOSEO_BROKEN_LINK_CHECKER_FILE ) ),
				'assetsPath'    => aioseoBrokenLinkChecker()->core->assets->getAssetsPath(),
				'marketingSite' => $this->getMarketingSiteUrl(),
				'connect'       => admin_url( 'index.php?page=broken-link-checker-connect' )
			],
			'user'                => [
				'capabilities'   => aioseoBrokenLinkChecker()->access->getAllCapabilities(),
				'data'           => wp_get_current_user(),
				'locale'         => function_exists( 'get_user_locale' ) ? get_user_locale() : get_locale(),
				'unfilteredHtml' => current_user_can( 'unfiltered_html' )
			],
			'isDev'               => $this->isDev(),
			'isSsl'               => is_ssl(),
			'isMultisite'         => is_multisite(),
			'isNetworkAdmin'      => is_network_admin(),
			'mainSite'            => is_main_site(),
			'hasUrlTrailingSlash' => '/' === user_trailingslashit( '' ),
			'nonce'               => wp_create_nonce( 'wp_rest' ),
			'translations'        => $this->getJedLocaleData( 'aioseo-broken-link-checker' )
		];

		switch ( $currentPage ) {
			case 'about':
				$this->addAboutData();
				break;
			case 'highlighter':
				$this->addHighlighterData();
				break;
			case 'links':
				$this->addBrokenLinksReportData();
				break;
			case 'seo-settings':
				$this->addSeoSettingsData();
				break;
			case 'setup-wizard':
				$this->addSetupWizardSettingsData();
				break;
			default:
				break;
		}

		return $this->vueData;
	}

	/**
	 * Adds the data for the About Us screen.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	private function addAboutData() {
		$this->vueData['plugins'] = $this->getPluginData();
	}

	/**
	 * Adds the data for the Highlighter screen.
	 *
	 * @since 1.2.0
	 *
	 * @return void
	 */
	private function addHighlighterData() {
		if ( is_admin() || ! is_singular() ) {
			return;
		}

		$this->vueData['brokenLinks'] = Models\LinkStatus::getBrokenByPostId( get_the_ID() );
	}

	/**
	 * Adds the data for the SEO Settings screen.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	private function addSeoSettingsData() {
		$this->vueData['plugins'] = $this->getPluginData();
	}

	/**
	 * Adds the data for the Setup Wizard screen.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	private function addSetupWizardSettingsData() {}

	/**
	 * Adds the data for the Broken Links Report screen.
	 *
	 * @since   1.0.0
	 * @version 1.1.0 Renamed to make it more specific.
	 *
	 * @return void
	 */
	private function addBrokenLinksReportData() {
		$limit = aioseoBrokenLinkChecker()->vueSettings->tablePagination['brokenLinks'];

		$this->vueData += [
			'linkStatuses' => $this->getLinkStatusesData( $limit ),
			'plugins'      => [
				'isAioseoActive'          => function_exists( 'aioseo' ),
				'isAioseoRedirectsActive' => function_exists( 'aioseoRedirects' )
			],
			'postTypes'    => $this->getPublicPostTypes( false, false, true ),
			'postStatuses' => $this->getPublicPostStatuses(),
			'scans'        => [
				'percentages' => [
					'links'        => aioseoBrokenLinkChecker()->main->links->data->getScanPercentage(),
					'linkStatuses' => aioseoBrokenLinkChecker()->main->linkStatus->data->getScanPercentage()
				]
			]
		];
	}

	/**
	 * Returns the Broken Links Report data.
	 *
	 * @since 1.0.0
	 *
	 * @param  int    $limit      The limit.
	 * @param  int    $offset     The offset.
	 * @param  string $searchTerm The search term.
	 * @param  string $filter     The active filter.
	 * @param  string $orderBy    The order by.
	 * @param  string $orderDir   The order direction.
	 * @return array              The data.
	 */
	public function getLinkStatusesData( $limit = 20, $offset = 0, $searchTerm = '', $filter = 'all', $orderBy = '', $orderDir = 'DESC' ) {
		$whereClause = Models\Link::getLinkWhereClause( $searchTerm );

		$rows      = [];
		$totalRows = [];
		switch ( $filter ) {
			case 'broken':
				$rows      = Models\LinkStatus::rowQuery( 'broken', $limit, $offset, $whereClause, $orderBy, $orderDir );
				$totalRows = Models\LinkStatus::rowCountQuery( 'broken', $whereClause );
				break;
			case 'redirects':
				$rows      = Models\LinkStatus::rowQuery( 'redirects', $limit, $offset, $whereClause, $orderBy, $orderDir );
				$totalRows = Models\LinkStatus::rowCountQuery( 'redirects', $whereClause );
				break;
			case 'dismissed':
				$rows      = Models\LinkStatus::rowQuery( 'dismissed', $limit, $offset, $whereClause, $orderBy, $orderDir );
				$totalRows = Models\LinkStatus::rowCountQuery( 'dismissed', $whereClause );
				break;
			case 'not-checked':
				$rows      = Models\LinkStatus::rowQuery( 'not-checked', $limit, $offset, $whereClause, $orderBy, $orderDir );
				$totalRows = Models\LinkStatus::rowCountQuery( 'not-checked', $whereClause );
				break;
			case 'all':
				$rows      = Models\LinkStatus::rowQuery( 'all', $limit, $offset, $whereClause, $orderBy, $orderDir );
				$totalRows = Models\LinkStatus::rowCountQuery( 'all', $whereClause );
				break;
			default:
				break;
		}

		$page = 0 === $offset ? 1 : ( $offset / $limit ) + 1;

		return [
			'rows'    => $rows,
			'totals'  => [
				'page'  => $page,
				'pages' => ceil( $totalRows / $limit ),
				'total' => $totalRows
			],
			'filters' => [
				[
					'slug'   => 'all',
					'name'   => __( 'All', 'aioseo-broken-link-checker' ),
					'count'  => Models\LinkStatus::rowCountQuery( 'all', $whereClause ),
					'active' => ( ! $filter || 'all' === $filter ) && ! $searchTerm ? true : false
				],
				[
					'slug'   => 'broken',
					'name'   => __( 'Broken', 'aioseo-broken-link-checker' ),
					'count'  => Models\LinkStatus::rowCountQuery( 'broken', $whereClause ),
					'active' => 'broken' === $filter ? true : false
				],
				[
					'slug'   => 'redirects',
					'name'   => __( 'Redirects', 'aioseo-broken-link-checker' ),
					'count'  => Models\LinkStatus::rowCountQuery( 'redirects', $whereClause ),
					'active' => 'redirects' === $filter ? true : false
				],
				[
					'slug'   => 'not-checked',
					'name'   => __( 'Not Checked Yet', 'aioseo-broken-link-checker' ),
					'count'  => Models\LinkStatus::rowCountQuery( 'not-checked', $whereClause ),
					'active' => 'not-checked' === $filter ? true : false
				],
				[
					'slug'   => 'dismissed',
					'name'   => __( 'Dismissed', 'aioseo-broken-link-checker' ),
					'count'  => Models\LinkStatus::rowCountQuery( 'dismissed', $whereClause ),
					'active' => 'dismissed' === $filter ? true : false
				]
			]
		];
	}

	/**
	 * Returns Jed-formatted localization data.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $domain The text domain.
	 * @return array          The information of the locale.
	 */
	private function getJedLocaleData( $domain ) {
		$translations = get_translations_for_domain( $domain );

		$locale = [
			'' => [
				'domain' => $domain,
				'lang'   => is_admin() && function_exists( 'get_user_locale' ) ? get_user_locale() : get_locale(),
			],
		];

		if ( ! empty( $translations->headers['Plural-Forms'] ) ) {
			$locale['']['plural_forms'] = $translations->headers['Plural-Forms'];
		}

		foreach ( $translations->entries as $msgid => $entry ) {
			if ( empty( $entry->translations ) || ! is_array( $entry->translations ) ) {
				continue;
			}

			$locale[ $msgid ] = $entry->translations;
		}

		return $locale;
	}

	/**
	 * Returns the marketing site URL.
	 *
	 * @since 1.0.0
	 *
	 * @return string The marketing site URL.
	 */
	private function getMarketingSiteUrl() {
		if ( defined( 'AIOSEO_MARKETING_SITE_URL' ) && AIOSEO_MARKETING_SITE_URL ) {
			return AIOSEO_MARKETING_SITE_URL;
		}

		return 'https://aioseo.com/';
	}
}Traits/Helpers/Wp.php000064400000032027151536237000010521 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Traits\Helpers;

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

use AIOSEO\BrokenLinkChecker\Utils;

/**
 * Contains all WP related helper methods.
 *
 * @since 1.0.0
 */
trait Wp {
	/**
	 * Returns all registered post statuses.
	 *
	 * @since 1.0.0
	 *
	 * @param  boolean $statusesOnly Whether or not to only return statuses.
	 * @return array                 List of post statuses.
	 */
	public function getPublicPostStatuses( $statusesOnly = false ) {
		$allStatuses = get_post_stati( [ 'show_in_admin_all_list' => true ], 'objects' );

		$postStatuses = [];
		foreach ( $allStatuses as $status => $data ) {
			if (
				! $data->public &&
				! $data->protected &&
				! $data->private
			) {
				continue;
			}

			if ( $statusesOnly ) {
				$postStatuses[] = $status;
				continue;
			}

			$postStatuses[] = [
				'label'  => $data->label,
				'status' => $status
			];
		}

		return $postStatuses;
	}

	/**
	 * Returns a list of public post types with slugs/icons.
	 *
	 * @since 1.0.0
	 *
	 * @param  boolean $namesOnly       Whether only the names should be returned.
	 * @param  boolean $hasArchivesOnly Whether or not to only include post types which have archives.
	 * @param  boolean $rewriteType     Whether or not to rewrite the type slugs.
	 * @return array                    List of public post types.
	 */
	public function getPublicPostTypes( $namesOnly = false, $hasArchivesOnly = false, $rewriteType = false ) {
		$postTypes       = [];
		$postTypeObjects = get_post_types( [ 'public' => true ], 'objects' );
		$woocommerce     = class_exists( 'woocommerce' );
		foreach ( $postTypeObjects as $postTypeObject ) {
			if ( empty( $postTypeObject->label ) ) {
				continue;
			}

			// We don't want to include archives for the WooCommerce shop page.
			if (
				$hasArchivesOnly &&
				(
					! $postTypeObject->has_archive ||
					( 'product' === $postTypeObject->name && $woocommerce )
				)
			) {
				continue;
			}

			if ( $namesOnly ) {
				$postTypes[] = $postTypeObject->name;
				continue;
			}

			if ( 'attachment' === $postTypeObject->name ) {
				// We have to check if the 'init' action has been fired to avoid a PHP notice
				// in WP 6.7+ due to loading translations too early.
				if ( did_action( 'init' ) ) {
					$postTypeObject->label = __( 'Attachments', 'aioseo-broken-link-checker' );
				}
			}

			if ( 'product' === $postTypeObject->name && $woocommerce ) {
				$postTypeObject->menu_icon = 'dashicons-products';
			}

			$name = $postTypeObject->name;
			if ( 'type' === $postTypeObject->name && $rewriteType ) {
				$name = '_aioseo_type';
			}

			$postTypes[] = [
				'name'         => $name,
				'label'        => ucwords( $postTypeObject->label ),
				'singular'     => ucwords( $postTypeObject->labels->singular_name ),
				'icon'         => $postTypeObject->menu_icon,
				'hasExcerpt'   => post_type_supports( $postTypeObject->name, 'excerpt' ),
				'hasArchive'   => $postTypeObject->has_archive,
				'hierarchical' => $postTypeObject->hierarchical,
				'taxonomies'   => get_object_taxonomies( $name ),
				'slug'         => isset( $postTypeObject->rewrite['slug'] ) ? $postTypeObject->rewrite['slug'] : $name
			];
		}

		return apply_filters( 'aioseo_blc_public_post_types', $postTypes, $namesOnly, $hasArchivesOnly );
	}

	/**
	 * Checks if the current user can edit posts of the given post type.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $postType The name of the post type.
	 * @return bool             Whether the user can edit posts of the given post type.
	 */
	public function canEditPostType( $postType ) {
		$capabilities = $this->getPostTypeCapabilities( $postType );

		return current_user_can( $capabilities['edit_posts'] );
	}

	/**
	 * Returns a list of capabilities for the given post type.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $postType The name of the post type.
	 * @return array            The capabilities.
	 */
	public function getPostTypeCapabilities( $postType ) {
		static $capabilities = [];
		if ( isset( $capabilities[ $postType ] ) ) {
			return $capabilities[ $postType ];
		}

		$postTypeObject = get_post_type_object( $postType );
		if ( ! is_a( $postTypeObject, 'WP_Post_Type' ) ) {
			$capabilities[ $postType ] = [];

			return $capabilities[ $postType ];
		}

		if ( ! is_array( $postTypeObject->capability_type ) ) {
			$postTypeObject->capability_type = [
				$postTypeObject->capability_type,
				$postTypeObject->capability_type . 's'
			];
		}

		// Singular base for meta capabilities, plural base for primitive capabilities.
		list( $singularBase, $pluralBase ) = $postTypeObject->capability_type;

		$capabilities[ $postType ] = [
			'edit_post'          => 'edit_' . $singularBase,
			'read_post'          => 'read_' . $singularBase,
			'delete_post'        => 'delete_' . $singularBase,
			'edit_posts'         => 'edit_' . $pluralBase,
			'edit_others_posts'  => 'edit_others_' . $pluralBase,
			'delete_posts'       => 'delete_' . $pluralBase,
			'publish_posts'      => 'publish_' . $pluralBase,
			'read_private_posts' => 'read_private_' . $pluralBase,
		];

		return $capabilities[ $postType ];
	}

	/**
	 * Returns the current post object.
	 *
	 * @since 1.0.0
	 *
	 * @param  int|null      $postId The post ID.
	 * @return \WP_Post|null         The post object.
	 */
	public function getPost( $postId = null ) {
		$postId = is_a( $postId, 'WP_Post' ) ? $postId->ID : $postId;

		if ( $this->isWooCommerceShopPage( $postId ) ) {
			return get_post( wc_get_page_id( 'shop' ) );
		}

		if ( is_front_page() || is_home() ) {
			$showOnFront = 'page' === get_option( 'show_on_front' );
			if ( $showOnFront ) {
				if ( is_front_page() ) {
					$pageOnFront = (int) get_option( 'page_on_front' );

					return get_post( $pageOnFront );
				} elseif ( is_home() ) {
					$pageForPosts = (int) get_option( 'page_for_posts' );

					return get_post( $pageForPosts );
				}
			}
		}

		// We need to check these conditions and cannot always return get_post() because we'll return the first post on archive pages (dynamic homepage, term pages, etc.).
		// https://github.com/awesomemotive/aioseo/issues/2419
		if (
			$this->isScreenBase( 'post' ) ||
			$postId ||
			is_singular()
		) {
			return get_post( $postId );
		}

		return null;
	}

	/**
	 * Returns true if the request is a non-legacy REST API request.
	 * This function was copied from WooCommerce and improved.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if this is a REST API request.
	 */
	public function isRestApiRequest() {
		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return true;
		}

		global $wp_rewrite; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		if ( empty( $wp_rewrite ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			return false;
		}

		if ( empty( $_SERVER['REQUEST_URI'] ) ) {
			return false;
		}

		$restUrl = wp_parse_url( get_rest_url() );
		$restUrl = $restUrl['path'] . ( ! empty( $restUrl['query'] ) ? '?' . $restUrl['query'] : '' );

		$isRestApiRequest = ( 0 === strpos( $_SERVER['REQUEST_URI'], $restUrl ) );

		return apply_filters( 'aioseo_is_rest_api_request', $isRestApiRequest );
	}

	/**
	 * Checks whether the current request is an AJAX, CRON or REST request.
	 *
	 * @since 1.0.0
	 *
	 * @return bool Wether the request is an AJAX, CRON or REST request.
	 */
	public function isAjaxCronRestRequest() {
		return wp_doing_ajax() || wp_doing_cron() || $this->isRestApiRequest();
	}

	/**
	 * Check if the post passed in is a valid post, not a revision or autosave.
	 *
	 * @since 1.0.0
	 *
	 * @param  \WP_Post $post                The Post object to check.
	 * @param  array    $allowedPostStatuses Allowed post statuses.
	 * @return bool                          True if valid, false if not.
	 */
	public function isValidPost( $post, $allowedPostStatuses = [ 'publish' ] ) {
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return false;
		}

		if ( ! is_object( $post ) ) {
			$post = get_post( $post );
		}

		// In order to prevent recursion, we are skipping scheduled-action posts.
		if (
			! is_object( $post ) ||
			'scheduled-action' === $post->post_type ||
			'revision' === $post->post_type ||
			! in_array( $post->post_status, $allowedPostStatuses, true )
		) {
			return false;
		}

		return true;
	}

	/**
	 * Returns a list of plugins with the active status.
	 *
	 * @since 1.0.0
	 *
	 * @return array List of plugins with active status.
	 */
	public function getPluginData() {
		static $pluginData = [];
		if ( ! empty( $pluginData ) ) {
			return $pluginData;
		}

		$pluginUpgrader   = new Utils\PluginUpgraderSilentAjax();
		$installedPlugins = array_keys( get_plugins() );

		foreach ( $pluginUpgrader->pluginSlugs as $key => $slug ) {
			$pluginData[ $key ] = [
				'basename'    => $slug,
				'installed'   => in_array( $slug, $installedPlugins, true ),
				'activated'   => is_plugin_active( $slug ),
				'adminUrl'    => admin_url( $pluginUpgrader->pluginAdminUrls[ $key ] ),
				'canInstall'  => $this->canInstall(),
				'canActivate' => $this->canActivate(),
				'canUpdate'   => $this->canUpdate(),
				'wpLink'      => ! empty( $pluginUpgrader->wpPluginLinks[ $key ] ) ? $pluginUpgrader->wpPluginLinks[ $key ] : null
			];
		}

		return $pluginData;
	}

	/**
	 * Installs and activates a given addon or plugin.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $name    The addon name/SKU.
	 * @param  bool   $network Whether or not we are in a network environment.
	 * @return bool            Whether or not the installation was succesful.
	 */
	public function installAddon( $name, $network = false ) {
		if ( ! $this->canInstall() ) {
			return false;
		}

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

		// Set the current screen to avoid undefined notices.
		set_current_screen( 'toplevel_page_broken-link-checker' );

		// Prepare variables.
		$url = esc_url_raw(
			add_query_arg(
				[
					'page' => 'broken-link-checker-links'
				],
				admin_url( 'admin.php' )
			)
		);

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

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

		// Activate the plugin silently.
		$pluginUrl = ! empty( $installer->pluginSlugs[ $name ] ) ? $installer->pluginSlugs[ $name ] : $name;
		$activated = activate_plugin( $pluginUrl, '', $network );

		if ( ! is_wp_error( $activated ) ) {
			return $name;
		}

		// Using output buffering to prevent the FTP form from being displayed in the screen.
		ob_start();
		$creds = request_filesystem_credentials( $url, '', false, false, null );
		ob_end_clean();

		// Check for file system permissions.
		$fs = aioseoBrokenLinkChecker()->core->fs->noConflict();
		$fs->init( $creds );
		if ( false === $creds || ! $fs->isWpfsValid() ) {
			return false;
		}

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

		$installLink = ! empty( $installer->pluginLinks[ $name ] ) ? $installer->pluginLinks[ $name ] : null;
		// Check if this is an addon and if we have a download link.
		if ( empty( $installLink ) ) {
			return false;
		}

		$installer->install( $installLink );

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

		$pluginBasename = $installer->plugin_info();
		if ( ! $pluginBasename ) {
			return false;
		}

		// Activate the plugin silently.
		$activated = activate_plugin( $pluginBasename, '', $network );

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

		return $pluginBasename;
	}

	/**
	 * Determine if plugins can be installed.
	 *
	 * @since 1.0.0
	 *
	 * @return bool Whether the plugin can be installed.
	 */
	public function canInstall() {
		if ( ! current_user_can( 'install_plugins' ) ) {
			return false;
		}

		// Determine whether file modifications are allowed.
		if ( ! wp_is_file_mod_allowed( 'aioseo_blc_can_install' ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Determine if plugins can be updated.
	 *
	 * @since 1.0.0
	 *
	 * @return bool  Whether the plugin can be updated.
	 */
	public function canUpdate() {
		if ( ! current_user_can( 'update_plugins' ) ) {
			return false;
		}

		// Determine whether file modifications are allowed.
		if ( ! wp_is_file_mod_allowed( 'aioseo_blc_can_update' ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Determine if plugins can be activated.
	 *
	 * @since 1.0.0
	 *
	 * @return bool Whether the plugin can be activated.
	 */
	public function canActivate() {
		if ( ! current_user_can( 'activate_plugins' ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Returns the charset for the site.
	 *
	 * @since 1.0.0
	 *
	 * @return string The name of the charset.
	 */
	public function getCharset() {
		static $charset = null;
		if ( null !== $charset ) {
			return $charset;
		}

		$charset = get_option( 'blog_charset' );
		$charset = $charset ? $charset : 'UTF-8';

		return $charset;
	}
}Traits/Helpers/WpContext.php000064400000001723151536237000012065 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Traits\Helpers;

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

/**
 * Contains all context related helper methods.
 * This includes methods to check the context of the current request, but also get WP objects.
 *
 * @since 1.0.0
 */
trait WpContext {
	/**
	 * Checks whether we're on the given screen.
	 *
	 * @since 1.0.0
	 *
	 * @param  string  $screenName The screen name.
	 * @return boolean             Whether we're on the given screen.
	 */
	public function isScreenBase( $screenName ) {
		$screen = $this->getCurrentScreen();
		if ( ! $screen || ! isset( $screen->base ) ) {
			return false;
		}

		return $screen->base === $screenName;
	}

	/**
	 * Gets current admin screen
	 *
	 * @since 1.0.0
	 *
	 * @return false|\WP_Screen|null
	 */
	public function getCurrentScreen() {
		if ( ! is_admin() || ! function_exists( 'get_current_screen' ) ) {
			return false;
		}

		return get_current_screen();
	}
}Traits/Helpers/WpMultisite.php000064400000002725151536237000012423 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Traits\Helpers;

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

/**
 * Contains methods related to multisites.
 *
 * @since 1.0.0
 */
trait WpMultisite {
	/**
	 * Returns the current site.
	 *
	 * @since 1.0.0
	 *
	 * @return \WP_Site|Object A WP_Site instance of the current site or an object representing the same.
	 */
	public function getSite() {
		if ( is_multisite() ) {
			return get_site();
		}

		return (object) [
			'domain' => $this->getSiteDomain( true ),
			'path'   => $this->getHomePath( true )
		];
	}

	/**
	 * Returns the network ID.
	 *
	 * @since 1.0.0
	 *
	 * @return int The integer of the blog/site id.
	 */
	public function getNetworkId() {
		if ( is_multisite() ) {
			return get_network()->site_id;
		}

		return get_current_blog_id();
	}

	/**
	 * Wrapper for switch_to_blog especially for non-multisite setups.
	 *
	 * @since 1.0.0
	 *
	 * @param  int  $blogId The blog ID to switch to.
	 * @return bool         True in all cases.
	 */
	public function switchToBlog( $blogId ) {
		if ( ! is_multisite() ) {
			return true;
		}

		return switch_to_blog( $blogId );
	}

	/**
	 * Wrapper for restore_current_blog especially for non-multisite setups.
	 *
	 * @since 1.0.0
	 *
	 * @return bool Whether we're already on the current blog or not in a multisite environment.
	 */
	public function restoreCurrentBlog() {
		if ( ! is_multisite() ) {
			return false;
		}

		return restore_current_blog();
	}
}Traits/Helpers/WpUri.php000064400000003650151536237000011201 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Traits\Helpers;

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

/**
 * Contains all WordPress related URL, URI, path, slug, etc. related helper methods.
 *
 * @since 1.0.0
 */
trait WpUri {
	/**
	 * Returns the site domain.
	 *
	 * @since 1.0.0
	 *
	 * @param  bool   $unfiltered Whether to get the unfiltered value.
	 * @return string             The site's domain.
	 */
	public function getSiteDomain( $unfiltered = false ) {
		return wp_parse_url( $this->getHomeUrl( $unfiltered ), PHP_URL_HOST );
	}

	/**
	 * Returns the site URL.
	 * NOTE: For multisites inside a sub-directory, this returns the URL for the main site.
	 * This is intentional.
	 *
	 * @since 1.0.0
	 *
	 * @param  bool   $unfiltered Whether to get the unfiltered value.
	 * @return string             The site's domain.
	 */
	public function getSiteUrl( $unfiltered = false ) {
		$homeUrl = $this->getHomeUrl( $unfiltered );

		return wp_parse_url( $homeUrl, PHP_URL_SCHEME ) . '://' . wp_parse_url( $homeUrl, PHP_URL_HOST );
	}

	/**
	 * Retrieve the home path.
	 *
	 * @since 1.0.0
	 *
	 * @param  bool   $unfiltered Whether to get the unfiltered value.
	 * @return string             The home path.
	 */
	public function getHomePath( $unfiltered = false ) {
		$path = wp_parse_url( $this->getHomeUrl( $unfiltered ), PHP_URL_PATH );

		return $path ? trailingslashit( $path ) : '/';
	}

	/**
	 * Returns the home URL.
	 *
	 * @since 1.2.3
	 *
	 * @param  bool   $unfiltered Whether to get the unfiltered value.
	 * @return string             The home URL.
	 */
	private function getHomeUrl( $unfiltered = false ) {
		$homeUrl = home_url();
		if ( $unfiltered ) {
			// We want to get this value straight from the DB to prevent plugins like WPML from filtering it.
			// This will otherwise mess with things like license activation requests and redirects.
			$homeUrl = get_option( 'home' );
		}

		return $homeUrl;
	}
}Traits/Options.php000064400000067364151536237000010200 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Traits;

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

/**
 * The Options trait.
 *
 * @since 1.0.0
 */
trait Options {
	/**
	 * Whether or not this instance is a clone.
	 *
	 * @since 1.0.0
	 *
	 * @var bool
	 */
	public $isClone = false;

	/**
	 * Whether or not the options need to be saved to the DB.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public $shouldSave = false;

	/**
	 * The name to lookup the options with.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public $optionsName = '';

	/**
	 * Holds the localized options.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	public $localized = [];

	/**
	 * The group key we are working with.
	 *
	 * @since 1.0.0
	 *
	 * @var string|null
	 */
	protected $groupKey = null;

	/**
	 * Allows us to create unlimited number of sub groups.
	 * Like so: options->breadcrumbs->templates->taxonomies->tags->template
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $subGroups = [];

	/**
	 * Any arguments associated with a dynamic method.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $arguments = [];

	/**
	 * The value to set on an option.
	 *
	 * @since 1.0.0
	 *
	 * @var mixed
	 */
	protected $value = null;

	/**
	 * Holds all the defaults after they have been merged.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $defaultsMerged = [];

	/**
	 * Initialize network options.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function initNetwork() {
		$this->optionsName = $this->optionsName . '_network';
		$this->init();
	}

	/**
	 * Retrieve an option or null if missing.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $name      The name of the property that is missing on the class.
	 * @param  array  $arguments The arguments passed into the method.
	 * @return mixed             The value from the options or default/null.
	 */
	public function __call( $name, $arguments = [] ) {
		if ( $this->setGroupKey( $name, $arguments ) ) {
			return $this;
		}

		// If we need to set a sub-group, do that now.
		$cachedOptions = aioseoBrokenLinkChecker()->core->optionsCache->getOptions( $this->optionsName );
		$defaults      = $cachedOptions[ $this->groupKey ];
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				$defaults = $defaults[ $subGroup ];
			}
		}

		if ( ! isset( $defaults[ $name ] ) ) {
			$this->resetGroups();

			return ! empty( $this->arguments[0] )
				? $this->arguments[0]
				: $this->getDefault( $name, false );
		}

		if ( empty( $defaults[ $name ]['type'] ) ) {
			return $this->setSubGroup( $name );
		}

		$value = isset( $cachedOptions[ $this->groupKey ][ $name ]['value'] )
			? $cachedOptions[ $this->groupKey ][ $name ]['value']
			: (
				! empty( $this->arguments[0] )
					? $this->arguments[0]
					: $this->getDefault( $name, false )
			);

		$this->resetGroups();

		return $value;
	}

	/**
	 * Retrieve an option or null if missing.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $name The name of the property that is missing on the class.
	 * @return mixed        The value from the options or default/null.
	 */
	public function __get( $name ) {
		if ( 'type' === $name ) {
			$name = '_aioseo_type';
		}

		if ( $this->setGroupKey( $name ) ) {
			return $this;
		}

		// If we need to set a sub-group, do that now.
		$cachedOptions = aioseoBrokenLinkChecker()->core->optionsCache->getOptions( $this->optionsName );
		$defaults      = $cachedOptions[ $this->groupKey ];
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				$defaults = $defaults[ $subGroup ];
			}
		}

		if ( ! isset( $defaults[ $name ] ) ) {
			$default = $this->getDefault( $name, false );
			$this->resetGroups();

			return $default;
		}

		if ( ! isset( $defaults[ $name ]['type'] ) ) {
			return $this->setSubGroup( $name );
		}

		$value = $this->getDefault( $name, false );

		if ( isset( $defaults[ $name ]['value'] ) ) {
			$preserveHtml = ! empty( $defaults[ $name ]['preserveHtml'] );
			if ( $preserveHtml ) {
				if ( is_array( $defaults[ $name ]['value'] ) ) {
					foreach ( $defaults[ $name ]['value'] as $k => $v ) {
						$defaults[ $name ]['value'][ $k ] = html_entity_decode( $v, ENT_NOQUOTES );
					}
				} else {
					$defaults[ $name ]['value'] = html_entity_decode( $defaults[ $name ]['value'], ENT_NOQUOTES );
				}
			}
			$value = $defaults[ $name ]['value'];

			// Localized value.
			if ( isset( $defaults[ $name ]['localized'] ) ) {
				$localizedKey = $this->groupKey;
				if ( ! empty( $this->subGroups ) ) {
					foreach ( $this->subGroups as $subGroup ) {
						$localizedKey .= '_' . $subGroup;
					}
				}

				$localizedKey .= '_' . $name;

				if ( ! empty( $this->localized[ $localizedKey ] ) ) {
					$value = $this->localized[ $localizedKey ];
					// We need to rebuild the keywords as a json string.
					if ( 'keywords' === $name ) {
						$keywords = explode( ',', $value );
						foreach ( $keywords as $k => $keyword ) {
							$keywords[ $k ] = [
								'label' => $keyword,
								'value' => $keyword
							];
						}

						$value = wp_json_encode( $keywords );
					}
				}
			}
		}

		$this->resetGroups();

		return $value;
	}

	/**
	 * Sets the option value and saves to the database.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $name  The name of the option.
	 * @param  mixed  $value The value to set.
	 * @return void
	 */
	public function __set( $name, $value ) {
		if ( $this->setGroupKey( $name, null, $value ) ) {
			return $this;
		}

		// If we need to set a sub-group, do that now.
		$cachedOptions = aioseoBrokenLinkChecker()->core->optionsCache->getOptions( $this->optionsName );
		$defaults      = json_decode( wp_json_encode( $cachedOptions[ $this->groupKey ] ), true );
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				$defaults = &$defaults[ $subGroup ];
			}
		}

		if ( ! isset( $defaults[ $name ] ) ) {
			$default = $this->getDefault( $name, false );
			$this->resetGroups();

			return $default;
		}

		if ( empty( $defaults[ $name ]['type'] ) ) {
			return $this->setSubGroup( $name );
		}

		$preserveHtml               = ! empty( $defaults[ $name ]['preserveHtml'] );
		$localized                  = ! empty( $defaults[ $name ]['localized'] );
		$defaults[ $name ]['value'] = $this->sanitizeField( $this->value, $defaults[ $name ]['type'], $preserveHtml );

		if ( $localized ) {
			$localizedKey = $this->groupKey;
			if ( ! empty( $this->subGroups ) ) {
				foreach ( $this->subGroups as $subGroup ) {
					$localizedKey .= '_' . $subGroup;
				}
			}

			$localizedKey  .= '_' . $name;
			$localizedValue = $defaults[ $name ]['value'];

			if ( 'keywords' === $name ) {
				$keywords = json_decode( $localizedValue ) ? json_decode( $localizedValue ) : [];
				foreach ( $keywords as $k => $keyword ) {
					$keywords[ $k ] = $keyword->value;
				}

				$localizedValue = implode( ',', $keywords );
			}

			$this->localized[ $localizedKey ] = $localizedValue;
			update_option( $this->optionsName . '_localized', $this->localized );
		}

		$originalDefaults = json_decode( wp_json_encode( $cachedOptions[ $this->groupKey ] ), true );
		$pointer          = &$originalDefaults; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		foreach ( $this->subGroups as $subGroup ) {
			$pointer = &$pointer[ $subGroup ];
		}
		$pointer = $defaults;

		$cachedOptions[ $this->groupKey ] = $originalDefaults;
		aioseoBrokenLinkChecker()->core->optionsCache->setOptions( $this->optionsName, $cachedOptions );

		$this->resetGroups();

		$this->update();
	}

	/**
	 * Checks if an option is set or returns null if not.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $name The name of the option.
	 * @return mixed        True or null.
	 */
	public function __isset( $name ) {
		if ( $this->setGroupKey( $name ) ) {
			return $this;
		}

		// If we need to set a sub-group, do that now.
		$cachedOptions = aioseoBrokenLinkChecker()->core->optionsCache->getOptions( $this->optionsName );
		$defaults      = $cachedOptions[ $this->groupKey ];
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				$defaults = &$defaults[ $subGroup ];
			}
		}

		if ( ! isset( $defaults[ $name ] ) ) {
			$this->resetGroups();

			return false;
		}

		if ( empty( $defaults[ $name ]['type'] ) ) {
			return $this->setSubGroup( $name );
		}

		$value = isset( $defaults[ $name ]['value'] )
			? false === empty( $defaults[ $name ]['value'] )
			: false;

			$this->resetGroups();

		return $value;
	}

	/**
	 * Unsets the option value and saves to the database.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $name  The name of the option.
	 * @return void
	 */
	public function __unset( $name ) {
		if ( $this->setGroupKey( $name ) ) {
			return $this;
		}

		// If we need to set a sub-group, do that now.
		$cachedOptions = aioseoBrokenLinkChecker()->core->optionsCache->getOptions( $this->optionsName );
		$defaults      = json_decode( wp_json_encode( $cachedOptions[ $this->groupKey ] ), true );
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				$defaults = &$defaults[ $subGroup ];
			}
		}

		if ( ! isset( $defaults[ $name ] ) ) {
			$this->groupKey  = null;
			$this->subGroups = [];

			return;
		}

		if ( empty( $defaults[ $name ]['type'] ) ) {
			return $this->setSubGroup( $name );
		}

		if ( ! isset( $defaults[ $name ]['value'] ) ) {
			return;
		}

		unset( $defaults[ $name ]['value'] );

		$cachedOptions[ $this->groupKey ] = $defaults;
		aioseoBrokenLinkChecker()->core->optionsCache->setOptions( $this->optionsName, $cachedOptions );

		$this->resetGroups();

		$this->update();
	}

	/**
	 * Retrieves all options.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $include Keys to include.
	 * @param  array $exclude Keys to exclude.
	 * @return array          An array of options.
	 */
	public function all( $include = [], $exclude = [] ) {
		$originalGroupKey  = $this->groupKey;
		$originalSubGroups = $this->subGroups;

		// Make sure our dynamic options have loaded.
		$this->init();

		// Refactor options.
		$cachedOptions = aioseoBrokenLinkChecker()->core->optionsCache->getOptions( $this->optionsName );
		$refactored    = $this->convertOptionsToValues( $cachedOptions );

		$this->groupKey = null;

		if ( ! $originalGroupKey ) {
			return $this->allFiltered( $refactored, $include, $exclude );
		}

		if ( empty( $originalSubGroups ) ) {
			$all = $refactored[ $originalGroupKey ];

			return $this->allFiltered( $all, $include, $exclude );
		}

		$returnable = &$refactored[ $originalGroupKey ]; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		foreach ( $originalSubGroups as $subGroup ) {
			$returnable = &$returnable[ $subGroup ];
		}

		$this->resetGroups();

		return $this->allFiltered( $returnable, $include, $exclude );
	}

	/**
	 * Reset the current option to the defaults.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $include Keys to include.
	 * @param  array $exclude Keys to exclude.
	 * @return void
	 */
	public function reset( $include = [], $exclude = [] ) {
		$originalGroupKey  = $this->groupKey;
		$originalSubGroups = $this->subGroups;

		// Make sure our dynamic options have loaded.
		$this->init();

		$cachedOptions = aioseoBrokenLinkChecker()->core->optionsCache->getOptions( $this->optionsName );

		// If we don't have a group key set, it means we want to reset everything.
		if ( empty( $originalGroupKey ) ) {
			$groupKeys = array_keys( $cachedOptions );
			foreach ( $groupKeys as $groupKey ) {
				$this->groupKey = $groupKey;
				$this->reset();
			}

			// Since we just finished resetting everything, we can return early.
			return;
		}

		// If we need to set a sub-group, do that now.
		$keys     = array_merge( [ $originalGroupKey ], $originalSubGroups );
		$defaults = json_decode( wp_json_encode( $cachedOptions[ $originalGroupKey ] ), true );
		if ( ! empty( $originalSubGroups ) ) {
			foreach ( $originalSubGroups as $subGroup ) {
				$defaults = $defaults[ $subGroup ];
			}
		}

		// Refactor options.
		$resetValues = $this->resetValues( $defaults, $this->defaultsMerged, $keys, $include, $exclude );
		$defaults    = array_replace_recursive( $defaults, $resetValues );

		$originalDefaults = json_decode( wp_json_encode( $cachedOptions[ $originalGroupKey ] ), true );
		$pointer          = &$originalDefaults; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		foreach ( $originalSubGroups as $subGroup ) {
			$pointer = &$pointer[ $subGroup ];
		}
		$pointer = $defaults;

		$cachedOptions[ $originalGroupKey ] = $originalDefaults;
		aioseoBrokenLinkChecker()->core->optionsCache->setOptions( $this->optionsName, $cachedOptions );

		$this->resetGroups();

		$this->update();
	}

	/**
	 * Resets all values in a group.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $defaults The defaults array we are currently working with.
	 * @param  array $values   The values to adjust.
	 * @param  array $keys     Parent keys for the current group we are parsing.
	 * @param  array $include  Keys to include.
	 * @param  array $exclude  Keys to exclude.
	 * @return array           The modified values.
	 */
	protected function resetValues( $values, $defaults, $keys = [], $include = [], $exclude = [] ) {
		$values = $this->allFiltered( $values, $include, $exclude );
		foreach ( $values as $key => $value ) {
			$option = $this->isAnOption( $key, $defaults, $keys );
			if ( $option ) {
				$values[ $key ]['value'] = isset( $values[ $key ]['default'] ) ? $values[ $key ]['default'] : null;
				continue;
			}

			$keys[]         = $key;
			$values[ $key ] = $this->resetValues( $value, $defaults, $keys );
			array_pop( $keys );
		}

		return $values;
	}

	/**
	 * Checks if the current group has an option or group.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $optionOrGroup The option or group to look for.
	 * @param  bool   $resetGroups   Whether or not to reset the groups after.
	 * @return bool                  True if it does, false if not.
	 */
	public function has( $optionOrGroup = '', $resetGroups = true ) {
		if ( 'type' === $optionOrGroup ) {
			$optionOrGroup = '_aioseo_type';
		}

		$originalGroupKey  = $this->groupKey;
		$originalSubGroups = $this->subGroups;

		static $hasInitialized = false;
		if ( ! $hasInitialized ) {
			$hasInitialized = true;
			$this->init();
		}

		// If we need to set a sub-group, do that now.
		$cachedOptions = aioseoBrokenLinkChecker()->core->optionsCache->getOptions( $this->optionsName );
		$defaults      = $originalGroupKey ? $cachedOptions[ $originalGroupKey ] : $cachedOptions;
		if ( ! empty( $originalSubGroups ) ) {
			foreach ( $originalSubGroups as $subGroup ) {
				$defaults = $defaults[ $subGroup ];
			}
		}

		if ( $resetGroups ) {
			$this->resetGroups();
		}

		if ( ! empty( $defaults[ $optionOrGroup ] ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Filters the results based on passed in array.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $all     All the options to filter.
	 * @param  array $include Keys to include.
	 * @param  array $exclude Keys to exclude.
	 * @return array          The filtered options.
	 */
	private function allFiltered( $all, $include, $exclude ) {
		if ( ! empty( $include ) ) {
			return array_intersect_ukey( $all, $include, function ( $key1, $key2 ) use ( $include ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
				if ( in_array( $key1, $include, true ) ) {
					return 0;
				}

				return -1;
			} );
		}

		if ( ! empty( $exclude ) ) {
			return array_diff_ukey( $all, $exclude, function ( $key1, $key2 ) use ( $exclude ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
				if ( ! in_array( $key1, $exclude, true ) ) {
					return 0;
				}

				return -1;
			} );
		}

		return $all;
	}

	/**
	 * Gets the default value for an option.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $name The option name.
	 * @return mixed        The default value.
	 */
	public function getDefault( $name, $resetGroups = true ) {
		$defaults = $this->defaultsMerged[ $this->groupKey ];
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				if ( empty( $defaults[ $subGroup ] ) ) {
					return null;
				}
				$defaults = $defaults[ $subGroup ];
			}
		}

		if ( $resetGroups ) {
			$this->resetGroups();
		}

		if ( ! isset( $defaults[ $name ] ) ) {
			return null;
		}

		if ( empty( $defaults[ $name ]['type'] ) ) {
			return $this->setSubGroup( $name );
		}

		return isset( $defaults[ $name ]['default'] )
			? $defaults[ $name ]['default']
			: null;
	}

	/**
	 * Gets the defaults options.
	 *
	 * @since 1.0.0
	 *
	 * @return array An array of dafults.
	 */
	public function getDefaults() {
		return $this->defaults;
	}

	/**
	 * Updates the options in the database.
	 *
	 * @since 1.0.0
	 *
	 * @param  string     $optionsName An optional option name to update.
	 * @param  string     $defaults    The defaults to filter the options by.
	 * @param  array|null $options     An optional options array.
	 * @return void
	 */
	public function update( $optionsName = null, $defaults = null, $options = null ) {
		$optionsName = empty( $optionsName ) ? $this->optionsName : $optionsName;
		$defaults    = empty( $defaults ) ? $this->defaults : $defaults;

		// First, we need to filter our options.
		$options = $this->filterOptions( $defaults, $options );

		// Refactor options.
		$refactored = $this->convertOptionsToValues( $options );

		$this->resetGroups();

		// The following needs to happen here (possibly a clone) as well as in the main instance.
		$originalInstance = $this->getOriginalInstance();

		// Update the DB options.
		aioseoBrokenLinkChecker()->core->optionsCache->setDb( $optionsName, $refactored );

		// Force a save here and in the main class.
		$this->shouldSave             = true;
		$originalInstance->shouldSave = true;
	}

	/**
	 * Updates the options in the database.
	 *
	 * @since 1.0.0
	 *
	 * @param  boolean $force       Whether or not to force an immediate save.
	 * @param  string  $optionsName An optional option name to update.
	 * @param  string  $defaults    The defaults to filter the options by.
	 * @return void
	 */
	public function save( $force = false, $optionsName = null, $defaults = null ) {
		if ( ! $this->shouldSave && ! $force ) {
			return;
		}

		$optionsName = empty( $optionsName ) ? $this->optionsName : $optionsName;
		$defaults    = empty( $defaults ) ? $this->defaults : $defaults;

		$this->update( $optionsName );

		// First, we need to filter our options.
		$options = $this->filterOptions( $defaults, null );

		// Refactor options.
		$refactored = $this->convertOptionsToValues( $options );

		$this->resetGroups();

		update_option( $optionsName, wp_json_encode( $refactored ) );
	}

	/**
	 * Filter options to match our defaults.
	 *
	 * @since 1.0.0
	 *
	 * @param  array      $defaults The defaults to use in filtering.
	 * @param  array|null $options  An optional options array.
	 * @return array                An array of filtered options.
	 */
	public function filterOptions( $defaults, $options = null ) {
		$cachedOptions = aioseoBrokenLinkChecker()->core->optionsCache->getOptions( $this->optionsName );
		$options       = ! empty( $options ) ? $options : json_decode( wp_json_encode( $cachedOptions ), true );

		return $this->filterRecursively( $options, $defaults );
	}

	/**
	 * Filters options in a loop.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $options  An array of options to filter.
	 * @param  array $defaults An array of defaults to filter against.
	 * @return array           A filtered array of options.
	 */
	public function filterRecursively( $options, $defaults ) {
		if ( ! is_array( $options ) ) {
			return $options;
		}

		foreach ( $options as $key => $value ) {
			if ( ! isset( $defaults[ $key ] ) ) {
				unset( $options[ $key ] );
				continue;
			}

			if ( ! isset( $value['type'] ) ) {
				$options[ $key ] = $this->filterRecursively( $options[ $key ], $defaults[ $key ] );
				continue;
			}
		}

		return $options;
	}

	/**
	 * Sanitizes the value before allowing it to be saved.
	 *
	 * @since 1.0.0
	 *
	 * @param  mixed  $value The value to sanitize.
	 * @param  string $type  The type of sanitization to do.
	 * @return mixed         The sanitized value.
	 */
	public function sanitizeField( $value, $type, $preserveHtml = false ) {
		switch ( $type ) {
			case 'boolean':
				return (bool) $value;
			case 'html':
				return sanitize_textarea_field( $value );
			case 'string':
				return sanitize_text_field( $value );
			case 'number':
				return intval( $value );
			case 'array':
				$array = [];
				foreach ( (array) $value as $k => $v ) {
					$array[ $k ] = sanitize_text_field( $preserveHtml ? htmlspecialchars( $v, ENT_NOQUOTES, 'UTF-8' ) : $v );
				}

				return $array;
			case 'float':
				return floatval( $value );
		}
	}

	/**
	 * Checks to see if we need to set the group key. If so, will return true.
	 *
	 * @since 1.0.0
	 *
	 * @param  string  $name      The name of the option to set.
	 * @param  array   $arguments Any arguments needed if this was a method called.
	 * @param  mixed   $value     The value if we are setting an option.
	 * @return boolean            Whether or not we need to set the group key.
	 */
	private function setGroupKey( $name, $arguments = null, $value = null ) {
		$this->arguments = $arguments;
		$this->value     = $value;

		if ( empty( $this->groupKey ) ) {
			$groups = array_keys( $this->defaultsMerged );
			if ( in_array( $name, $groups, true ) ) {
				$this->groupKey = $name;

				return true;
			}

			$this->groupKey = $groups[0];
		}

		return false;
	}

	/**
	 * Sets the sub group key. Will set and return the instance.
	 *
	 * @since 1.0.0
	 *
	 * @param  string                                    $name      The name of the option to set.
	 * @param  array                                     $arguments Any arguments needed if this was a method called.
	 * @param  mixed                                     $value     The value if we are setting an option.
	 * @return \AIOSEO\BrokenLinkChecker\Options\Options            The options object.
	 */
	private function setSubGroup( $name, $arguments = null, $value = null ) {
		if ( ! is_null( $arguments ) ) {
			$this->arguments = $arguments;
		}
		if ( ! is_null( $value ) ) {
			$this->value = $value;
		}

		$defaults = $this->defaultsMerged[ $this->groupKey ];
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				$defaults = $defaults[ $subGroup ];
			}
		}

		$groups = array_keys( $defaults );
		if ( in_array( $name, $groups, true ) ) {
			$this->subGroups[] = $name;
		}

		return $this;
	}

	/**
	 * Reset groups.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	protected function resetGroups() {
		$this->groupKey  = null;
		$this->subGroups = [];
	}

	/**
	 * Converts an associative array of values into a structure
	 * that works with our defaults.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $defaults The defaults array we are currently working with.
	 * @param  array $values   The values to adjust.
	 * @param  array $keys     Parent keys for the current group we are parsing.
	 * @param  bool  $sanitize Whether or not we should sanitize the value.
	 * @return array           The modified values.
	 */
	protected function addValueToValuesArray( $defaults, $values, $keys = [], $sanitize = false ) {
		foreach ( $values as $key => $value ) {
			$option = $this->isAnOption( $key, $defaults, $keys );
			if ( $option ) {
				$preserveHtml   = ! empty( $option['preserveHtml'] );
				$newValue       = $sanitize ? $this->sanitizeField( $value, $option['type'], $preserveHtml ) : $value;
				$values[ $key ] = [
					'value' => $newValue
				];

				// If this is a localized string, let's save it to our localized options.
				if ( $sanitize && ! empty( $option['localized'] ) ) {
					$localizedKey = '';
					foreach ( $keys as $k ) {
						$localizedKey .= $k . '_';
					}

					$localizedKey  .= $key;
					$localizedValue = $newValue;
					if ( 'keywords' === $key ) {
						$keywords = json_decode( $localizedValue ) ? json_decode( $localizedValue ) : [];
						foreach ( $keywords as $k => $keyword ) {
							$keywords[ $k ] = $keyword->value;
						}

						$localizedValue = implode( ',', $keywords );
					}

					$this->localized[ $localizedKey ] = $localizedValue;
				}
				continue;
			}

			if ( ! is_array( $value ) ) {
				continue;
			}

			$keys[]         = $key;
			$values[ $key ] = $this->addValueToValuesArray( $defaults, $value, $keys, $sanitize );
			array_pop( $keys );
		}

		return $values;
	}

	/**
	 * Our options array has values (or defaults).
	 * This method converts them to how we would store them in the DB.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $options The options array.
	 * @return array           The converted options array.
	 */
	public function convertOptionsToValues( $options, $optionKey = 'type' ) {
		foreach ( $options as $key => $value ) {
			if ( ! is_array( $value ) ) {
				continue;
			}

			if ( ! isset( $value[ $optionKey ] ) ) {
				$options[ $key ] = $this->convertOptionsToValues( $value, $optionKey );
				continue;
			}

			$options[ $key ] = null;

			if ( isset( $value['value'] ) ) {
				$preserveHtml = ! empty( $value['preserveHtml'] );
				if ( $preserveHtml ) {
					if ( is_array( $value['value'] ) ) {
						foreach ( $value['value'] as $k => $v ) {
							$value['value'][ $k ] = html_entity_decode( $v, ENT_NOQUOTES );
						}
					} else {
						$value['value'] = html_entity_decode( $value['value'], ENT_NOQUOTES );
					}
				}
				$options[ $key ] = $value['value'];
				continue;
			}

			if ( isset( $value['default'] ) ) {
				$options[ $key ] = $value['default'];
			}
		}

		return $options;
	}

	/**
	 * This checks to see if the current array/option is really an option
	 * and not just another parent with a subgroup.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $key      The current array key we are working with.
	 * @param  array  $defaults The defaults array to check against.
	 * @param  array  $keys     The parent keys to loop through.
	 * @return bool             Whether or not this is an option.
	 */
	private function isAnOption( $key, $defaults, $keys ) {
		if ( ! empty( $keys ) ) {
			foreach ( $keys as $k ) {
				$defaults = isset( $defaults[ $k ] ) ? $defaults[ $k ] : [];
			}
		}

		if ( isset( $defaults[ $key ]['type'] ) ) {
			return $defaults[ $key ];
		}

		return false;
	}

	/**
	 * Refreshes the options from the database.
	 *
	 * We need this during the migration to update through clones.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function refresh() {
		// Reset DB options to clear the cache.
		aioseoBrokenLinkChecker()->core->optionsCache->resetDb();
		$this->init();
	}

	/**
	 * Returns the DB options.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $optionsName The options name.
	 * @return array               The options.
	 */
	public function getDbOptions( $optionsName ) {
		$cache = aioseoBrokenLinkChecker()->core->optionsCache->getDb( $optionsName );
		if ( empty( $cache ) ) {
			$options = json_decode( get_option( $optionsName ), true );
			$options = ! empty( $options ) ? $options : [];

			// Set the cache.
			aioseoBrokenLinkChecker()->core->optionsCache->setDb( $optionsName, $options );
		}

		return aioseoBrokenLinkChecker()->core->optionsCache->getDb( $optionsName );
	}

	/**
	 * In order to not have a conflict, we need to return a clone.
	 *
	 * @since 1.0.0
	 *
	 * @return \AIOSEO\BrokenLinkChecker\Options\Options The cloned Options object.
	 */
	public function noConflict() {
		$class          = clone $this;
		$class->isClone = true;

		return $class;
	}

	/**
	 * Get original instance. Since this could be a cloned object, let's get the original instance.
	 *
	 * @since 1.0.0
	 *
	 * @return self
	 */
	public function getOriginalInstance() {
		if ( ! $this->isClone ) {
			return $this;
		}

		$class      = new \ReflectionClass( get_called_class() );
		$optionName = aioseoBrokenLinkChecker()->helpers->toCamelCase( $class->getShortName() );

		if ( isset( aioseoBrokenLinkChecker()->{ $optionName } ) ) {
			return aioseoBrokenLinkChecker()->{ $optionName };
		}

		return $this;
	}
}Utils/Access.php000064400000010722151536237000007562 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Utils;

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

class Access {
	/**
	 * Capabilities for our users.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $capabilities = [
		'aioseo_blc_about_us_page',
		'aioseo_blc_broken_links_page',
		'aioseo_blc_setup_wizard_page'
	];

	/**
	 * Roles we check capabilities against.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $roles = [
		'superadmin'    => 'superadmin',
		'administrator' => 'administrator',
		'editor'        => 'editor',
		'author'        => 'author',
		'contributor'   => 'contributor'
	];

	/**
	 * Whether or not we are updating roles.
	 *
	 * @since 1.1.0
	 *
	 * @var bool
	 */
	private $isUpdatingRoles = false;

	/**
	 * Adds capabilities into WordPress for the current user.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function addCapabilities() {
		foreach ( $this->roles as $wpRole => $role ) {
			$roleObject = get_role( $wpRole );
			if ( ! is_object( $roleObject ) ) {
				continue;
			}

			if ( current_user_can( 'edit_posts' ) || $this->isAdmin() ) {
				foreach ( $this->capabilities as $cap ) {
					$roleObject->add_cap( $cap );
				}
			}
		}

		$this->removeCapabilities();
	}

	/**
	 * Removes capabilities for any unknown role.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function removeCapabilities() {
		$this->isUpdatingRoles = true;

		// Clear out capabilities for unknown roles.
		$wpRoles  = wp_roles();
		$allRoles = $wpRoles->roles;
		foreach ( $allRoles as $key => $wpRole ) {
			$checkRole = is_multisite() ? 'superadmin' : 'administrator';
			if ( $checkRole === $key ) {
				continue;
			}

			if ( in_array( $key, $this->roles, true ) ) {
				continue;
			}

			$role = get_role( $key );
			if ( empty( $role ) ) {
				continue;
			}

			if ( $this->isAdmin( $key ) ) {
				continue;
			}

			foreach ( $this->capabilities as $capability ) {
				if ( $role->has_cap( $capability ) ) {
					$role->remove_cap( $capability );
				}
			}
		}
	}

	/**
	 * Checks if the current user has the capability.
	 *
	 * @since 1.0.0
	 *
	 * @param  string      $capability The capability to check against.
	 * @param  string|null $checkRole  A role to check against.
	 * @return bool                    Whether or not the user has this capability.
	 */
	public function hasCapability( $capability, $checkRole = null ) {
		// Only admins have access.
		if ( $this->isAdmin( $checkRole ) ) {
			return true;
		}

		if (
			(
				$this->can( 'publish_posts', $checkRole ) ||
				$this->can( 'edit_posts', $checkRole )
			) &&
			false !== strpos( $capability, 'aioseo_blc_' )
		) {
			return true;
		}

		return false;
	}

	/**
	 * Gets all the capabilities for the current user.
	 *
	 * @since 1.0.0
	 *
	 * @param  string|null $role A role to check against.
	 * @return array             An array of capabilities.
	 */
	public function getAllCapabilities( $role = null ) {
		$capabilities = [];
		foreach ( $this->capabilities as $cap ) {
			$capabilities[ $cap ] = $this->hasCapability( $cap, $role );
		}

		return $capabilities;
	}

	/**
	 * If the current user is an admin, or superadmin, they have access to all caps regardless.
	 *
	 * @since 1.0.0
	 *
	 * @param  string|null $role The role to check admin privileges if we have one.
	 * @return bool              Whether not the user/role is an admin.
	 */
	public function isAdmin( $role = null ) {
		if ( $role ) {
			if ( ( is_multisite() && 'superadmin' === $role ) || 'administrator' === $role ) {
				return true;
			}

			return false;
		}

		if ( ( is_multisite() && current_user_can( 'superadmin' ) ) || current_user_can( 'administrator' ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Check if the passed in role can publish posts.
	 *
	 * @since 1.0.0
	 *
	 * @param  string  $capability The capability to check against.
	 * @param  string  $role       The role to check.
	 * @return boolean             True if the role can publish.
	 */
	protected function can( $capability, $role ) {
		if ( empty( $role ) ) {
			return current_user_can( $capability );
		}

		$wpRoles  = wp_roles();
		$allRoles = $wpRoles->roles;
		foreach ( $allRoles as $key => $wpRole ) {
			if ( $key === $role ) {
				$r = get_role( $key );
				if ( $r->has_cap( $capability ) ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Returns the capability list.
	 *
	 * @since 1.2.4
	 *
	 * @return array An array of capabilities.
	 */
	public function getCapabilityList() {
		return $this->capabilities;
	}
}Utils/ActionScheduler.php000064400000020614151536237000011436 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Utils;

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

/**
 * This class makes sure the Action Scheduler tables always exist.
 *
 * @since 1.0.0
 */
class ActionScheduler {
	/**
	 * The Action Scheduler group.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private $actionSchedulerGroup = 'aioseo_blc';

	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		add_action( 'action_scheduler_after_execute', [ $this, 'cleanup' ], 1000, 2 );
		add_action( 'plugins_loaded', [ $this, 'maybeRecreateTables' ] );
	}

	/**
	 * Maybe register the `{$table_prefix}_actionscheduler_{$suffix}` tables with WordPress and create them if needed.
	 * Hooked into `plugins_loaded` action hook.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function maybeRecreateTables() {
		if ( ! is_admin() ) {
			return;
		}

		if ( ! apply_filters( 'action_scheduler_enable_recreate_data_store', true ) ) {
			return;
		}

		if (
			! class_exists( 'ActionScheduler' ) ||
			! class_exists( 'ActionScheduler_HybridStore' ) ||
			! class_exists( 'ActionScheduler_StoreSchema' ) ||
			! class_exists( 'ActionScheduler_LoggerSchema' )
		) {
			return;
		}

		$store = \ActionScheduler::store();

		if ( ! is_a( $store, 'ActionScheduler_HybridStore' ) ) {
			$store = new \ActionScheduler_HybridStore();
		}

		$tableList = [
			'actionscheduler_actions',
			'actionscheduler_logs',
			'actionscheduler_groups',
			'actionscheduler_claims',
		];

		foreach ( $tableList as $tableName ) {
			if ( ! aioseoBrokenLinkChecker()->core->db->tableExists( $tableName ) ) {
				add_action( 'action_scheduler/created_table', [ $store, 'set_autoincrement' ], 10, 2 );

				$storeSchema  = new \ActionScheduler_StoreSchema();
				$loggerSchema = new \ActionScheduler_LoggerSchema();
				$storeSchema->register_tables( true );
				$loggerSchema->register_tables( true );

				remove_action( 'action_scheduler/created_table', [ $store, 'set_autoincrement' ] );

				break;
			}
		}
	}

	/**
	 * Cleans up the Action Scheduler tables after one of our actions completes.
	 * Hooked into `action_scheduler_after_execute` action hook.
	 *
	 * @since 1.0.0
	 *
	 * @param  int                     $actionId The action ID processed.
	 * @param  \ActionScheduler_Action $action   Class instance.
	 * @return void
	 */
	public function cleanup( $actionId, $action ) {
		if (
			// Bail if this isn't one of our actions or if we're in a dev environment.
			$this->actionSchedulerGroup !== $action->get_group() ||
			defined( 'AIOSEO_BROKEN_LINK_CHECKER_DEV_VERSION' ) ||
			// Bail if the tables don't exist.
			! aioseoBrokenLinkChecker()->core->db->tableExists( 'actionscheduler_actions' ) ||
			! aioseoBrokenLinkChecker()->core->db->tableExists( 'actionscheduler_groups' )
		) {
			return;
		}

		$prefix = aioseoBrokenLinkChecker()->core->db->db->prefix;

		// Clean up logs associated with entries in the actions table.
		aioseoBrokenLinkChecker()->core->db->execute(
			"DELETE al FROM {$prefix}actionscheduler_logs as al
			JOIN {$prefix}actionscheduler_actions as aa on `aa`.`action_id` = `al`.`action_id`
			JOIN {$prefix}actionscheduler_groups as ag on `ag`.`group_id` = `aa`.`group_id`
			WHERE `ag`.`slug` = '{$this->actionSchedulerGroup}'
			AND `aa`.`status` IN ('complete', 'failed', 'canceled');"
		);

		// Clean up actions.
		aioseoBrokenLinkChecker()->core->db->execute(
			"DELETE aa FROM {$prefix}actionscheduler_actions as aa
			JOIN {$prefix}actionscheduler_groups as ag on `ag`.`group_id` = `aa`.`group_id`
			WHERE `ag`.`slug` = '{$this->actionSchedulerGroup}'
			AND `aa`.`status` IN ('complete', 'failed', 'canceled');"
		);

		// Clean up logs where there was no group.
		aioseoBrokenLinkChecker()->core->db->execute(
			"DELETE al FROM {$prefix}actionscheduler_logs as al
			JOIN {$prefix}actionscheduler_actions as aa on `aa`.`action_id` = `al`.`action_id`
			WHERE `aa`.`hook` LIKE '{$this->actionSchedulerGroup}_%'
			AND `aa`.`group_id` = 0
			AND `aa`.`status` IN ('complete', 'failed', 'canceled');"
		);

		// Clean up actions that start with aioseo_ and have no group.
		aioseoBrokenLinkChecker()->core->db->execute(
			"DELETE aa FROM {$prefix}actionscheduler_actions as aa
			WHERE `aa`.`hook` LIKE '{$this->actionSchedulerGroup}_%'
			AND `aa`.`group_id` = 0
			AND `aa`.`status` IN ('complete', 'failed', 'canceled');"
		);
	}

	/**
	 * Schedules a single action at a specific time in the future.
	 * @NOTE: This method differs from the one in the main plugin!
	 *
	 * @since 1.0.0
	 *
	 * @param  string  $actionName The action name.
	 * @param  int     $time       The time to add to the current time.
	 * @param  array   $args       Args passed down to the action.
	 * @return boolean             Whether the action was scheduled.
	 */
	public function scheduleSingle( $actionName, $time, $args = [] ) {
		try {
			if ( empty( $this->getPendingActions( $actionName, $args ) ) ) {
				as_schedule_single_action( time() + $time, $actionName, $args, $this->actionSchedulerGroup );

				return true;
			}
		} catch ( \RuntimeException $e ) {
			// Nothing needs to happen.
		}

		return false;
	}

	/**
	 * Checks if a given action is already scheduled.
	 *
	 * @since 1.0.0
	 *
	 * @param  string  $actionName The action name.
	 * @param  array   $args       Args passed down to the action.
	 * @return boolean             Whether the action is already scheduled.
	 */
	public function isScheduled( $actionName, $args = [] ) {
		$actions = array_merge(
			$this->getRunningActions( $actionName, $args ),
			$this->getPendingActions( $actionName, $args )
		);

		return ! empty( $actions );
	}

	/**
	 * Returns the running actions for a given action.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $actionName The action name.
	 * @param  array  $args       Args passed down to the action.
	 * @return array              The actions.
	 */
	public function getRunningActions( $actionName, $args = [] ) {
		$runningArgs = [
			'hook'     => $actionName,
			'status'   => \ActionScheduler_Store::STATUS_RUNNING,
			'per_page' => 1
		];

		if ( empty( $args ) ) {
			$runningArgs['args'] = $args;
		}

		$actions = as_get_scheduled_actions( $runningArgs );

		return $actions;
	}

	/**
	 * Returns the pending actions for a given action.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $actionName The action name.
	 * @param  array  $args       Args passed down to the action.
	 * @return array              The actions.
	 */
	public function getPendingActions( $actionName, $args = [] ) {
		$pendingArgs = [
			'hook'     => $actionName,
			'status'   => \ActionScheduler_Store::STATUS_PENDING,
			'per_page' => 1
		];

		if ( empty( $args ) ) {
			$pendingArgs['args'] = $args;
		}

		$actions = as_get_scheduled_actions( $pendingArgs );

		return $actions;
	}

	/**
	 * Unschedule an action.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $actionName The action name to unschedule.
	 * @param  array  $args       Args passed down to the action.
	 * @return void
	 */
	public function unschedule( $actionName, $args = [] ) {
		try {
			if ( as_next_scheduled_action( $actionName ) ) {
				as_unschedule_action( $actionName, $args, $this->actionSchedulerGroup );
			}
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Schedules a recurring action.
	 *
	 * @since 1.0.0
	 *
	 * @param  string  $actionName The action name.
	 * @param  int     $time       The seconds to add to the current time.
	 * @param  int     $interval   The interval in seconds.
	 * @param  array   $args       Args passed down to the action.
	 * @return boolean             Whether the action was scheduled.
	 */
	public function scheduleRecurrent( $actionName, $time, $interval = 60, $args = [] ) {
		try {
			if ( ! $this->isScheduled( $actionName ) ) {
				as_schedule_recurring_action( time() + $time, $interval, $actionName, $args, $this->actionSchedulerGroup );

				return true;
			}
		} catch ( \RuntimeException $e ) {
			// Nothing needs to happen.
		}

		return false;
	}

	/**
	 * Schedule a single async action.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $actionName The name of the action.
	 * @param  array  $args       Any relevant arguments.
	 * @return void
	 */
	public function scheduleAsync( $actionName, $args = [] ) {
		try {
			// Run the task immediately using an async action.
			as_enqueue_async_action( $actionName, $args, $this->actionSchedulerGroup );
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}
}Utils/Helpers.php000064400000024143151536237000007765 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Utils;

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

use AIOSEO\BrokenLinkChecker\Traits\Helpers as TraitHelpers;

/**
 * Contains helper functions
 *
 * @since 1.0.0
 */
class Helpers {
	use TraitHelpers\Api;
	use TraitHelpers\Arrays;
	use TraitHelpers\Constants;
	use TraitHelpers\DateTime;
	use TraitHelpers\Strings;
	use TraitHelpers\ThirdParty;
	use TraitHelpers\Vue;
	use TraitHelpers\Wp;
	use TraitHelpers\WpContext;
	use TraitHelpers\WpMultisite;
	use TraitHelpers\WpUri;

	/**
	 * Checks if we are in a dev environment or not.
	 *
	 * @since 1.0.0
	 *
	 * @return boolean True if we are, false if not.
	 */
	public function isDev() {
		return aioseoBrokenLinkChecker()->isDev || isset( $_REQUEST['aioseo-dev'] ); // phpcs:ignore HM.Security.NonceVerification.Recommended
	}

	/**
	 * Applies wp_kses_post on the given string, but also allows some other tags we support.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $string The string.
	 * @return string         The sanitized string.
	 */
	public function wpKsesPhrase( $string ) {
		$allowedHtmlTags = wp_kses_allowed_html( 'post' );

		$customTags = [
			'ta' => [
				'linkid' => [],
				'href'   => []
			]
		];

		$allowedHtmlTags = array_merge( $allowedHtmlTags, $customTags );

		return wp_kses( $string, $allowedHtmlTags );
	}

	/**
	 * Returns the scannable post types.
	 *
	 * @since 1.0.0
	 *
	 * @return array The scannable post types.
	 */
	public function getScannablePostTypes() {
		static $scannablePostTypes = null;
		if ( null !== $scannablePostTypes ) {
			return $scannablePostTypes;
		}

		// We exclude these post types to optimize performance.
		$nonSupportedPostTypes = [ 'attachment' ];
		$scannablePostTypes    = array_diff(
			$this->getPublicPostTypes( true ),
			$nonSupportedPostTypes
		);

		return $scannablePostTypes;
	}

	/**
	 * Returns the time that elapsed since the initial call to this function.
	 *
	 * @since 1.0.0
	 *
	 * @return int|null The time that has elapsed.
	 */
	public function timeElapsed() {
		static $last = null;

		$now    = microtime( true );
		$return = null !== $last ? $now - $last : null;

		if ( null === $last ) {
			$last = $now;
		}

		return $return;
	}

	/**
	 * Checks whether the current post can be scanned.
	 *
	 * @since 1.0.0
	 *
	 * @param  \WP_Post $post The post object.
	 * @return bool           Whether the post is scannable.
	 */
	public function isScannablePost( $post ) {
		if ( ! is_object( $post ) ) {
			return false;
		}

		$postTypes = array_diff( $this->getPublicPostTypes( true ), [ 'attachment' ] );
		if ( ! in_array( $post->post_type, $postTypes, true ) ) {
			return false;
		}

		if ( ! aioseoBrokenLinkChecker()->helpers->isValidPost( $post, $this->getPublicPostStatuses( true ) ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Returns the post title or a placeholder if there isn't one.
	 *
	 * @since 1.0.0
	 *
	 * @param  int    $postId The post ID.
	 * @return string         The post title.
	 */
	public function getPostTitle( $postId ) {
		static $titles = [];
		if ( isset( $titles[ $postId ] ) ) {
			return $titles[ $postId ];
		}

		$post  = get_post( $postId );
		$title = $post->post_title;
		$title = $title ? $title : __( '(no title)' ); // phpcs:ignore AIOSEO.Wp.I18n.MissingArgDomain

		$titles[ $postId ] = $this->decodeHtmlEntities( $title );

		return $titles[ $postId ];
	}


	/**
	 * Checks if the given post is excluded from Broken Link Checker.
	 *
	 * @since 1.0.0
	 *
	 * @param  int  $postId The post ID.
	 * @return bool         Whether the post is excluded.
	 */
	public function isExcludedPost( $postId ) {
		$excludedPostIds      = $this->getExcludedPostIds();
		$includedPostTypes    = $this->getIncludedPostTypes();
		// We include auto-drafts here because all new posts are otherwise excluded before they are saved.
		$includedPostStatuses = array_merge( $this->getIncludedPostStatuses(), [ 'auto-draft' ] );
		$post                 = get_post( $postId );

		return in_array( (int) $postId, $excludedPostIds, true ) ||
			! in_array( $post->post_type, $includedPostTypes, true ) ||
			! in_array( $post->post_status, $includedPostStatuses, true );
	}

	/**
	 * Returns the IDs of posts that are excluded from Broken Link Checker.
	 *
	 * @since 1.0.0
	 *
	 * @return array The post IDs.
	 */
	public function getExcludedPostIds() {
		static $excludedPostIds = null;
		if ( null === $excludedPostIds ) {
			if ( ! aioseoBrokenLinkChecker()->options->advanced->enable ) {
				$excludedPostIds = [];

				return $excludedPostIds;
			}

			$excludedPostIds = [];
			$excludedPosts   = aioseoBrokenLinkChecker()->options->advanced->excludePosts;
			foreach ( $excludedPosts as $excludedPost ) {
				$excludedPost = json_decode( $excludedPost );
				if ( ! empty( $excludedPost->value ) ) {
					$excludedPostIds[] = $excludedPost->value;
				}
			}
		}

		return $excludedPostIds;
	}

	/**
	 * Returns the post types that Broken Link Checker is enabled for.
	 *
	 * @since 1.0.0
	 *
	 * @return array The included post types.
	 */
	public function getIncludedPostTypes() {
		static $includedPostTypes = null;
		if ( null !== $includedPostTypes ) {
			return $includedPostTypes;
		}

		$includedPostTypes = [];
		$postTypes         = aioseoBrokenLinkChecker()->options->advanced->postTypes->all();
		if ( ! aioseoBrokenLinkChecker()->options->advanced->enable || ! empty( $postTypes['all'] ) ) {
			$includedPostTypes = $this->getScannablePostTypes();
		} else {
			// Determine the intersection to make sure that we only consider post types that are currently registered.
			$includedPostTypes = array_intersect(
				$postTypes['included'],
				$this->getScannablePostTypes()
			);
		}

		foreach ( $includedPostTypes as $k => $postType ) {
			if ( ! $this->canEditPostType( $postType ) ) {
				unset( $includedPostTypes[ $k ] );
			}
		}

		return $includedPostTypes;
	}

	/**
	 * Returns the post statuses that Broken Link Checker is enabled for.
	 *
	 * @since 1.0.0
	 *
	 * @return array The included post statuses.
	 */
	public function getIncludedPostStatuses() {
		static $includedPostStatuses = null;
		if ( null !== $includedPostStatuses ) {
			return $includedPostStatuses;
		}

		$includedPostStatuses = [];
		$postStatuses         = aioseoBrokenLinkChecker()->options->advanced->postStatuses->all();
		if ( ! aioseoBrokenLinkChecker()->options->advanced->enable || ! empty( $postStatuses['all'] ) ) {
			$includedPostStatuses = $this->getPublicPostStatuses( true );
		} else {
			// Determine the intersection to make sure that we only consider post statuses that are currently registered.
			$includedPostStatuses = array_intersect(
				$postStatuses['included'],
				$this->getPublicPostStatuses( true )
			);
		}

		return $includedPostStatuses;
	}

	/**
	 * Generates a UTM URL from the URL and medium/content that are passed in.
	 *
	 * @since 1.0.0
	 *
	 * @param  string      $url     The URL to parse.
	 * @param  string      $medium  The UTM medium parameter.
	 * @param  string|null $content The UTM content parameter or null.
	 * @param  boolean     $esc     Whether or not to escape the URL.
	 * @return string               The new URL.
	 */
	public function utmUrl( $url, $medium, $content = null, $esc = true ) {
		// First, remove any existing utm parameters on the URL.
		$url = remove_query_arg( [
			'utm_source',
			'utm_medium',
			'utm_campaign',
			'utm_content'
		], $url );

		// Generate the new arguments.
		$args = [
			'utm_source'   => 'WordPress',
			'utm_campaign' => 'plugin',
			'utm_medium'   => $medium
		];

		// Content is not used by default.
		if ( $content ) {
			$args['utm_content'] = $content;
		}

		// Return the new URL.
		$url = add_query_arg( $args, $url );

		return $esc ? esc_url( $url ) : $url;
	}

	/**
	 * Returns the excluded domains.
	 *
	 * @since 1.1.1
	 *
	 * @return array The excluded domains.
	 */
	public function getExcludedDomains() {
		if ( ! aioseoBrokenLinkChecker()->options->advanced->enable ) {
			return [];
		}

		$excludedDomains = aioseoBrokenLinkChecker()->options->advanced->excludeDomains;
		if ( ! is_string( $excludedDomains ) ) {
			return [];
		}

		$pattern = '/([\.?!][\r\n\s]+|\r|\n|\s{2,})/u';

		return array_map( 'trim', preg_split( $pattern, (string) $excludedDomains, -1, PREG_SPLIT_NO_EMPTY ) );
	}

	/**
	 * Checks if the given string is serialized, and if so, unserializes it.
	 * If the serialized string contains an object, we abort to prevent PHP object injection.
	 *
	 * @since 1.2.0
	 *
	 * @param  string       $string The string.
	 * @return string|array         The string or unserialized data.
	 */
	public function maybeUnserialize( $string ) {
		if ( ! is_string( $string ) ) {
			return $string;
		}

		$string = trim( $string );
		if ( is_serialized( $string ) && ! $this->stringContains( $string, 'O:' ) ) {
			// We want to add extra hardening for PHP versions greater than 5.6.
			return version_compare( PHP_VERSION, '7.0', '<' )
				? @unserialize( $string )
				: @unserialize( $string, [ 'allowed_classes' => false ] ); // phpcs:disable PHPCompatibility.FunctionUse.NewFunctionParameters.unserialize_optionsFound
		}

		return $string;
	}

	/**
	 * Returns user roles in the current WP install.
	 *
	 * @since 1.2.4
	 *
	 * @return array An array of user roles.
	 */
	public function getUserRoles() {
		global $wp_roles; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		$wpRoles = $wp_roles; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if ( ! is_object( $wpRoles ) ) {
			// Don't assign this to the global because otherwise WordPress won't override it.
			$wpRoles = new \WP_Roles();
		}

		$roleNames = $wpRoles->get_names();
		asort( $roleNames );

		return $roleNames;
	}

	/**
	 * Check if the current request is uninstalling (deleting) Broken Link Checker.
	 *
	 * @since {Pnext}
	 *
	 * @return bool Whether Broken Link Checker is being uninstalled/deleted or not.
	 */
	public function isUninstalling() {
		if (
			defined( 'AIOSEO_BROKEN_LINK_CHECKER_FILE' ) &&
			defined( 'WP_UNINSTALL_PLUGIN' )
		) {
			// Make sure `plugin_basename()` exists.
			include_once ABSPATH . 'wp-admin/includes/plugin.php';

			return WP_UNINSTALL_PLUGIN === plugin_basename( AIOSEO_BROKEN_LINK_CHECKER_FILE );
		}

		return false;
	}
}Utils/PluginUpgraderSilentAjax.php000064400000022761151536237000013302 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Utils;

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

use WP_Error;

/** \WP_Upgrader class */
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';

/** \Plugin_Upgrader class */
require_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php';

/**
 * In WP 5.3 a PHP 5.6 splat operator (...$args) was added to \WP_Upgrader_Skin::feedback().
 * We need to remove all calls to *Skin::feedback() method, as we can't override it in own Skins
 * without breaking support for PHP 5.3-5.5.
 *
 * @internal Please do not use this class outside of core AIOSEO development.
 *
 * @since 1.0.0
 */
class PluginUpgraderSilentAjax extends \Plugin_Upgrader {
	/**
	 * An array of links to install the plugins from.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	public $pluginLinks = [
		'aioseo'               => 'https://downloads.wordpress.org/plugins/all-in-one-seo-pack.zip',
		'optinMonster'         => 'https://downloads.wordpress.org/plugin/optinmonster.zip',
		'wpForms'              => 'https://downloads.wordpress.org/plugin/wpforms-lite.zip',
		'miLite'               => 'https://downloads.wordpress.org/plugin/google-analytics-for-wordpress.zip',
		'emLite'               => 'https://downloads.wordpress.org/plugin/google-analytics-dashboard-for-wp.zip',
		'wpMail'               => 'https://downloads.wordpress.org/plugin/wp-mail-smtp.zip',
		'rafflePress'          => 'https://downloads.wordpress.org/plugin/rafflepress.zip',
		'seedProd'             => 'https://downloads.wordpress.org/plugin/coming-soon.zip',
		'trustPulse'           => 'https://downloads.wordpress.org/plugin/trustpulse-api.zip',
		'instagramFeed'        => 'https://downloads.wordpress.org/plugin/instagram-feed.zip',
		'facebookFeed'         => 'https://downloads.wordpress.org/plugin/custom-facebook-feed.zip',
		'twitterFeed'          => 'https://downloads.wordpress.org/plugin/custom-twitter-feeds.zip',
		'youTubeFeed'          => 'https://downloads.wordpress.org/plugin/feeds-for-youtube.zip',
		'pushEngage'           => 'https://downloads.wordpress.org/plugins/pushengage.zip',
		'sugarCalendar'        => 'https://downloads.wordpress.org/plugins/sugar-calendar-lite.zip',
		'wpSimplePay'          => 'https://downloads.wordpress.org/plugins/stripe.zip',
		'easyDigitalDownloads' => 'https://downloads.wordpress.org/plugins/easy-digital-downloads.zip',
		'wpcode'               => 'https://downloads.wordpress.org/plugin/insert-headers-and-footers.zip',
		'searchWp'             => '',
		'affiliateWp'          => '',
		'charitable'           => 'https://downloads.wordpress.org/plugin/charitable.zip',
		'duplicator'           => 'https://downloads.wordpress.org/plugin/duplicator.zip'
	];

	/**
	 * An array of links to install the plugins from wordpress.org.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	public $wpPluginLinks = [
		'aioseo'        => 'https://wordpress.org/plugins/all-in-one-seo-pack/',
		'optinMonster'  => 'https://wordpress.org/plugin/optinmonster/',
		'wpForms'       => 'https://wordpress.org/plugin/wpforms-lite/',
		'miLite'        => 'https://wordpress.org/plugin/google-analytics-for-wordpress/',
		'emLite'        => 'https://wordpress.org/plugin/google-analytics-dashboard-for-wp/',
		'wpMail'        => 'https://wordpress.org/plugin/wp-mail-smtp/',
		'rafflePress'   => 'https://wordpress.org/plugin/rafflepress/',
		'seedProd'      => 'https://wordpress.org/plugin/coming-soon/',
		'trustPulse'    => 'https://wordpress.org/plugin/trustpulse-api/',
		'instagramFeed' => 'https://wordpress.org/plugin/instagram-feed/',
		'facebookFeed'  => 'https://wordpress.org/plugin/custom-facebook-feed/',
		'twitterFeed'   => 'https://wordpress.org/plugin/custom-twitter-feeds/',
		'youTubeFeed'   => 'https://wordpress.org/plugin/feeds-for-youtube/',
		'pushEngage'    => 'https://wordpress.org/plugins/pushengage/',
		'sugarCalendar' => 'https://wordpress.org/plugins/sugar-calendar-lite/',
		'wpSimplePay'   => 'https://wordpress.org/plugins/stripe/',
		'searchWp'      => 'https://searchwp.com/',
		'affiliateWp'   => 'https://affiliatewp.com/',
		'wpcode'        => 'https://wordpress.org/plugins/insert-headers-and-footers/',
		'charitable'    => 'https://wordpress.org/plugins/charitable/',
		'duplicator'    => 'https://wordpress.org/plugins/duplicator/'
	];

	/**
	 * An array of slugs to check if plugins are activated.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	public $pluginSlugs = [
		'aioseo'                  => 'all-in-one-seo-pack/all_in_one_seo_pack.php',
		'aioseoPro'               => 'all-in-one-seo-pack-pro/all_in_one_seo_pack.php',
		'optinMonster'            => 'optinmonster/optin-monster-wp-api.php',
		'wpForms'                 => 'wpforms-lite/wpforms.php',
		'wpFormsPro'              => 'wpforms/wpforms.php',
		'miLite'                  => 'google-analytics-for-wordpress/googleanalytics.php',
		'miPro'                   => 'google-analytics-premium/googleanalytics-premium.php',
		'emLite'                  => 'google-analytics-dashboard-for-wp/gadwp.php',
		'emPro'                   => 'exactmetrics-premium/exactmetrics-premium.php',
		'wpMail'                  => 'wp-mail-smtp/wp_mail_smtp.php',
		'wpMailPro'               => 'wp-mail-smtp-pro/wp_mail_smtp.php',
		'rafflePress'             => 'rafflepress/rafflepress.php',
		'rafflePressPro'          => 'rafflepress-pro/rafflepress-pro.php',
		'seedProd'                => 'coming-soon/coming-soon.php',
		'seedProdPro'             => 'seedprod-coming-soon-pro-5/seedprod-coming-soon-pro-5.php',
		'trustPulse'              => 'trustpulse-api/trustpulse.php',
		'instagramFeed'           => 'instagram-feed/instagram-feed.php',
		'instagramFeedPro'        => 'instagram-feed-pro/instagram-feed.php',
		'facebookFeed'            => 'custom-facebook-feed/custom-facebook-feed.php',
		'facebookFeedPro'         => 'custom-facebook-feed-pro/custom-facebook-feed.php',
		'twitterFeed'             => 'custom-twitter-feeds/custom-twitter-feed.php',
		'twitterFeedPro'          => 'custom-twitter-feeds-pro/custom-twitter-feed.php',
		'youTubeFeed'             => 'feeds-for-youtube/youtube-feed.php',
		'youTubeFeedPro'          => 'youtube-feed-pro/youtube-feed.php',
		'pushEngage'              => 'pushengage/main.php',
		'sugarCalendar'           => 'sugar-calendar-lite/sugar-calendar-lite.php',
		'sugarCalendarPro'        => 'sugar-calendar/sugar-calendar.php',
		'wpSimplePay'             => 'stripe/stripe-checkout.php',
		'wpSimplePayPro'          => 'wp-simple-pay-pro-3/simple-pay.php',
		'easyDigitalDownloads'    => 'easy-digital-downloads/easy-digital-downloads.php',
		'easyDigitalDownloadsPro' => 'easy-digital-downloads-pro/easy-digital-downloads.php',
		'searchWp'                => 'searchwp/index.php',
		'affiliateWp'             => 'affiliate-wp/affiliate-wp.php',
		'wpcode'                  => 'insert-headers-and-footers/ihaf.php',
		'wpcodePro'               => 'wpcode-premium/wpcode.php',
		'charitable'              => 'charitable/charitable.php',
		'duplicator'              => 'duplicator/duplicator.php'
	];

	/**
	 * An array of links for admin settings.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	public $pluginAdminUrls = [
		'aioseo'                  => 'admin.php?page=aioseo-settings',
		'aioseoPro'               => 'admin.php?page=aioseo-settings',
		'optinMonster'            => 'admin.php?page=optin-monster-api-settings',
		'wpForms'                 => 'admin.php?page=wpforms-settings',
		'wpFormsPro'              => 'admin.php?page=wpforms-settings',
		'miLite'                  => 'admin.php?page=monsterinsights_settings#/',
		'miPro'                   => 'admin.php?page=monsterinsights_settings#/',
		'emLite'                  => 'admin.php?page=exactmetrics_settings#/',
		'emPro'                   => 'admin.php?page=exactmetrics_settings#/',
		'wpMail'                  => 'admin.php?page=wp-mail-smtp',
		'wpMailPro'               => 'admin.php?page=wp-mail-smtp',
		'seedProd'                => 'admin.php?page=seedprod_lite',
		'seedProdPro'             => 'admin.php?page=seedprod_pro',
		'rafflePress'             => 'admin.php?page=rafflepress_lite#/settings',
		'rafflePressPro'          => 'admin.php?page=rafflepress_pro#/settings',
		'trustPulse'              => 'admin.php?page=trustpulse',
		'instagramFeed'           => 'admin.php?page=sb-instagram-feed',
		'instagramFeedPro'        => 'admin.php?page=sb-instagram-feed',
		'facebookFeed'            => 'admin.php?page=cff-top',
		'facebookFeedPro'         => 'admin.php?page=cff-top',
		'twitterFeed'             => 'admin.php?page=ctf-settings',
		'twitterFeedPro'          => 'admin.php?page=ctf-settings',
		'youTubeFeed'             => 'admin.php?page=youtube-feed-settings',
		'youTubeFeedPro'          => 'admin.php?page=youtube-feed-settings',
		'pushEngage'              => 'admin.php?page=pushengage',
		'sugarCalendar'           => 'admin.php?page=sugar-calendar',
		'sugarCalendarPro'        => 'admin.php?page=sugar-calendar',
		'wpSimplePay'             => 'edit.php?post_type=simple-pay',
		'wpSimplePayPro'          => 'edit.php?post_type=simple-pay',
		'easyDigitalDownloads'    => 'edit.php?post_type=download&page=edd-settings',
		'easyDigitalDownloadsPro' => 'edit.php?post_type=download&page=edd-settings',
		'searchWp'                => 'options-general.php?page=searchwp',
		'affiliateWp'             => 'admin.php?page=affiliate-wp',
		'wpcode'                  => 'admin.php?page=wpcode',
		'wpcodePro'               => 'admin.php?page=wpcode',
		'charitable'              => 'admin.php?page=charitable-settings',
		'duplicator'              => 'admin.php?page=duplicator-settings'
	];
}Utils/PluginUpgraderSkin.php000064400000003223151536237000012134 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Utils;

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

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

/**
 * The PluginSilentUpgraderSkin class.
 *
 * @internal Please do not use this class outside of core AIOSEO development.
 *
 * @since 1.0.0
 */
class PluginUpgraderSkin extends \WP_Upgrader_Skin {
	/**
	 * Empty out the header of its HTML content and only check to see if it has
	 * been performed or not.
	 *
	 * @since 1.0.0
	 */
	public function header() {}

	/**
	 * Empty out the footer of its HTML contents.
	 *
	 * @since 1.0.0
	 */
	public function footer() {}

	/**
	 * Instead of outputting HTML for errors, just return them.
	 * AJAX request will just ignore it.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $errors Array of errors with the install process.
	 * @return void
	 */
	public function error( $errors ) {
		if ( ! empty( $errors ) ) {
			wp_send_json_error( $errors );
		}
	}

	/**
	 * Empty out JavaScript output that calls function to decrement the update counts.
	 *
	 * @since 1.0.0
	 *
	 * @param string $type Type of update count to decrement.
	 */
	public function decrement_update_count( $type ) {} // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable, PSR1.Methods.CamelCapsMethodName.NotCamelCaps

	/**
	 * @since 1.0.0
	 *
	 * @param  string $feedback Message data.
	 * @param  mixed  ...$args  Optional text replacements.
	 * @return void
	 */
	public function feedback( $feedback, ...$args ) {} // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
}Utils/VueSettings.php000064400000007143151536237000010644 0ustar00<?php
namespace AIOSEO\BrokenLinkChecker\Utils;

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

/**
 * Vue Settings for the user.
 *
 * @since 1.0.0
 */
class VueSettings {
	/**
	 * The name to lookup the settings with.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private $settingsName = '';

	/**
	 * The settings array.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private $settings = [];

	/**
	 * All the default settings.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private $defaults = [
		'toggledCards'    => [
			'generalSettings'  => true,
			'advancedSettings' => true
		],
		'toggledRadio'    => [],
		'tablePagination' => [
			'brokenLinks' => 20,
			'linksTable'  => 20
		]
	];

	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 *
	 * @param string $settings List of settings.
	 */
	public function __construct( $settings = '_aioseo_blc_settings' ) {
		$this->settingsName = $settings;

		$userMeta       = get_user_meta( get_current_user_id(), $settings, true );
		$this->settings = ! empty( $userMeta ) ? array_replace_recursive( $this->defaults, $userMeta ) : $this->defaults;
	}

	/**
	 * Retrieves all settings.
	 *
	 * @since 1.0.0
	 *
	 * @return array List of settings.
	 */
	public function all() {
		return array_replace_recursive( $this->defaults, $this->settings );
	}

	/**
	 * Retrieve a setting or null if missing.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $name      The name of the property that is missing on the class.
	 * @param  array  $arguments The arguments passed into the method.
	 * @return mixed             The value from the settings or default/null.
	 */
	public function __call( $name, $arguments = [] ) {
		$value = isset( $this->settings[ $name ] ) ? $this->settings[ $name ] : ( ! empty( $arguments[0] ) ? $arguments[0] : $this->getDefault( $name ) );

		return $value;
	}

	/**
	 * Retrieve a setting or null if missing.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $name The name of the property that is missing on the class.
	 * @return mixed        The value from the settings or default/null.
	 */
	public function __get( $name ) {
		$value = isset( $this->settings[ $name ] ) ? $this->settings[ $name ] : $this->getDefault( $name );

		return $value;
	}

	/**
	 * Sets the settings value and saves to the database.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $name  The name of the settings.
	 * @param  mixed  $value The value to set.
	 * @return void
	 */
	public function __set( $name, $value ) {
		$this->settings[ $name ] = $value;

		$this->update();
	}

	/**
	 * Checks if an settings is set or returns null if not.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $name The name of the settings.
	 * @return mixed        True or null.
	 */
	public function __isset( $name ) {
		return isset( $this->settings[ $name ] ) ? false === empty( $this->settings[ $name ] ) : null;
	}

	/**
	 * Unsets the settings value and saves to the database.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $name  The name of the settings.
	 * @return void
	 */
	public function __unset( $name ) {
		if ( ! isset( $this->settings[ $name ] ) ) {
			return;
		}

		unset( $this->settings[ $name ] );

		$this->update();
	}

	/**
	 * Gets the default value for a setting.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $name The settings name.
	 * @return mixed        The default value.
	 */
	public function getDefault( $name ) {
		return isset( $this->defaults[ $name ] ) ? $this->defaults[ $name ] : null;
	}

	/**
	 * Updates the settings in the database.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function update() {
		update_user_meta( get_current_user_id(), $this->settingsName, $this->settings );
	}
}init/init.php000064400000002117151536237000007166 0ustar00<?php
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! function_exists( 'aioseoMaybePluginIsDisabled' ) ) {
	/**
	 * Disable the AIOSEO if triggered externally.
	 *
	 * @since   4.1.5
	 * @version 4.5.0 Added the $file parameter and Lite check.
	 *
	 * @param  string $file The plugin file.
	 * @return bool         True if the plugin should be disabled.
	 */
	function aioseoMaybePluginIsDisabled( $file ) {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';
		if (
			'all-in-one-seo-pack/all_in_one_seo_pack.php' === plugin_basename( $file ) &&
			is_plugin_active( 'all-in-one-seo-pack-pro/all_in_one_seo_pack.php' )
		) {
			return true;
		}

		if ( ! defined( 'AIOSEO_DEV_VERSION' ) && ! isset( $_REQUEST['aioseo-dev'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			return false;
		}

		if ( ! isset( $_REQUEST['aioseo-disable-plugin'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			return false;
		}

		return true;
	}
}init/notices.php000064400000020417151536237000007672 0ustar00<?php
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable Generic.Arrays.DisallowLongArraySyntax.Found
if ( ! function_exists( 'aioseo_php_notice' ) ) {
	/**
	 * Display the notice after deactivation.
	 *
	 * @since 4.0.0
	 */
	function aioseo_php_notice() {
		$medium = false !== strpos( AIOSEO_PHP_VERSION_DIR, 'pro' ) ? 'proplugin' : 'liteplugin';
		?>
		<div class="notice notice-error">
			<p>
				<?php
				echo wp_kses(
					sprintf(
							// Translators: 1 - Opening HTML bold tag, 2 - Closing HTML bold tag, 3 - Opening HTML link tag, 4 - Closing HTML link tag.
						__( 'Your site is running an %1$sinsecure version%2$s of PHP that is no longer supported. Please contact your web hosting provider to update your PHP version or switch to a %3$srecommended WordPress hosting company%4$s.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'<strong>',
						'</strong>',
						'<a href="https://www.wpbeginner.com/wordpress-hosting/" target="_blank" rel="noopener noreferrer">',
						'</a>'
					),
					array(
						'a'      => array(
							'href'   => array(),
							'target' => array(),
							'rel'    => array(),
						),
						'strong' => array(),
					)
				);
				?>
				<br><br>
				<?php
				echo wp_kses(
					sprintf(
							// Translators: 1 - Opening HTML bold tag, 2 - Closing HTML bold tag, 3 - The short plugin name ("AIOSEO"), 4 - Opening HTML link tag, 5 - Closing HTML link tag.
						__( '%1$sNote:%2$s %3$s plugin is disabled on your site until you fix the issue. %4$sRead more for additional information.%5$s', 'all-in-one-seo-pack' ),
						'<strong>',
						'</strong>',
						'AIOSEO',
						'<a href="https://aioseo.com/docs/supported-php-version/?utm_source=WordPress&utm_medium=' . $medium . '&utm_campaign=outdated-php-notice" target="_blank" rel="noopener noreferrer">', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'</a>'
					),
					array(
						'a'      => array(
							'href'   => array(),
							'target' => array(),
							'rel'    => array(),
						),
						'strong' => array(),
					)
				);
				?>
			</p>
		</div>

		<?php
		// In case this is on plugin activation.
		if ( isset( $_GET['activate'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended	
			unset( $_GET['activate'] );
		}
	}
}

if ( ! function_exists( 'aioseo_php_notice_deprecated' ) ) {
	/**
	 * Display the notice after deactivation.
	 *
	 * @since 4.0.0
	 */
	function aioseo_php_notice_deprecated() {
		$medium = false !== strpos( AIOSEO_PHP_VERSION_DIR, 'pro' ) ? 'proplugin' : 'liteplugin';
		?>
		<div class="notice notice-error">
			<p>
				<?php
				echo wp_kses(
					sprintf(
						// Translators: 1 - Opening HTML bold tag, 2 - Closing HTML bold tag, 3 - Opening HTML link tag, 4 - Closing HTML link tag.
						__( 'Your site is running an %1$soutdated version%2$s of PHP that is no longer supported and may cause issues with %3$s. Please contact your web hosting provider to update your PHP version or switch to a %4$srecommended WordPress hosting company%5$s.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'<strong>',
						'</strong>',
						'<strong>AIOSEO</strong>',
						'<a href="https://www.wpbeginner.com/wordpress-hosting/" target="_blank" rel="noopener noreferrer">',
						'</a>'
					),
					array(
						'a'      => array(
							'href'   => array(),
							'target' => array(),
							'rel'    => array(),
						),
						'strong' => array(),
					)
				);
				?>
				<br><br>
				<?php
				echo wp_kses(
					sprintf(
						// phpcs:ignore Generic.Files.LineLength.MaxExceeded
						// Translators: 1 - Opening HTML bold tag, 2 - Closing HTML bold tag, 3 - The PHP version, 4 - The current year, 5 - The short plugin name ("AIOSEO"), 6 - Opening HTML link tag, 7 - Closing HTML link tag.
						__( '%1$sNote:%2$s Support for PHP %3$s will be discontinued in %4$s. After this, if no further action is taken, %5$s functionality will be disabled. %6$sRead more for additional information.%7$s', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'<strong>',
						'</strong>',
						PHP_VERSION,
						gmdate( 'Y' ),
						'AIOSEO',
						'<a href="https://aioseo.com/docs/supported-php-version/?utm_source=WordPress&utm_medium=' . $medium . '&utm_campaign=outdated-php-notice" target="_blank" rel="noopener noreferrer">', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'</a>'
					),
					array(
						'a'      => array(
							'href'   => array(),
							'target' => array(),
							'rel'    => array(),
						),
						'strong' => array(),
					)
				);
				?>
			</p>
		</div>

		<?php
		// In case this is on plugin activation.
		if ( isset( $_GET['activate'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended	
			unset( $_GET['activate'] );
		}
	}
}

if ( ! function_exists( 'aioseo_wordpress_notice' ) ) {
	/**
	 * Display the notice after deactivation.
	 *
	 * @since 4.1.2
	 */
	function aioseo_wordpress_notice() {
		$medium = false !== strpos( AIOSEO_PHP_VERSION_DIR, 'pro' ) ? 'proplugin' : 'liteplugin';
		?>
		<div class="notice notice-error">
			<p>
				<?php
				echo wp_kses(
					sprintf(
							// Translators: 1 - Opening HTML bold tag, 2 - Closing HTML bold tag, 3 - The plugin name ("All in One SEO").
						__( 'Your site is running an %1$sinsecure version%2$s of WordPress that is no longer supported. Please update your site to the latest version of WordPress in order to continue using %3$s.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'<strong>',
						'</strong>',
						'All in One SEO'
					),
					array(
						'strong' => array(),
					)
				);
				?>
				<br><br>
				<?php
				echo wp_kses(
					sprintf(
						// phpcs:ignore Generic.Files.LineLength.MaxExceeded
						// Translators: 1 - Opening HTML bold tag, 2 - Closing HTML bold tag, 3 - The short plugin name ("AIOSEO"), 4 - The current year, 5 - Opening HTML link tag, 6 - Closing HTML link tag.
						__( '%1$sNote:%2$s %3$s will be discontinuing support for WordPress versions older than version 5.7 by the end of %4$s. %5$sRead more for additional information.%6$s', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'<strong>',
						'</strong>',
						'AIOSEO',
						gmdate( 'Y' ),
						'<a href="https://aioseo.com/docs/update-wordpress/?utm_source=WordPress&utm_medium=' . $medium . '&utm_campaign=outdated-wordpress-notice" target="_blank" rel="noopener noreferrer">', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'</a>'
					),
					array(
						'a'      => array(
							'href'   => array(),
							'target' => array(),
							'rel'    => array(),
						),
						'strong' => array(),
					)
				);
				?>
			</p>
		</div>

		<?php
		// In case this is on plugin activation.
		if ( isset( $_GET['activate'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended	
			unset( $_GET['activate'] );
		}
	}
}

if ( ! function_exists( 'aioseo_lite_notice' ) ) {
	/**
	 * Display the notice after deactivation when Pro is still active
	 * and user wanted to activate the Lite version of the plugin.
	 *
	 * @since 4.0.0
	 */
	function aioseo_lite_notice() {

		global $aioseoLiteJustActivated, $aioseoLiteJustDeactivated;

		if (
			empty( $aioseoLiteJustActivated ) ||
			empty( $aioseoLiteJustDeactivated )
		) {
			return;
		}

		// Currently tried to activate Lite with Pro still active, so display the message.
		printf(
			'<div class="notice notice-warning">
				<p>%1$s</p>
				<p>%2$s</p>
			</div>',
			esc_html__( 'Heads up!', 'all-in-one-seo-pack' ),
			// Translators: 1 - "AIOSEO Pro", 2 - "AIOSEO Lite".
			sprintf( esc_html__( 'Your site already has %1$s activated. If you want to switch to %2$s, please first go to Plugins > Installed Plugins and deactivate %1$s. Then, you can activate %2$s.', 'all-in-one-seo-pack' ), 'AIOSEO Pro', 'AIOSEO Lite' ) // phpcs:ignore Generic.Files.LineLength.MaxExceeded
		);

		if ( isset( $_GET['activate'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended	
			unset( $_GET['activate'] );
		}

		unset( $aioseoLiteJustActivated, $aioseoLiteJustDeactivated );
	}
}AIOSEO.php000064400000031153151536241160006243 0ustar00<?php
namespace AIOSEO\Plugin {
	// Exit if accessed directly.
	if ( ! defined( 'ABSPATH' ) ) {
		exit;
	}

	/**
	 * Main AIOSEO class.
	 * We extend the abstract class as that one holds all the class properties.
	 *
	 * @since 4.0.0
	 */
	final class AIOSEO extends \AIOSEOAbstract {

		/**
		 * Holds the instance of the plugin currently in use.
		 *
		 * @since 4.0.0
		 *
		 * @var AIOSEO
		 */
		private static $instance;

		/**
		 * Plugin version for enqueueing, etc.
		 * The value is retrieved from the AIOSEO_VERSION constant.
		 *
		 * @since 4.0.0
		 *
		 * @var string
		 */
		public $version = '';

		/**
		 * Paid returns true, free (Lite) returns false.
		 *
		 * @since 4.0.0
		 *
		 * @var boolean
		 */
		public $pro = false;

		/**
		 * Returns 'Pro' or 'Lite'.
		 *
		 * @since 4.0.0
		 *
		 * @var boolean
		 */
		public $versionPath = 'Lite';

		/**
		 * Whether we're in a dev environment.
		 *
		 * @since 4.1.9
		 *
		 * @var bool
		 */
		public $isDev = false;

		/**
		 * Uninstall class instance.
		 *
		 * @since 4.8.1
		 *
		 * @var Common\Main\Uninstall
		 */
		public $uninstall = null;

		/**
		 * Main AIOSEO Instance.
		 *
		 * Insures that only one instance of AIOSEO exists in memory at any one
		 * time. Also prevents needing to define globals all over the place.
		 *
		 * @since 4.0.0
		 *
		 * @return AIOSEO The aioseo instance.
		 */
		public static function instance() {
			if ( null === self::$instance || ! self::$instance instanceof self ) {
				self::$instance = new self();

				self::$instance->init();

				// Load our addons on the action right after plugins_loaded.
				add_action( 'sanitize_comment_cookies', [ self::$instance, 'loadAddons' ] );
			}

			return self::$instance;
		}

		/**
		 * Initialize All in One SEO!
		 *
		 * @since 4.0.0
		 *
		 * @return void
		 */
		private function init() {
			$this->constants();
			$this->includes();
			$this->preLoad();
			if ( ! $this->core->isUninstalling() ) {
				$this->load();
			}
		}

		/**
		 * Setup plugin constants.
		 * All the path/URL related constants are defined in main plugin file.
		 *
		 * @since 4.0.0
		 *
		 * @return void
		 */
		private function constants() {
			$defaultHeaders = [
				'name'    => 'Plugin Name',
				'version' => 'Version',
			];

			$pluginData = get_file_data( AIOSEO_FILE, $defaultHeaders );

			$constants = [
				'AIOSEO_PLUGIN_BASENAME'   => plugin_basename( AIOSEO_FILE ),
				'AIOSEO_PLUGIN_NAME'       => $pluginData['name'],
				'AIOSEO_PLUGIN_SHORT_NAME' => 'AIOSEO',
				'AIOSEO_PLUGIN_URL'        => plugin_dir_url( AIOSEO_FILE ),
				'AIOSEO_VERSION'           => $pluginData['version'],
				'AIOSEO_MARKETING_URL'     => 'https://aioseo.com/',
				'AIOSEO_MARKETING_DOMAIN'  => 'aioseo.com'
			];

			foreach ( $constants as $constant => $value ) {
				if ( ! defined( $constant ) ) {
					define( $constant, $value );
				}
			}

			$this->version = AIOSEO_VERSION;
		}

		/**
		 * Including the new files with PHP 5.3 style.
		 *
		 * @since 4.0.0
		 *
		 * @return void
		 */
		private function includes() {
			$dependencies = [
				'/vendor/autoload.php'                                      => true,
				'/vendor/woocommerce/action-scheduler/action-scheduler.php' => true,
				'/vendor/jwhennessey/phpinsight/autoload.php'               => false,
				'/vendor_prefixed/monolog/monolog/src/Monolog/Logger.php'   => false
			];

			foreach ( $dependencies as $path => $shouldRequire ) {
				if ( ! file_exists( AIOSEO_DIR . $path ) ) {
					// Something is not right.
					status_header( 500 );
					wp_die( esc_html__( 'Plugin is missing required dependencies. Please contact support for more information.', 'all-in-one-seo-pack' ) );
				}

				if ( $shouldRequire ) {
					require_once AIOSEO_DIR . $path;
				}
			}

			$this->loadVersion();
		}

		/**
		 * Load the version of the plugin we are currently using.
		 *
		 * @since 4.1.9
		 *
		 * @return void
		 */
		private function loadVersion() {
			$proDir = is_dir( plugin_dir_path( AIOSEO_FILE ) . 'app/Pro' );

			if (
				! class_exists( '\Dotenv\Dotenv' ) ||
				! file_exists( AIOSEO_DIR . '/build/.env' )
			) {
				$this->pro         = $proDir;
				$this->versionPath = $proDir ? 'Pro' : 'Lite';

				return;
			}

			$dotenv = \Dotenv\Dotenv::createUnsafeImmutable( AIOSEO_DIR, '/build/.env' );
			$dotenv->load();

			$version = defined( 'AIOSEO_DEV_VERSION' )
				? strtolower( AIOSEO_DEV_VERSION )
				: strtolower( getenv( 'VITE_VERSION' ) );
			if ( ! empty( $version ) ) {
				$this->isDev = true;

				if ( file_exists( AIOSEO_DIR . '/build/filters.php' ) ) {
					require_once AIOSEO_DIR . '/build/filters.php';
				}
			}

			if ( $proDir && 'pro' === $version ) {
				$this->pro         = true;
				$this->versionPath = 'Pro';
			}
		}

		/**
		 * Runs before we load the plugin.
		 *
		 * @since 4.0.0
		 *
		 * @return void
		 */
		private function preLoad() {
			$this->core = new Common\Core\Core();

			$this->backwardsCompatibility();

			// Internal Options.
			$this->helpers                = $this->pro ? new Pro\Utils\Helpers() : new Lite\Utils\Helpers();
			$this->internalNetworkOptions = ( $this->pro && $this->helpers->isPluginNetworkActivated() ) ? new Pro\Options\InternalNetworkOptions() : new Common\Options\InternalNetworkOptions();
			$this->internalOptions        = $this->pro ? new Pro\Options\InternalOptions() : new Lite\Options\InternalOptions();
			$this->uninstall              = new Common\Main\Uninstall();

			// Run pre-updates.
			$this->preUpdates = $this->pro ? new Pro\Main\PreUpdates() : new Common\Main\PreUpdates();
		}

		/**
		 * To prevent errors and bugs from popping up,
		 * we will run this backwards compatibility method.
		 *
		 * @since 4.1.9
		 *
		 * @return void
		 */
		private function backwardsCompatibility() {
			$this->db           = $this->core->db;
			$this->cache        = $this->core->cache;
			$this->transients   = $this->cache;
			$this->cachePrune   = $this->core->cachePrune;
			$this->optionsCache = $this->core->optionsCache;
		}

		/**
		 * To prevent errors and bugs from popping up,
		 * we will run this backwards compatibility method.
		 *
		 * @since 4.2.0
		 *
		 * @return void
		 */
		private function backwardsCompatibilityLoad() {
			$this->postSettings->integrations = $this->standalone->pageBuilderIntegrations;
		}

		/**
		 * Load our classes.
		 *
		 * @since 4.0.0
		 *
		 * @return void
		 */
		public function load() {
			// Load external translations if this is a Pro install.
			if ( $this->pro ) {
				$translations = new Pro\Main\Translations(
					'plugin',
					'all-in-one-seo-pack',
					'https://aioseo.com/aioseo-plugin/all-in-one-seo-pack/packages.json'
				);
				$translations->init();

				$translations = new Pro\Main\Translations(
					'plugin',
					'aioseo-pro',
					'https://aioseo.com/aioseo-plugin/aioseo-pro/packages.json'
				);
				$translations->init();
			}

			$this->addons             = $this->pro ? new Pro\Utils\Addons() : new Common\Utils\Addons();
			$this->features           = $this->pro ? new Pro\Utils\Features() : new Common\Utils\Features();
			$this->tags               = $this->pro ? new Pro\Utils\Tags() : new Common\Utils\Tags();
			$this->blocks             = new Common\Utils\Blocks();
			$this->breadcrumbs        = $this->pro ? new Pro\Breadcrumbs\Breadcrumbs() : new Common\Breadcrumbs\Breadcrumbs();
			$this->dynamicBackup      = $this->pro ? new Pro\Options\DynamicBackup() : new Common\Options\DynamicBackup();
			$this->options            = $this->pro ? new Pro\Options\Options() : new Lite\Options\Options();
			$this->networkOptions     = ( $this->pro && $this->helpers->isPluginNetworkActivated() ) ? new Pro\Options\NetworkOptions() : new Common\Options\NetworkOptions();
			$this->dynamicOptions     = $this->pro ? new Pro\Options\DynamicOptions() : new Common\Options\DynamicOptions();
			$this->backup             = new Common\Utils\Backup();
			$this->access             = $this->pro ? new Pro\Utils\Access() : new Common\Utils\Access();
			$this->usage              = $this->pro ? new Pro\Admin\Usage() : new Lite\Admin\Usage();
			$this->siteHealth         = $this->pro ? new Pro\Admin\SiteHealth() : new Common\Admin\SiteHealth();
			$this->networkLicense     = $this->pro && $this->helpers->isPluginNetworkActivated() ? new Pro\Admin\NetworkLicense() : null;
			$this->license            = $this->pro ? new Pro\Admin\License() : null;
			$this->autoUpdates        = $this->pro ? new Pro\Admin\AutoUpdates() : null;
			$this->updates            = $this->pro ? new Pro\Main\Updates() : new Common\Main\Updates();
			$this->meta               = $this->pro ? new Pro\Meta\Meta() : new Common\Meta\Meta();
			$this->social             = $this->pro ? new Pro\Social\Social() : new Common\Social\Social();
			$this->robotsTxt          = new Common\Tools\RobotsTxt();
			$this->htaccess           = new Common\Tools\Htaccess();
			$this->term               = $this->pro ? new Pro\Admin\Term() : null;
			$this->notices            = $this->pro ? new Pro\Admin\Notices\Notices() : new Lite\Admin\Notices\Notices();
			$this->wpNotices          = new Common\Admin\Notices\WpNotices();
			$this->admin              = $this->pro ? new Pro\Admin\Admin() : new Lite\Admin\Admin();
			$this->networkAdmin       = $this->helpers->isPluginNetworkActivated() ? ( $this->pro ? new Pro\Admin\NetworkAdmin() : new Common\Admin\NetworkAdmin() ) : null;
			$this->activate           = $this->pro ? new Pro\Main\Activate() : new Common\Main\Activate();
			$this->conflictingPlugins = $this->pro ? new Pro\Admin\ConflictingPlugins() : new Common\Admin\ConflictingPlugins();
			$this->migration          = $this->pro ? new Pro\Migration\Migration() : new Common\Migration\Migration();
			$this->importExport       = $this->pro ? new Pro\ImportExport\ImportExport() : new Common\ImportExport\ImportExport();
			$this->sitemap            = $this->pro ? new Pro\Sitemap\Sitemap() : new Common\Sitemap\Sitemap();
			$this->htmlSitemap        = new Common\Sitemap\Html\Sitemap();
			$this->templates          = $this->pro ? new Pro\Utils\Templates() : new Common\Utils\Templates();
			$this->categoryBase       = new Common\Main\CategoryBase();
			$this->postSettings       = $this->pro ? new Pro\Admin\PostSettings() : new Lite\Admin\PostSettings();
			$this->standalone         = new Common\Standalone\Standalone();
			$this->searchStatistics   = $this->pro ? new Pro\SearchStatistics\SearchStatistics() : new Common\SearchStatistics\SearchStatistics();
			$this->slugMonitor        = new Common\Admin\SlugMonitor();
			$this->schema             = $this->pro ? new Pro\Schema\Schema() : new Common\Schema\Schema();
			$this->actionScheduler    = new Common\Utils\ActionScheduler();
			$this->seoRevisions       = $this->pro ? new Pro\SeoRevisions\SeoRevisions() : new Common\SeoRevisions\SeoRevisions();
			$this->ai                 = $this->pro ? new Pro\Ai\Ai() : new Common\Ai\Ai();
			$this->filters            = $this->pro ? new Pro\Main\Filters() : new Lite\Main\Filters();
			$this->crawlCleanup       = new Common\QueryArgs\CrawlCleanup();
			$this->searchCleanup      = new Common\SearchCleanup\SearchCleanup();
			$this->emailReports       = new Common\EmailReports\EmailReports();
			$this->thirdParty         = new Common\ThirdParty\ThirdParty();
			$this->writingAssistant   = new Common\WritingAssistant\WritingAssistant();
			$this->llms               = new Common\Llms\Llms();

			if ( ! wp_doing_ajax() && ! wp_doing_cron() ) {
				$this->rss       = new Common\Rss();
				$this->main      = $this->pro ? new Pro\Main\Main() : new Common\Main\Main();
				$this->head      = $this->pro ? new Pro\Main\Head() : new Common\Main\Head();
				$this->dashboard = $this->pro ? new Pro\Admin\Dashboard() : new Common\Admin\Dashboard();
				$this->api       = $this->pro ? new Pro\Api\Api() : new Lite\Api\Api();
				$this->help      = new Common\Help\Help();
			}

			$this->backwardsCompatibilityLoad();

			add_action( 'init', [ $this, 'loadInit' ], 999 );
		}

		/**
		 * Things that need to load after init.
		 *
		 * @since 4.0.0
		 *
		 * @return void
		 */
		public function loadInit() {
			$this->settings = new Common\Utils\VueSettings( '_aioseo_settings' );
			$this->sitemap->init();

			// We call this again to reset any post types/taxonomies that have not yet been set up.
			$this->dynamicOptions->refresh();
		}

		/**
		 * Loads our addons.
		 *
		 * Runs right after the plugins_loaded hook.
		 *
		 * @since 4.0.0
		 *
		 * @return void
		 */
		public function loadAddons() {
			do_action( 'aioseo_loaded' );
		}
	}
}

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

	/**
	 * The function which returns the one AIOSEO instance.
	 *
	 * @since 4.0.0
	 *
	 * @return AIOSEO\Plugin\AIOSEO The instance.
	 */
	function aioseo() {
		return AIOSEO\Plugin\AIOSEO::instance();
	}
}AIOSEOAbstract.php000064400000024773151536241160007741 0ustar00<?php
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Abstract class holding the class properties of our main AIOSEO class.
 *
 * @since 4.2.7
 */
abstract class AIOSEOAbstract {
	/**
	 * Core class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Core\Core
	 */
	public $core = null;

	/**
	 * Helpers class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Lite\Utils\Helpers|\AIOSEO\Plugin\Pro\Utils\Helpers
	 */
	public $helpers = null;

	/**
	 * InternalNetworkOptions class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Options\InternalNetworkOptions|\AIOSEO\Plugin\Pro\Options\InternalNetworkOptions
	 */
	public $internalNetworkOptions = null;

	/**
	 * InternalOptions class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Lite\Options\InternalOptions|\AIOSEO\Plugin\Pro\Options\InternalOptions
	 */
	public $internalOptions = null;

	/**
	 * PreUpdates class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Main\PreUpdates|\AIOSEO\Plugin\Pro\Main\PreUpdates
	 */
	public $preUpdates = null;

	/**
	 * Db class instance.
	 * This prop is set for backwards compatibility.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\Database
	 */
	public $db = null;

	/**
	 * Transients class instance.
	 * This prop is set for backwards compatibility.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\Cache
	 */
	public $transients = null;

	/**
	 * OptionsCache class instance.
	 * This prop is set for backwards compatibility.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Options\Cache
	 */
	public $optionsCache = null;

	/**
	 * PostSettings class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Lite\Admin\PostSettings|\AIOSEO\Plugin\Pro\Admin\PostSettings
	 */
	public $postSettings = null;

	/**
	 * Standalone class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Standalone\Standalone
	 */
	public $standalone = null;

	/**
	 * Search Statistics class instance.
	 *
	 * @since 4.3.0
	 *
	 * @var \AIOSEO\Plugin\Pro\SearchStatistics\SearchStatistics
	 */
	public $searchStatistics = null;

	/**
	 * Tags class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Pro\Utils\Tags
	 */
	public $tags = null;

	/**
	 * Addons class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\Blocks
	 */
	public $blocks = null;

	/**
	 * Breadcrumbs class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Breadcrumbs\Breadcrumbs|\AIOSEO\Plugin\Pro\Breadcrumbs\Breadcrumbs
	 */
	public $breadcrumbs = null;

	/**
	 * DynamicBackup class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Options\DynamicBackup|\AIOSEO\Plugin\Pro\Options\DynamicBackup
	 */
	public $dynamicBackup = null;

	/**
	 * NetworkOptions class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Options\NetworkOptions|\AIOSEO\Plugin\Pro\Options\NetworkOptions
	 */
	public $networkOptions = null;

	/**
	 * Backup class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\Backup
	 */
	public $backup = null;

	/**
	 * Access class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\Access|\AIOSEO\Plugin\Pro\Utils\Access
	 */
	public $access = null;

	/**
	 * NetworkLicense class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var null|\AIOSEO\Plugin\Pro\Admin\NetworkLicense
	 */
	public $networkLicense = null;

	/**
	 * License class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var null|\AIOSEO\Plugin\Pro\Admin\License
	 */
	public $license = null;

	/**
	 * Updates class isntance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Main\Updates|\AIOSEO\Plugin\Pro\Main\Updates
	 */
	public $updates = null;

	/**
	 * Meta class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Pro\Meta\Meta
	 */
	public $meta = null;

	/**
	 * Social class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Social\Social|\AIOSEO\Plugin\Pro\Social\Social
	 */
	public $social = null;

	/**
	 * RobotsTxt class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Tools\RobotsTxt
	 */
	public $robotsTxt = null;

	/**
	 * Htaccess class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Tools\Htaccess
	 */
	public $htaccess = null;

	/**
	 * Term class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var null|\AIOSEO\Plugin\Pro\Admin\Term
	 */
	public $term = null;

	/**
	 * Notices class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Lite\Admin\Notices\Notices|\AIOSEO\Plugin\Pro\Admin\Notices\Notices
	 */
	public $notices = null;

	/**
	 * WpNotices class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Admin\Notices\WpNotices
	 */
	public $wpNotices = null;

	/**
	 * Admin class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Lite\Admin\Admin|\AIOSEO\Plugin\Pro\Admin\Admin
	 */
	public $admin = null;

	/**
	 * NetworkAdmin class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Admin\NetworkAdmin|\AIOSEO\Plugin\Pro\Admin\NetworkAdmin
	 */
	public $networkAdmin = null;

	/**
	 * Activate class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Main\Activate|\AIOSEO\Plugin\Pro\Main\Activate
	 */
	public $activate = null;

	/**
	 * ConflictingPlugins class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Admin\ConflictingPlugins|\AIOSEO\Plugin\Pro\Admin\ConflictingPlugins
	 */
	public $conflictingPlugins = null;

	/**
	 * Migration class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Pro\Migration\Migration
	 */
	public $migration = null;

	/**
	 * ImportExport class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\ImportExport\ImportExport
	 */
	public $importExport = null;

	/**
	 * Sitemap class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Sitemap\Sitemap|\AIOSEO\Plugin\Pro\Sitemap\Sitemap
	 */
	public $sitemap = null;

	/**
	 * HtmlSitemap class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Sitemap\Html\Sitemap
	 */
	public $htmlSitemap = null;

	/**
	 * CategoryBase class instance.
	 *
	 * @since   4.2.7
	 * @version 4.7.1 Moved from Pro to Common.
	 *
	 * @var null|\AIOSEO\Plugin\Common\Main\CategoryBase
	 */
	public $categoryBase = null;

	/**
	 * SlugMonitor class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Admin\SlugMonitor
	 */
	public $slugMonitor = null;

	/**
	 * Schema class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Pro\Schema\Schema
	 */
	public $schema = null;

	/**
	 * Rss class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Rss
	 */
	public $rss = null;

	/**
	 * Main class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Main\Main|\AIOSEO\Plugin\Pro\Main\Main
	 */
	public $main = null;

	/**
	 * Head class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Main\Head|\AIOSEO\Plugin\Pro\Main\Head
	 */
	public $head = null;

	/**
	 * Dashboard class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Admin\Dashboard|\AIOSEO\Plugin\Pro\Admin\Dashboard
	 */
	public $dashboard = null;

	/**
	 * API class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Lite\Api\Api|\AIOSEO\Plugin\Pro\Api\Api
	 */
	public $api = null;

	/**
	 * Help class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Help\Help
	 */
	public $help = null;

	/**
	 * Settings class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\VueSettings
	 */
	public $settings = null;

	/**
	 * Cache class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\Cache
	 */
	public $cache = null;

	/**
	 * CachePrune class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\CachePrune
	 */
	public $cachePrune = null;

	/**
	 * Addons class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Pro\Utils\Addons|\AIOSEO\Plugin\Common\Utils\Addons
	 */
	public $addons = null;

	/**
	 * Addons class instance.
	 *
	 * @since 4.3.0
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\Features|\AIOSEO\Plugin\Pro\Utils\Features
	 */
	public $features = null;

	/**
	 * Options class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Options\Options|\AIOSEO\Plugin\Pro\Options\Options
	 */
	public $options = null;

	/**
	 * DynamicOptions class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Options\DynamicOptions|\AIOSEO\Plugin\Pro\Options\DynamicOptions
	 */
	public $dynamicOptions = null;

	/**
	 * Usage class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Lite\Admin\Usage|\AIOSEO\Plugin\Pro\Admin\Usage
	 */
	public $usage = null;

	/**
	 * SiteHealth class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Admin\SiteHealth|\AIOSEO\Plugin\Pro\Admin\SiteHealth
	 */
	public $siteHealth = null;

	/**
	 * AutoUpdates class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Pro\Admin\AutoUpdates
	 */
	public $autoUpdates = null;

	/**
	 * Templates class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\Templates|\AIOSEO\Plugin\Pro\Utils\Templates
	 */
	public $templates = null;

	/**
	 * Filters class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Lite\Main\Filters|\AIOSEO\Plugin\Pro\Main\Filters
	 */
	public $filters = null;

	/**
	 * ActionScheduler class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\ActionScheduler
	 */
	public $actionScheduler = null;

	/**
	 * AI class instance.
	 *
	 * @since 4.3.3
	 *
	 * @var null|\AIOSEO\Plugin\Pro\Ai\Ai
	 */
	public $ai = null;

	/**
	 * SeoRevisions class instance.
	 *
	 * @since 4.4.0
	 *
	 * @var null|\AIOSEO\Plugin\Pro\SeoRevisions\SeoRevisions
	 */
	public $seoRevisions = null;

	/**
	 * Crawl Cleanup class instance.
	 *
	 * @since 4.5.8
	 *
	 * @var \AIOSEO\Plugin\Common\QueryArgs\CrawlCleanup
	 */
	public $crawlCleanup = null;

	/**
	 * Search Cleanup class instance.
	 *
	 * @since 4.8.0
	 *
	 * @var \AIOSEO\Plugin\Common\SearchCleanup\SearchCleanup
	 */
	public $searchCleanup = null;

	/**
	 * EmailReports class instance.
	 *
	 * @since 4.7.2
	 *
	 * @var null|\AIOSEO\Plugin\Common\EmailReports\EmailReports
	 */
	public $emailReports = null;

	/**
	 * ThirdParty class instance.
	 *
	 * @since 4.7.6
	 *
	 * @var \AIOSEO\Plugin\Common\ThirdParty\ThirdParty
	 */
	public $thirdParty = null;

	/**
	 * WritingAssistant class instance.
	 *
	 * @since 4.7.4
	 *
	 * @var \AIOSEO\Plugin\Common\WritingAssistant\WritingAssistant
	 */
	public $writingAssistant = null;

	/**
	 * Llms class instance.
	 *
	 * @since 4.8.4
	 *
	 * @var \AIOSEO\Plugin\Common\Llms\Llms
	 */
	public $llms = null;
}Common/Admin/Admin.php000064400000115413151536241160010576 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

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

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

/**
 * Abstract class that Pro and Lite both extend.
 *
 * @since 4.0.0
 */
class Admin {
	/**
	 * The page slug for the sidebar.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $pageSlug = 'aioseo';

	/**
	 * Sidebar menu name.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	public $menuName = 'All in One SEO';

	/**
	 * An array of pages for the admin.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $pages = [];

	/**
	 * The current page we are enqueuing.
	 *
	 * @since 4.1.3
	 *
	 * @var string
	 */
	protected $currentPage;

	/**
	 * An array of items to add to the admin bar.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $adminBarMenuItems = [];

	/**
	 * An array of asset slugs to use.
	 *
	 * @since 4.1.9
	 *
	 * @var array
	 */
	protected $assetSlugs = [
		'plugins' => 'src/app/plugins/main.js',
		'pages'   => 'src/vue/pages/{page}/main.js'
	];

	/**
	 * Connect class instance.
	 *
	 * @since 4.4.3
	 *
	 * @var \AIOSEO\Plugin\Lite\Admin\Connect|null
	 */
	public $connect = null;

	/**
	 * Pointers class instance.
	 *
	 * @since 4.8.3
	 *
	 * @var \AIOSEO\Plugin\Common\Admin\Pointers|null
	 */
	public $pointers = null;

	/**
	 * Whether we're editing a post or term.
	 *
	 * @since 4.7.7
	 *
	 * @var bool
	 */
	private $isEditor = false;

	/**
	 * Construct method.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		new Pointers();
		new SeoAnalysis();
		new WritingAssistant();

		include_once ABSPATH . 'wp-admin/includes/plugin.php';
		if (
			is_network_admin() &&
			! is_plugin_active_for_network( plugin_basename( AIOSEO_FILE ) )
		) {
			return;
		}

		add_action( 'aioseo_unslash_escaped_data_posts', [ $this, 'unslashEscapedDataPosts' ] );

		add_action( 'wp_ajax_aioseo-dismiss-active-menu-tooltip', [ $this, 'dismissActiveMenuTooltips' ] );

		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		add_filter( 'language_attributes', [ $this, 'alwaysAddHtmlDirAttribute' ], 3000 );

		add_action( 'sanitize_comment_cookies', [ $this, 'init' ], 20 );

		add_action( 'admin_menu', [ $this, 'deactivationSurvey' ], 100 );
	}

	/**
	 * Runs the deactivation survey.
	 *
	 * @since 4.5.5
	 *
	 * @return void
	 */
	public function deactivationSurvey() {
		new DeactivationSurvey( AIOSEO_PLUGIN_NAME, dirname( plugin_basename( AIOSEO_FILE ) ) );
	}

	/**
	 * Always add dir attribute to HTML tag.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $output The HTML language attribute.
	 * @return string         The possibly modified HTML language attribute.
	 */
	public function alwaysAddHtmlDirAttribute( $output ) {
		if ( is_rtl() || preg_match( '/dir=[\'"](ltr|rtl|auto)[\'"]/i', (string) $output ) ) {
			return $output;
		}

		return 'dir="ltr" ' . $output;
	}

	/**
	 * Initialize the admin.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function init() {
		// Add the admin bar menu.
		if ( is_user_logged_in() && ( ! is_multisite() || ! is_network_admin() ) ) {
			add_action( 'admin_bar_menu', [ $this, 'adminBarMenu' ], 1000 );
		}

		if ( is_admin() ) {
			// Add the menu to the sidebar.
			add_action( 'admin_menu', [ $this, 'addMenu' ] );
			add_action( 'admin_menu', [ $this, 'hideScheduledActionsMenu' ], 99999 );

			// Add Score to Publish metabox.
			add_action( 'post_submitbox_misc_actions', [ $this, 'addPublishScore' ] );

			add_action( 'admin_init', [ $this, 'addPluginScripts' ] );

			// Add redirects messages to trashed posts.
			add_filter( 'bulk_post_updated_messages', [ $this, 'appendTrashedMessage' ], PHP_INT_MAX );

			$this->registerLinkFormatHooks();

			add_action( 'admin_footer', [ $this, 'addAioseoModalPortal' ] );
		}

		$this->loadTextDomain();

		add_action( 'init', [ $this, 'setPages' ] );
	}

	/**
	 * Sets our menu pages.
	 * It is important this runs AFTER we've loaded the text domain.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function setPages() {
		// TODO: Remove this after a couple months.
		$newIndicator = '<span class="aioseo-menu-new-indicator">&nbsp;NEW!</span>';

		$this->pages = [
			$this->pageSlug            => [
				'menu_title' => esc_html__( 'Dashboard', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-settings'          => [
				'menu_title' => is_network_admin()
					? esc_html__( 'Network Settings', 'all-in-one-seo-pack' )
					: esc_html__( 'General Settings', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-search-appearance' => [
				'menu_title' => esc_html__( 'Search Appearance', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-social-networks'   => [
				'menu_title' => esc_html__( 'Social Networks', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-sitemaps'          => [
				'menu_title' => esc_html__( 'Sitemaps', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-link-assistant'    => [
				'menu_title' => esc_html__( 'Link Assistant', 'all-in-one-seo-pack' ),
				'capability' => 'aioseo_link_assistant_settings',
				'parent'     => $this->pageSlug
			],
			'aioseo-redirects'         => [
				'menu_title' => esc_html__( 'Redirects', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-local-seo'         => [
				'menu_title' => esc_html__( 'Local SEO', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-seo-analysis'      => [
				'menu_title' => esc_html__( 'SEO Analysis', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-search-statistics' => [
				'menu_title' => esc_html__( 'Search Statistics', 'all-in-one-seo-pack' ) . $newIndicator,
				'page_title' => esc_html__( 'Search Statistics', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-tools'             => [
				'menu_title' => is_network_admin()
					? esc_html__( 'Network Tools', 'all-in-one-seo-pack' )
					: esc_html__( 'Tools', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-feature-manager'   => [
				'menu_title' => esc_html__( 'Feature Manager', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-monsterinsights'   => [
				'menu_title'          => esc_html__( 'Analytics', 'all-in-one-seo-pack' ),
				'parent'              => 'aioseo-monsterinsights',
				'hide_admin_bar_menu' => true
			],
			'aioseo-about'             => [
				'menu_title' => esc_html__( 'About Us', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-seo-revisions'     => [
				'menu_title'          => esc_html__( 'SEO Revisions', 'all-in-one-seo-pack' ),
				'parent'              => 'aioseo-seo-revisions',
				'hide_admin_bar_menu' => true
			],
		];
	}

	/**
	 * Registers our custom link format hooks.
	 *
	 * @since 4.0.16
	 *
	 * @return void
	 */
	private function registerLinkFormatHooks() {
		if ( apply_filters( 'aioseo_disable_link_format', false ) ) {
			return;
		}

		add_action( 'wp_enqueue_editor', [ $this, 'addClassicLinkFormatScript' ], 999999 );

		global $wp_version; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if ( version_compare( $wp_version, '5.3', '>=' ) || is_plugin_active( 'gutenberg/gutenberg.php' ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			add_action( 'current_screen', [ $this, 'addGutenbergLinkFormatScript' ] );
			add_action( 'enqueue_block_editor_assets', [ $this, 'enqueueBlockEditorLinkFormat' ] );
		}
	}

	/**
	 * Enqueues the link format script for the Block Editor.
	 *
	 * @since 4.1.8
	 *
	 * @return void
	 */
	public function enqueueBlockEditorLinkFormat() {
		wp_enqueue_script( 'aioseo-link-format' );

		if ( ! wp_style_is( 'aioseo-link-format', 'enqueued' ) ) {
			wp_enqueue_style(
				'aioseo-link-format',
				aioseo()->core->assets->getAssetsPath( false ) . '/link-format/link-format-block.css',
				[],
				aioseo()->version
			);
		}
	}

	/**
	 * Enqueues the plugins script.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addPluginScripts() {
		global $pagenow;

		if ( 'plugins.php' !== $pagenow && 'plugin-install.php' !== $pagenow ) {
			return;
		}

		aioseo()->core->assets->load( $this->assetSlugs['plugins'], [], [
			'basename'           => AIOSEO_PLUGIN_BASENAME,
			'conflictingPlugins' => aioseo()->conflictingPlugins->getConflictingPluginSlugs()
		], 'aioseoPlugins' );
	}

	/**
	 * Enqueues our link format for the Classic Editor.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addClassicLinkFormatScript() {
		wp_deregister_script( 'wplink' );

		wp_enqueue_script(
			'wplink',
			aioseo()->core->assets->getAssetsPath( false ) . '/link-format/link-format-classic.js',
			[ 'jquery', 'wp-a11y' ],
			aioseo()->version,
			true
		);

		wp_localize_script(
			'wplink',
			'aioseoL10n',
			[
				'title'          => esc_html__( 'Insert/edit link', 'all-in-one-seo-pack' ),
				'update'         => esc_html__( 'Update', 'all-in-one-seo-pack' ),
				'save'           => esc_html__( 'Add Link', 'all-in-one-seo-pack' ),
				'noTitle'        => esc_html__( '(no title)', 'default' ), // phpcs:ignore AIOSEO.Wp.I18n.TextDomainMismatch, WordPress.WP.I18n.TextDomainMismatch
				'labelTitle'     => esc_html__( 'Title', 'all-in-one-seo-pack' ),
				'noMatchesFound' => esc_html__( 'No results found.', 'all-in-one-seo-pack' ),
				'linkSelected'   => esc_html__( 'Link selected.', 'all-in-one-seo-pack' ),
				'linkInserted'   => esc_html__( 'Link has been inserted.', 'all-in-one-seo-pack' ),
				// Translators: 1 - HTML whitespace character, 2 - Opening HTML code tag, 3 - Closing HTML code tag.
				'noFollow'       => sprintf( esc_html__( '%1$sAdd %2$srel="nofollow"%3$s to link', 'all-in-one-seo-pack' ), '&nbsp;', '<code>', '</code>' ),
				// Translators: 1 - HTML whitespace character, 2 - Opening HTML code tag, 3 - Closing HTML code tag.
				'sponsored'      => sprintf( esc_html__( '%1$sAdd %2$srel="sponsored"%3$s to link', 'all-in-one-seo-pack' ), '&nbsp;', '<code>', '</code>' ),
				// Translators: 1 - HTML whitespace character, 2 - Opening HTML code tag, 3 - Closing HTML code tag.
				'ugc'            => sprintf( esc_html__( '%1$sAdd %2$srel="UGC"%3$s to link', 'all-in-one-seo-pack' ), '&nbsp;', '<code>', '</code>' ),
				// Translators: Minimum input length in characters to start searching posts in the "Insert/edit link" modal.
				'minInputLength' => (int) _x( '3', 'minimum input length for searching post links', 'all-in-one-seo-pack' ),
			]
		);
	}

	/**
	 * Registers our link format for the Block Editor.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addGutenbergLinkFormatScript() {
		if ( ! aioseo()->helpers->isScreenBase( 'post' ) ) {
			return;
		}

		$linkFormat = 'block';
		if ( is_plugin_active( 'gutenberg/gutenberg.php' ) ) {
			$data = get_plugin_data( WP_CONTENT_DIR . '/plugins/gutenberg/gutenberg.php', false, false );
			if ( version_compare( $data['Version'], '7.4.0', '<' ) ) {
				$linkFormat = 'block-old';
			}
		} else {
			if ( version_compare( get_bloginfo( 'version' ), '5.4', '<' ) ) {
				$linkFormat = 'block-old';
			}
		}

		wp_register_script(
			'aioseo-link-format',
			aioseo()->core->assets->getAssetsPath( false ) . "link-format/link-format-$linkFormat.js",
			[
				'wp-blocks',
				'wp-i18n',
				'wp-element',
				'wp-plugins',
				'wp-components',
				'wp-edit-post',
				'wp-api',
				'wp-editor',
				'wp-hooks',
				'lodash'
			],
			aioseo()->version,
			true
		);
	}

	/**
	 * Adds All in One SEO to the Admin Bar.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function adminBarMenu() {
		if ( false === apply_filters( 'aioseo_show_in_admin_bar', true ) ) {
			return;
		}

		$firstPageSlug = $this->getFirstAvailablePageSlug();
		if ( ! $firstPageSlug ) {
			return;
		}

		$classes           = is_admin()
			? 'wp-core-ui wp-ui-notification aioseo-menu-notification-counter'
			: 'aioseo-menu-notification-counter aioseo-menu-notification-counter-frontend';
		$notificationCount = count( Models\Notification::getAllActiveNotifications() );
		$htmlCount         = 10 > $notificationCount ? $notificationCount : '!';
		$htmlCount         = $htmlCount ? "<div class=\"{$classes}\">" . $htmlCount . '</div>' : '';
		$htmlCount        .= '<div id="aioseo-menu-new-notifications"></div>';

		$this->adminBarMenuItems[] = [
			'id'    => 'aioseo-main',
			'title' => '<div class="ab-item aioseo-logo svg"></div><span class="text">' . esc_html__( 'SEO', 'all-in-one-seo-pack' ) . '</span>' . wp_kses_post( $htmlCount ),
			'href'  => esc_url( admin_url( 'admin.php?page=' . $firstPageSlug ) )
		];

		if ( $notificationCount ) {
			$this->adminBarMenuItems[] = [
				'parent' => 'aioseo-main',
				'id'     => 'aioseo-notifications',
				'title'  => esc_html__( 'Notifications', 'all-in-one-seo-pack' ) . ' <div class="aioseo-menu-notification-indicator"></div>',
				'href'   => admin_url( 'admin.php?page=' . $firstPageSlug . '&notifications=true' ),
			];
		}

		$this->adminBarMenuItems[] = aioseo()->standalone->seoPreview->getAdminBarMenuItemNode();

		$currentScreen = aioseo()->helpers->getCurrentScreen();
		if (
			is_admin() &&
			( 'post' === $currentScreen->base || 'term' === $currentScreen->base )
		) {
			$this->isEditor = true;
		}

		$htmlSitemapRequested = aioseo()->htmlSitemap->isDedicatedPage;
		if ( $htmlSitemapRequested || ! is_admin() || $this->isEditor ) {
			$this->addPageAnalyzerMenuItems();
		}

		if ( $htmlSitemapRequested ) {
			global $wp_admin_bar; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			$wp_admin_bar->remove_node( 'edit' ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		}

		$this->addSettingsMenuItems();
		$this->addEditSeoMenuItem();

		// Actually add in the menu bar items.
		$this->addAdminBarMenuItems();
	}

	/**
	 * Actually adds the menu items to the admin bar.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function addAdminBarMenuItems() {
		global $wp_admin_bar; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		foreach ( $this->adminBarMenuItems as $item ) {
			$wp_admin_bar->add_menu( $item ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		}
	}

	/**
	 * Adds the Analyze this Page menu item to the admin bar.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addPageAnalyzerMenuItems() {
		$url           = '';
		$currentScreen = aioseo()->helpers->getCurrentScreen();
		if (
			is_singular() ||
			( is_admin() && 'post' === $currentScreen->base )
		) {
			$post = aioseo()->helpers->getPost();
			if ( is_a( $post, 'WP_Post' ) && 'publish' === $post->post_status && '' !== $post->post_name ) {
				$url = get_permalink( $post->ID );
			}
		}

		if (
			is_category() ||
			is_tag() ||
			is_tax() ||
			( is_admin() && 'term' === $currentScreen->base )
		) {
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended, HM.Security.NonceVerification.Recommended
			$termId = ! empty( $_REQUEST['tag_ID'] ) ? intval( $_REQUEST['tag_ID'] ) : 0;
			$term   = is_admin() && $termId ? get_term( $termId ) : get_queried_object();
			if ( is_a( $term, 'WP_Term' ) ) {
				$url = get_term_link( $term );
			}
		}

		if ( ! $url ) {
			return;
		}

		$this->adminBarMenuItems[] = [
			'id'     => 'aioseo-analyze-page',
			'parent' => 'aioseo-main',
			'title'  => esc_html__( 'Analyze this page', 'all-in-one-seo-pack' )
		];

		$url = urlencode( $url );

		$submenuItems = [
			[
				'id'    => 'aioseo-analyze-page-pagespeed',
				'title' => esc_html__( 'Google Page Speed Test', 'all-in-one-seo-pack' ),
				'href'  => 'https://pagespeed.web.dev/report?url=' . $url
			],
			[
				'id'    => 'aioseo-analyze-page-rich-results-test',
				'title' => esc_html__( 'Google Rich Results Test', 'all-in-one-seo-pack' ),
				'href'  => 'https://search.google.com/test/rich-results?url=' . $url
			],
			[
				'id'    => 'aioseo-analyze-page-schema-org-validator',
				'title' => esc_html__( 'Schema.org Validator', 'all-in-one-seo-pack' ),
				'href'  => 'https://validator.schema.org/?url=' . $url
			],
			[
				'id'    => 'aioseo-analyze-page-inlinks',
				'title' => esc_html__( 'Inbound Links', 'all-in-one-seo-pack' ),
				'href'  => 'https://search.google.com/search-console/links/drilldown?resource_id=' . urlencode( get_option( 'siteurl' ) ) . '&type=EXTERNAL&target=' . $url . '&domain='
			],
			[
				'id'    => 'aioseo-analyze-page-facebookdebug',
				'title' => esc_html__( 'Facebook Debugger', 'all-in-one-seo-pack' ),
				'href'  => 'https://developers.facebook.com/tools/debug/?q=' . $url
			],
			[
				'id'    => 'aioseo-external-tools-linkedin-post-inspector',
				'title' => esc_html__( 'LinkedIn Post Inspector', 'all-in-one-seo-pack' ),
				'href'  => "https://www.linkedin.com/post-inspector/inspect/$url"
			],
			[
				'id'    => 'aioseo-analyze-page-htmlvalidation',
				'title' => esc_html__( 'HTML Validator', 'all-in-one-seo-pack' ),
				'href'  => '//validator.w3.org/check?uri=' . $url
			],
			[
				'id'    => 'aioseo-analyze-page-cssvalidation',
				'title' => esc_html__( 'CSS Validator', 'all-in-one-seo-pack' ),
				'href'  => '//jigsaw.w3.org/css-validator/validator?uri=' . $url
			]
		];

		foreach ( $submenuItems as $item ) {
			$this->adminBarMenuItems[] = [
				'parent' => 'aioseo-analyze-page',
				'id'     => $item['id'],
				'title'  => $item['title'],
				'href'   => $item['href'],
				'meta'   => [ 'target' => '_blank' ]
			];
		}
	}

	/**
	 * Adds the current post menu items to the admin bar.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	protected function addEditSeoMenuItem() {
		// Don't show if we're on the home page and the home page is the latest posts or if we're not in a singular context.
		if ( aioseo()->helpers->isDynamicHomePage() || ! is_singular() ) {
			return;
		}

		$post = aioseo()->helpers->getPost();
		if ( empty( $post ) ) {
			return;
		}

		$href = get_edit_post_link( $post->ID );
		if ( ! $href ) {
			return;
		}

		$this->adminBarMenuItems[] = [
			'id'     => 'aioseo-edit-' . $post->ID,
			'parent' => 'aioseo-main',
			'title'  => esc_html__( 'Edit SEO', 'all-in-one-seo-pack' ),
			'href'   => $href . '#aioseo-settings',
		];
	}

	/**
	 * Add the settings items to the menu bar.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function addSettingsMenuItems() {
		if ( ! is_admin() || $this->isEditor ) {
			$this->adminBarMenuItems[] = [
				'id'     => 'aioseo-settings-main',
				'parent' => 'aioseo-main',
				// Translators: This is an action link users can click to open the General Settings menu.
				'title'  => esc_html__( 'SEO Settings', 'all-in-one-seo-pack' )
			];
		}

		$parent = is_admin() && ! $this->isEditor ? 'aioseo-main' : 'aioseo-settings-main';
		foreach ( $this->pages as $id => $page ) {
			// Remove page from admin bar menu.
			if ( ! empty( $page['hide_admin_bar_menu'] ) ) {
				continue;
			}

			if ( ! current_user_can( $this->getPageRequiredCapability( $id ) ) ) {
				continue;
			}

			$this->adminBarMenuItems[] = [
				'id'     => $id,
				'parent' => $parent,
				'title'  => $page['menu_title'],
				'href'   => esc_url( admin_url( 'admin.php?page=' . $id ) )
			];
		}
	}

	/**
	 * Get the required capability for given admin page.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $pageSlug The slug of the page.
	 * @return string           The required capability.
	 */
	public function getPageRequiredCapability( $pageSlug ) { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return apply_filters( 'aioseo_manage_seo', 'aioseo_manage_seo' );
	}

	/**
	 * Add the menu inside of WordPress.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addMenu() {
		$this->addMainMenu();

		foreach ( $this->pages as $slug => $page ) {
			$hook = add_submenu_page(
				$page['parent'],
				! empty( $page['page_title'] ) ? $page['page_title'] : $page['menu_title'],
				$page['menu_title'],
				$this->getPageRequiredCapability( $slug ),
				$slug,
				[ $this, 'page' ]
			);

			add_action( "load-{$hook}", [ $this, 'hooks' ] );
		}

		if ( ! current_user_can( $this->getPageRequiredCapability( $this->pageSlug ) ) ) {
			remove_submenu_page( $this->pageSlug, $this->pageSlug );
		}

		global $submenu;
		if ( current_user_can( $this->getPageRequiredCapability( 'aioseo-redirects' ) ) ) {
			$submenu['tools.php'][] = [
				esc_html__( 'Redirection Manager', 'all-in-one-seo-pack' ),
				$this->getPageRequiredCapability( 'aioseo-redirects' ),
				admin_url( '/admin.php?page=aioseo-redirects' )
			];
		}

		if ( current_user_can( $this->getPageRequiredCapability( 'aioseo-search-appearance' ) ) ) {
			$submenu['users.php'][] = [
				esc_html__( 'Author SEO', 'all-in-one-seo-pack' ),
				$this->getPageRequiredCapability( 'aioseo-search-appearance' ),
				admin_url( '/admin.php?page=aioseo-search-appearance/#author-seo' )
			];
		}

		// We use the global submenu, because we are adding an external link here.
		$count         = count( Models\Notification::getAllActiveNotifications() );
		$firstPageSlug = $this->getFirstAvailablePageSlug();
		if (
			$count &&
			! empty( $submenu[ $this->pageSlug ] ) &&
			! empty( $firstPageSlug )
		) {
			array_unshift( $submenu[ $this->pageSlug ], [
				esc_html__( 'Notifications', 'all-in-one-seo-pack' ) . '<div class="aioseo-menu-notification-indicator"></div>',
				$this->getPageRequiredCapability( $firstPageSlug ),
				admin_url( 'admin.php?page=' . $firstPageSlug . '&notifications=true' )
			] );
		}
	}

	/**
	 * Add the main menu.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $slug which slug to use.
	 * @return void
	 */
	protected function addMainMenu( $slug = 'aioseo' ) {
		add_menu_page(
			$this->menuName,
			$this->menuName,
			$this->getPageRequiredCapability( $slug ),
			$slug,
			'__return_true',
			'data:image/svg+xml;base64,' . base64_encode( aioseo()->helpers->logo( 16, 16, '#A0A5AA' ) ),
			'80.01234567890'
		);
	}

	/**
	 * Hides the Scheduled Actions menu.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	public function hideScheduledActionsMenu() {
		if ( ! apply_filters( 'aioseo_hide_action_scheduler_menu', true ) ) {
			return;
		}

		global $submenu;
		if ( ! isset( $submenu['tools.php'] ) ) {
			return;
		}

		foreach ( $submenu['tools.php'] as $index => $props ) {
			if ( ! empty( $props[2] ) && 'action-scheduler' === $props[2] ) {
				unset( $submenu['tools.php'][ $index ] );

				return;
			}
		}
	}

	/**
	 * Output the HTML for the page.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function page() {
		echo '<div id="aioseo-app">';
		aioseo()->templates->getTemplate( 'admin/settings-page.php' );
		echo '</div>';

		if ( aioseo()->standalone->flyoutMenu->isEnabled() ) {
			echo '<div id="aioseo-flyout-menu"></div>';
		}
	}

	/**
	 * Hooks for loading our pages.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function hooks() {
		$currentScreen = aioseo()->helpers->getCurrentScreen();
		global $admin_page_hooks; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		if ( ! is_object( $currentScreen ) || empty( $currentScreen->id ) || empty( $admin_page_hooks ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			return;
		}

		$pages = [
			'dashboard',
			'settings',
			'search-appearance',
			'social-networks',
			'sitemaps',
			'link-assistant',
			'redirects',
			'local-seo',
			'seo-analysis',
			'search-statistics',
			'tools',
			'feature-manager',
			'monsterinsights',
			'about',
			'seo-revisions'
		];

		foreach ( $pages as $page ) {
			$addScripts = false;

			if ( 'toplevel_page_aioseo' === $currentScreen->id ) {
				$addScripts = true;
			}

			if ( ! empty( $admin_page_hooks['aioseo'] ) && $currentScreen->id === $admin_page_hooks['aioseo'] ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				$addScripts = true;
			}

			if ( strpos( $currentScreen->id, 'aioseo-' . $page ) !== false ) {
				$addScripts = true;
			}

			if ( ! $addScripts ) {
				continue;
			}

			if ( 'tools' === $page ) {
				$this->checkForRedirects();
			}

			// Redirect our Analytics page to the appropriate plugin page.
			if ( 'monsterinsights' === $page ) {

				$pluginData = aioseo()->helpers->getPluginData();

				if (
					(
						$pluginData['miLite']['activated'] ||
						$pluginData['miPro']['activated']
					) &&
					function_exists( 'MonsterInsights' ) &&
					function_exists( 'monsterinsights_get_ua' )
				) {
					if ( (bool) monsterinsights_get_ua() ) {
						wp_safe_redirect( $pluginData['miLite']['adminUrl'] );
						exit;
					}
				}

				if (
					(
						$pluginData['emLite']['activated'] ||
						$pluginData['emPro']['activated']
					) &&
					function_exists( 'ExactMetrics' ) &&
					function_exists( 'exactmetrics_get_ua' )
				) {
					if ( (bool) exactmetrics_get_ua() ) {
						wp_safe_redirect( $pluginData['emLite']['adminUrl'] );
						exit;
					}
				}
			}

			// We don't want any plugin adding notices to our screens. Let's clear them out here.
			remove_all_actions( 'admin_notices' );
			remove_all_actions( 'network_admin_notices' );
			remove_all_actions( 'all_admin_notices' );
			remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );

			$this->currentPage = $page;
			add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAssets' ], 11 );
			add_action( 'admin_enqueue_scripts', [ aioseo()->filters, 'dequeueThirdPartyAssets' ], 99999 );
			add_action( 'admin_enqueue_scripts', [ aioseo()->filters, 'dequeueThirdPartyAssetsEarly' ], 0 );

			add_action( 'in_admin_footer', [ $this, 'addFooterPromotion' ] );
			add_filter( 'admin_footer_text', [ $this, 'addFooterText' ] );

			// Only enqueue the media library if we need it in our module
			if ( in_array( $page, [
				'social-networks',
				'search-appearance',
				'local-seo'
			], true ) ) {
				wp_enqueue_media();
			}

			break;
		}
	}

	/**
	 * Checks whether the current page is an AIOSEO menu page.
	 *
	 * @since 4.2.0
	 *
	 * @return bool Whether the current page is an AIOSEO menu page.
	 */
	public function isAioseoScreen() {
		$currentScreen = aioseo()->helpers->getCurrentScreen();
		if ( empty( $currentScreen->id ) ) {
			return false;
		}

		$adminPages = array_keys( $this->pages );
		$adminPages = array_map( function( $slug ) {
			if ( 'aioseo' === $slug ) {
				return 'toplevel_page_aioseo';
			}

			return 'all-in-one-seo_page_' . $slug;
		}, $adminPages );

		return in_array( $currentScreen->id, $adminPages, true );
	}

	/**
	 * Enqueue admin assets for the current page.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	public function enqueueAssets() {
		$page = str_replace( '{page}', $this->currentPage, $this->assetSlugs['pages'] );
		aioseo()->core->assets->load( $page, [], aioseo()->helpers->getVueData( $this->currentPage ) );
	}

	/**
	 * Add footer text to the WordPress admin screens.
	 *
	 * @since 4.0.0
	 *
	 * @return string The footer text.
	 */
	public function addFooterText() {
		$linkText = esc_html__( 'Give us a 5-star rating!', 'all-in-one-seo-pack' );
		$href     = 'https://wordpress.org/support/plugin/all-in-one-seo-pack/reviews/?filter=5#new-post';

		$link1 = sprintf(
			'<a href="%1$s" target="_blank" title="%2$s">&#9733;&#9733;&#9733;&#9733;&#9733;</a>',
			$href,
			$linkText
		);

		$link2 = sprintf(
			'<a href="%1$s" target="_blank" title="%2$s">WordPress.org</a>',
			$href,
			$linkText
		);

		printf(
			// Translators: 1 - The plugin name ("All in One SEO"), - 2 - This placeholder will be replaced with star icons, - 3 - "WordPress.org" - 4 - The plugin name ("All in One SEO").
			esc_html__( 'Please rate %1$s %2$s on %3$s to help us spread the word. Thank you!', 'all-in-one-seo-pack' ),
			sprintf( '<strong>%1$s</strong>', esc_html( AIOSEO_PLUGIN_NAME ) ),
			wp_kses_post( $link1 ),
			wp_kses_post( $link2 )
		);

		// Stop WP Core from outputting its version number and instead add both theirs & ours.
		global $wp_version; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		printf(
			wp_kses_post( '<p class="alignright">%1$s</p>' ),
			sprintf(
				// Translators: 1 - WP Core version number, 2 - AIOSEO version number.
				esc_html__( 'WordPress %1$s | AIOSEO %2$s', 'all-in-one-seo-pack' ),
				esc_html( $wp_version ), // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				esc_html( AIOSEO_VERSION )
			)
		);

		remove_filter( 'update_footer', 'core_update_footer' );

		return '';
	}

	/**
	 * Renders the SEO Score button in the Publish metabox.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post The post object.
	 * @return void
	 */
	public function addPublishScore( $post ) {
		$pageAnalysisCapability = aioseo()->access->hasCapability( 'aioseo_page_analysis' );
		if ( empty( $pageAnalysisCapability ) ) {
			return;
		}

		if ( aioseo()->helpers->isTruSeoEligible( $post->ID ) ) {
			$score = (int) Models\Post::getPost( $post->ID )->seo_score;
			$path  = 'M10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47716 0 0 4.47715 0 10C0 15.5228 4.47716 20 10 20ZM8.40767 3.65998C8.27222 3.45353 8.02129 3.357 7.79121 3.43828C7.52913 3.53087 7.27279 3.63976 7.02373 3.76429C6.80511 3.87361 6.69542 4.12332 6.74355 4.36686L6.91501 5.23457C6.95914 5.45792 6.86801 5.68459 6.69498 5.82859C6.42152 6.05617 6.16906 6.31347 5.94287 6.59826C5.80229 6.77526 5.58046 6.86908 5.36142 6.82484L4.51082 6.653C4.27186 6.60473 4.02744 6.71767 3.92115 6.94133C3.86111 7.06769 3.80444 7.19669 3.75129 7.32826C3.69815 7.45983 3.64929 7.59212 3.60464 7.72495C3.52562 7.96007 3.62107 8.21596 3.82396 8.35351L4.54621 8.84316C4.73219 8.96925 4.82481 9.19531 4.80234 9.42199C4.7662 9.78671 4.76767 10.1508 4.80457 10.5089C4.82791 10.7355 4.73605 10.9619 4.55052 11.0886L3.82966 11.5811C3.62734 11.7193 3.53274 11.9753 3.61239 12.2101C3.70314 12.4775 3.80985 12.7391 3.93188 12.9932C4.03901 13.2163 4.28373 13.3282 4.5224 13.2791L5.37279 13.1042C5.59165 13.0591 5.8138 13.1521 5.95491 13.3287C6.17794 13.6077 6.43009 13.8653 6.70918 14.0961C6.88264 14.2396 6.97459 14.4659 6.93122 14.6894L6.76282 15.5574C6.71551 15.8013 6.8262 16.0507 7.04538 16.1591C7.16921 16.2204 7.29563 16.2782 7.42457 16.3324C7.55352 16.3867 7.68316 16.4365 7.81334 16.4821C8.19418 16.6154 8.72721 16.1383 9.1213 15.7855C9.31563 15.6116 9.4355 15.3654 9.43677 15.1018C9.43677 15.1004 9.43678 15.099 9.43678 15.0976L9.43677 13.6462C9.43677 13.6308 9.43736 13.6155 9.43852 13.6004C8.27454 13.3165 7.40918 12.248 7.40918 10.9732V9.43198C7.40918 9.31483 7.50224 9.21986 7.61706 9.21986H8.338V7.70343C8.338 7.49405 8.50433 7.32432 8.70952 7.32432C8.9147 7.32432 9.08105 7.49405 9.08105 7.70343V9.21986H11.0316V7.70343C11.0316 7.49405 11.1979 7.32432 11.4031 7.32432C11.6083 7.32432 11.7746 7.49405 11.7746 7.70343V9.21986H12.4956C12.6104 9.21986 12.7034 9.31483 12.7034 9.43198V10.9732C12.7034 12.2883 11.7825 13.3838 10.5628 13.625C10.5631 13.632 10.5632 13.6391 10.5632 13.6462L10.5632 15.0914C10.5632 15.36 10.6867 15.6107 10.8868 15.7853C11.2879 16.1351 11.8302 16.6079 12.2088 16.4742C12.4708 16.3816 12.7272 16.2727 12.9762 16.1482C13.1949 16.0389 13.3046 15.7891 13.2564 15.5456L13.085 14.6779C13.0408 14.4545 13.132 14.2278 13.305 14.0838C13.5785 13.8563 13.8309 13.599 14.0571 13.3142C14.1977 13.1372 14.4195 13.0434 14.6385 13.0876L15.4892 13.2595C15.7281 13.3077 15.9725 13.1948 16.0788 12.9711C16.1389 12.8448 16.1955 12.7158 16.2487 12.5842C16.3018 12.4526 16.3507 12.3204 16.3953 12.1875C16.4744 11.9524 16.3789 11.6965 16.176 11.559L15.4537 11.0693C15.2678 10.9432 15.1752 10.7171 15.1976 10.4905C15.2338 10.1258 15.2323 9.76167 15.1954 9.40357C15.1721 9.17699 15.2639 8.95062 15.4495 8.82387L16.1703 8.33141C16.3726 8.1932 16.4672 7.93715 16.3876 7.70238C16.2968 7.43495 16.1901 7.17337 16.0681 6.91924C15.961 6.69615 15.7162 6.58422 15.4776 6.63333L14.6272 6.8083C14.4083 6.85333 14.1862 6.76033 14.0451 6.58377C13.822 6.30474 13.5699 6.04713 13.2908 5.81632C13.1173 5.67287 13.0254 5.44652 13.0688 5.22301L13.2372 4.35503C13.2845 4.11121 13.1738 3.86179 12.9546 3.75334C12.8308 3.69208 12.7043 3.63424 12.5754 3.58002C12.4465 3.52579 12.3168 3.47593 12.1866 3.43037C11.9562 3.34974 11.7055 3.44713 11.5707 3.65416L11.0908 4.39115C10.9672 4.58093 10.7457 4.67543 10.5235 4.65251C10.1661 4.61563 9.80932 4.61712 9.45837 4.65477C9.23633 4.6786 9.01448 4.58486 8.89027 4.39554L8.40767 3.65998Z'; // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			?>
			<div class="misc-pub-section aioseo-score-settings">
				<svg viewBox="0 0 20 20" width="20" height="20" xmlns="http://www.w3.org/2000/svg">
					<path fill-rule="evenodd" clip-rule="evenodd" d="<?php echo esc_attr( $path ); ?>" fill="#82878C" />
				</svg>
				<span>
					<?php
						echo sprintf(
							// Translators: 1 - The short plugin name ("AIOSEO").
							esc_html__( '%1$s Score', 'all-in-one-seo-pack' ),
							esc_html( AIOSEO_PLUGIN_SHORT_NAME )
						);
					?>
				</span>
				<div id="aioseo-post-settings-sidebar-button" class="aioseo-score-button classic-editor <?php echo esc_attr( $this->getScoreClass( $score ) ); ?>">
					<span id="aioseo-post-score"><?php echo esc_attr( $score . '/100' ); ?></span>
				</div>
			</div>
			<?php
		}
	}

	/**
	 * Check the query args to see if we need to redirect to an external URL.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	protected function checkForRedirects() {}

	/**
	 * Starts the cleaning procedure to fix escaped, corrupted data.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	public function scheduleUnescapeData() {
		aioseo()->core->cache->update( 'unslash_escaped_data_posts', time(), WEEK_IN_SECONDS );
		aioseo()->actionScheduler->scheduleSingle( 'aioseo_unslash_escaped_data_posts', 120 );
	}

	/**
	 * Unlashes corrupted escaped data in posts.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	public function unslashEscapedDataPosts() {
		$postsToUnslash = apply_filters( 'aioseo_debug_unslash_escaped_posts', 200 );
		$timeStarted    = gmdate( 'Y-m-d H:i:s', aioseo()->core->cache->get( 'unslash_escaped_data_posts' ) );

		$posts = aioseo()->core->db->start( 'aioseo_posts' )
			->select( '*' )
			->whereRaw( "updated < '$timeStarted'" )
			->orderBy( 'updated ASC' )
			->limit( $postsToUnslash )
			->run()
			->result();

		if ( empty( $posts ) ) {
			aioseo()->core->cache->delete( 'unslash_escaped_data_posts' );

			return;
		}

		aioseo()->actionScheduler->scheduleSingle( 'aioseo_unslash_escaped_data_posts', 120, [], true );

		foreach ( $posts as $post ) {
			$aioseoPost = Models\Post::getPost( $post->post_id );
			foreach ( $this->getColumnsToUnslash() as $columnName ) {
				// Remove backslashes but preserve encoded unicode characters in JSON data.
				$aioseoPost->$columnName = aioseo()->helpers->pregReplace( '/\\\(?![uU][+]?[a-zA-Z0-9]{4})/', '', $post->$columnName );
			}
			$aioseoPost->images          = null;
			$aioseoPost->image_scan_date = null;
			$aioseoPost->videos          = null;
			$aioseoPost->video_scan_date = null;
			$aioseoPost->save();
		}
	}

	/**
	 * Returns a list of names of database columns that should be unslashed when cleaning the corrupted data.
	 *
	 * @since 4.1.2
	 *
	 * @return array The list of column names.
	 */
	protected function getColumnsToUnslash() {
		return [
			'title',
			'description',
			'keywords',
			'keyphrases',
			'page_analysis',
			'canonical_url',
			'og_title',
			'og_description',
			'og_image_custom_url',
			'og_image_custom_fields',
			'og_video',
			'og_custom_url',
			'og_article_section',
			'og_article_tags',
			'twitter_title',
			'twitter_description',
			'twitter_image_custom_url',
			'twitter_image_custom_fields',
			'schema_type_options',
			'local_seo',
			'options'
		];
	}

	/**
	 * Get the first available page item for the current user.
	 *
	 * @since 4.1.3
	 *
	 * @return bool|string The page slug.
	 */
	public function getFirstAvailablePageSlug() {
		foreach ( $this->pages as $slug => $page ) {
			// Ignore other pages.
			if ( $this->pageSlug !== $page['parent'] ) {
				continue;
			}

			if ( current_user_can( $this->getPageRequiredCapability( $slug ) ) ) {
				return $slug;
			}
		}

		return false;
	}

	/**
	 * Appends a message to the default WordPress "trashed" message.
	 *
	 * @since 4.1.2
	 *
	 * @param  array $messages The original messages.
	 * @return array           The modified messages.
	 */
	public function appendTrashedMessage( $messages ) {
		// Let advanced users override this.

		if ( apply_filters( 'aioseo_redirects_disable_trashed_posts_suggestions', false ) ) {
			return $messages;
		}

		if ( function_exists( 'aioseoRedirects' ) && aioseoRedirects()->options->monitor->trash ) {
			return $messages;
		}

		if ( empty( $_GET['ids'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended  
			return $messages;
		}

		$ids = array_map( 'intval', explode( ',', sanitize_text_field( wp_unslash( $_GET['ids'] ) ) ) ); // phpcs:ignore HM.Security.NonceVerification.Recommended, HM.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended, Generic.Files.LineLength.MaxExceeded

		$posts = [];
		foreach ( $ids as $id ) {
			// We need to clone the post here so we can get a real permalink for the post even if it is not published already.
			$post = aioseo()->helpers->getPost( $id );
			if ( ! is_a( $post, 'WP_Post' ) ) {
				continue;
			}

			$post->post_status = 'publish';
			$post->post_name   = sanitize_title(
				$post->post_name ? $post->post_name : $post->post_title,
				$post->ID
			);

			$posts[] = [
				'url'    => str_replace( '__trashed', '', get_permalink( $post ) ),
				'target' => '/',
				'type'   => 301
			];
		}

		if ( empty( $posts ) ) {
			return $messages;
		}

		$url         = aioseo()->slugMonitor->manualRedirectUrl( $posts );
		$addRedirect = _n( 'Add Redirect to improve SEO', 'Add Redirects to improve SEO', count( $posts ), 'all-in-one-seo-pack' );

		$postType = get_post_type( $id );
		if ( empty( $messages[ $postType ]['trashed'] ) ) {
			$messages[ $postType ]['trashed'] = $messages['post']['trashed'];
		}

		$messages[ $postType ]['trashed'] = $messages[ $postType ]['trashed'] . '&nbsp;<a href="' . $url . '" class="aioseo-redirects-trashed-post">' . $addRedirect . '</a> |';

		return $messages;
	}

	/**
	* Get the class name for the Score button.
	* Depending on the score the button should have different color.
	*
	* @since 4.0.0
	*
	* @param  int    $score The content to retrieve from the remote URL.
	* @return string        The class name for Score button.
	*/
	private function getScoreClass( $score ) {
		$scoreClass = 50 < $score ? 'score-orange' : 'score-red';

		if ( 0 === $score ) {
			$scoreClass = 'score-none';
		}

		if ( $score >= 80 ) {
			$scoreClass = 'score-green';
		}

		return $scoreClass;
	}

	/**
	 * Loads the plugin text domain.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function loadTextDomain() {
		aioseo()->helpers->loadTextDomain( 'all-in-one-seo-pack' );
	}

	/**
	 * Add the div for the modal portal.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	public function addAioseoModalPortal() {
		echo '<div id="aioseo-modal-portal"></div>';
	}

	/**
	 * Outputs the element we can mount our footer promotion standalone Vue app on.
	 * Also enqueues the assets.
	 *
	 * @since   4.3.6
	 * @version 4.4.3
	 *
	 * @return void
	 */
	public function addFooterPromotion() {
		echo wp_kses_post( '<div id="aioseo-footer-links"></div>' );

		aioseo()->core->assets->load( 'src/vue/standalone/footer-links/main.js' );
	}
}Common/Admin/ConflictingPlugins.php000064400000011151151536241160013341 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Checks for conflicting plugins.
 *
 * @since 4.0.0
 */
class ConflictingPlugins {
	/**
	 * A list of conflicting plugin slugs.
	 *
	 * @since 4.5.1
	 *
	 * @var array
	 */
	protected $conflictingPluginSlugs = [
		// Note: We should NOT add Jetpack here since they automatically disable their SEO module when ours is active.
		'wordpress-seo',
		'seo-by-rank-math',
		'wp-seopress',
		'autodescription',
		'slim-seo',
		'squirrly-seo',
		'google-sitemap-generator',
		'xml-sitemap-feed',
		'www-xml-sitemap-generator-org',
		'google-sitemap-plugin',
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		// We don't want to trigger our notices when not in the admin.
		if ( ! is_admin() ) {
			return;
		}

		add_action( 'init', [ $this, 'init' ], 20 );
	}

	/**
	 * Initialize the conflicting plugins check.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function init() {
		if ( ! current_user_can( 'activate_plugins' ) ) {
			return;
		}

		$conflictingPlugins = $this->getAllConflictingPlugins();

		$notification = Models\Notification::getNotificationByName( 'conflicting-plugins' );
		if ( empty( $conflictingPlugins ) ) {
			if ( ! $notification->exists() ) {
				return;
			}

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

			return;
		}

		aioseo()->notices->conflictingPlugins( $conflictingPlugins );
	}

	/**
	 * Get a list of all conflicting plugins.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of conflicting plugins.
	 */
	public function getAllConflictingPlugins() {
		$conflictingSeoPlugins     = $this->getConflictingPlugins( 'seo' );
		$conflictingSitemapPlugins = [];

		if (
			aioseo()->options->sitemap->general->enable ||
			aioseo()->options->sitemap->rss->enable
		) {
			$conflictingSitemapPlugins = $this->getConflictingPlugins( 'sitemap' );
		}

		return array_merge( $conflictingSeoPlugins, $conflictingSitemapPlugins );
	}

	/**
	 * Get a list of conflicting plugins for AIOSEO.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $type A type to look for.
	 * @return array        An array of conflicting plugins.
	 */
	public function getConflictingPlugins( $type ) {
		$activePlugins = wp_get_active_and_valid_plugins();
		if ( is_multisite() ) {
			$activePlugins = array_merge( $activePlugins, wp_get_active_network_plugins() );
		}

		$conflictingPlugins = [];
		switch ( $type ) {
			// Note: We should NOT add Jetpack here since they automatically disable their SEO module when ours is active.
			case 'seo':
				$conflictingPlugins = [
					'Rank Math SEO'     => 'seo-by-rank-math/rank-math.php',
					'Rank Math SEO Pro' => 'seo-by-rank-math-pro/rank-math-pro.php',
					'SEOPress'          => 'wp-seopress/seopress.php',
					'The SEO Framework' => 'autodescription/autodescription.php',
					'Yoast SEO'         => 'wordpress-seo/wp-seo.php',
					'Yoast SEO Premium' => 'wordpress-seo-premium/wp-seo-premium.php'
				];
				break;
			case 'sitemap':
				$conflictingPlugins = [
					'Google XML Sitemaps'          => 'google-sitemap-generator/sitemap.php',
					'Google XML Sitemap Generator' => 'www-xml-sitemap-generator-org/www-xml-sitemap-generator-org.php',
					'Sitemap by BestWebSoft'       => 'google-sitemap-plugin/google-sitemap-plugin.php',
					'XML Sitemap & Google News'    => 'xml-sitemap-feed/xml-sitemap.php'
				];
				break;
		}

		$activeConflictingPlugins = [];
		foreach ( $activePlugins as $pluginFilePath ) {
			foreach ( $conflictingPlugins as $index => $pluginPath ) {
				if ( false !== strpos( $pluginFilePath, $pluginPath ) ) {
					$activeConflictingPlugins[ $index ] = $pluginPath;
				}
			}
		}

		return $activeConflictingPlugins;
	}

	/**
	 * Deactivate conflicting plugins.
	 *
	 * @since 4.5.1
	 *
	 * @param array $types An array of types to look for.
	 * @return void
	 */
	public function deactivateConflictingPlugins( $types ) {
		$seo     = in_array( 'seo', $types, true ) ? $this->getConflictingPlugins( 'seo' ) : [];
		$sitemap = in_array( 'sitemap', $types, true ) ? $this->getConflictingPlugins( 'sitemap' ) : [];
		$plugins = array_merge(
			$seo,
			$sitemap
		);

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

		foreach ( $plugins as $pluginPath ) {
			if ( is_plugin_active( $pluginPath ) ) {
				deactivate_plugins( $pluginPath );
			}
		}
	}

	/**
	 * Get a list of conflicting plugin slugs.
	 *
	 * @since 4.5.1
	 *
	 * @return array An array of conflicting plugin slugs.
	 */
	public function getConflictingPluginSlugs() {
		return $this->conflictingPluginSlugs;
	}
}Common/Admin/Dashboard.php000064400000010721151536241160011431 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

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

/**
 * Class that holds our dashboard widget.
 *
 * @since 4.0.0
 */
class Dashboard {
	/**
	 * Class Constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'wp_dashboard_setup', [ $this, 'addDashboardWidgets' ] );
	}

	/**
	 * Registers our dashboard widgets.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	public function addDashboardWidgets() {
		// Add the SEO Setup widget.
		if (
			$this->canShowWidget( 'seoSetup' ) &&
			apply_filters( 'aioseo_show_seo_setup', true ) &&
			( aioseo()->access->isAdmin() || aioseo()->access->hasCapability( 'aioseo_setup_wizard' ) ) &&
			! aioseo()->standalone->setupWizard->isCompleted()
		) {
			wp_add_dashboard_widget(
				'aioseo-seo-setup',
				// Translators: 1 - The plugin short name ("AIOSEO").
				sprintf( esc_html__( '%s Setup', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME ),
				[
					$this,
					'outputSeoSetup',
				],
				null,
				null,
				'normal',
				'high'
			);
		}

		// Add the Overview widget.
		if (
			$this->canShowWidget( 'seoOverview' ) &&
			apply_filters( 'aioseo_show_seo_overview', true ) &&
			( aioseo()->access->isAdmin() || aioseo()->access->hasCapability( 'aioseo_page_analysis' ) ) &&
			aioseo()->options->advanced->truSeo
		) {
			wp_add_dashboard_widget(
				'aioseo-overview',
				// Translators: 1 - The plugin short name ("AIOSEO").
				sprintf( esc_html__( '%s Overview', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME ),
				[
					$this,
					'outputSeoOverview',
				]
			);
		}

		// Add the News widget.
		if (
			$this->canShowWidget( 'seoNews' ) &&
			apply_filters( 'aioseo_show_seo_news', true ) &&
			aioseo()->access->isAdmin()
		) {
			wp_add_dashboard_widget(
				'aioseo-rss-feed',
				esc_html__( 'SEO News', 'all-in-one-seo-pack' ),
				[
					$this,
					'displayRssDashboardWidget',
				]
			);
		}
	}

	/**
	 * Whether or not to show the widget.
	 *
	 * @since   4.0.0
	 * @version 4.2.8
	 *
	 * @param  string  $widget The widget to check if can show.
	 * @return boolean True if yes, false otherwise.
	 */
	protected function canShowWidget( $widget ) { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return true;
	}

	/**
	 * Output the SEO Setup widget.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	public function outputSeoSetup() {
		$this->output( 'aioseo-seo-setup-app' );
	}

	/**
	 * Output the SEO Overview widget.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	public function outputSeoOverview() {
		$this->output( 'aioseo-overview-app' );
	}

	/**
	 * Output the widget wrapper for the Vue App.
	 *
	 * @since 4.2.0
	 *
	 * @param  string $appId The App ID to print out.
	 * @return void
	 */
	private function output( $appId ) {
		// Enqueue the scripts for the widget.
		$this->enqueue();

		// Opening tag.
		echo '<div id="' . esc_attr( $appId ) . '">';

		// Loader element.
		require AIOSEO_DIR . '/app/Common/Views/parts/loader.php';

		// Closing tag.
		echo '</div>';
	}

	/**
	 * Enqueue the scripts and styles.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	private function enqueue() {
		aioseo()->core->assets->load( 'src/vue/standalone/dashboard-widgets/main.js', [], aioseo()->helpers->getVueData( 'dashboard' ) );
	}

	/**
	 * Display RSS Dashboard Widget
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function displayRssDashboardWidget() {
		// Check if the user has chosen not to display this widget through screen options.
		$currentScreen = aioseo()->helpers->getCurrentScreen();
		if ( empty( $currentScreen->id ) ) {
			return;
		}

		$hiddenWidgets = get_user_meta( get_current_user_id(), 'metaboxhidden_' . $currentScreen->id );
		if ( $hiddenWidgets && count( $hiddenWidgets ) > 0 && is_array( $hiddenWidgets[0] ) && in_array( 'aioseo-rss-feed', $hiddenWidgets[0], true ) ) {
			return;
		}

		$rssItems = aioseo()->helpers->fetchAioseoArticles();
		if ( ! $rssItems ) {
			esc_html_e( 'Temporarily unable to load feed.', 'all-in-one-seo-pack' );

			return;
		}
		?>
		<ul>
			<?php
			foreach ( $rssItems as $item ) {
				?>
				<li>
					<a target="_blank" href="<?php echo esc_url( $item['url'] ); ?>" rel="noopener noreferrer">
						<?php echo esc_html( $item['title'] ); ?>
					</a>
					<span><?php echo esc_html( $item['date'] ); ?></span>
					<div>
						<?php echo esc_html( wp_strip_all_tags( $item['content'] ) ) . '...'; ?>
					</div>
				</li>
				<?php
			}

			?>
		</ul>
		<?php
	}
}Common/Admin/DeactivationSurvey.php000064400000022720151536241160013374 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

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

/**
 * Deactivation survey.
 *
 * @since 4.5.5
 */
class DeactivationSurvey {
	/**
	 * The API URL we are calling.
	 *
	 * @since 4.5.5
	 *
	 * @var string
	 */
	public $apiUrl = 'https://plugin.aioseo.com/wp-json/am-deactivate-survey/v1/deactivation-data';

	/**
	 * Name for this plugin.
	 *
	 * @since 4.5.5
	 *
	 * @var string
	 */
	public $name;

	/**
	 * Unique slug for this plugin.
	 *
	 * @since 4.5.5
	 *
	 * @var string
	 */
	public $plugin;

	/**
	 * Primary class constructor.
	 *
	 * @since 4.5.5
	 *
	 * @param string $name Plugin name.
	 * @param string $plugin Plugin slug.
	 */
	public function __construct( $name = '', $plugin = '' ) {
		$this->name   = $name;
		$this->plugin = $plugin;

		// Don't run deactivation survey on dev sites.
		if ( aioseo()->helpers->isDev() ) {
			// return;
		}

		add_action( 'admin_print_scripts', [ $this, 'js' ], 20 );
		add_action( 'admin_print_scripts', [ $this, 'css' ] );
		add_action( 'admin_footer', [ $this, 'modal' ] );
	}

	/**
	 * Returns the URL of the remote endpoint.
	 *
	 * @since 4.5.5
	 *
	 * @return string The URL.
	 */
	public function getApiUrl() {
		if ( defined( 'AIOSEO_DEACTIVATION_SURVEY_URL' ) ) {
			return AIOSEO_DEACTIVATION_SURVEY_URL;
		}

		return $this->apiUrl;
	}

	/**
	 * Checks if current admin screen is the plugins page.
	 *
	 * @since 4.5.5
	 *
	 * @return bool True if it is, false if not.
	 */
	public function isPluginPage() {
		$screen = aioseo()->helpers->getCurrentScreen();
		if ( empty( $screen->id ) ) {
			return false;
		}

		return in_array( $screen->id, [ 'plugins', 'plugins-network' ], true );
	}

	/**
	 * Survey javascript.
	 *
	 * @since 4.5.5
	 *
	 * @return void
	 */
	public function js() {
		if ( ! $this->isPluginPage() ) {
			return;
		}

		?>
		<script type="text/javascript">
		window.addEventListener("load", function() {
			var deactivateLink = document.querySelector('#the-list [data-slug="<?php echo esc_html( $this->plugin ); ?>"] span.deactivate a') ||
				document.querySelector('#deactivate-<?php echo esc_html( $this->plugin ); ?>'),
				overlay = document.querySelector('#am-deactivate-survey-<?php echo esc_html( $this->plugin ); ?>'),
				form = overlay.querySelector('form'),
				formOpen = false;

			deactivateLink.addEventListener('click', function(event) {
				event.preventDefault();
				overlay.style.display = 'table';
				formOpen = true;
				form.querySelector('.am-deactivate-survey-option:first-of-type input[type=radio]').focus();
			});

			form.addEventListener('change', function(event) {
				if (event.target.matches('input[type=radio]')) {
					event.preventDefault();
					Array.from(form.querySelectorAll('input[type=text], .error')).forEach(function(el) { el.style.display = 'none'; });
					Array.from(form.querySelectorAll('.am-deactivate-survey-option')).forEach(function(el) { el.classList.remove('selected'); });
					var option = event.target.closest('.am-deactivate-survey-option');
					option.classList.add('selected');
					
					var otherField = option.querySelector('input[type=text]');
					if (otherField) {
						otherField.style.display = 'block';
						otherField.focus();
					}
				}
			});

			form.addEventListener('click', function(event) {
				if (event.target.matches('.am-deactivate-survey-deactivate')) {
					event.preventDefault();
					window.location.href = deactivateLink.getAttribute('href');
				}
			});

			form.addEventListener('submit', function(event) {
				event.preventDefault();
				if (!form.querySelector('input[type=radio]:checked')) {
					if(!form.querySelector('span[class="error"]')) {
						form.querySelector('.am-deactivate-survey-footer')
						.insertAdjacentHTML('afterbegin', '<span class="error"><?php echo esc_js( __( 'Please select an option', 'all-in-one-seo-pack' ) ); ?></span>');
					}
					return;
				}

				var selected = form.querySelector('.selected');
				var otherField = selected.querySelector('input[type=text]');
				var data = {
					code: selected.querySelector('input[type=radio]').value,
					reason: selected.querySelector('.am-deactivate-survey-option-reason').textContent,
					details: otherField ? otherField.value : '',
					site: '<?php echo esc_url( home_url() ); ?>',
					plugin: '<?php echo esc_html( $this->plugin ); ?>'
				}

				var submitSurvey = fetch('<?php echo esc_url( $this->getApiUrl() ); ?>', {
					method: 'POST',
					body: JSON.stringify(data),
					headers: { 'Content-Type': 'application/json' }
				});

				submitSurvey.finally(function() {
					window.location.href = deactivateLink.getAttribute('href');
				});
			});

			document.addEventListener('keyup', function(event) {
				if (27 === event.keyCode && formOpen) {
					overlay.style.display = 'none';
					formOpen = false;
					deactivateLink.focus();
				}
			});
		});
		</script>
		<?php
	}

	/**
	 * Survey CSS.
	 *
	 * @since 4.5.5
	 *
	 * @return void
	 */
	public function css() {
		if ( ! $this->isPluginPage() ) {
			return;
		}

		?>
		<style type="text/css">
		.am-deactivate-survey-modal {
			display: none;
			table-layout: fixed;
			position: fixed;
			z-index: 9999;
			width: 100%;
			height: 100%;
			text-align: center;
			font-size: 14px;
			top: 0;
			left: 0;
			background: rgba(0,0,0,0.8);
		}
		.am-deactivate-survey-wrap {
			display: table-cell;
			vertical-align: middle;
		}
		.am-deactivate-survey {
			background-color: #fff;
			max-width: 550px;
			margin: 0 auto;
			padding: 30px;
			text-align: left;
		}
		.am-deactivate-survey .error {
			display: block;
			color: red;
			margin: 0 0 10px 0;
		}
		.am-deactivate-survey-title {
			display: block;
			font-size: 18px;
			font-weight: 700;
			text-transform: uppercase;
			border-bottom: 1px solid #ddd;
			padding: 0 0 18px 0;
			margin: 0 0 18px 0;
		}
		.am-deactivate-survey-title span {
			color: #999;
			margin-right: 10px;
		}
		.am-deactivate-survey-desc {
			display: block;
			font-weight: 600;
			margin: 0 0 18px 0;
		}
		.am-deactivate-survey-option {
			margin: 0 0 10px 0;
		}
		.am-deactivate-survey-option-input {
			margin-right: 10px !important;
		}
		.am-deactivate-survey-option-details {
			display: none;
			width: 90%;
			margin: 10px 0 0 30px;
		}
		.am-deactivate-survey-footer {
			margin-top: 18px;
		}
		.am-deactivate-survey-deactivate {
			float: right;
			font-size: 13px;
			color: #ccc;
			text-decoration: none;
			padding-top: 7px;
		}
		</style>
		<?php
	}

	/**
	 * Survey modal.
	 *
	 * @since 4.5.5
	 *
	 * @return void
	 */
	public function modal() {
		if ( ! $this->isPluginPage() ) {
			return;
		}

		$options = [
			1 => [
				'title' => esc_html__( 'I no longer need the plugin', 'all-in-one-seo-pack' ),
			],
			2 => [
				'title'   => esc_html__( 'I\'m switching to a different plugin', 'all-in-one-seo-pack' ),
				'details' => esc_html__( 'Please share which plugin', 'all-in-one-seo-pack' ),
			],
			3 => [
				'title' => esc_html__( 'I couldn\'t get the plugin to work', 'all-in-one-seo-pack' ),
			],
			4 => [
				'title' => esc_html__( 'It\'s a temporary deactivation', 'all-in-one-seo-pack' ),
			],
			5 => [
				'title'   => esc_html__( 'Other', 'all-in-one-seo-pack' ),
				'details' => esc_html__( 'Please share the reason', 'all-in-one-seo-pack' ),
			],
		];
		?>

		<div class="am-deactivate-survey-modal" id="am-deactivate-survey-<?php echo esc_html( $this->plugin ); ?>">
			<div class="am-deactivate-survey-wrap">
				<form class="am-deactivate-survey" method="post">
					<span class="am-deactivate-survey-title"><span class="dashicons dashicons-testimonial"></span><?php echo ' ' . esc_html__( 'Quick Feedback', 'all-in-one-seo-pack' ); ?></span>
					<span class="am-deactivate-survey-desc">
						<?php
						echo esc_html(
							sprintf(
								// Translators: 1 - The plugin name.
								__( 'If you have a moment, please share why you are deactivating %1$s:', 'all-in-one-seo-pack' ),
								$this->name
							)
						);
						?>
					</span>
					<div class="am-deactivate-survey-options">
						<?php foreach ( $options as $id => $option ) : ?>
							<div class="am-deactivate-survey-option">
								<label for="am-deactivate-survey-option-<?php echo esc_html( $this->plugin ); ?>-<?php echo intval( $id ); ?>" class="am-deactivate-survey-option-label">
									<input
										id="am-deactivate-survey-option-<?php echo esc_html( $this->plugin ); ?>-<?php echo intval( $id ); ?>"
										class="am-deactivate-survey-option-input"
										type="radio"
										name="code"
										value="<?php echo intval( $id ); ?>"
									/>
									<span class="am-deactivate-survey-option-reason"><?php echo esc_html( $option['title'] ); ?></span>
								</label>
								<?php if ( ! empty( $option['details'] ) ) : ?>
									<input class="am-deactivate-survey-option-details" type="text" placeholder="<?php echo esc_html( $option['details'] ); ?>" />
								<?php endif; ?>
							</div>
						<?php endforeach; ?>
					</div>
					<div class="am-deactivate-survey-footer">
						<button type="submit" class="am-deactivate-survey-submit button button-primary button-large">
							<?php
							echo sprintf(
								// Translators: 1 - & symbol.
								esc_html__( 'Submit %1$s Deactivate', 'all-in-one-seo-pack' ),
								'&amp;'
							);
							?>
						</button>
						<a href="#" class="am-deactivate-survey-deactivate">
						<?php
						echo sprintf(
							// Translators: 1 - & symbol.
							esc_html__( 'Skip %1$s Deactivate', 'all-in-one-seo-pack' ),
							'&amp;'
						);
						?>
						</a>
					</div>
				</form>
			</div>
		</div>
		<?php
	}
}Common/Admin/NetworkAdmin.php000064400000003222151536241160012142 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

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

/**
 * Abstract class that Pro and Lite both extend.
 *
 * @since 4.2.5
 */
class NetworkAdmin extends Admin {
	/**
	 * Construct method.
	 *
	 * @since 4.2.5
	 */
	public function __construct() {
		include_once ABSPATH . 'wp-admin/includes/plugin.php';
		if (
			is_network_admin() &&
			! is_plugin_active_for_network( plugin_basename( AIOSEO_FILE ) )
		) {
			return;
		}

		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		add_action( 'sanitize_comment_cookies', [ $this, 'init' ], 21 );
	}

	/**
	 * Initialize the admin.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	public function init() {
		add_action( 'network_admin_menu', [ $this, 'addNetworkMenu' ] );

		add_action( 'init', [ $this, 'setPages' ] );
	}

	/**
	 * Add the network menu inside of WordPress.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	public function addNetworkMenu() {
		$this->addMainMenu( 'aioseo' );

		foreach ( $this->pages as $slug => $page ) {
			if (
				'aioseo-settings' !== $slug &&
				'aioseo-tools' !== $slug &&
				'aioseo-about' !== $slug &&
				'aioseo-feature-manager' !== $slug
			) {
				continue;
			}

			$hook = add_submenu_page(
				$this->pageSlug,
				! empty( $page['page_title'] ) ? $page['page_title'] : $page['menu_title'],
				$page['menu_title'],
				$this->getPageRequiredCapability( $slug ),
				$slug,
				[ $this, 'page' ]
			);
			add_action( "load-{$hook}", [ $this, 'hooks' ] );
		}

		// Remove the "dashboard" submenu page that is not needed in the network admin.
		remove_submenu_page( $this->pageSlug, $this->pageSlug );
	}
}Common/Admin/Notices/ConflictingPlugins.php000064400000012466151536241160014757 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin\Notices;

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

/**
 * Handles the Conflicting Plugins notice..
 *
 * @since 4.5.1
 */
class ConflictingPlugins {
	/**
	 * Class constructor.
	 *
	 * @since 4.5.1
	 */
	public function __construct() {
		add_action( 'wp_ajax_aioseo-dismiss-conflicting-plugins-notice', [ $this, 'dismissNotice' ] );
		add_action( 'wp_ajax_aioseo-deactivate-conflicting-plugins-notice', [ $this, 'deactivateConflictingPlugins' ] );
	}

	/**
	 * Go through all the checks to see if we should show the notice.
	 *
	 * @since 4.5.1
	 *
	 * @return void
	 */
	public function maybeShowNotice() {
		$dismissed = get_option( '_aioseo_conflicting_plugins_dismissed', true );
		if ( '1' === $dismissed ) {
			return;
		}

		if ( ! current_user_can( 'activate_plugins' ) ) {
			return;
		}

		// Only show if there are conflicting plugins.
		$conflictingPlugins = aioseo()->conflictingPlugins->getAllConflictingPlugins();
		if ( empty( $conflictingPlugins ) ) {
			return;
		}

		$this->showNotice();

		// Print the script to the footer.
		add_action( 'admin_footer', [ $this, 'printScript' ] );
	}

	/**
	 * Renders the notice.
	 *
	 * @since 4.5.1
	 *
	 * @return void
	 */
	public function showNotice() {
		$type = ! empty( aioseo()->conflictingPlugins->getConflictingPlugins( 'seo' ) ) ? 'SEO' : 'sitemap';
		?>
		<div class="notice notice-error aioseo-conflicting-plugin-notice is-dismissible">
			<p>
				<?php
				echo wp_kses(
					sprintf(
						// phpcs:ignore Generic.Files.LineLength.MaxExceeded
						// Translators: 1 - Type of conflicting plugin (i.e. SEO or Sitemap), 2 - Opening HTML link tag, 3 - Closing HTML link tag.
						__( 'Please keep only one %1$s plugin active, otherwise, you might lose your rankings and traffic. %2$sClick here to Deactivate.%3$s', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						$type,
						'<a href="#" rel="noopener noreferrer" class="deactivate-conflicting-plugins">',
						'</a>'
					),
					[
						'a'      => [
							'href'  => [],
							'rel'   => [],
							'class' => []
						],
						'strong' => [],
					]
				);
				?>
			</p>
		</div>

		<style>
			#conflicting_seo_plugins.rank-math-notice {
				display: none;
			}
		</style>

		<?php
	}

	/**
	 * Print the script for dismissing the notice.
	 *
	 * @since 4.5.1
	 *
	 * @return void
	 */
	public function printScript() {
		// Create a nonce.
		$nonce1 = wp_create_nonce( 'aioseo-dismiss-conflicting-plugins' );
		$nonce2 = wp_create_nonce( 'aioseo-deactivate-conflicting-plugins' );
		?>
		<script>
			window.addEventListener('load', function () {
				var dismissBtn,
					deactivateBtn

				// Add an event listener to the dismiss button.
				dismissBtn = document.querySelector('.aioseo-conflicting-plugin-notice .notice-dismiss')
				dismissBtn.addEventListener('click', function (event) {
					var httpRequest = new XMLHttpRequest(),
						postData    = ''

					// Build the data to send in our request.
					postData += '&action=aioseo-dismiss-conflicting-plugins-notice'
					postData += '&nonce=<?php echo esc_html( $nonce1 ); ?>'

					httpRequest.open('POST', '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>')
					httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
					httpRequest.send(postData)
				})

				deactivateBtn = document.querySelector('.aioseo-conflicting-plugin-notice .deactivate-conflicting-plugins')
				deactivateBtn.addEventListener('click', function (event) {
					event.preventDefault()

					var httpRequest = new XMLHttpRequest(),
						postData    = ''

					// Build the data to send in our request.
					postData += '&action=aioseo-deactivate-conflicting-plugins-notice'
					postData += '&nonce=<?php echo esc_html( $nonce2 ); ?>'

					httpRequest.open('POST', '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>')
					httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
					httpRequest.onerror = function () {
						window.location.reload()
					}
					httpRequest.onload = function () {
						window.location.reload()
					}
					httpRequest.send(postData)
				})
			});
		</script>
		<?php
	}

	/**
	 * Dismiss the notice.
	 *
	 * @since 4.5.1
	 *
	 * @return string The successful response.
	 */
	public function dismissNotice() {
		// Early exit if we're not on a aioseo-dismiss-conflicting-plugins-notice action.
		if ( ! isset( $_POST['action'] ) || 'aioseo-dismiss-conflicting-plugins-notice' !== $_POST['action'] ) {
			return wp_send_json_error( 'invalid-action' );
		}

		check_ajax_referer( 'aioseo-dismiss-conflicting-plugins', 'nonce' );

		update_option( '_aioseo_conflicting_plugins_dismissed', true );

		return wp_send_json_success();
	}

	/**
	 * Deactivates the conflicting plugins.
	 *
	 * @since 4.5.1
	 *
	 * @return string The successful response.
	 */
	public function deactivateConflictingPlugins() {
		// Early exit if we're not on a aioseo-dismiss-conflicting-plugins-notice action.
		if ( ! isset( $_POST['action'] ) || 'aioseo-deactivate-conflicting-plugins-notice' !== $_POST['action'] ) {
			return wp_send_json_error( 'invalid-action' );
		}

		check_ajax_referer( 'aioseo-deactivate-conflicting-plugins', 'nonce' );

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

		return wp_send_json_success();
	}
}Common/Admin/Notices/DeprecatedWordPress.php000064400000011266151536241160015064 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin\Notices;

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

/**
 * WordPress Deprecated Notice.
 *
 * @since 4.1.2
 */
class DeprecatedWordPress {
	/**
	 * Class Constructor.
	 *
	 * @since 4.1.2
	 */
	public function __construct() {
		add_action( 'wp_ajax_aioseo-dismiss-deprecated-wordpress-notice', [ $this, 'dismissNotice' ] );
	}

	/**
	 * Go through all the checks to see if we should show the notice.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	public function maybeShowNotice() {
		global $wp_version; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		$dismissed = get_option( '_aioseo_deprecated_wordpress_dismissed', true );
		if ( '1' === $dismissed ) {
			return;
		}

		// Show to users that interact with our pluign.
		if ( ! current_user_can( 'publish_posts' ) ) {
			return;
		}

		// Show if WordPress version is deprecated.
		if ( version_compare( $wp_version, '5.4', '>=' ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			return;
		}

		$this->showNotice();

		// Print the script to the footer.
		add_action( 'admin_footer', [ $this, 'printScript' ] );
	}

	/**
	 * Actually show the review plugin.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	public function showNotice() {
		$medium = false !== strpos( AIOSEO_PHP_VERSION_DIR, 'pro' ) ? 'proplugin' : 'liteplugin';
		?>
		<div class="notice notice-warning aioseo-deprecated-wordpress-notice is-dismissible">
			<p>
				<?php
				echo wp_kses(
					sprintf(
						// Translators: 1 - Opening HTML bold tag, 2 - Closing HTML bold tag.
						__( 'Your site is running an %1$soutdated version%2$s of WordPress. We recommend using the latest version of WordPress in order to keep your site secure.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'<strong>',
						'</strong>'
					),
					[
						'strong' => [],
					]
				);
				?>
				<br><br>
				<?php
				echo wp_kses(
					sprintf(
						// phpcs:ignore Generic.Files.LineLength.MaxExceeded
						// Translators: 1 - Opening HTML bold tag, 2 - Closing HTML bold tag, 3 - The short plugin name ("AIOSEO"), 4 - The current year, 5 - Opening HTML link tag, 6 - Closing HTML link tag.
						__( '%1$sNote:%2$s %3$s will be discontinuing support for WordPress versions older than version 5.7 by the end of %4$s. %5$sRead more for additional information.%6$s', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'<strong>',
						'</strong>',
						'AIOSEO',
						gmdate( 'Y' ),
						'<a href="https://aioseo.com/docs/update-wordpress/?utm_source=WordPress&utm_medium=' . $medium . '&utm_campaign=outdated-wordpress-notice" target="_blank" rel="noopener noreferrer">', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'</a>'
					),
					[
						'a'      => [
							'href'   => [],
							'target' => [],
							'rel'    => [],
						],
						'strong' => [],
					]
				);
				?>
			</p>
		</div>

		<?php
		// In case this is on plugin activation.
		if ( isset( $_GET['activate'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			unset( $_GET['activate'] );
		}
	}

	/**
	 * Print the script for dismissing the notice.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	public function printScript() {
		// Create a nonce.
		$nonce = wp_create_nonce( 'aioseo-dismiss-deprecated-wordpress' );
		?>
		<script>
			window.addEventListener('load', function () {
				var dismissBtn

				// Add an event listener to the dismiss button.
				dismissBtn = document.querySelector('.aioseo-deprecated-wordpress-notice .notice-dismiss')
				dismissBtn.addEventListener('click', function (event) {
					var httpRequest = new XMLHttpRequest(),
						postData    = ''

					// Build the data to send in our request.
					postData += '&action=aioseo-dismiss-deprecated-wordpress-notice'
					postData += '&nonce=<?php echo esc_html( $nonce ); ?>'

					httpRequest.open('POST', '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>')
					httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
					httpRequest.send(postData)
				})
			});
		</script>
		<?php
	}

	/**
	 * Dismiss the deprecated WordPress notice.
	 *
	 * @since 4.1.2
	 *
	 * @return string The successful response.
	 */
	public function dismissNotice() {
		// Early exit if we're not on a aioseo-dismiss-deprecated-wordpress-notice action.
		if ( ! isset( $_POST['action'] ) || 'aioseo-dismiss-deprecated-wordpress-notice' !== $_POST['action'] ) {
			return;
		}

		check_ajax_referer( 'aioseo-dismiss-deprecated-wordpress', 'nonce' );

		update_option( '_aioseo_deprecated_wordpress_dismissed', true );

		return wp_send_json_success();
	}
}Common/Admin/Notices/Import.php000064400000002375151536241160012426 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin\Notices;

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

/**
 * Plugin import notice.
 *
 * @since 4.0.0
 */
class Import {
	/**
	 * Go through all the checks to see if we should show the notice.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function maybeShowNotice() {
		if ( ! aioseo()->importExport->isImportRunning() ) {
			return;
		}

		$this->showNotice();
	}

	/**
	 * Register the notice so that it appears.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function showNotice() {
		$string1 = __( 'SEO Meta Import In Progress', 'all-in-one-seo-pack' );
		// Translators: 1 - The plugin name ("All in One SEO").
		$string2 = sprintf( __( '%1$s is importing your existing SEO data in the background.', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_NAME );
		$string3 = __( 'This notice will automatically disappear as soon as the import has completed. Meanwhile, everything should continue to work as expected.', 'all-in-one-seo-pack' );
		?>
		<div class="notice notice-info aioseo-migration">
			<p><strong><?php echo esc_html( $string1 ); ?></strong></p>
			<p><?php echo esc_html( $string2 ); ?></p>
			<p><?php echo esc_html( $string3 ); ?></p>
		</div>
		<style>
		</style>
		<?php
	}
}Common/Admin/Notices/Migration.php000064400000003044151536241160013077 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin\Notices;

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

/**
 * V3 to V4 migration notice.
 *
 * @since 4.0.0
 */
class Migration {
	/**
	 * Go through all the checks to see if we should show the notice.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function maybeShowNotice() {
		$transientPosts = aioseo()->core->cache->get( 'v3_migration_in_progress_posts' );
		$transientTerms = aioseo()->core->cache->get( 'v3_migration_in_progress_terms' );
		if ( ! $transientPosts && ! $transientTerms ) {
			return;
		}

		$this->showNotice();
	}

	/**
	 * Register the notice so that it appears.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function showNotice() {
		// Translators: 1 - The plugin name ("AIOSEO).
		$string1 = sprintf( __( '%1$s V3->V4 Migration In Progress', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME );
		// Translators: 1 - The plugin name ("All in One SEO").
		$string2 = sprintf( __( '%1$s is currently upgrading your database and migrating your SEO data in the background.', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_NAME );
		$string3 = __( 'This notice will automatically disappear as soon as the migration has completed. Meanwhile, everything should continue to work as expected.', 'all-in-one-seo-pack' );
		?>
		<div class="notice notice-info aioseo-migration">
			<p><strong><?php echo esc_html( $string1 ); ?></strong></p>
			<p><?php echo esc_html( $string2 ); ?></p>
			<p><?php echo esc_html( $string3 ); ?></p>
		</div>
		<style>
		</style>
		<?php
	}
}Common/Admin/Notices/Notices.php000064400000041164151536241160012557 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin\Notices;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Abstract class that Pro and Lite both extend.
 *
 * @since 4.0.0
 */
class Notices {
	/**
	 * Source of notifications content.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $url = 'https://plugin-cdn.aioseo.com/wp-content/notifications.json';

	/**
	 * Review class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Review
	 */
	private $review = null;

	/**
	 * Migration class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Migration
	 */
	private $migration = null;

	/**
	 * Import class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Import
	 */
	private $import = null;

	/**
	 * DeprecatedWordPress class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var DeprecatedWordPress
	 */
	private $deprecatedWordPress = null;

	/**
	 * ConflictingPlugins class instance.
	 *
	 * @since 4.5.1
	 *
	 * @var ConflictingPlugins
	 */
	private $conflictingPlugins = null;

	/**
	 * Class Constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'aioseo_admin_notifications_update', [ $this, 'update' ] );

		if ( ! is_admin() ) {
			return;
		}

		add_action( 'updated_option', [ $this, 'maybeResetBlogVisibility' ], 10, 3 );
		add_action( 'init', [ $this, 'init' ], 2 );

		$this->review              = new Review();
		$this->migration           = new Migration();
		$this->import              = new Import();
		$this->deprecatedWordPress = new DeprecatedWordPress();
		$this->conflictingPlugins  = new ConflictingPlugins();

		add_action( 'admin_notices', [ $this, 'notices' ] );
	}

	/**
	 * Initialize notifications.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function init() {
		// If our tables do not exist, create them now.
		if ( ! aioseo()->core->db->tableExists( 'aioseo_notifications' ) ) {
			aioseo()->updates->addInitialCustomTablesForV4();
		}

		$this->maybeUpdate();
		$this->initInternalNotices();
		$this->deleteInternalNotices();
	}

	/**
	 * Checks if we should update our notifications.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function maybeUpdate() {
		$nextRun = aioseo()->core->networkCache->get( 'admin_notifications_update' );
		if ( null !== $nextRun && time() < $nextRun ) {
			return;
		}

		// Schedule the action.
		aioseo()->actionScheduler->scheduleAsync( 'aioseo_admin_notifications_update' );

		// Update the cache.
		aioseo()->core->networkCache->update( 'admin_notifications_update', time() + DAY_IN_SECONDS );
	}

	/**
	 * Update Notifications from the server.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function update() {
		$notifications = $this->fetch();
		foreach ( $notifications as $notification ) {
			// First, let's check to see if this notification already exists. If so, we want to override it.
			$n = aioseo()->core->db
				->start( 'aioseo_notifications' )
				->where( 'notification_id', $notification->id )
				->run()
				->model( 'AIOSEO\\Plugin\\Common\\Models\\Notification' );

			$buttons = [
				'button1' => [
					'label' => ! empty( $notification->btns->main->text ) ? $notification->btns->main->text : null,
					'url'   => ! empty( $notification->btns->main->url ) ? $notification->btns->main->url : null
				],
				'button2' => [
					'label' => ! empty( $notification->btns->alt->text ) ? $notification->btns->alt->text : null,
					'url'   => ! empty( $notification->btns->alt->url ) ? $notification->btns->alt->url : null
				]
			];

			if ( $n->exists() ) {
				$n->title           = $notification->title;
				$n->content         = $notification->content;
				$n->type            = ! empty( $notification->notification_type ) ? $notification->notification_type : 'info';
				$n->level           = $notification->type;
				$n->notification_id = $notification->id;
				$n->start           = ! empty( $notification->start ) ? $notification->start : null;
				$n->end             = ! empty( $notification->end ) ? $notification->end : null;
				$n->button1_label   = $buttons['button1']['label'];
				$n->button1_action  = $buttons['button1']['url'];
				$n->button2_label   = $buttons['button2']['label'];
				$n->button2_action  = $buttons['button2']['url'];
				$n->save();
				continue;
			}

			$n                  = new Models\Notification();
			$n->slug            = uniqid();
			$n->title           = $notification->title;
			$n->content         = $notification->content;
			$n->type            = ! empty( $notification->notification_type ) ? $notification->notification_type : 'info';
			$n->level           = $notification->type;
			$n->notification_id = $notification->id;
			$n->start           = ! empty( $notification->start ) ? $notification->start : null;
			$n->end             = ! empty( $notification->end ) ? $notification->end : null;
			$n->button1_label   = $buttons['button1']['label'];
			$n->button1_action  = $buttons['button1']['url'];
			$n->button2_label   = $buttons['button2']['label'];
			$n->button2_action  = $buttons['button2']['url'];
			$n->dismissed       = 0;
			$n->save();

			// Since we've added a new remote notification, let's show the notification drawer.
			aioseo()->core->cache->update( 'show_notifications_drawer', true );
		}
	}

	/**
	 * Fetches the feed of notifications.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of notifications.
	 */
	private function fetch() {
		$response = aioseo()->helpers->wpRemoteGet( $this->getUrl() );

		if ( is_wp_error( $response ) ) {
			return [];
		}

		$body = wp_remote_retrieve_body( $response );

		if ( empty( $body ) ) {
			return [];
		}

		return $this->verify( json_decode( $body ) );
	}

	/**
	 * Verify notification data before it is saved.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $notifications Array of notifications items to verify.
	 * @return array                An array of verified notifications.
	 */
	private function verify( $notifications ) {
		$data = [];
		if ( ! is_array( $notifications ) || empty( $notifications ) ) {
			return $data;
		}

		foreach ( $notifications as $notification ) {
			// The message and license should never be empty, if they are, ignore.
			if ( empty( $notification->content ) || empty( $notification->type ) ) {
				continue;
			}

			if ( ! is_array( $notification->type ) ) {
				$notification->type = [ $notification->type ];
			}
			foreach ( $notification->type as $type ) {
				// Ignore if type does not match.
				if ( ! $this->validateType( $type ) ) {
					continue 2;
				}
			}

			// Ignore if expired.
			if ( ! empty( $notification->end ) && time() > strtotime( $notification->end ) ) {
				continue;
			}

			// Ignore if notification existed before installing AIOSEO.
			// Prevents bombarding the user with notifications after activation.
			$activated = aioseo()->internalOptions->internal->firstActivated( time() );
			if (
				! empty( $notification->start ) &&
				$activated > strtotime( $notification->start )
			) {
				continue;
			}

			$data[] = $notification;
		}

		return $data;
	}

	/**
	 * Validates the notification type.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $type The notification type we are targeting.
	 * @return boolean       True if yes, false if no.
	 */
	public function validateType( $type ) {
		$validated = false;

		if ( 'all' === $type ) {
			$validated = true;
		}

		// Store notice if version matches.
		if ( $this->versionMatch( aioseo()->version, $type ) ) {
			$validated = true;
		}

		return $validated;
	}

	/**
	 * Version Compare.
	 *
	 * @since 4.0.0
	 *
	 * @param  string       $currentVersion The current version being used.
	 * @param  string|array $compareVersion The version to compare with.
	 * @return bool                         True if we match, false if not.
	 */
	public function versionMatch( $currentVersion, $compareVersion ) {
		if ( is_array( $compareVersion ) ) {
			foreach ( $compareVersion as $compare_single ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				$recursiveResult = $this->versionMatch( $currentVersion, $compare_single ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				if ( $recursiveResult ) {
					return true;
				}
			}

			return false;
		}

		$currentParse = explode( '.', $currentVersion );
		if ( strpos( $compareVersion, '-' ) ) {
			$compareParse = explode( '-', $compareVersion );
		} elseif ( strpos( $compareVersion, '.' ) ) {
			$compareParse = explode( '.', $compareVersion );
		} else {
			return false;
		}

		$currentCount = count( $currentParse );
		$compareCount = count( $compareParse );
		for ( $i = 0; $i < $currentCount || $i < $compareCount; $i++ ) {
			if ( isset( $compareParse[ $i ] ) && 'x' === strtolower( $compareParse[ $i ] ) ) {
				unset( $compareParse[ $i ] );
			}

			if ( ! isset( $currentParse[ $i ] ) ) {
				unset( $compareParse[ $i ] );
			} elseif ( ! isset( $compareParse[ $i ] ) ) {
				unset( $currentParse[ $i ] );
			}
		}

		foreach ( $compareParse as $index => $subNumber ) {
			if ( $currentParse[ $index ] !== $subNumber ) {
				return false;
			}
		}

		return true;
	}


	/**
	 * Gets the URL for the notifications api.
	 *
	 * @since 4.0.0
	 *
	 * @return string The URL to use for the api requests.
	 */
	private function getUrl() {
		if ( defined( 'AIOSEO_NOTIFICATIONS_URL' ) ) {
			return AIOSEO_NOTIFICATIONS_URL;
		}

		return $this->url;
	}

	/**
	 * Add notices.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function notices() {
		// Double check we're actually in the admin before outputting anything.
		if ( ! is_admin() ) {
			return;
		}

		$this->review->maybeShowNotice();
		$this->migration->maybeShowNotice();
		$this->import->maybeShowNotice();
		$this->deprecatedWordPress->maybeShowNotice();
		$this->conflictingPlugins->maybeShowNotice();
	}

	/**
	 * Initialize the internal notices.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function initInternalNotices() {
		$this->blogVisibility();
		$this->descriptionFormat();
	}

	/**
	 * Deletes internal notices we no longer need.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function deleteInternalNotices() {
		$pluginData = aioseo()->helpers->getPluginData();
		if ( $pluginData['miPro']['installed'] || $pluginData['miLite']['installed'] ) {
			$notification = Models\Notification::getNotificationByName( 'install-mi' );
			if ( ! $notification->exists() ) {
				return;
			}

			Models\Notification::deleteNotificationByName( 'install-mi' );
		}

		if ( $pluginData['optinMonster']['installed'] ) {
			$notification = Models\Notification::getNotificationByName( 'install-om' );
			if ( ! $notification->exists() ) {
				return;
			}

			Models\Notification::deleteNotificationByName( 'install-om' );
		}
	}

	/**
	 * Extends a notice by a (default) 1 week start date.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $notice The notice to extend.
	 * @param  string $start  How long to extend.
	 * @return void
	 */
	public function remindMeLater( $notice, $start = '+1 week' ) {
		$notification = Models\Notification::getNotificationByName( $notice );
		if ( ! $notification->exists() ) {
			return;
		}

		$notification->start = gmdate( 'Y-m-d H:i:s', strtotime( $start ) );
		$notification->save();
	}

	/**
	 * Add a notice if the blog is set to hidden.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function blogVisibility() {
		$notification = Models\Notification::getNotificationByName( 'blog-visibility' );
		if ( get_option( 'blog_public' ) ) {
			if ( $notification->exists() ) {
				Models\Notification::deleteNotificationByName( 'blog-visibility' );
			}

			return;
		}

		if ( $notification->exists() || ! current_user_can( 'manage_options' ) ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'blog-visibility',
			'title'             => __( 'Search Engines Blocked', 'all-in-one-seo-pack' ),
			'content'           => sprintf(
				// Translators: 1 - The plugin short name ("AIOSEO").
				__( 'Warning: %1$s has detected that you are blocking access to search engines. You can change this in Settings > Reading if this was unintended.', 'all-in-one-seo-pack' ),
				AIOSEO_PLUGIN_SHORT_NAME
			),
			'type'              => 'error',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Fix Now', 'all-in-one-seo-pack' ),
			'button1_action'    => admin_url( 'options-reading.php' ),
			'button2_label'     => __( 'Remind Me Later', 'all-in-one-seo-pack' ),
			'button2_action'    => 'http://action#notification/blog-visibility-reminder',
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Add a notice if the description format is missing the Description tag.
	 *
	 * @since 4.0.5
	 *
	 * @return void
	 */
	private function descriptionFormat() {
		$notification = Models\Notification::getNotificationByName( 'description-format' );
		if ( ! in_array( 'descriptionFormat', aioseo()->internalOptions->deprecatedOptions, true ) ) {
			if ( $notification->exists() ) {
				Models\Notification::deleteNotificationByName( 'description-format' );
			}

			return;
		}

		$descriptionFormat = aioseo()->options->deprecated->searchAppearance->global->descriptionFormat;
		if ( false !== strpos( $descriptionFormat, '#description' ) ) {
			if ( $notification->exists() ) {
				Models\Notification::deleteNotificationByName( 'description-format' );
			}

			return;
		}

		if ( $notification->exists() ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'description-format',
			'title'             => __( 'Invalid Description Format', 'all-in-one-seo-pack' ),
			'content'           => sprintf(
				// Translators: 1 - The plugin short name ("AIOSEO").
				__( 'Warning: %1$s has detected that you may have an invalid description format. This could lead to descriptions not being properly applied to your content.', 'all-in-one-seo-pack' ),
				AIOSEO_PLUGIN_SHORT_NAME
			) . ' ' . __( 'A Description tag is required in order to properly display your meta descriptions on your site.', 'all-in-one-seo-pack' ),
			'type'              => 'error',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Fix Now', 'all-in-one-seo-pack' ),
			'button1_action'    => 'http://route#aioseo-search-appearance&aioseo-scroll=description-format&aioseo-highlight=description-format:advanced',
			'button2_label'     => __( 'Remind Me Later', 'all-in-one-seo-pack' ),
			'button2_action'    => 'http://action#notification/description-format-reminder',
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Check if blog visibility is changing and add/delete the appropriate notification.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $optionName The name of the option we are checking.
	 * @param  mixed  $oldValue   The old value.
	 * @param  mixed  $newValue   The new value.
	 * @return void
	 */
	public function maybeResetBlogVisibility( $optionName, $oldValue = '', $newValue = '' ) {
		if ( 'blog_public' === $optionName ) {
			if ( 1 === intval( $newValue ) ) {
				$notification = Models\Notification::getNotificationByName( 'blog-visibility' );
				if ( ! $notification->exists() ) {
					return;
				}

				Models\Notification::deleteNotificationByName( 'blog-visibility' );

				return;
			}

			$this->blogVisibility();
		}
	}

	/**
	 * Add a notice if the blog is set to hidden.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function conflictingPlugins( $plugins = [] ) {
		if ( empty( $plugins ) ) {
			return;
		}

		$content = sprintf(
			// Translators: 1 - The plugin short name ("AIOSEO").
			__( 'Warning: %1$s has detected other active SEO or sitemap plugins. We recommend that you deactivate the following plugins to prevent any conflicts:', 'all-in-one-seo-pack' ),
			AIOSEO_PLUGIN_SHORT_NAME
		) . '<ul>';

		foreach ( $plugins as $pluginName => $pluginPath ) {
			$content .= '<li><strong>' . $pluginName . '</strong></li>';
		}

		$content .= '</ul>';

		// Update an existing notice.
		$notification = Models\Notification::getNotificationByName( 'conflicting-plugins' );
		if ( $notification->exists() ) {
			$notification->content = $content;
			$notification->save();

			return;
		}

		// Create a new one if it doesn't exist.
		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'conflicting-plugins',
			'title'             => __( 'Conflicting Plugins Detected', 'all-in-one-seo-pack' ),
			'content'           => $content,
			'type'              => 'error',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Fix Now', 'all-in-one-seo-pack' ),
			'button1_action'    => 'http://action#sitemap/deactivate-conflicting-plugins?refresh',
			'button2_label'     => __( 'Remind Me Later', 'all-in-one-seo-pack' ),
			'button2_action'    => 'http://action#notification/conflicting-plugins-reminder',
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}
}Common/Admin/Notices/Review.php000064400000024427151536241160012417 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin\Notices;

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

/**
 * Review Plugin Notice.
 *
 * @since 4.0.0
 */
class Review {
	/**
	 * Class Constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'wp_ajax_aioseo-dismiss-review-plugin-cta', [ $this, 'dismissNotice' ] );
	}

	/**
	 * Go through all the checks to see if we should show the notice.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function maybeShowNotice() {
		$dismissed = get_user_meta( get_current_user_id(), '_aioseo_plugin_review_dismissed', true );
		if ( '3' === $dismissed || '4' === $dismissed ) {
			return;
		}

		if ( ! empty( $dismissed ) && $dismissed > time() ) {
			return;
		}

		// Only show to users that interact with our pluign.
		if ( ! current_user_can( 'publish_posts' ) ) {
			return;
		}

		// Only show if plugin has been active for over 10 days.
		if ( ! aioseo()->internalOptions->internal->firstActivated ) {
			aioseo()->internalOptions->internal->firstActivated = time();
		}

		$activated = aioseo()->internalOptions->internal->firstActivated( time() );
		if ( $activated > strtotime( '-10 days' ) ) {
			return;
		}

		if ( get_option( 'aioseop_options' ) || get_option( 'aioseo_options_v3' ) ) {
			$this->showNotice();
		} else {
			$this->showNotice2();
		}

		// Print the script to the footer.
		add_action( 'admin_footer', [ $this, 'printScript' ] );
	}

	/**
	 * Actually show the review plugin.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function showNotice() {
		$feedbackUrl = add_query_arg(
			[
				'wpf7528_24'   => untrailingslashit( home_url() ),
				'wpf7528_26'   => aioseo()->options->has( 'general' ) && aioseo()->options->general->has( 'licenseKey' )
					? aioseo()->options->general->licenseKey
					: '',
				'wpf7528_27'   => aioseo()->pro ? 'pro' : 'lite',
				'wpf7528_28'   => AIOSEO_VERSION,
				'utm_source'   => aioseo()->pro ? 'proplugin' : 'liteplugin',
				'utm_medium'   => 'review-notice',
				'utm_campaign' => 'feedback',
				'utm_content'  => AIOSEO_VERSION,
			],
			'https://aioseo.com/plugin-feedback/'
		);

		$string1 = sprintf(
			// Translators: 1 - The plugin short name ("AIOSEO").
			__( 'Are you enjoying %1$s?', 'all-in-one-seo-pack' ),
			AIOSEO_PLUGIN_NAME
		);
		$string2  = __( 'Yes I love it', 'all-in-one-seo-pack' );
		$string3  = __( 'Not Really...', 'all-in-one-seo-pack' );
		$string4  = sprintf(
					// Translators: 1 - The plugin name ("All in One SEO").
			__( 'We\'re sorry to hear you aren\'t enjoying %1$s. We would love a chance to improve. Could you take a minute and let us know what we can do better?', 'all-in-one-seo-pack' ),
			AIOSEO_PLUGIN_NAME
		); // phpcs:ignore Generic.Files.LineLength.MaxExceeded
		$string5  = __( 'Give feedback', 'all-in-one-seo-pack' );
		$string6  = __( 'No thanks', 'all-in-one-seo-pack' );
		$string7  = __( 'That\'s awesome! Could you please do us a BIG favor and give it a 5-star rating on WordPress to help us spread the word and boost our motivation?', 'all-in-one-seo-pack' );
		// Translators: 1 - The plugin name ("All in One SEO").
		$string9  = __( 'Ok, you deserve it', 'all-in-one-seo-pack' );
		$string10 = __( 'Nope, maybe later', 'all-in-one-seo-pack' );
		$string11 = __( 'I already did', 'all-in-one-seo-pack' );

		?>
		<div class="notice notice-info aioseo-review-plugin-cta is-dismissible">
			<div class="step-1">
				<p><?php echo esc_html( $string1 ); ?></p>
				<p>
					<a href="#" class="aioseo-review-switch-step-3" data-step="3"><?php echo esc_html( $string2 ); ?></a> 🙂 |
					<a href="#" class="aioseo-review-switch-step-2" data-step="2"><?php echo esc_html( $string3 ); ?></a>
				</p>
			</div>
			<div class="step-2" style="display:none;">
				<p><?php echo esc_html( $string4 ); ?></p>
				<p>
					<a href="<?php echo esc_url( $feedbackUrl ); ?>" class="aioseo-dismiss-review-notice" target="_blank" rel="noopener noreferrer"><?php echo esc_html( $string5 ); ?></a>&nbsp;&nbsp;
					<a href="#" class="aioseo-dismiss-review-notice" target="_blank" rel="noopener noreferrer"><?php echo esc_html( $string6 ); ?></a>
				</p>
			</div>
			<div class="step-3" style="display:none;">
				<p><?php echo esc_html( $string7 ); ?></p>
				<p>
					<a href="https://wordpress.org/support/plugin/all-in-one-seo-pack/reviews/?filter=5#new-post" class="aioseo-dismiss-review-notice" target="_blank" rel="noopener noreferrer">
						<?php echo esc_html( $string9 ); ?>
					</a>&nbsp;&bull;&nbsp;
					<a href="#" class="aioseo-dismiss-review-notice-delay" target="_blank" rel="noopener noreferrer">
						<?php echo esc_html( $string10 ); ?>
					</a>&nbsp;&bull;&nbsp;
					<a href="#" class="aioseo-dismiss-review-notice" target="_blank" rel="noopener noreferrer">
						<?php echo esc_html( $string11 ); ?>
					</a>
				</p>
			</div>
		</div>
		<?php
	}

	/**
	 * Actually show the review plugin 2.0.
	 *
	 * @since 4.2.2
	 *
	 * @return void
	 */
	public function showNotice2() {
		$string1 = sprintf(
			// Translators: 1 - The plugin name ("All in One SEO").
			__( 'Hey, we noticed you have been using %1$s for some time - that’s awesome! Could you please do us a BIG favor and give it a 5-star rating on WordPress to help us spread the word and boost our motivation?', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'<strong>' . esc_html( AIOSEO_PLUGIN_NAME ) . '</strong>'
		);

		// Translators: 1 - The plugin name ("All in One SEO").
		$string9  = __( 'Ok, you deserve it', 'all-in-one-seo-pack' );
		$string10 = __( 'Nope, maybe later', 'all-in-one-seo-pack' );
		$string11 = __( 'I already did', 'all-in-one-seo-pack' );

		?>
		<div class="notice notice-info aioseo-review-plugin-cta is-dismissible">
			<div class="step-3">
				<p><?php echo $string1; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p>
				<p>
					<a href="https://wordpress.org/support/plugin/all-in-one-seo-pack/reviews/?filter=5#new-post" class="aioseo-dismiss-review-notice" target="_blank" rel="noopener noreferrer">
						<?php echo esc_html( $string9 ); ?>
					</a>&nbsp;&bull;&nbsp;
					<a href="#" class="aioseo-dismiss-review-notice-delay" target="_blank" rel="noopener noreferrer">
						<?php echo esc_html( $string10 ); ?>
					</a>&nbsp;&bull;&nbsp;
					<a href="#" class="aioseo-dismiss-review-notice" target="_blank" rel="noopener noreferrer">
						<?php echo esc_html( $string11 ); ?>
					</a>
				</p>
			</div>
		</div>
		<?php
	}

	/**
	 * Print the script for dismissing the notice.
	 *
	 * @since 4.0.13
	 *
	 * @return void
	 */
	public function printScript() {
		// Create a nonce.
		$nonce = wp_create_nonce( 'aioseo-dismiss-review' );
		?>
		<style>
			.aioseop-notice-review_plugin_cta .aioseo-action-buttons {
				display: none;
			}
			@keyframes dismissBtnVisible {
				from { opacity: 0.99; }
				to { opacity: 1; }
			}
			.aioseo-review-plugin-cta button.notice-dismiss {
				animation-duration: 0.001s;
				animation-name: dismissBtnVisible;
			}
		</style>
		<script>
			window.addEventListener('load', function () {
				var aioseoSetupButton,
					dismissBtn

				aioseoSetupButton = function (dismissBtn) {
					var notice      = document.querySelector('.notice.aioseo-review-plugin-cta'),
						delay       = false,
						relay       = true,
						stepOne     = notice.querySelector('.step-1'),
						stepTwo     = notice.querySelector('.step-2'),
						stepThree   = notice.querySelector('.step-3')

					// Add an event listener to the dismiss button.
					dismissBtn.addEventListener('click', function (event) {
						var httpRequest = new XMLHttpRequest(),
							postData    = ''

						// Build the data to send in our request.
						postData += '&delay=' + delay
						postData += '&relay=' + relay
						postData += '&action=aioseo-dismiss-review-plugin-cta'
						postData += '&nonce=<?php echo esc_html( $nonce ); ?>'

						httpRequest.open('POST', '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>')
						httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
						httpRequest.send(postData)
					})

					notice.addEventListener('click', function (event) {
						if (event.target.matches('.aioseo-review-switch-step-3')) {
							event.preventDefault()
							stepOne.style.display   = 'none'
							stepTwo.style.display   = 'none'
							stepThree.style.display = 'block'
						}
						if (event.target.matches('.aioseo-review-switch-step-2')) {
							event.preventDefault()
							stepOne.style.display   = 'none'
							stepThree.style.display = 'none'
							stepTwo.style.display   = 'block'
						}
						if (event.target.matches('.aioseo-dismiss-review-notice-delay')) {
							event.preventDefault()
							delay = true
							relay = false
							dismissBtn.click()
						}
						if (event.target.matches('.aioseo-dismiss-review-notice')) {
							if ('#' === event.target.getAttribute('href')) {
								event.preventDefault()
							}
							relay = false
							dismissBtn.click()
						}
					})
				}

				dismissBtn = document.querySelector('.aioseo-review-plugin-cta .notice-dismiss')
				if (!dismissBtn) {
					document.addEventListener('animationstart', function (event) {
						if (event.animationName == 'dismissBtnVisible') {
							dismissBtn = document.querySelector('.aioseo-review-plugin-cta .notice-dismiss')
							if (dismissBtn) {
								aioseoSetupButton(dismissBtn)
							}
						}
					}, false)

				} else {
					aioseoSetupButton(dismissBtn)
				}
			});
		</script>
		<?php
	}

	/**
	 * Dismiss the review plugin CTA.
	 *
	 * @since 4.0.0
	 *
	 * @return string The successful response.
	 */
	public function dismissNotice() {
		// Early exit if we're not on a aioseo-dismiss-review-plugin-cta action.
		if ( ! isset( $_POST['action'] ) || 'aioseo-dismiss-review-plugin-cta' !== $_POST['action'] ) {
			return;
		}

		check_ajax_referer( 'aioseo-dismiss-review', 'nonce' );
		$delay = isset( $_POST['delay'] ) ? 'true' === sanitize_text_field( wp_unslash( $_POST['delay'] ) ) : false;
		$relay = isset( $_POST['relay'] ) ? 'true' === sanitize_text_field( wp_unslash( $_POST['relay'] ) ) : false;

		if ( ! $delay ) {
			update_user_meta( get_current_user_id(), '_aioseo_plugin_review_dismissed', $relay ? '4' : '3' );

			return wp_send_json_success();
		}

		update_user_meta( get_current_user_id(), '_aioseo_plugin_review_dismissed', strtotime( '+1 week' ) );

		return wp_send_json_success();
	}
}Common/Admin/Notices/WpNotices.php000064400000015515151536241160013067 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin\Notices;

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

/**
 * WpNotices class.
 *
 * @since 4.2.3
 */
class WpNotices {
	/**
	 * Notices array
	 *
	 * @since 4.2.3
	 *
	 * @var array
	 */
	private $notices = [];

	/**
	 * The cache key.
	 *
	 * @since 4.2.3
	 *
	 * @var string
	 */
	private $cacheKey = 'wp_notices';

	/**
	 * Class Constructor.
	 *
	 * @since 4.2.3
	 */
	public function __construct() {
		add_action( 'rest_api_init', [ $this, 'registerApiField' ] );
		add_action( 'enqueue_block_editor_assets', [ $this, 'enqueueScripts' ] );
		add_action( 'admin_notices', [ $this, 'adminNotices' ] );
	}

	/**
	 * Enqueue notices scripts.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	public function enqueueScripts() {
		aioseo()->core->assets->load( 'src/vue/standalone/wp-notices/main.js' );
	}

	/**
	 * Registers an API field with notices.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	public function registerApiField() {
		foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
			register_rest_field( $postType, 'aioseo_notices', [
				'get_callback' => [ $this, 'apiGetNotices' ]
			] );
		}
	}

	/**
	 * API field callback.
	 *
	 * @since 4.2.3
	 *
	 * @return array Notices array
	 */
	public function apiGetNotices() {
		$notices = $this->getNoticesInContext();

		// Notices show only one time.
		$this->removeNotices( $notices );

		return $notices;
	}

	/**
	 * Get all notices.
	 *
	 * @since 4.2.3
	 *
	 * @return array Notices array
	 */
	public function getNotices() {
		if ( empty( $this->notices ) ) {
			$this->notices = (array) aioseo()->core->cache->get( $this->cacheKey );
		}

		return ! empty( $this->notices ) ? $this->notices : [];
	}

	/**
	 * Get all notices in the current context.
	 *
	 * @since 4.2.6
	 *
	 * @return array Notices array
	 */
	public function getNoticesInContext() {
		$contextNotices = $this->getNotices();
		foreach ( $contextNotices as $key => $notice ) {
			if ( empty( $notice['allowedContexts'] ) ) {
				continue;
			}

			$allowed = false;
			foreach ( $notice['allowedContexts'] as $allowedContext ) {
				if ( $this->isAllowedContext( $allowedContext ) ) {
					$allowed = true;
					break;
				}
			}

			if ( ! $allowed ) {
				unset( $contextNotices[ $key ] );
			}
		}

		return $contextNotices;
	}

	/**
	 * Test if we are in the current context.
	 *
	 * @since 4.2.6
	 *
	 * @param  string $context The context to test. (posts)
	 * @return bool            Is the required context.
	 */
	private function isAllowedContext( $context ) {
		switch ( $context ) {
			case 'posts':
				return aioseo()->helpers->isScreenPostList() ||
						aioseo()->helpers->isScreenPostEdit() ||
						aioseo()->helpers->isAjaxCronRestRequest();
		}

		return false;
	}

	/**
	 * Finds a notice by message.
	 *
	 * @since 4.2.3
	 *
	 * @param  string     $message The message string.
	 * @param  string     $type    The message type.
	 * @return void|array          The found notice.
	 */
	public function getNotice( $message, $type = '' ) {
		$notices = $this->getNotices();
		foreach ( $notices as $notice ) {
			if ( $notice['options']['id'] === $this->getNoticeId( $message, $type ) ) {
				return $notice;
			}
		}
	}

	/**
	 * Generates a notice id.
	 *
	 * @since 4.2.3
	 *
	 * @param  string $message The message string.
	 * @param  string $type    The message type.
	 * @return string          The notice id.
	 */
	public function getNoticeId( $message, $type = '' ) {
		return md5( $message . $type );
	}

	/**
	 * Clear notices.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	public function clearNotices() {
		$this->notices = [];
		$this->updateCache();
	}

	/**
	 * Remove certain notices.
	 *
	 * @since 4.2.6
	 *
	 * @param  array $notices A list of notices to remove.
	 * @return void
	 */
	public function removeNotices( $notices ) {
		foreach ( array_keys( $notices ) as $noticeKey ) {
			unset( $this->notices[ $noticeKey ] );
		}
		$this->updateCache();
	}

	/**
	 * Adds a notice.
	 *
	 * @since 4.2.3
	 *
	 * @param  string $message         The message.
	 * @param  string $status          The message status [success, info, warning, error]
	 * @param  array  $options         Options for the message. https://developer.wordpress.org/block-editor/reference-guides/data/data-core-notices/#createnotice
	 * @param  array  $allowedContexts The contexts where this notice will show.
	 * @return void
	 */
	public function addNotice( $message, $status = 'warning', $options = [], $allowedContexts = [] ) {
		$type = ! empty( $options['type'] ) ? $options['type'] : '';
		$foundNotice = $this->getNotice( $message, $type );
		if ( empty( $message ) || ! empty( $foundNotice ) ) {
			return;
		}

		$notice = [
			'message'         => $message,
			'status'          => $status,
			'options'         => wp_parse_args( $options, [
				'id'            => $this->getNoticeId( $message, $type ),
				'isDismissible' => true
			] ),
			'allowedContexts' => $allowedContexts
		];

		$this->notices[] = $notice;
		$this->updateCache();
	}

	/**
	 * Show notices on classic editor.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	public function adminNotices() {
		// Double check we're actually in the admin before outputting anything.
		if ( ! is_admin() ) {
			return;
		}

		$notices = $this->getNoticesInContext();
		foreach ( $notices as $notice ) {
			// Hide snackbar notices on classic editor.
			if ( ! empty( $notice['options']['type'] ) && 'snackbar' === $notice['options']['type'] ) {
				continue;
			}

			$status = ! empty( $notice['status'] ) ? $notice['status'] : 'warning';
			$class  = ! empty( $notice['options']['class'] ) ? $notice['options']['class'] : '';
			?>
			<div
				class="notice notice-<?php echo esc_attr( $status ) ?> <?php echo esc_attr( $class ) ?>">
				<?php echo '<p>' . $notice['message'] . '</p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
				<?php
				if ( ! empty( $notice['options']['actions'] ) ) {
					foreach ( $notice['options']['actions'] as $action ) {
						echo '<p>';
						if ( ! empty( $action['url'] ) ) {
							$class  = ! empty( $action['class'] ) ? $action['class'] : '';
							$target = ! empty( $action['target'] ) ? $action['target'] : '';
							echo '<a 
								href="' . esc_attr( $action['url'] ) . '" 
								class="' . esc_attr( $class ) . '"
								target="' . esc_attr( $target ) . '"
							>';
						}
						echo $action['label']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
						if ( ! empty( $action['url'] ) ) {
							echo '</a>';
						}
						echo '</p>';
					}
					?>
				<?php } ?>
			</div>
			<?php
		}

		// Notices show only one time.
		$this->removeNotices( $notices );
	}

	/**
	 * Helper to update the cache with the current notices array.
	 *
	 * @since 4.2.6
	 *
	 * @return void
	 */
	private function updateCache() {
		aioseo()->core->cache->update( $this->cacheKey, $this->notices );
	}
}Common/Admin/Pointers.php000064400000010011151536241160011335 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Handles the pointers for the admin.
 *
 * @since 4.8.3
 */
class Pointers {
	/**
	 * Class constructor.
	 *
	 * @since 4.8.3
	 */
	public function __construct() {
		if ( ! is_admin() ) {
			return;
		}

		add_action( 'admin_init', [ $this, 'maybeDismissPointer' ] );
		add_action( 'in_admin_header', [ $this, 'init' ] );
	}

	/**
	 * Initializes the pointers.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	public function init() {
		$this->registerKwRankTracker();
	}

	/**
	 * Checks if a pointer should be dismissed.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	public function maybeDismissPointer() {
		if (
			! isset( $_GET['aioseo-dismiss-pointer'] ) ||
			! isset( $_GET['aioseo-dismiss-pointer-nonce'] ) ||
			! wp_verify_nonce( $_GET['aioseo-dismiss-pointer-nonce'], 'aioseo-dismiss-pointer' )
		) {
			return;
		}

		$pointer = sanitize_text_field( wp_unslash( $_GET['aioseo-dismiss-pointer'] ) );
		update_user_meta( get_current_user_id(), "_aioseo-$pointer-dismissed", true );
	}

	/**
	 * Registers a pointer.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	public function registerPointer( $id, $pageSlug, $args ) {
		if ( get_user_meta( get_current_user_id(), "_aioseo-$id-dismissed", true ) ) {
			return;
		}

		if ( "all-in-one-seo_page_aioseo-{$pageSlug}" === aioseo()->helpers->getCurrentScreen()->id ) {
			return;
		}

		wp_enqueue_style( 'wp-pointer' );
		wp_enqueue_script( 'wp-pointer' );

		// phpcs:disable AIOSEO.Wp.I18n.NonSingularStringLiteralText, Squiz.PHP.EmbeddedPhp, Generic.WhiteSpace.ScopeIndent.IncorrectExact
		?>
		<script>
			jQuery( document ).ready( function( $ ) {
				var isClosed = false;
				var pointer  = $( '#toplevel_page_aioseo > a' ).pointer( {
					content :
						"<h3><?php esc_html_e( $args['title'], 'all-in-one-seo-pack' ); ?><\/h3>" +
						"<h4><?php esc_html_e( $args['subtitle'], 'all-in-one-seo-pack' ); ?><\/h4>" +
						"<p><?php esc_html_e( $args['content'], 'all-in-one-seo-pack' ); ?><\/p>" +
						"<?php
							echo sprintf(
								'<p><a class=\"button button-primary\" href=\"%s\">%s</a></p>',
								esc_attr( esc_url( $args['url'] ) ),
								esc_html__( $args['button'], 'all-in-one-seo-pack' )
							);
						?>",
					position : {
						edge  : <?php echo is_rtl() ? "'right'" : "'left'"; ?>,
						align : 'center'
					},
					pointerWidth : 420,
					show: function(event, el) {
						el.pointer.css({'position':'fixed'});
						el.pointer.addClass('aioseo-wp-pointer');
					},
					close : function() {
						isClosed = true;
						jQuery.get(
							window.location.href,
							{
								'aioseo-dismiss-pointer'       : '<?php echo esc_js( $id ); ?>',
								'aioseo-dismiss-pointer-nonce' : '<?php echo esc_js( wp_create_nonce( 'aioseo-dismiss-pointer' ) ); ?>'
							}
						);
					}
				} ).pointer('open');
			} );
		</script>
		<?php
		// phpcs:enable
	}

	/**
	 * Registers the KW Rank Tracker pointer.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	public function registerKwRankTracker() {
		if (
			! current_user_can( 'aioseo_search_statistics_settings' ) ||
			(
				is_object( aioseo()->license ) &&
				aioseo()->license->hasCoreFeature( 'search-statistics', 'keyword-rank-tracker' ) &&
				aioseo()->searchStatistics->api->auth->isConnected()
			)
		) {
			return;
		}

		$nonce = wp_create_nonce( 'aioseo-dismiss-pointer' );

		$args = [
			'title'    => 'NEW! Keyword Rank Tracker',
			'subtitle' => 'Get insights into how your site is performing for your most important keywords',
			'content'  => 'Track keywords and combine them into groups to see how your site is performing for key topics in Google search results.',
			'url'      => admin_url( 'admin.php?aioseo-dismiss-pointer=kw-rank-tracker&aioseo-dismiss-pointer-nonce=' . $nonce . '&page=aioseo-search-statistics#/keyword-rank-tracker' ),
			'button'   => 'Unlock Keyword Rank Tracker'
		];

		$this->registerPointer( 'kw-rank-tracker', 'search-statistics', $args );
	}
}Common/Admin/PostSettings.php000064400000031421151536241160012210 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Abstract class that Pro and Lite both extend.
 *
 * @since 4.0.0
 */
class PostSettings {
	/**
	 * The integrations instance.
	 *
	 * @since 4.4.3
	 *
	 * @var array[object]
	 */
	public $integrations;

	/**
	 * Initialize the admin.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function __construct() {
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}

		// Clear the Post Type Overview cache.
		add_action( 'save_post', [ $this, 'clearPostTypeOverviewCache' ], 100 );
		add_action( 'delete_post', [ $this, 'clearPostTypeOverviewCache' ], 100 );
		add_action( 'wp_trash_post', [ $this, 'clearPostTypeOverviewCache' ], 100 );

		if ( wp_doing_ajax() || wp_doing_cron() || ! is_admin() ) {
			return;
		}

		// Load Vue APP.
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueuePostSettingsAssets' ] );

		// Add metabox.
		add_action( 'add_meta_boxes', [ $this, 'addPostSettingsMetabox' ] );

		// Add metabox (upsell) to terms on init hook.
		add_action( 'init', [ $this, 'init' ], 1000 );

		// Save metabox.
		add_action( 'save_post', [ $this, 'saveSettingsMetabox' ] );
		add_action( 'edit_attachment', [ $this, 'saveSettingsMetabox' ] );
		add_action( 'add_attachment', [ $this, 'saveSettingsMetabox' ] );

		// Filter the sql clauses to show posts filtered by our params.
		add_filter( 'posts_clauses', [ $this, 'changeClausesToFilterPosts' ], 10, 2 );
	}

	/**
	 * Enqueues the JS/CSS for the on page/posts settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function enqueuePostSettingsAssets() {
		if (
			aioseo()->helpers->isScreenBase( 'event-espresso' ) ||
			aioseo()->helpers->isScreenBase( 'post' ) ||
			aioseo()->helpers->isScreenBase( 'term' ) ||
			aioseo()->helpers->isScreenBase( 'edit-tags' ) ||
			aioseo()->helpers->isScreenBase( 'site-editor' )
		) {
			$page = null;
			if (
				aioseo()->helpers->isScreenBase( 'event-espresso' ) ||
				aioseo()->helpers->isScreenBase( 'post' )
			) {
				$page = 'post';
			}

			aioseo()->core->assets->load( 'src/vue/standalone/post-settings/main.js', [], aioseo()->helpers->getVueData( $page ) );
			aioseo()->core->assets->load( 'src/vue/standalone/link-format/main.js', [], aioseo()->helpers->getVueData( $page ) );
		}

		$screen = aioseo()->helpers->getCurrentScreen();
		if ( empty( $screen->id ) ) {
			return;
		}

		if ( 'attachment' === $screen->id ) {
			wp_enqueue_media();
		}
	}

	/**
	 * Check whether or not we can add the metabox.
	 *
	 * @since 4.1.7
	 *
	 * @param  string  $postType The post type to check.
	 * @return boolean           Whether or not can add the Metabox.
	 */
	public function canAddPostSettingsMetabox( $postType ) {
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();

		$pageAnalysisSettingsCapability = aioseo()->access->hasCapability( 'aioseo_page_analysis' );
		$generalSettingsCapability      = aioseo()->access->hasCapability( 'aioseo_page_general_settings' );
		$socialSettingsCapability       = aioseo()->access->hasCapability( 'aioseo_page_social_settings' );
		$schemaSettingsCapability       = aioseo()->access->hasCapability( 'aioseo_page_schema_settings' );
		$aiContentSettingsCapability    = aioseo()->access->hasCapability( 'aioseo_page_ai_content_settings' );
		$linkAssistantCapability        = aioseo()->access->hasCapability( 'aioseo_page_link_assistant_settings' );
		$redirectsCapability            = aioseo()->access->hasCapability( 'aioseo_page_redirects_manage' );
		$advancedSettingsCapability     = aioseo()->access->hasCapability( 'aioseo_page_advanced_settings' );
		$seoRevisionsSettingsCapability = aioseo()->access->hasCapability( 'aioseo_page_seo_revisions_settings' );

		if (
			$dynamicOptions->searchAppearance->postTypes->has( $postType ) &&
			$dynamicOptions->searchAppearance->postTypes->$postType->advanced->showMetaBox &&
			! (
				empty( $pageAnalysisSettingsCapability ) &&
				empty( $generalSettingsCapability ) &&
				empty( $socialSettingsCapability ) &&
				empty( $schemaSettingsCapability ) &&
				empty( $aiContentSettingsCapability ) &&
				empty( $linkAssistantCapability ) &&
				empty( $redirectsCapability ) &&
				empty( $advancedSettingsCapability ) &&
				empty( $seoRevisionsSettingsCapability )
			)
		) {
			return true;
		}

		return false;
	}

	/**
	 * Adds a meta box to page/posts screens.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addPostSettingsMetabox() {
		$screen = aioseo()->helpers->getCurrentScreen();
		if ( empty( $screen->post_type ) ) {
			return;
		}

		$postType = $screen->post_type;
		if ( $this->canAddPostSettingsMetabox( $postType ) ) {
			// Translators: 1 - The plugin short name ("AIOSEO").
			$aioseoMetaboxTitle = sprintf( esc_html__( '%1$s Settings', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME );

			add_meta_box(
				'aioseo-settings',
				$aioseoMetaboxTitle,
				[ $this, 'postSettingsMetabox' ],
				[ $postType ],
				'normal',
				apply_filters( 'aioseo_post_metabox_priority', 'high' )
			);
		}
	}

	/**
	 * Render the on page/posts settings metabox with Vue App wrapper.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function postSettingsMetabox() {
		$this->postSettingsHiddenField();
		?>
		<div id="aioseo-post-settings-metabox">
			<?php aioseo()->templates->getTemplate( 'parts/loader.php' ); ?>
		</div>
		<?php
	}

	/**
	 * Adds the hidden field where all the metabox data goes.
	 *
	 * @since 4.0.17
	 *
	 * @return void
	 */
	public function postSettingsHiddenField() {
		static $fieldExists = false;
		if ( $fieldExists ) {
			return;
		}

		$fieldExists = true;

		?>
		<div id="aioseo-post-settings-field">
			<input type="hidden" name="aioseo-post-settings" id="aioseo-post-settings" value=""/>
			<?php wp_nonce_field( 'aioseoPostSettingsNonce', 'PostSettingsNonce' ); ?>
		</div>
		<?php
	}

	/**
	 * Handles metabox saving.
	 *
	 * @since 4.0.3
	 *
	 * @param  int  $postId Post ID.
	 * @return void
	 */
	public function saveSettingsMetabox( $postId ) {
		if ( ! aioseo()->helpers->isValidPost( $postId, [ 'all' ] ) ) {
			return;
		}

		// Security check.
		if ( ! isset( $_POST['PostSettingsNonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['PostSettingsNonce'] ) ), 'aioseoPostSettingsNonce' ) ) {
			return;
		}

		// If we don't have our post settings input, we can safely skip.
		if ( ! isset( $_POST['aioseo-post-settings'] ) ) {
			return;
		}

		// Check user permissions.
		if ( ! current_user_can( 'edit_post', $postId ) ) {
			return;
		}

		$currentPost = json_decode( wp_unslash( ( $_POST['aioseo-post-settings'] ) ), true );
		$currentPost = aioseo()->helpers->sanitize( $currentPost );

		// If there is no data, there likely was an error, e.g. if the hidden field wasn't populated on load and the user saved the post without making changes in the metabox.
		// In that case we should return to prevent a complete reset of the data.

		if ( empty( $currentPost ) ) {
			return;
		}

		Models\Post::savePost( $postId, $currentPost );
	}

	/**
	 * Clear the Post Type Overview cache from our cache table.
	 *
	 * @since 4.2.0
	 *
	 * @param  int  $postId The Post ID being updated/deleted.
	 * @return void
	 */
	public function clearPostTypeOverviewCache( $postId ) {
		$postType = get_post_type( $postId );
		if ( empty( $postType ) ) {
			return;
		}

		aioseo()->core->cache->delete( $postType . '_overview_data' );
	}

	/**
	 * Get a list of post types with an overview showing how many posts are good, okay and so on.
	 *
	 * @since 4.2.0
	 *
	 * @return array The list of post types with the overview.
	 */
	public function getPostTypesOverview() {
		$overviewData      = [];
		$eligiblePostTypes = aioseo()->helpers->getTruSeoEligiblePostTypes();
		foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
			if ( ! in_array( $postType, $eligiblePostTypes, true ) ) {
				continue;
			}

			$overviewData[ $postType ] = $this->getPostTypeOverview( $postType );
		}

		return $overviewData;
	}

	/**
	 * Get how many posts are good, okay, needs improvement or are missing the focus keyphrase for the given post type.
	 *
	 * @since 4.2.0
	 *
	 * @param  string $postType The post type name.
	 * @return array            The overview data for the given post type.
	 */
	public function getPostTypeOverview( $postType ) {
		$overviewData = aioseo()->core->cache->get( $postType . '_overview_data' );
		if ( null !== $overviewData ) {
			return $overviewData;
		}

		$eligiblePostTypes = aioseo()->helpers->getTruSeoEligiblePostTypes();
		if ( ! in_array( $postType, $eligiblePostTypes, true ) ) {
			return [
				'total'               => 0,
				'withoutFocusKeyword' => 0,
				'needsImprovement'    => 0,
				'okay'                => 0,
				'good'                => 0
			];
		}

		$specialPageIds             = aioseo()->helpers->getSpecialPageIds();
		$implodedPageIdPlaceholders = array_fill( 0, count( $specialPageIds ), '%d' );
		$implodedPageIdPlaceholders = implode( ', ', $implodedPageIdPlaceholders );

		global $wpdb;

		// phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
		$overviewData = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT
					COUNT(*) as total,
					COALESCE( SUM(CASE WHEN ap.keyphrases = '' OR ap.keyphrases IS NULL OR ap.keyphrases LIKE %s THEN 1 ELSE 0 END), 0) as withoutFocusKeyword,
					COALESCE( SUM(CASE WHEN ap.seo_score IS NULL OR ap.seo_score = 0 THEN 1 ELSE 0 END), 0) as withoutTruSeoScore,
					COALESCE( SUM(CASE WHEN ap.seo_score > 0 AND ap.seo_score < 50 THEN 1 ELSE 0 END), 0) as needsImprovement,
					COALESCE( SUM(CASE WHEN ap.seo_score BETWEEN 50 AND 79 THEN 1 ELSE 0 END), 0) as okay,
					COALESCE( SUM(CASE WHEN ap.seo_score >= 80 THEN 1 ELSE 0 END), 0) as good
				FROM {$wpdb->posts} as p
				LEFT JOIN {$wpdb->prefix}aioseo_posts as ap ON ap.post_id = p.ID
				WHERE p.post_status = 'publish'
				AND p.post_type = %s
				AND p.ID NOT IN ( $implodedPageIdPlaceholders )",
				'{"focus":{"keyphrase":""%',
				$postType,
				...array_values( $specialPageIds )
			),
			ARRAY_A
		);

		// Ensure sure all the values are integers.
		foreach ( $overviewData as $key => $value ) {
			$overviewData[ $key ] = (int) $value;
		}

		// Give me the raw SQL of the query.
		aioseo()->core->cache->update( $postType . '_overview_data', $overviewData, HOUR_IN_SECONDS );

		return $overviewData;
	}

	/**
	 * Change the JOIN and WHERE clause to filter just the posts we need to show depending on the query string.
	 *
	 * @since 4.2.0
	 *
	 * @param  array     $clauses Associative array of the clauses for the query.
	 * @param  \WP_Query $query   The WP_Query instance (passed by reference).
	 * @return array              The clauses array updated.
	 */
	public function changeClausesToFilterPosts( $clauses, $query = null ) {
		if ( ! is_admin() || ! $query->is_main_query() ) {
			return $clauses;
		}

		$filter = filter_input( INPUT_GET, 'aioseo-filter' );
		if ( empty( $filter ) ) {
			return $clauses;
		}

		$whereClause        = '';
		$noKeyphrasesClause = "(aioseo_p.keyphrases = '' OR aioseo_p.keyphrases IS NULL OR aioseo_p.keyphrases LIKE '{\"focus\":{\"keyphrase\":\"\"%')";
		switch ( $filter ) {
			case 'withoutFocusKeyword':
				$whereClause = " AND $noKeyphrasesClause ";
				break;
			case 'withoutTruSeoScore':
				$whereClause = ' AND ( aioseo_p.seo_score IS NULL OR aioseo_p.seo_score = 0 ) ';
				break;
			case 'needsImprovement':
				$whereClause = ' AND ( aioseo_p.seo_score > 0 AND aioseo_p.seo_score < 50 ) ';
				break;
			case 'okay':
				$whereClause = ' AND aioseo_p.seo_score BETWEEN 50 AND 80 ';
				break;
			case 'good':
				$whereClause = ' AND aioseo_p.seo_score > 80 ';
				break;
		}

		$prefix            = aioseo()->core->db->prefix;
		$postsTable        = aioseo()->core->db->db->posts;
		$clauses['join']  .= " LEFT JOIN {$prefix}aioseo_posts AS aioseo_p ON ({$postsTable}.ID = aioseo_p.post_id) ";
		$clauses['where'] .= $whereClause;

		add_action( 'wp', [ $this, 'filterPostsAfterChangingClauses' ] );

		return $clauses;
	}

	/**
	 * Filter the posts array to remove the ones that are not eligible for page analysis.
	 * Hooked into `wp` action hook.
	 *
	 * @since 4.7.1
	 *
	 * @return void
	 */
	public function filterPostsAfterChangingClauses() {
		remove_action( 'wp', [ $this, 'filterPostsAfterChangingClauses' ] );
		// phpcs:disable Squiz.NamingConventions.ValidVariableName
		global $wp_query;
		if ( ! empty( $wp_query->posts ) && is_array( $wp_query->posts ) ) {
			$wp_query->posts = array_filter( $wp_query->posts, function ( $post ) {
				return aioseo()->helpers->isTruSeoEligible( $post->ID );
			} );

			// Update `post_count` for pagination.
			if ( isset( $wp_query->post_count ) ) {
				$wp_query->post_count = count( $wp_query->posts );
			}
		}
		// phpcs:enable Squiz.NamingConventions.ValidVariableName
	}
}Common/Admin/SeoAnalysis.php000064400000001661151536241160011777 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

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

use AIOSEO\Plugin\Common\Models\SeoAnalyzerResult;

/**
 * Handles all admin code for the SEO Analysis menu.
 *
 * @since 4.2.6
 */
class SeoAnalysis {
	/**
	 * Class constructor.
	 *
	 * @since 4.2.6
	 */
	public function __construct() {
		add_action( 'save_post', [ $this, 'bustStaticHomepageResults' ] );
	}

	/**
	 * Busts the SEO Analysis for the static homepage when it is updated.
	 *
	 * @since 4.2.6
	 *
	 * @param  int  $postId The post ID.
	 * @return void
	 */
	public function bustStaticHomepageResults( $postId ) {
		if ( ! aioseo()->helpers->isStaticHomePage( $postId ) ) {
			return;
		}

		aioseo()->internalOptions->internal->siteAnalysis->score = 0;
		SeoAnalyzerResult::deleteByUrl( null );

		aioseo()->core->cache->delete( 'analyze_site_code' );
		aioseo()->core->cache->delete( 'analyze_site_body' );
	}
}Common/Admin/SiteHealth.php000064400000040333151536241160011576 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

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

/**
 * WP Site Health class.
 *
 * @since 4.0.0
 */
class SiteHealth {
	/**
	 * Class Constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_filter( 'site_status_tests', [ $this, 'registerTests' ], 0 );
		add_filter( 'debug_information', [ $this, 'addDebugInfo' ], 0 );
	}

	/**
	 * Add AIOSEO WP Site Health tests.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $tests The current filters array.
	 * @return array
	 */
	public function registerTests( $tests ) {
		$tests['direct']['aioseo_site_public'] = [
			'label' => 'AIOSEO Site Public',
			'test'  => [ $this, 'testCheckSitePublic' ],
		];
		$tests['direct']['aioseo_site_info'] = [
			'label' => 'AIOSEO Site Info',
			'test'  => [ $this, 'testCheckSiteInfo' ],
		];
		$tests['direct']['aioseo_google_search_console'] = [
			'label' => 'AIOSEO Google Search Console',
			'test'  => [ $this, 'testCheckGoogleSearchConsole' ],
		];
		$tests['direct']['aioseo_plugin_update'] = [
			'label' => 'AIOSEO Plugin Update',
			'test'  => [ $this, 'testCheckPluginUpdate' ],
		];

		$tests['direct']['aioseo_schema_markup'] = [
			'label' => 'AIOSEO Schema Markup',
			'test'  => [ $this, 'testCheckSchemaMarkup' ],
		];

		return $tests;
	}

	/**
	 * Adds our site health debug info.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $debugInfo The debug info.
	 * @return array $debugInfo The debug info.
	 */
	public function addDebugInfo( $debugInfo ) {
		$fields = [];

		$noindexed = $this->noindexed();
		if ( $noindexed ) {
			$fields['noindexed'] = $this->field(
				__( 'Noindexed content', 'all-in-one-seo-pack' ),
				implode( ', ', $noindexed )
			);
		}

		$nofollowed = $this->nofollowed();
		if ( $nofollowed ) {
			$fields['nofollowed'] = $this->field(
				__( 'Nofollowed content', 'all-in-one-seo-pack' ),
				implode( ', ', $nofollowed )
			);
		}

		if ( ! count( $fields ) ) {
			return $debugInfo;
		}

		$debugInfo['aioseo'] = [
			'label'       => __( 'SEO', 'all-in-one-seo-pack' ),
			'description' => sprintf(
				// Translators: 1 - The plugin short name ("AIOSEO").
				__( 'The fields below contain important SEO information from %1$s that may effect your site.', 'all-in-one-seo-pack' ),
				AIOSEO_PLUGIN_SHORT_NAME
			),
			'private'     => false,
			'show_count'  => true,
			'fields'      => $fields,
		];

		return $debugInfo;
	}

	/**
	 * Checks whether the site is public.
	 *
	 * @since 4.0.0
	 *
	 * @return array The test result.
	 */
	public function testCheckSitePublic() {
		$test = 'aioseo_site_public';

		if ( ! get_option( 'blog_public' ) ) {
			return $this->result(
				$test,
				'critical',
				__( 'Your site does not appear in search results', 'all-in-one-seo-pack' ),
				__( 'Your site is set to private. This means WordPress asks search engines to exclude your website from search results.', 'all-in-one-seo-pack' ),
				$this->actionLink( admin_url( 'options-reading.php' ), __( 'Go to Settings > Reading', 'all-in-one-seo-pack' ) )
			);
		}

		return $this->result(
			$test,
			'good',
			__( 'Your site appears in search results', 'all-in-one-seo-pack' ),
			__( 'Your site is set to public. Search engines will index your website and it will appear in search results.', 'all-in-one-seo-pack' )
		);
	}

	/**
	 * Checks whether the site title and tagline are set.
	 *
	 * @since 4.0.0
	 *
	 * @return array The test result.
	 */
	public function testCheckSiteInfo() {
		$siteTitle   = get_bloginfo( 'name' );
		$siteTagline = get_bloginfo( 'description' );

		if ( ! $siteTitle || ! $siteTagline ) {
			return $this->result(
				'aioseo_site_info',
				'recommended',
				__( 'Your Site Title and/or Tagline are blank', 'all-in-one-seo-pack' ),
				sprintf(
					// Translators: 1 - The plugin short name ("AIOSEO").
					__(
						'Your Site Title and/or Tagline are blank. We recommend setting both of these values as %1$s requires these for various features, including our schema markup',
						'all-in-one-seo-pack'
					),
					AIOSEO_PLUGIN_SHORT_NAME
				),
				$this->actionLink( admin_url( 'options-general.php' ), __( 'Go to Settings > General', 'all-in-one-seo-pack' ) )
			);
		}

		return $this->result(
			'aioseo_site_info',
			'good',
			__( 'Your Site Title and Tagline are set', 'all-in-one-seo-pack' ),
			sprintf(
				// Translators: 1 - The plugin short name ("AIOSEO").
				__( 'Great! These are required for %1$s\'s schema markup and are often used as fallback values for various other features.', 'all-in-one-seo-pack' ),
				AIOSEO_PLUGIN_SHORT_NAME
			)
		);
	}

	/**
	 * Checks whether Google Search Console is connected.
	 *
	 * @since 4.6.2
	 *
	 * @return array The test result.
	 */
	public function testCheckGoogleSearchConsole() {
		$googleSearchConsole = aioseo()->searchStatistics->api->auth->isConnected();

		if ( ! $googleSearchConsole ) {
			return $this->result(
				'aioseo_google_search_console',
				'recommended',
				__( 'Connect Your Site with Google Search Console', 'all-in-one-seo-pack' ),
				__( 'Sync your site with Google Search Console and get valuable insights right inside your WordPress dashboard. Track keyword rankings and search performance for individual posts with actionable insights to help you rank higher in search results!', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				$this->actionLink( admin_url( 'admin.php?page=aioseo-settings&aioseo-scroll=google-search-console-settings&aioseo-highlight=google-search-console-settings#/webmaster-tools?activetool=googleSearchConsole' ), __( 'Connect to Google Search Console', 'all-in-one-seo-pack' ) ) // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			);
		}

		return $this->result(
			'aioseo_google_search_console',
			'good',
			__( 'Google Search Console is Connected', 'all-in-one-seo-pack' ),
			__( 'Awesome! Google Search Console is connected to your site. This will help you monitor and maintain your site\'s presence in Google Search results.', 'all-in-one-seo-pack' )
		);
	}

	/**
	 * Checks whether the required settings for our schema markup are set.
	 *
	 * @since 4.0.0
	 *
	 * @return array The test result.
	 */
	public function testCheckSchemaMarkup() {
		$menuPath = admin_url( 'admin.php?page=aioseo-search-appearance' );

		if ( 'organization' === aioseo()->options->searchAppearance->global->schema->siteRepresents ) {
			if (
				! aioseo()->options->searchAppearance->global->schema->organizationName ||
				(
					! aioseo()->options->searchAppearance->global->schema->organizationLogo &&
					! aioseo()->helpers->getSiteLogoUrl()
				)
			) {
				return $this->result(
					'aioseo_schema_markup',
					'recommended',
					__( 'Your Organization Name and/or Logo are blank', 'all-in-one-seo-pack' ),
					sprintf(
						// Translators: 1 - The plugin short name ("AIOSEO").
						__( 'Your Organization Name and/or Logo are blank. These values are required for %1$s\'s Organization schema markup.', 'all-in-one-seo-pack' ),
						AIOSEO_PLUGIN_SHORT_NAME
					),
					$this->actionLink( $menuPath, __( 'Go to Schema Settings', 'all-in-one-seo-pack' ) )
				);
			}

			return $this->result(
				'aioseo_schema_markup',
				'good',
				__( 'Your Organization Name and Logo are set', 'all-in-one-seo-pack' ),
				sprintf(
					// Translators: 1 - The plugin short name ("AIOSEO").
					__( 'Awesome! These are required for %1$s\'s Organization schema markup.', 'all-in-one-seo-pack' ),
					AIOSEO_PLUGIN_SHORT_NAME
				)
			);
		}

		if (
			! aioseo()->options->searchAppearance->global->schema->person ||
			(
				'manual' === aioseo()->options->searchAppearance->global->schema->person &&
				(
					! aioseo()->options->searchAppearance->global->schema->personName ||
					! aioseo()->options->searchAppearance->global->schema->personLogo
				)
			)
		) {
			return $this->result(
				'aioseo_schema_markup',
				'recommended',
				__( 'Your Person Name and/or Image are blank', 'all-in-one-seo-pack' ),
				sprintf(
					// Translators: 1 - The plugin short name ("AIOSEO").
					__( 'Your Person Name and/or Image are blank. These values are required for %1$s\'s Person schema markup.', 'all-in-one-seo-pack' ),
					AIOSEO_PLUGIN_SHORT_NAME
				),
				$this->actionLink( $menuPath, __( 'Go to Schema Settings', 'all-in-one-seo-pack' ) )
			);
		}

		return $this->result(
			'aioseo_schema_markup',
			'good',
			__( 'Your Person Name and Image are set', 'all-in-one-seo-pack' ),
			sprintf(
				// Translators: 1 - The plugin short name ("AIOSEO").
				__( 'Awesome! These are required for %1$s\'s Person schema markup.', 'all-in-one-seo-pack' ),
				AIOSEO_PLUGIN_SHORT_NAME
			)
		);
	}

	/**
	 * Checks if the plugin should be updated.
	 *
	 * @since 4.7.2
	 *
	 * @return bool Whether the plugin should be updated.
	 */
	public function shouldUpdate() {
		$response = wp_remote_get( 'https://api.wordpress.org/plugins/info/1.0/all-in-one-seo-pack.json' );
		$body     = wp_remote_retrieve_body( $response );
		if ( ! $body ) {
			// Something went wrong.
			return false;
		}

		$pluginData = json_decode( $body );

		return version_compare( AIOSEO_VERSION, $pluginData->version, '<' );
	}

	/**
	 * Checks whether the required settings for our schema markup are set.
	 *
	 * @since 4.0.0
	 *
	 * @return array The test result.
	 */
	public function testCheckPluginUpdate() {
		if ( $this->shouldUpdate() ) {
			return $this->result(
				'aioseo_plugin_update',
				'critical',
				sprintf(
					// Translators: 1 - The plugin short name ("AIOSEO").
					__( '%1$s needs to be updated', 'all-in-one-seo-pack' ),
					AIOSEO_PLUGIN_SHORT_NAME
				),
				sprintf(
					// Translators: 1 - The plugin short name ("AIOSEO").
					__( 'An update is available for %1$s. Upgrade to the latest version to receive all the latest features, bug fixes and security improvements.', 'all-in-one-seo-pack' ),
					AIOSEO_PLUGIN_SHORT_NAME
				),
				$this->actionLink( admin_url( 'plugins.php' ), __( 'Go to Plugins', 'all-in-one-seo-pack' ) )
			);
		}

		return $this->result(
			'aioseo_plugin_update',
			'good',
			sprintf(
				// Translators: 1 - The plugin short name ("AIOSEO").
				__( '%1$s is updated to the latest version', 'all-in-one-seo-pack' ),
				AIOSEO_PLUGIN_SHORT_NAME
			),
			__( 'Fantastic! By updating to the latest version, you have access to all the latest features, bug fixes and security improvements.', 'all-in-one-seo-pack' )
		);
	}

	/**
	 * Returns a list of noindexed content.
	 *
	 * @since 4.0.0
	 *
	 * @return array $noindexed A list of noindexed content.
	 */
	protected function noindexed() {
		$globalDefault = aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default;
		if (
			! $globalDefault &&
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindex
		) {
			return [
				__( 'Your entire site is set to globally noindex content.', 'all-in-one-seo-pack' )
			];
		}

		$noindexed = [];

		if (
			! $globalDefault &&
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindexPaginated
		) {
			$noindexed[] = __( 'Paginated Content', 'all-in-one-seo-pack' );
		}

		$archives = [
			'author' => __( 'Author Archives', 'all-in-one-seo-pack' ),
			'date'   => __( 'Date Archives', 'all-in-one-seo-pack' ),
			'search' => __( 'Search Page', 'all-in-one-seo-pack' )
		];

		// Archives.
		foreach ( $archives as $name => $type ) {
			if (
				! aioseo()->options->searchAppearance->archives->{ $name }->advanced->robotsMeta->default &&
				aioseo()->options->searchAppearance->archives->{ $name }->advanced->robotsMeta->noindex
			) {
				$noindexed[] = $type;
			}
		}

		foreach ( aioseo()->helpers->getPublicPostTypes() as $postType ) {
			if (
				aioseo()->dynamicOptions->searchAppearance->postTypes->has( $postType['name'] ) &&
				! aioseo()->dynamicOptions->searchAppearance->postTypes->{ $postType['name'] }->advanced->robotsMeta->default &&
				aioseo()->dynamicOptions->searchAppearance->postTypes->{ $postType['name'] }->advanced->robotsMeta->noindex
			) {
				$noindexed[] = $postType['label'] . ' (' . $postType['name'] . ')';
			}
		}

		foreach ( aioseo()->helpers->getPublicTaxonomies() as $taxonomy ) {
			if (
				aioseo()->dynamicOptions->searchAppearance->taxonomies->has( $taxonomy['name'] ) &&
				! aioseo()->dynamicOptions->searchAppearance->taxonomies->{ $taxonomy['name'] }->advanced->robotsMeta->default &&
				aioseo()->dynamicOptions->searchAppearance->taxonomies->{ $taxonomy['name'] }->advanced->robotsMeta->noindex
			) {
				$noindexed[] = $taxonomy['label'] . ' (' . $taxonomy['name'] . ')';
			}
		}

		return $noindexed;
	}

	/**
	 * Returns a list of nofollowed content.
	 *
	 * @since 4.0.0
	 *
	 * @return array $nofollowed A list of nofollowed content.
	 */
	protected function nofollowed() {
		$globalDefault = aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default;
		if (
			! $globalDefault &&
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->nofollow
		) {
			return [
				__( 'Your entire site is set to globally nofollow content.', 'all-in-one-seo-pack' )
			];
		}

		$nofollowed = [];

		if (
			! $globalDefault &&
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->nofollowPaginated
		) {
			$nofollowed[] = __( 'Paginated Content', 'all-in-one-seo-pack' );
		}

		$archives = [
			'author' => __( 'Author Archives', 'all-in-one-seo-pack' ),
			'date'   => __( 'Date Archives', 'all-in-one-seo-pack' ),
			'search' => __( 'Search Page', 'all-in-one-seo-pack' )
		];

		// Archives.
		foreach ( $archives as $name => $type ) {
			if (
				! aioseo()->options->searchAppearance->archives->{ $name }->advanced->robotsMeta->default &&
				aioseo()->options->searchAppearance->archives->{ $name }->advanced->robotsMeta->nofollow
			) {
				$nofollowed[] = $type;
			}
		}

		foreach ( aioseo()->helpers->getPublicPostTypes() as $postType ) {
			if (
				aioseo()->dynamicOptions->searchAppearance->postTypes->has( $postType['name'] ) &&
				! aioseo()->dynamicOptions->searchAppearance->postTypes->{ $postType['name'] }->advanced->robotsMeta->default &&
				aioseo()->dynamicOptions->searchAppearance->postTypes->{ $postType['name'] }->advanced->robotsMeta->nofollow
			) {
				$nofollowed[] = $postType['label'] . ' (' . $postType['name'] . ')';
			}
		}

		foreach ( aioseo()->helpers->getPublicTaxonomies() as $taxonomy ) {
			if (
				aioseo()->dynamicOptions->searchAppearance->taxonomies->has( $taxonomy['name'] ) &&
				! aioseo()->dynamicOptions->searchAppearance->taxonomies->{ $taxonomy['name'] }->advanced->robotsMeta->default &&
				aioseo()->dynamicOptions->searchAppearance->taxonomies->{ $taxonomy['name'] }->advanced->robotsMeta->nofollow
			) {
				$nofollowed[] = $taxonomy['label'] . ' (' . $taxonomy['name'] . ')';
			}
		}

		return $nofollowed;
	}

	/**
	 * Returns a debug info data field.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $label   The field label.
	 * @param  string  $value   The field value.
	 * @param  boolean $private Whether the field shouldn't be included if the debug info is copied.
	 * @return array            The debug info data field.
	 */
	private function field( $label, $value, $private = false ) {
		return [
			'label'   => $label,
			'value'   => $value,
			'private' => $private,
		];
	}

	/**
	 * Returns the test result.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name        The test name.
	 * @param  string $status      The result status.
	 * @param  string $header      The test header.
	 * @param  string $description The result description.
	 * @param  string $actions     The result actions.
	 * @return array               The test result.
	 */
	protected function result( $name, $status, $header, $description, $actions = '' ) {
		$color = 'blue';
		switch ( $status ) {
			case 'good':
				break;
			case 'recommended':
				$color = 'orange';
				break;
			case 'critical':
				$color = 'red';
				break;
			default:
				break;
		}

		return [
			'test'        => $name,
			'status'      => $status,
			'label'       => $header,
			'description' => $description,
			'actions'     => $actions,
			'badge'       => [
				'label' => AIOSEO_PLUGIN_SHORT_NAME,
				'color' => $color,
			],
		];
	}

	/**
	 * Returns an action link.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $path   The path.
	 * @param  string $anchor The anchor text.
	 * @return string         The action link.
	 */
	protected function actionLink( $path, $anchor ) {
		return sprintf(
			'<p><a href="%1$s">%2$s</a></p>',
			$path,
			$anchor
		);
	}
}Common/Admin/SlugMonitor.php000064400000012421151536241160012023 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

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

/**
 * Monitors changes to post slugs.
 *
 * @since 4.2.3
 */
class SlugMonitor {
	/**
	 * Holds posts that have been updated.
	 *
	 * @since 4.2.3
	 *
	 * @var array
	 */
	private $updatedPosts = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.2.3
	 */
	public function __construct() {
		// We can't monitor changes without permalinks enabled.
		if ( ! get_option( 'permalink_structure' ) ) {
			return;
		}

		add_action( 'pre_post_update', [ $this, 'prePostUpdate' ] );

		// WP 5.6+.
		if ( function_exists( 'wp_after_insert_post' ) ) {
			add_action( 'wp_after_insert_post', [ $this, 'afterInsertPost' ], 11, 4 );
		} else {
			add_action( 'post_updated', [ $this, 'postUpdated' ], 11, 3 );
		}
	}

	/**
	 * Remember the previous post permalink.
	 *
	 * @since 4.2.3
	 *
	 * @param  integer $postId The post ID.
	 * @return void
	 */
	public function prePostUpdate( $postId ) {
		$this->updatedPosts[ $postId ] = get_permalink( $postId );
	}

	/**
	 * Called when a post has been completely inserted ( with categories and meta ).
	 *
	 * @since 4.2.3
	 *
	 * @param  integer       $postId     The post ID.
	 * @param  \WP_Post      $post       The post object.
	 * @param  bool          $update     Whether this is an existing post being updated.
	 * @param  null|\WP_Post $postBefore The post object before changes were made.
	 * @return void
	 */
	public function afterInsertPost( $postId, $post = null, $update = false, $postBefore = null ) {
		if ( ! $update ) {
			return;
		}

		$this->postUpdated( $postId, $post, $postBefore );
	}

	/**
	 * Called when a post has been updated - check if the slug has changed.
	 *
	 * @since 4.2.3
	 *
	 * @param  integer  $postId     The post ID.
	 * @param  \WP_Post $post       The post object.
	 * @param  \WP_Post $postBefore The post object before changes were made.
	 * @return void
	 */
	public function postUpdated( $postId, $post = null, $postBefore = null ) {
		if ( ! isset( $this->updatedPosts[ $postId ] ) ) {
			return;
		}

		$before = aioseo()->helpers->getPermalinkPath( $this->updatedPosts[ $postId ] );
		$after  = aioseo()->helpers->getPermalinkPath( get_permalink( $postId ) );
		if ( ! aioseo()->helpers->hasPermalinkChanged( $before, $after ) ) {
			return;
		}

		// Can we monitor this slug?
		if ( ! $this->canMonitorPost( $post, $postBefore ) ) {
			return;
		}

		// Ask aioseo-redirects if automatic redirects is monitoring it.
		if ( $this->automaticRedirect( $post->post_type, $before, $after ) ) {
			return;
		}

		// Filter to allow users to disable the slug monitor messages.
		if ( apply_filters( 'aioseo_redirects_disable_slug_monitor', false ) ) {
			return;
		}

		$redirectUrl = $this->manualRedirectUrl( [
			'url'    => $before,
			'target' => $after,
			'type'   => 301
		] );

		$message = __( 'The permalink for this post just changed! This could result in 404 errors for your site visitors.', 'all-in-one-seo-pack' );

		// Default notice redirecting to the Redirects screen.
		$action = [
			'url'    => $redirectUrl,
			'label'  => __( 'Add Redirect to improve SEO', 'all-in-one-seo-pack' ),
			'target' => '_blank',
			'class'  => 'aioseo-redirects-slug-changed'
		];

		// If redirects is active we'll show add-redirect in a modal.
		if ( aioseo()->addons->getLoadedAddon( 'redirects' ) ) {
			// We need to remove the target here so the action keeps the url used by the add-redirect modal.
			unset( $action['target'] );
		}

		aioseo()->wpNotices->addNotice( $message, 'warning', [ 'actions' => [ $action ] ], [ 'posts' ] );
	}

	/**
	 * Checks if this is a post we can monitor.
	 *
	 * @since 4.2.3
	 *
	 * @param  \WP_Post $post       The post object.
	 * @param  \WP_Post $postBefore The post object before changes were made.
	 * @return boolean              True if we can monitor this post.
	 */
	private function canMonitorPost( $post, $postBefore ) {
		// Check that this is for the expected post.
		if ( ! isset( $post->ID ) || ! isset( $this->updatedPosts[ $post->ID ] ) ) {
			return false;
		}

		// Don't do anything if we're not published.
		if ( 'publish' !== $post->post_status || 'publish' !== $postBefore->post_status ) {
			return false;
		}

		// Don't do anything is the post type is not public.
		if ( ! is_post_type_viewable( $post->post_type ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Tries to add a automatic redirect.
	 *
	 * @since 4.2.3
	 *
	 * @param  string $postType The post type.
	 * @param  string $before   The url before.
	 * @param  string $after    The url after.
	 * @return bool             True if an automatic redirect was added.
	 */
	private function automaticRedirect( $postType, $before, $after ) {
		if ( ! aioseo()->addons->getLoadedAddon( 'redirects' ) ) {
			return false;
		}

		return aioseoRedirects()->monitor->automaticRedirect( $postType, $before, $after );
	}

	/**
	 * Generates a URL for adding manual redirects.
	 *
	 * @since 4.2.3
	 *
	 * @param  array  $urls An array of [url, target, type, slash, case, regex].
	 * @return string       The redirect link.
	 */
	public function manualRedirectUrl( $urls ) {
		if ( ! aioseo()->addons->getLoadedAddon( 'redirects' ) ) {
			return admin_url( 'admin.php?page=aioseo-redirects' );
		}

		return aioseoRedirects()->helpers->manualRedirectUrl( $urls );
	}
}Common/Admin/Usage.php000064400000013743151536241160010615 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

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

/**
 * Usage tracking class.
 *
 * @since 4.0.0
 */
abstract class Usage {
	/**
	 * Returns the current plugin version type ("lite" or "pro").
	 *
	 * @since 4.1.3
	 *
	 * @return string The version type.
	 */
	abstract public function getType();

	/**
	 * Source of notifications content.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $url = 'https://aiousage.com/v1/track';

	/**
	 * Whether or not usage tracking is enabled.
	 *
	 * @since 4.0.0
	 *
	 * @var bool
	 */
	protected $enabled = false;

	/**
	 * Class Constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'init' ], 2 );
	}

	/**
	 * Runs on the init action.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function init() {
		try {
			$action = 'aioseo_send_usage_data';
			if ( ! $this->enabled ) {
				aioseo()->actionScheduler->unschedule( $action );

				return;
			}

			// Register the action handler.
			add_action( $action, [ $this, 'process' ] );

			if ( ! as_next_scheduled_action( $action ) ) {
				as_schedule_recurring_action( $this->generateStartDate(), WEEK_IN_SECONDS, $action, [], 'aioseo' );

				// Run the task immediately using an async action.
				as_enqueue_async_action( $action, [], 'aioseo' );
			}
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Processes the usage tracking.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function process() {
		if ( ! $this->enabled ) {
			return;
		}

		wp_remote_post(
			$this->getUrl(),
			[
				'timeout'    => 10,
				'headers'    => array_merge( [
					'Content-Type' => 'application/json; charset=utf-8'
				], aioseo()->helpers->getApiHeaders() ),
				'user-agent' => aioseo()->helpers->getApiUserAgent(),
				'body'       => wp_json_encode( $this->getData() )
			]
		);
	}

	/**
	 * Gets the URL for the notifications api.
	 *
	 * @since 4.0.0
	 *
	 * @return string The URL to use for the api requests.
	 */
	private function getUrl() {
		if ( defined( 'AIOSEO_USAGE_TRACKING_URL' ) ) {
			return AIOSEO_USAGE_TRACKING_URL;
		}

		return $this->url;
	}

	/**
	 * Retrieves the data to send in the usage tracking.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of data to send.
	 */
	protected function getData() {
		$themeData = wp_get_theme();
		$type      = $this->getType();

		return [
			// Generic data (environment).
			'url'                           => home_url(),
			'php_version'                   => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION,
			'wp_version'                    => get_bloginfo( 'version' ),
			'mysql_version'                 => aioseo()->core->db->db->db_version(),
			'server_version'                => isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '',
			'is_ssl'                        => is_ssl(),
			'is_multisite'                  => is_multisite(),
			'sites_count'                   => function_exists( 'get_blog_count' ) ? (int) get_blog_count() : 1,
			'active_plugins'                => $this->getActivePlugins(),
			'theme_name'                    => $themeData->name,
			'theme_version'                 => $themeData->version,
			'user_count'                    => function_exists( 'get_user_count' ) ? get_user_count() : null,
			'locale'                        => get_locale(),
			'timezone_offset'               => wp_timezone_string(),
			'email'                         => get_bloginfo( 'admin_email' ),
			// AIOSEO specific data.
			'aioseo_version'                => AIOSEO_VERSION,
			'aioseo_license_key'            => null,
			'aioseo_license_type'           => null,
			'aioseo_is_pro'                 => false,
			"aioseo_{$type}_installed_date" => aioseo()->internalOptions->internal->installed,
			'aioseo_settings'               => $this->getSettings()
		];
	}

	/**
	 * Get the settings and escape the quotes so it can be JSON encoded.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of settings data.
	 */
	private function getSettings() {
		$settings = aioseo()->options->all();
		array_walk_recursive( $settings, function( &$v ) {
			if ( is_string( $v ) && strpos( $v, '&quot' ) !== false ) {
				$v = str_replace( '&quot', '&#x5c;&quot', $v );
			}
		});

		$settings = $this->filterPrivateSettings( $settings );

		$internal = aioseo()->internalOptions->all();
		array_walk_recursive( $internal, function( &$v ) {
			if ( is_string( $v ) && strpos( $v, '&quot' ) !== false ) {
				$v = str_replace( '&quot', '&#x5c;&quot', $v );
			}
		});

		return [
			'options'  => $settings,
			'internal' => $internal
		];
	}

	/**
	 * Return a list of active plugins.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of active plugin data.
	 */
	private function getActivePlugins() {
		if ( ! function_exists( 'get_plugins' ) ) {
			include ABSPATH . '/wp-admin/includes/plugin.php';
		}
		$active  = get_option( 'active_plugins', [] );
		$plugins = array_intersect_key( get_plugins(), array_flip( $active ) );

		return array_map(
			static function ( $plugin ) {
				if ( isset( $plugin['Version'] ) ) {
					return $plugin['Version'];
				}

				return 'Not Set';
			},
			$plugins
		);
	}

	/**
	 * Generate a random start date for usage tracking.
	 *
	 * @since 4.0.0
	 *
	 * @return integer The randomized start date.
	 */
	private function generateStartDate() {
		$tracking = [
			'days'    => wp_rand( 0, 6 ) * DAY_IN_SECONDS,
			'hours'   => wp_rand( 0, 23 ) * HOUR_IN_SECONDS,
			'minutes' => wp_rand( 0, 23 ) * HOUR_IN_SECONDS,
			'seconds' => wp_rand( 0, 59 )
		];

		return strtotime( 'next sunday' ) + array_sum( $tracking );
	}

	/**
	 * Anonimizes or obfuscates the value of certain settings.
	 *
	 * @since 4.3.2
	 *
	 * @param  array $settings The settings.
	 * @return array           The altered settings.
	 */
	private function filterPrivateSettings( $settings ) {
		if ( ! empty( $settings['localBusiness']['maps']['apiKey'] ) ) {
			$settings['localBusiness']['maps']['apiKey'] = true;
		}

		return $settings;
	}
}Common/Admin/WritingAssistant.php000064400000005005151536241160013056 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

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

use AIOSEO\Plugin\Common\Models;

/**
 * The Admin class.
 *
 * @since 4.7.4
 */
class WritingAssistant {
	/**
	 * Class constructor.
	 *
	 * @since 4.7.4
	 */
	public function __construct() {
		add_action( 'add_meta_boxes', [ $this, 'addMetabox' ] );
		add_action( 'delete_post', [ $this, 'deletePost' ] );
	}

	/**
	 * Deletes the writing assistant post.
	 *
	 * @since 4.7.4
	 *
	 * @param  int  $postId The post id.
	 * @return void
	 */
	public function deletePost( $postId ) {
		Models\WritingAssistantPost::getPost( $postId )->delete();
	}

	/**
	 * Adds a meta box to the page/posts screens.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	public function addMetabox() {
		if ( ! aioseo()->access->hasCapability( 'aioseo_page_writing_assistant_settings' ) ) {
			return;
		}

		$postType = get_post_type();
		if (
			(
				! aioseo()->options->writingAssistant->postTypes->all &&
				! in_array( $postType, aioseo()->options->writingAssistant->postTypes->included, true )
			) ||
			! in_array( $postType, aioseo()->helpers->getPublicPostTypes( true ), true )
		) {
			return;
		}

		// Skip post types that do not support an editor.
		if ( ! post_type_supports( $postType, 'editor' ) ) {
			return;
		}

		// Ignore certain plugins.
		if (
			aioseo()->thirdParty->webStories->isPluginActive() &&
			'web-story' === $postType
		) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAssets' ] );

		// Translators: 1 - The plugin short name ("AIOSEO").
		$aioseoMetaboxTitle = sprintf( esc_html__( '%1$s Writing Assistant', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME );

		add_meta_box(
			'aioseo-writing-assistant-metabox',
			$aioseoMetaboxTitle,
			[ $this, 'renderMetabox' ],
			null,
			'normal',
			'low'
		);
	}

	/**
	 * Render the on-page settings metabox with the Vue App wrapper.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	public function renderMetabox() {
		?>
		<div id="aioseo-writing-assistant-metabox-app">
			<?php aioseo()->templates->getTemplate( 'parts/loader.php' ); ?>
		</div>
		<?php
	}

	/**
	 * Enqueues the JS/CSS for the standalone.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	public function enqueueAssets() {
		if ( ! aioseo()->helpers->isScreenBase( 'post' ) ) {
			return;
		}

		aioseo()->core->assets->load(
			'src/vue/standalone/writing-assistant/main.js',
			[],
			aioseo()->writingAssistant->helpers->getStandaloneVueData(),
			'aioseoWritingAssistant'
		);
	}
}Common/Ai/Ai.php000064400000014304151536241160007375 0ustar00<?php

namespace AIOSEO\Plugin\Common\Ai;

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

/**
 * AI class.
 *
 * @since 4.8.4
 */
class Ai {
	/**
	 * The base URL for the licensing server.
	 *
	 * @since 4.8.4
	 *
	 * @var string
	 */
	private $licensingUrl = 'https://licensing.aioseo.com/v1/';

	/**
	 * The action name for fetching credits.
	 *
	 * @since 4.8.4
	 *
	 * @var string
	 */
	protected $creditFetchAction = 'aioseo_ai_update_credits';

	/**
	 * Class constructor.
	 *
	 * @since 4.8.4
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'getAccessToken' ] );

		add_action( 'init', [ $this, 'scheduleCreditFetchAction' ] );
		add_action( $this->creditFetchAction, [ $this, 'updateCredits' ] );

		// If param is set, fetch credits but just once per 5 minutes to prevent abuse.
		if ( isset( $_REQUEST['aioseo-ai-credits'] ) && ! aioseo()->core->cache->get( 'ai_get_credits' ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended
			add_action( 'init', [ $this, 'updateCredits' ] );

			aioseo()->core->cache->update( 'ai_get_credits', true, 5 * MINUTE_IN_SECONDS );
		}
	}

	/**
	 * Gets an access token from the server.
	 * This is the one-time access token that includes 50 free credits.
	 *
	 * @since 4.8.4
	 *
	 * @param  bool $refresh Whether to refresh the access token.
	 * @return void
	 */
	public function getAccessToken( $refresh = false ) {
		// Check if user has an access token. If not, get one from the server.
		if ( aioseo()->internalOptions->internal->ai->accessToken && ! $refresh ) {
			return;
		}

		if ( aioseo()->cache->get( 'ai-access-token-error' ) ) {
			return;
		}

		$response = wp_remote_post( $this->getApiUrl() . 'ai/auth/', [
			'body' => [
				'domain' => aioseo()->helpers->getSiteDomain()
			]
		] );

		if ( is_wp_error( $response ) ) {
			aioseo()->cache->update( 'ai-access-token-error', true, 1 * HOUR_IN_SECONDS );

			// Schedule another, one-time event in approx. 1 hour from now.
			aioseo()->actionScheduler->scheduleSingle( $this->creditFetchAction, 1 * ( HOUR_IN_SECONDS + wp_rand( 0, 30 * MINUTE_IN_SECONDS ) ), [] );

			return;
		}

		$body = wp_remote_retrieve_body( $response );
		$data = json_decode( $body );
		if ( empty( $data->accessToken ) ) {
			aioseo()->cache->update( 'ai-access-token-error', true, 1 * HOUR_IN_SECONDS );

			// Schedule another, one-time event in approx. 1 hour from now.
			aioseo()->actionScheduler->scheduleSingle( $this->creditFetchAction, 1 * ( HOUR_IN_SECONDS + wp_rand( 0, 30 * MINUTE_IN_SECONDS ) ), [] );

			return;
		}

		aioseo()->internalOptions->internal->ai->accessToken        = sanitize_text_field( $data->accessToken );
		aioseo()->internalOptions->internal->ai->isTrialAccessToken = $data->isFree ?? false;

		// Fetch the credit totals.
		$this->updateCredits( true );
	}

	/**
	 * Schedules the credit fetch action.
	 *
	 * @since 4.8.4
	 *
	 * @return void
	 */
	public function scheduleCreditFetchAction() {
		// If not set up, create a scheduled action to refresh the credits each day.
		if ( ! aioseo()->actionScheduler->isScheduled( $this->creditFetchAction ) ) {
			aioseo()->actionScheduler->scheduleRecurrent( $this->creditFetchAction, DAY_IN_SECONDS, DAY_IN_SECONDS, [] );
		}
	}

	/**
	 * Gets the credit data from the server and updates our options.
	 *
	 * @since 4.8.4
	 *
	 * @param  bool $refresh Whether to refresh the credits forcefully.
	 * @return void
	 */
	public function updateCredits( $refresh = false ) {
		if ( aioseo()->cache->get( 'ai-credits-error' ) && ! $refresh ) {
			return;
		}

		if ( ! aioseo()->internalOptions->internal->ai->accessToken ) {
			return;
		}

		$response = aioseo()->helpers->wpRemoteGet( $this->getApiUrl() . 'ai/credits/', [
			'headers' => $this->getRequestHeaders()
		] );

		if ( is_wp_error( $response ) ) {
			aioseo()->cache->update( 'ai-credits-error', true, HOUR_IN_SECONDS );

			// Schedule another, one-time event in approx. 1 hour from now.
			aioseo()->actionScheduler->scheduleSingle( $this->creditFetchAction, 1 * ( HOUR_IN_SECONDS + wp_rand( 0, 30 * MINUTE_IN_SECONDS ) ), [] );

			return;
		}

		$body = wp_remote_retrieve_body( $response );
		$data = json_decode( $body );
		if ( empty( $data->success ) ) {
			aioseo()->cache->update( 'ai-credits-error', true, HOUR_IN_SECONDS );

			// Schedule another, one-time event in approx. 1 hour from now.
			aioseo()->actionScheduler->scheduleSingle( $this->creditFetchAction, 1 * ( HOUR_IN_SECONDS + wp_rand( 0, 30 * MINUTE_IN_SECONDS ) ), [] );

			return;
		}

		$orders = [];
		if ( ! empty( $data->orders ) ) {
			foreach ( $data->orders as $order ) {
				if (
					empty( $order->total ) ||
					! isset( $order->remaining ) ||
					! isset( $order->expires )
				) {
					continue;
				}

				$orders[] = [
					'total'     => intval( $order->total ),
					'remaining' => intval( $order->remaining ),
					'expires'   => intval( $order->expires )
				];
			}
		}

		aioseo()->internalOptions->internal->ai->credits->orders    = $orders;
		aioseo()->internalOptions->internal->ai->credits->total     = isset( $data->total ) ? intval( $data->total ) : 0;
		aioseo()->internalOptions->internal->ai->credits->remaining = isset( $data->remaining ) ? intval( $data->remaining ) : 0;

		if ( ! empty( $data->license ) ) {
			aioseo()->internalOptions->internal->ai->credits->license->total     = intval( $data->license->total );
			aioseo()->internalOptions->internal->ai->credits->license->remaining = intval( $data->license->remaining );
			aioseo()->internalOptions->internal->ai->credits->license->expires   = intval( $data->license->expires );
		} else {
			aioseo()->internalOptions->internal->ai->credits->license->reset();
		}
	}

	/**
	 * Returns the default request headers.
	 *
	 * @since 4.8.4
	 *
	 * @return array The default request headers.
	 */
	protected function getRequestHeaders() {
		$headers = [
			'X-AIOSEO-Ai-Token'  => aioseo()->internalOptions->internal->ai->accessToken,
			'X-AIOSEO-Ai-Domain' => aioseo()->helpers->getSiteDomain()
		];

		return $headers;
	}

	/**
	 * Returns the API URL of the licensing server.
	 *
	 * @since 4.8.4
	 *
	 * @return string The URL.
	 */
	protected function getApiUrl() {
		if ( defined( 'AIOSEO_LICENSING_URL' ) ) {
			return AIOSEO_LICENSING_URL;
		}

		return $this->licensingUrl;
	}
}Common/Api/Ai.php000064400000045460151536241160007564 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

use AIOSEO\Plugin\Common\Models;

/**
 * AI route class for the API.
 *
 * @since 4.8.4
 */
class Ai {
	/**
	 * The AI Generator API URL.
	 *
	 * @since 4.8.4
	 *
	 * @var string
	 */
	private static $aiGeneratorApiUrl = 'https://ai-generator.aioseo.com/v1/';

	/**
	 * Stores the access token.
	 *
	 * @since 4.8.4
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function storeAccessToken( $request ) {
		$body        = $request->get_json_params();
		$accessToken = sanitize_text_field( $body['accessToken'] );
		if ( ! $accessToken ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Missing access token.'
			], 400 );
		}

		aioseo()->internalOptions->internal->ai->accessToken        = $accessToken;
		aioseo()->internalOptions->internal->ai->isTrialAccessToken = false;

		aioseo()->ai->updateCredits( true );

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

	/**
	 * Generates title suggestions based on the provided content and options.
	 *
	 * @since 4.8.4
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function generateTitles( $request ) {
		$body         = $request->get_json_params();
		$postId       = ! empty( $body['postId'] ) ? (int) $body['postId'] : 0;
		$postContent  = ! empty( $body['postContent'] ) ? sanitize_text_field( $body['postContent'] ) : '';
		$focusKeyword = ! empty( $body['focusKeyword'] ) ? sanitize_text_field( $body['focusKeyword'] ) : '';
		$rephrase     = isset( $body['rephrase'] ) ? boolval( $body['rephrase'] ) : false;
		$titles       = ! empty( $body['titles'] ) ? $body['titles'] : [];
		$options      = $body['options'] ?? [];
		if ( ! $postContent || empty( $options ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Missing required parameters.'
			], 400 );
		}

		foreach ( $options as $k => $option ) {
			$options[ $k ] = aioseo()->helpers->sanitizeOption( $option );
		}

		foreach ( $titles as $k => $title ) {
			$titles[ $k ] = sanitize_text_field( $title );
		}

		$response = aioseo()->helpers->wpRemotePost( self::getAiGeneratorApiUrl() . 'meta/title/', [
			'timeout' => 60,
			'headers' => self::getRequestHeaders(),
			'body'    => wp_json_encode( [
				'postContent'  => $postContent,
				'focusKeyword' => $focusKeyword,
				'tone'         => $options['tone'],
				'audience'     => $options['audience'],
				'rephrase'     => $rephrase,
				'titles'       => $titles
			] )
		] );

		$responseCode = wp_remote_retrieve_response_code( $response );
		if ( 200 !== $responseCode ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Failed to generate meta titles.'
			], 400 );
		}

		$responseBody = json_decode( wp_remote_retrieve_body( $response ) );
		$titles       = aioseo()->helpers->sanitizeOption( $responseBody->titles );
		if ( empty( $responseBody->success ) || empty( $titles ) ) {
			if ( 'insufficient_credits' === $responseBody->code ) {
				aioseo()->internalOptions->internal->ai->credits->remaining = $responseBody->remaining ?? 0;
			}

			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Failed to generate meta titles.'
			], 400 );
		}

		self::updateAiOptions( $responseBody );

		// Decode HTML entities again. Vue will escape data if needed.
		foreach ( $titles as $k => $title ) {
			$titles[ $k ] = aioseo()->helpers->decodeHtmlEntities( $title );
		}

		// Get the post and save the data.
		$aioseoPost             = Models\Post::getPost( $postId );
		$aioseoPost->ai         = Models\Post::getDefaultAiOptions( $aioseoPost->ai );
		$aioseoPost->ai->titles = $titles;
		$aioseoPost->save();

		return new \WP_REST_Response( [
			'success'   => true,
			'titles'    => $titles,
			'aiOptions' => aioseo()->internalOptions->internal->ai->all()
		], 200 );
	}

	/**
	 * Generates description suggestions based on the provided content and options.
	 *
	 * @since 4.8.4
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function generateDescriptions( $request ) {
		$body         = $request->get_json_params();
		$postId       = ! empty( $body['postId'] ) ? (int) $body['postId'] : 0;
		$postContent  = ! empty( $body['postContent'] ) ? sanitize_text_field( $body['postContent'] ) : '';
		$focusKeyword = ! empty( $body['focusKeyword'] ) ? sanitize_text_field( $body['focusKeyword'] ) : '';
		$rephrase     = isset( $body['rephrase'] ) ? boolval( $body['rephrase'] ) : false;
		$descriptions = ! empty( $body['descriptions'] ) ? $body['descriptions'] : [];
		$options      = $body['options'] ?? [];
		if ( ! $postContent || empty( $options ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Missing required parameters.'
			], 400 );
		}

		foreach ( $options as $k => $option ) {
			$options[ $k ] = aioseo()->helpers->sanitizeOption( $option );
		}

		foreach ( $descriptions as $k => $description ) {
			$descriptions[ $k ] = sanitize_text_field( $description );
		}

		$response = aioseo()->helpers->wpRemotePost( self::getAiGeneratorApiUrl() . 'meta/description/', [
			'timeout' => 60,
			'headers' => self::getRequestHeaders(),
			'body'    => wp_json_encode( [
				'postContent'  => $postContent,
				'focusKeyword' => $focusKeyword,
				'tone'         => $options['tone'],
				'audience'     => $options['audience'],
				'rephrase'     => $rephrase,
				'descriptions' => $descriptions
			] )
		] );

		$responseCode = wp_remote_retrieve_response_code( $response );
		if ( 200 !== $responseCode ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Failed to generate meta descriptions.'
			], 400 );
		}

		$responseBody = json_decode( wp_remote_retrieve_body( $response ) );
		$descriptions = aioseo()->helpers->sanitizeOption( $responseBody->descriptions );
		if ( empty( $responseBody->success ) || empty( $descriptions ) ) {
			if ( 'insufficient_credits' === $responseBody->code ) {
				aioseo()->internalOptions->internal->ai->credits->remaining = $responseBody->remaining ?? 0;
			}

			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Failed to generate meta descriptions.'
			], 400 );
		}

		self::updateAiOptions( $responseBody );

		// Decode HTML entities again. Vue will escape data if needed.
		foreach ( $descriptions as $k => $description ) {
			$descriptions[ $k ] = aioseo()->helpers->decodeHtmlEntities( $description );
		}

		// Get the post and save the data.
		$aioseoPost                   = Models\Post::getPost( $postId );
		$aioseoPost->ai               = Models\Post::getDefaultAiOptions( $aioseoPost->ai );
		$aioseoPost->ai->descriptions = $descriptions;
		$aioseoPost->save();

		return new \WP_REST_Response( [
			'success'      => true,
			'descriptions' => $descriptions,
			'aiOptions'    => aioseo()->internalOptions->internal->ai->all()
		], 200 );
	}

	/**
	 * Generates social posts based on the provided content and options.
	 *
	 * @since 4.8.4
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function generateSocialPosts( $request ) {
		$body        = $request->get_json_params();
		$postId      = ! empty( $body['postId'] ) ? (int) $body['postId'] : 0;
		$postContent = ! empty( $body['postContent'] ) ? sanitize_text_field( $body['postContent'] ) : '';
		$permalink   = ! empty( $body['permalink'] ) ? esc_url_raw( urldecode( $body['permalink'] ) ) : '';
		$options     = $body['options'] ?? [];
		if ( ! $postContent || ! $permalink || empty( $options['media'] ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Missing required parameters.'
			], 400 );
		}

		foreach ( $options as $k => $option ) {
			$options[ $k ] = aioseo()->helpers->sanitizeOption( $option );
		}

		$response = aioseo()->helpers->wpRemotePost( self::getAiGeneratorApiUrl() . 'social-posts/', [
			'timeout' => 60,
			'headers' => self::getRequestHeaders(),
			'body'    => wp_json_encode( [
				'postContent' => $postContent,
				'url'         => $permalink,
				'tone'        => $options['tone'],
				'audience'    => $options['audience'],
				'media'       => $options['media']
			] )
		] );

		$responseCode = wp_remote_retrieve_response_code( $response );
		if ( 200 !== $responseCode ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Failed to generate social posts.'
			], 400 );
		}

		$responseBody = json_decode( wp_remote_retrieve_body( $response ) );
		if ( empty( $responseBody->success ) || empty( $responseBody->snippets ) ) {
			if ( 'insufficient_credits' === $responseBody->code ) {
				aioseo()->internalOptions->internal->ai->credits->remaining = $responseBody->remaining ?? 0;
			}

			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Failed to generate social posts.'
			], 400 );
		}

		$socialPosts = [];
		foreach ( $responseBody->snippets as $type => $content ) {
			if ( 'email' === $type ) {
				$socialPosts[ $type ] = [
					'subject' => aioseo()->helpers->decodeHtmlEntities( sanitize_text_field( $content->subject ) ),
					'preview' => aioseo()->helpers->decodeHtmlEntities( sanitize_text_field( $content->preview ) ),
					'content' => aioseo()->helpers->decodeHtmlEntities( strip_tags( $content->content, '<a>' ) )
				];

				continue;
			}

			// Strip all tags except <a>.
			$socialPosts[ $type ] = aioseo()->helpers->decodeHtmlEntities( strip_tags( $content, '<a>' ) );
		}

		self::updateAiOptions( $responseBody );

		// Get the post and save the data.
		$aioseoPost     = Models\Post::getPost( $postId );
		$aioseoPost->ai = Models\Post::getDefaultAiOptions( $aioseoPost->ai );

		// Replace the social posts with the new ones, but don't overwrite the existing ones that weren't regenerated.
		foreach ( $socialPosts as $type => $content ) {
			$aioseoPost->ai->socialPosts->{ $type } = $content;
		}

		$aioseoPost->save();

		return new \WP_REST_Response( [
			'success'   => true,
			'snippets'  => $aioseoPost->ai->socialPosts, // Return all the social posts, not just the new ones.
			'aiOptions' => aioseo()->internalOptions->internal->ai->all()
		], 200 );
	}

	/**
	 * Generates FAQs based on the provided content and options.
	 *
	 * @since 4.8.4
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function generateFaqs( $request ) {
		$body        = $request->get_json_params();
		$postId      = ! empty( $body['postId'] ) ? (int) $body['postId'] : 0;
		$postContent = ! empty( $body['postContent'] ) ? $body['postContent'] : '';
		$rephrase    = isset( $body['rephrase'] ) ? boolval( $body['rephrase'] ) : false;
		$faqs        = ! empty( $body['faqs'] ) ? $body['faqs'] : [];
		$options     = $body['options'] ?? [];
		if ( ! $postContent || empty( $options ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Missing required parameters.'
			], 400 );
		}

		foreach ( $options as $k => $option ) {
			$options[ $k ] = aioseo()->helpers->sanitizeOption( $option );
		}

		foreach ( $faqs as $k => $faq ) {
			$faqs[ $k ]['question'] = sanitize_text_field( $faq['question'] );
			$faqs[ $k ]['answer']   = sanitize_text_field( $faq['answer'] );
		}

		$response = aioseo()->helpers->wpRemotePost( self::getAiGeneratorApiUrl() . 'faqs/', [
			'timeout' => 60,
			'headers' => self::getRequestHeaders(),
			'body'    => wp_json_encode( [
				'postContent' => $postContent,
				'tone'        => $options['tone'],
				'audience'    => $options['audience'],
				'rephrase'    => $rephrase,
				'faqs'        => $faqs
			] ),
		] );

		$responseCode = wp_remote_retrieve_response_code( $response );
		if ( 200 !== $responseCode ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Failed to generate FAQs.'
			], 400 );
		}

		$responseBody = json_decode( wp_remote_retrieve_body( $response ) );
		$faqs         = aioseo()->helpers->sanitizeOption( $responseBody->faqs );
		if ( empty( $responseBody->success ) || empty( $responseBody->faqs ) ) {
			if ( 'insufficient_credits' === $responseBody->code ) {
				aioseo()->internalOptions->internal->ai->credits->remaining = $responseBody->remaining ?? 0;
			}

			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Failed to generate FAQs.'
			], 400 );
		}

		self::updateAiOptions( $responseBody );

		// Decode HTML entities again. Vue will escape data if needed.
		foreach ( $faqs as $k => $faq ) {
			$faqs[ $k ]['question'] = aioseo()->helpers->decodeHtmlEntities( $faq['question'] );
			$faqs[ $k ]['answer']   = aioseo()->helpers->decodeHtmlEntities( $faq['answer'] );
		}

		// Get the post and save the data.
		$aioseoPost           = Models\Post::getPost( $postId );
		$aioseoPost->ai       = Models\Post::getDefaultAiOptions( $aioseoPost->ai );
		$aioseoPost->ai->faqs = $faqs;
		$aioseoPost->save();

		return new \WP_REST_Response( [
			'success'   => true,
			'faqs'      => $faqs,
			'aiOptions' => aioseo()->internalOptions->internal->ai->all()
		], 200 );
	}

	/**
	 * Generates key points based on the provided content and options.
	 *
	 * @since 4.8.4
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function generateKeyPoints( $request ) {
		$body        = $request->get_json_params();
		$postId      = ! empty( $body['postId'] ) ? (int) $body['postId'] : 0;
		$postContent = ! empty( $body['postContent'] ) ? $body['postContent'] : '';
		$rephrase    = isset( $body['rephrase'] ) ? boolval( $body['rephrase'] ) : false;
		$keyPoints   = ! empty( $body['keyPoints'] ) ? $body['keyPoints'] : [];
		$options     = $body['options'] ?? [];
		if ( ! $postContent || empty( $options ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Missing required parameters.'
			], 400 );
		}

		foreach ( $options as $k => $option ) {
			$options[ $k ] = aioseo()->helpers->sanitizeOption( $option );
		}

		foreach ( $keyPoints as $k => $keyPoint ) {
			$keyPoints[ $k ]['title']       = sanitize_text_field( $keyPoint['title'] );
			$keyPoints[ $k ]['explanation'] = sanitize_text_field( $keyPoint['explanation'] );
		}

		$response = aioseo()->helpers->wpRemotePost( self::getAiGeneratorApiUrl() . 'key-points/', [
			'timeout' => 60,
			'headers' => self::getRequestHeaders(),
			'body'    => wp_json_encode( [
				'postContent' => $postContent,
				'tone'        => $options['tone'],
				'audience'    => $options['audience'],
				'rephrase'    => $rephrase,
				'keyPoints'   => $keyPoints
			] ),
		] );

		$responseCode = wp_remote_retrieve_response_code( $response );
		if ( 200 !== $responseCode ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Failed to generate key points.'
			], 400 );
		}

		$responseBody = json_decode( wp_remote_retrieve_body( $response ) );
		$keyPoints    = aioseo()->helpers->sanitizeOption( $responseBody->keyPoints );
		if ( empty( $responseBody->success ) || empty( $keyPoints ) ) {
			if ( 'insufficient_credits' === $responseBody->code ) {
				aioseo()->internalOptions->internal->ai->credits->remaining = $responseBody->remaining ?? 0;
			}

			return new \WP_REST_Response( [
				'success' => false,
				'message' => 'Failed to generate key points.'
			], 400 );
		}

		self::updateAiOptions( $responseBody );

		// Decode HTML entities again. Vue will escape data if needed.
		foreach ( $keyPoints as $k => $keyPoint ) {
			$keyPoints[ $k ]['title']       = aioseo()->helpers->decodeHtmlEntities( $keyPoint['title'] );
			$keyPoints[ $k ]['explanation'] = aioseo()->helpers->decodeHtmlEntities( $keyPoint['explanation'] );
		}

		// Get the post and save the data.
		$aioseoPost                = Models\Post::getPost( $postId );
		$aioseoPost->ai            = Models\Post::getDefaultAiOptions( $aioseoPost->ai );
		$aioseoPost->ai->keyPoints = $keyPoints;
		$aioseoPost->save();

		return new \WP_REST_Response( [
			'success'   => true,
			'keyPoints' => $keyPoints,
			'aiOptions' => aioseo()->internalOptions->internal->ai->all()
		], 200 );
	}

	/**
	 * Updates the AI options.
	 *
	 * @since 4.8.4
	 *
	 * @param object $responseBody The response body.
	 */
	private static function updateAiOptions( $responseBody ) {
		aioseo()->internalOptions->internal->ai->credits->total     = (int) $responseBody->total ?? 0;
		aioseo()->internalOptions->internal->ai->credits->remaining = (int) $responseBody->remaining ?? 0;

		// Get existing orders and append the new ones to prevent 'Indirect modification of overloaded prop' PHP warning.
		$existingOrders = aioseo()->internalOptions->internal->ai->credits->orders ?? [];
		$existingOrders = array_merge( $existingOrders, aioseo()->helpers->sanitizeOption( $responseBody->orders ) );

		aioseo()->internalOptions->internal->ai->credits->orders = $existingOrders;

		if ( ! empty( $responseBody->license ) ) {
			aioseo()->internalOptions->internal->ai->credits->license->total     = (int) $responseBody->license->total ?? 0;
			aioseo()->internalOptions->internal->ai->credits->license->remaining = (int) $responseBody->license->remaining ?? 0;
			aioseo()->internalOptions->internal->ai->credits->license->expires   = (int) $responseBody->license->expires ?? 0;
		}
	}

	/**
	 * Returns the default request headers.
	 *
	 * @since 4.8.4
	 *
	 * @return array The default request headers.
	 */
	protected static function getRequestHeaders() {
		$headers = [
			'Content-Type'       => 'application/json',
			'X-AIOSEO-Ai-Token'  => aioseo()->internalOptions->internal->ai->accessToken,
			'X-AIOSEO-Ai-Domain' => aioseo()->helpers->getSiteDomain()
		];

		if ( aioseo()->pro && aioseo()->license->getLicenseKey() ) {
			$headers['X-AIOSEO-License'] = aioseo()->license->getLicenseKey();
		}

		return $headers;
	}

	/**
	 * Returns the AI Generator API URL.
	 *
	 * @since 4.8.4
	 *
	 * @return string The AI Generator API URL.
	 */
	public static function getAiGeneratorApiUrl() {
		return defined( 'AIOSEO_AI_GENERATOR_URL' ) ? AIOSEO_AI_GENERATOR_URL : self::$aiGeneratorApiUrl;
	}

	/**
	 * Deactivates the access token.
	 *
	 * @since 4.8.4
	 *
	 * @param  \WP_REST_Request  $request The REST Request
	 * @return \WP_REST_Response          The response.
	 */
	public static function deactivate( $request ) {
		$body    = $request->get_json_params();
		$network = is_multisite() && ! empty( $body['network'] ) ? (bool) $body['network'] : false;

		$internalOptions = aioseo()->internalOptions;
		if ( $network ) {
			$internalOptions = aioseo()->internalNetworkOptions;
		}

		$internalOptions->internal->ai->reset();

		aioseo()->ai->getAccessToken( true );

		return new \WP_REST_Response( [
			'success' => true,
			'aiData'  => $internalOptions->internal->ai->all()
		], 200 );
	}
}Common/Api/Analyze.php000064400000015146151536241170010635 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 );
	}
}Common/Api/Api.php000064400000052160151536241170007740 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

/**
 * Api class for the admin.
 *
 * @since 4.0.0
 */
class Api {
	/**
	 * The REST API Namespace
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	public $namespace = 'aioseo/v1';

	/**
	 * The routes we use in the rest API.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $routes = [
		// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
		'GET'    => [
			'options'                                     => [ 'callback' => [ 'Settings', 'getOptions' ], 'access' => 'everyone' ],
			'ping'                                        => [ 'callback' => [ 'Ping', 'ping' ], 'access' => 'everyone' ],
			'post'                                        => [ 'callback' => [ 'PostsTerms', 'getPostData' ], 'access' => 'everyone' ],
			'post/(?P<postId>[\d]+)/first-attached-image' => [ 'callback' => [ 'PostsTerms', 'getFirstAttachedImage' ], 'access' => 'aioseo_page_social_settings' ],
			'user/(?P<userId>[\d]+)/image'                => [ 'callback' => [ 'User', 'getUserImage' ], 'access' => 'aioseo_page_social_settings' ],
			'tags'                                        => [ 'callback' => [ 'Tags', 'getTags' ], 'access' => 'everyone' ],
			'search-statistics/url/auth'                  => [ 'callback' => [ 'SearchStatistics', 'getAuthUrl' ], 'access' => [ 'aioseo_search_statistics_settings', 'aioseo_general_settings', 'aioseo_setup_wizard' ] ], // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'search-statistics/url/reauth'                => [ 'callback' => [ 'SearchStatistics', 'getReauthUrl' ], 'access' => [ 'aioseo_search_statistics_settings', 'aioseo_general_settings' ] ],
			'writing-assistant/keyword/(?P<postId>[\d]+)' => [ 'callback' => [ 'WritingAssistant', 'getPostKeyword' ], 'access' => 'aioseo_page_writing_assistant_settings' ],
			'writing-assistant/user-info'                 => [ 'callback' => [ 'WritingAssistant', 'getUserInfo' ], 'access' => 'aioseo_page_writing_assistant_settings' ],
			'writing-assistant/user-options'              => [ 'callback' => [ 'WritingAssistant', 'getUserOptions' ], 'access' => 'aioseo_page_writing_assistant_settings' ],
			'writing-assistant/report-history'            => [ 'callback' => [ 'WritingAssistant', 'getReportHistory' ], 'access' => 'aioseo_page_writing_assistant_settings' ],
			'seo-analysis/homeresults'                    => [ 'callback' => [ 'Analyze', 'getHomeResults' ], 'access' => 'aioseo_seo_analysis_settings' ],
			'seo-analysis/competitors'                    => [ 'callback' => [ 'Analyze', 'getCompetitorsResults' ], 'access' => 'aioseo_seo_analysis_settings' ]
		],
		'POST'   => [
			'ai/auth'                                               => [ 'callback' => [ 'Ai', 'storeAccessToken' ], 'access' => 'aioseo_page_ai_content_settings' ],
			'ai/meta/title'                                         => [ 'callback' => [ 'Ai', 'generateTitles' ], 'access' => 'aioseo_page_ai_content_settings' ],
			'ai/meta/description'                                   => [ 'callback' => [ 'Ai', 'generateDescriptions' ], 'access' => 'aioseo_page_ai_content_settings' ],
			'ai/faqs'                                               => [ 'callback' => [ 'Ai', 'generateFaqs' ], 'access' => 'aioseo_page_ai_content_settings' ],
			'ai/key-points'                                         => [ 'callback' => [ 'Ai', 'generateKeyPoints' ], 'access' => 'aioseo_page_ai_content_settings' ],
			'ai/social-posts'                                       => [ 'callback' => [ 'Ai', 'generateSocialPosts' ], 'access' => 'aioseo_page_ai_content_settings' ],
			'ai/deactivate'                                         => [ 'callback' => [ 'Ai', 'deactivate' ], 'access' => 'aioseo_page_ai_content_settings' ],
			'htaccess'                                              => [ 'callback' => [ 'Tools', 'saveHtaccess' ], 'access' => 'aioseo_tools_settings' ],
			'post'                                                  => [
				'callback' => [ 'PostsTerms', 'updatePosts' ],
				'access'   => [
					'aioseo_page_analysis',
					'aioseo_page_general_settings',
					'aioseo_page_advanced_settings',
					'aioseo_page_schema_settings',
					'aioseo_page_social_settings'
				]
			],
			'post/(?P<postId>[\d]+)/disable-primary-term-education' => [ 'callback' => [ 'PostsTerms', 'disablePrimaryTermEducation' ], 'access' => 'aioseo_page_general_settings' ],
			'post/(?P<postId>[\d]+)/disable-link-format-education'  => [ 'callback' => [ 'PostsTerms', 'disableLinkFormatEducation' ], 'access' => 'aioseo_page_general_settings' ],
			'post/(?P<postId>[\d]+)/update-internal-link-count'     => [ 'callback' => [ 'PostsTerms', 'updateInternalLinkCount' ], 'access' => 'aioseo_page_general_settings' ],
			'post/(?P<postId>[\d]+)/process-content'                => [ 'callback' => [ 'PostsTerms', 'processContent' ], 'access' => 'aioseo_page_general_settings' ],
			'posts-list/load-details-column'                        => [ 'callback' => [ 'PostsTerms', 'loadPostDetailsColumn' ], 'access' => 'aioseo_page_general_settings' ],
			'posts-list/update-details-column'                      => [ 'callback' => [ 'PostsTerms', 'updatePostDetailsColumn' ], 'access' => 'aioseo_page_general_settings' ],
			'terms-list/load-details-column'                        => [ 'callback' => [ 'PostsTerms', 'loadTermDetailsColumn' ], 'access' => 'aioseo_page_general_settings' ],
			'terms-list/update-details-column'                      => [ 'callback' => [ 'PostsTerms', 'updateTermDetailsColumn' ], 'access' => 'aioseo_page_general_settings' ],
			'keyphrases'                                            => [ 'callback' => [ 'PostsTerms', 'updatePostKeyphrases' ], 'access' => 'aioseo_page_analysis' ],
			'analyze'                                               => [ 'callback' => [ 'Analyze', 'analyzeSite' ], 'access' => 'aioseo_seo_analysis_settings' ],
			'analyze-headline'                                      => [ 'callback' => [ 'Analyze', 'analyzeHeadline' ], 'access' => 'everyone' ],
			'analyze-headline/delete'                               => [ 'callback' => [ 'Analyze', 'deleteHeadline' ], 'access' => 'aioseo_seo_analysis_settings' ],
			'analyze/delete-site'                                   => [ 'callback' => [ 'Analyze', 'deleteSite' ], 'access' => 'aioseo_seo_analysis_settings' ],
			'clear-log'                                             => [ 'callback' => [ 'Tools', 'clearLog' ], 'access' => 'aioseo_tools_settings' ],
			'connect'                                               => [ 'callback' => [ 'Connect', 'saveConnectToken' ], 'access' => [ 'aioseo_general_settings', 'aioseo_setup_wizard' ] ],
			'connect-pro'                                           => [ 'callback' => [ 'Connect', 'processConnect' ], 'access' => [ 'aioseo_general_settings', 'aioseo_setup_wizard' ] ],
			'connect-url'                                           => [ 'callback' => [ 'Connect', 'getConnectUrl' ], 'access' => [ 'aioseo_general_settings', 'aioseo_setup_wizard' ] ],
			'backup'                                                => [ 'callback' => [ 'Tools', 'createBackup' ], 'access' => 'aioseo_tools_settings' ],
			'backup/restore'                                        => [ 'callback' => [ 'Tools', 'restoreBackup' ], 'access' => 'aioseo_tools_settings' ],
			'email-debug-info'                                      => [ 'callback' => [ 'Tools', 'emailDebugInfo' ], 'access' => 'aioseo_tools_settings' ],
			'migration/fix-blank-formats'                           => [ 'callback' => [ 'Migration', 'fixBlankFormats' ], 'access' => 'any' ],
			'notification/blog-visibility-reminder'                 => [ 'callback' => [ 'Notifications', 'blogVisibilityReminder' ], 'access' => 'any' ],
			'notification/conflicting-plugins-reminder'             => [ 'callback' => [ 'Notifications', 'conflictingPluginsReminder' ], 'access' => 'any' ],
			'notification/description-format-reminder'              => [ 'callback' => [ 'Notifications', 'descriptionFormatReminder' ], 'access' => 'any' ],
			'notification/email-reports-enable'                     => [ 'callback' => [ 'EmailSummary', 'enableEmailReports' ], 'access' => 'any' ],
			'notification/install-addons-reminder'                  => [ 'callback' => [ 'Notifications', 'installAddonsReminder' ], 'access' => 'any' ],
			'notification/install-aioseo-image-seo-reminder'        => [ 'callback' => [ 'Notifications', 'installImageSeoReminder' ], 'access' => 'any' ],
			'notification/install-aioseo-local-business-reminder'   => [ 'callback' => [ 'Notifications', 'installLocalBusinessReminder' ], 'access' => 'any' ],
			'notification/install-aioseo-news-sitemap-reminder'     => [ 'callback' => [ 'Notifications', 'installNewsSitemapReminder' ], 'access' => 'any' ],
			'notification/install-aioseo-video-sitemap-reminder'    => [ 'callback' => [ 'Notifications', 'installVideoSitemapReminder' ], 'access' => 'any' ],
			'notification/install-mi-reminder'                      => [ 'callback' => [ 'Notifications', 'installMiReminder' ], 'access' => 'any' ],
			'notification/install-om-reminder'                      => [ 'callback' => [ 'Notifications', 'installOmReminder' ], 'access' => 'any' ],
			'notification/v3-migration-custom-field-reminder'       => [ 'callback' => [ 'Notifications', 'migrationCustomFieldReminder' ], 'access' => 'any' ],
			'notification/v3-migration-schema-number-reminder'      => [ 'callback' => [ 'Notifications', 'migrationSchemaNumberReminder' ], 'access' => 'any' ],
			'notifications/dismiss'                                 => [ 'callback' => [ 'Notifications', 'dismissNotifications' ], 'access' => 'any' ],
			'objects'                                               => [ 'callback' => [ 'PostsTerms', 'searchForObjects' ], 'access' => [ 'aioseo_search_appearance_settings', 'aioseo_sitemap_settings' ] ], // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'options'                                               => [ 'callback' => [ 'Settings', 'saveChanges' ], 'access' => 'any' ],
			'plugins/deactivate'                                    => [ 'callback' => [ 'Plugins', 'deactivatePlugins' ], 'access' => 'aioseo_feature_manager_settings' ],
			'plugins/install'                                       => [ 'callback' => [ 'Plugins', 'installPlugins' ], 'access' => [ 'install_plugins', 'aioseo_feature_manager_settings' ] ],
			'plugins/upgrade'                                       => [ 'callback' => [ 'Plugins', 'upgradePlugins' ], 'access' => [ 'update_plugins', 'aioseo_feature_manager_settings' ] ],
			'reset-settings'                                        => [ 'callback' => [ 'Settings', 'resetSettings' ], 'access' => 'aioseo_tools_settings' ],
			'search-statistics/sitemap/delete'                      => [ 'callback' => [ 'SearchStatistics', 'deleteSitemap' ], 'access' => [ 'aioseo_search_statistics_settings', 'aioseo_general_settings' ] ], // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'search-statistics/sitemap/ignore'                      => [ 'callback' => [ 'SearchStatistics', 'ignoreSitemap' ], 'access' => [ 'aioseo_search_statistics_settings', 'aioseo_general_settings' ] ], // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'settings/export'                                       => [ 'callback' => [ 'Settings', 'exportSettings' ], 'access' => 'aioseo_tools_settings' ],
			'settings/export-content'                               => [ 'callback' => [ 'Settings', 'exportContent' ], 'access' => 'aioseo_tools_settings' ],
			'settings/hide-setup-wizard'                            => [ 'callback' => [ 'Settings', 'hideSetupWizard' ], 'access' => 'any' ],
			'settings/hide-upgrade-bar'                             => [ 'callback' => [ 'Settings', 'hideUpgradeBar' ], 'access' => 'any' ],
			'settings/import'                                       => [ 'callback' => [ 'Settings', 'importSettings' ], 'access' => 'aioseo_tools_settings' ],
			'settings/import/(?P<siteId>[\d]+)'                     => [ 'callback' => [ 'Settings', 'importSettings' ], 'access' => 'aioseo_tools_settings' ],
			'settings/import-plugins'                               => [ 'callback' => [ 'Settings', 'importPlugins' ], 'access' => 'aioseo_tools_settings' ],
			'settings/toggle-card'                                  => [ 'callback' => [ 'Settings', 'toggleCard' ], 'access' => 'any' ],
			'settings/toggle-radio'                                 => [ 'callback' => [ 'Settings', 'toggleRadio' ], 'access' => 'any' ],
			'settings/dismiss-alert'                                => [ 'callback' => [ 'Settings', 'dismissAlert' ], 'access' => 'any' ],
			'settings/items-per-page'                               => [ 'callback' => [ 'Settings', 'changeItemsPerPage' ], 'access' => 'any' ],
			'settings/semrush-country'                              => [ 'callback' => [ 'Settings', 'changeSemrushCountry' ], 'access' => 'any' ],
			'settings/do-task'                                      => [ 'callback' => [ 'Settings', 'doTask' ], 'access' => 'aioseo_tools_settings' ],
			'sitemap/deactivate-conflicting-plugins'                => [ 'callback' => [ 'Sitemaps', 'deactivateConflictingPlugins' ], 'access' => 'any' ],
			'sitemap/delete-static-files'                           => [ 'callback' => [ 'Sitemaps', 'deleteStaticFiles' ], 'access' => 'aioseo_sitemap_settings' ],
			'sitemap/validate-html-sitemap-slug'                    => [ 'callback' => [ 'Sitemaps', 'validateHtmlSitemapSlug' ], 'access' => 'aioseo_sitemap_settings' ],
			'tools/delete-robots-txt'                               => [ 'callback' => [ 'Tools', 'deleteRobotsTxt' ], 'access' => 'aioseo_tools_settings' ],
			'tools/import-robots-txt'                               => [ 'callback' => [ 'Tools', 'importRobotsTxt' ], 'access' => 'aioseo_tools_settings' ],
			'wizard'                                                => [ 'callback' => [ 'Wizard', 'saveWizard' ], 'access' => 'aioseo_setup_wizard' ],
			'integration/semrush/authenticate'                      => [
				'callback' => [ 'Semrush', 'semrushAuthenticate', 'AIOSEO\\Plugin\\Common\\Api\\Integrations' ],
				'access'   => 'aioseo_page_analysis'
			],
			'integration/semrush/refresh'                           => [
				'callback' => [ 'Semrush', 'semrushRefresh', 'AIOSEO\\Plugin\\Common\\Api\\Integrations' ],
				'access'   => 'aioseo_page_analysis'
			],
			'integration/semrush/keyphrases'                        => [
				'callback' => [ 'Semrush', 'semrushGetKeyphrases', 'AIOSEO\\Plugin\\Common\\Api\\Integrations' ],
				'access'   => 'aioseo_page_analysis'
			],
			'integration/wpcode/snippets'                           => [
				'callback' => [ 'WpCode', 'getSnippets', 'AIOSEO\\Plugin\\Common\\Api\\Integrations' ],
				'access'   => 'aioseo_tools_settings'
			],
			'crawl-cleanup'                                         => [
				'callback' => [ 'CrawlCleanup', 'fetchLogs', 'AIOSEO\\Plugin\\Common\\QueryArgs' ],
				'access'   => 'aioseo_search_appearance_settings'
			],
			'crawl-cleanup/block'                                   => [
				'callback' => [ 'CrawlCleanup', 'blockArg', 'AIOSEO\\Plugin\\Common\\QueryArgs' ],
				'access'   => 'aioseo_search_appearance_settings'
			],
			'crawl-cleanup/delete-blocked'                          => [
				'callback' => [ 'CrawlCleanup', 'deleteBlocked', 'AIOSEO\\Plugin\\Common\\QueryArgs' ],
				'access'   => 'aioseo_search_appearance_settings'
			],
			'crawl-cleanup/delete-unblocked'                        => [
				'callback' => [ 'CrawlCleanup', 'deleteLog', 'AIOSEO\\Plugin\\Common\\QueryArgs' ],
				'access'   => 'aioseo_search_appearance_settings'
			],
			'email-summary/send'                                    => [
				'callback' => [ 'EmailSummary', 'send' ],
				'access'   => 'aioseo_page_advanced_settings'
			],
			'writing-assistant/process'                             => [
				'callback' => [ 'WritingAssistant', 'processKeyword' ],
				'access'   => 'aioseo_page_writing_assistant_settings'
			],
			'writing-assistant/content-analysis'                    => [
				'callback' => [ 'WritingAssistant', 'getContentAnalysis' ],
				'access'   => 'aioseo_page_writing_assistant_settings'
			],
			'writing-assistant/disconnect'                          => [
				'callback' => [ 'WritingAssistant', 'disconnect' ],
				'access'   => 'aioseo_page_writing_assistant_settings'
			],
			'writing-assistant/user-options'                        => [
				'callback' => [ 'WritingAssistant', 'saveUserOptions' ],
				'access'   => 'aioseo_page_writing_assistant_settings'
			],
			'writing-assistant/set-report-progress'                 => [
				'callback' => [ 'WritingAssistant', 'setReportProgress' ],
				'access'   => 'aioseo_page_writing_assistant_settings'
			]
		],
		'DELETE' => [
			'backup'                 => [ 'callback' => [ 'Tools', 'deleteBackup' ], 'access' => 'aioseo_tools_settings' ],
			'search-statistics/auth' => [ 'callback' => [ 'SearchStatistics', 'deleteAuth' ], 'access' => [ 'aioseo_search_statistics_settings', 'aioseo_general_settings' ] ]
		]
		// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_filter( 'rest_allowed_cors_headers', [ $this, 'allowedHeaders' ] );
		add_action( 'rest_api_init', [ $this, 'registerRoutes' ] );
	}

	/**
	 * Get all the routes to register.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of routes.
	 */
	protected function getRoutes() {
		return $this->routes;
	}

	/**
	 * Registers the API routes.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function registerRoutes() {
		$class = new \ReflectionClass( get_called_class() );
		foreach ( $this->getRoutes() as $method => $data ) {
			foreach ( $data as $route => $options ) {
				register_rest_route(
					$this->namespace,
					$route,
					[
						'methods'             => $method,
						'permission_callback' => empty( $options['permissions'] ) ? [ $this, 'validRequest' ] : [ $this, $options['permissions'] ],
						'callback'            => is_array( $options['callback'] )
							? [
								(
									! empty( $options['callback'][2] )
										? $options['callback'][2] . '\\' . $options['callback'][0]
										: (
											class_exists( $class->getNamespaceName() . '\\' . $options['callback'][0] )
												? $class->getNamespaceName() . '\\' . $options['callback'][0]
												: __NAMESPACE__ . '\\' . $options['callback'][0]
										)
								),
								$options['callback'][1]
							]
							: [ $this, $options['callback'] ]
					]
				);
			}
		}
	}

	/**
	 * Sets headers that are allowed for our API routes.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function allowHeaders() {
		// TODO: Remove this entire function after a while. It's only here to ensure compatibility with people that are still using Image SEO 1.0.3 or lower.
		header( 'Access-Control-Allow-Headers: X-WP-Nonce' );
	}

	/**
	 * Sets headers that are allowed for our API routes.
	 *
	 * @since 4.1.1
	 *
	 * @param  array $allowHeaders The allowed request headers.
	 * @return array $allowHeaders The allowed request headers.
	 */
	public function allowedHeaders( $allowHeaders ) {
		if ( ! array_search( 'X-WP-Nonce', $allowHeaders, true ) ) {
			$allowHeaders[] = 'X-WP-Nonce';
		}

		return $allowHeaders;
	}

	/**
	 * Determine if logged in or has the proper permissions.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request $request The REST Request.
	 * @return bool                      True if validated, false if not.
	 */
	public function validRequest( $request ) {
		return is_user_logged_in() && $this->validateAccess( $request );
	}

	/**
	 * Validates access from the routes array.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request $request The REST Request.
	 * @return bool                      True if validated, false if not.
	 */
	public function validateAccess( $request ) {
		$routeData = $this->getRouteData( $request );
		if ( empty( $routeData ) || empty( $routeData['access'] ) ) {
			return false;
		}

		// Admins always have access.
		if ( aioseo()->access->isAdmin() ) {
			return true;
		}

		switch ( $routeData['access'] ) {
			case 'everyone':
				// Any user is able to access the route.
				return true;
			default:
				return aioseo()->access->hasCapability( $routeData['access'] );
		}
	}

	/**
	 * Returns the data for the route that is being accessed.
	 *
	 * @since 4.1.6
	 *
	 * @param  \WP_REST_Request $request The REST Request.
	 * @return array                     The route data.
	 */
	protected function getRouteData( $request ) {
		// NOTE: Since WordPress uses case-insensitive patterns to match routes,
		// we are forcing everything to lowercase to ensure we have the proper route.
		// This prevents users with lower privileges from accessing routes they shouldn't.
		$route     = aioseo()->helpers->toLowercase( $request->get_route() );
		$route     = untrailingslashit( str_replace( '/' . $this->namespace . '/', '', $route ) );
		$routeData = isset( $this->getRoutes()[ $request->get_method() ][ $route ] ) ? $this->getRoutes()[ $request->get_method() ][ $route ] : [];

		// No direct route name, let's try the regexes.
		if ( empty( $routeData ) ) {
			foreach ( $this->getRoutes()[ $request->get_method() ] as $routeRegex => $routeInfo ) {
				$routeRegex = str_replace( '@', '\@', $routeRegex );
				if ( preg_match( "@{$routeRegex}@", (string) $route ) ) {
					$routeData = $routeInfo;
					break;
				}
			}
		}

		return $routeData;
	}
}Common/Api/Connect.php000064400000004650151536241170010621 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 );
	}
}Common/Api/EmailSummary.php000064400000003027151536241170011632 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 );
	}
}Common/Api/Integrations/Semrush.php000064400000004331151536241170013320 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 );
	}
}Common/Api/Integrations/WpCode.php000064400000001656151536241170013062 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 );
	}
}Common/Api/Migration.php000064400000003575151536241170011166 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 );
	}
}Common/Api/Network.php000064400000002547151536241170010664 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 );
	}
}Common/Api/Notifications.php000064400000010563151536241170012041 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 );
	}
}Common/Api/Ping.php000064400000000631151536241170010120 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 );
	}
}Common/Api/Plugins.php000064400000010534151536241170010647 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( 'activate_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 );
	}
}Common/Api/PostsTerms.php000064400000034521151536241170011353 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 );
	}
}Common/Api/SearchStatistics.php000064400000013703151536241170012507 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 );
	}
}Common/Api/Settings.php000064400000057343151536241170011037 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 = [
			'ai',
			'keywords',
			'keyphrases',
			'page_analysis',
			'primary_term',
			'og_article_tags',
			'schema',
			'options',
			'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'                    => '',
					'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 );
	}
}Common/Api/Sitemaps.php000064400000011453151536241170011014 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( 'activate_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;
	}
}Common/Api/Tags.php000064400000000604151536241170010121 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 );
	}
}Common/Api/Tools.php000064400000015430151536241170010326 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 );
	}
}Common/Api/User.php000064400000001402151536241170010136 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 );
	}
}Common/Api/Wizard.php000064400000043654151536241170010477 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 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 ) {
		$body           = $request->get_json_params();
		$section        = ! empty( $body['section'] ) ? sanitize_text_field( $body['section'] ) : null;
		$wizard         = ! empty( $body['wizard'] ) ? $body['wizard'] : null;
		$network        = ! empty( $body['network'] ) ? $body['network'] : false;
		$options        = aioseo()->options->noConflict();
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();

		aioseo()->internalOptions->internal->wizard = wp_json_encode( $wizard );

		// Process the importers.
		if ( 'importers' === $section && ! empty( $wizard['importers'] ) ) {
			$importers = $wizard['importers'];

			try {
				foreach ( $importers as $plugin ) {
					aioseo()->importExport->startImport( $plugin, [
						'settings',
						'postMeta',
						'termMeta'
					] );
				}
			} catch ( \Exception $e ) {
				// Import failed. Let's create a notification but move on.
				$notification = Models\Notification::getNotificationByName( 'import-failed' );
				if ( ! $notification->exists() ) {
					Models\Notification::addNotification( [
						'slug'              => uniqid(),
						'notification_name' => 'import-failed',
						'title'             => __( 'SEO Plugin Import Failed', 'all-in-one-seo-pack' ),
						'content'           => __( 'Unfortunately, there was an error importing your SEO plugin settings. This could be due to an incompatibility in the version installed. Make sure you are on the latest version of the plugin and try again.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'type'              => 'error',
						'level'             => [ 'all' ],
						'button1_label'     => __( 'Try Again', 'all-in-one-seo-pack' ),
						'button1_action'    => 'http://route#aioseo-tools&aioseo-scroll=aioseo-import-others&aioseo-highlight=aioseo-import-others:import-export',
						'start'             => gmdate( 'Y-m-d H:i:s' )
					] );
				}
			}
		}

		// Save the category section.
		if (
			( 'category' === $section || 'searchAppearance' === $section ) && // We allow the user to update the site title/description in search appearance.
			! empty( $wizard['category'] )
		) {
			$category = $wizard['category'];
			if ( ! empty( $category['category'] ) ) {
				aioseo()->internalOptions->internal->category = $category['category'];
			}

			if ( ! empty( $category['categoryOther'] ) ) {
				aioseo()->internalOptions->internal->categoryOther = $category['categoryOther'];
			}

			// If the home page is a static page, let's find and set that,
			// otherwise set our home page settings.
			$staticHomePage = 'page' === get_option( 'show_on_front' ) ? get_post( get_option( 'page_on_front' ) ) : null;
			if ( ! empty( $staticHomePage ) ) {
				$update = false;
				$page   = Models\Post::getPost( $staticHomePage->ID );
				if ( ! empty( $category['siteTitle'] ) ) {
					$update      = true;
					$page->title = $category['siteTitle'];
				}

				if ( ! empty( $category['metaDescription'] ) ) {
					$update            = true;
					$page->description = $category['metaDescription'];
				}

				if ( $update ) {
					$page->save();
				}
			}

			if ( empty( $staticHomePage ) ) {
				if ( ! empty( $category['siteTitle'] ) ) {
					$options->searchAppearance->global->siteTitle = $category['siteTitle'];
				}

				if ( ! empty( $category['metaDescription'] ) ) {
					$options->searchAppearance->global->metaDescription = $category['metaDescription'];
				}
			}
		}

		// Save the additional information section.
		if ( 'additionalInformation' === $section && ! empty( $wizard['additionalInformation'] ) ) {
			$additionalInformation = $wizard['additionalInformation'];
			if ( ! empty( $additionalInformation['siteRepresents'] ) ) {
				$options->searchAppearance->global->schema->siteRepresents = $additionalInformation['siteRepresents'];
			}

			if ( ! empty( $additionalInformation['person'] ) ) {
				$options->searchAppearance->global->schema->person = $additionalInformation['person'];
			}

			if ( ! empty( $additionalInformation['organizationName'] ) ) {
				$options->searchAppearance->global->schema->organizationName = $additionalInformation['organizationName'];
			}

			if ( ! empty( $additionalInformation['organizationDescription'] ) ) {
				$options->searchAppearance->global->schema->organizationDescription = $additionalInformation['organizationDescription'];
			}

			if ( ! empty( $additionalInformation['phone'] ) ) {
				$options->searchAppearance->global->schema->phone = $additionalInformation['phone'];
			}

			if ( ! empty( $additionalInformation['organizationLogo'] ) ) {
				$options->searchAppearance->global->schema->organizationLogo = $additionalInformation['organizationLogo'];
			}

			if ( ! empty( $additionalInformation['personName'] ) ) {
				$options->searchAppearance->global->schema->personName = $additionalInformation['personName'];
			}

			if ( ! empty( $additionalInformation['personLogo'] ) ) {
				$options->searchAppearance->global->schema->personLogo = $additionalInformation['personLogo'];
			}

			if ( ! empty( $additionalInformation['socialShareImage'] ) ) {
				$options->social->facebook->general->defaultImagePosts = $additionalInformation['socialShareImage'];
				$options->social->twitter->general->defaultImagePosts  = $additionalInformation['socialShareImage'];
			}

			if ( ! empty( $additionalInformation['social'] ) && ! empty( $additionalInformation['social']['profiles'] ) ) {
				$profiles = $additionalInformation['social']['profiles'];
				if ( ! empty( $profiles['sameUsername'] ) ) {
					$sameUsername = $profiles['sameUsername'];
					if ( isset( $sameUsername['enable'] ) ) {
						$options->social->profiles->sameUsername->enable = $sameUsername['enable'];
					}

					if ( ! empty( $sameUsername['username'] ) ) {
						$options->social->profiles->sameUsername->username = $sameUsername['username'];
					}

					if ( ! empty( $sameUsername['included'] ) ) {
						$options->social->profiles->sameUsername->included = $sameUsername['included'];
					}
				}

				if ( ! empty( $profiles['urls'] ) ) {
					$urls = $profiles['urls'];
					if ( ! empty( $urls['facebookPageUrl'] ) ) {
						$options->social->profiles->urls->facebookPageUrl = $urls['facebookPageUrl'];
					}

					if ( ! empty( $urls['twitterUrl'] ) ) {
						$options->social->profiles->urls->twitterUrl = $urls['twitterUrl'];
					}

					if ( ! empty( $urls['instagramUrl'] ) ) {
						$options->social->profiles->urls->instagramUrl = $urls['instagramUrl'];
					}

					if ( ! empty( $urls['tiktokUrl'] ) ) {
						$options->social->profiles->urls->tiktokUrl = $urls['tiktokUrl'];
					}

					if ( ! empty( $urls['pinterestUrl'] ) ) {
						$options->social->profiles->urls->pinterestUrl = $urls['pinterestUrl'];
					}

					if ( ! empty( $urls['youtubeUrl'] ) ) {
						$options->social->profiles->urls->youtubeUrl = $urls['youtubeUrl'];
					}

					if ( ! empty( $urls['linkedinUrl'] ) ) {
						$options->social->profiles->urls->linkedinUrl = $urls['linkedinUrl'];
					}

					if ( ! empty( $urls['tumblrUrl'] ) ) {
						$options->social->profiles->urls->tumblrUrl = $urls['tumblrUrl'];
					}

					if ( ! empty( $urls['yelpPageUrl'] ) ) {
						$options->social->profiles->urls->yelpPageUrl = $urls['yelpPageUrl'];
					}

					if ( ! empty( $urls['soundCloudUrl'] ) ) {
						$options->social->profiles->urls->soundCloudUrl = $urls['soundCloudUrl'];
					}

					if ( ! empty( $urls['wikipediaUrl'] ) ) {
						$options->social->profiles->urls->wikipediaUrl = $urls['wikipediaUrl'];
					}

					if ( ! empty( $urls['myspaceUrl'] ) ) {
						$options->social->profiles->urls->myspaceUrl = $urls['myspaceUrl'];
					}

					if ( ! empty( $urls['googlePlacesUrl'] ) ) {
						$options->social->profiles->urls->googlePlacesUrl = $urls['googlePlacesUrl'];
					}

					if ( ! empty( $urls['wordPressUrl'] ) ) {
						$options->social->profiles->urls->wordPressUrl = $urls['wordPressUrl'];
					}

					if ( ! empty( $urls['blueskyUrl'] ) ) {
						$options->social->profiles->urls->blueskyUrl = $urls['blueskyUrl'];
					}

					if ( ! empty( $urls['threadsUrl'] ) ) {
						$options->social->profiles->urls->threadsUrl = $urls['threadsUrl'];
					}
				}
			}

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

		// Save the features section.
		if ( 'features' === $section && ! empty( $wizard['features'] ) ) {
			self::installPlugins( $wizard['features'], $network );

			if ( in_array( 'email-reports', $wizard['features'], true ) ) {
				$options->advanced->emailSummary->enable = true;
			}
		}

		// Save the search appearance section.
		if ( 'searchAppearance' === $section && ! empty( $wizard['searchAppearance'] ) ) {
			$searchAppearance = $wizard['searchAppearance'];

			if ( isset( $searchAppearance['underConstruction'] ) ) {
				update_option( 'blog_public', ! $searchAppearance['underConstruction'] );
			}

			if (
				! empty( $searchAppearance['postTypes'] ) &&
				! empty( $searchAppearance['postTypes']['postTypes'] )
			) {
				// Robots.
				if ( ! empty( $searchAppearance['postTypes']['postTypes']['all'] ) ) {
					foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
						if ( $dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
							$dynamicOptions->searchAppearance->postTypes->$postType->show                          = true;
							$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = true;
							$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->noindex = false;
						}
					}
				} else {
					foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
						if ( $dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
							if ( in_array( $postType, (array) $searchAppearance['postTypes']['postTypes']['included'], true ) ) {
								$dynamicOptions->searchAppearance->postTypes->$postType->show                          = true;
								$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = true;
								$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->noindex = false;
							} else {
								$dynamicOptions->searchAppearance->postTypes->$postType->show                          = false;
								$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = false;
								$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->noindex = true;
							}
						}
					}
				}

				// Sitemaps.
				if ( isset( $searchAppearance['postTypes']['postTypes']['all'] ) ) {
					$options->sitemap->general->postTypes->all = $searchAppearance['postTypes']['postTypes']['all'];
				}

				if ( isset( $searchAppearance['postTypes']['postTypes']['included'] ) ) {
					$options->sitemap->general->postTypes->included = $searchAppearance['postTypes']['postTypes']['included'];
				}
			}

			if ( isset( $searchAppearance['multipleAuthors'] ) ) {
				$options->searchAppearance->archives->author->show                          = $searchAppearance['multipleAuthors'];
				$options->searchAppearance->archives->author->advanced->robotsMeta->default = $searchAppearance['multipleAuthors'];
				$options->searchAppearance->archives->author->advanced->robotsMeta->noindex = ! $searchAppearance['multipleAuthors'];
			}

			if ( isset( $searchAppearance['redirectAttachmentPages'] ) && $dynamicOptions->searchAppearance->postTypes->has( 'attachment' ) ) {
				$dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = $searchAppearance['redirectAttachmentPages'] ? 'attachment' : 'disabled';
			}

			if ( isset( $searchAppearance['emailReports'] ) ) {
				$options->advanced->emailSummary->enable = $searchAppearance['emailReports'];
			}
		}

		// Save the smart recommendations section.
		if ( 'smartRecommendations' === $section && ! empty( $wizard['smartRecommendations'] ) ) {
			$smartRecommendations = $wizard['smartRecommendations'];
			if ( ! empty( $smartRecommendations['accountInfo'] ) && ! aioseo()->internalOptions->internal->siteAnalysis->connectToken ) {
				$url      = defined( 'AIOSEO_CONNECT_DIRECT_URL' ) ? AIOSEO_CONNECT_DIRECT_URL : 'https://aioseo.com/wp-json/aioseo-lite-connect/v1/connect/';
				$response = wp_remote_post( $url, [
					'timeout'    => 10,
					'headers'    => array_merge( [
						'Content-Type' => 'application/json'
					], aioseo()->helpers->getApiHeaders() ),
					'user-agent' => aioseo()->helpers->getApiUserAgent(),
					'body'       => wp_json_encode( [
						'accountInfo' => $smartRecommendations['accountInfo'],
						'homeurl'     => home_url()
					] )
				] );

				$token = json_decode( wp_remote_retrieve_body( $response ) );
				if ( ! empty( $token->token ) ) {
					aioseo()->internalOptions->internal->siteAnalysis->connectToken = $token->token;
				}
			}
		}

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

	/**
	 * Install all plugins that were selected in the features page of the Setup Wizard.
	 *
	 * @since 4.5.5
	 *
	 * @param  array $features The features that were selected.
	 * @param  bool  $network  Whether to install the plugins on the network.
	 * @return void
	 */
	private static function installPlugins( $features, $network ) {
		$pluginData = aioseo()->helpers->getPluginData();

		if ( in_array( 'analytics', $features, true ) ) {
			self::installMonsterInsights( $network );
		}

		if ( in_array( 'conversion-tools', $features, true ) && ! $pluginData['optinMonster']['activated'] ) {
			self::installOptinMonster( $network );
		}

		if ( in_array( 'broken-link-checker', $features, true ) && ! $pluginData['brokenLinkChecker']['activated'] ) {
			self::installBlc( $network );
		}
	}

	/**
	 * Installs the MonsterInsights plugin.
	 *
	 * @since 4.5.5
	 *
	 * @param  bool $network Whether to install the plugin on the network.
	 * @return void
	 */
	private static function installMonsterInsights( $network ) {
		$pluginData = aioseo()->helpers->getPluginData();

		$args = [
			'id'                => 'miLite',
			'pluginName'        => 'MonsterInsights',
			'pluginLongName'    => 'MonsterInsights Analytics',
			'notification-name' => 'install-mi'
		];

		// If MI Pro is active, bail.
		if ( $pluginData['miPro']['activated'] ) {
			return;
		}

		// If MI Pro is installed but not active, activate MI Pro.
		if ( $pluginData['miPro']['installed'] ) {
			$args['id'] = 'miPro';
		}

		if ( self::installPlugin( $args, $network ) ) {
			delete_transient( '_monsterinsights_activation_redirect' );
		}
	}

	/**
	 * Installs the OptinMonster plugin.
	 *
	 * @since 4.5.5
	 *
	 * @param  bool $network Whether to install the plugin on the network.
	 * @return void
	 */
	private static function installOptinMonster( $network ) {
		$args = [
			'id'                => 'optinMonster',
			'pluginName'        => 'OptinMonster',
			'pluginLongName'    => 'OptinMonster Conversion Tools',
			'notification-name' => 'install-om'
		];

		if ( self::installPlugin( $args, $network ) ) {
			delete_transient( 'optin_monster_api_activation_redirect' );
		}
	}

	/**
	 * Installs the Broken Link Checker plugin.
	 *
	 * @since 4.5.5
	 *
	 * @param  bool $network Whether to install the plugin on the network.
	 * @return void
	 */
	private static function installBlc( $network ) {
		$args = [
			'id'                => 'brokenLinkChecker',
			'pluginName'        => 'Broken Link Checker',
			'notification-name' => 'install-blc'
		];

		if ( self::installPlugin( $args, $network ) && function_exists( 'aioseoBrokenLinkChecker' ) ) {
			aioseoBrokenLinkChecker()->core->cache->delete( 'activation_redirect' );
		}
	}

	/**
	 * Helper method to install plugins through the Setup Wizard.
	 * Creates a notification if the plugin can't be installed.
	 *
	 * @since 4.5.5
	 *
	 * @param  array $args    The plugin arguments.
	 * @param  bool  $network Whether to install the plugin on the network.
	 * @return bool           Whether the plugin was installed.
	 */
	private static function installPlugin( $args, $network = false ) {
		if ( aioseo()->addons->canInstall() ) {
			return aioseo()->addons->installAddon( $args['id'], $network );
		}

		$pluginData = aioseo()->helpers->getPluginData();

		$notification = Models\Notification::getNotificationByName( $args['notification-name'] );
		if ( ! $notification->exists() ) {
			Models\Notification::addNotification( [
				'slug'              => uniqid(),
				'notification_name' => $args['notification-name'],
				'title'             => sprintf(
					// Translators: 1 - A plugin name (e.g. "MonsterInsights", "Broken Link Checker", etc.).
					__( 'Install %1$s', 'all-in-one-seo-pack' ),
					$args['pluginName']
				),
				'content'           => sprintf(
					// Translators: 1 - A plugin name (e.g. "MonsterInsights", "Broken Link Checker", etc.), 2 - The plugin short name ("AIOSEO").
					__( 'You selected to install the free %1$s plugin during the setup of %2$s, but there was an issue during installation. Click below to manually install.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
					AIOSEO_PLUGIN_SHORT_NAME,
					! empty( $args['pluginLongName'] ) ? $args['pluginLongName'] : $args['pluginName']
				),
				'type'              => 'info',
				'level'             => [ 'all' ],
				'button1_label'     => sprintf(
					// Translators: 1 - A plugin name (e.g. "MonsterInsights", "Broken Link Checker", etc.).
					__( 'Install %1$s', 'all-in-one-seo-pack' ),
					$args['pluginName']
				),
				'button1_action'    => $pluginData[ $args['id'] ]['wpLink'],
				'button2_label'     => __( 'Remind Me Later', 'all-in-one-seo-pack' ),
				'button2_action'    => "http://action#notification/{$args['notification-name']}-reminder",
				'start'             => gmdate( 'Y-m-d H:i:s' )
			] );
		}

		return false;
	}
}Common/Api/WritingAssistant.php000064400000021163151536241170012543 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 );
	}
}Common/Breadcrumbs/Block.php000064400000012762151536241170012005 0ustar00<?php
namespace AIOSEO\Plugin\Common\Breadcrumbs;

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

/**
 * Breadcrumb Block.
 *
 * @since 4.1.1
 */
class Block {
	/**
	 * The primary term list.
	 *
	 * @since 4.3.6
	 *
	 * @var array
	 */
	private $primaryTerm = [];

	/**
	 * The breadcrumb settings.
	 *
	 * @since 4.8.3
	 *
	 * @var array
	 */
	private $breadcrumbSettings = [
		'default'            => true,
		'separator'          => '›',
		'showHomeCrumb'      => true,
		'showTaxonomyCrumbs' => true,
		'showParentCrumbs'   => true,
		'parentTemplate'     => 'default',
		'template'           => 'default',
		'taxonomy'           => ''
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.1
	 */
	public function __construct() {
		$this->register();
	}

	/**
	 * Registers the block.
	 *
	 * @since 4.1.1
	 *
	 * @return void
	 */
	public function register() {
		aioseo()->blocks->registerBlock(
			'aioseo/breadcrumbs', [
				'attributes'      => [
					'primaryTerm'        => [
						'type'    => 'string',
						'default' => null
					],
					'breadcrumbSettings' => [
						'type'    => 'object',
						'default' => $this->breadcrumbSettings
					]
				],
				'render_callback' => [ $this, 'render' ]
			]
		);
	}

	/**
	 * Renders the block.
	 *
	 * @since 4.1.1
	 *
	 * @param  array  $blockAttributes The block attributes.
	 * @return string                  The output from the output buffering.
	 */
	public function render( $blockAttributes ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		// phpcs:disable HM.Security.ValidatedSanitizedInput.InputNotSanitized, HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		$postId = ! empty( $_GET['post_id'] ) ? (int) sanitize_text_field( wp_unslash( $_GET['post_id'] ) ) : false;
		// phpcs:enable

		if ( ! empty( $blockAttributes['primaryTerm'] ) ) {
			$this->primaryTerm = json_decode( $blockAttributes['primaryTerm'], true );
		}

		if ( ! empty( $blockAttributes['breadcrumbSettings'] ) ) {
			$this->breadcrumbSettings = $blockAttributes['breadcrumbSettings'];
		}

		aioseo()->breadcrumbs->setOverride( $this->getBlockOverrides() );

		if ( aioseo()->blocks->isRenderingBlockInEditor() && ! empty( $postId ) ) {
			add_filter( 'get_object_terms', [ $this, 'temporarilyAddTerm' ], 10, 3 );
			$breadcrumbs = aioseo()->breadcrumbs->frontend->sideDisplay( false, 'post' === get_post_type( $postId ) ? 'post' : 'single', get_post( $postId ) );
			remove_filter( 'get_object_terms', [ $this, 'temporarilyAddTerm' ], 10 );

			if (
				in_array( 'breadcrumbsEnable', aioseo()->internalOptions->deprecatedOptions, true ) &&
				! aioseo()->options->deprecated->breadcrumbs->enable
			) {
				return '<p>' .
						sprintf(
							// Translators: 1 - The plugin short name ("AIOSEO"), 2 - Opening HTML link tag, 3 - Closing HTML link tag.
							__( 'Breadcrumbs are currently disabled, so this block will be rendered empty. You can enable %1$s\'s breadcrumb functionality under %2$sGeneral Settings > Breadcrumbs%3$s.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
							AIOSEO_PLUGIN_SHORT_NAME,
							'<a href="' . esc_url( admin_url( 'admin.php?page=aioseo-settings#/breadcrumbs' ) ) . '" target="_blank">',
							'</a>'
						) .
						'</p>';
			}

			return $breadcrumbs;
		}

		return aioseo()->breadcrumbs->frontend->display( false );
	}

	/**
	 * Temporarily adds the primary term to the list of terms.
	 *
	 * @since 4.3.6
	 *
	 * @param  array  $terms      The list of terms.
	 * @param  array  $objectIds  The object IDs.
	 * @param  array  $taxonomies The taxonomies.
	 * @return array              The list of terms.
	 */
	public function temporarilyAddTerm( $terms, $objectIds, $taxonomies ) {
		$taxonomy = $taxonomies[0];
		if ( empty( $this->primaryTerm ) || empty( $this->primaryTerm[ $taxonomy ] ) ) {
			return $terms;
		}

		$term = aioseo()->helpers->getTerm( $this->primaryTerm[ $taxonomy ] );
		if ( is_a( $term, 'WP_Term' ) ) {
			$terms[] = $term;
		}

		return $terms;
	}

	/**
	 * Get the block overrides.
	 *
	 * @since 4.8.3
	 *
	 * @return array
	 */
	private function getBlockOverrides() {
		$default = filter_var( $this->breadcrumbSettings['default'], FILTER_VALIDATE_BOOLEAN );
		if ( true === $default || ! aioseo()->pro ) {
			return [];
		}

		return [
			'default'            => false,
			'taxonomy'           => $this->breadcrumbSettings['taxonomy'] ?? '',
			'separator'          => $this->breadcrumbSettings['separator'] ?? '›',
			'showHomeCrumb'      => filter_var( $this->breadcrumbSettings['showHomeCrumb'], FILTER_VALIDATE_BOOLEAN ),
			'showTaxonomyCrumbs' => filter_var( $this->breadcrumbSettings['showTaxonomyCrumbs'], FILTER_VALIDATE_BOOLEAN ),
			'showParentCrumbs'   => filter_var( $this->breadcrumbSettings['showParentCrumbs'], FILTER_VALIDATE_BOOLEAN ),
			'template'           => empty( $this->breadcrumbSettings['template'] ) ? '' : [
				'templateType' => 'custom',
				'template'     => aioseo()->helpers->decodeHtmlEntities( aioseo()->helpers->encodeOutputHtml( $this->breadcrumbSettings['template'] ) )
			],
			'parentTemplate'     => empty( $this->breadcrumbSettings['parentTemplate'] ) ? '' : [
				'templateType' => 'custom',
				'template'     => aioseo()->helpers->decodeHtmlEntities( aioseo()->helpers->encodeOutputHtml( $this->breadcrumbSettings['parentTemplate'] ) )
			],
			'primaryTerm'        => ! empty( $this->primaryTerm[ $this->breadcrumbSettings['taxonomy'] ] ) ? $this->primaryTerm[ $this->breadcrumbSettings['taxonomy'] ] : null
		];
	}
}Common/Breadcrumbs/Breadcrumbs.php000064400000053445151536241170013207 0ustar00<?php
namespace AIOSEO\Plugin\Common\Breadcrumbs {
	// Exit if accessed directly.
	if ( ! defined( 'ABSPATH' ) ) {
		exit;
	}

	/**
	 * Class Breadcrumbs.
	 *
	 * @since 4.1.1
	 */
	class Breadcrumbs {
		/** Instance of the frontend class.
		 *
		 * @since 4.1.1
		 *
		 * @var \AIOSEO\Plugin\Common\Breadcrumbs\Frontend|\AIOSEO\Plugin\Pro\Breadcrumbs\Frontend
		 */
		public $frontend;

		/**
		 * Instance of the shortcode class.
		 *
		 * @since 4.1.1
		 *
		 * @var Shortcode
		 */
		public $shortcode;

		/**
		 * Instance of the block class.
		 *
		 * @since 4.1.1
		 *
		 * @var Block
		 */
		public $block;

		/**
		 * Instance of the tags class.
		 *
		 * @since 4.1.1
		 *
		 * @var Tags
		 */
		public $tags;

		/**
		 * Array of crumbs.
		 *
		 * @since 4.1.1
		 *
		 * @var array An array of crumbs.
		 */
		public $breadcrumbs;

		/**
		 * Array of options to override.
		 *
		 * @since 4.8.3
		 *
		 * @var array An array of options to override.
		 */
		protected $override = [];

		/**
		 * Breadcrumbs constructor.
		 *
		 * @since 4.1.1
		 */
		public function __construct() {
			$this->frontend  = new Frontend();
			$this->shortcode = new Shortcode();
			$this->block     = new Block();

			add_action( 'widgets_init', [ $this, 'registerWidget' ] );

			// Init Tags class later as we need post types registered.
			add_action( 'init', [ $this, 'init' ], 50 );
		}

		public function init() {
			$this->tags = new Tags();
		}

		/**
		 * Helper to add crumbs on the breadcrumb array.
		 *
		 * @since 4.1.1
		 *
		 * @param  array $crumbs A single crumb or an array of crumbs.
		 * @return void
		 */
		public function addCrumbs( $crumbs ) {
			if ( empty( $crumbs ) || ! is_array( $crumbs ) ) {
				return;
			}

			// If it's a single crumb put it inside an array to merge.
			if ( isset( $crumbs['label'] ) ) {
				$crumbs = [ $crumbs ];
			}

			$this->breadcrumbs = array_merge( $this->breadcrumbs, $crumbs );
		}

		/**
		 * Builds a crumb array based on a type and a reference.
		 *
		 * @since 4.1.1
		 *
		 * @param  string $type      The type of breadcrumb ( post, single, page, category, tag, taxonomy, postTypeArchive, date,
		 *                           author, search, notFound, blog ).
		 * @param  mixed  $reference The reference can be an object ( WP_Post | WP_Term | WP_Post_Type | WP_User ), an array, an int or a string.
		 * @param  array  $paged     A reference for a paged crumb.
		 * @return array             An array of breadcrumbs with their label, link, type and reference.
		 */
		public function buildBreadcrumbs( $type, $reference, $paged = [] ) {
			// Clear the breadcrumb array and build a new one.
			$this->breadcrumbs = [];

			// Add breadcrumb prefix.
			$this->addCrumbs( $this->getPrefixCrumb( $type, $reference ) );

			// Set a home page in the beginning of the breadcrumb.
			$this->addCrumbs( $this->maybeGetHomePageCrumb( $type, $reference ) );

			// Woocommerce shop page support.
			$this->addCrumbs( $this->maybeGetWooCommerceShopCrumb() );

			// Blog home.
			if (
				aioseo()->options->breadcrumbs->showBlogHome &&
				in_array( $type, [ 'category', 'tag', 'post', 'author', 'date' ], true )
			) {
				$this->addCrumbs( $this->getBlogCrumb() );
			}

			switch ( $type ) {
				case 'post':
				case 'single':
					$this->addCrumbs( $this->getPostArchiveCrumb( $reference ) );
					$this->addCrumbs( $this->getPostTaxonomyCrumbs( $reference ) );
					$this->addCrumbs( $this->getPostParentCrumbs( $reference ) );
					$this->addCrumbs( $this->getPostCrumb( $reference ) );
					break;
				case 'page':
					$this->addCrumbs( $this->getPostParentCrumbs( $reference, 'page' ) );
					$this->addCrumbs( $this->getPostCrumb( $reference, 'page' ) );
					break;
				case 'category':
				case 'tag':
				case 'taxonomy':
					$this->addCrumbs( $this->getTermTaxonomyParentCrumbs( $reference ) );
					$this->addCrumbs( $this->getTermTaxonomyCrumb( $reference ) );
					break;
				case 'postTypeArchive':
					$this->addCrumbs( $this->getPostTypeArchiveCrumb( $reference ) );
					break;
				case 'date':
					$this->addCrumbs( $this->getDateCrumb( $reference ) );
					break;
				case 'author':
					$this->addCrumbs( $this->getAuthorCrumb( $reference ) );
					break;
				case 'blog':
					$this->addCrumbs( $this->getBlogCrumb() );
					break;
				case 'search':
					$this->addCrumbs( $this->getSearchCrumb( $reference ) );
					break;
				case 'notFound':
					$this->addCrumbs( $this->getNotFoundCrumb() );
					break;
				case 'preview':
					$this->addCrumbs( $this->getPreviewCrumb( $reference ) );
					break;
				case 'wcProduct':
					$this->addCrumbs( $this->getPostTaxonomyCrumbs( $reference ) );
					$this->addCrumbs( $this->getPostParentCrumbs( $reference ) );
					$this->addCrumbs( $this->getPostCrumb( $reference ) );
					break;
				case 'buddypress':
					$this->addCrumbs( aioseo()->standalone->buddyPress->component->getCrumbs() );
					break;
			}

			// Paged crumb.
			if ( ! empty( $paged['paged'] ) ) {
				$this->addCrumbs( $this->getPagedCrumb( $paged ) );
			}

			// Maybe remove the last crumb.
			if ( ! $this->showCurrentItem( $type, $reference ) ) {
				array_pop( $this->breadcrumbs );
			}

			// Remove empty crumbs.
			$this->breadcrumbs = array_filter( $this->breadcrumbs );

			return $this->breadcrumbs;
		}

		/**
		 * Gets the prefix crumb.
		 *
		 * @since 4.1.1
		 *
		 * @param  string $type      The type of breadcrumb.
		 * @param  mixed  $reference The breadcrumb reference.
		 * @return array             A crumb.
		 */
		public function getPrefixCrumb( $type, $reference ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
			if ( 0 === strlen( aioseo()->options->breadcrumbs->breadcrumbPrefix ) ) {
				return [];
			}

			return $this->makeCrumb( aioseo()->options->breadcrumbs->breadcrumbPrefix, '', 'prefix' );
		}

		/**
		 * Gets the 404 crumb.
		 *
		 * @since 4.1.1
		 *
		 * @return array A crumb.
		 */
		public function getNotFoundCrumb() {
			return $this->makeCrumb( aioseo()->options->breadcrumbs->errorFormat404, '', 'notFound' );
		}

		/**
		 * Gets the search crumb.
		 *
		 * @since 4.1.1
		 *
		 * @param  string $searchQuery The search query for reference.
		 * @return array               A crumb.
		 */
		public function getSearchCrumb( $searchQuery ) {
			return $this->makeCrumb( aioseo()->options->breadcrumbs->searchResultFormat, get_search_link( $searchQuery ), 'search', $searchQuery );
		}

		/**
		 * Gets the preview crumb.
		 *
		 * @since 4.1.5
		 *
		 * @param  string $label The preview label.
		 * @return array         A crumb.
		 */
		public function getPreviewCrumb( $label ) {
			return $this->makeCrumb( $label, '', 'preview' );
		}

		/**
		 * Gets the post type archive crumb.
		 *
		 * @since 4.1.1
		 *
		 * @param  \WP_Post_Type $postType The post type object for reference.
		 * @return array                   A crumb.
		 */
		public function getPostTypeArchiveCrumb( $postType ) {
			return $this->makeCrumb( aioseo()->options->breadcrumbs->archiveFormat, get_post_type_archive_link( $postType->name ), 'postTypeArchive', $postType );
		}

		/**
		 * Gets a post crumb.
		 *
		 * @since 4.1.1
		 *
		 * @param  \WP_Post $post    A post object for reference.
		 * @param  string   $type    The breadcrumb type.
		 * @param  string   $subType The breadcrumb subType.
		 * @return array             A crumb.
		 */
		public function getPostCrumb( $post, $type = 'single', $subType = '' ) {
			return $this->makeCrumb( get_the_title( $post ), get_permalink( $post ), $type, $post, $subType );
		}

		/**
		 * Gets the term crumb.
		 *
		 * @since 4.1.1
		 *
		 * @param  \WP_Term $term    The term object for reference.
		 * @param  string   $subType The breadcrumb subType.
		 * @return array             A crumb.
		 */
		public function getTermTaxonomyCrumb( $term, $subType = '' ) {
			return $this->makeCrumb( $term->name, get_term_link( $term ), 'taxonomy', $term, $subType );
		}

		/**
		 * Gets the paged crumb.
		 *
		 * @since 4.1.1
		 *
		 * @param  array $reference The paged array for reference.
		 * @return array             A crumb.
		 */
		public function getPagedCrumb( $reference ) {
			return $this->makeCrumb( sprintf( '%1$s %2$s', __( 'Page', 'all-in-one-seo-pack' ), $reference['paged'] ), $reference['link'], 'paged', $reference );
		}

		/**
		 * Gets the author crumb.
		 *
		 * @since 4.1.1
		 *
		 * @param  \WP_User $wpUser A WP_User object.
		 * @return array            A crumb.
		 */
		public function getAuthorCrumb( $wpUser ) {
			return $this->makeCrumb( $wpUser->display_name, get_author_posts_url( $wpUser->ID ), 'author', $wpUser );
		}

		/**
		 * Gets the date crumb.
		 *
		 * @since 4.1.1
		 *
		 * @param  array $reference An array of year, month and day values.
		 * @return array            A crumb.
		 */
		public function getDateCrumb( $reference ) {
			$dateCrumb = [];
			$addMonth  = false;
			$addYear   = false;
			if ( ! empty( $reference['day'] ) ) {
				$addMonth    = true;
				$addYear     = true;
				$dateCrumb[] = $this->makeCrumb(
					zeroise( (int) $reference['day'], 2 ),
					get_day_link( $reference['year'], $reference['month'], $reference['day'] ),
					'day',
					$reference['day']
				);
			}
			if ( ! empty( $reference['month'] ) || $addMonth ) {
				$addYear     = true;
				$dateCrumb[] = $this->makeCrumb(
					zeroise( (int) $reference['month'], 2 ),
					get_month_link( $reference['year'], $reference['month'] ),
					'month',
					$reference['month']
				);

			}
			if ( ! empty( $reference['year'] ) || $addYear ) {
				$dateCrumb[] = $this->makeCrumb(
					$reference['year'],
					get_year_link( $reference['year'] ),
					'year',
					$reference['year']
				);
			}

			return array_reverse( $dateCrumb );
		}

		/**
		 * Gets an array of crumbs parents for the term.
		 *
		 * @since 4.1.1
		 *
		 * @param  \WP_Term $term A WP_Term object.
		 * @return array          An array of parent crumbs.
		 */
		public function getTermTaxonomyParentCrumbs( $term ) {
			$crumbs = [];

			$termHierarchy = $this->getTermHierarchy( $term->term_id, $term->taxonomy );
			if ( ! empty( $termHierarchy ) ) {
				foreach ( $termHierarchy as $parentTermId ) {
					$parentTerm = aioseo()->helpers->getTerm( $parentTermId, $term->taxonomy );
					$crumbs[]   = $this->getTermTaxonomyCrumb( $parentTerm, 'parent' );
				}
			}

			return $crumbs;
		}

		/**
		 * Helper function to create a standard crumb array.
		 *
		 * @since 4.1.1
		 *
		 * @param  string $label     The crumb label.
		 * @param  string $link      The crumb url.
		 * @param  null   $type      The crumb type.
		 * @param  null   $reference The crumb reference.
		 * @param  null   $subType   The crumb subType ( single/parent ).
		 * @return array             A crumb array.
		 */
		public function makeCrumb( $label, $link = '', $type = null, $reference = null, $subType = null ) {
			return [
				'label'     => $label,
				'link'      => $link,
				'type'      => $type,
				'subType'   => $subType,
				'reference' => $reference
			];
		}

		/**
		 * Gets a post archive crumb if it's post type has archives.
		 *
		 * @since 4.1.1
		 *
		 * @param  int|\WP_Post $post An ID or a WP_Post object.
		 * @return array              A crumb.
		 */
		public function getPostArchiveCrumb( $post ) {
			$postType = get_post_type_object( get_post_type( $post ) );
			if ( ! $postType || ! $postType->has_archive ) {
				return [];
			}

			return $this->makeCrumb( $postType->labels->name, get_post_type_archive_link( $postType->name ), 'postTypeArchive', $postType );
		}

		/**
		 * Gets a post's taxonomy crumbs.
		 *
		 * @since 4.1.1
		 *
		 * @param  int|\WP_Post $post     An ID or a WP_Post object.
		 * @param  null         $taxonomy A taxonomy to use. If none is provided the first one with terms selected will be used.
		 * @return array                  An array of term crumbs.
		 */
		public function getPostTaxonomyCrumbs( $post, $taxonomy = null ) {
			$crumbs = [];

			$overrideTaxonomy = $this->getOverride( 'taxonomy' );
			if ( ! empty( $overrideTaxonomy ) ) {
				$taxonomy = $overrideTaxonomy;
			}

			if ( $taxonomy && ! is_array( $taxonomy ) ) {
				$taxonomy = [ $taxonomy ];
			}

			$termHierarchy = $this->getPostTaxTermHierarchy( $post, $taxonomy );
			if ( ! empty( $termHierarchy['terms'] ) ) {
				foreach ( $termHierarchy['terms'] as $termId ) {
					$term     = aioseo()->helpers->getTerm( $termId, $termHierarchy['taxonomy'] );
					$crumbs[] = $this->makeCrumb( $term->name, get_term_link( $term, $termHierarchy['taxonomy'] ), 'taxonomy', $term, 'parent' );
				}
			}

			return $crumbs;
		}

		/**
		 * Gets the post's parent crumbs.
		 *
		 * @since 4.1.1
		 *
		 * @param  int|\WP_Post $post An ID or a WP_Post object.
		 * @param  string       $type The crumb type.
		 * @return array              An array of the post parent crumbs.
		 */
		public function getPostParentCrumbs( $post, $type = 'single' ) {
			$crumbs = [];
			if ( ! is_post_type_hierarchical( get_post_type( $post ) ) ) {
				return $crumbs;
			}

			$postHierarchy = $this->getPostHierarchy( $post );
			if ( ! empty( $postHierarchy ) ) {
				foreach ( $postHierarchy as $parentID ) {
					// Do not include the Home Page.
					if ( aioseo()->helpers->getHomePageId() === $parentID ) {
						continue;
					}

					$crumbs[] = $this->getPostCrumb( get_post( $parentID ), $type, 'parent' );
				}
			}

			return $crumbs;
		}

		/**
		 * Function to extend on pro for extra functionality.
		 *
		 * @since 4.1.1
		 *
		 * @param  string $type      The type of breadcrumb.
		 * @param  mixed  $reference The breadcrumb reference.
		 * @return bool              Show current item.
		 */
		public function showCurrentItem( $type = null, $reference = null ) {
			return apply_filters( 'aioseo_breadcrumbs_show_current_item', aioseo()->options->breadcrumbs->showCurrentItem, $type, $reference );
		}

		/**
		 * Gets a home page crumb.
		 *
		 * @since 4.1.1
		 *
		 * @param  string     $type      The type of breadcrumb.
		 * @param  mixed      $reference The breadcrumb reference.
		 * @return array|void            The home crumb.
		 */
		public function maybeGetHomePageCrumb( $type = null, $reference = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
			if ( aioseo()->options->breadcrumbs->homepageLink ) {
				return $this->getHomePageCrumb();
			}
		}

		/**
		 * Gets a home page crumb.
		 *
		 * @since 4.1.1
		 *
		 * @return array The home crumb.
		 */
		public function getHomePageCrumb() {
			$homePageId = aioseo()->helpers->getHomePageId();

			$label = '';
			if ( $homePageId ) {
				$label = get_the_title( $homePageId );
			}

			if ( 0 < strlen( aioseo()->options->breadcrumbs->homepageLabel ) ) {
				$label = aioseo()->options->breadcrumbs->homepageLabel;
			}

			// Label fallback.
			if ( empty( $label ) ) {
				$label = __( 'Home', 'all-in-one-seo-pack' );
			}

			return $this->makeCrumb( $label, get_home_url(), 'homePage', aioseo()->helpers->getHomePage() );
		}

		/**
		 * Gets the blog crumb.
		 *
		 * @since 4.1.1
		 *
		 * @return array The blog crumb.
		 */
		public function getBlogCrumb() {
			$crumb = [];

			$blogPage = aioseo()->helpers->getBlogPage();
			if ( null !== $blogPage ) {
				$crumb = $this->makeCrumb( $blogPage->post_title, get_permalink( $blogPage ), 'blog', $blogPage );
			}

			return $crumb;
		}

		/**
		 * Maybe add the shop crumb to products and product categories.
		 *
		 * @since 4.5.5
		 *
		 * @return array The shop crumb.
		 */
		public function maybeGetWooCommerceShopCrumb() {
			$crumb = [];
			if (
				aioseo()->helpers->isWooCommerceShopPage() ||
				aioseo()->helpers->isWooCommerceProductPage() ||
				aioseo()->helpers->isWooCommerceTaxonomyPage()
			) {
				$crumb = $this->getWooCommerceShopCrumb();
			}

			return $crumb;
		}

		/**
		 * Gets the shop crumb.
		 * @see WC_Breadcrumb::prepend_shop_page()
		 *
		 * @since 4.5.5
		 *
		 * @return array The shop crumb.
		 */
		public function getWooCommerceShopCrumb() {
			$crumb = [];

			if (
				! function_exists( 'wc_get_page_id' ) ||
				apply_filters( 'aioseo_woocommerce_breadcrumb_hide_shop', false )
			) {
				return $crumb;
			}

			$shopPageId = wc_get_page_id( 'shop' );
			$shopPage   = get_post( $shopPageId );

			// WC checks if the permalink contains the shop page in the URI, but we prefer to
			// always show the shop page as the first crumb if it exists and it's not the home page.
			if (
				$shopPageId &&
				$shopPage &&
				aioseo()->helpers->getHomePageId() !== $shopPageId
			) {
				$crumb = $this->makeCrumb( get_the_title( $shopPage ), get_permalink( $shopPage ), 'wcShop' );
			}

			return $crumb;
		}

		/**
		 * Gets a post's term hierarchy for a list of taxonomies selecting the one that has a lengthier hierarchy.
		 *
		 * @since 4.1.1
		 *
		 * @param  int|\WP_Post $post                An ID or a WP_Post object.
		 * @param  array        $taxonomies          An array of taxonomy names.
		 * @param  false        $skipUnselectedTerms Allow unselected terms to be filtered out from the crumbs.
		 * @return array                             An array of the taxonomy name + a term hierarchy.
		 */
		public function getPostTaxTermHierarchy( $post, $taxonomies = [], $skipUnselectedTerms = false ) {
			// Get all taxonomies attached to the post.
			if ( empty( $taxonomies ) ) {
				$taxonomies = get_object_taxonomies( get_post_type( $post ), 'objects' );
				$taxonomies = wp_filter_object_list( $taxonomies, [ 'public' => true ], 'and', 'name' );
			}

			foreach ( $taxonomies as $taxonomy ) {
				$primaryTerm         = aioseo()->standalone->primaryTerm->getPrimaryTerm( $post->ID, $taxonomy );
				$overridePrimaryTerm = $this->getOverride( 'primaryTerm' );
				if ( ! empty( $overridePrimaryTerm ) ) {
					$primaryTerm = ! is_a( $overridePrimaryTerm, 'WP_Term' ) ? get_term( $overridePrimaryTerm, $taxonomy ) : $overridePrimaryTerm;
				}

				$terms = wp_get_object_terms( $post->ID, $taxonomy, [
					'orderby' => 'term_id',
					'order'   => 'ASC',
				] );
				// Use the first taxonomy with terms.
				if ( empty( $terms ) || is_wp_error( $terms ) ) {
					continue;
				}

				// Determines the lengthier term hierarchy.
				$termHierarchy = [];
				foreach ( $terms as $term ) {
					// Gets our filtered ancestors.
					$ancestors = $this->getFilteredTermHierarchy( $term->term_id, $term->taxonomy, $skipUnselectedTerms ? $terms : [] );

					// Merge the current term to be used in the breadcrumbs.
					$ancestors = array_merge( $ancestors, [ $term->term_id ] );

					// If the current term is the primary term, use it.
					if ( is_a( $primaryTerm, 'WP_Term' ) && $primaryTerm->term_id === $term->term_id ) {
						$termHierarchy = $ancestors;
						break;
					}

					$termHierarchy = ( count( $termHierarchy ) < count( $ancestors ) ) ? $ancestors : $termHierarchy;
				}

				// Return a top to bottom hierarchy.
				return [
					'taxonomy' => $taxonomy,
					'terms'    => $termHierarchy
				];
			}

			return [];
		}

		/**
		 * Filters a term's parent hierarchy against other terms.
		 *
		 * @since 4.1.1
		 *
		 * @param  int    $termId               A term id.
		 * @param  string $taxonomy             The taxonomy name.
		 * @param  array  $termsToFilterAgainst Terms to filter out of the hierarchy.
		 * @return array                        The term's parent hierarchy.
		 */
		public function getFilteredTermHierarchy( $termId, $taxonomy, $termsToFilterAgainst = [] ) {
			$ancestors = $this->getTermHierarchy( $termId, $taxonomy );

			// Keep only selected terms in the hierarchy.
			if ( ! empty( $termsToFilterAgainst ) ) {
				// If it's a WP_Term array make it a term_id array.
				if ( is_a( current( $termsToFilterAgainst ), 'WP_Term' ) ) {
					$termsToFilterAgainst = wp_list_pluck( $termsToFilterAgainst, 'term_id' );
				}

				$ancestors = array_intersect( $ancestors, $termsToFilterAgainst );
			}

			return $ancestors;
		}

		/**
		 * Gets a term's parent hierarchy.
		 *
		 * @since 4.1.1
		 *
		 * @param  int    $termId   A term id.
		 * @param  string $taxonomy A taxonomy name.
		 * @return array            The term parent hierarchy.
		 */
		public function getTermHierarchy( $termId, $taxonomy ) {
			// Return a top to bottom hierarchy.
			return array_reverse( get_ancestors( $termId, $taxonomy, 'taxonomy' ) );
		}

		/**
		 * Gets a post's parent hierarchy.
		 *
		 * @since 4.1.1
		 *
		 * @param  int|\WP_Post $post An ID or a WP_Post object.
		 * @return array              The post parent hierarchy.
		 */
		public function getPostHierarchy( $post ) {
			$postId = ! empty( $post->ID ) ? $post->ID : $post;

			// Return a top to bottom hierarchy.
			return array_reverse( get_ancestors( $postId, '', 'post_type' ) );
		}

		/**
		 * Register our breadcrumb widget.
		 *
		 * @since 4.1.1
		 *
		 * @return void
		 */
		public function registerWidget() {
			if ( aioseo()->helpers->canRegisterLegacyWidget( 'aioseo-breadcrumb-widget' ) ) {
				register_widget( 'AIOSEO\Plugin\Common\Breadcrumbs\Widget' );
			}
		}

		/**
		 * Setter for the override property.
		 *
		 * @since 4.8.3
		 *
		 * @param  array $toOverride Array containing data to override.
		 * @return void
		 */
		public function setOverride( $toOverride = [] ) {
			$this->override = $toOverride;
		}

		/**
		 * Getter for the override property.
		 *
		 * @since 4.8.3
		 *
		 * @param  string $optionName Optional. The specific option name to retrieve.
		 * @return array              Array containing data to override.
		 */
		public function getOverride( $optionName = null ) {
			if ( empty( $this->override ) ) {
				return $optionName ? null : [];
			}

			$value = $this->override[ $optionName ] ?? null;

			return $optionName ? $value : $this->override;
		}
	}
}

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

	if ( ! function_exists( 'aioseo_breadcrumbs' ) ) {
		/**
		 * Global function for breadcrumbs output.
		 *
		 * @since 4.1.1
		 *
		 * @param  boolean     $echo Echo or return the output.
		 * @return string|void       The output.
		 */
		function aioseo_breadcrumbs( $echo = true ) {
			return aioseo()->breadcrumbs->frontend->display( $echo );
		}
	}
}Common/Breadcrumbs/Frontend.php000064400000021055151536241170012525 0ustar00<?php
namespace AIOSEO\Plugin\Common\Breadcrumbs;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Class Frontend.
 *
 * @since 4.1.1
 */
class Frontend {
	/**
	 * A local 'cached' crumb array.
	 *
	 * @since 4.1.1
	 *
	 * @var array
	 */
	public $breadcrumbs = [];

	/**
	 * Gets the current page's breadcrumbs.
	 *
	 * @since 4.1.1
	 *
	 * @return array
	 */
	public function getBreadcrumbs() {
		if ( ! empty( $this->breadcrumbs ) ) {
			return apply_filters( 'aioseo_breadcrumbs_trail', $this->breadcrumbs );
		}

		$reference = get_queried_object();
		$type      = '';
		if ( BuddyPressIntegration::isComponentPage() ) {
			$type = 'buddypress';
		}

		if ( ! $type ) {
			// These types need the queried object for reference.
			if ( is_object( $reference ) ) {
				if ( is_single() ) {
					$type = 'single';
				}

				if ( is_singular( 'post' ) ) {
					$type = 'post';
				}

				if ( is_page() && ! is_front_page() ) {
					$type = 'page';
				}

				if ( is_category() || is_tag() ) {
					$type = 'category';
				}

				if ( is_tax() ) {
					$type = 'taxonomy';
				}

				if ( is_post_type_archive() ) {
					$type = 'postTypeArchive';
				}

				if ( is_author() ) {
					$type = 'author';
				}

				if ( is_home() ) {
					$type = 'blog';
				}

				// Support WC shop page.
				if ( aioseo()->helpers->isWooCommerceShopPage() ) {
					$type = 'wcShop';
				}

				// Support WC products.
				if ( aioseo()->helpers->isWooCommerceProductPage() ) {
					$type = 'wcProduct';
				}
			}

			if ( is_date() ) {
				$type      = 'date';
				$reference = [
					'year'  => get_query_var( 'year' ),
					'month' => get_query_var( 'monthnum' ),
					'day'   => get_query_var( 'day' )
				];
			}

			if ( is_search() ) {
				$type      = 'search';
				$reference = htmlspecialchars( sanitize_text_field( get_search_query() ) );
			}

			if ( is_404() ) {
				$type = 'notFound';
			}
		}

		$paged = false;
		if ( is_paged() || ( is_singular() && 1 < get_query_var( 'page' ) ) ) {
			global $wp;
			$paged = [
				'paged' => get_query_var( 'paged' ) ? get_query_var( 'paged' ) : get_query_var( 'page' ),
				'link'  => home_url( $wp->request )
			];
		}

		return apply_filters( 'aioseo_breadcrumbs_trail', aioseo()->breadcrumbs->buildBreadcrumbs( $type, $reference, $paged ) );
	}

	/**
	 * Helper function to display breadcrumbs for a specific page.
	 *
	 * @since 4.1.1
	 *
	 * @param  bool        $echo      Print out the breadcrumb.
	 * @param  string      $type      The type for the breadcrumb.
	 * @param  string      $reference A reference to be used for rendering the breadcrumb.
	 * @return string|void            A html breadcrumb.
	 */
	public function sideDisplay( $echo = true, $type = '', $reference = '' ) {
		// Save previously built breadcrumbs.
		$previousCrumbs = $this->breadcrumbs;

		// Build and run the sideDisplay.
		$this->breadcrumbs = aioseo()->breadcrumbs->buildBreadcrumbs( $type, $reference );
		$sideDisplay       = $this->display( $echo );

		// Restore previously built breadcrumbs.
		$this->breadcrumbs = $previousCrumbs;

		return $sideDisplay;
	}

	/**
	 * Display a generic breadcrumb preview.
	 *
	 * @since 4.1.5
	 *
	 * @param  bool        $echo  Print out the breadcrumb.
	 * @param  string      $label The preview crumb label.
	 * @return string|void        A html breadcrumb.
	 */
	public function preview( $echo = true, $label = '' ) {
		// Translators: "Crumb" refers to a part of the breadcrumb trail.
		$label = empty( $label ) ? __( 'Sample Crumb', 'all-in-one-seo-pack' ) : $label;

		return $this->sideDisplay( $echo, 'preview', $label );
	}

	/**
	 * Display the breadcrumb in the frontend.
	 *
	 * @since 4.1.1
	 *
	 * @param  bool        $echo Print out the breadcrumb.
	 * @return string|void       A html breadcrumb.
	 */
	public function display( $echo = true ) {
		if (
			in_array( 'breadcrumbsEnable', aioseo()->internalOptions->deprecatedOptions, true ) &&
			! aioseo()->options->deprecated->breadcrumbs->enable
		) {
			return;
		}

		if ( ! apply_filters( 'aioseo_breadcrumbs_output', true ) ) {
			return;
		}

		// We can only run after this action because we need all post types loaded.
		if ( ! did_action( 'init' ) ) {
			return;
		}

		$breadcrumbs = $this->getBreadcrumbs();
		if ( empty( $breadcrumbs ) ) {
			return;
		}

		$breadcrumbsCount = count( $breadcrumbs );

		$display = '<div class="aioseo-breadcrumbs">';
		foreach ( $breadcrumbs as $breadcrumb ) {
			--$breadcrumbsCount;

			$breadcrumbDisplay = $this->breadcrumbToDisplay( $breadcrumb );

			// Strip link from Last crumb.
			if (
				0 === $breadcrumbsCount &&
				aioseo()->breadcrumbs->showCurrentItem() &&
				! $this->linkCurrentItem() &&
				'default' === $breadcrumbDisplay['templateType']
			) {
				$breadcrumbDisplay['template'] = $this->stripLink( $breadcrumbDisplay['template'] );
			}

			$display .= $breadcrumbDisplay['template'];

			if ( 0 < $breadcrumbsCount ) {
				$display .= $this->getSeparator();
			}
		}
		$display .= '</div>';

		// Final security cleaning.
		$display = wp_kses_post( $display );

		if ( $echo ) {
			echo $display; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}

		return $display;
	}

	/**
	 * Turns a crumb array into a rendered html crumb.
	 *
	 * @since 4.1.1
	 *
	 * @param  array       $item The crumb array.
	 * @return string|void       The crumb html.
	 */
	protected function breadcrumbToDisplay( $item ) {
		$templateItem = $this->getCrumbTemplate( $item );
		if ( empty( $templateItem['template'] ) ) {
			return;
		}

		// Do tags.
		$templateItem['template'] = aioseo()->breadcrumbs->tags->replaceTags( $templateItem['template'], $item );
		$templateItem['template'] = preg_replace_callback(
			'/>(?![^<]*>)(?![^>]*")([^<]*?)>/',
			function ( $matches ) {
				return '>' . $matches[1] . '>';
			},
			htmlentities( $templateItem['template'] )
		);

		// Restore html.
		$templateItem['template'] = aioseo()->helpers->decodeHtmlEntities( $templateItem['template'] );

		// Remove html link if it comes back from the template but we passed no links to it.
		if ( empty( $item['link'] ) ) {
			$templateItem['template'] = $this->stripLink( $templateItem['template'] );
		}

		// Allow shortcodes to run in the final html.
		$templateItem['template'] = do_shortcode( $templateItem['template'] );

		return $templateItem;
	}

	/**
	 * Helper function to get a crumb's template.
	 *
	 * @since 4.1.1
	 *
	 * @param  array $crumb The crumb array.
	 * @return string       The html template.
	 */
	protected function getTemplate( $crumb ) {
		return $this->getDefaultTemplate( $crumb );
	}

	/**
	 * Helper function to get a crumb's template.
	 *
	 * @since 4.1.1
	 *
	 * @param  array $crumb The crumb array.
	 * @return array        The template type and html.
	 */
	protected function getCrumbTemplate( $crumb ) {
		return [
			'templateType' => 'default',
			'template'     => $this->getTemplate( $crumb )
		];
	}

	/**
	 * Default html template.
	 *
	 * @since 4.1.1
	 *
	 * @param  string $type      The crumb's type.
	 * @param  mixed  $reference The crumb's reference.
	 * @return string            The default crumb template.
	 */
	public function getDefaultTemplate( $type = '', $reference = '' ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return <<<TEMPLATE
<span class="aioseo-breadcrumb">
	<a href="#breadcrumb_link" title="#breadcrumb_label">#breadcrumb_label</a>
</span>
TEMPLATE;
	}

	/**
	 * Helper function to strip a html link from the crumb.
	 *
	 * @since 4.1.1
	 *
	 * @param  string $html The crumb's html.
	 * @return string       A crumb html without links.
	 */
	public function stripLink( $html ) {
		return preg_replace( '/<a\s.*?>|<\/a>/is', '', (string) $html );
	}

	/**
	 * Get the breadcrumb configured separator.
	 *
	 * @since 4.1.1
	 *
	 * @return string The separator html.
	 */
	public function getSeparator() {
		$separator = aioseo()->options->breadcrumbs->separator;

		$separatorToOverride = aioseo()->breadcrumbs->getOverride( 'separator' );
		if ( ! empty( $separatorToOverride ) ) {
			$separator = $separatorToOverride;
		}

		$separator = apply_filters( 'aioseo_breadcrumbs_separator_symbol', $separator );

		return apply_filters( 'aioseo_breadcrumbs_separator', '<span class="aioseo-breadcrumb-separator">' . esc_html( $separator ) . '</span>' );
	}

	/**
	 * Function to filter the linkCurrentItem option.
	 *
	 * @since 4.1.3
	 *
	 * @return bool Link current item.
	 */
	public function linkCurrentItem() {
		return apply_filters( 'aioseo_breadcrumbs_link_current_item', aioseo()->options->breadcrumbs->linkCurrentItem );
	}
}Common/Breadcrumbs/Shortcode.php000064400000001037151536241170012676 0ustar00<?php
namespace AIOSEO\Plugin\Common\Breadcrumbs;

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

/**
 * Class Shortcode.
 *
 * @since 4.1.1
 */
class Shortcode {
	/**
	 * Shortcode constructor.
	 *
	 * @since 4.1.1
	 */
	public function __construct() {
		add_shortcode( 'aioseo_breadcrumbs', [ $this, 'display' ] );
	}

	/**
	 * Shortcode callback.
	 *
	 * @since 4.1.1
	 *
	 * @return string|void The breadcrumb html.
	 */
	public function display() {
		return aioseo()->breadcrumbs->frontend->display( false );
	}
}Common/Breadcrumbs/Tags.php000064400000026211151536241170011643 0ustar00<?php
namespace AIOSEO\Plugin\Common\Breadcrumbs;

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

/**
 * Class to replace tag values with their data counterparts.
 *
 * @since 4.1.1
 */
class Tags {
	/**
	 * Tags constructor.
	 *
	 * @since 4.1.1
	 */
	public function __construct() {
		aioseo()->tags->addContext( $this->getContexts() );
		aioseo()->tags->addTags( $this->getTags() );
	}

	/**
	 * Replace the tags in the string provided.
	 *
	 * @since 4.1.1
	 *
	 * @param  string  $string           The string with tags.
	 * @param  array   $item             The breadcrumb item.
	 * @param  boolean $stripPunctuation Whether we should strip punctuation after the tags have been converted.
	 * @return string                    The string with tags replaced.
	 */
	public function replaceTags( $string, $item, $stripPunctuation = false ) {
		if ( ! $string || ! preg_match( '/#/', (string) $string ) ) {
			return $string;
		}

		// Replace separator tag so we don't strip it as punctuation.
		$separatorTag = aioseo()->tags->denotationChar . 'separator_sa';
		$string       = preg_replace( "/$separatorTag(?![a-zA-Z0-9_])/im", '>thisisjustarandomplaceholder<', (string) $string );

		// Replace custom breadcrumb tags.
		foreach ( $this->getTags() as $tag ) {
			$tagId   = aioseo()->tags->denotationChar . $tag['id'];
			$pattern = "/$tagId(?![a-zA-Z0-9_])/im";
			if ( preg_match( $pattern, (string) $string ) ) {
				$tagValue = str_replace( '$', '\$', (string) $this->getTagValue( $tag, $item ) );
				$string   = preg_replace( $pattern, $tagValue, (string) $string );
			}
		}

		if ( $stripPunctuation ) {
			$string = aioseo()->helpers->stripPunctuation( $string );
		}

		// Remove any remaining tags from the title attribute.
		$string = preg_replace_callback( '/title="([^"]*)"/i', function ( $matches ) {
			$sanitizedTitle = wp_strip_all_tags( html_entity_decode( $matches[1] ) );

			return 'title="' . esc_attr( $sanitizedTitle ) . '"';
		}, html_entity_decode( $string ) );

		return preg_replace(
			'/>thisisjustarandomplaceholder<(?![a-zA-Z0-9_])/im',
			aioseo()->helpers->decodeHtmlEntities( aioseo()->options->searchAppearance->global->separator ),
			(string) $string
		);
	}

	/**
	 * Get the value of the tag to replace.
	 *
	 * @since 4.1.1
	 *
	 * @param  string $tag  The tag to look for.
	 * @param  int    $item The crumb array.
	 * @return string       The value of the tag.
	 */
	public function getTagValue( $tag, $item ) {
		$product = false;
		if ( 0 === stripos( $tag['id'], 'breadcrumb_wc_product_' ) ) {
			$product = wc_get_product( $item['reference'] );
			if ( ! $product ) {
				return;
			}
		}

		switch ( $tag['id'] ) {
			case 'breadcrumb_link':
				return $item['link'];
			case 'breadcrumb_separator':
				return aioseo()->breadcrumbs->frontend->getSeparator();
			case 'breadcrumb_wc_product_price':
				return $product ? wc_price( $product->get_price() ) : '';
			case 'breadcrumb_wc_product_sku':
				return $product ? $product->get_sku() : '';
			case 'breadcrumb_wc_product_brand':
				return $product ? aioseo()->helpers->getWooCommerceBrand( $product->get_id() ) : '';
			case 'breadcrumb_author_first_name':
				return $item['reference']->first_name;
			case 'breadcrumb_author_last_name':
				return $item['reference']->last_name;
			case 'breadcrumb_archive_post_type_name':
				return $item['reference']->label;
			case 'breadcrumb_search_string':
				return $item['reference'];
			case 'breadcrumb_format_page_number':
				return $item['reference']['paged'];
			default:
				return $item['label'];
		}
	}

	/**
	 * Gets our breadcrumb custom tags.
	 *
	 * @since 4.1.1
	 *
	 * @return array An array of tags.
	 */
	public function getTags() {
		$tags = [
			[
				'id'          => 'breadcrumb_link',
				'name'        => __( 'Permalink', 'all-in-one-seo-pack' ),
				'description' => __( 'The permalink.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_label',
				'name'        => __( 'Label', 'all-in-one-seo-pack' ),
				'description' => __( 'The label.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_post_title',
				// Translators: 1 - The type of page (Post, Page, Category, Tag, etc.).
				'name'        => sprintf( __( '%1$s Title', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'The original title of the current post.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_taxonomy_title',
				// Translators: 1 - The type of page (Post, Page, Category, Tag, etc.).
				'name'        => sprintf( __( '%1$s Title', 'all-in-one-seo-pack' ), 'Category' ),
				// Translators: 1 - The name of a taxonomy.
				'description' => sprintf( __( 'The %1$s title.', 'all-in-one-seo-pack' ), 'Category' )
			],
			[
				'id'          => 'breadcrumb_separator',
				'name'        => __( 'Separator', 'all-in-one-seo-pack' ),
				'description' => __( 'The crumb separator.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_blog_page_title',
				'name'        => __( 'Blog Page Title', 'all-in-one-seo-pack' ),
				'description' => __( 'The blog page title.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_author_display_name',
				'name'        => __( 'Author Display Name', 'all-in-one-seo-pack' ),
				'description' => __( 'The author\'s display name.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_author_first_name',
				'name'        => __( 'Author First Name', 'all-in-one-seo-pack' ),
				'description' => __( 'The author\'s first name.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_author_last_name',
				'name'        => __( 'Author Last Name', 'all-in-one-seo-pack' ),
				'description' => __( 'The author\'s last name.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_search_result_format',
				'name'        => __( 'Search result format', 'all-in-one-seo-pack' ),
				'description' => __( 'The search result format.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_404_error_format',
				'name'        => __( '404 Error Format', 'all-in-one-seo-pack' ),
				'description' => __( 'The 404 error format.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_date_archive_year',
				'name'        => __( 'Year', 'all-in-one-seo-pack' ),
				'description' => __( 'The year.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_date_archive_month',
				'name'        => __( 'Month', 'all-in-one-seo-pack' ),
				'description' => __( 'The month.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_date_archive_day',
				'name'        => __( 'Day', 'all-in-one-seo-pack' ),
				'description' => __( 'The day.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_search_string',
				'name'        => __( 'Search String', 'all-in-one-seo-pack' ),
				'description' => __( 'The search string.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_format_page_number',
				'name'        => __( 'Page Number', 'all-in-one-seo-pack' ),
				'description' => __( 'The page number.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_archive_post_type_format',
				'name'        => __( 'Archive format', 'all-in-one-seo-pack' ),
				'description' => __( 'The archive format.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_archive_post_type_name',
				'name'        => __( 'Post Type Name', 'all-in-one-seo-pack' ),
				'description' => __( 'The archive post type name.', 'all-in-one-seo-pack' )
			]
		];

		$postTypes = aioseo()->helpers->getPublicPostTypes();
		foreach ( $postTypes as $postType ) {
			if ( 'product' === $postType['name'] && aioseo()->helpers->isWoocommerceActive() ) {
				$tags[] = [
					'id'          => 'breadcrumb_wc_product_price',
					// Translators: 1 - The name of a post type.
					'name'        => sprintf( __( '%1$s Price', 'all-in-one-seo-pack' ), $postType['singular'] ),
					// Translators: 1 - The name of a post type.
					'description' => sprintf( __( 'The %1$s price.', 'all-in-one-seo-pack' ), $postType['singular'] )
				];
				$tags[] = [
					'id'          => 'breadcrumb_wc_product_sku',
					// Translators: 1 - The name of a post type.
					'name'        => sprintf( __( '%1$s SKU', 'all-in-one-seo-pack' ), $postType['singular'] ),
					// Translators: 1 - The name of a post type.
					'description' => sprintf( __( 'The %1$s SKU.', 'all-in-one-seo-pack' ), $postType['singular'] )
				];
				$tags[] = [
					'id'          => 'breadcrumb_wc_product_brand',
					// Translators: 1 - The name of a post type.
					'name'        => sprintf( __( '%1$s Brand', 'all-in-one-seo-pack' ), $postType['singular'] ),
					// Translators: 1 - The name of a post type.
					'description' => sprintf( __( 'The %1$s brand.', 'all-in-one-seo-pack' ), $postType['singular'] )
				];
			}
		}

		return $tags;
	}

	/**
	 * Gets our breadcrumb contexts.
	 *
	 * @since 4.1.1
	 *
	 * @return array An array of contexts.
	 */
	public function getContexts() {
		$contexts = [];

		$baseTags = [ 'breadcrumb_link', 'breadcrumb_separator' ];

		$postTypes = aioseo()->helpers->getPublicPostTypes();
		foreach ( $postTypes as $postType ) {
			$contexts[ 'breadcrumbs-post-type-' . $postType['name'] ] = array_merge( $baseTags, [ 'breadcrumb_post_title' ] );

			if ( 'product' === $postType['name'] && aioseo()->helpers->isWoocommerceActive() ) {
				$contexts[ 'breadcrumbs-post-type-' . $postType['name'] ] = array_merge( $contexts[ 'breadcrumbs-post-type-' . $postType['name'] ], [
					'breadcrumb_wc_product_price',
					'breadcrumb_wc_product_sku',
					'breadcrumb_wc_product_brand'
				] );
			}
		}

		$taxonomies = aioseo()->helpers->getPublicTaxonomies();
		foreach ( $taxonomies as $taxonomy ) {
			$contexts[ 'breadcrumbs-taxonomy-' . $taxonomy['name'] ] = array_merge( $baseTags, [ 'breadcrumb_taxonomy_title' ] );
		}

		$archives = aioseo()->helpers->getPublicPostTypes( false, true, true );
		foreach ( $archives as $archive ) {
			$contexts[ 'breadcrumbs-post-type-archive-' . $archive['name'] ] = array_merge( $baseTags, [
				'breadcrumb_archive_post_type_format',
				'breadcrumb_archive_post_type_name'
			] );
		}

		$contexts['breadcrumbs-blog-archive'] = array_merge( $baseTags, [ 'breadcrumb_blog_page_title' ] );

		$contexts['breadcrumbs-author'] = array_merge( $baseTags, [
			'breadcrumb_author_display_name',
			'breadcrumb_author_first_name',
			'breadcrumb_author_last_name'
		] );

		$contexts['breadcrumbs-search']             = array_merge( $baseTags, [ 'breadcrumb_search_result_format', 'breadcrumb_search_string' ] );
		$contexts['breadcrumbs-notFound']           = array_merge( $baseTags, [ 'breadcrumb_404_error_format' ] );
		$contexts['breadcrumbs-date-archive-year']  = array_merge( $baseTags, [ 'breadcrumb_date_archive_year' ] );
		$contexts['breadcrumbs-date-archive-month'] = array_merge( $baseTags, [ 'breadcrumb_date_archive_month' ] );
		$contexts['breadcrumbs-date-archive-day']   = array_merge( $baseTags, [ 'breadcrumb_date_archive_day' ] );

		$contexts['breadcrumbs-format-archive'] = [ 'breadcrumb_archive_post_type_name' ];
		$contexts['breadcrumbs-format-search']  = [ 'breadcrumb_search_string' ];
		$contexts['breadcrumbs-format-paged']   = [ 'breadcrumb_format_page_number' ];

		return $contexts;
	}
}Common/Breadcrumbs/Widget.php000064400000006410151536241170012167 0ustar00<?php
namespace AIOSEO\Plugin\Common\Breadcrumbs;

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

/**
 * Class Widget.
 *
 * @since 4.1.1
 */
class Widget extends \WP_Widget {
	/**
	 * The default attributes.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $defaults = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.1
	 */
	public function __construct() {
		// Widget defaults.
		$this->defaults = [
			'title' => ''
		];

		// Widget Slug.
		$widgetSlug = 'aioseo-breadcrumb-widget';

		// Widget basics.
		$widgetOps = [
			'classname'   => $widgetSlug,
			'description' => esc_html__( 'Display the current page breadcrumb.', 'all-in-one-seo-pack' ),
		];

		// Widget controls.
		$controlOps = [
			'id_base' => $widgetSlug,
		];

		// Translators: 1 - The plugin short name ("AIOSEO").
		$name = sprintf( esc_html__( '%1$s - Breadcrumbs', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME );
		$name .= ' ' . esc_html__( '(legacy)', 'all-in-one-seo-pack' );
		parent::__construct( $widgetSlug, $name, $widgetOps, $controlOps );
	}

	/**
	 * Widget callback.
	 *
	 * @since 4.1.1
	 *
	 * @param  array $args     Widget args.
	 * @param  array $instance The widget instance options.
	 * @return void
	 */
	public function widget( $args, $instance ) {
		// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
		// Merge with defaults.
		$instance = wp_parse_args( (array) $instance, $this->defaults );

		echo $args['before_widget'];

		// Title.
		if ( ! empty( $instance['title'] ) ) {
			echo $args['before_title'];
			echo apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base );
			echo $args['after_title'];
		}

		// If not being previewed in the Customizer maybe show the dummy preview.
		if (
			! is_customize_preview() &&
			(
				false !== strpos( wp_get_referer(), admin_url( 'widgets.php' ) ) ||
				false !== strpos( wp_get_referer(), admin_url( 'customize.php' ) )
			)
		) {
			aioseo()->breadcrumbs->frontend->preview();
		} else {
			aioseo()->breadcrumbs->frontend->display();
		}

		echo $args['after_widget'];
		// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Widget option update.
	 *
	 * @since 4.1.1
	 *
	 * @param array $newInstance New instance options.
	 * @param array $oldInstance Old instance options.
	 * @return array              Processed new instance options.
	 */
	public function update( $newInstance, $oldInstance ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$newInstance['title'] = wp_strip_all_tags( $newInstance['title'] );

		return $newInstance;
	}

	/**
	 * Widget options form.
	 *
	 * @since 4.1.1
	 *
	 * @param array $instance The widget instance options.
	 * @return void
	 */
	public function form( $instance ) {
		// Merge with defaults.
		$instance = wp_parse_args( (array) $instance, $this->defaults );
		?>
		<p>
			<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
				<?php echo esc_html( __( 'Title:', 'all-in-one-seo-pack' ) ); ?>
			</label>
			<input
					type="text"
					id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
					name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
					value="<?php echo esc_attr( $instance['title'] ); ?>"
					class="widefat"
			/>
		</p>
		<?php
	}
}Common/Core/Core.php000064400000005345151536241170010301 0ustar00<?php
namespace AIOSEO\Plugin\Common\Core;

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

use AIOSEO\Plugin\Common\Options;
use AIOSEO\Plugin\Common\Utils;

/**
 * Loads core classes.
 *
 * @since 4.1.9
 */
class Core {
	/**
	 * List of AIOSEO tables.
	 *
	 * @since 4.2.5
	 *
	 * @var array
	 */
	private $aioseoTables = [
		'aioseo_cache',
		'aioseo_crawl_cleanup_blocked_args',
		'aioseo_crawl_cleanup_logs',
		'aioseo_links',
		'aioseo_links_suggestions',
		'aioseo_notifications',
		'aioseo_posts',
		'aioseo_redirects',
		'aioseo_redirects_404',
		'aioseo_redirects_404_logs',
		'aioseo_redirects_hits',
		'aioseo_redirects_logs',
		'aioseo_terms',
		'aioseo_search_statistics_objects',
		'aioseo_revisions',
		'aioseo_seo_analyzer_results'
	];

	/**
	 * Filesystem class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Utils\Filesystem
	 */
	public $fs = null;

	/**
	 * Assets class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Utils\Assets
	 */
	public $assets = null;

	/**
	 * DB class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Utils\Database
	 */
	public $db = null;

	/**
	 * Cache class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Utils\Cache
	 */
	public $cache = null;

	/**
	 * NetworkCache class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Utils\NetworkCache
	 */
	public $networkCache = null;

	/**
	 * CachePrune class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Utils\CachePrune
	 */
	public $cachePrune = null;

	/**
	 * Options Cache class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Options\Cache
	 */
	public $optionsCache = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.1.9
	 */
	public function __construct() {
		$this->fs           = new Utils\Filesystem( $this );
		$this->assets       = new Utils\Assets( $this );
		$this->db           = new Utils\Database();
		$this->cache        = new Utils\Cache();
		$this->networkCache = new Utils\NetworkCache();
		$this->cachePrune   = new Utils\CachePrune();
		$this->optionsCache = new Options\Cache();
	}

	/**
	 * Get all the DB tables with prefix.
	 *
	 * @since 4.2.5
	 *
	 * @return array An array of tables.
	 */
	public function getDbTables() {
		global $wpdb;

		$tables = [];
		foreach ( $this->aioseoTables as $tableName ) {
			$tables[] = $wpdb->prefix . $tableName;
		}

		return $tables;
	}

	/**
	 * Check if the current request is uninstalling (deleting) AIOSEO.
	 *
	 * @since 4.3.7
	 *
	 * @return bool Whether AIOSEO is being uninstalled/deleted or not.
	 */
	public function isUninstalling() {
		if (
			defined( 'AIOSEO_FILE' ) &&
			defined( 'WP_UNINSTALL_PLUGIN' )
		) {
			// Make sure `plugin_basename()` exists.
			include_once ABSPATH . 'wp-admin/includes/plugin.php';

			return WP_UNINSTALL_PLUGIN === plugin_basename( AIOSEO_FILE );
		}

		return false;
	}
}Common/EmailReports/EmailReports.php000064400000004222151536241170013526 0ustar00<?php

namespace AIOSEO\Plugin\Common\EmailReports;

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

use AIOSEO\Plugin\Common\Models;

/**
 * EmailReports class.
 *
 * @since 4.7.2
 */
class EmailReports {
	/**
	 * Mail object.
	 *
	 * @since 4.7.2
	 *
	 * @var Mail
	 */
	public $mail = null;

	/**
	 * Summary object.
	 *
	 * @since 4.7.2
	 *
	 * @var Summary\Summary
	 */
	public $summary;

	/**
	 * Class constructor.
	 *
	 * @since 4.7.2
	 */
	public function __construct() {
		$this->mail    = new Mail();
		$this->summary = new Summary\Summary();

		add_action( 'aioseo_email_reports_enable_reminder', [ $this, 'enableReminder' ] );
	}

	/**
	 * Enable reminder.
	 *
	 * @since 4.7.7
	 *
	 * @return void
	 */
	public function enableReminder() {
		// User already enabled email reports.
		if ( aioseo()->options->advanced->emailSummary->enable ) {
			return;
		}

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

		// Add notification.
		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'email-reports-enable-reminder',
			'title'             => __( 'Email Reports', 'all-in-one-seo-pack' ),
			'content'           => __( 'Stay ahead in SEO with our new email digest! Get the latest tips, trends, and tools delivered right to your inbox, helping you optimize smarter and faster. Enable it today and never miss an update that can take your rankings to the next level.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'type'              => 'info',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Enable Email Reports', 'all-in-one-seo-pack' ),
			'button1_action'    => 'https://route#aioseo-settings&aioseo-scroll=aioseo-email-summary-row&aioseo-highlight=aioseo-email-summary-row:advanced',
			'button2_label'     => __( 'All Good, I\'m already getting it', 'all-in-one-seo-pack' ),
			'button2_action'    => 'http://action#notification/email-reports-enable',
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}
}Common/EmailReports/Mail.php000064400000001163151536241170012003 0ustar00<?php

namespace AIOSEO\Plugin\Common\EmailReports;

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

/**
 * Mail class.
 *
 * @since 4.7.2
 */
class Mail {
	/**
	 * Send the email.
	 *
	 * @since 4.7.2
	 *
	 * @param  mixed $to      Receiver.
	 * @param  mixed $subject Email subject.
	 * @param  mixed $message Message.
	 * @param  array $headers Email headers.
	 * @return bool           Whether the email was sent successfully.
	 */
	public function send( $to, $subject, $message, $headers = [ 'Content-Type: text/html; charset=UTF-8' ] ) {
		return wp_mail( $to, $subject, $message, $headers );
	}
}Common/EmailReports/Summary/Content.php000064400000050371151536241170014175 0ustar00<?php

namespace AIOSEO\Plugin\Common\EmailReports\Summary;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Summary content class.
 *
 * @since 4.7.2
 */
class Content {
	/**
	 * The date range data.
	 *
	 * @since 4.7.2
	 *
	 * @var array
	 */
	public $dateRange;

	/**
	 * The SEO Statistics data.
	 *
	 * @since 4.7.2
	 *
	 * @var array
	 */
	private $seoStatistics = [];

	/**
	 * The Keywords data.
	 *
	 * @since 4.7.2
	 *
	 * @var array
	 */
	private $keywords = [];

	/**
	 * The Search Statistics page URL.
	 *
	 * @since 4.7.2
	 *
	 * @var string
	 */
	public $searchStatisticsUrl;

	/**
	 * The featured image placeholder URL.
	 *
	 * @since 4.7.3
	 *
	 * @var string
	 */
	public $featuredImagePlaceholder = 'https://static.aioseo.io/report/ste/featured-image-placeholder.png';

	/**
	 * Class constructor.
	 *
	 * @since 4.7.2
	 *
	 * @param  array $dateRange The date range data.
	 * @return void
	 */
	public function __construct( $dateRange ) {
		$this->dateRange           = $dateRange;
		$this->searchStatisticsUrl = admin_url( 'admin.php?page=aioseo-search-statistics' );

		$this->setSeoStatistics();
		$this->setKeywords();
	}

	/**
	 * Sets the SEO Statistics data.
	 *
	 * @since 4.7.2
	 *
	 * @return void
	 */
	private function setSeoStatistics() {
		try {
			$seoStatistics = aioseo()->searchStatistics->getSeoStatisticsData( [
				'startDate' => gmdate( 'Y-m-d', $this->dateRange['startDateRaw'] ),
				'endDate'   => gmdate( 'Y-m-d', $this->dateRange['endDateRaw'] ),
				'orderBy'   => 'clicks',
				'orderDir'  => 'desc',
				'limit'     => '5',
				'offset'    => '0',
				'filter'    => 'all',
			] );

			if ( empty( $seoStatistics['data'] ) ) {
				return;
			}

			$this->seoStatistics = $seoStatistics['data'];
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Sets the Keywords data.
	 *
	 * @since 4.7.2
	 *
	 * @return void
	 */
	private function setKeywords() {
		try {
			$keywords = aioseo()->searchStatistics->getKeywordsData( [
				'startDate' => gmdate( 'Y-m-d', $this->dateRange['startDateRaw'] ),
				'endDate'   => gmdate( 'Y-m-d', $this->dateRange['endDateRaw'] ),
				'orderBy'   => 'clicks',
				'orderDir'  => 'desc',
				'limit'     => '5',
				'offset'    => '0',
				'filter'    => 'all',
			] );

			if ( empty( $keywords['data'] ) ) {
				return;
			}

			$this->keywords = $keywords['data'];
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Retrieves the content performance data.
	 *
	 * @since 4.7.2
	 *
	 * @return array The content performance data or an empty array.
	 */
	public function getPostsStatistics() {
		if ( ! $this->seoStatistics ) {
			return [];
		}

		$result = [
			'winning' => [
				'url'   => add_query_arg( [
					'aioseo-scroll' => 'aioseo-search-statistics-post-table',
					'aioseo-tab'    => 'seo-statistics',
					'table-filter'  => 'TopWinningPages'
				], $this->searchStatisticsUrl ),
				'items' => []
			],
			'losing'  => [
				'url'   => add_query_arg( [
					'aioseo-scroll' => 'aioseo-search-statistics-post-table',
					'aioseo-tab'    => 'seo-statistics',
					'table-filter'  => 'TopLosingPages'
				], $this->searchStatisticsUrl ),
				'items' => []
			]
		];

		foreach ( array_slice( $this->seoStatistics['pages']['topWinning']['rows'], 0, 3 ) as $row ) {
			$postId                       = $row['objectId'] ?? 0;
			$result['winning']['items'][] = [
				'title'      => $row['objectTitle'],
				'url'        => get_permalink( $postId ),
				'tru_seo'    => aioseo()->helpers->isTruSeoEligible( $postId ) ? $this->parseSeoScore( $row['seoScore'] ?? 0 ) : [],
				'clicks'     => $this->parseClicks( $row['clicks'] ),
				'difference' => [
					'clicks' => $this->parseDifference( $row['difference']['clicks'] ?? '' ),
				]
			];
		}

		$result['winning']['show_tru_seo'] = ! empty( array_filter( array_column( $result['winning']['items'], 'tru_seo' ) ) );

		foreach ( array_slice( $this->seoStatistics['pages']['topLosing']['rows'], 0, 3 ) as $row ) {
			$postId                      = $row['objectId'] ?? 0;
			$result['losing']['items'][] = [
				'title'      => $row['objectTitle'],
				'url'        => get_permalink( $postId ),
				'tru_seo'    => aioseo()->helpers->isTruSeoEligible( $postId ) ? $this->parseSeoScore( $row['seoScore'] ?? 0 ) : [],
				'clicks'     => $this->parseClicks( $row['clicks'] ),
				'difference' => [
					'clicks' => $this->parseDifference( $row['difference']['clicks'] ?? '' ),
				]
			];
		}

		$result['losing']['show_tru_seo'] = ! empty( array_filter( array_column( $result['losing']['items'], 'tru_seo' ) ) );

		return $result;
	}

	/**
	 * Retrieves the milestones data.
	 *
	 * @since 4.7.2
	 *
	 * @return array The milestones data or an empty array.
	 */
	public function getMilestones() { // phpcs:ignore Generic.Files.LineLength.MaxExceeded
		$milestones = [];
		if ( ! $this->seoStatistics ) {
			return $milestones;
		}

		$currentData = [
			'impressions' => $this->seoStatistics['statistics']['impressions'] ?? null,
			'clicks'      => $this->seoStatistics['statistics']['clicks'] ?? null,
			'ctr'         => $this->seoStatistics['statistics']['ctr'] ?? null,
			'keywords'    => $this->seoStatistics['statistics']['keywords'] ?? null,
		];
		$difference  = [
			'impressions' => $this->seoStatistics['statistics']['difference']['impressions'] ?? null,
			'clicks'      => $this->seoStatistics['statistics']['difference']['clicks'] ?? null,
			'ctr'         => $this->seoStatistics['statistics']['difference']['ctr'] ?? null,
			'keywords'    => $this->seoStatistics['statistics']['difference']['keywords'] ?? null,
		];

		if ( is_numeric( $currentData['impressions'] ) && is_numeric( $difference['impressions'] ) ) {
			$intDifference = intval( $difference['impressions'] );
			$message       = esc_html__( 'Your site has received the same number of impressions compared to the previous period.', 'all-in-one-seo-pack' );

			if ( $intDifference > 0 ) {
				// Translators: 1 - The number of impressions, 2 - The percentage increase.
				$message = esc_html__( 'Your site has received %1$s more impressions compared to the previous period, which is a %2$s increase.', 'all-in-one-seo-pack' );
			}

			if ( $intDifference < 0 ) {
				// Translators: 1 - The number of impressions, 2 - The percentage increase.
				$message = esc_html__( 'Your site has received %1$s fewer impressions compared to the previous period, which is a %2$s decrease.', 'all-in-one-seo-pack' );
			}

			if ( false !== strpos( $message, '%1' ) ) {
				$percentageDiff = 0 === absint( $currentData['impressions'] )
					? 100
					: round( ( absint( $intDifference ) / absint( $currentData['impressions'] ) ) * 100, 2 );
				$percentageDiff = false !== strpos( $percentageDiff, '.' )
					? number_format_i18n( $percentageDiff, count( explode( '.', $percentageDiff ) ) )
					: $percentageDiff;
				$message        = sprintf(
					$message,
					'<strong>' . aioseo()->helpers->compactNumber( absint( $intDifference ) ) . '</strong>',
					'<strong>' . $percentageDiff . '%</strong>'
				);
			}

			$milestones[] = [
				'message'    => $message,
				'background' => '#f0f6ff',
				'color'      => '#004F9D',
				'icon'       => 'icon-milestone-impressions'
			];
		}

		if ( is_numeric( $currentData['clicks'] ) && is_numeric( $difference['clicks'] ) ) {
			$intDifference = intval( $difference['clicks'] );
			$message       = esc_html__( 'Your site has received the same number of clicks compared to the previous period.', 'all-in-one-seo-pack' );

			if ( $intDifference > 0 ) {
				// Translators: 1 - The number of clicks, 2 - The percentage increase.
				$message = esc_html__( 'Your site has received %1$s more clicks compared to the previous period, which is a %2$s increase.', 'all-in-one-seo-pack' );
			}

			if ( $intDifference < 0 ) {
				// Translators: 1 - The number of clicks, 2 - The percentage increase.
				$message = esc_html__( 'Your site has received %1$s fewer clicks compared to the previous period, which is a %2$s decrease.', 'all-in-one-seo-pack' );
			}

			if ( false !== strpos( $message, '%1' ) ) {
				$percentageDiff = 0 === absint( $currentData['clicks'] )
					? 100
					: round( ( absint( $intDifference ) / absint( $currentData['clicks'] ) ) * 100, 2 );
				$percentageDiff = false !== strpos( $percentageDiff, '.' )
					? number_format_i18n( $percentageDiff, count( explode( '.', $percentageDiff ) ) )
					: $percentageDiff;
				$message        = sprintf(
					$message,
					'<strong>' . aioseo()->helpers->compactNumber( absint( $intDifference ) ) . '</strong>',
					'<strong>' . $percentageDiff . '%</strong>'
				);
			}

			$milestones[] = [
				'message'    => $message,
				'background' => '#ecfdf5',
				'color'      => '#077647',
				'icon'       => 'icon-milestone-clicks'
			];
		}

		if ( is_numeric( $currentData['ctr'] ) && is_numeric( $difference['ctr'] ) ) {
			$intDifference = floatval( $difference['ctr'] );
			$message       = esc_html__( 'Your site has the same CTR compared to the previous period.', 'all-in-one-seo-pack' );

			if ( $intDifference > 0 ) {
				// Translators: 1 - The CTR.
				$message = esc_html__( 'Your site has a %1$s higher CTR compared to the previous period.', 'all-in-one-seo-pack' );
			}

			if ( $intDifference < 0 ) {
				// Translators: 1 - The CTR.
				$message = esc_html__( 'Your site has a %1$s lower CTR compared to the previous period.', 'all-in-one-seo-pack' );
			}

			if ( false !== strpos( $message, '%1' ) ) {
				$message = sprintf(
					$message,
					'<strong>' . number_format_i18n( abs( $intDifference ), count( explode( '.', $intDifference ) ) ) . '%</strong>'
				);
			}

			$milestones[] = [
				'message'    => $message,
				'background' => '#fffbeb',
				'color'      => '#be6903',
				'icon'       => 'icon-milestone-ctr'
			];
		}

		if ( is_numeric( $currentData['keywords'] ) && is_numeric( $difference['keywords'] ) ) {
			$intDifference = intval( $difference['keywords'] );
			$message       = esc_html__( 'Your site ranked for the same number of keywords compared to the previous period.', 'all-in-one-seo-pack' );

			if ( $intDifference > 0 ) {
				// Translators: 1 - The number of keywords, 2 - The percentage increase.
				$message = esc_html__( 'Your site ranked for %1$s more keywords compared to the previous period, which is a %2$s increase.', 'all-in-one-seo-pack' );
			}

			if ( $intDifference < 0 ) {
				// Translators: 1 - The number of keywords, 2 - The percentage increase.
				$message = esc_html__( 'Your site ranked for %1$s fewer keywords compared to the previous period, which is a %2$s decrease.', 'all-in-one-seo-pack' );
			}

			if ( false !== strpos( $message, '%1' ) ) {
				$percentageDiff = 0 === absint( $currentData['keywords'] )
					? 100
					: round( ( absint( $intDifference ) / absint( $currentData['keywords'] ) ) * 100, 2 );
				$percentageDiff = false !== strpos( $percentageDiff, '.' )
					? number_format_i18n( $percentageDiff, count( explode( '.', $percentageDiff ) ) )
					: $percentageDiff;
				$message        = sprintf(
					$message,
					'<strong>' . aioseo()->helpers->compactNumber( absint( $intDifference ) ) . '</strong>',
					'<strong>' . $percentageDiff . '%</strong>'
				);
			}

			$milestones[] = [
				'message'    => $message,
				'background' => '#fef2f2',
				'color'      => '#ab2039',
				'icon'       => 'icon-milestone-keywords'
			];
		}

		return $milestones;
	}

	/**
	 * Retrieves the keyword performance data.
	 *
	 * @since 4.7.2
	 *
	 * @return array The keyword performance data or an empty array.
	 */
	public function getKeywords() {
		if ( ! $this->keywords ) {
			return [];
		}

		$result = [
			'winning' => [
				'url'   => add_query_arg( [
					'aioseo-scroll' => 'aioseo-search-statistics-keywords-table',
					'aioseo-tab'    => 'keyword-rank-tracker',
					'tab'           => 'AllKeywords',
					'table-filter'  => 'TopWinningKeywords'
				], $this->searchStatisticsUrl ),
				'items' => []
			],
			'losing'  => [
				'url'   => add_query_arg( [
					'aioseo-scroll' => 'aioseo-search-statistics-keywords-table',
					'aioseo-tab'    => 'keyword-rank-tracker',
					'tab'           => 'AllKeywords',
					'table-filter'  => 'TopLosingKeywords'
				], $this->searchStatisticsUrl ),
				'items' => []
			]
		];

		foreach ( array_slice( $this->keywords['topWinning'], 0, 3 ) as $row ) {
			$result['winning']['items'][] = [
				'title'      => $row['keyword'],
				'clicks'     => $this->parseClicks( $row['clicks'] ),
				'difference' => [
					'clicks' => $this->parseDifference( $row['difference']['clicks'] ?? '' ),
				]
			];
		}

		foreach ( array_slice( $this->keywords['topLosing'], 0, 3 ) as $row ) {
			$result['losing']['items'][] = [
				'title'      => $row['keyword'],
				'clicks'     => $this->parseClicks( $row['clicks'] ),
				'difference' => [
					'clicks' => $this->parseDifference( $row['difference']['clicks'] ?? '' ),
				]
			];
		}

		return $result;
	}

	/**
	 * Retrieves the posts data.
	 *
	 * @since 4.7.2
	 *
	 * @return array The posts' data.
	 */
	public function getAioPosts() {
		$result = [
			'publish'  => [],
			'optimize' => [],
			'cta'      => [
				'text' => esc_html__( 'Create New Post', 'all-in-one-seo-pack' ),
				'url'  => admin_url( 'post-new.php' )
			],
		];

		// 1. Retrieve the published posts.
		$publishPosts = aioseo()->core->db
			->start( 'posts as wp' )
			->select( 'wp.ID, wp.post_title, aio.seo_score' )
			->join( 'aioseo_posts as aio', 'aio.post_id = wp.ID', 'INNER' )
			->whereIn( 'wp.post_type', [ 'post' ] )
			->whereIn( 'wp.post_status', [ 'publish' ] )
			->orderBy( 'wp.post_date DESC' )
			->limit( 5 )
			->run()
			->result();

		if ( $publishPosts ) {
			$items             = $this->parsePosts( $publishPosts );
			$result['publish'] = [
				'url'          => admin_url( 'edit.php?post_status=publish&post_type=post' ),
				'items'        => $items,
				'show_stats'   => ! empty( array_filter( array_column( $items, 'stats' ) ) ),
				'show_tru_seo' => ! empty( array_filter( array_column( $items, 'tru_seo' ) ) ),
			];
		}

		// 2. Retrieve the posts to optimize.
		$optimizePosts = aioseo()->searchStatistics->getContentRankingsData( [
			'endDate'  => gmdate( 'Y-m-d', $this->dateRange['endDateRaw'] ),
			'orderBy'  => 'decayPercent',
			'orderDir' => 'asc',
			'limit'    => '3',
			'offset'   => '0',
			'filter'   => 'all',
		] );

		if ( is_array( $optimizePosts['data']['paginated']['rows'] ?? '' ) ) {
			$items = [];
			foreach ( array_slice( $optimizePosts['data']['paginated']['rows'], 0, 3 ) as $i => $row ) {
				$postId      = $row['objectId'] ?? 0;
				$items[ $i ] = [
					'title'         => $row['objectTitle'],
					'url'           => get_permalink( $postId ),
					'image_url'     => $this->getThumbnailUrl( $postId ),
					'tru_seo'       => aioseo()->helpers->isTruSeoEligible( $postId ) ? $this->parseSeoScore( $row['seoScore'] ?? 0 ) : [],
					'decay_percent' => $this->parseDifference( $row['decayPercent'] ?? '', true ),
					'issues'        => [
						'url'   => add_query_arg( [
							'aioseo-tab' => 'post-detail',
							'post'       => $postId
						], $this->searchStatisticsUrl ),
						'items' => []
					]
				];

				$aioPost = Models\Post::getPost( $postId );
				if ( $aioPost ) {
					$items[ $i ]['issues']['items'] = aioseo()->searchStatistics->helpers->getSuggestedChanges( $aioPost );
				}
			}

			$result['optimize'] = [
				'url'          => add_query_arg( [
					'aioseo-tab' => 'content-rankings',
				], $this->searchStatisticsUrl ),
				'items'        => $items,
				'show_tru_seo' => ! empty( array_filter( array_column( $items, 'tru_seo' ) ) ),
			];
		}

		return $result;
	}

	/**
	 * Retrieves the resources data.
	 *
	 * @since 4.7.2
	 *
	 * @return array The resources' data.
	 */
	public function getResources() {
		$items = aioseo()->helpers->fetchAioseoArticles( true );

		return array_slice( array_filter( $items ), 0, 3 );
	}

	/**
	 * Returns if Search Statistics content is allowed.
	 *
	 * @since 4.7.3
	 *
	 * @return bool Whether Search Statistics content is allowed.
	 */
	public function allowSearchStatistics() {
		static $return = null;
		if ( isset( $return ) ) {
			return $return;
		}

		$return = aioseo()->searchStatistics->api->auth->isConnected() &&
					aioseo()->license &&
					aioseo()->license->hasCoreFeature( 'search-statistics', 'seo-statistics' ) &&
					aioseo()->license->hasCoreFeature( 'search-statistics', 'keyword-rankings' );

		return $return;
	}

	/**
	 * Parses the SEO score.
	 *
	 * @since 4.7.2
	 *
	 * @param  int|string $score The SEO score.
	 * @return array             The parsed SEO score.
	 */
	private function parseSeoScore( $score ) {
		$score  = intval( $score );
		$parsed = [
			'value' => $score,
			'color' => '#a1a1a1',
			'text'  => $score ? "$score/100" : esc_html__( 'N/A', 'all-in-one-seo-pack' ),
		];

		if ( $parsed['value'] > 79 ) {
			$parsed['color'] = '#00aa63';
		} elseif ( $parsed['value'] > 49 ) {
			$parsed['color'] = '#ff8c00';
		} elseif ( $parsed['value'] > 0 ) {
			$parsed['color'] = '#df2a4a';
		}

		return $parsed;
	}

	/**
	 * Parses a difference.
	 *
	 * @since 4.7.2
	 *
	 * @param  int|string $number     The number to parse.
	 * @param  bool       $percentage Whether to return the text result as a percentage.
	 * @return array                  The parsed result.
	 */
	private function parseDifference( $number, $percentage = false ) {
		$parsed = [
			'color' => '#a1a1a1',
			'text'  => esc_html__( 'N/A', 'all-in-one-seo-pack' ),
		];
		if ( ! is_numeric( $number ) ) {
			return $parsed;
		}

		$number         = intval( $number );
		$parsed['text'] = aioseo()->helpers->compactNumber( absint( $number ) );

		if ( $percentage ) {
			$parsed['text'] = $number . '%';
		}

		if ( $number > 0 ) {
			$parsed['color'] = '#00aa63';
		} elseif ( $number < 0 ) {
			$parsed['color'] = '#df2a4a';
		}

		return $parsed;
	}

	/**
	 * Parses the clicks number.
	 *
	 * @since 4.7.2
	 *
	 * @param  float|int|string $number The number of clicks.
	 * @return string                   The parsed number of clicks.
	 */
	private function parseClicks( $number ) {
		return aioseo()->helpers->compactNumber( $number );
	}

	/**
	 * Parses the posts data.
	 *
	 * @since 4.7.2
	 *
	 * @param  array $posts The posts.
	 * @return array        The parsed posts' data.
	 */
	private function parsePosts( $posts ) {
		$parsed = [];
		foreach ( $posts as $k => $item ) {
			$parsed[ $k ] = [
				'title'     => aioseo()->helpers->truncate( $item->post_title, 75 ),
				'url'       => get_permalink( $item->ID ),
				'image_url' => $this->getThumbnailUrl( $item->ID ),
				'tru_seo'   => aioseo()->helpers->isTruSeoEligible( $item->ID ) ? $this->parseSeoScore( $item->seo_score ?? 0 ) : [],
				'stats'     => []
			];

			try {
				$statistics = [];
				if (
					$this->allowSearchStatistics() &&
					method_exists( aioseo()->searchStatistics, 'getPostDetailSeoStatisticsData' )
				) {
					$statistics = aioseo()->searchStatistics->getPostDetailSeoStatisticsData( [
						'startDate' => gmdate( 'Y-m-d', $this->dateRange['startDateRaw'] ),
						'endDate'   => gmdate( 'Y-m-d', $this->dateRange['endDateRaw'] ),
						'postId'    => $item->ID,
					], false );
				}

				if ( isset( $statistics['data']['statistics']['position'] ) ) {
					$parsed[ $k ]['stats'][] = [
						'icon'  => 'position',
						'label' => esc_html__( 'Position', 'all-in-one-seo-pack' ),
						'value' => round( floatval( $statistics['data']['statistics']['position'] ) ),
					];
				}

				if ( isset( $statistics['data']['statistics']['ctr'] ) ) {
					$value                   = round( floatval( $statistics['data']['statistics']['ctr'] ), 2 );
					$parsed[ $k ]['stats'][] = [
						'icon'  => 'ctr',
						'label' => 'CTR',
						'value' => ( number_format_i18n( $value, count( explode( '.', $value ) ) ) ) . '%',
					];
				}

				if ( isset( $statistics['data']['statistics']['impressions'] ) ) {
					$parsed[ $k ]['stats'][] = [
						'icon'  => 'impressions',
						'label' => esc_html__( 'Impressions', 'all-in-one-seo-pack' ),
						'value' => aioseo()->helpers->compactNumber( $statistics['data']['statistics']['impressions'] ),
					];
				}
			} catch ( \Exception $e ) {
				// Do nothing.
			}
		}

		return $parsed;
	}

	/**
	 * Retrieves the thumbnail URL.
	 *
	 * @since 4.7.2
	 *
	 * @param  int    $postId The post ID.
	 * @return string         The post featured image URL (thumbnail size).
	 */
	private function getThumbnailUrl( $postId ) {
		$imageUrl = get_the_post_thumbnail_url( $postId );

		return $imageUrl ?: $this->featuredImagePlaceholder;
	}
}Common/EmailReports/Summary/Summary.php000064400000022740151536241170014217 0ustar00<?php

namespace AIOSEO\Plugin\Common\EmailReports\Summary;

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

/**
 * Summary class.
 *
 * @since 4.7.2
 */
class Summary {
	/**
	 * The action hook to execute when the event is run.
	 *
	 * @since 4.7.2
	 *
	 * @var string
	 */
	public $actionHook = 'aioseo_report_summary';

	/**
	 * Recipient for the email. Multiple recipients can be separated by a comma.
	 *
	 * @since 4.7.2
	 *
	 * @var string
	 */
	private $recipient;

	/**
	 * Email chosen frequency. Can be either 'weekly' or 'monthly'.
	 *
	 * @since 4.7.2
	 *
	 * @var string
	 */
	private $frequency;

	/**
	 * Class constructor.
	 *
	 * @since 4.7.2
	 */
	public function __construct() {
		// No need to run any of this during a WP AJAX request.
		if ( wp_doing_ajax() ) {
			return;
		}

		// No need to keep trying scheduling unless on admin.
		add_action( 'admin_init', [ $this, 'maybeSchedule' ], 20 );

		add_action( $this->actionHook, [ $this, 'cronTrigger' ] );
	}

	/**
	 * The summary cron callback.
	 * Hooked into `{@see self::$actionHook}` action hook.
	 *
	 * @since 4.7.2
	 *
	 * @param  string $frequency The frequency of the email.
	 * @return void
	 */
	public function cronTrigger( $frequency ) {
		// Keep going only if the feature is enabled.
		if (
			! aioseo()->options->advanced->emailSummary->enable ||
			! apply_filters( 'aioseo_report_summary_enable', true, $frequency )
		) {
			return;
		}

		// Get all recipients for the given frequency.
		$recipients = wp_list_filter( aioseo()->options->advanced->emailSummary->recipients, [ 'frequency' => $frequency ] );
		if ( ! $recipients ) {
			return;
		}

		try {
			// Get only the email addresses.
			$recipients = array_column( $recipients, 'email' );

			$this->run( [
				'recipient' => implode( ',', $recipients ),
				'frequency' => $frequency,
			] );
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Trigger the sending of the summary.
	 *
	 * @since 4.7.2
	 *
	 * @param  array      $data All the initial data needed for the summary to be sent.
	 * @throws \Exception       If the email could not be sent.
	 * @return void
	 */
	public function run( $data ) {
		try {
			$this->recipient = $data['recipient'] ?? '';
			$this->frequency = $data['frequency'] ?? '';

			aioseo()->emailReports->mail->send( $this->getRecipient(), $this->getSubject(), $this->getContentHtml(), $this->getHeaders() );
		} catch ( \Exception $e ) {
			throw new \Exception( esc_html( $e->getMessage() ), esc_html( $e->getCode() ) );
		}
	}

	/**
	 * Maybe (re)schedule the summary.
	 *
	 * @since 4.7.2
	 *
	 * @return void
	 */
	public function maybeSchedule() {
		$allowedFrequencies = $this->getAllowedFrequencies();

		// Add at least 6 hours after the day starts.
		$addToStart = HOUR_IN_SECONDS * 6;
		// Add the timezone offset.
		$addToStart -= aioseo()->helpers->getTimeZoneOffset();
		// Add a random time offset to avoid all emails being sent at the same time. 1440 * 3 = 3 days range.
		$addToStart += aioseo()->helpers->generateRandomTimeOffset( aioseo()->helpers->getSiteDomain( true ), 1440 * 3 ) * MINUTE_IN_SECONDS;

		foreach ( $allowedFrequencies as $frequency => $data ) {
			aioseo()->actionScheduler->scheduleRecurrent( $this->actionHook, $data['start'] + $addToStart, $data['interval'], compact( 'frequency' ) );
		}
	}

	/**
	 * Get one or more valid recipients.
	 *
	 * @since 4.7.2
	 *
	 * @throws \Exception If no valid recipient was set for the email.
	 * @return string     The valid recipients.
	 */
	private function getRecipient() {
		$recipients = array_map( 'trim', explode( ',', $this->recipient ) );
		$recipients = array_filter( $recipients, 'is_email' );

		if ( empty( $recipients ) ) {
			throw new \Exception( 'No valid recipient was set for the email.' ); // Not shown to the user.
		}

		return implode( ',', $recipients );
	}

	/**
	 * Get email subject.
	 *
	 * @since 4.7.2
	 *
	 * @return string The email subject.
	 */
	private function getSubject() {
		// Translators: 1 - Date range.
		$out = esc_html__( 'Your SEO Performance Report for %1$s', 'all-in-one-seo-pack' );

		$dateRange = $this->getDateRange();
		$suffix    = date_i18n( 'F', $dateRange['endDateRaw'] );
		if ( 'weekly' === $this->frequency ) {
			$suffix = $dateRange['range'];
		}

		return sprintf( $out, $suffix );
	}

	/**
	 * Get content html.
	 *
	 * @since 4.7.2
	 *
	 * @return string The email content.
	 */
	private function getContentHtml() { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$dateRange        = $this->getDateRange();
		$content          = new Content( $dateRange );
		$upsell           = [
			'search-statistics' => []
		];
		$preHeader        = sprintf(
			// Translators: 1 - The plugin short name ("AIOSEO").
			esc_html__( 'Dive into your top-performing pages with %1$s and uncover growth opportunities.', 'all-in-one-seo-pack' ),
			AIOSEO_PLUGIN_SHORT_NAME
		);
		$iconCalendar     = 'weekly' === $this->frequency
			? 'icon-calendar-weekly'
			: 'icon-calendar-monthly';
		$heading          = 'weekly' === $this->frequency
			? esc_html__( 'Your Weekly SEO Email Summary', 'all-in-one-seo-pack' )
			: esc_html__( 'Your Monthly SEO Email Summary', 'all-in-one-seo-pack' );
		$subheading       = 'weekly' === $this->frequency
			? esc_html__( 'Let\'s take a look at your SEO updates and content progress this week.', 'all-in-one-seo-pack' )
			: esc_html__( 'Let\'s take a look at your SEO updates and content progress this month.', 'all-in-one-seo-pack' );
		$statisticsReport = [
			'posts'      => [],
			'keywords'   => [],
			'milestones' => [],
			'cta'        => [
				'text' => esc_html__( 'See All SEO Statistics', 'all-in-one-seo-pack' ),
				'url'  => $content->searchStatisticsUrl
			],
		];

		if ( ! $content->allowSearchStatistics() ) {
			$upsell['search-statistics'] = [
				'cta' => [
					'text' => esc_html__( 'Unlock Search Statistics', 'all-in-one-seo-pack' ),
					'url'  => $content->searchStatisticsUrl,
				],
			];
		}

		if ( ! $upsell['search-statistics'] ) {
			$subheading = 'weekly' === $this->frequency
				? esc_html__( 'Let\'s take a look at how your site has performed in search results this week.', 'all-in-one-seo-pack' )
				: esc_html__( 'Let\'s take a look at how your site has performed in search results this month.', 'all-in-one-seo-pack' );

			$statisticsReport['posts']      = $content->getPostsStatistics();
			$statisticsReport['keywords']   = $content->getKeywords();
			$statisticsReport['milestones'] = $content->getMilestones();
		}

		$mktUrl = trailingslashit( AIOSEO_MARKETING_URL );
		$medium = 'email-report-summary';

		$posts     = $content->getAioPosts();
		$resources = [
			'posts' => array_map( function ( $item ) use ( $medium, $content ) {
				return array_merge( $item, [
					'url'   => aioseo()->helpers->utmUrl( $item['url'], $medium ),
					'image' => [
						'url' => ! empty( $item['image']['sizes']['medium']['source_url'] )
							? $item['image']['sizes']['medium']['source_url']
							: $content->featuredImagePlaceholder
					]
				] );
			}, $content->getResources() ),
			'cta'   => [
				'text' => esc_html__( 'See All Resources', 'all-in-one-seo-pack' ),
				'url'  => aioseo()->helpers->utmUrl( 'https://aioseo.com/blog/', $medium ),
			],
		];
		$links     = [
			'disable'        => admin_url( 'admin.php?page=aioseo-settings&aioseo-scroll=aioseo-email-summary-row&aioseo-highlight=aioseo-email-summary-row&aioseo-tab=advanced' ),
			'update'         => admin_url( 'update-core.php' ),
			'marketing-site' => aioseo()->helpers->utmUrl( $mktUrl, $medium ),
			'facebook'       => aioseo()->helpers->utmUrl( $mktUrl . 'plugin/facebook', $medium ),
			'linkedin'       => aioseo()->helpers->utmUrl( $mktUrl . 'plugin/linkedin', $medium ),
			'youtube'        => aioseo()->helpers->utmUrl( $mktUrl . 'plugin/youtube', $medium ),
			'twitter'        => aioseo()->helpers->utmUrl( $mktUrl . 'plugin/twitter', $medium ),
		];

		ob_start();
		require AIOSEO_DIR . '/app/Common/Views/report/summary.php';

		return ob_get_clean();
	}

	/**
	 * Get email headers.
	 *
	 * @since 4.7.2
	 *
	 * @return array The email headers.
	 */
	private function getHeaders() {
		return [ 'Content-Type: text/html; charset=UTF-8' ];
	}

	/**
	 * Get all allowed frequencies.
	 *
	 * @since 4.7.2
	 *
	 * @return array The email allowed frequencies.
	 */
	private function getAllowedFrequencies() {
		$time           = time();
		$secondsTillNow = $time - strtotime( 'today' );

		return [
			'weekly'  => [
				'interval' => WEEK_IN_SECONDS,
				'start'    => strtotime( 'next Monday' ) - $time
			],
			'monthly' => [
				'interval' => MONTH_IN_SECONDS,
				'start'    => ( strtotime( 'first day of next month' ) + ( DAY_IN_SECONDS * 2 ) - $secondsTillNow ) - $time
			]
		];
	}

	/**
	 * Retrieves the date range data based on the frequency.
	 *
	 * @since 4.7.3
	 *
	 * @return array The date range data.
	 */
	private function getDateRange() {
		$dateFormat = get_option( 'date_format' );

		// If frequency is 'monthly'.
		$endDateRaw   = strtotime( 'last day of last month' );
		$startDateRaw = strtotime( 'first day of last month' );

		// If frequency is 'weekly'.
		if ( 'weekly' === $this->frequency ) {
			$endDateRaw   = strtotime( 'last Saturday' );
			$startDateRaw = strtotime( 'last Sunday', $endDateRaw );
		}

		$endDate   = date_i18n( $dateFormat, $endDateRaw );
		$startDate = date_i18n( $dateFormat, $startDateRaw );

		return [
			'endDate'      => $endDate,
			'endDateRaw'   => $endDateRaw,
			'startDate'    => $startDate,
			'startDateRaw' => $startDateRaw,
			'range'        => "$startDate - $endDate",
		];
	}
}Common/Help/Help.php000064400000003001151536241170010264 0ustar00<?php
namespace AIOSEO\Plugin\Common\Help;

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

class Help {
	/**
	 * Source of the documentation content.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $url = 'https://cdn.aioseo.com/wp-content/docs.json';

	/**
	 * Settings.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $settings = [
		'docsUrl'          => 'https://aioseo.com/docs/',
		'supportTicketUrl' => 'https://aioseo.com/account/support/',
		'upgradeUrl'       => 'https://aioseo.com/pricing/'
	];

	/**
	 * Gets the URL for the notifications api.
	 *
	 * @since 4.0.0
	 *
	 * @return string The URL to use for the api requests.
	 */
	private function getUrl() {
		if ( defined( 'AIOSEO_DOCS_FEED_URL' ) ) {
			return AIOSEO_DOCS_FEED_URL;
		}

		return $this->url;
	}

	/**
	 * Returns the help docs for our menus.
	 *
	 * @since 4.0.0
	 *
	 * @return array The help docs.
	 */
	public function getDocs() {
		$helpDocs = aioseo()->core->networkCache->get( 'admin_help_docs' );
		if ( null !== $helpDocs ) {
			if ( is_array( $helpDocs ) ) {
				return $helpDocs;
			}

			return json_decode( $helpDocs, true );
		}

		$request = aioseo()->helpers->wpRemoteGet( $this->getUrl() );
		if ( is_wp_error( $request ) ) {
			aioseo()->core->networkCache->update( 'admin_help_docs', [], DAY_IN_SECONDS );
		}

		$helpDocs = wp_remote_retrieve_body( $request );

		aioseo()->core->networkCache->update( 'admin_help_docs', $helpDocs, WEEK_IN_SECONDS );

		return json_decode( $helpDocs, true );
	}
}Common/ImportExport/Helpers.php000064400000004430151536241170012571 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport;

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

/**
 * Contains helper methods for the import from other plugins.
 *
 * @since 4.0.0
 */
abstract class Helpers {
	/**
	 * Converts macros to smart tags.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $value The string with macros.
	 * @return string        The string with macros converted.
	 */
	abstract public function macrosToSmartTags( $value );

	/**
	 * Maps a list of old settings from V3 to their counterparts in V4.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $mappings      The old settings, mapped to their new settings.
	 * @param  array $group         The old settings group.
	 * @param  bool  $convertMacros Whether to convert the old V3 macros to V4 smart tags.
	 * @return void
	 */
	public function mapOldToNew( $mappings, $group, $convertMacros = false ) {
		if (
			! is_array( $mappings ) ||
			! is_array( $group ) ||
			! count( $mappings ) ||
			! count( $group )
		) {
			return;
		}

		$mainOptions    = aioseo()->options->noConflict();
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		foreach ( $mappings as $name => $values ) {
			if ( ! isset( $group[ $name ] ) ) {
				continue;
			}

			$error      = false;
			$options    = ! empty( $values['dynamic'] ) ? $dynamicOptions : $mainOptions;
			$lastOption = '';
			for ( $i = 0; $i < count( $values['newOption'] ); $i++ ) {
				$lastOption = $values['newOption'][ $i ];
				if ( ! $options->has( $lastOption, false ) ) {
					$error = true;
					break;
				}

				if ( count( $values['newOption'] ) - 1 !== $i ) {
					$options = $options->$lastOption;
				}
			}

			if ( $error ) {
				continue;
			}

			switch ( $values['type'] ) {
				case 'boolean':
					if ( ! empty( $group[ $name ] ) ) {
						$options->$lastOption = true;
						break;
					}
					$options->$lastOption = false;
					break;
				case 'integer':
				case 'float':
					$value = aioseo()->helpers->sanitizeOption( $group[ $name ] );
					if ( $value ) {
						$options->$lastOption = $value;
					}
					break;
				default:
					$value = $group[ $name ];
					if ( $convertMacros ) {
						$value = $this->macrosToSmartTags( $value );
					}
					$options->$lastOption = aioseo()->helpers->sanitizeOption( $value );
					break;
			}
		}
	}
}Common/ImportExport/ImportExport.php000064400000025310151536241170013643 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Handles the importing/exporting of settings and SEO data.
 *
 * @since 4.0.0
 */
class ImportExport {
	/**
	 * List of plugins for importing.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $plugins = [];

	/**
	 * YoastSeo class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var YoastSeo\YoastSeo
	 */
	public $yoastSeo = null;

	/**
	 * RankMath class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var RankMath\RankMath
	 */
	public $rankMath = null;

	/**
	 * SeoPress class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var SeoPress\SeoPress
	 */
	public $seoPress = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->yoastSeo = new YoastSeo\YoastSeo( $this );
		$this->rankMath = new RankMath\RankMath( $this );
		$this->seoPress = new SeoPress\SeoPress( $this );
	}

	/**
	 * Converts the content of a given V3 .ini settings file to an array of settings.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $contents The .ini file contents.
	 * @return array            The settings.
	 */
	public function importIniData( $contents ) {
		$lines = array_filter( preg_split( '/\r\n|\r|\n/', (string) $contents ) );

		$sections     = [];
		$sectionLabel = '';
		$sectionCount = 0;

		foreach ( $lines as $line ) {
			$line = trim( $line );
			// Ignore comments.
			if ( preg_match( '#^;.*#', (string) $line ) || preg_match( '#\<(\?php|script)#', (string) $line ) ) {
				continue;
			}

			$matches = [];
			if ( preg_match( '#^\[(\S+)\]$#', (string) $line, $label ) ) {
				$sectionLabel = strval( $label[1] );
				if ( 'post_data' === $sectionLabel ) {
					$sectionCount++;
				}
				if ( ! isset( $sections[ $sectionLabel ] ) ) {
					$sections[ $sectionLabel ] = [];
				}
			} elseif ( preg_match( "#^(\S+)\s*=\s*'(.*)'$#", (string) $line, $matches ) ) {
				if ( 'post_data' === $sectionLabel ) {
					$sections[ $sectionLabel ][ $sectionCount ][ $matches[1] ] = $matches[2];
				} else {
					$sections[ $sectionLabel ][ $matches[1] ] = $matches[2];
				}
			} elseif ( preg_match( '#^(\S+)\s*=\s*NULL$#', (string) $line, $matches ) ) {
				if ( 'post_data' === $sectionLabel ) {
					$sections[ $sectionLabel ][ $sectionCount ][ $matches[1] ] = '';
				} else {
					$sections[ $sectionLabel ][ $matches[1] ] = '';
				}
			} else {
				continue;
			}
		}

		$sanitizedSections = [];
		foreach ( $sections as $section => $options ) {
			$sanitizedSection = [];
			foreach ( $options as $option => $value ) {
				$sanitizedSection[ $option ] = $this->convertAndSanitize( $value );
			}
			$sanitizedSections[ $section ] = $sanitizedSection;
		}

		$oldOptions = [];
		$postData   = [];
		foreach ( $sanitizedSections as $label => $data ) {
			switch ( $label ) {
				case 'aioseop_options':
					$oldOptions = array_merge( $oldOptions, $data );
					break;
				case 'aiosp_feature_manager_options':
				case 'aiosp_opengraph_options':
				case 'aiosp_sitemap_options':
				case 'aiosp_video_sitemap_options':
				case 'aiosp_schema_local_business_options':
				case 'aiosp_image_seo_options':
				case 'aiosp_robots_options':
				case 'aiosp_bad_robots_options':
					$oldOptions['modules'][ $label ] = $data;
					break;
				case 'post_data':
					$postData = $data;
					break;
				default:
					break;
			}
		}

		if ( ! empty( $oldOptions ) ) {
			aioseo()->migration->migrateSettings( $oldOptions );
		}

		if ( ! empty( $postData ) ) {
			$this->importOldPostMeta( $postData );
		}

		return true;
	}

	/**
	 * Imports the post meta from V3.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $postData The post data.
	 * @return void
	 */
	private function importOldPostMeta( $postData ) {
		$mappedMeta = [
			'_aioseop_title'              => 'title',
			'_aioseop_description'        => 'description',
			'_aioseop_custom_link'        => 'canonical_url',
			'_aioseop_sitemap_exclude'    => '',
			'_aioseop_disable'            => '',
			'_aioseop_noindex'            => 'robots_noindex',
			'_aioseop_nofollow'           => 'robots_nofollow',
			'_aioseop_sitemap_priority'   => 'priority',
			'_aioseop_sitemap_frequency'  => 'frequency',
			'_aioseop_keywords'           => 'keywords',
			'_aioseop_opengraph_settings' => ''
		];

		$excludedPosts        = [];
		$sitemapExcludedPosts = [];

		require_once ABSPATH . 'wp-admin/includes/post.php';
		foreach ( $postData as $post => $values ) {
			$postId = \post_exists( $values['post_title'], '', $values['post_date'] );
			if ( ! $postId ) {
				continue;
			}

			$meta = [
				'post_id' => $postId,
			];

			foreach ( $values as $name => $value ) {
				if ( ! in_array( $name, array_keys( $mappedMeta ), true ) ) {
					continue;
				}

				switch ( $name ) {
					case '_aioseop_sitemap_exclude':
						if ( empty( $value ) ) {
							break;
						}
						$sitemapExcludedPosts[] = $postId;
						break;
					case '_aioseop_disable':
						if ( empty( $value ) ) {
							break;
						}
						$excludedPosts[] = $postId;
						break;
					case '_aioseop_noindex':
					case '_aioseop_nofollow':
						$meta[ $mappedMeta[ $name ] ] = ! empty( $value );
						if ( ! empty( $value ) ) {
							$meta['robots_default'] = false;
						}
						break;
					case '_aioseop_keywords':
						$meta[ $mappedMeta[ $name ] ] = aioseo()->migration->helpers->oldKeywordsToNewKeywords( $value );
						break;
					case '_aioseop_opengraph_settings':
						$class = new \AIOSEO\Plugin\Common\Migration\Meta();
						$meta += $class->convertOpenGraphMeta( $value );
						break;
					default:
						$meta[ $mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
						break;
				}
			}
			$post = Models\Post::getPost( $postId );
			$post->set( $meta );
			$post->save();
		}

		if ( count( $excludedPosts ) ) {
			$deprecatedOptions = aioseo()->internalOptions->internal->deprecatedOptions;
			if ( ! in_array( 'excludePosts', $deprecatedOptions, true ) ) {
				array_push( $deprecatedOptions, 'excludePosts' );
				aioseo()->internalOptions->internal->deprecatedOptions = $deprecatedOptions;
			}

			$posts = aioseo()->options->deprecated->searchAppearance->advanced->excludePosts;

			foreach ( $excludedPosts as $id ) {
				if ( ! intval( $id ) ) {
					continue;
				}
				$post = get_post( $id );
				if ( ! is_object( $post ) ) {
					continue;
				}
				$excludedPost        = new \stdClass();
				$excludedPost->type  = $post->post_type;
				$excludedPost->value = $post->ID;
				$excludedPost->label = $post->post_title;
				$excludedPost->link  = get_permalink( $post );

				$posts[] = wp_json_encode( $excludedPost );
			}
			aioseo()->options->deprecated->searchAppearance->advanced->excludePosts = $posts;
		}

		if ( count( $sitemapExcludedPosts ) ) {
			aioseo()->options->sitemap->general->advancedSettings->enable = true;

			$posts = aioseo()->options->sitemap->general->advancedSettings->excludePosts;
			foreach ( $sitemapExcludedPosts as $id ) {
				if ( ! intval( $id ) ) {
					continue;
				}
				$post = get_post( $id );
				if ( ! is_object( $post ) ) {
					continue;
				}
				$excludedPost        = new \stdClass();
				$excludedPost->type  = $post->post_type;
				$excludedPost->value = $post->ID;
				$excludedPost->label = $post->post_title;
				$excludedPost->link  = get_permalink( $post );

				$posts[] = wp_json_encode( $excludedPost );
			}
			aioseo()->options->sitemap->general->advancedSettings->excludePosts = $posts;
		}
	}

	/**
	 * Unserializes an option value if needed and then sanitizes it.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $value The option value.
	 * @return mixed         The sanitized, converted option value.
	 */
	private function convertAndSanitize( $value ) {
		$value = aioseo()->helpers->maybeUnserialize( $value );

		switch ( gettype( $value ) ) {
			case 'boolean':
				return (bool) $value;
			case 'string':
				return esc_html( wp_strip_all_tags( wp_check_invalid_utf8( trim( $value ) ) ) );
			case 'integer':
				return intval( $value );
			case 'double':
				return floatval( $value );
			case 'array':
				$sanitized = [];
				foreach ( (array) $value as $k => $v ) {
					$sanitized[ $k ] = $this->convertAndSanitize( $v );
				}

				return $sanitized;
			default:
				return '';
		}
	}

	/**
	 * Starts an import.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $plugin  The slug of the plugin to import.
	 * @param  array $settings Which settings to import.
	 * @return void
	 */
	public function startImport( $plugin, $settings ) {
		// First cancel any scans running that might interfere with our import.
		$this->cancelScans();

		foreach ( $this->plugins as $pluginData ) {
			if ( $pluginData['slug'] === $plugin ) {
				$pluginData['class']->doImport( $settings );

				return;
			}
		}
	}

	/**
	 * Cancel scans that are currently running and could conflict with our migration.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function cancelScans() {
		// Figure out how to check if these addons are enabled and then get the action names that way.
		aioseo()->actionScheduler->unschedule( 'aioseo_video_sitemap_scan' );
		aioseo()->actionScheduler->unschedule( 'aioseo_image_sitemap_scan' );
	}

	/**
	 * Checks if an import is currently running.
	 *
	 * @since 4.1.4
	 *
	 * @return boolean True if an import is currently running.
	 */
	public function isImportRunning() {
		$importsRunning = aioseo()->core->cache->get( 'import_%_meta_%' );

		return ! empty( $importsRunning );
	}

	/**
	 * Adds plugins to the import/export.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $plugins The plugins to add.
	 * @return void
	 */
	public function addPlugins( $plugins ) {
		$this->plugins = array_merge( $this->plugins, $plugins );
	}

	/**
	 * Get the plugins we allow importing from.
	 *
	 * @since 4.0.0
	 *
	 * @return array
	 */
	public function plugins() {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';
		$plugins          = [];
		$installedPlugins = array_keys( get_plugins() );
		foreach ( $this->plugins as $importerPlugin ) {
			$data = [
				'slug'      => $importerPlugin['slug'],
				'name'      => $importerPlugin['name'],
				'version'   => null,
				'canImport' => false,
				'basename'  => $importerPlugin['basename'],
				'installed' => false
			];

			if ( in_array( $importerPlugin['basename'], $installedPlugins, true ) ) {
				$pluginData = get_file_data( trailingslashit( WP_PLUGIN_DIR ) . $importerPlugin['basename'], [
					'name'    => 'Plugin Name',
					'version' => 'Version',
				] );

				$canImport = false;
				if ( version_compare( $importerPlugin['version'], $pluginData['version'], '<=' ) ) {
					$canImport = true;
				}

				$data['name']      = $pluginData['name'];
				$data['version']   = $pluginData['version'];
				$data['canImport'] = $canImport;
				$data['installed'] = true;
			}

			$plugins[] = $data;
		}

		return $plugins;
	}
}Common/ImportExport/Importer.php000064400000002675151536241170013001 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport;

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

/**
 * Imports the settings and meta data from other plugins.
 *
 * @since 4.0.0
 */
abstract class Importer {
	/**
	 * Imports the settings.
	 *
	 * @since 4.2.7
	 *
	 * @return void
	 */
	protected function importSettings() {}

	/**
	 * Imports the post meta.
	 *
	 * @since 4.2.7
	 *
	 * @return void
	 */
	protected function importPostMeta() {}

	/**
	 * Imports the term meta.
	 *
	 * @since 4.2.7
	 *
	 * @return void
	 */
	protected function importTermMeta() {}

	/**
	 * PostMeta class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Object
	 */
	protected $postMeta = null;

	/**
	 * TermMeta class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Object
	 */
	protected $termMeta = null;

	/**
	 * Helpers class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Object
	 */
	public $helpers = null;

	/**
	 * Starts the import.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $options What the user wants to import.
	 * @return void
	 */
	public function doImport( $options = [] ) {
		if ( empty( $options ) ) {
			$this->importSettings();
			$this->importPostMeta();
			$this->importTermMeta();

			return;
		}

		foreach ( $options as $optionName ) {
			switch ( $optionName ) {
				case 'settings':
					$this->importSettings();
					break;
				case 'postMeta':
					$this->postMeta->scheduleImport();
					break;
				default:
					break;
			}
		}
	}
}Common/ImportExport/RankMath/GeneralSettings.php000064400000005777151536241170016011 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\RankMath;

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

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the General Settings.
 *
 * @since 4.0.0
 */
class GeneralSettings {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->options = get_option( 'rank-math-options-general' );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->isTruSeoDisabled();
		$this->migrateRedirectAttachments();
		$this->migrateStripCategoryBase();
		$this->migrateRssContentSettings();

		$settings = [
			'google_verify'    => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'google' ] ],
			'bing_verify'      => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'bing' ] ],
			'yandex_verify'    => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'yandex' ] ],
			'baidu_verify'     => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'baidu' ] ],
			'pinterest_verify' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'pinterest' ] ],
		];

		aioseo()->importExport->rankMath->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Checks whether TruSEO should be disabled.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function isTruSeoDisabled() {
		if ( ! empty( $this->options['frontend_seo_score'] ) ) {
			aioseo()->options->advanced->truSeo = 'on' === $this->options['frontend_seo_score'];
		}
	}

	/**
	 * Migrates the Redirect Attachments setting.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateRedirectAttachments() {
		if ( isset( $this->options['attachment_redirect_urls'] ) ) {
			if ( 'on' === $this->options['attachment_redirect_urls'] ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'attachment_parent';
			} else {
				aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'disabled';
			}
		}
	}

	/**
	 * Migrates the Strip Category Base setting.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	private function migrateStripCategoryBase() {
		if ( isset( $this->options['strip_category_base'] ) ) {
			aioseo()->options->searchAppearance->advanced->removeCategoryBase = 'on' === $this->options['strip_category_base'] ? true : false;
		}
	}

	/**
	 * Migrates the RSS content settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateRssContentSettings() {
		if ( isset( $this->options['rss_before_content'] ) ) {
			aioseo()->options->rssContent->before = esc_html( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options['rss_before_content'] ) );
		}

		if ( isset( $this->options['rss_after_content'] ) ) {
			aioseo()->options->rssContent->after = esc_html( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options['rss_after_content'] ) );
		}
	}
}Common/ImportExport/RankMath/Helpers.php000064400000007433151536241170014304 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\RankMath;

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

use AIOSEO\Plugin\Common\ImportExport;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Contains helper methods for the import from Rank Math.
 *
 * @since 4.0.0
 */
class Helpers extends ImportExport\Helpers {
	/**
	 * Converts the macros from Rank Math to our own smart tags.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string   The string with macros.
	 * @param  string $pageType The page type.
	 * @return string $string   The string with smart tags.
	 */
	public function macrosToSmartTags( $string, $pageType = null ) {
		$macros = $this->getMacros( $pageType );

		if ( preg_match( '#%BLOGDESCLINK%#', (string) $string ) ) {
			$blogDescriptionLink = '<a href="' .
				aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'url' ) ) . '">' .
				aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) ) . ' - ' .
				aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'description' ) ) . '</a>';

			$string = str_replace( '%BLOGDESCLINK%', $blogDescriptionLink, $string );
		}

		if ( preg_match_all( '#%customfield\(([^%\s]*)\)%#', (string) $string, $matches ) && ! empty( $matches[1] ) ) {
			foreach ( $matches[1] as $name ) {
				$string = aioseo()->helpers->pregReplace( "#%customfield\($name\)%#", "#custom_field-$name", $string );
			}
		}

		if ( preg_match_all( '#%customterm\(([^%\s]*)\)%#', (string) $string, $matches ) && ! empty( $matches[1] ) ) {
			foreach ( $matches[1] as $name ) {
				$string = aioseo()->helpers->pregReplace( "#%customterm\($name\)%#", "#tax_name-$name", $string );
			}
		}

		foreach ( $macros as $macro => $tag ) {
			$string = aioseo()->helpers->pregReplace( "#$macro(?![a-zA-Z0-9_])#im", $tag, $string );
		}

		// Strip out all remaining tags.
		$string = aioseo()->helpers->pregReplace( '/%[^\%\s]*\([^\%]*\)%/i', '', aioseo()->helpers->pregReplace( '/%[^\%\s]*%/i', '', $string ) );

		return trim( $string );
	}

	/**
	 * Returns the macro mappings.
	 *
	 * @since 4.1.1
	 *
	 * @param  string $pageType The page type.
	 * @return array  $macros   The macros.
	 */
	protected function getMacros( $pageType = null ) {
		$macros = [
			'%sitename%'         => '#site_title',
			'%blog_title%'       => '#site_title',
			'%blog_description%' => '#tagline',
			'%sitedesc%'         => '#tagline',
			'%sep%'              => '#separator_sa',
			'%post_title%'       => '#post_title',
			'%page_title%'       => '#post_title',
			'%postname%'         => '#post_title',
			'%title%'            => '#post_title',
			'%seo_title%'        => '#post_title',
			'%excerpt%'          => '#post_excerpt',
			'%wc_shortdesc%'     => '#post_excerpt',
			'%category%'         => '#taxonomy_title',
			'%term%'             => '#taxonomy_title',
			'%term_description%' => '#taxonomy_description',
			'%currentdate%'      => '#current_date',
			'%currentday%'       => '#current_day',
			'%currentyear%'      => '#current_year',
			'%currentmonth%'     => '#current_month',
			'%name%'             => '#author_first_name #author_last_name',
			'%author%'           => '#author_first_name #author_last_name',
			'%date%'             => '#post_date',
			'%year%'             => '#current_year',
			'%search_query%'     => '#search_term',
			// RSS Content tags.
			'%AUTHORLINK%'       => '#author_link',
			'%POSTLINK%'         => '#post_link',
			'%BLOGLINK%'         => '#site_link',
			'%FEATUREDIMAGE%'    => '#featured_image'
		];

		switch ( $pageType ) {
			case 'archive':
				$macros['%title%'] = '#archive_title';
				break;
			case 'term':
				$macros['%title%'] = '#taxonomy_title';
				break;
			default:
				$macros['%title%'] = '#post_title';
				break;
		}

		// Strip all other tags.
		$macros['%[^%]*%'] = '';

		return $macros;
	}
}Common/ImportExport/RankMath/PostMeta.php000064400000022432151536241170014432 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\RankMath;

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

use AIOSEO\Plugin\Common\Models;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Imports the post meta from Rank Math.
 *
 * @since 4.0.0
 */
class PostMeta {
	/**
	 * The batch import action name.
	 *
	 * @since 4.0.0
	 * @version 4.8.3 Moved from RankMath class to here.
	 *
	 * @var string
	 */
	public $postActionName = 'aioseo_import_post_meta_rank_math';

	/**
	 * The mapped meta
	 *
	 * @since 4.8.3
	 *
	 * @var array
	 */
	private $mappedMeta = [
		'rank_math_title'                => 'title',
		'rank_math_description'          => 'description',
		'rank_math_canonical_url'        => 'canonical_url',
		'rank_math_focus_keyword'        => 'keyphrases',
		'rank_math_robots'               => '',
		'rank_math_advanced_robots'      => '',
		'rank_math_facebook_title'       => 'og_title',
		'rank_math_facebook_description' => 'og_description',
		'rank_math_facebook_image'       => 'og_image_custom_url',
		'rank_math_twitter_use_facebook' => 'twitter_use_og',
		'rank_math_twitter_title'        => 'twitter_title',
		'rank_math_twitter_description'  => 'twitter_description',
		'rank_math_twitter_image'        => 'twitter_image_custom_url',
		'rank_math_twitter_card_type'    => 'twitter_card',
		'rank_math_primary_category'     => 'primary_term',
		'rank_math_pillar_content'       => 'pillar_content',
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.8.3
	 */
	public function __construct() {
		add_action( $this->postActionName, [ $this, 'importPostMeta' ] );
	}

	/**
	 * Schedules the post meta import.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function scheduleImport() {
		try {
			if ( as_next_scheduled_action( $this->postActionName ) ) {
				return;
			}

			if ( ! aioseo()->core->cache->get( 'import_post_meta_rank_math' ) ) {
				aioseo()->core->cache->update( 'import_post_meta_rank_math', time(), WEEK_IN_SECONDS );
			}

			as_schedule_single_action( time(), $this->postActionName, [], 'aioseo' );
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Get all posts to be imported
	 *
	 * @since 4.8.3
	 *
	 * @param  int   $postsPerAction The number of posts to import per action.
	 * @return array                 The posts to be imported.
	 */
	protected function getPostsToImport( $postsPerAction = 100 ) {
		$publicPostTypes = implode( "', '", aioseo()->helpers->getPublicPostTypes( true ) );
		$timeStarted     = gmdate( 'Y-m-d H:i:s', aioseo()->core->cache->get( 'import_post_meta_rank_math' ) );

		$posts = aioseo()->core->db
			->start( 'posts' . ' as p' )
			->select( 'p.ID, p.post_type' )
			->join( 'postmeta as pm', '`p`.`ID` = `pm`.`post_id`' )
			->leftJoin( 'aioseo_posts as ap', '`p`.`ID` = `ap`.`post_id`' )
			->whereRaw( "pm.meta_key LIKE 'rank_math_%'" )
			->whereRaw( "( p.post_type IN ( '$publicPostTypes' ) )" )
			->whereRaw( "( ap.post_id IS NULL OR ap.updated < '$timeStarted' )" )
			->orderBy( 'p.ID DESC' )
			->groupBy( 'p.ID' )
			->limit( $postsPerAction )
			->run()
			->result();

		return $posts;
	}

	/**
	 * Imports the post meta.
	 *
	 * @since 4.0.0
	 *
	 * @return array The posts that were imported.
	 */
	public function importPostMeta() {
		$postsPerAction = apply_filters( 'aioseo_import_rank_math_posts_per_action', 100 );
		$posts          = $this->getPostsToImport( $postsPerAction );
		if ( ! $posts || ! count( $posts ) ) {
			aioseo()->core->cache->delete( 'import_post_meta_rank_math' );

			return [];
		}

		foreach ( $posts as $post ) {
			$postMeta = aioseo()->core->db
				->start( 'postmeta' . ' as pm' )
				->select( 'pm.meta_key, pm.meta_value' )
				->where( 'pm.post_id', $post->ID )
				->whereRaw( "`pm`.`meta_key` LIKE 'rank_math_%'" )
				->run()
				->result();

			$meta = array_merge( [
				'post_id' => (int) $post->ID,
			], $this->getMetaData( $postMeta, $post ) );

			$aioseoPost = Models\Post::getPost( $post->ID );
			$aioseoPost->set( $meta );
			$aioseoPost->save();

			aioseo()->migration->meta->migrateAdditionalPostMeta( $post->ID );
		}

		// Clear the Overview cache.
		aioseo()->postSettings->clearPostTypeOverviewCache( $posts[0]->ID );

		if ( count( $posts ) === $postsPerAction ) {
			try {
				as_schedule_single_action( time() + 5, $this->postActionName, [], 'aioseo' );
			} catch ( \Exception $e ) {
				// Do nothing.
			}
		} else {
			aioseo()->core->cache->delete( 'import_post_meta_rank_math' );
		}

		return $posts;
	}

	/**
	 * Get the meta data by post meta.
	 *
	 * @since 4.8.3
	 *
	 * @param object $postMeta The post meta from database.
	 * @param object $post     The post object.
	 * @return array           The meta data.
	 */
	public function getMetaData( $postMeta, $post ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$meta = [
			'post_id'             => $post->ID,
			'robots_default'      => true,
			'robots_noarchive'    => false,
			'canonical_url'       => '',
			'robots_nofollow'     => false,
			'robots_noimageindex' => false,
			'robots_noindex'      => false,
			'robots_noodp'        => false,
			'robots_nosnippet'    => false,
			'keyphrases'          => [
				'focus'      => [ 'keyphrase' => '' ],
				'additional' => []
			],
		];
		foreach ( $postMeta as $record ) {
			$name  = $record->meta_key;
			$value = $record->meta_value;

			if (
				! in_array( $post->post_type, [ 'page', 'attachment' ], true ) &&
				preg_match( '#^rank_math_schema_([^\s]*)$#', (string) $name, $match ) && ! empty( $match[1] )
			) {
				switch ( $match[1] ) {
					case 'Article':
					case 'NewsArticle':
					case 'BlogPosting':
						$meta['schema_type'] = 'Article';
						$meta['schema_type_options'] = wp_json_encode(
							[ 'article' => [ 'articleType' => $match[1] ] ]
						);
						break;
					default:
						break;
				}
			}

			if ( ! in_array( $name, array_keys( $this->mappedMeta ), true ) ) {
				continue;
			}

			switch ( $name ) {
				case 'rank_math_focus_keyword':
					$keyphrases     = array_map( 'trim', explode( ',', $value ) );
					$keyphraseArray = [
						'focus'      => [ 'keyphrase' => aioseo()->helpers->sanitizeOption( $keyphrases[0] ) ],
						'additional' => []
					];
					unset( $keyphrases[0] );
					foreach ( $keyphrases as $keyphrase ) {
						$keyphraseArray['additional'][] = [ 'keyphrase' => aioseo()->helpers->sanitizeOption( $keyphrase ) ];
					}

					$meta['keyphrases'] = $keyphraseArray;
					break;
				case 'rank_math_robots':
					$value = aioseo()->helpers->maybeUnserialize( $value );
					if ( ! empty( $value ) ) {
						$supportedValues        = [ 'index', 'noindex', 'nofollow', 'noarchive', 'noimageindex', 'nosnippet' ];
						$meta['robots_default'] = false;

						foreach ( $supportedValues as $val ) {
							$meta[ "robots_$val" ] = false;
						}

						// This is a separated foreach as we can import any and all values.
						foreach ( $value as $robotsName ) {
							$meta[ "robots_$robotsName" ] = true;
						}
					}
					break;
				case 'rank_math_advanced_robots':
					$value = aioseo()->helpers->maybeUnserialize( $value );
					if ( isset( $value['max-snippet'] ) && is_numeric( $value['max-snippet'] ) ) {
						$meta['robots_default']     = false;
						$meta['robots_max_snippet'] = intval( $value['max-snippet'] );
					}
					if ( isset( $value['max-video-preview'] ) && is_numeric( $value['max-video-preview'] ) ) {
						$meta['robots_default']          = false;
						$meta['robots_max_videopreview'] = intval( $value['max-video-preview'] );
					}
					if ( ! empty( $value['max-image-preview'] ) ) {
						$meta['robots_default']          = false;
						$meta['robots_max_imagepreview'] = aioseo()->helpers->sanitizeOption( lcfirst( $value['max-image-preview'] ) );
					}
					break;
				case 'rank_math_facebook_image':
					$meta['og_image_type']        = 'custom_image';
					$meta[ $this->mappedMeta[ $name ] ] = esc_url( $value );
					break;
				case 'rank_math_twitter_image':
					$meta['twitter_image_type']   = 'custom_image';
					$meta[ $this->mappedMeta[ $name ] ] = esc_url( $value );
					break;
				case 'rank_math_twitter_card_type':
					preg_match( '#large#', (string) $value, $match );
					$meta[ $this->mappedMeta[ $name ] ] = ! empty( $match ) ? 'summary_large_image' : 'summary';
					break;
				case 'rank_math_twitter_use_facebook':
					$meta[ $this->mappedMeta[ $name ] ] = 'on' === $value;
					break;
				case 'rank_math_primary_category':
					$taxonomy                     = 'category';
					$options                      = new \stdClass();
					$options->$taxonomy           = (int) $value;
					$meta[ $this->mappedMeta[ $name ] ] = wp_json_encode( $options );
					break;
				case 'rank_math_title':
				case 'rank_math_description':
					if ( 'page' === $post->post_type ) {
						$value = aioseo()->helpers->pregReplace( '#%category%#', '', $value );
						$value = aioseo()->helpers->pregReplace( '#%excerpt%#', '', $value );
					}
					$value = aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $value );

					$meta[ $this->mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
					break;
				case 'rank_math_pillar_content':
					$meta['pillar_content'] = 'on' === $value ? 1 : 0;
					break;
				default:
					$meta[ $this->mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
					break;
			}
		}

		return $meta;
	}
}Common/ImportExport/RankMath/RankMath.php000064400000002063151536241170014401 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\RankMath;

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

use AIOSEO\Plugin\Common\ImportExport;

class RankMath extends ImportExport\Importer {
	/**
	 * A list of plugins to look for to import.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $plugins = [
		[
			'name'     => 'Rank Math SEO',
			'version'  => '1.0',
			'basename' => 'seo-by-rank-math/rank-math.php',
			'slug'     => 'rank-math-seo'
		]
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 *
	 * @param ImportExport\ImportExport $importer the ImportExport class.
	 */
	public function __construct( $importer ) {
		$this->helpers  = new Helpers();
		$this->postMeta = new PostMeta();

		$plugins = $this->plugins;
		foreach ( $plugins as $key => $plugin ) {
			$plugins[ $key ]['class'] = $this;
		}
		$importer->addPlugins( $plugins );
	}


	/**
	 * Imports the settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function importSettings() {
		new GeneralSettings();
		new TitleMeta();
		new Sitemap();
	}
}Common/ImportExport/RankMath/Sitemap.php000064400000011643151536241170014302 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\RankMath;

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

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the sitemap settings.
 *
 * @since 4.0.0
 */
class Sitemap {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->options = get_option( 'rank-math-options-sitemap' );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->migrateIncludedObjects();
		$this->migrateIncludeImages();
		$this->migrateExcludedPosts();
		$this->migrateExcludedTerms();

		$settings = [
			'items_per_page' => [ 'type' => 'string', 'newOption' => [ 'sitemap', 'general', 'linksPerIndex' ] ],
		];

		aioseo()->options->sitemap->general->indexes = true;
		aioseo()->importExport->rankMath->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the included post types and taxonomies.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateIncludedObjects() {
		$includedPostTypes  = [];
		$includedTaxonomies = [];

		$allowedPostTypes = array_values( array_diff( aioseo()->helpers->getPublicPostTypes( true ), aioseo()->helpers->getNoindexedPostTypes() ) );
		foreach ( $allowedPostTypes as $postType ) {
			foreach ( $this->options as $name => $value ) {
				if ( preg_match( "#pt_{$postType}_sitemap$#", (string) $name, $match ) && 'on' === $this->options[ $name ] ) {
					$includedPostTypes[] = $postType;
				}
			}
		}

		$allowedTaxonomies = array_values( array_diff( aioseo()->helpers->getPublicTaxonomies( true ), aioseo()->helpers->getNoindexedTaxonomies() ) );
		foreach ( $allowedTaxonomies as $taxonomy ) {
			foreach ( $this->options as $name => $value ) {
				if ( preg_match( "#tax_{$taxonomy}_sitemap$#", (string) $name, $match ) && 'on' === $this->options[ $name ] ) {
					$includedTaxonomies[] = $taxonomy;
				}
			}
		}

		aioseo()->options->sitemap->general->postTypes->included = $includedPostTypes;
		if ( count( $allowedPostTypes ) !== count( $includedPostTypes ) ) {
			aioseo()->options->sitemap->general->postTypes->all = false;
		}

		aioseo()->options->sitemap->general->taxonomies->included = $includedTaxonomies;
		if ( count( $allowedTaxonomies ) !== count( $includedTaxonomies ) ) {
			aioseo()->options->sitemap->general->taxonomies->all = false;
		}
	}

	/**
	 * Migrates the Redirect Attachments setting.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateIncludeImages() {
		if ( ! empty( $this->options['include_images'] ) ) {
			if ( 'off' === $this->options['include_images'] ) {
				aioseo()->options->sitemap->general->advancedSettings->enable        = true;
				aioseo()->options->sitemap->general->advancedSettings->excludeImages = true;
			}
		}
	}

	/**
	 * Migrates the posts that are excluded from the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateExcludedPosts() {
		if ( empty( $this->options['exclude_posts'] ) ) {
			return;
		}

		$rmExcludedPosts = array_filter( explode( ',', $this->options['exclude_posts'] ) );
		$excludedPosts   = aioseo()->options->sitemap->general->advancedSettings->excludePosts;

		if ( count( $rmExcludedPosts ) ) {
			foreach ( $rmExcludedPosts as $rmExcludedPost ) {
				$post = get_post( trim( $rmExcludedPost ) );
				if ( ! is_object( $post ) ) {
					continue;
				}

				$excludedPost        = new \stdClass();
				$excludedPost->value = $post->ID;
				$excludedPost->type  = $post->post_type;
				$excludedPost->label = $post->post_title;
				$excludedPost->link  = get_permalink( $post->ID );

				array_push( $excludedPosts, wp_json_encode( $excludedPost ) );
			}
			aioseo()->options->sitemap->general->advancedSettings->enable = true;
		}
		aioseo()->options->sitemap->general->advancedSettings->excludePosts = $excludedPosts;
	}

	/**
	 * Migrates the terms that are excluded from the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateExcludedTerms() {
		if ( empty( $this->options['exclude_terms'] ) ) {
			return;
		}

		$rmExcludedTerms = array_filter( explode( ',', $this->options['exclude_terms'] ) );
		$excludedTerms   = aioseo()->options->sitemap->general->advancedSettings->excludeTerms;

		if ( count( $rmExcludedTerms ) ) {
			foreach ( $rmExcludedTerms as $rmExcludedTerm ) {
				$term = get_term( trim( $rmExcludedTerm ) );
				if ( ! is_object( $term ) ) {
					continue;
				}

				$excludedTerm        = new \stdClass();
				$excludedTerm->value = $term->term_id;
				$excludedTerm->type  = $term->taxonomy;
				$excludedTerm->label = $term->name;
				$excludedTerm->link  = get_term_link( $term );

				array_push( $excludedTerms, wp_json_encode( $excludedTerm ) );
			}
			aioseo()->options->sitemap->general->advancedSettings->enable = true;
		}
		aioseo()->options->sitemap->general->advancedSettings->excludeTerms = $excludedTerms;
	}
}Common/ImportExport/RankMath/TitleMeta.php000064400000047334151536241170014576 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\RankMath;

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

use AIOSEO\Plugin\Common\ImportExport;
use AIOSEO\Plugin\Common\Models;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Search Appearance settings.
 *
 * @since 4.0.0
 */
class TitleMeta extends ImportExport\SearchAppearance {
	/**
	 * Our robot meta settings.
	 *
	 * @since 4.0.0
	 */
	private $robotMetaSettings = [
		'noindex',
		'nofollow',
		'noarchive',
		'noimageindex',
		'nosnippet'
	];

	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->options = get_option( 'rank-math-options-titles' );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->migrateHomePageSettings();
		$this->migratePostTypeSettings();
		$this->migratePostTypeArchiveSettings();
		$this->migrateArchiveSettings();
		$this->migrateRobotMetaSettings();
		$this->migrateKnowledgeGraphSettings();
		$this->migrateSocialMetaSettings();

		$settings = [
			'title_separator' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'separator' ] ],
		];

		aioseo()->importExport->rankMath->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the homepage settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateHomePageSettings() {
		if ( isset( $this->options['homepage_title'] ) ) {
			aioseo()->options->searchAppearance->global->siteTitle =
				aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options['homepage_title'] ) );
		}

		if ( isset( $this->options['homepage_description'] ) ) {
			aioseo()->options->searchAppearance->global->metaDescription =
				aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options['homepage_description'] ) );
		}

		if ( isset( $this->options['homepage_facebook_title'] ) ) {
			aioseo()->options->social->facebook->homePage->title = aioseo()->helpers->sanitizeOption( $this->options['homepage_facebook_title'] );
		}

		if ( isset( $this->options['homepage_facebook_description'] ) ) {
			aioseo()->options->social->facebook->homePage->description = aioseo()->helpers->sanitizeOption( $this->options['homepage_facebook_description'] );
		}

		if ( isset( $this->options['homepage_facebook_image'] ) ) {
			aioseo()->options->social->facebook->homePage->image = esc_url( $this->options['homepage_facebook_image'] );
		}
	}

	/**
	 * Migrates the archive settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateArchiveSettings() {
		$archives = [
			'author',
			'date'
		];

		foreach ( $archives as $archive ) {
			// Reset existing values first.
			foreach ( $this->robotMetaSettings as $robotsMetaName ) {
				aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->$robotsMetaName = false;
			}

			if ( isset( $this->options[ "disable_{$archive}_archives" ] ) ) {
				aioseo()->options->searchAppearance->archives->$archive->show                          = 'off' === $this->options[ "disable_{$archive}_archives" ];
				aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->default = 'on' === $this->options[ "disable_{$archive}_archives" ];
				aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->noindex = 'on' === $this->options[ "disable_{$archive}_archives" ];
			}

			if ( isset( $this->options[ "{$archive}_archive_title" ] ) ) {
				$value = aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options[ "{$archive}_archive_title" ], 'archive' ) );
				if ( 'date' !== $archive ) {
					// Archive Title tag needs to be stripped since we don't support it for author archives.
					$value = aioseo()->helpers->pregReplace( '/#archive_title/', '', $value );
				}
				aioseo()->options->searchAppearance->archives->$archive->title = $value;
			}

			if ( isset( $this->options[ "{$archive}_archive_description" ] ) ) {
				aioseo()->options->searchAppearance->archives->$archive->metaDescription =
					aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options[ "{$archive}_archive_description" ], 'archive' ) );
			}

			if ( ! empty( $this->options[ "{$archive}_custom_robots" ] ) ) {
				aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->default = 'off' === $this->options[ "{$archive}_custom_robots" ];
			}

			if ( ! empty( $this->options[ "{$archive}_robots" ] ) ) {
				foreach ( $this->options[ "{$archive}_robots" ] as $robotsName ) {
					if ( 'index' === $robotsName ) {
						continue;
					}

					if ( 'noindex' === $robotsName ) {
						aioseo()->options->searchAppearance->archives->{$archive}->show = false;
					}

					aioseo()->options->searchAppearance->archives->{$archive}->advanced->robotsMeta->{$robotsName} = true;
				}
			}

			if ( ! empty( $this->options[ "{$archive}_advanced_robots" ] ) ) {
				if ( isset( $this->options[ "{$archive}_advanced_robots" ]['max-snippet'] ) && is_numeric( $this->options[ "{$archive}_advanced_robots" ]['max-snippet'] ) ) {
					aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->maxSnippet = intval( $this->options[ "{$archive}_advanced_robots" ]['max-snippet'] );
				}
				if ( isset( $this->options[ "{$archive}_advanced_robots" ]['max-video-preview'] ) && is_numeric( isset( $this->options[ "{$archive}_advanced_robots" ]['max-video-preview'] ) ) ) {
					aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->maxVideoPreview = intval( $this->options[ "{$archive}_advanced_robots" ]['max-video-preview'] );
				}
				if ( ! empty( $this->options[ "{$archive}_advanced_robots" ]['max-image-preview'] ) ) {
					aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->maxImagePreview =
						aioseo()->helpers->sanitizeOption( lcfirst( $this->options[ "{$archive}_advanced_robots" ]['max-image-preview'] ) );
				}
			}
		}

		if ( isset( $this->options['search_title'] ) ) {
			// Archive Title tag needs to be stripped since we don't support it for search archives.
			$value = aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options['search_title'], 'archive' ) );
			aioseo()->options->searchAppearance->archives->search->title = aioseo()->helpers->pregReplace( '/#archive_title/', '', $value );
		}

		if ( ! empty( $this->options['noindex_search'] ) ) {
			aioseo()->options->searchAppearance->archives->search->show                          = 'off' === $this->options['noindex_search'];
			aioseo()->options->searchAppearance->archives->search->advanced->robotsMeta->default = 'on' === $this->options['noindex_search'];
			aioseo()->options->searchAppearance->archives->search->advanced->robotsMeta->noindex = 'on' === $this->options['noindex_search'];
		}
	}

	/**
	 * Migrates the post type settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migratePostTypeSettings() {
		$supportedSettings = [
			'title',
			'description',
			'custom_robots',
			'robots',
			'advanced_robots',
			'default_rich_snippet',
			'default_article_type',
			'add_meta_box'
		];

		foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
			// Reset existing values first.
			foreach ( $this->robotMetaSettings as $robotsMetaName ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->$robotsMetaName = false;
			}

			foreach ( $this->options as $name => $value ) {
				if ( ! preg_match( "#^pt_{$postType}_(.*)$#", (string) $name, $match ) || ! in_array( $match[1], $supportedSettings, true ) ) {
					continue;
				}

				switch ( $match[1] ) {
					case 'title':
						if ( 'page' === $postType ) {
							$value = aioseo()->helpers->pregReplace( '#%category%#', '', $value );
							$value = aioseo()->helpers->pregReplace( '#%excerpt%#', '', $value );
						}
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->title =
							aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $value ) );
						break;
					case 'description':
						if ( 'page' === $postType ) {
							$value = aioseo()->helpers->pregReplace( '#%category%#', '', $value );
							$value = aioseo()->helpers->pregReplace( '#%excerpt%#', '', $value );
						}
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->metaDescription =
							aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $value ) );
						break;
					case 'custom_robots':
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = 'off' === $value;
						break;
					case 'robots':
						if ( ! empty( $value ) ) {
							foreach ( $value as $robotsName ) {
								if ( 'index' === $robotsName ) {
									continue;
								}

								if ( 'noindex' === $robotsName ) {
									aioseo()->dynamicOptions->searchAppearance->postTypes->{$postType}->show = false;
								}

								aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->$robotsName = true;
							}
						}
						break;
					case 'advanced_robots':
						if ( isset( $value['max-snippet'] ) && is_numeric( $value['max-snippet'] ) ) {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->maxSnippet = intval( $value['max-snippet'] );
						}
						if ( isset( $value['max-video-preview'] ) && is_numeric( $value['max-video-preview'] ) ) {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->maxVideoPreview = intval( $value['max-video-preview'] );
						}
						if ( ! empty( $value['max-image-preview'] ) ) {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->maxImagePreview =
								aioseo()->helpers->sanitizeOption( $value['max-image-preview'] );
						}
						break;
					case 'add_meta_box':
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->showMetaBox = 'on' === $value;
						break;
					case 'default_rich_snippet':
						$value = aioseo()->helpers->pregReplace( '#\s#', '', $value );
						if ( 'off' === lcfirst( $value ) || in_array( $postType, [ 'page', 'attachment' ], true ) ) {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->schemaType = 'none';
							break;
						}
						if ( in_array( ucfirst( $value ), ImportExport\SearchAppearance::$supportedSchemaGraphs, true ) ) {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->schemaType = ucfirst( $value );
						}
						break;
					case 'default_article_type':
						if ( in_array( $postType, [ 'page', 'attachment' ], true ) ) {
							break;
						}
						$value = aioseo()->helpers->pregReplace( '#\s#', '', $value );
						if ( in_array( ucfirst( $value ), ImportExport\SearchAppearance::$supportedArticleGraphs, true ) ) {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->articleType = ucfirst( $value );
						} else {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->articleType = 'BlogPosting';
						}
						break;
					default:
						break;
				}
			}
		}
	}

	/**
	 * Migrates the post type archive settings.
	 *
	 * @since 4.0.16
	 *
	 * @return void
	 */
	private function migratePostTypeArchiveSettings() {
		$supportedSettings = [
			'title',
			'description'
		];

		foreach ( aioseo()->helpers->getPublicPostTypes( true, true ) as $postType ) {
			foreach ( $this->options as $name => $value ) {
				if ( ! preg_match( "#^pt_{$postType}_archive_(.*)$#", (string) $name, $match ) || ! in_array( $match[1], $supportedSettings, true ) ) {
					continue;
				}

				switch ( $match[1] ) {
					case 'title':
						aioseo()->dynamicOptions->searchAppearance->archives->$postType->title =
							aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $value, 'archive' ) );
						break;
					case 'description':
						aioseo()->dynamicOptions->searchAppearance->archives->$postType->metaDescription =
							aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $value, 'archive' ) );
						break;
					default:
						break;
				}
			}
		}
	}


	/**
	 * Migrates the robots meta settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateRobotMetaSettings() {
		// Reset existing values first.
		foreach ( $this->robotMetaSettings as $robotsMetaName ) {
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->$robotsMetaName = false;
		}

		if ( ! empty( $this->options['robots_global'] ) ) {
			foreach ( $this->options['robots_global'] as $robotsName ) {
				if ( 'index' === $robotsName ) {
					continue;
				}
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default     = false;
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->$robotsName = true;
			}
		}

		if ( ! empty( $this->options['advanced_robots_global'] ) ) {
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default = false;

			if ( isset( $this->options['robots_global']['max-snippet'] ) && is_numeric( $this->options['robots_global']['max-snippet'] ) ) {
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->maxSnippet = intval( $this->options['robots_global']['max-snippet'] );
			}
			if ( isset( $this->options['robots_global']['max-video-preview'] ) && is_numeric( $this->options['robots_global']['max-video-preview'] ) ) {
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->maxVideoPreview = intval( $this->options['robots_global']['max-video-preview'] );
			}
			if ( ! empty( $this->options['robots_global']['max-image-preview'] ) ) {
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->maxImagePreview =
					aioseo()->helpers->sanitizeOption( $this->options['robots_global']['max-image-preview'] );
			}
		}

		if ( ! empty( $this->options['noindex_paginated_pages'] ) ) {
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default          = false;
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindexPaginated = 'on' === $this->options['noindex_paginated_pages'];
		}
	}

	/**
	 * Migrates the Knowledge Graph settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateKnowledgeGraphSettings() {
		if ( empty( $this->options['knowledgegraph_type'] ) ) {
			return;
		}

		aioseo()->options->searchAppearance->global->schema->siteRepresents =
			'company' === $this->options['knowledgegraph_type'] ? 'organization' : 'person';

		if ( ! empty( $this->options['knowledgegraph_name'] ) && 'company' === $this->options['knowledgegraph_type'] ) {
			aioseo()->options->searchAppearance->global->schema->organizationName = aioseo()->helpers->sanitizeOption( $this->options['knowledgegraph_name'] );
		} elseif ( ! empty( $this->options['knowledgegraph_logo'] ) ) {
			aioseo()->options->searchAppearance->global->schema->person     = 'manual';
			aioseo()->options->searchAppearance->global->schema->personName = aioseo()->helpers->sanitizeOption( $this->options['knowledgegraph_name'] );
		}

		if ( ! empty( $this->options['knowledgegraph_logo'] ) && 'company' === $this->options['knowledgegraph_type'] ) {
			aioseo()->options->searchAppearance->global->schema->organizationLogo = esc_url( $this->options['knowledgegraph_logo'] );
		} elseif ( ! empty( $this->options['knowledgegraph_logo'] ) ) {
			aioseo()->options->searchAppearance->global->schema->person     = 'manual';
			aioseo()->options->searchAppearance->global->schema->personLogo = esc_url( $this->options['knowledgegraph_logo'] );
		}

		$this->migrateKnowledgeGraphPhoneNumber();
	}

	/**
	 * Migrates the Knowledge Graph phone number.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateKnowledgeGraphPhoneNumber() {
		if ( empty( $this->options['phone'] ) ) {
			return;
		}

		$phoneNumber = aioseo()->helpers->sanitizeOption( $this->options['phone'] );
		if ( ! preg_match( '#\+\d+#', (string) $phoneNumber ) ) {
			$notification = Models\Notification::getNotificationByName( 'v3-migration-schema-number' );
			if ( $notification->notification_name ) {
				return;
			}

			Models\Notification::addNotification( [
				'slug'              => uniqid(),
				'notification_name' => 'v3-migration-schema-number',
				'title'             => __( 'Invalid Phone Number for Knowledge Graph', 'all-in-one-seo-pack' ),
				'content'           => sprintf(
					// Translators: 1 - The phone number.
					__( 'We were unable to import the phone number that you previously entered for your Knowledge Graph schema markup.
					As it needs to be internationally formatted, please enter it (%1$s) with the country code, e.g. +1 (555) 555-1234.', 'all-in-one-seo-pack' ),
					"<strong>$phoneNumber</strong>"
				),
				'type'              => 'warning',
				'level'             => [ 'all' ],
				'button1_label'     => __( 'Fix Now', 'all-in-one-seo-pack' ),
				'button1_action'    => 'http://route#aioseo-search-appearance&aioseo-scroll=schema-graph-phone&aioseo-highlight=schema-graph-phone:schema-markup',
				'button2_label'     => __( 'Remind Me Later', 'all-in-one-seo-pack' ),
				'button2_action'    => 'http://action#notification/v3-migration-schema-number-reminder',
				'start'             => gmdate( 'Y-m-d H:i:s' )
			] );

			return;
		}
		aioseo()->options->searchAppearance->global->schema->phone = $phoneNumber;
	}

	/**
	 * Migrates the Social Meta settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateSocialMetaSettings() {
		if ( ! empty( $this->options['open_graph_image'] ) ) {
			$defaultImage = esc_url( $this->options['open_graph_image'] );
			aioseo()->options->social->facebook->general->defaultImagePosts = $defaultImage;
			aioseo()->options->social->twitter->general->defaultImagePosts  = $defaultImage;
		}

		if ( ! empty( $this->options['social_url_facebook'] ) ) {
			aioseo()->options->social->profiles->urls->facebookPageUrl = esc_url( $this->options['social_url_facebook'] );
		}

		if ( ! empty( $this->options['facebook_author_urls'] ) ) {
			aioseo()->options->social->facebook->advanced->enable    = true;
			aioseo()->options->social->facebook->advanced->authorUrl = esc_url( $this->options['facebook_author_urls'] );
		}

		if ( ! empty( $this->options['facebook_admin_id'] ) ) {
			aioseo()->options->social->facebook->advanced->enable  = true;
			aioseo()->options->social->facebook->advanced->adminId = aioseo()->helpers->sanitizeOption( $this->options['facebook_admin_id'] );
		}

		if ( ! empty( $this->options['facebook_app_id'] ) ) {
			aioseo()->options->social->facebook->advanced->enable = true;
			aioseo()->options->social->facebook->advanced->appId  = aioseo()->helpers->sanitizeOption( $this->options['facebook_app_id'] );
		}

		if ( ! empty( $this->options['twitter_author_names'] ) ) {
			aioseo()->options->social->profiles->urls->twitterUrl =
				'https://x.com/' . aioseo()->helpers->sanitizeOption( $this->options['twitter_author_names'] );
		}

		if ( ! empty( $this->options['twitter_card_type'] ) ) {
			preg_match( '#large#', $this->options['twitter_card_type'], $match );
			aioseo()->options->social->twitter->general->defaultCardType = ! empty( $match ) ? 'summary_large_image' : 'summary';
		}
	}

	/**
	 * Migrates the default social image for posts.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateDefaultPostSocialImage() {
		if ( ! empty( $this->options['open_graph_image'] ) ) {
			$defaultImage = esc_url( $this->options['open_graph_image'] );
			aioseo()->options->social->facebook->general->defaultImagePosts = $defaultImage;
			aioseo()->options->social->twitter->general->defaultImagePosts  = $defaultImage;
		}
	}
}Common/ImportExport/SearchAppearance.php000064400000001535151536241170014357 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport;

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

/**
 * Migrates the Search Appearance settings.
 *
 * @since 4.0.0
 */
abstract class SearchAppearance {
	/**
	 * The schema graphs we support.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public static $supportedSchemaGraphs = [
		'none',
		'WebPage',
		'Article'
	];

	/**
	 * The WebPage graphs we support.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public static $supportedWebPageGraphs = [
		'AboutPage',
		'CollectionPage',
		'ContactPage',
		'FAQPage',
		'ItemPage',
		'ProfilePage',
		'RealEstateListing',
		'SearchResultsPage',
		'WebPage'
	];

	/**
	 * The Article graphs we support.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public static $supportedArticleGraphs = [
		'Article',
		'BlogPosting',
		'NewsArticle'
	];
}Common/ImportExport/SeoPress/Analytics.php000064400000001527151536241170014665 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

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

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Analytics Settings.
 *
 * @since 4.1.4
 */
class Analytics {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 */
	public function __construct() {
		$this->options = get_option( 'seopress_google_analytics_option_name' );
		if ( empty( $this->options ) ) {
			return;
		}

		$settings = [
			'seopress_google_analytics_other_tracking' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'miscellaneousVerification' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}
}Common/ImportExport/SeoPress/Breadcrumbs.php000064400000003403151536241170015162 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

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

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Breadcrumb settings.
 *
 * @since 4.1.4
 */
class Breadcrumbs {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 */
	public function __construct() {
		$this->options = get_option( 'seopress_pro_option_name' );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->migrate();
	}

	/**
	 * Migrates the Breadcrumbs settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrate() {
		if ( ! empty( $this->options['seopress_breadcrumbs_i18n_search'] ) ) {
			aioseo()->options->breadcrumbs->searchResultFormat = sprintf( '%1$s #breadcrumb_archive_post_type_name', $this->options['seopress_breadcrumbs_i18n_search'] );
		}

		if ( ! empty( $this->options['seopress_breadcrumbs_remove_blog_page'] ) ) {
			aioseo()->options->breadcrumbs->showBlogHome = false;
		}

		$settings = [
			'seopress_breadcrumbs_enable'    => [ 'type' => 'boolean', 'newOption' => [ 'breadcrumbs', 'enable' ] ],
			'seopress_breadcrumbs_separator' => [ 'type' => 'string', 'newOption' => [ 'breadcrumbs', 'separator' ] ],
			'seopress_breadcrumbs_i18n_home' => [ 'type' => 'string', 'newOption' => [ 'breadcrumbs', 'homepageLabel' ] ],
			'seopress_breadcrumbs_i18n_here' => [ 'type' => 'string', 'newOption' => [ 'breadcrumbs', 'breadcrumbPrefix' ] ],
			'seopress_breadcrumbs_i18n_404'  => [ 'type' => 'string', 'newOption' => [ 'breadcrumbs', 'errorFormat404' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}
}Common/ImportExport/SeoPress/GeneralSettings.php000064400000010307151536241170016030 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

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

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the General Settings.
 *
 * @since 4.1.4
 */
class GeneralSettings {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * List of our access control roles.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $roles = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 */
	public function __construct() {
		$this->options = get_option( 'seopress_advanced_option_name' );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->roles = aioseo()->access->getRoles();

		$this->migrateBlockMetaboxRoles();
		$this->migrateBlockContentAnalysisRoles();
		$this->migrateAttachmentRedirects();

		$settings = [
			'seopress_advanced_advanced_google'    => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'google' ] ],
			'seopress_advanced_advanced_bing'      => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'bing' ] ],
			'seopress_advanced_advanced_pinterest' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'pinterest' ] ],
			'seopress_advanced_advanced_yandex'    => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'yandex' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates Block AIOSEO metabox setting.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateBlockMetaboxRoles() {
		$seoPressRoles = ! empty( $this->options['seopress_advanced_security_metaboxe_role'] ) ? $this->options['seopress_advanced_security_metaboxe_role'] : '';
		if ( empty( $seoPressRoles ) ) {
			return;
		}

		$roleSettings = [ 'useDefault', 'pageAnalysis', 'pageGeneralSettings', 'pageSocialSettings', 'pageSchemaSettings', 'pageAdvancedSettings' ];

		foreach ( $seoPressRoles as $wpRole => $value ) {
			$role = $this->roles[ $wpRole ];
			if ( empty( $role ) || aioseo()->access->isAdmin( $role ) ) {
				continue;
			}

			if ( aioseo()->options->accessControl->has( $role ) ) {
				foreach ( $roleSettings as $setting ) {
					aioseo()->options->accessControl->$role->$setting = false;
				}
			} elseif ( aioseo()->dynamicOptions->accessControl->has( $role ) ) {
				foreach ( $roleSettings as $setting ) {
					aioseo()->dynamicOptions->accessControl->$role->$setting = false;
				}
			}
		}
	}

	/**
	 * Migrates Block Content analysis metabox setting.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateBlockContentAnalysisRoles() {
		$seoPressRoles = ! empty( $this->options['seopress_advanced_security_metaboxe_ca_role'] ) ? $this->options['seopress_advanced_security_metaboxe_ca_role'] : '';
		if ( empty( $seoPressRoles ) ) {
			return;
		}

		$roleSettings = [ 'useDefault', 'pageAnalysis' ];

		foreach ( $seoPressRoles as $wpRole => $value ) {
			$role = $this->roles[ $wpRole ];
			if ( empty( $role ) || aioseo()->access->isAdmin( $role ) ) {
				continue;
			}

			if ( aioseo()->options->accessControl->has( $role ) ) {
				foreach ( $roleSettings as $setting ) {
					aioseo()->options->accessControl->$role->$setting = false;
				}
			} elseif ( aioseo()->dynamicOptions->accessControl->has( $role ) ) {
				foreach ( $roleSettings as $setting ) {
					aioseo()->dynamicOptions->accessControl->$role->$setting = false;
				}
			}
		}
	}

	/**
	 * Migrates redirect attachment pages settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateAttachmentRedirects() {
		if ( ! empty( $this->options['seopress_advanced_advanced_attachments'] ) ) {
			aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'attachment_parent';
		}

		if ( ! empty( $this->options['seopress_advanced_advanced_attachments_file'] ) ) {
			aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'attachment';
		}

		if ( empty( $this->options['seopress_advanced_advanced_attachments'] ) && empty( $this->options['seopress_advanced_advanced_attachments_file'] ) ) {
			aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'disabled';
		}
	}
}Common/ImportExport/SeoPress/Helpers.php000064400000007264151536241170014344 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

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

use AIOSEO\Plugin\Common\ImportExport;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Contains helper methods for the import from SEOPress.
 *
 * @since 4.1.4
 */
class Helpers extends ImportExport\Helpers {
	/**
	 * Converts the macros from SEOPress to our own smart tags.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $string   The string with macros.
	 * @param  string $postType The post type.
	 * @return string           The string with smart tags.
	 */
	public function macrosToSmartTags( $string, $postType = null ) {
		$macros = $this->getMacros( $postType );

		foreach ( $macros as $macro => $tag ) {
			$string = aioseo()->helpers->pregReplace( "#$macro(?![a-zA-Z0-9_])#im", $tag, $string );
		}

		return trim( $string );
	}

	/**
	 * Returns the macro mappings.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $postType The post type.
	 * @param  string $pageType The page type.
	 * @return array  $macros   The macros.
	 */
	protected function getMacros( $postType = null, $pageType = null ) {
		$macros = [
			'%%sep%%'                   => '#separator_sa',
			'%%sitetitle%%'             => '#site_title',
			'%%sitename%%'              => '#site_title',
			'%%tagline%%'               => '#tagline',
			'%%sitedesc%%'              => '#tagline',
			'%%title%%'                 => '#site_title',
			'%%post_title%%'            => '#post_title',
			'%%post_excerpt%%'          => '#post_excerpt',
			'%%excerpt%%'               => '#post_excerpt',
			'%%post_content%%'          => '#post_content',
			'%%post_url%%'              => '#permalink',
			'%%post_date%%'             => '#post_date',
			'%%post_permalink%%'        => '#permalink',
			'%%date%%'                  => '#post_date',
			'%%post_author%%'           => '#author_name',
			'%%post_category%%'         => '#categories',
			'%%_category_title%%'       => '#taxonomy_title',
			'%%_category_description%%' => '#taxonomy_description',
			'%%tag_title%%'             => '#taxonomy_title',
			'%%tag_description%%'       => '#taxonomy_description',
			'%%term_title%%'            => '#taxonomy_title',
			'%%term_description%%'      => '#taxonomy_description',
			'%%search_keywords%%'       => '#search_term',
			'%%current_pagination%%'    => '#page_number',
			'%%page%%'                  => '#page_number',
			'%%archive_title%%'         => '#archive_title',
			'%%archive_date%%'          => '#archive_date',
			'%%wc_single_price%%'       => '#woocommerce_price',
			'%%wc_sku%%'                => '#woocommerce_sku',
			'%%currentday%%'            => '#current_day',
			'%%currentmonth%%'          => '#current_month',
			'%%currentmonth_short%%'    => '#current_month',
			'%%currentyear%%'           => '#current_year',
			'%%currentdate%%'           => '#current_date',
			'%%author_first_name%%'     => '#author_first_name',
			'%%author_last_name%%'      => '#author_last_name',
			'%%author_website%%'        => '#author_link',
			'%%author_nickname%%'       => '#author_first_name',
			'%%author_bio%%'            => '#author_bio',
			'%%currentmonth_num%%'      => '#current_month',
		];

		if ( $postType ) {
			$postType = get_post_type_object( $postType );
			if ( ! empty( $postType ) ) {
				$macros += [
					'%%cpt_plural%%' => $postType->labels->name,
				];
			}
		}

		switch ( $pageType ) {
			case 'archive':
				$macros['%%title%%'] = '#archive_title';
				break;
			case 'term':
				$macros['%%title%%'] = '#taxonomy_title';
				break;
			default:
				$macros['%%title%%'] = '#post_title';
				break;
		}

		// Strip all other tags.
		$macros['%%[^%]*%%'] = '';

		return $macros;
	}
}Common/ImportExport/SeoPress/PostMeta.php000064400000015547151536241170014501 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

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

use AIOSEO\Plugin\Common\Models;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Imports the post meta from SEOPress.
 *
 * @since 4.1.4
 */
class PostMeta {
	/**
	 * The mapped meta
	 *
	 * @since 4.1.4
	 *
	 * @var array
	 */
	private $mappedMeta = [
		'_seopress_analysis_target_kw'   => '',
		'_seopress_robots_archive'       => 'robots_noarchive',
		'_seopress_robots_canonical'     => 'canonical_url',
		'_seopress_robots_follow'        => 'robots_nofollow',
		'_seopress_robots_imageindex'    => 'robots_noimageindex',
		'_seopress_robots_index'         => 'robots_noindex',
		'_seopress_robots_odp'           => 'robots_noodp',
		'_seopress_robots_snippet'       => 'robots_nosnippet',
		'_seopress_social_twitter_desc'  => 'twitter_description',
		'_seopress_social_twitter_img'   => 'twitter_image_custom_url',
		'_seopress_social_twitter_title' => 'twitter_title',
		'_seopress_social_fb_desc'       => 'og_description',
		'_seopress_social_fb_img'        => 'og_image_custom_url',
		'_seopress_social_fb_title'      => 'og_title',
		'_seopress_titles_desc'          => 'description',
		'_seopress_titles_title'         => 'title',
		'_seopress_robots_primary_cat'   => 'primary_term'
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 */
	public function scheduleImport() {
		if ( aioseo()->actionScheduler->scheduleSingle( aioseo()->importExport->seoPress->postActionName, 0 ) ) {
			if ( ! aioseo()->core->cache->get( 'import_post_meta_seopress' ) ) {
				aioseo()->core->cache->update( 'import_post_meta_seopress', time(), WEEK_IN_SECONDS );
			}
		}
	}

	/**
	 * Imports the post meta.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function importPostMeta() {
		$postsPerAction  = apply_filters( 'aioseo_import_seopress_posts_per_action', 100 );
		$publicPostTypes = implode( "', '", aioseo()->helpers->getPublicPostTypes( true ) );
		$timeStarted     = gmdate( 'Y-m-d H:i:s', aioseo()->core->cache->get( 'import_post_meta_seopress' ) );

		$posts = aioseo()->core->db
			->start( 'posts as p' )
			->select( 'p.ID, p.post_type' )
			->join( 'postmeta as pm', '`p`.`ID` = `pm`.`post_id`' )
			->leftJoin( 'aioseo_posts as ap', '`p`.`ID` = `ap`.`post_id`' )
			->whereRaw( "pm.meta_key LIKE '_seopress_%'" )
			->whereRaw( "( p.post_type IN ( '$publicPostTypes' ) )" )
			->whereRaw( "( ap.post_id IS NULL OR ap.updated < '$timeStarted' )" )
			->groupBy( 'p.ID' )
			->orderBy( 'p.ID DESC' )
			->limit( $postsPerAction )
			->run()
			->result();

		if ( ! $posts || ! count( $posts ) ) {
			aioseo()->core->cache->delete( 'import_post_meta_seopress' );

			return;
		}

		foreach ( $posts as $post ) {
			$postMeta = aioseo()->core->db
				->start( 'postmeta' . ' as pm' )
				->select( 'pm.meta_key, pm.meta_value' )
				->where( 'pm.post_id', $post->ID )
				->whereRaw( "`pm`.`meta_key` LIKE '_seopress_%'" )
				->run()
				->result();

			$meta = array_merge( [
				'post_id' => (int) $post->ID,
			], $this->getMetaData( $postMeta, $post->ID ) );

			if ( ! $postMeta || ! count( $postMeta ) ) {
				$aioseoPost = Models\Post::getPost( (int) $post->ID );
				$aioseoPost->set( $meta );
				$aioseoPost->save();

				aioseo()->migration->meta->migrateAdditionalPostMeta( $post->ID );

				continue;
			}

			$aioseoPost = Models\Post::getPost( (int) $post->ID );
			$aioseoPost->set( $meta );
			$aioseoPost->save();

			aioseo()->migration->meta->migrateAdditionalPostMeta( $post->ID );

			// Clear the Overview cache.
			aioseo()->postSettings->clearPostTypeOverviewCache( $post->ID );
		}

		if ( count( $posts ) === $postsPerAction ) {
			aioseo()->actionScheduler->scheduleSingle( aioseo()->importExport->seoPress->postActionName, 5, [], true );
		} else {
			aioseo()->core->cache->delete( 'import_post_meta_seopress' );
		}
	}

	/**
	 * Get the meta data by post meta.
	 *
	 * @since 4.1.4
	 *
	 * @param object $postMeta The post meta from database.
	 * @return array           The meta data.
	 */
	public function getMetaData( $postMeta, $postId ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$meta = [
			'robots_default'      => true,
			'robots_noarchive'    => false,
			'canonical_url'       => '',
			'robots_nofollow'     => false,
			'robots_noimageindex' => false,
			'robots_noindex'      => false,
			'robots_noodp'        => false,
			'robots_nosnippet'    => false,
			'twitter_use_og'      => aioseo()->options->social->twitter->general->useOgData,
			'twitter_title'       => '',
			'twitter_description' => ''
		];
		foreach ( $postMeta as $record ) {
			$name  = $record->meta_key;
			$value = $record->meta_value;

			if ( ! in_array( $name, array_keys( $this->mappedMeta ), true ) ) {
				continue;
			}

			switch ( $name ) {
				case '_seopress_analysis_target_kw':
					$keyphrases     = array_map( 'trim', explode( ',', $value ) );
					$keyphraseArray = [
						'focus'      => [ 'keyphrase' => aioseo()->helpers->sanitizeOption( $keyphrases[0] ) ],
						'additional' => []
					];
					unset( $keyphrases[0] );
					foreach ( $keyphrases as $keyphrase ) {
						$keyphraseArray['additional'][] = [ 'keyphrase' => aioseo()->helpers->sanitizeOption( $keyphrase ) ];
					}

					$meta['keyphrases'] = $keyphraseArray;
					break;
				case '_seopress_robots_snippet':
				case '_seopress_robots_archive':
				case '_seopress_robots_imageindex':
				case '_seopress_robots_odp':
				case '_seopress_robots_follow':
				case '_seopress_robots_index':
					if ( 'yes' === $value ) {
						$meta['robots_default']             = false;
						$meta[ $this->mappedMeta[ $name ] ] = true;
					}
					break;
				case '_seopress_social_twitter_img':
					$meta['twitter_use_og']             = false;
					$meta['twitter_image_type']         = 'custom_image';
					$meta[ $this->mappedMeta[ $name ] ] = esc_url( $value );
					break;
				case '_seopress_social_twitter_desc':
				case '_seopress_social_twitter_title':
					$meta['twitter_use_og']             = false;
					$meta[ $this->mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
					break;
				case '_seopress_social_fb_img':
					$meta['og_image_type']              = 'custom_image';
					$meta[ $this->mappedMeta[ $name ] ] = esc_url( $value );
					break;
				case '_seopress_robots_primary_cat':
					$taxonomy                           = 'category';
					$options                            = new \stdClass();
					$options->$taxonomy                 = (int) $value;
					$meta[ $this->mappedMeta[ $name ] ] = wp_json_encode( $options );
					break;
				case '_seopress_titles_title':
				case '_seopress_titles_desc':
					$value = aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $value );
				default:
					$meta[ $this->mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
					break;
			}
		}

		return $meta;
	}
}Common/ImportExport/SeoPress/RobotsTxt.php000064400000002357151536241170014710 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

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

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the robots.txt settings.
 *
 * @since 4.1.4
 */
class RobotsTxt {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 */
	public function __construct() {
		$this->options = get_option( 'seopress_pro_option_name', [] );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->migrateRobotsTxt();

		$settings = [
			'seopress_robots_enable' => [ 'type' => 'boolean', 'newOption' => [ 'tools', 'robots', 'enable' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the robots.txt.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function migrateRobotsTxt() {
		$lines = ! empty( $this->options['seopress_robots_file'] ) ? (string) $this->options['seopress_robots_file'] : '';

		if ( $lines ) {
			$allRules = aioseo()->robotsTxt->extractRules( $lines );

			aioseo()->options->tools->robots->rules = aioseo()->robotsTxt->prepareRobotsTxt( $allRules );
		}
	}
}Common/ImportExport/SeoPress/Rss.php000064400000002242151536241170013500 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

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

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the RSS settings.
 *
 * @since 4.1.4
 */
class Rss {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 */
	public function __construct() {
		$this->options = get_option( 'seopress_pro_option_name' );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->migrateRss();
	}

	/**
	 * Migrates the RSS settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function migrateRss() {
		if ( ! empty( $this->options['seopress_rss_before_html'] ) ) {
			aioseo()->options->rssContent->before = esc_html( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $this->options['seopress_rss_before_html'] ) );
		}

		if ( ! empty( $this->options['seopress_rss_after_html'] ) ) {
			aioseo()->options->rssContent->after = esc_html( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $this->options['seopress_rss_after_html'] ) );
		}
	}
}Common/ImportExport/SeoPress/SeoPress.php000064400000002761151536241170014502 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

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

use AIOSEO\Plugin\Common\ImportExport;

class SeoPress extends ImportExport\Importer {
	/**
	 * A list of plugins to look for to import.
	 *
	 * @since 4.1.4
	 *
	 * @var array
	 */
	public $plugins = [
		[
			'name'     => 'SEOPress',
			'version'  => '4.0',
			'basename' => 'wp-seopress/seopress.php',
			'slug'     => 'seopress'
		],
		[
			'name'     => 'SEOPress PRO',
			'version'  => '4.0',
			'basename' => 'wp-seopress-pro/seopress-pro.php',
			'slug'     => 'seopress-pro'
		],
	];

	/**
	 * The post action name.
	 *
	 * @since 4.1.4
	 *
	 * @var string
	 */
	public $postActionName = 'aioseo_import_post_meta_seopress';

	/**
	 * The post action name.
	 *
	 * @since 4.1.4
	 *
	 * @param ImportExport\ImportExport $importer The main importer class.
	 */
	public function __construct( $importer ) {
		$this->helpers  = new Helpers();
		$this->postMeta = new PostMeta();
		add_action( $this->postActionName, [ $this->postMeta, 'importPostMeta' ] );

		$plugins = $this->plugins;
		foreach ( $plugins as $key => $plugin ) {
			$plugins[ $key ]['class'] = $this;
		}
		$importer->addPlugins( $plugins );
	}

	/**
	 * Imports the settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	protected function importSettings() {
		new GeneralSettings();
		new Analytics();
		new SocialMeta();
		new Titles();
		new Sitemap();
		new RobotsTxt();
		new Rss();
		new Breadcrumbs();
	}
}Common/ImportExport/SeoPress/Sitemap.php000064400000003600151536241170014332 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

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

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Sitemap Settings.
 *
 * @since 4.1.4
 */
class Sitemap {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 */
	public function __construct() {
		$this->options = get_option( 'seopress_xml_sitemap_option_name' );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->migratePostTypesInclude();
		$this->migrateTaxonomiesInclude();

		$settings = [
			'seopress_xml_sitemap_general_enable' => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'general', 'enable' ] ],
			'seopress_xml_sitemap_author_enable'  => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'general', 'author' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the post types to include in sitemap settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function migratePostTypesInclude() {
		$postTypesMigrate = $this->options['seopress_xml_sitemap_post_types_list'];
		$postTypesInclude = [];

		foreach ( $postTypesMigrate as $postType => $options ) {
			$postTypesInclude[] = $postType;
		}

		aioseo()->options->sitemap->general->postTypes->included = $postTypesInclude;
	}

	/**
	 * Migrates the taxonomies to include in sitemap settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function migrateTaxonomiesInclude() {
		$taxonomiesMigrate = $this->options['seopress_xml_sitemap_taxonomies_list'];
		$taxonomiesInclude = [];

		foreach ( $taxonomiesMigrate as $taxonomy => $options ) {
			$taxonomiesInclude[] = $taxonomy;
		}

		aioseo()->options->sitemap->general->taxonomies->included = $taxonomiesInclude;
	}
}Common/ImportExport/SeoPress/SocialMeta.php000064400000012322151536241170014752 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

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

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Social Meta Settings.
 *
 * @since 4.1.4
 */
class SocialMeta {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 */
	public function __construct() {
		$this->options = get_option( 'seopress_social_option_name' );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->migrateSocialUrls();
		$this->migrateKnowledge();
		$this->migrateFacebookSettings();
		$this->migrateTwitterSettings();
	}

	/**
	 * Migrates Basic Social Profiles URLs.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateSocialUrls() {
		$settings = [
			'seopress_social_accounts_facebook'   => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'facebookPageUrl' ] ],
			'seopress_social_accounts_twitter'    => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'twitterUrl' ] ],
			'seopress_social_accounts_pinterest'  => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'pinterestUrl' ] ],
			'seopress_social_accounts_instagram'  => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'instagramUrl' ] ],
			'seopress_social_accounts_youtube'    => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'youtubeUrl' ] ],
			'seopress_social_accounts_linkedin'   => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'linkedinUrl' ] ],
			'seopress_social_accounts_myspace'    => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'myspaceUrl' ] ],
			'seopress_social_accounts_soundcloud' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'soundCloudUrl' ] ],
			'seopress_social_accounts_tumblr'     => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'tumblrUrl' ] ],
			'seopress_social_accounts_wordpress'  => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'wordPressUrl' ] ],
			'seopress_social_accounts_bluesky'    => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'blueskyUrl' ] ],
			'seopress_social_accounts_threads'    => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'threadsUrl' ] ]
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates Knowledge Graph data.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateKnowledge() {
		$type = 'organization';
		if ( ! empty( $this->options['seopress_social_knowledge_type'] ) ) {
			$type = strtolower( $this->options['seopress_social_knowledge_type'] );
			if ( 'person' === $type ) {
				aioseo()->options->searchAppearance->global->schema->person = 'manual';
			}
		}

		aioseo()->options->searchAppearance->global->schema->siteRepresents = $type;

		$settings = [
			'seopress_social_knowledge_img'   => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', $type . 'Logo' ] ],
			'seopress_social_knowledge_name'  => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', $type . 'Name' ] ],
			'seopress_social_knowledge_phone' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'phone' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the Facebook settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateFacebookSettings() {
		if ( ! empty( $this->options['seopress_social_facebook_admin_id'] ) || ! empty( $this->options['seopress_social_facebook_app_id'] ) ) {
			aioseo()->options->social->facebook->advanced->enable = true;
		}

		$settings = [
			'seopress_social_facebook_og'       => [ 'type' => 'boolean', 'newOption' => [ 'social', 'facebook', 'general', 'enable' ] ],
			'seopress_social_facebook_img'      => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'image' ] ],
			'seopress_social_facebook_admin_id' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'advanced', 'adminId' ] ],
			'seopress_social_facebook_app_id'   => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'advanced', 'appId' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the Twitter settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateTwitterSettings() {
		if ( ! empty( $this->options['seopress_social_twitter_card_img_size'] ) ) {
			$twitterCard = ( 'large' === $this->options['seopress_social_twitter_card_img_size'] ) ? 'summary-card' : 'summary';
			aioseo()->options->social->twitter->general->defaultCardType = $twitterCard;
		}

		$settings = [
			'seopress_social_twitter_card'     => [ 'type' => 'boolean', 'newOption' => [ 'social', 'twitter', 'general', 'enable' ] ],
			'seopress_social_twitter_card_img' => [ 'type' => 'string', 'newOption' => [ 'social', 'twitter', 'homePage', 'image' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}
}Common/ImportExport/SeoPress/Titles.php000064400000026226151536241170014205 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

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

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Titles Settings.
 *
 * @since 4.1.4
 */
class Titles {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 */
	public function __construct() {
		$this->options = get_option( 'seopress_titles_option_name' );
		if ( empty( $this->options ) ) {
			return;
		}

		if (
			! empty( $this->options['seopress_titles_archives_author_title'] ) ||
			! empty( $this->options['seopress_titles_archives_author_desc'] ) ||
			! empty( $this->options['seopress_titles_archives_author_noindex'] )
			) {
			aioseo()->options->searchAppearance->archives->author->show = true;
		}

		if (
			! empty( $this->options['seopress_titles_archives_date_title'] ) ||
			! empty( $this->options['seopress_titles_archives_date_desc'] ) ||
			! empty( $this->options['seopress_titles_archives_date_noindex'] )
			) {
			aioseo()->options->searchAppearance->archives->date->show = true;
		}

		if (
			! empty( $this->options['seopress_titles_archives_search_title'] ) ||
			! empty( $this->options['seopress_titles_archives_search_desc'] )
			) {
			aioseo()->options->searchAppearance->archives->search->show = true;
		}

		$this->migrateTitleFormats();
		$this->migrateDescriptionFormats();
		$this->migrateNoIndexFormats();
		$this->migratePostTypeSettings();
		$this->migrateTaxonomiesSettings();
		$this->migrateArchiveSettings();
		$this->migrateAdvancedSettings();

		$settings = [
			'seopress_titles_sep' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'separator' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options, true );
	}

	/**
	 * Migrates the title formats.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateTitleFormats() {
		$settings = [
			'seopress_titles_home_site_title'       => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'siteTitle' ] ],
			'seopress_titles_archives_author_title' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'author', 'title' ] ],
			'seopress_titles_archives_date_title'   => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'date', 'title' ] ],
			'seopress_titles_archives_search_title' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'search', 'title' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options, true );
	}

	/**
	 * Migrates the description formats.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateDescriptionFormats() {
		$settings = [
			'seopress_titles_home_site_desc'       => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'metaDescription' ] ],
			'seopress_titles_archives_author_desc' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'author', 'metaDescription' ] ],
			'seopress_titles_archives_date_desc'   => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'date', 'metaDescription' ] ],
			'seopress_titles_archives_search_desc' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'search', 'metaDescription' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options, true );
	}

	/**
	 * Migrates the NoIndex formats.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateNoIndexFormats() {
		$settings = [
			'seopress_titles_archives_author_noindex' => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'archives', 'author', 'show' ] ],
			'seopress_titles_archives_date_noindex'   => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'archives', 'date', 'show' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the post type settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migratePostTypeSettings() {
		$titles = $this->options['seopress_titles_single_titles'];
		if ( empty( $titles ) ) {
			return;
		}

		foreach ( $titles as $postType => $options ) {
			if ( ! aioseo()->dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
				continue;
			}

			if ( ! empty( $options['title'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->title =
					aioseo()->helpers->sanitizeOption( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $options['title'] ) );
			}

			if ( ! empty( $options['description'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->metaDescription =
					aioseo()->helpers->sanitizeOption( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $options['description'] ) );
			}

			if ( ! empty( $options['enable'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->showMetaBox = false;
			}

			if ( ! empty( $options['noindex'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->show = false;
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = false;
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->noindex = true;
			}

			if ( ! empty( $options['nofollow'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->show = false;
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = false;
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->nofollow = true;
			}

			if ( ! empty( $options['date'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->showDateInGooglePreview = false;
			}

			if ( ! empty( $options['thumb_gcs'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->showPostThumbnailInSearch = true;
			}
		}
	}

	/**
	 * Migrates the taxonomies settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateTaxonomiesSettings() {
		$titles = ! empty( $this->options['seopress_titles_tax_titles'] ) ? $this->options['seopress_titles_tax_titles'] : '';
		if ( empty( $titles ) ) {
			return;
		}

		foreach ( $titles as $taxonomy => $options ) {
			if ( ! aioseo()->dynamicOptions->searchAppearance->taxonomies->has( $taxonomy ) ) {
				continue;
			}

			if ( ! empty( $options['title'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->title =
					aioseo()->helpers->sanitizeOption( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $options['title'] ) );
			}

			if ( ! empty( $options['description'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->metaDescription =
					aioseo()->helpers->sanitizeOption( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $options['description'] ) );
			}

			if ( ! empty( $options['enable'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->showMetaBox = false;
			}

			if ( ! empty( $options['noindex'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->show = false;
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->default = false;
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->noindex = true;
			}

			if ( ! empty( $options['nofollow'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->show = false;
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->default = false;
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->nofollow = true;
			}
		}
	}

	/**
	 * Migrates the archives settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateArchiveSettings() {
		$titles = $this->options['seopress_titles_archive_titles'];
		if ( empty( $titles ) ) {
			return;
		}

		foreach ( $titles as $archive => $options ) {
			if ( ! aioseo()->dynamicOptions->searchAppearance->archives->has( $archive ) ) {
				continue;
			}

			if ( ! empty( $options['title'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->archives->$archive->title =
					aioseo()->helpers->sanitizeOption( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $options['title'] ) );
			}

			if ( ! empty( $options['description'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->archives->$archive->metaDescription =
					aioseo()->helpers->sanitizeOption( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $options['description'] ) );
			}

			if ( ! empty( $options['noindex'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->archives->$archive->show = false;
				aioseo()->dynamicOptions->searchAppearance->archives->$archive->advanced->robotsMeta->default = false;
				aioseo()->dynamicOptions->searchAppearance->archives->$archive->advanced->robotsMeta->noindex = true;
			}

			if ( ! empty( $options['nofollow'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->archives->$archive->show = false;
				aioseo()->dynamicOptions->searchAppearance->archives->$archive->advanced->robotsMeta->default = false;
				aioseo()->dynamicOptions->searchAppearance->archives->$archive->advanced->robotsMeta->nofollow = true;
			}
		}
	}

	/**
	 * Migrates the advanced settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateAdvancedSettings() {
		if (
			! empty( $this->options['seopress_titles_noindex'] ) || ! empty( $this->options['seopress_titles_nofollow'] ) || ! empty( $this->options['seopress_titles_noodp'] ) ||
			! empty( $this->options['seopress_titles_noimageindex'] ) || ! empty( $this->options['seopress_titles_noarchive'] ) ||
			! empty( $this->options['seopress_titles_nosnippet'] ) || ! empty( $this->options['seopress_titles_paged_noindex'] )
		) {
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default = false;
		}

		$settings = [
			'seopress_titles_noindex'       => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'noindex' ] ],
			'seopress_titles_nofollow'      => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'nofollow' ] ],
			'seopress_titles_noodp'         => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'noodp' ] ],
			'seopress_titles_noimageindex'  => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'noimageindex' ] ],
			'seopress_titles_noarchive'     => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'noarchive' ] ],
			'seopress_titles_nosnippet'     => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'nosnippet' ] ],
			'seopress_titles_paged_noindex' => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'noindexPaginated' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}
}Common/ImportExport/YoastSeo/GeneralSettings.php000064400000002233151536241170016032 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;

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

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the General Settings.
 *
 * @since 4.0.0
 */
class GeneralSettings {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->options = get_option( 'wpseo' );
		if ( empty( $this->options ) ) {
			return;
		}

		$settings = [
			'googleverify'       => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'google' ] ],
			'msverify'           => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'bing' ] ],
			'yandexverify'       => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'yandex' ] ],
			'baiduverify'        => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'baidu' ] ],
			'enable_xml_sitemap' => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'general', 'enable' ] ]
		];

		aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options );
	}
}Common/ImportExport/YoastSeo/Helpers.php000064400000011315151536241170014337 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;

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

use AIOSEO\Plugin\Common\ImportExport;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Contains helper methods for the import from Rank Math.
 *
 * @since 4.0.0
 */
class Helpers extends ImportExport\Helpers {
	/**
	 * Converts the macros from Yoast SEO to our own smart tags.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string   The string with macros.
	 * @param  string $postType The post type.
	 * @param  string $pageType The page type.
	 * @return string $string   The string with smart tags.
	 */
	public function macrosToSmartTags( $string, $postType = null, $pageType = null ) {
		$macros = $this->getMacros( $postType, $pageType );

		if ( preg_match( '#%%BLOGDESCLINK%%#', (string) $string ) ) {
			$blogDescriptionLink = '<a href="' .
				aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'url' ) ) . '">' .
				aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) ) . ' - ' .
				aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'description' ) ) . '</a>';

			$string = str_replace( '%%BLOGDESCLINK%%', $blogDescriptionLink, $string );
		}

		if ( preg_match_all( '#%%cf_([^%]*)%%#', (string) $string, $matches ) && ! empty( $matches[1] ) ) {
			foreach ( $matches[1] as $name ) {
				if ( ! preg_match( '#\s#', (string) $name ) ) {
					$string = aioseo()->helpers->pregReplace( "#%%cf_$name%%#", "#custom_field-$name", $string );
				}
			}
		}

		if ( preg_match_all( '#%%tax_([^%]*)%%#', (string) $string, $matches ) && ! empty( $matches[1] ) ) {
			foreach ( $matches[1] as $name ) {
				if ( ! preg_match( '#\s#', (string) $name ) ) {
					$string = aioseo()->helpers->pregReplace( "#%%tax_$name%%#", "#tax_name-$name", $string );
				}
			}
		}

		foreach ( $macros as $macro => $tag ) {
			$string = aioseo()->helpers->pregReplace( "#$macro(?![a-zA-Z0-9_])#im", $tag, $string );
		}

		// Strip out all remaining tags.
		$string = aioseo()->helpers->pregReplace( '/%[^\%\s]*\([^\%]*\)%/i', '', aioseo()->helpers->pregReplace( '/%[^\%\s]*%/i', '', $string ) );

		return trim( $string );
	}

	/**
	 * Returns the macro mappings.
	 *
	 * @since 4.1.1
	 *
	 * @param  string $postType The post type.
	 * @param  string $pageType The page type.
	 * @return array  $macros   The macros.
	 */
	protected function getMacros( $postType = null, $pageType = null ) {
		$macros = [
			'%%sitename%%'             => '#site_title',
			'%%sitedesc%%'             => '#tagline',
			'%%sep%%'                  => '#separator_sa',
			'%%term_title%%'           => '#taxonomy_title',
			'%%term_description%%'     => '#taxonomy_description',
			'%%category_description%%' => '#taxonomy_description',
			'%%tag_description%%'      => '#taxonomy_description',
			'%%primary_category%%'     => '#taxonomy_title',
			'%%archive_title%%'        => '#archive_title',
			'%%pagenumber%%'           => '#page_number',
			'%%caption%%'              => '#attachment_caption',
			'%%name%%'                 => '#author_first_name #author_last_name',
			'%%user_description%%'     => '#author_bio',
			'%%date%%'                 => '#archive_date',
			'%%currentday%%'           => '#current_day',
			'%%currentmonth%%'         => '#current_month',
			'%%currentyear%%'          => '#current_year',
			'%%searchphrase%%'         => '#search_term',
			'%%AUTHORLINK%%'           => '#author_link',
			'%%POSTLINK%%'             => '#post_link',
			'%%BLOGLINK%%'             => '#site_link',
			'%%category%%'             => '#categories',
			'%%parent_title%%'         => '#parent_title',
			'%%wc_sku%%'               => '#woocommerce_sku',
			'%%wc_price%%'             => '#woocommerce_price',
			'%%wc_brand%%'             => '#woocommerce_brand',
			'%%excerpt%%'              => '#post_excerpt',
			'%%excerpt_only%%'         => '#post_excerpt_only'
			/* '%%tag%%'                  => '',
			'%%id%%'                   => '',
			'%%page%%'                 => '',
			'%%modified%%'             => '',
			'%%pagetotal%%'            => '',
			'%%focuskw%%'              => '',
			'%%term404%%'              => '',
			'%%ct_desc_[^%]*%%'        => '' */
		];

		if ( $postType ) {
			$postType = get_post_type_object( $postType );
			if ( ! empty( $postType ) ) {
				$macros += [
					'%%pt_single%%' => $postType->labels->singular_name,
					'%%pt_plural%%' => $postType->labels->name,
				];
			}
		}

		switch ( $pageType ) {
			case 'archive':
				$macros['%%title%%'] = '#archive_title';
				break;
			case 'term':
				$macros['%%title%%'] = '#taxonomy_title';
				break;
			default:
				$macros['%%title%%'] = '#post_title';
				break;
		}

		// Strip all other tags.
		$macros['%%[^%]*%%'] = '';

		return $macros;
	}
}Common/ImportExport/YoastSeo/PostMeta.php000064400000025720151536241170014476 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;

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

use AIOSEO\Plugin\Common\ImportExport;
use AIOSEO\Plugin\Common\Models;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Imports the post meta from Yoast SEO.
 *
 * @since 4.0.0
 */
class PostMeta {
	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function scheduleImport() {
		try {
			if ( as_next_scheduled_action( aioseo()->importExport->yoastSeo->postActionName ) ) {
				return;
			}

			if ( ! aioseo()->core->cache->get( 'import_post_meta_yoast_seo' ) ) {
				aioseo()->core->cache->update( 'import_post_meta_yoast_seo', time(), WEEK_IN_SECONDS );
			}

			as_schedule_single_action( time(), aioseo()->importExport->yoastSeo->postActionName, [], 'aioseo' );
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Imports the post meta.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function importPostMeta() {
		$postsPerAction  = apply_filters( 'aioseo_import_yoast_seo_posts_per_action', 100 );
		$publicPostTypes = implode( "', '", aioseo()->helpers->getPublicPostTypes( true ) );
		$timeStarted     = gmdate( 'Y-m-d H:i:s', aioseo()->core->cache->get( 'import_post_meta_yoast_seo' ) );

		$posts = aioseo()->core->db
			->start( 'posts' . ' as p' )
			->select( 'p.ID, p.post_type' )
			->leftJoin( 'aioseo_posts as ap', '`p`.`ID` = `ap`.`post_id`' )
			->whereRaw( "( p.post_type IN ( '$publicPostTypes' ) )" )
			->whereRaw( "( ap.post_id IS NULL OR ap.updated < '$timeStarted' )" )
			->orderBy( 'p.ID DESC' )
			->limit( $postsPerAction )
			->run()
			->result();

		if ( ! $posts || ! count( $posts ) ) {
			aioseo()->core->cache->delete( 'import_post_meta_yoast_seo' );

			return;
		}

		$mappedMeta = [
			'_yoast_wpseo_title'                 => 'title',
			'_yoast_wpseo_metadesc'              => 'description',
			'_yoast_wpseo_canonical'             => 'canonical_url',
			'_yoast_wpseo_meta-robots-noindex'   => 'robots_noindex',
			'_yoast_wpseo_meta-robots-nofollow'  => 'robots_nofollow',
			'_yoast_wpseo_meta-robots-adv'       => '',
			'_yoast_wpseo_focuskw'               => '',
			'_yoast_wpseo_focuskeywords'         => '',
			'_yoast_wpseo_opengraph-title'       => 'og_title',
			'_yoast_wpseo_opengraph-description' => 'og_description',
			'_yoast_wpseo_opengraph-image'       => 'og_image_custom_url',
			'_yoast_wpseo_twitter-title'         => 'twitter_title',
			'_yoast_wpseo_twitter-description'   => 'twitter_description',
			'_yoast_wpseo_twitter-image'         => 'twitter_image_custom_url',
			'_yoast_wpseo_schema_page_type'      => '',
			'_yoast_wpseo_schema_article_type'   => '',
			'_yoast_wpseo_is_cornerstone'        => 'pillar_content'
		];

		foreach ( $posts as $post ) {
			$postMeta = aioseo()->core->db
				->start( 'postmeta' . ' as pm' )
				->select( 'pm.meta_key, pm.meta_value' )
				->where( 'pm.post_id', $post->ID )
				->whereRaw( "`pm`.`meta_key` LIKE '_yoast_wpseo_%'" )
				->run()
				->result();

			$featuredImage = get_the_post_thumbnail_url( $post->ID );
			$meta          = [
				'post_id'                  => (int) $post->ID,
				'twitter_use_og'           => true,
				'og_image_type'            => $featuredImage ? 'featured' : 'content',
				'pillar_content'           => 0,
				'canonical_url'            => '',
				'robots_default'           => true,
				'robots_noarchive'         => false,
				'robots_nofollow'          => false,
				'robots_noimageindex'      => false,
				'robots_noindex'           => false,
				'robots_noodp'             => false,
				'robots_nosnippet'         => false,
				'title'                    => '',
				'description'              => '',
				'og_title'                 => '',
				'og_description'           => '',
				'og_image_custom_url'      => '',
				'twitter_title'            => '',
				'twitter_description'      => '',
				'twitter_image_custom_url' => '',
				'twitter_image_type'       => 'default'
			];

			if ( ! $postMeta || ! count( $postMeta ) ) {
				$aioseoPost = Models\Post::getPost( (int) $post->ID );
				$aioseoPost->set( $meta );
				$aioseoPost->save();

				aioseo()->migration->meta->migrateAdditionalPostMeta( $post->ID );
				continue;
			}

			$title = '';
			foreach ( $postMeta as $record ) {
				$name  = $record->meta_key;
				$value = $record->meta_value;

				// Handles primary taxonomy terms.
				// We need to handle it separately because it's stored in a different format.
				if ( false !== stripos( $name, '_yoast_wpseo_primary_' ) ) {
					sscanf( $name, '_yoast_wpseo_primary_%s', $taxonomy );
					if ( null === $taxonomy ) {
						continue;
					}

					$options = new \stdClass();
					if ( isset( $meta['primary_term'] ) ) {
						$options = json_decode( $meta['primary_term'] );
					}

					$options->$taxonomy   = (int) $value;
					$meta['primary_term'] = wp_json_encode( $options );
				}

				if ( ! in_array( $name, array_keys( $mappedMeta ), true ) ) {
					continue;
				}

				switch ( $name ) {
					case '_yoast_wpseo_meta-robots-noindex':
					case '_yoast_wpseo_meta-robots-nofollow':
						if ( (bool) $value ) {
							$meta[ $mappedMeta[ $name ] ] = (bool) $value;
							$meta['robots_default']       = false;
						}
						break;
					case '_yoast_wpseo_meta-robots-adv':
						$supportedValues = [ 'index', 'noarchive', 'noimageindex', 'nosnippet' ];
						foreach ( $supportedValues as $val ) {
							$meta[ "robots_$val" ] = false;
						}

						// This is a separated foreach so we can import any and all values.
						$values = explode( ',', $value );
						if ( $values ) {
							$meta['robots_default'] = false;

							foreach ( $values as $value ) {
								$meta[ "robots_$value" ] = true;
							}
						}
						break;
					case '_yoast_wpseo_canonical':
						$meta[ $mappedMeta[ $name ] ] = esc_url( $value );
						break;
					case '_yoast_wpseo_opengraph-image':
						$meta['og_image_type']        = 'custom_image';
						$meta[ $mappedMeta[ $name ] ] = esc_url( $value );
						break;
					case '_yoast_wpseo_twitter-image':
						$meta['twitter_use_og']       = false;
						$meta['twitter_image_type']   = 'custom_image';
						$meta[ $mappedMeta[ $name ] ] = esc_url( $value );
						break;
					case '_yoast_wpseo_schema_page_type':
						$value = aioseo()->helpers->pregReplace( '#\s#', '', $value );
						if ( in_array( $post->post_type, [ 'post', 'page', 'attachment' ], true ) ) {
							break;
						}

						if ( ! in_array( $value, ImportExport\SearchAppearance::$supportedWebPageGraphs, true ) ) {
							break;
						}

						$meta[ $mappedMeta[ $name ] ] = 'WebPage';
						$meta['schema_type_options']  = wp_json_encode( [
							'webPage' => [
								'webPageType' => $value
							]
						] );
						break;
					case '_yoast_wpseo_schema_article_type':
						$value = aioseo()->helpers->pregReplace( '#\s#', '', $value );
						if ( 'none' === lcfirst( $value ) ) {
							$meta[ $mappedMeta[ $name ] ] = 'None';
							break;
						}

						if ( in_array( $post->post_type, [ 'page', 'attachment' ], true ) ) {
							break;
						}

						$options = new \stdClass();
						if ( isset( $meta['schema_type_options'] ) ) {
							$options = json_decode( $meta['schema_type_options'] );
						}

						$options->article = [ 'articleType' => 'Article' ];
						if ( in_array( $value, ImportExport\SearchAppearance::$supportedArticleGraphs, true ) ) {
							$options->article = [ 'articleType' => $value ];
						} else {
							$options->article = [ 'articleType' => 'BlogPosting' ];
						}

						$meta['schema_type']         = 'Article';
						$meta['schema_type_options'] = wp_json_encode( $options );
						break;
					case '_yoast_wpseo_focuskw':
						$focusKeyphrase = [
							'focus' => [ 'keyphrase' => aioseo()->helpers->sanitizeOption( $value ) ]
						];

						// Merge with existing keyphrases if the array key already exists.
						if ( ! empty( $meta['keyphrases'] ) ) {
							$meta['keyphrases'] = array_merge( $meta['keyphrases'], $focusKeyphrase );
						} else {
							$meta['keyphrases'] = $focusKeyphrase;
						}
						break;
					case '_yoast_wpseo_focuskeywords':
						$keyphrases = [];
						if ( ! empty( $meta[ $mappedMeta[ $name ] ] ) ) {
							$keyphrases = (array) json_decode( $meta[ $mappedMeta[ $name ] ] );
						}

						$yoastKeyphrases = json_decode( $value, true );
						if ( is_array( $yoastKeyphrases ) ) {
							foreach ( $yoastKeyphrases as $yoastKeyphrase ) {
								if ( ! empty( $yoastKeyphrase['keyword'] ) ) {
									$keyphrase = [ 'keyphrase' => aioseo()->helpers->sanitizeOption( $yoastKeyphrase['keyword'] ) ];

									if ( ! isset( $keyphrases['additional'] ) ) {
										$keyphrases['additional'] = [];
									}

									$keyphrases['additional'][] = $keyphrase;
								}
							}
						}

						if ( ! empty( $keyphrases ) ) {
							// Merge with existing keyphrases if the array key already exists.
							if ( ! empty( $meta['keyphrases'] ) ) {
								$meta['keyphrases'] = array_merge( $meta['keyphrases'], $keyphrases );
							} else {
								$meta['keyphrases'] = $keyphrases;
							}
						}
						break;
					case '_yoast_wpseo_title':
					case '_yoast_wpseo_metadesc':
					case '_yoast_wpseo_opengraph-title':
					case '_yoast_wpseo_opengraph-description':
					case '_yoast_wpseo_twitter-title':
					case '_yoast_wpseo_twitter-description':
						if ( 'page' === $post->post_type ) {
							$value = aioseo()->helpers->pregReplace( '#%%primary_category%%#', '', $value );
							$value = aioseo()->helpers->pregReplace( '#%%excerpt%%#', '', $value );
						}

						if ( '_yoast_wpseo_twitter-title' === $name || '_yoast_wpseo_twitter-description' === $name ) {
							$meta['twitter_use_og'] = false;
						}

						$value = aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $value, 'post', $post->post_type );

						if ( '_yoast_wpseo_title' === $name ) {
							$title = $value;
						}

						$meta[ $mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
						break;
					case '_yoast_wpseo_is_cornerstone':
						$meta['pillar_content'] = (bool) $value ? 1 : 0;
						break;
					default:
						$meta[ $mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
						break;
				}
			}

			// Resetting the `twitter_use_og` option if the user has a custom title and no twitter title.
			if ( $meta['twitter_use_og'] && $title && empty( $meta['twitter_title'] ) ) {
				$meta['twitter_use_og'] = false;
				$meta['twitter_title']  = $title;
			}

			$aioseoPost = Models\Post::getPost( (int) $post->ID );
			$aioseoPost->set( $meta );
			$aioseoPost->save();

			aioseo()->migration->meta->migrateAdditionalPostMeta( $post->ID );

			// Clear the Overview cache.
			aioseo()->postSettings->clearPostTypeOverviewCache( $post->ID );
		}

		if ( count( $posts ) === $postsPerAction ) {
			try {
				as_schedule_single_action( time() + 5, aioseo()->importExport->yoastSeo->postActionName, [], 'aioseo' );
			} catch ( \Exception $e ) {
				// Do nothing.
			}
		} else {
			aioseo()->core->cache->delete( 'import_post_meta_yoast_seo' );
		}
	}
}Common/ImportExport/YoastSeo/SearchAppearance.php000064400000034145151536241170016130 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;

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

use AIOSEO\Plugin\Common\ImportExport;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Search Appearance settings.
 *
 * @since 4.0.0
 */
class SearchAppearance {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Whether the homepage social settings have been imported here.
	 *
	 * @since 4.2.4
	 *
	 * @var bool
	 */
	public $hasImportedHomepageSocialSettings = false;

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->options = get_option( 'wpseo_titles' );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->migrateSeparator();
		$this->migrateTitleFormats();
		$this->migrateDescriptionFormats();
		$this->migrateNoindexSettings();
		$this->migratePostTypeSettings();
		$this->migratePostTypeArchiveSettings();
		$this->migrateRedirectAttachments();
		$this->migrateKnowledgeGraphSettings();
		$this->migrateRssContentSettings();
		$this->migrateStripCategoryBase();
		$this->migrateHomepageSocialSettings();
	}

	/**
	 * Migrates the title/description separator.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateSeparator() {
		$separators = [
			'sc-dash'   => '-',
			'sc-ndash'  => '&ndash;',
			'sc-mdash'  => '&mdash;',
			'sc-colon'  => ':',
			'sc-middot' => '&middot;',
			'sc-bull'   => '&bull;',
			'sc-star'   => '*',
			'sc-smstar' => '&#8902;',
			'sc-pipe'   => '|',
			'sc-tilde'  => '~',
			'sc-laquo'  => '&laquo;',
			'sc-raquo'  => '&raquo;',
			'sc-lt'     => '&lt;',
			'sc-gt'     => '&gt;',
		];

		if ( ! empty( $this->options['separator'] ) && in_array( $this->options['separator'], array_keys( $separators ), true ) ) {
			aioseo()->options->searchAppearance->global->separator = $separators[ $this->options['separator'] ];
		}
	}

	/**
	 * Migrates the title formats.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateTitleFormats() {
		aioseo()->options->searchAppearance->global->siteTitle =
			aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $this->options['title-home-wpseo'] ) );

		aioseo()->options->searchAppearance->archives->date->title =
			aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $this->options['title-archive-wpseo'], null, 'archive' ) );

		// Archive Title tag needs to be stripped since we don't support it for these two archives.
		$value = aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $this->options['title-author-wpseo'], null, 'archive' ) );
		aioseo()->options->searchAppearance->archives->author->title = aioseo()->helpers->pregReplace( '/#archive_title/', '', $value );

		$value = aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $this->options['title-search-wpseo'], null, 'archive' ) );
		aioseo()->options->searchAppearance->archives->search->title = aioseo()->helpers->pregReplace( '/#archive_title/', '', $value );
	}

	/**
	 * Migrates the description formats.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateDescriptionFormats() {
		$settings = [
			'metadesc-home-wpseo'    => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'metaDescription' ] ],
			'metadesc-author-wpseo'  => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'author', 'metaDescription' ] ],
			'metadesc-archive-wpseo' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'date', 'metaDescription' ] ],
		];

		aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options, true );
	}

	/**
	 * Migrates the noindex settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateNoindexSettings() {
		if ( ! empty( $this->options['noindex-author-wpseo'] ) ) {
			aioseo()->options->searchAppearance->archives->author->show = false;
			aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->default = false;
			aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->noindex = true;
		} else {
			aioseo()->options->searchAppearance->archives->author->show = true;
		}

		if ( ! empty( $this->options['noindex-archive-wpseo'] ) ) {
			aioseo()->options->searchAppearance->archives->date->show = false;
			aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->default = false;
			aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->noindex = true;
		} else {
			aioseo()->options->searchAppearance->archives->date->show = true;
		}
	}

	/**
	 * Migrates the post type settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migratePostTypeSettings() {
		$supportedSettings = [
			'title',
			'metadesc',
			'noindex',
			'display-metabox-pt',
			'schema-page-type',
			'schema-article-type'
		];

		foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
			foreach ( $this->options as $name => $value ) {
				if ( ! preg_match( "#(.*)-$postType$#", (string) $name, $match ) || ! in_array( $match[1], $supportedSettings, true ) ) {
					continue;
				}

				switch ( $match[1] ) {
					case 'title':
						if ( 'page' === $postType ) {
							$value = aioseo()->helpers->pregReplace( '#%%primary_category%%#', '', $value );
							$value = aioseo()->helpers->pregReplace( '#%%excerpt%%#', '', $value );
						}
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->title =
							aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $value, $postType ) );
						break;
					case 'metadesc':
						if ( 'page' === $postType ) {
							$value = aioseo()->helpers->pregReplace( '#%%primary_category%%#', '', $value );
							$value = aioseo()->helpers->pregReplace( '#%%excerpt%%#', '', $value );
						}
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->metaDescription =
							aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $value, $postType ) );
						break;
					case 'noindex':
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->show = empty( $value ) ? true : false;
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = empty( $value ) ? true : false;
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->noindex = empty( $value ) ? false : true;
						break;
					case 'display-metabox-pt':
						if ( empty( $value ) ) {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->showMetaBox = false;
						}
						break;
					case 'schema-page-type':
						$value = aioseo()->helpers->pregReplace( '#\s#', '', $value );
						if ( in_array( $postType, [ 'post', 'page', 'attachment' ], true ) ) {
							break;
						}
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->schemaType = 'WebPage';
						if ( in_array( $value, ImportExport\SearchAppearance::$supportedWebPageGraphs, true ) ) {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->webPageType = $value;
						}
						break;
					case 'schema-article-type':
						$value = aioseo()->helpers->pregReplace( '#\s#', '', $value );
						if ( 'none' === lcfirst( $value ) ) {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->articleType = 'none';
							break;
						}

						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->articleType = 'Article';
						if ( in_array( $value, ImportExport\SearchAppearance::$supportedArticleGraphs, true ) ) {
							if ( ! in_array( $postType, [ 'page', 'attachment' ], true ) ) {
								aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->articleType = $value;
							}
						} else {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->articleType = 'BlogPosting';
						}
						break;
					default:
						break;
				}
			}
		}
	}

	/**
	 * Migrates the post type archive settings.
	 *
	 * @since 4.0.16
	 *
	 * @return void
	 */
	private function migratePostTypeArchiveSettings() {
		$supportedSettings = [
			'title',
			'metadesc',
			'noindex'
		];

		foreach ( aioseo()->helpers->getPublicPostTypes( true, true ) as $postType ) {
			foreach ( $this->options as $name => $value ) {
				if ( ! preg_match( "#(.*)-ptarchive-$postType$#", (string) $name, $match ) || ! in_array( $match[1], $supportedSettings, true ) ) {
					continue;
				}

				switch ( $match[1] ) {
					case 'title':
						aioseo()->dynamicOptions->searchAppearance->archives->$postType->title =
							aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $value, $postType, 'archive' ) );
						break;
					case 'metadesc':
						aioseo()->dynamicOptions->searchAppearance->archives->$postType->metaDescription =
							aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $value, $postType, 'archive' ) );
						break;
					case 'noindex':
						aioseo()->dynamicOptions->searchAppearance->archives->$postType->show = empty( $value ) ? true : false;
						aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->default = empty( $value ) ? true : false;
						aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->noindex = empty( $value ) ? false : true;
						break;
					default:
						break;
				}
			}
		}
	}

	/**
	 * Migrates the Knowledge Graph settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateKnowledgeGraphSettings() {
		if ( ! empty( $this->options['company_or_person'] ) ) {
			aioseo()->options->searchAppearance->global->schema->siteRepresents =
				'company' === $this->options['company_or_person'] ? 'organization' : 'person';
		}

		$settings = [
			'company_or_person_user_id' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'person' ] ],
			'person_logo'               => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'personLogo' ] ],
			'person_name'               => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'personName' ] ],
			'company_name'              => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'organizationName' ] ],
			'company_logo'              => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'organizationLogo' ] ],
			'org-email'                 => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'email' ] ],
			'org-phone'                 => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'phone' ] ],
			'org-description'           => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'organizationDescription' ] ],
			'org-founding-date'         => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'foundingDate' ] ],
		];

		aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options );

		// Additional Info
		// Reset data
		aioseo()->options->noConflict()->searchAppearance->global->schema->numberOfEmployees->reset();

		$numberOfEmployees = $this->options['org-number-employees'];
		if ( ! empty( $numberOfEmployees ) ) {
			list( $num1, $num2 ) = explode( '-', $numberOfEmployees );

			if ( $num2 ) {
				aioseo()->options->noConflict()->searchAppearance->global->schema->numberOfEmployees->isRange = true;
				aioseo()->options->noConflict()->searchAppearance->global->schema->numberOfEmployees->from    = (int) $num1;
				aioseo()->options->noConflict()->searchAppearance->global->schema->numberOfEmployees->to      = (int) $num2;
			} else {
				aioseo()->options->noConflict()->searchAppearance->global->schema->numberOfEmployees->number = (int) $num1;
			}
		}
	}

	/**
	 * Migrates the RSS content settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateRssContentSettings() {
		if ( isset( $this->options['rssbefore'] ) ) {
			aioseo()->options->rssContent->before = esc_html( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $this->options['rssbefore'] ) );
		}

		if ( isset( $this->options['rssafter'] ) ) {
			aioseo()->options->rssContent->after = esc_html( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $this->options['rssafter'] ) );
		}
	}

	/**
	 * Migrates the Redirect Attachments setting.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateRedirectAttachments() {
		aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = empty( $this->options['disable-attachment'] ) ? 'disabled' : 'attachment';
	}

	/**
	 * Migrates the strip category base option.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	private function migrateStripCategoryBase() {
		aioseo()->options->searchAppearance->advanced->removeCategoryBase = empty( $this->options['stripcategorybase'] ) ? false : true;
	}

	/**
	 * Migrate the social settings for the homepage.
	 *
	 * @since 4.2.4
	 *
	 * @return void
	 */
	private function migrateHomepageSocialSettings() {
		if (
			empty( $this->options['open_graph_frontpage_title'] ) &&
			empty( $this->options['open_graph_frontpage_desc'] ) &&
			empty( $this->options['open_graph_frontpage_image'] )
		) {
			return;
		}

		$this->hasImportedHomepageSocialSettings = true;

		$settings = [
			// These settings can also be found in the SocialMeta class, but Yoast recently moved them here.
			// We'll still keep them in the other class for backwards compatibility.
			'open_graph_frontpage_title' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'title' ] ],
			'open_graph_frontpage_desc'  => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'description' ] ],
			'open_graph_frontpage_image' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'image' ] ]
		];

		aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options, true );
	}
}Common/ImportExport/YoastSeo/SocialMeta.php000064400000014250151536241170014757 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;

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

use AIOSEO\Plugin\Common\Models;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Social Meta.
 *
 * @since 4.0.0
 */
class SocialMeta {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->options = get_option( 'wpseo_social' );

		if ( empty( $this->options ) ) {
			return;
		}

		$this->migrateSocialUrls();
		$this->migrateFacebookSettings();
		$this->migrateTwitterSettings();
		$this->migrateFacebookAdminId();
		$this->migrateSiteName();
		$this->migrateArticleTags();
		$this->migrateAdditionalTwitterData();

		$settings = [
			'pinterestverify' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'pinterest' ] ]
		];

		aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the Social URLs.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateSocialUrls() {
		$settings = [
			'facebook_site' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'facebookPageUrl' ] ],
			'instagram_url' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'instagramUrl' ] ],
			'linkedin_url'  => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'linkedinUrl' ] ],
			'myspace_url'   => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'myspaceUrl' ] ],
			'pinterest_url' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'pinterestUrl' ] ],
			'youtube_url'   => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'youtubeUrl' ] ],
			'wikipedia_url' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'wikipediaUrl' ] ],
			'wordpress_url' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'wordPressUrl' ] ],
		];

		if ( ! empty( $this->options['twitter_site'] ) ) {
			aioseo()->options->social->profiles->urls->twitterUrl =
				'https://x.com/' . aioseo()->helpers->sanitizeOption( $this->options['twitter_site'] );
		}

		aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the Facebook settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateFacebookSettings() {
		if ( ! empty( $this->options['og_default_image'] ) ) {
			$defaultImage = esc_url( $this->options['og_default_image'] );
			aioseo()->options->social->facebook->general->defaultImagePosts       = $defaultImage;
			aioseo()->options->social->facebook->general->defaultImageSourcePosts = 'default';

			aioseo()->options->social->twitter->general->defaultImagePosts       = $defaultImage;
			aioseo()->options->social->twitter->general->defaultImageSourcePosts = 'default';
		}

		$settings = [
			'opengraph' => [ 'type' => 'boolean', 'newOption' => [ 'social', 'facebook', 'general', 'enable' ] ],
		];

		if ( ! aioseo()->importExport->yoastSeo->searchAppearance->hasImportedHomepageSocialSettings ) {
			// These settings were moved to the Search Appearance tab of Yoast, but we'll leave this here to support older versions.
			// However, we want to make sure we import them only if the other ones aren't set.
			$settings = array_merge( $settings, [
				'og_frontpage_title' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'title' ] ],
				'og_frontpage_desc'  => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'description' ] ],
				'og_frontpage_image' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'image' ] ]
			] );
		}

		aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options, true );

		// Migrate home page object type.
		aioseo()->options->social->facebook->homePage->objectType = 'website';
		if ( 'page' === get_option( 'show_on_front' ) ) {
			$staticHomePageId = get_option( 'page_on_front' );

			// We must check if the ID exists because one might select the static homepage option but not actually set one.
			if ( ! $staticHomePageId ) {
				return;
			}

			$aioseoPost = Models\Post::getPost( (int) $staticHomePageId );
			$aioseoPost->set( [
				'og_object_type' => 'website'
			] );
			$aioseoPost->save();
		}
	}

	/**
	 * Migrates the Twitter settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateTwitterSettings() {
		$settings = [
			'twitter'           => [ 'type' => 'boolean', 'newOption' => [ 'social', 'twitter', 'general', 'enable' ] ],
			'twitter_card_type' => [ 'type' => 'string', 'newOption' => [ 'social', 'twitter', 'general', 'defaultCardType' ] ],
		];

		aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the Facebook admin ID.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateFacebookAdminId() {
		if ( ! empty( $this->options['fbadminapp'] ) ) {
			aioseo()->options->social->facebook->advanced->enable = true;
			aioseo()->options->social->facebook->advanced->adminId = aioseo()->helpers->sanitizeOption( $this->options['fbadminapp'] );
		}
	}

	/**
	 * Yoast sets the og:site_name to '#site_title';
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateSiteName() {
		aioseo()->options->social->facebook->general->siteName = '#site_title';
	}

	/**
	 * Yoast uses post tags by default, so we need to enable this.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateArticleTags() {
		aioseo()->options->social->facebook->advanced->enable              = true;
		aioseo()->options->social->facebook->advanced->generateArticleTags = true;
		aioseo()->options->social->facebook->advanced->usePostTagsInTags   = true;
		aioseo()->options->social->facebook->advanced->useKeywordsInTags   = false;
		aioseo()->options->social->facebook->advanced->useCategoriesInTags = false;
	}

	/**
	 * Enable additional Twitter Data.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateAdditionalTwitterData() {
		aioseo()->options->social->twitter->general->additionalData = true;
	}
}Common/ImportExport/YoastSeo/UserMeta.php000064400000005176151536241170014472 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;

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

use AIOSEO\Plugin\Common\ImportExport;
use AIOSEO\Plugin\Common\Models;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Imports the user meta from Yoast SEO.
 *
 * @since 4.0.0
 */
class UserMeta {
	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function scheduleImport() {
		aioseo()->actionScheduler->scheduleSingle( aioseo()->importExport->yoastSeo->userActionName, 30 );

		if ( ! aioseo()->core->cache->get( 'import_user_meta_yoast_seo' ) ) {
			aioseo()->core->cache->update( 'import_user_meta_yoast_seo', 0, WEEK_IN_SECONDS );
		}
	}

	/**
	 * Imports the post meta.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function importUserMeta() {
		$usersPerAction = 100;
		$offset         = aioseo()->core->cache->get( 'import_user_meta_yoast_seo' );

		$usersMeta = aioseo()->core->db
			->start( aioseo()->core->db->db->usermeta . ' as um', true )
			->whereRaw( "um.meta_key IN ('facebook', 'twitter', 'instagram', 'linkedin', 'myspace', 'pinterest', 'soundcloud', 'tumblr', 'wikipedia', 'youtube', 'mastodon', 'bluesky', 'threads')" )
			->whereRaw( "um.meta_value != ''" )
			->limit( $usersPerAction, $offset )
			->run()
			->result();

		if ( ! $usersMeta || ! count( $usersMeta ) ) {
			aioseo()->core->cache->delete( 'import_user_meta_yoast_seo' );

			return;
		}

		$mappedMeta = [
			'facebook'   => 'aioseo_facebook_page_url',
			'twitter'    => 'aioseo_twitter_url',
			'instagram'  => 'aioseo_instagram_url',
			'linkedin'   => 'aioseo_linkedin_url',
			'myspace'    => 'aioseo_myspace_url',
			'pinterest'  => 'aioseo_pinterest_url',
			'soundcloud' => 'aioseo_sound_cloud_url',
			'tumblr'     => 'aioseo_tumblr_url',
			'wikipedia'  => 'aioseo_wikipedia_url',
			'youtube'    => 'aioseo_youtube_url',
			'bluesky'    => 'aioseo_bluesky_url',
			'threads'    => 'aioseo_threads_url',
			'mastodon'   => 'aioseo_profiles_additional_urls'
		];

		foreach ( $usersMeta as $meta ) {
			if ( isset( $mappedMeta[ $meta->meta_key ] ) ) {
				$value = 'twitter' === $meta->meta_key ? 'https://x.com/' . $meta->meta_value : $meta->meta_value;
				update_user_meta( $meta->user_id, $mappedMeta[ $meta->meta_key ], $value );
			}
		}

		if ( count( $usersMeta ) === $usersPerAction ) {
			aioseo()->core->cache->update( 'import_user_meta_yoast_seo', 100 + $offset, WEEK_IN_SECONDS );
			aioseo()->actionScheduler->scheduleSingle( aioseo()->importExport->yoastSeo->userActionName, 5, [], true );
		} else {
			aioseo()->core->cache->delete( 'import_user_meta_yoast_seo' );
		}
	}
}Common/ImportExport/YoastSeo/YoastSeo.php000064400000004174151536241170014510 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;

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

use AIOSEO\Plugin\Common\ImportExport;

class YoastSeo extends ImportExport\Importer {
	/**
	 * A list of plugins to look for to import.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $plugins = [
		[
			'name'     => 'Yoast SEO',
			'version'  => '14.0',
			'basename' => 'wordpress-seo/wp-seo.php',
			'slug'     => 'yoast-seo'
		],
		[
			'name'     => 'Yoast SEO Premium',
			'version'  => '14.0',
			'basename' => 'wordpress-seo-premium/wp-seo-premium.php',
			'slug'     => 'yoast-seo-premium'
		],
	];

	/**
	 * The post action name.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	public $postActionName = 'aioseo_import_post_meta_yoast_seo';

	/**
	 * The user action name.
	 *
	 * @since 4.1.4
	 *
	 * @var string
	 */
	public $userActionName = 'aioseo_import_user_meta_yoast_seo';

	/**
	 * UserMeta class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var UserMeta
	 */
	private $userMeta = null;

	/**
	 * SearchAppearance class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var SearchAppearance
	 */
	public $searchAppearance = null;

	/**
	 * The post action name.
	 *
	 * @since 4.0.0
	 *
	 * @param ImportExport\ImportExport $importer The main importer class.
	 */
	public function __construct( $importer ) {
		$this->helpers  = new Helpers();
		$this->postMeta = new PostMeta();
		$this->userMeta = new UserMeta();

		add_action( $this->postActionName, [ $this->postMeta, 'importPostMeta' ] );
		add_action( $this->userActionName, [ $this->userMeta, 'importUserMeta' ] );

		$plugins = $this->plugins;
		foreach ( $plugins as $key => $plugin ) {
			$plugins[ $key ]['class'] = $this;
		}
		$importer->addPlugins( $plugins );
	}

	/**
	 * Imports the settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function importSettings() {
		new GeneralSettings();
		$this->searchAppearance = new SearchAppearance();
		// NOTE: The Social Meta settings need to be imported after the Search Appearance ones because some imports depend on what was imported there.
		new SocialMeta();
		$this->userMeta->scheduleImport();
	}
}Common/Integrations/BbPress.php000064400000000776151536241170012532 0ustar00<?php
namespace AIOSEO\Plugin\Common\Integrations;

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

/**
 * Class to integrate with the bbPress plugin.
 *
 * @since 4.8.1
 */
class BbPress {
	/**
	 * Returns whether the current page is a bbPress component page.
	 *
	 * @since 4.8.1
	 *
	 * @return bool Whether the current page is a bbPress component page.
	 */
	public static function isComponentPage() {
		return ! empty( aioseo()->standalone->bbPress->component->templateType );
	}
}Common/Integrations/BuddyPress.php000064400000010740151536241170013246 0ustar00<?php
namespace AIOSEO\Plugin\Common\Integrations;

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

/**
 * Class to integrate with the BuddyPress plugin.
 *
 * @since 4.7.6
 */
class BuddyPress {
	/**
	 * Call the callback given by the first parameter.
	 *
	 * @since 4.7.6
	 *
	 * @param  callable   $callback The function to be called.
	 * @param  mixed      ...$args  Zero or more parameters to be passed to the function
	 * @return mixed|null           The function result or null if the function is not callable.
	 */
	public static function callFunc( $callback, ...$args ) {
		if ( is_callable( $callback ) ) {
			return call_user_func( $callback, ...$args );
		}

		return null;
	}

	/**
	 * Returns the BuddyPress email custom post type slug.
	 *
	 * @since 4.7.6
	 *
	 * @return string The BuddyPress email custom post type slug if found or an empty string.
	 */
	public static function getEmailCptSlug() {
		$slug = '';
		if ( aioseo()->helpers->isPluginActive( 'buddypress' ) ) {
			$slug = self::callFunc( 'bp_get_email_post_type' );
		}

		return is_scalar( $slug ) ? strval( $slug ) : '';
	}

	/**
	 * Retrieves the BuddyPress component archive page permalink.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $component The BuddyPress component.
	 * @return string            The component archive page permalink.
	 */
	public static function getComponentArchiveUrl( $component ) {
		switch ( $component ) {
			case 'activity':
				$output = self::callFunc( 'bp_get_activity_directory_permalink' );
				break;
			case 'member':
				$output = self::callFunc( 'bp_get_members_directory_permalink' );
				break;
			case 'group':
				$output = self::callFunc( 'bp_get_groups_directory_url' );
				break;
			default:
				$output = '';
		}

		return is_scalar( $output ) ? strval( $output ) : '';
	}

	/**
	 * Returns the BuddyPress component single page permalink.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $component The BuddyPress component.
	 * @param  mixed  $id        The component ID.
	 * @return string            The component single page permalink.
	 */
	public static function getComponentSingleUrl( $component, $id ) {
		switch ( $component ) {
			case 'activity':
				$output = self::callFunc( 'bp_activity_get_permalink', $id );
				break;
			case 'group':
				$output = self::callFunc( 'bp_get_group_url', $id );
				break;
			case 'member':
				$output = self::callFunc( 'bp_core_get_userlink', $id, false, true );
				break;
			default:
				$output = '';
		}

		return is_scalar( $output ) ? strval( $output ) : '';
	}

	/**
	 * Returns the BuddyPress component edit link.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $component The BuddyPress component.
	 * @param  mixed  $id        The component ID.
	 * @return string            The component edit link.
	 */
	public static function getComponentEditUrl( $component, $id ) {
		switch ( $component ) {
			case 'activity':
				$output = add_query_arg( [
					'page'   => 'bp-activity',
					'aid'    => $id,
					'action' => 'edit'
				], self::callFunc( 'bp_get_admin_url', 'admin.php' ) );
				break;
			case 'group':
				$output = add_query_arg( [
					'page'   => 'bp-groups',
					'gid'    => $id,
					'action' => 'edit'
				], self::callFunc( 'bp_get_admin_url', 'admin.php' ) );
				break;
			case 'member':
				$output = get_edit_user_link( $id );
				break;
			default:
				$output = '';
		}

		return is_scalar( $output ) ? strval( $output ) : '';
	}

	/**
	 * Returns whether the BuddyPress component is active or not.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $component The BuddyPress component.
	 * @return bool              Whether the BuddyPress component is active.
	 */
	public static function isComponentActive( $component ) {
		static $active = [];
		if ( isset( $active[ $component ] ) ) {
			return $active[ $component ];
		}

		switch ( $component ) {
			case 'activity':
				$active[ $component ] = self::callFunc( 'bp_is_active', 'activity' );
				break;
			case 'group':
				$active[ $component ] = self::callFunc( 'bp_is_active', 'groups' );
				break;
			case 'member':
				$active[ $component ] = self::callFunc( 'bp_is_active', 'members' );
				break;
			default:
				$active[ $component ] = false;
		}

		return $active[ $component ];
	}

	/**
	 * Returns whether the current page is a BuddyPress component page.
	 *
	 * @since 4.7.6
	 *
	 * @return bool Whether the current page is a BuddyPress component page.
	 */
	public static function isComponentPage() {
		return ! empty( aioseo()->standalone->buddyPress->component->templateType );
	}
}Common/Integrations/Semrush.php000064400000013171151536241170012611 0ustar00<?php
namespace AIOSEO\Plugin\Common\Integrations;

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

/**
 * Class to integrate with the Semrush API.
 *
 * @since 4.0.16
 */
class Semrush {
	/**
	 * The Oauth2 URL.
	 *
	 * @since 4.0.16
	 *
	 * @var string
	 */
	public static $url = 'https://oauth.semrush.com/oauth2/access_token';

	/**
	 * The client ID for the Oauth2 integration.
	 *
	 * @since 4.0.16
	 *
	 * @var string
	 */
	public static $clientId = 'aioseo';

	/**
	 * The client secret for the Oauth2 integration.
	 *
	 * @since 4.0.16
	 *
	 * @var string
	 */
	public static $clientSecret = 'sdDUjYt6umO7sKM7mp4OrN8yeePTOQBy';

	/**
	 * Static method to authenticate the user.
	 *
	 * @since 4.0.16
	 *
	 * @param  string $authorizationCode The authorization code for the Oauth2 authentication.
	 * @return bool                      Whether the user is succesfully authenticated.
	 */
	public static function authenticate( $authorizationCode ) {
		$time     = time();
		$response = wp_remote_post( self::$url, [
			'headers' => [ 'Content-Type' => 'application/json' ],
			'body'    => wp_json_encode( [
				'client_id'     => self::$clientId,
				'client_secret' => self::$clientSecret,
				'grant_type'    => 'authorization_code',
				'code'          => $authorizationCode,
				'redirect_uri'  => 'https://oauth.semrush.com/oauth2/aioseo/success'
			] )
		] );

		$responseCode = wp_remote_retrieve_response_code( $response );
		if ( 200 === $responseCode ) {
			$tokens = json_decode( wp_remote_retrieve_body( $response ) );

			return self::saveTokens( $tokens, $time );
		}

		return false;
	}

	/**
	 * Static method to refresh the tokens once expired.
	 *
	 * @since 4.0.16
	 *
	 * @return bool Whether the tokens were successfully renewed.
	 */
	public static function refreshTokens() {
		$refreshToken = aioseo()->internalOptions->integrations->semrush->refreshToken;
		if ( empty( $refreshToken ) ) {
			self::reset();

			return false;
		}

		$time     = time();
		$response = wp_remote_post( self::$url, [
			'headers' => [ 'Content-Type' => 'application/json' ],
			'body'    => wp_json_encode( [
				'client_id'     => self::$clientId,
				'client_secret' => self::$clientSecret,
				'grant_type'    => 'refresh_token',
				'refresh_token' => $refreshToken
			] )
		] );

		$responseCode = wp_remote_retrieve_response_code( $response );
		if ( 200 === $responseCode ) {
			$tokens = json_decode( wp_remote_retrieve_body( $response ) );

			return self::saveTokens( $tokens, $time );
		}

		return false;
	}

	/**
	 * Clears out the internal options to reset the tokens.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	private static function reset() {
		aioseo()->internalOptions->integrations->semrush->accessToken  = '';
		aioseo()->internalOptions->integrations->semrush->tokenType    = '';
		aioseo()->internalOptions->integrations->semrush->expires      = '';
		aioseo()->internalOptions->integrations->semrush->refreshToken = '';
	}

	/**
	 * Checks if the token has expired
	 *
	 * @since 4.0.16
	 *
	 * @return boolean Whether or not the token has expired.
	 */
	public static function hasExpired() {
		$tokens = self::getTokens();

		return time() >= $tokens['expires'];
	}

	/**
	 * Returns the tokens.
	 *
	 * @since 4.0.16
	 *
	 * @return array An array of token data.
	 */
	public static function getTokens() {
		return aioseo()->internalOptions->integrations->semrush->all();
	}

	/**
	 * Saves the token options.
	 *
	 * @since 4.0.16
	 *
	 * @param  Object $tokens The tokens object.
	 * @param  string $time   The time set before the request was made.
	 * @return bool           Whether the response was valid and successfully saved.
	 */
	public static function saveTokens( $tokens, $time ) {
		$expectedProps = [
			'access_token',
			'token_type',
			'expires_in',
			'refresh_token'
		];

		// If the oAuth response does not include all expected properties, drop it.
		foreach ( $expectedProps as $prop ) {
			if ( empty( $tokens->$prop ) ) {
				return false;
			}
		}

		// Save the options.
		aioseo()->internalOptions->integrations->semrush->accessToken  = $tokens->access_token;
		aioseo()->internalOptions->integrations->semrush->tokenType    = $tokens->token_type;
		aioseo()->internalOptions->integrations->semrush->expires      = $time + $tokens->expires_in;
		aioseo()->internalOptions->integrations->semrush->refreshToken = $tokens->refresh_token;

		return true;
	}

	/**
	 * API call to get keyphrases from semrush.
	 *
	 * @since 4.0.16
	 *
	 * @param  string      $keyphrase A primary keyphrase.
	 * @param  string      $database  A country database.
	 * @return object|bool            The response object or false if the tokens could not be refreshed.
	 */
	public static function getKeyphrases( $keyphrase, $database ) {
		if ( self::hasExpired() ) {
			$success = self::refreshTokens();
			if ( ! $success ) {
				return false;
			}
		}

		$transientKey = 'semrush_keyphrases_' . $keyphrase . '_' . $database;
		$results      = aioseo()->core->cache->get( $transientKey );

		if ( null !== $results ) {
			return $results;
		}

		$params = [
			'phrase'         => $keyphrase,
			'export_columns' => 'Ph,Nq,Td',
			'database'       => strtolower( $database ),
			'display_limit'  => 10,
			'display_offset' => 0,
			'display_sort'   => 'nq_desc',
			'display_filter' => '%2B|Nq|Lt|1000',
			'access_token'   => aioseo()->internalOptions->integrations->semrush->accessToken
		];

		$url = 'https://oauth.semrush.com/api/v1/keywords/phrase_fullsearch?' . http_build_query( $params );

		$response = wp_remote_get( $url );
		$body     = json_decode( wp_remote_retrieve_body( $response ) );

		aioseo()->core->cache->update( $transientKey, $body );

		return $body;
	}
}Common/Integrations/WpCode.php000064400000006041151536241170012342 0ustar00<?php
namespace AIOSEO\Plugin\Common\Integrations;

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

/**
 * Route class for the API.
 *
 * @since 4.3.8
 */
class WpCode {
	/**
	 * Load the WPCode snippets for our desired username or return an empty array if not available.
	 *
	 * @since 4.3.8
	 *
	 * @return array The snippets.
	 */
	public static function loadWpCodeSnippets() {
		$snippets = self::getPlaceholderSnippets();
		if ( function_exists( 'wpcode_get_library_snippets_by_username' ) ) {
			$snippets = wpcode_get_library_snippets_by_username( 'aioseo' );
		}

		return $snippets;
	}

	/**
	 * Checks if the plugin is installed, either the lite or premium version.
	 *
	 * @since 4.3.8
	 *
	 * @return bool True if the plugin is installed.
	 */
	public static function isPluginInstalled() {
		return self::isProInstalled() || self::isLiteInstalled();
	}

	/**
	 * Is the pro plugin installed.
	 *
	 * @since 4.3.8
	 *
	 * @return bool True if the pro plugin is installed.
	 */
	public static function isProInstalled() {
		$installedPlugins = array_keys( get_plugins() );

		return in_array( 'wpcode-premium/wpcode.php', $installedPlugins, true );
	}

	/**
	 * Is the lite plugin installed.
	 *
	 * @since 4.3.8
	 *
	 * @return bool True if the lite plugin is installed.
	 */
	public static function isLiteInstalled() {
		$installedPlugins = array_keys( get_plugins() );

		return in_array( 'insert-headers-and-footers/ihaf.php', $installedPlugins, true );
	}

	/**
	 * Basic check if the plugin is active by looking for the main function.
	 *
	 * @since 4.3.8
	 *
	 * @return bool True if the plugin is active.
	 */
	public static function isPluginActive() {
		return function_exists( 'wpcode' );
	}

	/**
	 * Checks if the plugin is active but needs to be updated by checking if the function to load the
	 * library snippets by username exists.
	 *
	 * @since 4.3.8
	 *
	 * @return bool True if the plugin is active but needs to be updated.
	 */
	public static function pluginNeedsUpdate() {
		return self::isPluginActive() && ! function_exists( 'wpcode_get_library_snippets_by_username' );
	}

	/**
	 * Get placeholder snippets if the WPCode snippets are not available.
	 *
	 * @since 4.3.8
	 *
	 * @return array The placeholder snippets.
	 */
	private static function getPlaceholderSnippets() {
		$snippetTitles = [
			'Disable autogenerated shipping details schema for WooCommerce',
			'Disable SEO Preview feature',
			'Disable Shortcode Parsing in All in One SEO',
			'Enable WooCommerce Product Attributes in Search Appearance',
			'Fix LearnPress conflict that hides AIOSEO tabs on settings pages',
			'Limit Meta Description to 160 characters',
			'Limit SEO Title to 60 characters',
			'Noindex Product Search Pages',
			'Noindex Products under a Product Category',
		];

		$placeholderSnippets = [];
		foreach ( $snippetTitles as $snippetTitle ) {
			// Add placeholder install link so we show a button.
			$placeholderSnippets[] = [
				'title'   => $snippetTitle,
				'install' => 'https://library.wpcode.com/'
			];
		}

		return $placeholderSnippets;
	}
}Common/Llms/Llms.php000064400000014714151536241170010337 0ustar00<?php
namespace AIOSEO\Plugin\Common\Llms;

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

/**
 * Handles the LLMS.txt generation.
 *
 * @since 4.8.4
 */
class Llms {
	/**
	 * Site title
	 *
	 * since 4.8.4
	 *
	 * @var string
	 */
	private $title;

	/**
	 * Site description
	 *
	 * since 4.8.4
	 *
	 * @var string
	 */
	private $description;

	/**
	 * Site link
	 *
	 * since 4.8.4
	 *
	 * @var string
	 */
	private $link;

	/**
	 * Plugin version
	 *
	 * since 4.8.4
	 *
	 * @var string
	 */
	private $version;

	public function __construct() {
		if ( is_admin() || ! aioseo()->options->advanced->llmsTxt ) {
			return;
		}

		add_action( 'parse_request', [ $this, 'checkRequest' ] );
	}

	/**
	 * Checks if the request is for the LLMS.txt file.
	 *
	 * @since 4.8.4
	 *
	 * @param \WP   $wp The WordPress request object.
	 * @return void
	 */
	public function checkRequest( $wp ) {
		$slug = $wp->request ?? aioseo()->helpers->cleanSlug( $wp->request );
		if ( ! $slug && isset( $_SERVER['REQUEST_URI'] ) ) {
			// We must fallback to the REQUEST URI in case the site uses plain permalinks.
			$slug = aioseo()->helpers->cleanSlug( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) );
		}

		if ( 'llms.txt' !== $slug ) {
			return;
		}

		$this->setSiteInfo();
		$this->generate();
	}

	/**
	 * Sets the site info.
	 *
	 * @since 4.8.4
	 *
	 * @return void
	 */
	private function setSiteInfo() {
		$isMultisite = is_multisite();
		$this->title = $isMultisite
			? get_blog_option( get_current_blog_id(), 'blogname' )
			: get_bloginfo( 'name' );
		$this->title = $this->title ?? aioseo()->meta->title->getHomePageTitle();

		$this->description = $isMultisite
			? get_blog_option( get_current_blog_id(), 'blogdescription' )
			: get_bloginfo( 'description' );
		$this->description = $this->description ?? aioseo()->meta->description->getHomePageDescription();

		$this->link = $isMultisite
			? get_blog_option( get_current_blog_id(), 'siteurl' )
			: home_url();

		$this->version = aioseo()->helpers->getAioseoVersion();
	}

	/**
	 * Generates the LLMS.txt file.
	 *
	 * @since 4.8.4
	 *
	 * @return void
	 */
	private function generate() {
		$this->headers();

		$content  = $this->getHeader();
		$content .= $this->getSiteDescription();
		$content .= $this->getSitemapUrl();
		$content .= $this->getRecentContent();

		echo $content; #phpcs:ignore
		exit;
	}

	/**
	 * Gets the header section of the llms.txt file.
	 *
	 * @since 4.8.4
	 *
	 * @return string
	 */
	private function getHeader() {
		$introText = sprintf(
			/* translators: 1 - The plugin name ("All in One SEO"), 2 - The version number */
			esc_html__( 'Generated by %1$s v%2$s, this is an llms.txt file, used by LLMs to index the site.', 'all-in-one-seo-pack' ),
			esc_html( AIOSEO_PLUGIN_NAME ),
			esc_html( aioseo()->version )
		);

		if ( $this->title ) {
			$introText .= esc_html( "\n\n# {$this->title}\n\n" );
		}

		return $introText;
	}

	/**
	 * Gets the site description section of the llms.txt file.
	 *
	 * @since 4.8.4
	 *
	 * @return string
	 */
	private function getSiteDescription() {
		if ( $this->description ) {
			return "{$this->description}\n\n";
		}

		return '';
	}

	/**
	 * Gets the sitemap link section of the llms.txt file.
	 *
	 * @since 4.8.4
	 *
	 * @return string
	 */
	private function getSitemapUrl() {
		if ( ! aioseo()->options->sitemap->general->enable ) {
			return '';
		}

		$sitemapUrl = site_url( 'sitemap.xml' );

		return "## Sitemaps\n\n- [XML Sitemap]({$sitemapUrl}): Contains all public/indexable URLs for this website.\n\n";
	}

	/**
	 * Gets the recent content section of the llms.txt file.
	 *
	 * @since 4.8.4
	 *
	 * @return string
	 */
	private function getRecentContent() {
		$content = '';

		$postTypes                       = array_filter( aioseo()->helpers->getPublicPostTypes( true ), function( $type ) {
			return 'attachment' !== $type;
		} );
		$originalSitemapType             = aioseo()->sitemap->type;
		$originalLinksPerIndex           = aioseo()->sitemap->linksPerIndex;
		$originalIndexes                 = aioseo()->sitemap->indexes;

		aioseo()->sitemap->type          = 'llms';
		aioseo()->sitemap->linksPerIndex = 20;
		aioseo()->sitemap->indexes       = true;

		foreach ( $postTypes as $postType ) {
			$postTypeObject = get_post_type_object( $postType );
			if ( ! $postTypeObject ) {
				continue;
			}

			$recentPosts = aioseo()->sitemap->query->posts( $postType );

			if ( ! empty( $recentPosts ) ) {
				$content .= '## ' . $postTypeObject->labels->name . "\n\n";
				foreach ( $recentPosts as $post ) {
					$content .= '- [' . aioseo()->helpers->decodeHtmlEntities( $post->post_title ) . '](' . aioseo()->helpers->decodeUrl( get_permalink( $post->ID ) ) . ")\n";
				}
				$content .= "\n";
			}
		}

		$taxonomies = aioseo()->helpers->getPublicTaxonomies( true );

		// Get recent terms for each taxonomy using sitemap query
		foreach ( $taxonomies as $taxonomy ) {
			$taxonomyObject = get_taxonomy( $taxonomy );
			if ( ! $taxonomyObject ) {
				continue;
			}

			$terms = aioseo()->sitemap->query->terms( $taxonomy );

			if ( ! empty( $terms ) ) {
				$content .= '## ' . $taxonomyObject->labels->name . "\n\n";
				foreach ( $terms as $term ) {
					if ( is_object( $term ) && ! empty( $term->term_id ) && ! empty( $term->name ) ) {
						$content .= '- [' . aioseo()->helpers->decodeHtmlEntities( $term->name ) . '](' . aioseo()->helpers->decodeUrl( get_term_link( $term->term_id, $taxonomy ) ) . ")\n";
					}
				}
				$content .= "\n";
			}
		}

		// Restore original sitemap settings
		aioseo()->sitemap->type          = $originalSitemapType;
		aioseo()->sitemap->linksPerIndex = $originalLinksPerIndex;
		aioseo()->sitemap->indexes       = $originalIndexes;

		return $content;
	}

	/**
	 * Sets the HTTP headers for the LLMS.txt.
	 *
	 * @since 4.8.4
	 *
	 * @return void
	 */
	public function headers() {
		$charset = aioseo()->helpers->getCharset();
		header( "Content-Type: text/plain; charset=$charset", true );
		header( 'X-Robots-Tag: noindex, follow', true );
	}

	/**
	 * Gets the LLMs.txt URL if accessible.
	 *
	 * @since 4.8.4
	 *
	 * @return array The LLMs.txt URL if accessible, null otherwise.
	 */
	public function getUrl() {
		$url          = site_url( '/llms.txt' );
		$isAccessible = false;

		if ( aioseo()->options->advanced->llmsTxt ) {
			$response     = wp_remote_head( $url );
			$isAccessible = ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response );
		}

		return [
			'url'          => $url,
			'isAccessible' => $isAccessible
		];
	}
}Common/Main/Activate.php000064400000010263151536241170011140 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

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

/**
 * Abstract class that Pro and Lite both extend.
 *
 * @since 4.0.0
 */
class Activate {
	/**
	 * Construct method.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		register_activation_hook( AIOSEO_FILE, [ $this, 'activate' ] );
		register_deactivation_hook( AIOSEO_FILE, [ $this, 'deactivate' ] );

		// The following only needs to happen when in the admin.
		if ( ! is_admin() ) {
			return;
		}

		// This needs to run on at least 1000 because we load the roles in the Access class on 999.
		add_action( 'init', [ $this, 'init' ], 1000 );
	}

	/**
	 * Initialize activation.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	public function init() {
		// If Pro just deactivated the lite version, we need to manually run the activation hook, because it doesn't run here.
		$proDeactivatedLite = (bool) aioseo()->core->cache->get( 'pro_just_deactivated_lite' );
		if ( ! $proDeactivatedLite ) {
			// Also check for the old transient in the options table (because a user might switch from an older Lite version that lacks the Cache class).
			$proDeactivatedLite = (bool) get_option( '_aioseo_cache_pro_just_deactivated_lite' );
		}

		if ( $proDeactivatedLite ) {
			aioseo()->core->cache->delete( 'pro_just_deactivated_lite' );
			$this->activate( false );
		}
	}

	/**
	 * Runs on activation.
	 *
	 * @since 4.0.17
	 *
	 * @param  bool $networkWide Whether or not this is a network wide activation.
	 * @return void
	 */
	public function activate( $networkWide ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		aioseo()->access->addCapabilities();

		// Make sure our tables exist.
		aioseo()->updates->addInitialCustomTablesForV4();

		// Set the activation timestamps.
		$time = time();
		aioseo()->internalOptions->internal->activated = $time;

		if ( ! aioseo()->internalOptions->internal->firstActivated ) {
			aioseo()->internalOptions->internal->firstActivated = $time;
		}

		aioseo()->core->cache->clear();

		$this->maybeRunSetupWizard();
	}

	/**
	 * Runs on deactivation.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function deactivate() {
		aioseo()->access->removeCapabilities();
	}

	/**
	 * Check if we should redirect on activation.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	private function maybeRunSetupWizard() {
		if ( '0.0' !== aioseo()->internalOptions->internal->lastActiveVersion ) {
			return;
		}

		$oldOptions = get_option( 'aioseop_options' );
		if ( ! empty( $oldOptions ) ) {
			return;
		}

		if ( is_network_admin() ) {
			return;
		}

		if ( isset( $_GET['activate-multi'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			return;
		}

		// Sets 30 second transient for welcome screen redirect on activation.
		aioseo()->core->cache->update( 'activation_redirect', true, 30 );
	}

	/**
	 * Adds our capabilities to all roles on the next request and the installing user on the current request after upgrading to Pro.
	 *


	 *
	 * @since 4.1.4.4
	 *
	 * @return void
	 */
	public function addCapabilitiesOnUpgrade() {
		// In case the user is switching to Pro via the AIOSEO Connect feature,
		// we need to set this transient here as the regular activation hooks won't run and Pro otherwise won't clear the cache and add the required capabilities.
		aioseo()->core->cache->update( 'pro_just_deactivated_lite', true );

		// Doing the above isn't sufficient because the current user will be lacking the capabilities on the first request. Therefore, we add them manually just for him.
		$userId = function_exists( 'get_current_user_id' ) && get_current_user_id()
			? get_current_user_id() // If there is a logged in user, the user is switching from Lite to Pro via the Plugins menu.
			: aioseo()->core->cache->get( 'connect_active_user' ); // If there is no logged in user, we're upgrading via AIOSEO Connect.

		$user = get_userdata( $userId );
		if ( is_object( $user ) ) {
			$capabilities = aioseo()->access->getCapabilityList();
			foreach ( $capabilities as $capability ) {
				$user->add_cap( $capability );
			}
		}

		aioseo()->core->cache->delete( 'connect_active_user' );
	}
}Common/Main/CategoryBase.php000064400000017205151536241170011753 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

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

/**
 * Main class with methods that are called.
 *
 * @since   4.2.0
 * @version 4.7.1 Moved from Pro to Common.
 */
class CategoryBase {
	/**
	 * Class constructor.
	 *
	 * @since 4.2.0
	 */
	public function __construct() {
		if ( ! aioseo()->options->searchAppearance->advanced->removeCategoryBase ) {
			return;
		}

		add_filter( 'query_vars', [ $this, 'queryVars' ] );
		add_filter( 'request', [ $this, 'maybeRedirectCategoryUrl' ] );
		add_filter( 'category_rewrite_rules', [ $this, 'categoryRewriteRules' ] );
		add_filter( 'term_link', [ $this, 'modifyTermLink' ], 10, 3 );

		// Flush rewrite rules on any of the following actions.
		add_action( 'created_category', [ $this, 'scheduleFlushRewrite' ] );
		add_action( 'delete_category', [ $this, 'scheduleFlushRewrite' ] );
		add_action( 'edited_category', [ $this, 'scheduleFlushRewrite' ] );
	}

	/**
	 * Add the redirect var to the query vars if the "strip category bases" option is enabled.
	 *
	 * @since 4.2.0
	 *
	 * @param  array $queryVars Query vars to filter.
	 * @return array            The filtered query vars.
	 */
	public function queryVars( $queryVars ) {
		$queryVars[] = 'aioseo_category_redirect';

		return $queryVars;
	}

	/**
	 * Redirect the category URL to the new one.
	 *
	 * @param  array $queryVars Query vars to check for redirect var.
	 * @return array            The original query vars.
	 */
	public function maybeRedirectCategoryUrl( $queryVars ) {
		if ( isset( $queryVars['aioseo_category_redirect'] ) ) {
			$categoryUrl = trailingslashit( get_option( 'home' ) ) . user_trailingslashit( $queryVars['aioseo_category_redirect'], 'category' );
			wp_redirect( $categoryUrl, 301, 'AIOSEO' );
			die;
		}

		return $queryVars;
	}

	/**
	 * Rewrite the category base.
	 *
	 * @since 4.2.0
	 *
	 * @return array The rewritten rules.
	 */
	public function categoryRewriteRules() {
		global $wp_rewrite; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		$categoryRewrite = $this->getCategoryRewriteRules();

		// Redirect from the old base.
		$categoryStructure = $wp_rewrite->get_category_permastruct(); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$categoryBase      = trim( str_replace( '%category%', '(.+)', $categoryStructure ), '/' ) . '$';

		// Add the rewrite rules.
		$categoryRewrite[ $categoryBase ] = 'index.php?aioseo_category_redirect=$matches[1]';

		return $categoryRewrite;
	}

	/**
	 * Get the rewrite rules for the category.
	 *
	 * @since 4.2.0
	 *
	 * @return array An array of category rewrite rules.
	 */
	private function getCategoryRewriteRules() {
		global $wp_rewrite; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		$categoryRewrite = [];
		$categories      = get_categories( [ 'hide_empty' => false ] );

		if ( empty( $categories ) ) {
			return $categoryRewrite;
		}

		$blogPrefix      = $this->getBlogPrefix();
		$paginationBase = $wp_rewrite->pagination_base; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		foreach ( $categories as $category ) {
			$nicename        = $this->getCategoryParents( $category ) . $category->slug;
			$categoryRewrite = $this->addCategoryRewrites( $categoryRewrite, $nicename, $blogPrefix, $paginationBase );

			// Also add the rules for uppercase.
			$filteredNicename = $this->convertEncodedToUppercase( $nicename );

			if ( $nicename !== $filteredNicename ) {
				$categoryRewrite = $this->addCategoryRewrites( $categoryRewrite, $filteredNicename, $blogPrefix, $paginationBase );
			}
		}

		return $categoryRewrite;
	}

	/**
	 * Get the blog prefix.
	 *
	 * @since 4.2.0
	 *
	 * @return string The prefix for the blog.
	 */
	private function getBlogPrefix() {
		$permalinkStructure = get_option( 'permalink_structure' );
		if (
			is_multisite() &&
			! is_subdomain_install() &&
			is_main_site() &&
			0 === strpos( $permalinkStructure, '/blog/' )
		) {
			return 'blog/';
		}

		return '';
	}

	/**
	 * Retrieve category parents with separator.
	 *
	 * @since 4.2.0
	 *
	 * @param  \WP_Term $category the category instance.
	 * @return string             A list of category parents.
	 */
	private function getCategoryParents( $category ) {
		if (
			$category->parent === $category->term_id ||
			absint( $category->parent ) < 1
		) {
			return '';
		}

		$parents = get_category_parents( $category->parent, false, '/', true );

		return is_wp_error( $parents ) ? '' : $parents;
	}

	/**
	 * Walks through category nicename and convert encoded parts
	 * into uppercase using $this->encode_to_upper().
	 *
	 * @since 4.2.0
	 *
	 * @param  string $nicename The encoded category string.
	 * @return string           The converted category string.
	 */
	private function convertEncodedToUppercase( $nicename ) {
		// Checks if name has any encoding in it.
		if ( false === strpos( $nicename, '%' ) ) {
			return $nicename;
		}

		$nicenames = explode( '/', $nicename );
		$nicenames = array_map( [ $this, 'convertToUppercase' ], $nicenames );

		return implode( '/', $nicenames );
	}

	/**
	 * Converts the encoded URI string to uppercase.
	 *
	 * @since 4.2.0
	 *
	 * @param  string $encoded The encoded category string.
	 * @return string          The converted category string.
	 */
	private function convertToUppercase( $encoded ) {
		if ( false === strpos( $encoded, '%' ) ) {
			return $encoded;
		}

		return strtoupper( $encoded );
	}

	/**
	 * Adds the required category rewrites rules.
	 *
	 * @since 4.2.0
	 *
	 * @param  array  $categoryRewrite  The current set of rules.
	 * @param  string $categoryNicename The category nicename.
	 * @param  string $blogPrefix       Multisite blog prefix.
	 * @param  string $paginationBase   WP_Query pagination base.
	 * @return array                    The added set of rules.
	 */
	private function addCategoryRewrites( $categoryRewrite, $categoryNicename, $blogPrefix, $paginationBase ) {
		$categoryRewrite[ $blogPrefix . '(' . $categoryNicename . ')/(?:feed/)?(feed|rdf|rss|rss2|atom)/?$' ]   = 'index.php?category_name=$matches[1]&feed=$matches[2]';
		$categoryRewrite[ $blogPrefix . '(' . $categoryNicename . ')/' . $paginationBase . '/?([0-9]{1,})/?$' ] = 'index.php?category_name=$matches[1]&paged=$matches[2]';
		$categoryRewrite[ $blogPrefix . '(' . $categoryNicename . ')/?$' ]                                      = 'index.php?category_name=$matches[1]';

		return $categoryRewrite;
	}

	/**
	 * Remove the category base from the category link.
	 *
	 * @since 4.2.0
	 *
	 * @param  string $link     Term link.
	 * @param  object $term     The current Term Object.
	 * @param  string $taxonomy The current Taxonomy.
	 * @return string           The modified term link.
	 */
	public function modifyTermLink( $link, $term = null, $taxonomy = '' ) {
		if ( 'category' !== $taxonomy ) {
			return $link;
		}

		$categoryBase = get_option( 'category_base' );
		if ( empty( $categoryBase ) ) {
			global $wp_rewrite; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			$categoryStructure = $wp_rewrite->get_category_permastruct(); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			$categoryBase      = trim( str_replace( '%category%', '', $categoryStructure ), '/' );
		}

		// Remove initial slash, if there is one (we remove the trailing slash in the regex replacement and don't want to end up short a slash).
		if ( '/' === substr( $categoryBase, 0, 1 ) ) {
			$categoryBase = substr( $categoryBase, 1 );
		}

		$categoryBase .= '/';

		return preg_replace( '`' . preg_quote( (string) $categoryBase, '`' ) . '`u', '', (string) $link, 1 );
	}

	/**
	 * Flush the rewrite rules.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	public function scheduleFlushRewrite() {
		aioseo()->options->flushRewriteRules();
	}
}Common/Main/Filters.php000064400000042522151536241170011013 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

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

use AIOSEO\Plugin\Common\Models;
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Abstract class that Pro and Lite both extend.
 *
 * @since 4.0.0
 */
abstract class Filters {
	/**
	 * The plugin we are checking.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $plugin;

	/**
	 * ID of the WooCommerce product that is being duplicated.
	 *
	 * @since 4.1.4
	 *
	 * @var integer
	 */
	private static $originalProductId;

	/**
	 * Construct method.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_filter( 'wp_optimize_get_tables', [ $this, 'wpOptimizeAioseoTables' ] );

		// This action needs to run on AJAX/cron for scheduled rewritten posts in Yoast Duplicate Post.
		add_action( 'duplicate_post_after_rewriting', [ $this, 'updateRescheduledPostMeta' ], 10, 2 );

		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		add_filter( 'plugin_row_meta', [ $this, 'pluginRowMeta' ], 10, 2 );
		add_filter( 'plugin_action_links_' . AIOSEO_PLUGIN_BASENAME, [ $this, 'pluginActionLinks' ], 10, 2 );

		// Genesis theme compatibility.
		add_filter( 'genesis_detect_seo_plugins', [ $this, 'genesisTheme' ] );

		// WeGlot compatibility.
		if ( isset( $_SERVER['REQUEST_URI'] ) && preg_match( '#(/default-sitemap\.xsl)$#i', (string) sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ) {
			add_filter( 'weglot_active_translation_before_treat_page', '__return_false' );
		}

		add_filter( 'wpml_tm_adjust_translation_fields', [ $this, 'defineMetaFieldsForWpml' ] );

		if ( isset( $_SERVER['REQUEST_URI'] ) && preg_match( '#(\.xml)$#i', (string) sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ) {
			add_filter( 'jetpack_boost_should_defer_js', '__return_false' );
		}

		// GoDaddy CDN compatibility.
		add_filter( 'wpaas_cdn_file_ext', [ $this, 'goDaddySitemapXml' ] );

		// Duplicate Post integration.
		add_action( 'dp_duplicate_post', [ $this, 'duplicatePost' ], 10, 2 );
		add_action( 'dp_duplicate_page', [ $this, 'duplicatePost' ], 10, 2 );
		add_action( 'woocommerce_product_duplicate_before_save', [ $this, 'scheduleDuplicateProduct' ], 10, 2 );
		add_action( 'add_post_meta', [ $this, 'rewriteAndRepublish' ], 10, 3 );

		// BBpress compatibility.
		add_action( 'init', [ $this, 'resetUserBBPress' ], -1 );
		add_filter( 'the_title', [ $this, 'maybeRemoveBBPressReplyFilter' ], 0, 2 );

		// Bypass the JWT Auth plugin's unnecessary restrictions. https://wordpress.org/plugins/jwt-auth/
		add_filter( 'jwt_auth_default_whitelist', [ $this, 'allowRestRoutes' ] );

		// Clear the site authors cache.
		add_action( 'profile_update', [ $this, 'clearAuthorsCache' ] );
		add_action( 'user_register', [ $this, 'clearAuthorsCache' ] );

		add_filter( 'aioseo_public_post_types', [ $this, 'removeInvalidPublicPostTypes' ] );
		add_filter( 'aioseo_public_taxonomies', [ $this, 'removeInvalidPublicTaxonomies' ] );

		add_action( 'admin_print_scripts', [ $this, 'removeEmojiDetectionScripts' ], 0 );

		// Disable Jetpack sitemaps module.
		if ( aioseo()->options->sitemap->general->enable ) {
			add_filter( 'jetpack_get_available_modules', [ $this, 'disableJetpackSitemaps' ] );
		}

		add_action( 'after_setup_theme', [ $this, 'removeHelloElementorDescriptionTag' ] );
		add_action( 'wp', [ $this, 'removeAvadaOgTags' ] );
		add_action( 'init', [ $this, 'declareAioseoFollowingConsentApi' ] );
	}

	/**
	 * Declares AIOSEO and its addons as following the Consent API.
	 *
	 * @since 4.6.5
	 *
	 * @return void
	 */
	public function declareAioseoFollowingConsentApi() {
		add_filter( 'wp_consent_api_registered_all-in-one-seo-pack/all_in_one_seo_pack.php', '__return_true' );
		add_filter( 'wp_consent_api_registered_all-in-one-seo-pack-pro/all_in_one_seo_pack.php', '__return_true' );

		foreach ( aioseo()->addons->getAddons() as $addon ) {
			if ( empty( $addon->installed ) || empty( $addon->basename ) ) {
				continue;
			}
			if ( isset( $addon->basename ) ) {
				add_filter( 'wp_consent_api_registered_' . $addon->basename, '__return_true' );
			}
		}
	}

	/**
	 * Removes emoji detection scripts on WP 6.2 which broke our Emojis.
	 *
	 * @since 4.3.4.1
	 *
	 * @return void
	 */
	public function removeEmojiDetectionScripts() {
		global $wp_version; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if ( version_compare( $wp_version, '6.2', '>=' ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
		}
	}

	/**
	 * Resets the current user if bbPress is active.
	 * We have to do this because our calls to wp_get_current_user() set the current user early and this breaks core functionality in bbPress.
	 *

	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	public function resetUserBBPress() {
		if ( function_exists( 'bbpress' ) ) {
			global $current_user; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			$current_user = null; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		}
	}

	/**
	 * Removes the bbPress title filter when adding a new reply with empty title to avoid fatal error.
	 *

	 *
	 * @since 4.3.1
	 *
	 * @param  string $title The post title.
	 * @param  int    $id    The post ID (optional - in order to fix an issue where other plugins/themes don't pass in the second arg).
	 * @return string        The post title.
	 */
	public function maybeRemoveBBPressReplyFilter( $title, $id = 0 ) {
		if (
			function_exists( 'bbp_get_reply_post_type' ) &&
			get_post_type( $id ) === bbp_get_reply_post_type() &&
			aioseo()->helpers->isScreenBase( 'post' )
		) {
			remove_filter( 'the_title', 'bbp_get_reply_title_fallback', 2 );
		}

		return $title;
	}

	/**
	 * Duplicates the model when duplicate post is triggered.
	 *
	 * @since 4.1.1
	 *
	 * @param  integer  $targetPostId The target post ID.
	 * @param  \WP_Post $sourcePost   The source post object.
	 * @return void
	 */
	public function duplicatePost( $targetPostId, $sourcePost = null ) {
		$sourcePostId     = ! empty( $sourcePost->ID ) ? $sourcePost->ID : $sourcePost;
		$sourceAioseoPost = Models\Post::getPost( $sourcePostId );
		$targetPost       = Models\Post::getPost( $targetPostId );

		$columns = $sourceAioseoPost->getColumns();
		foreach ( $columns as $column => $value ) {
			// Skip the ID column.
			if ( 'id' === $column ) {
				continue;
			}

			if ( 'post_id' === $column ) {
				$targetPost->$column = $targetPostId;
				continue;
			}

			$targetPost->$column = $sourceAioseoPost->$column;
		}

		$targetPost->save();
	}

	/**
	 * Duplicates the model when rewrite and republish is triggered.
	 *
	 * @since 4.3.4
	 *
	 * @param  integer $postId    The post ID.
	 * @param  string  $metaKey   The meta key.
	 * @param  mixed   $metaValue The meta value.
	 * @return void
	 */
	public function rewriteAndRepublish( $postId, $metaKey = '', $metaValue = '' ) {
		if ( '_dp_has_rewrite_republish_copy' !== $metaKey ) {
			return;
		}

		$originalPost = aioseo()->helpers->getPost( $postId );
		if ( ! is_object( $originalPost ) ) {
			return;
		}

		$this->duplicatePost( (int) $metaValue, $originalPost );
	}

	/**
	 * Updates the model when a post is republished.
	 * Yoast Duplicate Post doesn't do this since we store our data in a custom table.
	 *
	 * @since 4.6.7
	 *
	 * @param  int  $scheduledPostId The ID of the scheduled post.
	 * @param  int  $originalPostId  The ID of the original post.
	 * @return void
	 */
	public function updateRescheduledPostMeta( $scheduledPostId, $originalPostId ) {
		$this->duplicatePost( $originalPostId, $scheduledPostId );

		// Delete the AIOSEO post record for the scheduled post.
		$scheduledAioseoPost = Models\Post::getPost( $scheduledPostId );
		$scheduledAioseoPost->delete();
	}

	/**
	 * Schedules an action to duplicate our meta after the duplicated WooCommerce product has been saved.
	 *
	 * @since 4.1.4
	 *
	 * @param  \WC_Product $newProduct      The new, duplicated product.
	 * @param  \WC_Product $originalProduct The original product.
	 * @return void
	 */
	public function scheduleDuplicateProduct( $newProduct, $originalProduct = null ) {
		self::$originalProductId = $originalProduct->get_id();
		add_action( 'wp_insert_post', [ $this, 'duplicateProduct' ], 10, 2 );
	}

	/**
	 * Duplicates our meta for the new WooCommerce product.
	 *
	 * @since 4.1.4
	 *
	 * @param  integer  $postId The new post ID.
	 * @param  \WP_Post $post   The new post object.
	 * @return void
	 */
	public function duplicateProduct( $postId, $post = null ) {
		if ( ! self::$originalProductId || 'product' !== $post->post_type ) {
			return;
		}

		$this->duplicatePost( $postId, self::$originalProductId );
	}

	/**
	 * Disable SEO inside the Genesis theme if it's running.
	 *
	 * @since 4.0.3
	 *
	 * @param  array $array An array of checks.
	 * @return array        An array with our function added.
	 */
	public function genesisTheme( $array ) {
		if ( empty( $array ) || ! isset( $array['functions'] ) ) {
			return $array;
		}

		$array['functions'][] = 'aioseo';

		return $array;
	}

	/**
	 * Remove XML from the GoDaddy CDN so our urls remain intact.
	 *
	 * @since 4.0.5
	 *
	 * @param  array $extensions The original extensions list.
	 * @return array             The extensions list without xml.
	 */
	public function goDaddySitemapXml( $extensions ) {
		$key = array_search( 'xml', $extensions, true );
		unset( $extensions[ $key ] );

		return $extensions;
	}

	/**
	 * Registers our row meta for the plugins page.
	 *
	 * @since 4.0.0
	 *
	 * @param  array  $actions    List of existing actions.
	 * @param  string $pluginFile The plugin file.
	 * @return array              List of action links.
	 */
	abstract public function pluginRowMeta( $actions, $pluginFile = '' );

	/**
	 * Registers our action links for the plugins page.
	 *
	 * @since 4.0.0
	 *
	 * @param  array  $actions    List of existing actions.
	 * @param  string $pluginFile The plugin file.
	 * @return array              List of action links.
	 */
	abstract public function pluginActionLinks( $actions, $pluginFile = '' );

	/**
	 * Parses the action links.
	 *
	 * @since 4.0.0
	 *
	 * @param  array  $actions     The actions.
	 * @param  string $pluginFile  The plugin file.
	 * @param  array  $actionLinks The action links.
	 * @param  string $position    The position.
	 * @return array               The parsed actions.
	 */
	protected function parseActionLinks( $actions, $pluginFile, $actionLinks = [], $position = 'after' ) {
		if ( empty( $this->plugin ) ) {
			$this->plugin = AIOSEO_PLUGIN_BASENAME;
		}

		if ( $this->plugin === $pluginFile && ! empty( $actionLinks ) ) {
			foreach ( $actionLinks as $key => $value ) {
				$link = [
					$key => sprintf(
						'<a href="%1$s" %2$s target="_blank">%3$s</a>',
						esc_url( $value['url'] ),
						isset( $value['title'] ) ? 'title="' . esc_attr( $value['title'] ) . '"' : '',
						$value['label']
					)
				];

				$actions = 'after' === $position ? array_merge( $actions, $link ) : array_merge( $link, $actions );
			}
		}

		return $actions;
	}

	/**
	 * Add our routes to this plugins allow list.
	 *
	 * @since 4.1.4
	 *
	 * @param  array $allowList The original list.
	 * @return array            The modified list.
	 */
	public function allowRestRoutes( $allowList ) {
		return array_merge( $allowList, [
			'/aioseo/'
		] );
	}

	/**
	 * Clear the site authors cache when user is updated or registered.
	 *
	 * @since 4.1.8
	 *
	 * @return void
	 */
	public function clearAuthorsCache() {
		aioseo()->core->cache->delete( 'site_authors' );
	}

	/**
	 * Filters out post types that aren't really public when getPublicPostTypes() is called.
	 *
	 * @since 4.1.9
	 *
	 * @param  array[object]|array[string] $postTypes The post types.
	 * @return array[object]|array[string]            The filtered post types.
	 */
	public function removeInvalidPublicPostTypes( $postTypes ) {
		$postTypesToRemove = [
			'fusion_element', // Avada
			'elementor_library',
			'redirect_rule', // Safe Redirect Manager
			'seedprod',
			'tcb_lightbox',

			// Thrive Themes internal post types.
			'tva_module',
			'tvo_display',
			'tvo_capture',
			'tva_module',
			'tve_lead_1c_signup',
			'tve_form_type',
			'tvd_login_edit',
			'tve_global_cond_set',
			'tve_cond_display',
			'tve_lead_2s_lightbox',
			'tcb_symbol',
			'td_nm_notification',
			'tvd_content_set',
			'tve_saved_lp',
			'tve_notifications',
			'tve_user_template',
			'tve_video_data',
			'tva_course_type',
			'tva-acc-restriction',
			'tva_course_overview',
			'tve_ult_schedule',
			'tqb_optin',
			'tqb_splash',
			'tva_certificate',
			'tva_course_overview',

			// BuddyPress post types.
			BuddyPressIntegration::getEmailCptSlug()
		];

		foreach ( $postTypes as $index => $postType ) {
			if ( is_string( $postType ) && in_array( $postType, $postTypesToRemove, true ) ) {
				unset( $postTypes[ $index ] );
				continue;
			}

			if ( is_array( $postType ) && in_array( $postType['name'], $postTypesToRemove, true ) ) {
				unset( $postTypes[ $index ] );
			}
		}

		return array_values( $postTypes );
	}

	/**
	 * Filters out taxonomies that aren't really public when getPublicTaxonomies() is called.
	 *
	 * @since 4.2.4
	 *
	 * @param  array[object]|array[string] $taxonomies The taxonomies.
	 * @return array[object]|array[string]             The filtered taxonomies.
	 */
	public function removeInvalidPublicTaxonomies( $taxonomies ) {
		$taxonomiesToRemove = [
			'fusion_tb_category',
			'element_category',
			'template_category',

			// Thrive Themes internal taxonomies.
			'tcb_symbols_tax'
		];

		foreach ( $taxonomies as $index => $taxonomy ) {
			if ( is_string( $taxonomy ) && in_array( $taxonomy, $taxonomiesToRemove, true ) ) {
				unset( $taxonomies[ $index ] );
				continue;
			}

			if ( is_array( $taxonomy ) && in_array( $taxonomy['name'], $taxonomiesToRemove, true ) ) {
				unset( $taxonomies[ $index ] );
			}
		}

		return array_values( $taxonomies );
	}

	/**
	 * Disable Jetpack sitemaps module.
	 *
	 * @since 4.2.2
	 */
	public function disableJetpackSitemaps( $active ) {
		unset( $active['sitemaps'] );

		return $active;
	}

	/**
	 * Dequeues third-party scripts from the other plugins or themes that crashes our menu pages.
	 *
	 * @since   4.1.9
	 * @version 4.3.1
	 *
	 * @return void
	 */
	public function dequeueThirdPartyAssets() {
		// TagDiv Opt-in Builder plugin.
		wp_dequeue_script( 'tds_js_vue_files_last' );

		// MyListing theme.
		if ( function_exists( 'mylisting' ) ) {
			wp_dequeue_script( 'vuejs' );
			wp_dequeue_script( 'theme-script-vendor' );
			wp_dequeue_script( 'theme-script-main' );
		}

		// Voxel theme.
		if ( class_exists( '\Voxel\Controllers\Assets_Controller' ) ) {
			wp_dequeue_script( 'vue' );
			wp_dequeue_script( 'vx:backend.js' );
		}

		// Meta tags for seo plugin.
		if ( class_exists( '\Pagup\MetaTags\Settings' ) ) {
			wp_dequeue_script( 'pmt__vuejs' );
			wp_dequeue_script( 'pmt__script' );
		}

		// Plugin: Wpbingo Core (By TungHV).
		if ( strpos( wp_styles()->query( 'bwp-lookbook-css' )->src ?? '', 'wpbingo' ) !== false ) {
			wp_dequeue_style( 'bwp-lookbook-css' );
		}
	}

	/**
	 * Dequeues third-party scripts from the other plugins or themes that crashes our menu pages.
	 *
	 * @version 4.3.2
	 *
	 * @return void
	 */
	public function dequeueThirdPartyAssetsEarly() {
		// Disables scripts for plugins StmMotorsExtends and StmPostType.
		if ( class_exists( 'STM_Metaboxes' ) ) {
			remove_action( 'admin_enqueue_scripts', [ 'STM_Metaboxes', 'wpcfto_scripts' ] );
		}

		// Disables scripts for LearnPress plugin.
		if ( function_exists( 'learn_press_admin_assets' ) ) {
			remove_action( 'admin_enqueue_scripts', [ learn_press_admin_assets(), 'load_scripts' ] );
		}
	}

	/**
	 * Removes the duplicate meta description tag from the Hello Elementor theme.
	 *
	 * @since 4.4.3
	 *
	 * @link https://developers.elementor.com/docs/hello-elementor-theme/hello_elementor_add_description_meta_tag/
	 *
	 * @return void
	 */
	public function removeHelloElementorDescriptionTag() {
		remove_action( 'wp_head', 'hello_elementor_add_description_meta_tag' );
	}

	/**
	 * Removes the Avada OG tags.
	 *
	 * @since 4.6.5
	 *
	 * @return void
	 */
	public function removeAvadaOgTags() {
		if ( function_exists( 'Avada' ) ) {
			$avada = Avada();
			if ( is_object( $avada->head ?? null ) ) {
				remove_action( 'wp_head', [ $avada->head, 'insert_og_meta' ], 5 );
			}
		}
	}

	/**
	 * Prevent WP-Optimize from deleting our tables.
	 *
	 * @since 4.4.5
	 *
	 * @param  array $tables List of tables.
	 * @return array         Filtered tables.
	 */
	public function wpOptimizeAioseoTables( $tables ) {
		foreach ( $tables as &$table ) {
			if (
				is_object( $table ) &&
				property_exists( $table, 'Name' ) &&
				false !== stripos( $table->Name, 'aioseo_' )
			) {
				$table->is_using       = true;
				$table->can_be_removed = false;
			}
		}

		return $tables;
	}

	/**
	 * Defines specific meta fields for WPML so character limits can be applied when auto-translating fields.
	 *
	 * @since 4.8.3.2
	 *
	 * @param  array $fields The fields.
	 * @return array         The modified fields.
	 */
	public function defineMetaFieldsForWpml( $fields ) {
		foreach ( $fields as &$field ) {
			if ( empty( $field['field_type'] ) ) {
				continue;
			}

			$fieldKey = strtolower( preg_replace( '/^(field-)(.*)(-0)$/', '$2', $field['field_type'] ) );

			switch ( $fieldKey ) {
				case '_aioseo_title':
					$field['purpose'] = 'seo_title';
					break;
				case '_aioseo_description':
					$field['purpose'] = 'seo_meta_description';
					break;
			}
		}

		return $fields;
	}
}Common/Main/Head.php000064400000006613151536241200010237 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

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

use AIOSEO\Plugin\Common\Meta;

/**
 * Outputs anything we need to the head of the site.
 *
 * @since 4.0.0
 */
class Head {
	/**
	 * The page title.
	 *
	 * @since 4.0.5
	 *
	 * @var string
	 */
	private static $pageTitle = null;

	/**
	 * Title class instance.
	 *
	 * @since 4.3.9
	 *
	 * @var Title
	 */
	private $title;

	/**
	 * Links class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Meta\Links
	 */
	protected $links = null;

	/**
	 * Keywords class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Meta\Keywords
	 */
	protected $keywords = null;

	/**
	 * Verification class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Meta\SiteVerification
	 */
	protected $verification = null;

	/**
	 * The views to output.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	protected $views = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'wp', [ $this, 'registerTitleHooks' ], 1000 );
		add_action( 'wp_head', [ $this, 'wpHead' ], 1 );

		$this->title        = new Title();
		$this->links        = new Meta\Links();
		$this->keywords     = new Meta\Keywords();
		$this->verification = new Meta\SiteVerification();
		$this->views        = [
			'meta'    => AIOSEO_DIR . '/app/Common/Views/main/meta.php',
			'social'  => AIOSEO_DIR . '/app/Common/Views/main/social.php',
			'schema'  => AIOSEO_DIR . '/app/Common/Views/main/schema.php',
			'clarity' => AIOSEO_DIR . '/app/Common/Views/main/clarity.php'
		];
	}

	/**
	 * Registers our title hooks.
	 *
	 * @since 4.0.5
	 *
	 * @return void
	 */
	public function registerTitleHooks() {
		if ( apply_filters( 'aioseo_disable', false ) || apply_filters( 'aioseo_disable_title_rewrites', false ) ) {
			return;
		}

		add_filter( 'pre_get_document_title', [ $this, 'getTitle' ], 99999 );
		add_filter( 'wp_title', [ $this, 'getTitle' ], 99999 );
		if ( ! current_theme_supports( 'title-tag' ) ) {
			add_action( 'template_redirect', [ $this->title, 'startOutputBuffering' ], 99999 );
			add_action( 'wp_head', [ $this->title, 'endOutputBuffering' ], 99999 );
		}
	}

	/**
	 * Outputs the head.
	 *
	 * @since 4.0.5
	 * @version 4.6.1
	 *
	 * @return void
	 */
	public function wpHead() {
		$included = new Meta\Included();
		if ( is_admin() || wp_doing_ajax() || wp_doing_cron() || ! $included->isIncluded() ) {
			return;
		}

		$this->output();
	}

	/**
	 * Returns the page title.
	 *
	 * @since 4.0.5
	 *
	 * @param  string $wpTitle   The original page title from WordPress.
	 * @return string $pageTitle The page title.
	 */
	public function getTitle( $wpTitle = '' ) {
		if ( null !== self::$pageTitle ) {
			return self::$pageTitle;
		}
		self::$pageTitle = aioseo()->meta->title->filterPageTitle( $wpTitle );

		return self::$pageTitle;
	}

	/**
	 * The output function itself.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function output() {
		remove_action( 'wp_head', 'rel_canonical' );

		$views = apply_filters( 'aioseo_meta_views', $this->views );
		if ( empty( $views ) ) {
			return;
		}

		echo "\n\t\t<!-- " . sprintf(
			'%1$s %2$s',
			esc_html( AIOSEO_PLUGIN_NAME ),
			aioseo()->helpers->getAioseoVersion() // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		) . " - aioseo.com -->\n";

		foreach ( $views as $view ) {
			require $view;
		}

		echo "\t\t<!-- " . esc_html( AIOSEO_PLUGIN_NAME ) . " -->\n\n";
	}
}Common/Main/Main.php000064400000003016151536241200010254 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Abstract class that Pro and Lite both extend.
 *
 * @since 4.0.0
 */
class Main {
	/**
	 * Construct method.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		new Media();
		new QueryArgs();

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueTranslations' ] );
		add_action( 'wp_enqueue_scripts', [ $this, 'enqueueFrontEndAssets' ] );
		add_action( 'admin_footer', [ $this, 'adminFooter' ] );
	}

	/**
	 * Enqueues the translations seperately so it can be called from anywhere.
	 *
	 * @since 4.1.9
	 *
	 * @return void
	 */
	public function enqueueTranslations() {
		aioseo()->core->assets->load( 'src/vue/standalone/app/main.js', [], [
			'translations' => aioseo()->helpers->getJedLocaleData( 'all-in-one-seo-pack' )
		], 'aioseoTranslations' );
	}

	/**
	 * Enqueue styles on the front-end.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function enqueueFrontEndAssets() {
		$canManageSeo = apply_filters( 'aioseo_manage_seo', 'aioseo_manage_seo' );
		if (
			! aioseo()->helpers->isAdminBarEnabled() ||
			! ( current_user_can( $canManageSeo ) || aioseo()->access->canManage() )
		) {
			return;
		}

		aioseo()->core->assets->enqueueCss( 'src/vue/assets/scss/app/admin-bar.scss' );
	}

	/**
	 * Enqueue the footer file to let vue attach.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function adminFooter() {
		echo '<div id="aioseo-admin"></div>';
	}
}Common/Main/Media.php000064400000002224151536241200010407 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

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

/**
 * Media class.
 *
 * @since 4.0.0
 */
class Media {
	/**
	 * Construct method.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'template_redirect', [ $this, 'attachmentRedirect' ], 1 );
	}

	/**
	 * If the user wants to redirect attachment pages, this is where we do it.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function attachmentRedirect() {
		if ( ! is_attachment() ) {
			return;
		}

		if (
			! aioseo()->dynamicOptions->searchAppearance->postTypes->has( 'attachment' )
		) {
			return;
		}

		$redirect = aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls;
		if ( 'disabled' === $redirect ) {
			return;
		}

		if ( 'attachment' === $redirect ) {
			$url = wp_get_attachment_url( get_queried_object_id() );
			if ( empty( $url ) ) {
				return;
			}

			return wp_safe_redirect( $url, 301, AIOSEO_PLUGIN_SHORT_NAME );
		}

		global $post;
		if ( ! empty( $post->post_parent ) ) {
			wp_safe_redirect( urldecode( get_permalink( $post->post_parent ) ), 301 );
		}
	}
}Common/Main/PreUpdates.php000064400000004065151536241200011451 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

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

/**
 * This class contains pre-updates necessary for the next updates class to run.
 *
 * @since 4.1.5
 */
class PreUpdates {
	/**
	 * Class constructor.
	 *
	 * @since 4.1.5
	 */
	public function __construct() {
		// We don't want an AJAX request check here since the plugin might be installed/activated for the first time via AJAX (e.g. EDD/BLC).
		// If that's the case, the cache table needs to be created before the activation hook runs.
		if ( wp_doing_cron() ) {
			return;
		}

		$lastActiveVersion = aioseo()->internalOptions->internal->lastActiveVersion;
		if ( aioseo()->version !== $lastActiveVersion ) {
			// Bust the table/columns cache so that we can start the update migrations with a fresh slate.
			aioseo()->internalOptions->database->installedTables = '';
		}

		if ( version_compare( $lastActiveVersion, '4.1.5', '<' ) ) {
			$this->createCacheTable();
		}

		if ( version_compare( $lastActiveVersion, AIOSEO_VERSION, '<' ) ) {
			aioseo()->core->cache->clear();
		}
	}

	/**
	 * Creates a new aioseo_cache table.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	public function createCacheTable() {
		$db             = aioseo()->core->db->db;
		$charsetCollate = '';

		if ( ! empty( $db->charset ) ) {
			$charsetCollate .= "DEFAULT CHARACTER SET {$db->charset}";
		}
		if ( ! empty( $db->collate ) ) {
			$charsetCollate .= " COLLATE {$db->collate}";
		}

		$tableName = aioseo()->core->cache->getTableName();
		if ( ! aioseo()->core->db->tableExists( $tableName ) ) {
			$tableName = $db->prefix . $tableName;

			aioseo()->core->db->execute(
				"CREATE TABLE {$tableName} (
					`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					`key` varchar(80) NOT NULL,
					`value` longtext NOT NULL,
					`expiration` datetime NULL,
					`created` datetime NOT NULL,
					`updated` datetime NOT NULL,
					PRIMARY KEY (`id`),
					UNIQUE KEY ndx_aioseo_cache_key (`key`),
					KEY ndx_aioseo_cache_expiration (`expiration`)
				) {$charsetCollate};"
			);
		}
	}
}Common/Main/QueryArgs.php000064400000013015151536241200011312 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

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

use AIOSEO\Plugin\Common\Models\CrawlCleanupLog;
use AIOSEO\Plugin\Common\Models\CrawlCleanupBlockedArg;

/**
 * Query arguments class.
 *
 * @since   4.2.1
 * @version 4.5.8
 */
class QueryArgs {
	/**
	 * Construct method.
	 *
	 * @since 4.2.1
	 */
	public function __construct() {
		if (
			is_admin() ||
			aioseo()->helpers->isWpLoginPage() ||
			aioseo()->helpers->isAjaxCronRestRequest() ||
			aioseo()->helpers->isDoingWpCli()
		) {
			return;
		}

		add_action( 'template_redirect', [ $this, 'maybeRemoveQueryArgs' ], 1 );

		$this->removeReplyToCom();
	}

	/**
	 * Check if we can remove query args.
	 *
	 * @since 4.5.8
	 *
	 * @return boolean True if the query args can be removed.
	 */
	private function canRemoveQueryArgs() {
		if (
			! aioseo()->options->searchAppearance->advanced->blockArgs->enable ||
			is_user_logged_in() ||
			is_admin() ||
			is_robots() ||
			get_query_var( 'aiosp_sitemap_path' ) ||
			empty( $_GET ) // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		) {
			return false;
		}

		if ( is_singular() ) {
			global $post;
			$thePost = aioseo()->helpers->getPost( $post->ID );

			// Leave the preview query arguments intact.
			if (
				// phpcs:disable phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
				isset( $_GET['preview'] ) &&
				isset( $_GET['preview_nonce'] ) &&
				// phpcs:enable
				wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['preview_nonce'] ) ), 'post_preview_' . $thePost->ID ) &&
				current_user_can( 'edit_post', $thePost->ID )
			) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Maybe remove query args.
	 *
	 * @since 4.5.8
	 *
	 * @return void
	 */
	public function maybeRemoveQueryArgs() {
		if ( ! $this->canRemoveQueryArgs() ) {
			return;
		}

		$currentRequest = aioseo()->helpers->getRequestUrl();

		// Remove the home path from the url for subfolder installs.
		$currentRequest       = aioseo()->helpers->excludeHomePath( $currentRequest );
		$currentRequestParsed = wp_parse_url( $currentRequest );

		// No query args? Never mind!
		if ( empty( $currentRequestParsed['query'] ) ) {
			return;
		}

		parse_str( $currentRequestParsed['query'], $currentRequestQueryArgs );
		$notAllowed          = [];
		$recognizedQueryLogs = [];

		foreach ( $currentRequestQueryArgs as $key => $value ) {
			if ( ! is_string( $value ) ) {
				continue;
			}
			$this->addQueryLog( $currentRequestParsed['path'], $key, $value );

			$blocked = CrawlCleanupBlockedArg::getByKeyValue( $key, null );
			if ( ! $blocked->exists() ) {
				$blocked = CrawlCleanupBlockedArg::getByKeyValue( $key, $value );
			}

			if ( ! $blocked->exists() ) {
				$blocked = CrawlCleanupBlockedArg::matchRegex( $key, $value );
			}

			if ( $blocked->exists() ) {
				$queryArg = $key . ( $value ? '=' . $value : null );
				$notAllowed[] = $queryArg;
				$blocked->addHit();
				continue;
			}

			$recognizedQueryLogs[ $key ] = empty( $value ) ? true : $value;
		}

		if ( ! empty( $notAllowed ) ) {
			$newUrl = home_url( $currentRequestParsed['path'] );

			header( 'Content-Type: redirect', true );
			header_remove( 'Content-Type' );
			header_remove( 'Last-Modified' );
			header_remove( 'X-Pingback' );

			wp_safe_redirect( add_query_arg( $recognizedQueryLogs, $newUrl ), 301, AIOSEO_PLUGIN_SHORT_NAME . ' Crawl Cleanup' );
			exit;
		}
	}

	/**
	 * Remove ?replytocom.
	 *
	 * @since 4.5.8
	 *
	 * @return void
	 */
	private function removeReplyToCom() {
		if ( ! apply_filters( 'aioseo_remove_reply_to_com', true ) ) {
			return;
		}

		add_filter( 'comment_reply_link', [ $this, 'removeReplyToComLink' ] );
		add_action( 'template_redirect', [ $this, 'replyToComRedirect' ], 1 );
	}

	/**
	 * Remove ?replytocom.
	 *
	 * @since 4.7.3
	 *
	 * @param  string $link The comment link as a string.
	 * @return string       The modified link.
	 */
	public function removeReplyToComLink( $link ) {
		return preg_replace( '`href=(["\'])(?:.*(?:\?|&|&#038;)replytocom=(\d+)#respond)`', 'href=$1#comment-$2', (string) $link );
	}

	/**
	 * Redirects out the ?replytocom variables.
	 *
	 * @since 4.7.3
	 *
	 * @return void
	 */
	public function replyToComRedirect() {
		// phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		$replyToCom = absint( sanitize_text_field( wp_unslash( $_GET['replytocom'] ?? null ) ) );

		if ( ! empty( $replyToCom ) && is_singular() ) {
			$url = get_permalink( $GLOBALS['post']->ID );
			if ( isset( $_SERVER['QUERY_STRING'] ) ) {
				$queryString = remove_query_arg( 'replytocom', sanitize_text_field( wp_unslash( $_SERVER['QUERY_STRING'] ) ) );
				if ( ! empty( $queryString ) ) {
					$url = add_query_arg( [], $url ) . '?' . $queryString;
				}
			}
			$url = add_query_arg( [], $url ) . '#comment-' . $replyToCom;

			wp_safe_redirect( $url, 301, AIOSEO_PLUGIN_SHORT_NAME );
			exit;
		}
	}

	/**
	 * Add query args log.
	 *
	 * @since 4.5.8
	 *
	 * @param string $path  A String of the path to create a slug.
	 * @param string $key   A String of key from query arg.
	 * @param string $value A String of value from query arg.
	 * @return void
	 */
	private function addQueryLog( $path, $key, $value = null ) {
		$slug = $path . '?' . $key . ( 0 < strlen( $value ) ? '=' . $value : '' );
		$log  = CrawlCleanupLog::getBySlug( $slug );

		$data = [
			'slug'  => $slug,
			'key'   => $key,
			'value' => $value
		];

		$log->set( $data );
		$log->create();
	}
}Common/Main/Title.php000064400000004366151536241200010462 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

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

/**
 * Document Title class.
 *
 * @since 4.3.9
 */
class Title {
	/**
	 * Keeps the buffer level.
	 *
	 * @since 4.3.9
	 *
	 * @var int
	 */
	private $bufferLevel = 0;

	/**
	 * Starts the output buffering.
	 *
	 * @since   4.3.2
	 * @version 4.3.9
	 *
	 * @return void
	 */
	public function startOutputBuffering() {
		ob_start();

		$this->bufferLevel = ob_get_level();
	}

	/**
	 * Ends the output buffering.
	 *
	 * @since   4.3.2
	 * @version 4.3.9
	 *
	 * @return void
	 */
	public function endOutputBuffering() {
		// Bail if our code didn't start the output buffering at all.
		if ( 0 === $this->bufferLevel ) {
			return;
		}

		/**
		 * In case the current buffer level is different from the one we kept earlier, then: either a plugin started a new buffer or ended our buffer earlier.
		 * If that's the case, we can't properly rewrite the document title anymore since we don't know what buffer content we'd parse below.
		 * In order to avoid conflicts/errors (blank/broken pages), we just bail.
		 * If we bail, the page won't have the title set by AIOSEO, but this can be fixed if the active theme starts supporting the "title-tag" feature {@link https://codex.wordpress.org/Title_Tag}.
		 */
		if ( ob_get_level() !== $this->bufferLevel ) {
			return;
		}

		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo $this->rewriteTitle( (string) ob_get_clean() );
	}

	/**
	 * Replace the page document title.
	 *
	 * @since   4.0.5
	 * @version 4.3.2
	 * @version 4.3.9
	 *
	 * @param  string $content The buffer content.
	 * @return string          The rewritten title.
	 */
	private function rewriteTitle( $content ) {
		if ( strpos( $content, '<!-- All in One SEO' ) === false ) {
			return $content;
		}

		// Remove all existing title tags.
		$content   = preg_replace( '#<title.*?/title>#s', '', (string) $content );
		$pageTitle = aioseo()->helpers->escapeRegexReplacement( aioseo()->head->getTitle() );

		// Return new output with our new title tag included in our own comment block.
		return preg_replace( '/(<!--\sAll\sin\sOne\sSEO[a-z0-9\s.]+\s-\saioseo\.com\s-->)/i', "$1\r\n\t\t<title>$pageTitle</title>", (string) $content, 1 );
	}
}Common/Main/Uninstall.php000064400000006005151536241200011342 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

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

use AIOSEO\Plugin\Common\Utils;

/**
 * Handles plugin deinstallation.
 *
 * @since 4.8.1
 */
class Uninstall {
	/**
	 * Removes all data.
	 *
	 * @since 4.8.1
	 *
	 * @param  bool $force Whether we should ignore the uninstall option or not. We ignore it when we reset all data via the Debug Panel.
	 * @return void
	 */
	public function dropData( $force = false ) {
		// Don't call `aioseo()->options` as it's not loaded during uninstall.
		$aioseoOptions = get_option( 'aioseo_options', '' );
		$aioseoOptions = json_decode( $aioseoOptions, true );

		// Confirm that user has decided to remove all data, otherwise stop.
		if (
			! $force &&
			empty( $aioseoOptions['advanced']['uninstall'] )
		) {
			return;
		}

		// Drop our custom tables.
		$this->uninstallDb();

		// Delete all our custom capabilities.
		$this->uninstallCapabilities();
	}

	/**
	 * Removes all our tables and options.
	 *
	 * @since 4.2.3
	 * @version 4.8.1 Moved from Core to Uninstall.
	 *
	 * @return void
	 */
	private function uninstallDb() {
		// Delete all our custom tables.
		global $wpdb;

		// phpcs:disable WordPress.DB.DirectDatabaseQuery
		foreach ( aioseo()->core->getDbTables() as $tableName ) {
			$wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $tableName ) );
		}

		// Delete all AIOSEO Locations and Location Categories.
		$wpdb->delete( $wpdb->posts, [ 'post_type' => 'aioseo-location' ], [ '%s' ] );
		$wpdb->delete( $wpdb->term_taxonomy, [ 'taxonomy' => 'aioseo-location-category' ], [ '%s' ] );

		// Delete all the plugin settings.
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", 'aioseo\_%' ) );

		// Remove any transients we've left behind.
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", '\_aioseo\_%' ) );
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", 'aioseo\_%' ) );

		// Delete all entries from the action scheduler table.
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}actionscheduler_actions WHERE hook LIKE %s", 'aioseo\_%' ) );
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}actionscheduler_groups WHERE slug = %s", 'aioseo' ) );
		// phpcs:enable
	}

	/**
	 * Removes all our custom capabilities.
	 *
	 * @since 4.8.1
	 *
	 * @return void
	 */
	private function uninstallCapabilities() {
		$access             = new Utils\Access();
		$customCapabilities = $access->getCapabilityList() ?? [];
		$roles              = aioseo()->helpers->getUserRoles();

		// Loop through roles and remove custom capabilities.
		foreach ( $roles as $roleName => $roleInfo ) {
			$role = get_role( $roleName );

			if ( $role ) {
				$role->remove_cap( 'aioseo_admin' );
				$role->remove_cap( 'aioseo_manage_seo' );

				foreach ( $customCapabilities as $capability ) {
					$role->remove_cap( $capability );
				}
			}
		}

		remove_role( 'aioseo_manager' );
		remove_role( 'aioseo_editor' );
	}
}Common/Main/Updates.php000064400000204663151536241200011010 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Updater class.
 *
 * @since 4.0.0
 */
class Updates {

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'aioseo_v4_migrate_post_schema', [ $this, 'migratePostSchema' ] );
		add_action( 'aioseo_v4_migrate_post_schema_default', [ $this, 'migratePostSchemaDefault' ] );
		add_action( 'aioseo_v419_remove_revision_records', [ $this, 'removeRevisionRecords' ] );

		if (
			wp_doing_ajax() ||
			wp_doing_cron()
		) {
			return;
		}

		add_action( 'init', [ $this, 'init' ], 1001 );
		add_action( 'init', [ $this, 'runUpdates' ], 1002 );
		add_action( 'init', [ $this, 'updateLatestVersion' ], 3000 );
	}

	/**
	 * Sets the latest active version if it is not set yet.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function init() {
		if ( '0.0' !== aioseo()->internalOptions->internal->lastActiveVersion ) {
			return;
		}

		// It's possible the user may not have capabilities. Let's add them now.
		aioseo()->access->addCapabilities();

		$oldOptions = get_option( 'aioseop_options' );
		if ( ! empty( $oldOptions['last_active_version'] ) ) {
			aioseo()->internalOptions->internal->lastActiveVersion = $oldOptions['last_active_version'];
		}

		$this->addInitialCustomTablesForV4();
		add_action( 'wp_loaded', [ $this, 'setDefaultSocialImages' ], 1001 );
	}

	/**
	 * Runs our migrations.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function runUpdates() {
		$lastActiveVersion = aioseo()->internalOptions->internal->lastActiveVersion;
		// Don't run updates if the last active version is the same as the current version.
		if ( aioseo()->version === $lastActiveVersion ) {
			// Allow addons to run their updates.
			do_action( 'aioseo_run_updates', $lastActiveVersion );

			return;
		}

		// Try to acquire the lock.
		if ( ! aioseo()->core->db->acquireLock( 'aioseo_run_updates_lock', 0 ) ) {
			// If we couldn't acquire the lock, exit early without doing anything.
			// This means another process is already running updates.
			return;
		}

		// The dynamic options have not yet fully loaded, so let's refresh here to force that to happen.
		aioseo()->dynamicOptions->refresh(); // TODO: Check if we still need this since it already runs on 999 in the main AIOSEO file.

		if ( version_compare( $lastActiveVersion, '4.0.5', '<' ) ) {
			$this->addImageScanDateColumn();
		}

		if ( version_compare( $lastActiveVersion, '4.0.6', '<' ) ) {
			$this->disableTwitterUseOgDefault();
			$this->updateMaxImagePreviewDefault();
		}

		if ( ! aioseo()->pro && version_compare( $lastActiveVersion, '4.0.6', '=' ) && 'posts' !== get_option( 'show_on_front' ) ) {
			aioseo()->migration->helpers->redoMigration();
		}

		if ( version_compare( $lastActiveVersion, '4.0.13', '<' ) ) {
			$this->removeDuplicateRecords();
		}

		if ( version_compare( $lastActiveVersion, '4.0.17', '<' ) ) {
			$this->removeLocationColumn();
		}

		if ( version_compare( $lastActiveVersion, '4.1.2', '<' ) ) {
			$this->clearProductImages();
		}

		if ( version_compare( $lastActiveVersion, '4.1.3', '<' ) ) {
			$this->addNotificationsNewColumn();
			$this->noindexWooCommercePages();
			$this->accessControlNewCapabilities();
		}

		if ( version_compare( $lastActiveVersion, '4.1.3.3', '<' ) ) {
			$this->accessControlNewCapabilities();
		}

		if ( version_compare( $lastActiveVersion, '4.1.4.3', '<' ) ) {
			$this->migrateDynamicSettings();
		}

		if ( version_compare( $lastActiveVersion, '4.1.5', '<' ) ) {
			aioseo()->actionScheduler->unschedule( 'aioseo_cleanup_action_scheduler' );
			// Schedule routine to remove our old transients from the options table.
			aioseo()->actionScheduler->scheduleSingle( aioseo()->core->cachePrune->getOptionCacheCleanAction(), MINUTE_IN_SECONDS );

			// Refresh with new Redirects capability.
			$this->accessControlNewCapabilities();

			// Regenerate the sitemap if using a static one to update the data for the new stylesheets.
			aioseo()->sitemap->regenerateStaticSitemap();

			$this->fixSchemaTypeDefault();
		}

		if ( version_compare( $lastActiveVersion, '4.1.6', '<' ) ) {
			// Remove the recurring scheduled action for notifications.
			aioseo()->actionScheduler->unschedule( 'aioseo_admin_notifications_update' );

			$this->migrateOgTwitterImageColumns();

			// Set the OG data to false for current installs.
			aioseo()->options->social->twitter->general->useOgData = false;
		}

		if ( version_compare( $lastActiveVersion, '4.1.8', '<' ) ) {
			$this->addLimitModifiedDateColumn();

			// Refresh with new Redirects Page capability.
			$this->accessControlNewCapabilities();
		}

		if ( version_compare( $lastActiveVersion, '4.1.9', '<' ) ) {
			$this->fixTaxonomyTags();
			$this->scheduleRemoveRevisionsRecords();
		}

		if ( version_compare( $lastActiveVersion, '4.0.0', '>=' ) && version_compare( $lastActiveVersion, '4.2.0', '<' ) ) {
			$this->migrateDeprecatedRunShortcodesSetting();
		}

		if ( version_compare( $lastActiveVersion, '4.2.1', '<' ) ) {
			// Force WordPress to flush the rewrite rules.
			aioseo()->options->flushRewriteRules();

			Models\Notification::deleteNotificationByName( 'deprecated-filters' );
			Models\Notification::deleteNotificationByName( 'deprecated-filters-v2' );
		}

		if ( version_compare( $lastActiveVersion, '4.2.2', '<' ) ) {
			aioseo()->internalOptions->database->installedTables = '';

			$this->addOptionsColumn();
			$this->removeTabsColumn();
			$this->migrateUserContactMethods();

			// Unschedule any static sitemap regeneration actions to remove any that failed and are still in-progress as a result.
			aioseo()->actionScheduler->unschedule( 'aioseo_static_sitemap_regeneration' );
		}

		if ( version_compare( $lastActiveVersion, '4.2.4', '<' ) ) {
			$this->addNotificationsAddonColumn();
		}

		if ( version_compare( $lastActiveVersion, '4.2.5', '<' ) ) {
			$this->addSchemaColumn();
			$this->schedulePostSchemaMigration();
		}

		if ( version_compare( $lastActiveVersion, '4.2.4.2', '>' ) && version_compare( $lastActiveVersion, '4.2.6', '<' ) ) {
			// The default graphs only need to be remigrated if the user was on 4.2.5 or 4.2.5.1.
			$this->schedulePostSchemaDefaultMigration();
		}

		if ( version_compare( $lastActiveVersion, '4.2.8', '<' ) ) {
			$this->migrateDashboardWidgetsOptions();
		}

		if ( version_compare( $lastActiveVersion, '4.3.6', '<' ) ) {
			$this->addPrimaryTermColumn();
		}

		if ( version_compare( $lastActiveVersion, '4.3.9', '<' ) ) {
			$this->migratePriorityColumn();
		}

		if ( version_compare( $lastActiveVersion, '4.4.2', '<' ) ) {
			$this->updateRobotsTxtRules();
		}

		if ( version_compare( $lastActiveVersion, '4.5.1', '<' ) ) {
			$this->checkForGaAnalyticsV3();
		}

		if ( version_compare( $lastActiveVersion, '4.5.8', '<' ) ) {
			$this->addQueryArgMonitorTables();
			$this->addQueryArgMonitorNotification();
		}

		if ( version_compare( $lastActiveVersion, '4.5.9', '<' ) ) {
			$this->deprecateNoPaginationForCanonicalUrlsSetting();
		}

		if ( version_compare( $lastActiveVersion, '4.6.5', '<' ) ) {
			$this->deprecateBreadcrumbsEnabledSetting();
		}

		if ( version_compare( $lastActiveVersion, '4.7.4', '<' ) ) {
			$this->addWritingAssistantTables();
			aioseo()->access->addCapabilities();
		}

		if ( version_compare( $lastActiveVersion, '4.7.5', '<' ) ) {
			$this->cancelScheduledSitemapPings();
		}

		if ( version_compare( $lastActiveVersion, '4.7.7', '<' ) ) {
			$this->disableEmailReports();
		}

		if ( version_compare( $lastActiveVersion, '4.7.9', '<' ) ) {
			$this->fixSavedHeadlines();
			$this->rescheduleEmailReport();
		}

		if ( version_compare( $lastActiveVersion, '4.8.3', '<' ) ) {
			$this->resetImageScanDate();
			$this->addSeoAnalyzerResultsTable();
			$this->migrateSeoAnalyzerResults();
			$this->migrateSeoAnalyzerCompetitors();
			$this->addBreadcrumbSettingsColumn();
		}

		if ( version_compare( $lastActiveVersion, '4.8.3.1', '<' ) ) {
			aioseo()->core->cache->delete( 'analyze_site_code' );
			aioseo()->core->cache->delete( 'analyze_site_body' );
		}

		if ( version_compare( $lastActiveVersion, '4.8.4', '<' ) ) {
			$this->addAiColumn();
		}

		if ( version_compare( $lastActiveVersion, '4.8.4.1', '<' ) ) {
			aioseo()->ai->updateCredits( true );
		}

		do_action( 'aioseo_run_updates', $lastActiveVersion );

		// Always clear the cache if the last active version is different from our current.

		if ( version_compare( $lastActiveVersion, AIOSEO_VERSION, '<' ) ) {
			aioseo()->core->cache->clear();
		}
	}

	/**
	 * Retrieve the raw options from the database for migration.
	 *
	 * @since 4.1.4
	 *
	 * @return array An array of options.
	 */
	private function getRawOptions() {
		// Options from the DB.
		$commonOptions = json_decode( get_option( aioseo()->options->optionsName ), true );
		if ( empty( $commonOptions ) ) {
			$commonOptions = [];
		}

		return $commonOptions;
	}

	/**
	 * Updates the latest version after all migrations and updates have run.
	 *
	 * @since 4.0.3
	 *
	 * @return void
	 */
	public function updateLatestVersion() {
		if ( aioseo()->internalOptions->internal->lastActiveVersion === aioseo()->version ) {
			return;
		}

		aioseo()->internalOptions->internal->lastActiveVersion = aioseo()->version;

		// Bust the tableExists and columnExists cache.
		aioseo()->internalOptions->database->installedTables = '';

		// Bust the DB cache so we can make sure that everything is fresh.
		aioseo()->core->db->bustCache();
	}

	/**
	 * Adds our custom tables for V4.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addInitialCustomTablesForV4() {
		$db             = aioseo()->core->db->db;
		$charsetCollate = '';

		if ( ! empty( $db->charset ) ) {
			$charsetCollate .= "DEFAULT CHARACTER SET {$db->charset}";
		}
		if ( ! empty( $db->collate ) ) {
			$charsetCollate .= " COLLATE {$db->collate}";
		}

		// Check for notifications table.
		if ( ! aioseo()->core->db->tableExists( 'aioseo_notifications' ) ) {
			$tableName = $db->prefix . 'aioseo_notifications';

			aioseo()->core->db->execute(
				"CREATE TABLE {$tableName} (
					id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					slug varchar(13) NOT NULL,
					title text NOT NULL,
					content longtext NOT NULL,
					type varchar(64) NOT NULL,
					level text NOT NULL,
					notification_id bigint(20) unsigned DEFAULT NULL,
					notification_name varchar(255) DEFAULT NULL,
					start datetime DEFAULT NULL,
					end datetime DEFAULT NULL,
					button1_label varchar(255) DEFAULT NULL,
					button1_action varchar(255) DEFAULT NULL,
					button2_label varchar(255) DEFAULT NULL,
					button2_action varchar(255) DEFAULT NULL,
					dismissed tinyint(1) NOT NULL DEFAULT 0,
					created datetime NOT NULL,
					updated datetime NOT NULL,
					PRIMARY KEY (id),
					UNIQUE KEY ndx_aioseo_notifications_slug (slug),
					KEY ndx_aioseo_notifications_dates (start, end),
					KEY ndx_aioseo_notifications_type (type),
					KEY ndx_aioseo_notifications_dismissed (dismissed)
				) {$charsetCollate};"
			);
		}

		if ( ! aioseo()->core->db->tableExists( 'aioseo_posts' ) ) {
			$tableName = $db->prefix . 'aioseo_posts';

			// Incorrect defaults are adjusted below through migrations.
			aioseo()->core->db->execute(
				"CREATE TABLE {$tableName} (
					id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					post_id bigint(20) unsigned NOT NULL,
					title text DEFAULT NULL,
					description text DEFAULT NULL,
					keywords mediumtext DEFAULT NULL,
					keyphrases longtext DEFAULT NULL,
					page_analysis longtext DEFAULT NULL,
					canonical_url text DEFAULT NULL,
					og_title text DEFAULT NULL,
					og_description text DEFAULT NULL,
					og_object_type varchar(64) DEFAULT 'default',
					og_image_type varchar(64) DEFAULT 'default',
					og_image_custom_url text DEFAULT NULL,
					og_image_custom_fields text DEFAULT NULL,
					og_custom_image_width int(11) DEFAULT NULL,
					og_custom_image_height int(11) DEFAULT NULL,
					og_video varchar(255) DEFAULT NULL,
					og_custom_url text DEFAULT NULL,
					og_article_section text DEFAULT NULL,
					og_article_tags text DEFAULT NULL,
					twitter_use_og tinyint(1) DEFAULT 1,
					twitter_card varchar(64) DEFAULT 'default',
					twitter_image_type varchar(64) DEFAULT 'default',
					twitter_image_custom_url text DEFAULT NULL,
					twitter_image_custom_fields text DEFAULT NULL,
					twitter_title text DEFAULT NULL,
					twitter_description text DEFAULT NULL,
					seo_score int(11) DEFAULT 0 NOT NULL,
					schema_type varchar(20) DEFAULT NULL,
					schema_type_options longtext DEFAULT NULL,
					pillar_content tinyint(1) DEFAULT NULL,
					robots_default tinyint(1) DEFAULT 1 NOT NULL,
					robots_noindex tinyint(1) DEFAULT 0 NOT NULL,
					robots_noarchive tinyint(1) DEFAULT 0 NOT NULL,
					robots_nosnippet tinyint(1) DEFAULT 0 NOT NULL,
					robots_nofollow tinyint(1) DEFAULT 0 NOT NULL,
					robots_noimageindex tinyint(1) DEFAULT 0 NOT NULL,
					robots_noodp tinyint(1) DEFAULT 0 NOT NULL,
					robots_notranslate tinyint(1) DEFAULT 0 NOT NULL,
					robots_max_snippet int(11) DEFAULT NULL,
					robots_max_videopreview int(11) DEFAULT NULL,
					robots_max_imagepreview varchar(20) DEFAULT 'none',
					tabs mediumtext DEFAULT NULL,
					images longtext DEFAULT NULL,
					priority tinytext DEFAULT NULL,
					frequency tinytext DEFAULT NULL,
					videos longtext DEFAULT NULL,
					video_thumbnail text DEFAULT NULL,
					video_scan_date datetime DEFAULT NULL,
					local_seo longtext DEFAULT NULL,
					created datetime NOT NULL,
					updated datetime NOT NULL,
					PRIMARY KEY (id),
					KEY ndx_aioseo_posts_post_id (post_id)
				) {$charsetCollate};"
			);
		}

		// Reset the cache for the installed tables.
		aioseo()->internalOptions->database->installedTables = '';
	}

	/**
	 * Sets the default social images.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function setDefaultSocialImages() {
		$siteLogo = aioseo()->helpers->getSiteLogoUrl();
		if ( $siteLogo && ! aioseo()->internalOptions->internal->migratedVersion ) {
			if ( ! aioseo()->options->social->facebook->general->defaultImagePosts ) {
				aioseo()->options->social->facebook->general->defaultImagePosts = $siteLogo;
			}
			if ( ! aioseo()->options->social->twitter->general->defaultImagePosts ) {
				aioseo()->options->social->twitter->general->defaultImagePosts = $siteLogo;
			}
		}
	}

	/**
	 * Adds the image scan date column to our posts table.
	 *
	 * @since 4.0.5
	 *
	 * @return void
	 */
	public function addImageScanDateColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'image_scan_date' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				ADD image_scan_date datetime DEFAULT NULL AFTER images"
			);

			// Reset the cache for the installed tables.
			aioseo()->internalOptions->database->installedTables = '';
		}
	}

	/**
	 * Adds the breadcrumb settings column to our posts table.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	public function addBreadcrumbSettingsColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'breadcrumb_settings' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				ADD `breadcrumb_settings` longtext DEFAULT NULL AFTER local_seo"
			);

			// Reset the cache for the installed tables.
			aioseo()->internalOptions->database->installedTables = '';
		}
	}

	/**
	 * Modifes the default value of the twitter_use_og column.
	 *
	 * @since 4.0.6
	 *
	 * @return void
	 */
	protected function disableTwitterUseOgDefault() {
		if ( aioseo()->core->db->tableExists( 'aioseo_posts' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				MODIFY twitter_use_og tinyint(1) DEFAULT 0"
			);
		}
	}

	/**
	 * Modifes the default value of the robots_max_imagepreview column.
	 *
	 * @since 4.0.6
	 *
	 * @return void
	 */
	protected function updateMaxImagePreviewDefault() {
		if ( aioseo()->core->db->tableExists( 'aioseo_posts' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				MODIFY robots_max_imagepreview varchar(20) DEFAULT 'large'"
			);
		}
	}

	/**
	 * Deletes duplicate records in our custom tables.
	 *
	 * @since 4.0.13
	 *
	 * @return void
	 */
	public function removeDuplicateRecords() {
		$duplicates = aioseo()->core->db->start( 'aioseo_posts' )
			->select( 'post_id, min(id) as id' )
			->groupBy( 'post_id having count(post_id) > 1' )
			->orderByRaw( 'count(post_id) DESC' )
			->run()
			->result();

		if ( empty( $duplicates ) ) {
			return;
		}

		foreach ( $duplicates as $duplicate ) {
			$postId        = $duplicate->post_id;
			$firstRecordId = $duplicate->id;

			aioseo()->core->db->delete( 'aioseo_posts' )
				->whereRaw( "( id > $firstRecordId AND post_id = $postId )" )
				->run();
		}
	}

	/**
	 * Removes the location column.
	 *
	 * @since 4.0.17
	 *
	 * @return void
	 */
	public function removeLocationColumn() {
		if ( aioseo()->core->db->columnExists( 'aioseo_posts', 'location' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				DROP location"
			);
		}
	}

	/**
	 * Clears the image data for WooCommerce Products so that we scan them again and include product gallery images.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	public function clearProductImages() {
		if ( ! aioseo()->helpers->isWooCommerceActive() ) {
			return;
		}

		aioseo()->core->db->update( 'aioseo_posts as ap' )
			->join( 'posts as p', 'ap.post_id = p.ID' )
			->where( 'p.post_type', 'product' )
			->set(
				[
					'images'          => null,
					'image_scan_date' => null
				]
			)
			->run();
	}

	/**
	 * Adds the new flag to the notifications table.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	public function addNotificationsNewColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_notifications', 'new' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_notifications';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				ADD new tinyint(1) NOT NULL DEFAULT 1 AFTER dismissed"
			);

			// Reset the cache for the installed tables.
			aioseo()->internalOptions->database->installedTables = '';

			aioseo()->core->db
				->update( 'aioseo_notifications' )
				->where( 'new', 1 )
				->set( 'new', 0 )
				->run();
		}
	}

	/**
	 * Noindexes the WooCommerce cart, checkout and account pages.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	public function noindexWooCommercePages() {
		if ( ! aioseo()->helpers->isWooCommerceActive() ) {
			return;
		}

		$cartId     = (int) get_option( 'woocommerce_cart_page_id' );
		$checkoutId = (int) get_option( 'woocommerce_checkout_page_id' );
		$accountId  = (int) get_option( 'woocommerce_myaccount_page_id' );

		$cartPage     = Models\Post::getPost( $cartId );
		$checkoutPage = Models\Post::getPost( $checkoutId );
		$accountPage  = Models\Post::getPost( $accountId );

		$newMeta = [
			'robots_default' => false,
			'robots_noindex' => true
		];

		if ( $cartPage->exists() ) {
			$cartPage->set( $newMeta );
			$cartPage->save();
		}
		if ( $checkoutPage->exists() ) {
			$checkoutPage->set( $newMeta );
			$checkoutPage->save();
		}
		if ( $accountPage->exists() ) {
			$accountPage->set( $newMeta );
			$accountPage->save();
		}
	}

	/**
	 * Adds the new capabilities for all the roles.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	protected function accessControlNewCapabilities() {
		aioseo()->access->addCapabilities();
	}

	/**
	 * Migrate dynamic settings to a separate options structure.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	protected function migrateDynamicSettings() {
		$rawOptions = $this->getRawOptions();
		$options    = aioseo()->dynamicOptions->noConflict();

		// Sitemap post type priorities/frequencies.
		if (
			! empty( $rawOptions['sitemap']['dynamic']['priority']['postTypes'] )
		) {
			foreach ( $rawOptions['sitemap']['dynamic']['priority']['postTypes'] as $postTypeName => $data ) {
				if ( $options->sitemap->priority->postTypes->has( $postTypeName ) ) {
					$options->sitemap->priority->postTypes->$postTypeName->priority  = $data['priority'];
					$options->sitemap->priority->postTypes->$postTypeName->frequency = $data['frequency'];
				}
			}
		}

		// Sitemap taxonomy priorities/frequencies.
		if (
			! empty( $rawOptions['sitemap']['dynamic']['priority']['taxonomies'] )
		) {
			foreach ( $rawOptions['sitemap']['dynamic']['priority']['taxonomies'] as $taxonomyName => $data ) {
				if ( $options->sitemap->priority->taxonomies->has( $taxonomyName ) ) {
					$options->sitemap->priority->taxonomies->$taxonomyName->priority  = $data['priority'];
					$options->sitemap->priority->taxonomies->$taxonomyName->frequency = $data['frequency'];
				}
			}
		}

		// Facebook post type object types.
		if (
			! empty( $rawOptions['social']['facebook']['general']['dynamic']['postTypes'] )
		) {
			foreach ( $rawOptions['social']['facebook']['general']['dynamic']['postTypes'] as $postTypeName => $data ) {
				if ( $options->social->facebook->general->postTypes->has( $postTypeName ) ) {
					$options->social->facebook->general->postTypes->$postTypeName->objectType = $data['objectType'];
				}
			}
		}

		// Search appearance post type data.
		if (
			! empty( $rawOptions['searchAppearance']['dynamic']['postTypes'] )
		) {
			foreach ( $rawOptions['searchAppearance']['dynamic']['postTypes'] as $postTypeName => $data ) {
				if ( $options->searchAppearance->postTypes->has( $postTypeName ) ) {
					$options->searchAppearance->postTypes->$postTypeName->show            = $data['show'];
					$options->searchAppearance->postTypes->$postTypeName->title           = $data['title'];
					$options->searchAppearance->postTypes->$postTypeName->metaDescription = $data['metaDescription'];
					$options->searchAppearance->postTypes->$postTypeName->schemaType      = $data['schemaType'];
					$options->searchAppearance->postTypes->$postTypeName->webPageType     = $data['webPageType'];
					$options->searchAppearance->postTypes->$postTypeName->articleType     = $data['articleType'];
					$options->searchAppearance->postTypes->$postTypeName->customFields    = $data['customFields'];

					// Advanced settings.
					$advanced = ! empty( $data['advanced']['robotsMeta'] ) ? $data['advanced']['robotsMeta'] : null;
					if ( ! empty( $advanced ) ) {
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->default         = $data['advanced']['robotsMeta']['default'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->noindex         = $data['advanced']['robotsMeta']['noindex'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->nofollow        = $data['advanced']['robotsMeta']['nofollow'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->noarchive       = $data['advanced']['robotsMeta']['noarchive'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->noimageindex    = $data['advanced']['robotsMeta']['noimageindex'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->notranslate     = $data['advanced']['robotsMeta']['notranslate'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->nosnippet       = $data['advanced']['robotsMeta']['nosnippet'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->noodp           = $data['advanced']['robotsMeta']['noodp'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->maxSnippet      = $data['advanced']['robotsMeta']['maxSnippet'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->maxVideoPreview = $data['advanced']['robotsMeta']['maxVideoPreview'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->maxImagePreview = $data['advanced']['robotsMeta']['maxImagePreview'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->showDateInGooglePreview     = $data['advanced']['showDateInGooglePreview'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->showPostThumbnailInSearch   = $data['advanced']['showPostThumbnailInSearch'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->showMetaBox                 = $data['advanced']['showMetaBox'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->bulkEditing                 = $data['advanced']['bulkEditing'];
					}

					if ( 'attachment' === $postTypeName ) {
						$options->searchAppearance->postTypes->$postTypeName->redirectAttachmentUrls = $data['redirectAttachmentUrls'];
					}
				}
			}
		}

		// Search appearance taxonomy data.
		if (
			! empty( $rawOptions['searchAppearance']['dynamic']['taxonomies'] )
		) {
			foreach ( $rawOptions['searchAppearance']['dynamic']['taxonomies'] as $taxonomyName => $data ) {
				if ( $options->searchAppearance->taxonomies->has( $taxonomyName ) ) {
					$options->searchAppearance->taxonomies->$taxonomyName->show            = $data['show'];
					$options->searchAppearance->taxonomies->$taxonomyName->title           = $data['title'];
					$options->searchAppearance->taxonomies->$taxonomyName->metaDescription = $data['metaDescription'];

					// Advanced settings.
					$advanced = ! empty( $data['advanced']['robotsMeta'] ) ? $data['advanced']['robotsMeta'] : null;
					if ( ! empty( $advanced ) ) {
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->default         = $data['advanced']['robotsMeta']['default'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->noindex         = $data['advanced']['robotsMeta']['noindex'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->nofollow        = $data['advanced']['robotsMeta']['nofollow'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->noarchive       = $data['advanced']['robotsMeta']['noarchive'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->noimageindex    = $data['advanced']['robotsMeta']['noimageindex'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->notranslate     = $data['advanced']['robotsMeta']['notranslate'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->nosnippet       = $data['advanced']['robotsMeta']['nosnippet'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->noodp           = $data['advanced']['robotsMeta']['noodp'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->maxSnippet      = $data['advanced']['robotsMeta']['maxSnippet'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->maxVideoPreview = $data['advanced']['robotsMeta']['maxVideoPreview'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->maxImagePreview = $data['advanced']['robotsMeta']['maxImagePreview'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->showDateInGooglePreview     = $data['advanced']['showDateInGooglePreview'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->showPostThumbnailInSearch   = $data['advanced']['showPostThumbnailInSearch'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->showMetaBox                 = $data['advanced']['showMetaBox'];
					}
				}
			}
		}
	}

	/**
	 * Fixes the default value for the post schema type.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	private function fixSchemaTypeDefault() {
		if ( aioseo()->core->db->tableExists( 'aioseo_posts' ) && aioseo()->core->db->columnExists( 'aioseo_posts', 'schema_type' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				MODIFY schema_type varchar(20) DEFAULT 'default'"
			);
		}
	}

	/**
	 * Add in image with/height columns and image URL for caching.
	 *
	 * @since 4.1.6
	 *
	 * @return void
	 */
	protected function migrateOgTwitterImageColumns() {
		if ( aioseo()->core->db->tableExists( 'aioseo_posts' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';

			// OG Columns.
			if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'og_image_url' ) ) {
				aioseo()->core->db->execute(
					"ALTER TABLE {$tableName} ADD og_image_url text DEFAULT NULL AFTER og_image_type"
				);
			}

			if ( aioseo()->core->db->columnExists( 'aioseo_posts', 'og_custom_image_height' ) ) {
				aioseo()->core->db->execute(
					"ALTER TABLE {$tableName} CHANGE COLUMN og_custom_image_height og_image_height int(11) DEFAULT NULL AFTER og_image_url"
				);
			} elseif ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'og_image_height' ) ) {
				aioseo()->core->db->execute(
					"ALTER TABLE {$tableName} ADD og_image_height int(11) DEFAULT NULL AFTER og_image_url"
				);
			}

			if ( aioseo()->core->db->columnExists( 'aioseo_posts', 'og_custom_image_width' ) ) {
				aioseo()->core->db->execute(
					"ALTER TABLE {$tableName} CHANGE COLUMN og_custom_image_width og_image_width int(11) DEFAULT NULL AFTER og_image_url"
				);
			} elseif ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'og_image_width' ) ) {
				aioseo()->core->db->execute(
					"ALTER TABLE {$tableName} ADD og_image_width int(11) DEFAULT NULL AFTER og_image_url"
				);
			}

			// Twitter image url columnn.
			if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'twitter_image_url' ) ) {
				aioseo()->core->db->execute(
					"ALTER TABLE {$tableName} ADD twitter_image_url text DEFAULT NULL AFTER twitter_image_type"
				);
			}

			// Reset the cache for the installed tables.
			aioseo()->internalOptions->database->installedTables = '';
		}
	}

	/**
	 * Adds the limit modified date column to our posts table.
	 *
	 * @since 4.1.8
	 *
	 * @return void
	 */
	private function addLimitModifiedDateColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'limit_modified_date' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				ADD limit_modified_date tinyint(1) NOT NULL DEFAULT 0 AFTER local_seo"
			);

			// Reset the cache for the installed tables.
			aioseo()->internalOptions->database->installedTables = '';
		}
	}

	/**
	 * Fixes tags that should not be in the search appearance taxonomy options.
	 *
	 * @since 4.1.9
	 *
	 * @return void
	 */
	protected function fixTaxonomyTags() {
		$searchAppearanceTaxonomies = aioseo()->dynamicOptions->searchAppearance->taxonomies->all();

		$replaces = [
			'#breadcrumb_separator' => '#separator_sa',
			'#breadcrumb_'          => '#',
			'#blog_title'           => '#site_title'
		];

		foreach ( $searchAppearanceTaxonomies as $taxonomy => $searchAppearanceTaxonomy ) {
			aioseo()->dynamicOptions->searchAppearance->taxonomies->{$taxonomy}->title = str_replace(
				array_keys( $replaces ),
				array_values( $replaces ),
				$searchAppearanceTaxonomy['title']
			);

			aioseo()->dynamicOptions->searchAppearance->taxonomies->{$taxonomy}->metaDescription = str_replace(
				array_keys( $replaces ),
				array_values( $replaces ),
				$searchAppearanceTaxonomy['metaDescription']
			);
		}
	}

	/**
	 * Removes any AIOSEO Post records for revisions.
	 *
	 * @since 4.1.9
	 *
	 * @return void
	 */
	public function removeRevisionRecords() {
		$postsTableName       = aioseo()->core->db->prefix . 'posts';
		$aioseoPostsTableName = aioseo()->core->db->prefix . 'aioseo_posts';
		$limit                = 5000;

		aioseo()->core->db->execute(
			"DELETE FROM `$aioseoPostsTableName`
			WHERE `post_id` IN (
				SELECT `ID`
				FROM `$postsTableName`
				WHERE `post_parent` != 0
				AND `post_type` = 'revision'
				AND `post_status` = 'inherit'
			)
			LIMIT {$limit}"
		);

		// If the limit equals the amount of post IDs found, there might be more revisions left, so we need a new scan.
		if ( aioseo()->core->db->rowsAffected() === $limit ) {
			$this->scheduleRemoveRevisionsRecords();
		}
	}

	/**
	 * Enables the new shortcodes parsing setting if it was already enabled before as a deprecated setting.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	private function migrateDeprecatedRunShortcodesSetting() {
		if (
			in_array( 'runShortcodesInDescription', aioseo()->internalOptions->deprecatedOptions, true ) &&
			! aioseo()->options->deprecated->searchAppearance->advanced->runShortcodesInDescription
		) {
			return;
		}

		aioseo()->options->searchAppearance->advanced->runShortcodes = true;
	}

	/**
	 * Add options column.
	 *
	 * @since 4.2.2
	 *
	 * @return void
	 */
	private function addOptionsColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'options' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				ADD `options` longtext DEFAULT NULL AFTER `limit_modified_date`"
			);

			// Reset the cache for the installed tables.
			aioseo()->internalOptions->database->installedTables = '';
		}
	}

	/**
	 * Remove the tabs column as it is unnecessary.
	 *
	 * @since 4.2.2
	 *
	 * @return void
	 */
	protected function removeTabsColumn() {
		if ( aioseo()->core->db->columnExists( 'aioseo_posts', 'tabs' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				DROP tabs"
			);
		}
	}

	/**
	 * Migrates the user contact methods to the new format.
	 *
	 * @since 4.2.2
	 *
	 * @return void
	 */
	private function migrateUserContactMethods() {
		$userMetaTableName = aioseo()->core->db->db->usermeta;

		aioseo()->core->db->execute(
			"UPDATE `$userMetaTableName`
			SET `meta_key` = 'aioseo_facebook_page_url'
			WHERE `meta_key` = 'aioseo_facebook'"
		);

		aioseo()->core->db->execute(
			"UPDATE `$userMetaTableName`
			SET `meta_key` = 'aioseo_twitter_url'
			WHERE `meta_key` = 'aioseo_twitter'"
		);
	}

	/**
	 * Add an addon column to the notifications table.
	 *
	 * @since 4.2.4
	 *
	 * @return void
	 */
	private function addNotificationsAddonColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_notifications', 'addon' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_notifications';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				ADD `addon` varchar(64) DEFAULT NULL AFTER `slug`"
			);

			// Reset the cache for the installed tables.
			aioseo()->internalOptions->database->installedTables = '';
		}
	}

	/**
	 * Adds the schema column.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	private function addSchemaColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'schema' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				ADD `schema` longtext DEFAULT NULL AFTER `seo_score`"
			);
		}
	}

	/**
	 * Schedules the post schema migration.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	private function schedulePostSchemaMigration() {
		aioseo()->actionScheduler->scheduleSingle( 'aioseo_v4_migrate_post_schema', 10 );

		if ( ! aioseo()->core->cache->get( 'v4_migrate_post_schema_default_date' ) ) {
			aioseo()->core->cache->update( 'v4_migrate_post_schema_default_date', gmdate( 'Y-m-d H:i:s' ), 3 * MONTH_IN_SECONDS );
		}
	}

	/**
	 * Migrates then post schema to the new JSON column.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	public function migratePostSchema() {
		$posts = aioseo()->core->db->start( 'aioseo_posts' )
			->select( '*' )
			->whereRaw( '`schema` IS NULL' )
			->limit( 40 )
			->run()
			->models( 'AIOSEO\\Plugin\\Common\\Models\\Post' );

		if ( empty( $posts ) ) {
			return;
		}

		foreach ( $posts as $post ) {
			$this->migratePostSchemaHelper( $post );
		}

		// Once done, schedule the next action.
		aioseo()->actionScheduler->scheduleSingle( 'aioseo_v4_migrate_post_schema', 30, [], true );
	}

	/**
	 * Schedules the post schema migration to fix the default graphs.
	 *
	 * @since 4.2.6
	 *
	 * @return void
	 */
	private function schedulePostSchemaDefaultMigration() {
		aioseo()->actionScheduler->scheduleSingle( 'aioseo_v4_migrate_post_schema_default', 30 );
	}

	/**
	 * Migrates the post schema to the new JSON column again for posts using the default.
	 * This is needed to fix an oversight because in 4.2.5 we didn't migrate any properties set to the default graph.
	 *
	 * @since 4.2.6
	 *
	 * @return void
	 */
	public function migratePostSchemaDefault() {
		$migrationStartDate = aioseo()->core->cache->get( 'v4_migrate_post_schema_default_date' );
		if ( ! $migrationStartDate ) {
			return;
		}

		$posts = aioseo()->core->db->start( 'aioseo_posts' )
			->select( '*' )
			->where( 'schema_type =', 'default' )
			->whereRaw( "updated < '$migrationStartDate'" )
			->limit( 40 )
			->run()
			->models( 'AIOSEO\\Plugin\\Common\\Models\\Post' );

		if ( empty( $posts ) ) {
			aioseo()->core->cache->delete( 'v4_migrate_post_schema_default_date' );

			return;
		}

		foreach ( $posts as $post ) {
			$this->migratePostSchemaHelper( $post );
		}

		// Once done, schedule the next action.
		aioseo()->actionScheduler->scheduleSingle( 'aioseo_v4_migrate_post_schema_default', 30, [], true );
	}

	/**
	 * Helper function for the schema migration.
	 *
	 * @since  4.2.5
	 *
	 * @param  Models\Post $aioseoPost The AIOSEO post object.
	 * @return Models\Post             The modified AIOSEO post object.
	 */
	public function migratePostSchemaHelper( $aioseoPost ) {
		$post              = aioseo()->helpers->getPost( $aioseoPost->post_id );
		$schemaType        = $aioseoPost->schema_type;
		$schemaTypeOptions = json_decode( (string) $aioseoPost->schema_type_options );
		$schemaOptions     = Models\Post::getDefaultSchemaOptions( '', $post );

		if ( empty( $schemaTypeOptions ) ) {
			$aioseoPost->schema = $schemaOptions;
			$aioseoPost->save();

			return $aioseoPost;
		}

		// If the post is set to the default schema type, set the default for post type but then also get the properties.
		$isDefault = 'default' === $schemaType;
		if ( $isDefault ) {
			$dynamicOptions = aioseo()->dynamicOptions->noConflict();
			if ( ! empty( $post->post_type ) && $dynamicOptions->searchAppearance->postTypes->has( $post->post_type ) ) {
				$schemaOptions->default->graphName = $dynamicOptions->searchAppearance->postTypes->{$post->post_type}->schemaType;
				$schemaType                        = $dynamicOptions->searchAppearance->postTypes->{$post->post_type}->schemaType;
			}
		}

		$graph = [];
		switch ( $schemaType ) {
			case 'Article':
				$graph = [
					'id'         => '#aioseo-article-' . uniqid(),
					'slug'       => 'article',
					'graphName'  => 'Article',
					'label'      => __( 'Article', 'all-in-one-seo-pack' ),
					'properties' => [
						'type'        => ! empty( $schemaTypeOptions->article->articleType ) ? $schemaTypeOptions->article->articleType : 'Article',
						'name'        => '#post_title',
						'headline'    => '#post_title',
						'description' => '#post_excerpt',
						'image'       => '',
						'keywords'    => '',
						'author'      => [
							'name' => '#author_name',
							'url'  => '#author_url'
						],
						'dates'       => [
							'include'       => true,
							'datePublished' => '',
							'dateModified'  => ''
						]
					]
				];
				break;
			case 'Course':
				$graph = [
					'id'         => '#aioseo-course-' . uniqid(),
					'slug'       => 'course',
					'graphName'  => 'Course',
					'label'      => __( 'Course', 'all-in-one-seo-pack' ),
					'properties' => [
						'name'        => ! empty( $schemaTypeOptions->course->name ) ? $schemaTypeOptions->course->name : '#post_title',
						'description' => ! empty( $schemaTypeOptions->course->description ) ? $schemaTypeOptions->course->description : '#post_excerpt',
						'provider'    => [
							'name'  => ! empty( $schemaTypeOptions->course->provider ) ? $schemaTypeOptions->course->provider : '',
							'url'   => '',
							'image' => ''
						]
					]
				];
				break;
			case 'Product':
				$graph = [
					'id'         => '#aioseo-product-' . uniqid(),
					'slug'       => 'product',
					'graphName'  => 'Product',
					'label'      => __( 'Product', 'all-in-one-seo-pack' ),
					'properties' => [
						'autogenerate' => true,
						'name'         => '#post_title',
						'description'  => ! empty( $schemaTypeOptions->product->description ) ? $schemaTypeOptions->product->description : '#post_excerpt',
						'brand'        => ! empty( $schemaTypeOptions->product->brand ) ? $schemaTypeOptions->product->brand : '',
						'image'        => '',
						'identifiers'  => [
							'sku'  => ! empty( $schemaTypeOptions->product->sku ) ? $schemaTypeOptions->product->sku : '',
							'gtin' => '',
							'mpn'  => ''
						],
						'offer'        => [
							'price'        => ! empty( $schemaTypeOptions->product->price ) ? (float) $schemaTypeOptions->product->price : '',
							'currency'     => ! empty( $schemaTypeOptions->product->currency ) ? $schemaTypeOptions->product->currency : '',
							'availability' => ! empty( $schemaTypeOptions->product->availability ) ? $schemaTypeOptions->product->availability : '',
							'validUntil'   => ! empty( $schemaTypeOptions->product->priceValidUntil ) ? $schemaTypeOptions->product->priceValidUntil : ''
						],
						'rating'       => [
							'minimum' => 1,
							'maximum' => 5
						],
						'reviews'      => []
					]
				];

				$identifierType = ! empty( $schemaTypeOptions->product->identifierType ) ? $schemaTypeOptions->product->identifierType : '';
				$identifier     = ! empty( $schemaTypeOptions->product->identifier ) ? $schemaTypeOptions->product->identifier : '';
				if ( preg_match( '/gtin/i', (string) $identifierType ) ) {
					$graph['properties']['identifiers']['gtin'] = $identifier;
				}

				if ( preg_match( '/mpn/i', (string) $identifierType ) ) {
					$graph['properties']['identifiers']['mpn'] = $identifier;
				}

				$reviews = ! empty( $schemaTypeOptions->product->reviews ) ? $schemaTypeOptions->product->reviews : [];
				if ( ! empty( $reviews ) ) {
					foreach ( $reviews as $reviewData ) {
						$reviewData = json_decode( $reviewData );
						if ( empty( $reviewData ) ) {
							continue;
						}

						$graph['properties']['reviews'][] = [
							'rating'   => $reviewData->rating,
							'headline' => $reviewData->headline,
							'content'  => $reviewData->content,
							'author'   => $reviewData->author
						];
					}
				}
				break;
			case 'Recipe':
				$graph = [
					'id'         => '#aioseo-recipe-' . uniqid(),
					'slug'       => 'recipe',
					'graphName'  => 'Recipe',
					'label'      => __( 'Recipe', 'all-in-one-seo-pack' ),
					'properties' => [
						'name'         => ! empty( $schemaTypeOptions->recipe->name ) ? $schemaTypeOptions->recipe->name : '#post_title',
						'description'  => ! empty( $schemaTypeOptions->recipe->description ) ? $schemaTypeOptions->recipe->description : '#post_excerpt',
						'author'       => ! empty( $schemaTypeOptions->recipe->author ) ? $schemaTypeOptions->recipe->author : '#author_name',
						'ingredients'  => ! empty( $schemaTypeOptions->recipe->ingredients ) ? $schemaTypeOptions->recipe->ingredients : '',
						'dishType'     => ! empty( $schemaTypeOptions->recipe->dishType ) ? $schemaTypeOptions->recipe->dishType : '',
						'cuisineType'  => ! empty( $schemaTypeOptions->recipe->cuisineType ) ? $schemaTypeOptions->recipe->cuisineType : '',
						'keywords'     => ! empty( $schemaTypeOptions->recipe->keywords ) ? $schemaTypeOptions->recipe->keywords : '',
						'image'        => ! empty( $schemaTypeOptions->recipe->image ) ? $schemaTypeOptions->recipe->image : '',
						'nutrition'    => [
							'servings' => ! empty( $schemaTypeOptions->recipe->servings ) ? $schemaTypeOptions->recipe->servings : '',
							'calories' => ! empty( $schemaTypeOptions->recipe->calories ) ? $schemaTypeOptions->recipe->calories : ''
						],
						'timeRequired' => [
							'preparation' => ! empty( $schemaTypeOptions->recipe->preparationTime ) ? $schemaTypeOptions->recipe->preparationTime : '',
							'cooking'     => ! empty( $schemaTypeOptions->recipe->cookingTime ) ? $schemaTypeOptions->recipe->cookingTime : ''
						],
						'instructions' => [],
						'rating'       => [
							'minimum' => 1,
							'maximum' => 5
						],
						'reviews'      => []
					]
				];

				$instructions = ! empty( $schemaTypeOptions->recipe->instructions ) ? $schemaTypeOptions->recipe->instructions : [];
				if ( ! empty( $instructions ) ) {
					foreach ( $instructions as $instructionData ) {
						$instructionData = json_decode( $instructionData );
						if ( empty( $instructionData ) ) {
							continue;
						}

						$graph['properties']['instructions'][] = [
							'name'  => '',
							'text'  => $instructionData->content,
							'image' => ''
						];
					}
				}

				$reviews = ! empty( $schemaTypeOptions->recipe->reviews ) ? $schemaTypeOptions->recipe->reviews : [];
				if ( ! empty( $reviews ) ) {
					foreach ( $reviews as $reviewData ) {
						$reviewData = json_decode( $reviewData );
						if ( empty( $reviewData ) ) {
							continue;
						}

						$graph['properties']['reviews'][] = [
							'rating'   => $reviewData->rating,
							'headline' => $reviewData->headline,
							'content'  => $reviewData->content,
							'author'   => $reviewData->author
						];
					}
				}
				break;
			case 'SoftwareApplication':
				$graph = [
					'id'         => '#aioseo-software-application-' . uniqid(),
					'slug'       => 'software-application',
					'graphName'  => 'SoftwareApplication',
					'label'      => __( 'Software', 'all-in-one-seo-pack' ),
					'properties' => [
						'name'            => ! empty( $schemaTypeOptions->software->name ) ? $schemaTypeOptions->software->name : '#post_title',
						'description'     => '#post_excerpt',
						'price'           => ! empty( $schemaTypeOptions->software->price ) ? (float) $schemaTypeOptions->software->price : '',
						'currency'        => ! empty( $schemaTypeOptions->software->currency ) ? $schemaTypeOptions->software->currency : '',
						'operatingSystem' => ! empty( $schemaTypeOptions->software->operatingSystems ) ? $schemaTypeOptions->software->operatingSystems : '',
						'category'        => ! empty( $schemaTypeOptions->software->category ) ? $schemaTypeOptions->software->category : '',
						'rating'          => [
							'value'   => '',
							'minimum' => 1,
							'maximum' => 5
						],
						'review'          => [
							'headline' => '',
							'content'  => '',
							'author'   => ''
						]
					]
				];

				$reviews = ! empty( $schemaTypeOptions->software->reviews ) ? $schemaTypeOptions->software->reviews : [];
				if ( ! empty( $reviews[0] ) ) {
					$reviewData = json_decode( $reviews[0] );
					if ( empty( $reviewData ) ) {
						break;
					}

					$graph['properties']['rating']['value'] = $reviewData->rating;
					$graph['properties']['review'] = [
						'headline' => $reviewData->headline,
						'content'  => $reviewData->content,
						'author'   => $reviewData->author
					];
				}
				break;
			case 'WebPage':
				if ( 'FAQPage' === $schemaTypeOptions->webPage->webPageType ) {
					$graph = [
						'id'         => '#aioseo-faq-page-' . uniqid(),
						'slug'       => 'faq-page',
						'graphName'  => 'FAQPage',
						'label'      => __( 'FAQ Page', 'all-in-one-seo-pack' ),
						'properties' => [
							'type'        => $schemaTypeOptions->webPage->webPageType,
							'name'        => '#post_title',
							'description' => '#post_excerpt',
							'questions'   => []
						]
					];

					$faqs = $schemaTypeOptions->faq->pages;
					if ( ! empty( $faqs ) ) {
						foreach ( $faqs as $faqData ) {
							$faqData = json_decode( $faqData );
							if ( empty( $faqData ) ) {
								continue;
							}

							$graph['properties']['questions'][] = [
								'question' => $faqData->question,
								'answer'   => $faqData->answer
							];
						}
					}
				} else {
					$graph = [
						'id'         => '#aioseo-web-page-' . uniqid(),
						'slug'       => 'web-page',
						'graphName'  => 'WebPage',
						'label'      => __( 'Web Page', 'all-in-one-seo-pack' ),
						'properties' => [
							'type'        => $schemaTypeOptions->webPage->webPageType,
							'name'        => '',
							'description' => ''
						]
					];
				}
				break;
			case 'default':
				$dynamicOptions = aioseo()->dynamicOptions->noConflict();
				if ( ! empty( $post->post_type ) && $dynamicOptions->searchAppearance->postTypes->has( $post->post_type ) ) {
					$schemaOptions->defaultGraph = $dynamicOptions->searchAppearance->postTypes->{$post->post_type}->schemaType;
				}
				break;
			case 'none':
				// If "none', we simply don't have to migrate anything.
			default:
				break;
		}

		if ( ! empty( $graph ) ) {
			if ( $isDefault ) {
				$schemaOptions->default->data->{$schemaType} = $graph;
			} else {
				$schemaOptions->graphs[]           = $graph;
				$schemaOptions->default->isEnabled = false;
			}
		}

		$aioseoPost->schema = $schemaOptions;
		$aioseoPost->save();

		return $aioseoPost;
	}

	/**
	 * Updates the dashboardWidgets with the new array format.
	 *
	 * @since 4.2.8
	 *
	 * @return void
	 */
	private function migrateDashboardWidgetsOptions() {
		$rawOptions = $this->getRawOptions();

		if ( empty( $rawOptions ) || ! is_bool( $rawOptions['advanced']['dashboardWidgets'] ) ) {
			return;
		}

		$widgets = [ 'seoNews' ];

		// If the dashboardWidgets was activated, let's turn on the other widgets.
		if ( ! empty( $rawOptions['advanced']['dashboardWidgets'] ) ) {
			$widgets[] = 'seoOverview';
			$widgets[] = 'seoSetup';
		}

		aioseo()->options->advanced->dashboardWidgets = $widgets;
	}

	/**
	 * Adds the primary_term column to the aioseo_posts table.
	 *
	 * @since 4.3.6
	 *
	 * @return void
	 */
	private function addPrimaryTermColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'primary_term' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				ADD `primary_term` longtext DEFAULT NULL AFTER `page_analysis`"
			);
		}
	}

	/**
	 * Schedules the revision records removal.
	 *
	 * @since 4.3.1
	 *
	 * @return void
	 */
	private function scheduleRemoveRevisionsRecords() {
		aioseo()->actionScheduler->scheduleSingle( 'aioseo_v419_remove_revision_records', 10, [], true );
	}

	/**
	 * Casts the priority column to a float.
	 *
	 * @since 4.3.9
	 *
	 * @return void
	 */
	private function migratePriorityColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'priority' ) ) {
			return;
		}

		$prefix               = aioseo()->core->db->prefix;
		$aioseoPostsTableName = $prefix . 'aioseo_posts';

		// First, cast the default value to NULL since it's a string.
		aioseo()->core->db->execute( "UPDATE {$aioseoPostsTableName} SET priority = NULL WHERE priority = 'default'" );

		// Then, alter the column to a float.
		aioseo()->core->db->execute( "ALTER TABLE {$aioseoPostsTableName} MODIFY priority float" );
	}

	/**
	 * Update the custom robots.txt rules to the new format,
	 * by replacing `rule` and `directoryPath` with `directive` and `fieldValue`, respectively.
	 *
	 * @since 4.4.2
	 *
	 * @return void
	 */
	private function updateRobotsTxtRules() {
		$rawOptions   = $this->getRawOptions();
		$currentRules = $rawOptions && ! empty( $rawOptions['tools']['robots']['rules'] )
			? $rawOptions['tools']['robots']['rules']
			: [];
		if ( empty( $currentRules ) || ! is_array( $currentRules ) ) {
			return;
		}

		$newRules = [];
		foreach ( $currentRules as $oldRule ) {
			$parsedRule = json_decode( $oldRule, true );
			if ( empty( $parsedRule['rule'] ) && empty( $parsedRule['directoryPath'] ) ) {
				continue;
			}

			$newRule = [
				'userAgent'  => array_key_exists( 'userAgent', $parsedRule ) ? $parsedRule['userAgent'] : '',
				'directive'  => array_key_exists( 'rule', $parsedRule ) ? $parsedRule['rule'] : '',
				'fieldValue' => array_key_exists( 'directoryPath', $parsedRule ) ? $parsedRule['directoryPath'] : '',
			];

			$newRules[] = wp_json_encode( $newRule );
		}

		if ( $newRules ) {
			aioseo()->options->tools->robots->rules = $newRules;
		}
	}

	/**
	 * Checks if the user is currently using the old GA Analytics v3 integration and create a notification.
	 *
	 * @since 4.5.1
	 *
	 * @return void
	 */
	private function checkForGaAnalyticsV3() {
		// If either MonsterInsights or ExactMetrics is active, let's return early.
		$pluginData = aioseo()->helpers->getPluginData();
		if (
			$pluginData['miPro']['activated'] ||
			$pluginData['miLite']['activated'] ||
			$pluginData['emPro']['activated'] ||
			$pluginData['emLite']['activated']
		) {
			return;
		}

		$rawOptions = $this->getRawOptions();
		if ( empty( $rawOptions['deprecated']['webmasterTools']['googleAnalytics']['id'] ) ) {
			return;
		}

		// Let's clear the notification if the search is working again.
		$notification = Models\Notification::getNotificationByName( 'google-analytics-v3-deprecation' );
		if ( $notification->exists() ) {
			$notification->dismissed = false;
			$notification->save();

			return;
		}

		// Determine which plugin name to use.
		$pluginName = 'MonsterInsights';
		if (
			(
				$pluginData['emPro']['installed'] ||
				$pluginData['emLite']['installed']
			) &&
			! $pluginData['miPro']['installed'] &&
			! $pluginData['miLite']['installed']
		) {
			$pluginName = 'ExactMetrics';
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'google-analytics-v3-deprecation',
			'title'             => __( 'Universal Analytics V3 Deprecation Notice', 'all-in-one-seo-pack' ),
			'content'           => sprintf(
				// Translators: 1 - Line break HTML tags, 2 - Plugin short name ("AIOSEO"), Analytics plugin name (e.g. "MonsterInsights").
				__( 'You have been using the %2$s Google Analytics V3 (Universal Analytics) integration which has been deprecated by Google and is no longer supported. This may affect your website\'s data accuracy and performance.%1$sTo ensure a seamless analytics experience, we recommend migrating to %3$s, a powerful analytics solution.%1$s%3$s offers advanced features such as real-time tracking, enhanced e-commerce analytics, and easy-to-understand reports, helping you make informed decisions to grow your online presence effectively.%1$sClick the button below to be redirected to the %3$s setup process, where you can start benefiting from its robust analytics capabilities immediately.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				'<br><br>',
				AIOSEO_PLUGIN_SHORT_NAME,
				$pluginName
			),
			'type'              => 'error',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Fix Now', 'all-in-one-seo-pack' ),
			'button1_action'    => admin_url( 'admin.php?page=aioseo-monsterinsights' ),
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Adds our custom tables for the query arg monitor.
	 *
	 * @since 4.5.8
	 *
	 * @return void
	 */
	public function addQueryArgMonitorTables() {
		$db             = aioseo()->core->db->db;
		$charsetCollate = '';

		if ( ! empty( $db->charset ) ) {
			$charsetCollate .= "DEFAULT CHARACTER SET {$db->charset}";
		}
		if ( ! empty( $db->collate ) ) {
			$charsetCollate .= " COLLATE {$db->collate}";
		}

		// Check for crawl cleanup logs table.
		if ( ! aioseo()->core->db->tableExists( 'aioseo_crawl_cleanup_logs' ) ) {
			$tableName = $db->prefix . 'aioseo_crawl_cleanup_logs';

			aioseo()->core->db->execute(
				"CREATE TABLE {$tableName} (
					`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					`slug` text NOT NULL,
					`key` text NOT NULL,
					`value` text,
					`hash` varchar(40) NOT NULL,
					`hits` int(20) NOT NULL DEFAULT 1,
					`created` datetime NOT NULL,
					`updated` datetime NOT NULL,
					PRIMARY KEY (id),
					UNIQUE KEY ndx_aioseo_crawl_cleanup_logs_hash (hash)
				) {$charsetCollate};"
			);
		}

		// Check for crawl cleanup blocked table.
		if ( ! aioseo()->core->db->tableExists( 'aioseo_crawl_cleanup_blocked_args' ) ) {
			$tableName = $db->prefix . 'aioseo_crawl_cleanup_blocked_args';

			aioseo()->core->db->execute(
				"CREATE TABLE {$tableName} (
					`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					`key` text,
					`value` text,
					`key_value_hash` varchar(40),
					`regex` varchar(150),
					`hits` int(20) NOT NULL DEFAULT 0,
					`created` datetime NOT NULL,
					`updated` datetime NOT NULL,
					PRIMARY KEY (id),
					UNIQUE KEY ndx_aioseo_crawl_cleanup_blocked_args_key_value_hash (key_value_hash),
					UNIQUE KEY ndx_aioseo_crawl_cleanup_blocked_args_regex (regex)
				) {$charsetCollate};"
			);
		}
	}

	/**
	 * Adds a notification for the query arg monitor.
	 *
	 * @since 4.5.8
	 *
	 * @return void
	 */
	private function addQueryArgMonitorNotification() {
		$options = $this->getRawOptions();
		if (
			empty( $options['searchAppearance']['advanced']['crawlCleanup']['enable'] ) ||
			empty( $options['searchAppearance']['advanced']['crawlCleanup']['removeUnrecognizedQueryArgs'] )
		) {
			return;
		}

		$notification = Models\Notification::getNotificationByName( 'crawl-cleanup-updated' );
		if ( $notification->exists() ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'crawl-cleanup-updated',
			'title'             => __( 'Crawl Cleanup changes you should know about', 'all-in-one-seo-pack' ),
			'content'           => __( 'We\'ve made some significant changes to how we monitor Query Args for our Crawl Cleanup feature. Instead of DISABLING all query args and requiring you to add individual exceptions, we\'ve now changed it to ALLOW all query args by default with the option to easily block unrecognized ones through our new log table.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'type'              => 'info',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Learn More', 'all-in-one-seo-pack' ),
			'button1_action'    => 'http://route#aioseo-search-appearance&aioseo-scroll=aioseo-query-arg-monitoring&aioseo-highlight=aioseo-query-arg-monitoring:advanced',
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Deprecates the "No Pagination for Canonical URLs" setting.
	 *
	 * @since 4.5.9
	 *
	 * @return void
	 */
	public function deprecateNoPaginationForCanonicalUrlsSetting() {
		$options = $this->getRawOptions();
		if ( empty( $options['searchAppearance']['advanced']['noPaginationForCanonical'] ) ) {
			return;
		}

		$deprecatedOptions = aioseo()->internalOptions->deprecatedOptions;
		if ( ! in_array( 'noPaginationForCanonical', $deprecatedOptions, true ) ) {
			$deprecatedOptions[]                         = 'noPaginationForCanonical';
			aioseo()->internalOptions->deprecatedOptions = $deprecatedOptions;
		}

		aioseo()->options->deprecated->searchAppearance->advanced->noPaginationForCanonical = true;
	}

	/**
	 * Deprecates the "Breadcrumbs enabled" setting.
	 *
	 * @since 4.6.5
	 *
	 * @return void
	 */
	public function deprecateBreadcrumbsEnabledSetting() {
		$options = $this->getRawOptions();
		if ( ! isset( $options['breadcrumbs']['enable'] ) || 1 === intval( $options['breadcrumbs']['enable'] ) ) {
			return;
		}

		$deprecatedOptions = aioseo()->internalOptions->deprecatedOptions;
		if ( ! in_array( 'breadcrumbsEnable', $deprecatedOptions, true ) ) {
			$deprecatedOptions[]                         = 'breadcrumbsEnable';
			aioseo()->internalOptions->deprecatedOptions = $deprecatedOptions;
		}

		aioseo()->options->deprecated->breadcrumbs->enable = false;
	}

	/**
	 * Add tables for Writing Assistant.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	private function addWritingAssistantTables() {
		$db             = aioseo()->core->db->db;
		$charsetCollate = '';

		if ( ! empty( $db->charset ) ) {
			$charsetCollate .= "DEFAULT CHARACTER SET {$db->charset}";
		}
		if ( ! empty( $db->collate ) ) {
			$charsetCollate .= " COLLATE {$db->collate}";
		}

		if ( ! aioseo()->core->db->tableExists( 'aioseo_writing_assistant_posts' ) ) {
			$tableName = $db->prefix . 'aioseo_writing_assistant_posts';

			aioseo()->core->db->execute(
				"CREATE TABLE {$tableName} (
					`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					`post_id` bigint(20) unsigned DEFAULT NULL,
					`keyword_id` bigint(20) unsigned DEFAULT NULL,
					`content_analysis_hash` VARCHAR(40) DEFAULT NULL,
					`content_analysis` text DEFAULT NULL,
					`created` datetime NOT NULL,
					`updated` datetime NOT NULL,
					PRIMARY KEY (id),
					UNIQUE KEY ndx_aioseo_writing_assistant_posts_post_id (post_id),
					KEY ndx_aioseo_writing_assistant_posts_keyword_id (keyword_id)
				) {$charsetCollate};"
			);
		}

		if ( ! aioseo()->core->db->tableExists( 'aioseo_writing_assistant_keywords' ) ) {
			$tableName = $db->prefix . 'aioseo_writing_assistant_keywords';

			aioseo()->core->db->execute(
				"CREATE TABLE {$tableName} (
					`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					`uuid` varchar(40) NOT NULL,
					`keyword` varchar(255) NOT NULL,
					`country` varchar(10) NOT NULL DEFAULT 'us',
					`language` varchar(10) NOT NULL DEFAULT 'en',
					`progress` tinyint(3) DEFAULT 0,
					`keywords` mediumtext NULL,
					`competitors` mediumtext NULL,
					`created` datetime NOT NULL,
					`updated` datetime NOT NULL,
					PRIMARY KEY (id),
					UNIQUE KEY ndx_aioseo_writing_assistant_keywords_uuid (uuid),
					KEY ndx_aioseo_writing_assistant_keywords_keyword (keyword)
				) {$charsetCollate};"
			);
		}
	}

	/**
	 * Cancels all outstanding sitemap ping actions.
	 * This is needed because we've removed the Ping class.
	 *
	 * @since 4.7.5
	 *
	 * @return void
	 */
	private function cancelScheduledSitemapPings() {
		as_unschedule_all_actions( 'aioseo_sitemap_ping' );
		as_unschedule_all_actions( 'aioseo_sitemap_ping_recurring' );
	}

	/**
	 * Disable email reports.
	 *
	 * @since 4.7.7
	 *
	 * @return void
	 */
	private function disableEmailReports() {
		aioseo()->options->advanced->emailSummary->enable = false;

		// Schedule a notification to remind the user to enable email reports in 2 weeks.
		aioseo()->actionScheduler->scheduleSingle( 'aioseo_email_reports_enable_reminder', 2 * WEEK_IN_SECONDS );
	}

	/**
	 * Cancels all occurrences of the report summary task.
	 * This is needed in order to force the scheduled date to be reset.
	 *
	 * @since 4.7.9
	 *
	 * @return void
	 */
	private function rescheduleEmailReport() {
		as_unschedule_all_actions( aioseo()->emailReports->summary->actionHook );
	}

	/**
	 * Fixes headlines that could not be analyzed.
	 *
	 * @since 4.7.9
	 *
	 * @return void
	 */
	private function fixSavedHeadlines() {
		$headlines = aioseo()->internalOptions->internal->headlineAnalysis->headlines;
		if ( empty( $headlines ) ) {
			return;
		}

		foreach ( $headlines as $key => $headline ) {
			if ( ! json_decode( $headline ) ) {
				unset( $headlines[ $key ] );
			}
		}

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

	/**
	 * Resets the image scan date in order to force a new scan.
	 * This is needed because we're now storing relative URLs in order to support site migrations.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	private function resetImageScanDate() {
		aioseo()->core->db->update( 'aioseo_posts' )
			->set(
				[
					'image_scan_date' => null
				]
			)
			->run();
	}

	/**
	 * Adds our custom table for the SeoAnalysis/SeoAnalyzer homepage and competitor results.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	private function addSeoAnalyzerResultsTable() {
		$db             = aioseo()->core->db->db;
		$charsetCollate = '';

		if ( ! empty( $db->charset ) ) {
			$charsetCollate .= "DEFAULT CHARACTER SET {$db->charset}";
		}
		if ( ! empty( $db->collate ) ) {
			$charsetCollate .= " COLLATE {$db->collate}";
		}

		// Check for seo analyzer results table.
		if ( ! aioseo()->core->db->tableExists( 'aioseo_seo_analyzer_results' ) ) {
			$tableName = $db->prefix . 'aioseo_seo_analyzer_results';

			aioseo()->core->db->execute(
				"CREATE TABLE {$tableName} (
					`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					`data` text NOT NULL,
					`score` varchar(255),
					`competitor_url` varchar(255),
					`created` datetime NOT NULL,
					`updated` datetime NOT NULL,
					PRIMARY KEY (id),
					KEY ndx_aioseo_seo_analyzer_results_competitor_url (competitor_url)
				) {$charsetCollate};"
			);

			// Reset the cache for the installed tables.
			aioseo()->internalOptions->database->installedTables = '';
		}
	}

	/**
	 * Migrate the SeoAnalyzer homepage results from the Internal Optinos to the new table.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	private function migrateSeoAnalyzerResults() {
		$internalOptions = $this->getRawInternalOptions();
		$results         = ! empty( $internalOptions['internal']['siteAnalysis']['results'] ) ? $internalOptions['internal']['siteAnalysis']['results'] : [];
		if ( empty( $results ) ) {
			return;
		}

		$parsedData = [
			'results' => is_string( $results ) ? json_decode( $results, true ) : $results,
			'score'   => $internalOptions['internal']['siteAnalysis']['score'],
		];

		Models\SeoAnalyzerResult::addResults( $parsedData );

		aioseo()->core->cache->delete( 'analyze_site_code' );
		aioseo()->core->cache->delete( 'analyze_site_body' );
	}

	/**
	 * Migrate the SeoAnalyzer competitors results from the Internal Optinos to the new table.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	private function migrateSeoAnalyzerCompetitors() {
		$internalOptions = $this->getRawInternalOptions();
		$competitors     = ! empty( $internalOptions['internal']['siteAnalysis']['competitors'] ) ? $internalOptions['internal']['siteAnalysis']['competitors'] : [];
		if ( empty( $competitors ) ) {
			return;
		}

		foreach ( $competitors as $url => $competitor ) {
			$parsedData = is_string( $competitor ) ? json_decode( $competitor, true ) : $competitor;
			$results    = empty( $parsedData['results'] ) ? [] : $parsedData['results'];
			if ( empty( $results ) ) {
				continue;
			}

			Models\SeoAnalyzerResult::addResults( [
				'results' => $results,
				'score'   => $parsedData['score'],
			], $url );
		}

		aioseo()->core->cache->delete( 'analyze_site_code' );
		aioseo()->core->cache->delete( 'analyze_site_body' );
	}

	/**
	* Adds the AI column to our posts table.
	*
	* @since 4.8.4
	*
	* @return void
	*/
	public function addAiColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'ai' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			if ( aioseo()->core->db->columnExists( 'aioseo_posts', 'open_ai' ) ) {
				aioseo()->core->db->execute(
					"ALTER TABLE {$tableName}
					ADD ai longtext DEFAULT NULL AFTER open_ai"
				);
			} else {
				aioseo()->core->db->execute(
					"ALTER TABLE {$tableName}
					ADD ai longtext DEFAULT NULL AFTER options"
				);
			}
		}
	}

	/**
	 * Returns the raw options from the database.
	 *
	 * @since 4.8.3
	 *
	 * @return array
	 */
	private function getRawInternalOptions() {
		// Options from the DB.
		$internalOptions = json_decode( get_option( aioseo()->internalOptions->optionsName ), true );
		if ( empty( $internalOptions ) ) {
			$internalOptions = [];
		}

		return $internalOptions;
	}
}Common/Meta/Amp.php000064400000002202151536241200010103 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

/**
 * Adds support for Google AMP.
 *
 * @since 4.0.0
 */
class Amp {
	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'runAmp' ] );
	}

	/**
	 * Run the AMP hooks.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function runAmp() {
		if ( is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		// Add social meta to AMP plugin.
		$enableAmp = apply_filters( 'aioseo_enable_amp_social_meta', true );

		if ( $enableAmp ) {
			$useSchema = apply_filters( 'aioseo_amp_schema', true );

			if ( $useSchema ) {
				add_action( 'amp_post_template_head', [ $this, 'removeHooksAmpSchema' ], 9 );
			}

			add_action( 'amp_post_template_head', [ aioseo()->head, 'output' ], 11 );
		}
	}

	/**
	 * Remove Hooks with AMP's Schema.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function removeHooksAmpSchema() {
		// Remove AMP Schema hook used for outputting data.
		remove_action( 'amp_post_template_head', 'amp_print_schemaorg_metadata' );
	}
}Common/Meta/Description.php000064400000021263151536241200011661 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Handles the (Open Graph) description.
 *
 * @since 4.0.0
 */
class Description {
	/**
	 * Helpers class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Helpers
	 */
	public $helpers = null;

	/**
	 * Class constructor.
	 *
	* @since 4.1.2
	 */
	public function __construct() {
		$this->helpers = new Helpers( 'description' );
	}

	/**
	 * Returns the homepage description.
	 *
	 * @since 4.0.0
	 *
	 * @return string The homepage description.
	 */
	public function getHomePageDescription() {
		if ( 'page' === get_option( 'show_on_front' ) ) {
			$description = $this->getPostDescription( (int) get_option( 'page_on_front' ) );

			return $description ? $description : aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'description' ) );
		}

		$description = aioseo()->options->searchAppearance->global->metaDescription;
		if ( aioseo()->helpers->isWpmlActive() ) {
			// Allow WPML to translate the title if the homepage is not static.
			$description = apply_filters( 'wpml_translate_single_string', $description, 'admin_texts_aioseo_options_localized', '[aioseo_options_localized]searchAppearance_global_metaDescription' );
		}

		$description = $this->helpers->prepare( $description );

		return $description ? $description : aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'description' ) );
	}

	/**
	 * Returns the description for the current page.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post    The post object (optional).
	 * @param  boolean  $default Whether we want the default value, not the post one.
	 * @return string            The page description.
	 */
	public function getDescription( $post = null, $default = false ) {
		if ( BuddyPressIntegration::isComponentPage() ) {
			return aioseo()->standalone->buddyPress->component->getMeta( 'description' );
		}

		if ( is_home() ) {
			return $this->getHomePageDescription();
		}

		if ( $post || is_singular() || aioseo()->helpers->isStaticPage() ) {
			$description = $this->getPostDescription( $post, $default );
			if ( $description ) {
				return $description;
			}

			if ( is_attachment() ) {
				$post    = empty( $post ) ? aioseo()->helpers->getPost() : $post;
				$caption = wp_get_attachment_caption( $post->ID );

				return $caption ? $this->helpers->prepare( $caption ) : $this->helpers->prepare( $post->post_content );
			}
		}

		if ( is_category() || is_tag() || is_tax() ) {
			$term = $post ? $post : aioseo()->helpers->getTerm();

			return $this->getTermDescription( $term, $default );
		}

		if ( is_author() ) {
			$description = $this->helpers->prepare( aioseo()->options->searchAppearance->archives->author->metaDescription );
			if ( $description ) {
				return $description;
			}

			$author = get_queried_object();

			return $author ? $this->helpers->prepare( get_the_author_meta( 'description', $author->ID ) ) : '';
		}

		if ( is_date() ) {
			return $this->helpers->prepare( aioseo()->options->searchAppearance->archives->date->metaDescription );
		}

		if ( is_search() ) {
			return $this->helpers->prepare( aioseo()->options->searchAppearance->archives->search->metaDescription );
		}

		if ( is_post_type_archive() ) {
			$postType = get_queried_object();
			if ( is_a( $postType, 'WP_Post_Type' ) ) {
				return $this->helpers->prepare( $this->getArchiveDescription( $postType->name ) );
			}
		}

		return '';
	}

	/**
	 * Returns the description for a given post.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|int $post    The post object or ID.
	 * @param  boolean      $default Whether we want the default value, not the post one.
	 * @return string                The post description.
	 */
	public function getPostDescription( $post, $default = false ) {
		$post = $post && is_object( $post ) ? $post : aioseo()->helpers->getPost( $post );
		if ( ! is_a( $post, 'WP_Post' ) ) {
			return '';
		}

		static $posts = [];
		if ( isset( $posts[ $post->ID ] ) ) {
			return $posts[ $post->ID ];
		}

		$description = '';
		$metaData    = aioseo()->meta->metaData->getMetaData( $post );
		if ( ! empty( $metaData->description ) && ! $default ) {
			$description = $this->helpers->prepare( $metaData->description, $post->ID, false );
		}

		if (
			$description ||
			(
				in_array( 'autogenerateDescriptions', aioseo()->internalOptions->deprecatedOptions, true ) &&
				! aioseo()->options->deprecated->searchAppearance->advanced->autogenerateDescriptions
			)
		) {
			$posts[ $post->ID ] = $description;

			return $description;
		}

		$description = $this->helpers->sanitize( $this->getPostTypeDescription( $post->post_type ), $post->ID, $default );

		$generateDescriptions = apply_filters( 'aioseo_generate_descriptions_from_content', true, [ $post ] );
		if ( ! $description && ! post_password_required( $post ) ) {
			$description = $post->post_excerpt;
			if (
				$generateDescriptions &&
				in_array( 'useContentForAutogeneratedDescriptions', aioseo()->internalOptions->deprecatedOptions, true ) &&
				aioseo()->options->deprecated->searchAppearance->advanced->useContentForAutogeneratedDescriptions
			) {
				$description = aioseo()->helpers->getDescriptionFromContent( $post );
			}

			$description = $this->helpers->sanitize( $description, $post->ID, $default );
			if ( ! $description && $generateDescriptions && $post->post_content ) {
				$description = $this->helpers->sanitize( aioseo()->helpers->getDescriptionFromContent( $post ), $post->ID, $default );
			}
		}

		if ( ! is_paged() ) {
			if ( in_array( 'descriptionFormat', aioseo()->internalOptions->deprecatedOptions, true ) ) {
				$descriptionFormat = aioseo()->options->deprecated->searchAppearance->global->descriptionFormat;
				if ( $descriptionFormat ) {
					$description = preg_replace( '/#description/', $description, (string) $descriptionFormat );
				}
			}
		}

		$posts[ $post->ID ] = $description ? $this->helpers->prepare( $description, $post->ID, $default ) : $this->helpers->prepare( term_description( '' ), $post->ID, $default );

		return $posts[ $post->ID ];
	}

	/**
	 * Retrieve the default description for the archive template.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $postType The custom post type.
	 * @return string           The description.
	 */
	public function getArchiveDescription( $postType ) {
		static $archiveDescription = [];
		if ( isset( $archiveDescription[ $postType ] ) ) {
			return $archiveDescription[ $postType ];
		}

		$archiveDescription[ $postType ] = '';

		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		if ( $dynamicOptions->searchAppearance->archives->has( $postType ) ) {
			$archiveDescription[ $postType ] = aioseo()->dynamicOptions->searchAppearance->archives->{$postType}->metaDescription;
		}

		return $archiveDescription[ $postType ];
	}

	/**
	 * Retrieve the default description for the post type.
	 *
	 * @since 4.0.6
	 *
	 * @param  string $postType The post type.
	 * @return string           The description.
	 */
	public function getPostTypeDescription( $postType ) {
		static $postTypeDescription = [];
		if ( isset( $postTypeDescription[ $postType ] ) ) {
			return $postTypeDescription[ $postType ];
		}

		if ( aioseo()->dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
			$description = aioseo()->dynamicOptions->searchAppearance->postTypes->{$postType}->metaDescription;
		}

		$postTypeDescription[ $postType ] = empty( $description ) ? '' : $description;

		return $postTypeDescription[ $postType ];
	}

	/**
	 * Returns the term description.
	 *
	 * @since 4.0.6
	 *
	 * @param  \WP_Term $term    The term object.
	 * @param  boolean  $default Whether we want the default value, not the post one.
	 * @return string            The term description.
	 */
	public function getTermDescription( $term, $default = false ) {
		if ( ! is_a( $term, 'WP_Term' ) ) {
			return '';
		}

		static $terms = [];
		if ( isset( $terms[ $term->term_id ] ) ) {
			return $terms[ $term->term_id ];
		}

		$description = '';
		if (
			in_array( 'autogenerateDescriptions', aioseo()->internalOptions->deprecatedOptions, true ) &&
			! aioseo()->options->deprecated->searchAppearance->advanced->autogenerateDescriptions
		) {
			$terms[ $term->term_id ] = $description;

			return $description;
		}

		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		if ( ! $description && $dynamicOptions->searchAppearance->taxonomies->has( $term->taxonomy ) ) {
			$description = $this->helpers->prepare( aioseo()->dynamicOptions->searchAppearance->taxonomies->{$term->taxonomy}->metaDescription, false, $default );
		}

		$terms[ $term->term_id ] = $description ? $description : $this->helpers->prepare( term_description( $term->term_id ), false, $default );

		return $terms[ $term->term_id ];
	}
}Common/Meta/Helpers.php000064400000006324151536241200011001 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

/**
 * Contains helper methods for the title/description classes.
 *
 * @since 4.1.2
 */
class Helpers {
	use Traits\Helpers\BuddyPress;

	/**
	 * The name of the class where this instance is constructed.
	 *
	 * @since 4.1.2
	 *
	 * @param string $name The name of the class. Either "title" or "description".
	 */
	private $name;

	/**
	 * Supported filters we can run after preparing the value.
	 *
	 * @since 4.1.2
	 *
	 * @var array
	 */
	private $supportedFilters = [
		'title'       => 'aioseo_title',
		'description' => 'aioseo_description'
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.2
	 *
	 * @param string $name The name of the class where this instance is constructed.
	 */
	public function __construct( $name ) {
		$this->name = $name;
	}

	/**
	 * Sanitizes the title/description.
	 *
	 * @since 4.1.2
	 *
	 * @param  string   $value       The value.
	 * @param  int|bool $objectId    The post/term ID.
	 * @param  bool     $replaceTags Whether the smart tags should be replaced.
	 * @return string                The sanitized value.
	 */
	public function sanitize( $value, $objectId = false, $replaceTags = false ) {
		$value = $replaceTags ? $value : aioseo()->tags->replaceTags( $value, $objectId );
		$value = aioseo()->helpers->doShortcodes( $value );

		$value = aioseo()->helpers->decodeHtmlEntities( $value );
		$value = $this->encodeExceptions( $value );
		$value = wp_strip_all_tags( strip_shortcodes( $value ) );
		// Because we encoded the exceptions, we need to decode them again first to prevent double encoding later down the line.
		$value = aioseo()->helpers->decodeHtmlEntities( $value );

		// Trim internal and external whitespace.
		$value = preg_replace( '/[\s]+/u', ' ', (string) trim( $value ) );

		return aioseo()->helpers->internationalize( $value );
	}

	/**
	 * Prepares the title/description before returning it.
	 *
	 * @since 4.1.2
	 *
	 * @param  string   $value       The value.
	 * @param  int|bool $objectId    The post/term ID.
	 * @param  bool     $replaceTags Whether the smart tags should be replaced.
	 * @return string                The sanitized value.
	 */
	public function prepare( $value, $objectId = false, $replaceTags = false ) {
		if (
			! empty( $value ) &&
			! is_admin() &&
			1 < aioseo()->helpers->getPageNumber()
		) {
			$value .= '&nbsp;' . trim( aioseo()->options->searchAppearance->advanced->pagedFormat );
		}

		$value = $replaceTags ? $value : aioseo()->tags->replaceTags( $value, $objectId );
		$value = apply_filters( $this->supportedFilters[ $this->name ], $value );

		return $this->sanitize( $value, $objectId, $replaceTags );
	}

	/**
	 * Encodes a number of exceptions before we strip tags.
	 * We need this function to allow certain character (combinations) in the title/description.
	 *
	 * @since 4.1.1
	 *
	 * @param  string $string The string.
	 * @return string $string The string with exceptions encoded.
	 */
	public function encodeExceptions( $string ) {
		$exceptions = [ '<3' ];
		foreach ( $exceptions as $exception ) {
			$string = preg_replace( "/$exception/", aioseo()->helpers->encodeOutputHtml( $exception ), (string) $string );
		}

		return $string;
	}
}Common/Meta/Included.php000064400000006250151536241200011124 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

/**
 * To check whether SEO is enabled for the queried object.
 *
 * @since 4.0.0
 */
class Included {
	/**
	 * Checks whether the queried object is included.
	 *
	 * @since 4.0.0
	 *
	 * @return bool
	 */
	public function isIncluded() {
		if ( is_admin() || is_feed() ) {
			return false;
		}

		if ( apply_filters( 'aioseo_disable', false ) || $this->isExcludedGlobal() ) {
			return false;
		}

		if ( ! $this->isQueriedObjectPublic() ) {
			return false;
		}

		return true;
	}

	/**
	 * Checks whether the queried object is public.
	 *
	 * @since 4.2.2
	 *
	 * @return bool Whether the queried object is public.
	 */
	protected function isQueriedObjectPublic() {
		$queriedObject = get_queried_object(); // Don't use the getTerm helper here.

		if ( is_a( $queriedObject, 'WP_Post' ) ) {
			return aioseo()->helpers->isPostTypePublic( $queriedObject->post_type );
		}

		// Check if the current page is a post type archive page.
		if ( is_a( $queriedObject, 'WP_Post_Type' ) ) {
			return aioseo()->helpers->isPostTypePublic( $queriedObject->name );
		}

		if ( is_a( $queriedObject, 'WP_Term' ) ) {
			if ( aioseo()->helpers->isWooCommerceProductAttribute( $queriedObject->taxonomy ) ) {
				// Check if the attribute has archives enabled.
				$taxonomy = get_taxonomy( $queriedObject->taxonomy );

				return $taxonomy->public;
			}

			return aioseo()->helpers->isTaxonomyPublic( $queriedObject->taxonomy );
		}

		// Return true in all other cases (e.g. search page, date archive, etc.).
		return true;
	}

	/**
	 * Checks whether the queried object has been excluded globally.
	 *
	 * @since 4.0.0
	 *
	 * @return bool
	 */
	protected function isExcludedGlobal() {
		if ( is_category() || is_tag() || is_tax() ) {
			return $this->isTaxExcludedGlobal();
		}

		if ( ! in_array( 'excludePosts', aioseo()->internalOptions->deprecatedOptions, true ) ) {
			return false;
		}

		$excludedPosts = aioseo()->options->deprecated->searchAppearance->advanced->excludePosts;

		if ( empty( $excludedPosts ) ) {
			return false;
		}

		$ids = [];
		foreach ( $excludedPosts as $object ) {
			$object = json_decode( $object );
			if ( is_int( $object->value ) ) {
				$ids[] = (int) $object->value;
			}
		}

		$post = aioseo()->helpers->getPost();
		if ( empty( $post ) ) {
			return false;
		}

		if ( in_array( (int) $post->ID, $ids, true ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Checks whether the queried object has been excluded globally.
	 *
	 * @since 4.0.0
	 *
	 * @return bool
	 */
	protected function isTaxExcludedGlobal() {
		if ( ! in_array( 'excludeTerms', aioseo()->internalOptions->deprecatedOptions, true ) ) {
			return false;
		}

		$excludedTerms = aioseo()->options->deprecated->searchAppearance->advanced->excludeTerms;

		if ( empty( $excludedTerms ) ) {
			return false;
		}

		$ids = [];
		foreach ( $excludedTerms as $object ) {
			$object = json_decode( $object );
			if ( is_int( $object->value ) ) {
				$ids[] = (int) $object->value;
			}
		}

		$term = aioseo()->helpers->getTerm();
		if ( in_array( (int) $term->term_id, $ids, true ) ) {
			return true;
		}

		return false;
	}
}Common/Meta/Keywords.php000064400000017722151536241200011212 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Handles the keywords.
 *
 * @since 4.0.0
 */
class Keywords {
	/**
	 * Get the keywords for the meta output.
	 *
	 * @since 4.0.0
	 *
	 * @return string The keywords as a string.
	 */
	public function getKeywords() {
		if ( ! aioseo()->options->searchAppearance->advanced->useKeywords ) {
			return '';
		}

		if ( BuddyPressIntegration::isComponentPage() ) {
			return aioseo()->standalone->buddyPress->component->getMeta( 'keywords' );
		}

		$isStaticArchive = aioseo()->helpers->isWooCommerceShopPage() || aioseo()->helpers->isStaticPostsPage();
		$dynamicContent  = is_archive() || is_post_type_archive() || is_home() || aioseo()->helpers->isWooCommerceShopPage() || is_category() || is_tag() || is_tax();
		$generate        = aioseo()->options->searchAppearance->advanced->dynamicallyGenerateKeywords;
		if ( $dynamicContent && $generate ) {
			return $this->prepareKeywords( $this->getGeneratedKeywords() );
		}

		if ( is_front_page() && ! aioseo()->helpers->isStaticHomePage() ) {
			$keywords = $this->extractMetaKeywords( aioseo()->options->searchAppearance->global->keywords );

			return $this->prepareKeywords( $keywords );
		}

		if ( $dynamicContent && ! $isStaticArchive ) {
			if ( is_date() ) {
				$keywords = $this->extractMetaKeywords( aioseo()->options->searchAppearance->archives->date->advanced->keywords );

				return $this->prepareKeywords( $keywords );
			}

			if ( is_author() ) {
				$keywords = $this->extractMetaKeywords( aioseo()->options->searchAppearance->archives->author->advanced->keywords );

				return $this->prepareKeywords( $keywords );
			}

			if ( is_search() ) {
				$keywords = $this->extractMetaKeywords( aioseo()->options->searchAppearance->archives->search->advanced->keywords );

				return $this->prepareKeywords( $keywords );
			}

			$postType = get_queried_object();

			return is_a( $postType, 'WP_Post_Type' )
				? $this->prepareKeywords( $this->getArchiveKeywords( $postType->name ) )
				: '';
		}

		return $this->prepareKeywords( $this->getAllKeywords() );
	}

	/**
	 * Retrieves the default keywords for the archive template.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $postType The post type.
	 * @return array            The keywords.
	 */
	public function getArchiveKeywords( $postType ) {
		static $archiveKeywords = [];
		if ( isset( $archiveKeywords[ $postType ] ) ) {
			return $archiveKeywords[ $postType ];
		}

		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		if ( $dynamicOptions->searchAppearance->archives->has( $postType ) ) {
			$keywords = $this->extractMetaKeywords( aioseo()->dynamicOptions->searchAppearance->archives->{ $postType }->advanced->keywords );
		}

		$archiveKeywords[ $postType ] = empty( $keywords ) ? [] : $keywords;

		return $archiveKeywords[ $postType ];
	}

	/**
	 * Get generated keywords for an archive page.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of generated keywords.
	 */
	private function getGeneratedKeywords() {
		global $posts, $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		$keywords        = [];
		$isStaticArchive = aioseo()->helpers->isWooCommerceShopPage() || aioseo()->helpers->isStaticPostsPage();
		if ( $isStaticArchive ) {
			$keywords = $this->getAllKeywords();
		} elseif ( is_front_page() && ! aioseo()->helpers->isStaticHomePage() ) {
			$keywords = $this->extractMetaKeywords( aioseo()->options->searchAppearance->global->keywords );
		} elseif ( is_category() || is_tag() || is_tax() ) {
			$metaData = aioseo()->meta->metaData->getMetaData();
			if ( ! empty( $metaData->keywords ) ) {
				$keywords = $this->extractMetaKeywords( $metaData->keywords );
			}
		}

		$wpPosts = $posts;
		if ( empty( $posts ) ) {
			$wpPosts = array_filter( [ aioseo()->helpers->getPost() ] );
		}

		// Turn off current query so we can get specific post data.
		// phpcs:disable Squiz.NamingConventions.ValidVariableName
		$originalTag      = $wp_query->is_tag;
		$originalTax      = $wp_query->is_tax;
		$originalCategory = $wp_query->is_category;

		$wp_query->is_tag      = false;
		$wp_query->is_tax      = false;
		$wp_query->is_category = false;

		foreach ( $wpPosts as $post ) {
			$metaData    = aioseo()->meta->metaData->getMetaData( $post );
			$tmpKeywords = $this->extractMetaKeywords( $metaData->keywords );
			if ( count( $tmpKeywords ) ) {
				foreach ( $tmpKeywords as $keyword ) {
					$keywords[] = $keyword;
				}
			}
		}

		$wp_query->is_tag      = $originalTag;
		$wp_query->is_tax      = $originalTax;
		$wp_query->is_category = $originalCategory;
		// phpcs:enable Squiz.NamingConventions.ValidVariableName

		return $keywords;
	}

	/**
	 * Returns the keywords.
	 *
	 * @since 4.0.0
	 *
	 * @return array A list of unique keywords.
	 */
	public function getAllKeywords() {
		$keywords = [];
		$post     = aioseo()->helpers->getPost();
		$metaData = aioseo()->meta->metaData->getMetaData();
		if ( ! empty( $metaData->keywords ) ) {
			$keywords = $this->extractMetaKeywords( $metaData->keywords );
		}

		if ( $post ) {
			if ( aioseo()->options->searchAppearance->advanced->useTagsForMetaKeywords ) {
				$keywords = array_merge( $keywords, aioseo()->helpers->getAllTags( $post->ID ) );
			}

			if ( aioseo()->options->searchAppearance->advanced->useCategoriesForMetaKeywords && ! is_page() ) {
				$keywords = array_merge( $keywords, aioseo()->helpers->getAllCategories( $post->ID ) );
			}
		}

		return $keywords;
	}

	/**
	 * Prepares the keywords for display.
	 *
	 * @since 4.0.0
	 *
	 * @param  array  $keywords Raw keywords.
	 * @return string           A list of prepared keywords, comma-separated.
	 */
	public function prepareKeywords( $keywords ) {
		$keywords = $this->getUniqueKeywords( $keywords );
		$keywords = trim( $keywords );
		$keywords = aioseo()->helpers->internationalize( $keywords );
		$keywords = stripslashes( $keywords );
		$keywords = str_replace( '"', '', $keywords );
		$keywords = wp_filter_nohtml_kses( $keywords );

		return apply_filters( 'aioseo_keywords', $keywords );
	}

	/**
	 * Returns an array of keywords, based on a stringified list separated by commas.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $keywords The keywords string.
	 * @return array            The keywords.
	 */
	public function keywordStringToList( $keywords ) {
		$keywords = str_replace( '"', '', $keywords );

		return ! empty( $keywords ) ? explode( ',', $keywords ) : [];
	}

	/**
	 * Returns a stringified list of unique keywords, separated by commas.
	 *
	 * @since 4.0.0
	 *
	 * @param  array        $keywords The keywords.
	 * @param  boolean      $toString Whether or not to turn it into a comma separated string.
	 * @return string|array           The keywords.
	 */
	public function getUniqueKeywords( $keywords, $toString = true ) {
		$keywords = $this->keywordsToLowerCase( $keywords );

		return $toString ? implode( ',', $keywords ) : $keywords;
	}

	/**
	 * Returns the keywords in lowercase.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $keywords The keywords.
	 * @return array           The formatted keywords.
	 */
	private function keywordsToLowerCase( $keywords ) {
		$smallKeywords = [];
		if ( ! is_array( $keywords ) ) {
			$keywords = $this->keywordStringToList( $keywords );
		}
		if ( ! empty( $keywords ) ) {
			foreach ( $keywords as $keyword ) {
				$smallKeywords[] = trim( aioseo()->helpers->toLowercase( $keyword ) );
			}
		}

		return array_unique( $smallKeywords );
	}

	/**
	 * Extract keywords and then return as a string.
	 *
	 * @since 4.0.0
	 *
	 * @param  array|string $keywords An array of keywords or a json string.
	 * @return array                  An array of keywords that were extracted.
	 */
	public function extractMetaKeywords( $keywords ) {
		$extracted = [];

		$keywords = is_string( $keywords ) ? json_decode( $keywords ) : $keywords;

		if ( ! empty( $keywords ) ) {
			foreach ( $keywords as $keyword ) {
				$extracted[] = trim( $keyword->value );
			}
		}

		return $extracted;
	}
}Common/Meta/Links.php000064400000011442151536241200010454 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

/**
 * Instantiates the meta links "next" and "prev".
 *
 * @since 4.0.0
 */
class Links {
	/**
	 * Get the prev/next links for the current page.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of link data.
	 */
	public function getLinks() {
		$links = [
			'prev' => '',
			'next' => '',
		];

		if ( is_home() || is_archive() || is_paged() ) {
			$links = $this->getHomeLinks();
		}

		if ( is_page() || is_single() ) {
			global $post;
			$links = $this->getPostLinks( $post );
		}

		$links['prev'] = apply_filters( 'aioseo_prev_link', $links['prev'] );
		$links['next'] = apply_filters( 'aioseo_next_link', $links['next'] );

		return $links;
	}

	/**
	 * Get the prev/next links for the current page (home/archive, etc.).
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of link data.
	 */
	private function getHomeLinks() {
		$prev = '';
		$next = '';
		$page = aioseo()->helpers->getPageNumber();

		global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$maxPage = $wp_query->max_num_pages; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if ( $page > 1 ) {
			$prev = get_previous_posts_page_link();
		}
		if ( $page < $maxPage ) {
			$next  = get_next_posts_page_link();
			$paged = is_paged();
			if ( ! is_single() ) {
				if ( ! $paged ) {
					$page = 1;
				}
				$nextpage = intval( $page ) + 1;
				if ( ! $maxPage || $maxPage >= $nextpage ) {
					$next = get_pagenum_link( $nextpage );
				}
			}
		}

		// Remove trailing slashes if not set in the permalink structure.
		$prev = aioseo()->helpers->maybeRemoveTrailingSlash( $prev );
		$next = aioseo()->helpers->maybeRemoveTrailingSlash( $next );

		// Remove any query args that may be set on the URL, except if the site is using plain permalinks.
		$permalinkStructure = get_option( 'permalink_structure' );
		if ( ! empty( $permalinkStructure ) ) {
			$prev = explode( '?', $prev )[0];
			$next = explode( '?', $next )[0];
		}

		return [
			'prev' => $prev,
			'next' => $next,
		];
	}

	/**
	 * Get the prev/next links for the current post.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post The post.
	 * @return array          An array of link data.
	 */
	private function getPostLinks( $post ) {
		$prev     = '';
		$next     = '';
		$numpages = 1;
		$page     = aioseo()->helpers->getPageNumber();
		$content  = is_a( $post, 'WP_Post' ) ? $post->post_content : '';
		if ( false !== strpos( $content, '<!--nextpage-->', 0 ) ) {
			$content = str_replace( "\n<!--nextpage-->\n", '<!--nextpage-->', $content );
			$content = str_replace( "\n<!--nextpage-->", '<!--nextpage-->', $content );
			$content = str_replace( "<!--nextpage-->\n", '<!--nextpage-->', $content );
			// Ignore nextpage at the beginning of the content.
			if ( 0 === strpos( $content, '<!--nextpage-->', 0 ) ) {
				$content = substr( $content, 15 );
			}
			$pages    = explode( '<!--nextpage-->', $content );
			$numpages = count( $pages );
		} else {
			$page = null;
		}
		if ( ! empty( $page ) ) {
			if ( $page > 1 ) {
				$prev = $this->getLinkPage( $page - 1 );
			}
			if ( $page + 1 <= $numpages ) {
				$next = $this->getLinkPage( $page + 1 );
			}
		}

		return [
			'prev' => $prev,
			'next' => $next,
		];
	}

	/**
	 * This is a clone of _wp_link_page, except that we don't output HTML.
	 *
	 * @since 4.0.0
	 *
	 * @param  integer $number The page number.
	 * @return string          The URL.
	 */
	private function getLinkPage( $number ) {
		global $wp_rewrite; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$post      = get_post();
		$queryArgs = [];

		if ( 1 === (int) $number ) {
			$url = get_permalink();
		} else {
			if ( ! get_option( 'permalink_structure' ) || in_array( $post->post_status, [ 'draft', 'pending' ], true ) ) {
				$url = add_query_arg( 'page', $number, get_permalink() );
			} elseif ( 'page' === get_option( 'show_on_front' ) && get_option( 'page_on_front' ) === $post->ID ) {
				$url = trailingslashit( get_permalink() ) . user_trailingslashit( "$wp_rewrite->pagination_base/" . $number, 'single_paged' ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			} else {
				$url = trailingslashit( get_permalink() ) . user_trailingslashit( $number, 'single_paged' );
			}
		}

		if ( is_preview() ) {
			// phpcs:disable HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended	
			if ( ( 'draft' !== $post->post_status ) && isset( $_GET['preview_id'], $_GET['preview_nonce'] ) ) {
				$queryArgs['preview_id']    = sanitize_text_field( wp_unslash( $_GET['preview_id'] ) );
				$queryArgs['preview_nonce'] = sanitize_text_field( wp_unslash( $_GET['preview_nonce'] ) );
			}
			// phpcs:enable

			$url = get_preview_post_link( $post, $queryArgs, $url );
		}

		return esc_url( $url );
	}
}Common/Meta/Meta.php000064400000002700151536241200010257 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Instantiates the Meta classes.
 *
 * @since 4.0.0
 */
class Meta {
	/**
	 * MetaData class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var MetaData
	 */
	public $metaData = null;

	/**
	 * Title class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Title
	 */
	public $title = null;

	/**
	 * Description class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Description
	 */
	public $description = null;

	/**
	 * Keywords class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Keywords
	 */
	public $keywords = null;

	/**
	 * Robots class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Robots
	 */
	public $robots = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->metaData     = new MetaData();
		$this->title        = new Title();
		$this->description  = new Description();
		$this->keywords     = new Keywords();
		$this->robots       = new Robots();

		new Amp();
		new Links();

		add_action( 'delete_post', [ $this, 'deletePostMeta' ], 1000 );
	}

	/**
	 * When we delete the meta, we want to delete our post model.
	 *
	 * @since 4.0.1
	 *
	 * @param  integer $postId The ID of the post.
	 * @return void
	 */
	public function deletePostMeta( $postId ) {
		$aioseoPost = Models\Post::getPost( $postId );
		if ( $aioseoPost->exists() ) {
			$aioseoPost->delete();
		}
	}
}Common/Meta/MetaData.php000064400000007556151536241200011067 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Handles fetching metadata for the current object.
 *
 * @since 4.0.0
 */
class MetaData {
	/**
	 * The cached meta data for posts.
	 *
	 * @since 4.1.7
	 *
	 * @var array
	 */
	private $posts = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'wpml_pro_translation_completed', [ $this, 'updateWpmlLocalization' ], 1000, 3 );
	}

	/**
	 * Update the localized data in our posts table.
	 *
	 * @since 4.0.0
	 *
	 * @param  integer $postId The post ID.
	 * @param  array   $fields An array of fields to update.
	 * @return void
	 */
	public function updateWpmlLocalization( $postId, $fields = [], $job = null ) {
		$aioseoFields = [
			'_aioseo_title',
			'_aioseo_description',
			'_aioseo_keywords',
			'_aioseo_og_title',
			'_aioseo_og_description',
			'_aioseo_twitter_title',
			'_aioseo_twitter_description'
		];

		$parentId    = $job->original_doc_id;
		$parentPost  = Models\Post::getPost( $parentId );
		$currentPost = Models\Post::getPost( $postId );
		$columns     = $parentPost->getColumns();
		foreach ( $columns as $column => $value ) {
			// Skip the ID columns.
			if ( 'id' === $column || 'post_id' === $column ) {
				continue;
			}

			$currentPost->$column = $parentPost->$column;
		}

		$currentPost->post_id = $postId;

		foreach ( $aioseoFields as $aioseoField ) {
			if ( ! empty( $fields[ 'field-' . $aioseoField . '-0' ] ) ) {
				$value = $fields[ 'field-' . $aioseoField . '-0' ]['data'];
				if ( '_aioseo_keywords' === $aioseoField ) {
					$value = explode( ',', $value );
					foreach ( $value as $k => $keyword ) {
						$value[ $k ] = [
							'label' => $keyword,
							'value' => $keyword
						];
					}

					$value = wp_json_encode( $value );
				}
				$currentPost->{ str_replace( '_aioseo_', '', $aioseoField ) } = $value;
			}
		}

		$currentPost->save();
	}

	/**
	 * Returns the metadata for the current object.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post         $post The post object (optional).
	 * @return Models\Post|bool       The meta data or false.
	 */
	public function getMetaData( $post = null ) {
		if ( ! $post ) {
			$post = aioseo()->helpers->getPost();
		}

		if ( $post ) {
			$post = is_object( $post ) ? $post : aioseo()->helpers->getPost( $post );
			// If we still have no post, let's return false.
			if ( ! is_a( $post, 'WP_Post' ) ) {
				return false;
			}

			if ( isset( $this->posts[ $post->ID ] ) ) {
				return $this->posts[ $post->ID ];
			}

			$this->posts[ $post->ID ] = Models\Post::getPost( $post->ID );

			if ( ! $this->posts[ $post->ID ]->exists() ) {
				$migratedMeta = aioseo()->migration->meta->getMigratedPostMeta( $post->ID );
				if ( ! empty( $migratedMeta ) ) {
					foreach ( $migratedMeta as $k => $v ) {
						$this->posts[ $post->ID ]->{$k} = $v;
					}

					$this->posts[ $post->ID ]->save();
				}
			}

			return $this->posts[ $post->ID ];
		}

		return false;
	}

	/**
	 * Returns the cached OG image from the meta data.
	 *
	 * @since 4.1.6
	 *
	 * @param  Object $metaData The meta data object.
	 * @return array            An array of image data.
	 */
	public function getCachedOgImage( $metaData ) {
		return [
			$metaData->og_image_url,
			isset( $metaData->og_image_width ) ? $metaData->og_image_width : null,
			isset( $metaData->og_image_height ) ? $metaData->og_image_height : null
		];
	}

	/**
	 * Busts the meta data cache for a given post.
	 *
	 * @since 4.1.7
	 *
	 * @param  int         $postId   The post ID.
	 * @param  Models\Post $metaData The meta data.
	 * @return void
	 */
	public function bustPostCache( $postId, $metaData = null ) {
		if ( null === $metaData || ! is_a( $metaData, 'AIOSEO\Plugin\Common\Models\Post' ) ) {
			unset( $this->posts[ $postId ] );
		}

		$this->posts[ $postId ] = $metaData;
	}
}Common/Meta/Robots.php000064400000026417151536241200010654 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Handles the robots meta tag.
 *
 * @since 4.0.0
 */
class Robots {
	/**
	 * The robots meta tag attributes.
	 *
	 * We'll already set the keys on construction so that we always output the attributes in the same order.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $attributes = [
		'noindex'           => '',
		'nofollow'          => '',
		'noarchive'         => '',
		'nosnippet'         => '',
		'noimageindex'      => '',
		'noodp'             => '',
		'notranslate'       => '',
		'max-snippet'       => '',
		'max-image-preview' => '',
		'max-video-preview' => ''
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.16
	 */
	public function __construct() {
		add_action( 'wp_loaded', [ $this, 'unregisterWooCommerceNoindex' ] );
		add_action( 'template_redirect', [ $this, 'noindexFeed' ] );
		add_action( 'wp_head', [ $this, 'disableWpRobotsCore' ], -1 );
	}

	/**
	 * Prevents WooCommerce from noindexing the Cart/Checkout pages.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	public function unregisterWooCommerceNoindex() {
		if ( has_action( 'wp_head', 'wc_page_noindex' ) ) {
			remove_action( 'wp_head', 'wc_page_noindex' );
		}
	}

	/**
	 * Prevents WP Core from outputting its own robots meta tag.
	 *
	 * @since 4.0.16
	 *
	 * @return void
	 */
	public function disableWpRobotsCore() {
		remove_all_filters( 'wp_robots' );
	}

	/**
	 * Noindexes RSS feed pages.
	 *
	 * @since 4.0.17
	 *
	 * @return void
	 */
	public function noindexFeed() {
		if (
			! is_feed() ||
			( ! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default && ! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindexFeed )
		) {
			return;
		}

		header( 'X-Robots-Tag: noindex, follow', true );
	}

	/**
	 * Returns the robots meta tag value.
	 *
	 * @since 4.0.0
	 *
	 * @return mixed The robots meta tag value or false.
	 */
	public function meta() {
		// We need this check to happen first as spammers can attempt to make the page appear like a post or term by using URL params e.g. "cat=".
		if ( is_search() ) {
			$this->globalValues( [ 'archives', 'search' ] );

			return $this->metaHelper();
		}

		if ( BuddyPressIntegration::isComponentPage() ) {
			return aioseo()->standalone->buddyPress->component->getMeta( 'robots' );
		}

		if ( is_category() || is_tag() || is_tax() ) {
			$this->term();

			return $this->metaHelper();
		}

		if ( is_home() && 'page' !== get_option( 'show_on_front' ) ) {
			$this->globalValues();

			return $this->metaHelper();
		}

		$post = aioseo()->helpers->getPost();
		if ( $post ) {
			$this->post();

			return $this->metaHelper();
		}

		if ( is_author() ) {
			$this->globalValues( [ 'archives', 'author' ] );

			return $this->metaHelper();
		}

		if ( is_date() ) {
			$this->globalValues( [ 'archives', 'date' ] );

			return $this->metaHelper();
		}

		if ( is_404() ) {
			return apply_filters( 'aioseo_404_robots', 'noindex' );
		}

		if ( is_archive() ) {
			$this->archives();

			return $this->metaHelper();
		}
	}

	/**
	 * Stringifies and filters the robots meta tag value.
	 *
	 * Acts as a helper for meta().
	 *
	 * @since 4.0.0
	 *
	 * @param  bool         $array Whether or not to return the value as an array.
	 * @return array|string        The robots meta tag value.
	 */
	public function metaHelper( $array = false ) {
		$pageNumber = aioseo()->helpers->getPageNumber();
		if ( 1 < $pageNumber || aioseo()->helpers->getCommentPageNumber() ) {
			if (
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default ||
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindexPaginated
			) {
				$this->attributes['noindex'] = 'noindex';
			}

			if (
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default ||
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->nofollowPaginated
			) {
				$this->attributes['nofollow'] = 'nofollow';
			}
		}

		// Never allow users to noindex the first page of the homepage.
		if ( is_front_page() && 1 === $pageNumber ) {
			$this->attributes['noindex'] = '';
		}

		// Because we prevent WordPress Core from outputting a robots tag in disableWpRobotsCore(), we need to noindex/nofollow non-public sites ourselves.
		if ( ! get_option( 'blog_public' ) ) {
			$this->attributes['noindex']  = 'noindex';
			$this->attributes['nofollow'] = 'nofollow';
		}

		$this->attributes = array_filter( (array) apply_filters( 'aioseo_robots_meta', $this->attributes ) );

		return $array ? $this->attributes : implode( ', ', $this->attributes );
	}

	/**
	 * Sets the attributes for the current post.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|null $post The post object.
	 * @return void
	 */
	public function post( $post = null ) {
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		$post           = aioseo()->helpers->getPost( $post );
		$metaData       = aioseo()->meta->metaData->getMetaData( $post );

		if ( ! empty( $metaData ) && ! $metaData->robots_default ) {
			$this->metaValues( $metaData );

			return;
		}

		if ( $dynamicOptions->searchAppearance->postTypes->has( $post->post_type ) ) {
			$this->globalValues( [ 'postTypes', $post->post_type ], true );
		}
	}

	/**
	 * Returns the robots meta tag value for the current term.
	 *
	 * @since 4.0.6
	 *
	 * @param  \WP_Term|null $term The term object if any.
	 * @return void
	 */
	public function term( $term = null ) {
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		$term           = is_a( $term, 'WP_Term' ) ? $term : aioseo()->helpers->getTerm();

		// Misbehaving themes/plugins can manipulate the loop and make archives return a post as the queried object.
		if ( ! is_a( $term, 'WP_Term' ) ) {
			return;
		}

		if ( $dynamicOptions->searchAppearance->taxonomies->has( $term->taxonomy ) ) {
			$this->globalValues( [ 'taxonomies', $term->taxonomy ], true );

			return;
		}

		$this->globalValues();
	}

	/**
	 * Sets the attributes for the current archive.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function archives() {
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		$postType       = aioseo()->helpers->getTerm();
		if ( ! empty( $postType->name ) && $dynamicOptions->searchAppearance->archives->has( $postType->name ) ) {
			$this->globalValues( [ 'archives', $postType->name ], true );
		}
	}

	/**
	 * Sets the attributes based on the global values.
	 *
	 * @since 4.0.0
	 *
	 * @param  array   $optionOrder     The order in which the options need to be called to get the relevant robots meta settings.
	 * @param  boolean $isDynamicOption Whether this is for a dynamic option.
	 * @return void
	 */
	public function globalValues( $optionOrder = [], $isDynamicOption = false ) {
		$robotsMeta = [];
		if ( count( $optionOrder ) ) {
			$options = $isDynamicOption
				? aioseo()->dynamicOptions->noConflict( true )->searchAppearance
				: aioseo()->options->noConflict()->searchAppearance;

			foreach ( $optionOrder as $option ) {
				if ( ! $options->has( $option, false ) ) {
					return;
				}
				$options = $options->$option;
			}

			$clonedOptions = clone $options;
			if ( ! $clonedOptions->show ) {
				$this->attributes['noindex'] = 'noindex';
			}

			if ( ! isset( $options->advanced->robotsMeta ) ) {
				$robotsMeta = aioseo()->options->searchAppearance->advanced->globalRobotsMeta->all();
			} else {
				$robotsMeta = $options->advanced->robotsMeta->all();
				if ( $robotsMeta['default'] ) {
					$robotsMeta = aioseo()->options->searchAppearance->advanced->globalRobotsMeta->all();
				}
			}
		} else {
			$robotsMeta = aioseo()->options->searchAppearance->advanced->globalRobotsMeta->all();
		}

		$this->attributes['max-image-preview'] = 'max-image-preview:large';

		if ( $robotsMeta['default'] ) {
			return;
		}

		if ( $robotsMeta['noindex'] ) {
			$this->attributes['noindex'] = 'noindex';
		}
		if ( $robotsMeta['nofollow'] ) {
			$this->attributes['nofollow'] = 'nofollow';
		}
		if ( $robotsMeta['noarchive'] ) {
			$this->attributes['noarchive'] = 'noarchive';
		}
		$noSnippet = $robotsMeta['nosnippet'];
		if ( $noSnippet ) {
			$this->attributes['nosnippet'] = 'nosnippet';
		}
		if ( $robotsMeta['noodp'] ) {
			$this->attributes['noodp'] = 'noodp';
		}
		if ( $robotsMeta['notranslate'] ) {
			$this->attributes['notranslate'] = 'notranslate';
		}
		$maxSnippet = $robotsMeta['maxSnippet'];
		if ( ! $noSnippet && is_numeric( $maxSnippet ) ) {
			$this->attributes['max-snippet'] = "max-snippet:$maxSnippet";
		}
		$maxImagePreview = $robotsMeta['maxImagePreview'];
		$noImageIndex    = $robotsMeta['noimageindex'];
		if ( ! $noImageIndex && $maxImagePreview && in_array( $maxImagePreview, [ 'none', 'standard', 'large' ], true ) ) {
			$this->attributes['max-image-preview'] = "max-image-preview:$maxImagePreview";
		}
		$maxVideoPreview = $robotsMeta['maxVideoPreview'];
		if ( isset( $maxVideoPreview ) && is_numeric( $maxVideoPreview ) ) {
			$this->attributes['max-video-preview'] = "max-video-preview:$maxVideoPreview";
		}

		// Check this last so that we can prevent max-image-preview from being output if noimageindex is enabled.
		if ( $noImageIndex ) {
			$this->attributes['max-image-preview'] = '';
			$this->attributes['noimageindex']      = 'noimageindex';
		}
	}

	/**
	 * Sets the attributes from the meta data.
	 *
	 * @since 4.0.0
	 *
	 * @param  \AIOSEO\Plugin\Common\Models\Post|\AIOSEO\Plugin\Pro\Models\Term $metaData The post/term meta data.
	 * @return void
	 */
	protected function metaValues( $metaData ) {
		if ( $metaData->robots_noindex || $this->isPasswordProtected() ) {
			$this->attributes['noindex'] = 'noindex';
		}
		if ( $metaData->robots_nofollow ) {
			$this->attributes['nofollow'] = 'nofollow';
		}
		if ( $metaData->robots_noarchive ) {
			$this->attributes['noarchive'] = 'noarchive';
		}
		if ( $metaData->robots_nosnippet ) {
			$this->attributes['nosnippet'] = 'nosnippet';
		}
		if ( $metaData->robots_noodp ) {
			$this->attributes['noodp'] = 'noodp';
		}
		if ( $metaData->robots_notranslate ) {
			$this->attributes['notranslate'] = 'notranslate';
		}
		if ( ! $metaData->robots_nosnippet && isset( $metaData->robots_max_snippet ) && is_numeric( $metaData->robots_max_snippet ) ) {
			$this->attributes['max-snippet'] = "max-snippet:$metaData->robots_max_snippet";
		}
		if ( ! $metaData->robots_noimageindex && $metaData->robots_max_imagepreview && in_array( $metaData->robots_max_imagepreview, [ 'none', 'standard', 'large' ], true ) ) {
			$this->attributes['max-image-preview'] = "max-image-preview:$metaData->robots_max_imagepreview";
		}
		if ( isset( $metaData->robots_max_videopreview ) && is_numeric( $metaData->robots_max_videopreview ) ) {
			$this->attributes['max-video-preview'] = "max-video-preview:$metaData->robots_max_videopreview";
		}

		// Check this last so that we can prevent max-image-preview from being output if noimageindex is enabled.
		if ( $metaData->robots_noimageindex ) {
			$this->attributes['max-image-preview'] = '';
			$this->attributes['noimageindex']      = 'noimageindex';
		}
	}

	/**
	 * Checks whether the current post is password protected.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether the post is password protected.
	 */
	private function isPasswordProtected() {
		$post = aioseo()->helpers->getPost();

		return is_object( $post ) && $post->post_password;
	}
}Common/Meta/SiteVerification.php000064400000001650151536241200012643 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

/**
 * Handles the site verification meta tags.
 *
 * @since 4.0.0
 */
class SiteVerification {
	/**
	 * An array of webmaster tools and their meta names.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $webmasterTools = [
		'google'    => 'google-site-verification',
		'bing'      => 'msvalidate.01',
		'pinterest' => 'p:domain_verify',
		'yandex'    => 'yandex-verification',
		'baidu'     => 'baidu-site-verification'
	];

	/**
	 * Returns the robots meta tag value.
	 *
	 * @since 4.0.0
	 *
	 * @return mixed The robots meta tag value or false.
	 */
	public function meta() {
		$metaArray = [];
		foreach ( $this->webmasterTools as $key => $metaName ) {
			$value = aioseo()->options->webmasterTools->$key;
			if ( ! empty( $value ) ) {
				$metaArray[ $metaName ] = $value;
			}
		}

		return $metaArray;
	}
}Common/Meta/Title.php000064400000015461151536241200010462 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Handles the title.
 *
 * @since 4.0.0
 */
class Title {
	/**
	 * Helpers class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Helpers
	 */
	public $helpers = null;

	/**
	 * Class constructor.
	 *
	* @since 4.1.2
	 */
	public function __construct() {
		$this->helpers = new Helpers( 'title' );
	}

	/**
	 * Returns the filtered page title.
	 *
	 * Acts as a helper for getTitle() because we need to encode the title before sending it back to the filter.
	 *
	 * @since 4.0.0
	 *
	 * @return string The page title.
	 */
	public function filterPageTitle( $wpTitle = '' ) {
		$title = $this->getTitle();

		return ! empty( $title ) ? aioseo()->helpers->encodeOutputHtml( $title ) : $wpTitle;
	}

	/**
	 * Returns the homepage title.
	 *
	 * @since 4.0.0
	 *
	 * @return string The homepage title.
	 */
	public function getHomePageTitle() {
		if ( 'page' === get_option( 'show_on_front' ) ) {
			$title = $this->getPostTitle( (int) get_option( 'page_on_front' ) );

			return $title ? $title : aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) );
		}

		$title = aioseo()->options->searchAppearance->global->siteTitle;
		if ( aioseo()->helpers->isWpmlActive() ) {
			// Allow WPML to translate the title if the homepage is not static.
			$title = apply_filters( 'wpml_translate_single_string', $title, 'admin_texts_aioseo_options_localized', '[aioseo_options_localized]searchAppearance_global_siteTitle' );
		}

		$title = $this->helpers->prepare( $title );

		return $title ? $title : aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) );
	}

	/**
	 * Returns the title for the current page.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post    The post object (optional).
	 * @param  boolean  $default Whether we want the default value, not the post one.
	 * @return string            The page title.
	 */
	public function getTitle( $post = null, $default = false ) {
		if ( BuddyPressIntegration::isComponentPage() ) {
			return aioseo()->standalone->buddyPress->component->getMeta( 'title' );
		}

		if ( is_home() ) {
			return $this->getHomePageTitle();
		}

		if ( $post || is_singular() || aioseo()->helpers->isStaticPage() ) {
			return $this->getPostTitle( $post, $default );
		}

		if ( is_category() || is_tag() || is_tax() ) {
			$term = $post ? $post : aioseo()->helpers->getTerm();

			return $this->getTermTitle( $term, $default );
		}

		if ( is_author() ) {
			return $this->helpers->prepare( aioseo()->options->searchAppearance->archives->author->title );
		}

		if ( is_date() ) {
			return $this->helpers->prepare( aioseo()->options->searchAppearance->archives->date->title );
		}

		if ( is_search() ) {
			return $this->helpers->prepare( aioseo()->options->searchAppearance->archives->search->title );
		}

		if ( is_post_type_archive() ) {
			$postType = get_queried_object();
			if ( is_a( $postType, 'WP_Post_Type' ) ) {
				return $this->helpers->prepare( $this->getArchiveTitle( $postType->name ) );
			}
		}

		return '';
	}

	/**
	 * Returns the post title.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|int $post    The post object or ID.
	 * @param  boolean      $default Whether we want the default value, not the post one.
	 * @return string                The post title.
	 */
	public function getPostTitle( $post, $default = false ) {
		$post = $post && is_object( $post ) ? $post : aioseo()->helpers->getPost( $post );
		if ( ! is_a( $post, 'WP_Post' ) ) {
			return '';
		}

		static $posts = [];
		if ( isset( $posts[ $post->ID ] ) ) {
			return $posts[ $post->ID ];
		}

		$title    = '';
		$metaData = aioseo()->meta->metaData->getMetaData( $post );

		if ( ! empty( $metaData->title ) && ! $default ) {
			$title = $this->helpers->prepare( $metaData->title, $post->ID );
		}

		if ( ! $title ) {
			$title = $this->helpers->prepare( $this->getPostTypeTitle( $post->post_type ), $post->ID, $default );
		}

		// If this post is the static home page and we have no title, let's reset to the site name.
		if ( empty( $title ) && 'page' === get_option( 'show_on_front' ) && (int) get_option( 'page_on_front' ) === $post->ID ) {
			$title = aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) );
		}

		if ( empty( $title ) ) {
			// Just return the WP default.
			$title = get_the_title( $post->ID ) . ' - ' . get_bloginfo( 'name' );
			$title = aioseo()->helpers->decodeHtmlEntities( $title );
		}

		$posts[ $post->ID ] = $title;

		return $posts[ $post->ID ];
	}

	/**
	 * Retrieve the default title for the archive template.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $postType The custom post type.
	 * @return string           The title.
	 */
	public function getArchiveTitle( $postType ) {
		static $archiveTitle = [];
		if ( isset( $archiveTitle[ $postType ] ) ) {
			return $archiveTitle[ $postType ];
		}

		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		if ( $dynamicOptions->searchAppearance->archives->has( $postType ) ) {
			$title = aioseo()->dynamicOptions->searchAppearance->archives->{ $postType }->title;
		}

		$archiveTitle[ $postType ] = empty( $title ) ? '' : $title;

		return $archiveTitle[ $postType ];
	}

	/**
	 * Retrieve the default title for the post type.
	 *
	 * @since 4.0.6
	 *
	 * @param  string $postType The post type.
	 * @return string           The title.
	 */
	public function getPostTypeTitle( $postType ) {
		static $postTypeTitle = [];
		if ( isset( $postTypeTitle[ $postType ] ) ) {
			return $postTypeTitle[ $postType ];
		}

		if ( aioseo()->dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
			$title = aioseo()->dynamicOptions->searchAppearance->postTypes->{$postType}->title;
		}

		$postTypeTitle[ $postType ] = empty( $title ) ? '' : $title;

		return $postTypeTitle[ $postType ];
	}

	/**
	 * Returns the term title.
	 *
	 * @since 4.0.6
	 *
	 * @param  \WP_Term $term    The term object.
	 * @param  boolean  $default Whether we want the default value, not the post one.
	 * @return string            The term title.
	 */
	public function getTermTitle( $term, $default = false ) {
		if ( ! is_a( $term, 'WP_Term' ) ) {
			return '';
		}

		static $terms = [];
		if ( isset( $terms[ $term->term_id ] ) ) {
			return $terms[ $term->term_id ];
		}

		$title          = '';
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		if ( ! $title && $dynamicOptions->searchAppearance->taxonomies->has( $term->taxonomy ) ) {
			$newTitle = aioseo()->dynamicOptions->searchAppearance->taxonomies->{$term->taxonomy}->title;
			$newTitle = preg_replace( '/#taxonomy_title/', aioseo()->helpers->escapeRegexReplacement( $term->name ), (string) $newTitle );
			$title    = $this->helpers->prepare( $newTitle, $term->term_id, $default );
		}

		$terms[ $term->term_id ] = $title;

		return $terms[ $term->term_id ];
	}
}Common/Meta/Traits/Helpers/BuddyPress.php000064400000001370151536241200014327 0ustar00<?php

namespace AIOSEO\Plugin\Common\Meta\Traits\Helpers;

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

/**
 * Contains BuddyPress specific helper methods.
 *
 * @since 4.7.6
 */
trait BuddyPress {
	/**
	 * Sanitizes the title/description.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $value       The value.
	 * @param  int    $objectId    The object ID.
	 * @param  bool   $replaceTags Whether the smart tags should be replaced.
	 * @return string              The sanitized value.
	 */
	public function bpSanitize( $value, $objectId = 0, $replaceTags = false ) {
		$value = $replaceTags ? $value : aioseo()->standalone->buddyPress->tags->replaceTags( $value, $objectId );

		return $this->sanitize( $value, $objectId, true );
	}
}Common/Migration/GeneralSettings.php000064400000103572151536241200013543 0ustar00<?php
namespace AIOSEO\Plugin\Common\Migration;

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

use AIOSEO\Plugin\Common\Models;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the General Settings from V3.
 *
 * @since 4.0.0
 */
class GeneralSettings {
	/**
	 * The old V3 options.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $oldOptions = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->oldOptions = aioseo()->migration->oldOptions;

		$this->migrateSeparatorCharacter();
		$this->setDefaultArticleType();
		$this->migrateHomePageMeta();
		$this->migrateTitleFormats();
		$this->migrateDescriptionFormat();
		$this->migrateNoindexSettings();
		$this->migrateNofollowSettings();
		$this->migratePostSeoColumns();
		$this->migrateSocialUrls();
		$this->migrateSchemaMarkupSettings();
		$this->migrateHomePageKeywords();
		$this->migrateDeprecatedAdvancedOptions();
		$this->migrateRssContentSettings();
		$this->migrateRedirectToParent();
		$this->migrateDisabledPosts();
		$this->migrateNoPaginationForCanonicalUrls();

		$settings = [
			'aiosp_admin_bar'                  => [ 'type' => 'boolean', 'newOption' => [ 'advanced', 'adminBarMenu' ] ],
			'aiosp_google_verify'              => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'google' ] ],
			'aiosp_bing_verify'                => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'bing' ] ],
			'aiosp_pinterest_verify'           => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'pinterest' ] ],
			'aiosp_yandex_verify'              => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'yandex' ] ],
			'aiosp_baidu_verify'               => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'baidu' ] ],
			'aiosp_schema_site_represents'     => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'siteRepresents' ] ],
			'aiosp_schema_organization_name'   => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'organizationName' ] ],
			'aiosp_schema_person_manual_name'  => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'personName' ] ],
			'aiosp_schema_organization_logo'   => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'organizationLogo' ] ],
			'aiosp_schema_person_manual_image' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'personLogo' ] ],
			'aiosp_togglekeywords'             => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'useKeywords' ] ],
			'aiosp_use_categories'             => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'useCategoriesForMetaKeywords' ] ],
			'aiosp_use_tags_as_keywords'       => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'useTagsForMetaKeywords' ] ],
			'aiosp_dynamic_postspage_keywords' => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'dynamicallyGenerateKeywords' ] ],
			'aiosp_run_shortcodes'             => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'runShortcodes' ] ]
		];

		aioseo()->migration->helpers->mapOldToNew( $settings, aioseo()->migration->oldOptions );
	}

	/**
	 * Migrates the separator character.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateSeparatorCharacter() {
		aioseo()->options->searchAppearance->global->separator = '|';
	}

	/**
	 * Set the default posts schema type to Article.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function setDefaultArticleType() {
		if ( aioseo()->dynamicOptions->searchAppearance->postTypes->has( 'post' ) ) {
			aioseo()->dynamicOptions->searchAppearance->postTypes->post->articleType = 'Article';
		}
	}

	/**
	 * Migrates the homepage meta.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateHomePageMeta() {
		$this->migrateHomePageTitle();
		$this->migrateHomePageDescription();

		// If the homepage is a static one, we should migrate the meta now.
		$showOnFront = get_option( 'show_on_front' );
		$pageOnFront = (int) get_option( 'page_on_front' );
		if ( 'page' !== $showOnFront || ! $pageOnFront ) {
			return;
		}

		$post       = 'page' === $showOnFront && $pageOnFront ? get_post( $pageOnFront ) : '';
		$aioseoPost = Models\Post::getPost( $post->ID );

		$postMeta = aioseo()->core->db
			->start( 'postmeta' . ' as pm' )
			->select( 'pm.meta_key, pm.meta_value' )
			->where( 'pm.post_id', $post->ID )
			->whereRaw( "`pm`.`meta_key` LIKE '_aioseop_%'" )
			->run()
			->result();

		$mappedMeta = [
			'_aioseop_nofollow'           => 'robots_nofollow',
			'_aioseop_sitemap_priority'   => 'priority',
			'_aioseop_sitemap_frequency'  => 'frequency',
			'_aioseop_keywords'           => 'keywords',
			'_aioseop_opengraph_settings' => '',
		];

		$meta = [
			'post_id' => $post->ID,
		];

		foreach ( $postMeta as $record ) {
			$name  = $record->meta_key;
			$value = $record->meta_value;

			if ( ! in_array( $name, array_keys( $mappedMeta ), true ) ) {
				continue;
			}

			switch ( $name ) {
				case '_aioseop_nofollow':
					$meta[ $mappedMeta[ $name ] ] = ! empty( $value );
					if ( ! empty( $value ) ) {
						$meta['robots_default'] = false;
					}
					break;
				case '_aioseop_keywords':
					$meta[ $mappedMeta[ $name ] ] = aioseo()->migration->helpers->oldKeywordsToNewKeywords( $value );
					break;
				case '_aioseop_opengraph_settings':
					$class = new Meta();
					$meta += $class->convertOpenGraphMeta( $value );

					// We'll deal with the OG title/description in the Social Meta migration class.
					if ( isset( $meta['og_title'] ) ) {
						unset( $meta['og_title'] );
					}
					if ( isset( $meta['og_description'] ) ) {
						unset( $meta['og_description'] );
					}
					break;
				default:
					$meta[ $mappedMeta[ $name ] ] = aioseo()->helpers->sanitizeOption( $value );
					break;
			}
		}

		$aioseoPost->set( $meta );
		$aioseoPost->save();
	}

	/**
	 * Migrates the homepage title.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateHomePageTitle() {
		$showOnFront   = get_option( 'show_on_front' );
		$pageOnFront   = (int) get_option( 'page_on_front' );

		$homePageTitle = ! empty( $this->oldOptions['aiosp_home_title'] ) ? $this->oldOptions['aiosp_home_title'] : '';
		$format        = $this->oldOptions['aiosp_home_page_title_format'];

		if ( 'posts' === $showOnFront ) {
			$homePageTitle = $homePageTitle ? $homePageTitle : get_bloginfo( 'name' );
			$title         = empty( $format ) ? $homePageTitle : aioseo()->helpers->pregReplace( '#%page_title%#', $homePageTitle, $format );
			$title         = aioseo()->migration->helpers->macrosToSmartTags( $title );
			aioseo()->options->searchAppearance->global->siteTitle = aioseo()->helpers->sanitizeOption( $title );

			return;
		}

		// Set the setting globally regardless of what happens below.
		if ( ! empty( $homePageTitle ) ) {
			$title = aioseo()->migration->helpers->macrosToSmartTags( aioseo()->helpers->pregReplace( '#%page_title%#', $homePageTitle, $format ) );
			aioseo()->options->searchAppearance->global->siteTitle = aioseo()->helpers->sanitizeOption( $title );
		}

		$post       = 'page' === $showOnFront && $pageOnFront ? get_post( $pageOnFront ) : '';
		$metaTitle  = get_post_meta( $post->ID, '_aioseop_title', true );

		$homePageTitle = '';
		if ( empty( $this->oldOptions['aiosp_use_static_home_info'] ) ) {
			$homePageTitle = ! empty( $this->oldOptions['aiosp_home_title'] ) ? $this->oldOptions['aiosp_home_title'] : '#site_title';
			$homePageTitle = ! empty( $metaTitle ) ? $metaTitle : $homePageTitle;
			$homePageTitle = empty( $format ) ? $homePageTitle : aioseo()->helpers->pregReplace( '#%page_title%#', $homePageTitle, $format );
			$homePageTitle = aioseo()->migration->helpers->macrosToSmartTags( $homePageTitle );
		} else {
			if ( ! empty( $metaTitle ) ) {
				$homePageTitle = empty( $format ) ? $metaTitle : aioseo()->helpers->pregReplace( '#%page_title%#', $metaTitle, $format );
				$homePageTitle = aioseo()->migration->helpers->macrosToSmartTags( $homePageTitle );
			}
		}

		$aioseoPost = Models\Post::getPost( $post->ID );
		$aioseoPost->set( [
			'post_id' => $post->ID,
			'title'   => aioseo()->helpers->sanitizeOption( $homePageTitle )
		] );
		$aioseoPost->save();

		$this->maybeShowHomePageTitleNotice( $post );
	}

	/**
	 * Check if we should display a notice warning users that their homepage title may have changed.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post The post object.
	 * @return void
	 */
	private function maybeShowHomePageTitleNotice( $post ) {
		$metaTitle     = get_post_meta( $post->ID, '_aioseop_title', true );
		$homePageTitle = ! empty( $this->oldOptions['aiosp_home_title'] ) ? $this->oldOptions['aiosp_home_title'] : '';

		if (
			empty( $this->oldOptions['aiosp_use_static_home_info'] ) &&
			$metaTitle &&
			( trim( $homePageTitle ) !== trim( $metaTitle ) )
		) {
			$this->showHomePageSettingsNotice();
		}
	}

	/**
	 * Migrates the homepage description.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateHomePageDescription() {
		$showOnFront         = get_option( 'show_on_front' );
		$pageOnFront         = (int) get_option( 'page_on_front' );

		$homePageDescription = ! empty( $this->oldOptions['aiosp_home_description'] ) ? $this->oldOptions['aiosp_home_description'] : '';
		$format              = $this->oldOptions['aiosp_description_format'];

		if ( 'posts' === $showOnFront ) {
			// If the description had the page_title macro, we want to replace it with the actual page title itself.
			$homePageDescription = $homePageDescription ? $homePageDescription : get_bloginfo( 'description' );
			$homePageTitle       = ! empty( $this->oldOptions['aiosp_home_title'] ) ? $this->oldOptions['aiosp_home_title'] : get_bloginfo( 'name' );
			$format              = aioseo()->helpers->pregReplace( '#%page_title%#', $homePageTitle, $format );
			$description         = empty( $format ) ? $homePageDescription : aioseo()->helpers->pregReplace( '#%description%#', $homePageDescription, $format );
			$description         = aioseo()->migration->helpers->macrosToSmartTags( $description );
			aioseo()->options->searchAppearance->global->metaDescription = aioseo()->helpers->sanitizeOption( $description );

			return;
		}

		// Set the setting globally regardless of what happens below.
		if ( ! empty( $homePageDescription ) ) {
			$homePageTitle = ! empty( $this->oldOptions['aiosp_home_title'] ) ? $this->oldOptions['aiosp_home_title'] : get_bloginfo( 'name' );
			$format        = aioseo()->helpers->pregReplace( '#%page_title%#', $homePageTitle, $format );
			$description   = aioseo()->migration->helpers->macrosToSmartTags( aioseo()->helpers->pregReplace( '#%description%#', $homePageDescription, $format ) );
			aioseo()->options->searchAppearance->global->metaDescription = aioseo()->helpers->sanitizeOption( $description );
		}

		$post             = 'page' === $showOnFront && $pageOnFront ? get_post( $pageOnFront ) : '';
		$metaDescription  = get_post_meta( $post->ID, '_aioseop_description', true );

		$homePageDescription = '';
		if ( empty( $this->oldOptions['aiosp_use_static_home_info'] ) ) {
			$homePageDescription = ! empty( $this->oldOptions['aiosp_home_description'] ) ? $this->oldOptions['aiosp_home_description'] : '';
			$homePageDescription = ! empty( $metaDescription ) ? $metaDescription : $homePageDescription;
		} else {
			if ( ! empty( $metaDescription ) ) {
				$homePageDescription = empty( $format ) ? $metaDescription : aioseo()->helpers->pregReplace( '#%description%#', $metaDescription, $format );
				$homePageDescription = aioseo()->migration->helpers->macrosToSmartTags( $homePageDescription );
			}
		}

		$homePageDescription = empty( $format ) ? $homePageDescription : aioseo()->helpers->pregReplace( '#(%description%|%page_title%)#', $homePageDescription, $format );
		$homePageDescription = aioseo()->migration->helpers->macrosToSmartTags( $homePageDescription );

		$aioseoPost = Models\Post::getPost( $post->ID );
		$aioseoPost->set( [
			'post_id'     => $post->ID,
			'description' => aioseo()->helpers->sanitizeOption( $homePageDescription )
		] );
		$aioseoPost->save();

		$this->maybeShowHomePageDescriptionNotice( $post );
	}

		/**
	 * Check if we should display a notice warning users that their homepage title may have changed.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post The post object.
	 * @return void
	 */
	private function maybeShowHomePageDescriptionNotice( $post ) {
		$metaDescription     = get_post_meta( $post->ID, '_aioseop_description', true );
		$homePageDescription = ! empty( $this->oldOptions['aiosp_home_description'] ) ? $this->oldOptions['aiosp_home_description'] : '';

		if (
			empty( $this->oldOptions['aiosp_use_static_home_info'] ) &&
			$metaDescription &&
			( trim( $homePageDescription ) !== trim( $metaDescription ) )
		) {
			$this->showHomePageSettingsNotice();
		}
	}

	/**
	 * Shows the homepage settings notice.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function showHomePageSettingsNotice() {
		$notification = Models\Notification::getNotificationByName( 'v3-migration-homepage-settings' );
		if ( $notification->notification_name ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'v3-migration-homepage-settings',
			'title'             => __( 'Review Your Homepage Title & Description', 'all-in-one-seo-pack' ),
			'content'           => sprintf(
				// Translators: 1 - All in One SEO.
				__( 'Due to a bug in the previous version of %1$s, your homepage title and description may have changed. Please take a minute to review your homepage settings to verify that they are correct.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				AIOSEO_PLUGIN_NAME
			),
			'type'              => 'warning',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Review Now', 'all-in-one-seo-pack' ),
			'button1_action'    => 'http://route#aioseo-search-appearance&aioseo-scroll=home-page-settings&aioseo-highlight=home-page-settings:global-settings',
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Migrates the title formats.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateTitleFormats() {
		if ( ! empty( $this->oldOptions['aiosp_archive_title_format'] ) ) {
			$archives = array_keys( aioseo()->dynamicOptions->searchAppearance->archives->all() );
			$format   = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $this->oldOptions['aiosp_archive_title_format'] ) );
			foreach ( $archives as $archive ) {
				aioseo()->dynamicOptions->searchAppearance->archives->$archive->title = $format;
			}
		}

		$settings = [
			'aiosp_post_title_format'       => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'postTypes', 'post', 'title' ], 'dynamic' => true ],
			'aiosp_page_title_format'       => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'postTypes', 'page', 'title' ], 'dynamic' => true ],
			'aiosp_attachment_title_format' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'postTypes', 'attachment', 'title' ], 'dynamic' => true ],
			'aiosp_category_title_format'   => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'taxonomies', 'category', 'title' ], 'dynamic' => true ],
			'aiosp_tag_title_format'        => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'taxonomies', 'post_tag', 'title' ], 'dynamic' => true ],
			'aiosp_date_title_format'       => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'date', 'title' ] ],
			'aiosp_author_title_format'     => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'author', 'title' ] ],
			'aiosp_search_title_format'     => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'search', 'title' ] ],
			'aiosp_paged_format'            => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'advanced', 'pagedFormat' ] ]
		];

		foreach ( $this->oldOptions as $name => $value ) {
			if (
				! in_array( $name, array_keys( $settings ), true ) &&
				preg_match( '#aiosp_(.*)_title_format#', (string) $name, $slug )
			) {
				if ( empty( $slug[1] ) ) {
					continue;
				}

				$objectSlug = aioseo()->helpers->pregReplace( '#_tax#', '', $slug[1] );
				if ( in_array( $objectSlug, aioseo()->helpers->getPublicPostTypes( true ), true ) ) {
					$settings[ $name ] = [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'postTypes', $objectSlug, 'title' ], 'dynamic' => true ];
					continue;
				}
				if ( in_array( $objectSlug, aioseo()->helpers->getPublicTaxonomies( true ), true ) ) {
					$settings[ $name ] = [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'taxonomies', $objectSlug, 'title' ], 'dynamic' => true ];
				}
			}
		}

		aioseo()->migration->helpers->mapOldToNew( $settings, $this->oldOptions, true );

		// Check if any of the title formats were empty and register a notification if so.
		$found = false;
		foreach ( $settings as $k => $v ) {
			if ( 'aiosp_home_page_title_format' === $k ) {
				continue;
			}

			if ( isset( $this->oldOptions[ $k ] ) && empty( $this->oldOptions[ $k ] ) ) {
				$found = true;
				break;
			}
		}

		if ( ! $found ) {
			Models\Notification::deleteNotificationByName( 'v3-migration-title-formats-blank' );

			return;
		}

		$notification = Models\Notification::getNotificationByName( 'v3-migration-title-formats-blank' );
		if ( $notification->notification_name ) {
			return;
		}

		$p1 = sprintf(
			// Translators: 1 - The plugin short name ("AIOSEO"), 2 - The plugin short name ("AIOSEO"), 3 - Opening link tag, 4 - Closing link tag.
			__( '%1$s migrated all your title formats, some of which were blank. If you were purposely using blank formats in the previous version of %2$s and want WordPress to handle your titles, you can safely dismiss this message. For more information, check out our documentation on %3$sblank title formats%4$s.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			AIOSEO_PLUGIN_SHORT_NAME,
			AIOSEO_PLUGIN_SHORT_NAME,
			'<a href="' . aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . '/docs/blank-title-formats-detected', 'notifications-center', 'v3-migration-title-formats-blank' ) . '">',
			'</a>'
		);

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'v3-migration-title-formats-blank',
			'title'             => __( 'Blank Title Formats Detected', 'all-in-one-seo-pack' ),
			'content'           => $p1,
			'type'              => 'warning',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Learn More', 'all-in-one-seo-pack' ),
			'button1_action'    => aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . '/docs/blank-title-formats-detected', 'notifications-center', 'v3-migration-title-formats-blank' ),
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Migrates the description format.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateDescriptionFormat() {
		if (
			! empty( $this->oldOptions['aiosp_generate_descriptions'] ) &&
			empty( $this->oldOptions['aiosp_skip_excerpt'] )
		) {
			foreach ( aioseo()->helpers->getPublicPostTypes() as $postType ) {
				if ( empty( $postType['supports']['excerpt'] ) ) {
					continue;
				}

				if ( aioseo()->dynamicOptions->searchAppearance->postTypes->has( $postType['name'] ) ) {
					aioseo()->dynamicOptions->searchAppearance->postTypes->{$postType['name']}->metaDescription = '#post_excerpt';
				}
			}
		}

		if (
			empty( $this->oldOptions['aiosp_description_format'] ) ||
			'%description%' === trim( $this->oldOptions['aiosp_description_format'] )
		) {
			return;
		}

		$deprecatedOptions = aioseo()->internalOptions->internal->deprecatedOptions;
		array_push( $deprecatedOptions, 'descriptionFormat' );
		aioseo()->internalOptions->internal->deprecatedOptions = $deprecatedOptions;

		$format = aioseo()->migration->helpers->macrosToSmartTags( $this->oldOptions['aiosp_description_format'] );
		aioseo()->options->deprecated->searchAppearance->global->descriptionFormat = aioseo()->helpers->sanitizeOption( $format );
	}

	/**
	 * Migrates the noindex settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateNoindexSettings() {
		if ( ! isset( $this->oldOptions['aiosp_cpostnoindex'] ) && ! isset( $this->oldOptions['aiosp_tax_noindex'] ) ) {
			return;
		}

		$noindexedPostTypes = is_array( $this->oldOptions['aiosp_cpostnoindex'] ) ? $this->oldOptions['aiosp_cpostnoindex'] : explode( ', ', $this->oldOptions['aiosp_cpostnoindex'] );
		foreach ( array_intersect( aioseo()->helpers->getPublicPostTypes( true ), $noindexedPostTypes ) as $postType ) {
			if ( aioseo()->dynamicOptions->noConflict()->searchAppearance->postTypes->has( $postType ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->show = false;
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = false;
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->noindex = true;
			}
		}

		$noindexedTaxonomies = isset( $this->oldOptions['aiosp_tax_noindex'] ) ? (array) $this->oldOptions['aiosp_tax_noindex'] : [];
		if ( ! empty( $this->oldOptions['aiosp_category_noindex'] ) ) {
			$noindexedTaxonomies[] = 'category';
		}

		if ( ! empty( $this->oldOptions['aiosp_tags_noindex'] ) ) {
			$noindexedTaxonomies[] = 'post_tag';
		}

		if ( ! empty( $noindexedTaxonomies ) ) {
			foreach ( array_intersect( aioseo()->helpers->getPublicTaxonomies( true ), $noindexedTaxonomies ) as $taxonomy ) {
				if ( aioseo()->dynamicOptions->noConflict()->searchAppearance->taxonomies->has( $taxonomy ) ) {
					aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->show = false;
					aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->default = false;
					aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->noindex = true;
				}
			}
		}

		if ( ! empty( $this->oldOptions['aiosp_archive_date_noindex'] ) ) {
			aioseo()->options->searchAppearance->archives->date->show = false;
			aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->default = false;
			aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->noindex = true;
		}

		if ( ! empty( $this->oldOptions['aiosp_archive_author_noindex'] ) ) {
			aioseo()->options->searchAppearance->archives->author->show = false;
			aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->default = false;
			aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->noindex = true;
		}

		if ( ! empty( $this->oldOptions['aiosp_search_noindex'] ) ) {
			aioseo()->options->searchAppearance->archives->search->show = false;
			aioseo()->options->searchAppearance->archives->search->advanced->robotsMeta->default = false;
			aioseo()->options->searchAppearance->archives->search->advanced->robotsMeta->noindex = true;
		} else {
			// We need to do this as V4 will noindex the search page otherwise.
			aioseo()->options->searchAppearance->archives->search->show = true;
			aioseo()->options->searchAppearance->archives->search->advanced->robotsMeta->default = true;
			aioseo()->options->searchAppearance->archives->search->advanced->robotsMeta->noindex = false;
		}

		if ( ! empty( $this->oldOptions['aiosp_paginated_noindex'] ) ) {
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default          = false;
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindexPaginated = true;
		}
	}

	/**
	 * Migrates the nofollow settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateNofollowSettings() {
		if ( ! empty( $this->oldOptions['aiosp_cpostnofollow'] ) ) {
			foreach ( array_intersect( aioseo()->helpers->getPublicPostTypes( true ), $this->oldOptions['aiosp_cpostnofollow'] ) as $postType ) {
				if ( aioseo()->dynamicOptions->noConflict()->searchAppearance->postTypes->has( $postType ) ) {
					aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default  = false;
					aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->nofollow = true;
				}
			}
		}

		if ( ! empty( $this->oldOptions['aiosp_paginated_nofollow'] ) ) {
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->nofollowPaginated = true;
		}
	}

	/**
	 * Migrates the post SEO columns.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migratePostSeoColumns() {
		if ( ! isset( $this->oldOptions['aiosp_posttypecolumns'] ) ) {
			return;
		}

		$publicPostTypes = aioseo()->helpers->getPublicPostTypes( true );
		$postTypes       = array_intersect( (array) $this->oldOptions['aiosp_posttypecolumns'], $publicPostTypes );

		aioseo()->options->advanced->postTypes->included = array_values( $postTypes );
		if ( count( $publicPostTypes ) !== count( $postTypes ) ) {
			aioseo()->options->advanced->postTypes->all = false;
		}
	}

	/**
	 * Migrates the schema social URLs.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateSocialUrls() {
		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_facebook_publisher'] ) ) {
			aioseo()->options->social->profiles->urls->facebookPageUrl = esc_url( wp_strip_all_tags( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_facebook_publisher'] ) );
			aioseo()->options->social->profiles->sameUsername->enable = false;
		}

		if ( empty( $this->oldOptions['aiosp_schema_social_profile_links'] ) ) {
			return;
		}

		$socialUrls = aioseo()->helpers->pregReplace( '/\s/', '\r\n', $this->oldOptions['aiosp_schema_social_profile_links'] );
		$socialUrls = array_filter( explode( '\r\n', $socialUrls ) );

		if ( ! count( $socialUrls ) ) {
			return;
		}

		$supportedNetworks = [
			'facebook.com'   => 'facebookPageUrl',
			'twitter.com'    => 'twitterUrl',
			'instagram.com'  => 'instagramUrl',
			'tiktok.com'     => 'tiktokUrl',
			'pinterest.com'  => 'pinterestUrl',
			'youtube.com'    => 'youtubeUrl',
			'linkedin.com'   => 'linkedinUrl',
			'tumblr.com'     => 'tumblrUrl',
			'yelp.com'       => 'yelpPageUrl',
			'soundcloud.com' => 'soundCloudUrl',
			'wikipedia.org'  => 'wikipediaUrl',
			'myspace.com'    => 'myspaceUrl',
			'wordpress.org'  => 'wordpressUrl',
			'bsky.app'       => 'blueskyUrl',
			'threads.net'    => 'threadsUrl'
		];

		$found = false;
		foreach ( $supportedNetworks as $url => $settingName ) {
			$url = aioseo()->helpers->escapeRegex( $url );
			foreach ( $socialUrls as $socialUrl ) {
				if ( preg_match( "/.*$url.*/", (string) $socialUrl ) ) {
					aioseo()->options->social->profiles->urls->$settingName = esc_url( wp_strip_all_tags( $socialUrl ) );
					$found = true;
				}
			}
		}

		if ( $found ) {
			aioseo()->options->social->profiles->sameUsername->enable = false;
		}
	}

	/**
	 * Migrates the Schema Markup settings in the General Settings menu.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateSchemaMarkupSettings() {
		$this->migrateSchemaPhoneNumber();

		if (
			isset( $this->oldOptions['aiosp_schema_markup'] ) &&
			empty( $this->oldOptions['aiosp_schema_markup'] )
		) {
			$deprecatedOptions = aioseo()->internalOptions->internal->deprecatedOptions;
			array_push( $deprecatedOptions, 'enableSchemaMarkup' );
			aioseo()->internalOptions->internal->deprecatedOptions = $deprecatedOptions;
			aioseo()->options->deprecated->searchAppearance->global->schema->enableSchemaMarkup = false;
		}

		if ( ! empty( $this->oldOptions['aiosp_schema_person_user'] ) ) {
			if ( -1 === (int) $this->oldOptions['aiosp_schema_person_user'] ) {
				aioseo()->options->searchAppearance->global->schema->person = 'manual';
			} else {
				aioseo()->options->searchAppearance->global->schema->person = intval( $this->oldOptions['aiosp_schema_person_user'] );
			}
		}
	}

	/**
	 * Migrates the schema phone number.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateSchemaPhoneNumber() {
		if ( empty( $this->oldOptions['aiosp_schema_phone_number'] ) ) {
			return;
		}

		$phoneNumber = aioseo()->helpers->sanitizeOption( $this->oldOptions['aiosp_schema_phone_number'] );
		if ( ! preg_match( '#\+\d+#', (string) $phoneNumber ) ) {
			$notification = Models\Notification::getNotificationByName( 'v3-migration-schema-number' );
			if ( $notification->notification_name ) {
				return;
			}

			Models\Notification::addNotification( [
				'slug'              => uniqid(),
				'notification_name' => 'v3-migration-schema-number',
				'title'             => __( 'Invalid Phone Number for Knowledge Graph', 'all-in-one-seo-pack' ),
				'content'           => sprintf(
					// Translators: 1 - The phone number.
					__( 'The phone number that you previously entered for your Knowledge Graph schema markup is invalid. As it needs to be internationally formatted, please enter it (%1$s) again with the country code, e.g. +1 (555) 555-1234.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
					"<strong>$phoneNumber</strong>"
				),
				'type'              => 'warning',
				'level'             => [ 'all' ],
				'button1_label'     => __( 'Fix Now', 'all-in-one-seo-pack' ),
				'button1_action'    => 'http://route#aioseo-search-appearance&aioseo-scroll=schema-graph-phone&aioseo-highlight=schema-graph-phone:global-settings',
				'button2_label'     => __( 'Remind Me Later', 'all-in-one-seo-pack' ),
				'button2_action'    => 'http://action#notification/v3-migration-schema-number-reminder',
				'start'             => gmdate( 'Y-m-d H:i:s' )
			] );

			return;
		}
		aioseo()->options->searchAppearance->global->schema->phone = $phoneNumber;
	}

	/**
	 * Migrates the homepage keywords.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateHomePageKeywords() {
		if ( ! empty( $this->oldOptions['aiosp_home_keywords'] ) ) {
			aioseo()->options->searchAppearance->global->keywords = aioseo()->migration->helpers->oldKeywordsToNewKeywords( $this->oldOptions['aiosp_home_keywords'] );
		}
	}

	/**
	 * Migrates the deprecated V3 advanced General Settings options.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateDeprecatedAdvancedOptions() {
		$deprecatedOptions = aioseo()->internalOptions->internal->deprecatedOptions;

		if ( empty( $this->oldOptions['aiosp_generate_descriptions'] ) ) {
			array_push( $deprecatedOptions, 'autogenerateDescriptions' );
			aioseo()->options->deprecated->searchAppearance->advanced->autogenerateDescriptions = false;
		} else {
			if ( ! empty( $this->oldOptions['aiosp_skip_excerpt'] ) ) {
				array_push( $deprecatedOptions, 'useContentForAutogeneratedDescriptions' );
				aioseo()->options->deprecated->searchAppearance->advanced->useContentForAutogeneratedDescriptions = true;
			}
		}

		aioseo()->internalOptions->internal->deprecatedOptions = $deprecatedOptions;
	}

	/**
	 * Migrates the RSS content settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateRssContentSettings() {
		if ( isset( $this->oldOptions['aiosp_rss_content_before'] ) ) {
			aioseo()->options->rssContent->before = esc_html( aioseo()->migration->helpers->macrosToSmartTags( $this->oldOptions['aiosp_rss_content_before'] ) );
		}

		if ( isset( $this->oldOptions['aiosp_rss_content_after'] ) ) {
			aioseo()->options->rssContent->after = esc_html( aioseo()->migration->helpers->macrosToSmartTags( $this->oldOptions['aiosp_rss_content_after'] ) );
		}
	}

	/**
	 * Migrates the Redirect Attachment to Parent setting.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateRedirectToParent() {
		if ( isset( $this->oldOptions['aiosp_redirect_attachement_parent'] ) ) {
			if ( ! empty( $this->oldOptions['aiosp_redirect_attachement_parent'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'attachment_parent';
			} else {
				aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'disabled';
			}
		}
	}

	/**
	 * Migrates the excluded posts.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateDisabledPosts() {
		if ( empty( $this->oldOptions['aiosp_ex_pages'] ) ) {
			return;
		}

		$deprecatedOptions = aioseo()->internalOptions->internal->deprecatedOptions;
		if ( ! in_array( 'excludePosts', $deprecatedOptions, true ) ) {
			array_push( $deprecatedOptions, 'excludePosts' );
			aioseo()->internalOptions->internal->deprecatedOptions = $deprecatedOptions;
		}

		$excludedPosts = aioseo()->options->deprecated->searchAppearance->advanced->excludePosts;
		$pages         = explode( ',', $this->oldOptions['aiosp_ex_pages'] );
		if ( count( $pages ) ) {
			foreach ( $pages as $page ) {
				$page = trim( $page );
				$id   = intval( $page );
				if ( ! $id ) {
					$post = get_page_by_path( $page, OBJECT, aioseo()->helpers->getPublicPostTypes( true ) );
					if ( $post && is_object( $post ) ) {
						$id = $post->ID;
					}
				}

				if ( $id ) {
					$post = get_post( $id );
					if ( ! is_object( $post ) ) {
						continue;
					}

					$excludedPost        = new \stdClass();
					$excludedPost->value = $id;
					$excludedPost->type  = $post->post_type;
					$excludedPost->label = $post->post_title;
					$excludedPost->link  = get_permalink( $id );

					array_push( $excludedPosts, wp_json_encode( $excludedPost ) );
				}
			}
		}
		aioseo()->options->deprecated->searchAppearance->advanced->excludePosts = $excludedPosts;
	}

	/**
	 * Migrates the deprecated "No Pagination for Canonical URLs" setting.
	 *
	 * @since 4.5.9
	 *
	 * @return void
	 */
	private function migrateNoPaginationForCanonicalUrls() {
		if ( empty( $this->oldOptions['aiosp_no_paged_canonical_links'] ) ) {
			return;
		}

		$deprecatedOptions = aioseo()->internalOptions->deprecatedOptions;
		if ( ! in_array( 'noPaginationForCanonical', $deprecatedOptions, true ) ) {
			$deprecatedOptions[]                         = 'noPaginationForCanonical';
			aioseo()->internalOptions->deprecatedOptions = $deprecatedOptions;
		}

		aioseo()->options->deprecated->searchAppearance->advanced->noPaginationForCanonical = true;
	}
}Common/Migration/Helpers.php000064400000020076151536241200012044 0ustar00<?php
namespace AIOSEO\Plugin\Common\Migration;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Contains a number of helper functions for the V3 migration.
 *
 * @since 4.0.0
 */
class Helpers {
	/**
	 * Maps a list of old settings from V3 to their counterparts in V4.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $mappings      The old settings, mapped to their new settings.
	 * @param  array $group         The old settings group.
	 * @param  bool  $convertMacros Whether to convert the old V3 macros to V4 smart tags.
	 * @return void
	 */
	public function mapOldToNew( $mappings, $group, $convertMacros = false ) {
		if (
			! is_array( $mappings ) ||
			! is_array( $group ) ||
			! count( $mappings ) ||
			! count( $group )
		) {
			return;
		}

		$mainOptions    = aioseo()->options->noConflict();
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		foreach ( $mappings as $name => $values ) {
			if ( ! isset( $group[ $name ] ) ) {
				continue;
			}

			$error      = false;
			$options    = ! empty( $values['dynamic'] ) ? $dynamicOptions : $mainOptions;
			$lastOption = '';
			for ( $i = 0; $i < count( $values['newOption'] ); $i++ ) {
				$lastOption = $values['newOption'][ $i ];
				if ( ! $options->has( $lastOption, false ) ) {
					$error = true;
					break;
				}

				if ( count( $values['newOption'] ) - 1 !== $i ) {
					$options = $options->$lastOption;
				}
			}

			if ( $error ) {
				continue;
			}

			switch ( $values['type'] ) {
				case 'boolean':
					if ( ! empty( $group[ $name ] ) ) {
						$options->$lastOption = true;
						break;
					}
					$options->$lastOption = false;
					break;
				case 'integer':
				case 'float':
					$value = aioseo()->helpers->sanitizeOption( $group[ $name ] );
					if ( $value ) {
						$options->$lastOption = $value;
					}
					break;
				default:
					$value = $group[ $name ];
					if ( $convertMacros ) {
						$value = $this->macrosToSmartTags( $value );
					}
					$options->$lastOption = aioseo()->helpers->sanitizeOption( $value );
					break;
			}
		}
	}

	/**
	 * Replaces the macros from V3 with our new Smart Tags from V4.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string The string.
	 * @return string $string The converted string.
	 */
	public function macrosToSmartTags( $string ) {
		$macros = [
			'%site_title%'             => '#site_title',
			'%blog_title%'             => '#site_title',
			'%site_description%'       => '#tagline',
			'%blog_description%'       => '#tagline',
			'%wp_title%'               => '#post_title',
			'%post_title%'             => '#post_title',
			'%page_title%'             => '#post_title',
			'%post_date%'              => '#post_date',
			'%post_month%'             => '#post_month',
			'%post_year%'              => '#post_year',
			'%date%'                   => '#archive_date',
			'%day%'                    => '#post_day',
			'%month%'                  => '#post_month',
			'%monthnum%'               => '#post_month',
			'%year%'                   => '#post_year',
			'%current_date%'           => '#current_date',
			'%current_day%'            => '#current_day',
			'%current_month%'          => '#current_month',
			'%current_month_i18n%'     => '#current_month',
			'%current_year%'           => '#current_year',
			'%category_title%'         => '#taxonomy_title',
			'%tag%'                    => '#taxonomy_title',
			'%tag_title%'              => '#taxonomy_title',
			'%archive_title%'          => '#archive_title',
			'%taxonomy_title%'         => '#taxonomy_title',
			'%taxonomy_description%'   => '#taxonomy_description',
			'%tag_description%'        => '#taxonomy_description',
			'%category_description%'   => '#taxonomy_description',
			'%author%'                 => '#author_name',
			'%search%'                 => '#search_term',
			'%page%'                   => '#page_number',
			'%site_link%'              => '#site_link',
			'%site_link_raw%'          => '#site_link_alt',
			'%post_link%'              => '#post_link',
			'%post_link_raw%'          => '#post_link_alt',
			'%author_name%'            => '#author_name',
			'%author_link%'            => '#author_link',
			'%image_title%'            => '#image_title',
			'%image_seo_title%'        => '#image_seo_title',
			'%image_seo_description%'  => '#image_seo_description',
			'%post_seo_title%'         => '#post_seo_title',
			'%post_seo_description%'   => '#post_seo_description',
			'%alt_tag%'                => '#alt_tag',
			'%description%'            => '#description',
			// These need to run last so we don't replace other known tags.
			'%.*_title%'               => '#post_title',
			'%[^%]*_author_login%'     => '#author_first_name #author_last_name',
			'%[^%]*_author_nicename%'  => '#author_first_name #author_last_name',
			'%[^%]*_author_firstname%' => '#author_first_name',
			'%[^%]*_author_lastname%'  => '#author_last_name',
		];

		if ( preg_match_all( '#%cf_([^%]*)%#', (string) $string, $matches ) && ! empty( $matches[1] ) ) {
			foreach ( $matches[1] as $name ) {
				if ( preg_match( '#\s#', (string) $name ) ) {
					$notification = Models\Notification::getNotificationByName( 'v3-migration-custom-field' );
					if ( ! $notification->notification_name ) {
						Models\Notification::addNotification( [
							'slug'              => uniqid(),
							'notification_name' => 'v3-migration-custom-field',
							'title'             => __( 'Custom field names with spaces detected', 'all-in-one-seo-pack' ),
							'content'           => sprintf(
								// Translators: 1 - The plugin short name ("AIOSEO"), 2 - Same as previous.
								__( '%1$s has detected that you have one or more custom fields with spaces in their name.
								In order for %2$s to correctly parse these custom fields, their names cannot contain any spaces.', 'all-in-one-seo-pack' ),
								AIOSEO_PLUGIN_SHORT_NAME,
								AIOSEO_PLUGIN_SHORT_NAME
							),
							'type'              => 'warning',
							'level'             => [ 'all' ],
							'button1_label'     => __( 'Remind Me Later', 'all-in-one-seo-pack' ),
							'button1_action'    => 'http://action#notification/v3-migration-custom-field-reminder',
							'start'             => gmdate( 'Y-m-d H:i:s' )
						] );
					}
				} else {
					$string = aioseo()->helpers->pregReplace( "#%cf_$name%#", "#custom_field-$name", $string );
				}
			}
		}

		if ( preg_match_all( '#%tax_([^%]*)%#', (string) $string, $matches ) && ! empty( $matches[1] ) ) {
			foreach ( $matches[1] as $name ) {
				if ( ! preg_match( '#\s#', (string) $name ) ) {
					$string = aioseo()->helpers->pregReplace( "#%tax_$name%#", "#tax_name-$name", $string );
				}
			}
		}

		foreach ( $macros as $macro => $tag ) {
			$string = aioseo()->helpers->pregReplace( "#$macro(?![a-zA-Z0-9_])#im", $tag, $string );
		}

		$string = preg_replace( '/%([a-f0-9]{2}[^%]*)%/i', '#$1#', (string) $string );

		return $string;
	}

	/**
	 * Converts the old comma-separated keywords format to the new JSON format.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $keywords A comma-separated list of keywords.
	 * @return string $keywords The keywords formatted in JSON.
	 */
	public function oldKeywordsToNewKeywords( $keywords ) {
		if ( ! $keywords ) {
			return '';
		}

		$oldKeywords = array_filter( explode( ',', $keywords ) );
		if ( ! is_array( $oldKeywords ) ) {
			return '';
		}

		$keywords = [];
		foreach ( $oldKeywords as $oldKeyword ) {
			$oldKeyword = aioseo()->helpers->sanitizeOption( $oldKeyword );

			$keyword        = new \stdClass();
			$keyword->label = $oldKeyword;
			$keyword->value = $oldKeyword;

			$keywords[] = $keyword;
		}

		return $keywords;
	}

	/**
	 * Resets the plugin so that the migration can run again.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public static function redoMigration() {
		aioseo()->core->db->delete( 'options' )
			->whereRaw( "`option_name` LIKE 'aioseo_options_internal%'" )
			->run();

		aioseo()->core->cache->delete( 'v3_migration_in_progress_posts' );
		aioseo()->core->cache->delete( 'v3_migration_in_progress_terms' );

		aioseo()->actionScheduler->unschedule( 'aioseo_migrate_post_meta' );
		aioseo()->actionScheduler->unschedule( 'aioseo_migrate_term_meta' );
	}
}Common/Migration/Meta.php000064400000031130151536241200011321 0ustar00<?php
namespace AIOSEO\Plugin\Common\Migration;

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

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

use AIOSEO\Plugin\Common\Models;

/**
 * Migrates the post meta from V3.
 *
 * @since 4.0.0
 */
class Meta {
	/**
	 * Holds the old options array.
	 *
	 * @since 4.0.3
	 *
	 * @var array|null
	 */
	protected static $oldOptions = null;

	/**
	 * Migrates the plugin meta data.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function migrateMeta() {
		try {
			if ( as_next_scheduled_action( 'aioseo_migrate_post_meta' ) ) {
				return;
			}

			as_schedule_single_action( time() + 30, 'aioseo_migrate_post_meta', [], 'aioseo' );
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Migrates the post meta data from V3.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function migratePostMeta() {
		if ( aioseo()->core->cache->get( 'v3_migration_in_progress_settings' ) ) {
			aioseo()->actionScheduler->scheduleSingle( 'aioseo_migrate_post_meta', 30, [], true );

			return;
		}

		$postsPerAction  = 50;
		$publicPostTypes = implode( "', '", aioseo()->helpers->getPublicPostTypes( true ) );
		$timeStarted     = gmdate( 'Y-m-d H:i:s', aioseo()->core->cache->get( 'v3_migration_in_progress_posts' ) );

		$postsToMigrate = aioseo()->core->db
			->start( 'posts' . ' as p' )
			->select( 'p.ID' )
			->leftJoin( 'aioseo_posts as ap', '`p`.`ID` = `ap`.`post_id`' )
			->whereRaw( "( ap.post_id IS NULL OR ap.updated < '$timeStarted' )" )
			->whereRaw( "( p.post_type IN ( '$publicPostTypes' ) )" )
			->whereRaw( 'p.post_status NOT IN( \'auto-draft\' )' )
			->orderBy( 'p.ID DESC' )
			->limit( $postsPerAction )
			->run()
			->result();

		if ( ! $postsToMigrate || ! count( $postsToMigrate ) ) {
			aioseo()->core->cache->delete( 'v3_migration_in_progress_posts' );

			return;
		}

		foreach ( $postsToMigrate as $post ) {
			$newPostMeta = $this->getMigratedPostMeta( $post->ID );

			$aioseoPost = Models\Post::getPost( $post->ID );
			$aioseoPost->set( $newPostMeta );
			$aioseoPost->save();

			$this->updateLocalizedPostMeta( $post->ID, $newPostMeta );
			$this->migrateAdditionalPostMeta( $post->ID );
		}

		if ( count( $postsToMigrate ) === $postsPerAction ) {
			try {
				as_schedule_single_action( time() + 30, 'aioseo_migrate_post_meta', [], 'aioseo' );
			} catch ( \Exception $e ) {
				// Do nothing.
			}
		} else {
			aioseo()->core->cache->delete( 'v3_migration_in_progress_posts' );
		}
	}

	/**
	 * Returns the migrated post meta for a given post.
	 *
	 * @since 4.0.3
	 *
	 * @param  int   $postId The post ID.
	 * @return array         The post meta.
	 */
	public function getMigratedPostMeta( $postId ) {
		if ( is_category() || is_tag() || is_tax() || ! is_numeric( $postId ) ) {
			return [];
		}

		if ( null === self::$oldOptions ) {
			self::$oldOptions = get_option( 'aioseop_options' );
		}

		if ( empty( self::$oldOptions ) ) {
			return [];
		}

		$postMeta = aioseo()->core->db
			->start( 'postmeta' . ' as pm' )
			->select( 'pm.meta_key, pm.meta_value' )
			->where( 'pm.post_id', $postId )
			->whereRaw( "`pm`.`meta_key` LIKE '_aioseop_%'" )
			->run()
			->result();

		$mappedMeta = [
			'_aioseop_title'              => 'title',
			'_aioseop_description'        => 'description',
			'_aioseop_custom_link'        => 'canonical_url',
			'_aioseop_sitemap_exclude'    => '',
			'_aioseop_disable'            => '',
			'_aioseop_noindex'            => 'robots_noindex',
			'_aioseop_nofollow'           => 'robots_nofollow',
			'_aioseop_sitemap_priority'   => 'priority',
			'_aioseop_sitemap_frequency'  => 'frequency',
			'_aioseop_keywords'           => 'keywords',
			'_aioseop_opengraph_settings' => ''
		];

		$meta = [
			'post_id' => $postId,
		];

		if ( ! $postMeta || ! count( $postMeta ) ) {
			return $meta;
		}

		foreach ( $postMeta as $record ) {
			$name  = $record->meta_key;
			$value = $record->meta_value;

			if ( ! in_array( $name, array_keys( $mappedMeta ), true ) ) {
				continue;
			}

			switch ( $name ) {
				case '_aioseop_description':
					$meta[ $mappedMeta[ $name ] ] = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $value ) );
					break;
				case '_aioseop_title':
					if ( ! empty( $value ) ) {
						$meta[ $mappedMeta[ $name ] ] = $this->getPostTitle( $postId, $value );
					}
					break;
				case '_aioseop_sitemap_exclude':
					if ( empty( $value ) ) {
						break;
					}
					$this->migrateExcludedPost( $postId );
					break;
				case '_aioseop_disable':
					if ( empty( $value ) ) {
						break;
					}
					$this->migrateSitemapExcludedPost( $postId );
					break;
				case '_aioseop_noindex':
				case '_aioseop_nofollow':
					if ( 'on' === (string) $value ) {
						$meta['robots_default']       = false;
						$meta[ $mappedMeta[ $name ] ] = true;
					} elseif ( 'off' === (string) $value ) {
						$meta['robots_default'] = false;
					}
					break;
				case '_aioseop_keywords':
					$meta[ $mappedMeta[ $name ] ] = aioseo()->migration->helpers->oldKeywordsToNewKeywords( $value );
					break;
				case '_aioseop_opengraph_settings':
					$meta += $this->convertOpenGraphMeta( $value );
					break;
				case '_aioseop_sitemap_priority':
				case '_aioseop_sitemap_frequency':
					if ( empty( $value ) ) {
						$meta[ $mappedMeta[ $name ] ] = 'default';
						break;
					}
					$meta[ $mappedMeta[ $name ] ] = $value;
					break;
				default:
					$meta[ $mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
					break;
			}
		}

		return $meta;
	}

	/**
	 * Migrates a given disabled post from V3.
	 *
	 * @since 4.0.3
	 *
	 * @param  int  $postId The post ID.
	 * @return void
	 */
	private function migrateExcludedPost( $postId ) {
		$post = get_post( $postId );
		if ( ! is_object( $post ) ) {
			return;
		}

		aioseo()->options->sitemap->general->advancedSettings->enable = true;
		$excludedPosts = aioseo()->options->sitemap->general->advancedSettings->excludePosts;

		foreach ( $excludedPosts as $excludedPost ) {
			$excludedPost = json_decode( $excludedPost );
			if ( $excludedPost->value === $postId ) {
				return;
			}
		}

		$excludedPost = [
			'value' => $post->ID,
			'type'  => $post->post_type,
			'label' => $post->post_title,
			'link'  => get_permalink( $post )
		];

		$excludedPosts[] = wp_json_encode( $excludedPost );
		aioseo()->options->sitemap->general->advancedSettings->excludePosts = $excludedPosts;

		$deprecatedOptions = aioseo()->internalOptions->internal->deprecatedOptions;
		if ( ! in_array( 'excludePosts', $deprecatedOptions, true ) ) {
			array_push( $deprecatedOptions, 'excludePosts' );
			aioseo()->internalOptions->internal->deprecatedOptions = $deprecatedOptions;
		}
	}

	/**
	 * Migrates a given sitemap excluded post from V3.
	 *
	 * @since 4.0.3
	 *
	 * @param  int  $postId The post ID.
	 * @return void
	 */
	private function migrateSitemapExcludedPost( $postId ) {
		$post = get_post( $postId );
		if ( ! is_object( $post ) ) {
			return;
		}

		$excludedPosts = aioseo()->options->deprecated->searchAppearance->advanced->excludePosts;
		foreach ( $excludedPosts as $excludedPost ) {
			$excludedPost = json_decode( $excludedPost );
			if ( $excludedPost->value === $postId ) {
				return;
			}
		}

		$excludedPost = [
			'value' => $post->ID,
			'type'  => $post->post_type,
			'label' => $post->post_title,
			'link'  => get_permalink( $post )
		];

		$excludedPosts[] = wp_json_encode( $excludedPost );
		aioseo()->options->deprecated->searchAppearance->advanced->excludePosts = $excludedPosts;
	}

	/**
	 * Updates the traditional post meta table with the new data.
	 *
	 * @since 4.1.0
	 *
	 * @param  int   $postId  The post ID.
	 * @param  array $newMeta The new meta data.
	 * @return void
	 */
	protected function updateLocalizedPostMeta( $postId, $newMeta ) {
		$localizedFields = [
			'title',
			'description',
			'keywords',
			'og_title',
			'og_description',
			'og_article_section',
			'og_article_tags',
			'twitter_title',
			'twitter_description'
		];

		foreach ( $newMeta as $k => $v ) {
			if ( ! in_array( $k, $localizedFields, true ) ) {
				continue;
			}

			if ( in_array( $k, [ 'keywords', 'og_article_tags' ], true ) ) {
				$v = ! empty( $v ) ? aioseo()->helpers->jsonTagsToCommaSeparatedList( $v ) : '';
			}

			update_post_meta( $postId, "_aioseo_{$k}", $v );
		}
	}

	/**
	 * Migrates additional post meta data.
	 *
	 * @since 4.0.2
	 *
	 * @param  int  $postId The post ID.
	 * @return void
	 */
	public function migrateAdditionalPostMeta( $postId ) {
		static $disabled = null;

		if ( null === $disabled ) {
			$disabled = (
				! aioseo()->options->sitemap->general->enable ||
				(
					aioseo()->options->sitemap->general->advancedSettings->enable &&
					aioseo()->options->sitemap->general->advancedSettings->excludeImages
				)
			);
		}
		if ( $disabled ) {
			return;
		}

		aioseo()->sitemap->image->scanPost( $postId );
	}

	/**
	 * Maps the old Open Graph meta to the social meta columns in V4.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $ogMeta The old V3 Open Graph meta.
	 * @return array $meta   The mapped meta.
	 */
	public function convertOpenGraphMeta( $ogMeta ) {
		$ogMeta = aioseo()->helpers->maybeUnserialize( $ogMeta );

		if ( ! is_array( $ogMeta ) ) {
			return [];
		}

		$mappedSocialMeta = [
			'aioseop_opengraph_settings_title'             => 'og_title',
			'aioseop_opengraph_settings_desc'              => 'og_description',
			'aioseop_opengraph_settings_image'             => 'og_image_custom_url',
			'aioseop_opengraph_settings_imagewidth'        => 'og_image_width',
			'aioseop_opengraph_settings_imageheight'       => 'og_image_height',
			'aioseop_opengraph_settings_video'             => 'og_video',
			'aioseop_opengraph_settings_videowidth'        => 'og_video_width',
			'aioseop_opengraph_settings_videoheight'       => 'og_video_height',
			'aioseop_opengraph_settings_category'          => 'og_object_type',
			'aioseop_opengraph_settings_section'           => 'og_article_section',
			'aioseop_opengraph_settings_tag'               => 'og_article_tags',
			'aioseop_opengraph_settings_setcard'           => 'twitter_card',
			'aioseop_opengraph_settings_customimg_twitter' => 'twitter_image_custom_url',
		];

		$meta = [];
		foreach ( $ogMeta as $name => $value ) {
			if ( ! in_array( $name, array_keys( $mappedSocialMeta ), true ) ) {
				continue;
			}

			switch ( $name ) {
				case 'aioseop_opengraph_settings_desc':
				case 'aioseop_opengraph_settings_title':
					$meta[ $mappedSocialMeta[ $name ] ] = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $value ) );
					break;
				case 'aioseop_opengraph_settings_image':
					$value = strval( $value );
					if ( empty( $value ) ) {
						break;
					}

					$meta['og_image_type']              = 'custom_image';
					$meta[ $mappedSocialMeta[ $name ] ] = strval( $value );
					break;
				case 'aioseop_opengraph_settings_video':
					$meta[ $mappedSocialMeta[ $name ] ] = esc_url( $value );
					break;
				case 'aioseop_opengraph_settings_customimg_twitter':
					$value = strval( $value );
					if ( empty( $value ) ) {
						break;
					}
					$meta['twitter_image_type']         = 'custom_image';
					$meta['twitter_use_og']             = false;
					$meta[ $mappedSocialMeta[ $name ] ] = strval( $value );
					break;
				case 'aioseop_opengraph_settings_imagewidth':
				case 'aioseop_opengraph_settings_imageheight':
				case 'aioseop_opengraph_settings_videowidth':
				case 'aioseop_opengraph_settings_videoheight':
					$value = intval( $value );
					if ( ! $value || $value <= 0 ) {
						break;
					}
					$meta[ $mappedSocialMeta[ $name ] ] = $value;
					break;
				case 'aioseop_opengraph_settings_tag':
					$meta[ $mappedSocialMeta[ $name ] ] = aioseo()->migration->helpers->oldKeywordsToNewKeywords( $value );
					break;
				default:
					$meta[ $mappedSocialMeta[ $name ] ] = esc_html( strval( $value ) );
					break;
			}
		}

		return $meta;
	}

	/**
	 * Returns the title as it was in V3.
	 *
	 * @since 4.0.0
	 *
	 * @param  int    $postId   The post ID.
	 * @param  string $seoTitle The old SEO title.
	 * @return string           The title.
	 */
	protected function getPostTitle( $postId, $seoTitle = '' ) {
		$post = get_post( $postId );
		if ( ! is_object( $post ) ) {
			return '';
		}

		$postType    = $post->post_type;
		$oldOptions  = get_option( 'aioseo_options_v3' );
		$titleFormat = isset( $oldOptions[ "aiosp_{$postType}_title_format" ] ) ? $oldOptions[ "aiosp_{$postType}_title_format" ] : '';

		$seoTitle = aioseo()->helpers->pregReplace( '/(%post_title%|%page_title%)/', $seoTitle, $titleFormat );

		return aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $seoTitle ) );
	}
}Common/Migration/Migration.php000064400000014014151536241200012366 0ustar00<?php
namespace AIOSEO\Plugin\Common\Migration;

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

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Handles the migration from V3 to V4.
 */
class Migration {
	/**
	 * The old V3 options.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $oldOptions = [];

	/**
	 * Meta class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Meta
	 */
	public $meta = null;

	/**
	 * Helpers class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Helpers
	 */
	public $helpers = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->meta    = new Meta();
		$this->helpers = new Helpers();

		// NOTE: This needs to go above the is_admin check in order for it to run at all.
		add_action( 'aioseo_migrate_post_meta', [ $this->meta, 'migratePostMeta' ] );

		if ( ! is_admin() ) {
			return;
		}

		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		add_action( 'init', [ $this, 'init' ], 2000 );
	}

	/**
	 * Initializes the class.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function init() {
		// Since the version numbers may vary, we only want to compare the first 3 numbers.
		$lastActiveVersion = aioseo()->internalOptions->internal->lastActiveVersion;
		$lastActiveVersion = $lastActiveVersion ? explode( '-', $lastActiveVersion ) : null;

		if ( version_compare( $lastActiveVersion[0], '4.0.0', '<' ) ) {
			aioseo()->internalOptions->internal->migratedVersion = $lastActiveVersion[0];
			add_action( 'wp_loaded', [ $this, 'doMigration' ] );
		}

		// Run our migration again for V4 users between v4.0.0 and v4.0.4.
		if (
			version_compare( $lastActiveVersion[0], '4.0.0', '>=' ) &&
			version_compare( $lastActiveVersion[0], '4.0.4', '<' ) &&
			get_option( 'aioseop_options' )
		) {
			add_action( 'wp_loaded', [ $this, 'redoMetaMigration' ] );
		}

		// Stop migration for new v4 users where it was incorrectly triggered.
		if ( version_compare( $lastActiveVersion[0], '4.0.4', '=' ) && ! get_option( 'aioseop_options' ) ) {
			aioseo()->core->cache->delete( 'v3_migration_in_progress_posts' );
			aioseo()->core->cache->delete( 'v3_migration_in_progress_terms' );

			try {
				aioseo()->actionScheduler->unschedule( 'aioseo_migrate_post_meta' );
				aioseo()->actionScheduler->unschedule( 'aioseo_migrate_term_meta' );
			} catch ( \Exception $e ) {
				// Do nothing.
			}
		}
	}

	/**
	 * Starts the migration.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function doMigration() {
		// If our tables do not exist, create them now.
		if ( ! aioseo()->core->db->tableExists( 'aioseo_posts' ) ) {
			aioseo()->updates->addInitialCustomTablesForV4();
		}

		$this->oldOptions = ( new OldOptions() )->oldOptions;

		if (
			! $this->oldOptions ||
			! is_array( $this->oldOptions ) ||
			! count( $this->oldOptions )
		) {
			return;
		}

		update_option( 'aioseo_options_v3', $this->oldOptions );

		aioseo()->core->cache->update( 'v3_migration_in_progress_posts', time(), WEEK_IN_SECONDS );

		$this->migrateSettings();
		$this->meta->migrateMeta();
	}

	/**
	 * Reruns the post meta migration.
	 *
	 * This is meant for users on v4.0.0, v4.0.1 or v4.0.2 where the migration might have failed.
	 *
	 * @since 4.0.3
	 *
	 * @return void
	 */
	public function redoMetaMigration() {
		aioseo()->core->cache->update( 'v3_migration_in_progress_posts', time(), WEEK_IN_SECONDS );
		$this->meta->migrateMeta();
	}

	/**
	 * Migrates the plugin settings.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $oldOptions The old options. We pass it in directly via the Importer/Exporter.
	 * @return void
	 */
	public function migrateSettings( $oldOptions = [] ) {
		if ( empty( $this->oldOptions ) && ! empty( $oldOptions ) ) {
			$this->oldOptions = ( new OldOptions( $oldOptions ) )->oldOptions;

			if (
				! $this->oldOptions ||
				! is_array( $this->oldOptions ) ||
				! count( $this->oldOptions )
			) {
				return;
			}
		}

		aioseo()->core->cache->update( 'v3_migration_in_progress_settings', time() );

		new GeneralSettings();

		if ( ! isset( $this->oldOptions['modules']['aiosp_feature_manager_options'] ) ) {
			new Sitemap();
			aioseo()->core->cache->delete( 'v3_migration_in_progress_settings' );

			return;
		}

		$this->migrateFeatureManager();

		if ( isset( $this->oldOptions['modules']['aiosp_feature_manager_options']['aiosp_feature_manager_enable_opengraph'] ) ) {
			new SocialMeta();
		}

		if ( isset( $this->oldOptions['modules']['aiosp_feature_manager_options']['aiosp_feature_manager_enable_sitemap'] ) ) {
			new Sitemap();
		}

		if ( isset( $this->oldOptions['modules']['aiosp_feature_manager_options']['aiosp_feature_manager_enable_robots'] ) ) {
			new RobotsTxt();
		}

		if ( aioseo()->helpers->isWpmlActive() ) {
			new Wpml();
		}

		aioseo()->core->cache->delete( 'v3_migration_in_progress_settings' );
	}

	/**
	 * Migrates the Feature Manager settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function migrateFeatureManager() {
		if ( empty( $this->oldOptions['modules']['aiosp_feature_manager_options'] ) ) {
			return;
		}

		if ( empty( $this->oldOptions['modules']['aiosp_feature_manager_options']['aiosp_feature_manager_enable_opengraph'] ) ) {
			aioseo()->options->social->facebook->general->enable = false;
			aioseo()->options->social->twitter->general->enable  = false;
		}

		if ( empty( $this->oldOptions['modules']['aiosp_feature_manager_options']['aiosp_feature_manager_enable_sitemap'] ) ) {
			aioseo()->options->sitemap->general->enable = false;
			aioseo()->options->sitemap->rss->enable     = false;
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_feature_manager_options']['aiosp_feature_manager_enable_robots'] ) ) {
			aioseo()->options->tools->robots->enable = true;
		}
	}

	/**
	 * Checks whether the V3 migration is running.
	 *
	 * @since 4.1.8
	 *
	 * @return bool Whether the V3 migration is running.
	 */
	public function isMigrationRunning() {
		return aioseo()->core->cache->get( 'v3_migration_in_progress_settings' ) || aioseo()->core->cache->get( 'v3_migration_in_progress_posts' );
	}
}Common/Migration/OldOptions.php000064400000014772151536241200012542 0ustar00<?php
namespace AIOSEO\Plugin\Common\Migration;

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

/**
 * Updates and holds the old options from V3.
 *
 * @since 4.0.0
 */
class OldOptions {
	/**
	 * The old options from V3.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $oldOptions = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 *
	 * @param array $oldOptions The old options. We pass it in directly via the Importer/Exporter.
	 */
	public function __construct( $oldOptions = [] ) {
		$this->oldOptions = ! empty( $oldOptions ) ? $oldOptions : get_option( 'aioseop_options' );

		if (
			! $this->oldOptions ||
			! is_array( $this->oldOptions ) ||
			! count( $this->oldOptions )
		) {
			return;
		}

		$this->runPreV4Migrations();
		$this->fixSettingValues();
	}

	/**
	 * Runs all pre-V4 migrations to update the old options to the latest state.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function runPreV4Migrations() {
		$lastActiveVersion = aioseo()->internalOptions->internal->lastActiveVersion;
		if ( version_compare( $lastActiveVersion, aioseo()->version, '<' ) ) {

			$this->doVersionUpdates( $lastActiveVersion );
			aioseo()->internalOptions->internal->lastActiveVersion = aioseo()->version;
		}
	}

	/**
	 * Runs all pre-V4 version-based migrations.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $oldVersion The old version number to compare against.
	 * @return void
	 */
	protected function doVersionUpdates( $oldVersion ) {
		if ( version_compare( $oldVersion, '3.0', '<' ) ) {
			$this->sitemapExclTerms201905();
		}

		if ( version_compare( $oldVersion, '3.1', '<' ) ) {
			$this->resetFlushRewriteRules201906();
		}

		if (
			version_compare( $oldVersion, '3.2', '<' ) ||
			version_compare( $oldVersion, '3.2.6', '<' )
		) {
			$this->updateSchemaMarkup201907();
		}

		if ( version_compare( $oldVersion, '4.0.0', '<' ) ) {
			$this->updateArchiveNoIndexSettings20200413();
			$this->updateArchiveTitleFormatSettings20200413();
		}
	}

	/**
	 * Converts "excl_categories" to "excl_terms".
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function sitemapExclTerms201905() {
		if (
			empty( $this->oldOptions['modules'] ) ||
			empty( $this->oldOptions['modules']['aiosp_sitemap_options'] )
		) {
			return;
		}

		$options = $this->oldOptions['modules']['aiosp_sitemap_options'];
		if ( ! empty( $options['aiosp_sitemap_excl_categories'] ) ) {
			$options['aiosp_sitemap_excl_terms']['category']['taxonomy'] = 'category';
			$options['aiosp_sitemap_excl_terms']['category']['terms']    = $options['aiosp_sitemap_excl_categories'];
			unset( $options['aiosp_sitemap_excl_categories'] );

			$this->oldOptions['modules']['aiosp_sitemap_options'] = $options;
		}
	}

	/**
	 * Flushes rewrite rules for XML Sitemap URL changes.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function resetFlushRewriteRules201906() {
		add_action( 'shutdown', 'flush_rewrite_rules' );
	}

	/**
	 * Adds a number of schema markup settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function updateSchemaMarkup201907() {
		$updateValues = [
			'aiosp_schema_markup'               => '1',
			'aiosp_schema_search_results_page'  => '1',
			'aiosp_schema_social_profile_links' => '',
			'aiosp_schema_site_represents'      => 'organization',
			'aiosp_schema_organization_name'    => '',
			'aiosp_schema_organization_logo'    => '',
			'aiosp_schema_person_user'          => '1',
			'aiosp_schema_phone_number'         => '',
			'aiosp_schema_contact_type'         => 'none',
		];

		if ( isset( $this->oldOptions['aiosp_schema_markup'] ) ) {
			if ( empty( $this->oldOptions['aiosp_schema_markup'] ) || 'off' === $this->oldOptions['aiosp_schema_markup'] ) {
				$updateValues['aiosp_schema_markup'] = '0';
			}
		}
		if ( isset( $this->oldOptions['aiosp_google_sitelinks_search'] ) ) {
			if ( empty( $this->oldOptions['aiosp_google_sitelinks_search'] ) || 'off' === $this->oldOptions['aiosp_google_sitelinks_search'] ) {
				$updateValues['aiosp_schema_search_results_page'] = '0';
			}
		}
		if ( isset( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_profile_links'] ) ) {
			$updateValues['aiosp_schema_social_profile_links'] = $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_profile_links'];
		}
		if ( isset( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_person_or_org'] ) ) {
			if ( 'person' === $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_person_or_org'] ) {
				$updateValues['aiosp_schema_site_represents'] = 'person';
			}
		}
		if ( isset( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_social_name'] ) ) {
			$updateValues['aiosp_schema_organization_name'] = $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_social_name'];
		}

		foreach ( $updateValues as $k => $v ) {
			$this->oldOptions[ $k ] = $v;
		}
	}

	/**
	 * Migrate setting for noindex archives.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function updateArchiveNoIndexSettings20200413() {
		if ( isset( $this->oldOptions['aiosp_archive_noindex'] ) ) {
			$this->oldOptions['aiosp_archive_date_noindex']   = $this->oldOptions['aiosp_archive_noindex'];
			$this->oldOptions['aiosp_archive_author_noindex'] = $this->oldOptions['aiosp_archive_noindex'];
			unset( $this->oldOptions['aiosp_archive_noindex'] );
		}
	}

	/**
	 * Migrate settings for archive title formats.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function updateArchiveTitleFormatSettings20200413() {
		if (
			isset( $this->oldOptions['aiosp_archive_title_format'] ) &&
			empty( $this->oldOptions['aiosp_date_title_format'] )
		) {
			$this->oldOptions['aiosp_date_title_format'] = $this->oldOptions['aiosp_archive_title_format'];
			unset( $this->oldOptions['aiosp_archive_title_format'] );
		}

		if (
			isset( $this->oldOptions['aiosp_archive_title_format'] ) &&
			'%date% | %site_title%' === $this->oldOptions['aiosp_archive_title_format']
		) {
			unset( $this->oldOptions['aiosp_archive_title_format'] );
		}
	}

	/**
	 * Corrects the value of a number of settings in V3 that are illogical.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function fixSettingValues() {
		$settingsToFix = [
			'aiosp_togglekeywords'
		];
		foreach ( $settingsToFix as $settingToFix ) {
			if ( isset( $this->oldOptions[ $settingToFix ] ) ) {
				if ( '1' === (string) $this->oldOptions[ $settingToFix ] ) {
					$this->oldOptions[ $settingToFix ] = '';
					continue;
				}
				$this->oldOptions[ $settingToFix ] = 'on';
			}
		}
	}
}Common/Migration/RobotsTxt.php000064400000002723151536241200012411 0ustar00<?php
namespace AIOSEO\Plugin\Common\Migration;

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

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Robots.txt settings from V3.
 *
 * @since 4.0.0
 */
class RobotsTxt {
	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$oldOptions = aioseo()->migration->oldOptions;

		$rules = aioseo()->options->tools->robots->rules;

		if (
			! empty( $oldOptions['modules']['aiosp_robots_options'] ) &&
			! empty( $oldOptions['modules']['aiosp_robots_options']['aiosp_robots_rules'] )
		) {
			$rules += $this->convertRules( $oldOptions['modules']['aiosp_robots_options']['aiosp_robots_rules'] );
		}

		aioseo()->options->tools->robots->rules = $rules;
	}

	/**
	 * Converts the old Robots.txt rules to the new format.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $oldRules The old rules.
	 * @return array $newRules The converted rules.
	 */
	private function convertRules( $oldRules ) {
		$newRules = [];
		foreach ( $oldRules as $oldRule ) {
			$newRule                = new \stdClass();
			$newRule->userAgent     = aioseo()->helpers->sanitizeOption( $oldRule['agent'] );
			$newRule->rule          = aioseo()->helpers->sanitizeOption( lcfirst( $oldRule['type'] ) );
			$newRule->directoryPath = aioseo()->helpers->sanitizeOption( $oldRule['path'] );

			array_push( $newRules, wp_json_encode( $newRule ) );
		}

		return $newRules;
	}
}Common/Migration/Sitemap.php000064400000035775151536241200012060 0ustar00<?php
namespace AIOSEO\Plugin\Common\Migration;

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

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the XML Sitemap settings from V3.
 *
 * @since 4.0.0
 */
class Sitemap {
	/**
	 * The old V3 options.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $oldOptions = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->oldOptions = aioseo()->migration->oldOptions;

		if ( empty( $this->oldOptions['modules']['aiosp_sitemap_options'] ) ) {
			return;
		}

		$this->checkIfStatic();
		$this->migrateLinksPerIndex();
		$this->migrateIncludedObjects();
		$this->migratePrioFreq();
		$this->migrateAdditionalPages();
		$this->migrateExcludedPages();
		$this->regenerateSitemap();

		$settings = [
			'aiosp_sitemap_indexes'          => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'general', 'indexes' ] ],
			'aiosp_sitemap_archive'          => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'general', 'date' ] ],
			'aiosp_sitemap_author'           => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'general', 'author' ] ],
			'aiosp_sitemap_images'           => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'excludeImages' ] ],
			'aiosp_sitemap_rss_sitemap'      => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'rss', 'enable' ] ],
			'aiosp_sitemap_filename'         => [ 'type' => 'string', 'newOption' => [ 'sitemap', 'general', 'filename' ] ],
			'aiosp_sitemap_publication_name' => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'news', 'publicationName' ] ],
			'aiosp_sitemap_rewrite'          => [ 'type' => 'boolean', 'newOption' => [ 'deprecated', 'sitemap', 'general', 'advancedSettings', 'dynamic' ] ]
		];

		aioseo()->migration->helpers->mapOldToNew( $settings, $this->oldOptions['modules']['aiosp_sitemap_options'] );

		if (
			aioseo()->options->sitemap->general->advancedSettings->excludePosts ||
			aioseo()->options->sitemap->general->advancedSettings->excludeTerms ||
			aioseo()->options->sitemap->general->advancedSettings->excludeImages ||
			( in_array( 'staticSitemap', aioseo()->internalOptions->internal->deprecatedOptions, true ) && ! aioseo()->options->deprecated->sitemap->general->advancedSettings->dynamic )
		) {
			aioseo()->options->sitemap->general->advancedSettings->enable = true;
		}
	}

	/**
	 * Check if the sitemap is statically generated.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function checkIfStatic() {
		if (
			isset( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_rewrite'] ) &&
			empty( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_rewrite'] )
		) {
			$deprecatedOptions = aioseo()->internalOptions->internal->deprecatedOptions;
			array_push( $deprecatedOptions, 'staticSitemap' );
			aioseo()->internalOptions->internal->deprecatedOptions = $deprecatedOptions;

			aioseo()->options->deprecated->sitemap->general->advancedSettings->dynamic = false;
		}
	}

	/**
	 * Migrates the amount of links per sitemap index.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateLinksPerIndex() {
		if ( ! empty( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_max_posts'] ) ) {
			$value = intval( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_max_posts'] );
			if ( ! $value ) {
				return;
			}
			$value = $value > 50000 ? 50000 : $value;
			aioseo()->options->sitemap->general->linksPerIndex = $value;
		}
	}

	/**
	 * Migrates the excluded object settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function migrateExcludedPages() {
		if (
			empty( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_excl_terms'] ) &&
			empty( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_excl_pages'] )
		) {
			return;
		}

		$excludedPosts = aioseo()->options->sitemap->general->advancedSettings->excludePosts;
		if ( ! empty( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_excl_pages'] ) ) {
			$pages = explode( ',', $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_excl_pages'] );
			if ( count( $pages ) ) {
				foreach ( $pages as $page ) {
					$page = trim( $page );
					$id   = intval( $page );
					if ( ! $id ) {
						$post = get_page_by_path( $page, OBJECT, aioseo()->helpers->getPublicPostTypes( true ) );
						if ( $post && is_object( $post ) ) {
							$id = $post->ID;
						}
					}

					if ( $id ) {
						$post = get_post( $id );
						if ( ! is_object( $post ) ) {
							continue;
						}

						$excludedPost        = new \stdClass();
						$excludedPost->value = $id;
						$excludedPost->type  = $post->post_type;
						$excludedPost->label = $post->post_title;
						$excludedPost->link  = get_permalink( $id );

						array_push( $excludedPosts, wp_json_encode( $excludedPost ) );
					}
				}
			}
		}
		aioseo()->options->sitemap->general->advancedSettings->excludePosts = $excludedPosts;

		$excludedTerms = aioseo()->options->sitemap->general->advancedSettings->excludeTerms;
		if ( ! empty( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_excl_terms'] ) ) {
			foreach ( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_excl_terms'] as $taxonomy ) {
				foreach ( $taxonomy['terms'] as $id ) {
					$term = get_term( $id );
					if ( ! is_a( $term, 'WP_Term' ) ) {
						continue;
					}

					$excludedTerm        = new \stdClass();
					$excludedTerm->value = $id;
					$excludedTerm->type  = $term->taxonomy;
					$excludedTerm->label = $term->name;
					$excludedTerm->link  = get_term_link( $term );

					array_push( $excludedTerms, wp_json_encode( $excludedTerm ) );
				}
			}
		}
		aioseo()->options->sitemap->general->advancedSettings->excludeTerms = $excludedTerms;
	}

	/**
	 * Migrates the objects that are included in the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function migrateIncludedObjects() {
		if (
			! isset( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_posttypes'] ) &&
			! isset( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_taxonomies'] )
		) {
			return;
		}

		if ( ! is_array( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_posttypes'] ) ) {
			$this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_posttypes'] = [];
		}

		if ( ! is_array( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_taxonomies'] ) ) {
			$this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_taxonomies'] = [];
		}

		$publicPostTypes  = aioseo()->helpers->getPublicPostTypes( true );
		$publicTaxonomies = aioseo()->helpers->getPublicTaxonomies( true );

		if ( in_array( 'all', $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_posttypes'], true ) ) {
			aioseo()->options->sitemap->general->postTypes->all      = true;
			aioseo()->options->sitemap->general->postTypes->included = array_values( $publicPostTypes );
		} else {
			$allPostTypes = true;
			foreach ( $publicPostTypes as $postType ) {
				if ( ! in_array( $postType, $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_posttypes'], true ) ) {
					$allPostTypes = false;
				}
			}

			aioseo()->options->sitemap->general->postTypes->all      = $allPostTypes;
			aioseo()->options->sitemap->general->postTypes->included = array_values(
				array_intersect( $publicPostTypes, $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_posttypes'] )
			);
		}

		if ( in_array( 'all', $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_taxonomies'], true ) ) {
			aioseo()->options->sitemap->general->taxonomies->all      = true;
			aioseo()->options->sitemap->general->taxonomies->included = array_values( $publicTaxonomies );
		} else {
			$allTaxonomies = true;
			foreach ( $publicTaxonomies as $taxonomy ) {
				if ( ! in_array( $taxonomy, $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_taxonomies'], true ) ) {
					$allTaxonomies = false;
				}
			}

			aioseo()->options->sitemap->general->taxonomies->all      = $allTaxonomies;
			aioseo()->options->sitemap->general->taxonomies->included = array_values(
				array_intersect( $publicTaxonomies, $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_taxonomies'] )
			);
		}
	}

	/**
	 * Migrates the additional pages that are included in the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateAdditionalPages() {
		if ( empty( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_addl_pages'] ) ) {
			return;
		}

		$pages = [];
		foreach ( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_addl_pages'] as $url => $values ) {
			$page               = new \stdClass();
			$page->url          = esc_url( wp_strip_all_tags( $url ) );
			$page->priority     = [ 'label' => $values['prio'], 'value' => $values['prio'] ];
			$page->frequency    = [ 'label' => $values['freq'], 'value' => $values['freq'] ];
			$page->lastModified = gmdate( 'm/d/Y', strtotime( $values['mod'] ) );

			$pages[] = wp_json_encode( $page );
		}

		aioseo()->options->sitemap->general->additionalPages->enable = true;
		aioseo()->options->sitemap->general->additionalPages->pages  = $pages;
	}

	/**
	 * Migrates the priority/frequency settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migratePrioFreq() {
		$settings = [
			'aiosp_sitemap_prio_homepage'   => [ 'type' => 'float', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'homePage', 'priority' ] ],
			'aiosp_sitemap_freq_homepage'   => [ 'type' => 'string', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'homePage', 'frequency' ] ],
			'aiosp_sitemap_prio_post'       => [ 'type' => 'float', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'postTypes', 'priority' ] ],
			'aiosp_sitemap_freq_post'       => [ 'type' => 'string', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'postTypes', 'frequency' ] ],
			'aiosp_sitemap_prio_post_post'  => [ 'type' => 'float', 'newOption' => [ 'sitemap', 'priority', 'postTypes', 'post', 'priority' ], 'dynamic' => true ],
			'aiosp_sitemap_freq_post_post'  => [ 'type' => 'string', 'newOption' => [ 'sitemap', 'priority', 'postTypes', 'post', 'frequency' ], 'dynamic' => true ],
			'aiosp_sitemap_prio_taxonomies' => [ 'type' => 'float', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'taxonomies', 'priority' ] ],
			'aiosp_sitemap_freq_taxonomies' => [ 'type' => 'string', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'taxonomies', 'frequency' ] ],
			'aiosp_sitemap_prio_archive'    => [ 'type' => 'float', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'archive', 'priority' ] ],
			'aiosp_sitemap_freq_archive'    => [ 'type' => 'string', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'archive', 'frequency' ] ],
			'aiosp_sitemap_prio_author'     => [ 'type' => 'float', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'author', 'priority' ] ],
			'aiosp_sitemap_freq_author'     => [ 'type' => 'string', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'author', 'frequency' ] ],
		];

		foreach ( $this->oldOptions['modules']['aiosp_sitemap_options'] as $name => $value ) {
			// Ignore fixed settings.
			if ( in_array( $name, array_keys( $settings ), true ) ) {
				continue;
			}

			$type = false;
			$slug = '';
			if ( preg_match( '#aiosp_sitemap_prio_(.*)#', (string) $name, $slug ) ) {
				$type = 'priority';
			} elseif ( preg_match( '#aiosp_sitemap_freq_(.*)#', (string) $name, $slug ) ) {
				$type = 'frequency';
			}

			if ( empty( $slug ) || empty( $slug[1] ) ) {
				continue;
			}

			$objectSlug = aioseo()->helpers->pregReplace( '#post_(?!tag)|taxonomies_#', '', $slug[1] );

			if ( in_array( $objectSlug, aioseo()->helpers->getPublicPostTypes( true ), true ) ) {
				$settings[ $name ] = [
					'type'      => 'priority' === $type ? 'float' : 'string',
					'newOption' => [ 'sitemap', 'priority', 'postTypes', $objectSlug, $type ],
					'dynamic'   => true
				];
				continue;
			}

			if ( in_array( $objectSlug, aioseo()->helpers->getPublicTaxonomies( true ), true ) ) {
				$settings[ $name ] = [
					'type'      => 'priority' === $type ? 'float' : 'string',
					'newOption' => [ 'sitemap', 'priority', 'taxonomies', $objectSlug, $type ],
					'dynamic'   => true
				];
			}
		}

		$mainOptions    = aioseo()->options->noConflict();
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		foreach ( $settings as $name => $values ) {
			// If setting is set to default, do nothing.
			if (
				empty( $this->oldOptions['modules']['aiosp_sitemap_options'][ $name ] ) ||
				'no' === $this->oldOptions['modules']['aiosp_sitemap_options'][ $name ]
			) {
				unset( $settings[ $name ] );
				continue;
			}

			// If value is "Select Individual", set grouped to false.
			$value = $this->oldOptions['modules']['aiosp_sitemap_options'][ $name ];
			if ( 'sel' === $value ) {
				if ( preg_match( '#post$#', (string) $name ) ) {
					aioseo()->options->sitemap->general->advancedSettings->priority->postTypes->grouped = false;
				} else {
					aioseo()->options->sitemap->general->advancedSettings->priority->taxonomies->grouped = false;
				}
				continue;
			}

			$object        = new \stdClass();
			$object->label = $value;
			$object->value = $value;

			$error      = false;
			$options    = ! empty( $values['dynamic'] ) ? $dynamicOptions : $mainOptions;
			$lastOption = '';
			for ( $i = 0; $i < count( $values['newOption'] ); $i++ ) {
				$lastOption = $values['newOption'][ $i ];
				if ( ! $options->has( $lastOption, false ) ) {
					$error = true;
					break;
				}

				if ( count( $values['newOption'] ) - 1 !== $i ) {
					$options = $options->$lastOption;
				}
			}

			if ( $error ) {
				continue;
			}

			$options->$lastOption = wp_json_encode( $object );
		}

		if ( count( $settings ) ) {
			$mainOptions->sitemap->general->advancedSettings->enable = true;
		}
	}

	/**
	 * Regenerates the sitemap if it is static.
	 *
	 * We need to do this since the stylesheet URLs have changed.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function regenerateSitemap() {
		if (
			isset( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_rewrite'] ) &&
			empty( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_rewrite'] )
		) {
			$files         = aioseo()->sitemap->file->files();
			$detectedFiles = [];
			foreach ( $files as $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;
				}
			}

			$fs = aioseo()->core->fs;
			if ( count( $detectedFiles ) && $fs->isWpfsValid() ) {
				foreach ( $detectedFiles as $file ) {
					$fs->fs->delete( $file, false, 'f' );
				}
			}

			aioseo()->sitemap->file->generate( true );
		}
	}
}Common/Migration/SocialMeta.php000064400000045565151536241200012475 0ustar00<?php
namespace AIOSEO\Plugin\Common\Migration;

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

use AIOSEO\Plugin\Common\Models;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Social Meta settings from V3.
 *
 * @since 4.0.0
 */
class SocialMeta {
	/**
	 * The old V3 options.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $oldOptions = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->oldOptions = aioseo()->migration->oldOptions;

		if ( empty( $this->oldOptions['modules']['aiosp_opengraph_options'] ) ) {
			return;
		}

		$this->migrateHomePageOgTitle();
		$this->migrateHomePageOgDescription();
		$this->migrateTwitterUsername();
		$this->migrateTwitterCardType();
		$this->migrateSocialPostImageSettings();
		$this->migrateDefaultObjectTypes();
		$this->migrateAdvancedSettings();
		$this->migrateProfileSocialUrls();

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_sitename'] ) ) {
			aioseo()->options->social->facebook->general->siteName = aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_sitename'] );
		}

		$settings = [
			'aiosp_opengraph_facebook_author' => [ 'type' => 'boolean', 'newOption' => [ 'social', 'facebook', 'general', 'showAuthor' ] ],
			'aiosp_opengraph_twitter_creator' => [ 'type' => 'boolean', 'newOption' => [ 'social', 'twitter', 'general', 'showAuthor' ] ],
		];

		aioseo()->migration->helpers->mapOldToNew( $settings, $this->oldOptions['modules']['aiosp_opengraph_options'] );

		$this->maybeShowOgNotices();
	}

	/**
	 * Check if we need to add a notice about the OG deprecated settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function maybeShowOgNotices() {
		$include = [];

		// Check if any of thw following are set to true.
		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_generate_descriptions'] ) ) {
			$include[] = __( 'Use Content for Autogenerated Descriptions', 'all-in-one-seo-pack' );
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_description_shortcodes'] ) ) {
			$include[] = __( 'Run Shortcodes in Description', 'all-in-one-seo-pack' );
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_title_shortcodes'] ) ) {
			$include[] = __( 'Run Shortcodes in Title', 'all-in-one-seo-pack' );
		}

		if ( empty( $include ) ) {
			return;
		}

		$content = __( 'Due to some changes in how our Open Graph integration works, your Facebook Titles and Descriptions may have changed. You were using the following options that have been removed:', 'all-in-one-seo-pack' ) . '<ul>'; // phpcs:ignore Generic.Files.LineLength.MaxExceeded

		foreach ( $include as $setting ) {
			$content .= '<li><strong>' . $setting . '</strong></li>';
		}

		$content .= '</ul>';

		$notification = Models\Notification::getNotificationByName( 'v3-migration-deprecated-opengraph' );
		if ( $notification->notification_name ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'v3-migration-deprecated-opengraph',
			'title'             => __( 'Review Your Facebook Open Graph Titles and Descriptions', 'all-in-one-seo-pack' ),
			'content'           => $content,
			'type'              => 'warning',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Learn More', 'all-in-one-seo-pack' ),
			'button1_action'    => aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'docs/deprecated-opengraph-settings', 'notifications-center', 'v3-migration-deprecated-opengraph' ),
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Migrates the Open Graph homepage title.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateHomePageOgTitle() {
		$showOnFront        = get_option( 'show_on_front' );
		$pageOnFront        = (int) get_option( 'page_on_front' );
		$useHomePageMeta    = ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_setmeta'] );
		$format             = $this->oldOptions['aiosp_home_page_title_format'];

		// Latest Posts.
		if ( 'posts' === $showOnFront ) {
			$ogTitle = aioseo()->helpers->pregReplace( '#%page_title%#', '#site_title', $format );
			if ( ! $useHomePageMeta ) {
				if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_hometitle'] ) ) {
					$ogTitle = $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_hometitle'];
				}
				aioseo()->options->social->facebook->homePage->title = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $ogTitle ) );
				aioseo()->options->social->twitter->homePage->title  = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $ogTitle ) );

				return;
			}
			$title   = aioseo()->options->searchAppearance->global->siteTitle;
			$ogTitle = $title ? $title : $ogTitle;
			aioseo()->options->social->facebook->homePage->title = aioseo()->helpers->sanitizeOption( $ogTitle );
			aioseo()->options->social->twitter->homePage->title  = aioseo()->helpers->sanitizeOption( $ogTitle );

			return;
		}

		// Static Home Page.
		$post       = 'page' === $showOnFront && $pageOnFront ? aioseo()->helpers->getPost( $pageOnFront ) : '';
		$aioseoPost = Models\Post::getPost( $post->ID );
		$seoTitle   = get_post_meta( $post->ID, '_aioseop_title', true );
		$ogMeta     = get_post_meta( $post->ID, '_aioseop_opengraph_settings', true );

		if ( ! $ogMeta ) {
			return;
		}

		$ogMeta = aioseo()->helpers->maybeUnserialize( $ogMeta );

		$ogTitle = '';
		if ( ! $useHomePageMeta ) {
			if ( empty( $this->oldOptions['aiosp_use_static_home_info'] ) ) {
				$ogTitle = ! empty( $this->oldOptions['aiosp_home_title'] ) ? $this->oldOptions['aiosp_home_title'] : $ogTitle;
				if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_hometitle'] ) ) {
					$ogTitle = $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_hometitle'];
				}
				if ( ! empty( $ogMeta['aioseop_opengraph_settings_title'] ) ) {
					$ogTitle = $ogMeta['aioseop_opengraph_settings_title'];
				} elseif ( ! empty( $seoTitle ) ) {
					if ( empty( $ogTitle ) ) {
						$ogTitle = $seoTitle;
					} elseif ( empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_hometitle'] ) ) {
						$ogTitle = $seoTitle;
					}
				}
			}
		} else {
			if ( empty( $this->oldOptions['aiosp_use_static_home_info'] ) ) {
				$ogTitle = $aioseoPost->title;
				if ( ! empty( $ogMeta['aioseop_opengraph_settings_title'] ) ) {
					$ogTitle = $ogMeta['aioseop_opengraph_settings_title'];
				}
				$ogTitle = ! empty( $this->oldOptions['aiosp_home_title'] ) ? $this->oldOptions['aiosp_home_title'] : $ogTitle;
				if ( ! empty( $seoTitle ) ) {
					$ogTitle = $seoTitle;
				}
			} else {
				$ogTitle = ! empty( $seoTitle ) ? $seoTitle : $ogTitle;
			}
		}

		$ogTitle = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $ogTitle ) );
		$aioseoPost->set( [
			'post_id'       => $post->ID,
			'og_title'      => $ogTitle,
			'twitter_title' => $ogTitle
		] );
		$aioseoPost->save();
	}

	/**
	 * Migrates the Open Graph homepage description.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateHomePageOgDescription() {
		$showOnFront        = get_option( 'show_on_front' );
		$pageOnFront        = (int) get_option( 'page_on_front' );
		$useHomePageMeta    = ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_setmeta'] );
		$format             = $this->oldOptions['aiosp_description_format'];

		if ( 'posts' === $showOnFront ) {
			$ogDescription = aioseo()->helpers->pregReplace( '#%description%#', '#tagline', $format );
			if ( ! $useHomePageMeta ) {
				if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_description'] ) ) {
					$ogDescription = $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_description'];
				}
				aioseo()->options->social->facebook->homePage->description = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $ogDescription ) );
				aioseo()->options->social->twitter->homePage->description = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $ogDescription ) );

				return;
			}
			$description   = aioseo()->options->searchAppearance->global->metaDescription;
			$ogDescription = $description ? $description : $ogDescription;
			aioseo()->options->social->facebook->homePage->description = aioseo()->helpers->sanitizeOption( $ogDescription );
			aioseo()->options->social->twitter->homePage->description  = aioseo()->helpers->sanitizeOption( $ogDescription );

			return;
		}

		$post           = 'page' === $showOnFront && $pageOnFront ? aioseo()->helpers->getPost( $pageOnFront ) : '';
		$aioseoPost     = Models\Post::getPost( $post->ID );
		$seoDescription = get_post_meta( $post->ID, '_aioseop_description', true );
		$ogMeta         = get_post_meta( $post->ID, '_aioseop_opengraph_settings', true );

		if ( ! $ogMeta ) {
			return;
		}

		$ogMeta = aioseo()->helpers->maybeUnserialize( $ogMeta );

		$ogDescription = '';
		if ( ! $useHomePageMeta ) {
			if ( empty( $this->oldOptions['aiosp_use_static_home_info'] ) ) {
				$ogDescription = ! empty( $this->oldOptions['aiosp_home_description'] ) ? $this->oldOptions['aiosp_home_description'] : $ogDescription;
				if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_description'] ) ) {
					$ogDescription = $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_description'];
				}
				if ( ! empty( $ogMeta['aioseop_opengraph_settings_desc'] ) ) {
					$ogDescription = $ogMeta['aioseop_opengraph_settings_desc'];
				} elseif ( ! empty( $seoDescription ) ) {
					if ( empty( $ogDescription ) ) {
						$ogDescription = $seoDescription;
					} elseif ( empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_description'] ) ) {
						$ogDescription = $seoDescription;
					}
				}
			}
		} else {
			if ( empty( $this->oldOptions['aiosp_use_static_home_info'] ) ) {
				$ogDescription = $aioseoPost->description;
				if ( ! empty( $ogMeta['aioseop_opengraph_settings_desc'] ) ) {
					$ogDescription = $ogMeta['aioseop_opengraph_settings_desc'];
				}
				$ogDescription = ! empty( $this->oldOptions['aiosp_home_description'] ) ? $this->oldOptions['aiosp_home_description'] : $ogDescription;
				if ( ! empty( $seoDescription ) ) {
					$ogDescription = $seoDescription;
				}
			} else {
				$ogDescription = ! empty( $seoDescription ) ? $seoDescription : $ogDescription;
			}
		}

		$ogDescription = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $ogDescription ) );
		$aioseoPost->set( [
			'post_id'             => $post->ID,
			'og_description'      => $ogDescription,
			'twitter_description' => $ogDescription
		] );
		$aioseoPost->save();
	}

	/**
	 * Migrates the Open Graph default post images.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateSocialPostImageSettings() {
		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_homeimage'] ) ) {
			$value = esc_url( wp_strip_all_tags( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_homeimage'] ) );
			aioseo()->options->social->facebook->homePage->image = $value;
			aioseo()->options->social->twitter->homePage->image  = $value;
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_defimg'] ) ) {
			$value = aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_defimg'] );
			aioseo()->options->social->facebook->general->defaultImageSourcePosts = $value;
			aioseo()->options->social->twitter->general->defaultImageSourcePosts  = $value;
		}

		if (
			! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_dimg'] ) &&
			! preg_match( '/default-user-image.png$/', (string) $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_dimg'] )
		) {
			$value = esc_url( wp_strip_all_tags( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_dimg'] ) );
			aioseo()->options->social->facebook->general->defaultImagePosts = $value;
			aioseo()->options->social->twitter->general->defaultImagePosts  = $value;
		} else {
			aioseo()->options->social->facebook->general->defaultImagePosts = '';
			aioseo()->options->social->twitter->general->defaultImagePosts  = '';
		}

		if (
			! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_dimgwidth'] ) ||
			! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_dimgheight'] )
		) {
			aioseo()->options->social->facebook->general->defaultImageWidthPosts =
				aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_dimgwidth'] );
			aioseo()->options->social->facebook->general->defaultImageHeightPosts =
				aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_dimgheight'] );
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_meta_key'] ) ) {
			$value = aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_meta_key'] );
			aioseo()->options->social->facebook->general->customFieldImagePosts = $value;
			aioseo()->options->social->twitter->general->customFieldImagePosts  = $value;
		}
	}

	/**
	 * Migrates the Twitter username.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateTwitterUsername() {
		if (
			! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_twitter_site'] ) &&
			! aioseo()->options->social->profiles->urls->twitterUrl
		) {
			$username = ltrim( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_twitter_site'], '@' );
			aioseo()->options->social->profiles->urls->twitterUrl =
				esc_url( 'https://x.com/' . aioseo()->social->twitter->prepareUsername( aioseo()->helpers->sanitizeOption( $username ), false ) );
		}
	}

	/**
	 * Migrates the Twitter card type.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateTwitterCardType() {
		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_defcard'] ) ) {
			aioseo()->options->social->twitter->general->defaultCardType =
				aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_defcard'] );
			aioseo()->options->social->twitter->homePage->cardType =
				aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_defcard'] );
		}
	}

	/**
	 * Migrates the default object types.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateDefaultObjectTypes() {
		foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
			$settingName = "aiosp_opengraph_{$postType}_fb_object_type";
			if ( ! in_array( $settingName, array_keys( $this->oldOptions['modules']['aiosp_opengraph_options'] ), true ) ) {
				continue;
			}

			$dynamicOptions = aioseo()->dynamicOptions->noConflict();
			if ( $dynamicOptions->social->facebook->general->postTypes->has( $postType ) ) {
				aioseo()->dynamicOptions->social->facebook->general->postTypes->$postType->objectType =
					aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options'][ $settingName ] );
			}

			if ( 'post' === $postType ) {
				aioseo()->options->social->facebook->homePage->objectType =
					aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options'][ $settingName ] );
			}
		}
	}

	/**
	 * Migrates a number of advanced settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateAdvancedSettings() {
		$advancedEnabled = false;

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_key'] ) ) {
			$advancedEnabled = true;
			aioseo()->options->social->facebook->advanced->adminId = aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_key'] );
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_appid'] ) ) {
			$advancedEnabled = true;
			aioseo()->options->social->facebook->advanced->appId  = aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_appid'] );
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_gen_tags'] ) ) {
			$advancedEnabled = true;
			aioseo()->options->social->facebook->advanced->generateArticleTags = true;
		} else {
			aioseo()->options->social->facebook->advanced->generateArticleTags = false;
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_gen_keywords'] ) ) {
			$advancedEnabled = true;
			aioseo()->options->social->facebook->advanced->useKeywordsInTags = true;
		} else {
			aioseo()->options->social->facebook->advanced->useKeywordsInTags = false;
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_gen_categories'] ) ) {
			$advancedEnabled = true;
			aioseo()->options->social->facebook->advanced->useCategoriesInTags = true;
		} else {
			aioseo()->options->social->facebook->advanced->useCategoriesInTags = false;
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_gen_post_tags'] ) ) {
			$advancedEnabled = true;
			aioseo()->options->social->facebook->advanced->usePostTagsInTags = true;
		} else {
			aioseo()->options->social->facebook->advanced->usePostTagsInTags = false;
		}

		aioseo()->options->social->facebook->advanced->enable = $advancedEnabled;
	}

	/**
	 * Migrates the social URLs for the author users.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateProfileSocialUrls() {
		$records = aioseo()->core->db
			->start( aioseo()->core->db->db->usermeta, true )
			->select( '*' )
			->where( 'meta_key', 'facebook' )
			->run()
			->result();

		if ( count( $records ) ) {
			foreach ( $records as $record ) {
				if ( ! empty( $record->user_id ) && ! empty( $record->meta_value ) ) {
					update_user_meta(
						(int) $record->user_id,
						'aioseo_facebook',
						esc_url( $record->meta_value )
					);
				}
			}
		}

		$records = aioseo()->core->db
			->start( aioseo()->core->db->db->usermeta, true )
			->select( '*' )
			->where( 'meta_key', 'twitter' )
			->run()
			->result();

		if ( count( $records ) ) {
			foreach ( $records as $record ) {
				if ( ! empty( $record->user_id ) && ! empty( $record->meta_value ) ) {
					update_user_meta(
						(int) $record->user_id,
						'aioseo_twitter',
						sanitize_text_field( $record->meta_value )
					);
				}
			}
		}
	}
}Common/Migration/Wpml.php000064400000010215151536241200011353 0ustar00<?php
namespace AIOSEO\Plugin\Common\Migration;

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

/**
 * Migrates the WPML settings from V3.
 *
 * @since 4.0.0
 */
class Wpml {
	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		// If the tables don't exist (could happen), return early.
		if ( ! aioseo()->core->db->tableExists( 'icl_strings' ) && ! aioseo()->core->db->tableExists( 'icl_string_translations' ) ) {
			return;
		}

		$strings = [
			'[aioseop_options]aiosp_home_title'       => '[aioseo_options_localized]searchAppearance_global_siteTitle',
			'[aioseop_options]aiosp_home_description' => '[aioseo_options_localized]searchAppearance_global_metaDescription',
			'[aioseop_options]aiosp_home_keywords'    => '[aioseo_options_localized]searchAppearance_global_keywords'
		];

		try {
			$v3Results = aioseo()->core->db->start( 'icl_strings' )
				->where( 'context', 'admin_texts_aioseop_options' )
				->whereIn( 'name', array_keys( $strings ) )
				->run()
				->result();

			$v4Results = aioseo()->core->db->start( 'icl_strings' )
				->where( 'context', 'admin_texts_aioseo_options_localized' )
				->whereIn( 'name', array_values( $strings ) )
				->run()
				->result();

			if ( ! empty( $v3Results ) ) {
				foreach ( $v3Results as $result ) {
					$translations = aioseo()->core->db->start( 'icl_string_translations' )
						->where( 'string_id', $result->id )
						->run()
						->result();

					if ( empty( $translations ) ) {
						continue;
					}

					$v4ResultId = null;
					if ( ! empty( $v4Results ) ) {
						foreach ( $v4Results as $r ) {
							if ( $r->name === $strings[ $result->name ] ) {
								$v4ResultId = $r->id;
								break;
							}
						}
					}

					if ( ! $v4ResultId ) {
						$v4ResultId = aioseo()->core->db
							->insert( 'icl_strings' )
							->set( [
								'language'                => $result->language,
								'context'                 => 'admin_texts_aioseo_options_localized',
								'name'                    => $strings[ $result->name ],
								'value'                   => $result->value,
								'string_package_id'       => $result->string_package_id,
								'location'                => $result->location,
								'wrap_tag'                => $result->wrap_tag,
								'type'                    => $result->type,
								'title'                   => $result->title,
								'status'                  => $result->status,
								'gettext_context'         => $result->gettext_context,
								'domain_name_context_md5' => md5( 'admin_texts_aioseo_options_localized' . $strings[ $result->name ] ),
								'translation_priority'    => $result->translation_priority,
								'word_count'              => $result->word_count
							] )
							->run()
							->insertId();
					}

					foreach ( $translations as $translation ) {
						// Check if the translation exists first or we'll get a DB error.
						$v4Translation = aioseo()->core->db->start( 'icl_string_translations' )
							->where( 'string_id', $v4ResultId )
							->where( 'language', $translation->language )
							->run()
							->result();

						if ( ! empty( $v4Translation ) ) {
							aioseo()->core->db->update( 'icl_string_translations' )
								->where( 'string_id', $v4ResultId )
								->where( 'language', $translation->language )
								->set( [
									'value' => $translation->value
								] )
								->run();
							continue;
						}

						aioseo()->core->db
							->insert( 'icl_string_translations' )
							->set( [
								'string_id'           => $v4ResultId,
								'language'            => $translation->language,
								'status'              => $translation->status,
								'value'               => $translation->value,
								'mo_string'           => $translation->mo_string,
								'translator_id'       => $translation->translator_id,
								'translation_service' => $translation->translation_service,
								'batch_id'            => $translation->batch_id,
								'translation_date'    => $translation->translation_date
							] )
							->run();
					}
				}
			}
		} catch ( \Exception $e ) {
			// If there are any errors, let's just abort. We dont' want to do anything more.
		}
	}
}Common/Models/CrawlCleanupBlockedArg.php000064400000012252151536241200014227 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

use AIOSEO\Plugin\Common\Models as CommonModels;
/**
 * The Crawl Cleanup Blocked Arg DB Model.
 *
 * @since 4.5.8
 */
class CrawlCleanupBlockedArg extends CommonModels\Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.5.8
	 *
	 * @var string
	 */
	protected $table = 'aioseo_crawl_cleanup_blocked_args';

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @since 4.5.8
	 *
	 * @var array
	 */
	protected $hidden = [ 'id' ];

	/**
	 * Fields that should be numeric values.
	 *
	 * @since 4.5.8
	 *
	 * @var array
	 */
	protected $integerFields = [ 'id', 'hits' ];

	/**
	 * Field to count hits.
	 *
	 * @since 4.5.8
	 *
	 * @var integer
	 */
	protected $hits = 0;

	/**
	 * Field for Regex.
	 *
	 * @since 4.5.8
	 *
	 * @var string
	 */
	public $regex = null;

	/**
	 * Field that contains the hash for key+value
	 *
	 * @since 4.5.8
	 *
	 * @var string
	 */
	public $key_value_hash = null;

	/**
	 * Separator used to merge key and value string.
	 *
	 * @since 4.5.8
	 *
	 * @var string
	 */
	private static $keyValueSeparator = '=';

	/**
	 * Separator used to merge key and value string.
	 *
	 * @since 4.5.8
	 *
	 * @var CrawlCleanupBlockedArg|null
	 */
	private static $regexBlockedArgs = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.5.8
	 *
	 * @param mixed $var This can be the primary key of the resource, or it could be an array of data to manufacture a resource without a database query.
	 */
	public function __construct( $var = null ) {
		parent::__construct( $var );
	}

	/**
	 * Get Blocked row using Key and Value.
	 *
	 * @since 4.5.8
	 *
	 * @param  string                 $key   The key to search.
	 * @param  string                 $value The value to search.
	 * @return CrawlCleanupBlockedArg        The CrawlCleanupBlockedArg object.
	 */
	public static function getByKeyValue( $key, $value ) {
		$keyValue = self::getKeyValueString( $key, $value );

		return aioseo()->core->db
			->start( 'aioseo_crawl_cleanup_blocked_args' )
			->where( 'key_value_hash', sha1( $keyValue ) )
			->run()
			->model( 'AIOSEO\\Plugin\\Common\\Models\\CrawlCleanupBlockedArg' );
	}

	/**
	 * Get Blocked row using Regex Value.
	 *
	 * @since 4.5.8
	 *
	 * @param  string                 $regex The regex value to search.
	 * @return CrawlCleanupBlockedArg        The CrawlCleanupBlockedArg object.
	 */
	public static function getByRegex( $regex ) {
		return aioseo()->core->db
			->start( 'aioseo_crawl_cleanup_blocked_args' )
			->where( 'regex', $regex )
			->run()
			->model( 'AIOSEO\\Plugin\\Common\\Models\\CrawlCleanupBlockedArg' );
	}

	/**
	 * Look for regex match by key and value.
	 *
	 * @since 4.5.8
	 *
	 * @param  string                 $key   The key to search.
	 * @param  string                 $value The value to search.
	 * @return CrawlCleanupBlockedArg        The CrawlCleanupBlockedArg object.
	 */
	public static function matchRegex( $key, $value ) {
		$keyValue = self::getKeyValueString( $key, $value );
		$regexBlockedArgs = self::getRegexBlockedArgs();

		foreach ( $regexBlockedArgs as $regexQueryArg ) {
			$escapedRegex = str_replace( '@', '\@', $regexQueryArg->regex );
			if ( preg_match( "@{$escapedRegex}@", (string) $keyValue ) ) {
				return new CrawlCleanupBlockedArg( $regexQueryArg->id );
			}
		}

		return new CrawlCleanupBlockedArg();
	}

	/**
	 * Get Regex rows.
	 *
	 * @since 4.5.8
	 *
	 * @return CrawlCleanupBlockedArg The CrawlCleanupBlockedArg object.
	 */
	public static function getRegexBlockedArgs() {
		if ( null === self::$regexBlockedArgs ) {
			self::$regexBlockedArgs = aioseo()->core->db
				->start( 'aioseo_crawl_cleanup_blocked_args' )
				->select( 'id, regex' )
				->whereRaw( 'regex IS NOT NULL' )
				->run()
				->result();
		}

		return self::$regexBlockedArgs;
	}

	/**
	 * Transforms data as needed.
	 *
	 * @since 4.5.8
	 *
	 * @param  array $data The data array to transform.
	 * @return array       The transformed data.
	 */
	protected function transform( $data, $set = false ) {
		$data = parent::transform( $data, $set );

		// Create key+value hash.
		if ( ! empty( $data['key'] ) ) {
			$keyValue = self::getKeyValueString( $data['key'], $data['value'] );
			$data['key_value_hash'] = sha1( $keyValue );
		}

		// Case hits number are empty start with 0.
		if ( empty( $data['hits'] ) ) {
			$data['hits'] = 0;
		}

		return $data;
	}

	/**
	 * Increase hits and save.
	 *
	 * @since 4.5.8
	 *
	 */
	public function addHit() {
		if ( $this->id ) {
			$this->hits++;
			parent::save();
		}
	}

	/**
	 * Return string with key and value with pattern model defined.
	 *
	 * @since 4.5.8
	 *
	 * @param  string $key   The key to merge.
	 * @param  string $value The value to merge.
	 * @return string        The result string merging key and value (case not empty).
	 */
	public static function getKeyValueString( $key, $value ) {
		return $key . ( $value ? self::getKeyValueSeparator() . $value : '' );
	}

	/**
	 * Return string to separate key and value.
	 *
	 * @since 4.5.8
	 *
	 * @return string The separator for key and value.
	 */
	public static function getKeyValueSeparator() {
		return self::$keyValueSeparator;
	}
}Common/Models/CrawlCleanupLog.php000064400000003434151536241200012755 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

use AIOSEO\Plugin\Common\Models as CommonModels;

/**
 * The Crawl Cleanup Log DB Model.
 *
 * @since 4.5.8
 */
class CrawlCleanupLog extends CommonModels\Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.5.8
	 *
	 * @var string
	 */
	protected $table = 'aioseo_crawl_cleanup_logs';

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @since 4.5.8
	 *
	 * @var array
	 */
	protected $hidden = [ 'id' ];

	/**
	 * Fields that should be numeric values.
	 *
	 * @since 4.5.8
	 *
	 * @var array
	 */
	protected $integerFields = [ 'id', 'hits' ];

	/**
	 * Field to count hits.
	 *
	 * @since 4.5.8
	 *
	 * @var integer
	 */
	public $hits = 0;


	/**
	 * Create a Log in case it doesn't exist.
	 *
	 * @since 4.5.8
	 *
	 * @return void
	 */
	public function create() {
		if ( null !== $this->id ) {
			$this->hits++;
		}

		parent::save();
	}

	/**
	 * Get Crawl Cleanup passing Slug
	 *
	 * @since 4.5.8
	 *
	 * @param  string          $slug The Slug to search.
	 * @return CrawlCleanupLog       The CrawlCleanupLog object.
	 */
	public static function getBySlug( $slug ) {
		return aioseo()->core->db
			->start( 'aioseo_crawl_cleanup_logs' )
			->where( 'hash', sha1( $slug ) )
			->run()
			->model( 'AIOSEO\\Plugin\\Common\\Models\\CrawlCleanupLog' );
	}

	/**
	 * Transforms data as needed.
	 *
	 * @since 4.5.8
	 *
	 * @param  array $data The data array to transform.
	 * @return array       The transformed data.
	 */
	protected function transform( $data, $set = false ) {
		$data = parent::transform( $data, $set );

		// Create slug hash.
		if ( ! empty( $data['slug'] ) ) {
			$data['hash'] = sha1( $data['slug'] );
		}

		return $data;
	}
}Common/Models/Model.php000064400000025156151536241200011000 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

/**
 * Base Model class.
 *
 * @since 4.0.0
 */
#[\AllowDynamicProperties]
class Model implements \JsonSerializable {
	/**
	 * Fields that can be null when saving to the database.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $nullFields = [];

	/**
	 * Fields that should be encoded/decoded on save/get.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $jsonFields = [];

	/**
	 * Fields that should be boolean values.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $booleanFields = [];

	/**
	 * Fields that should be integer values.
	 *
	 * @since   4.1.0
	 * @version 4.7.3 Renamed from numericFields to integerFields.
	 *
	 * @var array
	 */
	protected $integerFields = [];

	/**
	 * Fields that should be float values.
	 *
	 * @since 4.7.3
	 *
	 * @var array
	 */
	protected $floatFields = [];

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $hidden = [];

	/**
	 * The table used in the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $table = '';

	/**
	 * The primary key retrieved from the table.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $pk = 'id';

	/**
	 * The ID of the model.
	 * This needs to be null in order for MySQL to auto-increment correctly if the NO_AUTO_VALUE_ON_ZERO SQL mode is enabled.
	 *
	 * @since 4.2.7
	 *
	 * @var int|null
	 */
	public $id = null;

	/**
	 * An array of columns from the DB that we can use.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private static $columns = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 *
	 * @param mixed $var This can be the primary key of the resource, or it could be an array of data to manufacture a resource without a database query.
	 */
	public function __construct( $var = null ) {
		$skip = [ 'id', 'created', 'updated' ];
		$fields = [];
		foreach ( $this->getColumns() as $column => $value ) {
			if ( ! in_array( $column, $skip, true ) ) {
				$fields[ $column ] = $value;
			}
		}

		$this->applyKeys( $fields );

		// Process straight through if we were given a valid array.
		if ( is_array( $var ) || is_object( $var ) ) {
			// Apply keys to object.
			$this->applyKeys( $var );

			if ( $this->exists() ) {
				return true;
			}

			return false;
		}

		return $this->loadData( $var );
	}

	/**
	 * Load the data from the database!
	 *
	 * @since 4.0.0
	 *
	 * @param  mixed      $var Generally the primary key to load up the model from the DB.
	 * @return Model|bool      Returns the current object.
	 */
	protected function loadData( $var = null ) {
		// Return false if var is invalid or not supplied.
		if ( null === $var ) {
			return false;
		}

		$query = aioseo()->core->db
			->start( $this->table )
			->where( $this->pk, $var )
			->limit( 1 )
			->output( 'ARRAY_A' );

		$result = $query->run();
		if ( ! $result || $result->nullSet() ) {
			return $this;
		}

		// Apply keys to object.
		$this->applyKeys( $result->result()[0] );

		return $this;
	}

	/**
	 * Take the keys from the result array and add them to the Model.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $array The array of keys and values to add to the Model.
	 * @return void
	 */
	protected function applyKeys( $array ) {
		if ( ! is_object( $array ) && ! is_array( $array ) ) {
			throw new \Exception( '$array must either be an object or an array.' );
		}

		foreach ( (array) $array as $key => $value ) {
			$key = trim( $key );
			$this->$key = $value;

			if ( null === $value && in_array( $key, $this->nullFields, true ) ) {
				continue;
			}

			if ( in_array( $key, $this->jsonFields, true ) ) {
				if ( $value ) {
					$this->$key = is_string( $value ) ? json_decode( $value ) : $value;
				}
				continue;
			}

			if ( in_array( $key, $this->booleanFields, true ) ) {
				$this->$key = (bool) $value;
				continue;
			}

			if ( in_array( $key, $this->integerFields, true ) ) {
				$this->$key = (int) $value;
				continue;
			}

			if ( in_array( $key, $this->floatFields, true ) ) {
				$this->$key = (float) $value;
				continue;
			}
		}
	}

	/**
	 * Let's filter out any properties we cannot save to the database.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $keys The array of keys to filter.
	 * @return array       The array of valid columns for the database query.
	 */
	protected function filter( $keys ) {
		$fields    = [];
		$skip      = [ 'created', 'updated' ];
		$dbColumns = aioseo()->db->getColumns( $this->table );

		foreach ( $dbColumns as $column ) {
			if ( ! in_array( $column, $skip, true ) && array_key_exists( $column, $keys ) ) {
				$fields[ $column ] = $keys[ $column ];
			}
		}

		return $fields;
	}

	/**
	 * Transforms the data to be null if it exists in the nullFields variables.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $data The data array to transform.
	 * @return array       The transformed data.
	 */
	protected function transform( $data, $set = false ) {
		foreach ( $this->nullFields as $field ) {
			if ( isset( $data[ $field ] ) && empty( $data[ $field ] ) ) {
				// Because sitemap prio can both be 0 and null, we need to make sure it's 0 if it's set.
				if ( 'priority' === $field && 0.0 === $data[ $field ] ) {
					continue;
				}

				$data[ $field ] = null;
			}
		}

		foreach ( $this->booleanFields as $field ) {
			if ( isset( $data[ $field ] ) && '' === $data[ $field ] ) {
				unset( $data[ $field ] );
			} elseif ( isset( $data[ $field ] ) ) {
				$data[ $field ] = (bool) $data[ $field ] ? 1 : 0;
			}
		}

		if ( $set ) {
			return $data;
		}

		foreach ( $this->integerFields as $field ) {
			if ( isset( $data[ $field ] ) ) {
				$data[ $field ] = (int) $data[ $field ];
			}
		}

		foreach ( $this->jsonFields as $field ) {
			if ( isset( $data[ $field ] ) && ! aioseo()->helpers->isJsonString( $data[ $field ] ) ) {
				if ( is_array( $data[ $field ] ) && aioseo()->helpers->isArrayNumeric( $data[ $field ] ) ) {
					$data[ $field ] = array_values( $data[ $field ] );
				}
				$data[ $field ] = wp_json_encode( $data[ $field ] );
			}
		}

		return $data;
	}

	/**
	 * Sets a piece of data to the requested resource.
	 *
	 * @since 4.0.0
	 */
	public function set() {
		$args  = func_get_args();
		$count = func_num_args();

		if ( ! is_array( $args[0] ) && $count < 2 ) {
			throw new \Exception( 'The set method must contain at least 2 arguments: key and value. Or an array of data. Only one argument was passed and it was not an array.' );
		}

		$key   = $args[0];
		$value = ! empty( $args[1] ) ? $args[1] : null;

		// Make sure we have a key.
		if ( false === $key ) {
			return false;
		}

		// If it's not an array, make it one.
		if ( ! is_array( $key ) ) {
			$key = [ $key => $value ];
		}

		// Preprocess data.
		$key = $this->transform( $key, true );

		// Save the items in this object.
		foreach ( $key as $k => $v ) {
			if ( ! empty( $k ) ) {
				$this->$k = $v;
			}
		}
	}

	/**
	 * Delete the Model Resource itself.
	 *
	 * @since 4.0.0
	 *
	 * @return null
	 */
	public function delete() {
		if ( ! $this->exists() ) {
			return;
		}

		aioseo()->core->db
			->delete( $this->table )
			->where( $this->pk, $this->id )
			->run();

		return null;
	}

	/**
	 * Saves data to the requested resource.
	 *
	 * @since 4.0.0
	 */
	public function save() {
		$fields = $this->transform( $this->filter( (array) get_object_vars( $this ) ) );

		$id = null;
		if ( count( $fields ) > 0 ) {
			$pk = $this->pk;

			if ( isset( $this->$pk ) && '' !== $this->$pk ) {
				// PK specified.
				$pkv   = $this->$pk;
				$query = aioseo()->core->db
					->start( $this->table )
					->where( [ $pk => $pkv ] )
					->resetCache()
					->run();

				if ( ! $query->nullSet() ) {
					// Row exists in database.
					$fields['updated'] = gmdate( 'Y-m-d H:i:s' );
					aioseo()->core->db
						->update( $this->table )
						->set( $fields )
						->where( [ $pk => $pkv ] )
						->run();
					$id = $this->$pk;
				} else {
					// Row does not exist.
					$fields[ $pk ]     = $pkv;
					$fields['created'] = gmdate( 'Y-m-d H:i:s' );
					$fields['updated'] = gmdate( 'Y-m-d H:i:s' );

					$id = aioseo()->core->db
						->insert( $this->table )
						->set( $fields )
						->run()
						->insertId();

					if ( $id ) {
						$this->$pk = $id;
					}
				}
			} else {
				$fields['created'] = gmdate( 'Y-m-d H:i:s' );
				$fields['updated'] = gmdate( 'Y-m-d H:i:s' );

				$id = aioseo()->core->db
					->insert( $this->table )
					->set( $fields )
					->run()
					->insertId();

				if ( $id ) {
					$this->$pk = $id;
				}
			}
		}

		// Refresh the resource.
		$this->reset( $id );
	}

	/**
	 * Check if the model exists in the database.
	 *
	 * @since 4.0.0
	 *
	 * @return bool If the model exists, true otherwise false.
	 */
	public function exists() {
		return ( ! empty( $this->{$this->pk} ) ) ? true : false;
	}

	/**
	 * Resets a resource by forcing internal updates to be applied.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $id The resource ID.
	 * @return void
	 */
	public function reset( $id = null ) {
		$id = ! empty( $id ) ? $id : $this->{$this->pk};
		$this->__construct( $id );
	}

	/**
	 * Helper function to remove data we don't want serialized.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of data that we are okay with serializing.
	 */
	#[\ReturnTypeWillChange]
	// The attribute above omits a deprecation notice from PHP 8.1 that is thrown because the return type of jsonSerialize() isn't "mixed".
	// Once PHP 7.x is our minimum supported version, this can be removed in favour of overriding the return type in the method signature like this -
	// public function jsonSerialize() : array
	public function jsonSerialize() {
		$array = [];

		foreach ( $this->getColumns() as $column => $value ) {
			if ( in_array( $column, $this->hidden, true ) ) {
				continue;
			}

			$array[ $column ] = isset( $this->$column ) ? $this->$column : null;
		}

		return $array;
	}

	/**
	 * Retrieves the columns for the model.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of columns.
	 */
	public function getColumns() {
		if ( empty( self::$columns[ get_called_class() ] ) ) {
			self::$columns[ get_called_class() ] = [];

			// Let's set the columns that are available by default.
			$table   = aioseo()->core->db->prefix . $this->table;
			$results = aioseo()->core->db->start( $table )
				->output( 'OBJECT' )
				->execute( 'SHOW COLUMNS FROM `' . $table . '`', true )
				->result();

			foreach ( $results as $col ) {
				self::$columns[ get_called_class() ][ $col->Field ] = $col->Default;
			}

			if ( ! empty( $this->appends ) ) {
				foreach ( $this->appends as $append ) {
					self::$columns[ get_called_class() ][ $append ] = null;
				}
			}
		}

		return self::$columns[ get_called_class() ];
	}
}Common/Models/Notification.php000064400000023010151536241200012351 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

/**
 * The Notification DB Model.
 *
 * @since 4.0.0
 */
class Notification extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $table = 'aioseo_notifications';

	/**
	 * An array of fields to set to null if already empty when saving to the database.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $nullFields = [
		'start',
		'end',
		'notification_id',
		'notification_name',
		'button1_label',
		'button1_action',
		'button2_label',
		'button2_action'
	];

	/**
	 * Fields that should be json encoded on save and decoded on get.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $jsonFields = [ 'level' ];

	/**
	 * Fields that should be boolean values.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $booleanFields = [ 'dismissed' ];

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @var array
	 */
	protected $hidden = [ 'id' ];

	/**
	 * An array of fields attached to this resource.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $columns = [
		'id',
		'slug',
		'addon',
		'title',
		'content',
		'type',
		'level',
		'notification_id',
		'notification_name',
		'start',
		'end',
		'button1_label',
		'button1_action',
		'button2_label',
		'button2_action',
		'dismissed',
		'new',
		'created',
		'updated'
	];

	/**
	 * Get the list of notifications.
	 *
	 * @since 4.1.3
	 *
	 * @param  bool  $reset Whether or not to reset the notifications.
	 * @return array        An array of notifications.
	 */
	public static function getNotifications( $reset = true ) {
		static $notifications = null;
		if ( null !== $notifications ) {
			return $notifications;
		}

		$notifications = [
			'active'    => self::getAllActiveNotifications(),
			'new'       => self::getNewNotifications( $reset ),
			'dismissed' => self::getAllDismissedNotifications()
		];

		return $notifications;
	}

	/**
	 * Get an array of active notifications.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of active notifications.
	 */
	public static function getAllActiveNotifications() {
		static $activeNotifications = null;
		if ( null !== $activeNotifications ) {
			return $activeNotifications;
		}

		$staticNotifications = self::getStaticNotifications();
		$notifications       = array_values( json_decode( wp_json_encode( self::getActiveNotifications() ), true ) );

		$activeNotifications = ! empty( $staticNotifications )
			? array_merge( $staticNotifications, $notifications )
			: $notifications;

		return $activeNotifications;
	}

	/**
	 * Get all new notifications. After retrieving them, this will reset them.
	 * This means that calling this method twice will result in no results
	 * the second time. The only exception is to pass false as a reset variable to prevent it.
	 *
	 * @since 4.1.3
	 *
	 * @param  bool  $reset Whether or not to reset the new notifications.
	 * @return array        An array of new notifications if any exist.
	 */
	public static function getNewNotifications( $reset = true ) {
		static $newNotifications = null;
		if ( null !== $newNotifications ) {
			return $newNotifications;
		}

		$newNotifications = self::filterNotifications(
			aioseo()->core->db
				->start( 'aioseo_notifications' )
				->where( 'dismissed', 0 )
				->where( 'new', 1 )
				->whereRaw( "(start <= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR start IS NULL)" )
				->whereRaw( "(end >= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR end IS NULL)" )
				->orderBy( 'start DESC' )
				->orderBy( 'created DESC' )
				->run()
				->models( 'AIOSEO\\Plugin\\Common\\Models\\Notification' )
		);

		if ( $reset ) {
			self::resetNewNotifications();
		}

		return $newNotifications;
	}

	/**
	 * Resets all new notifications.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	public static function resetNewNotifications() {
		aioseo()->core->db
			->update( 'aioseo_notifications' )
			->where( 'new', 1 )
			->set( 'new', 0 )
			->run();
	}

	/**
	 * Returns all static notifications.
	 *
	 * @since 4.1.2
	 *
	 * @return array An array of static notifications.
	 */
	public static function getStaticNotifications() {
		$staticNotifications = [];
		$notifications       = [
			'unlicensed-addons',
			'review'
		];

		foreach ( $notifications as $notification ) {
			switch ( $notification ) {
				case 'review':
					// If they intentionally dismissed the main notification, we don't show the repeat one.
					$originalDismissed = get_user_meta( get_current_user_id(), '_aioseo_plugin_review_dismissed', true );
					if ( '4' !== $originalDismissed ) {
						break;
					}

					$dismissed = get_user_meta( get_current_user_id(), '_aioseo_notification_plugin_review_dismissed', true );
					if ( '3' === $dismissed ) {
						break;
					}

					if ( ! empty( $dismissed ) && $dismissed > time() ) {
						break;
					}

					$activated = aioseo()->internalOptions->internal->firstActivated( time() );
					if ( $activated > strtotime( '-20 days' ) ) {
						break;
					}

					$isV3                  = get_option( 'aioseop_options' ) || get_option( 'aioseo_options_v3' );
					$staticNotifications[] = [
						'slug'      => 'notification-' . $notification,
						'component' => 'notifications-' . $notification . ( $isV3 ? '' : '2' )
					];
					break;
				case 'unlicensed-addons':
					$unlicensedAddons = aioseo()->addons->unlicensedAddons();
					if ( empty( $unlicensedAddons['addons'] ) ) {
						break;
					}

					$staticNotifications[] = [
						'slug'      => 'notification-' . $notification,
						'component' => 'notifications-' . $notification,
						'addons'    => $unlicensedAddons['addons'],
						'message'   => $unlicensedAddons['message']
					];
					break;
			}
		}

		return $staticNotifications;
	}

	/**
	 * Retrieve active notifications.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of active notifications or empty.
	 */
	protected static function getActiveNotifications() {
		return self::filterNotifications(
			aioseo()->core->db
				->start( 'aioseo_notifications' )
				->where( 'dismissed', 0 )
				->whereRaw( "(start <= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR start IS NULL)" )
				->whereRaw( "(end >= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR end IS NULL)" )
				->orderBy( 'start DESC' )
				->orderBy( 'created DESC' )
				->run()
				->models( 'AIOSEO\\Plugin\\Common\\Models\\Notification' )
		);
	}

	/**
	 * Get an array of dismissed notifications.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of dismissed notifications.
	 */
	protected static function getAllDismissedNotifications() {
		return array_values( json_decode( wp_json_encode( self::getDismissedNotifications() ), true ) );
	}

	/**
	 * Retrieve dismissed notifications.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of dismissed notifications or empty.
	 */
	protected static function getDismissedNotifications() {
		static $dismissedNotifications = null;
		if ( null !== $dismissedNotifications ) {
			return $dismissedNotifications;
		}

		$dismissedNotifications = self::filterNotifications(
			aioseo()->core->db
				->start( 'aioseo_notifications' )
				->where( 'dismissed', 1 )
				->orderBy( 'updated DESC' )
				->run()
				->models( 'AIOSEO\\Plugin\\Common\\Models\\Notification' )
		);

		return $dismissedNotifications;
	}

	/**
	 * Returns a notification by its name.
	 *
	 * @since 4.0.0
	 *
	 * @param  string       $name The notification name.
	 * @return Notification       The notification.
	 */
	public static function getNotificationByName( $name ) {
		return aioseo()->core->db
			->start( 'aioseo_notifications' )
			->where( 'notification_name', $name )
			->run()
			->model( 'AIOSEO\\Plugin\\Common\\Models\\Notification' );
	}

	/**
	 * Stores a new notification in the DB.
	 *
	 * @since 4.0.0
	 *
	 * @param  array        $fields       The fields.
	 * @return Notification $notification The notification.
	 */
	public static function addNotification( $fields ) {
		// Set the dismissed status to false.
		$fields['dismissed'] = 0;

		$notification = new self();
		$notification->set( $fields );
		$notification->save();

		return $notification;
	}

	/**
	 * Deletes a notification by its name.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name The notification name.
	 * @return void
	 */
	public static function deleteNotificationByName( $name ) {
		aioseo()->core->db
			->delete( 'aioseo_notifications' )
			->where( 'notification_name', $name )
			->run();
	}

	/**
	 * Filters the notifications based on the targeted plan levels.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $notifications          The notifications
	 * @return array $remainingNotifications The remaining notifications.
	 */
	protected static function filterNotifications( $notifications ) {
		$remainingNotifications = [];
		foreach ( $notifications as $notification ) {
			// If announcements are disabled and this is an announcement, skip adding it and move on.
			if (
				! aioseo()->options->advanced->announcements &&
				'success' === $notification->type
			) {
				continue;
			}

			// If this is an addon notification and the addon is disabled, skip adding it and move on.
			if ( ! empty( $notification->addon ) && ! aioseo()->addons->getLoadedAddon( $notification->addon ) ) {
				continue;
			}

			$levels = $notification->level;
			if ( ! is_array( $levels ) ) {
				$levels = empty( $notification->level ) ? [ 'all' ] : [ $notification->level ];
			}

			foreach ( $levels as $level ) {
				if ( ! aioseo()->notices->validateType( $level ) ) {
					continue 2;
				}
			}

			$remainingNotifications[] = $notification;
		}

		return $remainingNotifications;
	}
}Common/Models/Post.php000064400000076335151536241200010672 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

/**
 * The Post DB Model.
 *
 * @since 4.0.0
 */
class Post extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $table = 'aioseo_posts';

	/**
	 * Fields that should be json encoded on save and decoded on get.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $jsonFields = [
		'keywords',
		'keyphrases',
		'page_analysis',
		'schema',
		'images',
		'videos',
		'ai',
		'options',
		'local_seo',
		'primary_term',
		'breadcrumb_settings',
		'og_article_tags',
		'ai'
	];

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $hidden = [ 'id' ];

	/**
	 * Fields that should be boolean values.
	 *
	 * @since 4.0.13
	 *
	 * @var array
	 */
	protected $booleanFields = [
		'twitter_use_og',
		'pillar_content',
		'robots_default',
		'robots_noindex',
		'robots_noarchive',
		'robots_nosnippet',
		'robots_nofollow',
		'robots_noimageindex',
		'robots_noodp',
		'robots_notranslate',
		'limit_modified_date',
	];

	/**
	 * Fields that can be null when saved.
	 *
	 * @since 4.5.7
	 *
	 * @var array
	 */
	protected $nullFields = [
		'priority'
	];

	/**
	 * Fields that should be float values.
	 *
	 * @since 4.7.3
	 *
	 * @var array
	 */
	protected $floatFields = [
		'priority'
	];

	/**
	 * Returns a Post with a given ID.
	 *
	 * @since 4.0.0
	 *
	 * @param  int  $postId The post ID.
	 * @return Post         The Post object.
	 */
	public static function getPost( $postId ) {
		// This is needed to prevent an error when upgrading from 4.1.8 to 4.1.9.
		// WordPress deletes the attachment .zip file for the new plugin version after installing it, which triggers the "delete_post" hook.
		// In-between the 4.1.8 to 4.1.9 update, the new Core class does not exist yet, causing the PHP error.
		// TODO: Delete this in a future release.
		$post = new self();
		if ( ! property_exists( aioseo(), 'core' ) ) {
			return $post;
		}

		$post = aioseo()->core->db->start( 'aioseo_posts' )
			->where( 'post_id', $postId )
			->run()
			->model( 'AIOSEO\\Plugin\\Common\\Models\\Post' );

		if ( ! $post->exists() ) {
			$post->post_id = $postId;
			$post          = self::setDynamicDefaults( $post, $postId );
		} else {
			$post = self::runDynamicMigrations( $post );
		}

		// Set options object.
		$post = self::setOptionsDefaults( $post );

		return apply_filters( 'aioseo_get_post', $post );
	}

	/**
	 * Sets the dynamic defaults on the post object if it doesn't exist in the DB yet.
	 *
	 * @since 4.1.4
	 *
	 * @param  Post $post   The Post object.
	 * @param  int  $postId The post ID.
	 * @return Post         The modified Post object.
	 */
	private static function setDynamicDefaults( $post, $postId ) {
		if ( 'page' === get_post_type( $postId ) ) { // This check cannot be deleted and is required to prevent errors after WordPress cleans up the attachment it creates when a plugin is updated.
			$isWooCommerceCheckoutPage = aioseo()->helpers->isWooCommerceCheckoutPage( $postId );
			if (
				$isWooCommerceCheckoutPage ||
				aioseo()->helpers->isWooCommerceCartPage( $postId ) ||
				aioseo()->helpers->isWooCommerceAccountPage( $postId )
			) {
				$post->robots_default = false;
				$post->robots_noindex = true;
			}
		}

		if ( aioseo()->helpers->isStaticHomePage( $postId ) ) {
			$post->og_object_type = 'website';
		}

		$post->twitter_use_og = aioseo()->options->social->twitter->general->useOgData;

		if ( property_exists( $post, 'schema' ) && null === $post->schema ) {
			$post->schema = self::getDefaultSchemaOptions();
		}

		return $post;
	}

	/**
	 * Migrates removed QAPage schema on-the-fly when the post is loaded.
	 *
	 * @since 4.1.8
	 *
	 * @param  Post $aioseoPost The post object.
	 * @return Post             The modified post object.
	 */
	private static function migrateRemovedQaSchema( $aioseoPost ) {
		if ( ! $aioseoPost->schema_type || 'webpage' !== strtolower( $aioseoPost->schema_type ) ) {
			return $aioseoPost;
		}

		$schemaTypeOptions = json_decode( $aioseoPost->schema_type_options );
		if ( 'qapage' !== strtolower( $schemaTypeOptions->webPage->webPageType ) ) {
			return $aioseoPost;
		}

		$schemaTypeOptions->webPage->webPageType = 'WebPage';
		$aioseoPost->schema_type_options         = wp_json_encode( $schemaTypeOptions );
		$aioseoPost->save();

		return $aioseoPost;
	}

	/**
	 * Runs dynamic migrations whenever the post object is loaded.
	 *
	 * @since 4.1.7
	 *
	 * @param  Post $post The Post object.
	 * @return Post       The modified Post object.
	 */
	private static function runDynamicMigrations( $post ) {
		$post = self::migrateRemovedQaSchema( $post );
		$post = self::migrateImageTypes( $post );
		$post = self::runDynamicSchemaMigration( $post );
		$post = self::migrateKoreaCountryCodeSchemas( $post );

		return $post;
	}


	/**
	 * Migrates the post's schema data when it is loaded.
	 *
	 * @since 4.2.5
	 *
	 * @param  Post $post The Post object.
	 * @return Post       The modified Post object.
	 */
	private static function runDynamicSchemaMigration( $post ) {
		if ( ! property_exists( $post, 'schema' ) ) {
			return $post;
		}

		if ( null === $post->schema ) {
			$post = aioseo()->updates->migratePostSchemaHelper( $post );
		}

		// If the schema prop isn't set yet, we want to set it here.
		// We also want to run this regardless of whether it is already set to make sure the default schema graph
		// is correctly propagated on the frontend after changing it.
		$post->schema = self::getDefaultSchemaOptions( $post->schema );

		// Filter out null or empty graphs.
		$post->schema->graphs = array_filter( $post->schema->graphs, function( $graph ) {
			return ! empty( $graph );
		} );

		foreach ( $post->schema->graphs as $graph ) {
			// If the first character of the graph ID isn't a pound, add one.
			// We have to do this because the schema migration in 4.2.5 didn't add the pound for custom graphs.
			if ( property_exists( $graph, 'id' ) && '#' !== substr( $graph->id, 0, 1 ) ) {
				$graph->id = '#' . $graph->id;
			}

			// If the graph has an old rating value, we need to migrate it to the review.
			if (
				property_exists( $graph, 'id' ) &&
				preg_match( '/(movie|software-application)/', (string) $graph->id ) &&
				property_exists( $graph->properties, 'rating' ) &&
				property_exists( $graph->properties->rating, 'value' )
			) {
				$graph->properties->review->rating = $graph->properties->rating->value;
				unset( $graph->properties->rating->value );
			}

			// If the graph has audience data, we need to migrate it to the correct one.
			if (
				property_exists( $graph, 'id' ) &&
				preg_match( '/(product|product-review)/', $graph->id ) &&
				property_exists( $graph->properties, 'audience' )
			) {
				$graph->properties->audience = self::migratePostAudienceAgeSchema( $graph->properties->audience );
			}
		}

		return $post;
	}

	/**
	 * Migrates the post's image types when it is loaded.
	 *
	 * @since 4.2.5
	 *
	 * @param  Post $post The Post object.
	 * @return Post       The modified Post object.
	 */
	private static function migrateImageTypes( $post ) {
		$pageBuilder = aioseo()->helpers->getPostPageBuilderName( $post->post_id );
		if ( ! $pageBuilder ) {
			return $post;
		}

		$deprecatedImageSources = 'seedprod' === strtolower( $pageBuilder )
			? [ 'auto', 'custom', 'featured' ]
			: [ 'auto' ];

		if ( ! empty( $post->og_image_type ) && in_array( $post->og_image_type, $deprecatedImageSources, true ) ) {
			$post->og_image_type = 'default';
		}

		if ( ! empty( $post->twitter_image_type ) && in_array( $post->twitter_image_type, $deprecatedImageSources, true ) ) {
			$post->twitter_image_type = 'default';
		}

		return $post;
	}

	/**
	 * Saves the Post object.
	 *
	 * @since 4.0.3
	 *
	 * @param  int              $postId The Post ID.
	 * @param  array            $data   The post data to save.
	 * @return bool|void|string         Whether the post data was saved or a DB error message.
	 */
	public static function savePost( $postId, $data ) {
		if ( empty( $data ) ) {
			return false;
		}

		$thePost = self::getPost( $postId );
		$data    = apply_filters( 'aioseo_save_post', $data, $thePost );

		// Before setting the data, we check if the title/description are the same as the defaults and clear them if so.
		$data    = self::checkForDefaultFormat( $postId, $thePost, $data );
		$thePost = self::sanitizeAndSetDefaults( $postId, $thePost, $data );

		// Update traditional post meta so that it can be used by multilingual plugins.
		self::updatePostMeta( $postId, $data );

		$thePost->save();
		$thePost->reset();

		$lastError = aioseo()->core->db->lastError();
		if ( ! empty( $lastError ) ) {
			return $lastError;
		}

		// Fires once an AIOSEO post has been saved.
		do_action( 'aioseo_insert_post', $postId );
	}

	/**
	 * Checks if the title/description is the same as their default format in Search Appearance and nulls it if this is the case.
	 * Doing this ensures that updates to the default title/description format also propogate to the post.
	 *
	 * @since 4.1.5
	 *
	 * @param  int   $postId  The post ID.
	 * @param  Post  $thePost The Post object.
	 * @param  array $data    The data.
	 * @return array          The data.
	 */
	private static function checkForDefaultFormat( $postId, $thePost, $data ) {
		$data['title']       = trim( (string) $data['title'] );
		$data['description'] = trim( (string) $data['description'] );

		$post                     = aioseo()->helpers->getPost( $postId );
		$defaultTitleFormat       = trim( aioseo()->meta->title->getPostTypeTitle( $post->post_type ) );
		$defaultDescriptionFormat = trim( aioseo()->meta->description->getPostTypeDescription( $post->post_type ) );
		if ( ! empty( $data['title'] ) && $data['title'] === $defaultTitleFormat ) {
			$data['title'] = null;
		}

		if ( ! empty( $data['description'] ) && $data['description'] === $defaultDescriptionFormat ) {
			$data['description'] = null;
		}

		return $data;
	}

	/**
	 * Sanitize the keyphrases posted data.
	 *
	 * @since 4.2.8
	 *
	 * @param  array $data An array containing the keyphrases field data.
	 * @return array       The sanitized data.
	 */
	private static function sanitizeKeyphrases( $data ) {
		if (
			! empty( $data['focus']['analysis'] ) &&
			is_array( $data['focus']['analysis'] )
		) {
			foreach ( $data['focus']['analysis'] as &$analysis ) {
				// Remove unnecessary 'title' and 'description'.
				unset( $analysis['title'] );
				unset( $analysis['description'] );
			}
		}

		if (
			! empty( $data['additional'] ) &&
			is_array( $data['additional'] )
		) {
			foreach ( $data['additional'] as &$additional ) {
				if (
					! empty( $additional['analysis'] ) &&
					is_array( $additional['analysis'] )
				) {
					foreach ( $additional['analysis'] as &$additionalAnalysis ) {
						// Remove unnecessary 'title' and 'description'.
						unset( $additionalAnalysis['title'] );
						unset( $additionalAnalysis['description'] );
					}
				}
			}
		}

		return $data;
	}

	/**
	 * Sanitize the page_analysis posted data.
	 *
	 * @since 4.2.7
	 *
	 * @param  array $data An array containing the page_analysis field data.
	 * @return array       The sanitized data.
	 */
	private static function sanitizePageAnalysis( $data ) {
		if (
			empty( $data['analysis'] ) ||
			! is_array( $data['analysis'] )
		) {
			return $data;
		}

		foreach ( $data['analysis'] as &$analysis ) {
			foreach ( $analysis as $key => $result ) {
				// Remove unnecessary data.
				foreach ( [ 'title', 'description', 'highlightSentences' ] as $keyToRemove ) {
					if ( isset( $analysis[ $key ][ $keyToRemove ] ) ) {
						unset( $analysis[ $key ][ $keyToRemove ] );
					}
				}
			}
		}

		return $data;
	}

	/**
	 * Sanitizes the post data and sets it (or the default value) to the Post object.
	 *
	 * @since 4.1.5
	 *
	 * @param  int   $postId  The post ID.
	 * @param  Post  $thePost The Post object.
	 * @param  array $data    The data.
	 * @return Post           The Post object with data set.
	 */
	protected static function sanitizeAndSetDefaults( $postId, $thePost, $data ) {
		// General
		$thePost->post_id                     = $postId;
		$thePost->title                       = ! empty( $data['title'] ) ? sanitize_text_field( $data['title'] ) : null;
		$thePost->description                 = ! empty( $data['description'] ) ? sanitize_text_field( $data['description'] ) : null;
		$thePost->canonical_url               = ! empty( $data['canonicalUrl'] ) ? sanitize_text_field( $data['canonicalUrl'] ) : null;
		$thePost->keywords                    = ! empty( $data['keywords'] ) ? aioseo()->helpers->sanitize( $data['keywords'] ) : null;
		$thePost->pillar_content              = isset( $data['pillar_content'] ) ? rest_sanitize_boolean( $data['pillar_content'] ) : 0;
		// TruSEO
		$thePost->keyphrases                  = ! empty( $data['keyphrases'] ) ? self::sanitizeKeyphrases( $data['keyphrases'] ) : null;
		$thePost->page_analysis               = ! empty( $data['page_analysis'] ) ? self::sanitizePageAnalysis( $data['page_analysis'] ) : null;
		$thePost->seo_score                   = ! empty( $data['seo_score'] ) ? sanitize_text_field( $data['seo_score'] ) : 0;
		// Sitemap
		$thePost->priority                    = isset( $data['priority'] ) ? ( 'default' === sanitize_text_field( $data['priority'] ) ? null : (float) $data['priority'] ) : null;
		$thePost->frequency                   = ! empty( $data['frequency'] ) ? sanitize_text_field( $data['frequency'] ) : 'default';
		// Robots Meta
		$thePost->robots_default              = isset( $data['default'] ) ? rest_sanitize_boolean( $data['default'] ) : 1;
		$thePost->robots_noindex              = isset( $data['noindex'] ) ? rest_sanitize_boolean( $data['noindex'] ) : 0;
		$thePost->robots_nofollow             = isset( $data['nofollow'] ) ? rest_sanitize_boolean( $data['nofollow'] ) : 0;
		$thePost->robots_noarchive            = isset( $data['noarchive'] ) ? rest_sanitize_boolean( $data['noarchive'] ) : 0;
		$thePost->robots_notranslate          = isset( $data['notranslate'] ) ? rest_sanitize_boolean( $data['notranslate'] ) : 0;
		$thePost->robots_noimageindex         = isset( $data['noimageindex'] ) ? rest_sanitize_boolean( $data['noimageindex'] ) : 0;
		$thePost->robots_nosnippet            = isset( $data['nosnippet'] ) ? rest_sanitize_boolean( $data['nosnippet'] ) : 0;
		$thePost->robots_noodp                = isset( $data['noodp'] ) ? rest_sanitize_boolean( $data['noodp'] ) : 0;
		$thePost->robots_max_snippet          = isset( $data['maxSnippet'] ) && is_numeric( $data['maxSnippet'] ) ? (int) sanitize_text_field( $data['maxSnippet'] ) : -1;
		$thePost->robots_max_videopreview     = isset( $data['maxVideoPreview'] ) && is_numeric( $data['maxVideoPreview'] ) ? (int) sanitize_text_field( $data['maxVideoPreview'] ) : -1;
		$thePost->robots_max_imagepreview     = ! empty( $data['maxImagePreview'] ) ? sanitize_text_field( $data['maxImagePreview'] ) : 'large';
		// Open Graph Meta
		$thePost->og_title                    = ! empty( $data['og_title'] ) ? sanitize_text_field( $data['og_title'] ) : null;
		$thePost->og_description              = ! empty( $data['og_description'] ) ? sanitize_text_field( $data['og_description'] ) : null;
		$thePost->og_object_type              = ! empty( $data['og_object_type'] ) ? sanitize_text_field( $data['og_object_type'] ) : 'default';
		$thePost->og_image_type               = ! empty( $data['og_image_type'] ) ? sanitize_text_field( $data['og_image_type'] ) : 'default';
		$thePost->og_image_url                = null; // We'll reset this below.
		$thePost->og_image_width              = null; // We'll reset this below.
		$thePost->og_image_height             = null; // We'll reset this below.
		$thePost->og_image_custom_url         = ! empty( $data['og_image_custom_url'] ) ? esc_url_raw( $data['og_image_custom_url'] ) : null;
		$thePost->og_image_custom_fields      = ! empty( $data['og_image_custom_fields'] ) ? sanitize_text_field( $data['og_image_custom_fields'] ) : null;
		$thePost->og_video                    = ! empty( $data['og_video'] ) ? sanitize_text_field( $data['og_video'] ) : '';
		$thePost->og_article_section          = ! empty( $data['og_article_section'] ) ? sanitize_text_field( $data['og_article_section'] ) : null;
		$thePost->og_article_tags             = ! empty( $data['og_article_tags'] ) ? aioseo()->helpers->sanitize( $data['og_article_tags'] ) : null;
		// Twitter Meta
		$thePost->twitter_title               = ! empty( $data['twitter_title'] ) ? sanitize_text_field( $data['twitter_title'] ) : null;
		$thePost->twitter_description         = ! empty( $data['twitter_description'] ) ? sanitize_text_field( $data['twitter_description'] ) : null;
		$thePost->twitter_use_og              = isset( $data['twitter_use_og'] ) ? rest_sanitize_boolean( $data['twitter_use_og'] ) : 0;
		$thePost->twitter_card                = ! empty( $data['twitter_card'] ) ? sanitize_text_field( $data['twitter_card'] ) : 'default';
		$thePost->twitter_image_type          = ! empty( $data['twitter_image_type'] ) ? sanitize_text_field( $data['twitter_image_type'] ) : 'default';
		$thePost->twitter_image_url           = null; // We'll reset this below.
		$thePost->twitter_image_custom_url    = ! empty( $data['twitter_image_custom_url'] ) ? esc_url_raw( $data['twitter_image_custom_url'] ) : null;
		$thePost->twitter_image_custom_fields = ! empty( $data['twitter_image_custom_fields'] ) ? sanitize_text_field( $data['twitter_image_custom_fields'] ) : null;
		// Schema
		$thePost->schema                      = ! empty( $data['schema'] ) ? self::getDefaultSchemaOptions( $data['schema'] ) : null;
		$thePost->local_seo                   = ! empty( $data['local_seo'] ) ? $data['local_seo'] : null;
		$thePost->limit_modified_date         = isset( $data['limit_modified_date'] ) ? rest_sanitize_boolean( $data['limit_modified_date'] ) : 0;
		$thePost->ai                          = ! empty( $data['ai'] ) ? self::getDefaultAiOptions( $data['ai'] ) : null;
		$thePost->updated                     = gmdate( 'Y-m-d H:i:s' );
		$thePost->primary_term                = ! empty( $data['primary_term'] ) ? $data['primary_term'] : null;
		$thePost->breadcrumb_settings         = isset( $data['breadcrumb_settings']['default'] ) && false === $data['breadcrumb_settings']['default'] ? $data['breadcrumb_settings'] : null;

		// Before we determine the OG/Twitter image, we need to set the meta data cache manually because the changes haven't been saved yet.
		aioseo()->meta->metaData->bustPostCache( $thePost->post_id, $thePost );

		// Set the OG/Twitter image data.
		$thePost = self::setOgTwitterImageData( $thePost );

		if ( ! $thePost->exists() ) {
			$thePost->created = gmdate( 'Y-m-d H:i:s' );
		}

		// Update defaults from addons.
		foreach ( aioseo()->addons->getLoadedAddons() as $addon ) {
			if ( isset( $addon->postModel ) && method_exists( $addon->postModel, 'sanitizeAndSetDefaults' ) ) {
				$thePost = $addon->postModel->sanitizeAndSetDefaults( $postId, $thePost, $data );
			}
		}

		return $thePost;
	}

	/**
	 * Set the OG/Twitter image data on the post object.
	 *
	 * @since 4.1.6
	 *
	 * @param  Post $thePost The Post object to modify.
	 * @return Post          The modified Post object.
	 */
	public static function setOgTwitterImageData( $thePost ) {
		// Set the OG image.
		if (
			in_array( $thePost->og_image_type, [
				'featured',
				'content',
				'attach',
				'custom',
				'custom_image'
			], true )
		) {
			// Disable the cache.
			aioseo()->social->image->useCache = false;

			// Set the image details.
			$ogImage                  = aioseo()->social->facebook->getImage( $thePost->post_id );
			$thePost->og_image_url    = is_array( $ogImage ) ? $ogImage[0] : $ogImage;
			$thePost->og_image_width  = aioseo()->social->facebook->getImageWidth();
			$thePost->og_image_height = aioseo()->social->facebook->getImageHeight();

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

		// Set the Twitter image.
		if (
			! $thePost->twitter_use_og &&
			in_array( $thePost->twitter_image_type, [
				'featured',
				'content',
				'attach',
				'custom',
				'custom_image'
			], true )
		) {
			// Disable the cache.
			aioseo()->social->image->useCache = false;

			// Set the image details.
			$ogImage                    = aioseo()->social->twitter->getImage( $thePost->post_id );
			$thePost->twitter_image_url = is_array( $ogImage ) ? $ogImage[0] : $ogImage;

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

		return $thePost;
	}

	/**
	 * Saves some of the data as post meta so that it can be used for localization.
	 *
	 * @since 4.1.5
	 *
	 * @param  int   $postId The post ID.
	 * @param  array $data   The data.
	 * @return void
	 */
	public static function updatePostMeta( $postId, $data ) {
		// Update the post meta as well for localization.
		$keywords      = ! empty( $data['keywords'] ) ? aioseo()->helpers->jsonTagsToCommaSeparatedList( $data['keywords'] ) : [];
		$ogArticleTags = ! empty( $data['og_article_tags'] ) ? aioseo()->helpers->jsonTagsToCommaSeparatedList( $data['og_article_tags'] ) : [];

		update_post_meta( $postId, '_aioseo_title', $data['title'] );
		update_post_meta( $postId, '_aioseo_description', $data['description'] );
		update_post_meta( $postId, '_aioseo_keywords', $keywords );
		update_post_meta( $postId, '_aioseo_og_title', $data['og_title'] );
		update_post_meta( $postId, '_aioseo_og_description', $data['og_description'] );
		update_post_meta( $postId, '_aioseo_og_article_section', $data['og_article_section'] );
		update_post_meta( $postId, '_aioseo_og_article_tags', $ogArticleTags );
		update_post_meta( $postId, '_aioseo_twitter_title', $data['twitter_title'] );
		update_post_meta( $postId, '_aioseo_twitter_description', $data['twitter_description'] );
	}

	/**
	 * Returns the default values for the TruSEO page analysis.
	 *
	 * @since 4.0.0
	 *
	 * @param  object|null $pageAnalysis The page analysis object.
	 * @return object                    The default values.
	 */
	public static function getPageAnalysisDefaults( $pageAnalysis = null ) {
		$defaults = [
			'analysis' => [
				'basic'       => [
					'lengthContent' => [
						'error'    => 1,
						'maxScore' => 9,
						'score'    => 6,
					],
				],
				'title'       => [
					'titleLength' => [
						'error'    => 1,
						'maxScore' => 9,
						'score'    => 1,
					],
				],
				'readability' => [
					'contentHasAssets' => [
						'error'    => 1,
						'maxScore' => 5,
						'score'    => 0,
					],
				]
			]
		];

		if ( empty( $pageAnalysis ) ) {
			return json_decode( wp_json_encode( $defaults ) );
		}

		return $pageAnalysis;
	}

	/**
	 * Returns a JSON object with default schema options.
	 *
	 * @since 4.2.5
	 *
	 * @param  string        $existingOptions The existing options in JSON.
	 * @param  null|\WP_Post $post            The post object.
	 * @return object                         The existing options with defaults added in JSON.
	 */
	public static function getDefaultSchemaOptions( $existingOptions = '', $post = null ) {
		$defaultGraphName = aioseo()->schema->getDefaultPostTypeGraph( $post );

		$defaults = [
			'blockGraphs'  => [],
			'customGraphs' => [],
			'default'      => [
				'data'      => [
					'Article'             => [],
					'Course'              => [],
					'Dataset'             => [],
					'FAQPage'             => [],
					'Movie'               => [],
					'Person'              => [],
					'Product'             => [],
					'ProductReview'       => [],
					'Car'                 => [],
					'Recipe'              => [],
					'Service'             => [],
					'SoftwareApplication' => [],
					'WebPage'             => []
				],
				'graphName' => $defaultGraphName,
				'isEnabled' => true,
			],
			'graphs'       => []
		];

		if ( empty( $existingOptions ) ) {
			return json_decode( wp_json_encode( $defaults ) );
		}

		$existingOptions = json_decode( wp_json_encode( $existingOptions ), true );
		$existingOptions = array_replace_recursive( $defaults, $existingOptions );

		if ( isset( $existingOptions['defaultGraph'] ) && ! empty( $existingOptions['defaultPostTypeGraph'] ) ) {
			$existingOptions['default']['isEnabled'] = ! empty( $existingOptions['defaultGraph'] );

			unset( $existingOptions['defaultGraph'] );
			unset( $existingOptions['defaultPostTypeGraph'] );
		}

		// Reset the default graph type to make sure it's accurate.
		if ( $defaultGraphName ) {
			$existingOptions['default']['graphName'] = $defaultGraphName;
		}

		return json_decode( wp_json_encode( $existingOptions ) );
	}

	/**
	 * Returns the defaults for the keyphrases column.
	 *
	 * @since 4.1.7
	 *
	 * @param  null|object $keyphrases The database keyphrases.
	 * @return object                  The defaults.
	 */
	public static function getKeyphrasesDefaults( $keyphrases = null ) {
		$defaults = [
			'focus'      => [
				'keyphrase' => '',
				'score'     => 0,
				'analysis'  => [
					'keyphraseInTitle' => [
						'score'    => 0,
						'maxScore' => 9,
						'error'    => 1
					]
				]
			],
			'additional' => []
		];

		if ( empty( $keyphrases ) ) {
			return json_decode( wp_json_encode( $defaults ) );
		}

		if ( empty( $keyphrases->focus ) ) {
			$keyphrases->focus = $defaults['focus'];
		}

		if ( empty( $keyphrases->additional ) ) {
			$keyphrases->additional = $defaults['additional'];
		}

		return $keyphrases;
	}

	/**
	 * Returns the defaults for the options column.
	 *
	 * @since   4.2.2
	 * @version 4.2.9
	 *
	 * @param  Post $post   The Post object.
	 * @return Post         The modified Post object.
	 */
	public static function setOptionsDefaults( $post ) {
		$defaults = [
			'linkFormat'  => [
				'internalLinkCount'      => 0,
				'linkAssistantDismissed' => false
			],
			'primaryTerm' => [
				'productEducationDismissed' => false
			]
		];

		if ( empty( $post->options ) ) {
			$post->options = json_decode( wp_json_encode( $defaults ) );

			return $post;
		}

		$post->options = json_decode( wp_json_encode( $post->options ), true );
		$post->options = array_replace_recursive( $defaults, $post->options );
		$post->options = json_decode( wp_json_encode( $post->options ) );

		return $post;
	}

	/**
	 * Returns the default breadcrumb settings options.
	 *
	 * @since 4.8.3
	 *
	 * @param  array  $postType        The post type.
	 * @param  array  $existingOptions The existing options.
	 * @return object                  The default options.
	 */
	public static function getDefaultBreadcrumbSettingsOptions( $postType, $existingOptions = [] ) {
		$default       = aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->useDefaultTemplate ?? true;
		$showHomeCrumb = $default ? aioseo()->options->breadcrumbs->homepageLink : aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->showHomeCrumb ?? true;

		$defaults = [
			'default'            => true,
			'separator'          => aioseo()->options->breadcrumbs->separator,
			'showHomeCrumb'      => $showHomeCrumb ?? true,
			'showTaxonomyCrumbs' => aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->showTaxonomyCrumbs ?? true,
			'showParentCrumbs'   => aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->showParentCrumbs ?? true,
			'template'           => aioseo()->helpers->encodeOutputHtml( aioseo()->breadcrumbs->frontend->getDefaultTemplate( 'single' ) ),
			'parentTemplate'     => aioseo()->helpers->encodeOutputHtml( aioseo()->breadcrumbs->frontend->getDefaultTemplate( 'single' ) ),
			'taxonomy'           => aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->taxonomy ?? '',
			'primaryTerm'        => null
		];

		if ( empty( $existingOptions ) ) {
			return json_decode( wp_json_encode( $defaults ) );
		}

		$existingOptions = json_decode( wp_json_encode( $existingOptions ), true );
		$existingOptions = array_replace_recursive( $defaults, $existingOptions );

		return json_decode( wp_json_encode( $existingOptions ) );
	}

	/**
	 * Migrates the post's audience age schema data when it is loaded.
	 * Min age: [0 => newborns, 0.25 => infants, 1 => toddlers, 5 => kids, 13 => adults]
	 * Max age: [0.25 => newborns, 1 => infants, 5 => toddlers, 13 => kids]
	 *
	 * @since 4.7.9
	 *
	 * @param  object $audience The audience data.
	 * @return object
	 */
	public static function migratePostAudienceAgeSchema( $audience ) {
		$ages = [ 0, 0.25, 1, 5, 13 ];

		// converts variable to integer if it's a number otherwise is null.
		$parsedMinAge = filter_var( $audience->minimumAge, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE );
		$parsedMaxAge = filter_var( $audience->maximumAge, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE );

		if ( null === $parsedMinAge && null === $parsedMaxAge ) {
			return $audience;
		}

		$minAge = is_numeric( $parsedMinAge ) ? $parsedMinAge : 0;
		$maxAge = is_numeric( $parsedMaxAge ) ? $parsedMaxAge : null;

		// get the minimumAge if available or the nearest bigger one.
		foreach ( $ages as $age ) {
			if ( $age >= $minAge ) {
				$audience->minimumAge = $age;
				break;
			}
		}

		// get the maximumAge if available or the nearest bigger one.
		foreach ( $ages as $age ) {
			if ( $age >= $maxAge ) {
				$maxAge = $age;
				break;
			}
		}

		// makes sure the maximumAge is 13 below
		if ( null !== $maxAge ) {
			$audience->maximumAge = 13 < $maxAge ? 13 : $maxAge;
		}

		// Minimum age 13 is for adults.
		// If minimumAge is still higher or equal 13 then it's for adults and maximumAge should be empty.
		if ( 13 <= $audience->minimumAge ) {
			$audience->minimumAge = 13;
			$audience->maximumAge = null;
		}

		return $audience;
	}

	/**
	 * Migrates update Korea country code for Person, Product, Event, and JobsPosting schemas.
	 *
	 * @since 4.7.1
	 *
	 * @param  Post $aioseoPost The post object.
	 * @return Post             The modified post object.
	 */
	private static function migrateKoreaCountryCodeSchemas( $aioseoPost ) {
		if ( empty( $aioseoPost->schema ) || empty( $aioseoPost->schema->graphs ) ) {
			return $aioseoPost;
		}

		foreach ( $aioseoPost->schema->graphs as $key => $graph ) {
			if ( isset( $aioseoPost->schema->graphs[ $key ]->properties->location->country ) ) {
				$aioseoPost->schema->graphs[ $key ]->properties->location->country = self::invertKoreaCode( $graph->properties->location->country );
			}

			if ( isset( $aioseoPost->schema->graphs[ $key ]->properties->shippingDestinations ) ) {
				$aioseoPost->schema->graphs[ $key ]->properties->shippingDestinations = array_map( function( $item ) {
					$item->country = self::invertKoreaCode( $item->country );

					return $item;
				}, $graph->properties->shippingDestinations );
			}
		}

		$aioseoPost->save();

		return $aioseoPost;
	}

	/**
	 * Utility function to invert the country code for Korea.
	 *
	 * @since 4.7.1
	 *
	 * @param  string $code country code.
	 * @return string       country code.
	 */
	public static function invertKoreaCode( $code ) {
		return 'KP' === $code ? 'KR' : $code;
	}

	/**
	 * Returns the default AI options.
	 *
	 * @since 4.8.4
	 *
	 * @param  array $existingOptions The existing options.
	 * @return object                 The default options.
	 */
	public static function getDefaultAiOptions( $existingOptions = [] ) {
		$defaults = [
			'faqs'         => [],
			'keyPoints'    => [],
			'titles'       => [],
			'descriptions' => [],
			'socialPosts'  => [
				'email'     => [],
				'linkedin'  => [],
				'twitter'   => [],
				'facebook'  => [],
				'instagram' => []
			]
		];

		if ( empty( $existingOptions ) ) {
			return json_decode( wp_json_encode( $defaults ) );
		}

		$existingOptions = array_replace_recursive( $defaults, (array) $existingOptions );

		return json_decode( wp_json_encode( $existingOptions ) );
	}
}Common/Models/SeoAnalyzerResult.php000064400000011075151536241200013366 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

/**
 * The SeoAnalyzerResult Model.
 *
 * @since 4.8.3
 */
class SeoAnalyzerResult extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.8.3
	 *
	 * @var string
	 */
	protected $table = 'aioseo_seo_analyzer_results';

	/**
	 * Fields that should be json encoded on save and decoded on get.
	 *
	 * @since 4.8.3
	 *
	 * @var array
	 */
	protected $jsonFields = [
		'data'
	];

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @since 4.8.3
	 *
	 * @var array
	 */
	protected $hidden = [ 'id' ];

	/**
	 * Fields that can be null when saved.
	 *
	 * @since 4.8.3
	 *
	 * @var array
	 */
	protected $nullFields = [
		'competitor_url',
	];

	/**
	 * An array of columns from the DB that we can use.
	 *
	 * @since 4.8.3
	 *
	 * @var array
	 */
	protected $columns = [
		'id',
		'score',
		'data',
		'competitor_url',
		'created',
		'updated',
	];

	/**
	 * Returns all not competitors results.
	 *
	 * @since 4.8.3
	 *
	 * @return array List of results.
	 */
	public static function getResults() {
		$results = aioseo()->core->db->start( 'aioseo_seo_analyzer_results' )
			->select( '*' )
			->where( 'competitor_url', null )
			->run()
			->result();

		if ( empty( $results ) ) {
			return [];
		}

		return self::parseObjects( $results );
	}

	/**
	 * Returns all competitors results.
	 *
	 * @since 4.8.3
	 *
	 * @return array List of results.
	 */
	public static function getCompetitorsResults() {
		$results = aioseo()->core->db->start( 'aioseo_seo_analyzer_results' )
			->select( '*' )
			->whereRaw( 'competitor_url IS NOT NULL' )
			->orderBy( 'updated DESC' )
			->run()
			->result();

		if ( empty( $results ) ) {
			return [];
		}

		return self::parseObjects( $results, true );
	}

	/**
	 * Parse results to the front end format.
	 *
	 * @since 4.8.3
	 *
	 * @param  array $objects      List of objects.
	 * @param  bool  $isCompetitor Flag that indicates if is parsing a competitor or a homepage result.
	 * @return array               List of results.
	 */
	private static function parseObjects( $objects, $isCompetitor = false ) {
		$results = [];

		foreach ( $objects as $obj ) {
			$data = json_decode( $obj->data ?? '[]', true );

			if ( ! $isCompetitor ) {
				$results['score'] = $obj->score ?? 0;
			}

			foreach ( $data as $result ) {
				$metadata = $result['metadata'] ?? [];
				$item     = empty( $result['status'] ) && ! empty( $metadata['value'] ) ? $metadata['value'] : array_merge( $metadata, [ 'status' => $result['status'] ] );

				if ( $isCompetitor ) {
					if ( empty( $obj->competitor_url ) || empty( $result['group'] ) || empty( $result['name'] ) ) {
						continue;
					}

					$results[ $obj->competitor_url ]['results'][ $result['group'] ][ $result['name'] ] = $item;
					$results[ $obj->competitor_url ]['score'] = ! empty( $obj->score ) ? $obj->score : 0;
				} else {
					$results['results'][ $result['group'] ][ $result['name'] ] = $item;
				}
			}
		}

		return $results;
	}

	/**
	 * Delete results by competitor url, if null we are deleting the homepage results.
	 *
	 * @since 4.8.3
	 *
	 * @param  string $url The competitor url.
	 * @return void
	 */
	public static function deleteByUrl( $url ) {
		aioseo()->core->db
			->delete( 'aioseo_seo_analyzer_results' )
			->where( 'competitor_url', $url )
			->run();
	}

	/**
	 * Add multiple results at once.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	public static function addResults( $results, $competitorUrl = null ) {
		if ( empty( $results['results'] ) ) {
			return;
		}

		// Delete the results for the competitor url if it exists.
		self::deleteByUrl( $competitorUrl );

		$data = [
			'competitor_url' => $competitorUrl,
			'score'          => $results['score'],
			'data'           => []
		];

		foreach ( $results['results'] as $group => $items ) {
			foreach ( $items as $name => $result ) {
				$fields = [
					'name'     => $name,
					'group'    => $group,
					'status'   => empty( $result['status'] ) ? null : $result['status'],
					'metadata' => null,
				];

				if ( ! is_array( $result ) ) {
					$fields['metadata'] = [ 'value' => $result ];
				} else {
					$metadata = [];
					foreach ( $result as $key => $value ) {
						if ( 'status' !== $key ) {
							$metadata[ $key ] = $value;
						}
					}

					if ( ! empty( $metadata ) ) {
						$fields['metadata'] = $metadata;
					}
				}

				$data['data'][] = $fields;
			}
		}

		$data['data'] = wp_json_encode( $data['data'] );
		$newResult = new SeoAnalyzerResult( $data );
		$newResult->save();
	}
}Common/Models/WritingAssistantKeyword.php000064400000003047151536241200014615 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

/**
 * Class Keyword.
 *
 * @since 4.7.4
 */
class WritingAssistantKeyword extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.7.4
	 *
	 * @var string
	 */
	protected $table = 'aioseo_writing_assistant_keywords';

	/**
	 * Fields that should be numeric values.
	 *
	 * @since 4.7.4
	 *
	 * @var array
	 */
	protected $integerFields = [ 'id', 'progress' ];

	/**
	 * Fields that should be boolean values.
	 *
	 * @since 4.7.4
	 *
	 * @var array
	 */
	protected $booleanFields = [];

	/**
	 * Fields that should be encoded/decoded on save/get.
	 *
	 * @since 4.7.4
	 *
	 * @var array
	 */
	protected $jsonFields = [ 'keywords', 'competitors', 'content_analysis' ];

	/**
	 * Gets a keyword.
	 *
	 * @since 4.7.4
	 *
	 * @param  string $keyword  A keyword.
	 * @param  string $country  The country code.
	 * @param  string $language The language code.
	 * @return object           A keyword found.
	 */
	public static function getKeyword( $keyword, $country, $language ) {
		$dbKeyword = aioseo()->core->db->start( 'aioseo_writing_assistant_keywords' )
			->where( 'keyword', $keyword )
			->where( 'country', $country )
			->where( 'language', $language )
			->run()
			->model( 'AIOSEO\Plugin\Common\Models\WritingAssistantKeyword' );

		if ( ! $dbKeyword->exists() ) {
			$dbKeyword->keyword  = $keyword;
			$dbKeyword->country  = $country;
			$dbKeyword->language = $language;
		}

		return $dbKeyword;
	}
}Common/Models/WritingAssistantPost.php000064400000006734151536241200014124 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

/**
 * Class Posts.
 *
 * @since 4.7.4
 */
class WritingAssistantPost extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.7.4
	 *
	 * @var string
	 */
	protected $table = 'aioseo_writing_assistant_posts';

	/**
	 * Fields that should be integer values.
	 *
	 * @since 4.7.4
	 *
	 * @var array
	 */
	protected $integerFields = [ 'id', 'post_id', 'keyword_id' ];

	/**
	 * Fields that should be boolean values.
	 *
	 * @since 4.7.4
	 *
	 * @var array
	 */
	protected $booleanFields = [];

	/**
	 * Fields that should be encoded/decoded on save/get.
	 *
	 * @since 4.7.4
	 *
	 * @var array
	 */
	protected $jsonFields = [ 'content_analysis' ];

	/**
	 * Gets a post's content analysis.
	 *
	 * @since 4.7.4
	 *
	 * @param  int   $postId A post ID.
	 * @return array         The post content's analysis.
	 */
	public static function getContentAnalysis( $postId ) {
		$post = self::getPost( $postId );

		return ! empty( $post->content_analysis ) && is_object( $post->content_analysis ) ? (array) $post->content_analysis : [];
	}

	/**
	 * Gets a writing assistant post.
	 *
	 * @since 4.7.4
	 *
	 * @param  int                  $postId A post ID.
	 * @return WritingAssistantPost         The post object.
	 */
	public static function getPost( $postId ) {
		$post = aioseo()->core->db->start( 'aioseo_writing_assistant_posts' )
			->where( 'post_id', $postId )
			->run()
			->model( 'AIOSEO\Plugin\Common\Models\WritingAssistantPost' );

		if ( ! $post->exists() ) {
			$post->post_id = $postId;
		}

		return $post;
	}

	/**
	 * Gets a post's current keyword.
	 *
	 * @since 4.7.4
	 *
	 * @param  int                          $postId A post ID.
	 * @return WritingAssistantKeyword|bool         An attached keyword.
	 */
	public static function getKeyword( $postId ) {
		$post = self::getPost( $postId );
		if ( ! $post->exists() || empty( $post->keyword_id ) ) {
			return false;
		}

		$keyword = aioseo()->core->db->start( 'aioseo_writing_assistant_keywords' )
			->where( 'id', $post->keyword_id )
			->run()
			->model( 'AIOSEO\Plugin\Common\Models\WritingAssistantKeyword' );

		// This is here so this property is reactive in the frontend.
		if ( ! empty( $keyword->keywords ) ) {
			foreach ( $keyword->keywords as &$keyph ) {
				$keyph->contentCount = 0;
			}
		}

		// Help sorting in the frontend.
		if ( ! empty( $keyword->competitors->competitors ) ) {
			foreach ( $keyword->competitors->competitors as &$competitor ) {
				$competitor->wasAnalyzed = true;
				if ( 0 >= $competitor->wordCount ) {
					$competitor->wordCount        = 0;
					$competitor->readabilityScore = 999;
					$competitor->readabilityGrade = '';
					$competitor->gradeScore       = 0;
					$competitor->grade            = '';
					$competitor->wasAnalyzed      = false;
				}

				$competitor->readabilityScore = (float) $competitor->readabilityScore;
			}
		}

		return $keyword;
	}

	/**
	 * Return if a post has a keyword.
	 *
	 * @since 4.7.4
	 *
	 * @param  int     $postId A post ID.
	 * @return boolean         Has a keyword.
	 */
	public static function hasKeyword( $postId ) {
		$post = self::getPost( $postId );

		return (bool) $post->keyword_id;
	}

	/**
	 * Attaches a keyword to a post.
	 *
	 * @since 4.7.4
	 *
	 * @param  int  $keywordId The keyword ID.
	 * @return void
	 */
	public function attachKeyword( $keywordId ) {
		$this->keyword_id = $keywordId;
		$this->save();
	}
}Common/Options/Cache.php000064400000003103151536241200011137 0ustar00<?php
namespace AIOSEO\Plugin\Common\Options;

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

/**
 * Class that holds all the cache for the AIOSEO options.
 *
 * @since 4.1.4
 */
class Cache {
	/**
	 * The DB options cache.
	 *
	 * @since 4.1.4
	 *
	 * @var array
	 */
	private static $db = [];

	/**
	 * The options cache.
	 *
	 * @since 4.1.4
	 *
	 * @var array
	 */
	private static $options = [];

	/**
	 * Sets the cache for the DB option.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $name  The cache name.
	 * @param  array  $value The value.
	 * @return void
	 */
	public function setDb( $name, $value ) {
		self::$db[ $name ] = $value;
	}

	/**
	 * Gets the cache for the DB option.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $name The cache name.
	 * @return array        The data from the cache.
	 */
	public function getDb( $name ) {
		return ! empty( self::$db[ $name ] ) ? self::$db[ $name ] : [];
	}

	/**
	 * Sets the cache for the options.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $name  The cache name.
	 * @param  array  $value The value.
	 * @return void
	 */
	public function setOptions( $name, $value ) {
		self::$options[ $name ] = $value;
	}

	/**
	 * Gets the cache for the options.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $name The cache name.
	 * @return array        The data from the cache.
	 */
	public function getOptions( $name ) {
		return ! empty( self::$options[ $name ] ) ? self::$options[ $name ] : [];
	}

	/**
	 * Resets the DB cache.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function resetDb() {
		self::$db = [];
	}
}Common/Options/DynamicBackup.php000064400000021020151536241200012644 0ustar00<?php
namespace AIOSEO\Plugin\Common\Options;

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

/**
 * Handles the dynamic backup.
 *
 * @since 4.1.3
 */
class DynamicBackup {
	/**
	 * A the name of the option to save dynamic backups to.
	 *
	 * @since 4.1.3
	 *
	 * @var string
	 */
	protected $optionsName = 'aioseo_dynamic_settings_backup';

	/**
	 * The dynamic backup.
	 *
	 * @since 4.1.3
	 *
	 * @var array
	 */
	protected $backup = [];

	/**
	 * Whether the backup should be updated.
	 *
	 * @since 4.1.3
	 *
	 * @var boolean
	 */
	protected $shouldBackup = false;

	/**
	 * The option defaults.
	 *
	 * @since 4.1.3
	 *
	 * @var array
	 */
	protected $defaultOptions = [];

	/**
	 * The public post types.
	 *
	 * @since 4.1.5
	 *
	 * @var array
	 */
	protected $postTypes = [];

	/**
	 * The public taxonomies.
	 *
	 * @since 4.1.5
	 *
	 * @var array
	 */
	protected $taxonomies = [];

	/**
	 * The public archives.
	 *
	 * @since 4.1.5
	 *
	 * @var array
	 */
	protected $archives = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.3
	 */
	public function __construct() {
		add_action( 'wp_loaded', [ $this, 'init' ], 5000 );
		add_action( 'shutdown', [ $this, 'updateBackup' ] );
	}

	/**
	 * Updates the backup after restoring options.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	public function updateBackup() {
		if ( $this->shouldBackup ) {
			$this->shouldBackup = false;
			$backup = aioseo()->dynamicOptions->convertOptionsToValues( $this->backup, 'value' );
			update_option( $this->optionsName, wp_json_encode( $backup ), 'no' );
		}
	}

	/**
	 * Checks whether data from the backup has to be restored.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	public function init() {
		$this->postTypes  = wp_list_pluck( aioseo()->helpers->getPublicPostTypes( false, false, true ), 'name' );
		$this->taxonomies = wp_list_pluck( aioseo()->helpers->getPublicTaxonomies( false, true ), 'name' );
		$this->archives   = wp_list_pluck( aioseo()->helpers->getPublicPostTypes( false, true, true ), 'name' );

		$backup = json_decode( get_option( $this->optionsName ), true );
		if ( empty( $backup ) ) {
			update_option( $this->optionsName, '{}', 'no' );

			return;
		}

		$this->backup         = $backup;
		$this->defaultOptions = aioseo()->dynamicOptions->getDefaults();

		$this->restorePostTypes();
		$this->restoreTaxonomies();
		$this->restoreArchives();
	}

	/**
	 * Restores the dynamic Post Types options.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	protected function restorePostTypes() {
		foreach ( $this->postTypes as $postType ) {
			// Restore the post types for Search Appearance.
			if ( ! empty( $this->backup['postTypes'][ $postType ]['searchAppearance'] ) ) {
				$this->restoreOptions( $this->backup['postTypes'][ $postType ]['searchAppearance'], [ 'searchAppearance', 'postTypes', $postType ] );
				unset( $this->backup['postTypes'][ $postType ]['searchAppearance'] );
				$this->shouldBackup = true;
			}

			// Restore the post types for Social Networks.
			if ( ! empty( $this->backup['postTypes'][ $postType ]['social']['facebook'] ) ) {
				$this->restoreOptions( $this->backup['postTypes'][ $postType ]['social']['facebook'], [ 'social', 'facebook', 'general', 'postTypes', $postType ] );
				unset( $this->backup['postTypes'][ $postType ]['social']['facebook'] );
				$this->shouldBackup = true;
			}
		}
	}

	/**
	 * Restores the dynamic Taxonomies options.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	protected function restoreTaxonomies() {
		foreach ( $this->taxonomies as $taxonomy ) {
			// Restore the taxonomies for Search Appearance.
			if ( ! empty( $this->backup['taxonomies'][ $taxonomy ]['searchAppearance'] ) ) {
				$this->restoreOptions( $this->backup['taxonomies'][ $taxonomy ]['searchAppearance'], [ 'searchAppearance', 'taxonomies', $taxonomy ] );
				unset( $this->backup['taxonomies'][ $taxonomy ]['searchAppearance'] );
				$this->shouldBackup = true;
			}

			// Restore the taxonomies for Social Networks.
			if ( ! empty( $this->backup['taxonomies'][ $taxonomy ]['social']['facebook'] ) ) {
				$this->restoreOptions( $this->backup['taxonomies'][ $taxonomy ]['social']['facebook'], [ 'social', 'facebook', 'general', 'taxonomies', $taxonomy ] );
				unset( $this->backup['taxonomies'][ $taxonomy ]['social']['facebook'] );
				$this->shouldBackup = true;
			}
		}
	}

	/**
	 * Restores the dynamic Archives options.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	protected function restoreArchives() {
		foreach ( $this->archives as $postType ) {
			// Restore the archives for Search Appearance.
			if ( ! empty( $this->backup['archives'][ $postType ]['searchAppearance'] ) ) {
				$this->restoreOptions( $this->backup['archives'][ $postType ]['searchAppearance'], [ 'searchAppearance', 'archives', $postType ] );
				unset( $this->backup['archives'][ $postType ]['searchAppearance'] );
				$this->shouldBackup = true;
			}
		}
	}

	/**
	 * Restores the backuped options.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $backupOptions The options to be restored.
	 * @param  array $groups        The group that the option should be restored to.
	 * @return void
	 */
	protected function restoreOptions( $backupOptions, $groups ) {
		$defaultOptions = $this->defaultOptions;
		foreach ( $groups as $group ) {
			if ( ! isset( $defaultOptions[ $group ] ) ) {
				return;
			}

			$defaultOptions = $defaultOptions[ $group ];
		}

		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		foreach ( $backupOptions as $setting => $value ) {
			// Check if the option exists before proceeding. If not, it might be a group.
			$type = $defaultOptions[ $setting ]['type'] ?? '';
			if (
				! $type &&
				is_array( $value ) &&
				aioseo()->helpers->isArrayAssociative( $value )
			) {
				$nextGroups = array_merge( $groups, [ $setting ] );

				$this->restoreOptions( $backupOptions[ $setting ], $nextGroups );

				continue;
			}

			// If we still can't find the option, it might be a group.
			if ( ! $type ) {
				continue;
			}

			foreach ( $groups as $group ) {
				$dynamicOptions = $dynamicOptions->$group;
			}

			$dynamicOptions->$setting = $value;
		}
	}

	/**
	 * Maybe backup the options if it has disappeared.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $newOptions An array of options to check.
	 * @return void
	 */
	public function maybeBackup( $newOptions ) {
		$this->maybeBackupPostType( $newOptions );
		$this->maybeBackupTaxonomy( $newOptions );
		$this->maybeBackupArchives( $newOptions );
	}

	/**
	 * Maybe backup the Post Types.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $newOptions An array of options to check.
	 * @return void
	 */
	protected function maybeBackupPostType( $newOptions ) {
		// Maybe backup the post types for Search Appearance.
		foreach ( $newOptions['searchAppearance']['postTypes'] as $dynamicPostTypeName => $dynamicPostTypeSettings ) {
			$found = in_array( $dynamicPostTypeName, $this->postTypes, true );
			if ( ! $found ) {
				$this->backup['postTypes'][ $dynamicPostTypeName ]['searchAppearance'] = $dynamicPostTypeSettings;
				$this->shouldBackup = true;
			}
		}

		// Maybe backup the post types for Social Networks.
		foreach ( $newOptions['social']['facebook']['general']['postTypes'] as $dynamicPostTypeName => $dynamicPostTypeSettings ) {
			$found = in_array( $dynamicPostTypeName, $this->postTypes, true );
			if ( ! $found ) {
				$this->backup['postTypes'][ $dynamicPostTypeName ]['social']['facebook'] = $dynamicPostTypeSettings;
				$this->shouldBackup = true;
			}
		}
	}

	/**
	 * Maybe backup the Taxonomies.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $newOptions An array of options to check.
	 * @return void
	 */
	protected function maybeBackupTaxonomy( $newOptions ) {
		// Maybe backup the taxonomies for Search Appearance.
		foreach ( $newOptions['searchAppearance']['taxonomies'] as $dynamicTaxonomyName => $dynamicTaxonomySettings ) {
			$found = in_array( $dynamicTaxonomyName, $this->taxonomies, true );
			if ( ! $found ) {
				$this->backup['taxonomies'][ $dynamicTaxonomyName ]['searchAppearance'] = $dynamicTaxonomySettings;
				$this->shouldBackup = true;
			}
		}
	}

	/**
	 * Maybe backup the Archives.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $newOptions An array of options to check.
	 * @return void
	 */
	protected function maybeBackupArchives( $newOptions ) {
		// Maybe backup the archives for Search Appearance.
		foreach ( $newOptions['searchAppearance']['archives'] as $archiveName => $archiveSettings ) {
			$found = in_array( $archiveName, $this->archives, true );
			if ( ! $found ) {
				$this->backup['archives'][ $archiveName ]['searchAppearance'] = $archiveSettings;
				$this->shouldBackup = true;
			}
		}
	}
}Common/Options/DynamicOptions.php000064400000026113151536241200013102 0ustar00<?php
namespace AIOSEO\Plugin\Common\Options;

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

use AIOSEO\Plugin\Common\Traits;

/**
 * Handles the dynamic options.
 *
 * @since 4.1.4
 */
class DynamicOptions {
	use Traits\Options;

	/**
	 * The default options.
	 *
	 * @since 4.1.4
	 *
	 * @var array
	 */
	protected $defaults = [
		// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
		'sitemap'          => [
			'priority' => [
				'postTypes'  => [],
				'taxonomies' => []
			]
		],
		'social'           => [
			'facebook' => [
				'general' => [
					'postTypes' => []
				]
			]
		],
		'searchAppearance' => [
			'postTypes'  => [],
			'taxonomies' => [],
			'archives'   => []
		]
		// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 *
	 * @param string $optionsName The options name.
	 */
	public function __construct( $optionsName = 'aioseo_options_dynamic' ) {
		$this->optionsName = $optionsName;

		// Load defaults in case this is a complete fresh install.
		$this->init();

		add_action( 'shutdown', [ $this, 'save' ] );
	}

	/**
	 * Initializes the options.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	protected function init() {
		$this->addDynamicDefaults();
		$this->setDbOptions();
	}

	/**
	 * Sets the DB options.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	protected function setDbOptions() {
		$dbOptions = $this->getDbOptions( $this->optionsName );

		// Refactor options.
		$this->defaultsMerged = array_replace_recursive( $this->defaults, $this->defaultsMerged );

		$dbOptions = array_replace_recursive(
			$this->defaultsMerged,
			$this->addValueToValuesArray( $this->defaultsMerged, $dbOptions )
		);

		aioseo()->core->optionsCache->setOptions( $this->optionsName, $dbOptions );

		// Get the localized options.
		$dbOptionsLocalized = get_option( $this->optionsName . '_localized' );
		if ( empty( $dbOptionsLocalized ) ) {
			$dbOptionsLocalized = [];
		}
		$this->localized = $dbOptionsLocalized;
	}

	/**
	 * Sanitizes, then saves the options to the database.
	 *
	 * @since 4.1.4
	 *
	 * @param  array $options An array of options to sanitize, then save.
	 * @return void
	 */
	public function sanitizeAndSave( $options ) {
		if ( ! is_array( $options ) ) {
			return;
		}

		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );

		aioseo()->dynamicBackup->maybeBackup( $cachedOptions );

		// First, recursively replace the new options into the cached state.
		// It's important we use the helper method since we want to replace populated arrays with empty ones if needed (when a setting was cleared out).
		$dbOptions = aioseo()->helpers->arrayReplaceRecursive(
			$cachedOptions,
			$this->addValueToValuesArray( $cachedOptions, $options, [], true )
		);

		// Now, we must also intersect both arrays to delete any individual keys that were unset.
		// We must do this because, while arrayReplaceRecursive will update the values for keys or empty them out,
		// it will keys that aren't present in the replacement array unaffected in the target array.
		$dbOptions = aioseo()->helpers->arrayIntersectRecursive(
			$dbOptions,
			$this->addValueToValuesArray( $cachedOptions, $options, [], true ),
			'value'
		);

		// Update the cache state.
		aioseo()->core->optionsCache->setOptions( $this->optionsName, $dbOptions );

		// Update localized options.
		update_option( $this->optionsName . '_localized', $this->localized );

		// Finally, save the new values to the DB.
		$this->save( true );
	}

	/**
	 * Adds some defaults that are dynamically generated.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function addDynamicDefaults() {
		$this->addDynamicPostTypeDefaults();
		$this->addDynamicTaxonomyDefaults();
		$this->addDynamicArchiveDefaults();
	}

	/**
	 * Adds the dynamic defaults for the public post types.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	protected function addDynamicPostTypeDefaults() {
		$postTypes = aioseo()->helpers->getPublicPostTypes( false, false, false, [ 'include' => [ 'buddypress' ] ] );
		foreach ( $postTypes as $postType ) {
			if ( 'type' === $postType['name'] ) {
				$postType['name'] = '_aioseo_type';
			}

			$defaultTitle = '#post_title #separator_sa #site_title';
			if ( ! empty( $postType['defaultTitle'] ) ) {
				$defaultTitle = $postType['defaultTitle'];
			}
			$defaultDescription = ! empty( $postType['supports']['excerpt'] ) ? '#post_excerpt' : '#post_content';
			if ( ! empty( $postType['defaultDescription'] ) ) {
				$defaultDescription = $postType['defaultDescription'];
			}
			$defaultSchemaType  = 'WebPage';
			$defaultWebPageType = 'WebPage';
			$defaultArticleType = 'BlogPosting';

			switch ( $postType['name'] ) {
				case 'post':
					$defaultSchemaType = 'Article';
					break;
				case 'attachment':
					$defaultDescription = '#attachment_caption';
					$defaultSchemaType  = 'ItemPage';
					$defaultWebPageType = 'ItemPage';
					break;
				case 'product':
					$defaultSchemaType  = 'WebPage';
					$defaultWebPageType = 'ItemPage';
					break;
				case 'news':
					$defaultArticleType = 'NewsArticle';
					break;
				case 'web-story':
					$defaultWebPageType = 'WebPage';
					$defaultSchemaType  = 'WebPage';
					break;
				default:
					break;
			}

			$defaultOptions = array_replace_recursive(
				$this->getDefaultSearchAppearanceOptions(),
				[
					'title'           => [
						'type'      => 'string',
						'localized' => true,
						'default'   => $defaultTitle
					],
					'metaDescription' => [
						'type'      => 'string',
						'localized' => true,
						'default'   => $defaultDescription
					],
					'schemaType'      => [
						'type'    => 'string',
						'default' => $defaultSchemaType
					],
					'webPageType'     => [
						'type'    => 'string',
						'default' => $defaultWebPageType
					],
					'articleType'     => [
						'type'    => 'string',
						'default' => $defaultArticleType
					],
					'customFields'    => [ 'type' => 'html' ],
					'advanced'        => [
						'bulkEditing' => [
							'type'    => 'string',
							'default' => 'enabled'
						]
					]
				]
			);

			if ( 'attachment' === $postType['name'] ) {
				$defaultOptions['redirectAttachmentUrls'] = [
					'type'    => 'string',
					'default' => 'attachment'
				];
			}

			$this->defaults['searchAppearance']['postTypes'][ $postType['name'] ] = $defaultOptions;
			$this->setDynamicSocialOptions( 'postTypes', $postType['name'] );
			$this->setDynamicSitemapOptions( 'postTypes', $postType['name'] );
		}
	}

	/**
	 * Adds the dynamic defaults for the public taxonomies.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	protected function addDynamicTaxonomyDefaults() {
		$taxonomies = aioseo()->helpers->getPublicTaxonomies();
		foreach ( $taxonomies as $taxonomy ) {
			if ( 'type' === $taxonomy['name'] ) {
				$taxonomy['name'] = '_aioseo_type';
			}

			$defaultOptions = array_replace_recursive(
				$this->getDefaultSearchAppearanceOptions(),
				[
					'title'           => [
						'type'      => 'string',
						'localized' => true,
						'default'   => '#taxonomy_title #separator_sa #site_title'
					],
					'metaDescription' => [
						'type'      => 'string',
						'localized' => true,
						'default'   => '#taxonomy_description'
					],
				]
			);

			$this->setDynamicSitemapOptions( 'taxonomies', $taxonomy['name'] );

			$this->defaults['searchAppearance']['taxonomies'][ $taxonomy['name'] ] = $defaultOptions;
		}
	}

	/**
	 * Adds the dynamic defaults for the archive pages.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	protected function addDynamicArchiveDefaults() {
		$postTypes = aioseo()->helpers->getPublicPostTypes( false, true, false, [ 'include' => [ 'buddypress' ] ] );
		foreach ( $postTypes as $postType ) {
			if ( 'type' === $postType['name'] ) {
				$postType['name'] = '_aioseo_type';
			}

			$defaultOptions = array_replace_recursive(
				$this->getDefaultSearchAppearanceOptions(),
				[
					'title'           => [
						'type'      => 'string',
						'localized' => true,
						'default'   => '#archive_title #separator_sa #site_title'
					],
					'metaDescription' => [
						'type'      => 'string',
						'localized' => true,
						'default'   => ''
					],
					'customFields'    => [ 'type' => 'html' ],
					'advanced'        => [
						'keywords' => [
							'type'      => 'string',
							'localized' => true
						]
					]
				]
			);

			$this->defaults['searchAppearance']['archives'][ $postType['name'] ] = $defaultOptions;
		}
	}

	/**
	 * Returns the search appearance options for dynamic objects.
	 *
	 * @since 4.1.4
	 *
	 * @return array The default options.
	 */
	protected function getDefaultSearchAppearanceOptions() {
		return [ // phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
			'show'     => [ 'type' => 'boolean', 'default' => true ],
			'advanced' => [
				'robotsMeta'                => [
					'default'         => [ 'type' => 'boolean', 'default' => true ],
					'noindex'         => [ 'type' => 'boolean', 'default' => false ],
					'nofollow'        => [ 'type' => 'boolean', 'default' => false ],
					'noarchive'       => [ 'type' => 'boolean', 'default' => false ],
					'noimageindex'    => [ 'type' => 'boolean', 'default' => false ],
					'notranslate'     => [ 'type' => 'boolean', 'default' => false ],
					'nosnippet'       => [ 'type' => 'boolean', 'default' => false ],
					'noodp'           => [ 'type' => 'boolean', 'default' => false ],
					'maxSnippet'      => [ 'type' => 'number', 'default' => -1 ],
					'maxVideoPreview' => [ 'type' => 'number', 'default' => -1 ],
					'maxImagePreview' => [ 'type' => 'string', 'default' => 'large' ]
				],
				'showDateInGooglePreview'   => [ 'type' => 'boolean', 'default' => true ],
				'showPostThumbnailInSearch' => [ 'type' => 'boolean', 'default' => true ],
				'showMetaBox'               => [ 'type' => 'boolean', 'default' => true ]
			]
		]; // phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	}

	/**
	 * Sets the dynamic social settings for a given post type or taxonomy.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $objectType Whether the object belongs to the dynamic "postTypes" or "taxonomies".
	 * @param  string $objectName The object name.
	 * @return void
	 */
	protected function setDynamicSocialOptions( $objectType, $objectName ) {
		$defaultOptions = [
			'objectType' => [
				'type'    => 'string',
				'default' => 'article'
			]
		];

		$this->defaults['social']['facebook']['general'][ $objectType ][ $objectName ] = $defaultOptions;
	}

	/**
	 * Sets the dynamic sitemap settings for a given post type or taxonomy.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $objectType Whether the object belongs to the dynamic "postTypes" or "taxonomies".
	 * @param  string $objectName The object name.
	 * @return void
	 */
	protected function setDynamicSitemapOptions( $objectType, $objectName ) {
		$this->defaults['sitemap']['priority'][ $objectType ][ $objectName ] = [
			'priority'  => [
				'type'    => 'string',
				'default' => '{"label":"default","value":"default"}'
			],
			'frequency' => [
				'type'    => 'string',
				'default' => '{"label":"default","value":"default"}'
			]
		];
	}
}Common/Options/InternalNetworkOptions.php000064400000001622151536241200014642 0ustar00<?php
namespace AIOSEO\Plugin\Common\Options;

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

use AIOSEO\Plugin\Common\Traits;
use AIOSEO\Plugin\Common\Utils;

/**
 * Class that holds all internal network options for AIOSEO.
 *
 * @since 4.2.5
 */
class InternalNetworkOptions {
	use Traits\Options;
	use Traits\NetworkOptions;

	/**
	 * Holds the helpers class.
	 *
	 * @since 4.2.5
	 *
	 * @var Utils\Helpers
	 */
	protected $helpers;

	/**
	 * All the default options.
	 *
	 * @since 4.2.5
	 *
	 * @var array
	 */
	protected $defaults = [];

	/**
	 * The Construct method.
	 *
	 * @since 4.2.5
	 *
	 * @param string $optionsName The options name.
	 */
	public function __construct( $optionsName = 'aioseo_options_network_internal' ) {
		$this->helpers     = new Utils\Helpers();
		$this->optionsName = $optionsName;

		$this->init();

		add_action( 'shutdown', [ $this, 'save' ] );
	}
}Common/Options/InternalOptions.php000064400000014340151536241200013271 0ustar00<?php
namespace AIOSEO\Plugin\Common\Options;

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

use AIOSEO\Plugin\Common\Traits;

/**
 * Class that holds all internal options for AIOSEO.
 *
 * @since 4.0.0
 */
class InternalOptions {
	use Traits\Options;

	/**
	 * Holds a list of all the possible deprecated options.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $allDeprecatedOptions = [
		'autogenerateDescriptions',
		'breadcrumbsEnable',
		'descriptionFormat',
		'enableSchemaMarkup',
		'excludePosts',
		'excludeTerms',
		'googleAnalytics',
		'noPaginationForCanonical',
		'staticSitemap',
		'staticVideoSitemap',
		'useContentForAutogeneratedDescriptions'
	];

	/**
	 * All the default options.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $defaults = [
		// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
		'internal'     => [
			'connectLicenseKey' => [ 'type' => 'string' ],
			'lastActiveVersion' => [ 'type' => 'string', 'default' => '0.0' ],
			'migratedVersion'   => [ 'type' => 'string' ],
			'siteAnalysis'      => [
				'connectToken' => [ 'type' => 'string' ],
			],
			'headlineAnalysis'  => [
				'headlines' => [ 'type' => 'array', 'default' => [] ]
			],
			'wizard'            => [ 'type' => 'string' ],
			'category'          => [ 'type' => 'string' ],
			'categoryOther'     => [ 'type' => 'string' ],
			'deprecatedOptions' => [ 'type' => 'array', 'default' => [] ],
			'searchStatistics'  => [
				'profile'    => [ 'type' => 'array', 'default' => [] ],
				'trustToken' => [ 'type' => 'string' ],
				'rolling'    => [ 'type' => 'string', 'default' => 'last28Days' ],
				'site'       => [
					'verified'  => [ 'type' => 'boolean', 'default' => false ],
					'lastFetch' => [ 'type' => 'number', 'default' => 0 ]
				],
				'sitemap'    => [
					'list'      => [ 'type' => 'array', 'default' => [] ],
					'ignored'   => [ 'type' => 'array', 'default' => [] ],
					'lastFetch' => [ 'type' => 'number', 'default' => 0 ]
				]
			],
			'ai'                => [
				'accessToken'        => [ 'type' => 'string', 'default' => '' ],
				'isTrialAccessToken' => [ 'type' => 'boolean', 'default' => false ],
				'credits'            => [
					'total'     => [ 'type' => 'number', 'default' => 0 ],
					'remaining' => [ 'type' => 'number', 'default' => 0 ],
					'orders'    => [ 'type' => 'array', 'default' => [] ],
					'license'   => [
						'total'     => [ 'type' => 'number', 'default' => 0 ],
						'remaining' => [ 'type' => 'number', 'default' => 0 ],
						'expires'   => [ 'type' => 'number', 'default' => 0 ]
					]
				]
			]
		],
		'integrations' => [
			'semrush' => [
				'accessToken'  => [ 'type' => 'string' ],
				'tokenType'    => [ 'type' => 'string' ],
				'expires'      => [ 'type' => 'string' ],
				'refreshToken' => [ 'type' => 'string' ]
			]
		],
		'database'     => [
			'installedTables' => [ 'type' => 'string' ]
		]
		// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	];

	/**
	 * The Construct method.
	 *
	 * @since 4.0.0
	 *
	 * @param string $optionsName The options name.
	 */
	public function __construct( $optionsName = 'aioseo_options_internal' ) {
		$this->optionsName = $optionsName;

		$this->init();

		add_action( 'shutdown', [ $this, 'save' ] );
	}

	/**
	 * Initializes the options.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function init() {
		// Options from the DB.
		$dbOptions = $this->getDbOptions( $this->optionsName );

		// Refactor options.
		$this->defaultsMerged = array_replace_recursive( $this->defaults, $this->defaultsMerged );

		$options = array_replace_recursive(
			$this->defaultsMerged,
			$this->addValueToValuesArray( $this->defaultsMerged, $dbOptions )
		);

		aioseo()->core->optionsCache->setOptions( $this->optionsName, apply_filters( 'aioseo_get_options_internal', $options ) );

		// Get the localized options.
		$dbOptionsLocalized = get_option( $this->optionsName . '_localized' );
		if ( empty( $dbOptionsLocalized ) ) {
			$dbOptionsLocalized = [];
		}
		$this->localized = $dbOptionsLocalized;
	}

	/**
	 * Get all the deprecated options.
	 *
	 * @since 4.0.0
	 *
	 * @param  bool  $includeNamesAndValues Whether or not to include option names.
	 * @return array                        An array of deprecated options.
	 */
	public function getAllDeprecatedOptions( $includeNamesAndValues = false ) {
		if ( ! $includeNamesAndValues ) {
			return $this->allDeprecatedOptions;
		}

		$options = [];
		foreach ( $this->allDeprecatedOptions as $deprecatedOption ) {
			$options[] = [
				'label'   => ucwords( str_replace( '_', ' ', aioseo()->helpers->toSnakeCase( $deprecatedOption ) ) ),
				'value'   => $deprecatedOption,
				'enabled' => in_array( $deprecatedOption, aioseo()->internalOptions->internal->deprecatedOptions, true )
			];
		}

		return $options;
	}

	/**
	 * Sanitizes, then saves the options to the database.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $options An array of options to sanitize, then save.
	 * @return void
	 */
	public function sanitizeAndSave( $options ) {
		if ( ! is_array( $options ) ) {
			return;
		}

		// First, recursively replace the new options into the cached state.
		// It's important we use the helper method since we want to replace populated arrays with empty ones if needed (when a setting was cleared out).
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$dbOptions     = aioseo()->helpers->arrayReplaceRecursive(
			$cachedOptions,
			$this->addValueToValuesArray( $cachedOptions, $options, [], true )
		);

		// Now, we must also intersect both arrays to delete any individual keys that were unset.
		// We must do this because, while arrayReplaceRecursive will update the values for keys or empty them out,
		// it will keys that aren't present in the replacement array unaffected in the target array.
		$dbOptions = aioseo()->helpers->arrayIntersectRecursive(
			$dbOptions,
			$this->addValueToValuesArray( $cachedOptions, $options, [], true ),
			'value'
		);

		// Update the cache state.
		aioseo()->core->optionsCache->setOptions( $this->optionsName, $dbOptions );

		// Update localized options.
		update_option( $this->optionsName . '_localized', $this->localized );

		// Finally, save the new values to the DB.
		$this->save( true );
	}
}Common/Options/NetworkOptions.php000064400000003622151536241200013147 0ustar00<?php
namespace AIOSEO\Plugin\Common\Options;

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

use AIOSEO\Plugin\Common\Traits;
use AIOSEO\Plugin\Common\Utils;

/**
 * Class that holds all network options for AIOSEO.
 *
 * @since 4.2.5
 */
class NetworkOptions {
	use Traits\Options;
	use Traits\NetworkOptions;

	/**
	 * Holds the helpers class.
	 *
	 * @since 4.2.5
	 *
	 * @var Utils\Helpers
	 */
	protected $helpers;

	/**
	 * All the default options.
	 *
	 * @since 4.2.5
	 *
	 * @var array
	 */
	protected $defaults = [
		// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
		'searchAppearance' => [
			'advanced' => [
				'unwantedBots'  => [
					'all'      => [ 'type' => 'boolean', 'default' => false ],
					'settings' => [
						'googleAdsBot'             => [ 'type' => 'boolean', 'default' => false ],
						'openAiGptBot'             => [ 'type' => 'boolean', 'default' => false ],
						'commonCrawlCcBot'         => [ 'type' => 'boolean', 'default' => false ],
						'googleGeminiVertexAiBots' => [ 'type' => 'boolean', 'default' => false ]
					]
				],
				'searchCleanup' => [
					'settings' => [
						'preventCrawling' => [ 'type' => 'boolean', 'default' => false ]
					]
				]
			]
		],
		'tools'            => [
			'robots' => [
				'enable'         => [ 'type' => 'boolean', 'default' => false ],
				'rules'          => [ 'type' => 'array', 'default' => [] ],
				'robotsDetected' => [ 'type' => 'boolean', 'default' => true ],
			]
		]
		// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	];

	/**
	 * The Construct method.
	 *
	 * @since 4.2.5
	 *
	 * @param string $optionsName The options name.
	 */
	public function __construct( $optionsName = 'aioseo_options_network' ) {
		$this->helpers     = new Utils\Helpers();
		$this->optionsName = $optionsName;

		$this->init();

		add_action( 'shutdown', [ $this, 'save' ] );
	}
}Common/Options/Options.php000064400000106701151536241200011577 0ustar00<?php
namespace AIOSEO\Plugin\Common\Options;

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

use AIOSEO\Plugin\Common\Models;
use AIOSEO\Plugin\Common\Traits;

/**
 * Class that holds all options for AIOSEO.
 *
 * @since 4.0.0
 */
class Options {
	use Traits\Options;

	/**
	 * All the default options.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $defaults = [
		// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
		'internal'         => [],
		'webmasterTools'   => [
			'google'                    => [ 'type' => 'string' ],
			'bing'                      => [ 'type' => 'string' ],
			'yandex'                    => [ 'type' => 'string' ],
			'baidu'                     => [ 'type' => 'string' ],
			'pinterest'                 => [ 'type' => 'string' ],
			'microsoftClarityProjectId' => [ 'type' => 'string' ],
			'norton'                    => [ 'type' => 'string' ],
			'miscellaneousVerification' => [ 'type' => 'html' ]
		],
		'aiContent'        => [
			'country'  => [ 'type' => 'string', 'default' => 'us' ],
			'language' => [ 'type' => 'string', 'default' => 'en' ],
			'tone'     => [ 'type' => 'string', 'default' => 'formal' ],
			'audience' => [ 'type' => 'string', 'default' => 'general' ]
		],
		'breadcrumbs'      => [
			'separator'             => [ 'type' => 'string', 'default' => '&raquo;' ],
			'homepageLink'          => [ 'type' => 'boolean', 'default' => true ],
			'homepageLabel'         => [ 'type' => 'string', 'default' => 'Home' ],
			'breadcrumbPrefix'      => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
			'archiveFormat'         => [ 'type' => 'string', 'default' => 'Archives for #breadcrumb_archive_post_type_name', 'localized' => true ],
			'searchResultFormat'    => [ 'type' => 'string', 'default' => 'Search Results for \'#breadcrumb_search_string\'', 'localized' => true ],
			'errorFormat404'        => [ 'type' => 'string', 'default' => '404 - Page Not Found', 'localized' => true ],
			'showCurrentItem'       => [ 'type' => 'boolean', 'default' => true ],
			'linkCurrentItem'       => [ 'type' => 'boolean', 'default' => false ],
			'categoryFullHierarchy' => [ 'type' => 'boolean', 'default' => false ],
			'showBlogHome'          => [ 'type' => 'boolean', 'default' => false ]
		],
		'rssContent'       => [
			'before' => [ 'type' => 'html' ],
			'after'  => [
				'type'    => 'html',
				'default' => <<<TEMPLATE
&lt;p&gt;The post #post_link first appeared on #site_link.&lt;/p&gt;
TEMPLATE
			]
		],
		'advanced'         => [
			'truSeo'           => [ 'type' => 'boolean', 'default' => true ],
			'headlineAnalyzer' => [ 'type' => 'boolean', 'default' => true ],
			'llmsTxt'          => [ 'type' => 'boolean', 'default' => true ],
			'seoAnalysis'      => [ 'type' => 'boolean', 'default' => true ],
			'dashboardWidgets' => [ 'type' => 'array', 'default' => [ 'seoSetup', 'seoOverview', 'seoNews' ] ],
			'announcements'    => [ 'type' => 'boolean', 'default' => true ],
			'postTypes'        => [
				'all'      => [ 'type' => 'boolean', 'default' => true ],
				'included' => [ 'type' => 'array', 'default' => [ 'post', 'page', 'product' ] ],
			],
			'taxonomies'       => [
				'all'      => [ 'type' => 'boolean', 'default' => true ],
				'included' => [ 'type' => 'array', 'default' => [ 'category', 'post_tag', 'product_cat', 'product_tag' ] ],
			],
			'uninstall'        => [ 'type' => 'boolean', 'default' => false ],
			'emailSummary'     => [
				'enable'     => [ 'type' => 'boolean', 'default' => false ],
				'recipients' => [ 'type' => 'array', 'default' => [] ]
			]
		],
		'sitemap'          => [
			'general' => [
				'enable'           => [ 'type' => 'boolean', 'default' => true ],
				'filename'         => [ 'type' => 'string', 'default' => 'sitemap' ],
				'indexes'          => [ 'type' => 'boolean', 'default' => true ],
				'linksPerIndex'    => [ 'type' => 'number', 'default' => 1000 ],
				// @TODO: [V4+] Convert this to the dynamic options like in search appearance so we can have backups when plugins are deactivated.
				'postTypes'        => [
					'all'      => [ 'type' => 'boolean', 'default' => true ],
					'included' => [ 'type' => 'array', 'default' => [ 'post', 'page', 'attachment', 'product' ] ],
				],
				// @TODO: [V4+] Convert this to the dynamic options like in search appearance so we can have backups when plugins are deactivated.
				'taxonomies'       => [
					'all'      => [ 'type' => 'boolean', 'default' => true ],
					'included' => [ 'type' => 'array', 'default' => [ 'category', 'post_tag', 'product_cat', 'product_tag' ] ],
				],
				'author'           => [ 'type' => 'boolean', 'default' => false ],
				'date'             => [ 'type' => 'boolean', 'default' => false ],
				'additionalPages'  => [
					'enable' => [ 'type' => 'boolean', 'default' => false ],
					'pages'  => [ 'type' => 'array', 'default' => [] ]
				],
				'advancedSettings' => [
					'enable'        => [ 'type' => 'boolean', 'default' => false ],
					'excludeImages' => [ 'type' => 'boolean', 'default' => false ],
					'excludePosts'  => [ 'type' => 'array', 'default' => [] ],
					'excludeTerms'  => [ 'type' => 'array', 'default' => [] ],
					'priority'      => [
						'homePage'   => [
							'priority'  => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ],
							'frequency' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ]
						],
						'postTypes'  => [
							'grouped'   => [ 'type' => 'boolean', 'default' => true ],
							'priority'  => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ],
							'frequency' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ]
						],
						'taxonomies' => [
							'grouped'   => [ 'type' => 'boolean', 'default' => true ],
							'priority'  => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ],
							'frequency' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ]
						],
						'archive'    => [
							'priority'  => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ],
							'frequency' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ]
						],
						'author'     => [
							'priority'  => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ],
							'frequency' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ]
						]
					]
				]
			],
			'rss'     => [
				'enable'        => [ 'type' => 'boolean', 'default' => true ],
				'linksPerIndex' => [ 'type' => 'number', 'default' => 50 ],
				// @TODO: [V4+] Convert this to the dynamic options like in search appearance so we can have backups when plugins are deactivated.
				'postTypes'     => [
					'all'      => [ 'type' => 'boolean', 'default' => true ],
					'included' => [ 'type' => 'array', 'default' => [ 'post', 'page', 'product' ] ],
				]
			],
			'html'    => [
				'enable'           => [ 'type' => 'boolean', 'default' => true ],
				'pageUrl'          => [ 'type' => 'string', 'default' => '' ],
				'postTypes'        => [
					'all'      => [ 'type' => 'boolean', 'default' => true ],
					'included' => [ 'type' => 'array', 'default' => [ 'post', 'page', 'product' ] ],
				],
				'taxonomies'       => [
					'all'      => [ 'type' => 'boolean', 'default' => true ],
					'included' => [ 'type' => 'array', 'default' => [ 'category', 'post_tag', 'product_cat', 'product_tag' ] ],
				],
				'sortOrder'        => [ 'type' => 'string', 'default' => 'publish_date' ],
				'sortDirection'    => [ 'type' => 'string', 'default' => 'asc' ],
				'publicationDate'  => [ 'type' => 'boolean', 'default' => true ],
				'compactArchives'  => [ 'type' => 'boolean', 'default' => false ],
				'advancedSettings' => [
					'enable'        => [ 'type' => 'boolean', 'default' => false ],
					'nofollowLinks' => [ 'type' => 'boolean', 'default' => false ],
					'excludePosts'  => [ 'type' => 'array', 'default' => [] ],
					'excludeTerms'  => [ 'type' => 'array', 'default' => [] ]
				]
			],
		],
		'social'           => [
			'profiles' => [
				'sameUsername'   => [
					'enable'   => [ 'type' => 'boolean', 'default' => false ],
					'username' => [ 'type' => 'string' ],
					'included' => [ 'type' => 'array', 'default' => [ 'facebookPageUrl', 'twitterUrl', 'tiktokUrl', 'pinterestUrl', 'instagramUrl', 'youtubeUrl', 'linkedinUrl' ] ]
				],
				'urls'           => [
					'facebookPageUrl' => [ 'type' => 'string' ],
					'twitterUrl'      => [ 'type' => 'string' ],
					'instagramUrl'    => [ 'type' => 'string' ],
					'tiktokUrl'       => [ 'type' => 'string' ],
					'pinterestUrl'    => [ 'type' => 'string' ],
					'youtubeUrl'      => [ 'type' => 'string' ],
					'linkedinUrl'     => [ 'type' => 'string' ],
					'tumblrUrl'       => [ 'type' => 'string' ],
					'yelpPageUrl'     => [ 'type' => 'string' ],
					'soundCloudUrl'   => [ 'type' => 'string' ],
					'wikipediaUrl'    => [ 'type' => 'string' ],
					'myspaceUrl'      => [ 'type' => 'string' ],
					'googlePlacesUrl' => [ 'type' => 'string' ],
					'wordPressUrl'    => [ 'type' => 'string' ],
					'blueskyUrl'      => [ 'type' => 'string' ],
					'threadsUrl'      => [ 'type' => 'string' ]
				],
				'additionalUrls' => [ 'type' => 'string' ]
			],
			'facebook' => [
				'general'  => [
					'enable'                  => [ 'type' => 'boolean', 'default' => true ],
					'defaultImageSourcePosts' => [ 'type' => 'string', 'default' => 'default' ],
					'customFieldImagePosts'   => [ 'type' => 'string' ],
					'defaultImagePosts'       => [ 'type' => 'string', 'default' => '' ],
					'defaultImagePostsWidth'  => [ 'type' => 'number', 'default' => '' ],
					'defaultImagePostsHeight' => [ 'type' => 'number', 'default' => '' ],
					'showAuthor'              => [ 'type' => 'boolean', 'default' => true ],
					'siteName'                => [ 'type' => 'string', 'localized' => true, 'default' => '#site_title #separator_sa #tagline' ]
				],
				'homePage' => [
					'image'       => [ 'type' => 'string', 'default' => '' ],
					'title'       => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
					'description' => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
					'imageWidth'  => [ 'type' => 'number', 'default' => '' ],
					'imageHeight' => [ 'type' => 'number', 'default' => '' ],
					'objectType'  => [ 'type' => 'string', 'default' => 'website' ]
				],
				'advanced' => [
					'enable'              => [ 'type' => 'boolean', 'default' => false ],
					'adminId'             => [ 'type' => 'string', 'default' => '' ],
					'appId'               => [ 'type' => 'string', 'default' => '' ],
					'authorUrl'           => [ 'type' => 'string', 'default' => '' ],
					'generateArticleTags' => [ 'type' => 'boolean', 'default' => false ],
					'useKeywordsInTags'   => [ 'type' => 'boolean', 'default' => true ],
					'useCategoriesInTags' => [ 'type' => 'boolean', 'default' => true ],
					'usePostTagsInTags'   => [ 'type' => 'boolean', 'default' => true ]
				]
			],
			'twitter'  => [
				'general'  => [
					'enable'                  => [ 'type' => 'boolean', 'default' => true ],
					'useOgData'               => [ 'type' => 'boolean', 'default' => true ],
					'defaultCardType'         => [ 'type' => 'string', 'default' => 'summary_large_image' ],
					'defaultImageSourcePosts' => [ 'type' => 'string', 'default' => 'default' ],
					'customFieldImagePosts'   => [ 'type' => 'string' ],
					'defaultImagePosts'       => [ 'type' => 'string', 'default' => '' ],
					'showAuthor'              => [ 'type' => 'boolean', 'default' => true ],
					'additionalData'          => [ 'type' => 'boolean', 'default' => false ]
				],
				'homePage' => [
					'image'       => [ 'type' => 'string', 'default' => '' ],
					'title'       => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
					'description' => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
					'cardType'    => [ 'type' => 'string', 'default' => 'summary' ]
				],
			]
		],
		'searchAppearance' => [
			'global'   => [
				'separator'       => [ 'type' => 'string', 'default' => '&#45;' ],
				'siteTitle'       => [ 'type' => 'string', 'localized' => true, 'default' => '#site_title #separator_sa #tagline' ],
				'metaDescription' => [ 'type' => 'string', 'localized' => true, 'default' => '#tagline' ],
				'keywords'        => [ 'type' => 'string', 'localized' => true ],
				'schema'          => [
					'websiteName'             => [ 'type' => 'string', 'default' => '#site_title' ],
					'websiteAlternateName'    => [ 'type' => 'string' ],
					'siteRepresents'          => [ 'type' => 'string', 'default' => 'organization' ],
					'person'                  => [ 'type' => 'string' ],
					'organizationName'        => [ 'type' => 'string', 'default' => '#site_title' ],
					'organizationDescription' => [ 'type' => 'string', 'default' => '#tagline' ],
					'organizationLogo'        => [ 'type' => 'string' ],
					'personName'              => [ 'type' => 'string' ],
					'personLogo'              => [ 'type' => 'string' ],
					'phone'                   => [ 'type' => 'string' ],
					'email'                   => [ 'type' => 'string' ],
					'foundingDate'            => [ 'type' => 'string' ],
					'numberOfEmployees'       => [
						'isRange' => [ 'type' => 'boolean' ],
						'from'    => [ 'type' => 'number' ],
						'to'      => [ 'type' => 'number' ],
						'number'  => [ 'type' => 'number' ]
					]
				]
			],
			'advanced' => [
				'globalRobotsMeta'             => [
					'default'           => [ 'type' => 'boolean', 'default' => true ],
					'noindex'           => [ 'type' => 'boolean', 'default' => false ],
					'nofollow'          => [ 'type' => 'boolean', 'default' => false ],
					'noindexPaginated'  => [ 'type' => 'boolean', 'default' => true ],
					'nofollowPaginated' => [ 'type' => 'boolean', 'default' => true ],
					'noindexFeed'       => [ 'type' => 'boolean', 'default' => true ],
					'noarchive'         => [ 'type' => 'boolean', 'default' => false ],
					'noimageindex'      => [ 'type' => 'boolean', 'default' => false ],
					'notranslate'       => [ 'type' => 'boolean', 'default' => false ],
					'nosnippet'         => [ 'type' => 'boolean', 'default' => false ],
					'noodp'             => [ 'type' => 'boolean', 'default' => false ],
					'maxSnippet'        => [ 'type' => 'number', 'default' => -1 ],
					'maxVideoPreview'   => [ 'type' => 'number', 'default' => -1 ],
					'maxImagePreview'   => [ 'type' => 'string', 'default' => 'large' ]
				],
				'noIndexEmptyCat'              => [ 'type' => 'boolean', 'default' => true ],
				'removeStopWords'              => [ 'type' => 'boolean', 'default' => false ],
				'useKeywords'                  => [ 'type' => 'boolean', 'default' => false ],
				'keywordsLooking'              => [ 'type' => 'boolean', 'default' => true ],
				'useCategoriesForMetaKeywords' => [ 'type' => 'boolean', 'default' => false ],
				'useTagsForMetaKeywords'       => [ 'type' => 'boolean', 'default' => false ],
				'dynamicallyGenerateKeywords'  => [ 'type' => 'boolean', 'default' => false ],
				'pagedFormat'                  => [ 'type' => 'string', 'default' => '#separator_sa Page #page_number', 'localized' => true ],
				'runShortcodes'                => [ 'type' => 'boolean', 'default' => false ],
				'crawlCleanup'                 => [
					'enable' => [ 'type' => 'boolean', 'default' => false ],
					'feeds'  => [
						'global'         => [ 'type' => 'boolean', 'default' => true ],
						'globalComments' => [ 'type' => 'boolean', 'default' => false ],
						'staticBlogPage' => [ 'type' => 'boolean', 'default' => true ],
						'authors'        => [ 'type' => 'boolean', 'default' => true ],
						'postComments'   => [ 'type' => 'boolean', 'default' => false ],
						'search'         => [ 'type' => 'boolean', 'default' => false ],
						'attachments'    => [ 'type' => 'boolean', 'default' => false ],
						'archives'       => [
							'all'      => [ 'type' => 'boolean', 'default' => false ],
							'included' => [ 'type' => 'array', 'default' => [] ],
						],
						'taxonomies'     => [
							'all'      => [ 'type' => 'boolean', 'default' => false ],
							'included' => [ 'type' => 'array', 'default' => [ 'category' ] ],
						],
						'atom'           => [ 'type' => 'boolean', 'default' => false ],
						'rdf'            => [ 'type' => 'boolean', 'default' => false ],
						'paginated'      => [ 'type' => 'boolean', 'default' => false ]
					]
				],
				'unwantedBots'                 => [
					'all'      => [ 'type' => 'boolean', 'default' => false ],
					'settings' => [
						'googleAdsBot'             => [ 'type' => 'boolean', 'default' => false ],
						'openAiGptBot'             => [ 'type' => 'boolean', 'default' => false ],
						'commonCrawlCcBot'         => [ 'type' => 'boolean', 'default' => false ],
						'googleGeminiVertexAiBots' => [ 'type' => 'boolean', 'default' => false ]
					]
				],
				'searchCleanup'                => [
					'enable'   => [ 'type' => 'boolean', 'default' => false ],
					'settings' => [
						'maxAllowedNumberOfChars' => [ 'type' => 'number', 'default' => 50 ],
						'emojisAndSymbols'        => [ 'type' => 'boolean', 'default' => false ],
						'commonPatterns'          => [ 'type' => 'boolean', 'default' => false ],
						'redirectPrettyUrls'      => [ 'type' => 'boolean', 'default' => false ],
						'preventCrawling'         => [ 'type' => 'boolean', 'default' => false ]
					]
				],
				'blockArgs'                    => [
					'enable'                => [ 'type' => 'boolean', 'default' => false ],
					'optimizeUtmParameters' => [ 'type' => 'boolean', 'default' => false ],
					'logsRetention'         => [ 'type' => 'string', 'default' => '{"label":"1 week","value":"week"}' ]
				],
				'removeCategoryBase'           => [ 'type' => 'boolean', 'default' => false ]
			],
			'archives' => [
				'author' => [
					'show'            => [ 'type' => 'boolean', 'default' => true ],
					'title'           => [ 'type' => 'string', 'localized' => true, 'default' => '#author_name #separator_sa #site_title' ],
					'metaDescription' => [ 'type' => 'string', 'localized' => true, 'default' => '#author_bio' ],
					'advanced'        => [
						'robotsMeta'                => [
							'default'         => [ 'type' => 'boolean', 'default' => true ],
							'noindex'         => [ 'type' => 'boolean', 'default' => false ],
							'nofollow'        => [ 'type' => 'boolean', 'default' => false ],
							'noarchive'       => [ 'type' => 'boolean', 'default' => false ],
							'noimageindex'    => [ 'type' => 'boolean', 'default' => false ],
							'notranslate'     => [ 'type' => 'boolean', 'default' => false ],
							'nosnippet'       => [ 'type' => 'boolean', 'default' => false ],
							'noodp'           => [ 'type' => 'boolean', 'default' => false ],
							'maxSnippet'      => [ 'type' => 'number', 'default' => -1 ],
							'maxVideoPreview' => [ 'type' => 'number', 'default' => -1 ],
							'maxImagePreview' => [ 'type' => 'string', 'default' => 'large' ]
						],
						'showDateInGooglePreview'   => [ 'type' => 'boolean', 'default' => true ],
						'showPostThumbnailInSearch' => [ 'type' => 'boolean', 'default' => true ],
						'showMetaBox'               => [ 'type' => 'boolean', 'default' => true ],
						'keywords'                  => [ 'type' => 'string', 'localized' => true ]
					]
				],
				'date'   => [
					'show'            => [ 'type' => 'boolean', 'default' => true ],
					'title'           => [ 'type' => 'string', 'localized' => true, 'default' => '#archive_date #separator_sa #site_title' ],
					'metaDescription' => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
					'advanced'        => [
						'robotsMeta'                => [
							'default'         => [ 'type' => 'boolean', 'default' => true ],
							'noindex'         => [ 'type' => 'boolean', 'default' => false ],
							'nofollow'        => [ 'type' => 'boolean', 'default' => false ],
							'noarchive'       => [ 'type' => 'boolean', 'default' => false ],
							'noimageindex'    => [ 'type' => 'boolean', 'default' => false ],
							'notranslate'     => [ 'type' => 'boolean', 'default' => false ],
							'nosnippet'       => [ 'type' => 'boolean', 'default' => false ],
							'noodp'           => [ 'type' => 'boolean', 'default' => false ],
							'maxSnippet'      => [ 'type' => 'number', 'default' => -1 ],
							'maxVideoPreview' => [ 'type' => 'number', 'default' => -1 ],
							'maxImagePreview' => [ 'type' => 'string', 'default' => 'large' ]
						],
						'showDateInGooglePreview'   => [ 'type' => 'boolean', 'default' => true ],
						'showPostThumbnailInSearch' => [ 'type' => 'boolean', 'default' => true ],
						'showMetaBox'               => [ 'type' => 'boolean', 'default' => true ],
						'keywords'                  => [ 'type' => 'string', 'localized' => true ]
					]
				],
				'search' => [
					'show'            => [ 'type' => 'boolean', 'default' => false ],
					'title'           => [ 'type' => 'string', 'localized' => true, 'default' => '#search_term #separator_sa #site_title' ],
					'metaDescription' => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
					'advanced'        => [
						'robotsMeta'                => [
							'default'         => [ 'type' => 'boolean', 'default' => false ],
							'noindex'         => [ 'type' => 'boolean', 'default' => true ],
							'nofollow'        => [ 'type' => 'boolean', 'default' => false ],
							'noarchive'       => [ 'type' => 'boolean', 'default' => false ],
							'noimageindex'    => [ 'type' => 'boolean', 'default' => false ],
							'notranslate'     => [ 'type' => 'boolean', 'default' => false ],
							'nosnippet'       => [ 'type' => 'boolean', 'default' => false ],
							'noodp'           => [ 'type' => 'boolean', 'default' => false ],
							'maxSnippet'      => [ 'type' => 'number', 'default' => -1 ],
							'maxVideoPreview' => [ 'type' => 'number', 'default' => -1 ],
							'maxImagePreview' => [ 'type' => 'string', 'default' => 'large' ]
						],
						'showDateInGooglePreview'   => [ 'type' => 'boolean', 'default' => true ],
						'showPostThumbnailInSearch' => [ 'type' => 'boolean', 'default' => true ],
						'showMetaBox'               => [ 'type' => 'boolean', 'default' => true ],
						'keywords'                  => [ 'type' => 'string', 'localized' => true ]
					]
				]
			]
		],
		'searchStatistics' => [
			'postTypes' => [
				'all'      => [ 'type' => 'boolean', 'default' => true ],
				'included' => [ 'type' => 'array', 'default' => [ 'post', 'page' ] ],
			]
		],
		'tools'            => [
			'robots'       => [
				'enable'         => [ 'type' => 'boolean', 'default' => false ],
				'rules'          => [ 'type' => 'array', 'default' => [] ],
				'robotsDetected' => [ 'type' => 'boolean', 'default' => true ],
			],
			'importExport' => [
				'backup' => [
					'lastTime' => [ 'type' => 'string' ],
					'data'     => [ 'type' => 'string' ],
				]
			]
		],
		'deprecated'       => [
			'breadcrumbs'      => [
				'enable' => [ 'type' => 'boolean', 'default' => true ]
			],
			'searchAppearance' => [
				'global'   => [
					'descriptionFormat' => [ 'type' => 'string' ],
					'schema'            => [
						'enableSchemaMarkup' => [ 'type' => 'boolean', 'default' => true ]
					]
				],
				'advanced' => [
					'autogenerateDescriptions'               => [ 'type' => 'boolean', 'default' => true ],
					'runShortcodesInDescription'             => [ 'type' => 'boolean', 'default' => true ], // TODO: Remove this in a future update.
					'useContentForAutogeneratedDescriptions' => [ 'type' => 'boolean', 'default' => false ],
					'excludePosts'                           => [ 'type' => 'array', 'default' => [] ],
					'excludeTerms'                           => [ 'type' => 'array', 'default' => [] ],
					'noPaginationForCanonical'               => [ 'type' => 'boolean', 'default' => true ]
				]
			],
			'sitemap'          => [
				'general' => [
					'advancedSettings' => [
						'dynamic' => [ 'type' => 'boolean', 'default' => true ]
					]
				]
			]
		],
		'writingAssistant' => [
			'postTypes' => [
				'all'      => [ 'type' => 'boolean', 'default' => true ],
				'included' => [ 'type' => 'array', 'default' => [ 'post', 'page' ] ],
			]
		]
		// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	];

	/**
	 * The Construct method.
	 *
	 * @since 4.0.0
	 *
	 * @param string $optionsName An array of options.
	 */
	public function __construct( $optionsName = 'aioseo_options' ) {
		$this->optionsName = $optionsName;

		$this->init();

		add_action( 'shutdown', [ $this, 'save' ] );
	}

	/**
	 * Initializes the options.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function init() {
		$this->setInitialDefaults();
		add_action( 'init', [ $this, 'translateDefaults' ] );

		$this->setDbOptions();

		add_action( 'wp_loaded', [ $this, 'maybeFlushRewriteRules' ] );
	}

	/**
	 * Sets the DB options to the class after merging in new defaults and dropping unknown values.
	 *
	 * @since 4.0.14
	 *
	 * @return void
	 */
	public function setDbOptions() {
		// Refactor options.
		$this->defaultsMerged = array_replace_recursive( $this->defaults, $this->defaultsMerged );

		$dbOptions = $this->getDbOptions( $this->optionsName );

		$options = array_replace_recursive(
			$this->defaultsMerged,
			$this->addValueToValuesArray( $this->defaultsMerged, $dbOptions )
		);

		aioseo()->core->optionsCache->setOptions( $this->optionsName, apply_filters( 'aioseo_get_options', $options ) );

		// Get the localized options.
		$dbOptionsLocalized = get_option( $this->optionsName . '_localized' );
		if ( empty( $dbOptionsLocalized ) ) {
			$dbOptionsLocalized = [];
		}
		$this->localized = $dbOptionsLocalized;
	}

	/**
	 * Sets the initial defaults that can't be defined in the property because of PHP 5.4.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	protected function setInitialDefaults() {
		static $hasInitialized = false;
		if ( $hasInitialized ) {
			return;
		}

		$hasInitialized = true;

		$this->defaults['searchAppearance']['global']['schema']['organizationLogo']['default'] = aioseo()->helpers->getSiteLogoUrl() ? aioseo()->helpers->getSiteLogoUrl() : '';

		$this->defaults['advanced']['emailSummary']['recipients']['default'] = [
			[
				'email'     => get_bloginfo( 'admin_email' ),
				'frequency' => 'monthly',
			]
		];
	}

	/**
	 * For our defaults array, some options need to be translated, so we do that here.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function translateDefaults() {
		static $hasInitialized = false;
		if ( $hasInitialized ) {
			return;
		}

		$hasInitialized = true;

		$default = sprintf( '{"label":"%1$s","value":"default"}', __( 'default', 'all-in-one-seo-pack' ) );
		$this->defaults['sitemap']['general']['advancedSettings']['priority']['homePage']['priority']['default']    = $default;
		$this->defaults['sitemap']['general']['advancedSettings']['priority']['homePage']['frequency']['default']   = $default;
		$this->defaults['sitemap']['general']['advancedSettings']['priority']['postTypes']['priority']['default']   = $default;
		$this->defaults['sitemap']['general']['advancedSettings']['priority']['postTypes']['frequency']['default']  = $default;
		$this->defaults['sitemap']['general']['advancedSettings']['priority']['taxonomies']['priority']['default']  = $default;
		$this->defaults['sitemap']['general']['advancedSettings']['priority']['taxonomies']['frequency']['default'] = $default;

		$this->defaults['breadcrumbs']['homepageLabel']['default']      = __( 'Home', 'all-in-one-seo-pack' );
		$this->defaults['breadcrumbs']['archiveFormat']['default']      = sprintf( '%1$s #breadcrumb_archive_post_type_name', __( 'Archives for', 'all-in-one-seo-pack' ) );
		$this->defaults['breadcrumbs']['searchResultFormat']['default'] = sprintf( '%1$s \'#breadcrumb_search_string\'', __( 'Search Results for', 'all-in-one-seo-pack' ) );
		$this->defaults['breadcrumbs']['errorFormat404']['default']     = __( '404 - Page Not Found', 'all-in-one-seo-pack' );
	}

	/**
	 * Sanitizes, then saves the options to the database.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $options An array of options to sanitize, then save.
	 * @return void
	 */
	public function sanitizeAndSave( $options ) {
		$sitemapOptions                  = ! empty( $options['sitemap'] ) ? $options['sitemap'] : null;
		$oldSitemapOptions               = aioseo()->options->sitemap->all();
		$generalSitemapOptions           = ! empty( $options['sitemap']['general'] ) ? $options['sitemap']['general'] : null;
		$oldGeneralSitemapOptions        = aioseo()->options->sitemap->general->all();
		$deprecatedGeneralSitemapOptions = ! empty( $options['deprecated']['sitemap']['general'] )
				? $options['deprecated']['sitemap']['general']
				: null;
		$oldDeprecatedGeneralSitemapOptions = aioseo()->options->deprecated->sitemap->general->all();
		$oldPhoneOption                     = aioseo()->options->searchAppearance->global->schema->phone;
		$phoneNumberOptions                 = isset( $options['searchAppearance']['global']['schema']['phone'] )
				? $options['searchAppearance']['global']['schema']['phone']
				: null;
		$oldHtmlSitemapUrl = aioseo()->options->sitemap->html->pageUrl;
		$logsRetention     = isset( $options['searchAppearance']['advanced']['blockArgs']['logsRetention'] ) ? $options['searchAppearance']['advanced']['blockArgs']['logsRetention'] : null;
		$oldLogsRetention  = aioseo()->options->searchAppearance->advanced->blockArgs->logsRetention;

		// Remove category base.
		$removeCategoryBase    = isset( $options['searchAppearance']['advanced']['removeCategoryBase'] ) ? $options['searchAppearance']['advanced']['removeCategoryBase'] : null;
		$removeCategoryBaseOld = aioseo()->options->searchAppearance->advanced->removeCategoryBase;

		$options = $this->maybeRemoveUnfilteredHtmlFields( $options );

		$this->init();

		if ( ! is_array( $options ) ) {
			return;
		}

		$this->sanitizeEmailSummary( $options );

		// First, recursively replace the new options into the cached state.
		// It's important we use the helper method since we want to replace populated arrays with empty ones if needed (when a setting was cleared out).
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$dbOptions     = aioseo()->helpers->arrayReplaceRecursive(
			$cachedOptions,
			$this->addValueToValuesArray( $cachedOptions, $options, [], true )
		);

		// Now, we must also intersect both arrays to delete any individual keys that were unset.
		// We must do this because, while arrayReplaceRecursive will update the values for keys or empty them out,
		// it will keys that aren't present in the replacement array unaffected in the target array.
		$dbOptions = aioseo()->helpers->arrayIntersectRecursive(
			$dbOptions,
			$this->addValueToValuesArray( $cachedOptions, $options, [], true ),
			'value'
		);

		if ( isset( $options['social']['profiles']['additionalUrls'] ) ) {
			$dbOptions['social']['profiles']['additionalUrls'] = preg_replace( '/\h/', "\n", (string) $options['social']['profiles']['additionalUrls'] );
		}

		$newOptions = ! empty( $options['sitemap']['html'] ) ? $options['sitemap']['html'] : null;
		if ( ! empty( $newOptions ) && aioseo()->options->sitemap->html->enable ) {
			$newOptions = ! empty( $options['sitemap']['html'] ) ? $options['sitemap']['html'] : null;

			$pageUrl = wp_parse_url( $newOptions['pageUrl'] );
			$path    = ! empty( $pageUrl['path'] ) ? untrailingslashit( $pageUrl['path'] ) : '';
			if ( $path ) {
				$existingPage = get_page_by_path( $path, OBJECT );
				if ( is_object( $existingPage ) ) {
					// If the page exists, don't override the previous URL.
					$options['sitemap']['html']['pageUrl'] = $oldHtmlSitemapUrl;
				}
			}
		}

		// Update the cache state.
		aioseo()->core->optionsCache->setOptions( $this->optionsName, $dbOptions );

		// Update localized options.
		update_option( $this->optionsName . '_localized', $this->localized );

		// Finally, save the new values to the DB.
		$this->save( true );

		// If phone settings have changed, let's see if we need to dump the phone number notice.
		if (
			$phoneNumberOptions &&
			$phoneNumberOptions !== $oldPhoneOption
		) {
			$notification = Models\Notification::getNotificationByName( 'v3-migration-schema-number' );
			if ( $notification->exists() ) {
				Models\Notification::deleteNotificationByName( 'v3-migration-schema-number' );
			}
		}

		// If sitemap settings were changed, static files need to be regenerated.
		if (
			! empty( $deprecatedGeneralSitemapOptions ) &&
			! empty( $generalSitemapOptions )
		) {
			if (
				(
					aioseo()->helpers->arraysDifferent( $oldGeneralSitemapOptions, $generalSitemapOptions ) ||
					aioseo()->helpers->arraysDifferent( $oldDeprecatedGeneralSitemapOptions, $deprecatedGeneralSitemapOptions )
				) &&
				$generalSitemapOptions['advancedSettings']['enable'] &&
				! $deprecatedGeneralSitemapOptions['advancedSettings']['dynamic']
			) {
				aioseo()->sitemap->scheduleRegeneration();
			}
		}

		// Add or remove schedule for clearing crawl cleanup logs.
		if ( ! empty( $logsRetention ) && $oldLogsRetention !== $logsRetention ) {
			aioseo()->crawlCleanup->scheduleClearingLogs();
		}

		if ( ! empty( $sitemapOptions ) ) {
			aioseo()->searchStatistics->sitemap->maybeSync( $oldSitemapOptions, $sitemapOptions );
		}

		if (
			null !== $removeCategoryBase &&
			$removeCategoryBase !== $removeCategoryBaseOld
		) {
			aioseo()->options->flushRewriteRules();
		}

		// This is required in order for the Pro options to be refreshed before they save data again.
		$this->refresh();
	}

	/**
	 * Sanitizes the `emailSummary` option.
	 *
	 * @since 4.7.2
	 *
	 * @param  array $options All options, passed by reference.
	 * @return void
	 */
	private function sanitizeEmailSummary( &$options ) {
		foreach ( ( $options['advanced']['emailSummary']['recipients'] ?? [] ) as $k => &$recipient ) {
			$recipient['email'] = is_email( $recipient['email'] );

			// Remove empty emails.
			if ( empty( $recipient['email'] ) ) {
				unset( $options['advanced']['emailSummary']['recipients'][ $k ] );

				continue;
			}

			// Remove duplicate emails with the same frequency.
			foreach ( $options['advanced']['emailSummary']['recipients'] as $k2 => $recipient2 ) {
				if (
					$k !== $k2 &&
					$recipient['email'] === $recipient2['email'] &&
					$recipient['frequency'] === $recipient2['frequency']
				) {
					unset( $options['advanced']['emailSummary']['recipients'][ $k ] );

					break;
				}
			}
		}
	}

	/**
	 * If the user does not have access to unfiltered HTML, we need to remove them from saving.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $options An array of options.
	 * @return array          An array of options.
	 */
	private function maybeRemoveUnfilteredHtmlFields( $options ) {
		if ( current_user_can( 'unfiltered_html' ) ) {
			return $options;
		}

		if (
			! empty( $options['webmasterTools'] ) &&
			isset( $options['webmasterTools']['miscellaneousVerification'] )
		) {
			unset( $options['webmasterTools']['miscellaneousVerification'] );
		}

		if (
			! empty( $options['rssContent'] ) &&
			isset( $options['rssContent']['before'] )
		) {
			unset( $options['rssContent']['before'] );
		}

		if (
			! empty( $options['rssContent'] ) &&
			isset( $options['rssContent']['after'] )
		) {
			unset( $options['rssContent']['after'] );
		}

		return $options;
	}

	/**
	 * Indicate we need to flush rewrite rules on next load.
	 *
	 * @since 4.0.17
	 *
	 * @return void
	 */
	public function flushRewriteRules() {
		update_option( 'aioseo_flush_rewrite_rules_flag', true );
	}

	/**
	 * Flush rewrite rules if needed.
	 *
	 * @since 4.0.17
	 *
	 * @return void
	 */
	public function maybeFlushRewriteRules() {
		if ( get_option( 'aioseo_flush_rewrite_rules_flag' ) ) {
			flush_rewrite_rules();
			delete_option( 'aioseo_flush_rewrite_rules_flag' );
		}
	}
}Common/QueryArgs/CrawlCleanup.php000064400000024060151536241200013010 0ustar00<?php
namespace AIOSEO\Plugin\Common\QueryArgs;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Class to control Crawl Cleanup.
 *
 * @since 4.5.8
 */
class CrawlCleanup {

	/**
	 * Construct method.
	 *
	 * @since 4.5.8
	 */
	public function __construct() {
		// Add action to clear crawl cleanup logs.
		add_action( 'aioseo_crawl_cleanup_clear_logs', [ $this, 'clearLogs' ] );

		if ( aioseo()->options->searchAppearance->advanced->blockArgs->optimizeUtmParameters ) {
			add_action( 'template_redirect', [ $this, 'maybeRedirectUtmParameters' ], 50 );
		}
	}

	/**
	 * Redirects the UTM parameters to with (#) equivalent.
	 *
	 * @since 4.8.0
	 *
	 * @return void
	 */
	public function maybeRedirectUtmParameters() {
		$requestUri = aioseo()->helpers->getRequestUrl();
		if ( empty( $requestUri ) ) {
			return;
		}

		$parsed = wp_parse_url( $requestUri );
		if ( empty( $parsed['query'] ) ) {
			return;
		}

		$args = [];
		wp_parse_str( $parsed['query'], $args );

		// Reset query to reconstruct without utm_ parameters.
		$parsed['query'] = '';

		// Initialize the fragment key if it's not set.
		if ( ! isset( $parsed['fragment'] ) ) {
			$parsed['fragment'] = '';
		}

		// Check if there are any utm_ parameters and redirect accordingly.
		$utmFound = false;
		foreach ( $args as $key => $value ) {
			$keyValue = $key . '=' . $value;
			if ( 0 === stripos( $key, 'utm_' ) ) {
				$utmFound = true;
				// Rebuild the URL with # instead of ?.
				$parsed['fragment'] .= ! empty( $parsed['fragment'] ) ? '&' . $keyValue : $keyValue;
			} else {
				$parsed['query'] .= ! empty( $parsed['query'] ) ? '&' . $keyValue : $keyValue;
			}
		}

		if ( $utmFound ) {
			aioseo()->helpers->redirect( aioseo()->helpers->buildUrl( $parsed ), 301, 'Optimize UTM parameters' );
		}
	}

	/**
	 * Schedule clearing of the logs.
	 *
	 * @since 4.5.8
	 *
	 * @return void
	 */
	public function scheduleClearingLogs() {
		aioseo()->actionScheduler->unschedule( 'aioseo_crawl_cleanup_clear_logs' );
		$optionLength = json_decode( aioseo()->options->searchAppearance->advanced->blockArgs->logsRetention )->value;
		if (
			aioseo()->options->searchAppearance->advanced->blockArgs->enable &&
			'forever' !== $optionLength
		) {
			aioseo()->actionScheduler->scheduleRecurrent( 'aioseo_crawl_cleanup_clear_logs', 0, HOUR_IN_SECONDS );
		}
	}

	/**
	 * Clears the logs.
	 *
	 * @since 4.5.8
	 *
	 * @return void
	 */
	public function clearLogs() {
		$optionLength = json_decode( aioseo()->options->searchAppearance->advanced->blockArgs->logsRetention )->value;
		if ( 'forever' === $optionLength ) {
			return;
		}

		$date = gmdate( 'Y-m-d H:i:s', strtotime( '-1 ' . $optionLength ) );
		aioseo()->core->db
			->delete( 'aioseo_crawl_cleanup_logs' )
			->where( 'updated <', $date )
			->run();
	}

	/**
	 * Fetch Crawl Cleanup Logs.
	 *
	 * @since 4.5.8
	 *
	 * @param  \WP_REST_Request  $request The REST Request.
	 * @return \WP_REST_Response          The response.
	 */
	public static function fetchLogs( $request ) {
		$filter            = $request->get_param( 'filter' );
		$body              = $request->get_json_params();
		$orderByUnblocked  = ! empty( $body['orderBy'] ) ? sanitize_text_field( $body['orderBy'] ) : 'logs.updated';
		$orderByBlocked    = ! empty( $body['orderBy'] ) ? sanitize_text_field( $body['orderBy'] ) : 'b.id';
		$orderDir          = ! empty( $body['orderDir'] ) && ! empty( $body['orderBy'] ) ? strtoupper( sanitize_text_field( $body['orderDir'] ) ) : 'DESC';
		$limit             = ! empty( $body['limit'] ) ? intval( $body['limit'] ) : aioseo()->settings->tablePagination['queryArgs'];
		$offset            = ! empty( $body['offset'] ) ? intval( $body['offset'] ) : 0;
		$searchTerm        = ! empty( $body['searchTerm'] ) ? sanitize_text_field( $body['searchTerm'] ) : null;
		$keyValueSeparator = Models\CrawlCleanupBlockedArg::getKeyValueSeparator();
		$dateFormat        = get_option( 'date_format' );
		$timeFormat        = get_option( 'time_format' );
		$dateTimeFormat    = $dateFormat . ' ' . $timeFormat;

		// Query to get Arg Logs (unblocked) and the total.
		$queryUnblocked = aioseo()->core->db
			->start( 'aioseo_crawl_cleanup_logs as logs' )
			->select( ' logs.id,
						logs.slug,
						logs.key,
						logs.value,
						logs.hits,
						logs.updated' )
			->leftJoin( 'aioseo_crawl_cleanup_blocked_args as blocked',
				'blocked.key_value_hash = sha1(logs.key) OR
					blocked.key_value_hash = sha1(concat(logs.key, "' . $keyValueSeparator . '", logs.value))' )
			->limit( $limit, $offset );

		if ( ! empty( $searchTerm ) ) {
			// Apply escape to the search term.
			$searchTerm = esc_sql( aioseo()->core->db->db->esc_like( $searchTerm ) );
			$where = '
				(
					logs.slug LIKE \'%' . $searchTerm . '%\' OR
					logs.slug LIKE \'%' . str_replace( '%20', '-', $searchTerm ) . '%\' OR
					logs.slug LIKE \'%' . str_replace( '%20', '+', $searchTerm ) . '%\'
				)
			';

			$queryUnblocked->whereRaw( $where );
		}

		$queryUnblocked->where( 'blocked.id', null );
		$queryUnblocked->orderBy( "$orderByUnblocked $orderDir" );

		$rowsUnblocked = $queryUnblocked->run( false )->result();
		$totalUnblocked = $queryUnblocked->reset( [ 'limit' ] )->count();

		// Test logs (unblocked) to see if have some regex block.
		$regexMatches = [];
		foreach ( $rowsUnblocked as $unblocked ) {
			$blockedRegex = Models\CrawlCleanupBlockedArg::matchRegex( $unblocked->key, $unblocked->value );
			if ( $blockedRegex->exists() ) {
				$regexMatches[ $unblocked->id ] = $blockedRegex->regex;
			}
		}

		// Query to get Blocked Args and the total.
		$queryBlocked = aioseo()->core->db
			->select( ' b.id,
						b.key,
						b.value,
						b.regex,
						b.hits,
						b.updated' )
			->start( 'aioseo_crawl_cleanup_blocked_args as b' )
			->limit( $limit, $offset );

		if ( ! empty( $searchTerm ) ) {
			// Escape (esc_like) has already been applied.
			$searchTerms = [
				$searchTerm,
				str_replace( '%20', '-', $searchTerm ),
				str_replace( '%20', '+', $searchTerm )
			];

			$comparisons = [
				'b.key',
				'b.value',
				'b.regex',
				'CONCAT(b.key, \'' . $keyValueSeparator . '\', IF(b.value, b.value, \'*\'))'
			];

			$where = '';
			foreach ( $comparisons as $comparison ) {
				foreach ( $searchTerms as $s ) {
					if ( ! empty( $where ) ) {
						$where .= ' OR ';
					}

					$where .= aioseo()->db->db->prepare( " $comparison LIKE %s ", '%' . $s . '%' );
				}
			}

			$where = "( $where )";
			$queryBlocked->whereRaw( $where );
		}

		$queryBlocked->orderBy( "$orderByBlocked $orderDir" );

		$rowsBlocked = $queryBlocked->run( false )->result();
		$totalBlocked = $queryBlocked->reset( [ 'limit' ] )->count();

		switch ( $filter ) {
			case 'blocked':
				$total = $totalBlocked;
				$rows = $rowsBlocked;
				break;
			case 'unblocked':
				$total = $totalUnblocked;
				$rows = $rowsUnblocked;
				break;
			default:
				return new \WP_REST_Response( [
					'success' => false
				], 404 );
		}

		foreach ( $rows as $row ) {
			$row->updated = get_date_from_gmt( $row->updated, $dateTimeFormat );
		}

		return new \WP_REST_Response( [
			'success' => true,
			'rows'    => $rows,
			'regex'   => $regexMatches,
			'totals'  => [
				'total' => $total,
				'pages' => 0 === $total ? 1 : ceil( $total / $limit ),
				'page'  => 0 === $offset ? 1 : ( $offset / $limit ) + 1
			],
			'filters' => [
				[
					'slug'   => 'unblocked',
					'name'   => __( 'Unblocked', 'all-in-one-seo-pack' ),
					'count'  => $totalUnblocked,
					'active' => 'unblocked' === $filter
				],
				[
					'slug'   => 'blocked',
					'name'   => __( 'Blocked', 'all-in-one-seo-pack' ),
					'count'  => $totalBlocked,
					'active' => 'blocked' === $filter
				]
			]
		], 200 );
	}

	/**
	 * Set block Arg Query.
	 *
	 * @since 4.5.8
	 *
	 * @param  \WP_REST_Request  $request The REST Request.
	 * @return \WP_REST_Response          The response.
	 */
	public static function blockArg( $request ) {
		$body      = $request->get_json_params();
		$return    = true;
		$listSaved = [];
		$exists    = [];
		$error     = 0;

		try {
			foreach ( $body as $block ) {
				if ( $block ) {
					$blocked = Models\CrawlCleanupBlockedArg::getByKeyValue( $block['key'], $block['value'] );
					if ( ! $blocked->exists() && ! empty( $block['regex'] ) ) {
						$blocked = Models\CrawlCleanupBlockedArg::getByRegex( $block['regex'] );
					}

					if ( $blocked->exists() ) {
						$exists[] = [
							'key'   => $block['key'],
							'value' => $block['value']
						];

						$keyValue = sha1( Models\CrawlCleanupBlockedArg::getKeyValueString( $block['key'], $block['value'] ) );
						if ( ! in_array( $keyValue, $listSaved, true ) ) {
							$return = false;
							$error  = 1;
						}

						continue;
					}

					$blocked = new Models\CrawlCleanupBlockedArg();
					$blocked->set( $block );
					$blocked->save();

					$listSaved[] = $blocked->key_value_hash;
				}
			}
		} catch ( \Throwable $th ) {
			$return = false;
		}

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

	/**
	 * Delete Blocked Arg.
	 *
	 * @since 4.5.8
	 *
	 * @param  \WP_REST_Request  $request The REST Request.
	 * @return \WP_REST_Response          The response.
	 */
	public static function deleteBlocked( $request ) {
		$body = $request->get_json_params();
		$return = true;

		try {
			foreach ( $body as $block ) {
				$blocked = new Models\CrawlCleanupBlockedArg( $block );
				if ( $blocked->exists() ) {
					$blocked->delete();
				}
			}
		} catch ( \Throwable $th ) {
			$return = false;
		}

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

	/**
	 * Delete Log.
	 *
	 * @since 4.5.8
	 *
	 * @param  \WP_REST_Request  $request The REST Request.
	 * @return \WP_REST_Response          The response.
	 */
	public static function deleteLog( $request ) {
		$body = $request->get_json_params();
		$return = true;

		try {
			foreach ( $body as $block ) {
				$log = new Models\CrawlCleanupLog( $block );
				if ( $log->exists() ) {
					$log->delete();
				}
			}
		} catch ( \Throwable $th ) {
			$return = false;
		}

		return new \WP_REST_Response( [
			'success' => $return
		], 200 );
	}
}Common/Rss.php000064400000034711151536241200007261 0ustar00<?php
namespace AIOSEO\Plugin\Common;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Adds content before or after posts in the RSS feed.
 *
 * @since 4.0.0
 */
class Rss {
	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		if ( is_admin() ) {
			return;
		}

		add_filter( 'the_content_feed', [ $this, 'addRssContent' ] );
		add_filter( 'the_excerpt_rss', [ $this, 'addRssContentExcerpt' ] );

		// If Crawl Cleanup is disabled, return early.
		if ( ! aioseo()->options->searchAppearance->advanced->crawlCleanup->enable ) {
			return;
		}

		// Control which feed links are visible.
		remove_action( 'wp_head', 'feed_links_extra', 3 );
		add_action( 'wp_head', [ $this, 'rssFeedLinks' ], 3 );

		if ( ! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->global ) {
			add_filter( 'feed_links_show_posts_feed', '__return_false' );
		}

		if ( ! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->globalComments ) {
			add_filter( 'feed_links_show_comments_feed', '__return_false' );
		}

		// Disable feeds that we no longer want on this site.
		add_action( 'wp', [ $this, 'disableFeeds' ], -1000 );
	}

	/**
	 * Adds content before or after the RSS excerpt.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $content The post excerpt.
	 * @return string          The post excerpt with prepended/appended content.
	 */
	public function addRssContentExcerpt( $content ) {
		return $this->addRssContent( $content, 'excerpt' );
	}

	/**
	 * Adds content before or after the RSS post.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $content The post content.
	 * @param  string $type    Type of feed.
	 * @return string          The post content with prepended/appended content.
	 */
	public function addRssContent( $content, $type = 'complete' ) {
		$content = trim( $content );
		if ( empty( $content ) ) {
			return '';
		}

		if ( is_feed() ) {
			global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			$isHome = is_home();
			if ( $isHome ) {
				// If this feed is for the static blog page, we must temporarily set "is_home" to false.
				// Otherwise any getPost() calls will return the blog page object for every post in the feed.
				$wp_query->is_home = false; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			}

			$before = aioseo()->tags->replaceTags( aioseo()->options->rssContent->before, get_the_ID() );
			$after  = aioseo()->tags->replaceTags( aioseo()->options->rssContent->after, get_the_ID() );

			if ( $before || $after ) {
				if ( 'excerpt' === $type ) {
					$content = wpautop( $content );
				}
				$content = aioseo()->helpers->decodeHtmlEntities( $before ) . $content . aioseo()->helpers->decodeHtmlEntities( $after );
			}

			// Set back to the original value.
			$wp_query->is_home = $isHome; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		}

		return $content;
	}

	/**
	 * Disable feeds we don't want to have on this site.
	 *
	 * @since 4.2.1
	 *
	 * @return void
	 */
	public function disableFeeds() {
		$archives = aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->archives->included;

		if ( BuddyPressIntegration::isComponentPage() ) {
			list( $postType, $suffix ) = explode( '_', aioseo()->standalone->buddyPress->component->templateType );

			if (
				'feed' === $suffix &&
				! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->archives->all &&
				! in_array( $postType, $archives, true )
			) {
				$this->redirectRssFeed( BuddyPressIntegration::getComponentArchiveUrl( 'activity' ) );
			}
		}

		if ( ! is_feed() ) {
			return;
		}

		$rssFeed = get_query_var( 'feed' );
		$homeUrl = get_home_url();

		// Atom feed.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->atom &&
			'atom' === $rssFeed
		) {
			$this->redirectRssFeed( $homeUrl );
		}

		// RDF/RSS 1.0 feed.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->rdf &&
			'rdf' === $rssFeed
		) {
			$this->redirectRssFeed( $homeUrl );
		}

		// Global feed.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->global &&
			[ 'feed' => 'feed' ] === $GLOBALS['wp_query']->query
		) {
			$this->redirectRssFeed( $homeUrl );
		}

		// Global comments feed.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->globalComments &&
			is_comment_feed() &&
			! ( is_singular() || is_attachment() )
		) {
			$this->redirectRssFeed( $homeUrl );
		}

		// Static blog page feed.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->staticBlogPage &&
			aioseo()->helpers->getBlogPageId() === get_queried_object_id()
		) {
			$this->redirectRssFeed( $homeUrl );
		}

		// Post comment feeds.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->postComments &&
			is_comment_feed() &&
			is_singular()
		) {
			$this->redirectRssFeed( $homeUrl );
		}

		// Attachment feeds.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->attachments &&
			'feed' === $rssFeed &&
			get_query_var( 'attachment', false )
		) {
			$this->redirectRssFeed( $homeUrl );
		}

		// Author feeds.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->authors &&
			is_author()
		) {
			$this->redirectRssFeed( get_author_posts_url( (int) get_query_var( 'author' ) ) );
		}

		// Search results feed.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->search &&
			is_search()
		) {
			$this->redirectRssFeed( esc_url( trailingslashit( $homeUrl ) . '?s=' . get_search_query() ) );
		}

		// All post types.
		$postType = $this->getTheQueriedPostType();
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->archives->all &&
			! in_array( $postType, $archives, true ) &&
			is_post_type_archive()
		) {
			$this->redirectRssFeed( get_post_type_archive_link( $postType ) );
		}

		// All taxonomies.
		$taxonomies = aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->taxonomies->included;
		$term       = get_queried_object();
		if (
			is_a( $term, 'WP_Term' ) &&
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->taxonomies->all &&
			! in_array( $term->taxonomy, $taxonomies, true ) &&
			(
				is_category() ||
				is_tag() ||
				is_tax()
			)
		) {
			$termUrl = get_term_link( $term, $term->taxonomy );
			if ( is_wp_error( $termUrl ) ) {
				$termUrl = $homeUrl;
			}

			$this->redirectRssFeed( $termUrl );
		}

		if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
			return;
		}

		// Paginated feed pages. This one is last since we are using a regular expression to validate.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->paginated &&
			preg_match( '/(\d+\/|(?<=\/)page\/\d+\/)$/', (string) sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) )
		) {
			$this->redirectRssFeed( $homeUrl );
		}
	}

	/**
	 * Get the currently queried post type.
	 *
	 * @since 4.2.1
	 *
	 * @return string The queried post type.
	 */
	private function getTheQueriedPostType() {
		$postType = get_query_var( 'post_type' );
		if ( is_array( $postType ) ) {
			$postType = reset( $postType );
		}

		return $postType;
	}

	/**
	 * Redirect the feed to the appropriate URL.
	 *
	 * @since 4.2.1
	 *
	 * @return void
	 */
	private function redirectRssFeed( $url ) {
		if ( empty( $url ) ) {
			return;
		}

		// Set or remove headers.
		header_remove( 'Content-Type' );
		header_remove( 'Last-Modified' );
		header_remove( 'Expires' );

		$cache = 'public, max-age=604800, s-maxage=604800, stale-while-revalidate=120, stale-if-error=14400';
		if ( is_user_logged_in() ) {
			$cache = 'private, max-age=0';
		}

		header( 'Cache-Control: ' . $cache, true );

		wp_safe_redirect( $url, 301, AIOSEO_PLUGIN_SHORT_NAME );
	}

	/**
	 * Rewrite the RSS feed links.
	 *
	 * @since 4.2.1
	 *
	 * @param  array $args The arguments to filter.
	 * @return void
	 */
	public function rssFeedLinks( $args ) {
		$defaults = [
			// Translators: Separator between blog name and feed type in feed links.
			'separator'     => _x( '-', 'feed link', 'all-in-one-seo-pack' ),
			// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Post title.
			'singletitle'   => __( '%1$s %2$s %3$s Comments Feed', 'all-in-one-seo-pack' ),
			// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Category name.
			'cattitle'      => __( '%1$s %2$s %3$s Category Feed', 'all-in-one-seo-pack' ),
			// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Tag name.
			'tagtitle'      => __( '%1$s %2$s %3$s Tag Feed', 'all-in-one-seo-pack' ),
			// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Term name, 4: Taxonomy singular name.
			'taxtitle'      => __( '%1$s %2$s %3$s %4$s Feed', 'all-in-one-seo-pack' ),
			// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Author name.
			'authortitle'   => __( '%1$s %2$s Posts by %3$s Feed', 'all-in-one-seo-pack' ),
			// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Search query.
			'searchtitle'   => __( '%1$s %2$s Search Results for &#8220;%3$s&#8221; Feed', 'all-in-one-seo-pack' ),
			// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Post type name.
			'posttypetitle' => __( '%1$s %2$s %3$s Feed', 'all-in-one-seo-pack' ),
		];

		$args       = wp_parse_args( $args, $defaults );
		$attributes = [
			'title' => null,
			'href'  => null
		];

		if (
			aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->postComments &&
			is_singular()
		) {
			$attributes = $this->getPostCommentsAttributes( $args );
		}

		$archives = aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->archives->included;
		$postType = $this->getTheQueriedPostType();
		if (
			(
				aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->archives->all ||
				in_array( $postType, $archives, true )
			) &&
			is_post_type_archive()
		) {
			$attributes = $this->getPostTypeArchivesAttributes( $args );
		}

		// All taxonomies.
		$taxonomies = aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->taxonomies->included;
		$term       = get_queried_object();
		if (
			$term &&
			isset( $term->taxonomy ) &&
			(
				aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->taxonomies->all ||
				in_array( $term->taxonomy, $taxonomies, true )
			) &&
			(
				is_category() ||
				is_tag() ||
				is_tax()
			)
		) {
			$attributes = $this->getTaxonomiesAttributes( $args, $term );
		}

		if (
			aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->authors &&
			is_author()
		) {
			$attributes = $this->getAuthorAttributes( $args );
		}

		if (
			aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->search &&
			is_search()
		) {
			$attributes = $this->getSearchAttributes( $args );
		}

		if ( ! empty( $attributes['title'] ) && ! empty( $attributes['href'] ) ) {
			echo '<link rel="alternate" type="application/rss+xml" title="' . esc_attr( $attributes['title'] ) . '" href="' . esc_url( $attributes['href'] ) . '" />' . "\n";
		}
	}

	/**
	 * Retrieve the attributes for post comments feed.
	 *
	 * @since 4.2.1
	 *
	 * @param  array $args An array of arguments.
	 * @return array       An array of attributes.
	 */
	private function getPostCommentsAttributes( $args ) {
		$id    = 0;
		$post  = get_post( $id );
		$title = null;
		$href  = null;

		if (
			comments_open() ||
			pings_open() ||
			0 < $post->comment_count
		) {
			$title = sprintf(
				$args['singletitle'],
				get_bloginfo( 'name' ),
				$args['separator'],
				the_title_attribute( [ 'echo' => false ] )
			);

			$href = get_post_comments_feed_link( $post->ID );
		}

		return [
			'title' => $title,
			'href'  => $href
		];
	}

	/**
	 * Retrieve the attributes for post type archives feed.
	 *
	 * @since 4.2.1
	 *
	 * @param  array $args An array of arguments.
	 * @return array       An array of attributes.
	 */
	private function getPostTypeArchivesAttributes( $args ) {
		$postTypeObject = get_post_type_object( $this->getQueriedPostType() );
		$title          = sprintf( $args['posttypetitle'], get_bloginfo( 'name' ), $args['separator'], $postTypeObject->labels->name );
		$href           = get_post_type_archive_feed_link( $postTypeObject->name );

		return [
			'title' => $title,
			'href'  => $href
		];
	}

	/**
	 * Retrieve the attributes for taxonomies feed.
	 *
	 * @since 4.2.1
	 *
	 * @param  array $args    An array of arguments.
	 * @param  \WP_Term $term The term.
	 * @return array          An array of attributes.
	 */
	private function getTaxonomiesAttributes( $args, $term ) {
		$title = null;
		$href  = null;

		if ( is_category() ) {
			$title = sprintf( $args['cattitle'], get_bloginfo( 'name' ), $args['separator'], $term->name );
			$href  = get_category_feed_link( $term->term_id );
		}

		if ( is_tag() ) {
			$title = sprintf( $args['tagtitle'], get_bloginfo( 'name' ), $args['separator'], $term->name );
			$href  = get_tag_feed_link( $term->term_id );
		}

		if ( is_tax() ) {
			$tax   = get_taxonomy( $term->taxonomy );
			$title = sprintf( $args['taxtitle'], get_bloginfo( 'name' ), $args['separator'], $term->name, $tax->labels->singular_name );
			$href  = get_term_feed_link( $term->term_id, $term->taxonomy );
		}

		return [
			'title' => $title,
			'href'  => $href
		];
	}

	/**
	 * Retrieve the attributes for the author feed.
	 *
	 * @since 4.2.1
	 *
	 * @param  array $args An array of arguments.
	 * @return array       An array of attributes.
	 */
	private function getAuthorAttributes( $args ) {
		$authorId = (int) get_query_var( 'author' );
		$title    = sprintf( $args['authortitle'], get_bloginfo( 'name' ), $args['separator'], get_the_author_meta( 'display_name', $authorId ) );
		$href     = get_author_feed_link( $authorId );

		return [
			'title' => $title,
			'href'  => $href
		];
	}

	/**
	 * Retrieve the attributes for the search feed.
	 *
	 * @since 4.2.1
	 *
	 * @param  array $args An array of arguments.
	 * @return array       An array of attributes.
	 */
	private function getSearchAttributes( $args ) {
		$title = sprintf( $args['searchtitle'], get_bloginfo( 'name' ), $args['separator'], get_search_query( false ) );
		$href  = get_search_feed_link();

		return [
			'title' => $title,
			'href'  => $href
		];
	}

	/**
	 * Get the currently queried post type.
	 *
	 * @since 4.2.1
	 *
	 * @return string The currently queried post type.
	 */
	private function getQueriedPostType() {
		$postType = get_query_var( 'post_type' );
		if ( is_array( $postType ) ) {
			$postType = reset( $postType );
		}

		return $postType;
	}
}Common/Schema/Breadcrumb.php000064400000023613151536241200011757 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema;

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

/**
 * Determines the breadcrumb trail.
 *
 * @since 4.0.0
 */
class Breadcrumb {
	/**
	 * Returns the breadcrumb trail for the homepage.
	 *
	 * @since 4.0.0
	 *
	 * @return array The breadcrumb trail.
	 */
	public function home() {
		// Since we just need the root breadcrumb (homepage), we can call this immediately without passing any breadcrumbs.
		return $this->setPositions();
	}

	/**
	 * Returns the breadcrumb trail for the requested post.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post The post object.
	 * @return array          The breadcrumb trail.
	 */
	public function post( $post ) {
		// Check if page is the static homepage.
		if ( aioseo()->helpers->isStaticHomePage() ) {
			return $this->home();
		}

		if ( is_post_type_hierarchical( $post->post_type ) ) {
			return $this->setPositions( $this->postHierarchical( $post ) );
		}

		return $this->setPositions( $this->postNonHierarchical( $post ) );
	}

	/**
	 * Returns the breadcrumb trail for a hierarchical post.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post The post object.
	 * @return array          The breadcrumb trail.
	 */
	private function postHierarchical( $post ) {
		$breadcrumbs = [];
		do {
			array_unshift(
				$breadcrumbs,
				[
					'name'        => $post->post_title,
					'description' => aioseo()->meta->description->getDescription( $post ),
					'url'         => get_permalink( $post ),
					'type'        => aioseo()->helpers->isWooCommerceShopPage( $post->ID ) || is_home() ? 'CollectionPage' : $this->getPostWebPageGraph()
				]
			);

			if ( $post->post_parent ) {
				$post = get_post( $post->post_parent );
			} else {
				$post = false;
			}
		} while ( $post );

		return $breadcrumbs;
	}

	/**
	 * Returns the breadcrumb trail for a non-hierarchical post.
	 *
	 * In this case we need to compare the permalink structure with the permalink of the requested post and loop through all objects we're able to find.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post The post object.
	 * @return array          The breadcrumb trail.
	 */
	private function postNonHierarchical( $post ) {
		global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$homeUrl   = aioseo()->helpers->escapeRegex( home_url() );
		$permalink = get_permalink();
		$slug      = preg_replace( "/$homeUrl/", '', (string) $permalink );
		$tags      = array_filter( explode( '/', get_option( 'permalink_structure' ) ) ); // Permalink structure exploded into separate tag strings.
		$objects   = array_filter( explode( '/', $slug ) ); // Permalink slug exploded into separate object slugs.
		$postGraph = $this->getPostWebPageGraph();

		if ( count( $tags ) !== count( $objects ) ) {
			return [
				'name'        => $post->post_title,
				'description' => aioseo()->meta->description->getDescription( $post ),
				'url'         => $permalink,
				'type'        => $postGraph
			];
		}

		$pairs = array_reverse( array_combine( $tags, $objects ) );

		$breadcrumbs = [];
		$dateName    = null;
		$timestamp   = strtotime( $post->post_date );
		foreach ( $pairs as $tag => $object ) {
			// Escape the delimiter.
			$escObject = aioseo()->helpers->escapeRegex( $object );
			// Determine the slug for the object.
			preg_match( "/.*{$escObject}[\/]/", (string) $permalink, $url );
			if ( empty( $url[0] ) ) {
				continue;
			}

			$breadcrumb = [];
			switch ( $tag ) {
				case '%category%':
					$term = aioseo()->standalone->primaryTerm->getPrimaryTerm( $post->ID, 'category' );
					if ( ! $term ) {
						$term = get_category_by_slug( $object );
					}

					if ( ! $term ) {
						break;
					}
					// phpcs:disable Squiz.NamingConventions.ValidVariableName
					$oldQueriedObject         = $wp_query->queried_object;
					$wp_query->queried_object = $term;
					$wp_query->is_category    = true;

					$breadcrumb = [
						'name'        => $term->name,
						'description' => aioseo()->meta->description->getDescription(),
						'url'         => get_term_link( $term ),
						'type'        => 'CollectionPage'
					];

					$wp_query->queried_object = $oldQueriedObject;
					$wp_query->is_category    = false;
					// phpcs:enable Squiz.NamingConventions.ValidVariableName
					break;
				case '%author%':
					$breadcrumb = [
						'name'        => get_the_author_meta( 'display_name', $post->post_author ),
						'description' => aioseo()->meta->description->helpers->prepare( aioseo()->options->searchAppearance->archives->author->metaDescription ),
						'url'         => $url[0],
						'type'        => 'ProfilePage'
					];
					break;
				case '%postid%':
				case '%postname%':
					$breadcrumb = [
						'name'        => $post->post_title,
						'description' => aioseo()->meta->description->getDescription( $post ),
						'url'         => $url[0],
						'type'        => $postGraph
					];
					break;
				case '%year%':
					$dateName = gmdate( 'Y', $timestamp );
				case '%monthnum%':
					if ( ! $dateName ) {
						$dateName = gmdate( 'F', $timestamp );
					}
				case '%day%':
					if ( ! $dateName ) {
						$dateName = gmdate( 'j', $timestamp );
					}
					$breadcrumb = [
						'name'        => $dateName,
						'description' => aioseo()->meta->description->helpers->prepare( aioseo()->options->searchAppearance->archives->date->metaDescription ),
						'url'         => $url[0],
						'type'        => 'CollectionPage'
					];
					$dateName = null;
					break;
				default:
					break;
			}

			if ( $breadcrumb ) {
				array_unshift( $breadcrumbs, $breadcrumb );
			}
		}

		return $breadcrumbs;
	}

	/**
	 * Returns the breadcrumb trail for the requested term.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Term $term The term object.
	 * @return array          The breadcrumb trail.
	 */
	public function term( $term ) {
		if ( 'product_attributes' === $term->taxonomy ) {
			$term = get_term( $term->term_id );
		}

		$breadcrumbs = [];
		do {
			array_unshift(
				$breadcrumbs,
				[
					'name'        => $term->name,
					'description' => aioseo()->meta->description->getDescription(),
					'url'         => get_term_link( $term, $term->taxonomy ),
					'type'        => 'CollectionPage'
				]
			);

			if ( $term->parent ) {
				$term = aioseo()->helpers->getTerm( $term->parent, $term->taxonomy );
			} else {
				$term = false;
			}
		} while ( $term );

		return $this->setPositions( $breadcrumbs );
	}

	/**
	 * Returns the breadcrumb trail for the requested date archive.
	 *
	 * @since 4.0.0
	 *
	 * @return array The breadcrumb trail.
	 */
	public function date() {
		// phpcs:disable Squiz.NamingConventions.ValidVariableName
		global $wp_query;

		$oldYear            = $wp_query->is_year;
		$oldMonth           = $wp_query->is_month;
		$oldDay             = $wp_query->is_day;
		$wp_query->is_year  = true;
		$wp_query->is_month = false;
		$wp_query->is_day   = false;

		$breadcrumbs = [
			[
				'name'        => get_the_date( 'Y' ),
				'description' => aioseo()->meta->description->getDescription(),
				'url'         => trailingslashit( get_year_link( $wp_query->query_vars['year'] ) ),
				'type'        => 'CollectionPage'
			]
		];

		$wp_query->is_year = $oldYear;

		// Fall through if data archive is more specific than the year.
		if ( is_year() ) {
			return $this->setPositions( $breadcrumbs );
		}

		$wp_query->is_month = true;

		$breadcrumbs[] = [
			'name'        => get_the_date( 'F, Y' ),
			'description' => aioseo()->meta->description->getDescription(),
			'url'         => trailingslashit( get_month_link(
				$wp_query->query_vars['year'],
				$wp_query->query_vars['monthnum']
			) ),
			'type'        => 'CollectionPage'
		];

		$wp_query->is_month = $oldMonth;

		// Fall through if data archive is more specific than the year & month.
		if ( is_month() ) {
			return $this->setPositions( $breadcrumbs );
		}

		$wp_query->is_day = $oldDay;

		$breadcrumbs[] = [
			'name'        => get_the_date(),
			'description' => aioseo()->meta->description->getDescription(),
			'url'         => trailingslashit( get_day_link(
				$wp_query->query_vars['year'],
				$wp_query->query_vars['monthnum'],
				$wp_query->query_vars['day']
			) ),
			'type'        => 'CollectionPage'
		];
		// phpcs:enable Squiz.NamingConventions.ValidVariableName

		return $this->setPositions( $breadcrumbs );
	}

	/**
	 * Sets the position for each breadcrumb after adding the root breadcrumb first.
	 *
	 * If no breadcrumbs are passed, then we assume we're on the homepage and just need the root breadcrumb.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $breadcrumbs The breadcrumb trail.
	 * @return array              The modified breadcrumb trail.
	 */
	public function setPositions( $breadcrumbs = [] ) {
		// If the array isn't two-dimensional, then we need to wrap it in another array before continuing.
		if (
			count( $breadcrumbs ) &&
			count( $breadcrumbs ) === count( $breadcrumbs, COUNT_RECURSIVE )
		) {
			$breadcrumbs = [ $breadcrumbs ];
		}

		// The homepage needs to be root item of all trails.
		$homepage = [
			// Translators: This refers to the homepage of the site.
			'name'        => apply_filters( 'aioseo_schema_breadcrumbs_home', __( 'Home', 'all-in-one-seo-pack' ) ),
			'description' => aioseo()->meta->description->getHomePageDescription(),
			'url'         => trailingslashit( home_url() ),
			'type'        => 'posts' === get_option( 'show_on_front' ) ? 'CollectionPage' : 'WebPage'
		];
		array_unshift( $breadcrumbs, $homepage );

		$breadcrumbs = array_filter( $breadcrumbs );
		foreach ( $breadcrumbs as $index => &$breadcrumb ) {
			$breadcrumb['position'] = $index + 1;
		}

		return $breadcrumbs;
	}

	/**
	 * Returns the most relevant WebPage graph for the post.
	 *
	 * @since 4.2.5
	 *
	 * @return string The graph name.
	 */
	private function getPostWebPageGraph() {
		foreach ( aioseo()->schema->graphs as $graphName ) {
			if ( in_array( $graphName, aioseo()->schema->webPageGraphs, true ) ) {
				return $graphName;
			}
		}

		// Return the default if no WebPage graph was found.
		return 'WebPage';
	}
}Common/Schema/Context.php000064400000014327151536241200011337 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema;

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

/**
 * Determines the context.
 *
 * @since 4.0.0
 */
class Context {
	/**
	 * Breadcrumb class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Breadcrumb
	 */
	public $breadcrumb = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->breadcrumb = new Breadcrumb();
	}

	/**
	 * Returns the default context data.
	 *
	 * @since 4.3.0
	 *
	 * @return array The context data.
	 */
	public function defaults() {
		return [
			'name'        => aioseo()->meta->title->getTitle(),
			'description' => aioseo()->meta->description->getDescription(),
			'url'         => aioseo()->helpers->getUrl(),
			'breadcrumb'  => []
		];
	}

	/**
	 * Returns the context data for the homepage.
	 *
	 * @since 4.0.0
	 *
	 * @return array $context The context data.
	 */
	public function home() {
		$context = [
			'url'         => aioseo()->helpers->getUrl(),
			'breadcrumb'  => $this->breadcrumb->home(),
			'name'        => aioseo()->meta->title->getTitle(),
			'description' => aioseo()->meta->description->getDescription()
		];

		// Homepage set to show latest posts.
		if ( 'posts' === get_option( 'show_on_front' ) && is_home() ) {
			return $context;
		}

		// Homepage set to static page.
		$post = aioseo()->helpers->getPost();
		if ( ! $post ) {
			return [
				'name'        => '',
				'description' => '',
				'url'         => aioseo()->helpers->getUrl(),
				'breadcrumb'  => [],
			];
		}

		$context['object'] = $post;

		return $context;
	}

	/**
	 * Returns the context data for the requested post.
	 *
	 * @since 4.0.0
	 *
	 * @return array The context data.
	 */
	public function post() {
		$post = aioseo()->helpers->getPost();
		if ( ! $post ) {
			return [
				'name'        => '',
				'description' => '',
				'url'         => aioseo()->helpers->getUrl(),
				'breadcrumb'  => [],
			];
		}

		return [
			'name'        => aioseo()->meta->title->getTitle( $post ),
			'description' => aioseo()->meta->description->getDescription( $post ),
			'url'         => aioseo()->helpers->getUrl(),
			'breadcrumb'  => $this->breadcrumb->post( $post ),
			'object'      => $post,
		];
	}

	/**
	 * Returns the context data for the requested term archive.
	 *
	 * @since 4.0.0
	 *
	 * @return array The context data.
	 */
	public function term() {
		$term = aioseo()->helpers->getTerm();
		if ( ! $term ) {
			return [
				'name'        => '',
				'description' => '',
				'url'         => aioseo()->helpers->getUrl(),
				'breadcrumb'  => [],
			];
		}

		return [
			'name'        => aioseo()->meta->title->getTitle(),
			'description' => aioseo()->meta->description->getDescription(),
			'url'         => aioseo()->helpers->getUrl(),
			'breadcrumb'  => $this->breadcrumb->term( $term )
		];
	}

	/**
	 * Returns the context data for the requested author archive.
	 *
	 * @since 4.0.0
	 *
	 * @return array The context data.
	 */
	public function author() {
		$author = get_queried_object();
		if ( ! $author ) {
			return [
				'name'        => '',
				'description' => '',
				'url'         => aioseo()->helpers->getUrl(),
				'breadcrumb'  => [],
			];
		}

		$title       = aioseo()->meta->title->getTitle();
		$description = aioseo()->meta->description->getDescription();
		$url         = aioseo()->helpers->getUrl();

		if ( ! $description ) {
			$description = get_the_author_meta( 'description', $author->ID );
		}

		return [
			'name'        => $title,
			'description' => $description,
			'url'         => $url,
			'breadcrumb'  => $this->breadcrumb->setPositions( [
				'name'        => get_the_author_meta( 'display_name', $author->ID ),
				'description' => $description,
				'url'         => $url,
				'type'        => 'CollectionPage'
			] )
		];
	}

	/**
	 * Returns the context data for the requested post archive.
	 *
	 * @since 4.0.0
	 *
	 * @return array The context data.
	 */
	public function postArchive() {
		$postType = get_queried_object();
		if ( ! $postType ) {
			return [
				'name'        => '',
				'description' => '',
				'url'         => aioseo()->helpers->getUrl(),
				'breadcrumb'  => [],
			];
		}

		$title       = aioseo()->meta->title->getTitle();
		$description = aioseo()->meta->description->getDescription();
		$url         = aioseo()->helpers->getUrl();

		return [
			'name'        => $title,
			'description' => $description,
			'url'         => $url,
			'breadcrumb'  => $this->breadcrumb->setPositions( [
				'name'        => $postType->label,
				'description' => $description,
				'url'         => $url,
				'type'        => 'CollectionPage'
			] )
		];
	}

	/**
	 * Returns the context data for the requested data archive.
	 *
	 * @since 4.0.0
	 *
	 * @return array $context The context data.
	 */
	public function date() {
		$context = [
			'name'        => aioseo()->meta->title->getTitle(),
			'description' => aioseo()->meta->description->getDescription(),
			'url'         => aioseo()->helpers->getUrl()
		];

		$context['breadcrumb'] = $this->breadcrumb->date();

		return $context;
	}

	/**
	 * Returns the context data for the search page.
	 *
	 * @since 4.0.0
	 *
	 * @return array The context data.
	 */
	public function search() {
		global $s;
		$title       = aioseo()->meta->title->getTitle();
		$description = aioseo()->meta->description->getDescription();
		$url         = aioseo()->helpers->getUrl();

		return [
			'name'        => $title,
			'description' => $description,
			'url'         => $url,
			'breadcrumb'  => $this->breadcrumb->setPositions( [
				'name'        => $s ? $s : $title,
				'description' => $description,
				'url'         => $url,
				'type'        => 'SearchResultsPage'
			] )
		];
	}

	/**
	 * Returns the context data for the 404 Not Found page.
	 *
	 * @since 4.0.0
	 *
	 * @return array The context data.
	 */
	public function notFound() {
		$title       = aioseo()->meta->title->getTitle();
		$description = aioseo()->meta->description->getDescription();
		$url         = aioseo()->helpers->getUrl();

		return [
			'name'        => $title,
			'description' => $description,
			'url'         => $url,
			'breadcrumb'  => $this->breadcrumb->setPositions( [
				'name'        => __( 'Not Found', 'all-in-one-seo-pack' ),
				'description' => $description,
				'url'         => $url
			] )
		];
	}
}Common/Schema/Graphs/AmpStory.php000064400000002471151536241200012712 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs;

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

/**
 * AmpStory graph class.
 *
 * @since 4.7.6
 */
class AmpStory extends Graph {
	/**
	 * Returns the graph data.
	 *
	 * @since 4.7.6
	 *
	 * @return array The parsed graph data.
	 */
	public function get() {
		$post = aioseo()->helpers->getPost();
		if ( ! is_a( $post, 'WP_Post' ) || 'web-story' !== $post->post_type ) {
			return [];
		}

		$data = [
			'@type'         => 'AmpStory',
			'@id'           => aioseo()->schema->context['url'] . '#amp-story',
			'name'          => aioseo()->schema->context['name'],
			'headline'      => get_the_title(),
			'author'        => [
				'@id' => get_author_posts_url( $post->post_author ) . '#author'
			],
			'publisher'     => [ '@id' => trailingslashit( home_url() ) . '#' . aioseo()->options->searchAppearance->global->schema->siteRepresents ],
			'image'         => $this->getFeaturedImage(),
			'datePublished' => mysql2date( DATE_W3C, $post->post_date, false ),
			'dateModified'  => mysql2date( DATE_W3C, $post->post_modified, false ),
			'inLanguage'    => aioseo()->helpers->currentLanguageCodeBCP47()
		];

		if ( ! in_array( 'PersonAuthor', aioseo()->schema->graphs, true ) ) {
			aioseo()->schema->graphs[] = 'PersonAuthor';
		}

		return $data;
	}
}Common/Schema/Graphs/Article/Article.php000064400000011770151536241200014104 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\Article;

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

use AIOSEO\Plugin\Common\Schema\Graphs;

/**
 * Article graph class.
 *
 * @since 4.0.0
 */
class Article extends Graphs\Graph {
	/**
	 * Returns the graph data.
	 *
	 * @since 4.2.5
	 *
	 * @param  Object $graphData The graph data.
	 * @return array             The parsed graph data.
	 */
	public function get( $graphData = null ) {
		$post = aioseo()->helpers->getPost();
		if ( ! is_a( $post, 'WP_Post' ) ) {
			return [];
		}

		$data = [
			'@type'            => 'Article',
			'@id'              => ! empty( $graphData->id ) ? aioseo()->schema->context['url'] . $graphData->id : aioseo()->schema->context['url'] . '#article',
			'name'             => ! empty( $graphData->properties->name ) ? $graphData->properties->name : aioseo()->schema->context['name'],
			'headline'         => ! empty( $graphData->properties->headline ) ? $graphData->properties->headline : get_the_title(),
			'description'      => ! empty( $graphData->properties->description ) ? $graphData->properties->description : '',
			'author'           => [
				'@type' => 'Person',
				'name'  => ! empty( $graphData->properties->author->name ) ? $graphData->properties->author->name : get_the_author_meta( 'display_name' ),
				'url'   => ! empty( $graphData->properties->author->url ) ? $graphData->properties->author->url : '',
			],
			'publisher'        => [ '@id' => trailingslashit( home_url() ) . '#' . aioseo()->options->searchAppearance->global->schema->siteRepresents ],
			'image'            => ! empty( $graphData->properties->image ) ? $this->image( $graphData->properties->image ) : $this->postImage( $post ),
			'datePublished'    => ! empty( $graphData->properties->dates->datePublished )
				? mysql2date( DATE_W3C, $graphData->properties->dates->datePublished, false )
				: mysql2date( DATE_W3C, $post->post_date, false ),
			'dateModified'     => ! empty( $graphData->properties->dates->dateModified )
				? mysql2date( DATE_W3C, $graphData->properties->dates->dateModified, false )
				: mysql2date( DATE_W3C, $post->post_modified, false ),
			'inLanguage'       => aioseo()->helpers->currentLanguageCodeBCP47(),
			'commentCount'     => get_comment_count( $post->ID )['approved'],
			'mainEntityOfPage' => empty( $graphData ) ? [ '@id' => aioseo()->schema->context['url'] . '#webpage' ] : '',
			'isPartOf'         => empty( $graphData ) ? [ '@id' => aioseo()->schema->context['url'] . '#webpage' ] : ''
		];

		if ( empty( $graphData->properties->author->name ) ) {
			if ( ! in_array( 'PersonAuthor', aioseo()->schema->graphs, true ) ) {
				aioseo()->schema->graphs[] = 'PersonAuthor';
			}

			$data['author'] = [
				'@id' => get_author_posts_url( $post->post_author ) . '#author'
			];
		}

		if ( ! empty( $graphData->properties->keywords ) ) {
			$keywords = json_decode( $graphData->properties->keywords, true );
			$keywords = array_map( function ( $keywordObject ) {
				return $keywordObject['value'];
			}, $keywords );
			$data['keywords'] = implode( ', ', $keywords );
		}

		if ( isset( $graphData->properties->dates->include ) && ! $graphData->properties->dates->include ) {
			unset( $data['datePublished'] );
			unset( $data['dateModified'] );
		}

		$postTaxonomies = get_post_taxonomies( $post );
		$postTerms      = [];
		foreach ( $postTaxonomies as $taxonomy ) {
			$terms = get_the_terms( $post, $taxonomy );
			if ( $terms ) {
				$postTerms = array_merge( $postTerms, wp_list_pluck( $terms, 'name' ) );
			}
		}

		if ( ! empty( $postTerms ) ) {
			$data['articleSection'] = implode( ', ', $postTerms );
		}

		$pageNumber = aioseo()->helpers->getPageNumber();
		if ( 1 < $pageNumber ) {
			$data['pagination'] = $pageNumber;
		}

		return $data;
	}

	/**
	 * Returns the graph data for the post image.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post The post object.
	 * @return array          The image graph data.
	 */
	private function postImage( $post ) {
		$featuredImage = $this->getFeaturedImage();
		if ( $featuredImage ) {
			return $featuredImage;
		}

		preg_match_all( '#<img[^>]+src="([^">]+)"#', (string) $post->post_content, $matches );
		if ( isset( $matches[1] ) && isset( $matches[1][0] ) ) {
			$url     = aioseo()->helpers->removeImageDimensions( $matches[1][0] );
			$imageId = aioseo()->helpers->attachmentUrlToPostId( $url );
			if ( $imageId ) {
				return $this->image( $imageId, 'articleImage' );
			} else {
				return $this->image( $url, 'articleImage' );
			}
		}

		if ( 'organization' === aioseo()->options->searchAppearance->global->schema->siteRepresents ) {
			$logo = ( new Graphs\KnowledgeGraph\KgOrganization() )->logo();
			if ( ! empty( $logo ) ) {
				$logo['@id'] = trailingslashit( home_url() ) . '#articleImage';

				return $logo;
			}
		} else {
			$avatar = $this->avatar( $post->post_author, 'articleImage' );
			if ( $avatar ) {
				return $avatar;
			}
		}

		$imageId = aioseo()->helpers->getSiteLogoId();
		if ( $imageId ) {
			return $this->image( $imageId, 'articleImage' );
		}

		return [];
	}
}Common/Schema/Graphs/Article/BlogPosting.php000064400000001307151536241200014743 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\Article;

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

/**
 * Blog Posting graph class.
 *
 * @since 4.0.0
 */
class BlogPosting extends Article {
	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 *
	 * @return object $graphData The graph data.
	 * @return array             The parsed graph data.
	 */
	public function get( $graphData = null ) {
		$data = parent::get( $graphData );
		if ( ! $data ) {
			return [];
		}

		$data['@type'] = 'BlogPosting';
		$data['@id']   = ! empty( $graphData->id ) ? aioseo()->schema->context['url'] . $graphData->id : aioseo()->schema->context['url'] . '#blogposting';

		return $data;
	}
}Common/Schema/Graphs/Article/NewsArticle.php000064400000002315151536241200014734 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\Article;

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

/**
 * News Article graph class.
 *
 * @since 4.0.0
 */
class NewsArticle extends Article {
	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 *
	 * @param  object $graphData The graph data.
	 * @return array             The parsed graph data.
	 */
	public function get( $graphData = null ) {
		if ( ! empty( self::$overwriteGraphData[ __CLASS__ ] ) ) {
			$graphData = json_decode( wp_json_encode( wp_parse_args( self::$overwriteGraphData[ __CLASS__ ], $graphData ) ) );
		}

		$data = parent::get( $graphData );
		if ( ! $data ) {
			return [];
		}

		$data['@type'] = 'NewsArticle';
		$data['@id']   = ! empty( $graphData->id ) ? aioseo()->schema->context['url'] . $graphData->id : aioseo()->schema->context['url'] . '#newsarticle';

		$date = ! empty( $graphData->properties->datePublished )
			? mysql2date( 'F j, Y', $graphData->properties->datePublished, false )
			: get_the_date( 'F j, Y' );
		if ( $date ) {
			// Translators: 1 - A date (e.g. September 2, 2022).
			$data['dateline'] = sprintf( __( 'Published on %1$s.', 'all-in-one-seo-pack' ), $date );
		}

		return $data;
	}
}Common/Schema/Graphs/BreadcrumbList.php000064400000004205151536241200014033 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs;

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

/**
 * BreadcrumbList graph class.
 *
 * @since 4.0.0
 */
class BreadcrumbList extends Graph {
	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 *
	 * @return array The graph data.
	 */
	public function get() {
		$breadcrumbs = aioseo()->breadcrumbs->frontend->getBreadcrumbs() ?? '';
		if ( ! $breadcrumbs ) {
			return [];
		}

		// Set the position for each breadcrumb.
		foreach ( $breadcrumbs as $k => $breadcrumb ) {
			if ( ! isset( $breadcrumb['position'] ) ) {
				$breadcrumbs[ $k ]['position'] = $k + 1;
			}
		}

		$trailLength = count( $breadcrumbs );
		if ( ! $trailLength ) {
			return [];
		}

		$listItems = [];
		foreach ( $breadcrumbs as $breadcrumb ) {
			if ( empty( $breadcrumb['link'] ) ) {
				continue;
			}

			$listItem = [
				'@type'    => 'ListItem',
				'@id'      => $breadcrumb['link'] . '#listItem',
				'position' => $breadcrumb['position'],
				'name'     => $breadcrumb['label'] ?? ''
			];

			// Don't add "item" prop for last crumb.
			if ( $trailLength !== $breadcrumb['position'] ) {
				$listItem['item'] = $breadcrumb['link'];
			}

			if ( 1 === $trailLength ) {
				$listItems[] = $listItem;
				continue;
			}

			if ( $trailLength > $breadcrumb['position'] && ! empty( $breadcrumbs[ $breadcrumb['position'] ]['label'] ) ) {
				$listItem['nextItem'] = [
					'@type' => 'ListItem',
					'@id'   => $breadcrumbs[ $breadcrumb['position'] ]['link'] . '#listItem',
					'name'  => $breadcrumbs[ $breadcrumb['position'] ]['label'],
				];
			}

			if ( 1 < $breadcrumb['position'] && ! empty( $breadcrumbs[ $breadcrumb['position'] - 2 ]['label'] ) ) {
				$listItem['previousItem'] = [
					'@type' => 'ListItem',
					'@id'   => $breadcrumbs[ $breadcrumb['position'] - 2 ]['link'] . '#listItem',
					'name'  => $breadcrumbs[ $breadcrumb['position'] - 2 ]['label'],
				];
			}

			$listItems[] = $listItem;
		}

		$data = [
			'@type'           => 'BreadcrumbList',
			'@id'             => aioseo()->schema->context['url'] . '#breadcrumblist',
			'itemListElement' => $listItems
		];

		return $data;
	}
}Common/Schema/Graphs/Graph.php000064400000004542151536241200012176 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs;

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

use AIOSEO\Plugin\Common\Traits as CommonTraits;

/**
 * The base graph class.
 *
 * @since 4.0.0
 */
abstract class Graph {
	use Traits\Image;
	use CommonTraits\SocialProfiles;

	/**
	 * The graph data to overwrite.
	 *
	 * @since 4.7.6
	 *
	 * @var array
	 */
	protected static $overwriteGraphData = [];

	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 */
	abstract public function get();

	/**
	 * Iterates over a list of functions and sets the results as graph data.
	 *
	 * @since 4.0.13
	 *
	 * @param  array $data          The graph data to add to.
	 * @param  array $dataFunctions List of functions to loop over, associated with a graph property.
	 * @return array $data          The graph data with the results added.
	 */
	protected function getData( $data, $dataFunctions ) {
		foreach ( $dataFunctions as $k => $f ) {
			if ( ! method_exists( $this, $f ) ) {
				continue;
			}

			$value = $this->$f();
			if ( $value || in_array( $k, aioseo()->schema->nullableFields, true ) ) {
				$data[ $k ] = $value;
			}
		}

		return $data;
	}

	/**
	 * Decodes a multiselect field and returns the values.
	 *
	 * @since 4.6.4
	 *
	 * @param  string $json The JSON encoded multiselect field.
	 * @return array        The decoded values.
	 */
	protected function extractMultiselectTags( $json ) {
		$tags = is_string( $json ) ? json_decode( $json ) : [];
		if ( ! $tags ) {
			return [];
		}

		return wp_list_pluck( $tags, 'value' );
	}

	/**
	 * Merges in data from our addon plugins.
	 *
	 * @since   4.5.6
	 * @version 4.6.4 Moved to main graph class.
	 *
	 * @param  array $data The graph data.
	 * @return array       The graph data.
	 */
	protected function getAddonData( $data, $className, $methodName = 'getAdditionalGraphData' ) {
		$addonData = array_filter( aioseo()->addons->doAddonFunction( $className, $methodName, [
			'postId' => get_the_ID(),
			'data'   => $data
		] ) );

		foreach ( $addonData as $addonGraphData ) {
			$data = array_merge( $data, $addonGraphData );
		}

		return $data;
	}

	/**
	 * A way to overwrite the graph data.
	 *
	 * @since 4.7.6
	 *
	 * @param  array $data The data to overwrite.
	 * @return void
	 */
	public static function setOverwriteGraphData( $data ) {
		self::$overwriteGraphData[ static::class ] = $data;
	}
}Common/Schema/Graphs/KnowledgeGraph/KgOrganization.php000064400000005336151536241200016766 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\KnowledgeGraph;

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

use \AIOSEO\Plugin\Common\Schema\Graphs;

/**
 * Knowledge Graph Organization graph class.
 *
 * @since 4.0.0
 */
class KgOrganization extends Graphs\Graph {
	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 *
	 * @return array $data The graph data.
	 */
	public function get() {
		$homeUrl                 = trailingslashit( home_url() );
		$organizationName        = aioseo()->tags->replaceTags( aioseo()->options->searchAppearance->global->schema->organizationName );
		$organizationDescription = aioseo()->tags->replaceTags( aioseo()->options->searchAppearance->global->schema->organizationDescription );

		$data = [
			'@type'        => 'Organization',
			'@id'          => $homeUrl . '#organization',
			'name'         => $organizationName ? $organizationName : aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) ),
			'description'  => $organizationDescription,
			'url'          => $homeUrl,
			'email'        => aioseo()->options->searchAppearance->global->schema->email,
			'telephone'    => aioseo()->options->searchAppearance->global->schema->phone,
			'foundingDate' => aioseo()->options->searchAppearance->global->schema->foundingDate
		];

		$numberOfEmployeesData = aioseo()->options->searchAppearance->global->schema->numberOfEmployees->all();

		if (
			$numberOfEmployeesData['isRange'] &&
			isset( $numberOfEmployeesData['from'] ) &&
			isset( $numberOfEmployeesData['to'] ) &&
			0 < $numberOfEmployeesData['to']
		) {
			$data['numberOfEmployees'] = [
				'@type'    => 'QuantitativeValue',
				'minValue' => $numberOfEmployeesData['from'],
				'maxValue' => $numberOfEmployeesData['to']
			];
		}

		if (
			! $numberOfEmployeesData['isRange'] &&
			! empty( $numberOfEmployeesData['number'] )
		) {
			$data['numberOfEmployees'] = [
				'@type' => 'QuantitativeValue',
				'value' => $numberOfEmployeesData['number']
			];
		}

		$logo = $this->logo();
		if ( ! empty( $logo ) ) {
			$data['logo']  = $logo;
			$data['image'] = [ '@id' => $data['logo']['@id'] ];
		}

		$socialUrls = array_values( $this->getOrganizationProfiles() );
		if ( $socialUrls ) {
			$data['sameAs'] = $socialUrls;
		}

		$data = $this->getAddonData( $data, 'kgOrganization' );

		return $data;
	}

	/**
	 * Returns the logo data.
	 *
	 * @since 4.0.0
	 *
	 * @return array The logo data.
	 */
	public function logo() {
		$logo = aioseo()->options->searchAppearance->global->schema->organizationLogo;
		if ( $logo ) {
			return $this->image( $logo, 'organizationLogo' );
		}

		$imageId = aioseo()->helpers->getSiteLogoId();
		if ( $imageId ) {
			return $this->image( $imageId, 'organizationLogo' );
		}

		return [];
	}
}Common/Schema/Graphs/KnowledgeGraph/KgPerson.php000064400000003457151536241200015572 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\KnowledgeGraph;

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

use \AIOSEO\Plugin\Common\Schema\Graphs;

/**
 * Knowledge Graph Person graph class.
 * This is the main Person graph that can be set to represent the site.
 *
 * @since 4.0.0
 */
class KgPerson extends Graphs\Graph {
	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 *
	 * @return array $data The graph data.
	 */
	public function get() {
		if ( 'person' !== aioseo()->options->searchAppearance->global->schema->siteRepresents ) {
			return [];
		}

		$person = aioseo()->options->searchAppearance->global->schema->person;
		if ( 'manual' === $person ) {
			return $this->manual();
		}

		$person = intval( $person );
		if ( empty( $person ) ) {
			return [];
		}

		$data = [
			'@type' => 'Person',
			'@id'   => trailingslashit( home_url() ) . '#person',
			'name'  => get_the_author_meta( 'display_name', $person )
		];

		$avatar = $this->avatar( $person, 'personImage' );
		if ( $avatar ) {
			$data['image'] = $avatar;
		}

		$socialUrls = array_values( $this->getUserProfiles( $person ) );
		if ( $socialUrls ) {
			$data['sameAs'] = $socialUrls;
		}

		return $data;
	}

	/**
	 * Returns the data for the person if it is set manually.
	 *
	 * @since 4.0.0
	 *
	 * @return array $data The graph data.
	 */
	private function manual() {
		$data = [
			'@type' => 'Person',
			'@id'   => trailingslashit( home_url() ) . '#person',
			'name'  => aioseo()->options->searchAppearance->global->schema->personName
		];

		$logo = aioseo()->options->searchAppearance->global->schema->personLogo;
		if ( $logo ) {
			$data['image'] = $logo;
		}

		$socialUrls = array_values( $this->getOrganizationProfiles() );
		if ( $socialUrls ) {
			$data['sameAs'] = $socialUrls;
		}

		return $data;
	}
}Common/Schema/Graphs/Traits/Image.php000064400000005462151536241200013427 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\Traits;

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

/**
 * Trait that handles images for the graphs.
 *
 * @since 4.2.5
 */
trait Image {
	/**
	 * Builds the graph data for a given image with a given schema ID.
	 *
	 * @since 4.0.0
	 *
	 * @param int    $imageId The image ID.
	 * @param string $graphId The graph ID (optional).
	 * @return array $data    The image graph data.
	 */
	protected function image( $imageId, $graphId = '' ) {
		$attachmentId = is_string( $imageId ) && ! is_numeric( $imageId ) ? aioseo()->helpers->attachmentUrlToPostId( $imageId ) : $imageId;
		$imageUrl     = wp_get_attachment_image_url( $attachmentId, 'full' );

		$data = [
			'@type' => 'ImageObject',
			'url'   => $imageUrl ? $imageUrl : $imageId,
		];

		if ( $graphId ) {
			$baseUrl     = aioseo()->schema->context['url'] ?? aioseo()->helpers->getUrl();
			$data['@id'] = trailingslashit( $baseUrl ) . '#' . $graphId;
		}

		if ( ! $attachmentId ) {
			return $data;
		}

		$metaData = wp_get_attachment_metadata( $attachmentId );
		if ( $metaData && ! empty( $metaData['width'] ) && ! empty( $metaData['height'] ) ) {
			$data['width']  = (int) $metaData['width'];
			$data['height'] = (int) $metaData['height'];
		}

		$caption = $this->getImageCaption( $attachmentId );
		if ( ! empty( $caption ) ) {
			$data['caption'] = $caption;
		}

		return $data;
	}

	/**
	 * Get the image caption.
	 *
	 * @since 4.1.4
	 *
	 * @param  int    $attachmentId The attachment ID.
	 * @return string               The caption.
	 */
	private function getImageCaption( $attachmentId ) {
		$caption = wp_get_attachment_caption( $attachmentId );
		if ( ! empty( $caption ) ) {
			return $caption;
		}

		return get_post_meta( $attachmentId, '_wp_attachment_image_alt', true );
	}

	/**
	 * Returns the graph data for the avatar of a given user.
	 *
	 * @since 4.0.0
	 *
	 * @param  int    $userId  The user ID.
	 * @param  string $graphId The graph ID.
	 * @return array           The graph data.
	 */
	protected function avatar( $userId, $graphId ) {
		if ( ! get_option( 'show_avatars' ) ) {
			return [];
		}

		$avatar = get_avatar_data( $userId );
		if ( ! $avatar['found_avatar'] ) {
			return [];
		}

		return array_filter( [
			'@type'   => 'ImageObject',
			'@id'     => aioseo()->schema->context['url'] . "#$graphId",
			'url'     => $avatar['url'],
			'width'   => $avatar['width'],
			'height'  => $avatar['height'],
			'caption' => get_the_author_meta( 'display_name', $userId )
		] );
	}

	/**
	 * Returns the graph data for the post's featured image.
	 *
	 * @since 4.2.5
	 *
	 * @return string The featured image URL.
	 */
	protected function getFeaturedImage() {
		$post = aioseo()->helpers->getPost();

		return has_post_thumbnail( $post ) ? $this->image( get_post_thumbnail_id() ) : '';
	}
}Common/Schema/Graphs/WebPage/AboutPage.php000064400000000504151536241200014310 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

/**
 * AboutPage graph class.
 *
 * @since 4.0.0
 */
class AboutPage extends WebPage {
	/**
	 * The graph type.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $type = 'AboutPage';
}Common/Schema/Graphs/WebPage/CheckoutPage.php000064400000000642151536241200015006 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

/**
 * CheckoutPage graph class.
 *
 * @since 4.6.4
 */
class CheckoutPage extends WebPage {
	/**
	 * The graph type.
	 *
	 * This value can be overridden by WebPage child graphs that are more specific.
	 *
	 * @since 4.6.4
	 *
	 * @var string
	 */
	protected $type = 'CheckoutPage';
}Common/Schema/Graphs/WebPage/CollectionPage.php000064400000000523151536241200015332 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

/**
 * CollectionPage graph class.
 *
 * @since 4.0.0
 */
class CollectionPage extends WebPage {
	/**
	 * The graph type.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $type = 'CollectionPage';
}Common/Schema/Graphs/WebPage/ContactPage.php000064400000000512151536241200014630 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

/**
 * ContactPage graph class.
 *
 * @since 4.0.0
 */
class ContactPage extends WebPage {
	/**
	 * The graph type.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $type = 'ContactPage';
}Common/Schema/Graphs/WebPage/FAQPage.php000064400000000476151536241200013655 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

/**
 * FAQPage graph class.
 *
 * @since 4.0.0
 */
class FAQPage extends WebPage {
	/**
	 * The graph type.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $type = 'FAQPage';
}Common/Schema/Graphs/WebPage/ItemPage.php000064400000000626151536241200014141 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

/**
 * ItemPage graph class.
 *
 * @since 4.0.0
 */
class ItemPage extends WebPage {
	/**
	 * The graph type.
	 *
	 * This value can be overridden by WebPage child graphs that are more specific.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $type = 'ItemPage';
}Common/Schema/Graphs/WebPage/MedicalWebPage.php000064400000000650151536241200015234 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

/**
 * MedicalWebPage graph class.
 *
 * @since 4.6.4
 */
class MedicalWebPage extends WebPage {
	/**
	 * The graph type.
	 *
	 * This value can be overridden by WebPage child graphs that are more specific.
	 *
	 * @since 4.6.4
	 *
	 * @var string
	 */
	protected $type = 'MedicalWebPage';
}Common/Schema/Graphs/WebPage/PersonAuthor.php000064400000004016151536241200015074 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

use AIOSEO\Plugin\Common\Schema\Graphs;

/**
 * Person Author graph class.
 * This a secondary Person graph for post authors and BuddyPress profile pages.
 *
 * @since 4.0.0
 */
class PersonAuthor extends Graphs\Graph {
	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 *
	 * @param  int   $userId The user ID.
	 * @return array $data   The graph data.
	 */
	public function get( $userId = null ) {
		$post         = aioseo()->helpers->getPost();
		$user         = get_queried_object();
		$isAuthorPage = is_author() && is_a( $user, 'WP_User' );
		if (
			(
				( ! is_singular() && ! $isAuthorPage ) ||
				( is_singular() && ! is_a( $post, 'WP_Post' ) )
			) &&
			! $userId
		) {
			return [];
		}

		// Dynamically determine the User ID.
		if ( ! $userId ) {
			$userId = $isAuthorPage ? $user->ID : $post->post_author;
			if ( function_exists( 'bp_is_user' ) && bp_is_user() ) {
				$userId = intval( wp_get_current_user()->ID );
			}
		}

		if ( ! $userId ) {
			return [];
		}

		$authorUrl = get_author_posts_url( $userId );

		$data = [
			'@type' => 'Person',
			'@id'   => $authorUrl . '#author',
			'url'   => $authorUrl,
			'name'  => get_the_author_meta( 'display_name', $userId )
		];

		$avatar = $this->avatar( $userId, 'authorImage' );
		if ( $avatar ) {
			$data['image'] = $avatar;
		}

		$socialUrls = array_values( $this->getUserProfiles( $userId ) );
		if ( $socialUrls ) {
			$data['sameAs'] = $socialUrls;
		}

		if ( is_author() ) {
			$data['mainEntityOfPage'] = [
				'@id' => aioseo()->schema->context['url'] . '#profilepage'
			];
		}

		// Check if our addons need to modify this graph.
		$addonsPersonAuthorData = array_filter( aioseo()->addons->doAddonFunction( 'personAuthor', 'get', [
			'userId' => $userId,
			'data'   => $data
		] ) );

		foreach ( $addonsPersonAuthorData as $addonPersonAuthorData ) {
			$data = array_merge( $data, $addonPersonAuthorData );
		}

		return $data;
	}
}Common/Schema/Graphs/WebPage/ProfilePage.php000064400000004274151536241200014646 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * ProfilePage graph class.
 *
 * @since 4.0.0
 */
class ProfilePage extends WebPage {
	/**
	 * The graph type.
	 *
	 * @since 4.5.6
	 *
	 * @var string
	 */
	protected $type = 'ProfilePage';

	/**
	 * Returns the graph data.
	 *
	 * @since 4.5.4
	 *
	 * @return array The graph data.
	 */
	public function get() {
		$data = parent::get();

		$post   = aioseo()->helpers->getPost();
		$author = get_queried_object();
		if (
			! is_a( $author, 'WP_User' ) &&
			( is_singular() && ! is_a( $post, 'WP_Post' ) )
		) {
			return [];
		}

		global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$articles = [];
		$authorId = $author->ID ?? $post->post_author ?? 0;
		foreach ( $wp_query->posts as $post ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			if ( $post->post_author !== $authorId ) {
				continue;
			}

			$articles[] = [
				'@type'         => 'Article',
				'url'           => get_permalink( $post->ID ),
				'headline'      => $post->post_title,
				'datePublished' => mysql2date( DATE_W3C, $post->post_date, false ),
				'dateModified'  => mysql2date( DATE_W3C, $post->post_modified, false ),
				'author'        => [
					'@id' => get_author_posts_url( $authorId ) . '#author'
				]
			];
		}

		$data = array_merge( $data, [
			'dateCreated' => mysql2date( DATE_W3C, $author->user_registered, false ),
			'mainEntity'  => [
				'@id' => get_author_posts_url( $authorId ) . '#author'
			],
			'hasPart'     => $articles

		] );

		if (
			BuddyPressIntegration::isComponentPage() &&
			'bp-member_single' === aioseo()->standalone->buddyPress->component->templateType
		) {
			if ( ! isset( $data['mainEntity'] ) ) {
				$data['mainEntity'] = [];
			}

			$data['mainEntity']['@type'] = 'Person';
			$data['mainEntity']['name']  = aioseo()->standalone->buddyPress->component->author->display_name;
			$data['mainEntity']['url']   = BuddyPressIntegration::getComponentSingleUrl( 'member', aioseo()->standalone->buddyPress->component->author->ID );
		}

		return $data;
	}
}Common/Schema/Graphs/WebPage/RealEstateListing.php000064400000001244151536241200016026 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

/**
 * RealEstateListing graph class.
 *
 * @since 4.0.0
 */
class RealEstateListing extends WebPage {
	/**
	 * The graph type.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $type = 'RealEstateListing';

	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 *
	 * @return array $data The graph data.
	 */
	public function get() {
		$data = parent::get();
		$post = aioseo()->helpers->getPost();
		if ( ! $post ) {
			return $data;
		}

		$data['datePosted'] = mysql2date( DATE_W3C, $post->post_date, false );

		return $data;
	}
}Common/Schema/Graphs/WebPage/SearchResultsPage.php000064400000000534151536241200016030 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

/**
 * SearchResultsPage graph class.
 *
 * @since 4.0.0
 */
class SearchResultsPage extends WebPage {
	/**
	 * The graph type.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $type = 'SearchResultsPage';
}Common/Schema/Graphs/WebPage/WebPage.php000064400000005577151536241200013772 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

use AIOSEO\Plugin\Common\Schema\Graphs;

/**
 * WebPage graph class.
 *
 * @since 4.0.0
 */
class WebPage extends Graphs\Graph {
	/**
	 * The graph type.
	 *
	 * This value can be overridden by WebPage child graphs that are more specific.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $type = 'WebPage';

	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 *
	 * @return array $data The graph data.
	 */
	public function get() {
		$homeUrl = trailingslashit( home_url() );
		$data    = [
			'@type'       => $this->type,
			'@id'         => aioseo()->schema->context['url'] . '#' . strtolower( $this->type ),
			'url'         => aioseo()->schema->context['url'],
			'name'        => aioseo()->meta->title->getTitle(),
			'description' => aioseo()->schema->context['description'],
			'inLanguage'  => aioseo()->helpers->currentLanguageCodeBCP47(),
			'isPartOf'    => [ '@id' => $homeUrl . '#website' ]
		];

		$breadcrumbs = aioseo()->breadcrumbs->frontend->getBreadcrumbs() ?? '';
		if ( ! empty( $breadcrumbs ) ) {
			$data['breadcrumb'] = [ '@id' => aioseo()->schema->context['url'] . '#breadcrumblist' ];
		}

		if ( is_singular() && 'page' !== get_post_type() ) {
			$post = aioseo()->helpers->getPost();
			if ( is_a( $post, 'WP_Post' ) && post_type_supports( $post->post_type, 'author' ) ) {
				$author = get_author_posts_url( $post->post_author );
				if ( ! empty( $author ) ) {
					if ( ! in_array( 'PersonAuthor', aioseo()->schema->graphs, true ) ) {
						aioseo()->schema->graphs[] = 'PersonAuthor';
					}

					$data['author']  = [ '@id' => $author . '#author' ];
					$data['creator'] = [ '@id' => $author . '#author' ];
				}
			}
		}

		if ( isset( aioseo()->schema->context['description'] ) && aioseo()->schema->context['description'] ) {
			$data['description'] = aioseo()->schema->context['description'];
		}

		if ( is_singular() ) {
			if ( ! isset( aioseo()->schema->context['object'] ) || ! aioseo()->schema->context['object'] ) {
				return $this->getAddonData( $data, 'webPage' );
			}

			$post = aioseo()->schema->context['object'];
			if ( has_post_thumbnail( $post ) ) {
				$image = $this->image( get_post_thumbnail_id(), 'mainImage' );
				if ( $image ) {
					$data['image']              = $image;
					$data['primaryImageOfPage'] = [
						'@id' => aioseo()->schema->context['url'] . '#mainImage'
					];
				}
			}

			$data['datePublished'] = mysql2date( DATE_W3C, $post->post_date, false );
			$data['dateModified']  = mysql2date( DATE_W3C, $post->post_modified, false );

			return $this->getAddonData( $data, 'webPage' );
		}

		if ( is_front_page() ) {
			$data['about'] = [ '@id' => trailingslashit( home_url() ) . '#' . aioseo()->options->searchAppearance->global->schema->siteRepresents ];
		}

		return $this->getAddonData( $data, 'webPage' );
	}
}Common/Schema/Graphs/WebSite.php000064400000001747151536241200012503 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs;

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

/**
 * WebSite graph class.
 *
 * @since 4.0.0
 */
class WebSite extends Graph {
	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 *
	 * @return array $data The graph data.
	 */
	public function get() {
		$homeUrl = trailingslashit( home_url() );
		$data    = [
			'@type'         => 'WebSite',
			'@id'           => $homeUrl . '#website',
			'url'           => $homeUrl,
			'name'          => aioseo()->helpers->getWebsiteName(),
			'alternateName' => aioseo()->tags->replaceTags( aioseo()->options->searchAppearance->global->schema->websiteAlternateName ),
			'description'   => aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'description' ) ),
			'inLanguage'    => aioseo()->helpers->currentLanguageCodeBCP47(),
			'publisher'     => [ '@id' => $homeUrl . '#' . aioseo()->options->searchAppearance->global->schema->siteRepresents ]
		];

		return $data;
	}
}Common/Schema/Helpers.php000064400000006776151536241200011326 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema;

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

/**
 * Contains helper methods for our schema classes.
 *
 * @since 4.2.5
 */
class Helpers {
	/**
	 * Checks whether the schema markup feature is enabled.
	 *
	 * @since 4.2.5
	 *
	 * @return bool Whether the schema markup feature is enabled or not.
	 */
	public function isEnabled() {
		$isEnabled = ! in_array( 'enableSchemaMarkup', aioseo()->internalOptions->deprecatedOptions, true ) || aioseo()->options->deprecated->searchAppearance->global->schema->enableSchemaMarkup;

		return ! apply_filters( 'aioseo_schema_disable', ! $isEnabled );
	}

	/**
	 * Strips HTML and removes all blank properties in each of our graphs.
	 * Also parses properties that might contain smart tags.
	 *
	 * @since   4.0.13
	 * @version 4.2.5
	 *
	 * @param  array  $data        The graph data.
	 * @param  string $parentKey   The key of the group parent (optional).
	 * @param  bool   $replaceTags Whether the smart tags should be replaced.
	 * @return array               The cleaned graph data.
	 */
	public function cleanAndParseData( $data, $parentKey = '', $replaceTags = true ) {
		foreach ( $data as $k => &$v ) {
			if ( is_numeric( $v ) || is_bool( $v ) || is_null( $v ) ) {
				// Do nothing.
			} elseif ( is_array( $v ) ) {
				$v = $this->cleanAndParseData( $v, $k, $replaceTags );
			} else {
				// Check if the prop can contain some HTML tags.
				if (
					isset( aioseo()->schema->htmlAllowedFields[ $parentKey ] ) &&
					in_array( $k, aioseo()->schema->htmlAllowedFields[ $parentKey ], true )
				) {
					$v = trim( wp_kses_post( $v ) );
				} else {
					$v = trim( wp_strip_all_tags( $v ) );
				}

				$v = $replaceTags ? aioseo()->tags->replaceTags( $v, get_the_ID() ) : $v;
			}

			if ( empty( $v ) && ! in_array( $k, aioseo()->schema->nullableFields, true ) ) {
				unset( $data[ $k ] );
			} else {
				$data[ $k ] = $v;
			}
		}

		return $data;
	}

	/**
	 * Sorts the schema data and then returns it as JSON.
	 * We temporarily change the floating point precision in order to prevent rounding errors.
	 * Otherwise e.g. 4.9 could be output as 4.90000004.
	 *
	 * @since 4.2.7
	 *
	 * @param  array  $schema      The schema data.
	 * @param  bool   $replaceTags Whether the smart tags should be replaced.
	 * @return string              The schema as JSON.
	 */
	public function getOutput( $schema, $replaceTags = true ) {
		$schema['@graph'] = apply_filters( 'aioseo_schema_output', $schema['@graph'] );
		$schema['@graph'] = $this->cleanAndParseData( $schema['@graph'], '', $replaceTags );

		// Sort the graphs alphabetically.
		usort( $schema['@graph'], function ( $a, $b ) {
			$typeA = $a['@type'] ?? null;
			$typeB = $b['@type'] ?? null;

			if ( is_null( $typeA ) || is_array( $typeA ) ) {
				return 1;
			}

			if ( is_null( $typeB ) || is_array( $typeB ) ) {
				return -1;
			}

			return strcmp( $typeA, $typeB );
		} );

		// Allow users to control the default json_encode flags.
		// Some users report better SEO performance when non-Latin unicode characters are not escaped.
		$jsonFlags = apply_filters( 'aioseo_schema_json_flags', 0 );

		$json = isset( $_GET['aioseo-dev'] ) || aioseo()->schema->generatingValidatorOutput // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			? aioseo()->helpers->wpJsonEncode( $schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE )
			: aioseo()->helpers->wpJsonEncode( $schema, $jsonFlags );

		return $json;
	}
}Common/Schema/Schema.php000064400000021213151536241200011103 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
use AIOSEO\Plugin\Common\Integrations\BbPress as BbPressIntegration;

/**
 * Builds our schema.
 *
 * @since 4.0.0
 */
class Schema {
	/**
	 * The graphs that need to be generated.
	 *
	 * @since 4.2.5
	 *
	 * @var array
	 */
	public $graphs = [];

	/**
	 * The context data.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $context = [];

	/**
	 * Helpers class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Helpers
	 */
	public $helpers = null;

	/**
	 * The subdirectories that contain graph classes.
	 *
	 * @since 4.2.5
	 *
	 * @var array
	 */
	protected $graphSubDirectories = [
		'Article',
		'KnowledgeGraph',
		'WebPage'
	];

	/**
	 * All existing WebPage graphs.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $webPageGraphs = [
		'WebPage',
		'AboutPage',
		'CheckoutPage',
		'CollectionPage',
		'ContactPage',
		'FAQPage',
		'ItemPage',
		'MedicalWebPage',
		'ProfilePage',
		'RealEstateListing',
		'SearchResultsPage'
	];

	/**
	 * Fields that can be 0 or null, which shouldn't be stripped when cleaning the data.
	 *
	 * @since 4.1.2
	 *
	 * @var array
	 */
	public $nullableFields = [
		'price',          // Needs to be 0 if free for Software Application.
		'ratingValue',    // Needs to be 0 for 0 star ratings.
		'value',          // Needs to be 0 if free for product shipping details.
		'minValue',       // Needs to be 0 for product delivery time.
		'maxValue',       // Needs to be 0 for product delivery time.
		'suggestedMinAge' // Needs to be 0 for PeopleAudience minimum age.
	];

	/**
	 * List of mapped parents with properties that are allowed to contain a restricted set of HTML tags.
	 *
	 * @since 4.2.3
	 *
	 * @var array
	 */
	public $htmlAllowedFields = [
		// FAQPage
		'acceptedAnswer' => [
			'text'
		]
	];

	/**
	 * Whether we are generating the validator output.
	 *
	 * @since 4.6.3
	 *
	 * @var bool
	 */
	public $generatingValidatorOutput = false;

	/**
	 * Class constructor.
	 */
	public function __construct() {
		// No AJAX check since we need to be able to grab the schema output via the REST API.
		if ( wp_doing_cron() ) {
			return;
		}

		$this->helpers = new Helpers();
	}

	/**
	 * Returns the JSON schema output.
	 *
	 * @since 4.0.0
	 *
	 * @return string The JSON schema output.
	 */
	public function get() {
		// First, check if the schema is disabled.
		if ( ! $this->helpers->isEnabled() ) {
			return '';
		}

		$this->determineSmartGraphsAndContext();

		return $this->generateSchema();
	}

	/**
	 * Generates the JSON schema after the graphs/context have been determined.
	 *
	 * @since 4.2.5
	 *
	 * @return string The JSON schema output.
	 */
	protected function generateSchema() {
		// Now, filter the graphs.
		$this->graphs = apply_filters(
			'aioseo_schema_graphs',
			array_unique( array_filter( array_values( $this->graphs ) ) )
		);

		if ( ! $this->graphs ) {
			return '';
		}

		// Check if a WebPage graph is included. Otherwise add the default one.
		$webPageGraphFound = false;
		foreach ( $this->graphs as $graphName ) {
			if ( in_array( $graphName, $this->webPageGraphs, true ) ) {
				$webPageGraphFound = true;
				break;
			}
		}

		if ( ! $webPageGraphFound ) {
			$this->graphs[] = 'WebPage';
		}

		// Now that we've determined the graphs, start generating their data.
		$schema = [
			'@context' => 'https://schema.org',
			'@graph'   => []
		];

		// By determining the length of the array after every iteration, we are able to add additional graphs during runtime.
		// e.g. The Article graph may require a Person graph to be output for the author.
		$this->graphs = array_values( $this->graphs );
		for ( $i = 0; $i < count( $this->graphs ); $i++ ) {
			$namespace = $this->getGraphNamespace( $this->graphs[ $i ] );
			if ( $namespace ) {
				$schema['@graph'][] = ( new $namespace() )->get();
			}
		}

		return aioseo()->schema->helpers->getOutput( $schema );
	}

	/**
	 * Gets the relevant namespace for the given graph.
	 *
	 * @since 4.2.5
	 *
	 * @param  string $graphName The graph name.
	 * @return string            The namespace.
	 */
	protected function getGraphNamespace( $graphName ) {
		$namespace = "\AIOSEO\Plugin\Common\Schema\Graphs\\{$graphName}";
		if ( class_exists( $namespace ) ) {
			return $namespace;
		}

		// If we can't find it in the root dir, check if we can find it in a sub dir.
		foreach ( $this->graphSubDirectories as $dirName ) {
			$namespace = "\AIOSEO\Plugin\Common\Schema\Graphs\\{$dirName}\\{$graphName}";
			if ( class_exists( $namespace ) ) {
				return $namespace;
			}
		}

		return '';
	}

	/**
	 * Determines the smart graphs that need to be output by default, as well as the current context for the breadcrumbs.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	protected function determineSmartGraphsAndContext() {
		$this->graphs = array_merge( $this->graphs, $this->getDefaultGraphs() );

		$contextInstance = new Context();
		$this->context   = $contextInstance->defaults();

		if ( BuddyPressIntegration::isComponentPage() ) {
			aioseo()->standalone->buddyPress->component->determineSchemaGraphsAndContext( $contextInstance );

			return;
		}

		if ( BbPressIntegration::isComponentPage() ) {
			aioseo()->standalone->bbPress->component->determineSchemaGraphsAndContext();

			return;
		}

		if ( aioseo()->helpers->isDynamicHomePage() ) {
			$this->graphs[] = 'CollectionPage';
			$this->context  = $contextInstance->home();

			return;
		}

		if ( is_home() || aioseo()->helpers->isWooCommerceShopPage() ) {
			$this->graphs[] = 'CollectionPage';
			$this->context  = $contextInstance->post();

			return;
		}

		if ( is_singular() ) {
			$this->determineContextSingular( $contextInstance );

			if ( is_singular( 'web-story' ) ) {
				$this->graphs[] = 'AmpStory';
			}
		}

		if ( is_category() || is_tag() || is_tax() ) {
			$this->graphs[] = 'CollectionPage';
			$this->context  = $contextInstance->term();

			return;
		}

		if ( is_author() ) {
			$this->graphs[] = 'ProfilePage';
			$this->graphs[] = 'PersonAuthor';
			$this->context  = $contextInstance->author();
		}

		if ( is_post_type_archive() ) {
			$this->graphs[] = 'CollectionPage';
			$this->context  = $contextInstance->postArchive();

			return;
		}

		if ( is_date() ) {
			$this->graphs[] = 'CollectionPage';
			$this->context  = $contextInstance->date();

			return;
		}

		if ( is_search() ) {
			$this->graphs[] = 'SearchResultsPage';
			$this->context  = $contextInstance->search();

			return;
		}

		if ( is_404() ) {
			$this->context = $contextInstance->notFound();
		}
	}

	/**
	 * Determines the smart graphs and context for singular pages.
	 *
	 * @since 4.2.6
	 *
	 * @param  Context $contextInstance The Context class instance.
	 * @return void
	 */
	protected function determineContextSingular( $contextInstance ) {
		// If the current request is for the validator, we can't include the default graph here.
		// We need to include the default graph that the validator sent.
		// Don't do this if we're in Pro since we then need to get it from the post meta.
		if ( ! $this->generatingValidatorOutput ) {
			$this->graphs[] = $this->getDefaultPostGraph();
		}

		$this->context = $contextInstance->post();
	}

	/**
	 * Returns the default graph for the post type.
	 *
	 * @since 4.2.6
	 *
	 * @return string The default graph.
	 */
	public function getDefaultPostGraph() {
		return $this->getDefaultPostTypeGraph();
	}

	/**
	 * Returns the default graph for the current post type.
	 *
	 * @since 4.2.5
	 *
	 * @param  \WP_Post $post The post object.
	 * @return string         The default graph.
	 */
	public function getDefaultPostTypeGraph( $post = null ) {
		$post = $post ? $post : aioseo()->helpers->getPost();
		if ( ! is_a( $post, 'WP_Post' ) ) {
			return '';
		}

		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		if ( ! $dynamicOptions->searchAppearance->postTypes->has( $post->post_type ) ) {
			return '';
		}

		$defaultType = $dynamicOptions->searchAppearance->postTypes->{$post->post_type}->schemaType;
		switch ( $defaultType ) {
			case 'Article':
				return $dynamicOptions->searchAppearance->postTypes->{$post->post_type}->articleType;
			case 'WebPage':
				return $dynamicOptions->searchAppearance->postTypes->{$post->post_type}->webPageType;
			default:
				return $defaultType;
		}
	}

	/**
	 * Returns the default graphs that should be output on every page, regardless of its type.
	 *
	 * @since 4.2.5
	 *
	 * @return array The default graphs.
	 */
	protected function getDefaultGraphs() {
		$siteRepresents = ucfirst( aioseo()->options->searchAppearance->global->schema->siteRepresents );

		return [
			'BreadcrumbList',
			'Kg' . $siteRepresents,
			'WebSite'
		];
	}
}Common/SearchCleanup/SearchCleanup.php000064400000010313151536241200013734 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchCleanup;

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

/**
 * Class for Search Cleanup that handles prevention of search spams.
 *
 * @since 4.8.0
 */
class SearchCleanup {
	/**
	 * Patterns to match against to find spam.
	 *
	 * @since 4.8.0
	 *
	 * @var array
	 */
	private $patterns = [
		'/[:()【】[]]+/u',
		'/(TALK|QQ)\:/iu',
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.8.0
	 */
	public function __construct() {
		// If Crawl Cleanup is disabled, return early.
		if ( ! aioseo()->options->searchAppearance->advanced->crawlCleanup->enable ) {
			return;
		}

		if ( aioseo()->options->searchAppearance->advanced->searchCleanup->enable ) {
			add_filter( 'pre_get_posts', [ $this, 'validateSearch' ] );
		}

		if ( aioseo()->options->searchAppearance->advanced->searchCleanup->settings->redirectPrettyUrls ) {
			add_action( 'template_redirect', [ $this, 'maybeRedirectSearches' ], 0 );
		}
	}

	/**
	 * Check against unwanted patterns.
	 *
	 * @since 4.8.0
	 *
	 * @param  \WP_Query $query The main query.
	 * @return \WP_Query        The main query.
	 */
	public function validateSearch( $query ) {
		if ( ! $query->is_search() ) {
			return $query;
		}

		$searchString = rawurldecode( $query->get( 's' ) );

		$this->checkEmojis( $searchString );
		$this->checkCommonSpamPatterns( $searchString );
		$this->limitCharacters();

		return $query;
	}

	/**
	 * Limits the number of characters in the search term.
	 *
	 * @since 4.8.0
	 *
	 * @return void
	 */
	private function limitCharacters() {
		// We retrieve the search term unescaped as we want to count the characters. We make sure to escape it afterwards before we continue tom process it.
		$unescapedTerm = get_search_query( false );

		$maxAllowedNumberOfChars = aioseo()->options->searchAppearance->advanced->searchCleanup->settings->maxAllowedNumberOfChars;

		$rawSearchTerm = wp_unslash( $unescapedTerm );
		if ( mb_strlen( $rawSearchTerm, 'UTF-8' ) > $maxAllowedNumberOfChars ) {
			$newS = mb_substr( $rawSearchTerm, 0, $maxAllowedNumberOfChars, 'UTF-8' );
			set_query_var( 's', wp_slash( esc_attr( $newS ) ) );
		}
	}

	/**
	 * Check if query contains emojis and special characters.
	 *
	 * @since 4.8.0
	 *
	 * @param  string $searchString The search string.
	 * @return void
	 */
	private function checkEmojis( $searchString ) {
		if ( ! aioseo()->options->searchAppearance->advanced->searchCleanup->settings->emojisAndSymbols ) {
			return;
		}

		if ( aioseo()->helpers->hasEmojis( $searchString ) ) {
			aioseo()->helpers->notFoundPage();
		}
	}

	/**
	 * Checks against common search spam patterns.
	 *
	 * @since 4.8.0
	 *
	 * @param  string $searchString Search string.
	 * @return void
	 */
	private function checkCommonSpamPatterns( $searchString ) {
		if ( ! aioseo()->options->searchAppearance->advanced->searchCleanup->settings->commonPatterns ) {
			return;
		}

		$patterns = apply_filters( 'aioseo_search_cleanup_patterns', $this->patterns );
		foreach ( $patterns as $pattern ) {
			if ( preg_match( $pattern, $searchString ) ) {
				aioseo()->helpers->notFoundPage();
			}
		}
	}

	/**
	 * Redirect pretty search URLs to the "raw" equivalent
	 *
	 * @since 4.8.0
	 *
	 * @return void
	 */
	public function maybeRedirectSearches() {
		if ( ! is_search() ) {
			return;
		}

		$requestUri = aioseo()->helpers->getRequestUrl();
		if ( stripos( $requestUri, '/search/' ) === 0 ) {
			$args = [];

			$parsed = wp_parse_url( $requestUri );
			if ( ! empty( $parsed['query'] ) ) {
				wp_parse_str( $parsed['query'], $args );
			}

			// Extract the search query directly from the REQUEST_URI.
			$searchPath = trim( str_replace( '/search/', '', $parsed['path'] ), '/' );
			$args['s']  = aioseo()->helpers->decodeUrl( $searchPath );
			$properUrl  = home_url( '/' );

			if ( intval( get_query_var( 'paged' ) ) > 1 ) {
				$properUrl .= sprintf( 'page/%s/', \get_query_var( 'paged' ) );
				unset( $args['paged'] );
			}

			$properUrl = add_query_arg( array_map( 'rawurlencode_deep', $args ), $properUrl );

			if ( ! empty( $parsed['fragment'] ) ) {
				$properUrl .= '#' . rawurlencode( $parsed['fragment'] );
			}

			aioseo()->helpers->redirect( $properUrl, 301, 'We redirect pretty URLs to the raw format.' );
		}
	}
}Common/SearchStatistics/Api/Api.php000064400000004550151536241200013212 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchStatistics\Api;

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

/**
 * API class.
 *
 * @since   4.3.0
 * @version 4.6.2 Moved from Pro to Common.
 */
class Api {
	/**
	 * Holds the instance of the Auth class.
	 *
	 * @since 4.3.0
	 *
	 * @var Auth
	 */
	public $auth;

	/**
	 * Holds the instance of the TrustToken class.
	 *
	 * @since 4.3.0
	 *
	 * @var TrustToken
	 */
	public $trustToken;

	/**
	 * Holds the instance of the Listener class.
	 *
	 * @since 4.3.0
	 *
	 * @var Listener
	 */
	public $listener;

	/**
	 * The base URL for the Search Statistics microservice.
	 *
	 * @since 4.3.0
	 *
	 * @var string
	 */
	private $url = 'google.aioseo.com';

	/**
	 * The API version for the Search Statistics microservice.
	 *
	 * @since 4.3.0
	 *
	 * @var string
	 */
	private $version = 'v1';

	/**
	 * Class constructor.
	 *
	 * @since 4.3.0
	 */
	public function __construct() {
		$this->auth       = new Auth();
		$this->trustToken = new TrustToken();
		$this->listener   = new Listener();
	}

	/**
	 * Returns the site identifier key according to the WordPress keys.
	 *
	 * @since 4.3.0
	 *
	 * @return string The site identifier key.
	 */
	public function getSiteIdentifier() {
		$authKey       = defined( 'AUTH_KEY' ) ? AUTH_KEY : '';
		$secureAuthKey = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '';
		$loggedInKey   = defined( 'LOGGED_IN_KEY' ) ? LOGGED_IN_KEY : '';

		$siteIdentifier = $authKey . $secureAuthKey . $loggedInKey;
		$siteIdentifier = preg_replace( '/[^a-zA-Z0-9]/', '', (string) $siteIdentifier );
		$siteIdentifier = sanitize_text_field( $siteIdentifier );
		$siteIdentifier = trim( $siteIdentifier );
		$siteIdentifier = ( strlen( $siteIdentifier ) > 30 ) ? substr( $siteIdentifier, 0, 30 ) : $siteIdentifier;

		return $siteIdentifier;
	}

	/**
	 * Returns the URL of the remote endpoint.
	 *
	 * @since 4.3.0
	 *
	 * @return string The URL.
	 */
	public function getApiUrl() {
		if ( defined( 'AIOSEO_SEARCH_STATISTICS_API_URL' ) ) {
			return AIOSEO_SEARCH_STATISTICS_API_URL;
		}

		return $this->url;
	}

	/**
	 * Returns the version of the remote endpoint.
	 *
	 * @since 4.3.0
	 *
	 * @return string The version.
	 */
	public function getApiVersion() {
		if ( defined( 'AIOSEO_SEARCH_STATISTICS_API_VERSION' ) ) {
			return AIOSEO_SEARCH_STATISTICS_API_VERSION;
		}

		return $this->version;
	}
}Common/SearchStatistics/Api/Auth.php000064400000010064151536241200013377 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;
	}
}Common/SearchStatistics/Api/Listener.php000064400000016145151536241200014271 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 );
	}
}Common/SearchStatistics/Api/Request.php000064400000022526151536241200014134 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.' );
				}
			}
		}
	}
}Common/SearchStatistics/Api/TrustToken.php000064400000003001151536241200014611 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 );
	}
}Common/SearchStatistics/IndexStatus.php000064400000023210151536241200014235 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchStatistics;

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

/**
 * Index Status class.
 *
 * @since 4.8.2
 */
class IndexStatus {
	/**
	 * Retrieves the overview data.
	 *
	 * @since 4.8.2
	 *
	 * @return array The overview data.
	 */
	public function getOverview() {
		$data = [
			'post' => [
				'results' => [
					[
						'count'         => 164,
						'coverageState' => 'Submitted and Indexed', // No need to translate this. It's translated on the front-end.
					],
					[
						'count'         => 112,
						'coverageState' => 'Discovered - Currently Not Indexed',
					],
					[
						'count'         => 44,
						'coverageState' => 'Crawled - Currently Not Indexed',
					],
					[
						'count'         => 8,
						'coverageState' => 'URL is unknown to Google',
					]
				]
			]
		];

		$data['post']['total'] = array_sum( array_column( $data['post']['results'], 'count' ) );

		return $data;
	}

	/**
	 * Retrieves all the objects, formatted.
	 *
	 * @since 4.8.2
	 *
	 * @return array The formatted objects.
	 */
	public function getFormattedObjects() {
		$siteUrl = aioseo()->helpers->getSiteUrl();

		$rows = [
			[
				'objectId'             => 4,
				'objectTitle'          => 'Homepage',
				'verdict'              => 'PASS',
				'coverageState'        => 'Submitted and Indexed',
				'robotsTxtState'       => 'ALLOWED',
				'indexingState'        => 'INDEXING_ALLOWED',
				'pageFetchState'       => 'SUCCESSFUL',
				'crawledAs'            => 'MOBILE',
				'lastCrawlTime'        => aioseo()->helpers->dateToWpFormat( '2025-01-05 13:54:00' ),
				'userCanonical'        => $siteUrl,
				'googleCanonical'      => $siteUrl,
				'sitemap'              => [
					aioseo()->sitemap->helpers->getUrl( 'general' )
				],
				'referringUrls'        => [],
				'richResultsResult'    => [
					'detectedItems' => [
						[
							'richResultType' => 'Breadcrumbs',
							'items'          => [
								[
									'name' => 'Unnamed item'
								]
							]
						],
						[
							'richResultType' => 'FAQ',
							'items'          => [
								[
									'name' => 'Unnamed item'
								]
							]
						]
					]
				],
				'inspectionResultLink' => '#',
				'richResultsTestLink'  => '#'
			],
			[
				'objectId'             => 6,
				'objectTitle'          => 'About',
				'verdict'              => 'PASS',
				'coverageState'        => 'Submitted and Indexed',
				'robotsTxtState'       => 'ALLOWED',
				'indexingState'        => 'INDEXING_ALLOWED',
				'pageFetchState'       => 'SUCCESSFUL',
				'crawledAs'            => 'MOBILE',
				'lastCrawlTime'        => aioseo()->helpers->dateToWpFormat( '2025-01-06 09:22:00' ),
				'userCanonical'        => $siteUrl . '/about',
				'googleCanonical'      => $siteUrl . '/about',
				'sitemap'              => [
					aioseo()->sitemap->helpers->getUrl( 'general' )
				],
				'referringUrls'        => [
					$siteUrl
				],
				'richResultsResult'    => [
					'detectedItems' => [
						[
							'richResultType' => 'Breadcrumbs',
							'items'          => [
								[
									'name' => 'Unnamed item'
								]
							]
						]
					]
				],
				'inspectionResultLink' => '#',
				'richResultsTestLink'  => '#'
			],
			[
				'objectId'             => 1,
				'objectTitle'          => 'Contact Us',
				'verdict'              => 'PASS',
				'coverageState'        => 'Submitted and Indexed',
				'robotsTxtState'       => 'ALLOWED',
				'indexingState'        => 'INDEXING_ALLOWED',
				'pageFetchState'       => 'SUCCESSFUL',
				'crawledAs'            => 'DESKTOP',
				'lastCrawlTime'        => aioseo()->helpers->dateToWpFormat( '2025-01-02 16:47:00' ),
				'userCanonical'        => $siteUrl . '/contact-us',
				'googleCanonical'      => $siteUrl . '/contact-us',
				'sitemap'              => [
					aioseo()->sitemap->helpers->getUrl( 'general' )
				],
				'referringUrls'        => [
					$siteUrl
				],
				'richResultsResult'    => [
					'detectedItems' => [
						[
							'richResultType' => 'Breadcrumbs',
							'items'          => [
								[
									'name' => 'Unnamed item'
								]
							]
						],
						[
							'richResultType' => 'FAQ',
							'items'          => [
								[
									'name' => 'Unnamed item'
								]
							]
						]
					]
				],
				'inspectionResultLink' => '#',
				'richResultsTestLink'  => '#'
			],
			[
				'objectId'             => 2,
				'objectTitle'          => 'Pricing',
				'verdict'              => 'NEUTRAL',
				'coverageState'        => 'Crawled - Currently Not Indexed',
				'robotsTxtState'       => 'DISALLOWED',
				'indexingState'        => 'BLOCKED_BY_META_TAG',
				'pageFetchState'       => 'SUCCESSFUL',
				'crawledAs'            => 'DESKTOP',
				'lastCrawlTime'        => aioseo()->helpers->dateToWpFormat( '2024-01-15 11:00:00' ),
				'userCanonical'        => $siteUrl . '/pricing',
				'googleCanonical'      => $siteUrl . '/pricing',
				'sitemap'              => [
					aioseo()->sitemap->helpers->getUrl( 'general' )
				],
				'referringUrls'        => [
					$siteUrl
				],
				'richResultsResult'    => [
					'detectedItems' => [
						[
							'richResultType' => 'Breadcrumbs',
							'items'          => [
								[
									'name' => 'Unnamed item'
								]
							]
						],
						[
							'richResultType' => 'Product snippet',
							'items'          => [
								[
									'name'   => 'All in One SEO (AIOSEO)',
									'issues' => [
										[
											'issueMessage' => 'Missing field "priceValidUntil"',
											'severity'     => 'WARNING'
										]
									]
								]
							]
						]
					]
				],
				'inspectionResultLink' => '#',
				'richResultsTestLink'  => '#'
			],
			[
				'objectId'             => 3,
				'objectTitle'          => 'Blog',
				'verdict'              => 'PASS',
				'coverageState'        => 'Submitted and Indexed',
				'robotsTxtState'       => 'ALLOWED',
				'indexingState'        => 'INDEXED',
				'pageFetchState'       => 'SUCCESSFUL',
				'crawledAs'            => 'MOBILE',
				'lastCrawlTime'        => aioseo()->helpers->dateToWpFormat( '2024-03-01 08:00:00' ),
				'userCanonical'        => $siteUrl . '/blog',
				'googleCanonical'      => $siteUrl . '/blog',
				'sitemap'              => [
					aioseo()->sitemap->helpers->getUrl( 'general' )
				],
				'referringUrls'        => [
					$siteUrl
				],
				'inspectionResultLink' => '#',
				'richResultsTestLink'  => '#'
			],
		];

		return [
			'paginated' => [
				'rows'   => $rows,
				'totals' => [
					'total' => count( $rows ),
					'pages' => 1,
					'page'  => 1
				]
			]
		];
	}

	/**
	 * Returns the data for Vue.
	 *
	 * @since 4.8.2
	 *
	 * @return array The data for Vue.
	 */
	public function getVueData() {
		return [
			'objects'  => $this->getFormattedObjects(),
			'overview' => $this->getOverview(),
			'options'  => $this->getUiOptions()
		];
	}

	/**
	 * Retrieves options ideally only for Vue usage on the front-end.
	 *
	 * @since 4.8.2
	 *
	 * @return array The options.
	 */
	protected function getUiOptions() {
		$postTypeOptions = [
			[
				'label' => __( 'All Post Types', 'all-in-one-seo-pack' ),
				'value' => ''
			],
			[
				'label' => __( 'Post', 'all-in-one-seo-pack' ),
				'value' => 'post'
			],
			[
				'label' => __( 'Page', 'all-in-one-seo-pack' ),
				'value' => 'page'
			]
		];

		$statusOptions = [
			[
				'label' => __( 'Status (All)', 'all-in-one-seo-pack' ),
				'value' => ''
			],
			[
				'label' => __( 'Indexed', 'all-in-one-seo-pack' ),
				'value' => 'submitted',
				'color' => '#00AA63',
			],
			[
				'label' => __( 'Crawled, Not Indexed', 'all-in-one-seo-pack' ),
				'value' => 'crawled',
				'color' => '#F18200',
			],
			[
				'label' => __( 'Discovered, Not Indexed', 'all-in-one-seo-pack' ),
				'value' => 'discovered',
				'color' => '#005AE0',
			],
			[
				'label' => __( 'Other, Not Indexed', 'all-in-one-seo-pack' ),
				'value' => 'unknown|excluded|invalid|error',
				'color' => '#DF2A4A',
			],
			[
				'label' => __( 'No Results Yet', 'all-in-one-seo-pack' ),
				'value' => 'empty',
				'color' => '#999999',
			]
		];

		$robotsTxtStateOptions = [
			[
				'label' => __( 'Robots.txt (All)', 'all-in-one-seo-pack' ),
				'value' => ''
			],
			[
				'label' => __( 'Allowed', 'all-in-one-seo-pack' ),
				'value' => 'ALLOWED'
			],
			[
				'label' => __( 'Blocked', 'all-in-one-seo-pack' ),
				'value' => 'DISALLOWED'
			]
		];

		$crawledAsOptions = [
			[
				'label' => __( 'Crawled As (All)', 'all-in-one-seo-pack' ),
				'value' => ''
			],
			[
				'label' => __( 'Desktop', 'all-in-one-seo-pack' ),
				'value' => 'DESKTOP'
			],
			[
				'label' => __( 'Mobile', 'all-in-one-seo-pack' ),
				'value' => 'MOBILE'
			]
		];

		$pageFetchStateOptions = [
			[
				'label' => __( 'Page Fetch (All)', 'all-in-one-seo-pack' ),
				'value' => ''
			],
			[
				'label' => __( 'Successful', 'all-in-one-seo-pack' ),
				'value' => 'SUCCESSFUL'
			],
			[
				'label' => __( 'Error', 'all-in-one-seo-pack' ),
				'value' => 'SOFT_404,BLOCKED_ROBOTS_TXT,NOT_FOUND,ACCESS_DENIED,SERVER_ERROR,REDIRECT_ERROR,ACCESS_FORBIDDEN,BLOCKED_4XX,INTERNAL_CRAWL_ERROR,INVALID_URL'
			]
		];

		$additionalFilters = [
			'postTypeOptions'       => [
				'name'    => 'postType',
				'options' => $postTypeOptions
			],
			'statusOptions'         => [
				'name'    => 'status',
				'options' => $statusOptions
			],
			'robotsTxtStateOptions' => [
				'name'    => 'robotsTxtState',
				'options' => $robotsTxtStateOptions
			],
			'pageFetchStateOptions' => [
				'name'    => 'pageFetchState',
				'options' => $pageFetchStateOptions
			],
			'crawledAsOptions'      => [
				'name'    => 'crawledAs',
				'options' => $crawledAsOptions
			],
		];

		return [
			'table' => [
				'additionalFilters' => $additionalFilters
			]
		];
	}
}Common/SearchStatistics/KeywordRankTracker.php000064400000020037151536241200015542 0ustar00<?php

namespace AIOSEO\Plugin\Common\SearchStatistics;

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

/**
 * Keyword Rank Tracker class.
 *
 * @since 4.7.0
 */
class KeywordRankTracker {
	/**
	 * Retrieves all the keywords' statistics.
	 *
	 * @since 4.7.0
	 *
	 * @param  array $formattedKeywords The formatted keywords.
	 * @param  array $args              The arguments.
	 * @return array                    The statistics for the keywords.
	 */
	public function fetchKeywordsStatistics( &$formattedKeywords = [], $args = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return [
			'distribution'          => [
				'top3'       => '6.86',
				'top10'      => '11.03',
				'top50'      => '52.10',
				'top100'     => '30.01',
				'difference' => [
					'top3'   => '24.31',
					'top10'  => '33.70',
					'top50'  => '-30.50',
					'top100' => '-27.51'
				]
			],
			'distributionIntervals' => [
				[
					'date'   => '2022-10-23',
					'top3'   => '30.70',
					'top10'  => '38.60',
					'top50'  => '24.50',
					'top100' => '6.20'
				],
				[
					'date'   => '2022-10-30',
					'top3'   => '31.60',
					'top10'  => '42.10',
					'top50'  => '21.00',
					'top100' => '5.30'
				],
				[
					'date'   => '2022-11-06',
					'top3'   => '31.30',
					'top10'  => '44.40',
					'top50'  => '20.30',
					'top100' => '4.00'
				],
				[
					'date'   => '2022-11-13',
					'top3'   => '31.70',
					'top10'  => '44.20',
					'top50'  => '20.20',
					'top100' => '3.90'
				],
				[
					'date'   => '2022-11-20',
					'top3'   => '31.70',
					'top10'  => '45.70',
					'top50'  => '18.00',
					'top100' => '4.60'
				],
				[
					'date'   => '2022-11-27',
					'top3'   => '32.50',
					'top10'  => '47.80',
					'top50'  => '16.80',
					'top100' => '2.90'
				],
				[
					'date'   => '2022-12-04',
					'top3'   => '32.50',
					'top10'  => '47.20',
					'top50'  => '17.90',
					'top100' => '2.40'
				],
				[
					'date'   => '2022-12-11',
					'top3'   => '31.80',
					'top10'  => '43.70',
					'top50'  => '21.00',
					'top100' => '3.50'
				],
				[
					'date'   => '2022-12-18',
					'top3'   => '30.40',
					'top10'  => '43.60',
					'top50'  => '22.40',
					'top100' => '3.60'
				],
				[
					'date'   => '2022-12-25',
					'top3'   => '26.90',
					'top10'  => '37.20',
					'top50'  => '29.70',
					'top100' => '6.20'
				],
				[
					'date'   => '2023-01-01',
					'top3'   => '27.00',
					'top10'  => '33.80',
					'top50'  => '31.60',
					'top100' => '7.60'
				],
				[
					'date'   => '2023-01-08',
					'top3'   => '26.60',
					'top10'  => '38.60',
					'top50'  => '30.00',
					'top100' => '4.80'
				],
				[
					'date'   => '2023-01-16',
					'top3'   => '31.10',
					'top10'  => '43.90',
					'top50'  => '22.50',
					'top100' => '2.50'
				]
			]
		];
	}

	/**
	 * Retrieves all the keywords, formatted.
	 *
	 * @since 4.7.0
	 *
	 * @param  array $args The arguments.
	 * @return array       The formatted keywords.
	 */
	public function getFormattedKeywords( $args = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$statistics = [];
		for ( $i = 1; $i < 9; $i++ ) {
			$statistics[ $i ] = [
				'clicks'      => wp_rand( 1, 1000 ),
				'impressions' => wp_rand( 10, 10000 ),
				'ctr'         => wp_rand( 1, 99 ),
				'position'    => wp_rand( 1, 100 ),
				'history'     => [
					[
						'date'     => gmdate( 'Y-m-d', strtotime( '-30 days' ) ),
						'position' => wp_rand( 1, 15 ),
						'clicks'   => wp_rand( 10, 100 ),
					],
					[
						'date'     => gmdate( 'Y-m-d', strtotime( '-23 days' ) ),
						'position' => wp_rand( 1, 15 ),
						'clicks'   => wp_rand( 10, 100 ),
					],
					[
						'date'     => gmdate( 'Y-m-d', strtotime( '-16 days' ) ),
						'position' => wp_rand( 1, 15 ),
						'clicks'   => wp_rand( 10, 100 ),
					],
					[
						'date'     => gmdate( 'Y-m-d', strtotime( '-9 days' ) ),
						'position' => wp_rand( 1, 15 ),
						'clicks'   => wp_rand( 10, 100 ),
					],
					[
						'date'     => gmdate( 'Y-m-d', strtotime( '-2 days' ) ),
						'position' => wp_rand( 1, 15 ),
						'clicks'   => wp_rand( 10, 100 ),
					]
				]
			];
		}

		return [
			'rows'   => [
				[
					'id'         => 1,
					'name'       => 'best seo plugin',
					'favorited'  => false,
					'groups'     => [
						[
							'id'   => 1,
							'name' => 'Blog Pages Group'
						]
					],
					'statistics' => $statistics[1]
				],
				[
					'id'         => 2,
					'name'       => 'aioseo is the best',
					'favorited'  => true,
					'groups'     => [
						[
							'id'   => 2,
							'name' => 'Low Performance Group'
						]
					],
					'statistics' => $statistics[2]
				],
				[
					'id'         => 3,
					'name'       => 'analyze my seo',
					'favorited'  => false,
					'groups'     => [
						[
							'id'   => 3,
							'name' => 'High Performance Group'
						]
					],
					'statistics' => $statistics[3]
				],
				[
					'id'         => 4,
					'name'       => 'wordpress seo',
					'favorited'  => false,
					'groups'     => [],
					'statistics' => $statistics[4]
				],
				[
					'id'         => 5,
					'name'       => 'best seo plugin pro',
					'favorited'  => false,
					'groups'     => [],
					'statistics' => $statistics[5]
				],
				[
					'id'         => 6,
					'name'       => 'aioseo wordpress',
					'favorited'  => false,
					'groups'     => [],
					'statistics' => $statistics[6]
				],
				[
					'id'         => 7,
					'name'       => 'headline analyzer aioseo',
					'favorited'  => false,
					'groups'     => [],
					'statistics' => $statistics[7]
				],
				[
					'id'         => 8,
					'name'       => 'best seo plugin plugin',
					'favorited'  => false,
					'groups'     => [],
					'statistics' => $statistics[8]
				]
			],
			'totals' => [
				'total' => 8,
				'pages' => 1,
				'page'  => 1
			],
		];
	}

	/**
	 * Retrieves all the keyword groups, formatted.
	 *
	 * @since 4.7.0
	 *
	 * @param  array $args The arguments.
	 * @return array       The formatted keyword groups.
	 */
	public function getFormattedGroups( $args = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$statistics = [];
		for ( $i = 1; $i < 4; $i++ ) {
			$statistics[ $i ] = [
				'clicks'      => wp_rand( 1, 1000 ),
				'impressions' => wp_rand( 10, 10000 ),
				'ctr'         => wp_rand( 1, 99 ),
				'position'    => wp_rand( 1, 100 )
			];
		}

		return [
			'rows'   => [
				[
					'id'          => 1,
					'name'        => 'Blog Pages Group',
					'keywordsQty' => 1,
					'keywords'    => [],
					'statistics'  => $statistics[1]
				],
				[
					'id'          => 2,
					'name'        => 'Low Performance Group',
					'keywordsQty' => 1,
					'keywords'    => [],
					'statistics'  => $statistics[2]
				],
				[
					'id'          => 3,
					'name'        => 'High Performance Group',
					'keywordsQty' => 1,
					'keywords'    => [],
					'statistics'  => $statistics[3]
				]
			],
			'totals' => [
				'total' => 8,
				'pages' => 1,
				'page'  => 1
			],
		];
	}

	/**
	 * Returns the data for Vue.
	 *
	 * @since 4.7.0
	 *
	 * @return array The data for Vue.
	 */
	public function getVueData() {
		$formattedKeywords = $this->getFormattedKeywords();
		$formattedGroups   = $this->getFormattedGroups();

		return [
			// Dummy data to show on the UI.
			'keywords' => [
				'all'        => $formattedKeywords,
				'paginated'  => $formattedKeywords,
				'count'      => count( $formattedKeywords['rows'] ),
				'statistics' => $this->fetchKeywordsStatistics( $formattedKeywords ),
			],
			'groups'   => [
				'all'       => $formattedGroups,
				'paginated' => $formattedGroups,
				'count'     => count( $formattedGroups['rows'] ),
			],
		];
	}

	/**
	 * Returns the data for Vue.
	 *
	 * @since 4.7.0
	 *
	 * @return array The data.
	 */
	public function getVueDataEdit() {
		$formattedKeywords = $this->getFormattedKeywords();

		return [
			// Dummy data to show on the UI.
			'keywords' => [
				'all'       => $formattedKeywords,
				'paginated' => $formattedKeywords,
				'count'     => count( $formattedKeywords['rows'] ),
			],
		];
	}
}Common/SearchStatistics/Notices.php000064400000013237151536241200013376 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchStatistics;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Handles the notices for the Search Statistics.
 *
 * @since 4.6.2
 */
class Notices {
	/**
	 * Class constructor.
	 *
	 * @since 4.6.2
	 */
	public function __construct() {
		if ( ! is_admin() ) {
			return;
		}

		add_action( 'init', [ $this, 'init' ] );
	}

	/**
	 * Initialize the class.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	public function init() {
		$this->siteConnected();
		$this->siteVerified();
		$this->sitemapHasErrors();
	}

	/**
	 * Add a notice if the site is not connected.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	private function siteConnected() {
		$notification = Models\Notification::getNotificationByName( 'search-console-site-not-connected' );
		if ( aioseo()->searchStatistics->api->auth->isConnected() ) {
			if ( $notification->exists() ) {
				Models\Notification::deleteNotificationByName( 'search-console-site-not-connected' );
			}

			return;
		}

		if ( $notification->exists() ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'search-console-site-not-connected',
			'title'             => __( 'Have you connected your site to Google Search Console?', 'all-in-one-seo-pack' ),
			'content'           => sprintf(
				// Translators: 1 - The plugin short name ("AIOSEO").
				__( '%1$s can now verify whether your site is correctly verified with Google Search Console and that your sitemaps have been submitted correctly. Connect with Google Search Console now to ensure your content is being added to Google as soon as possible for increased rankings.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				AIOSEO_PLUGIN_SHORT_NAME
			),
			'type'              => 'warning',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Connect to Google Search Console', 'all-in-one-seo-pack' ),
			'button1_action'    => 'https://route#aioseo-settings&aioseo-scroll=google-search-console-settings&aioseo-highlight=google-search-console-settings:webmaster-tools?activetool=googleSearchConsole', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Add a notice if the site is not verified or was deleted.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	private function siteVerified() {
		$notification = Models\Notification::getNotificationByName( 'search-console-site-not-verified' );
		if (
			! aioseo()->searchStatistics->api->auth->isConnected() ||
			aioseo()->internalOptions->searchStatistics->site->verified ||
			0 === aioseo()->internalOptions->searchStatistics->site->lastFetch // Not fetched yet.
		) {
			if ( $notification->exists() ) {
				Models\Notification::deleteNotificationByName( 'search-console-site-not-verified' );
			}

			return;
		}

		if ( $notification->exists() ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'search-console-site-not-verified',
			'title'             => __( 'Your site was removed from Google Search Console.', 'all-in-one-seo-pack' ),
			'content'           => __( 'We detected that your site has been removed from Google Search Console. If this was done in error, click below to re-sync and resolve this issue.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'type'              => 'warning',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Reconnect Google Search Console', 'all-in-one-seo-pack' ),
			'button1_action'    => 'https://route#aioseo-settings&aioseo-scroll=google-search-console-settings&aioseo-highlight=google-search-console-settings:webmaster-tools?activetool=googleSearchConsole', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Add a notice if the sitemap has errors.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	private function sitemapHasErrors() {
		$notification = Models\Notification::getNotificationByName( 'search-console-sitemap-has-errors' );
		if (
			! aioseo()->searchStatistics->api->auth->isConnected() ||
			! aioseo()->internalOptions->searchStatistics->site->verified ||
			0 === aioseo()->internalOptions->searchStatistics->sitemap->lastFetch || // Not fetched yet.
			! aioseo()->searchStatistics->sitemap->getSitemapsWithErrors()
		) {
			if ( $notification->exists() ) {
				Models\Notification::deleteNotificationByName( 'search-console-sitemap-has-errors' );
			}

			return;
		}

		if ( $notification->exists() ) {
			return;
		}

		$lastFetch = aioseo()->internalOptions->searchStatistics->sitemap->lastFetch;
		$lastFetch = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $lastFetch );

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'search-console-sitemap-has-errors',
			'title'             => __( 'Your sitemap has errors.', 'all-in-one-seo-pack' ),
			'content'           => sprintf(
				// Translators: 1 - Last fetch date.
				__( 'We detected that your sitemap has errors. The last fetch was on %1$s. Click below to resolve this issue.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				$lastFetch
			),
			'type'              => 'warning',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Fix Sitemap Errors', 'all-in-one-seo-pack' ),
			'button1_action'    => 'https://route#aioseo-sitemaps&open-modal=true:general-sitemap', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}
}Common/SearchStatistics/SearchStatistics.php000064400000075513151536241200015257 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchStatistics;

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

/**
 * Class that holds our Search Statistics feature.
 *
 * @since 4.3.0
 */
class SearchStatistics {
	/**
	 * Holds the instance of the API class.
	 *
	 * @since 4.3.0
	 *
	 * @var Api\Api
	 */
	public $api;

	/**
	 * Holds the instance of the Site class.
	 *
	 * @since 4.6.2
	 *
	 * @var Site
	 */
	public $site;

	/**
	 * Holds the instance of the Sitemap class.
	 *
	 * @since 4.6.2
	 *
	 * @var Sitemap
	 */
	public $sitemap;

	/**
	 * Holds the instance of the Notices class.
	 *
	 * @since 4.6.2
	 *
	 * @var Notices
	 */
	public $notices;

	/**
	 * Holds the instance of the Keyword Rank Tracker class.
	 *
	 * @since 4.7.0
	 *
	 * @var KeywordRankTracker
	 */
	public $keywordRankTracker;

	/**
	 * Holds the instance of the Index Status class.
	 *
	 * @since 4.8.2
	 *
	 * @var IndexStatus
	 */
	public $indexStatus;

	/**
	 * Class constructor.
	 *
	 * @since 4.3.0
	 */
	public function __construct() {
		$this->api                = new Api\Api();
		$this->site               = new Site();
		$this->sitemap            = new Sitemap();
		$this->notices            = new Notices();
		$this->keywordRankTracker = new KeywordRankTracker();
		$this->indexStatus        = new IndexStatus();
	}

	/**
	 * Returns the data for Vue.
	 *
	 * @since 4.3.0
	 *
	 * @return array The data for Vue.
	 */
	public function getVueData() {
		$data = [
			'isConnected'         => aioseo()->searchStatistics->api->auth->isConnected(),
			'latestAvailableDate' => null,
			'range'               => [],
			'rolling'             => aioseo()->internalOptions->internal->searchStatistics->rolling,
			'authedSite'          => aioseo()->searchStatistics->api->auth->getAuthedSite(),
			'sitemapsWithErrors'  => aioseo()->searchStatistics->sitemap->getSitemapsWithErrors(),
			'data'                => [
				'seoStatistics'   => $this->getSeoOverviewData(),
				'keywords'        => $this->getKeywordsData(),
				'contentRankings' => $this->getContentRankingsData()
			]
		];

		return $data;
	}

	/**
	 * Resets the Search Statistics.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	public function reset() {
		aioseo()->internalOptions->searchStatistics->sitemap->reset();
		aioseo()->internalOptions->searchStatistics->site->reset();

		// Clear the cache for the Search Statistics.
		aioseo()->searchStatistics->clearCache();
	}

	/**
	 * Returns the data for the SEO Overview.
	 *
	 * @since 4.3.0
	 *
	 * @param  array $dateRange The date range.
	 * @return array            The data for the SEO Overview.
	 */
	protected function getSeoOverviewData( $dateRange = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$pageRows = [
			'/'                       => [
				'ctr'              => '1.25',
				'page'             => '/',
				'clicks'           => 15460,
				'position'         => '74.01',
				'difference'       => [
					'ctr'         => '-0.23',
					'decay'       => 192211,
					'clicks'      => -26,
					'current'     => true,
					'position'    => '19.66',
					'comparison'  => true,
					'impressions' => 192237
				],
				'impressions'      => 1235435,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 65
			],
			'/test-page/'             => [
				'ctr'              => '0.30',
				'page'             => '/test-page/',
				'clicks'           => 5688,
				'position'         => '35.28',
				'difference'       => [
					'ctr'         => '0.05',
					'decay'       => 378973,
					'clicks'      => 1941,
					'current'     => true,
					'position'    => '-2.33',
					'comparison'  => true,
					'impressions' => 377032
				],
				'impressions'      => 1881338,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/test-page/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 95
			],
			'/high-ranking-page/'     => [
				'ctr'              => '6.03',
				'page'             => '/high-ranking-page/',
				'clicks'           => 3452,
				'position'         => '22.85',
				'difference'       => [
					'ctr'         => '-0.95',
					'decay'       => -5986,
					'clicks'      => -898,
					'current'     => true,
					'position'    => '-0.22',
					'comparison'  => true,
					'impressions' => -5088
				],
				'impressions'      => 57248,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/high-ranking-page/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 100
			],
			'/pricing/'               => [
				'ctr'              => '1.35',
				'page'             => '/pricing/',
				'clicks'           => 2749,
				'position'         => '40.40',
				'difference'       => [
					'ctr'         => '-0.16',
					'decay'       => 15991,
					'clicks'      => -94,
					'current'     => true,
					'position'    => '9.77',
					'comparison'  => true,
					'impressions' => 16085
				],
				'impressions'      => 203794,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/pricing/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 100
			],
			'/features-and-benefits/' => [
				'ctr'              => '2.48',
				'page'             => '/features-and-benefits/',
				'clicks'           => 2600,
				'position'         => '15.53',
				'difference'       => [
					'ctr'         => '0.99',
					'decay'       => 23466,
					'clicks'      => 1367,
					'current'     => true,
					'position'    => '1.51',
					'comparison'  => true,
					'impressions' => 22099
				],
				'impressions'      => 104769,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/features-and-benefits/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 90
			],
			'/documentation/'         => [
				'ctr'              => '2.64',
				'page'             => '/documentation/',
				'clicks'           => 1716,
				'position'         => '27.85',
				'difference'       => [
					'ctr'         => '-0.04',
					'decay'       => 7274,
					'clicks'      => 167,
					'current'     => true,
					'position'    => '-9.51',
					'comparison'  => true,
					'impressions' => 7107
				],
				'impressions'      => 64883,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/documentation/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 93
			],
			'/blog/'                  => [
				'ctr'              => '3.75',
				'page'             => '/blog/',
				'clicks'           => 1661,
				'position'         => '36.60',
				'difference'       => [
					'ctr'         => '0.42',
					'decay'       => -3145,
					'clicks'      => 77,
					'current'     => true,
					'position'    => '-9.40',
					'comparison'  => true,
					'impressions' => -3222
				],
				'impressions'      => 44296,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/blog/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 97
			],
			'/blog/my-best-content/'  => [
				'ctr'              => '7.08',
				'page'             => '/blog/my-best-content/',
				'clicks'           => 1573,
				'position'         => '9.61',
				'difference'       => [
					'ctr'         => '0.16',
					'decay'       => -201,
					'clicks'      => 22,
					'current'     => true,
					'position'    => '-2.03',
					'comparison'  => true,
					'impressions' => -223
				],
				'impressions'      => 22203,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/blog/my-best-content/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 56
			],
			'/contact-us/'            => [
				'ctr'              => '1.45',
				'page'             => '/contact-us/',
				'clicks'           => 1550,
				'position'         => '32.05',
				'difference'       => [
					'ctr'         => '0.12',
					'decay'       => 1079,
					'clicks'      => 140,
					'current'     => true,
					'position'    => '-3.47',
					'comparison'  => true,
					'impressions' => 939
				],
				'impressions'      => 106921,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/contact-us/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 78
			],
			'/support/'               => [
				'ctr'              => '5.94',
				'page'             => '/support/',
				'clicks'           => 1549,
				'position'         => '25.83',
				'difference'       => [
					'ctr'         => '-0.74',
					'decay'       => 3885,
					'clicks'      => 62,
					'current'     => true,
					'position'    => '-1.48',
					'comparison'  => true,
					'impressions' => 3823
				],
				'impressions'      => 26099,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/support/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 86
			]
		];

		// Get the 10 most recent posts.
		$recentPosts = aioseo()->db->db->get_results(
			sprintf(
				'SELECT ID, post_title FROM %1$s WHERE post_status = "publish" AND post_type = "post" ORDER BY post_date DESC LIMIT 10',
				aioseo()->db->db->posts
			)
		);

		// Loop through the default page rows and update the key with the permalink from the most recent posts.
		$i = 0;
		foreach ( $pageRows as $key => $pageRow ) {
			// Get the permalink of the recent post that matches the $i index.
			$permalink = isset( $recentPosts[ $i ] ) ? get_permalink( $recentPosts[ $i ]->ID ) : '';

			// If we don't have a permalink, continue to the next row.
			if ( empty( $permalink ) ) {
				continue;
			}

			// Remove the domain from the permalink by parsing the URL and getting the path.
			$permalink = wp_parse_url( $permalink, PHP_URL_PATH );

			// If the permalink already exists, continue to the next row.
			if ( isset( $pageRows[ $permalink ] ) ) {
				// Update the objectId and objectTitle with the recent post ID and title.
				$pageRows[ $permalink ]['objectId']    = $recentPosts[ $i ]->ID;
				$pageRows[ $permalink ]['objectTitle'] = $recentPosts[ $i ]->post_title;

				continue;
			}

			$pageRows[ $permalink ] = $pageRows[ $key ];

			// Remove the old key.
			unset( $pageRows[ $key ] );

			// Update the objectId and objectTitle with the recent post ID and title.
			$pageRows[ $permalink ]['objectId']    = $recentPosts[ $i ]->ID;
			$pageRows[ $permalink ]['objectTitle'] = $recentPosts[ $i ]->post_title;

			$i++;
		}

		return [
			'statistics' => [
				'ctr'         => '0.74',
				'clicks'      => 111521,
				'keywords'    => 19335,
				'position'    => '49.28',
				'difference'  => [
					'ctr'         => '0.03',
					'clicks'      => 1736,
					'keywords'    => 2853,
					'position'    => '1.01',
					'impressions' => -475679
				],
				'impressions' => 14978074
			],
			'intervals'  => [
				[
					'ctr'         => '0.72',
					'date'        => '2022-10-23',
					'clicks'      => 7091,
					'position'    => '48.88',
					'impressions' => 985061
				],
				[
					'ctr'         => '0.77',
					'date'        => '2022-10-30',
					'clicks'      => 8544,
					'position'    => '46.48',
					'impressions' => 1111602
				],
				[
					'ctr'         => '0.73',
					'date'        => '2022-11-06',
					'clicks'      => 9087,
					'position'    => '48.44',
					'impressions' => 1247506
				],
				[
					'ctr'         => '0.75',
					'date'        => '2022-11-13',
					'clicks'      => 9952,
					'position'    => '50.03',
					'impressions' => 1326910
				],
				[
					'ctr'         => '0.73',
					'date'        => '2022-11-20',
					'clicks'      => 9696,
					'position'    => '50.28',
					'impressions' => 1324633
				],
				[
					'ctr'         => '0.69',
					'date'        => '2022-11-27',
					'clicks'      => 9590,
					'position'    => '51.03',
					'impressions' => 1382602
				],
				[
					'ctr'         => '0.71',
					'date'        => '2022-12-04',
					'clicks'      => 9691,
					'position'    => '51.07',
					'impressions' => 1365509
				],
				[
					'ctr'         => '0.71',
					'date'        => '2022-12-11',
					'clicks'      => 9291,
					'position'    => '51.22',
					'impressions' => 1316184
				],
				[
					'ctr'         => '0.80',
					'date'        => '2022-12-18',
					'clicks'      => 8659,
					'position'    => '48.20',
					'impressions' => 1081944
				],
				[
					'ctr'         => '0.75',
					'date'        => '2022-12-25',
					'clicks'      => 6449,
					'position'    => '49.31',
					'impressions' => 857591
				],
				[
					'ctr'         => '0.66',
					'date'        => '2023-01-01',
					'clicks'      => 5822,
					'position'    => '48.16',
					'impressions' => 876828
				],
				[
					'ctr'         => '0.77',
					'date'        => '2023-01-08',
					'clicks'      => 7501,
					'position'    => '47.34',
					'impressions' => 975764
				],
				[
					'ctr'         => '0.90',
					'date'        => '2023-01-16',
					'clicks'      => 10148,
					'position'    => '48.29',
					'impressions' => 1125940
				]
			],
			'pages'      => [
				'topPages'   => [
					'rows' => $pageRows
				],
				'paginated'  => [
					'rows'              => $pageRows,
					'totals'            => [
						'page'  => 1,
						'pages' => 1,
						'total' => 10
					],
					'filters'           => [
						[
							'slug'   => 'all',
							'name'   => 'All',
							'active' => true
						],
						[
							'slug'   => 'topLosing',
							'name'   => 'Top Losing',
							'active' => false
						],
						[
							'slug'   => 'topWinning',
							'name'   => 'Top Winning',
							'active' => false
						]
					],
					'additionalFilters' => [
						[
							'name'    => 'postType',
							'options' => [
								[
									'label' => __( 'All Content Types', 'all-in-one-seo-pack' ),
									'value' => ''
								]
							]
						]
					]
				],
				'topLosing'  => [
					'rows' => $pageRows
				],
				'topWinning' => [
					'rows' => $pageRows
				]
			]
		];
	}

	/**
	 * Returns the data for the Keywords.
	 *
	 * @since 4.3.0
	 *
	 * @param  array $args The arguments.
	 * @return array       The data for the Keywords.
	 */
	public function getKeywordsData( $args = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$keywordsRows = [
			[
				'ctr'         => '4.89',
				'clicks'      => 5000,
				'keyword'     => 'best seo plugin',
				'position'    => '1.93',
				'difference'  => [
					'ctr'         => '-1.06',
					'decay'       => 6590,
					'clicks'      => -652,
					'position'    => '0.07',
					'impressions' => 7242
				],
				'impressions' => 102233
			],
			[
				'ctr'         => '7.06',
				'clicks'      => 4404,
				'keyword'     => 'aioseo is the best',
				'position'    => '1.32',
				'difference'  => [
					'ctr'         => '0.13',
					'decay'       => 8586,
					'clicks'      => 633,
					'position'    => '0.12',
					'impressions' => 7953
				],
				'impressions' => 62357
			],
			[
				'ctr'         => '2.81',
				'clicks'      => 1715,
				'keyword'     => 'analyze my seo',
				'position'    => '6.29',
				'difference'  => [
					'ctr'         => '-0.03',
					'decay'       => 13217,
					'clicks'      => 347,
					'position'    => '-0.34',
					'impressions' => 12870
				],
				'impressions' => 61102
			],
			[
				'ctr'         => '7.46',
				'clicks'      => 717,
				'keyword'     => 'wordpress seo',
				'position'    => '1.18',
				'difference'  => [
					'ctr'         => '-0.69',
					'decay'       => 2729,
					'clicks'      => 144,
					'position'    => '-0.08',
					'impressions' => 2585
				],
				'impressions' => 9614
			],
			[
				'ctr'         => '6.66',
				'clicks'      => 446,
				'keyword'     => 'best seo plugin pro',
				'position'    => '1.65',
				'difference'  => [
					'ctr'         => '0.36',
					'decay'       => -121,
					'clicks'      => 16,
					'position'    => '0.33',
					'impressions' => -137
				],
				'impressions' => 6693
			],
			[
				'ctr'         => '7.39',
				'clicks'      => 409,
				'keyword'     => 'aioseo wordpress',
				'position'    => '1.77',
				'difference'  => [
					'ctr'         => '-0.39',
					'decay'       => 534,
					'clicks'      => 19,
					'position'    => '-0.13',
					'impressions' => 515
				],
				'impressions' => 5531
			],
			[
				'ctr'         => '1.11',
				'clicks'      => 379,
				'keyword'     => 'headline analyzer aioseo',
				'position'    => '8.41',
				'difference'  => [
					'ctr'         => '0.43',
					'decay'       => 134,
					'clicks'      => 147,
					'position'    => '-1.36',
					'impressions' => -13
				],
				'impressions' => 34043
			],
			[
				'ctr'         => '2.63',
				'clicks'      => 364,
				'keyword'     => 'best seo plugin plugin',
				'position'    => '2.38',
				'difference'  => [
					'ctr'         => '0.06',
					'decay'       => 836,
					'clicks'      => 29,
					'position'    => '0.20',
					'impressions' => 807
				],
				'impressions' => 13837
			],
			[
				'ctr'         => '1.52',
				'clicks'      => 326,
				'keyword'     => 'best seo plugin pack',
				'position'    => '4.14',
				'difference'  => [
					'ctr'         => '-0.19',
					'decay'       => -1590,
					'clicks'      => -66,
					'position'    => '0.64',
					'impressions' => -1524
				],
				'impressions' => 21450
			],
			[
				'ctr'         => '6.70',
				'clicks'      => 264,
				'keyword'     => 'youtube title analyzer aioseo',
				'position'    => '7.19',
				'difference'  => [
					'ctr'         => '4.73',
					'decay'       => 3842,
					'clicks'      => 257,
					'position'    => '-4.18',
					'impressions' => 3585
				],
				'impressions' => 3940
			]
		];

		return [
			'paginated'             => [
				'rows'    => $keywordsRows,
				'totals'  => [
					'page'  => 1,
					'pages' => 1,
					'total' => 10
				],
				'filters' => [
					[
						'slug'   => 'all',
						'name'   => 'All',
						'active' => true
					],
					[
						'slug'   => 'topLosing',
						'name'   => 'Top Losing',
						'active' => false
					],
					[
						'slug'   => 'topWinning',
						'name'   => 'Top Winning',
						'active' => false
					]
				]
			],
			'topLosing'             => $keywordsRows,
			'topWinning'            => $keywordsRows,
			'topKeywords'           => $keywordsRows,
			'distribution'          => [
				'top3'       => '6.86',
				'top10'      => '11.03',
				'top50'      => '52.10',
				'top100'     => '30.01',
				'difference' => [
					'top3'   => '24.31',
					'top10'  => '33.70',
					'top50'  => '-30.50',
					'top100' => '-27.51'
				]
			],
			'distributionIntervals' => [
				[
					'date'   => '2022-10-23',
					'top3'   => '30.70',
					'top10'  => '38.60',
					'top50'  => '24.50',
					'top100' => '6.20'
				],
				[
					'date'   => '2022-10-30',
					'top3'   => '31.60',
					'top10'  => '42.10',
					'top50'  => '21.00',
					'top100' => '5.30'
				],
				[
					'date'   => '2022-11-06',
					'top3'   => '31.30',
					'top10'  => '44.40',
					'top50'  => '20.30',
					'top100' => '4.00'
				],
				[
					'date'   => '2022-11-13',
					'top3'   => '31.70',
					'top10'  => '44.20',
					'top50'  => '20.20',
					'top100' => '3.90'
				],
				[
					'date'   => '2022-11-20',
					'top3'   => '31.70',
					'top10'  => '45.70',
					'top50'  => '18.00',
					'top100' => '4.60'
				],
				[
					'date'   => '2022-11-27',
					'top3'   => '32.50',
					'top10'  => '47.80',
					'top50'  => '16.80',
					'top100' => '2.90'
				],
				[
					'date'   => '2022-12-04',
					'top3'   => '32.50',
					'top10'  => '47.20',
					'top50'  => '17.90',
					'top100' => '2.40'
				],
				[
					'date'   => '2022-12-11',
					'top3'   => '31.80',
					'top10'  => '43.70',
					'top50'  => '21.00',
					'top100' => '3.50'
				],
				[
					'date'   => '2022-12-18',
					'top3'   => '30.40',
					'top10'  => '43.60',
					'top50'  => '22.40',
					'top100' => '3.60'
				],
				[
					'date'   => '2022-12-25',
					'top3'   => '26.90',
					'top10'  => '37.20',
					'top50'  => '29.70',
					'top100' => '6.20'
				],
				[
					'date'   => '2023-01-01',
					'top3'   => '27.00',
					'top10'  => '33.80',
					'top50'  => '31.60',
					'top100' => '7.60'
				],
				[
					'date'   => '2023-01-08',
					'top3'   => '26.60',
					'top10'  => '38.60',
					'top50'  => '30.00',
					'top100' => '4.80'
				],
				[
					'date'   => '2023-01-16',
					'top3'   => '31.10',
					'top10'  => '43.90',
					'top50'  => '22.50',
					'top100' => '2.50'
				]
			]
		];
	}

	/**
	 * Returns the content performance data.
	 *
	 * @since 4.7.2
	 *
	 * @return array The content performance data.
	 */
	public function getSeoStatisticsData( $args = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return [];
	}

	/**
	 * Returns the Content Rankings data.
	 *
	 * @since 4.3.6
	 *
	 * @param  array $args The arguments.
	 * @return array       The Content Rankings data.
	 */
	public function getContentRankingsData( $args = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return [
			'paginated' => [
				'rows'              => [
					'/'                       => [
						'points'           => [
							'2022-04' => 13655,
							'2022-05' => 12683,
							'2022-06' => 13923,
							'2022-07' => 13031,
							'2022-08' => 10978,
							'2022-09' => 9726,
							'2022-10' => 13943,
							'2022-11' => 21813,
							'2022-12' => 11163,
							'2023-01' => 4442,
							'2023-02' => 4798,
							'2023-03' => 5405
						],
						'page'             => '/',
						'peak'             => 21813,
						'decayPercent'     => 75,
						'decay'            => 16407,
						'recovering'       => false,
						'context'          => [
							'lastUpdated' => 'December 6, 2021'
						],
						'objectTitle'      => 'Homepage',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
					'/high-ranking-page/'     => [
						'points'           => [
							'2022-04' => 18426,
							'2022-05' => 18435,
							'2022-06' => 19764,
							'2022-07' => 14769,
							'2022-08' => 6486,
							'2022-09' => 11984,
							'2022-10' => 11539,
							'2022-11' => 9939,
							'2022-12' => 5340,
							'2023-01' => 3965,
							'2023-02' => 3799,
							'2023-03' => 5440
						],
						'page'             => '/high-ranking-page/',
						'peak'             => 19764,
						'decayPercent'     => 72,
						'decay'            => 14323,
						'recovering'       => false,
						'context'          => [
							'lastUpdated' => 'November 17, 2022'
						],
						'objectTitle'      => 'High Ranking Page',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
					'/pricing/'               => [
						'points'           => [
							'2022-04' => 5356,
							'2022-05' => 5425,
							'2022-06' => 5165,
							'2022-07' => 5479,
							'2022-08' => 4995,
							'2022-09' => 4466,
							'2022-10' => 4545,
							'2022-11' => 5361,
							'2022-12' => 3092,
							'2023-01' => 1948,
							'2023-02' => 1630,
							'2023-03' => 2341
						],
						'page'             => '/pricing/',
						'peak'             => 5479,
						'decayPercent'     => 57,
						'decay'            => 3137,
						'recovering'       => false,
						'context'          => [
							'lastUpdated' => 'December 8, 2021'
						],
						'objectTitle'      => 'Pricing',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
					'/features-and-benefits/' => [
						'points'           => [
							'2022-04' => 1272,
							'2022-05' => 4151,
							'2022-06' => 6953,
							'2022-07' => 7785,
							'2022-08' => 4177,
							'2022-09' => 3378,
							'2022-10' => 2553,
							'2022-11' => 3971,
							'2022-12' => 2143,
							'2023-01' => 2335,
							'2023-02' => 1666,
							'2023-03' => 4892
						],
						'page'             => '/features-and-benefits/',
						'peak'             => 7785,
						'decayPercent'     => 37,
						'decay'            => 2893,
						'recovering'       => false,
						'context'          => [
							'lastUpdated' => 'February 7, 2022'
						],
						'objectTitle'      => 'Features and Benefits',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
					'/documentation/'         => [
						'points'           => [
							'2022-04' => 594,
							'2022-05' => 385,
							'2022-06' => 337,
							'2022-07' => 378,
							'2022-08' => 714,
							'2022-09' => 2637,
							'2022-10' => 2831,
							'2022-11' => 2907,
							'2022-12' => 1851,
							'2023-01' => 277,
							'2023-02' => 226,
							'2023-03' => 175
						],
						'page'             => '/documentation/',
						'peak'             => 2907,
						'decayPercent'     => 93,
						'decay'            => 2731,
						'recovering'       => false,
						'context'          => [
							'lastUpdated' => 'January 7, 2022'
						],
						'objectTitle'      => 'Documentation',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
					'/blog/'                  => [
						'points'           => [
							'2022-04' => 2956,
							'2022-05' => 2363,
							'2022-06' => 2347,
							'2022-07' => 2154,
							'2022-08' => 2604,
							'2022-09' => 1995,
							'2022-10' => 1528,
							'2022-11' => 1578,
							'2022-12' => 1458,
							'2023-01' => 927,
							'2023-02' => 629,
							'2023-03' => 592
						],
						'page'             => '/blog/',
						'peak'             => 2956,
						'decayPercent'     => 79,
						'decay'            => 2363,
						'recovering'       => false,
						'context'          => [
							'lastUpdated' => 'April 21, 2022'
						],
						'objectTitle'      => 'Blog',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
					'/blog/my-best-content/'  => [
						'points'           => [
							'2022-04' => 1889,
							'2022-05' => 1714,
							'2022-06' => 2849,
							'2022-07' => 4175,
							'2022-08' => 5343,
							'2022-09' => 6360,
							'2022-10' => 6492,
							'2022-11' => 6955,
							'2022-12' => 6930,
							'2023-01' => 5880,
							'2023-02' => 5211,
							'2023-03' => 4683
						],
						'page'             => '/blog/my-best-content/',
						'peak'             => 6955,
						'decayPercent'     => 32,
						'decay'            => 2272,
						'recovering'       => false,
						'context'          => [
							'lastUpdated' => 'April 22, 2022'
						],
						'objectTitle'      => 'My Best Content',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
					'/contact-us/'            => [
						'points'           => [
							'2022-04' => 3668,
							'2022-05' => 3699,
							'2022-06' => 4934,
							'2022-07' => 5488,
							'2022-08' => 5092,
							'2022-09' => 5526,
							'2022-10' => 4694,
							'2022-11' => 4791,
							'2022-12' => 3989,
							'2023-01' => 4089,
							'2023-02' => 4189,
							'2023-03' => 4289
						],
						'page'             => '/contact-us/',
						'peak'             => 5526,
						'decayPercent'     => 34,
						'decay'            => 1907,
						'recovering'       => true,
						'context'          => [
							'lastUpdated' => 'January 28, 2022'
						],
						'objectTitle'      => 'Contact Us',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
					'/support/'               => [
						'points'           => [
							'2022-04' => 2715,
							'2022-05' => 2909,
							'2022-06' => 2981,
							'2022-07' => 2988,
							'2022-08' => 2586,
							'2022-09' => 2592,
							'2022-10' => 2391,
							'2022-11' => 2446,
							'2022-12' => 2045,
							'2023-01' => 1239,
							'2023-02' => 1077,
							'2023-03' => 1198
						],
						'page'             => '/support/',
						'peak'             => 2988,
						'decayPercent'     => 59,
						'decay'            => 1789,
						'recovering'       => false,
						'context'          => [
							'lastUpdated' => 'February 21, 2021'
						],
						'objectTitle'      => 'Support',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
					'/blog/top-10-contents/'  => [
						'points'           => [
							'2022-04' => 1889,
							'2022-05' => 1714,
							'2022-06' => 2849,
							'2022-07' => 4175,
							'2022-08' => 5343,
							'2022-09' => 6360,
							'2022-10' => 6492,
							'2022-11' => 6955,
							'2022-12' => 6930,
							'2023-01' => 5880,
							'2023-02' => 5211,
							'2023-03' => 4683
						],
						'page'             => '/blog/top-10-contents/',
						'peak'             => 6955,
						'decayPercent'     => 32,
						'decay'            => 2272,
						'recovering'       => false,
						'context'          => [
							'lastUpdated' => 'October 14, 2022'
						],
						'objectTitle'      => 'Top 10 Contents',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
				],
				'totals'            => [
					'page'  => 1,
					'pages' => 1,
					'total' => 10
				],
				'additionalFilters' => [
					[
						'name'    => 'postType',
						'options' => [
							[
								'label' => __( 'All Content Types', 'all-in-one-seo-pack' ),
								'value' => ''
							]
						]
					]
				]
			]
		];
	}

	/**
	 * Get minimum required values for the inspection result.
	 *
	 * @since 4.5.0
	 *
	 * @return array The inspection result.
	 */
	private function getInspectionResult() {
		$verdicts = [
			'PASS',
			'FAIL',
			'NEUTRAL'
		];

		return [
			'indexStatusResult' => [
				'verdict' => $verdicts[ array_rand( $verdicts ) ],
			]
		];
	}

	/**
	 * Clears the Search Statistics cache.
	 *
	 * @since   4.5.0
	 * @version 4.6.2 Moved from Pro to Common.
	 *
	 * @return void
	 */
	public function clearCache() {
		aioseo()->core->cache->clearPrefix( 'aioseo_search_statistics_' );
		aioseo()->core->cache->clearPrefix( 'search_statistics_' );
	}

	/**
	 * Returns all scheduled Search Statistics related actions.
	 *
	 * @since 4.6.2
	 *
	 * @return array The Search Statistics actions.
	 */
	protected function getActionSchedulerActions() {
		return [
			$this->site->action,
			$this->sitemap->action
		];
	}

	/**
	 * Cancels all scheduled Search Statistics related actions.
	 *
	 * @since   4.3.3
	 * @version 4.6.2 Moved from Pro to Common.
	 *
	 * @return void
	 */
	public function cancelActions() {
		foreach ( $this->getActionSchedulerActions() as $actionName ) {
			as_unschedule_all_actions( $actionName );
		}
	}
}Common/SearchStatistics/Site.php000064400000007077151536241200012703 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchStatistics;

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

/**
 * Handles the site for the search statistics.
 *
 * @since 4.6.2
 */
class Site {
	/**
	 * The action name.
	 *
	 * @since 4.6.2
	 *
	 * @var string
	 */
	public $action = 'aioseo_search_statistics_site_check';

	/**
	 * Class constructor.
	 *
	 * @since 4.6.2
	 */
	public function __construct() {
		add_action( 'admin_init', [ $this, 'init' ] );
		add_action( $this->action, [ $this, 'worker' ] );
	}

	/**
	 * Initialize the class.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	public function init() {
		if (
			! aioseo()->searchStatistics->api->auth->isConnected() ||
			aioseo()->actionScheduler->isScheduled( $this->action )
		) {
			return;
		}

		aioseo()->actionScheduler->scheduleAsync( $this->action );
	}

	/**
	 * Check whether the site is verified on Google Search Console and verifies it if needed.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	public function worker() {
		if ( ! aioseo()->searchStatistics->api->auth->isConnected() ) {
			return;
		}

		$siteStatus = $this->checkStatus();
		if ( empty( $siteStatus ) ) {
			// If it failed to communicate with the server, try again in a few hours.
			aioseo()->actionScheduler->scheduleSingle( $this->action, wp_rand( HOUR_IN_SECONDS, 2 * HOUR_IN_SECONDS ), [], true );

			return;
		}

		$this->processStatus( $siteStatus );

		// Schedule a new check for the next week.
		aioseo()->actionScheduler->scheduleSingle( $this->action, WEEK_IN_SECONDS + wp_rand( 0, 3 * DAY_IN_SECONDS ), [], true );
	}

	/**
	 * Maybe verifies the site on Google Search Console.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	public function maybeVerify() {
		if ( ! aioseo()->searchStatistics->api->auth->isConnected() ) {
			return;
		}

		$siteStatus = $this->checkStatus();
		if ( empty( $siteStatus ) ) {
			return;
		}

		$this->processStatus( $siteStatus );
	}

	/**
	 * Checks the site status on Google Search Console.
	 *
	 * @since 4.6.2
	 *
	 * @return array The site status.
	 */
	private function checkStatus() {
		$api      = new Api\Request( 'google-search-console/site/check/' );
		$response = $api->request();

		if ( is_wp_error( $response ) ) {
			return [];
		}

		return $response;
	}

	/**
	 * Processes the site status.
	 *
	 * @since 4.6.3
	 *
	 * @param  array $siteStatus The site status.
	 * @return void
	 */
	private function processStatus( $siteStatus ) {
		switch ( $siteStatus['code'] ) {
			case 'site_verified':
				aioseo()->internalOptions->searchStatistics->site->verified  = true;
				aioseo()->internalOptions->searchStatistics->site->lastFetch = time();
				break;
			case 'verification_needed':
				$this->verify( $siteStatus['data'] );
				break;
			case 'site_not_found':
			case 'couldnt_get_token':
			default:
				aioseo()->internalOptions->searchStatistics->site->verified  = false;
				aioseo()->internalOptions->searchStatistics->site->lastFetch = time();
		}
	}

	/**
	 * Verifies the site on Google Search Console.
	 *
	 * @since 4.6.2
	 *
	 * @param  string $token The verification token.
	 * @return void
	 */
	private function verify( $token = '' ) {
		if ( empty( $token ) ) {
			return;
		}

		aioseo()->options->webmasterTools->google = esc_attr( $token );

		$api      = new Api\Request( 'google-search-console/site/verify/' );
		$response = $api->request();

		if ( is_wp_error( $response ) || 'site_verified' !== $response['code'] ) {
			return;
		}

		aioseo()->internalOptions->searchStatistics->site->verified  = true;
		aioseo()->internalOptions->searchStatistics->site->lastFetch = time();
	}
}Common/SearchStatistics/Sitemap.php000064400000007546151536241200013402 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchStatistics;

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

/**
 * Handles the sitemaps for the search statistics.
 *
 * @since 4.6.2
 */
class Sitemap {
	/**
	 * The action name.
	 *
	 * @since 4.6.2
	 *
	 * @var string
	 */
	public $action = 'aioseo_search_statistics_sitemap_sync';

	/**
	 * Class constructor.
	 *
	 * @since 4.6.2
	 */
	public function __construct() {
		add_action( 'admin_init', [ $this, 'init' ] );
		add_action( $this->action, [ $this, 'worker' ] );
	}

	/**
	 * Initialize the class.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	public function init() {
		if (
			! aioseo()->searchStatistics->api->auth->isConnected() ||
			! aioseo()->internalOptions->searchStatistics->site->verified ||
			aioseo()->actionScheduler->isScheduled( $this->action )
		) {
			return;
		}

		aioseo()->actionScheduler->scheduleAsync( $this->action );
	}

	/**
	 * Sync the sitemap.
	 *
	 * @since 4.6.3
	 *
	 * @return void
	 */
	public function worker() {
		if ( ! $this->canSync() ) {
			return;
		}

		$api      = new Api\Request( 'google-search-console/sitemap/sync/', [ 'sitemaps' => aioseo()->sitemap->helpers->getSitemapUrls() ] );
		$response = $api->request();

		if ( is_wp_error( $response ) || empty( $response['data'] ) ) {
			// If it failed to communicate with the server, try again in a few hours.
			aioseo()->actionScheduler->scheduleSingle( $this->action, wp_rand( HOUR_IN_SECONDS, 2 * HOUR_IN_SECONDS ), [], true );

			return;
		}

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

		// Schedule a new sync for the next week.
		aioseo()->actionScheduler->scheduleSingle( $this->action, WEEK_IN_SECONDS + wp_rand( 0, 3 * DAY_IN_SECONDS ), [], true );
	}

	/**
	 * Maybe sync the sitemap after updating the options.
	 * It will check whether the sitemap options have changed and sync the sitemap if needed.
	 *
	 * @since 4.6.2
	 *
	 * @param array $oldSitemapOptions The old sitemap options.
	 * @param array $newSitemapOptions The new sitemap options.
	 *
	 * @return void
	 */
	public function maybeSync( $oldSitemapOptions, $newSitemapOptions ) {
		if (
			! $this->canSync() ||
			empty( $oldSitemapOptions ) ||
			empty( $newSitemapOptions )
		) {
			return;
		}

		// Ignore the HTML sitemap, since it's not actually a sitemap to be synced with Google.
		unset( $newSitemapOptions['html'] );

		$shouldResync = false;
		foreach ( $newSitemapOptions as $type => $options ) {
			if ( empty( $oldSitemapOptions[ $type ] ) ) {
				continue;
			}

			if ( $oldSitemapOptions[ $type ]['enable'] !== $options['enable'] ) {
				$shouldResync = true;
				break;
			}
		}

		if ( ! $shouldResync ) {
			return;
		}

		aioseo()->actionScheduler->unschedule( $this->action );
		aioseo()->actionScheduler->scheduleAsync( $this->action );
	}

	/**
	 * Get the sitemaps with errors.
	 *
	 * @since 4.6.2
	 *
	 * @return array
	 */
	public function getSitemapsWithErrors() {
		$sitemaps = aioseo()->internalOptions->searchStatistics->sitemap->list;
		$ignored  = aioseo()->internalOptions->searchStatistics->sitemap->ignored;
		if ( empty( $sitemaps ) ) {
			return [];
		}

		$errors         = [];
		$pluginSitemaps = aioseo()->sitemap->helpers->getSitemapUrls();
		foreach ( $sitemaps as $sitemap ) {
			if (
				empty( $sitemap['errors'] ) ||
				in_array( $sitemap['path'], $ignored, true ) || // Skip user-ignored sitemaps.
				in_array( $sitemap['path'], $pluginSitemaps, true ) // Skip plugin sitemaps.
			) {
				continue;
			}

			$errors[] = $sitemap;
		}

		return $errors;
	}

	/**
	 * Check if the sitemap can be synced.
	 *
	 * @since 4.6.2
	 *
	 * @return bool
	 */
	private function canSync() {
		return aioseo()->searchStatistics->api->auth->isConnected() && aioseo()->internalOptions->searchStatistics->site->verified;
	}
}Common/SeoRevisions/SeoRevisions.php000064400000002271151536241200013566 0ustar00<?php
namespace AIOSEO\Plugin\Common\SeoRevisions;

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

/**
 * SEO Revisions container class.
 *
 * @since 4.4.0
 */
class SeoRevisions {
	/**
	 * Returns the data for Vue.
	 *
	 * @since 4.4.0
	 *
	 * @return array The data.
	 */
	public function getVueDataCompare() {
		return [
			'currentUser' => $this->getVueDataCurrentUserMeta()
		];
	}

	/**
	 * Returns the data for Vue.
	 *
	 * @since 4.4.0
	 *
	 * @return array The data.
	 */
	public function getVueDataEdit() {
		return $this->getVueDataCompare();
	}

	/**
	 * Retrieve the current user info for usage on Vue UI.
	 *
	 * @since 4.4.0
	 *
	 * @return array Current logged-in user info.
	 */
	protected function getVueDataCurrentUserMeta() {
		$currentUserId = get_current_user_id();
		$avatarData    = get_avatar_data( $currentUserId, [
			'size'    => 32,
			'default' => 'mystery'
		] );

		return [
			'avatar'       => [
				'size' => absint( $avatarData['size'] ),
				'url'  => $avatarData['found_avatar'] ? esc_url( $avatarData['url'] ) : strval( get_avatar_url( 0, $avatarData ) )
			],
			'display_name' => get_the_author_meta( 'display_name', $currentUserId )
		];
	}
}Common/Sitemap/Content.php000064400000074527151536241200011537 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Determines which content should be included in the sitemap.
 *
 * @since 4.0.0
 */
class Content {
	/**
	 * Returns the entries for the requested sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @return array The sitemap entries.
	 */
	public function get() {
		if ( ! in_array( aioseo()->sitemap->type, [ 'general', 'rss' ], true ) || ! $this->isEnabled() ) {
			return [];
		}

		if ( 'rss' === aioseo()->sitemap->type ) {
			return $this->rss();
		}

		if ( 'general' !== aioseo()->sitemap->type ) {
			return [];
		}

		$indexesEnabled = aioseo()->options->sitemap->general->indexes;
		if ( ! $indexesEnabled ) {
			if ( 'root' === aioseo()->sitemap->indexName ) {
				// If indexes are disabled, throw all entries together into one big file.
				return $this->nonIndexed();
			}

			return [];
		}

		if ( 'root' === aioseo()->sitemap->indexName ) {
			return aioseo()->sitemap->root->indexes();
		}

		// Check if requested index has a dedicated method.
		$methodName = aioseo()->helpers->dashesToCamelCase( aioseo()->sitemap->indexName );
		if ( method_exists( $this, $methodName ) ) {
			return $this->$methodName();
		}

		// Check if requested index is a registered post type.
		if ( in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
			return $this->posts( aioseo()->sitemap->indexName );
		}

		// Check if requested index is a registered taxonomy.
		if (
			in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedTaxonomies(), true ) &&
			'product_attributes' !== aioseo()->sitemap->indexName
		) {
			return $this->terms( aioseo()->sitemap->indexName );
		}

		if (
			aioseo()->helpers->isWooCommerceActive() &&
			in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedTaxonomies(), true ) &&
			'product_attributes' === aioseo()->sitemap->indexName
		) {
			return $this->productAttributes();
		}

		return [];
	}

	/**
	 * Returns the total entries number for the requested sitemap.
	 *
	 * @since 4.1.5
	 *
	 * @return int The total entries number.
	 */
	public function getTotal() {
		if ( ! in_array( aioseo()->sitemap->type, [ 'general', 'rss' ], true ) || ! $this->isEnabled() ) {
			return 0;
		}

		if ( 'rss' === aioseo()->sitemap->type ) {
			return count( $this->rss() );
		}

		if ( 'general' !== aioseo()->sitemap->type ) {
			return 0;
		}

		$indexesEnabled = aioseo()->options->sitemap->general->indexes;
		if ( ! $indexesEnabled ) {
			if ( 'root' === aioseo()->sitemap->indexName ) {
				// If indexes are disabled, throw all entries together into one big file.
				return count( $this->nonIndexed() );
			}

			return 0;
		}

		if ( 'root' === aioseo()->sitemap->indexName ) {
			return count( aioseo()->sitemap->root->indexes() );
		}

		// Check if requested index has a dedicated method.
		$methodName = aioseo()->helpers->dashesToCamelCase( aioseo()->sitemap->indexName );
		if ( method_exists( $this, $methodName ) ) {
			$res = $this->$methodName();

			return ! empty( $res ) ? count( $res ) : 0;
		}

		// Check if requested index is a registered post type.
		if ( in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
			return aioseo()->sitemap->query->posts( aioseo()->sitemap->indexName, [ 'count' => true ] );
		}

		// Check if requested index is a registered taxonomy.
		if ( in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedTaxonomies(), true ) ) {
			return aioseo()->sitemap->query->terms( aioseo()->sitemap->indexName, [ 'count' => true ] );
		}

		return 0;
	}

	/**
	 * Checks if the requested sitemap is enabled.
	 *
	 * @since 4.0.0
	 *
	 * @return boolean Whether the sitemap is enabled.
	 */
	public function isEnabled() {
		$options = aioseo()->options->noConflict();
		if ( ! $options->sitemap->{aioseo()->sitemap->type}->enable ) {
			return false;
		}

		if ( $options->sitemap->{aioseo()->sitemap->type}->postTypes->all ) {
			return true;
		}

		$included = aioseo()->sitemap->helpers->includedPostTypes();

		return ! empty( $included );
	}

	/**
	 * Returns all sitemap entries if indexing is disabled.
	 *
	 * @since 4.0.0
	 *
	 * @return array $entries The sitemap entries.
	 */
	private function nonIndexed() {
		$additional       = $this->addl();
		$postTypes        = aioseo()->sitemap->helpers->includedPostTypes();
		$isStaticHomepage = 'page' === get_option( 'show_on_front' );
		$blogPageEntry    = [];
		$homePageEntry    = ! $isStaticHomepage ? [ array_shift( $additional ) ] : [];
		$entries          = array_merge( $additional, $this->author(), $this->date(), $this->postArchive() );

		if ( $postTypes ) {
			foreach ( $postTypes as $postType ) {
				$postTypeEntries = $this->posts( $postType );

				// If we don't have a static homepage, it's business as usual.
				if ( ! $isStaticHomepage ) {
					$entries = array_merge( $entries, $postTypeEntries );
					continue;
				}

				$homePageId = (int) get_option( 'page_on_front' );
				$blogPageId = (int) get_option( 'page_for_posts' );

				if ( 'post' === $postType && $blogPageId ) {
					$blogPageEntry[] = array_shift( $postTypeEntries );
				}

				if ( 'page' === $postType && $homePageId ) {
					$homePageEntry[] = array_shift( $postTypeEntries );
				}

				$entries = array_merge( $entries, $postTypeEntries );
			}
		}

		$taxonomies = aioseo()->sitemap->helpers->includedTaxonomies();
		if ( $taxonomies ) {
			foreach ( $taxonomies as $taxonomy ) {
				$entries = array_merge( $entries, $this->terms( $taxonomy ) );
			}
		}

		// Sort first by priority, then by last modified date.
		usort( $entries, function ( $a, $b ) {
			// If the priorities are equal, sort by last modified date.
			if ( $a['priority'] === $b['priority'] ) {
				return $a['lastmod'] > $b['lastmod'] ? -1 : 1;
			}

			return $a['priority'] > $b['priority'] ? -1 : 1;
		} );

		// Merge the arrays with the home page always first.
		return array_merge( $homePageEntry, $blogPageEntry, $entries );
	}

	/**
	 * Returns all post entries for a given post type.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $postType       The name of the post type.
	 * @param  array  $additionalArgs Any additional arguments for the post query.
	 * @return array                  The sitemap entries.
	 */
	public function posts( $postType, $additionalArgs = [] ) {
		$posts = aioseo()->sitemap->query->posts( $postType, $additionalArgs );
		if ( ! $posts ) {
			return [];
		}

		// Return if we're determining the root indexes.
		if ( ! empty( $additionalArgs['root'] ) && $additionalArgs['root'] ) {
			return $posts;
		}

		$entries          = [];
		$isStaticHomepage = 'page' === get_option( 'show_on_front' );
		$homePageId       = (int) get_option( 'page_on_front' );
		$excludeImages    = aioseo()->sitemap->helpers->excludeImages();
		foreach ( $posts as $post ) {
			$entry = [
				'loc'        => get_permalink( $post->ID ),
				'lastmod'    => aioseo()->helpers->dateTimeToIso8601( $this->getLastModified( $post ) ),
				'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', $post, $postType ),
				'priority'   => aioseo()->sitemap->priority->priority( 'postTypes', $post, $postType ),
			];

			if ( ! $excludeImages ) {
				$entry['images'] = ! empty( $post->images ) ? json_decode( $post->images ) : [];
			}

			// Override priority/frequency for static homepage.
			if ( $isStaticHomepage && ( $homePageId === $post->ID || aioseo()->helpers->wpmlIsHomePage( $post->ID ) ) ) {
				$entry['loc']        = aioseo()->helpers->maybeRemoveTrailingSlash( aioseo()->helpers->wpmlHomeUrl( $post->ID ) ?: $entry['loc'] );
				$entry['changefreq'] = aioseo()->sitemap->priority->frequency( 'homePage' );
				$entry['priority']   = aioseo()->sitemap->priority->priority( 'homePage' );
			}

			$entries[] = apply_filters( 'aioseo_sitemap_post', $entry, $post->ID, $postType, 'post' );
		}

		// We can't remove the post type here because other plugins rely on it.
		return apply_filters( 'aioseo_sitemap_posts', $entries, $postType );
	}

	/**
	 * Returns all post archive entries.
	 *
	 * @since 4.0.0
	 *
	 * @return array $entries The sitemap entries.
	 */
	private function postArchive() {
		$entries = [];
		foreach ( aioseo()->sitemap->helpers->includedPostTypes( true ) as $postType ) {
			if (
				aioseo()->dynamicOptions->noConflict()->searchAppearance->archives->has( $postType ) &&
				! aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->default &&
				aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->noindex
			) {
				continue;
			}

			$post = aioseo()->core->db
				->start( aioseo()->core->db->db->posts . ' as p', true )
				->select( 'p.ID' )
				->where( 'p.post_status', 'publish' )
				->where( 'p.post_type', $postType )
				->limit( 1 )
				->run()
				->result();

			if ( ! $post ) {
				continue;
			}

			$url = get_post_type_archive_link( $postType );
			if ( $url ) {
				$entry = [
					'loc'        => $url,
					'lastmod'    => aioseo()->sitemap->helpers->lastModifiedPostTime( $postType ),
					'changefreq' => aioseo()->sitemap->priority->frequency( 'archive' ),
					'priority'   => aioseo()->sitemap->priority->priority( 'archive' ),
				];

				// To be consistent with our other entry filters, we need to pass the entry ID as well, but as null in this case.
				$entries[] = apply_filters( 'aioseo_sitemap_archive_entry', $entry, null, $postType, 'archive' );
			}
		}

		return apply_filters( 'aioseo_sitemap_post_archives', $entries );
	}

	/**
	 * Returns all term entries for a given taxonomy.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $taxonomy       The name of the taxonomy.
	 * @param  array  $additionalArgs Any additional arguments for the term query.
	 * @return array                  The sitemap entries.
	 */
	public function terms( $taxonomy, $additionalArgs = [] ) {
		$terms = aioseo()->sitemap->query->terms( $taxonomy, $additionalArgs );
		if ( ! $terms ) {
			return [];
		}

		// Get all registered post types for the taxonomy.
		$postTypes = [];
		foreach ( get_post_types() as $postType ) {
			$taxonomies = get_object_taxonomies( $postType );
			foreach ( $taxonomies as $name ) {
				if ( $taxonomy === $name ) {
					$postTypes[] = $postType;
				}
			}
		}

		// Return if we're determining the root indexes.
		if ( ! empty( $additionalArgs['root'] ) && $additionalArgs['root'] ) {
			return $terms;
		}

		$entries = [];
		foreach ( $terms as $term ) {
			$entry = [
				'loc'        => get_term_link( $term->term_id ),
				'lastmod'    => $this->getTermLastModified( $term ),
				'changefreq' => aioseo()->sitemap->priority->frequency( 'taxonomies', $term, $taxonomy ),
				'priority'   => aioseo()->sitemap->priority->priority( 'taxonomies', $term, $taxonomy ),
				'images'     => aioseo()->sitemap->image->term( $term )
			];

			$entries[] = apply_filters( 'aioseo_sitemap_term', $entry, $term->term_id, $term->taxonomy, 'term' );
		}

		return apply_filters( 'aioseo_sitemap_terms', $entries );
	}

	/**
	 * Returns the last modified date for a given term.
	 *
	 * @since 4.0.0
	 *
	 * @param  int|object $term The term data object.
	 * @return string           The lastmod timestamp.
	 */
	public function getTermLastModified( $term ) {
		$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
		$termTaxonomyTable      = aioseo()->core->db->db->prefix . 'term_taxonomy';

		// If the term is an ID, get the term object.
		if ( is_numeric( $term ) ) {
			$term = aioseo()->helpers->getTerm( $term );
		}

		// First, check the count of the term. If it's 0, then we're dealing with a parent term that does not have
		// posts assigned to it. In this case, we need to get the last modified date of all its children.
		if ( empty( $term->count ) ) {
			$lastModified = aioseo()->core->db
				->start( aioseo()->core->db->db->posts . ' as p', true )
				->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
				->where( 'p.post_status', 'publish' )
				->whereRaw( "
				( `p`.`ID` IN
					(
						SELECT CONVERT(`tr`.`object_id`, unsigned)
						FROM `$termRelationshipsTable` as tr
						JOIN `$termTaxonomyTable` as tt ON `tr`.`term_taxonomy_id` = `tt`.`term_taxonomy_id`
						WHERE `tt`.`term_id` IN
							(
								SELECT `tt`.`term_id`
								FROM `$termTaxonomyTable` as tt
								WHERE `tt`.`parent` = '{$term->term_id}'
							)
					)
				)" )
				->run()
				->result();
		} else {
			$lastModified = aioseo()->core->db
				->start( aioseo()->core->db->db->posts . ' as p', true )
				->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
				->where( 'p.post_status', 'publish' )
				->whereRaw( "
				( `p`.`ID` IN
					(
						SELECT CONVERT(`tr`.`object_id`, unsigned)
						FROM `$termRelationshipsTable` as tr
						JOIN `$termTaxonomyTable` as tt ON `tr`.`term_taxonomy_id` = `tt`.`term_taxonomy_id`
						WHERE `tt`.`term_id` = '{$term->term_id}'
					)
				)" )
				->run()
				->result();
		}

		$lastModified = $lastModified[0]->last_modified ?? '';

		return aioseo()->helpers->dateTimeToIso8601( $lastModified );
	}

	/**
	 * Returns all additional pages.
	 *
	 * @since 4.0.0
	 *
	 * @param  bool  $shouldChunk Whether the entries should be chuncked. Is set to false when the static sitemap is generated.
	 * @return array              The sitemap entries.
	 */
	public function addl( $shouldChunk = true ) {
		$additionalPages = [];
		if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
			$additionalPages = array_map( 'json_decode', aioseo()->options->sitemap->general->additionalPages->pages );
			$additionalPages = array_filter( $additionalPages, function( $additionalPage ) {
				return ! empty( $additionalPage->url );
			} );
		}

		$entries = [];
		foreach ( $additionalPages as $additionalPage ) {
			$entries[] = [
				'loc'        => $additionalPage->url,
				'lastmod'    => aioseo()->sitemap->helpers->lastModifiedAdditionalPage( $additionalPage ),
				'changefreq' => $additionalPage->frequency->value,
				'priority'   => $additionalPage->priority->value,
				'isTimezone' => true
			];
		}

		$postTypes             = aioseo()->sitemap->helpers->includedPostTypes();
		$shouldIncludeHomepage = 'posts' === get_option( 'show_on_front' ) || ! in_array( 'page', $postTypes, true );
		if ( $shouldIncludeHomepage ) {
			$frontPageId  = (int) get_option( 'page_on_front' );
			$frontPageUrl = aioseo()->helpers->localizedUrl( '/' );
			$post         = aioseo()->helpers->getPost( $frontPageId );

			$homepageEntry = [
				'loc'        => aioseo()->helpers->maybeRemoveTrailingSlash( $frontPageUrl ),
				'lastmod'    => $post ? aioseo()->helpers->dateTimeToIso8601( $this->getLastModified( $post ) ) : aioseo()->sitemap->helpers->lastModifiedPostTime(),
				'changefreq' => aioseo()->sitemap->priority->frequency( 'homePage' ),
				'priority'   => aioseo()->sitemap->priority->priority( 'homePage' )
			];

			$translatedHomepages = aioseo()->helpers->wpmlHomePages();
			foreach ( $translatedHomepages as $languageCode => $translatedHomepage ) {
				if ( untrailingslashit( $translatedHomepage['url'] ) !== untrailingslashit( $homepageEntry['loc'] ) ) {
					$homepageEntry['languages'][] = [
						'language' => $languageCode,
						'location' => $translatedHomepage['url']
					];
				}
			}

			// Add homepage to the first position.
			array_unshift( $entries, $homepageEntry );
		}

		if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
			$entries = apply_filters( 'aioseo_sitemap_additional_pages', $entries );
		}

		if ( empty( $entries ) ) {
			return [];
		}

		if ( aioseo()->options->sitemap->general->indexes && $shouldChunk ) {
			$entries = aioseo()->sitemap->helpers->chunkEntries( $entries );
			$entries = $entries[ aioseo()->sitemap->pageNumber ] ?? [];
		}

		return $entries;
	}

	/**
	 * Returns all author archive entries.
	 *
	 * @since 4.0.0
	 *
	 * @return array The sitemap entries.
	 */
	public function author() {
		if (
			! aioseo()->sitemap->helpers->lastModifiedPost() ||
			! aioseo()->options->sitemap->general->author ||
			! aioseo()->options->searchAppearance->archives->author->show ||
			(
				! aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->default &&
				aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->noindex
			) ||
			(
				aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->default &&
				(
					! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default &&
					aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindex
				)
			)
		) {
			return [];
		}

		// Allow users to filter the authors in case their sites use a membership plugin or have custom code that affect the authors on their site.
		// e.g. there might be additional roles/conditions that need to be checked here.
		$authors = apply_filters( 'aioseo_sitemap_authors', [] );
		if ( empty( $authors ) ) {
			$usersTableName = aioseo()->core->db->db->users; // We get the table name from WPDB since multisites share the same table.
			$authors        = aioseo()->core->db->start( "$usersTableName as u", true )
				->select( 'u.ID as ID, u.user_nicename as nicename, MAX(p.post_modified_gmt) as lastModified' )
				->join( 'posts as p', 'u.ID = p.post_author' )
				->where( 'p.post_status', 'publish' )
				->whereIn( 'p.post_type', aioseo()->sitemap->helpers->getAuthorPostTypes() )
				->groupBy( 'u.ID' )
				->orderBy( 'lastModified DESC' )
				->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->pageNumber * aioseo()->sitemap->linksPerIndex )
				->run()
				->result();
		}

		if ( empty( $authors ) ) {
			return [];
		}

		$entries = [];
		foreach ( $authors as $authorData ) {
			$entry = [
				'loc'        => ! empty( $authorData->authorUrl )
					? $authorData->authorUrl
					: get_author_posts_url( $authorData->ID, $authorData->nicename ?: '' ),
				'lastmod'    => aioseo()->helpers->dateTimeToIso8601( $authorData->lastModified ),
				'changefreq' => aioseo()->sitemap->priority->frequency( 'author' ),
				'priority'   => aioseo()->sitemap->priority->priority( 'author' )
			];

			$entries[] = apply_filters( 'aioseo_sitemap_author_entry', $entry, $authorData->ID, $authorData->nicename, 'author' );
		}

		return apply_filters( 'aioseo_sitemap_author_archives', $entries );
	}

	/**
	 * Returns all data archive entries.
	 *
	 * @since 4.0.0
	 *
	 * @return array The sitemap entries.
	 */
	public function date() {
		if (
			! aioseo()->sitemap->helpers->lastModifiedPost() ||
			! aioseo()->options->sitemap->general->date ||
			! aioseo()->options->searchAppearance->archives->date->show ||
			(
				! aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->default &&
				aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->noindex
			) ||
			(
				aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->default &&
				(
					! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default &&
					aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindex
				)
			)
		) {
			return [];
		}

		$postsTable = aioseo()->core->db->db->posts;
		$dates      = aioseo()->core->db->execute(
			"SELECT
				YEAR(post_date) AS `year`,
				MONTH(post_date) AS `month`,
				post_date_gmt,
				post_modified_gmt
			FROM {$postsTable}
			WHERE post_type = 'post' AND post_status = 'publish'
			GROUP BY
				YEAR(post_date),
				MONTH(post_date)
			ORDER BY post_date ASC 
			LIMIT 50000",
			true
		)->result();

		if ( empty( $dates ) ) {
			return [];
		}

		$entries = [];
		$year    = '';
		foreach ( $dates as $date ) {
			$entry = [
				'lastmod'    => aioseo()->helpers->dateTimeToIso8601( $this->getLastModified( $date ) ),
				'changefreq' => aioseo()->sitemap->priority->frequency( 'date' ),
				'priority'   => aioseo()->sitemap->priority->priority( 'date' ),
			];

			// Include each year only once.
			if ( $year !== $date->year ) {
				$year         = $date->year;
				$entry['loc'] = get_year_link( $date->year );
				$entries[]    = apply_filters( 'aioseo_sitemap_date_entry', $entry, $date, 'year', 'date' );
			}

			$entry['loc'] = get_month_link( $date->year, $date->month );
			$entries[]    = apply_filters( 'aioseo_sitemap_date_entry', $entry, $date, 'month', 'date' );
		}

		return apply_filters( 'aioseo_sitemap_date_archives', $entries );
	}

	/**
	 * Returns all entries for the RSS Sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @return array The sitemap entries.
	 */
	public function rss() {
		$posts = aioseo()->sitemap->query->posts(
			aioseo()->sitemap->helpers->includedPostTypes(),
			[ 'orderBy' => '`p`.`post_modified_gmt` DESC' ]
		);

		if ( ! count( $posts ) ) {
			return [];
		}

		$entries = [];
		foreach ( $posts as $post ) {
			$entry = [
				'guid'        => get_permalink( $post->ID ),
				'title'       => get_the_title( $post ),
				'description' => get_post_field( 'post_excerpt', $post->ID ),
				'pubDate'     => aioseo()->helpers->dateTimeToRfc822( $this->getLastModified( $post ) )
			];

			// If the entry is the homepage, we need to check if the permalink structure
			// does not have a trailing slash. If so, we need to strip it because WordPress adds it
			// regardless for the home_url() in get_page_link() which is used in the get_permalink() function.
			static $homeId = null;
			if ( null === $homeId ) {
				$homeId = get_option( 'page_for_posts' );
			}

			if ( aioseo()->helpers->getHomePageId() === $post->ID ) {
				$entry['guid'] = aioseo()->helpers->maybeRemoveTrailingSlash( $entry['guid'] );
			}

			$entries[] = apply_filters( 'aioseo_sitemap_post_rss', $entry, $post->ID, $post->post_type, 'post' );
		}

		usort( $entries, function( $a, $b ) {
			return $a['pubDate'] < $b['pubDate'] ? 1 : 0;
		});

		return apply_filters( 'aioseo_sitemap_rss', $entries );
	}

	/**
	 * Returns the last modified date for a given post.
	 *
	 * @since 4.6.3
	 *
	 * @param  object $post The post object.
	 *
	 * @return string The last modified date.
	 */
	public function getLastModified( $post ) {
		$publishDate      = $post->post_date_gmt;
		$lastModifiedDate = $post->post_modified_gmt;

		// Get the date which is the latest.
		return $lastModifiedDate > $publishDate ? $lastModifiedDate : $publishDate;
	}

	/**
	 * Returns all entries for the BuddyPress Activity Sitemap.
	 * This method is automagically called from {@see get()} if the current index name equals to 'bp-activity'
	 *
	 * @since 4.7.6
	 *
	 * @return array The sitemap entries.
	 */
	public function bpActivity() {
		$entries = [];
		if ( ! in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
			return $entries;
		}

		$postType = 'bp-activity';
		$query    = aioseo()->core->db
			->start( 'bp_activity as a' )
			->select( '`a`.`id`, `a`.`date_recorded`' )
			->whereRaw( "a.is_spam = 0 AND a.hide_sitewide = 0 AND a.type NOT IN ('activity_comment', 'last_activity')" )
			->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->offset )
			->orderBy( 'a.date_recorded DESC' );

		$items = $query->run()
						->result();

		foreach ( $items as $item ) {
			$entry = [
				'loc'        => BuddyPressIntegration::getComponentSingleUrl( 'activity', $item->id ),
				'lastmod'    => aioseo()->helpers->dateTimeToIso8601( $item->date_recorded ),
				'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
				'priority'   => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
			];

			$entries[] = apply_filters( 'aioseo_sitemap_post', $entry, $item->id, $postType );
		}

		$archiveUrl = BuddyPressIntegration::getComponentArchiveUrl( 'activity' );
		if (
			aioseo()->helpers->isUrl( $archiveUrl ) &&
			! in_array( $postType, aioseo()->helpers->getNoindexedObjects( 'archives' ), true )
		) {
			$lastMod = ! empty( $items[0] ) ? $items[0]->date_recorded : current_time( 'mysql' );
			$entry   = [
				'loc'        => $archiveUrl,
				'lastmod'    => $lastMod,
				'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
				'priority'   => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
			];

			array_unshift( $entries, $entry );
		}

		return apply_filters( 'aioseo_sitemap_posts', $entries, $postType );
	}

	/**
	 * Returns all entries for the BuddyPress Group Sitemap.
	 * This method is automagically called from {@see get()} if the current index name equals to 'bp-group'
	 *
	 * @since 4.7.6
	 *
	 * @return array The sitemap entries.
	 */
	public function bpGroup() {
		$entries = [];
		if ( ! in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
			return $entries;
		}

		$postType = 'bp-group';
		$query    = aioseo()->core->db
			->start( 'bp_groups as g' )
			->select( '`g`.`id`, `g`.`date_created`, `gm`.`meta_value` as date_modified' )
			->leftJoin( 'bp_groups_groupmeta as gm', 'g.id = gm.group_id' )
			->whereRaw( "g.status = 'public' AND gm.meta_key = 'last_activity'" )
			->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->offset )
			->orderBy( 'gm.meta_value DESC' )
			->orderBy( 'g.date_created DESC' );

		$items = $query->run()
						->result();

		foreach ( $items as $item ) {
			$lastMod = $item->date_modified ?: $item->date_created;
			$entry   = [
				'loc'        => BuddyPressIntegration::getComponentSingleUrl( 'group', BuddyPressIntegration::callFunc( 'bp_get_group_by', 'id', $item->id ) ),
				'lastmod'    => aioseo()->helpers->dateTimeToIso8601( $lastMod ),
				'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
				'priority'   => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
			];

			$entries[] = apply_filters( 'aioseo_sitemap_post', $entry, $item->id, $postType );
		}

		$archiveUrl = BuddyPressIntegration::getComponentArchiveUrl( 'group' );
		if (
			aioseo()->helpers->isUrl( $archiveUrl ) &&
			! in_array( $postType, aioseo()->helpers->getNoindexedObjects( 'archives' ), true )
		) {
			$lastMod = ! empty( $items[0] ) ? $items[0]->date_modified : current_time( 'mysql' );
			$entry   = [
				'loc'        => $archiveUrl,
				'lastmod'    => $lastMod,
				'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
				'priority'   => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
			];

			array_unshift( $entries, $entry );
		}

		return apply_filters( 'aioseo_sitemap_posts', $entries, $postType );
	}

	/**
	 * Returns all entries for the BuddyPress Member Sitemap.
	 * This method is automagically called from {@see get()} if the current index name equals to 'bp-member'
	 *
	 * @since 4.7.6
	 *
	 * @return array The sitemap entries.
	 */
	public function bpMember() {
		$entries = [];
		if ( ! in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
			return $entries;
		}

		$postType = 'bp-member';
		$query    = aioseo()->core->db
			->start( 'bp_activity as a' )
			->select( '`a`.`user_id` as id, `a`.`date_recorded`' )
			->whereRaw( "a.component = 'members' AND a.type = 'last_activity'" )
			->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->offset )
			->orderBy( 'a.date_recorded DESC' );

		$items = $query->run()
			->result();

		foreach ( $items as $item ) {
			$entry = [
				'loc'        => BuddyPressIntegration::getComponentSingleUrl( 'member', $item->id ),
				'lastmod'    => aioseo()->helpers->dateTimeToIso8601( $item->date_recorded ),
				'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
				'priority'   => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
			];

			$entries[] = apply_filters( 'aioseo_sitemap_post', $entry, $item->id, $postType );
		}

		$archiveUrl = BuddyPressIntegration::getComponentArchiveUrl( 'member' );
		if (
			aioseo()->helpers->isUrl( $archiveUrl ) &&
			! in_array( $postType, aioseo()->helpers->getNoindexedObjects( 'archives' ), true )
		) {
			$lastMod = ! empty( $items[0] ) ? $items[0]->date_recorded : current_time( 'mysql' );
			$entry   = [
				'loc'        => $archiveUrl,
				'lastmod'    => $lastMod,
				'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
				'priority'   => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
			];

			array_unshift( $entries, $entry );
		}

		return apply_filters( 'aioseo_sitemap_posts', $entries, $postType );
	}

	/**
	 * Returns all entries for the WooCommerce Product Attributes sitemap.
	 * Note: This sitemap does not support pagination.
	 *
	 * @since 4.7.8
	 *
	 * @param  bool  $count Whether to return the count of the entries. This is used to determine the indexes.
	 * @return array        The sitemap entries.
	 */
	public function productAttributes( $count = false ) {
		$aioseoTermsTable           = aioseo()->core->db->prefix . 'aioseo_terms';
		$wcAttributeTaxonomiesTable = aioseo()->core->db->prefix . 'woocommerce_attribute_taxonomies';
		$termTaxonomyTable          = aioseo()->core->db->prefix . 'term_taxonomy';

		$selectClause = 'COUNT(*) as childProductAttributes';
		if ( ! $count ) {
			$selectClause = aioseo()->pro ? 'tt.term_id, tt.taxonomy, at.frequency, at.priority' : 'tt.term_id, tt.taxonomy';
		}

		$joinClause   = aioseo()->pro ? "LEFT JOIN {$aioseoTermsTable} AS at ON tt.term_id = at.term_id" : '';
		$whereClause  = aioseo()->pro ? 'AND (at.robots_noindex IS NULL OR at.robots_noindex = 0)' : '';
		$limitClause  = $count ? '' : 'LIMIT 50000';

		$result = aioseo()->core->db->execute(
			"SELECT {$selectClause}
			FROM {$termTaxonomyTable} AS tt
			JOIN {$wcAttributeTaxonomiesTable} AS wat ON tt.taxonomy = CONCAT('pa_', wat.attribute_name)
			{$joinClause}
			WHERE wat.attribute_public = 1
				{$whereClause}
				AND tt.count > 0
			{$limitClause};",
			true
		)->result();

		if ( $count ) {
			return ! empty( $result[0]->childProductAttributes ) ? (int) $result[0]->childProductAttributes : 0;
		}

		if ( empty( $result ) ) {
			return [];
		}

		$entries = [];
		foreach ( $result as $term ) {
			$term   = (object) $term;
			$termId = (int) $term->term_id;

			$entry = [
				'loc'        => get_term_link( $termId ),
				'lastmod'    => $this->getTermLastModified( $termId ),
				'changefreq' => aioseo()->sitemap->priority->frequency( 'taxonomies', $term, 'product_attributes' ),
				'priority'   => aioseo()->sitemap->priority->priority( 'taxonomies', $term, 'product_attributes' ),
				'images'     => aioseo()->sitemap->image->term( $term )
			];

			$entries[] = apply_filters( 'aioseo_sitemap_product_attributes', $entry, $termId, $term->taxonomy, 'term' );
		}

		return $entries;
	}
}Common/Sitemap/File.php000064400000020235151536241200010767 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

/**
 * Handles the static sitemap.
 *
 * @since 4.0.0
 */
class File {
	/**
	 * Whether the static files have already been updated during the current request.
	 *
	 * We keep track of this so that setting changes to do not trigger the regeneration multiple times.
	 *
	 * @since 4.0.0
	 *
	 * @var boolean
	 */
	private static $isUpdated = false;

	/**
	 * Generates the static sitemap files.
	 *
	 * @since 4.0.0
	 *
	 * @param  boolean $force Whether or not to force it through.
	 * @return void
	 */
	public function generate( $force = false ) {
		aioseo()->addons->doAddonFunction( 'file', 'generate', [ $force ] );

		// Exit if static sitemap generation isn't enabled.
		if (
			! $force &&
			(
				self::$isUpdated ||
				! aioseo()->options->sitemap->general->enable ||
				! aioseo()->options->sitemap->general->advancedSettings->enable ||
				! in_array( 'staticSitemap', aioseo()->internalOptions->internal->deprecatedOptions, true ) ||
				aioseo()->options->deprecated->sitemap->general->advancedSettings->dynamic
			)
		) {
			return;
		}

		$files           = [];
		self::$isUpdated = true;
		// We need to set these values here as setContext() doesn't run.
		// Subsequently, we need to manually reset the index name below for each query we run.
		// Also, since we need to chunck the entries manually, we cannot limit any queries and need to reset the amount of allowed URLs per index.
		aioseo()->sitemap->offset        = 0;
		aioseo()->sitemap->type          = 'general';
		$sitemapName                     = aioseo()->sitemap->helpers->filename();
		aioseo()->sitemap->indexes       = aioseo()->options->sitemap->general->indexes;
		aioseo()->sitemap->linksPerIndex = PHP_INT_MAX;
		aioseo()->sitemap->isStatic      = true;

		$additionalPages = [];
		if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
			foreach ( aioseo()->options->sitemap->general->additionalPages->pages as $additionalPage ) {
				$additionalPage = json_decode( $additionalPage );
				if ( empty( $additionalPage->url ) ) {
					continue;
				}

				// Decode Additional Page Url to properly show Unicode Characters.
				$additionalPages[] = $additionalPage;
			}
		}

		$postTypes       = aioseo()->sitemap->helpers->includedPostTypes();
		$additionalPages = apply_filters( 'aioseo_sitemap_additional_pages', $additionalPages );

		if (
			'posts' === get_option( 'show_on_front' ) ||
			count( $additionalPages ) ||
			! in_array( 'page', $postTypes, true )
		) {
			$entries            = aioseo()->sitemap->content->addl( false );
			$filename           = "addl-$sitemapName.xml";
			$files[ $filename ] = [
				'total'   => count( $entries ),
				'entries' => $entries
			];
		}

		if (
			aioseo()->sitemap->helpers->lastModifiedPost() &&
			aioseo()->options->sitemap->general->author
		) {
			$entries            = aioseo()->sitemap->content->author();
			$filename           = "author-$sitemapName.xml";
			$files[ $filename ] = [
				'total'   => count( $entries ),
				'entries' => $entries
			];
		}

		if (
			aioseo()->sitemap->helpers->lastModifiedPost() &&
			aioseo()->options->sitemap->general->date
		) {
			$entries            = aioseo()->sitemap->content->date();
			$filename           = "date-$sitemapName.xml";
			$files[ $filename ] = [
				'total'   => count( $entries ),
				'entries' => $entries
			];
		}

		$postTypes = aioseo()->sitemap->helpers->includedPostTypes();
		if ( $postTypes ) {
			foreach ( $postTypes as $postType ) {
				aioseo()->sitemap->indexName = $postType;

				$posts = aioseo()->sitemap->content->posts( $postType );
				if ( ! $posts ) {
					continue;
				}

				$total = aioseo()->sitemap->query->posts( $postType, [ 'count' => true ] );

				// We need to temporarily reset the linksPerIndex count here so that we can properly chunk.
				aioseo()->sitemap->linksPerIndex = aioseo()->options->sitemap->general->linksPerIndex;
				$chunks = aioseo()->sitemap->helpers->chunkEntries( $posts );
				aioseo()->sitemap->linksPerIndex = PHP_INT_MAX;

				if ( 1 === count( $chunks ) ) {
					$filename           = "$postType-$sitemapName.xml";
					$files[ $filename ] = [
						'total'   => $total,
						'entries' => $chunks[0]
					];
				} else {
					for ( $i = 1; $i <= count( $chunks ); $i++ ) {
						$filename           = "$postType-$sitemapName$i.xml";
						$files[ $filename ] = [
							'total'   => $total,
							'entries' => $chunks[ $i - 1 ]
						];
					}
				}
			}
		}

		$taxonomies = aioseo()->sitemap->helpers->includedTaxonomies();
		if ( $taxonomies ) {
			foreach ( $taxonomies as $taxonomy ) {
				aioseo()->sitemap->indexName = $taxonomy;

				$terms = aioseo()->sitemap->content->terms( $taxonomy );
				if ( ! $terms ) {
					continue;
				}

				$total = aioseo()->sitemap->query->terms( $taxonomy, [ 'count' => true ] );

				// We need to temporarily reset the linksPerIndex count here so that we can properly chunk.
				aioseo()->sitemap->linksPerIndex = aioseo()->options->sitemap->general->linksPerIndex;
				$chunks = aioseo()->sitemap->helpers->chunkEntries( $terms );
				aioseo()->sitemap->linksPerIndex = PHP_INT_MAX;

				if ( 1 === count( $chunks ) ) {
					$filename           = "$taxonomy-$sitemapName.xml";
					$files[ $filename ] = [
						'total'   => $total,
						'entries' => $chunks[0]
					];
				} else {
					for ( $i = 1; $i <= count( $chunks ); $i++ ) {
						$filename           = "$taxonomy-$sitemapName$i.xml";
						$files[ $filename ] = [
							'total'   => $total,
							'entries' => $chunks[ $i - 1 ]
						];
					}
				}
			}
		}
		$this->writeSitemaps( $files );
	}

	/**
	 * Writes all sitemap files.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $files The sitemap files.
	 * @return void
	 */
	public function writeSitemaps( $files ) {
		$sitemapName = aioseo()->sitemap->helpers->filename();
		if ( aioseo()->sitemap->indexes ) {
			$indexes = [];
			foreach ( $files as $filename => $data ) {
				if ( empty( $data['entries'] ) ) {
					continue;
				}
				$indexes[] = [
					'loc'     => trailingslashit( home_url() ) . $filename,
					'lastmod' => array_values( $data['entries'] )[0]['lastmod'],
					'count'   => count( $data['entries'] )
				];
			}
			$files[ "$sitemapName.xml" ] = [
				'total'   => 0,
				'entries' => $indexes,
			];
			foreach ( $files as $filename => $data ) {
				$this->writeSitemap( $filename, $data['entries'], $data['total'] );
			}

			return;
		}

		$content = [];
		foreach ( $files as $filename => $data ) {
			foreach ( $data['entries'] as $entry ) {
				$content[] = $entry;
			}
		}
		$this->writeSitemap( "$sitemapName.xml", $content, count( $content ) );
	}

	/**
	 * Writes a given sitemap file to the root dir.
	 *
	 * Helper function for writeSitemaps().
	 *
	 * @since 4.0.0
	 *
	 * @param  string $filename The name of the file.
	 * @param  array  $entries  The sitemap entries for the file.
	 * @return void
	 */
	protected function writeSitemap( $filename, $entries, $total = 0 ) {
		$sitemapName                 = aioseo()->sitemap->helpers->filename();
		aioseo()->sitemap->indexName = $filename;
		if ( "$sitemapName.xml" === $filename && aioseo()->sitemap->indexes ) {
			// Set index name to root so that we use the right output template.
			aioseo()->sitemap->indexName = 'root';
		}

		aioseo()->sitemap->xsl->saveXslData( $filename, $entries, $total );

		ob_start();
		aioseo()->sitemap->output->output( $entries );
		aioseo()->addons->doAddonFunction( 'output', 'output', [ $entries, $total ] );
		$content = ob_get_clean();

		$fs         = aioseo()->core->fs;
		$file       = ABSPATH . sanitize_file_name( $filename );
		$fileExists = $fs->exists( $file );
		if ( ! $fileExists || $fs->isWritable( $file ) ) {
			$fs->putContents( $file, $content );
		}
	}

	/**
	 * Return an array of sitemap files.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of files.
	 */
	public function files() {
		require_once ABSPATH . 'wp-admin/includes/file.php';
		$files = list_files( get_home_path(), 1 );
		if ( ! count( $files ) ) {
			return [];
		}

		$sitemapFiles = [];
		foreach ( $files as $filename ) {
			if ( preg_match( '#.*sitemap.*#', (string) $filename ) ) {
				$sitemapFiles[] = $filename;
			}
		}

		return $sitemapFiles;
	}
}Common/Sitemap/Helpers.php000064400000043274151536241200011522 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

/**
 * Contains general helper methods specific to the sitemap.
 *
 * @since 4.0.0
 */
class Helpers {
	/**
	 * Used to track the performance of the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 *            $memory The peak memory that is required to generate the sitemap.
	 *            $time   The time that is required to generate the sitemap.
	 */
	private $performance;

	/**
	 * Returns the sitemap filename.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $type The sitemap type. We pass it in when we need to get the filename for a specific sitemap outside of the context of the sitemap.
	 * @return string        The sitemap filename.
	 */
	public function filename( $type = '' ) {
		if ( ! $type ) {
			$type = isset( aioseo()->sitemap->type ) ? aioseo()->sitemap->type : 'general';
		}

		return apply_filters( 'aioseo_sitemap_filename', aioseo()->options->sitemap->$type->filename );
	}

	/**
	 * Returns the last modified post.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $additionalArgs Any additional arguments for the post query.
	 * @return mixed                 WP_Post object or false.
	 */
	public function lastModifiedPost( $additionalArgs = [] ) {
		$args = [
			'post_status'    => 'publish',
			'posts_per_page' => 1,
			'orderby '       => 'modified',
			'order'          => 'ASC'
		];

		if ( $additionalArgs ) {
			foreach ( $additionalArgs as $k => $v ) {
				$args[ $k ] = $v;
			}
		}

		$query = ( new \WP_Query( $args ) );
		if ( ! $query->post_count ) {
			return false;
		}

		return $query->posts[0];
	}

	/**
	 * Returns the timestamp of the last modified post.
	 *
	 * @since 4.0.0
	 *
	 * @param  array  $postTypes      The relevant post types.
	 * @param  array  $additionalArgs Any additional arguments for the post query.
	 * @return string                 Formatted date string (ISO 8601).
	 */
	public function lastModifiedPostTime( $postTypes = [ 'post', 'page' ], $additionalArgs = [] ) {
		if ( is_array( $postTypes ) ) {
			$postTypes = implode( "', '", $postTypes );
		}

		$query = aioseo()->core->db
			->start( aioseo()->core->db->db->posts . ' as p', true )
			->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
			->where( 'p.post_status', 'publish' )
			->whereRaw( "( `p`.`post_type` IN ( '$postTypes' ) )" );

		if ( isset( $additionalArgs['author'] ) ) {
			$query->where( 'p.post_author', $additionalArgs['author'] );
		}

		$lastModified = $query->run()
			->result();

		return ! empty( $lastModified[0]->last_modified )
			? aioseo()->helpers->dateTimeToIso8601( $lastModified[0]->last_modified )
			: '';
	}

	/**
	 * Returns the timestamp of the last modified additional page.
	 *
	 * @since 4.0.0
	 *
	 * @return string Formatted date string (ISO 8601).
	 */
	public function lastModifiedAdditionalPagesTime() {
		$pages = [];
		if ( 'posts' === get_option( 'show_on_front' ) || ! in_array( 'page', $this->includedPostTypes(), true ) ) {
			$frontPageId = (int) get_option( 'page_on_front' );
			$post        = aioseo()->helpers->getPost( $frontPageId );
			$pages[]     = $post ? strtotime( $post->post_modified_gmt ) : strtotime( aioseo()->sitemap->helpers->lastModifiedPostTime() );
		}

		foreach ( aioseo()->options->sitemap->general->additionalPages->pages as $page ) {
			$additionalPage = json_decode( $page );
			if ( empty( $additionalPage->url ) ) {
				continue;
			}

			$pages[] = strtotime( $additionalPage->lastModified );
		}

		if ( empty( $pages ) ) {
			$additionalPages = apply_filters( 'aioseo_sitemap_additional_pages', [] );
			if ( empty( $additionalPages ) ) {
				return false;
			}

			$lastModified = 0;
			$timestamp    = time();
			foreach ( $additionalPages as $page ) {
				if ( empty( $page['lastmod'] ) ) {
					continue;
				}
				$timestamp = strtotime( $page['lastmod'] );
				if ( ! $timestamp ) {
					continue;
				}
				if ( $lastModified < $timestamp ) {
					$lastModified = $timestamp;
				}
			}

			return 0 !== $lastModified ? aioseo()->helpers->dateTimeToIso8601( gmdate( 'Y-m-d H:i:s', $timestamp ) ) : false;
		}

		return aioseo()->helpers->dateTimeToIso8601( gmdate( 'Y-m-d H:i:s', max( $pages ) ) );
	}

	/**
	 * Formats a given image URL for usage in the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $url The URL.
	 * @return string      The formatted URL.
	 */
	public function formatUrl( $url ) {
		// Remove URL parameters.
		$url = strtok( $url, '?' );
		$url = htmlspecialchars( $url, ENT_COMPAT, 'UTF-8', false );

		return aioseo()->helpers->makeUrlAbsolute( $url );
	}

	/**
	 * Logs the performance of the sitemap for debugging purposes.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function logPerformance() {
		// Start logging the performance.
		if ( ! $this->performance ) {
			$this->performance['time']   = microtime( true );
			$this->performance['memory'] = ( memory_get_peak_usage( true ) / 1024 ) / 1024;

			return;
		}

		// Stop logging the performance.
		$time      = microtime( true ) - $this->performance['time'];
		$memory    = $this->performance['memory'];
		$type      = aioseo()->sitemap->type;
		$indexName = aioseo()->sitemap->indexName;

		// phpcs:disable WordPress.PHP.DevelopmentFunctions
		error_log( wp_json_encode( "$indexName index of $type sitemap generated in $time seconds using a maximum of $memory mb of memory." ) );
		// phpcs:enable WordPress.PHP.DevelopmentFunctions
	}

	/**
	 * Returns the post types that should be included in the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @param  boolean $hasArchivesOnly Whether or not to only include post types which have archives.
	 * @return array   $postTypes       The included post types.
	 */
	public function includedPostTypes( $hasArchivesOnly = false ) {
		if ( aioseo()->options->sitemap->{aioseo()->sitemap->type}->postTypes->all ) {
			$postTypes = aioseo()->helpers->getPublicPostTypes( true, $hasArchivesOnly );
		} else {
			$postTypes = aioseo()->options->sitemap->{aioseo()->sitemap->type}->postTypes->included;
		}

		if ( ! $postTypes ) {
			return $postTypes;
		}

		$options         = aioseo()->options->noConflict();
		$dynamicOptions  = aioseo()->dynamicOptions->noConflict();
		$publicPostTypes = aioseo()->helpers->getPublicPostTypes( true, $hasArchivesOnly );
		foreach ( $postTypes as $postType ) {
			// Check if post type is no longer registered.
			if ( ! in_array( $postType, $publicPostTypes, true ) || ! $dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
				$postTypes = aioseo()->helpers->unsetValue( $postTypes, $postType );
				continue;
			}

			// Check if post type isn't noindexed.
			if ( aioseo()->helpers->isPostTypeNoindexed( $postType ) ) {
				if ( ! $this->checkForIndexedPost( $postType ) ) {
					$postTypes = aioseo()->helpers->unsetValue( $postTypes, $postType );
					continue;
				}
			}

			if (
				$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default &&
				! $options->searchAppearance->advanced->globalRobotsMeta->default &&
				$options->searchAppearance->advanced->globalRobotsMeta->noindex
			) {
				if ( ! $this->checkForIndexedPost( $postType ) ) {
					$postTypes = aioseo()->helpers->unsetValue( $postTypes, $postType );
				}
			}
		}

		return $postTypes;
	}

	/**
	 * Checks if any post is explicitly indexed when the post type is noindexed.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $postType The post type to check for.
	 * @return bool             Whether or not there is an indexed post.
	 */
	private function checkForIndexedPost( $postType ) {
		$db    = aioseo()->core->db->noConflict();
		$posts = $db->start( aioseo()->core->db->db->posts . ' as p', true )
			->select( 'p.ID' )
			->join( 'aioseo_posts as ap', '`ap`.`post_id` = `p`.`ID`' )
			->where( 'p.post_status', 'attachment' === $postType ? 'inherit' : 'publish' )
			->where( 'p.post_type', $postType )
			->whereRaw( '( `ap`.`robots_default` = 0 AND `ap`.`robots_noindex` = 0 )' )
			->limit( 1 )
			->run()
			->result();

		if ( $posts && count( $posts ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Returns the taxonomies that should be included in the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @return array The included taxonomies.
	 */
	public function includedTaxonomies() {
		$taxonomies = [];
		if ( aioseo()->options->sitemap->{aioseo()->sitemap->type}->taxonomies->all ) {
			$taxonomies = get_taxonomies();
		} else {
			$taxonomies = aioseo()->options->sitemap->{aioseo()->sitemap->type}->taxonomies->included;
		}

		if ( ! $taxonomies ) {
			return [];
		}

		$options          = aioseo()->options->noConflict();
		$dynamicOptions   = aioseo()->dynamicOptions->noConflict();
		$publicTaxonomies = aioseo()->helpers->getPublicTaxonomies( true );
		foreach ( $taxonomies as $taxonomy ) {
			if (
				aioseo()->helpers->isWooCommerceActive() &&
				aioseo()->helpers->isWooCommerceProductAttribute( $taxonomy )
			) {
				$taxonomies = aioseo()->helpers->unsetValue( $taxonomies, $taxonomy );
				if ( ! in_array( 'product_attributes', $taxonomies, true ) ) {
					$taxonomies[] = 'product_attributes';
				}
				continue;
			}

			// Check if taxonomy is no longer registered.
			if ( ! in_array( $taxonomy, $publicTaxonomies, true ) || ! $dynamicOptions->searchAppearance->taxonomies->has( $taxonomy ) ) {
				$taxonomies = aioseo()->helpers->unsetValue( $taxonomies, $taxonomy );
				continue;
			}

			// Check if taxonomy isn't noindexed.
			if ( aioseo()->helpers->isTaxonomyNoindexed( $taxonomy ) ) {
				$taxonomies = aioseo()->helpers->unsetValue( $taxonomies, $taxonomy );
				continue;
			}

			if (
				$dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->default &&
				! $options->searchAppearance->advanced->globalRobotsMeta->default &&
				$options->searchAppearance->advanced->globalRobotsMeta->noindex
			) {
				$taxonomies = aioseo()->helpers->unsetValue( $taxonomies, $taxonomy );
				continue;
			}
		}

		return $taxonomies;
	}

	/**
	 * Splits sitemap entries into chuncks based on the max. amount of URLs per index.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $entries The sitemap entries.
	 * @return array          The chunked sitemap entries.
	 */
	public function chunkEntries( $entries ) {
		return array_chunk( $entries, aioseo()->sitemap->linksPerIndex, true );
	}

	/**
	 * Formats the last Modified date of a user-submitted additional page as an ISO 8601 date.
	 *
	 * @since 4.0.0
	 *
	 * @param  object $page The additional page object.
	 * @return string       The formatted datetime.
	 */
	public function lastModifiedAdditionalPage( $page ) {
		return aioseo()->helpers->isValidDate( $page->lastModified ) ? gmdate( 'c', strtotime( $page->lastModified ) ) : '';
	}

	/**
	 * Returns a list of excluded post IDs.
	 *
	 * @since 4.0.0
	 *
	 * @return string The excluded IDs.
	 */
	public function excludedPosts() {
		static $excludedPosts = null;
		if ( null === $excludedPosts ) {
			$excludedPosts = $this->excludedObjectIds( 'excludePosts' );
		}

		return $excludedPosts;
	}

	/**
	 * Returns a list of excluded term IDs.
	 *
	 * @since 4.0.0
	 *
	 * @return string The excluded IDs.
	 */
	public function excludedTerms() {
		static $excludedTerms = null;
		if ( null === $excludedTerms ) {
			$excludedTerms = $this->excludedObjectIds( 'excludeTerms' );
		}

		return $excludedTerms;
	}

	/**
	 * Returns a list of excluded IDs for a given option as a comma separated string.
	 *
	 * Helper method for excludedPosts() and excludedTerms().
	 *
	 * @since   4.0.0
	 * @version 4.4.7 Improved method name.
	 *
	 * @param  string $option The option name.
	 * @return string         The excluded IDs.
	 */
	private function excludedObjectIds( $option ) {
		$type = aioseo()->sitemap->type;

		if ( 'llms' === $type ) {
			return '';
		}

		// The RSS Sitemap needs to exclude whatever is excluded in the general sitemap.
		if ( 'rss' === $type ) {
			$type = 'general';
		}

		// Allow WPML to filter out hidden language posts/terms.
		$hiddenObjectIds = [];
		if ( aioseo()->helpers->isWpmlActive() ) {
			$hiddenLanguages = apply_filters( 'wpml_setting', [], 'hidden_languages' );
			foreach ( $hiddenLanguages as $language ) {
				$objectTypes = [];
				if ( 'excludePosts' === $option ) {
					$objectTypes = aioseo()->sitemap->helpers->includedPostTypes();
					$objectTypes = array_map( function( $postType ) {
						return "post_{$postType}";
					}, $objectTypes );
				}

				if ( 'excludeTerms' === $option ) {
					$objectTypes = aioseo()->sitemap->helpers->includedTaxonomies();
					$objectTypes = array_map( function( $taxonomy ) {
						return "tax_{$taxonomy}";
					}, $objectTypes );
				}

				$dbNoConflict = aioseo()->core->db->noConflict();
				$rows         = $dbNoConflict->start( 'icl_translations' )
					->select( 'element_id' )
					->whereIn( 'element_type', $objectTypes )
					->where( 'language_code', $language )
					->run()
					->result();

				$ids = array_map( function( $row ) {
					return (int) $row->element_id;
				}, $rows );

				$hiddenObjectIds = array_merge( $hiddenObjectIds, $ids );
			}
		}

		$hasFilter = has_filter( 'aioseo_sitemap_' . aioseo()->helpers->toSnakeCase( $option ) );
		$advanced  = aioseo()->options->sitemap->$type->advancedSettings->enable;
		$excluded  = array_merge( $hiddenObjectIds, aioseo()->options->sitemap->{$type}->advancedSettings->{$option} );

		if (
			! $advanced &&
			empty( $excluded ) &&
			! $hasFilter
		) {
			return '';
		}

		$ids = [];
		foreach ( $excluded as $object ) {
			if ( is_numeric( $object ) ) {
				$ids[] = (int) $object;
				continue;
			}

			$object = json_decode( $object );
			if ( is_int( $object->value ) ) {
				$ids[] = $object->value;
			}
		}

		if ( 'excludePosts' === $option ) {
			$ids = apply_filters( 'aioseo_sitemap_exclude_posts', $ids, $type );
		}

		if ( 'excludeTerms' === $option ) {
			$ids = apply_filters( 'aioseo_sitemap_exclude_terms', $ids, $type );
		}

		return count( $ids ) ? esc_sql( implode( ', ', $ids ) ) : '';
	}

	/**
	 * Returns the URLs of all active sitemaps.
	 *
	 * @since   4.0.0
	 * @version 4.6.2 Removed the prefix from the list of URLs.
	 *
	 * @return array $urls The sitemap URLs.
	 */
	public function getSitemapUrls() {
		static $urls = [];
		if ( $urls ) {
			return $urls;
		}

		$addonsUrls = array_filter( aioseo()->addons->doAddonFunction( 'helpers', 'getSitemapUrls' ) );

		foreach ( $addonsUrls as $addonUrls ) {
			$urls = array_merge( $urls, $addonUrls );
		}

		if ( aioseo()->options->sitemap->general->enable ) {
			$urls[] = $this->getUrl( 'general' );
		}
		if ( aioseo()->options->sitemap->rss->enable ) {
			$urls[] = $this->getUrl( 'rss' );
		}

		return $urls;
	}

	/**
	 * Returns the URLs of all active sitemaps with the 'Sitemap: ' prefix.
	 *
	 * @since 4.6.2
	 *
	 * @return array $urls The sitemap URLs.
	 */
	public function getSitemapUrlsPrefixed() {
		$urls = $this->getSitemapUrls();

		foreach ( $urls as &$url ) {
			$url = 'Sitemap: ' . $url;
		}

		return $urls;
	}

	/**
	 * Extracts existing sitemap URLs from the robots.txt file.
	 * We need this in case users have existing sitemap directives added to their robots.txt file.
	 *
	 * @since   4.0.10
	 * @version 4.4.9
	 *
	 * @return array The sitemap URLs.
	 */
	public function extractSitemapUrlsFromRobotsTxt() {
		// First, we need to remove our filter, so that it doesn't run unintentionally.
		remove_filter( 'robots_txt', [ aioseo()->robotsTxt, 'buildRules' ], 10000 );
		$robotsTxt = apply_filters( 'robots_txt', '', true );
		add_filter( 'robots_txt', [ aioseo()->robotsTxt, 'buildRules' ], 10000 );

		if ( ! $robotsTxt ) {
			return [];
		}

		$lines = explode( "\n", $robotsTxt );
		if ( ! is_array( $lines ) || ! count( $lines ) ) {
			return [];
		}

		return aioseo()->robotsTxt->extractSitemapUrls( $robotsTxt );
	}

	/**
	 * Returns the URL of the given sitemap type.
	 *
	 * @since 4.1.5
	 *
	 * @param  string $type The sitemap type.
	 * @return string       The sitemap URL.
	 */
	public function getUrl( $type ) {
		$url = home_url( 'sitemap.xml' );

		if ( 'rss' === $type ) {
			$url = home_url( 'sitemap.rss' );
		}

		if ( 'general' === $type ) {
			// Check if user has a custom filename from the V3 migration.
			$filename = $this->filename( 'general' ) ?: 'sitemap';
			$url      = home_url( $filename . '.xml' );
		}

		$addon = aioseo()->addons->getLoadedAddon( $type );
		if ( ! empty( $addon->helpers ) && method_exists( $addon->helpers, 'getUrl' ) ) {
			$url = $addon->helpers->getUrl();
		}

		return $url;
	}

	/**
	 * Returns if images should be excluded from the sitemap.
	 *
	 * @since 4.2.2
	 *
	 * @return bool
	 */
	public function excludeImages() {
		$shouldExclude = aioseo()->options->sitemap->general->advancedSettings->enable && aioseo()->options->sitemap->general->advancedSettings->excludeImages;

		return apply_filters( 'aioseo_sitemap_exclude_images', $shouldExclude );
	}

	/**
	 * Returns the post types to check against for the author sitemap.
	 *
	 * @since 4.4.4
	 *
	 * @return array The post types.
	 */
	public function getAuthorPostTypes() {
		// By default, WP only considers posts for author archives, but users can include additional post types.
		$postTypes = [ 'post' ];

		return apply_filters( 'aioseo_sitemap_author_post_types', $postTypes );
	}

	/**
	 * Decode the Urls from Posts and Terms so they properly show in the Sitemap.
	 *
	 * @since 4.6.9
	 *
	 * @param  mixed $data   The data to decode.
	 * @return array $result The converted data with decoded URLs.
	 */
	public function decodeSitemapEntries( $data ) {
		$result = [];

		if ( empty( $data ) ) {
			return $result;
		}

		// Decode Url to properly show Unicode Characters.
		foreach ( $data as $item ) {
			if ( isset( $item['loc'] ) ) {
				$item['loc'] = aioseo()->helpers->decodeUrl( $item['loc'] );
			}
			// This is for the RSS Sitemap.
			if ( isset( $item['guid'] ) ) {
				$item['guid'] = aioseo()->helpers->decodeUrl( $item['guid'] );
			}

			$result[] = $item;
		}

		return $result;
	}
}Common/Sitemap/Html/Block.php000064400000006636151536241200012057 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap\Html;

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

/**
 * Handles the HTML sitemap block.
 *
 * @since 4.1.3
 */
class Block {
	/**
	 * Class constructor.
	 *
	 * @since 4.1.1
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'register' ] );
	}

	/**
	 * Registers the block.
	 *
	 * @since  4.1.3
	 *
	 * @return void
	 */
	public function register() {
		aioseo()->blocks->registerBlock(
			'aioseo/html-sitemap', [
				'attributes'      => [
					'default'          => [
						'type'    => 'boolean',
						'default' => true
					],
					'post_types'       => [
						'type'    => 'string',
						'default' => wp_json_encode( [ 'post', 'page' ] )
					],
					'post_types_all'   => [
						'type'    => 'boolean',
						'default' => true
					],
					'taxonomies'       => [
						'type'    => 'string',
						'default' => wp_json_encode( [ 'category', 'post_tag' ] )
					],
					'taxonomies_all'   => [
						'type'    => 'boolean',
						'default' => true
					],
					'show_label'       => [
						'type'    => 'boolean',
						'default' => true
					],
					'archives'         => [
						'type'    => 'boolean',
						'default' => false
					],
					'publication_date' => [
						'type'    => 'boolean',
						'default' => true
					],
					'nofollow_links'   => [
						'type'    => 'boolean',
						'default' => false
					],
					'order_by'         => [
						'type'    => 'string',
						'default' => 'publish_date'
					],
					'order'            => [
						'type'    => 'string',
						'default' => 'asc'
					],
					'excluded_posts'   => [
						'type'    => 'string',
						'default' => wp_json_encode( [] )
					],
					'excluded_terms'   => [
						'type'    => 'string',
						'default' => wp_json_encode( [] )
					],
					'is_admin'         => [
						'type'    => 'boolean',
						'default' => false
					]
				],
				'render_callback' => [ $this, 'render' ],
				'editor_style'    => 'aioseo-html-sitemap'
			]
		);
	}

	/**
	 * Renders the block.
	 *
	 * @since 4.1.3
	 *
	 * @param  array  $attributes The attributes.
	 * @return string             The HTML sitemap code.
	 */
	public function render( $attributes ) {
		if ( ! $attributes['default'] ) {
			$jsonFields = [ 'post_types', 'taxonomies', 'excluded_posts', 'excluded_terms' ];
			foreach ( $attributes as $k => $v ) {
				if ( in_array( $k, $jsonFields, true ) ) {
					$attributes[ $k ] = json_decode( $v );
				}
			}

			$attributes['excluded_posts'] = $this->extractIds( $attributes['excluded_posts'] );
			$attributes['excluded_terms'] = $this->extractIds( $attributes['excluded_terms'] );

			if ( ! empty( $attributes['post_types_all'] ) ) {
				$attributes['post_types'] = aioseo()->helpers->getPublicPostTypes( true );
			}
			if ( ! empty( $attributes['taxonomies_all'] ) ) {
				$attributes['taxonomies'] = aioseo()->helpers->getPublicTaxonomies( true );
			}
		} else {
			$attributes = [];
		}

		$attributes = aioseo()->htmlSitemap->frontend->getAttributes( $attributes );

		return aioseo()->htmlSitemap->frontend->output( false, $attributes );
	}

	/**
	 * Extracts the IDs from the excluded objects.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $objects The objects.
	 * @return array          The object IDs.
	 */
	private function extractIds( $objects ) {
		return array_map( function ( $object ) {
			$object = json_decode( $object );

			return (int) $object->value;
		}, $objects );
	}
}Common/Sitemap/Html/CompactArchive.php000064400000005677151536241200013721 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap\Html;

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

/**
 * Handles the Compact Archive's output.
 *
 * @since 4.1.3
 */
class CompactArchive {
	/**
	 * The shortcode attributes.
	 *
	 * @since 4.1.3
	 *
	 * @var array
	 */
	private $attributes;

	/**
	 * Outputs the compact archives sitemap.
	 *
	 * @since 4.1.3
	 *
	 * @param  array   $attributes The shortcode attributes.
	 * @param  boolean $echo       Whether the HTML code should be printed or returned.
	 * @return string              The HTML for the compact archive.
	 */
	public function output( $attributes, $echo = true ) {
		$dateArchives     = ( new Query() )->archives();
		$this->attributes = $attributes;

		if ( 'asc' === strtolower( $this->attributes['order'] ) ) {
			$dateArchives = array_reverse( $dateArchives, true );
		}

		$data = [
			'dateArchives' => $dateArchives,
			'lines'        => ''
		];
		foreach ( $dateArchives as $year => $months ) {
			$data['lines'] .= $this->generateYearLine( $year, $months ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}

		ob_start();
		aioseo()->templates->getTemplate( 'sitemap/html/compact-archive.php', $data );
		$output = ob_get_clean();

		if ( $echo ) {
			echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}

		return $output;
	}
	// phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable

	/**
	* Generates the HTML for a year line.
	*
	* @since 4.1.3
	*
	* @param  int    $year   The year archive.
	* @param  array  $months The month archives for the current year.
	* @return string         The HTML code for the year.
	*/
	protected function generateYearLine( $year, $months ) {
		$html = '<li><strong><a href="' . get_year_link( $year ) . '">' . esc_html( $year ) . '</a>: </strong> ';

		for ( $month = 1; $month <= 12; $month++ ) {
			$html .= $this->generateMonth( $year, $months, $month );
		}

		$html .= '</li>' . "\n";

		return wp_kses_post( $html );
	}

	/**
	 * Generates the HTML for a month.
	 *
	 * @since 4.1.3
	 *
	 * @param  int    $year   The year archive.
	 * @param  array  $months All month archives for the current year.
	 * @param  int    $month  The month archive.
	 * @return string         The HTML code for the month.
	 */
	public function generateMonth( $year, $months, $month ) {
		$hasPosts         = isset( $months[ $month ] );
		$dummyDate        = strtotime( "2009/{$month}/25" );
		$monthAbbrevation = date_i18n( 'M', $dummyDate );

		$html = '<span class="aioseo-empty-month">' . esc_html( $monthAbbrevation ) . '</span> ';
		if ( $hasPosts ) {
			$noFollow = filter_var( $this->attributes['nofollow_links'], FILTER_VALIDATE_BOOLEAN );
			$html     = sprintf(
				'<a href="%1$s" title="%2$s"%3$s>%4$s</a> ',
				get_month_link( $year, $month ),
				esc_attr( date_i18n( 'F Y', $dummyDate ) ),
				$noFollow ? ' rel="nofollow"' : '',
				esc_html( $monthAbbrevation )
			);
		}

		return $html;
	}
}Common/Sitemap/Html/Frontend.php000064400000031777151536241200012610 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap\Html;

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

/**
 * Handles the output of the HTML sitemap.
 *
 * @since 4.1.3
 */
class Frontend {
	/**
	 * Instance of Query class.
	 *
	 * @since 4.1.3
	 *
	 * @var Query
	 */
	public $query;

	/**
	 * The attributes for the block/widget/shortcode.
	 *
	 * @since 4.1.3
	 *
	 * @var array
	 */
	private $attributes = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.3
	 */
	public function __construct() {
		$this->query = new Query();
	}

	/**
	 * Returns the attributes.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $attributes The user-defined attributes
	 * @return array             The defaults with user-defined attributes merged.
	 */
	public function getAttributes( $attributes = [] ) {
		aioseo()->sitemap->type = 'html';

		$defaults = [
			'label_tag'        => 'h4',
			'show_label'       => true,
			'order'            => aioseo()->options->sitemap->html->sortDirection,
			'order_by'         => aioseo()->options->sitemap->html->sortOrder,
			'nofollow_links'   => false,
			'publication_date' => aioseo()->options->sitemap->html->publicationDate,
			'archives'         => aioseo()->options->sitemap->html->compactArchives,
			'post_types'       => aioseo()->sitemap->helpers->includedPostTypes(),
			'taxonomies'       => aioseo()->sitemap->helpers->includedTaxonomies(),
			'excluded_posts'   => [],
			'excluded_terms'   => [],
			'is_admin'         => false
		];

		$attributes                   = shortcode_atts( $defaults, $attributes );
		$attributes['show_label']     = filter_var( $attributes['show_label'], FILTER_VALIDATE_BOOLEAN );
		$attributes['nofollow_links'] = filter_var( $attributes['nofollow_links'], FILTER_VALIDATE_BOOLEAN );
		$attributes['is_admin']       = filter_var( $attributes['is_admin'], FILTER_VALIDATE_BOOLEAN );

		return $attributes;
	}

	/**
	 * Formats the publish date according to what's set under Settings > General.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $date The date that should be formatted.
	 * @return string       The formatted date.
	 */
	private function formatDate( $date ) {
		$dateFormat = apply_filters( 'aioseo_html_sitemap_date_format', get_option( 'date_format' ) );

		return date_i18n( $dateFormat, strtotime( $date ) );
	}

	/**
	 * Returns the posts of a given post type that should be included.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $postType       The post type.
	 * @param  array  $additionalArgs Additional arguments for the post query (optional).
	 * @return array                  The post entries.
	 */
	private function posts( $postType, $additionalArgs = [] ) {
		$posts = $this->query->posts( $postType, $additionalArgs );
		if ( ! $posts ) {
			return [];
		}

		$entries = [];
		foreach ( $posts as $post ) {
			$entry = [
				'id'     => $post->ID,
				'title'  => get_the_title( $post ),
				'loc'    => get_permalink( $post->ID ),
				'date'   => $this->formatDate( $post->post_date_gmt ),
				'parent' => ! empty( $post->post_parent ) ? $post->post_parent : null
			];

			$entries[] = $entry;
		}

		return apply_filters( 'aioseo_html_sitemap_posts', $entries, $postType );
	}

	/**
	 * Returns the terms of a given taxonomy that should be included.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $taxonomy        The taxonomy name.
	 * @param  array  $additionalArgs  Additional arguments for the query (optional).
	 * @return array                   The term entries.
	 */
	private function terms( $taxonomy, $additionalArgs = [] ) {
		$terms = $this->query->terms( $taxonomy, $additionalArgs );
		if ( ! $terms ) {
			return [];
		}

		$entries = [];
		foreach ( $terms as $term ) {
			$entries[] = [
				'id'     => $term->term_id,
				'title'  => $term->name,
				'loc'    => get_term_link( $term->term_id ),
				'parent' => ! empty( $term->parent ) ? $term->parent : null
			];
		}

		return apply_filters( 'aioseo_html_sitemap_terms', $entries, $taxonomy );
	}

	/**
	 * Outputs the sitemap to the frontend.
	 *
	 * @since 4.1.3
	 *
	 * @param  bool  $echo       Whether the sitemap should be printed to the screen.
	 * @param  array $attributes The shortcode attributes.
	 * @return string|void       The HTML sitemap.
	 */
	public function output( $echo = true, $attributes = [] ) {
		$this->attributes = $attributes;

		if ( ! aioseo()->options->sitemap->html->enable ) {
			return;
		}

		aioseo()->sitemap->type = 'html';
		if ( filter_var( $attributes['archives'], FILTER_VALIDATE_BOOLEAN ) ) {
			return ( new CompactArchive() )->output( $attributes, $echo );
		}

		if ( ! empty( $attributes['default'] ) ) {
			$attributes = $this->getAttributes();
		}

		$noResultsMessage = esc_html__( 'No posts/terms could be found.', 'all-in-one-seo-pack' );
		if ( empty( $this->attributes['post_types'] ) && empty( $this->attributes['taxonomies'] ) ) {
			if ( $echo ) {
				echo $noResultsMessage; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			}

			return $noResultsMessage;
		}

		// TODO: Consider moving all remaining HTML code below to a dedicated view instead of printing it in PHP.
		$sitemap = sprintf(
			'<div class="aioseo-html-sitemap%s">',
			! $this->attributes['show_label'] ? ' labels-hidden' : ''
		);

		$sitemap .= '<style>.aioseo-html-sitemap.labels-hidden ul { margin: 0; }</style>';

		$hasPosts  = false;
		$postTypes = $this->getIncludedObjects( $this->attributes['post_types'] );
		foreach ( $postTypes as $postType ) {
			if ( 'attachment' === $postType ) {
				continue;
			}

			// Check if post type is still registered.
			if ( ! in_array( $postType, aioseo()->helpers->getPublicPostTypes( true ), true ) ) {
				continue;
			}

			$posts = $this->posts( $postType, $attributes );
			if ( empty( $posts ) ) {
				continue;
			}

			$hasPosts = true;

			$postTypeObject = get_post_type_object( $postType );
			$label          = ! empty( $postTypeObject->label ) ? $postTypeObject->label : ucfirst( $postType );

			$sitemap .= '<div class="aioseo-html-' . esc_attr( $postType ) . '-sitemap">';
			$sitemap .= $this->generateLabel( $label );

			if ( is_post_type_hierarchical( $postType ) ) {
				$sitemap .= $this->generateHierarchicalList( $posts ) . '</div>';
				if ( $this->attributes['show_label'] ) {
					$sitemap .= '<br />';
				}
				continue;
			}

			$sitemap .= $this->generateList( $posts );
			if ( $this->attributes['show_label'] ) {
				$sitemap .= '<br />';
			}
		}

		$hasTerms   = false;
		$taxonomies = $this->getIncludedObjects( $this->attributes['taxonomies'], false );
		foreach ( $taxonomies as $taxonomy ) {
			// Check if post type is still registered.
			if ( ! in_array( $taxonomy, aioseo()->helpers->getPublicTaxonomies( true ), true ) ) {
				continue;
			}

			$terms = $this->terms( $taxonomy, $attributes );
			if ( empty( $terms ) ) {
				continue;
			}

			$hasTerms = true;

			$taxonomyObject = get_taxonomy( $taxonomy );
			$label          = ! empty( $taxonomyObject->label ) ? $taxonomyObject->label : ucfirst( $taxonomy );

			$sitemap .= '<div class="aioseo-html-' . esc_attr( $taxonomy ) . '-sitemap">';
			$sitemap .= $this->generateLabel( $label );

			if ( is_taxonomy_hierarchical( $taxonomy ) ) {
				$sitemap .= $this->generateHierarchicalList( $terms ) . '</div>';
				if ( $this->attributes['show_label'] ) {
					$sitemap .= '<br />';
				}
				continue;
			}

			$sitemap .= $this->generateList( $terms );
			if ( $this->attributes['show_label'] ) {
				$sitemap .= '<br />';
			}
		}

		$sitemap .= '</div>';

		// Check if we actually were able to fetch any results.
		if ( ! $hasPosts && ! $hasTerms ) {
			$sitemap = $noResultsMessage;
		}

		if ( $echo ) {
			echo $sitemap; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}

		return $sitemap;
	}

	/**
	 * Generates the label for a section of the sitemap.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $label The label.
	 * @return string        The HTML code for the label.
	 */
	private function generateLabel( $label ) {
		$labelTag = ! empty( $this->attributes['label_tag'] ) ? $this->attributes['label_tag'] : 'h4';

		return $this->attributes['show_label']
			? wp_kses_post( sprintf( '<%2$s>%1$s</%2$s>', $label, $labelTag ) )
			: '';
	}

	/**
	 * Generates the HTML for a non-hierarchical list of objects.
	 *
	 * @since 4.1.3
	 *
	 * @param  array  $objects The object.
	 * @return string          The HTML code.
	 */
	private function generateList( $objects ) {
		$list = '<ul>';
		foreach ( $objects as $object ) {
			$list .= $this->generateListItem( $object ) . '</li>';
		}

		return $list . '</ul></div>';
	}

	/**
	 * Generates a list item for an object (without the closing tag).
	 * We cannot close it as the caller might need to generate a hierarchical structure inside the list item.
	 *
	 * @since 4.1.3
	 *
	 * @param  array  $object The object.
	 * @return string         The HTML code.
	 */
	private function generateListItem( $object ) {
		$li = '';
		if ( ! empty( $object['title'] ) ) {
			$li .= '<li>';

			// add nofollow to the link.
			if ( filter_var( $this->attributes['nofollow_links'], FILTER_VALIDATE_BOOLEAN ) ) {
				$li .= sprintf(
					'<a href="%1$s" %2$s %3$s>',
					esc_url( $object['loc'] ),
					'rel="nofollow"',
					$this->attributes['is_admin'] ? 'target="_blank"' : ''
				);
			} else {
				$li .= sprintf(
					'<a href="%1$s" %2$s>',
					esc_url( $object['loc'] ),
					$this->attributes['is_admin'] ? 'target="_blank"' : ''
				);
			}

			$li .= sprintf( '%s', esc_attr( $object['title'] ) );

			// add publication date on the list item.
			if ( ! empty( $object['date'] ) && filter_var( $this->attributes['publication_date'], FILTER_VALIDATE_BOOLEAN ) ) {
				$li .= sprintf( ' (%s)', esc_attr( $object['date'] ) );
			}

			$li .= '</a>';
		}

		return $li;
	}

	/**
	 * Generates the HTML for a hierarchical list of objects.
	 *
	 * @since 4.1.3
	 *
	 * @param  array  $objects The objects.
	 * @return string          The HTML of the hierarchical objects section.
	 */
	private function generateHierarchicalList( $objects ) {
		if ( empty( $objects ) ) {
			return '';
		}

		$objects = $this->buildHierarchicalTree( $objects );

		$list = '<ul>';
		foreach ( $objects as $object ) {
			$list .= $this->generateListItem( $object );

			if ( ! empty( $object['children'] ) ) {
				$list .= $this->generateHierarchicalTree( $object );
			}
			$list .= '</li>';
		}
		$list .= '</ul>';

		return $list;
	}

	/**
	 * Recursive helper function for generateHierarchicalList().
	 * Generates hierarchical structure for objects with child objects.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $object The object.
	 * @return string        The HTML code of the hierarchical tree.
	 */
	private function generateHierarchicalTree( $object ) {
		static $nestedLevel = 0;

		$tree = '<ul>';
		foreach ( $object['children'] as $child ) {
			$nestedLevel++;
			$tree .= $this->generateListItem( $child );
			if ( ! empty( $child['children'] ) ) {
				$tree .= $this->generateHierarchicalTree( $child );
			}
			$tree .= '</li>';
		}
		$tree .= '</ul>';

		return $tree;
	}

	/**
	 * Builds the structure for hierarchical objects that have a parent.
	 *
	 * @since 4.1.3
	 * @version 4.2.8
	 *
	 * @param  array $objects The list of hierarchical objects.
	 * @return array          Multidimensional array with the hierarchical structure.
	 */
	private function buildHierarchicalTree( $objects ) {
		$topLevelIds = [];
		$objects     = json_decode( wp_json_encode( $objects ) );

		foreach ( $objects as $listItem ) {

			// Create an array of top level IDs for later reference.
			if ( empty( $listItem->parent ) ) {
				array_push( $topLevelIds, $listItem->id );
			}

			// Create an array of children that belong to the current item.
			$children = array_filter( $objects, function( $child ) use ( $listItem ) {
				if ( ! empty( $child->parent ) ) {
					return absint( $child->parent ) === absint( $listItem->id );
				}
			} );

			if ( ! empty( $children ) ) {
				$listItem->children = $children;
			}
		}

		// Remove child objects from the root level since they've all been nested.
		$objects = array_filter( $objects, function ( $item ) use ( $topLevelIds ) {
			return in_array( $item->id, $topLevelIds, true );
		} );

		return array_values( json_decode( wp_json_encode( $objects ), true ) );
	}

	/**
	 * Returns the names of the included post types or taxonomies.
	 *
	 * @since 4.1.3
	 *
	 * @param  array|string $objects      The included post types/taxonomies.
	 * @param  boolean      $arePostTypes Whether the objects are post types.
	 * @return array                      The names of the included post types/taxonomies.
	 */
	private function getIncludedObjects( $objects, $arePostTypes = true ) {
		if ( is_array( $objects ) ) {
			return $objects;
		}

		if ( empty( $objects ) ) {
			return [];
		}

		$exploded = explode( ',', $objects );
		$objects  = array_map( function( $object ) {
			return trim( $object );
		}, $exploded );

		$publicObjects = $arePostTypes
			? aioseo()->helpers->getPublicPostTypes( true )
			: aioseo()->helpers->getPublicTaxonomies( true );

		$objects = array_filter( $objects, function( $object ) use ( $publicObjects ) {
			return in_array( $object, $publicObjects, true );
		});

		return $objects;
	}
}Common/Sitemap/Html/Query.php000064400000014575151536241200012133 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap\Html;

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

/**
 * Handles all queries for the HTML sitemap.
 *
 * @since 4.1.3
 */
class Query {
	/**
	 * Returns all eligible sitemap entries for a given post type.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $postType   The post type.
	 * @param  array  $attributes The attributes.
	 * @return array              The post objects.
	 */
	public function posts( $postType, $attributes ) {
		$fields  = '`ID`, `post_title`,';
		$fields .= '`post_parent`, `post_date_gmt`, `post_modified_gmt`';

		$orderBy = '';
		switch ( $attributes['order_by'] ) {
			case 'last_updated':
				$orderBy = 'post_modified_gmt';
				break;
			case 'alphabetical':
				$orderBy = 'post_title';
				break;
			case 'id':
				$orderBy = 'ID';
				break;
			case 'publish_date':
			default:
				$orderBy = 'post_date_gmt';
				break;
		}

		switch ( strtolower( $attributes['order'] ) ) {
			case 'desc':
				$orderBy .= ' DESC';
				break;
			default:
				$orderBy .= ' ASC';
		}

		$query = aioseo()->core->db
			->start( 'posts' )
			->select( $fields )
			->where( 'post_status', 'publish' )
			->where( 'post_type', $postType );

		$excludedPosts = $this->getExcludedObjects( $attributes );
		if ( $excludedPosts ) {
			$query->whereRaw( "( `ID` NOT IN ( $excludedPosts ) )" );
		}

		$posts = $query->orderBy( $orderBy )
			->run()
			->result();

		foreach ( $posts as $post ) {
			$post->ID = (int) $post->ID;
		}

		return $posts;
	}

	/**
	 * Returns all eligble sitemap entries for a given taxonomy.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $taxonomy   The taxonomy name.
	 * @param  array  $attributes The attributes.
	 * @return array              The term objects.
	 */
	public function terms( $taxonomy, $attributes = [] ) {
		$fields                 = 't.term_id, t.name, tt.parent';
		$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
		$termTaxonomyTable      = aioseo()->core->db->db->prefix . 'term_taxonomy';

		$orderBy = '';
		switch ( $attributes['order_by'] ) {
			case 'alphabetical':
				$orderBy = 't.name';
				break;
			// We can only sort by date after getting the terms.
			case 'id':
			case 'publish_date':
			case 'last_updated':
			default:
				$orderBy = 't.term_id';
				break;
		}

		switch ( strtolower( $attributes['order'] ) ) {
			case 'desc':
				$orderBy .= ' DESC';
				break;
			default:
				$orderBy .= ' ASC';
		}

		$query = aioseo()->core->db
			->start( 'terms as t' )
			->select( $fields )
			->join( 'term_taxonomy as tt', 't.term_id = tt.term_id' )
			->whereRaw( "
			( `t`.`term_id` IN
				(
					SELECT `tt`.`term_id`
					FROM `$termTaxonomyTable` as tt
					WHERE `tt`.`taxonomy` = '$taxonomy'
					AND `tt`.`count` > 0
				)
			)" );

		$excludedTerms = $this->getExcludedObjects( $attributes, false );
		if ( $excludedTerms ) {
			$query->whereRaw("
				( `t`.`term_id` NOT IN
					(
						SELECT `tr`.`term_taxonomy_id`
						FROM `$termRelationshipsTable` as tr
						WHERE `tr`.`term_taxonomy_id` IN ( $excludedTerms )
					)
				)" );
		}

		$terms = $query->orderBy( $orderBy )
			->run()
			->result();

		foreach ( $terms as $term ) {
			$term->term_id  = (int) $term->term_id;
			$term->taxonomy = $taxonomy;
		}

		$shouldSort = false;
		if ( 'last_updated' === $attributes['order_by'] ) {
			$shouldSort = true;
			foreach ( $terms as $term ) {
				$term->timestamp = strtotime( aioseo()->sitemap->content->getTermLastModified( $term->term_id ) );
			}
		}

		if ( 'publish_date' === $attributes['order_by'] ) {
			$shouldSort = true;
			foreach ( $terms as $term ) {
				$term->timestamp = strtotime( $this->getTermPublishDate( $term->term_id ) );
			}
		}

		if ( $shouldSort ) {
			if ( 'asc' === strtolower( $attributes['order'] ) ) {
				usort( $terms, function( $term1, $term2 ) {
					return $term1->timestamp > $term2->timestamp ? 1 : 0;
				} );
			} else {
				usort( $terms, function( $term1, $term2 ) {
					return $term1->timestamp < $term2->timestamp ? 1 : 0;
				} );
			}
		}

		return $terms;
	}

	/**
	 * Returns a list of date archives that can be included.
	 *
	 * @since 4.1.3
	 *
	 * @return array The date archives.
	 */
	public function archives() {
		$result = aioseo()->core->db
			->start( 'posts', false, 'SELECT DISTINCT' )
			->select( 'YEAR(post_date) AS year, MONTH(post_date) AS month' )
			->where( 'post_type', 'post' )
			->where( 'post_status', 'publish' )
			->whereRaw( "post_password=''" )
			->orderBy( 'year DESC' )
			->orderBy( 'month DESC' )
			->run()
			->result();

		$dates = [];
		foreach ( $result as $date ) {
			$dates[ $date->year ][ $date->month ] = 1;
		}

		return $dates;
	}

	/**
	 * Returns the publish date for a given term.
	 * This is the publish date of the oldest post that is assigned to the term.
	 *
	 * @since 4.1.3
	 *
	 * @param  int $termId The term ID.
	 * @return int         The publish date timestamp.
	 */
	public function getTermPublishDate( $termId ) {
		$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';

		$post = aioseo()->core->db
			->start( 'posts as p' )
			->select( 'MIN(`p`.`post_date_gmt`) as publish_date' )
			->whereRaw( "
			( `p`.`ID` IN
				(
					SELECT `tr`.`object_id`
					FROM `$termRelationshipsTable` as tr
					WHERE `tr`.`term_taxonomy_id` = '$termId'
				)
			)" )
			->run()
			->result();

		return ! empty( $post[0]->publish_date ) ? strtotime( $post[0]->publish_date ) : 0;
	}

	/**
	 * Returns a comma-separated string of excluded object IDs.
	 *
	 * @since 4.1.3
	 *
	 * @param  array   $attributes The attributes.
	 * @param  boolean $posts      Whether the objects are posts.
	 * @return string              The excluded object IDs.
	 */
	private function getExcludedObjects( $attributes, $posts = true ) {
		$excludedObjects = $posts
			? aioseo()->sitemap->helpers->excludedPosts()
			: aioseo()->sitemap->helpers->excludedTerms();
		$key             = $posts ? 'excluded_posts' : 'excluded_terms';

		if ( ! empty( $attributes[ $key ] ) ) {
			$ids = explode( ',', $excludedObjects );

			$extraIds = [];
			if ( is_array( $attributes[ $key ] ) ) {
				$extraIds = $attributes[ $key ];
			}
			if ( is_string( $attributes[ $key ] ) ) {
				$extraIds = array_map( 'trim', explode( ',', $attributes[ $key ] ) );
			}

			$ids = array_filter( array_merge( $ids, $extraIds ), 'is_numeric' );

			$excludedObjects = esc_sql( implode( ', ', $ids ) );
		}

		return $excludedObjects;
	}
}Common/Sitemap/Html/Shortcode.php000064400000001336151536241200012747 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap\Html;

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

/**
 * Handles the HTML sitemap shortcode.
 *
 * @since 4.1.3
 */
class Shortcode {
	/**
	 * Class constructor.
	 *
	 * @since 4.1.3
	 */
	public function __construct() {
		add_shortcode( 'aioseo_html_sitemap', [ $this, 'render' ] );
	}

	/**
	 * Shortcode callback.
	 *
	 * @since 4.1.3
	 *
	 * @param  array       $attributes The shortcode attributes.
	 * @return string|void             The HTML sitemap.
	 */
	public function render( $attributes ) {
		$attributes = aioseo()->htmlSitemap->frontend->getAttributes( $attributes );

		return aioseo()->htmlSitemap->frontend->output( false, $attributes );
	}
}Common/Sitemap/Html/Sitemap.php000064400000014025151536241200012416 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap\Html {
	// Exit if accessed directly.
	if ( ! defined( 'ABSPATH' ) ) {
		exit;
	}

	/**
	* Main class for the HTML sitemap.
	*
	* @since 4.1.3
	*/
	class Sitemap {
		/** Instance of the frontend class.
		 *
		 * @since 4.1.3
		 *
		 * @var Frontend
		 */
		public $frontend;

		/**
		 * Instance of the shortcode class.
		 *
		 * @since 4.1.3
		 *
		 * @var Shortcode
		 */
		public $shortcode;

		/**
		 * Instance of the block class.
		 *
		 * @since 4.1.3
		 *
		 * @var Block
		 */
		public $block;

		/**
		 * Whether the current queried page is the dedicated sitemap page.
		 *
		 * @since 4.1.3
		 *
		 * @var bool
		 */
		public $isDedicatedPage = false;

		/**
		 * Class constructor.
		 *
		 * @since 4.1.3
		 */
		public function __construct() {
			$this->frontend  = new Frontend();
			$this->shortcode = new Shortcode();
			$this->block     = new Block();

			add_action( 'widgets_init', [ $this, 'registerWidget' ] );
			add_filter( 'aioseo_canonical_url', [ $this, 'getCanonicalUrl' ] );

			if ( ! is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
				add_action( 'template_redirect', [ $this, 'checkForDedicatedPage' ] );
			}
		}

		/**
		 * Register our HTML sitemap widget.
		 *
		 * @since 4.1.3
		 *
		 * @return void
		 */
		public function registerWidget() {
			if ( aioseo()->helpers->canRegisterLegacyWidget( 'aioseo-html-sitemap-widget' ) ) {
				register_widget( 'AIOSEO\Plugin\Common\Sitemap\Html\Widget' );
			}
		}

		/**
		 * Checks whether the current request is for our dedicated HTML sitemap page.
		 *
		 * @since 4.1.3
		 *
		 * @return void
		 */
		public function checkForDedicatedPage() {
			if ( ! aioseo()->options->sitemap->html->enable ) {
				return;
			}

			global $wp;
			$sitemapUrl = aioseo()->options->sitemap->html->pageUrl;
			if ( ! $sitemapUrl || empty( $wp->request ) ) {
				return;
			}

			$sitemapUrl = wp_parse_url( $sitemapUrl );
			if ( empty( $sitemapUrl['path'] ) ) {
				return;
			}

			$sitemapUrl = trim( $sitemapUrl['path'], '/' );
			if ( trim( $wp->request, '/' ) === $sitemapUrl ) {
				$this->isDedicatedPage = true;
				$this->generatePage();
			}
		}

		/**
		 * Checks whether the current request is for our dedicated HTML sitemap page.
		 *
		 * @since 4.1.3
		 *
		 * @return void
		 */
		private function generatePage() {
			global $wp_query, $wp, $post; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

			$postId     = -1337; // Set a negative ID to prevent conflicts with existing posts.
			$sitemapUrl = aioseo()->options->sitemap->html->pageUrl;
			$path       = trim( wp_parse_url( $sitemapUrl )['path'], '/' );

			$fakePost                 = new \stdClass();
			$fakePost->ID             = $postId;
			$fakePost->post_author    = 1;
			$fakePost->post_date      = current_time( 'mysql' );
			$fakePost->post_date_gmt  = current_time( 'mysql', 1 );
			$fakePost->post_title     = apply_filters( 'aioseo_html_sitemap_page_title', __( 'Sitemap', 'all-in-one-seo-pack' ) );
			$fakePost->post_content   = '[aioseo_html_sitemap archives=false]';
			// We're using post instead of page to prevent calls to get_ancestors(), which will trigger errors.
			// To loead the page template, we set is_page to true on the WP_Query object.
			$fakePost->post_type      = 'post';
			$fakePost->post_status    = 'publish';
			$fakePost->comment_status = 'closed';
			$fakePost->ping_status    = 'closed';
			$fakePost->post_name      = $path;
			$fakePost->filter         = 'raw'; // Needed to prevent calls to the database when creating the WP_Post object.
			$postObject               = new \WP_Post( $fakePost );

			$post = $postObject;

			// We'll set as much properties on the WP_Query object as we can to prevent conflicts with other plugins/themes.
			// phpcs:disable Squiz.NamingConventions.ValidVariableName
			$wp_query->is_404            = false;
			$wp_query->is_page           = true;
			$wp_query->is_singular       = true;
			$wp_query->post              = $postObject;
			$wp_query->posts             = [ $postObject ];
			$wp_query->queried_object    = $postObject;
			$wp_query->queried_object_id = $postId;
			$wp_query->found_posts       = 1;
			$wp_query->post_count        = 1;
			$wp_query->max_num_pages     = 1;

			unset( $wp_query->query['error'] );
			$wp_query->query_vars['error'] = '';
			// phpcs:enable Squiz.NamingConventions.ValidVariableName

			// We need to add the post object to the cache so that get_post() calls don't trigger database calls.
			wp_cache_add( $postId, $postObject, 'posts' );

			$GLOBALS['wp_query'] = $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			$wp->register_globals();

			// Setting is_404 is not sufficient, so we still need to change the status code.
			status_header( 200 );
		}

		/**
		 * Get the canonical URL for the dedicated HTML sitemap page.
		 *
		 * @since 4.5.7
		 *
		 * @param  string $originalUrl The canonical URL.
		 * @return string              The canonical URL.
		 */
		public function getCanonicalUrl( $originalUrl ) {
			$sitemapOptions = aioseo()->options->sitemap->html;

			if ( ! $sitemapOptions->enable || ! $this->isDedicatedPage ) {
				return $originalUrl;
			}

			// If the user has set a custom URL for the sitemap page, use that.
			if ( $sitemapOptions->pageUrl ) {
				return $sitemapOptions->pageUrl;
			}

			// Return the current URL of WP.
			global $wp;

			return home_url( $wp->request );
		}
	}
}

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

	if ( ! function_exists( 'aioseo_html_sitemap' ) ) {
		/**
		 * Global function that can be used to print the HTML sitemap.
		 *
		 * @since 4.1.3
		 *
		 * @param  array   $attributes User-defined attributes that override the default settings.
		 * @param  boolean $echo       Whether to echo the output or return it.
		 * @return string              The HTML sitemap code.
		 */
		function aioseo_html_sitemap( $attributes = [], $echo = true ) {
			$attributes = aioseo()->htmlSitemap->frontend->getAttributes( $attributes );

			return aioseo()->htmlSitemap->frontend->output( $echo, $attributes );
		}
	}
}Common/Sitemap/Html/Widget.php000064400000013607151536241200012244 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap\Html;

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

/**
 * Class Widget.
 *
 * @since 4.1.3
 */
class Widget extends \WP_Widget {
	/**
	 * The default attributes.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $defaults = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.3
	 */
	public function __construct() {
		// The default widget settings.
		$this->defaults = [
			'title'            => '',
			'show_label'       => 'on',
			'archives'         => '',
			'nofollow_links'   => '',
			'order'            => 'asc',
			'order_by'         => 'publish_date',
			'publication_date' => 'on',
			'post_types'       => [ 'post', 'page' ],
			'taxonomies'       => [ 'category', 'post_tag' ],
			'excluded_posts'   => '',
			'excluded_terms'   => ''
		];

		$widgetSlug     = 'aioseo-html-sitemap-widget';
		$widgetOptions  = [
			'classname'   => $widgetSlug,
			// Translators: The short plugin name ("AIOSEO").
			'description' => sprintf( esc_html__( '%1$s HTML sitemap widget.', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME )
		];
		$controlOptions = [
			'id_base' => $widgetSlug
		];

		// Translators: 1 - The plugin short name ("AIOSEO").
		$name = sprintf( esc_html__( '%1$s - HTML Sitemap', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME );
		$name .= ' ' . esc_html__( '(legacy)', 'all-in-one-seo-pack' );
		parent::__construct( $widgetSlug, $name, $widgetOptions, $controlOptions );
	}

	/**
	 * Callback for the widget.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $args     The widget arguments.
	 * @param  array $instance The widget instance options.
	 * @return void
	 */
	public function widget( $args, $instance ) {
		if ( ! aioseo()->options->sitemap->html->enable ) {
			return;
		}

		// Merge with defaults.
		$instance = wp_parse_args( (array) $instance, $this->defaults );

		echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped

		if ( ! empty( $instance['title'] ) ) {
			echo $args['before_title'] . apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped,Generic.Files.LineLength.MaxExceeded
		}

		$instance = aioseo()->htmlSitemap->frontend->getAttributes( $instance );
		aioseo()->htmlSitemap->frontend->output( true, $instance );
		echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Callback to update the widget options.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $newOptions The new options.
	 * @param  array $oldOptions The old options.
	 * @return array             The new options.
	 */
	public function update( $newOptions, $oldOptions ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$settings = [
			'title',
			'order',
			'order_by',
			'show_label',
			'publication_date',
			'archives',
			'excluded_posts',
			'excluded_terms'
		];

		foreach ( $settings as $setting ) {
			$newOptions[ $setting ] = ! empty( $newOptions[ $setting ] ) ? wp_strip_all_tags( $newOptions[ $setting ] ) : '';
		}

		$includedPostTypes = [];
		if ( ! empty( $newOptions['post_types'] ) ) {
			$postTypes = $this->getPublicPostTypes( true );
			foreach ( $newOptions['post_types'] as $v ) {
				if ( is_numeric( $v ) ) {
					$includedPostTypes[] = $postTypes[ $v ];
				} else {
					$includedPostTypes[] = $v;
				}
			}
		}
		$newOptions['post_types'] = $includedPostTypes;

		$includedTaxonomies = [];
		if ( ! empty( $newOptions['taxonomies'] ) ) {
			$taxonomies = aioseo()->helpers->getPublicTaxonomies( true );
			foreach ( $newOptions['taxonomies'] as $v ) {
				if ( is_numeric( $v ) ) {
					$includedTaxonomies[] = $taxonomies[ $v ];
				} else {
					$includedTaxonomies[] = $v;
				}
			}
		}
		$newOptions['taxonomies'] = $includedTaxonomies;

		if ( ! empty( $newOptions['excluded_posts'] ) ) {
			$newOptions['excluded_posts'] = $this->sanitizeExcludedIds( $newOptions['excluded_posts'] );
		}

		if ( ! empty( $newOptions['excluded_terms'] ) ) {
			$newOptions['excluded_terms'] = $this->sanitizeExcludedIds( $newOptions['excluded_terms'] );
		}

		return $newOptions;
	}

	/**
	 * Callback for the widgets options form.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $instance The widget options.
	 * @return void
	 */
	public function form( $instance ) {
		// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$instance        = wp_parse_args( (array) $instance, $this->defaults );
		$postTypeObjects = $this->getPublicPostTypes();
		$postTypes       = $this->getPublicPostTypes( true );
		$taxonomyObjects = aioseo()->helpers->getPublicTaxonomies();
		// phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable

		include AIOSEO_DIR . '/app/Common/Views/sitemap/html/widget-options.php';
	}

	/**
	 * Returns the public post types (without attachments).
	 *
	 * @since 4.1.3
	 *
	 * @param  boolean $namesOnly Whether only the names should be returned.
	 * @return array              The public post types.
	 */
	private function getPublicPostTypes( $namesOnly = false ) {
		$postTypes = aioseo()->helpers->getPublicPostTypes( $namesOnly );
		foreach ( $postTypes as $k => $postType ) {
			if ( is_array( $postType ) && 'attachment' === $postType['name'] ) {
				unset( $postTypes[ $k ] );
				break;
			}
			if ( ! is_array( $postType ) && 'attachment' === $postType ) {
				unset( $postTypes[ $k ] );
				break;
			}
		}

		return array_values( $postTypes );
	}

	/**
	 * Sanitizes the excluded IDs by removing any non-integer values.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $ids The IDs as a string, comma-separated.
	 * @return string      The sanitized IDs as a string, comma-separated.
	 */
	private function sanitizeExcludedIds( $ids ) {
		$ids = array_map( 'trim', explode( ',', $ids ) );
		$ids = array_filter( $ids, 'is_numeric' );
		$ids = esc_sql( implode( ', ', $ids ) );

		return $ids;
	}
}Common/Sitemap/Image/Image.php000064400000022536151536241200012162 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap\Image;

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

/**
 * Determines which images are included in a post/term.
 *
 * @since 4.0.0
 */
class Image {
	/**
	 * The image scan action name.
	 *
	 * @since 4.0.13
	 *
	 * @var string
	 */
	private $imageScanAction = 'aioseo_image_sitemap_scan';

	/**
	 * The supported image extensions.
	 *
	 * @since 4.2.2
	 *
	 * @var array[string]
	 */
	public $supportedExtensions = [
		'gif',
		'heic',
		'jpeg',
		'jpg',
		'png',
		'svg',
		'webp',
		'ico'
	];

	/**
	 * The post object.
	 *
	 * @since 4.2.7
	 *
	 * @var \WP_Post
	 */
	private $post = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.0.5
	 */
	public function __construct() {
		// Column may not have been created yet.
		if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'image_scan_date' ) ) {
			return;
		}

		// NOTE: This needs to go above the is_admin check in order for it to run at all.
		add_action( $this->imageScanAction, [ $this, 'scanPosts' ] );

		// Don't schedule a scan if we are not in the admin.
		if ( ! is_admin() ) {
			return;
		}

		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		// Don't schedule a scan if an importer or the V3 migration is running.
		// We'll do our scans there.
		if (
			aioseo()->importExport->isImportRunning() ||
			aioseo()->migration->isMigrationRunning()
		) {
			return;
		}
		// Action Scheduler hooks.
		add_action( 'init', [ $this, 'scheduleScan' ], 3001 );
	}

	/**
	 * Schedules the image sitemap scan.
	 *
	 * @since 4.0.5
	 *
	 * @return void
	 */
	public function scheduleScan() {
		if (
			! aioseo()->options->sitemap->general->enable ||
			aioseo()->sitemap->helpers->excludeImages()
		) {
			return;
		}

		aioseo()->actionScheduler->scheduleSingle( $this->imageScanAction, 10 );
	}

	/**
	 * Scans posts for images.
	 *
	 * @since 4.0.5
	 *
	 * @return void
	 */
	public function scanPosts() {
		if (
			! aioseo()->options->sitemap->general->enable ||
			aioseo()->sitemap->helpers->excludeImages()
		) {
			return;
		}

		$postsPerScan = apply_filters( 'aioseo_image_sitemap_posts_per_scan', 10 );
		$postTypes    = implode( "', '", aioseo()->helpers->getPublicPostTypes( true ) );

		$posts = aioseo()->core->db
			->start( aioseo()->core->db->db->posts . ' as p', true )
			->select( '`p`.`ID`, `p`.`post_type`, `p`.`post_content`, `p`.`post_excerpt`, `p`.`post_modified_gmt`' )
			->leftJoin( 'aioseo_posts as ap', '`ap`.`post_id` = `p`.`ID`' )
			->whereRaw( '( `ap`.`id` IS NULL OR `p`.`post_modified_gmt` > `ap`.`image_scan_date` OR `ap`.`image_scan_date` IS NULL )' )
			->whereRaw( "`p`.`post_status` IN ( 'publish', 'inherit' )" )
			->whereRaw( "`p`.`post_type` IN ( '$postTypes' )" )
			->limit( $postsPerScan )
			->run()
			->result();

		if ( ! $posts ) {
			aioseo()->actionScheduler->scheduleSingle( $this->imageScanAction, 15 * MINUTE_IN_SECONDS, [], true );

			return;
		}

		foreach ( $posts as $post ) {
			$this->scanPost( $post );
		}

		aioseo()->actionScheduler->scheduleSingle( $this->imageScanAction, 30, [], true );
	}

	/**
	 * Returns the image entries for a given post.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|int $post The post object or ID.
	 * @return void
	 */
	public function scanPost( $post ) {
		if ( is_numeric( $post ) ) {
			$post = get_post( $post );
		}

		$this->post = $post;

		if ( ! empty( $post->post_password ) ) {
			$this->updatePost( $post->ID );

			return;
		}

		if ( 'attachment' === $post->post_type ) {
			if ( ! wp_attachment_is( 'image', $post->ID ) ) {
				$this->updatePost( $post->ID );

				return;
			}

			$image = $this->buildEntries( [ $post->ID ] );
			$this->updatePost( $post->ID, $image );

			return;
		}

		$images = $this->extract();
		$images = $this->removeImageDimensions( $images );

		$images = apply_filters( 'aioseo_sitemap_images', $images, $post );

		// Limit to a 1,000 URLs, in accordance to Google's specifications.
		$images = array_slice( $images, 0, 1000 );
		$this->updatePost( $post->ID, $this->buildEntries( $images ) );
	}

	/**
	 * Returns the image entries for a given term.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Term $term The term object.
	 * @return array          The image entries.
	 */
	public function term( $term ) {
		if ( aioseo()->sitemap->helpers->excludeImages() ) {
			return [];
		}

		$id = get_term_meta( $term->term_id, 'thumbnail_id', true );
		if ( ! $id ) {
			return [];
		}

		return $this->buildEntries( [ $id ] );
	}

	/**
	 * Builds the image entries.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $images The images, consisting of attachment IDs or external URLs.
	 * @return array         The image entries.
	 */
	private function buildEntries( $images ) {
		$entries = [];
		foreach ( $images as $image ) {
			$idOrUrl  = $this->getImageIdOrUrl( $image );
			$imageUrl = is_numeric( $idOrUrl ) ? wp_get_attachment_url( $idOrUrl ) : $idOrUrl;
			$imageUrl = aioseo()->sitemap->helpers->formatUrl( $imageUrl );
			if ( ! $imageUrl || ! preg_match( $this->getImageExtensionRegexPattern(), (string) $imageUrl ) ) {
				continue;
			}

			// If the image URL is not external, make it relative.
			// This is important for users who scan their sites in a local/staging environment and then
			// push the data to production.
			if ( ! aioseo()->helpers->isExternalUrl( $imageUrl ) ) {
				$imageUrl = aioseo()->helpers->makeUrlRelative( $imageUrl );
			}

			$entries[ $idOrUrl ] = [ 'image:loc' => $imageUrl ];
		}

		return array_values( $entries );
	}

	/**
	 * Returns the ID of the image if it's hosted on the site. Otherwise it returns the external URL.
	 *
	 * @since 4.1.3
	 *
	 * @param  int|string $image The attachment ID or URL.
	 * @return int|string        The attachment ID or URL.
	 */
	private function getImageIdOrUrl( $image ) {
		if ( is_numeric( $image ) ) {
			return $image;
		}

		$attachmentId = false;
		if ( aioseo()->helpers->isValidAttachment( $image ) ) {
			$attachmentId = aioseo()->helpers->attachmentUrlToPostId( $image );
		}

		return $attachmentId ? $attachmentId : $image;
	}

	/**
	 * Extracts all image URls and IDs from the post.
	 *
	 * @since 4.0.0
	 *
	 * @return array The image URLs and IDs.
	 */
	private function extract() {
		$images = [];

		if ( has_post_thumbnail( $this->post ) ) {
			$images[] = get_the_post_thumbnail_url( $this->post );
		}

		// Get the galleries here before doShortcodes() runs below to prevent buggy behaviour.
		// WordPress is supposed to only return the attached images but returns a different result if the shortcode has no valid attributes, so we need to grab them manually.
		$images = array_merge( $images, $this->getPostGalleryImages() );

		// Now, get the remaining images from image tags in the post content.
		$parsedPostContent = do_blocks( $this->post->post_content );
		$parsedPostContent = aioseo()->helpers->doShortcodes( $parsedPostContent, true, $this->post->ID );
		$parsedPostContent = preg_replace( '/\s\s+/u', ' ', (string) trim( $parsedPostContent ) ); // Trim both internal and external whitespace.

		// Get the images from any third-party plugins/themes that are active.
		$thirdParty = new ThirdParty( $this->post, $parsedPostContent );
		$images     = array_merge( $images, $thirdParty->extract() );

		preg_match_all( '#<(amp-)?img[^>]+src="([^">]+)"#', (string) $parsedPostContent, $matches );
		foreach ( $matches[2] as $url ) {
			$images[] = aioseo()->helpers->makeUrlAbsolute( $url );
		}

		return array_unique( $images );
	}

	/**
	 * Returns all images from WP Core post galleries.
	 *
	 * @since 4.2.2
	 *
	 * @return array[string] The image URLs.
	 */
	private function getPostGalleryImages() {
		$images    = [];
		$galleries = get_post_galleries( $this->post, false );
		foreach ( $galleries as $gallery ) {
			foreach ( $gallery['src'] as $imageUrl ) {
				$images[] = $imageUrl;
			}
		}

		// Now, get rid of them so that we don't process the shortcodes again.
		$regex                    = get_shortcode_regex( [ 'gallery' ] );
		$this->post->post_content = preg_replace( "/$regex/i", '', (string) $this->post->post_content );

		return $images;
	}

	/**
	 * Removes image dimensions from the slug.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $urls         The image URLs.
	 * @return array $preparedUrls The formatted image URLs.
	 */
	private function removeImageDimensions( $urls ) {
		$preparedUrls = [];
		foreach ( $urls as $url ) {
			$preparedUrls[] = aioseo()->helpers->removeImageDimensions( $url );
		}

		return array_unique( array_filter( $preparedUrls ) );
	}

	/**
	 * Stores the image data for a given post in our DB table.
	 *
	 * @since 4.0.5
	 *
	 * @param  int   $postId The post ID.
	 * @param  array $images The images.
	 * @return void
	 */
	private function updatePost( $postId, $images = [] ) {
		$post                    = \AIOSEO\Plugin\Common\Models\Post::getPost( $postId );
		$meta                    = $post->exists() ? [] : aioseo()->migration->meta->getMigratedPostMeta( $postId );
		$meta['post_id']         = $postId;
		$meta['images']          = ! empty( $images ) ? $images : null;
		$meta['image_scan_date'] = gmdate( 'Y-m-d H:i:s' );

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

	/**
	 * Returns the image extension regex pattern.
	 *
	 * @since 4.2.2
	 *
	 * @return string
	 */
	public function getImageExtensionRegexPattern() {
		static $pattern;
		if ( null !== $pattern ) {
			return $pattern;
		}

		$pattern = '/http.*\.(' . implode( '|', $this->supportedExtensions ) . ')$/i';

		return $pattern;
	}
}Common/Sitemap/Image/ThirdParty.php000064400000017641151536241200013233 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap\Image;

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

/**
 * Holds all code to extract images from third-party content.
 *
 * @since 4.2.2
 */
class ThirdParty {
	/**
	 * The post object.
	 *
	 * @since 4.2.2
	 *
	 * @var \WP_Post
	 */
	private $post;

	/**
	 * The parsed post content.
	 * The post object holds the unparsed content as we need that for Divi.
	 *
	 * @since 4.2.5
	 *
	 * @var string
	 */
	private $parsedPostContent;

	/**
	 * The image URLs and IDs.
	 *
	 * @since 4.2.2
	 *
	 * @var array[mixed]
	 */
	private $images = [];

	/**
	 * Divi shortcodes.
	 *
	 * @since 4.2.3
	 *
	 * @var string[]
	 */
	private $shortcodes = [
		'et_pb_section',
		'et_pb_column',
		'et_pb_row',
		'et_pb_image',
		'et_pb_gallery',
		'et_pb_accordion',
		'et_pb_accordion_item',
		'et_pb_counters',
		'et_pb_blurb',
		'et_pb_cta',
		'et_pb_code',
		'et_pb_contact_form',
		'et_pb_divider',
		'et_pb_filterable_portfolio',
		'et_pb_map',
		'et_pb_number_counter',
		'et_pb_post_slider',
		'et_pb_pricing_tables',
		'et_pb_pricing_table',
		'et_pb_shop',
		'et_pb_slider',
		'et_pb_slide',
		'et_pb_tabs',
		'et_pb_tab',
		'et_pb_text',
		'et_pb_video',
		'et_pb_audio',
		'et_pb_blog',
		'et_pb_circle_counter',
		'et_pb_comments',
		'et_pb_countdown_timer',
		'et_pb_signup',
		'et_pb_login',
		'et_pb_menu',
		'et_pb_team_member',
		'et_pb_post_nav',
		'et_pb_post_title',
		'et_pb_search',
		'et_pb_sidebar',
		'et_pb_social_media_follow',
		'et_pb_social_media_follow_network',
		'et_pb_testimonial',
		'et_pb_toggle',
		'et_pb_video_slider',
		'et_pb_video_slider_item',
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.2.2
	 *
	 * @param \WP_Post $post              The post object.
	 * @param string   $parsedPostContent The parsed post content.
	 */
	public function __construct( $post, $parsedPostContent ) {
		$this->post              = $post;
		$this->parsedPostContent = $parsedPostContent;
	}

	/**
	 * Extracts the images from third-party content.
	 *
	 * @since 4.2.2
	 *
	 * @return array[mixed] The image URLs and IDs.
	 */
	public function extract() {
		$integrations = [
			'acf',
			'divi',
			'nextGen',
			'wooCommerce',
			'kadenceBlocks'
		];

		foreach ( $integrations as $integration ) {
			$this->{$integration}();
		}

		return $this->images;
	}

	/**
	 * Extracts image URLs from ACF fields.
	 *
	 * @since 4.2.2
	 *
	 * @return void
	 */
	private function acf() {
		if ( ! class_exists( 'ACF' ) || ! function_exists( 'get_fields' ) ) {
			return;
		}

		$fields = get_fields( $this->post->ID );
		if ( ! $fields ) {
			return;
		}

		$images       = $this->acfHelper( $fields );
		$this->images = array_merge( $this->images, $images );
	}

	/**
	 * Helper function for acf().
	 *
	 * @since 4.2.2
	 *
	 * @param  array         $fields The ACF fields.
	 * @return array[string]         The image URLs or IDs.
	 */
	private function acfHelper( $fields ) {
		$images = [];
		foreach ( $fields as $value ) {
			if ( is_array( $value ) ) {
				// Recursively loop over grouped fields.
				// We continue on since arrays aren't necessarily groups and might also simply aLready contain the value we're looking for.
				$images = array_merge( $images, $this->acfHelper( $value ) );

				if ( isset( $value['type'] ) && 'image' !== strtolower( $value['type'] ) ) {
					$images[] = $value['url'];
				}

				continue;
			}

			// Capture the value if it's an image URL, but not the default thumbnail from ACF.
			if ( is_string( $value ) && preg_match( aioseo()->sitemap->image->getImageExtensionRegexPattern(), (string) $value ) && ! preg_match( '/media\/default\.png$/i', (string) $value ) ) {
				$images[] = $value;
				continue;
			}

			// Capture the value if it's a numeric image ID, but make sure it's not an array of random field object properties.
			if (
				is_numeric( $value ) &&
				! isset( $fields['ID'] ) &&
				! isset( $fields['thumbnail'] )
			) {
				$images[] = $value;
			}
		}

		return $images;
	}

	/**
	 * Extracts images from Divi shortcodes.
	 *
	 * @since 4.1.8
	 *
	 * @return void
	 */
	private function divi() {
		if ( ! defined( 'ET_BUILDER_VERSION' ) ) {
			return;
		}

		$urls  = [];
		$regex = implode( '|', array_map( 'preg_quote', $this->shortcodes ) );

		preg_match_all(
			"/\[($regex)(?![\w-])([^\]\/]*(?:\/(?!\])[^\]\/]*)*?)(?:(\/)\]|\](?:([^\[]*+(?:\[(?!\/\2\])[^\[]*+)*+)\[\/\2\])?)(\]?)/i",
			(string) $this->post->post_content,
			$matches,
			PREG_SET_ORDER
		);

		foreach ( $matches as $shortcode ) {
			$attributes = shortcode_parse_atts( $shortcode[0] );
			if ( ! empty( $attributes['src'] ) ) {
				$urls[] = $attributes['src'];
			}

			if ( ! empty( $attributes['image_src'] ) ) {
				$urls[] = $attributes['image_src'];
			}

			if ( ! empty( $attributes['image_url'] ) ) {
				$urls[] = $attributes['image_url'];
			}

			if ( ! empty( $attributes['portrait_url'] ) ) {
				$urls[] = $attributes['portrait_url'];
			}

			if ( ! empty( $attributes['image'] ) ) {
				$urls[] = $attributes['image'];
			}

			if ( ! empty( $attributes['background_image'] ) ) {
				$urls[] = $attributes['background_image'];
			}

			if ( ! empty( $attributes['logo'] ) ) {
				$urls[] = $attributes['logo'];
			}

			if ( ! empty( $attributes['gallery_ids'] ) ) {
				$attachmentIds = explode( ',', $attributes['gallery_ids'] );
				foreach ( $attachmentIds as $attachmentId ) {
					$urls[] = wp_get_attachment_url( $attachmentId );
				}
			}
		}

		$this->images = array_merge( $this->images, $urls );
	}

	/**
	 * Extracts the image IDs of more advanced NextGen Pro gallerlies like the Mosaic and Thumbnail Grid.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	private function nextGen() {
		if ( ! defined( 'NGG_PLUGIN_BASENAME' ) && ! defined( 'NGG_PRO_PLUGIN_BASENAME' ) ) {
			return;
		}

		preg_match_all( '/data-image-id=\"([0-9]*)\"/i', (string) $this->parsedPostContent, $imageIds );
		if ( ! empty( $imageIds[1] ) ) {
			$this->images = array_merge( $this->images, $imageIds[1] );
		}

		// For this specific check, we only want to parse blocks and do not want to run shortcodes because some NextGen blocks (e.g. Mosaic) are parsed into shortcodes.
		// And after parsing the shortcodes, the attributes we're looking for are gone.
		$contentWithBlocksParsed = do_blocks( $this->post->post_content );

		$imageIds = [];
		preg_match_all( '/\[ngg.*src="galleries" ids="(.*?)".*\]/i', (string) $contentWithBlocksParsed, $shortcodes );
		if ( empty( $shortcodes[1] ) ) {
			return;
		}

		foreach ( $shortcodes[1] as $shortcode ) {
			$galleryIds = explode( ',', $shortcode[0] );
			foreach ( $galleryIds as $galleryId ) {
				global $nggdb;
				$galleryImageIds = $nggdb->get_ids_from_gallery( $galleryId );
				if ( empty( $galleryImageIds ) ) {
					continue;
				}

				foreach ( $galleryImageIds as $galleryImageId ) {
					$image = $nggdb->find_image( $galleryImageId );
					if ( ! empty( $image ) ) {
						$imageIds[] = $image->get_permalink();
					}
				}
			}
		}

		$this->images = array_merge( $this->images, $imageIds );
	}

	/**
	 * Extracts the image IDs of WooCommerce product galleries.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	private function wooCommerce() {
		if ( ! aioseo()->helpers->isWooCommerceActive() || 'product' !== $this->post->post_type ) {
			return;
		}

		$productImageIds = get_post_meta( $this->post->ID, '_product_image_gallery', true );
		if ( ! $productImageIds ) {
			return;
		}

		$productImageIds = explode( ',', $productImageIds );
		$this->images    = array_merge( $this->images, $productImageIds );
	}

	/**
	 * Extracts the image IDs of Kadence Block galleries.
	 *
	 * @since 4.4.5
	 *
	 * @return void
	 */
	private function kadenceBlocks() {
		if ( ! defined( 'KADENCE_BLOCKS_VERSION' ) ) {
			return [];
		}

		$blocks = aioseo()->helpers->parseBlocks( $this->post );

		foreach ( $blocks as $block ) {
			if ( 'kadence/advancedgallery' === $block['blockName'] && ! empty( $block['attrs']['ids'] ) ) {
				$this->images = array_merge( $this->images, $block['attrs']['ids'] );
			}
		}
	}
}Common/Sitemap/Localization.php000064400000024757151536241200012555 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

/**
 * Handles sitemap localization logic.
 *
 * @since 4.2.1
 */
class Localization {
	/**
	 * This is cached so we don't do the lookup each query.
	 *
	 * @since 4.0.0
	 *
	 * @var boolean
	 */
	private static $wpml = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.2.1
	 */
	public function __construct() {
		if ( apply_filters( 'aioseo_sitemap_localization_disable', false ) ) {
			return;
		}

		if ( aioseo()->helpers->isWpmlActive() ) {
			self::$wpml = [
				'defaultLanguage' => apply_filters( 'wpml_default_language', null ),
				'activeLanguages' => apply_filters( 'wpml_active_languages', null )
			];

			add_filter( 'aioseo_sitemap_term', [ $this, 'localizeWpml' ], 10, 4 );
			add_filter( 'aioseo_sitemap_post', [ $this, 'localizeWpml' ], 10, 4 );
		}

		if ( aioseo()->helpers->isPluginActive( 'weglot' ) ) {
			add_filter( 'aioseo_sitemap_term', [ $this, 'localizeWeglot' ], 10, 4 );
			add_filter( 'aioseo_sitemap_post', [ $this, 'localizeWeglot' ], 10, 4 );
			add_filter( 'aioseo_sitemap_author_entry', [ $this, 'localizeWeglot' ], 10, 4 );
			add_filter( 'aioseo_sitemap_archive_entry', [ $this, 'localizeWeglot' ], 10, 4 );
			add_filter( 'aioseo_sitemap_date_entry', [ $this, 'localizeWeglot' ], 10, 4 );
			add_filter( 'aioseo_sitemap_product_attributes', [ $this, 'localizeWeglot' ], 10, 4 );
		}
	}

	/**
	 * Localize the entries for Weglot.
	 *
	 * @since 4.8.3
	 *
	 * @param  array       $entry      The entry.
	 * @param  mixed       $entryId    The object ID, null or a date object.
	 * @param  string      $objectName The post type, taxonomy name or date type ('year' or 'month').
	 * @param  string|null $entryType  Whether the entry represents a post, term, author, archive or date.
	 * @return array                   The entry.
	 */
	public function localizeWeglot( $entry, $entryId, $objectName, $entryType = null ) {
		try {
			$originalLang = function_exists( 'weglot_get_original_language' ) ? weglot_get_original_language() : '';
			$translations = function_exists( 'weglot_get_destination_languages' ) ? weglot_get_destination_languages() : [];
			if ( empty( $originalLang ) || empty( $translations ) ) {
				return $entry;
			}

			switch ( $entryType ) {
				case 'post':
					$permalink = get_permalink( $entryId );
					break;
				case 'term':
					$permalink = get_term_link( $entryId, $objectName );
					break;
				case 'author':
					$permalink = get_author_posts_url( $entryId, $objectName );
					break;
				case 'archive':
					$permalink = get_post_type_archive_link( $objectName );
					break;
				case 'date':
					$permalink = 'year' === $objectName ? get_year_link( $entryId->year ) : get_month_link( $entryId->year, $entryId->month );
					break;
				default:
					$permalink = '';
			}

			$entry['languages'] = [];
			foreach ( $translations as $translation ) {
				// If the translation is not public we skip it.
				if ( empty( $translation['public'] ) ) {
					continue;
				}

				$l10nPermalink = $this->weglotGetLocalizedUrl( $permalink, $translation['language_to'] );
				if ( ! empty( $l10nPermalink ) ) {
					$entry['languages'][] = [
						'language' => $translation['language_to'],
						'location' => $l10nPermalink
					];
				}
			}

			// Also include the main page as a translated variant, per Google's specifications, but only if we found at least one other language.
			if ( ! empty( $entry['languages'] ) ) {
				$entry['languages'][] = [
					'language' => $originalLang,
					'location' => aioseo()->helpers->decodeUrl( $entry['loc'] )
				];
			} else {
				unset( $entry['languages'] );
			}

			return $this->validateSubentries( $entry );
		} catch ( \Exception $e ) {
			// Do nothing. It only exists because some "weglot" functions above throw exceptions.
		}

		return $entry;
	}

	/**
	 * Localize the entries for WPML.
	 *
	 * @since   4.0.0
	 * @version 4.8.3 Rename from localizeEntry to localizeWpml.
	 *
	 * @param  array  $entry      The entry.
	 * @param  int    $entryId    The post/term ID.
	 * @param  string $objectName The post type or taxonomy name.
	 * @param  string $objectType Whether the entry is a post or term.
	 * @return array              The entry.
	 */
	public function localizeWpml( $entry, $entryId, $objectName, $objectType ) {
		$elementId   = $entryId;
		$elementType = 'post_' . $objectName;
		if ( 'term' === $objectType ) {
			$term        = aioseo()->helpers->getTerm( $entryId, $objectName );
			$elementId   = $term->term_taxonomy_id;
			$elementType = 'tax_' . $objectName;
		}

		$translationGroupId = apply_filters( 'wpml_element_trid', null, $elementId, $elementType );
		$translations       = apply_filters( 'wpml_get_element_translations', null, $translationGroupId, $elementType );
		if ( empty( $translations ) ) {
			return $entry;
		}

		$entry['languages'] = [];
		$hiddenLanguages    = apply_filters( 'wpml_setting', [], 'hidden_languages' );
		foreach ( $translations as $translation ) {
			if (
				empty( $translation->element_id ) ||
				! isset( self::$wpml['activeLanguages'][ $translation->language_code ] ) ||
				in_array( $translation->language_code, $hiddenLanguages, true )
			) {
				continue;
			}

			$currentLanguage = ! empty( self::$wpml['activeLanguages'][ $translation->language_code ] ) ? self::$wpml['activeLanguages'][ $translation->language_code ] : null;
			$languageCode    = ! empty( $currentLanguage['tag'] ) ? $currentLanguage['tag'] : $translation->language_code;

			if ( (int) $elementId === (int) $translation->element_id ) {
				$entry['language'] = $languageCode;
				continue;
			}

			$translatedObjectId = apply_filters( 'wpml_object_id', $entryId, $objectName, false, $translation->language_code );
			if (
				( 'post' === $objectType && $this->isExcludedPost( $translatedObjectId ) ) ||
				( 'term' === $objectType && $this->isExcludedTerm( $translatedObjectId ) )
			) {
				continue;
			}

			if ( 'post' === $objectType ) {
				$permalink = get_permalink( $translatedObjectId );

				// Special treatment for the home page translations.
				if ( 'page' === get_option( 'show_on_front' ) && aioseo()->helpers->wpmlIsHomePage( $entryId ) ) {
					$permalink = aioseo()->helpers->wpmlHomeUrl( $translation->language_code );
				}
			} else {
				$permalink = get_term_link( $translatedObjectId, $objectName );
			}

			if ( ! empty( $languageCode ) && ! empty( $permalink ) ) {
				$entry['languages'][] = [
					'language' => $languageCode,
					'location' => aioseo()->helpers->decodeUrl( $permalink )
				];
			}
		}

		// Also include the main page as a translated variant, per Google's specifications, but only if we found at least one other language.
		if ( ! empty( $entry['language'] ) && ! empty( $entry['languages'] ) ) {
			$entry['languages'][] = [
				'language' => $entry['language'],
				'location' => aioseo()->helpers->decodeUrl( $entry['loc'] )
			];
		} else {
			unset( $entry['languages'] );
		}

		return $this->validateSubentries( $entry );
	}

	/**
	 * Validates the subentries with translated variants to ensure all required values are set.
	 *
	 * @since 4.2.3
	 *
	 * @param  array $entry The entry.
	 * @return array        The validated entry.
	 */
	private function validateSubentries( $entry ) {
		if ( ! isset( $entry['languages'] ) ) {
			return $entry;
		}

		foreach ( $entry['languages'] as $index => $subentry ) {
			if ( empty( $subentry['language'] ) || empty( $subentry['location'] ) ) {
				unset( $entry['languages'][ $index ] );
			}
		}

		return $entry;
	}

	/**
	 * Checks whether the given post should be excluded.
	 *
	 * @since 4.2.4
	 *
	 * @param  int  $postId The post ID.
	 * @return bool         Whether the post should be excluded.
	 */
	private function isExcludedPost( $postId ) {
		static $excludedPostIds = null;
		if ( null === $excludedPostIds ) {
			$excludedPostIds = explode( ', ', aioseo()->sitemap->helpers->excludedPosts() );
			$excludedPostIds = array_map( function ( $postId ) {
				return (int) $postId;
			}, $excludedPostIds );
		}

		if ( in_array( $postId, $excludedPostIds, true ) ) {
			return true;
		}

		// Let's also check if the post is published and not password-protected.
		$post = get_post( $postId );
		if ( ! is_a( $post, 'WP_Post' ) ) {
			return true;
		}

		if ( ! empty( $post->post_password ) || 'publish' !== $post->post_status ) {
			return true;
		}

		// Now, we must also check for noindex.
		$metaData = aioseo()->meta->metaData->getMetaData( $post );
		if ( ! empty( $metaData->robots_noindex ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Checks whether the given term should be excluded.
	 *
	 * @since 4.2.4
	 *
	 * @param  int  $termId The term ID.
	 * @return bool         Whether the term should be excluded.
	 */
	private function isExcludedTerm( $termId ) {
		static $excludedTermIds = null;
		if ( null === $excludedTermIds ) {
			$excludedTermIds = explode( ', ', aioseo()->sitemap->helpers->excludedTerms() );
			$excludedTermIds = array_map( function ( $termId ) {
				return (int) $termId;
			}, $excludedTermIds );
		}

		if ( in_array( $termId, $excludedTermIds, true ) ) {
			return true;
		}

		// Now, we must also check for noindex.
		$term = aioseo()->helpers->getTerm( $termId );
		if ( ! is_a( $term, 'WP_Term' ) ) {
			return true;
		}

		// At least one post must be assigned to the term.
		$posts = aioseo()->core->db->start( 'term_relationships' )
			->select( 'object_id' )
			->where( 'term_taxonomy_id =', $term->term_taxonomy_id )
			->limit( 1 )
			->run()
			->result();

		if ( empty( $posts ) ) {
			return true;
		}

		$metaData = aioseo()->meta->metaData->getMetaData( $term );
		if ( ! empty( $metaData->robots_noindex ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Retrieves the localized URL.
	 *
	 * @since 4.8.3
	 *
	 * @param  string       $url  The page URL to localize.
	 * @param  string       $code The language code (e.g. 'br', 'en').
	 * @return string|false       The localized URL or false if it fails.
	 */
	private function weglotGetLocalizedUrl( $url, $code ) {
		try {
			if (
				! $url ||
				! function_exists( 'weglot_get_service' )
			) {
				return false;
			}

			$languageService   = weglot_get_service( 'Language_Service_Weglot' );
			$requestUrlService = weglot_get_service( 'Request_Url_Service_Weglot' );
			$wgUrl             = $requestUrlService->create_url_object( $url );
			$language          = $languageService->get_language_from_internal( $code );

			return $wgUrl->getForLanguage( $language );
		} catch ( \Exception $e ) {
			// Do nothing. It only exists because some "weglot" functions above throw exceptions.
		}

		return false;
	}
}Common/Sitemap/Output.php000064400000011644151536241200011414 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

/**
 * Handles outputting the sitemap.
 *
 * @since 4.0.0
 */
class Output {
	/**
	 * Outputs the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $entries The sitemap entries.
	 * @return void
	 */
	public function output( $entries ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		if ( ! in_array( aioseo()->sitemap->type, [ 'general', 'rss' ], true ) ) {
			return;
		}

		// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$entries       = aioseo()->sitemap->helpers->decodeSitemapEntries( $entries );
		$charset       = aioseo()->helpers->getCharset();
		$excludeImages = aioseo()->sitemap->helpers->excludeImages();
		$generation    = ! isset( aioseo()->sitemap->isStatic ) || aioseo()->sitemap->isStatic ? __( 'statically', 'all-in-one-seo-pack' ) : __( 'dynamically', 'all-in-one-seo-pack' );
		$version       = aioseo()->helpers->getAioseoVersion();

		if ( ! empty( $version ) ) {
			$version = 'v' . $version;
		}

		// Clear all output buffers to avoid conflicts.
		aioseo()->helpers->clearBuffers();

		echo '<?xml version="1.0" encoding="' . esc_attr( $charset ) . "\"?>\r\n";
		echo '<!-- ' . sprintf(
			// Translators: 1 - "statically" or "dynamically", 2 - The date, 3 - The time, 4 - The plugin name ("All in One SEO"), 5 - Currently installed version.
			esc_html__( 'This sitemap was %1$s generated on %2$s at %3$s by %4$s %5$s - the original SEO plugin for WordPress.', 'all-in-one-seo-pack' ),
			esc_html( $generation ),
			esc_html( date_i18n( get_option( 'date_format' ) ) ),
			esc_html( date_i18n( get_option( 'time_format' ) ) ),
			esc_html( AIOSEO_PLUGIN_NAME ),
			esc_html( $version )
		) . ' -->';

		if ( 'rss' === aioseo()->sitemap->type ) {
			$xslUrl = home_url() . '/default-sitemap.xsl';

			if ( ! is_multisite() ) {
				$title       = get_bloginfo( 'name' );
				$description = get_bloginfo( 'blogdescription' );
				$link        = home_url();
			} else {
				$title       = get_blog_option( get_current_blog_id(), 'blogname' );
				$description = get_blog_option( get_current_blog_id(), 'blogdescription' );
				$link        = get_blog_option( get_current_blog_id(), 'siteurl' );
			}

			$ttl = apply_filters( 'aioseo_sitemap_rss_ttl', 60 );

			echo "\r\n\r\n<?xml-stylesheet type=\"text/xsl\" href=\"" . esc_url( $xslUrl ) . "\"?>\r\n";
			include_once AIOSEO_DIR . '/app/Common/Views/sitemap/xml/rss.php';

			return;
		}

		if ( 'root' === aioseo()->sitemap->indexName && aioseo()->sitemap->indexes ) {
			$xslUrl = add_query_arg( 'sitemap', aioseo()->sitemap->indexName, home_url() . '/default-sitemap.xsl' );

			echo "\r\n\r\n<?xml-stylesheet type=\"text/xsl\" href=\"" . esc_url( $xslUrl ) . "\"?>\r\n";
			include AIOSEO_DIR . '/app/Common/Views/sitemap/xml/root.php';

			return;
		}

		$xslUrl = add_query_arg( 'sitemap', aioseo()->sitemap->indexName, home_url() . '/default-sitemap.xsl' );

		echo "\r\n\r\n<?xml-stylesheet type=\"text/xsl\" href=\"" . esc_url( $xslUrl ) . "\"?>\r\n";
		include AIOSEO_DIR . '/app/Common/Views/sitemap/xml/default.php';
	}

	/**
	 * Escapes and echoes the given XML tag value.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $value The tag value.
	 * @param  bool   $wrap  Whether the value should we wrapped in a CDATA section.
	 * @return void
	 */
	public function escapeAndEcho( $value, $wrap = true ) {
		$safeText = is_string( $value ) ? wp_check_invalid_utf8( $value, true ) : $value;
		$isZero   = is_numeric( $value ) ? 0 === (int) $value : false;
		if ( ! $safeText && ! $isZero ) {
			return;
		}

		$cdataRegex = '\<\!\[CDATA\[.*?\]\]\>';
		$regex      = "/(?=.*?{$cdataRegex})(?<non_cdata_followed_by_cdata>(.*?))(?<cdata>({$cdataRegex}))|(?<non_cdata>(.*))/sx";

		$safeText = (string) preg_replace_callback(
			$regex,
			static function( $matches ) {
				if ( ! $matches[0] ) {
					return '';
				}

				if ( ! empty( $matches['non_cdata'] ) ) {
					// Escape HTML entities in the non-CDATA section.
					return _wp_specialchars( $matches['non_cdata'], ENT_XML1 );
				}

				// Return the CDATA Section unchanged, escape HTML entities in the rest.
				return _wp_specialchars( $matches['non_cdata_followed_by_cdata'], ENT_XML1 ) . $matches['cdata'];
			},
			$safeText
		);

		$safeText = $safeText ? $safeText : ( $isZero ? $value : '' );

		if ( ! $wrap ) {
			return print( $safeText ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}

		printf( '<![CDATA[%1$s]]>', $safeText ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Returns the URL for the sitemap stylesheet.
	 *
	 * This is needed for compatibility with multilingual plugins such as WPML.
	 *
	 * @since 4.0.0
	 *
	 * @return string The URL to the sitemap stylesheet.
	 */
	private function xslUrl() {
		return esc_url( apply_filters( 'aioseo_sitemap_xsl_url', aioseo()->helpers->localizedUrl( '/sitemap.xsl' ) ) );
	}
}Common/Sitemap/Priority.php000064400000016577151536241200011747 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

/**
 * Determines the priority/frequency.
 *
 * @since 4.0.0
 */
class Priority {
	/**
	 * Whether the advanced settings are enabled for the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @var boolean
	 */
	private static $advanced;

	/**
	 * The global priority for the page type.
	 *
	 * @since 4.0.0
	 *
	 * @var boolean
	 */
	private static $globalPriority = [];

	/**
	 * The global frequency for the page type.
	 *
	 * @since 4.0.0
	 *
	 * @var boolean
	 */
	private static $globalFrequency = [];

	/**
	 * Whether or not we have grouped our settings.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private static $grouped = [];

	/**
	 * The current object type priority.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private static $objectTypePriority = [];

	/**
	 * The current object type frequency.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private static $objectTypeFrequency = [];

	/**
	 * Returns the sitemap priority for a given page.
	 *
	 * @since 4.0.0
	 *
	 * @param  string         $pageType   The type of page (e.g. homepage, blog, post, taxonomies, etc.).
	 * @param  \stdClass|bool $object     The post/term object (optional).
	 * @param  string         $objectType The post/term object type (optional).
	 * @return float                      The priority.
	 */
	public function priority( $pageType, $object = false, $objectType = '' ) {
		// Store setting values in static properties so that we can cache them.
		// Otherwise this has a significant impact on the load time of the sitemap.
		if ( ! self::$advanced ) {
			self::$advanced = aioseo()->options->sitemap->general->advancedSettings->enable;
		}

		if ( ! isset( self::$globalPriority[ $pageType . $objectType ] ) ) {
			$options = aioseo()->options->noConflict();

			$pageTypeConditional = 'date' === $pageType ? 'archive' : $pageType;
			self::$globalPriority[ $pageType . $objectType ] = self::$advanced && $options->sitemap->general->advancedSettings->priority->has( $pageTypeConditional )
				? json_decode( $options->sitemap->general->advancedSettings->priority->$pageTypeConditional->priority )
				: false;
		}

		if ( ! isset( self::$grouped[ $pageType . $objectType ] ) ) {
			$options = aioseo()->options->noConflict();
			self::$grouped[ $pageType . $objectType ] = self::$advanced &&
				$options->sitemap->general->advancedSettings->priority->has( $pageType ) &&
				$options->sitemap->general->advancedSettings->priority->$pageType->has( 'grouped' )
					? $options->sitemap->general->advancedSettings->priority->$pageType->grouped
					: true;
		}

		if ( empty( self::$grouped[ $pageType . $objectType ] ) && self::$advanced ) {
			if ( ! isset( self::$objectTypePriority[ $pageType . $objectType ] ) ) {
				$dynamicOptions = aioseo()->dynamicOptions->noConflict();
				self::$objectTypePriority[ $pageType . $objectType ] = $dynamicOptions->sitemap->priority->has( $pageType ) && $dynamicOptions->sitemap->priority->$pageType->has( $objectType )
					? json_decode( $dynamicOptions->sitemap->priority->$pageType->$objectType->priority )
					: false;
			}
		}

		$priority = $this->defaultPriority( $pageType );
		if ( self::$globalPriority[ $pageType . $objectType ] ) {
			$defaultValue = ! self::$grouped[ $pageType . $objectType ] &&
				self::$advanced &&
				! empty( self::$objectTypePriority[ $pageType . $objectType ] )
					? self::$objectTypePriority[ $pageType . $objectType ]
					: self::$globalPriority[ $pageType . $objectType ];
			$priority     = 'default' === $defaultValue->value ? $priority : $defaultValue->value;
		}

		return $priority;
	}

	/**
	 * Returns the sitemap frequency for a given page.
	 *
	 * @since 4.0.0
	 *
	 * @param  string         $pageType   The type of page (e.g. homepage, blog, post, taxonomies, etc.).
	 * @param  \stdClass|bool $object     The post/term object (optional).
	 * @param  string         $objectType The post/term object type (optional).
	 * @return float                      The frequency.
	 */
	public function frequency( $pageType, $object = false, $objectType = '' ) {
		// Store setting values in static properties so that we can cache them.
		// Otherwise this has a significant impact on the load time of the sitemap.
		if ( ! self::$advanced ) {
			self::$advanced = aioseo()->options->sitemap->general->advancedSettings->enable;
		}
		if ( ! isset( self::$globalFrequency[ $pageType . $objectType ] ) ) {
			$options = aioseo()->options->noConflict();
			$pageTypeConditional = 'date' === $pageType ? 'archive' : $pageType;
			self::$globalFrequency[ $pageType . $objectType ] = self::$advanced && $options->sitemap->general->advancedSettings->priority->has( $pageTypeConditional )
				? json_decode( $options->sitemap->general->advancedSettings->priority->$pageTypeConditional->frequency )
				: false;
		}

		if ( ! isset( self::$grouped[ $pageType . $objectType ] ) ) {
			$options = aioseo()->options->noConflict();
			self::$grouped[ $pageType . $objectType ] = self::$advanced &&
				$options->sitemap->general->advancedSettings->priority->has( $pageType ) &&
				$options->sitemap->general->advancedSettings->priority->$pageType->has( 'grouped' )
					? $options->sitemap->general->advancedSettings->priority->$pageType->grouped
					: true;
		}

		if ( empty( self::$grouped[ $pageType . $objectType ] ) && self::$advanced ) {
			if ( ! isset( self::$objectTypeFrequency[ $pageType . $objectType ] ) ) {
				$dynamicOptions = aioseo()->dynamicOptions->noConflict();

				self::$objectTypeFrequency[ $pageType . $objectType ] = $dynamicOptions->sitemap->priority->has( $pageType ) && $dynamicOptions->sitemap->priority->$pageType->has( $objectType )
					? json_decode( $dynamicOptions->sitemap->priority->$pageType->$objectType->frequency )
					: false;
			}
		}

		$frequency = $this->defaultFrequency( $pageType );
		if ( self::$globalFrequency[ $pageType . $objectType ] ) {
			$defaultValue = ! self::$grouped[ $pageType . $objectType ] &&
				self::$advanced &&
				! empty( self::$objectTypeFrequency[ $pageType . $objectType ] )
					? self::$objectTypeFrequency[ $pageType . $objectType ]
					: self::$globalFrequency[ $pageType . $objectType ];
			$frequency    = 'default' === $defaultValue->value ? $frequency : $defaultValue->value;
		}

		return $frequency;
	}

	/**
	 * Returns the default priority for the page.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $pageType The type of page.
	 * @return float            The default priority.
	 */
	private function defaultPriority( $pageType ) {
		$defaults = [
			'homePage'   => 1.0,
			'blog'       => 0.9,
			'sitemap'    => 0.8,
			'postTypes'  => 0.7,
			'archive'    => 0.5,
			'author'     => 0.3,
			'taxonomies' => 0.3,
			'other'      => 0.5
		];

		if ( array_key_exists( $pageType, $defaults ) ) {
			return $defaults[ $pageType ];
		}

		return $defaults['other'];
	}

	/**
	 * Returns the default frequency for the page.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $pageType The type of page.
	 * @return float            The default frequency.
	 */
	private function defaultFrequency( $pageType ) {
		$defaults = [
			'homePage'   => 'always',
			'sitemap'    => 'hourly',
			'blog'       => 'daily',
			'postTypes'  => 'weekly',
			'author'     => 'weekly',
			'archive'    => 'monthly',
			'taxonomies' => 'monthly',
			'other'      => 'weekly'
		];

		if ( array_key_exists( $pageType, $defaults ) ) {
			return $defaults[ $pageType ];
		}

		return $defaults['other'];
	}
}Common/Sitemap/Query.php000064400000027622151536241200011224 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

use AIOSEO\Plugin\Common\Utils as CommonUtils;

/**
 * Handles all complex queries for the sitemap.
 *
 * @since 4.0.0
 */
class Query {
	/**
	 * Returns all eligble sitemap entries for a given post type.
	 *
	 * @since 4.0.0
	 *
	 * @param  mixed            $postTypes      The post type(s). Either a singular string or an array of strings.
	 * @param  array            $additionalArgs Any additional arguments for the post query.
	 * @return array[object|int]                The post objects or the post count.
	 */
	public function posts( $postTypes, $additionalArgs = [] ) {
		$includedPostTypes = $postTypes;
		$postTypesArray    = ! is_array( $postTypes ) ? [ $postTypes ] : $postTypes;
		if ( is_array( $postTypes ) ) {
			$includedPostTypes = implode( "', '", $postTypes );
		}

		if (
			empty( $includedPostTypes ) ||
			( 'attachment' === $includedPostTypes && 'disabled' !== aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls )
		) {
			return [];
		}

		// Set defaults.
		$maxAge = '';
		$fields = implode( ', ', [
			'p.ID',
			'p.post_excerpt',
			'p.post_type',
			'p.post_password',
			'p.post_parent',
			'p.post_date_gmt',
			'p.post_modified_gmt',
			'ap.priority',
			'ap.frequency'
		] );

		if ( in_array( aioseo()->sitemap->type, [ 'html', 'rss', 'llms' ], true ) ) {
			$fields .= ', p.post_title';
		}

		if ( 'general' !== aioseo()->sitemap->type || ! aioseo()->sitemap->helpers->excludeImages() ) {
			$fields .= ', ap.images';
		}

		// Order by highest priority first (highest priority at the top),
		// then by post modified date (most recently updated at the top).
		$orderBy = 'ap.priority DESC, p.post_modified_gmt DESC';

		// Override defaults if passed as additional arg.
		foreach ( $additionalArgs as $name => $value ) {
			// Attachments need to be fetched with all their fields because we need to get their post parent further down the line.
			$$name = esc_sql( $value );
			if ( 'root' === $name && $value && 'attachment' !== $includedPostTypes ) {
				$fields = 'p.ID, p.post_type';
			}
			if ( 'count' === $name && $value ) {
				$fields = 'count(p.ID) as total';
			}
		}

		$query = aioseo()->core->db
			->start( aioseo()->core->db->db->posts . ' as p', true )
			->select( $fields )
			->leftJoin( 'aioseo_posts as ap', 'ap.post_id = p.ID' )
			->where( 'p.post_status', 'attachment' === $includedPostTypes ? 'inherit' : 'publish' )
			->whereRaw( "p.post_type IN ( '$includedPostTypes' )" );

		$homePageId = (int) get_option( 'page_on_front' );

		if ( ! is_array( $postTypes ) ) {
			if ( ! aioseo()->helpers->isPostTypeNoindexed( $includedPostTypes ) ) {
				$query->whereRaw( "( `ap`.`robots_noindex` IS NULL OR `ap`.`robots_default` = 1 OR `ap`.`robots_noindex` = 0 OR post_id = $homePageId )" );
			} else {
				$query->whereRaw( "( `ap`.`robots_default` = 0 AND `ap`.`robots_noindex` = 0 OR post_id = $homePageId )" );
			}
		} else {
			$robotsMetaSql = [];
			foreach ( $postTypes as $postType ) {
				if ( ! aioseo()->helpers->isPostTypeNoindexed( $postType ) ) {
					$robotsMetaSql[] = "( `p`.`post_type` = '$postType' AND ( `ap`.`robots_noindex` IS NULL OR `ap`.`robots_default` = 1 OR `ap`.`robots_noindex` = 0 OR post_id = $homePageId ) )";
				} else {
					$robotsMetaSql[] = "( `p`.`post_type` = '$postType' AND ( `ap`.`robots_default` = 0 AND `ap`.`robots_noindex` = 0 OR post_id = $homePageId ) )";
				}
			}
			$query->whereRaw( '( ' . implode( ' OR ', $robotsMetaSql ) . ' )' );
		}

		$excludedPosts = aioseo()->sitemap->helpers->excludedPosts();

		if ( $excludedPosts ) {
			$query->whereRaw( "( `p`.`ID` NOT IN ( $excludedPosts ) OR post_id = $homePageId )" );
		}

		// Exclude posts assigned to excluded terms.
		$excludedTerms = aioseo()->sitemap->helpers->excludedTerms();
		if ( $excludedTerms ) {
			$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
			$query->whereRaw("
				( `p`.`ID` NOT IN
					(
						SELECT `tr`.`object_id`
						FROM `$termRelationshipsTable` as tr
						WHERE `tr`.`term_taxonomy_id` IN ( $excludedTerms )
					)
				)" );
		}

		if ( $maxAge ) {
			$query->whereRaw( "( `p`.`post_date_gmt` >= '$maxAge' )" );
		}

		if (
			'rss' === aioseo()->sitemap->type ||
			(
				aioseo()->sitemap->indexes &&
				empty( $additionalArgs['root'] ) &&
				empty( $additionalArgs['count'] )
			)
		) {
			$query->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->offset );
		}

		$isStaticHomepage = 'page' === get_option( 'show_on_front' );
		if ( $isStaticHomepage ) {
			$excludedPostIds = array_map( 'intval', explode( ',', $excludedPosts ) );
			$blogPageId      = (int) get_option( 'page_for_posts' );

			if ( in_array( 'page', $postTypesArray, true ) ) {
				// Exclude the blog page from the pages post type.
				if ( $blogPageId ) {
					$query->whereRaw( "`p`.`ID` != $blogPageId" );
				}

				// Custom order by statement to always move the home page to the top.
				if ( $homePageId ) {
					$orderBy = "case when `p`.`ID` = $homePageId then 0 else 1 end, $orderBy";
				}
			}

			// Include the blog page in the posts post type unless manually excluded.
			if (
				$blogPageId &&
				! in_array( $blogPageId, $excludedPostIds, true ) &&
				in_array( 'post', $postTypesArray, true )
			) {
				// We are using a database class hack to get in an OR clause to
				// bypass all the other WHERE statements and just include the
				// blog page ID manually.
				$query->whereRaw( "1=1 OR `p`.`ID` = $blogPageId" );

				// Custom order by statement to always move the blog posts page to the top.
				$orderBy = "case when `p`.`ID` = $blogPageId then 0 else 1 end, $orderBy";
			}
		}

		$query->orderByRaw( $orderBy );
		$query = $this->filterPostQuery( $query, $postTypes );

		// Return the total if we are just counting the posts.
		if ( ! empty( $additionalArgs['count'] ) ) {
			return (int) $query->run( true, 'var' )
				->result();
		}

		$posts = $query->run()
			->result();

		// Convert ID from string to int.
		foreach ( $posts as $post ) {
			$post->ID = (int) $post->ID;
		}

		return $this->filterPosts( $posts );
	}

	/**
	 * Filters the post query.
	 *
	 * @since 4.1.4
	 *
	 * @param  \AIOSEO\Plugin\Common\Utils\Database $query    The query.
	 * @param  string                               $postType The post type.
	 * @return \AIOSEO\Plugin\Common\Utils\Database           The filtered query.
	 */
	private function filterPostQuery( $query, $postType ) {
		switch ( $postType ) {
			case 'product':
				return $this->excludeHiddenProducts( $query );
			default:
				break;
		}

		return $query;
	}

	/**
	 * Adds a condition to the query to exclude hidden WooCommerce products.
	 *
	 * @since 4.1.4
	 *
	 * @param  \AIOSEO\Plugin\Common\Utils\Database $query The query.
	 * @return \AIOSEO\Plugin\Common\Utils\Database        The filtered query.
	 */
	private function excludeHiddenProducts( $query ) {
		if (
			! aioseo()->helpers->isWooCommerceActive() ||
			! apply_filters( 'aioseo_sitemap_woocommerce_exclude_hidden_products', true )
		) {
			return $query;
		}

		static $hiddenProductIds = null;
		if ( null === $hiddenProductIds ) {
			$tempDb         = new CommonUtils\Database();
			$hiddenProducts = $tempDb->start( 'term_relationships as tr' )
				->select( 'tr.object_id' )
				->join( 'term_taxonomy as tt', 'tr.term_taxonomy_id = tt.term_taxonomy_id' )
				->join( 'terms as t', 'tt.term_id = t.term_id' )
				->where( 't.name', 'exclude-from-catalog' )
				->run()
				->result();

			if ( empty( $hiddenProducts ) ) {
				return $query;
			}

			$hiddenProductIds = [];
			foreach ( $hiddenProducts as $hiddenProduct ) {
				$hiddenProductIds[] = (int) $hiddenProduct->object_id;
			}
			$hiddenProductIds = esc_sql( implode( ', ', $hiddenProductIds ) );
		}

		$query->whereRaw( "p.ID NOT IN ( $hiddenProductIds )" );

		return $query;
	}

	/**
	 * Filters the queried posts.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $posts          The posts.
	 * @return array $remainingPosts The remaining posts.
	 */
	public function filterPosts( $posts ) {
		$remainingPosts = [];
		foreach ( $posts as $post ) {
			switch ( $post->post_type ) {
				case 'attachment':
					if ( ! $this->isInvalidAttachment( $post ) ) {
						$remainingPosts[] = $post;
					}
					break;
				default:
					$remainingPosts[] = $post;
					break;
			}
		}

		return $remainingPosts;
	}

	/**
	 * Excludes attachments if their post parent isn't published or parent post type isn't registered anymore.
	 *
	 * @since 4.0.0
	 *
	 * @param  Object  $post The post.
	 * @return boolean       Whether the attachment is invalid.
	 */
	private function isInvalidAttachment( $post ) {
		if ( empty( $post->post_parent ) ) {
			return false;
		}

		$parent = get_post( $post->post_parent );
		if ( ! is_object( $parent ) ) {
			return false;
		}

		if (
			'publish' !== $parent->post_status ||
			! in_array( $parent->post_type, get_post_types(), true ) ||
			$parent->post_password
		) {
			return true;
		}

		return false;
	}

	/**
	 * Returns all eligible sitemap entries for a given taxonomy.
	 *
	 * @since 4.0.0
	 *
	 * @param  string           $taxonomy       The taxonomy.
	 * @param  array            $additionalArgs Any additional arguments for the term query.
	 * @return array[object|int]                The term objects or the term count.
	 */
	public function terms( $taxonomy, $additionalArgs = [] ) {
		// Set defaults.
		$fields  = 't.term_id';
		$offset  = aioseo()->sitemap->offset;

		// Include term name for llms sitemap type
		if ( 'llms' === aioseo()->sitemap->type ) {
			$fields .= ', t.name';
		}

		// Override defaults if passed as additional arg.
		foreach ( $additionalArgs as $name => $value ) {
			$$name = esc_sql( $value );
			if ( 'root' === $name && $value ) {
				$fields = 't.term_id, tt.count';
			}
			if ( 'count' === $name && $value ) {
				$fields = 'count(t.term_id) as total';
			}
		}

		$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
		$termTaxonomyTable      = aioseo()->core->db->db->prefix . 'term_taxonomy';

		// Include all terms that have assigned posts or whose children have assigned posts.
		$query = aioseo()->core->db
			->start( aioseo()->core->db->db->terms . ' as t', true )
			->select( $fields )
			->leftJoin( 'term_taxonomy as tt', '`tt`.`term_id` = `t`.`term_id`' )
			->whereRaw( "
			( `t`.`term_id` IN
				(
					SELECT `tt`.`term_id`
					FROM `$termTaxonomyTable` as tt
					WHERE `tt`.`taxonomy` = '$taxonomy'
					AND 
						(
							`tt`.`count` > 0 OR
							EXISTS (
								SELECT 1
								FROM `$termTaxonomyTable` as tt2
								WHERE `tt2`.`parent` = `tt`.`term_id` 
								AND `tt2`.`count` > 0
							)
						)
				)
			)" );

		$excludedTerms = aioseo()->sitemap->helpers->excludedTerms();
		if ( $excludedTerms ) {
			$query->whereRaw("
				( `t`.`term_id` NOT IN
					(
						SELECT `tr`.`term_taxonomy_id`
						FROM `$termRelationshipsTable` as tr
						WHERE `tr`.`term_taxonomy_id` IN ( $excludedTerms )
					)
				)" );
		}

		if (
			aioseo()->sitemap->indexes &&
			empty( $additionalArgs['root'] ) &&
			empty( $additionalArgs['count'] )
		) {
			$query->limit( aioseo()->sitemap->linksPerIndex, $offset );
		}

		// Return the total if we are just counting the terms.
		if ( ! empty( $additionalArgs['count'] ) ) {
			return (int) $query->run( true, 'var' )
				->result();
		}

		$terms = $query->orderBy( 't.term_id ASC' )
			->run()
			->result();

		foreach ( $terms as $term ) {
			// Convert ID from string to int.
			$term->term_id = (int) $term->term_id;
			// Add taxonomy name to object manually instead of querying it to prevent redundant join.
			$term->taxonomy = $taxonomy;
		}

		return $terms;
	}

	/**
	 * Wipes all data and forces the plugin to rescan the site for images.
	 *
	 * @since 4.0.13
	 *
	 * @return void
	 */
	public function resetImages() {
		aioseo()->core->db
			->update( 'aioseo_posts' )
			->set(
				[
					'images'          => null,
					'image_scan_date' => null
				]
			)
			->run();
	}
}Common/Sitemap/RequestParser.php000064400000015755151536241200012730 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

/**
 * Parses the current request and checks whether we need to serve a sitemap or a stylesheet.
 *
 * @since 4.2.1
 */
class RequestParser {
	/**
	 * The cleaned slug of the current request.
	 *
	 * @since 4.2.1
	 *
	 * @var string
	 */
	public $slug;

	/**
	 * Whether we've checked if the page needs to be redirected.
	 *
	 * @since 4.2.3
	 *
	 * @var bool
	 */
	protected $checkedForRedirects = false;

	/**
	 * CLass constructor.
	 *
	 * @since 4.2.1
	 */
	public function __construct() {
		if ( is_admin() ) {
			return;
		}

		add_action( 'parse_request', [ $this, 'checkRequest' ] );
	}

	/**
	 * Checks whether we need to serve a sitemap or related stylesheet.
	 *
	 * @since 4.2.1
	 *
	 * @param  \WP  $wp The main WordPress environment instance.
	 * @return void
	 */
	public function checkRequest( $wp ) {
		$this->slug = $wp->request ?? aioseo()->helpers->cleanSlug( $wp->request );
		if ( ! $this->slug && isset( $_SERVER['REQUEST_URI'] ) ) {
			// We must fallback to the REQUEST URI in case the site uses plain permalinks.
			$this->slug = aioseo()->helpers->cleanSlug( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) );
		}

		if ( ! $this->slug ) {
			return;
		}

		// Check if we need to remove the trailing slash or redirect another sitemap URL like "wp-sitemap.xml".
		$this->maybeRedirect();

		$this->checkForXsl();

		if ( aioseo()->options->sitemap->general->enable ) {
			$this->checkForGeneralSitemap();
		}

		if ( aioseo()->options->sitemap->rss->enable ) {
			$this->checkForRssSitemap();
		}
	}

	/**
	 * Checks whether the general XML sitemap needs to be served.
	 *
	 * @since 4.2.1
	 *
	 * @return void
	 */
	private function checkForGeneralSitemap() {
		$fileName       = aioseo()->sitemap->helpers->filename( 'general' );
		$indexesEnabled = aioseo()->options->sitemap->general->indexes;

		if ( ! $indexesEnabled ) {
			// If indexes are disabled, check for the root index.
			if ( preg_match( "/^{$fileName}\.xml(\.gz)?$/i", (string) $this->slug, $match ) ) {
				$this->setContext( 'general', $fileName );
				aioseo()->sitemap->generate();
			}

			return;
		}

		// First, check for the root index.
		if ( preg_match( "/^{$fileName}\.xml(\.gz)?$/i", (string) $this->slug, $match ) ) {
			$this->setContext( 'general', $fileName );
			aioseo()->sitemap->generate();

			return;
		}

		if (
			// Now, check for the other indexes.
			preg_match( "/^(?P<objectName>.+)-{$fileName}\.xml(\.gz)?$/i", (string) $this->slug, $match ) ||
			preg_match( "/^(?P<objectName>.+)-{$fileName}(?P<pageNumber>\d+)\.xml(\.gz)?$/i", (string) $this->slug, $match )
		) {
			$pageNumber = ! empty( $match['pageNumber'] ) ? $match['pageNumber'] : 0;
			$this->setContext( 'general', $fileName, $match['objectName'], $pageNumber );
			aioseo()->sitemap->generate();
		}
	}

	/**
	 * Checks whether the RSS sitemap needs to be served.
	 *
	 * @since 4.2.1
	 *
	 * @return void
	 */
	private function checkForRssSitemap() {
		if ( ! preg_match( '/^sitemap(\.latest)?\.rss$/i', (string) $this->slug, $match ) ) {
			return;
		}

		$this->setContext( 'rss' );
		aioseo()->sitemap->generate();
	}

	/**
	 * Checks if we need to serve a stylesheet.
	 *
	 * @since 4.2.1
	 *
	 * @return void
	 */
	protected function checkForXsl() {
		// Trim off the URL params.
		$newSlug = preg_replace( '/\?.*$/', '', (string) $this->slug );
		if ( preg_match( '/^default-sitemap\.xsl$/i', (string) $newSlug ) ) {
			aioseo()->sitemap->xsl->generate();
		}
	}

	/**
	 * Sets the context for the requested sitemap.
	 *
	 * @since 4.2.1
	 *
	 * @param  string     $type       The sitemap type (e.g. "general" or "rss").
	 * @param  string     $fileName   The sitemap filename.
	 * @param  string     $indexName  The index name ("root" or an object name like "post", "page", "post_tag", etc.).
	 * @param  int        $pageNumber The index number.
	 * @return void|never
	 */
	public function setContext( $type, $fileName = 'sitemap', $indexName = 'root', $pageNumber = 0 ) {
		$indexesEnabled = aioseo()->options->sitemap->{$type}->indexes;

		aioseo()->sitemap->type          = $type;
		aioseo()->sitemap->filename      = $fileName;
		aioseo()->sitemap->indexes       = $indexesEnabled;
		aioseo()->sitemap->indexName     = $indexName;
		aioseo()->sitemap->linksPerIndex = aioseo()->options->sitemap->{$type}->linksPerIndex <= 50000 ? aioseo()->options->sitemap->{$type}->linksPerIndex : 50000;
		aioseo()->sitemap->pageNumber    = $pageNumber >= 1 ? $pageNumber - 1 : 0;
		aioseo()->sitemap->offset        = aioseo()->sitemap->linksPerIndex * aioseo()->sitemap->pageNumber;
		aioseo()->sitemap->isStatic      = false;
	}

	/**
	 * Redirects or alters the current request if:
	 * 1. The request includes our deprecated "aiosp_sitemap_path" URL param.
	 * 2. The request is for one of our sitemaps, but has a trailing slash.
	 * 3. The request is for the first index of a type, but has a page number.
	 * 4. The request is for a sitemap from WordPress Core/other plugin.
	 *
	 * @since 4.2.1
	 */
	protected function maybeRedirect() {
		if ( $this->checkedForRedirects ) {
			return;
		}

		$requestUri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
		if ( ! $requestUri ) {
			return;
		}

		$this->checkedForRedirects = true;

		// The request includes our deprecated "aiosp_sitemap_path" URL param.
		if ( preg_match( '/^\/\?aiosp_sitemap_path=root/i', (string) $requestUri ) ) {
			wp_safe_redirect( home_url( 'sitemap.xml' ) );
			exit;
		}

		// The request is for one of our sitemaps, but has a trailing slash.
		if ( preg_match( '/\/(.*sitemap[0-9]*?\.xml(\.gz)?|.*sitemap(\.latest)?\.rss)\/$/i', (string) $requestUri ) ) {
			wp_safe_redirect( home_url() . untrailingslashit( $requestUri ) );
			exit;
		}

		// The request is for the first index of a type, but has a page number.
		if ( preg_match( '/.*sitemap(0|1){1}?\.xml(\.gz)?$/i', (string) $requestUri ) ) {
			$pathWithoutNumber = preg_replace( '/(.*sitemap)(0|1){1}?(\.xml(\.gz)?)$/i', '$1$3', $requestUri );
			wp_safe_redirect( home_url() . $pathWithoutNumber );
			exit;
		}

		// The request is for a sitemap from WordPress Core/other plugin, but the general sitemap is enabled.
		if ( ! aioseo()->options->sitemap->general->enable ) {
			return;
		}

		$sitemapPatterns = [
			'general' => [
				'sitemap\.txt',
				'sitemaps\.xml',
				'sitemap-xml\.xml',
				'sitemap[0-9]+\.xml',
				'sitemap(|[-_\/])?index[0-9]*\.xml',
				'wp-sitemap\.xml',
			],
			'rss'     => [
				'rss[0-9]*\.xml',
			]
		];

		$addonSitemapPatterns = aioseo()->addons->doAddonFunction( 'helpers', 'getOtherSitemapPatterns' );
		if ( ! empty( $addonSitemapPatterns ) ) {
			$sitemapPatterns = array_merge( $sitemapPatterns, $addonSitemapPatterns );
		}

		foreach ( $sitemapPatterns as $type => $patterns ) {
			foreach ( $patterns as $pattern ) {
				if ( preg_match( "/^$pattern$/i", (string) $this->slug ) ) {
					wp_safe_redirect( aioseo()->sitemap->helpers->getUrl( $type ) );
					exit;
				}
			}
		}
	}
}Common/Sitemap/Root.php000064400000043317151536241200011041 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

/**
 * Determines which indexes should appear in the sitemap root index.
 *
 * @since 4.0.0
 */
class Root {
	/**
	 * Returns the indexes for the sitemap root index.
	 *
	 * @since 4.0.0
	 *
	 * @return array The indexes.
	 */
	public function indexes() {
		$indexes = [];
		if ( 'general' !== aioseo()->sitemap->type ) {
			$addonIndexes = aioseo()->addons->doAddonFunction( 'root', 'indexes' );

			foreach ( $addonIndexes as $addonIndex ) {
				if ( $addonIndex ) {
					return $addonIndex;
				}
			}

			return $indexes;
		}

		$filename   = aioseo()->sitemap->filename;
		$postTypes  = aioseo()->sitemap->helpers->includedPostTypes();
		$taxonomies = aioseo()->sitemap->helpers->includedTaxonomies();

		$indexes = array_merge( $indexes, $this->getAdditionalIndexes() );

		if ( $postTypes ) {
			$postArchives = [];

			foreach ( $postTypes as $postType ) {
				$postIndexes = $this->buildIndexesPostType( $postType );
				$indexes     = array_merge( $indexes, $postIndexes );

				if (
					get_post_type_archive_link( $postType ) &&
					aioseo()->dynamicOptions->noConflict()->searchAppearance->archives->has( $postType ) &&
					(
						aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->default ||
						! aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->noindex
					)
				) {
					$lastModifiedPostTime = aioseo()->sitemap->helpers->lastModifiedPostTime( $postType );
					if ( $lastModifiedPostTime ) {
						$postArchives[ $postType ] = $lastModifiedPostTime;
					}
				}
			}

			if ( ! empty( $postArchives ) ) {
				usort( $postArchives, function( $date1, $date2 ) {
					return $date1 < $date2 ? 1 : 0;
				} );

				$indexes[] = [
					'loc'     => aioseo()->helpers->localizedUrl( "/post-archive-$filename.xml" ),
					'lastmod' => $postArchives[0],
					'count'   => count( $postArchives )
				];
			}
		}

		if ( $taxonomies ) {
			foreach ( $taxonomies as $taxonomy ) {
				$indexes = array_merge( $indexes, $this->buildIndexesTaxonomy( $taxonomy ) );
			}
		}

		$postsTable = aioseo()->core->db->db->posts;
		if (
			aioseo()->sitemap->helpers->lastModifiedPost() &&
			aioseo()->options->sitemap->general->author &&
			aioseo()->options->searchAppearance->archives->author->show &&
			(
				aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->default ||
				! aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->noindex
			) &&
			(
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default ||
				! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindex
			)
		) {
			$usersTable        = aioseo()->core->db->db->users; // We get the table name from WPDB since multisites share the same table.
			$authorPostTypes   = aioseo()->sitemap->helpers->getAuthorPostTypes();
			$implodedPostTypes = aioseo()->helpers->implodeWhereIn( $authorPostTypes, true );
			$result            = aioseo()->core->db->execute(
				"SELECT count(*) as amountOfAuthors FROM
				(
					SELECT u.ID FROM {$usersTable} as u
					INNER JOIN {$postsTable} as p ON u.ID = p.post_author
					WHERE p.post_status = 'publish' AND p.post_type IN ( {$implodedPostTypes} )
					GROUP BY u.ID
				) as x",
				true
			)->result();

			if ( ! empty( $result[0]->amountOfAuthors ) ) {
				$indexes = array_merge( $indexes, $this->buildAuthorIndexes( (int) $result[0]->amountOfAuthors ) );
			}
		}

		if (
			aioseo()->sitemap->helpers->lastModifiedPost() &&
			aioseo()->options->sitemap->general->date &&
			aioseo()->options->searchAppearance->archives->date->show &&
			(
				aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->default ||
				! aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->noindex
			) &&
			(
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default ||
				! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindex
			)
		) {
			$result = aioseo()->core->db->execute(
				"SELECT count(*) as amountOfUrls FROM (
					SELECT post_date
					FROM {$postsTable}
					WHERE post_type = 'post' AND post_status = 'publish'
					GROUP BY
						YEAR(post_date),
						MONTH(post_date)
					LIMIT 50000
				) as dates",
				true
			)->result();

			$indexes[] = $this->buildIndex( 'date', $result[0]->amountOfUrls );
		}

		if (
			aioseo()->helpers->isWooCommerceActive() &&
			in_array( 'product_attributes', aioseo()->sitemap->helpers->includedTaxonomies(), true )
		) {
			$productAttributes = aioseo()->sitemap->content->productAttributes( true );

			if ( ! empty( $productAttributes ) ) {
				$indexes[] = $this->buildIndex( 'product_attributes', $productAttributes );
			}
		}

		if ( isset( aioseo()->standalone->buddyPress->sitemap ) ) {
			$indexes = array_merge( $indexes, aioseo()->standalone->buddyPress->sitemap->indexes() );
		}

		return apply_filters( 'aioseo_sitemap_indexes', array_filter( $indexes ) );
	}

	/**
	 * Returns the additional page indexes.
	 *
	 * @since 4.2.1
	 *
	 * @return array
	 */
	private function getAdditionalIndexes() {
		$additionalPages = [];
		if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
			$additionalPages = array_map( 'json_decode', aioseo()->options->sitemap->general->additionalPages->pages );
			$additionalPages = array_filter( $additionalPages, function( $additionalPage ) {
				return ! empty( $additionalPage->url );
			} );
		}

		$entries = [];
		foreach ( $additionalPages as $additionalPage ) {
			$entries[] = [
				'loc'        => $additionalPage->url,
				'lastmod'    => aioseo()->sitemap->helpers->lastModifiedAdditionalPage( $additionalPage ),
				'changefreq' => $additionalPage->frequency->value,
				'priority'   => $additionalPage->priority->value,
				'isTimezone' => true
			];
		}

		if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
			$entries = apply_filters( 'aioseo_sitemap_additional_pages', $entries );
		}

		$postTypes             = aioseo()->sitemap->helpers->includedPostTypes();
		$shouldIncludeHomepage = 'posts' === get_option( 'show_on_front' ) || ! in_array( 'page', $postTypes, true );
		if ( ! $shouldIncludeHomepage && ! count( $entries ) ) {
			return [];
		}

		$indexes = $this->buildAdditionalIndexes( $entries, $shouldIncludeHomepage );

		return $indexes;
	}

	/**
	 * Builds a given index.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $indexName    The index name.
	 * @param  integer $amountOfUrls The amount of URLs in the index.
	 * @return array                 The index.
	 */
	private function buildIndex( $indexName, $amountOfUrls ) {
		$filename = aioseo()->sitemap->filename;

		return [
			'loc'     => aioseo()->helpers->localizedUrl( "/$indexName-$filename.xml" ),
			'lastmod' => aioseo()->sitemap->helpers->lastModifiedPostTime(),
			'count'   => $amountOfUrls
		];
	}

	/**
	 * Builds the additional pages index.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $entries               The additional pages.
	 * @param  bool  $shouldIncludeHomepage Whether or not the homepage should be included.
	 * @return array                        The indexes.
	 */
	private function buildAdditionalIndexes( $entries, $shouldIncludeHomepage ) {
		if ( $shouldIncludeHomepage ) {
			$entries[] = [
				'loc'     => home_url(),
				'lastmod' => aioseo()->sitemap->helpers->lastModifiedPostTime()
			];
		}

		if ( empty( $entries ) ) {
			return [];
		}

		$filename  = aioseo()->sitemap->filename;
		$chunks    = aioseo()->sitemap->helpers->chunkEntries( $entries );

		$indexes = [];
		for ( $i = 0; $i < count( $chunks ); $i++ ) {
			$chunk       = array_values( $chunks[ $i ] );
			$indexNumber = 1 < count( $chunks ) ? $i + 1 : '';

			$index = [
				'loc'     => aioseo()->helpers->localizedUrl( "/addl-$filename$indexNumber.xml" ),
				'lastmod' => ! empty( $chunk[0]['lastmod'] ) ? aioseo()->helpers->dateTimeToIso8601( $chunk[0]['lastmod'] ) : '',
				'count'   => count( $chunks[ $i ] )
			];

			$indexes[] = $index;
		}

		return $indexes;
	}

	/**
	 * Builds the author archive indexes.
	 *
	 * @since 4.3.1
	 *
	 * @param  integer $amountOfAuthors The amount of author archives.
	 * @return array                    The indexes.
	 */
	private function buildAuthorIndexes( $amountOfAuthors ) {
		if ( ! $amountOfAuthors ) {
			return [];
		}

		$postTypes = aioseo()->sitemap->helpers->includedPostTypes();
		$filename  = aioseo()->sitemap->filename;
		$chunks    = $amountOfAuthors / aioseo()->sitemap->linksPerIndex;
		if ( $chunks < 1 ) {
			$chunks = 1;
		}

		$indexes = [];
		for ( $i = 0; $i < $chunks; $i++ ) {
			$indexNumber = 1 < $chunks ? $i + 1 : '';

			$usersTableName = aioseo()->core->db->db->users; // We get the table name from WPDB since multisites share the same table.
			$lastModified   = aioseo()->core->db->start( "$usersTableName as u", true )
				->select( 'MAX(p.post_modified_gmt) as lastModified' )
				->join( 'posts as p', 'u.ID = p.post_author' )
				->where( 'p.post_status', 'publish' )
				->whereIn( 'p.post_type', $postTypes )
				->groupBy( 'u.ID' )
				->orderBy( 'lastModified DESC' )
				->limit( aioseo()->sitemap->linksPerIndex, $i * aioseo()->sitemap->linksPerIndex )
				->run()
				->result();

			$lastModified = ! empty( $lastModified[0]->lastModified ) ? aioseo()->helpers->dateTimeToIso8601( $lastModified[0]->lastModified ) : '';

			$index = [
				'loc'     => aioseo()->helpers->localizedUrl( "/author-$filename$indexNumber.xml" ),
				'lastmod' => $lastModified,
				'count'   => $i + 1 === $chunks ? $amountOfAuthors % aioseo()->sitemap->linksPerIndex : aioseo()->sitemap->linksPerIndex
			];

			$indexes[] = $index;
		}

		return $indexes;
	}

	/**
	 * Builds indexes for all eligible posts of a given post type.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $postType The post type.
	 * @return array            The indexes.
	 */
	private function buildIndexesPostType( $postType ) {
		$prefix                 = aioseo()->core->db->prefix;
		$postsTable             = $prefix . 'posts';
		$aioseoPostsTable       = $prefix . 'aioseo_posts';
		$termRelationshipsTable = $prefix . 'term_relationships';
		$termTaxonomyTable      = $prefix . 'term_taxonomy';
		$termsTable             = $prefix . 'terms';
		$linksPerIndex          = aioseo()->sitemap->linksPerIndex;

		if ( 'attachment' === $postType && 'disabled' !== aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls ) {
			return [];
		}

		$excludedPostIds = [];
		$excludedTermIds = aioseo()->sitemap->helpers->excludedTerms();
		if ( ! empty( $excludedTermIds ) ) {
			$excludedTermIds = explode( ', ', $excludedTermIds );
			$excludedPostIds = aioseo()->core->db->start( 'term_relationships' )
				->select( 'object_id' )
				->whereIn( 'term_taxonomy_id', $excludedTermIds )
				->run()
				->result();

			$excludedPostIds = array_map( function( $post ) {
				return $post->object_id;
			}, $excludedPostIds );
		}

		if ( 'page' === $postType ) {
			$isStaticHomepage = 'page' === get_option( 'show_on_front' );
			if ( $isStaticHomepage ) {
				$blogPageId = (int) get_option( 'page_for_posts' );
				$excludedPostIds[] = $blogPageId;
			}
		}

		$whereClause         = '';
		$excludedPostsString = aioseo()->sitemap->helpers->excludedPosts();
		if ( ! empty( $excludedPostsString ) ) {
			$excludedPostIds = array_merge( $excludedPostIds, explode( ', ', $excludedPostsString ) );
		}

		if ( ! empty( $excludedPostIds ) ) {
			$implodedPostIds = aioseo()->helpers->implodeWhereIn( $excludedPostIds, true );
			$whereClause     = "AND p.ID NOT IN ( $implodedPostIds )";
		}

		if (
			apply_filters( 'aioseo_sitemap_woocommerce_exclude_hidden_products', true ) &&
			aioseo()->helpers->isWooCommerceActive() &&
			'product' === $postType
		) {
			$whereClause .= " AND p.ID NOT IN (
				SELECT CONVERT(tr.object_id, unsigned) AS object_id
				FROM {$termRelationshipsTable} AS tr
				JOIN {$termTaxonomyTable} AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
				JOIN {$termsTable} AS t ON tt.term_id = t.term_id
				WHERE t.name = 'exclude-from-catalog'
			)";
		}

		// Include the blog page in the posts post type unless manually excluded.
		$blogPageId = (int) get_option( 'page_for_posts' );
		if (
			$blogPageId &&
			! in_array( $blogPageId, $excludedPostIds, true ) &&
			'post' === $postType
		) {
			$whereClause .= " OR `p`.`ID` = $blogPageId ";
		}

		$posts = aioseo()->core->db->execute(
			aioseo()->core->db->db->prepare(
				"SELECT ID, post_modified_gmt
				FROM (
					SELECT @row := @row + 1 AS rownum, ID, post_modified_gmt
					FROM (
						SELECT p.ID, ap.priority, p.post_modified_gmt
						FROM {$postsTable} AS p
						LEFT JOIN {$aioseoPostsTable} AS ap ON p.ID = ap.post_id
						WHERE p.post_status = %s
							AND p.post_type = %s
							AND p.post_password = ''
							AND (ap.robots_noindex IS NULL OR ap.robots_default = 1 OR ap.robots_noindex = 0)
							{$whereClause}
						ORDER BY ap.priority DESC, p.post_modified_gmt DESC
					) AS x
					CROSS JOIN (SELECT @row := 0) AS vars
					ORDER BY post_modified_gmt DESC
				) AS y
				WHERE rownum = 1 OR rownum % %d = 1;",
				[
					'attachment' === $postType ? 'inherit' : 'publish',
					$postType,
					$linksPerIndex
				]
			),
			true
		)->result();

		$totalPosts = aioseo()->core->db->execute(
			aioseo()->core->db->db->prepare(
				"SELECT COUNT(*) as count
				FROM {$postsTable} as p
				LEFT JOIN {$aioseoPostsTable} as ap ON p.ID = ap.post_id
				WHERE p.post_status = %s
					AND p.post_type = %s
					AND p.post_password = ''
					AND (ap.robots_noindex IS NULL OR ap.robots_default = 1 OR ap.robots_noindex = 0)
					{$whereClause}
				",
				[
					'attachment' === $postType ? 'inherit' : 'publish',
					$postType
				]
			),
			true
		)->result();

		if ( $posts ) {
			$indexes   = [];
			$filename  = aioseo()->sitemap->filename;
			$postCount = count( $posts );
			for ( $i = 0; $i < $postCount; $i++ ) {
				$indexNumber = 0 !== $i && 1 < $postCount ? $i + 1 : '';

				$indexes[] = [
					'loc'     => aioseo()->helpers->localizedUrl( "/$postType-$filename$indexNumber.xml" ),
					'lastmod' => aioseo()->helpers->dateTimeToIso8601( $posts[ $i ]->post_modified_gmt ),
					'count'   => $linksPerIndex
				];
			}

			// We need to update the count of the last index since it won't necessarily be the same as the links per index.
			$indexes[ count( $indexes ) - 1 ]['count'] = $totalPosts[0]->count - ( $linksPerIndex * ( $postCount - 1 ) );

			return $indexes;
		}

		if ( ! $posts ) {
			$addonsPosts = aioseo()->addons->doAddonFunction( 'root', 'buildIndexesPostType', [ $postType ] );

			foreach ( $addonsPosts as $addonPosts ) {
				if ( $addonPosts ) {
					$posts = $addonPosts;
					break;
				}
			}
		}

		if ( ! $posts ) {
			return [];
		}

		return $this->buildIndexes( $postType, $posts );
	}

	/**
	 * Builds indexes for all eligible terms of a given taxonomy.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $taxonomy The taxonomy.
	 * @return array            The indexes.
	 */
	private function buildIndexesTaxonomy( $taxonomy ) {
		$terms = aioseo()->sitemap->content->terms( $taxonomy, [ 'root' => true ] );

		if ( ! $terms ) {
			$addonsTerms = aioseo()->addons->doAddonFunction( 'root', 'buildIndexesTaxonomy', [ $taxonomy ] );

			foreach ( $addonsTerms as $addonTerms ) {
				if ( $addonTerms ) {
					$terms = $addonTerms;
					break;
				}
			}
		}

		if ( ! $terms ) {
			return [];
		}

		return $this->buildIndexes( $taxonomy, $terms );
	}

	/**
	 * Builds indexes for a given type.
	 *
	 * Acts as a helper function for buildIndexesPostTypes() and buildIndexesTaxonomies().
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name    The name of the object parent.
	 * @param  array  $entries The sitemap entries.
	 * @return array           The indexes.
	 */
	public function buildIndexes( $name, $entries ) {
		$filename = aioseo()->sitemap->filename;
		$chunks   = aioseo()->sitemap->helpers->chunkEntries( $entries );
		$indexes  = [];
		for ( $i = 0; $i < count( $chunks ); $i++ ) {
			$chunk       = array_values( $chunks[ $i ] );
			$indexNumber = 0 !== $i && 1 < count( $chunks ) ? $i + 1 : '';

			$index = [
				'loc'   => aioseo()->helpers->localizedUrl( "/$name-$filename$indexNumber.xml" ),
				'count' => count( $chunks[ $i ] )
			];

			if ( isset( $entries[0]->ID ) ) {
				$ids = array_map( function( $post ) {
					return $post->ID;
				}, $chunk );
				$ids = implode( "', '", $ids );

				$lastModified = null;
				if ( ! apply_filters( 'aioseo_sitemap_lastmod_disable', false ) ) {
					$lastModified = aioseo()->core->db
						->start( aioseo()->core->db->db->posts . ' as p', true )
						->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
						->whereRaw( "( `p`.`ID` IN ( '$ids' ) )" )
						->run()
						->result();
				}

				if ( ! empty( $lastModified[0]->last_modified ) ) {
					$index['lastmod'] = aioseo()->helpers->dateTimeToIso8601( $lastModified[0]->last_modified );
				}
				$indexes[] = $index;
				continue;
			}

			$termIds = [];
			foreach ( $chunk as $term ) {
				$termIds[] = $term->term_id;
			}
			$termIds = implode( "', '", $termIds );

			$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';

			$lastModified = null;
			if ( ! apply_filters( 'aioseo_sitemap_lastmod_disable', false ) ) {
				$lastModified = aioseo()->core->db
					->start( aioseo()->core->db->db->posts . ' as p', true )
					->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
					->whereRaw( "
					( `p`.`ID` IN
						(
							SELECT CONVERT(`tr`.`object_id`, unsigned)
							FROM `$termRelationshipsTable` as tr
							WHERE `tr`.`term_taxonomy_id` IN ( '$termIds' )
						)
					)" )
					->run()
					->result();
			}

			if ( ! empty( $lastModified[0]->last_modified ) ) {
				$index['lastmod'] = aioseo()->helpers->dateTimeToIso8601( $lastModified[0]->last_modified );
			}
			$indexes[] = $index;
		}

		return $indexes;
	}
}Common/Sitemap/Sitemap.php000064400000025377151536241200011526 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Handles our sitemaps.
 *
 * @since 4.0.0
 */
class Sitemap extends SitemapAbstract {
	/**
	 * The sitemap filename.
	 *
	 * @since 4.4.2
	 *
	 * @var string
	 */
	public $filename = '';

	/**
	 * Whether the sitemap indexes are enabled.
	 *
	 * @since 4.4.2
	 *
	 * @var bool
	 */
	public $indexes = false;

	/**
	 * The sitemap index name.
	 *
	 * @since 4.4.2
	 *
	 * @var string
	 */
	public $indexName = '';

	/**
	 * The number of links per index.
	 *
	 * @since 4.4.2
	 *
	 * @var int
	 */
	public $linksPerIndex = 1000;

	/**
	 * The current page number.
	 *
	 * @since 4.4.2
	 *
	 * @var int
	 */
	public $pageNumber = 0;

	/**
	 * The entries' offset.
	 *
	 * @since 4.4.2
	 *
	 * @var int
	 */
	public $offset = 0;

	/**
	 * Whether the sitemap is static.
	 *
	 * @since 4.4.2
	 *
	 * @var bool
	 */
	public $isStatic = false;

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->content       = new Content();
		$this->root          = new Root();
		$this->query         = new Query();
		$this->file          = new File();
		$this->image         = new Image\Image();
		$this->priority      = new Priority();
		$this->output        = new Output();
		$this->helpers       = new Helpers();
		$this->requestParser = new RequestParser();
		$this->xsl           = new Xsl();

		new Localization();

		$this->disableWpSitemap();
	}

	/**
	 * Adds our hooks.
	 * Note: This runs init and is triggered in the main AIOSEO class.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function init() {
		add_action( 'aioseo_static_sitemap_regeneration', [ $this, 'regenerateStaticSitemap' ] );

		// Check if static files need to be updated.
		add_action( 'wp_insert_post', [ $this, 'regenerateOnUpdate' ] );
		add_action( 'edited_term', [ $this, 'regenerateStaticSitemap' ] );

		add_action( 'admin_init', [ $this, 'detectStatic' ] );

		$this->maybeAddHtaccessRewriteRules();
	}

	/**
	 * Disables the WP Core sitemap if our general sitemap is enabled.
	 *
	 * @since 4.2.1
	 *
	 * @return void
	 */
	protected function disableWpSitemap() {
		if ( ! aioseo()->options->sitemap->general->enable ) {
			return;
		}

		remove_action( 'init', 'wp_sitemaps_get_server' );
		add_filter( 'wp_sitemaps_enabled', '__return_false' );
	}

	/**
	 * Check if the .htaccess rewrite rules are present if the user is using Apache. If not, add them.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	private function maybeAddHtaccessRewriteRules() {
		if ( ! aioseo()->helpers->isApache() || wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		ob_start();
		aioseo()->templates->getTemplate( 'sitemap/htaccess-rewrite-rules.php' );
		$rewriteRules = ob_get_clean();

		$escapedRewriteRules = aioseo()->helpers->escapeRegex( $rewriteRules );

		$contents = aioseo()->helpers->decodeHtmlEntities( aioseo()->htaccess->getContents() );
		if ( get_option( 'permalink_structure' ) ) {
			if ( preg_match( '/All in One SEO Sitemap Rewrite Rules/i', (string) $contents ) && ! aioseo()->core->cache->get( 'aioseo_sitemap_htaccess_rewrite_rules_remove' ) ) {
				aioseo()->core->cache->update( 'aioseo_sitemap_htaccess_rewrite_rules_remove', time(), HOUR_IN_SECONDS );

				$contents = preg_replace( "/$escapedRewriteRules/i", '', (string) $contents );
				aioseo()->htaccess->saveContents( $contents );
			}

			return;
		}

		if ( preg_match( '/All in One SEO Sitemap Rewrite Rules/i', (string) $contents ) || aioseo()->core->cache->get( 'aioseo_sitemap_htaccess_rewrite_rules_add' ) ) {
			return;
		}

		aioseo()->core->cache->update( 'aioseo_sitemap_htaccess_rewrite_rules_add', time(), HOUR_IN_SECONDS );

		$contents .= $rewriteRules;

		aioseo()->htaccess->saveContents( $contents );
	}

	/**
	 * Checks if static sitemap files prevent dynamic sitemap generation.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function detectStatic() {
		$isGeneralSitemapStatic = aioseo()->options->sitemap->general->advancedSettings->enable &&
			in_array( 'staticSitemap', aioseo()->internalOptions->internal->deprecatedOptions, true ) &&
			! aioseo()->options->deprecated->sitemap->general->advancedSettings->dynamic;

		if ( $isGeneralSitemapStatic ) {
			Models\Notification::deleteNotificationByName( 'sitemap-static-files' );

			return;
		}

		require_once ABSPATH . 'wp-admin/includes/file.php';
		$files = list_files( get_home_path(), 1 );
		if ( ! count( $files ) ) {
			return;
		}

		$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;
					}
				}
			}
		}

		$this->maybeShowStaticSitemapNotification( $detectedFiles );
	}

	/**
	 * If there are files, show a notice, otherwise delete it.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $detectedFiles An array of detected files.
	 * @return void
	 */
	protected function maybeShowStaticSitemapNotification( $detectedFiles ) {
		if ( ! count( $detectedFiles ) ) {
			Models\Notification::deleteNotificationByName( 'sitemap-static-files' );

			return;
		}

		$notification = Models\Notification::getNotificationByName( 'sitemap-static-files' );
		if ( $notification->notification_name ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'sitemap-static-files',
			'title'             => __( 'Static sitemap files detected', 'all-in-one-seo-pack' ),
			'content'           => sprintf(
				// Translators: 1 - The plugin short name ("AIOSEO"), 2 - Same as previous.
				__( '%1$s has detected static sitemap files in the root folder of your WordPress installation.
				As long as these files are present, %2$s is not able to dynamically generate your sitemap.', 'all-in-one-seo-pack' ),
				AIOSEO_PLUGIN_SHORT_NAME,
				AIOSEO_PLUGIN_SHORT_NAME
			),
			'type'              => 'error',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Delete Static Files', 'all-in-one-seo-pack' ),
			'button1_action'    => 'http://action#sitemap/delete-static-files',
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Regenerates the static sitemap files when a post is updated.
	 *
	 * @since 4.0.0
	 *
	 * @param  integer $postId The post ID.
	 * @return void
	 */
	public function regenerateOnUpdate( $postId ) {
		if ( aioseo()->helpers->isValidPost( $postId ) ) {
			$this->scheduleRegeneration();
		}
	}

	/**
	 * Schedules an action to regenerate the static sitemap files.
	 *
	 * @since 4.0.5
	 *
	 * @return void
	 */
	public function scheduleRegeneration() {
		try {
			if (
				! aioseo()->options->deprecated->sitemap->general->advancedSettings->dynamic &&
				! as_next_scheduled_action( 'aioseo_static_sitemap_regeneration' )
			) {
				as_schedule_single_action( time() + 60, 'aioseo_static_sitemap_regeneration', [], 'aioseo' );
			}
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Regenerates the static sitemap files.
	 *
	 * @since 4.0.5
	 *
	 * @return void
	 */
	public function regenerateStaticSitemap() {
		aioseo()->sitemap->file->generate();
	}

	/**
	 * Generates the requested sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function generate() {
		if ( empty( $this->type ) ) {
			return;
		}

		// This is a hack to prevent WordPress from running it's default stuff during our processing.
		global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$wp_query->is_home = false; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		// This prevents the sitemap from including terms twice when WPML is active.
		if ( class_exists( 'SitePress' ) ) {
			global $sitepress_settings; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			// Before building the sitemap make sure links aren't translated.
			// The setting should not be updated in the DB.
			$sitepress_settings['auto_adjust_ids'] = 0; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		}

		// If requested sitemap should be static and doesn't exist, then generate it.
		// We'll then serve it dynamically for the current request so that we don't serve a blank page.
		$this->doesFileExist();

		$options = aioseo()->options->noConflict();
		if ( ! $options->sitemap->{aioseo()->sitemap->type}->enable ) {
			aioseo()->helpers->notFoundPage();

			return;
		}

		$entries = aioseo()->sitemap->content->get();
		$total   = aioseo()->sitemap->content->getTotal();
		if ( ! $entries ) {
			$addonsEntries = aioseo()->addons->doAddonFunction( 'content', 'get' );
			$addonTotals   = aioseo()->addons->doAddonFunction( 'content', 'getTotal' );
			foreach ( $addonsEntries as $addonSlug => $addonEntries ) {
				if ( ! empty( $addonEntries ) ) {
					$entries = $addonEntries;
					$total   = ! empty( $addonTotals[ $addonSlug ] ) ? $addonTotals[ $addonSlug ] : count( $entries );
					break;
				}
			}
		}

		if ( 0 === $total && empty( $entries ) ) {
			status_header( 404 );
		}

		$this->xsl->saveXslData(
			aioseo()->sitemap->requestParser->slug,
			$entries,
			$total
		);

		$this->headers();
		aioseo()->sitemap->output->output( $entries );
		aioseo()->addons->doAddonFunction( 'output', 'output', [ $entries ] );

		exit;
	}

	/**
	 * Checks if static file should be served and generates it if it doesn't exist.
	 *
	 * This essentially acts as a safety net in case a file doesn't exist yet or has been deleted.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function doesFileExist() {
		aioseo()->addons->doAddonFunction( 'sitemap', 'doesFileExist' );

		if (
			'general' !== $this->type ||
			! aioseo()->options->sitemap->general->advancedSettings->enable ||
			! in_array( 'staticSitemap', aioseo()->internalOptions->internal->deprecatedOptions, true ) ||
			aioseo()->options->sitemap->general->advancedSettings->dynamic
		) {
			return;
		}

		require_once ABSPATH . 'wp-admin/includes/file.php';
		if ( isset( $_SERVER['REQUEST_URI'] ) && ! aioseo()->core->fs->exists( get_home_path() . sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ) {
			$this->scheduleRegeneration();
		}
	}

	/**
	 * Sets the HTTP headers for the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function headers() {
		$charset = aioseo()->helpers->getCharset();
		header( "Content-Type: text/xml; charset=$charset", true );
		header( 'X-Robots-Tag: noindex, follow', true );
	}

	/**
	 * Registers an active sitemap addon and its classes.
	 * NOTE: This is deprecated and only there for users who already were using the previous sitemap addons version.
	 *
	 * @final 4.2.7
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addAddon() {}
}Common/Sitemap/SitemapAbstract.php000064400000004054151536241200013177 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

/**
 * Abstract class holding the class properties of our main AIOSEO class.
 *
 * @since 4.4.3
 */
abstract class SitemapAbstract {
	/**
	 * Content class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Content
	 */
	public $content = null;

	/**
	 * Root class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Root
	 */
	public $root = null;

	/**
	 * Query class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Query
	 */
	public $query = null;

	/**
	 * File class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var File
	 */
	public $file = null;

	/**
	 * Image class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Image\Image
	 */
	public $image = null;

	/**
	 * Priority class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Priority
	 */
	public $priority = null;

	/**
	 * Output class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Output
	 */
	public $output = null;

	/**
	 * Helpers class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Helpers
	 */
	public $helpers = null;

	/**
	 * RequestParser class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var RequestParser
	 */
	public $requestParser = null;

	/**
	 * Xsl class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Xsl
	 */
	public $xsl = null;

	/**
	 * The sitemap type (e.g. "general", "news", "video", "rss", etc.).
	 *
	 * @since 4.2.7
	 *
	 * @var string
	 */
	public $type = '';

	/**
	 * Index name.
	 *
	 * @since 4.4.3
	 *
	 * @var string
	 */
	public $indexName = '';

	/**
	 * Page number.
	 *
	 * @since 4.4.3
	 *
	 * @var int
	 */
	public $pageNumber = 0;

	/**
	 * Page number.
	 *
	 * @since 4.4.3
	 *
	 * @var int
	 */
	public $offset = 0;

	/**
	 * Indexes active.
	 *
	 * @since 4.4.3
	 *
	 * @var bool
	 */
	public $indexes = false;

	/**
	 * Links per index.
	 *
	 * @since 4.4.3
	 *
	 * @var int
	 */
	public $linksPerIndex = PHP_INT_MAX;

	/**
	 * Is static.
	 *
	 * @since 4.4.3
	 *
	 * @var bool
	 */
	public $isStatic = false;

	/**
	 * Filename.
	 *
	 * @since 4.4.3
	 *
	 * @var string
	 */
	public $filename = '';
}Common/Sitemap/Xsl.php000064400000012211151536241200010651 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

/**
 * Serves stylesheets for sitemaps.
 *
 * @since 4.2.1
 */
class Xsl {
	/**
	 * Generates the XSL stylesheet for the current sitemap.
	 *
	 * @since 4.2.1
	 *
	 * @return void
	 */
	public function generate() {
		aioseo()->sitemap->headers();

		$charset     = aioseo()->helpers->getCharset();
		$sitemapUrl  = wp_get_referer();
		$sitemapPath = aioseo()->helpers->getPermalinkPath( $sitemapUrl );

		// Figure out which sitemap we're serving.
		preg_match( '/\/(.*?)-?sitemap([0-9]*)\.xml/', (string) $sitemapPath, $sitemapInfo );
		$sitemapName = ! empty( $sitemapInfo[1] ) ? strtoupper( $sitemapInfo[1] ) : '';

		// Remove everything after ? from sitemapPath to avoid caching issues.
		$sitemapPath = wp_parse_url( $sitemapPath, PHP_URL_PATH ) ?: '';

		// These variables are used in the XSL file.
		// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$linksPerIndex = aioseo()->options->sitemap->general->linksPerIndex;
		$advanced      = aioseo()->options->sitemap->general->advancedSettings->enable;
		$excludeImages = aioseo()->sitemap->helpers->excludeImages();
		$sitemapParams = aioseo()->helpers->getParametersFromUrl( $sitemapUrl );
		$xslParams     = aioseo()->core->cache->get( 'aioseo_sitemap_' . aioseo()->helpers->cleanSlug( $sitemapPath ) );
		// phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable

		if ( ! empty( $sitemapInfo[1] ) ) {
			switch ( $sitemapInfo[1] ) {
				case 'addl':
					$sitemapName = __( 'Additional Pages', 'all-in-one-seo-pack' );
					// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
					$excludeImages = true;
					break;
				case 'post-archive':
					$sitemapName = __( 'Post Archive', 'all-in-one-seo-pack' );
					break;
				case 'bp-activity':
				case 'bp-group':
				case 'bp-member':
					$bpFakePostTypes = aioseo()->standalone->buddyPress->getFakePostTypes();
					$labels          = array_column( wp_list_filter( $bpFakePostTypes, [ 'name' => $sitemapInfo[1] ] ), 'label' );
					$sitemapName     = ! empty( $labels[0] ) ? $labels[0] : $sitemapName;
					break;
				case 'product_attributes':
					$sitemapName = __( 'Product Attributes', 'all-in-one-seo-pack' );
					break;
				default:
					if ( post_type_exists( $sitemapInfo[1] ) ) {
						$postTypeObject = get_post_type_object( $sitemapInfo[1] );
						$sitemapName    = $postTypeObject->labels->singular_name;
					}
					if ( taxonomy_exists( $sitemapInfo[1] ) ) {
						$taxonomyObject = get_taxonomy( $sitemapInfo[1] );
						$sitemapName    = $taxonomyObject->labels->singular_name;
					}
					break;
			}
		}

		$currentPage = ! empty( $sitemapInfo[2] ) ? (int) $sitemapInfo[2] : 1;

		// Translators: 1 - The sitemap name, 2 - The current page.
		$title = sprintf( __( '%1$s Sitemap %2$s', 'all-in-one-seo-pack' ), $sitemapName, $currentPage > 1 ? $currentPage : '' );
		$title = trim( $title );

		echo '<?xml version="1.0" encoding="' . esc_attr( $charset ) . '"?>';
		include_once AIOSEO_DIR . '/app/Common/Views/sitemap/xsl/default.php';
		exit;
	}

	/**
	 * Save the data to use in the XSL.
	 *
	 * @since 4.1.5
	 *
	 * @param  string $fileName The sitemap file name.
	 * @param  array  $entries  The sitemap entries.
	 * @param  int    $total    The total sitemap entries count.
	 * @return void
	 */
	public function saveXslData( $fileName, $entries, $total ) {
		$counts     = [];
		$datetime   = [];
		$dateFormat = get_option( 'date_format' );
		$timeFormat = get_option( 'time_format' );

		$entries = aioseo()->sitemap->helpers->decodeSitemapEntries( $entries );

		foreach ( $entries as $index ) {
			$url = ! empty( $index['guid'] ) ? $index['guid'] : $index['loc'];

			if ( ! empty( $index['count'] ) && aioseo()->options->sitemap->general->linksPerIndex !== (int) $index['count'] ) {
				$counts[ $url ] = $index['count'];
			}

			if ( ! empty( $index['lastmod'] ) || ! empty( $index['publicationDate'] ) || ! empty( $index['pubDate'] ) ) {
				$date             = ! empty( $index['lastmod'] ) ? $index['lastmod'] : ( ! empty( $index['publicationDate'] ) ? $index['publicationDate'] : $index['pubDate'] );
				$isTimezone       = ! empty( $index['isTimezone'] ) && $index['isTimezone'];
				$datetime[ $url ] = [
					'date' => $isTimezone ? date_i18n( $dateFormat, strtotime( $date ) ) : get_date_from_gmt( $date, $dateFormat ),
					'time' => $isTimezone ? date_i18n( $timeFormat, strtotime( $date ) ) : get_date_from_gmt( $date, $timeFormat )
				];
			}
		}

		$data = [
			'counts'     => $counts,
			'datetime'   => $datetime,
			'pagination' => [
				'showing' => count( $entries ),
				'total'   => $total
			]
		];

		// Set a high expiration date so we still have the cache for static sitemaps.
		aioseo()->core->cache->update( 'aioseo_sitemap_' . $fileName, $data, MONTH_IN_SECONDS );
	}

	/**
	 * Retrieve the data to use on the XSL.
	 *
	 * @since 4.2.1
	 *
	 * @param  string $fileName The sitemap file name.
	 * @return array            The XSL data for the given file name.
	 */
	public function getXslData( $fileName ) {
		return aioseo()->core->cache->get( 'aioseo_sitemap_' . $fileName );
	}
}Common/Social/Facebook.php000064400000040211151536241200011425 0ustar00<?php
namespace AIOSEO\Plugin\Common\Social;

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

use AIOSEO\Plugin\Common\Traits;

/**
 * Handles the Open Graph meta.
 *
 * @since 4.0.0
 */
class Facebook {
	use Traits\SocialProfiles;

	/**
	 * Returns the Open Graph image URL.
	 *
	 * @since 4.0.0
	 *
	 * @param  int    $postId The post ID (optional).
	 * @return string         The image URL.
	 */
	public function getImage( $postId = null ) {
		$post = aioseo()->helpers->getPost( $postId );
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			$image = aioseo()->options->social->facebook->homePage->image;
			if ( empty( $image ) ) {
				$image = aioseo()->social->image->getImage( 'facebook', aioseo()->options->social->facebook->general->defaultImageSourcePosts, $post );
			}

			return $image;
		}

		$metaData = aioseo()->meta->metaData->getMetaData( $post );

		$image = '';
		if ( ! empty( $metaData ) ) {
			$imageSource = ! empty( $metaData->og_image_type ) && 'default' !== $metaData->og_image_type
				? $metaData->og_image_type
				: aioseo()->options->social->facebook->general->defaultImageSourcePosts;

			$image = aioseo()->social->image->getImage( 'facebook', $imageSource, $post );
		}

		// Since we could be on an archive page, let's check again for that default image.
		if ( ! $image ) {
			$image = aioseo()->social->image->getImage( 'facebook', 'default' );
		}

		if ( ! $image ) {
			$image = aioseo()->helpers->getSiteLogoUrl();
		}

		// Allow users to control the default image per post type.
		return apply_filters(
			'aioseo_opengraph_default_image',
			$image,
			[
				$post,
				$this->getObjectType()
			]
		);
	}

	/**
	 * Returns the width of the Open Graph image.
	 *
	 * @since 4.0.0
	 *
	 * @return string The image width.
	 */
	public function getImageWidth() {
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			$width = aioseo()->options->social->facebook->homePage->imageWidth;

			return $width ? $width : aioseo()->options->social->facebook->general->defaultImagePostsWidth;
		}

		$metaData = aioseo()->meta->metaData->getMetaData();
		if ( ! empty( $metaData->og_custom_image_width ) ) {
			return $metaData->og_custom_image_width;
		}

		$image = $this->getImage();
		if ( is_array( $image ) ) {
			return $image[1];
		}

		return aioseo()->options->social->facebook->general->defaultImagePostsWidth;
	}

	/**
	 * Returns the height of the Open Graph image.
	 *
	 * @since 4.0.0
	 *
	 * @return string The image height.
	 */
	public function getImageHeight() {
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			$height = aioseo()->options->social->facebook->homePage->imageHeight;

			return $height ? $height : aioseo()->options->social->facebook->general->defaultImagePostsHeight;
		}

		$metaData = aioseo()->meta->metaData->getMetaData();
		if ( ! empty( $metaData->og_custom_image_height ) ) {
			return $metaData->og_custom_image_height;
		}

		$image = $this->getImage();
		if ( is_array( $image ) ) {
			return $image[2];
		}

		return aioseo()->options->social->facebook->general->defaultImagePostsHeight;
	}

	/**
	 * Returns the Open Graph video URL.
	 *
	 * @since 4.0.0
	 *
	 * @return string The video URL.
	 */
	public function getVideo() {
		$metaData = aioseo()->meta->metaData->getMetaData();

		return ! empty( $metaData->og_video ) ? $metaData->og_video : '';
	}

	/**
	 * Returns the width of the video.
	 *
	 * @since 4.0.0
	 *
	 * @return string The video width.
	 */
	public function getVideoWidth() {
		$metaData = aioseo()->meta->metaData->getMetaData();

		return ! empty( $metaData->og_video_width ) ? $metaData->og_video_width : '';
	}

	/**
	 * Returns the height of the video.
	 *
	 * @since 4.0.0
	 *
	 * @return string The video height.
	 */
	public function getVideoHeight() {
		$metaData = aioseo()->meta->metaData->getMetaData();

		return ! empty( $metaData->og_video_height ) ? $metaData->og_video_height : '';
	}

	/**
	 * Returns the site name.
	 *
	 * @since 4.0.0
	 *
	 * @return string The site name.
	 */
	public function getSiteName() {
		$title = aioseo()->helpers->decodeHtmlEntities( aioseo()->tags->replaceTags( aioseo()->options->social->facebook->general->siteName, get_the_ID() ) );
		if ( ! $title ) {
			$title = aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) );
		}

		return wp_strip_all_tags( $title );
	}

	/**
	 * Returns the Open Graph object type.
	 *
	 * @since 4.0.0
	 *
	 * @return string The object type.
	 */
	public function getObjectType() {
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			$type = aioseo()->options->social->facebook->homePage->objectType;

			return $type ? $type : 'website';
		}

		if ( is_post_type_archive() ) {
			return 'website';
		}

		$post     = aioseo()->helpers->getPost();
		$metaData = aioseo()->meta->metaData->getMetaData( $post );
		if ( ! empty( $metaData->og_object_type ) && 'default' !== $metaData->og_object_type ) {
			return $metaData->og_object_type;
		}

		$postType          = get_post_type();
		$dynamicOptions    = aioseo()->dynamicOptions->noConflict();
		$defaultObjectType = $dynamicOptions->social->facebook->general->postTypes->has( $postType )
			? $dynamicOptions->social->facebook->general->postTypes->$postType->objectType
			: '';

		return ! empty( $defaultObjectType ) ? $defaultObjectType : 'article';
	}

	/**
	 * Returns the Open Graph title for the current page.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|integer $post The post object or ID (optional).
	 * @return string                 The Open Graph title.
	 */
	public function getTitle( $post = null ) {
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			$title = aioseo()->meta->title->helpers->prepare( aioseo()->options->social->facebook->homePage->title );

			return $title ? $title : aioseo()->meta->title->getTitle();
		}

		$post     = aioseo()->helpers->getPost( $post );
		$metaData = aioseo()->meta->metaData->getMetaData( $post );

		$title = '';
		if ( ! empty( $metaData->og_title ) ) {
			$title = aioseo()->meta->title->helpers->prepare( $metaData->og_title );
		}

		if ( is_post_type_archive() ) {
			$postType = get_queried_object();
			if ( is_a( $postType, 'WP_Post_Type' ) ) {
				$dynamicOptions = aioseo()->dynamicOptions->noConflict();
				if ( $dynamicOptions->searchAppearance->archives->has( $postType->name ) ) {
					$title = aioseo()->meta->title->helpers->prepare( aioseo()->dynamicOptions->searchAppearance->archives->{ $postType->name }->title );
				}
			}
		}

		return $title
			? $title
			: (
				$post
					? aioseo()->meta->title->getPostTitle( $post )
					: $title
			);
	}

	/**
	 * Returns the Open Graph description.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|integer $post The post object or ID (optional).
	 * @return string                 The Open Graph description.
	 */
	public function getDescription( $post = null ) {
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			$description = aioseo()->meta->description->helpers->prepare( aioseo()->options->social->facebook->homePage->description );

			return $description ? $description : aioseo()->meta->description->getDescription();
		}

		$post     = aioseo()->helpers->getPost( $post );
		$metaData = aioseo()->meta->metaData->getMetaData( $post );

		$description = '';
		if ( ! empty( $metaData->og_description ) ) {
			$description = aioseo()->meta->description->helpers->prepare( $metaData->og_description );
		}

		if ( is_post_type_archive() ) {
			$postType = get_queried_object();
			if ( is_a( $postType, 'WP_Post_Type' ) ) {
				$dynamicOptions = aioseo()->dynamicOptions->noConflict();
				if ( $dynamicOptions->searchAppearance->archives->has( $postType->name ) ) {
					$description = aioseo()->meta->description->helpers->prepare( aioseo()->dynamicOptions->searchAppearance->archives->{ $postType->name }->metaDescription );
				}
			}
		}

		return $description
			? $description
			: (
				$post
					? aioseo()->meta->description->getPostDescription( $post )
					: $description
			);
	}

	/**
	 * Returns the Open Graph article section name.
	 *
	 * @since 4.0.0
	 *
	 * @return string The article section name.
	 */
	public function getSection() {
		$metaData = aioseo()->meta->metaData->getMetaData();

		return ! empty( $metaData->og_article_section ) ? $metaData->og_article_section : '';
	}

	/**
	 * Returns the Open Graph publisher URL.
	 *
	 * @since 4.0.0
	 *
	 * @return string The Open Graph publisher URL.
	 */
	public function getPublisher() {
		if ( ! aioseo()->options->social->profiles->sameUsername->enable ) {
			return aioseo()->options->social->profiles->urls->facebookPageUrl;
		}

		$username = aioseo()->options->social->profiles->sameUsername->username;

		return ( $username && in_array( 'facebookPageUrl', aioseo()->options->social->profiles->sameUsername->included, true ) )
			? 'https://facebook.com/' . $username
			: '';
	}

	/**
	 * Returns the published time.
	 *
	 * @since 4.0.0
	 *
	 * @return string The published time.
	 */
	public function getPublishedTime() {
		$post = aioseo()->helpers->getPost();

		return $post ? aioseo()->helpers->dateTimeToIso8601( $post->post_date_gmt ) : '';
	}

	/**
	 * Returns the last modified time.
	 *
	 * @since 4.0.0
	 *
	 * @return string The last modified time.
	 */
	public function getModifiedTime() {
		$post = aioseo()->helpers->getPost();

		return $post ? aioseo()->helpers->dateTimeToIso8601( $post->post_modified_gmt ) : '';
	}


	/**
	 * Returns the Open Graph author.
	 *
	 * @since 4.0.0
	 *
	 * @return string The Open Graph author.
	 */
	public function getAuthor() {
		$post = aioseo()->helpers->getPost();
		if ( ! is_a( $post, 'WP_Post' ) || ! aioseo()->options->social->facebook->general->showAuthor ) {
			return '';
		}

		$author       = '';
		$userProfiles = $this->getUserProfiles( $post->post_author );
		if ( ! empty( $userProfiles['facebookPageUrl'] ) ) {
			$author = $userProfiles['facebookPageUrl'];
		}

		if ( empty( $author ) ) {
			$author = aioseo()->options->social->facebook->advanced->authorUrl;
		}

		return $author;
	}

	/**
	 * Returns the Open Graph article tags.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of unique keywords.
	 */
	public function getArticleTags() {
		$post     = aioseo()->helpers->getPost();
		$metaData = aioseo()->meta->metaData->getMetaData( $post );
		$tags     = ! empty( $metaData->og_article_tags ) ? aioseo()->meta->keywords->extractMetaKeywords( $metaData->og_article_tags ) : [];

		if (
			$post &&
			aioseo()->options->social->facebook->advanced->enable &&
			aioseo()->options->social->facebook->advanced->generateArticleTags
		) {
			if ( aioseo()->options->social->facebook->advanced->useKeywordsInTags ) {
				$keywords = aioseo()->meta->keywords->getKeywords();
				$keywords = aioseo()->tags->parseCustomFields( $keywords );
				$keywords = aioseo()->meta->keywords->keywordStringToList( $keywords );
				$tags     = array_merge( $tags, $keywords );
			}

			if ( aioseo()->options->social->facebook->advanced->useCategoriesInTags ) {
				$tags = array_merge( $tags, aioseo()->helpers->getAllCategories( $post->ID ) );
			}

			if ( aioseo()->options->social->facebook->advanced->usePostTagsInTags ) {
				$tags = array_merge( $tags, aioseo()->helpers->getAllTags( $post->ID ) );
			}
		}

		return aioseo()->meta->keywords->getUniqueKeywords( $tags, false );
	}

	/**
	 * Retreive the locale.
	 *
	 * @since 4.1.4
	 *
	 * @return string The locale.
	 */
	public function getLocale() {
		$locale = get_locale();

		// These are the locales FB supports.
		$validLocales = [
			'af_ZA', // Afrikaans.
			'ak_GH', // Akan.
			'am_ET', // Amharic.
			'ar_AR', // Arabic.
			'as_IN', // Assamese.
			'ay_BO', // Aymara.
			'az_AZ', // Azerbaijani.
			'be_BY', // Belarusian.
			'bg_BG', // Bulgarian.
			'bp_IN', // Bhojpuri.
			'bn_IN', // Bengali.
			'br_FR', // Breton.
			'bs_BA', // Bosnian.
			'ca_ES', // Catalan.
			'cb_IQ', // Sorani Kurdish.
			'ck_US', // Cherokee.
			'co_FR', // Corsican.
			'cs_CZ', // Czech.
			'cx_PH', // Cebuano.
			'cy_GB', // Welsh.
			'da_DK', // Danish.
			'de_DE', // German.
			'el_GR', // Greek.
			'en_GB', // English (UK).
			'en_PI', // English (Pirate).
			'en_UD', // English (Upside Down).
			'en_US', // English (US).
			'em_ZM',
			'eo_EO', // Esperanto.
			'es_ES', // Spanish (Spain).
			'es_LA', // Spanish.
			'es_MX', // Spanish (Mexico).
			'et_EE', // Estonian.
			'eu_ES', // Basque.
			'fa_IR', // Persian.
			'fb_LT', // Leet Speak.
			'ff_NG', // Fulah.
			'fi_FI', // Finnish.
			'fo_FO', // Faroese.
			'fr_CA', // French (Canada).
			'fr_FR', // French (France).
			'fy_NL', // Frisian.
			'ga_IE', // Irish.
			'gl_ES', // Galician.
			'gn_PY', // Guarani.
			'gu_IN', // Gujarati.
			'gx_GR', // Classical Greek.
			'ha_NG', // Hausa.
			'he_IL', // Hebrew.
			'hi_IN', // Hindi.
			'hr_HR', // Croatian.
			'hu_HU', // Hungarian.
			'ht_HT', // Haitian Creole.
			'hy_AM', // Armenian.
			'id_ID', // Indonesian.
			'ig_NG', // Igbo.
			'is_IS', // Icelandic.
			'it_IT', // Italian.
			'ik_US',
			'iu_CA',
			'ja_JP', // Japanese.
			'ja_KS', // Japanese (Kansai).
			'jv_ID', // Javanese.
			'ka_GE', // Georgian.
			'kk_KZ', // Kazakh.
			'km_KH', // Khmer.
			'kn_IN', // Kannada.
			'ko_KR', // Korean.
			'ks_IN', // Kashmiri.
			'ku_TR', // Kurdish (Kurmanji).
			'ky_KG', // Kyrgyz.
			'la_VA', // Latin.
			'lg_UG', // Ganda.
			'li_NL', // Limburgish.
			'ln_CD', // Lingala.
			'lo_LA', // Lao.
			'lt_LT', // Lithuanian.
			'lv_LV', // Latvian.
			'mg_MG', // Malagasy.
			'mi_NZ', // Maori.
			'mk_MK', // Macedonian.
			'ml_IN', // Malayalam.
			'mn_MN', // Mongolian.
			'mr_IN', // Marathi.
			'ms_MY', // Malay.
			'mt_MT', // Maltese.
			'my_MM', // Burmese.
			'nb_NO', // Norwegian (bokmal).
			'nd_ZW', // Ndebele.
			'ne_NP', // Nepali.
			'nl_BE', // Dutch (Belgie).
			'nl_NL', // Dutch.
			'nn_NO', // Norwegian (nynorsk).
			'nr_ZA', // Southern Ndebele.
			'ns_ZA', // Northern Sotho.
			'ny_MW', // Chewa.
			'om_ET', // Oromo.
			'or_IN', // Oriya.
			'pa_IN', // Punjabi.
			'pl_PL', // Polish.
			'ps_AF', // Pashto.
			'pt_BR', // Portuguese (Brazil).
			'pt_PT', // Portuguese (Portugal).
			'qc_GT', // Quiché.
			'qu_PE', // Quechua.
			'qr_GR',
			'qz_MM', // Burmese (Zawgyi).
			'rm_CH', // Romansh.
			'ro_RO', // Romanian.
			'ru_RU', // Russian.
			'rw_RW', // Kinyarwanda.
			'sa_IN', // Sanskrit.
			'sc_IT', // Sardinian.
			'se_NO', // Northern Sami.
			'si_LK', // Sinhala.
			'su_ID', // Sundanese.
			'sk_SK', // Slovak.
			'sl_SI', // Slovenian.
			'sn_ZW', // Shona.
			'so_SO', // Somali.
			'sq_AL', // Albanian.
			'sr_RS', // Serbian.
			'ss_SZ', // Swazi.
			'st_ZA', // Southern Sotho.
			'sv_SE', // Swedish.
			'sw_KE', // Swahili.
			'sy_SY', // Syriac.
			'sz_PL', // Silesian.
			'ta_IN', // Tamil.
			'te_IN', // Telugu.
			'tg_TJ', // Tajik.
			'th_TH', // Thai.
			'tk_TM', // Turkmen.
			'tl_PH', // Filipino.
			'tl_ST', // Klingon.
			'tn_BW', // Tswana.
			'tr_TR', // Turkish.
			'ts_ZA', // Tsonga.
			'tt_RU', // Tatar.
			'tz_MA', // Tamazight.
			'uk_UA', // Ukrainian.
			'ur_PK', // Urdu.
			'uz_UZ', // Uzbek.
			've_ZA', // Venda.
			'vi_VN', // Vietnamese.
			'wo_SN', // Wolof.
			'xh_ZA', // Xhosa.
			'yi_DE', // Yiddish.
			'yo_NG', // Yoruba.
			'zh_CN', // Simplified Chinese (China).
			'zh_HK', // Traditional Chinese (Hong Kong).
			'zh_TW', // Traditional Chinese (Taiwan).
			'zu_ZA', // Zulu.
			'zz_TR', // Zazaki.
		];

		// Catch some weird locales served out by WP that are not easily doubled up.
		$fixLocales = [
			'ca' => 'ca_ES',
			'en' => 'en_US',
			'el' => 'el_GR',
			'et' => 'et_EE',
			'ja' => 'ja_JP',
			'sq' => 'sq_AL',
			'uk' => 'uk_UA',
			'vi' => 'vi_VN',
			'zh' => 'zh_CN',
		];

		if ( isset( $fixLocales[ $locale ] ) ) {
			$locale = $fixLocales[ $locale ];
		}

		// Convert locales like "es" to "es_ES", in case that works for the given locale (sometimes it does).
		if ( 2 === strlen( $locale ) ) {
			$locale = strtolower( $locale ) . '_' . strtoupper( $locale );
		}

		// Check to see if the locale is a valid FB one, if not, use en_US as a fallback.
		if ( ! in_array( $locale, $validLocales, true ) ) {
			$locale = strtolower( substr( $locale, 0, 2 ) ) . '_' . strtoupper( substr( $locale, 0, 2 ) );

			if ( ! in_array( $locale, $validLocales, true ) ) {
				$locale = 'en_US';
			}
		}

		return apply_filters( 'aioseo_og_locale', $locale );
	}
}Common/Social/Image.php000064400000017435151536241200010752 0ustar00<?php
namespace AIOSEO\Plugin\Common\Social;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Handles the Open Graph and Twitter Image.
 *
 * @since 4.0.0
 */
class Image {
	/**
	 * The type of image ("facebook" or "twitter").
	 *
	 * @since 4.1.6.2
	 *
	 * @var string
	 */
	protected $type;

	/**
	 * The post object.
	 *
	 * @since 4.1.6.2
	 *
	 * @var \WP_Post
	 */
	private $post;

	/**
	 * The default thumbnail size.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $thumbnailSize;

	/**
	 * Whether or not to use the cached images.
	 *
	 * @since 4.1.6
	 *
	 * @var boolean
	 */
	public $useCache = true;

	/**
	 * Returns the Facebook or Twitter image.
	 *
	 * @since 4.0.0
	 *
	 * @param  string        $type        The type ("Facebook" or "Twitter").
	 * @param  string        $imageSource The image source.
	 * @param  \WP_Post|null $post        The post object.
	 * @return string|array               The image data.
	 */
	public function getImage( $type, $imageSource, $post = null ) {
		$this->type          = $type;
		$this->post          = $post;
		$this->thumbnailSize = apply_filters( 'aioseo_thumbnail_size', 'fullsize' );
		$hash                = md5( wp_json_encode( [ $type, $imageSource, $post ] ) );

		static $images = [];
		if ( isset( $images[ $hash ] ) ) {
			return $images[ $hash ];
		}

		if ( 'auto' === $imageSource && aioseo()->helpers->getPostPageBuilderName( $post->ID ) ) {
			$imageSource = 'default';
		}

		if ( is_a( $this->post, 'WP_Post' ) ) {
			switch ( $imageSource ) {
				case 'featured':
					$image = $this->getFeaturedImage();
					break;
				case 'attach':
					$image = $this->getFirstAttachedImage();
					break;
				case 'content':
					$image = $this->getFirstImageInContent();
					break;
				case 'author':
					$image = $this->getAuthorAvatar();
					break;
				case 'auto':
					$image = $this->getFirstAvailableImage();
					break;
				case 'custom':
					$image = $this->getCustomFieldImage();
					break;
				case 'custom_image':
					$metaData = aioseo()->meta->metaData->getMetaData( $post );
					if ( empty( $metaData ) ) {
						break;
					}
					$image = 'facebook' === strtolower( $this->type )
						? $metaData->og_image_custom_url
						: $metaData->twitter_image_custom_url;
					break;
				case 'default':
				default:
					$image = aioseo()->options->social->{$this->type}->general->defaultImagePosts;
			}
		}

		if ( empty( $image ) ) {
			$image = aioseo()->options->social->{$this->type}->general->defaultImagePosts;
		}

		if ( is_array( $image ) ) {
			$images[ $hash ] = $image;

			return $images[ $hash ];
		}

		$imageWithoutDimensions = aioseo()->helpers->removeImageDimensions( $image );
		$attachmentId           = aioseo()->helpers->attachmentUrlToPostId( $imageWithoutDimensions );
		$images[ $hash ]        = $attachmentId ? wp_get_attachment_image_src( $attachmentId, $this->thumbnailSize ) : $image;

		return $images[ $hash ];
	}

	/**
	 * Returns the Featured Image for the post.
	 *
	 * @since 4.0.0
	 *
	 * @return array The image data.
	 */
	private function getFeaturedImage() {
		$cachedImage = $this->getCachedImage();
		if ( $cachedImage ) {
			return $cachedImage;
		}

		$imageId = get_post_thumbnail_id( $this->post->ID );

		return $imageId ? wp_get_attachment_image_src( $imageId, $this->thumbnailSize ) : '';
	}

	/**
	 * Returns the first attached image.
	 *
	 * @since 4.0.0
	 *
	 * @return string The image data.
	 */
	private function getFirstAttachedImage() {
		$cachedImage = $this->getCachedImage();
		if ( $cachedImage ) {
			return $cachedImage;
		}

		if ( 'attachment' === get_post_type( $this->post->ID ) ) {
			return wp_get_attachment_image_src( $this->post->ID, $this->thumbnailSize );
		}

		$attachments = get_children(
			[
				'post_parent'    => $this->post->ID,
				'post_status'    => 'inherit',
				'post_type'      => 'attachment',
				'post_mime_type' => 'image',
			]
		);

		return $attachments && count( $attachments ) ? wp_get_attachment_image_src( array_values( $attachments )[0]->ID, $this->thumbnailSize ) : '';
	}

	/**
	 * Returns the first image found in the post content.
	 *
	 * @since 4.0.0
	 *
	 * @return string The image URL.
	 */
	private function getFirstImageInContent() {
		$cachedImage = $this->getCachedImage();
		if ( $cachedImage ) {
			return $cachedImage;
		}

		$postContent = aioseo()->helpers->getPostContent( $this->post );
		preg_match_all( '|<img.*?src=[\'"](.*?)[\'"].*?>|i', (string) $postContent, $matches ); // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage

		// Ignore cover block background image - WP >= 5.7.
		if ( ! empty( $matches[0] ) && apply_filters( 'aioseo_social_image_ignore_cover_block', true, $this->post, $matches ) ) {
			foreach ( $matches[0] as $key => $match ) {
				if ( false !== stripos( $match, 'wp-block-cover__image-background' ) ) {
					unset( $matches[1][ $key ] );
				}
			}
		}

		return ! empty( $matches[1] ) ? current( $matches[1] ) : '';
	}

	/**
	 * Returns the author avatar.
	 *
	 * @since 4.0.0
	 *
	 * @return string The image URL.
	 */
	private function getAuthorAvatar() {
		$avatar = get_avatar( $this->post->post_author, 300 );
		preg_match( "/src='(.*?)'/i", (string) $avatar, $matches );

		return ! empty( $matches[1] ) ? $matches[1] : '';
	}

	/**
	 * Returns the first available image.
	 *
	 * @since 4.0.0
	 *
	 * @return string The image URL.
	 */
	private function getFirstAvailableImage() {
		// Disable the cache.
		$this->useCache = false;

		$image = $this->getCustomFieldImage();

		if ( ! $image ) {
			$image = $this->getFeaturedImage();
		}

		if ( ! $image ) {
			$image = $this->getFirstAttachedImage();
		}

		if ( ! $image ) {
			$image = $this->getFirstImageInContent();
		}

		if ( ! $image && 'twitter' === strtolower( $this->type ) ) {
			$image = aioseo()->options->social->twitter->homePage->image;
		}

		// Enable the cache.
		$this->useCache = true;

		return $image ? $image : aioseo()->options->social->facebook->homePage->image;
	}

	/**
	 * Returns the image from a custom field.
	 *
	 * @since 4.0.0
	 *
	 * @return string The image URL.
	 */
	private function getCustomFieldImage() {
		$cachedImage = $this->getCachedImage();
		if ( $cachedImage ) {
			return $cachedImage;
		}

		$prefix = 'facebook' === strtolower( $this->type ) ? 'og_' : 'twitter_';

		$aioseoPost   = Models\Post::getPost( $this->post->ID );
		$customFields = ! empty( $aioseoPost->{ $prefix . 'image_custom_fields' } )
			? $aioseoPost->{ $prefix . 'image_custom_fields' }
			: aioseo()->options->social->{$this->type}->general->customFieldImagePosts;

		if ( ! $customFields ) {
			return '';
		}

		$customFields = explode( ',', $customFields );
		foreach ( $customFields as $customField ) {
			$image = get_post_meta( $this->post->ID, $customField, true );

			if ( ! empty( $image ) ) {
				$image = is_array( $image ) ? $image[0] : $image;

				return is_numeric( $image )
					? wp_get_attachment_image_src( $image, $this->thumbnailSize )
					: $image;
			}
		}

		return '';
	}

	/**
	 * Returns the cached image if there is one.
	 *
	 * @since 4.1.6.2
	 *
	 * @param  \WP_Term     $object The object for which we need to get the cached image.
	 * @return string|array         The image URL or data.
	 */
	protected function getCachedImage( $object = null ) {
		if ( null === $object ) {
			// This isn't null if we call it from the Pro class.
			$object = $this->post;
		}

		$metaData = aioseo()->meta->metaData->getMetaData( $object );

		switch ( $this->type ) {
			case 'facebook':
				if ( ! empty( $metaData->og_image_url ) && $this->useCache ) {
					return aioseo()->meta->metaData->getCachedOgImage( $metaData );
				}
				break;
			case 'twitter':
				if ( ! empty( $metaData->twitter_image_url ) && $this->useCache ) {
					return $metaData->twitter_image_url;
				}
				break;
			default:
				break;
		}

		return '';
	}
}Common/Social/Output.php000064400000011065151536241200011221 0ustar00<?php
namespace AIOSEO\Plugin\Common\Social;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Outputs our social meta.
 *
 * @since 4.0.0
 */
class Output {

	/**
	 * Checks if the current page should have social meta.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether or not the page should have social meta.
	 */
	public function isAllowed() {
		if ( BuddyPressIntegration::isComponentPage() ) {
			return false;
		}

		if (
			! is_front_page() &&
			! is_home() &&
			! is_singular() &&
			! is_post_type_archive() &&
			! aioseo()->helpers->isWooCommerceShopPage()
		) {
			return false;
		}

		return true;
	}

	/**
	 * Returns the Open Graph meta.
	 *
	 * @since 4.0.0
	 *
	 * @return array The Open Graph meta.
	 */
	public function getFacebookMeta() {
		if ( ! $this->isAllowed() || ! aioseo()->options->social->facebook->general->enable ) {
			return [];
		}

		$meta = [
			'og:locale'      => aioseo()->social->facebook->getLocale(),
			'og:site_name'   => aioseo()->helpers->encodeOutputHtml( aioseo()->social->facebook->getSiteName() ),
			'og:type'        => aioseo()->social->facebook->getObjectType(),
			'og:title'       => aioseo()->helpers->encodeOutputHtml( aioseo()->social->facebook->getTitle() ),
			'og:description' => aioseo()->helpers->encodeOutputHtml( aioseo()->social->facebook->getDescription() ),
			'og:url'         => esc_url( aioseo()->helpers->canonicalUrl() ),
			'fb:app_id'      => aioseo()->options->social->facebook->advanced->appId,
			'fb:admins'      => implode( ',', array_map( 'trim', explode( ',', aioseo()->options->social->facebook->advanced->adminId ) ) ),
		];

		$image = aioseo()->social->facebook->getImage();
		if ( $image ) {
			$image = is_array( $image ) ? $image[0] : $image;
			$image = aioseo()->helpers->makeUrlAbsolute( $image );
			$image = set_url_scheme( esc_url( $image ) );

			$meta += [
				'og:image'            => $image,
				'og:image:secure_url' => is_ssl() ? $image : '',
				'og:image:width'      => aioseo()->social->facebook->getImageWidth(),
				'og:image:height'     => aioseo()->social->facebook->getImageHeight(),
			];
		}

		$video = aioseo()->social->facebook->getVideo();
		if ( $video ) {
			$video = set_url_scheme( esc_url( $video ) );

			$meta += [
				'og:video'            => $video,
				'og:video:secure_url' => is_ssl() ? $video : '',
				'og:video:width'      => aioseo()->social->facebook->getVideoWidth(),
				'og:video:height'     => aioseo()->social->facebook->getVideoHeight(),
			];
		}

		if ( ! empty( $meta['og:type'] ) && 'article' === $meta['og:type'] ) {
			$meta += [
				'article:section'        => aioseo()->social->facebook->getSection(),
				'article:tag'            => aioseo()->social->facebook->getArticleTags(),
				'article:published_time' => aioseo()->social->facebook->getPublishedTime(),
				'article:modified_time'  => aioseo()->social->facebook->getModifiedTime(),
				'article:publisher'      => aioseo()->social->facebook->getPublisher(),
				'article:author'         => aioseo()->social->facebook->getAuthor()
			];
		}

		return array_filter( apply_filters( 'aioseo_facebook_tags', $meta ) );
	}

	/**
	 * Returns the Twitter meta.
	 *
	 * @since 4.0.0
	 *
	 * @return array The Twitter meta.
	 */
	public function getTwitterMeta() {
		if ( ! $this->isAllowed() || ! aioseo()->options->social->twitter->general->enable ) {
			return [];
		}

		$meta = [
			'twitter:card'        => aioseo()->social->twitter->getCardType(),
			'twitter:site'        => aioseo()->social->twitter->prepareUsername( aioseo()->social->twitter->getTwitterUrl() ),
			'twitter:title'       => aioseo()->helpers->encodeOutputHtml( aioseo()->social->twitter->getTitle() ),
			'twitter:description' => aioseo()->helpers->encodeOutputHtml( aioseo()->social->twitter->getDescription() ),
			'twitter:creator'     => aioseo()->social->twitter->getCreator()
		];

		$image = aioseo()->social->twitter->getImage();
		if ( $image ) {
			$image = is_array( $image ) ? $image[0] : $image;
			$image = aioseo()->helpers->makeUrlAbsolute( $image );

			// Set the twitter image meta.
			$meta['twitter:image'] = $image;
		}

		if ( is_singular() ) {
			$additionalData = apply_filters( 'aioseo_social_twitter_additional_data', aioseo()->social->twitter->getAdditionalData() );
			if ( $additionalData ) {
				$i = 1;
				foreach ( $additionalData as $data ) {
					$meta[ "twitter:label$i" ] = $data['label'];
					$meta[ "twitter:data$i" ]  = $data['value'];
					$i++;
				}
			}
		}

		return array_filter( apply_filters( 'aioseo_twitter_tags', $meta ) );
	}
}Common/Social/Social.php000064400000010213151536241200011125 0ustar00<?php
namespace AIOSEO\Plugin\Common\Social;

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

/**
 * Handles the Social Meta.
 *
 * @package AIOSEO\Plugin\Common\Social
 *
 * @since 4.0.0
 */
class Social {
	/**
	 * The name of the action to bust the OG cache.
	 *
	 * @since 4.2.0
	 *
	 * @var string
	 */
	private $bustOgCacheActionName = 'aioseo_og_cache_bust_post';

	/**
	 * Image class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Image
	 */
	public $image = null;

	/**
	 * Facebook class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Facebook
	 */
	public $facebook = null;

	/**
	 * Twitter class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Twitter
	 */
	public $twitter = null;

	/**
	 * Output class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Output
	 */
	public $output = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->image = new Image();

		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		$this->facebook = new Facebook();
		$this->twitter  = new Twitter();
		$this->output   = new Output();

		$this->hooks();
	}

	/**
	 * Registers our hooks.
	 *
	 * @since 4.0.0
	 */
	protected function hooks() {
		add_action( $this->bustOgCacheActionName, [ $this, 'bustOgCachePost' ] );

		// To avoid duplicate sets of meta tags.
		add_filter( 'jetpack_enable_open_graph', '__return_false' );

		if ( ! is_admin() ) {
			add_filter( 'language_attributes', [ $this, 'addAttributes' ] );

			return;
		}

		// Forces a refresh of the Facebook cache.
		add_action( 'post_updated', [ $this, 'scheduleBustOgCachePost' ], 10, 2 );
	}

	/**
	 * Adds our attributes to the registered language attributes.
	 *
	 * @since   4.0.0
	 * @version 4.4.5 Adds trim function the html tag removing empty spaces.
	 *
	 * @param  string $htmlTag The 'html' tag as a string.
	 * @return string          The filtered 'html' tag as a string.
	 */
	public function addAttributes( $htmlTag ) {
		if ( ! aioseo()->options->social->facebook->general->enable ) {
			return $htmlTag;
		}

		$attributes = apply_filters( 'aioseo_opengraph_attributes', [ 'prefix="og: https://ogp.me/ns#"' ] );
		foreach ( $attributes as $attr ) {
			if ( strpos( $htmlTag, $attr ) === false ) {
				$htmlTag .= " $attr ";
			}
		}

		return trim( $htmlTag );
	}

	/**
	 * Schedule a ping to bust the OG cache.
	 *
	 * @since 4.2.0
	 *
	 * @param  int      $postId The post ID.
	 * @param  \WP_Post $post   The post object.
	 * @return void
	 */
	public function scheduleBustOgCachePost( $postId, $post = null ) {
		if ( ! aioseo()->helpers->isSbCustomFacebookFeedActive() || ! aioseo()->helpers->isValidPost( $post ) ) {
			return;
		}

		if ( aioseo()->actionScheduler->isScheduled( $this->bustOgCacheActionName, [ 'postId' => $postId ] ) ) {
			return;
		}

		// Schedule the new ping.
		aioseo()->actionScheduler->scheduleAsync( $this->bustOgCacheActionName, [ 'postId' => $postId ] );
	}

	/**
	 * Pings Facebook and asks them to bust the OG cache for a particular post.
	 *
	 * @since 4.2.0
	 *
	 * @see https://developers.facebook.com/docs/sharing/opengraph/using-objects#update
	 *
	 * @param  int  $postId The post ID.
	 * @return void
	 */
	public function bustOgCachePost( $postId ) {
		$post              = get_post( $postId );
		$customAccessToken = apply_filters( 'aioseo_facebook_access_token', '' );

		if (
			! aioseo()->helpers->isValidPost( $post ) ||
			( ! aioseo()->helpers->isSbCustomFacebookFeedActive() && ! $customAccessToken )
		) {
			return;
		}

		$permalink = get_permalink( $postId );
		$this->bustOgCacheHelper( $permalink );
	}

	/**
	 * Helper function for bustOgCache().
	 *
	 * @since 4.2.0
	 *
	 * @param  string $permalink The permalink.
	 * @return void
	 */
	protected function bustOgCacheHelper( $permalink ) {
		$accessToken = aioseo()->helpers->getSbAccessToken();
		$accessToken = apply_filters( 'aioseo_facebook_access_token', $accessToken );
		if ( ! $accessToken ) {
			return;
		}

		$url = sprintf(
			'https://graph.facebook.com/?%s',
			http_build_query(
				[
					'id'           => $permalink,
					'scrape'       => true,
					'access_token' => $accessToken
				]
			)
		);

		wp_remote_post( $url, [ 'blocking' => false ] );
	}
}Common/Social/Twitter.php000064400000017724151536241200011373 0ustar00<?php
namespace AIOSEO\Plugin\Common\Social;

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

use AIOSEO\Plugin\Common\Traits;

/**
 * Handles the Twitter meta.
 *
 * @since 4.0.0
 */
class Twitter {
	use Traits\SocialProfiles;

	/**
	 * Returns the Twitter URL for the site.
	 *
	 * @since 4.0.0
	 *
	 * @return string The Twitter URL.
	 */
	public function getTwitterUrl() {
		if ( ! aioseo()->options->social->profiles->sameUsername->enable ) {
			return aioseo()->options->social->profiles->urls->twitterUrl;
		}

		$userName = aioseo()->options->social->profiles->sameUsername->username;

		return ( $userName && in_array( 'twitterUrl', aioseo()->options->social->profiles->sameUsername->included, true ) )
			? 'https://x.com/' . $userName
			: '';
	}

	/**
	 * Returns the Twitter card type.
	 *
	 * @since 4.0.0
	 *
	 * @return string $card The card type.
	 */
	public function getCardType() {
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			return aioseo()->options->social->twitter->homePage->cardType;
		}

		$metaData = aioseo()->meta->metaData->getMetaData();

		return ! empty( $metaData->twitter_card ) && 'default' !== $metaData->twitter_card ? $metaData->twitter_card : aioseo()->options->social->twitter->general->defaultCardType;
	}

	/**
	 * Returns the Twitter creator.
	 *
	 * @since 4.0.0
	 *
	 * @return string The creator.
	 */
	public function getCreator() {
		$post = aioseo()->helpers->getPost();
		if (
			! is_a( $post, 'WP_Post' ) ||
			! post_type_supports( $post->post_type, 'author' ) ||
			! aioseo()->options->social->twitter->general->showAuthor
		) {
			return '';
		}

		$author       = '';
		$userProfiles = $this->getUserProfiles( $post->post_author );
		if ( ! empty( $userProfiles['twitterUrl'] ) ) {
			$author = $userProfiles['twitterUrl'];
		}

		if ( empty( $author ) ) {
			$author = aioseo()->social->twitter->getTwitterUrl();
		}

		$author = aioseo()->social->twitter->prepareUsername( $author );

		return $author;
	}

	/**
	 * Returns the Twitter image URL.
	 *
	 * @since 4.0.0
	 *
	 * @param  int    $postId The post ID (optional).
	 * @return string         The image URL.
	 */
	public function getImage( $postId = null ) {
		$post = aioseo()->helpers->getPost( $postId );
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			$image = aioseo()->options->social->twitter->homePage->image;
			if ( empty( $image ) ) {
				$image = aioseo()->options->social->facebook->homePage->image;
			}
			if ( empty( $image ) ) {
				$image = aioseo()->social->image->getImage( 'twitter', aioseo()->options->social->twitter->general->defaultImageSourcePosts, $post );
			}

			return $image ? $image : aioseo()->social->facebook->getImage();
		}

		$metaData = aioseo()->meta->metaData->getMetaData( $post );

		if ( ! empty( $metaData->twitter_use_og ) ) {
			return aioseo()->social->facebook->getImage();
		}

		$image = '';
		if ( ! empty( $metaData ) ) {
			$imageSource = ! empty( $metaData->twitter_image_type ) && 'default' !== $metaData->twitter_image_type
				? $metaData->twitter_image_type
				: aioseo()->options->social->twitter->general->defaultImageSourcePosts;

			$image = aioseo()->social->image->getImage( 'twitter', $imageSource, $post );
		}

		return $image ? $image : aioseo()->social->facebook->getImage();
	}

	/**
	 * Returns the Twitter title for the current page.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|integer $post The post object or ID (optional).
	 * @return string                 The Twitter title.
	 */
	public function getTitle( $post = null ) {
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			$title = aioseo()->meta->title->helpers->prepare( aioseo()->options->social->twitter->homePage->title );

			return $title ? $title : aioseo()->social->facebook->getTitle( $post );
		}

		$post     = aioseo()->helpers->getPost( $post );
		$metaData = aioseo()->meta->metaData->getMetaData( $post );

		if ( ! empty( $metaData->twitter_use_og ) ) {
			return aioseo()->social->facebook->getTitle( $post );
		}

		$title = '';
		if ( ! empty( $metaData->twitter_title ) ) {
			$title = aioseo()->meta->title->helpers->prepare( $metaData->twitter_title );
		}

		return $title ? $title : aioseo()->social->facebook->getTitle( $post );
	}

	/**
	 * Returns the Twitter description for the current page.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|integer $post The post object or ID (optional).
	 * @return string                 The Twitter description.
	 */
	public function getDescription( $post = null ) {
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			$description = aioseo()->meta->description->helpers->prepare( aioseo()->options->social->twitter->homePage->description );

			return $description ? $description : aioseo()->social->facebook->getDescription( $post );
		}

		$post     = aioseo()->helpers->getPost( $post );
		$metaData = aioseo()->meta->metaData->getMetaData( $post );

		if ( ! empty( $metaData->twitter_use_og ) ) {
			return aioseo()->social->facebook->getDescription( $post );
		}

		$description = '';
		if ( ! empty( $metaData->twitter_description ) ) {
			$description = aioseo()->meta->description->helpers->prepare( $metaData->twitter_description );
		}

		return $description ? $description : aioseo()->social->facebook->getDescription( $post );
	}

	/**
	 * Prepare twitter username for public display.
	 *
	 * We do things like strip out the URL, etc and return just (at)username.
	 * At the moment, we'll check for 1 of 3 things... (at)username, username, and https://x.com/username.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $profile   Twitter username.
	 * @param  boolean $includeAt Whether or not ot include the @ sign.
	 * @return string             Full Twitter username.
	 */
	public function prepareUsername( $profile, $includeAt = true ) {
		if ( ! $profile ) {
			return $profile;
		}

		$profile = (string) $profile;
		if ( preg_match( '/^(\@)?[A-Za-z0-9_]+$/', (string) $profile ) ) {
			if ( '@' !== $profile[0] && $includeAt ) {
				$profile = '@' . $profile;
			} elseif ( '@' === $profile[0] && ! $includeAt ) {
				$profile = ltrim( $profile, '@' );
			}
		}

		if ( strpos( $profile, 'twitter.com' ) || strpos( $profile, 'x.com' ) ) {
			$profile = esc_url( $profile );

			// Extract the twitter username from the URL.
			$parsedTwitterProfile = wp_parse_url( $profile );

			$path      = $parsedTwitterProfile['path'];
			$pathParts = explode( '/', $path );
			$profile   = $pathParts[1];

			if ( $profile ) {
				if ( '@' !== $profile[0] && $includeAt ) {
					$profile = '@' . $profile;
				}

				if ( '@' === $profile[0] && ! $includeAt ) {
					$profile = ltrim( $profile, '@' );
				}
			}
		}

		return $profile;
	}

	/**
	 * Get additional twitter data.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of additional twitter data.
	 */
	public function getAdditionalData() {
		if ( ! aioseo()->options->social->twitter->general->additionalData ) {
			return [];
		}

		$data = [];
		$post = aioseo()->helpers->getPost();
		if ( ! is_a( $post, 'WP_Post' ) ) {
			return $data;
		}

		if ( $post->post_author && post_type_supports( $post->post_type, 'author' ) ) {
			$data[] = [
				'label' => __( 'Written by', 'all-in-one-seo-pack' ),
				'value' => get_the_author_meta( 'display_name', $post->post_author )
			];
		}

		if ( ! empty( $post->post_content ) ) {
			$minutes = $this->getReadingTime( $post->post_content );
			if ( ! empty( $minutes ) ) {
				$data[] = [
					'label' => __( 'Est. reading time', 'all-in-one-seo-pack' ),
					// Translators: 1 - The estimated reading time.
					'value' => sprintf( _n( '%1$s minute', '%1$s minutes', $minutes, 'all-in-one-seo-pack' ), $minutes )
				];
			}
		}

		return $data;
	}

	/**
	 * Returns the estimated reading time for a string.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $string The string to count.
	 * @return integer         The estimated reading time as an integer.
	 */
	private function getReadingTime( $string ) {
		$wpm  = 200;
		$word = str_word_count( wp_strip_all_tags( $string ) );

		return round( $word / $wpm );
	}
}Common/Standalone/AdminBarNoindexWarning.php000064400000003166151536241200015132 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Handles the admin bar noindex warning.
 *
 * @since 4.6.7
 */
class AdminBarNoindexWarning {
	/**
	 * Class constructor.
	 *
	 * @since 4.6.7
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'init' ] );
	}

	/**
	 * Initializes the standalone.
	 *
	 * @since 4.6.7
	 *
	 * @return void
	 */
	public function init() {
		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		$isSitePublic = get_option( 'blog_public' );
		if ( $isSitePublic ) {
			return;
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScript' ] );
		add_action( 'wp_enqueue_scripts', [ $this, 'enqueueScript' ] );

		add_action( 'admin_bar_menu', [ $this, 'addAdminBarElement' ], 99999 );
	}

	/**
	 * Enqueues the script.
	 *
	 * @since 4.6.7
	 *
	 * @return void
	 */
	public function enqueueScript() {
		aioseo()->core->assets->load( 'src/vue/standalone/admin-bar-noindex-warning/main.js', [], [
			'optionsReadingUrl' => admin_url( 'options-reading.php' ),
		], 'aioseoAdminBarNoindexWarning' );
	}

	/**
	 * Adds the admin bar element.
	 *
	 * @since 4.6.7
	 *
	 * @param  \WP_Admin_Bar $wpAdminBar The admin bar object.
	 * @return void
	 */
	public function addAdminBarElement( $wpAdminBar ) {
		$wpAdminBar->add_node(
			[
				'id'    => 'aioseo-admin-bar-noindex-warning',
				'title' => __( 'Search Engines Blocked!', 'all-in-one-seo-pack' ),
				'href'  => admin_url( 'options-reading.php' )
			]
		);
	}
}Common/Standalone/BbPress/BbPress.php000064400000002273151536241200013500 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\BbPress;

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

/**
 * Handles the bbPress integration with AIOSEO.
 *
 * @since 4.8.1
 */
class BbPress {
	/**
	 * Instance of the Component class.
	 *
	 * @since 4.8.1
	 *
	 * @var Component
	 */
	public $component;

	/**
	 * Class constructor.
	 *
	 * @since 4.8.1
	 */
	public function __construct() {
		if (
			aioseo()->helpers->isAjaxCronRestRequest() ||
			! aioseo()->helpers->isPluginActive( 'bbpress' )
		) {
			return;
		}

		// Hook into `plugins_loaded` to ensure bbPress has loaded some necessary functions.
		add_action( 'plugins_loaded', [ $this, 'maybeLoad' ], 20 );
	}

	/**
	 * Hooked into `plugins_loaded` action hook.
	 *
	 * @since 4.8.1
	 *
	 * @return void
	 */
	public function maybeLoad() {
		// If the bbPress version is below 2 we bail.
		if ( ! function_exists( 'bbp_get_version' ) || version_compare( bbp_get_version(), '2', '<' ) ) {
			return;
		}

		add_action( 'wp', [ $this, 'setComponent' ] );
	}

	/**
	 * Hooked into `wp` action hook.
	 *
	 * @since 4.8.1
	 *
	 * @return void
	 */
	public function setComponent() {
		$this->component = new Component();
	}
}Common/Standalone/BbPress/Component.php000064400000005100151536241200014072 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\BbPress;

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

/**
 * bbPress Component class.
 *
 * @since 4.8.1
 */
class Component {
	/**
	 * The current component template type.
	 *
	 * @since 4.8.1
	 *
	 * @var string|null
	 */
	public $templateType = null;

	/**
	 * The topic single page data.
	 *
	 * @since 4.8.1
	 *
	 * @var array
	 */
	public $topic = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.8.1
	 */
	public function __construct() {
		if ( is_admin() ) {
			return;
		}

		$this->setTemplateType();
		$this->setTopic();
	}

	/**
	 * Sets the template type.
	 *
	 * @since 4.8.1
	 *
	 * @return void
	 */
	private function setTemplateType() {
		if ( function_exists( 'bbp_is_single_topic' ) && bbp_is_single_topic() ) {
			$this->templateType = 'bbp-topic_single';
		}
	}

	/**
	 * Sets the topic data.
	 *
	 * @since 4.8.1
	 *
	 * @return void
	 */
	private function setTopic() {
		if ( 'bbp-topic_single' !== $this->templateType ) {
			return;
		}

		if (
			! function_exists( 'bbpress' ) ||
			! function_exists( 'bbp_has_replies' ) ||
			! bbp_has_replies()
		) {
			return;
		}

		$replyQuery = bbpress()->reply_query ?? null;
		$replies    = $replyQuery->posts ?? [];
		$mainTopic  = is_array( $replies ) && ! empty( $replies ) ? array_shift( $replies ) : null;

		if ( $mainTopic instanceof \WP_Post ) {
			$this->topic = [
				'title'   => $mainTopic->post_title,
				'content' => $mainTopic->post_content,
				'date'    => $mainTopic->post_date,
				'author'  => get_the_author_meta( 'display_name', $mainTopic->post_author ),
			];

			$comments = [];
			if ( ! empty( $replies ) ) {
				foreach ( $replies as $reply ) {
					if ( ! $reply instanceof \WP_Post ) {
						continue;
					}

					$comments[ $reply->ID ] = [
						'content'       => $reply->post_content,
						'date_recorded' => $reply->post_date,
						'user_fullname' => get_the_author_meta( 'display_name', $reply->post_author ),
					];

					if ( ! empty( $reply->reply_to ) ) {
						$comments[ $reply->reply_to ]['children'][] = $comments[ $reply->ID ];

						unset( $comments[ $reply->ID ] );
					}
				}

				$this->topic['comment'] = array_values( $comments );
			}

			return;
		}

		$this->resetComponent();
	}

	/**
	 * Resets some of the component properties.
	 *
	 * @since 4.8.1
	 *
	 * @return void
	 */
	private function resetComponent() {
		$this->templateType = null;
	}

	/**
	 * Determines the schema type for the current component.
	 *
	 * @since 4.8.1
	 *
	 * @return void
	 */
	public function determineSchemaGraphsAndContext() {
	}
}Common/Standalone/Blocks/Blocks.php000064400000001166151536241200013232 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\Blocks;

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

/**
 * Loads core classes.
 *
 * @since 4.2.3
 */
abstract class Blocks {
	/**
	 * Class constructor.
	 *
	 * @since 4.2.3
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'init' ] );
	}

	/**
	 * Initializes our blocks.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	public function init() {
		$this->register();
	}

	/**
	 * Registers the block. This is a wrapper to be extended in the child class.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	public function register() {}
}Common/Standalone/Blocks/FaqPage.php000064400000001075151536241200013320 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\Blocks;

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

/**
 * FaqPage Block.
 *
 * @since 4.2.3
 */
class FaqPage extends Blocks {
	/**
	 * Register the block.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	public function register() {
		aioseo()->blocks->registerBlock( 'aioseo/faq',
			[
				'render_callback' => function( $attributes, $content ) {
					if ( isset( $attributes['hidden'] ) && true === $attributes['hidden'] ) {
						return '';
					}

					return $content;
				},
			]
		);
	}
}Common/Standalone/Blocks/KeyPoints.php000064400000000571151536241200013741 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\Blocks;

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

/**
 * KeyPoints Block.
 *
 * @since 4.8.4
 */
class KeyPoints extends Blocks {
	/**
	 * Register the block.
	 *
	 * @since 4.8.4
	 *
	 * @return void
	 */
	public function register() {
		aioseo()->blocks->registerBlock( 'aioseo/key-points' );
	}
}Common/Standalone/Blocks/TableOfContents.php000064400000000616151536241200015046 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\Blocks;

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

/**
 * Table of Contents Block.
 *
 * @since 4.2.3
 */
class TableOfContents extends Blocks {
	/**
	 * Register the block.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	public function register() {
		aioseo()->blocks->registerBlock( 'aioseo/table-of-contents' );
	}
}Common/Standalone/BuddyPress/BuddyPress.php000064400000023313151536241200014746 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\BuddyPress;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Handles the BuddyPress integration with AIOSEO.
 *
 * @since 4.7.6
 */
class BuddyPress {
	/**
	 * Instance of the Tags class.
	 *
	 * @since 4.7.6
	 *
	 * @var Tags
	 */
	public $tags;

	/**
	 * Instance of the Component class.
	 *
	 * @since 4.7.6
	 *
	 * @var Component
	 */
	public $component;

	/**
	 * Instance of the Sitemap class.
	 *
	 * @since 4.7.6
	 *
	 * @var Sitemap
	 */
	public $sitemap = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.7.6
	 */
	public function __construct() {
		if (
			aioseo()->helpers->isAjaxCronRestRequest() ||
			! aioseo()->helpers->isPluginActive( 'buddypress' )
		) {
			return;
		}

		// Hook into `plugins_loaded` to ensure BuddyPress has loaded some necessary functions.
		add_action( 'plugins_loaded', [ $this, 'maybeLoad' ], 20 );
	}

	/**
	 * Hooked into `plugins_loaded` action hook.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	public function maybeLoad() {
		// If the BuddyPress version is below 12 we bail.
		if ( ! function_exists( 'bp_get_version' ) || version_compare( bp_get_version(), '12', '<' ) ) {
			return;
		}

		// If none of the necessary BuddyPress components are active we bail.
		if (
			! BuddyPressIntegration::isComponentActive( 'activity' ) &&
			! BuddyPressIntegration::isComponentActive( 'group' ) &&
			! BuddyPressIntegration::isComponentActive( 'member' )
		) {
			return;
		}

		$this->sitemap = new Sitemap();

		add_action( 'init', [ $this, 'setTags' ], 20 );
		add_action( 'bp_parse_query', [ $this, 'setComponent' ], 20 );
	}

	/**
	 * Hooked into `init` action hook.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	public function setTags() {
		$this->tags = new Tags();
	}

	/**
	 * Hooked into `bp_parse_query` action hook.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	public function setComponent() {
		$this->component = new Component();
	}

	/**
	 * Adds the BuddyPress fake post types to the list of post types, so they appear under e.g. Search Appearance.
	 *
	 * @since 4.7.6
	 *
	 * @param  array $postTypes       Public post types from {@see \AIOSEO\Plugin\Common\Traits\Helpers\Wp::getPublicPostTypes}.
	 * @param  bool  $namesOnly       Whether only the names should be included.
	 * @param  bool  $hasArchivesOnly Whether to only include post types which have archives.
	 * @param  array $args            Additional arguments.
	 * @return void
	 */
	public function maybeAddPostTypes( &$postTypes, $namesOnly, $hasArchivesOnly, $args ) {
		// If one of these CPTs is already registered we bail, so we don't overwrite them and possibly break something.
		if (
			post_type_exists( 'bp-activity' ) ||
			post_type_exists( 'bp-group' ) ||
			post_type_exists( 'bp-member' )
		) {
			return;
		}

		/**
		 * The BP components are registered with the `buddypress` CPT which is not viewable, so we add it here to include our metadata inside <head>.
		 * {@see \AIOSEO\Plugin\Common\Main\Head::wpHead}.
		 */
		if (
			$namesOnly &&
			doing_action( 'wp_head' )
		) {
			$postTypes = array_merge( $postTypes, [ 'buddypress' ] );

			return;
		}

		$fakePostTypes = $this->getFakePostTypes();

		if ( ! BuddyPressIntegration::isComponentActive( 'activity' ) ) {
			unset( $fakePostTypes['bp-activity'] );
		}

		if ( ! BuddyPressIntegration::isComponentActive( 'group' ) ) {
			unset( $fakePostTypes['bp-group'] );
		}

		if ( ! BuddyPressIntegration::isComponentActive( 'member' ) ) {
			unset( $fakePostTypes['bp-member'] );
		}

		if ( $hasArchivesOnly ) {
			$fakePostTypes = array_filter( $fakePostTypes, function ( $postType ) {
				return $postType['hasArchive'];
			} );
		}

		if ( $namesOnly ) {
			$fakePostTypes = array_keys( $fakePostTypes );
		}

		// 0. Below we'll add/merge the BuddyPress post types only under certain conditions.
		$fakePostTypes = array_values( $fakePostTypes );
		$currentScreen = aioseo()->helpers->getCurrentScreen();

		if (
			// 1. If the `buddypress` CPT is set in the list of post types to be included.
			( ! empty( $args['include'] ) && in_array( 'buddypress', $args['include'], true ) ) ||
			// 2. If the current request is for the sitemap.
			( ! empty( aioseo()->sitemap->filename ) && 'general' === ( aioseo()->sitemap->type ?? '' ) ) ||
			// 3. If we're on the Search Appearance screen.
			( $currentScreen && strpos( $currentScreen->id, 'aioseo-search-appearance' ) !== false ) ||
			// 4. If we're on the BuddyPress component front-end screen.
			BuddyPressIntegration::isComponentPage()
		) {
			$postTypes = array_merge( $postTypes, $fakePostTypes );
		}
	}

	/**
	 * Get edit links for the SEO Preview data.
	 *
	 * @since 4.7.6
	 *
	 * @return array
	 */
	public function getVueDataSeoPreview() {
		$data = [
			'editGoogleSnippetUrl' => '',
			'editObjectBtnText'    => '',
			'editObjectUrl'        => '',
		];

		list( $postType, $suffix ) = explode( '_', aioseo()->standalone->buddyPress->component->templateType );

		$bpFakePostTypes  = $this->getFakePostTypes();
		$fakePostTypeData = array_values( wp_list_filter( $bpFakePostTypes, [ 'name' => $postType ] ) );
		$fakePostTypeData = $fakePostTypeData[0] ?? [];
		if ( ! $fakePostTypeData ) {
			return $data;
		}

		if ( 'single' === $suffix ) {
			switch ( $postType ) {
				case 'bp-activity':
					$componentId = aioseo()->standalone->buddyPress->component->activity['id'];
					break;
				case 'bp-group':
					$componentId = aioseo()->standalone->buddyPress->component->group['id'];
					break;
				case 'bp-member':
					$componentId = aioseo()->standalone->buddyPress->component->author->ID;
					break;
				default:
					$componentId = 0;
			}
		}

		$scrollToId                   = 'aioseo-card-' . $postType . ( 'single' === $suffix ? 'SA' : 'ArchiveArchives' );
		$data['editGoogleSnippetUrl'] = 'single' === $suffix
			? admin_url( 'admin.php?page=aioseo-search-appearance' ) . '#/content-types'
			: admin_url( 'admin.php?page=aioseo-search-appearance' ) . '#/archives';
		$data['editGoogleSnippetUrl'] = add_query_arg( [
			'aioseo-scroll'    => $scrollToId,
			'aioseo-highlight' => $scrollToId
		], $data['editGoogleSnippetUrl'] );

		$data['editObjectBtnText'] = sprintf(
			// Translators: 1 - A noun for something that's being edited ("Post", "Page", "Article", "Product", etc.).
			esc_html__( 'Edit %1$s', 'all-in-one-seo-pack' ),
			'single' === $suffix ? $fakePostTypeData['singular'] : $fakePostTypeData['label']
		);

		list( , $component ) = explode( '-', $postType );

		$data['editObjectUrl'] = 'single' === $suffix
			? BuddyPressIntegration::getComponentEditUrl( $component, $componentId ?? 0 )
			: BuddyPressIntegration::callFunc( 'bp_get_admin_url', add_query_arg( 'page', 'bp-rewrites', 'admin.php' ) );

		return $data;
	}

	/**
	 * Retrieves the BuddyPress fake post types.
	 *
	 * @since 4.7.6
	 *
	 * @return array The BuddyPress fake post types.
	 */
	public function getFakePostTypes() {
		return [
			'bp-activity' => [
				'name'               => 'bp-activity',
				'label'              => sprintf(
					// Translators: 1 - The hard coded string 'BuddyPress'.
					_x( 'Activities (%1$s)', 'BuddyPress', 'all-in-one-seo-pack' ),
					'BuddyPress'
				),
				'singular'           => 'Activity',
				'icon'               => 'dashicons-buddicons-buddypress-logo',
				'hasExcerpt'         => false,
				'hasArchive'         => true,
				'hierarchical'       => false,
				'taxonomies'         => [],
				'slug'               => 'bp-activity',
				'buddyPress'         => true,
				'defaultTags'        => [
					'postTypes' => [
						'title'       => [
							'bp_activity_action',
							'separator_sa',
							'site_title',
						],
						'description' => [
							'bp_activity_content',
							'separator_sa'
						]
					]
				],
				'defaultTitle'       => '#bp_activity_action #separator_sa #site_title',
				'defaultDescription' => '#bp_activity_content',
			],
			'bp-group'    => [
				'name'               => 'bp-group',
				'label'              => sprintf(
					// Translators: 1 - The hard coded string 'BuddyPress'.
					_x( 'Groups (%1$s)', 'BuddyPress', 'all-in-one-seo-pack' ),
					'BuddyPress'
				),
				'singular'           => 'Group',
				'icon'               => 'dashicons-buddicons-buddypress-logo',
				'hasExcerpt'         => false,
				'hasArchive'         => true,
				'hierarchical'       => false,
				'taxonomies'         => [],
				'slug'               => 'bp-group',
				'buddyPress'         => true,
				'defaultTags'        => [
					'postTypes' => [
						'title'       => [
							'bp_group_name',
							'separator_sa',
							'site_title',
						],
						'description' => [
							'bp_group_description',
							'separator_sa'
						]
					]
				],
				'defaultTitle'       => '#bp_group_name #separator_sa #site_title',
				'defaultDescription' => '#bp_group_description',
			],
			'bp-member'   => [
				'name'               => 'bp-member',
				'label'              => sprintf(
					// Translators: 1 - The hard coded string 'BuddyPress'.
					_x( 'Members (%1$s)', 'BuddyPress', 'all-in-one-seo-pack' ),
					'BuddyPress'
				),
				'singular'           => 'Member',
				'icon'               => 'dashicons-buddicons-buddypress-logo',
				'hasExcerpt'         => false,
				'hasArchive'         => true,
				'hierarchical'       => false,
				'taxonomies'         => [],
				'slug'               => 'bp-member',
				'buddyPress'         => true,
				'defaultTags'        => [
					'postTypes' => [
						'title'       => [
							'author_name',
							'separator_sa',
							'site_title',
						],
						'description' => [
							'author_bio',
							'separator_sa'
						]
					]
				],
				'defaultTitle'       => '#author_name #separator_sa #site_title',
				'defaultDescription' => '#author_bio',
			],
		];
	}
}Common/Standalone/BuddyPress/Component.php000064400000030273151536241200014627 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\BuddyPress;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
use AIOSEO\Plugin\Common\Schema\Graphs as CommonGraphs;

/**
 * BuddyPress Component class.
 *
 * @since 4.7.6
 */
class Component {
	/**
	 * The current component template type.
	 *
	 * @since 4.7.6
	 *
	 * @var string|null
	 */
	public $templateType = null;

	/**
	 * The component ID.
	 *
	 * @since 4.7.6
	 *
	 * @var int
	 */
	public $id = 0;

	/**
	 * The component author.
	 *
	 * @since 4.7.6
	 *
	 * @var \WP_User|false
	 */
	public $author = false;

	/**
	 * The component date.
	 *
	 * @since 4.7.6
	 *
	 * @var int|false
	 */
	public $date = false;

	/**
	 * The activity single page data.
	 *
	 * @since 4.7.6
	 *
	 * @var array
	 */
	public $activity = [];

	/**
	 * The group single page data.
	 *
	 * @since 4.7.6
	 *
	 * @var array
	 */
	public $group = [];

	/**
	 * The type of the group archive page.
	 *
	 * @since 4.7.6
	 *
	 * @var array
	 */
	public $groupType = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.7.6
	 */
	public function __construct() {
		if ( is_admin() ) {
			return;
		}

		$this->setTemplateType();
		$this->setId();
		$this->setAuthor();
		$this->setDate();
		$this->setActivity();
		$this->setGroup();
		$this->setGroupType();
	}

	/**
	 * Sets the template type.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	private function setTemplateType() {
		if ( BuddyPressIntegration::callFunc( 'bp_is_single_activity' ) ) {
			$this->templateType = 'bp-activity_single';
		} elseif ( BuddyPressIntegration::callFunc( 'bp_is_group' ) ) {
			$this->templateType = 'bp-group_single';
		} elseif (
			BuddyPressIntegration::callFunc( 'bp_is_user' ) &&
			false === BuddyPressIntegration::callFunc( 'bp_is_single_activity' )
		) {
			$this->templateType = 'bp-member_single';
		} elseif ( BuddyPressIntegration::callFunc( 'bp_is_activity_directory' ) ) {
			$this->templateType = 'bp-activity_archive';
		} elseif ( BuddyPressIntegration::callFunc( 'bp_is_members_directory' ) ) {
			$this->templateType = 'bp-member_archive';
		} elseif ( BuddyPressIntegration::callFunc( 'bp_is_groups_directory' ) ) {
			$this->templateType = 'bp-group_archive';
		} elseif (
			BuddyPressIntegration::callFunc( 'bp_is_current_action', 'feed' ) &&
			BuddyPressIntegration::callFunc( 'bp_is_activity_component' )
		) {
			$this->templateType = 'bp-activity_feed';
		}
	}

	/**
	 * Sets the component ID.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	private function setId() {
		switch ( $this->templateType ) {
			case 'bp-activity_single':
				$id = get_query_var( 'bp_member_action' );
				break;
			case 'bp-group_single':
				$id = get_query_var( 'bp_group' );
				break;
			case 'bp-member_single':
				$id = get_query_var( 'bp_member' );
				break;
			default:
				$id = $this->id;
		}

		$this->id = $id;
	}

	/**
	 * Sets the component author.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	private function setAuthor() {
		switch ( $this->templateType ) {
			case 'bp-activity_single':
				if ( ! $this->activity ) {
					$this->setActivity();
				}

				if ( $this->activity ) {
					$this->author = get_user_by( 'id', $this->activity['user_id'] );

					return;
				}

				break;
			case 'bp-group_single':
				if ( ! $this->group ) {
					$this->setGroup();
				}

				if ( $this->group ) {
					$this->author = get_user_by( 'id', $this->group['creator_id'] );

					return;
				}

				break;
			case 'bp-member_single':
				$this->author = get_user_by( 'slug', $this->id );

				return;
		}
	}

	/**
	 * Sets the component date.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	private function setDate() {
		switch ( $this->templateType ) {
			case 'bp-activity_single':
				if ( ! $this->activity ) {
					$this->setActivity();
				}
				$date = strtotime( $this->activity['date_recorded'] );
				break;
			case 'bp-group_single':
				if ( ! $this->group ) {
					$this->setGroup();
				}
				$date = strtotime( $this->group['date_created'] );
				break;
			default:
				$date = $this->date;
		}

		$this->date = $date;
	}

	/**
	 * Sets the activity data.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	private function setActivity() {
		if ( 'bp-activity_single' !== $this->templateType ) {
			return;
		}

		$activities = BuddyPressIntegration::callFunc( 'bp_activity_get_specific', [
			'activity_ids'     => [ $this->id ],
			'display_comments' => true
		] );
		if ( ! empty( $activities['activities'] ) ) {
			list( $activity ) = current( $activities );

			$this->activity = (array) $activity;

			// The `content_rendered` is AIOSEO specific.
			$this->activity['content_rendered'] = $this->activity['content'] ?? '';
			if ( ! empty( $this->activity['content'] ) ) {
				$this->activity['content_rendered'] = apply_filters( 'bp_get_activity_content', $this->activity['content'] );
			}

			return;
		}

		$this->resetComponent();
	}

	/**
	 * Sets the group data.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	private function setGroup() {
		if ( 'bp-group_single' !== $this->templateType ) {
			return;
		}

		$group = BuddyPressIntegration::callFunc( 'bp_get_group_by', 'slug', $this->id );
		if ( ! empty( $group ) ) {
			$this->group = (array) $group;

			return;
		}

		$this->resetComponent();
	}

	/**
	 * Sets the group type.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	private function setGroupType() {
		if ( 'bp-group_archive' !== $this->templateType ) {
			return;
		}

		$type = BuddyPressIntegration::callFunc( 'bp_get_current_group_directory_type' );
		if ( ! $type ) {
			return;
		}

		$term = get_term_by( 'slug', $type, 'bp_group_type' );
		if ( ! $term ) {
			return;
		}

		$meta = get_metadata( 'term', $term->term_id );
		if ( ! $meta ) {
			return;
		}

		$this->groupType = [
			'singular' => $meta['bp_type_singular_name'][0] ?? '',
			'plural'   => $meta['bp_type_name'][0] ?? '',
		];
	}

	/**
	 * Resets some of the component properties.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	private function resetComponent() {
		$this->templateType = null;
		$this->id           = 0;
	}

	/**
	 * Retrieves the SEO metadata value.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $which The SEO metadata to get.
	 * @return string        The SEO metadata value.
	 */
	public function getMeta( $which ) {
		list( $postType, $suffix ) = explode( '_', $this->templateType );

		switch ( $which ) {
			case 'title':
				$meta = 'single' === $suffix
					? aioseo()->meta->title->getPostTypeTitle( $postType )
					: aioseo()->meta->title->getArchiveTitle( $postType );
				$meta = aioseo()->meta->description->helpers->bpSanitize( $meta, $this->id );
				break;
			case 'description':
				$meta = 'single' === $suffix
					? aioseo()->meta->description->getPostTypeDescription( $postType )
					: aioseo()->meta->description->getArchiveDescription( $postType );
				$meta = aioseo()->meta->description->helpers->bpSanitize( $meta, $this->id );
				break;
			case 'keywords':
				$meta = 'single' === $suffix
					? ''
					: aioseo()->meta->keywords->getArchiveKeywords( $postType );
				$meta = aioseo()->meta->keywords->prepareKeywords( $meta );
				break;
			case 'robots':
				$dynamicOptions = aioseo()->dynamicOptions->noConflict();
				if ( 'single' === $suffix && $dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
					aioseo()->meta->robots->globalValues( [ 'postTypes', $postType ], true );
				} elseif ( $dynamicOptions->searchAppearance->archives->has( $postType ) ) {
					aioseo()->meta->robots->globalValues( [ 'archives', $postType ], true );
				}

				$meta = aioseo()->meta->robots->metaHelper();
				break;
			case 'canonical':
				$meta = '';
				if ( 'single' === $suffix ) {
					if ( 'bp-member' === $postType ) {
						$meta = BuddyPressIntegration::getComponentSingleUrl( 'member', $this->author->ID );
					} elseif ( 'bp-group' === $postType ) {
						$meta = BuddyPressIntegration::getComponentSingleUrl( 'group', $this->group['id'] );
					}
				}
				break;
			default:
				$meta = '';
		}

		return $meta;
	}

	/**
	 * Determines the schema type for the current component.
	 *
	 * @since 4.7.6
	 *
	 * @param  \AIOSEO\Plugin\Common\Schema\Context $contextInstance The Context class instance.
	 * @return void
	 */
	public function determineSchemaGraphsAndContext( $contextInstance ) {
		list( $postType ) = explode( '_', $this->templateType );

		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		if ( $dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
			$defaultType = $dynamicOptions->searchAppearance->postTypes->{$postType}->schemaType;
			switch ( $defaultType ) {
				case 'Article':
					aioseo()->schema->graphs[] = $dynamicOptions->searchAppearance->postTypes->{$postType}->articleType;
					break;
				case 'WebPage':
					aioseo()->schema->graphs[] = $dynamicOptions->searchAppearance->postTypes->{$postType}->webPageType;
					break;
				default:
					aioseo()->schema->graphs[] = $defaultType;
			}
		}

		switch ( $this->templateType ) {
			case 'bp-activity_single':
				$datePublished = $this->activity['date_recorded'];
				$contextUrl    = BuddyPressIntegration::getComponentSingleUrl( 'activity', $this->activity['id'] );

				break;
			case 'bp-group_single':
				$datePublished = $this->group['date_created'];
				$contextUrl    = BuddyPressIntegration::getComponentSingleUrl( 'group', $this->group['id'] );

				break;
			case 'bp-member_single':
				aioseo()->schema->graphs[] = 'ProfilePage';

				$contextUrl = BuddyPressIntegration::getComponentSingleUrl( 'member', $this->author->ID );

				break;
			case 'bp-activity_archive':
			case 'bp-group_archive':
			case 'bp-member_archive':
				list( , $component ) = explode( '-', $postType );

				$contextUrl     = BuddyPressIntegration::getComponentArchiveUrl( $component );
				$breadcrumbType = 'CollectionPage';

				break;
			default:
				break;
		}

		if ( ! empty( $datePublished ) ) {
			CommonGraphs\Article\NewsArticle::setOverwriteGraphData( [
				'properties' => compact( 'datePublished' )
			] );
		}

		if ( ! empty( $contextUrl ) ) {
			$name                = aioseo()->meta->title->getTitle();
			$description         = aioseo()->meta->description->getDescription();
			$breadcrumbPositions = [
				'name'        => $name,
				'description' => $description,
				'url'         => $contextUrl,
			];

			if ( ! empty( $breadcrumbType ) ) {
				$breadcrumbPositions['type'] = $breadcrumbType;
			}

			aioseo()->schema->context = [
				'name'        => $name,
				'description' => $description,
				'url'         => $contextUrl,
				'breadcrumb'  => $contextInstance->breadcrumb->setPositions( $breadcrumbPositions ),
			];
		}
	}

	/**
	 * Gets the breadcrumbs for the current component.
	 *
	 * @since 4.7.6
	 *
	 * @return array
	 */
	public function getCrumbs() {
		$crumbs = [];
		switch ( $this->templateType ) {
			case 'bp-activity_single':
				$crumbs[] = aioseo()->breadcrumbs->makeCrumb(
					BuddyPressIntegration::callFunc( 'bp_get_directory_title', 'activity' ),
					BuddyPressIntegration::getComponentArchiveUrl( 'activity' )
				);
				$crumbs[] = aioseo()->breadcrumbs->makeCrumb( sanitize_text_field( $this->activity['action'] ) );
				break;
			case 'bp-group_single':
				$crumbs[] = aioseo()->breadcrumbs->makeCrumb(
					BuddyPressIntegration::callFunc( 'bp_get_directory_title', 'groups' ),
					BuddyPressIntegration::getComponentArchiveUrl( 'group' )
				);
				$crumbs[] = aioseo()->breadcrumbs->makeCrumb( $this->group['name'] );
				break;
			case 'bp-member_single':
				$crumbs[] = aioseo()->breadcrumbs->makeCrumb(
					BuddyPressIntegration::callFunc( 'bp_get_directory_title', 'members' ),
					BuddyPressIntegration::getComponentArchiveUrl( 'member' )
				);
				$crumbs[] = aioseo()->breadcrumbs->makeCrumb( $this->author->display_name );
				break;
			case 'bp-activity_archive':
				$crumbs[] = aioseo()->breadcrumbs->makeCrumb( BuddyPressIntegration::callFunc( 'bp_get_directory_title', 'activity' ) );
				break;
			case 'bp-group_archive':
				$crumbs[] = aioseo()->breadcrumbs->makeCrumb( BuddyPressIntegration::callFunc( 'bp_get_directory_title', 'groups' ) );
				break;
			case 'bp-member_archive':
				$crumbs[] = aioseo()->breadcrumbs->makeCrumb( BuddyPressIntegration::callFunc( 'bp_get_directory_title', 'members' ) );
				break;
			default:
				break;
		}

		return $crumbs;
	}
}Common/Standalone/BuddyPress/Sitemap.php000064400000015654151536241200014275 0ustar00<?php

namespace AIOSEO\Plugin\Common\Standalone\BuddyPress;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * BuddyPress Sitemap class.
 *
 * @since 4.7.6
 */
class Sitemap {
	/**
	 * Returns the indexes for the sitemap root index.
	 *
	 * @since 4.7.6
	 *
	 * @return array The indexes.
	 */
	public function indexes() {
		$indexes           = [];
		$includedPostTypes = array_flip( aioseo()->sitemap->helpers->includedPostTypes() );
		$filterPostTypes   = array_filter( [
			BuddyPressIntegration::isComponentActive( 'activity' ) && isset( $includedPostTypes['bp-activity'] ) ? 'bp-activity' : '',
			BuddyPressIntegration::isComponentActive( 'group' ) && isset( $includedPostTypes['bp-group'] ) ? 'bp-group' : '',
			BuddyPressIntegration::isComponentActive( 'member' ) && isset( $includedPostTypes['bp-member'] ) ? 'bp-member' : '',
		] );

		foreach ( $filterPostTypes as $postType ) {
			$indexes = array_merge( $indexes, $this->buildIndexesPostType( $postType ) );
		}

		return $indexes;
	}

	/**
	 * Builds BuddyPress related root indexes.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $postType The BuddyPress fake post type.
	 * @return array            The BuddyPress related root indexes.
	 */
	private function buildIndexesPostType( $postType ) {
		switch ( $postType ) {
			case 'bp-activity':
				return $this->buildIndexesActivity();
			case 'bp-group':
				return $this->buildIndexesGroup();
			case 'bp-member':
				return $this->buildIndexesMember();
			default:
				return [];
		}
	}

	/**
	 * Builds activity root indexes.
	 *
	 * @since 4.7.6
	 *
	 * @return array The activity root indexes.
	 */
	private function buildIndexesActivity() {
		$activityTable = aioseo()->core->db->prefix . 'bp_activity';
		$linksPerIndex = aioseo()->sitemap->linksPerIndex;
		$items         = aioseo()->core->db->execute(
			aioseo()->core->db->db->prepare(
				"SELECT id, date_recorded
				FROM (
					SELECT @row := @row + 1 AS rownum, id, date_recorded
					FROM (
						SELECT a.id, a.date_recorded FROM $activityTable as a
						WHERE a.is_spam = 0
							AND a.hide_sitewide = 0
							AND a.type NOT IN ('activity_comment', 'last_activity')
						ORDER BY a.date_recorded DESC
					) AS x
					CROSS JOIN (SELECT @row := 0) AS vars
					ORDER BY date_recorded DESC
				) AS y
				WHERE rownum = 1 OR rownum % %d = 1;",
				[
					$linksPerIndex
				]
			),
			true
		)->result();

		$totalItems = aioseo()->core->db->execute(
			"SELECT COUNT(*) as count
			FROM $activityTable as a
			WHERE a.is_spam = 0
				AND a.hide_sitewide = 0
				AND a.type NOT IN ('activity_comment', 'last_activity')
			",
			true
		)->result();

		$indexes = [];
		if ( $items ) {
			$filename = aioseo()->sitemap->filename;
			$count    = count( $items );
			for ( $i = 0; $i < $count; $i++ ) {
				$indexNumber = 0 !== $i && 1 < $count ? $i + 1 : '';

				$indexes[] = [
					'loc'     => aioseo()->helpers->localizedUrl( "/bp-activity-$filename$indexNumber.xml" ),
					'lastmod' => aioseo()->helpers->dateTimeToIso8601( $items[ $i ]->date_recorded ),
					'count'   => $linksPerIndex
				];
			}

			// We need to update the count of the last index since it won't necessarily be the same as the links per index.
			$indexes[ count( $indexes ) - 1 ]['count'] = $totalItems[0]->count - ( $linksPerIndex * ( $count - 1 ) );
		}

		return $indexes;
	}

	/**
	 * Builds group root indexes.
	 *
	 * @since 4.7.6
	 *
	 * @return array The group root indexes.
	 */
	private function buildIndexesGroup() {
		$groupsTable     = aioseo()->core->db->prefix . 'bp_groups';
		$groupsMetaTable = aioseo()->core->db->prefix . 'bp_groups_groupmeta';
		$linksPerIndex   = aioseo()->sitemap->linksPerIndex;
		$items           = aioseo()->core->db->execute(
			aioseo()->core->db->db->prepare(
				"SELECT id, date_modified
				FROM (
					SELECT @row := @row + 1 AS rownum, id, date_modified
					FROM (
						SELECT g.id, gm.group_id, MAX(gm.meta_value) as date_modified FROM $groupsTable as g
						INNER JOIN $groupsMetaTable AS gm ON g.id = gm.group_id
						WHERE g.status = 'public'
							AND gm.meta_key = 'last_activity'
						GROUP BY g.id
						ORDER BY date_modified DESC
					) AS x
					CROSS JOIN (SELECT @row := 0) AS vars
					ORDER BY date_modified DESC
				) AS y
				WHERE rownum = 1 OR rownum % %d = 1;",
				[
					$linksPerIndex
				]
			),
			true
		)->result();

		$totalItems = aioseo()->core->db->execute(
			"SELECT COUNT(*) as count
			FROM $groupsTable as g
			WHERE g.status = 'public'
			",
			true
		)->result();

		$indexes = [];
		if ( $items ) {
			$filename = aioseo()->sitemap->filename;
			$count    = count( $items );
			for ( $i = 0; $i < $count; $i++ ) {
				$indexNumber = 0 !== $i && 1 < $count ? $i + 1 : '';

				$indexes[] = [
					'loc'     => aioseo()->helpers->localizedUrl( "/bp-group-$filename$indexNumber.xml" ),
					'lastmod' => aioseo()->helpers->dateTimeToIso8601( $items[ $i ]->date_modified ),
					'count'   => $linksPerIndex
				];
			}

			// We need to update the count of the last index since it won't necessarily be the same as the links per index.
			$indexes[ count( $indexes ) - 1 ]['count'] = $totalItems[0]->count - ( $linksPerIndex * ( $count - 1 ) );
		}

		return $indexes;
	}

	/**
	 * Builds member root indexes.
	 *
	 * @since 4.7.6
	 *
	 * @return array The member root indexes.
	 */
	private function buildIndexesMember() {
		$activityTable = aioseo()->core->db->prefix . 'bp_activity';
		$linksPerIndex = aioseo()->sitemap->linksPerIndex;
		$items         = aioseo()->core->db->execute(
			aioseo()->core->db->db->prepare(
				"SELECT user_id, date_recorded
				FROM (
					SELECT @row := @row + 1 AS rownum, user_id, date_recorded
					FROM (
						SELECT a.user_id, a.date_recorded FROM $activityTable as a
						WHERE a.component = 'members'
							AND a.type = 'last_activity'
						ORDER BY a.date_recorded DESC
					) AS x
					CROSS JOIN (SELECT @row := 0) AS vars
					ORDER BY date_recorded DESC
				) AS y
				WHERE rownum = 1 OR rownum % %d = 1;",
				[
					$linksPerIndex
				]
			),
			true
		)->result();

		$totalItems = aioseo()->core->db->execute(
			"SELECT COUNT(*) as count
			FROM $activityTable as a
			WHERE a.component = 'members'
				AND a.type = 'last_activity'
			",
			true
		)->result();

		$indexes = [];
		if ( $items ) {
			$filename = aioseo()->sitemap->filename;
			$count    = count( $items );
			for ( $i = 0; $i < $count; $i++ ) {
				$indexNumber = 0 !== $i && 1 < $count ? $i + 1 : '';

				$indexes[] = [
					'loc'     => aioseo()->helpers->localizedUrl( "/bp-member-$filename$indexNumber.xml" ),
					'lastmod' => aioseo()->helpers->dateTimeToIso8601( $items[ $i ]->date_recorded ),
					'count'   => $linksPerIndex
				];
			}

			// We need to update the count of the last index since it won't necessarily be the same as the links per index.
			$indexes[ count( $indexes ) - 1 ]['count'] = $totalItems[0]->count - ( $linksPerIndex * ( $count - 1 ) );
		}

		return $indexes;
	}
}Common/Standalone/BuddyPress/Tags.php000064400000023531151536241200013562 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\BuddyPress;

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

/**
 * BuddyPress Tags class.
 *
 * @since 4.7.6
 */
class Tags {
	/**
	 * Class constructor.
	 *
	 * @since 4.7.6
	 */
	public function __construct() {
		aioseo()->tags->addContext( $this->getContexts() );
		aioseo()->tags->addTags( $this->getTags() );
	}

	/**
	 * Retrieves the contexts for BuddyPress.
	 *
	 * @since 4.7.6
	 *
	 * @return array An array of contextual data.
	 */
	public function getContexts() {
		return [
			'bp-activityTitle'              => [
				'author_first_name',
				'author_last_name',
				'author_name',
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'post_date',
				'post_day',
				'post_month',
				'post_year',
				'separator_sa',
				'site_title',
				'tagline',
				'bp_activity_action',
				'bp_activity_content',
			],
			'bp-activityArchiveTitle'       => [
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'separator_sa',
				'site_title',
				'tagline',
				'archive_title',
			],
			'bp-activityDescription'        => [
				'author_first_name',
				'author_last_name',
				'author_name',
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'post_date',
				'post_day',
				'post_month',
				'post_year',
				'separator_sa',
				'site_title',
				'tagline',
				'bp_activity_action',
				'bp_activity_content',
			],
			'bp-activityArchiveDescription' => [
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'separator_sa',
				'site_title',
				'tagline',
				'archive_title',
			],
			'bp-groupTitle'                 => [
				'author_first_name',
				'author_last_name',
				'author_name',
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'post_date',
				'post_day',
				'post_month',
				'post_year',
				'separator_sa',
				'site_title',
				'tagline',
				'bp_group_name',
				'bp_group_description',
			],
			'bp-groupArchiveTitle'          => [
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'separator_sa',
				'site_title',
				'tagline',
				'archive_title',
				'bp_group_type_singular_name',
				'bp_group_type_plural_name',
			],
			'bp-groupDescription'           => [
				'author_first_name',
				'author_last_name',
				'author_name',
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'post_date',
				'post_day',
				'post_month',
				'post_year',
				'separator_sa',
				'site_title',
				'tagline',
				'bp_group_name',
				'bp_group_description',
			],
			'bp-groupArchiveDescription'    => [
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'separator_sa',
				'site_title',
				'tagline',
				'archive_title',
				'bp_group_type_singular_name',
				'bp_group_type_plural_name',
			],
			'bp-memberTitle'                => [
				'author_first_name',
				'author_last_name',
				'author_name',
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'separator_sa',
				'site_title',
				'tagline',
			],
			'bp-memberArchiveTitle'         => [
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'separator_sa',
				'site_title',
				'tagline',
				'archive_title',
			],
			'bp-memberDescription'          => [
				'author_first_name',
				'author_last_name',
				'author_name',
				'author_bio',
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'separator_sa',
				'site_title',
				'tagline',
			],
			'bp-memberArchiveDescription'   => [
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'separator_sa',
				'site_title',
				'tagline',
				'archive_title',
			],
		];
	}

	/**
	 * Retrieves the custom tags for BuddyPress.
	 *
	 * @since 4.7.6
	 *
	 * @return array An array of tags.
	 */
	public function getTags() {
		return [
			[
				'id'          => 'bp_activity_action',
				'name'        => _x( 'Activity Action', 'BuddyPress', 'all-in-one-seo-pack' ),
				'description' => _x( 'The activity action.', 'BuddyPress', 'all-in-one-seo-pack' ),
				'instance'    => $this,
			],
			[
				'id'          => 'bp_activity_content',
				'name'        => _x( 'Activity Content', 'BuddyPress', 'all-in-one-seo-pack' ),
				'description' => _x( 'The activity content.', 'BuddyPress', 'all-in-one-seo-pack' ),
				'instance'    => $this,
			],
			[
				'id'          => 'bp_group_name',
				'name'        => _x( 'Group Name', 'BuddyPress', 'all-in-one-seo-pack' ),
				'description' => _x( 'The group name.', 'BuddyPress', 'all-in-one-seo-pack' ),
				'instance'    => $this,
			],
			[
				'id'          => 'bp_group_description',
				'name'        => _x( 'Group Description', 'BuddyPress', 'all-in-one-seo-pack' ),
				'description' => _x( 'The group description.', 'BuddyPress', 'all-in-one-seo-pack' ),
				'instance'    => $this,
			],
			[
				'id'          => 'bp_group_type_singular_name',
				'name'        => _x( 'Group Type Singular Name', 'BuddyPress', 'all-in-one-seo-pack' ),
				'description' => _x( 'The group type singular name.', 'BuddyPress', 'all-in-one-seo-pack' ),
				'instance'    => $this,
			],
			[
				'id'          => 'bp_group_type_plural_name',
				'name'        => _x( 'Group Type Plural Name', 'BuddyPress', 'all-in-one-seo-pack' ),
				'description' => _x( 'The group type plural name.', 'BuddyPress', 'all-in-one-seo-pack' ),
				'instance'    => $this,
			],
		];
	}

	/**
	 * Replace the tags in the string provided.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $string The string to look for tags in.
	 * @param  int    $id     The object ID.
	 * @return string         The string with tags replaced.
	 */
	public function replaceTags( $string, $id ) {
		if ( ! $string || ! preg_match( '/' . aioseo()->tags->denotationChar . '/', $string ) ) {
			return $string;
		}

		foreach ( array_unique( aioseo()->helpers->flatten( $this->getContexts() ) ) as $tag ) {
			$tagId   = aioseo()->tags->denotationChar . $tag;
			$pattern = "/$tagId(?![a-zA-Z0-9_])/im";
			if ( preg_match( $pattern, $string ) ) {
				$tagValue = $this->getTagValue( [ 'id' => $tag ], $id );
				$string   = preg_replace( $pattern, '%|%' . aioseo()->helpers->escapeRegexReplacement( $tagValue ), $string );
			}
		}

		return str_replace( '%|%', '', $string );
	}

	/**
	 * Get the value of the tag to replace.
	 *
	 * @since 4.7.6
	 *
	 * @param  array    $tag        The tag to look for.
	 * @param  int|null $id         The object ID.
	 * @param  bool     $sampleData Whether to fill empty values with sample data.
	 * @return string               The value of the tag.
	 */
	public function getTagValue( $tag, $id = null, $sampleData = false ) {
		$sampleData = $sampleData || empty( aioseo()->standalone->buddyPress->component->templateType );

		switch ( $tag['id'] ) {
			case 'author_bio':
				$out = $sampleData
					? __( 'Sample author biography', 'all-in-one-seo-pack' )
					: aioseo()->standalone->buddyPress->component->author->description;
				break;
			case 'author_first_name':
				$out = $sampleData
					? wp_get_current_user()->first_name
					: aioseo()->standalone->buddyPress->component->author->first_name;
				break;
			case 'author_last_name':
				$out = $sampleData
					? wp_get_current_user()->last_name
					: aioseo()->standalone->buddyPress->component->author->last_name;
				break;
			case 'author_name':
				$out = $sampleData
					? wp_get_current_user()->display_name
					: aioseo()->standalone->buddyPress->component->author->display_name;
				break;
			case 'post_date':
				$out = $sampleData
					? aioseo()->tags->formatDateAsI18n( date_i18n( 'U' ) )
					: aioseo()->tags->formatDateAsI18n( aioseo()->standalone->buddyPress->component->date );
				break;
			case 'post_day':
				$out = $sampleData
					? date_i18n( 'd' )
					: date( 'd', aioseo()->standalone->buddyPress->component->date );
				break;
			case 'post_month':
				$out = $sampleData
					? date_i18n( 'F' )
					: date( 'F', aioseo()->standalone->buddyPress->component->date );
				break;
			case 'post_year':
				$out = $sampleData
					? date_i18n( 'Y' )
					: date( 'Y', aioseo()->standalone->buddyPress->component->date );
				break;
			case 'archive_title':
				$out = $sampleData
					? __( 'Sample Archive Title', 'all-in-one-seo-pack' )
					: esc_html( get_the_title() );
				break;
			case 'bp_activity_action':
				$out = $sampleData
					? _x( 'Sample Activity Action', 'BuddyPress', 'all-in-one-seo-pack' )
					: aioseo()->standalone->buddyPress->component->activity['action'];
				break;
			case 'bp_activity_content':
				$out = $sampleData
					? _x( 'Sample activity content', 'BuddyPress', 'all-in-one-seo-pack' )
					: aioseo()->standalone->buddyPress->component->activity['content_rendered'];
				break;
			case 'bp_group_name':
				$out = $sampleData
					? _x( 'Sample Group Name', 'BuddyPress', 'all-in-one-seo-pack' )
					: aioseo()->standalone->buddyPress->component->group['name'];
				break;
			case 'bp_group_description':
				$out = $sampleData
					? _x( 'Sample group description', 'BuddyPress', 'all-in-one-seo-pack' )
					: aioseo()->standalone->buddyPress->component->group['description'];
				break;
			case 'bp_group_type_singular_name':
				$out = $sampleData ? _x( 'Sample Type Singular', 'BuddyPress', 'all-in-one-seo-pack' ) : '';
				if ( ! empty( aioseo()->standalone->buddyPress->component->groupType ) ) {
					$out = aioseo()->standalone->buddyPress->component->groupType['singular'];
				}
				break;
			case 'bp_group_type_plural_name':
				$out = $sampleData ? _x( 'Sample Type Plural', 'BuddyPress', 'all-in-one-seo-pack' ) : '';
				if ( ! empty( aioseo()->standalone->buddyPress->component->groupType ) ) {
					$out = aioseo()->standalone->buddyPress->component->groupType['plural'];
				}
				break;
			default:
				$out = aioseo()->tags->getTagValue( $tag, $id );
		}

		return $out ?? '';
	}
}Common/Standalone/DetailsColumn.php000064400000020310151536241200013333 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Handles the AIOSEO Details post column.
 *
 * @since 4.2.0
 */
class DetailsColumn {
	/**
	 * The slug for the script.
	 *
	 * @since 4.2.0
	 *
	 * @var string
	 */
	protected $scriptSlug = 'src/vue/standalone/posts-table/main.js';

	/**
	 * Class constructor.
	 *
	 * @since 4.2.0
	 */
	public function __construct() {
		if ( wp_doing_ajax() ) {
			add_action( 'init', [ $this, 'addPostColumnsAjax' ], 1 );
		}

		if ( ! is_admin() || wp_doing_cron() ) {
			return;
		}

		add_action( 'current_screen', [ $this, 'registerColumnHooks' ], 1 );
	}

	/**
	 * Adds the columns to the page/post types.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function registerColumnHooks() {
		$screen = aioseo()->helpers->getCurrentScreen();
		if ( empty( $screen->base ) || empty( $screen->post_type ) ) {
			return;
		}

		if ( ! $this->shouldRegisterColumn( $screen->base, $screen->post_type ) ) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScripts' ] );

		if ( 'product' === $screen->post_type ) {
			add_filter( 'manage_edit-product_columns', [ $this, 'addColumn' ] );
			add_action( 'manage_posts_custom_column', [ $this, 'renderColumn' ], 10, 2 );

			return;
		}

		if ( 'attachment' === $screen->post_type ) {
			$enabled = apply_filters( 'aioseo_image_seo_media_columns', true );
			if ( ! $enabled ) {
				return;
			}

			add_filter( 'manage_media_columns', [ $this, 'addColumn' ] );
			add_action( 'manage_media_custom_column', [ $this, 'renderColumn' ], 10, 2 );

			return;
		}

		add_filter( "manage_edit-{$screen->post_type}_columns", [ $this, 'addColumn' ] );
		add_action( "manage_{$screen->post_type}_posts_custom_column", [ $this, 'renderColumn' ], 10, 2 );
	}

	/**
	 * Registers our post columns after a post has been quick-edited.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	public function addPostColumnsAjax() {
		if (
			! isset( $_POST['_inline_edit'], $_POST['post_ID'], $_POST['aioseo-has-details-column'] ) ||
			! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_inline_edit'] ) ), 'inlineeditnonce' )
		) {
			return;
		}

		$postId = (int) $_POST['post_ID'];
		if ( ! $postId ) {
			return;
		}

		$post     = get_post( $postId );
		$postType = $post->post_type;

		add_filter( "manage_edit-{$postType}_columns", [ $this, 'addColumn' ] );
		add_action( "manage_{$postType}_posts_custom_column", [ $this, 'renderColumn' ], 10, 2 );
	}

	/**
	 * Enqueues the JS/CSS for the page/posts table page.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function enqueueScripts() {
		$data          = aioseo()->helpers->getVueData();
		$data['posts'] = [];
		$data['terms'] = [];

		aioseo()->core->assets->load( $this->scriptSlug, [], $data );
	}

	/**
	 * Adds the AIOSEO Details column to the page/post tables in the admin.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $columns The columns we are adding ours onto.
	 * @return array          The modified columns.
	 */
	public function addColumn( $columns ) {
		$canManageSeo = apply_filters( 'aioseo_manage_seo', 'aioseo_manage_seo' );
		if (
			! current_user_can( $canManageSeo ) &&
			(
				! current_user_can( 'aioseo_page_general_settings' ) &&
				! current_user_can( 'aioseo_page_analysis' )
			)
		) {
			return $columns;
		}

		// Translators: 1 - The short plugin name ("AIOSEO").
		$columns['aioseo-details'] = sprintf( esc_html__( '%1$s Details', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME );

		return $columns;
	}

	/**
	 * Renders the column in the page/post table.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $columnName The column name.
	 * @param  int    $postId     The current rows, post id.
	 * @return void
	 */
	public function renderColumn( $columnName, $postId = 0 ) {
		if ( ! current_user_can( 'edit_post', $postId ) && ! current_user_can( 'aioseo_manage_seo' ) ) {
			return;
		}

		if ( 'aioseo-details' !== $columnName ) {
			return;
		}

		// Add this column/post to the localized array.
		global $wp_scripts; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if (
			! is_object( $wp_scripts ) || // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			! method_exists( $wp_scripts, 'get_data' ) || // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			! method_exists( $wp_scripts, 'add_data' ) // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		) {
			return;
		}

		$data = null;
		if ( is_object( $wp_scripts ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			$data = $wp_scripts->get_data( 'aioseo/js/' . $this->scriptSlug, 'data' ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		}

		if ( ! is_array( $data ) ) {
			$data = json_decode( str_replace( 'var aioseo = ', '', substr( $data, 0, -1 ) ), true );
		}

		// We have to temporarily modify the query here since the query incorrectly identifies
		// the current page as a category page when posts are filtered by a specific category.
		// phpcs:disable Squiz.NamingConventions.ValidVariableName
		global $wp_query;
		$originalQuery         = clone $wp_query;
		$wp_query->is_category = false;
		$wp_query->is_tag      = false;
		$wp_query->is_tax      = false;
		// phpcs:enable Squiz.NamingConventions.ValidVariableName

		$posts    = ! empty( $data['posts'] ) ? $data['posts'] : [];
		$postData = $this->getPostData( $postId, $columnName );

		$addonsColumnData = array_filter( aioseo()->addons->doAddonFunction( 'admin', 'renderColumnData', [
			$columnName,
			$postId,
			$postData
		] ) );

		$wp_query = $originalQuery; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		foreach ( $addonsColumnData as $addonColumnData ) {
			$postData = array_merge( $postData, $addonColumnData );
		}

		$posts[]       = $postData;
		$data['posts'] = $posts;

		$wp_scripts->add_data( 'aioseo/js/' . $this->scriptSlug, 'data', '' ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		wp_localize_script( 'aioseo/js/' . $this->scriptSlug, 'aioseo', $data );

		require AIOSEO_DIR . '/app/Common/Views/admin/posts/columns.php';
	}

	/**
	 * Gets the post data for the column.
	 *
	 * @since 4.5.0
	 *
	 * @param  int    $postId     The Post ID.
	 * @param  string $columnName The column name.
	 * @return array              The post data.
	 */
	protected function getPostData( $postId, $columnName ) {
		$nonce    = wp_create_nonce( "aioseo_meta_{$columnName}_{$postId}" );
		$thePost  = Models\Post::getPost( $postId );
		$postType = get_post_type( $postId );
		$postData = [
			'id'                 => $postId,
			'columnName'         => $columnName,
			'nonce'              => $nonce,
			'title'              => $thePost->title,
			'defaultTitle'       => aioseo()->meta->title->getPostTypeTitle( $postType ),
			'showTitle'          => apply_filters( 'aioseo_details_column_post_show_title', true, $postId ),
			'description'        => $thePost->description,
			'defaultDescription' => aioseo()->meta->description->getPostTypeDescription( $postType ),
			'showDescription'    => apply_filters( 'aioseo_details_column_post_show_description', true, $postId ),
			'value'              => ! empty( $thePost->seo_score ) ? (int) $thePost->seo_score : 0,
			'showMedia'          => false,
			'isSpecialPage'      => aioseo()->helpers->isSpecialPage( $postId ),
			'postType'           => $postType,
			'isPostVisible'      => aioseo()->helpers->isPostPubliclyViewable( $postId )
		];

		return $postData;
	}

	/**
	 * Checks whether the AIOSEO Details column should be registered.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether the column should be registered.
	 */
	public function shouldRegisterColumn( $screen, $postType ) {
		// Only allow users with the correct permissions to see the column.
		if ( ! current_user_can( 'aioseo_page_general_settings' ) ) {
			return false;
		}

		if ( 'type' === $postType ) {
			$postType = '_aioseo_type';
		}

		if ( 'edit' === $screen || 'upload' === $screen ) {
			if (
				aioseo()->options->advanced->postTypes->all &&
				in_array( $postType, aioseo()->helpers->getPublicPostTypes( true ), true )
			) {
				return true;
			}

			$postTypes = aioseo()->options->advanced->postTypes->included;
			if ( in_array( $postType, $postTypes, true ) ) {
				return true;
			}
		}

		return false;
	}
}Common/Standalone/FlyoutMenu.php000064400000003405151536241200012705 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

/**
 * Handles the flyout menu.
 *
 * @since 4.2.0
 */
class FlyoutMenu {
	/**
	 * Class constructor.
	 *
	 * @since 4.2.0
	 */
	public function __construct() {
		if (
			! is_admin() ||
			wp_doing_ajax() ||
			wp_doing_cron() ||
			! $this->isEnabled()
		) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAssets' ], 11 );
		add_filter( 'admin_body_class', [ $this, 'addBodyClass' ] );
	}

	/**
	 * Enqueues the required assets.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	public function enqueueAssets() {
		if ( ! $this->shouldEnqueue() ) {
			return;
		}

		aioseo()->core->assets->load( 'src/vue/standalone/flyout-menu/main.js' );
	}

	/**
	 * Filters the CSS classes for the body tag in the admin.
	 *
	 * @since 4.2.0
	 *
	 * @param  string $classes Space-separated list of CSS classes.
	 * @return string          Space-separated list of CSS classes.
	 */
	public function addBodyClass( $classes ) {
		if ( $this->shouldEnqueue() ) {
			// This adds a bottom margin to our menu so that we push the footer down and prevent the flyout menu from overlapping the "Save Changes" button.
			$classes .= ' aioseo-flyout-menu-enabled ';
		}

		return $classes;
	}

	/**
	 * Checks whether the flyout menu script should be enqueued.
	 *
	 * @since 4.2.0
	 *
	 * @return bool Whether the flyout menu script should be enqueued.
	 */
	private function shouldEnqueue() {
		return aioseo()->admin->isAioseoScreen();
	}

	/**
	 * Checks whether the flyout menu is enabled.
	 *
	 * @since 4.2.0
	 *
	 * @return bool Whether the flyout menu is enabled.
	 */
	public function isEnabled() {
		return apply_filters( 'aioseo_flyout_menu_enable', true );
	}
}Common/Standalone/HeadlineAnalyzer.php000064400000040326151536241200014020 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

/**
 * Handles the headline analysis.
 *
 * @since 4.1.2
 */
class HeadlineAnalyzer {
	/**
	 * Class constructor.
	 *
	 * @since 4.1.2
	 */
	public function __construct() {
		if ( ! is_admin() || wp_doing_cron() ) {
			return;
		}

		add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue' ] );

		if ( ! aioseo()->options->advanced->headlineAnalyzer ) {
			return;
		}

		add_filter( 'monsterinsights_headline_analyzer_enabled', '__return_false' );
		add_filter( 'exactmetrics_headline_analyzer_enabled', '__return_false' );
	}

	/**
	 * Enqueues the headline analyzer.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	public function enqueue() {
		if (
			! aioseo()->helpers->isScreenBase( 'post' ) ||
			! aioseo()->access->hasCapability( 'aioseo_page_analysis' )
		) {
			return;
		}

		if ( ! aioseo()->options->advanced->headlineAnalyzer ) {
			return;
		}

		$path = '/vendor/jwhennessey/phpinsight/autoload.php';
		if ( ! aioseo()->core->fs->exists( AIOSEO_DIR . $path ) ) {
			return;
		}
		require AIOSEO_DIR . $path;

		aioseo()->core->assets->load( 'src/vue/standalone/headline-analyzer/main.js' );
	}

	/**
	 * Returns the result of the analsyis.
	 *
	 * @since 4.1.2
	 *
	 * @param  string $title The title.
	 * @return array         The result.
	 */
	public function getResult( $title ) {
		$result = $this->getHeadlineScore( html_entity_decode( $title ) );

		return [
			'result'   => $result,
			'analysed' => ! $result->err,
			'sentence' => ucwords( wp_unslash( sanitize_text_field( $title ) ) ),
			'score'    => ! empty( $result->score ) ? $result->score : 0
		];
	}

	/**
	 * Returns the score.
	 *
	 * @since 4.1.2
	 *
	 * @param  string    $title The title.
	 * @return \stdClass        The result.
	 */
	public function getHeadlineScore( $title ) {
		$result                           = new \stdClass();
		$result->originalExplodedHeadline = explode( ' ', wp_unslash( $title ) );

		// Strip useless characters and whitespace.
		$title = preg_replace( '/[^A-Za-z0-9 ]/', '', (string) $title );
		$title = preg_replace( '!\s+!', ' ', (string) $title );
		$title = strtolower( $title );

		$result->input = $title;

		// If the headline is invalid, return an error.
		if ( ! $title || ' ' === $title || trim( $title ) === '' ) {
			$result->err = true;
			$result->msg = 'The headline is invalid.';

			return $result;
		}

		$totalScore               = 0;
		$explodedHeadline         = explode( ' ', $title );
		$result->explodedHeadline = $explodedHeadline;
		$result->err              = false;

		// The optimal length is 55 characters.
		$result->length = strlen( str_replace( ' ', '', $title ) );
		$totalScore     = $totalScore + 3;

		//phpcs:disable Squiz.ControlStructures.ControlSignature
		if ( $result->length <= 19 ) { $totalScore += 5; }
		elseif ( $result->length >= 20 && $result->length <= 34 ) { $totalScore += 8; }
		elseif ( $result->length >= 35 && $result->length <= 66 ) { $totalScore += 11; }
		elseif ( $result->length >= 67 && $result->length <= 79 ) { $totalScore += 8; }
		elseif ( $result->length >= 80 ) { $totalScore += 5; }

		// The average headline is 6-7 words long.
		$result->wordCount = count( $explodedHeadline );
		$totalScore        = $totalScore + 3;

		if ( 0 === $result->wordCount ) { $totalScore = 0; }
		elseif ( $result->wordCount >= 2 && $result->wordCount <= 4 ) { $totalScore += 5; }
		elseif ( $result->wordCount >= 5 && $result->wordCount <= 9 ) { $totalScore += 11; }
		elseif ( $result->wordCount >= 10 && $result->wordCount <= 11 ) { $totalScore += 8; }
		elseif ( $result->wordCount >= 12 ) { $totalScore += 5; }

		// Check for power words, emotional words, etc.
		$result->powerWords               = $this->matchWords( $result->input, $result->explodedHeadline, $this->powerWords() );
		$result->powerWordsPercentage     = count( $result->powerWords ) / $result->wordCount;
		$result->emotionWords             = $this->matchWords( $result->input, $result->explodedHeadline, $this->emotionPowerWords() );
		$result->emotionalWordsPercentage = count( $result->emotionWords ) / $result->wordCount;
		$result->commonWords              = $this->matchWords( $result->input, $result->explodedHeadline, $this->commonWords() );
		$result->commonWordsPercentage    = count( $result->commonWords ) / $result->wordCount;
		$result->uncommonWords            = $this->matchWords( $result->input, $result->explodedHeadline, $this->uncommonWords() );
		$result->uncommonWordsPercentage  = count( $result->uncommonWords ) / $result->wordCount;
		$result->detectedWordTypes        = [];

		if ( $result->emotionalWordsPercentage < 0.1 ) {
			$result->detectedWordTypes[] = 'emotion';
		} else {
			$totalScore = $totalScore + 15;
		}

		if ( $result->commonWordsPercentage < 0.2 ) {
			$result->detectedWordTypes[] = 'common';
		} else {
			$totalScore = $totalScore + 11;
		}

		if ( $result->uncommonWordsPercentage < 0.1 ) {
			$result->detectedWordTypes[] = 'uncommon';
		} else {
			$totalScore = $totalScore + 15;
		}

		if ( count( $result->powerWords ) < 1 ) {
			$result->detectedWordTypes[] = 'power';
		} else {
			$totalScore = $totalScore + 19;
		}

		if (
			$result->emotionalWordsPercentage >= 0.1 &&
			$result->commonWordsPercentage >= 0.2 &&
			$result->uncommonWordsPercentage >= 0.1 &&
			count( $result->powerWords ) >= 1
		) {
			$totalScore = $totalScore + 3;
		}

		$sentiment         = new \PHPInsight\Sentiment();
		$sentimentClass    = $sentiment->categorise( $title );
		$result->sentiment = $sentimentClass;

		$totalScore = $totalScore + ( 'pos' === $result->sentiment ? 10 : ( 'neg' === $result->sentiment ? 10 : 7 ) );

		$headlineTypes = [];
		if ( strpos( $title, 'how to' ) !== false || strpos( $title, 'howto' ) !== false ) {
			$headlineTypes[] = __( 'How-To', 'all-in-one-seo-pack' );
			$totalScore      = $totalScore + 7;
		}

		$listWords = array_intersect( $explodedHeadline, $this->numericalIndicators() );
		if ( preg_match( '~[0-9]+~', (string) $title ) || ! empty( $listWords ) ) {
			$headlineTypes[] = __( 'List', 'all-in-one-seo-pack' );
			$totalScore      = $totalScore + 7;
		}

		if ( in_array( $explodedHeadline[0], $this->primaryQuestionIndicators(), true ) ) {
			if ( in_array( $explodedHeadline[1], $this->secondaryQuestionIndicators(), true ) ) {
				$headlineTypes[] = __( 'Question', 'all-in-one-seo-pack' );
				$totalScore      = $totalScore + 7;
			}
		}

		if ( empty( $headlineTypes ) ) {
			$headlineTypes[] = __( 'General', 'all-in-one-seo-pack' );
			$totalScore      = $totalScore + 5;
		}

		$result->headlineTypes = $headlineTypes;
		$result->score         = $totalScore >= 93 ? 93 : $totalScore;

		return $result;
	}

	/**
	* Tries to find matches for power words, emotional words, etc. in the headline.
	*
	* @since 4.1.2
	*
	* @param  string $headline         The headline.
	* @param  array  $explodedHeadline The exploded headline.
	* @param  array  $words            The words to match.
	* @return array                    The matches that were found.
	*/
	public function matchWords( $headline, $explodedHeadline, $words ) {
		$foundMatches = [];
		foreach ( $words as $word ) {
			$strippedWord = preg_replace( '/[^A-Za-z0-9 ]/', '', (string) $word );

			// Check if word is a phrase.
			if ( strpos( $word, ' ' ) !== false ) {
				if ( strpos( $headline, $strippedWord ) !== false ) {
					$foundMatches[] = $word;
				}
				continue;
			}
			// Check if it is a single word.
			if ( in_array( $strippedWord, $explodedHeadline, true ) ) {
				$foundMatches[] = $word;
			}
		}

		return $foundMatches;
	}

	/**
	 * Returns a list of numerical indicators.
	 *
	 * @since 4.1.2
	 *
	 * @return array The list of numerical indicators.
	 */
	private function numericalIndicators() {
		return [
			'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'eleven', 'twelve', 'thirt', 'fift', 'hundred', 'thousand' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
		];
	}

	/**
	 * Returns a list of primary question indicators.
	 *
	 * @since 4.1.2
	 *
	 * @return array The list of primary question indicators.
	 */
	private function primaryQuestionIndicators() {
		return [
			'where', 'when', 'how', 'what', 'have', 'has', 'does', 'do', 'can', 'are', 'will' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
		];
	}

	/**
	 * Returns a list of secondary question indicators.
	 *
	 * @since 4.1.2
	 *
	 * @return array The list of secondary question indicators.
	 */
	private function secondaryQuestionIndicators() {
		return [
			'you', 'they', 'he', 'she', 'your', 'it', 'they', 'my', 'have', 'has', 'does', 'do', 'can', 'are', 'will' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
		];
	}

	/**
	 * Returns a list of power words.
	 *
	 * @since 4.1.2
	 *
	 * @return array The list of power words.
	 */
	private function powerWords() {
		return [
			'great', 'free', 'focus', 'remarkable', 'confidential', 'sale', 'wanted', 'obsession', 'sizable', 'new', 'absolutely lowest', 'surging', 'wonderful', 'professional', 'interesting', 'revisited', 'delivered', 'guaranteed', 'challenge', 'unique', 'secrets', 'special', 'lifetime', 'bargain', 'scarce', 'tested', 'highest', 'hurry', 'alert famous', 'improved', 'expert', 'daring', 'strong', 'immediately', 'advice', 'pioneering', 'unusual', 'limited', 'the truth about', 'destiny', 'outstanding', 'simplistic', 'compare', 'unsurpassed', 'energy', 'powerful', 'colorful', 'genuine', 'instructive', 'big', 'affordable', 'informative', 'liberal', 'popular', 'ultimate', 'mainstream', 'rare', 'exclusive', 'willpower', 'complete', 'edge', 'valuable', 'attractive', 'last chance', 'superior', 'how to', 'easily', 'exploit', 'unparalleled', 'endorsed', 'approved', 'quality', 'fascinating', 'unlimited', 'competitive', 'gigantic', 'compromise', 'discount', 'full', 'love', 'odd', 'fundamentals', 'mammoth', 'lavishly', 'bottom line', 'under priced', 'innovative', 'reliable', 'zinger', 'suddenly', 'it\'s here', 'terrific', 'simplified', 'perspective', 'just arrived', 'breakthrough', 'tremendous', 'launching', 'sure fire', 'emerging', 'helpful', 'skill', 'soar', 'profitable', 'special offer', 'reduced', 'beautiful', 'sampler', 'technology', 'better', 'crammed', 'noted', 'selected', 'shrewd', 'growth', 'luxury', 'sturdy', 'enormous', 'promising', 'unconditional', 'wealth', 'spotlight', 'astonishing', 'timely', 'successful', 'useful', 'imagination', 'bonanza', 'opportunities', 'survival', 'greatest', 'security', 'last minute', 'largest', 'high tech', 'refundable', 'monumental', 'colossal', 'latest', 'quickly', 'startling', 'now', 'important', 'revolutionary', 'quick', 'unlock', 'urgent', 'miracle', 'easy', 'fortune', 'amazing', 'magic', 'direct', 'authentic', 'exciting', 'proven', 'simple', 'announcing', 'portfolio', 'reward', 'strange', 'huge gift', 'revealing', 'weird', 'value', 'introducing', 'sensational', 'surprise', 'insider', 'practical', 'excellent', 'delighted', 'download' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
		];
	}

	/**
	 * Returns a list of common words.
	 *
	 * @since 4.1.2
	 *
	 * @return array The list of common words.
	 */
	private function commonWords() {
		return [
			'a', 'for', 'about', 'from', 'after', 'get', 'all', 'has', 'an', 'have', 'and', 'he', 'are', 'her', 'as', 'his', 'at', 'how', 'be', 'I', 'but', 'if', 'by', 'in', 'can', 'is', 'did', 'it', 'do', 'just', 'ever', 'like', 'll', 'these', 'me', 'they', 'most', 'things', 'my', 'this', 'no', 'to', 'not', 'up', 'of', 'was', 'on', 'what', 're', 'when', 'she', 'who', 'sould', 'why', 'so', 'will', 'that', 'with', 'the', 'you', 'their', 'your', 'there' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
		];
	}

	/**
	 * Returns a list of uncommon words.
	 *
	 * @since 4.1.2
	 *
	 * @return array The list of uncommon words.
	 */
	private function uncommonWords() {
		return [
			'actually', 'happened', 'need', 'thing', 'awesome', 'heart', 'never', 'think', 'baby', 'here', 'new', 'time', 'beautiful', 'its', 'now', 'valentines', 'being', 'know', 'old', 'video', 'best', 'life', 'one', 'want', 'better', 'little', 'out', 'watch', 'boy', 'look', 'people', 'way', 'dog', 'love', 'photos', 'ways', 'down', 'made', 'really', 'world', 'facebook', 'make', 'reasons', 'year', 'first', 'makes', 'right', 'years', 'found', 'man', 'see', 'you’ll', 'girl', 'media', 'seen', 'good', 'mind', 'social', 'guy', 'more', 'something' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
		];
	}

	/**
	 * Returns a list of emotional power words.
	 *
	 * @since 4.1.2
	 *
	 * @return array The list of emotional power words.
	 */
	private function emotionPowerWords() {
		return [
			'destroy', 'extra', 'in a', 'devastating', 'eye-opening', 'gift', 'in the world', 'devoted', 'fail', 'in the', 'faith', 'grateful', 'inexpensive', 'dirty', 'famous', 'disastrous', 'fantastic', 'greed', 'grit', 'insanely', 'disgusting', 'fearless', 'disinformation', 'feast', 'insidious', 'dollar', 'feeble', 'gullible', 'double', 'fire', 'hack', 'fleece', 'had enough', 'invasion', 'drowning', 'floundering', 'happy', 'ironclad', 'dumb', 'flush', 'hate', 'irresistibly', 'hazardous', 'is the', 'fool', 'is what happens when', 'fooled', 'helpless', 'it looks like a', 'embarrass', 'for the first time', 'help are the', 'jackpot', 'forbidden', 'hidden', 'jail', 'empower', 'force-fed', 'high', 'jaw-dropping', 'forgotten', 'jeopardy', 'energize', 'hoax', 'jubilant', 'foul', 'hope', 'killer', 'frantic', 'horrific', 'know it all', 'epic', 'how to make', 'evil', 'freebie', 'frenzy', 'hurricane', 'excited', 'fresh on the mind', 'frightening', 'hypnotic', 'lawsuit', 'frugal', 'illegal', 'fulfill', 'lick', 'explode', 'lies', 'exposed', 'gambling', 'like a normal', 'nightmare', 'results', 'line', 'no good', 'pound', 'loathsome', 'no questions asked', 'revenge', 'lonely', 'looks like a', 'obnoxious', 'preposterous', 'revolting', 'looming', 'priced', 'lost', 'prison', 'lowest', 'of the', 'privacy', 'rich', 'lunatic', 'off-limits', 'private', 'risky', 'lurking', 'offer', 'prize', 'ruthless', 'lust', 'official', 'luxurious', 'on the', 'profit', 'scary', 'lying', 'outlawed', 'protected', 'scream', 'searing', 'overcome', 'provocative', 'make you', 'painful', 'pummel', 'secure', 'pale', 'punish', 'marked down', 'panic', 'quadruple', 'seductively', 'massive', 'pay zero', 'seize', 'meltdown', 'payback', 'might look like a', 'peril', 'mind-blowing', 'shameless', 'minute', 'rave', 'shatter', 'piranha', 'reckoning', 'shellacking', 'mired', 'pitfall', 'reclaim', 'mistakes', 'plague', 'sick and tired', 'money', 'played', 'refugee', 'silly', 'money-grubbing', 'pluck', 'refund', 'moneyback', 'plummet', 'plunge', 'murder', 'pointless', 'sinful', 'myths', 'poor', 'remarkably', 'six-figure', 'never again', 'research', 'surrender', 'to the', 'varify', 'skyrocket', 'toxic', 'vibrant', 'slaughter', 'swindle', 'trap', 'victim', 'sleazy', 'taboo', 'treasure', 'victory', 'smash', 'tailspin', 'vindication', 'smug', 'tank', 'triple', 'viral', 'smuggled', 'tantalizing', 'triumph', 'volatile', 'sniveling', 'targeted', 'truth', 'vulnerable', 'snob', 'tawdry', 'try before you buy', 'tech', 'turn the tables', 'wanton', 'soaring', 'warning', 'teetering', 'unauthorized', 'spectacular', 'temporary fix', 'unbelievably', 'spine', 'tempting', 'uncommonly', 'what happened', 'spirit', 'what happens when', 'terror', 'under', 'what happens', 'staggering', 'underhanded', 'what this', 'that will make you', 'undo","when you see', 'that will make', 'unexpected', 'when you', 'strangle', 'that will', 'whip', 'the best', 'whopping', 'stuck up', 'the ranking of', 'wicked', 'stunning', 'the most', 'will make you', 'stupid', 'the reason why is', 'unscrupulous', 'thing ive ever seen', 'withheld', 'this is the', 'this is what happens', 'unusually', 'wondrous', 'this is what', 'uplifting', 'worry', 'sure', 'this is', 'wounded', 'surge', 'thrilled', 'you need to know', 'thrilling', 'valor', 'you need to', 'you see what', 'surprising', 'tired', 'you see', 'surprisingly', 'to be', 'vaporize' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
		];
	}
}Common/Standalone/LimitModifiedDate.php000064400000016355151536241200014123 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Limit Modified Date class.
 *
 * @since 4.1.8
 */
class LimitModifiedDate {
	/**
	 * Class constructor.
	 *
	 * @since 4.1.8
	 *
	 * @return void
	 */
	public function __construct() {
		if ( apply_filters( 'aioseo_last_modified_date_disable', false ) ) {
			return;
		}

		// Reset modified date when the post is updated.
		add_filter( 'wp_insert_post_data', [ $this, 'resetModifiedDate' ], 99999, 2 );
		add_filter( 'wp_insert_attachment_data', [ $this, 'resetModifiedDate' ], 99999, 2 );
		add_action( 'woocommerce_before_product_object_save', [ $this, 'limitWooCommerceModifiedDate' ] );

		add_action( 'rest_api_init', [ $this, 'registerRestHooks' ] );

		if ( ! is_admin() ) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScripts' ], 20 );
		add_action( 'post_submitbox_misc_actions', [ $this, 'classicEditorField' ] );
	}

	/**
	 * Register the REST API hooks.
	 *
	 * @since 4.1.8
	 *
	 * @return void
	 */
	public function registerRestHooks() {
		// Prevent REST API from dropping limit modified date value before updating the post.
		foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
			add_filter( "rest_pre_insert_$postType", [ $this, 'addLimitModifiedDateValue' ], 10, 2 );
		}
	}

	/**
	 * Enqueues the scripts for the Limited Modified Date functionality.
	 *
	 * @since 4.1.8
	 *
	 * @return void
	 */
	public function enqueueScripts() {
		if ( ! $this->isAllowed() || ! aioseo()->helpers->isScreenBase( 'post' ) ) {
			return;
		}

		// Only enqueue this script if the post-settings-metabox is already enqueued.
		if ( wp_script_is( 'aioseo/js/src/vue/standalone/post-settings/main.js', 'enqueued' ) ) {
			aioseo()->core->assets->load( 'src/vue/standalone/limit-modified-date/main.js' );
		}
	}

	/**
	 * Adds the Limit Modified Date field to the post object to prevent it from being dropped.
	 *
	 * @since 4.1.8
	 *
	 * @param  object           $preparedPost The post data.
	 * @param  \WP_REST_Request $restRequest  The request.
	 * @return object                         The modified post data.
	 */
	public function addLimitModifiedDateValue( $preparedPost, $restRequest = null ) {
		if ( 'PUT' !== $restRequest->get_method() ) {
			return $preparedPost;
		}

		$params = $restRequest->get_json_params();
		if ( empty( $params ) || ! isset( $params['aioseo_limit_modified_date'] ) ) {
			return $preparedPost;
		}

		$preparedPost->aioseo_limit_modified_date = $params['aioseo_limit_modified_date'];

		return $preparedPost;
	}

	/**
	 * Resets the modified date when a post is updated if the Limit Modified Date option is enabled.
	 *
	 * @since 4.1.8
	 *
	 * @param  array $data      An array of slashed, sanitized, and processed post data.
	 * @param  array $postArray An array of sanitized (and slashed) but otherwise unmodified post data.
	 * @return array            The modified sanitized post data.
	 */
	public function resetModifiedDate( $data, $postArray = [] ) {
		// If the ID isn't set, a new post is being inserted.
		if ( ! isset( $postArray['ID'] ) ) {
			return $data;
		}

		static $shouldReset = false;

		// Handle the REST API request from the Block Editor.
		if ( aioseo()->helpers->isRestApiRequest() ) {
			// If the value isn't set, then the value wasn't changed in the editor, and we can grab it from the post.
			if ( ! isset( $postArray['aioseo_limit_modified_date'] ) ) {
				$aioseoPost = Models\Post::getPost( $postArray['ID'] );
				if ( $aioseoPost->exists() && $aioseoPost->limit_modified_date ) {
					$shouldReset = true;
				}
			} else {
				if ( $postArray['aioseo_limit_modified_date'] ) {
					$shouldReset = true;
				}
			}
		}

		// Handle the POST request.
		if ( isset( $postArray['aioseo-post-settings'] ) ) {
			$aioseoData = json_decode( stripslashes( $postArray['aioseo-post-settings'] ) );
			if ( ! empty( $aioseoData->limit_modified_date ) ) {
				$shouldReset = true;
			}
		}

		// Handle post revision.
		if ( ! empty( $GLOBALS['action'] ) && in_array( $GLOBALS['action'], [ 'restore',  'inline-save' ], true ) ) {
			$aioseoPost = Models\Post::getPost( $postArray['ID'] );
			if ( $aioseoPost->exists() && $aioseoPost->limit_modified_date ) {
				$shouldReset = true;
			}
		}

		foreach ( aioseo()->standalone->pageBuilderIntegrations as $pageBuilder ) {
			if ( $pageBuilder->isBuiltWith( $postArray['ID'] ) && $pageBuilder->limitModifiedDate( $postArray['ID'] ) ) {
				$shouldReset = true;
				break;
			}
		}

		if ( $shouldReset && isset( $postArray['post_modified'], $postArray['post_modified_gmt'] ) ) {
			$originalPost = get_post( $postArray['ID'] );

			$data['post_modified']     = $originalPost->post_modified;
			$data['post_modified_gmt'] = $originalPost->post_modified_gmt;
		}

		return $data;
	}

	/**
	 * Limits the modified date for WooCommerce products.
	 *
	 * @since 4.8.1
	 *
	 * @param  \WC_Product $product The WooCommerce product.
	 * @return void
	 */
	public function limitWooCommerceModifiedDate( $product ) {
		if ( ! isset( $_POST['PostSettingsNonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['PostSettingsNonce'] ) ), 'aioseoPostSettingsNonce' ) ) {
			return;
		}

		if ( ! isset( $_POST['aioseo-post-settings'] ) ) {
			return;
		}

		$aioseoData = json_decode( sanitize_text_field( wp_unslash( ( $_POST['aioseo-post-settings'] ) ) ) );
		if ( empty( $aioseoData ) || empty( $aioseoData->limit_modified_date ) ) {
			return;
		}

		$product->set_date_modified( get_post_field( 'post_modified', $product->get_id() ) );
	}

	/**
	 * Add the checkbox in the Classic Editor.
	 *
	 * @since 4.1.8
	 *
	 * @param  \WP_Post $post The post object.
	 * @return void
	 */
	public function classicEditorField( $post ) {
		if ( ! $this->isAllowed( $post->post_type ) ) {
			return;
		}

		?>
		<div class="misc-pub-section">
			<div id="aioseo-limit-modified-date"></div>
		</div>
		<?php
	}

	/**
	 * Check if the Limit Modified Date functionality is allowed to run.
	 *
	 * @since 4.1.8
	 *
	 * @param  string $postType The current post type.
	 * @return bool             Whether the functionality is allowed.
	 */
	private function isAllowed( $postType = '' ) {
		if ( empty( $postType ) ) {
			$postType = get_post_type();
		}

		if ( class_exists( 'Limit_Modified_Date', false ) ) {
			return false;
		}

		if ( ! $this->isAllowedPostType( $postType ) ) {
			return false;
		}

		if ( ! aioseo()->access->hasCapability( 'aioseo_page_general_settings' ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Check if the given post type is allowed to limit the modified date.
	 *
	 * @since 4.1.8
	 *
	 * @param  string $postType The post type name.
	 * @return bool             Whether the post type is allowed.
	 */
	private function isAllowedPostType( $postType ) {
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		$postTypes      = aioseo()->helpers->getPublicPostTypes( true );
		$postTypes      = apply_filters( 'aioseo_limit_modified_date_post_types', $postTypes );

		if ( ! in_array( $postType, $postTypes, true ) ) {
			return false;
		}

		if ( ! $dynamicOptions->searchAppearance->postTypes->has( $postType ) || ! $dynamicOptions->searchAppearance->postTypes->$postType->advanced->showMetaBox ) {
			return false;
		}

		return true;
	}
}Common/Standalone/Notifications.php000064400000001357151536241200013413 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Handles the notifications standalone.
 *
 * @since 4.2.0
 */
class Notifications {
	/**
	 * Class constructor.
	 *
	 * @since 4.2.0
	 */
	public function __construct() {
		if ( ! is_admin() ) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScript' ] );
	}

	/**
	 * Enqueues the script.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	public function enqueueScript() {
		aioseo()->core->assets->load( 'src/vue/standalone/notifications/main.js', [], [
			'newNotifications' => count( Models\Notification::getNewNotifications() )
		], 'aioseoNotifications' );
	}
}Common/Standalone/PageBuilders/Avada.php000064400000005617151536241200014167 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;

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

/**
 * Integrate our SEO Panel with Avada Page Builder.
 *
 * @since 4.5.2
 */
class Avada extends Base {
	/**
	 * The plugin files.
	 *
	 * @since 4.5.2
	 *
	 * @var array
	 */
	public $plugins = [
		'fusion-builder/fusion-builder.php'
	];

	/**
	 * The integration slug.
	 *
	 * @since 4.5.2
	 *
	 * @var string
	 */
	public $integrationSlug = 'avada';

	/**
	 * Init the integration.
	 *
	 * @since 4.5.2
	 *
	 * @return void
	 */
	public function init() {
		add_action( 'fusion_enqueue_live_scripts', [ $this, 'enqueue' ] );
		add_action( 'fusion_builder_admin_scripts_hook', [ $this, 'enqueue' ] );
		add_action( 'wp_footer', [ $this, 'addSidebarWrapper' ] );
	}

	/**
	 * Check if we are in the front-end builder.
	 *
	 * @since 4.5.2
	 *
	 * @return boolean Whether or not we are in the front-end builder.
	 */
	public function isBuilder() {
		return function_exists( 'fusion_is_builder_frame' ) && fusion_is_builder_frame();
	}

	/**
	 * Check if we are in the front-end preview.
	 *
	 * @since 4.5.2
	 *
	 * @return boolean Whether or not we are in the front-end preview.
	 */
	public function isPreview() {
		return function_exists( 'fusion_is_preview_frame' ) && fusion_is_preview_frame();
	}

	/**
	 * Adds the sidebar wrapper in footer when is in page builder.
	 *
	 * @since 4.5.2
	 *
	 * @return void
	 */
	public function addSidebarWrapper() {
		if ( ! $this->isBuilder() ) {
			return;
		}

		echo '<div id="fusion-builder-aioseo-sidebar"></div>';
	}

	/**
	 * Enqueue the scripts and styles.
	 *
	 * @since 4.5.2
	 *
	 * @return void
	 */
	public function enqueue() {
		if ( ! aioseo()->postSettings->canAddPostSettingsMetabox( get_post_type( $this->getPostId() ) ) ) {
			return;
		}

		parent::enqueue();
	}

	/**
	 * Returns whether or not the given Post ID was built with WPBakery.
	 *
	 * @since 4.5.2
	 *
	 * @param  int $postId The Post ID.
	 * @return boolean     Whether or not the Post was built with WPBakery.
	 */
	public function isBuiltWith( $postId ) {
		return 'active' === get_post_meta( $postId, 'fusion_builder_status', true );
	}

	/**
	 * Returns whether should or not limit the modified date.
	 *
	 * @since 4.5.2
	 *
	 * @param  int     $postId The Post ID.
	 * @return boolean         Whether or not sholud limit the modified date.
	 */
	public function limitModifiedDate( $postId ) {
		// This method is supposed to be used in the `wp_ajax_fusion_app_save_post_content` action.
		if ( ! isset( $_POST['fusion_load_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['fusion_load_nonce'] ) ), 'fusion_load_nonce' ) ) {
			return false;
		}

		$editorPostId = ! empty( $_REQUEST['post_id'] ) ? intval( $_REQUEST['post_id'] ) : 0;
		if ( $editorPostId !== $postId ) {
			return false;
		}

		return ! empty( $_REQUEST['query']['aioseo_limit_modified_date'] );
	}
}Common/Standalone/PageBuilders/Base.php000064400000012307151536241200014017 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;

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

/**
 * Base class for each of our page builder integrations.
 *
 * @since 4.1.7
 */
abstract class Base {
	/**
	 * The plugin files we can integrate with.
	 *
	 * @since 4.1.7
	 *
	 * @var array
	 */
	public $plugins = [];

	/**
	 * The themes names we can integrate with.
	 *
	 * @since 4.1.7
	 *
	 * @var array
	 */
	public $themes = [];

	/**
	 * The integration slug.
	 *
	 * @since 4.1.7
	 *
	 * @var string
	 */
	public $integrationSlug = '';

	/**
	 * Class constructor.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function __construct() {
		// We need to delay it to give other plugins a chance to register custom post types.
		add_action( 'init', [ $this, '_init' ], PHP_INT_MAX );
	}

	/**
	 * The internal init function.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function _init() {
		// Check if we do have an integration slug.
		if ( empty( $this->integrationSlug ) ) {
			return;
		}

		// Check if the plugin or theme to integrate with is active.
		if ( ! $this->isPluginActive() && ! $this->isThemeActive() ) {
			return;
		}

		// Check if we can proceed with the integration.
		if ( apply_filters( 'aioseo_page_builder_integration_disable', false, $this->integrationSlug ) ) {
			return;
		}

		$this->init();
	}

	/**
	 * The init function.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function init() {}

	/**
	 * Check if the integration is active.
	 *
	 * @since 4.4.8
	 *
	 * @return bool Whether or not the integration is active.
	 */
	public function isActive() {
		return $this->isPluginActive() || $this->isThemeActive();
	}

	/**
	 * Check whether or not the plugin is active.
	 *
	 * @since 4.1.7
	 *
	 * @return bool Whether or not the plugin is active.
	 */
	public function isPluginActive() {
		include_once ABSPATH . 'wp-admin/includes/plugin.php';

		$plugins = apply_filters( 'aioseo_page_builder_integration_plugins', $this->plugins, $this->integrationSlug );

		foreach ( $plugins as $basename ) {
			if ( is_plugin_active( $basename ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Check whether or not the theme is active.
	 *
	 * @since 4.1.7
	 *
	 * @return bool Whether or not the theme is active.
	 */
	public function isThemeActive() {
		$themes = apply_filters( 'aioseo_page_builder_integration_themes', $this->themes, $this->integrationSlug );

		$theme = wp_get_theme();
		foreach ( $themes as $name ) {
			if ( $name === $theme->stylesheet || $name === $theme->template ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Enqueue the scripts and styles.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function enqueue() {
		$integrationSlug = $this->integrationSlug;
		aioseo()->core->assets->load( "src/vue/standalone/page-builders/$integrationSlug/main.js", [], aioseo()->helpers->getVueData( 'post', $this->getPostId(), $integrationSlug ) );

		aioseo()->core->assets->enqueueCss( 'src/vue/assets/scss/integrations/main.scss' );

		aioseo()->admin->addAioseoModalPortal();
		aioseo()->main->enqueueTranslations();
	}

	/**
	 * Get the post ID.
	 *
	 * @since 4.1.7
	 *
	 * @return int|null The post ID or null.
	 */
	public function getPostId() {
		// phpcs:disable HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		foreach ( [ 'id', 'post', 'post_id' ] as $key ) {
			if ( ! empty( $_GET[ $key ] ) ) {
				return (int) sanitize_text_field( wp_unslash( $_GET[ $key ] ) );
			}
		}
		// phpcs:enable

		if ( ! empty( $GLOBALS['post'] ) ) {
			return (int) $GLOBALS['post']->ID;
		}

		return null;
	}

	/**
	 * Returns the page builder edit url for the given Post ID.
	 *
	 * @since 4.3.1
	 *
	 * @param  int    $postId The Post ID.
	 * @return string         The Edit URL.
	 */
	public function getEditUrl( $postId ) { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return '';
	}

	/**
	 * Returns whether or not the given Post ID was built with the Page Builder.
	 *
	 * @since 4.1.7
	 *
	 * @param  int $postId The Post ID.
	 * @return boolean     Whether or not the Post was built with the Page Builder.
	 */
	public function isBuiltWith( $postId ) { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return false;
	}

	/**
	 * Checks whether or not we should prevent the date from being modified.
	 *
	 * @since 4.5.2
	 *
	 * @param  int  $postId The Post ID.
	 * @return bool         Whether or not we should prevent the date from being modified.
	 */
	public function limitModifiedDate( $postId ) { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return false;
	}

	/**
	 * Returns the processed page builder content.
	 *
	 * @since 4.5.2
	 *
	 * @param  int    $postId  The post id.
	 * @param  string $content The raw content.
	 * @return string          The processed content.
	 */
	public function processContent( $postId, $content = '' ) {
		if ( empty( $content ) ) {
			$post = get_post( $postId );
			if ( is_a( $post, 'WP_Post' ) ) {
				$content = $post->post_content;
			}
		}

		if ( aioseo()->helpers->isAjaxCronRestRequest() ) {
			return apply_filters( 'the_content', $content );
		}

		return $content;
	}
}Common/Standalone/PageBuilders/Divi.php000064400000012577151536241200014051 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;

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

/**
 * Integrate our SEO Panel with Divi Page Builder.
 *
 * @since 4.1.7
 */
class Divi extends Base {
	/**
	 * The theme name.
	 *
	 * @since 4.1.7
	 *
	 * @var array
	 */
	public $themes = [ 'Divi', 'Extra' ];

	/**
	 * The plugin files.
	 *
	 * @since 4.2.0
	 *
	 * @var array
	 */
	public $plugins = [
		'divi-builder/divi-builder.php'
	];

	/**
	 * The integration slug.
	 *
	 * @since 4.1.7
	 *
	 * @var string
	 */
	public $integrationSlug = 'divi';

	/**
	 * Init the integration.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function init() {
		add_action( 'wp', [ $this, 'maybeRun' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAdmin' ] );
	}

	/**
	 * Check if we are in the Page Builder and run the integrations.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function maybeRun() {
		$postType = get_post_type( $this->getPostId() );

		if (
			! defined( 'ET_BUILDER_PRODUCT_VERSION' ) ||
			! version_compare( '4.9.2', ET_BUILDER_PRODUCT_VERSION, '<=' ) ||
			! ( function_exists( 'et_core_is_fb_enabled' ) && et_core_is_fb_enabled() ) ||
			! aioseo()->postSettings->canAddPostSettingsMetabox( $postType )
		) {
			return;
		}

		add_action( 'wp_footer', [ $this, 'addContainers' ] );
		add_action( 'wp_footer', [ $this, 'addIframeWatcher' ] );
		add_action( 'wp_enqueue_scripts', [ $this, 'enqueue' ] );
		add_filter( 'script_loader_tag', [ $this, 'addEtTag' ], 10, 2 );
	}

	/**
	 * Enqueue the required scripts for the admin screen.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function enqueueAdmin() {
		if ( ! aioseo()->helpers->isScreenBase( 'toplevel_page_et_divi_options' ) ) {
			return;
		}

		aioseo()->core->assets->load( 'src/vue/standalone/page-builders/divi-admin/main.js', [], aioseo()->helpers->getVueData() );

		aioseo()->main->enqueueTranslations();
	}

	/**
	 * Add et attributes to script tags.
	 *
	 * @since 4.1.7
	 *
	 * @param  string $tag    The <script> tag for the enqueued script.
	 * @param  string $handle The script's registered handle.
	 * @return string         The tag.
	 */
	public function addEtTag( $tag, $handle = '' ) {
		$scriptHandles = [
			'aioseo/js/src/vue/standalone/page-builders/divi/main.js',
			'aioseo/js/src/vue/standalone/app/main.js'
		];

		if ( in_array( $handle, $scriptHandles, true ) ) {
			// These tags load in parent window only, not in Divi iframe.
			return preg_replace( '/<script/', '<script class="et_fb_ignore_iframe"', (string) $tag );
		}

		return $tag;
	}

	/**
	 * Add the Divi watcher.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function addIframeWatcher() {
		?>
		<script type="text/javascript">
			if (typeof jQuery === 'function') {
				jQuery(window).on('et_builder_api_ready et_fb_section_content_change', function(event) {
					window.parent.postMessage({ eventType : event.type })
				})
			}
		</script>
		<?php
	}

	/**
	 * Add the containers to mount our panel.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function addContainers() {
		echo '<div id="aioseo-app-modal" class="et_fb_ignore_iframe"><div class="et_fb_ignore_iframe"></div></div>';
		echo '<div id="aioseo-settings" class="et_fb_ignore_iframe"></div>';
		echo '<div id="aioseo-admin" class="et_fb_ignore_iframe"></div>';
		echo '<div id="aioseo-modal-portal" class="et_fb_ignore_iframe"></div>';
	}

	/**
	 * Returns whether or not the given Post ID was built with Divi.
	 *
	 * @since 4.1.7
	 *
	 * @param  int $postId The Post ID.
	 * @return boolean     Whether or not the Post was built with Divi.
	 */
	public function isBuiltWith( $postId ) {
		if ( ! function_exists( 'et_pb_is_pagebuilder_used' ) ) {
			return false;
		}

		return et_pb_is_pagebuilder_used( $postId );
	}

	/**
	 * Returns the Divi edit url for the given Post ID.
	 *
	 * @since 4.3.1
	 *
	 * @param  int    $postId The Post ID.
	 * @return string         The Edit URL.
	 */
	public function getEditUrl( $postId ) {
		if ( ! function_exists( 'et_fb_get_vb_url' ) ) {
			return '';
		}

		$isDiviLibrary = 'et_pb_layout' === get_post_type( $postId );
		$editUrl       = $isDiviLibrary ? get_edit_post_link( $postId, 'raw' ) : get_permalink( $postId );

		if ( et_pb_is_pagebuilder_used( $postId ) ) {
			$editUrl = et_fb_get_vb_url( $editUrl );
		} else {
			if ( ! et_pb_is_allowed( 'divi_builder_control' ) ) {
				// Prevent link when user lacks `Toggle Divi Builder` capability.
				return '';
			}

			$editUrl = add_query_arg(
				[ 'et_fb_activation_nonce' => wp_create_nonce( 'et_fb_activation_nonce_' . $postId ) ],
				$editUrl
			);
		}

		return $editUrl;
	}

	/**
	 * Checks whether or not we should prevent the date from being modified.
	 *
	 * @since 4.5.2
	 *
	 * @param  int  $postId The Post ID.
	 * @return bool         Whether or not we should prevent the date from being modified.
	 */
	public function limitModifiedDate( $postId ) {
		// This method is supposed to be used in the `wp_ajax_et_fb_ajax_save` action.
		if ( empty( $_REQUEST['et_fb_save_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['et_fb_save_nonce'] ) ), 'et_fb_save_nonce' ) ) {
			return false;
		}

		$editorPostId = ! empty( $_REQUEST['post_id'] ) ? intval( $_REQUEST['post_id'] ) : 0;
		if ( $editorPostId !== $postId ) {
			return false;
		}

		return ! empty( $_REQUEST['options']['conditional_tags']['aioseo_limit_modified_date'] );
	}
}Common/Standalone/PageBuilders/Elementor.php000064400000013656151536241200015107 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;

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

use Elementor\Controls_Manager as ControlsManager;
use Elementor\Core\DocumentTypes\PageBase;

/**
 * Integrate our SEO Panel with Elementor Page Builder.
 *
 * @since 4.1.7
 */
class Elementor extends Base {
	/**
	 * The plugin files.
	 *
	 * @since 4.1.7
	 *
	 * @var array
	 */
	public $plugins = [
		'elementor/elementor.php',
		'elementor-pro/elementor-pro.php',
	];

	/**
	 * The integration slug.
	 *
	 * @since 4.1.7
	 *
	 * @var string
	 */
	public $integrationSlug = 'elementor';

	/**
	 * Init the integration.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function init() {
		if ( ! aioseo()->postSettings->canAddPostSettingsMetabox( get_post_type( $this->getPostId() ) ) ) {
			return;
		}

		if ( ! did_action( 'elementor/init' ) ) {
			add_action( 'elementor/init', [ $this, 'addPanelTab' ] );
		} else {
			$this->addPanelTab();
		}

		add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'enqueue' ] );
		add_action( 'elementor/documents/register_controls', [ $this, 'registerDocumentControls' ] );
		add_action( 'elementor/editor/footer', [ $this, 'addContainers' ] );

		// Add the SEO tab to the main Elementor panel.
		add_action( 'elementor/editor/footer', [ $this, 'startCapturing' ], 0 );
		add_action( 'elementor/editor/footer', [ $this, 'endCapturing' ], 999 );
	}

	/**
	 * Start capturing buffer.
	 *
	 * @since 4.3.5
	 *
	 * @return void
	 */
	public function startCapturing() {
		ob_start();
	}

	/**
	 * End capturing buffer and add button.
	 * This is a hack to add the SEO tab to the main Elementor panel.
	 * We need to do this because Elementor doesn't provide a filter to add tabs to the main panel.
	 *
	 * @since 4.3.5
	 *
	 * @return void
	 */
	public function endCapturing() {
		$output  = ob_get_clean();
		$search  = '/(<div class="elementor-component-tab elementor-panel-navigation-tab" data-tab="global">.*<\/div>)/m';
		$replace = '${1}<div class="elementor-component-tab elementor-panel-navigation-tab" data-tab="aioseo">SEO</div>';
		echo preg_replace( $search, $replace, $output ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Add the AIOSEO Panel Tab on Elementor.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function addPanelTab() {
		ControlsManager::add_tab( 'aioseo', AIOSEO_PLUGIN_SHORT_NAME );
	}

	/**
	 * Register the Elementor Document Controls.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function registerDocumentControls( $document ) {
		// PageBase is the base class for documents like `post` `page` and etc.
		if ( ! $document instanceof PageBase || ! $document::get_property( 'has_elements' ) ) {
			return;
		}

		// This is needed to get the tab to appear, but will be overwritten in the JavaScript.
		$document->start_controls_section(
			'aioseo_section',
			[
				'label' => AIOSEO_PLUGIN_SHORT_NAME,
				'tab'   => 'aioseo',
			]
		);

		$document->end_controls_section();
	}

	/**
	 * Returns whether or not the given Post ID was built with Elementor.
	 *
	 * @since 4.1.7
	 *
	 * @param  int     $postId The Post ID.
	 * @return boolean         Whether or not the Post was built with Elementor.
	 */
	public function isBuiltWith( $postId ) {
		$document = $this->getElementorDocument( $postId );
		if ( ! $document ) {
			return false;
		}

		return $document->is_built_with_elementor();
	}

	/**
	 * Returns the Elementor edit url for the given Post ID.
	 *
	 * @since 4.3.1
	 *
	 * @param  int    $postId The Post ID.
	 * @return string         The Edit URL.
	 */
	public function getEditUrl( $postId ) {
		$document = $this->getElementorDocument( $postId );
		if ( ! $document || ! $document->is_editable_by_current_user() ) {
			return '';
		}

		return esc_url( $document->get_edit_url() );
	}

	/**
	 * Add the containers to mount our panel.
	 *
	 * @since 4.1.9
	 *
	 * @return void
	 */
	public function addContainers() {
		echo '<div id="aioseo-admin"></div>';
	}

	/**
	 * Returns the Elementor Document instance for the given Post ID.
	 *
	 * @since 4.3.5
	 *
	 * @param  int    $postId The Post ID.
	 * @return object         The Elementor Document instance.
	 */
	private function getElementorDocument( $postId ) {
		if (
			! class_exists( '\Elementor\Plugin' ) ||
			! is_object( \Elementor\Plugin::instance()->documents ) ||
			! method_exists( \Elementor\Plugin::instance()->documents, 'get' )
		) {
			return false;
		}

		$elementorDocument = \Elementor\Plugin::instance()->documents->get( $postId );
		if ( empty( $elementorDocument ) ) {
			return false;
		}

		return $elementorDocument;
	}

	/**
	 * Checks whether or not we should prevent the date from being modified.
	 * This method is supposed to be used in the `wp_ajax_seedprod_pro_save_lpage` action.
	 *
	 * @since 4.5.2
	 *
	 * @param  int  $postId The Post ID.
	 * @return bool         Whether or not we should prevent the date from being modified.
	 */
	public function limitModifiedDate( $postId ) {
		// This method is supposed to be used in the `wp_ajax_elementor_ajax` action.
		if ( empty( $_REQUEST['_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_nonce'] ) ), 'elementor_ajax' ) ) {
			return false;
		}

		$editorPostId = ! empty( $_REQUEST['editor_post_id'] ) ? (int) $_REQUEST['editor_post_id'] : false;
		if ( $editorPostId !== $postId ) {
			return false;
		}

		return ! empty( $_REQUEST['aioseo_limit_modified_date'] );
	}

	/**
	 * Get the post ID.
	 *
	 * @since 4.6.9
	 *
	 * @return int|null The post ID or null.
	 */
	public function getPostId() {
		// phpcs:disable HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		if ( aioseo()->helpers->isAjaxCronRestRequest() ) {
			foreach ( [ 'editor_post_id', 'initial_document_id' ] as $key ) {
				if ( ! empty( $_REQUEST[ $key ] ) ) {
					return intval( wp_unslash( $_REQUEST[ $key ] ) );
				}
			}
		}
		// phpcs:enable

		return parent::getPostId();
	}
}Common/Standalone/PageBuilders/SeedProd.php000064400000007154151536241200014656 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;

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

/**
 * Integrate our SEO Panel with SeedProd Page Builder.
 *
 * @since 4.1.7
 */
class SeedProd extends Base {
	/**
	 * The plugin files.
	 *
	 * @since 4.1.7
	 *
	 * @var array
	 */
	public $plugins = [
		'coming-soon/coming-soon.php',
		'seedprod-coming-soon-pro-5/seedprod-coming-soon-pro-5.php',
	];

	/**
	 * The integration slug.
	 *
	 * @since 4.1.7
	 *
	 * @var string
	 */
	public $integrationSlug = 'seedprod';

	/**
	 * Init the integration.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function init() {
		$postType = get_post_type( $this->getPostId() );

		if ( ! aioseo()->postSettings->canAddPostSettingsMetabox( $postType ) ) {
			return;
		}

		// SeedProd de-enqueues and de-register scripts/styles on priority PHP_INT_MAX.
		// Thus, we need to enqueue our scripts at the same priority for more compatibility.
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ], PHP_INT_MAX );
		add_filter( 'style_loader_tag', [ $this, 'replaceStyleTag' ], 10, 2 );
	}

	/**
	 * Enqueue the scripts and styles.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function enqueue() {
		if ( ! $this->isBuilderScreen() ) {
			return;
		}

		parent::enqueue();
	}

	/**
	 * Check whether or not is builder screen.
	 *
	 * @since 4.1.7
	 *
	 * @return boolean Whether or not is builder screen.
	 */
	public function isBuilderScreen() {
		$currentScreen = aioseo()->helpers->getCurrentScreen();

		return $currentScreen && preg_match( '/seedprod.*?_builder$/i', (string) $currentScreen->base );
	}

	/**
	 * Replace original tag to prevent being removed by SeedProd.
	 *
	 * @param  string $tag    The <link> tag for the enqueued style.
	 * @param  string $handle The style's registered handle.
	 * @return string         The tag.
	 */
	public function replaceStyleTag( $tag, $handle = '' ) {
		if ( ! $this->isBuilderScreen() ) {
			return $tag;
		}

		$aioseoCommonHandle = 'aioseo-' . $this->integrationSlug . '-common';

		if ( $aioseoCommonHandle === $handle ) {
			// All the *common.css links are removed from SeedProd.
			// https://github.com/awesomemotive/seedprod-plugins/blob/32854442979bfa068aadf9b8a8a929e5f9f353e5/seedprod-pro/resources/views/builder.php#L406
			$tag = str_ireplace( 'href=', 'data-href=', $tag );
		}

		return $tag;
	}

	/**
	 * Returns whether or not the given Post ID was built with SeedProd.
	 *
	 * @since 4.1.7
	 *
	 * @param  int $postId The Post ID.
	 * @return boolean     Whether or not the Post was built with SeedProd.
	 */
	public function isBuiltWith( $postId ) {
		$isSeedProd = get_post_meta( $postId, '_seedprod_page', true );
		if ( ! empty( $isSeedProd ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Checks whether or not we should prevent the date from being modified.
	 *
	 * @since 4.5.2
	 *
	 * @param  int  $postId The Post ID.
	 * @return bool         Whether or not we should prevent the date from being modified.
	 */
	public function limitModifiedDate( $postId ) {
		// This method is supposed to be used in the `wp_ajax_seedprod_pro_save_lpage` action.
		if ( wp_doing_ajax() && ! check_ajax_referer( 'seedprod_nonce', false, false ) ) {
			return false;
		}

		$landingPageId = ! empty( $_REQUEST['lpage_id'] ) ? (int) $_REQUEST['lpage_id'] : false;
		if ( $landingPageId !== $postId ) {
			return false;
		}

		$settings = ! empty( $_REQUEST['settings'] ) ? json_decode( sanitize_text_field( wp_unslash( $_REQUEST['settings'] ) ) ) : false;
		if ( empty( $settings ) || empty( $settings->aioseo_limit_modified_date ) ) {
			return false;
		}

		return true;
	}
}Common/Standalone/PageBuilders/SiteOrigin.php000064400000005157151536241200015226 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;

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

/**
 * Integrate our SEO Panel with SiteOrigin Page Builder.
 *
 * @since 4.6.6
 */
class SiteOrigin extends Base {
	/**
	 * The plugin files.
	 *
	 * @since 4.6.6
	 *
	 * @var array
	 */
	public $plugins = [
		'siteorigin-panels/siteorigin-panels.php'
	];

	/**
	 * The integration slug.
	 *
	 * @since 4.6.6
	 *
	 * @var string
	 */
	public $integrationSlug = 'siteorigin';

	/**
	 * Init the integration.
	 *
	 * @since 4.6.6
	 *
	 * @return void
	 */
	public function init() {
		$postType = get_post_type( $this->getPostId() );
		if ( empty( $postType ) ) {
			$postType = ! empty( $_GET['post_type'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type'] ) ) : 'post'; // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended, Generic.Files.LineLength.MaxExceeded
		}

		if ( ! aioseo()->postSettings->canAddPostSettingsMetabox( $postType ) ) {
			return;
		}

		add_action( 'siteorigin_panel_enqueue_admin_scripts', [ $this, 'enqueue' ] );
	}

	/**
	 * Returns whether or not the given Post ID was built with SiteOrigin.
	 *
	 * @since 4.6.6
	 *
	 * @param  int $postId The Post ID.
	 * @return bool        Whether or not the Post was built with SiteOrigin.
	 */
	public function isBuiltWith( $postId ) {
		$postObj = get_post( $postId );
		if (
			! empty( $postObj ) &&
			(
				preg_match( '/siteorigin_widget/', (string) $postObj->post_content ) ||
				preg_match( '/so-panel widget/', (string) $postObj->post_content )
			)
		) {
			return true;
		}

		return false;
	}

	/**
	 * Returns the processed page builder content.
	 *
	 * @since 4.6.6
	 *
	 * @param  int    $postId  The post id.
	 * @param  string $content The raw content.
	 * @return string          The processed content.
	 */
	public function processContent( $postId, $content = '' ) {
		// When performing a save_post action, we must execute the siteorigin_widget shortcodes if there are image widgets.
		// This ensures that the getFirstImageInContent method can locate the images, as SiteOrigin uses shortcodes for images.
		// We cache the first image in the content during post saving.
		if (
			doing_action( 'save_post' ) &&
			aioseo()->options->searchAppearance->advanced->runShortcodes &&
			(
				stripos( $content, 'SiteOrigin_Widget_Image_Widget' ) !== false ||
				stripos( $content, 'WP_Widget_Media_Image' ) !== false
			)
		) {
			$content = aioseo()->helpers->doAllowedShortcodes( $content, $postId, [ 'siteorigin_widget' ] );
		}

		return parent::processContent( $postId, $content );
	}
}Common/Standalone/PageBuilders/ThriveArchitect.php000064400000031364151536241200016241 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;

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

/**
 * Integrate our SEO Panel with Thrive Architect Page Builder.
 *
 * @since 4.6.6
 */
class ThriveArchitect extends Base {
	/**
	 * The plugin files.
	 *
	 * @since 4.6.6
	 *
	 * @var array
	 */
	public $plugins = [
		'thrive-visual-editor/thrive-visual-editor.php'
	];

	/**
	 * The integration slug.
	 *
	 * @since 4.6.6
	 *
	 * @var string
	 */
	public $integrationSlug = 'thrive-architect';

	/**
	 * Init the integration.
	 *
	 * @since 4.6.6
	 *
	 * @return void
	 */
	public function init() {
		add_filter( 'tcb_allowed_ajax_options', [ $this, 'makeSettingsAllowed' ] );

		if ( ! aioseo()->postSettings->canAddPostSettingsMetabox( get_post_type( $this->getPostId() ) ) ) {
			return;
		}

		add_action( 'tcb_main_frame_enqueue', [ $this, 'enqueue' ] );
		add_filter( 'tve_main_js_dependencies', [ $this, 'mainJsDependencies' ] );
		add_action( 'tcb_right_sidebar_content_settings', [ $this, 'addSettingsTab' ] );
		add_action( 'tcb_sidebar_extra_links', [ $this, 'addSidebarButton' ] );
		add_filter( 'tcb_main_frame_localize', [ $this, 'localizeData' ] );
	}

	/**
	 * Overrides the parent enqueue to add WordPress styles that we need.
	 *
	 * @since 4.6.6
	 *
	 * @return void
	 */
	public function enqueue() {
		wp_enqueue_style( 'common' );
		wp_enqueue_style( 'buttons' );
		wp_enqueue_style( 'forms' );
		wp_enqueue_style( 'list-tables' );
		wp_enqueue_style( 'wp-components' );

		print_admin_styles();

		parent::enqueue();
	}

	/**
	 * Add our javascript to the plugin dependencies.
	 *
	 * @since 4.6.6
	 *
	 * @param  array $dependencies The dependencies.
	 * @return array               The dependencies.
	 */
	public function mainJsDependencies( $dependencies ) {
		$dependencies[] = aioseo()->core->assets->jsHandle( "src/vue/standalone/page-builders/{$this->integrationSlug}/main.js" );

		return $dependencies;
	}

	/**
	 * Add the extra link to the sidebar.
	 *
	 * @since 4.6.6
	 *
	 * @return void
	 */
	public function addSidebarButton() {
		$tooltip = sprintf(
			// Translators: 1 - The plugin short name ("AIOSEO").
			esc_html__( '%1$s Settings', 'all-in-one-seo-pack' ),
			AIOSEO_PLUGIN_SHORT_NAME
		);

		// phpcs:disable Generic.Files.LineLength.MaxExceeded
		?>
		<a href="javascript:void(0)" class="mouseenter mouseleave sidebar-item tcb-sidebar-icon-aioseo" data-position="left" data-toggle="settings" data-tooltip="<?php echo esc_attr( $tooltip ); ?>">
			<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
				<path d="M10.0011 19C14.9722 19 19.0021 14.9706 19.0021 10C19.0021 5.02944 14.9722 1 10.0011 1C5.02991 1 1 5.02944 1 10C1 14.9706 5.02991 19 10.0011 19Z" stroke="currentColor"/>
				<path d="M9.99664 13.3228C9.99896 13.2104 9.47813 13.1752 9.37307 13.141C8.56228 12.8777 8.04027 12.3293 7.78204 11.5205C7.6493 11.1043 7.68851 10.6765 7.68163 10.2515C7.68162 10.2511 7.68134 10.2507 7.68093 10.2506V10.2506C7.68051 10.2504 7.68023 10.25 7.68023 10.2496C7.68069 9.99579 7.67884 9.74246 7.68115 9.48867C7.683 9.31099 7.74131 9.25453 7.92133 9.25222C8.07128 9.25037 8.22168 9.24482 8.37116 9.25407C8.48454 9.26101 8.86945 9.25407 8.86945 9.25407M9.99664 13.3228C9.98877 13.7037 10.0003 14.1192 10.0008 14.5M9.99664 13.3228C10.0036 13.7152 9.99664 14.1076 10.0008 14.5M9.99664 13.3228C9.99863 13.2265 10.5453 13.1946 10.6332 13.1715C11.6884 12.8929 12.42 11.955 12.4311 10.8639C12.4358 10.4137 12.433 9.96389 12.432 9.51366C12.432 9.30312 12.3788 9.25268 12.1641 9.25176C12.0197 9.25083 11.8749 9.24435 11.7314 9.25407C11.6213 9.26148 11.2793 9.25176 11.2793 9.25176M10.0008 14.5C10.0008 14.7878 9.72149 14.8117 9.50075 15C9.29018 15.1795 8.77054 15.5587 8.52758 15.6966C8.32442 15.8118 8.12033 15.8428 7.90097 15.7401C7.72373 15.6573 7.53862 15.5916 7.36276 15.5059C7.13877 15.3963 7.04344 15.1936 7.08879 14.947C7.1323 14.7091 7.17765 14.4718 7.22578 14.2348C7.27529 13.9933 7.21745 13.7897 7.02632 13.6291C6.78706 13.4283 6.5677 13.2071 6.37195 12.9637C6.21321 12.7671 6.01098 12.7102 5.76941 12.762C5.5445 12.8101 5.31866 12.8559 5.09282 12.9008C4.84339 12.9503 4.64902 12.8587 4.53425 12.6329C4.42457 12.4173 4.33433 12.1924 4.2589 11.9624C4.1793 11.719 4.24085 11.5191 4.4491 11.3683C4.64532 11.2262 4.84616 11.0911 5.04794 10.9574C5.26961 10.8107 5.34828 10.6071 5.31866 10.348C5.2858 10.0606 5.28673 9.7714 5.31635 9.48405C5.34411 9.21613 5.25711 9.01253 5.02757 8.86585C4.8383 8.74461 4.65318 8.6169 4.46853 8.4878C4.23576 8.32492 4.16541 8.12086 4.26028 7.85525C4.33803 7.6387 4.42318 7.42446 4.51852 7.21531C4.62218 6.98811 4.80822 6.89186 5.05349 6.93258C5.28025 6.97006 5.50609 7.0168 5.731 7.06585C5.99154 7.12322 6.20812 7.06631 6.3775 6.84929C6.56261 6.61238 6.77781 6.40322 7.00503 6.2061C7.18829 6.04693 7.27205 5.8549 7.21143 5.60549C7.16006 5.3931 7.12906 5.17608 7.08509 4.96184C7.01429 4.61803 7.1036 4.42924 7.4257 4.28024C7.6085 4.19603 7.79453 4.11783 7.98335 4.04842C8.22399 3.9605 8.42762 4.02574 8.57385 4.23536C8.71546 4.43849 8.84967 4.64718 8.98341 4.85587C9.12317 5.07382 9.32032 5.15896 9.57392 5.12703C9.86131 5.09094 10.1492 5.09325 10.437 5.12564C10.6763 5.15248 10.8669 5.0715 11.0002 4.86698C11.1261 4.67402 11.251 4.48014 11.3778 4.28765C11.5611 4.01001 11.7467 3.94384 12.054 4.05952C12.2479 4.13217 12.4395 4.21176 12.6264 4.3006C12.8731 4.41813 12.9684 4.6134 12.9198 4.87993C12.8763 5.11777 12.8291 5.35469 12.7828 5.59207C12.737 5.82621 12.7944 6.0261 12.9804 6.18297C13.2234 6.38842 13.4437 6.61561 13.6473 6.86086C13.8005 7.04502 13.993 7.11443 14.2337 7.05474C14.4567 6.99921 14.6839 6.95896 14.9098 6.91407C15.1648 6.86317 15.3661 6.95988 15.4822 7.19217C15.5882 7.40364 15.6761 7.6225 15.7506 7.84693C15.8335 8.09726 15.771 8.29901 15.5558 8.45495C15.3596 8.59654 15.1583 8.73166 14.9565 8.86585C14.7473 9.00466 14.6645 9.19855 14.6913 9.44425C14.7237 9.74363 14.7242 10.0435 14.6946 10.3429C14.6691 10.6038 14.7552 10.8033 14.9783 10.9467C15.1624 11.0652 15.3429 11.1897 15.523 11.3146C15.7816 11.4946 15.8506 11.6973 15.7451 11.9879C15.6766 12.1771 15.5993 12.3636 15.5174 12.5473C15.3818 12.8504 15.1907 12.9401 14.8621 12.8721C14.6423 12.8263 14.4211 12.7888 14.2017 12.7398C13.9939 12.6935 13.8213 12.7574 13.689 12.911C13.4627 13.1738 13.2234 13.4218 12.9638 13.6527C12.8088 13.7906 12.7467 13.9706 12.7958 14.1849C12.8485 14.4148 12.8874 14.6476 12.9337 14.879C12.9957 15.189 12.8999 15.3856 12.6088 15.5235C12.4478 15.5999 12.2789 15.66 12.1178 15.7364C11.911 15.8345 11.7175 15.8058 11.5236 15.7003C11.2265 15.5388 10.741 15.2332 10.5009 15C10.3403 14.8441 10.0031 14.7207 10.0008 14.5ZM11.2793 9.25176C11.2848 8.85382 11.2873 8.01509 11.2804 7.61719C11.2795 7.56905 11.2791 7.5401 11.279 7.52286C11.2788 7.50546 11.2793 7.50858 11.2794 7.52598C11.2798 7.63906 11.2816 8.09163 11.2833 8.28906C11.2796 8.687 11.2714 8.85382 11.2793 9.25176ZM11.2793 9.25176C11.2793 9.25176 10.9086 9.25685 10.7873 9.255C10.2968 9.24806 9.80624 9.24852 9.31569 9.255C9.19999 9.25638 8.86945 9.25407 8.86945 9.25407M8.86945 9.25407C8.87593 8.8677 8.87547 8.34389 8.87408 7.95752C8.87346 7.78806 8.87262 7.62829 8.87143 7.54953C8.8709 7.51441 8.86954 7.51752 8.86963 7.55263C8.86985 7.62907 8.8701 7.7811 8.86945 7.95752C8.86853 8.34435 8.86251 8.8677 8.86945 9.25407Z" stroke="currentColor"/>
			</svg>
		</a>
		<?php
		//phpcs:enable Generic.Files.LineLength.MaxExceeded
	}

	/**
	 * Adds the settings tab for AIOSEO in the Thrive Architect page builder.
	 *
	 * @since 4.6.6
	 *
	 * @return void
	 */
	public function addSettingsTab() {
		//phpcs:disable Generic.Files.LineLength.MaxExceeded
		?>
		<div class="tve-component s-item tcb-aioseo">
			<div class="dropdown-header">
				<div class="group-description s-name">
					<?php echo esc_html( AIOSEO_PLUGIN_SHORT_NAME ); ?>
				</div>
			</div>
			<div class="dropdown-content">
				<div class="tcb-aioseo-settings">
					<button class="click tcb-settings-modal-open-button s-item inside-button">
						<span class="s-name">
							<?php
								printf(
									// Translators: 1 - The plugin short name ("AIOSEO").
									esc_html__( '%1$s Settings', 'all-in-one-seo-pack' ),
									esc_html( AIOSEO_PLUGIN_SHORT_NAME )
								);
							?>
						</span>
					</button>
					<div class="mt-10 button-group">
						<div id="aioseo-score-btn-settings"></div>
						<button type="button" class="p-3 action-btn click" id="settings-action-btn">
							<svg class="when-active" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
								<path d="M9.61433 9.94582C10.145 11.0636 10.2253 12.3535 9.45767 13.2683C9.24696 13.5194 8.89896 13.533 8.66555 13.3372L6.22379 11.2883L4.64347 13.1717C4.62842 13.1896 4.59542 13.1925 4.58036 13.2104L3.42703 13.7098C3.28287 13.7722 3.12128 13.6366 3.15772 13.4838L3.44925 12.2613C3.4643 12.2434 3.47935 12.2254 3.4944 12.2075L5.07472 10.3241L2.63295 8.27524C2.3816 8.06433 2.35256 7.73431 2.56327 7.4832C3.3158 6.58636 4.59718 6.40838 5.7901 6.73691L7.81453 4.7983L7.06045 4.16555C6.80909 3.95464 6.78006 3.62462 6.99077 3.37351L7.7132 2.51255C7.90885 2.27937 8.25395 2.23272 8.50531 2.44363L13.3888 6.54141C13.6222 6.73725 13.6542 7.10028 13.4585 7.33345L12.7361 8.19441C12.5254 8.44552 12.1774 8.45917 11.944 8.26332L11.172 7.61551L9.61433 9.94582Z" fill="#FFFFFF"/>
							</svg>

							<svg class="when-inactive" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
								<path fill-rule="evenodd" clip-rule="evenodd" d="M5.87874 6.50704C5.88995 6.50899 5.90115 6.51097 5.91236 6.513L7.23686 5.29914L6.55461 4.72665C6.3212 4.5308 6.27436 4.18554 6.48527 3.93418L7.6905 2.49785C7.88635 2.26445 8.24956 2.23267 8.48297 2.42852L13.3665 6.52629C13.6179 6.73721 13.6317 7.08535 13.4358 7.31876L12.2306 8.75509C12.0197 9.00645 11.6895 9.03534 11.4381 8.82442L10.7738 8.26701L9.80841 9.78218C9.81235 9.79286 9.81625 9.80354 9.82011 9.81424L9.23164 9.32046L10.6275 7.16519L11.7766 8.12937L12.7408 6.9803L8.14451 3.12358L7.18033 4.27264L8.3294 5.23682L6.4644 6.99846L5.87874 6.50704ZM4.72914 6.45619C3.84314 6.53709 3.0184 6.91015 2.43354 7.63253C2.31301 7.77616 2.31817 8.02525 2.47976 8.16084L5.35242 10.5713L3.54458 12.7258L3.51445 12.7617L3.176 13.4568C3.09138 13.6305 3.28888 13.7962 3.44531 13.6827L4.07103 13.2287L4.10116 13.1928L5.909 11.0383L8.78167 13.4488C8.94326 13.5844 9.18946 13.5462 9.30998 13.4025C9.90734 12.6906 10.1364 11.8178 10.0663 10.9346L9.15211 10.1675C9.42355 10.9846 9.399 11.8779 8.95854 12.6181L3.26707 7.84242C3.90443 7.26739 4.79027 7.09684 5.64573 7.2253L4.72914 6.45619Z" fill="#50565F"/>
								<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4242 11.9993L3.23163 4.28583L3.68158 3.7496L12.8741 11.463L12.4242 11.9993Z" fill="#50565F"/>
							</svg>
						</button>
					</div>
				</div>
			</div>
		</div>
		<?php
		//phpcs:enable Generic.Files.LineLength.MaxExceeded
	}

	/**
	 * Localizes the data by adding the 'is_aioseo_settings_enabled' option to the provided data array.
	 *
	 * @since 4.6.6
	 *
	 * @param  array $data The data array to be localized.
	 * @return array       The localized data array with the 'is_aioseo_settings_enabled' option added.
	 */
	public function localizeData( $data ) {
		// We use get_option here since it is how Thrive Architect saves the settings.
		$data['is_aioseo_settings_enabled'] = get_option( 'is_aioseo_settings_enabled', true );

		return $data;
	}

	/**
	 * Adds 'is_aioseo_settings_enabled' to the list of allowed settings.
	 *
	 * @since 4.6.6
	 *
	 * @param  array $options The array of allowed settings.
	 * @return array          The updated array of allowed settings.
	 */
	public function makeSettingsAllowed( $options ) {
		$options[] = 'is_aioseo_settings_enabled';

		return $options;
	}

	/**
	 * Returns whether or not the given Post ID was built with Thrive Architect.
	 *
	 * @since 4.6.6
	 *
	 * @param  int     $postId The Post ID.
	 * @return boolean         Whether or not the Post was built with Thrive Architect.
	 */
	public function isBuiltWith( $postId ) {
		if ( ! function_exists( 'tcb_post' ) ) {
			return false;
		}

		return tcb_post( $postId )->editor_enabled();
	}

	/**
	 * Returns whether should or not limit the modified date.
	 *
	 * @since 4.6.6
	 *
	 * @param  int     $postId The Post ID.
	 * @return boolean         Whether or not sholud limit the modified date.
	 */
	public function limitModifiedDate( $postId ) {
		if ( ! class_exists( 'TCB_Editor_Ajax' ) ) {
			return false;
		}

		// This method is supposed to be used in the `wp_ajax_tcb_editor_ajax` action.
		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), \TCB_Editor_Ajax::NONCE_KEY ) ) {
			return false;
		}

		$editorPostId = ! empty( $_REQUEST['post_id'] ) ? intval( $_REQUEST['post_id'] ) : 0;
		if ( $editorPostId !== $postId ) {
			return false;
		}

		return ! empty( $_REQUEST['aioseo_limit_modified_date'] ) && 'true' === $_REQUEST['aioseo_limit_modified_date'];
	}
}Common/Standalone/PageBuilders/WPBakery.php000064400000006772151536241200014642 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;

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

/**
 * Integrate our SEO Panel with WPBakery Page Builder.
 *
 * @since 4.5.2
 */
class WPBakery extends Base {
	/**
	 * The plugin files.
	 *
	 * @since 4.5.2
	 *
	 * @var array
	 */
	public $plugins = [
		'js_composer/js_composer.php',
		'js_composer_salient/js_composer.php'
	];

	/**
	 * The integration slug.
	 *
	 * @since 4.5.2
	 *
	 * @var string
	 */
	public $integrationSlug = 'wpbakery';

	/**
	 * Init the integration.
	 *
	 * @since 4.5.2
	 *
	 * @return void
	 */
	public function init() {
		// Disable SEO meta tags from WP Bakery.
		if ( defined( 'WPB_VC_VERSION' ) && version_compare( WPB_VC_VERSION, '7.4', '>=' ) ) {
			add_filter( 'get_post_metadata', [ $this, 'maybeDisableWpBakeryMetaTags' ], 10, 3 );
		}

		if ( ! aioseo()->postSettings->canAddPostSettingsMetabox( get_post_type( $this->getPostId() ) ) ) {
			return;
		}

		add_action( 'vc_frontend_editor_enqueue_js_css', [ $this, 'enqueue' ] );
		add_action( 'vc_backend_editor_enqueue_js_css', [ $this, 'enqueue' ] );

		add_filter( 'vc_nav_front_controls', [ $this, 'addNavbarCotnrols' ] );
		add_filter( 'vc_nav_controls', [ $this, 'addNavbarCotnrols' ] );
	}

	/**
	 * Maybe disable WP Bakery meta tags.
	 *
	 * @since 4.7.1
	 *
	 * @param  mixed  $value    The value of the meta.
	 * @param  int    $objectId The object ID.
	 * @param  string $metaKey  The meta key.
	 * @return mixed            The value of the meta.
	 */
	public function maybeDisableWpBakeryMetaTags( $value, $objectId, $metaKey ) {
		if ( is_singular() && '_wpb_post_custom_seo_settings' === $metaKey ) {
			return null;
		}

		return $value;
	}

	public function addNavbarCotnrols( $controlList ) {
		$controlList[] = [
			'aioseo',
			'<li class="vc_show-mobile"><div id="aioseo-wpbakery" style="height: 100%;"></div></li>'
		];

		return $controlList;
	}

	/**
	 * Returns whether or not the given Post ID was built with WPBakery.
	 *
	 * @since 4.5.2
	 *
	 * @param  int $postId The Post ID.
	 * @return boolean     Whether or not the Post was built with WPBakery.
	 */
	public function isBuiltWith( $postId ) {
		$postObj = get_post( $postId );
		if ( ! empty( $postObj ) && preg_match( '/vc_row/', (string) $postObj->post_content ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Returns whether should or not limit the modified date.
	 *
	 * @since 4.5.2
	 *
	 * @param  int     $postId The Post ID.
	 * @return boolean         Whether or not sholud limit the modified date.
	 */
	public function limitModifiedDate( $postId ) {
		// This method is supposed to be used in the `saveAjaxFe` action.
		if ( empty( $_REQUEST['_vcnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_vcnonce'] ) ), 'vc-nonce-vc-admin-nonce' ) ) {
			return false;
		}

		$editorPostId = ! empty( $_REQUEST['post_id'] ) ? intval( $_REQUEST['post_id'] ) : 0;
		if ( $editorPostId !== $postId ) {
			return false;
		}

		return ! empty( $_REQUEST['aioseo_limit_modified_date'] ) && (bool) $_REQUEST['aioseo_limit_modified_date'];
	}

	/**
	 * Returns the processed page builder content.
	 *
	 * @since 4.5.2
	 *
	 * @param  int    $postId  The post id.
	 * @param  string $content The raw content.
	 * @return string          The processed content.
	 */
	public function processContent( $postId, $content = '' ) {
		if ( method_exists( '\WPBMap', 'addAllMappedShortcodes' ) ) {
			\WPBMap::addAllMappedShortcodes();
		}

		return parent::processContent( $postId, $content );
	}
}Common/Standalone/PrimaryTerm.php000064400000002402151536241200013045 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

/**
 * Handles the Primary Term feature.
 *
 * @since 4.3.6
 */
class PrimaryTerm {
	/**
	 * Class constructor.
	 *
	 * @since 4.3.6
	 */
	public function __construct() {
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}

		if ( wp_doing_ajax() || wp_doing_cron() || ! is_admin() ) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAssets' ] );
	}

	/**
	 * Enqueues the JS/CSS for the on page/posts settings.
	 *
	 * @since 4.3.6
	 *
	 * @return void
	 */
	public function enqueueAssets() {
		if ( ! aioseo()->helpers->isScreenBase( 'post' ) ) {
			return;
		}

		aioseo()->core->assets->load( 'src/vue/standalone/primary-term/main.js', [], aioseo()->helpers->getVueData( 'post' ) );
	}

	/**
	 * Returns the primary term for the given taxonomy name.
	 *
	 * @since 4.3.6
	 *
	 * @param  int            $postId       The post ID.
	 * @param  string         $taxonomyName The taxonomy name.
	 * @return \WP_Term|false               The term or false.
	 */
	public function getPrimaryTerm( $postId, $taxonomyName ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return false;
	}
}Common/Standalone/PublishPanel.php000064400000001365151536241200013167 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Handles the Publish Panel in the Block Editor.
 *
 * @since 4.2.0
 */
class PublishPanel {
	/**
	 * Class constructor.
	 *
	 * @since 4.2.0
	 */
	public function __construct() {
		if ( ! is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScript' ] );
	}

	/**
	 * Enqueues the script.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	public function enqueueScript() {
		if ( ! aioseo()->helpers->isScreenBase( 'post' ) ) {
			return;
		}

		aioseo()->core->assets->load( 'src/vue/standalone/publish-panel/main.js' );
	}
}Common/Standalone/SeoPreview.php000064400000021725151536241200012673 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

use AIOSEO\Plugin\Common\Models;
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Handles the SEO Preview feature on the front-end.
 *
 * @since 4.2.8
 */
class SeoPreview {
	/**
	 * Whether this feature is allowed on the current page or not.
	 *
	 * @since 4.2.8
	 *
	 * @var bool
	 */
	private $enable = false;

	/**
	 * The relative JS filename for this standalone.
	 *
	 * @since 4.3.1
	 *
	 * @var string
	 */
	private $mainAssetRelativeFilename = 'src/vue/standalone/seo-preview/main.js';

	/**
	 * Class constructor.
	 *
	 * @since 4.2.8
	 */
	public function __construct() {
		// Allow users to disable SEO Preview.
		if ( apply_filters( 'aioseo_seo_preview_disable', false ) ) {
			return;
		}

		// Hook into `wp` in order to have access to the WP queried object.
		add_action( 'wp', [ $this, 'init' ], 20 );
	}

	/**
	 * Initialize the feature.
	 * Hooked into `wp` action hook.
	 *
	 * @since 4.2.8
	 *
	 * @return void
	 */
	public function init() {
		if (
			is_admin() ||
			! aioseo()->helpers->isAdminBarEnabled() ||
			// If we're seeing the Divi theme Visual Builder.
			( function_exists( 'et_core_is_fb_enabled' ) && et_core_is_fb_enabled() ) ||
			aioseo()->helpers->isAmpPage()
		) {
			return;
		}

		$allow = [
			'archive',
			'attachment',
			'author',
			'date',
			'dynamic_home',
			'page',
			'search',
			'single',
			'taxonomy',
		];

		if ( ! in_array( aioseo()->helpers->getTemplateType(), $allow, true ) ) {
			return;
		}

		$this->enable = true;

		// Prevent Autoptimize from optimizing the translations for the SEO Preview. If we don't do this, Autoptimize can break the frontend for certain languages - #5235.
		if ( is_user_logged_in() && 'en_US' !== get_user_locale() ) {
			add_filter( 'autoptimize_filter_noptimize', '__return_true' );
		}

		// As WordPress uses priority 10 to print footer scripts we use 9 to make sure our script still gets output.
		add_action( 'wp_print_footer_scripts', [ $this, 'enqueueScript' ], 9 );
	}

	/**
	 * Hooked into `wp_print_footer_scripts` action hook.
	 * Enqueue the standalone JS the latest possible and prevent 3rd-party performance plugins from merging it.
	 *
	 * @since 4.3.1
	 *
	 * @return void
	 */
	public function enqueueScript() {
		aioseo()->core->assets->load( $this->mainAssetRelativeFilename, [], $this->getVueData(), 'aioseoSeoPreview' );
		aioseo()->main->enqueueTranslations();
	}

	/**
	 * Returns the data for Vue.
	 *
	 * @since 4.2.8
	 *
	 * @return array The data.
	 */
	private function getVueData() {
		$data = [
			'editGoogleSnippetUrl'   => '',
			'editFacebookSnippetUrl' => '',
			'editTwitterSnippetUrl'  => '',
			'editObjectBtnText'      => '',
			'editObjectUrl'          => '',
			'keyphrases'             => '',
			'page_analysis'          => '',
			'urls'                   => [
				'home'        => home_url(),
				'domain'      => aioseo()->helpers->getSiteDomain(),
				'mainSiteUrl' => aioseo()->helpers->getSiteUrl(),
			],
			'mainAssetCssQueue'      => aioseo()->core->assets->getJsAssetCssQueue( $this->mainAssetRelativeFilename ),
			'data'                   => [
				'isDev'           => aioseo()->helpers->isDev(),
				'siteName'        => aioseo()->helpers->getWebsiteName(),
				'usingPermalinks' => aioseo()->helpers->usingPermalinks()
			]
		];

		if ( BuddyPressIntegration::isComponentPage() ) {
			return array_merge( $data, aioseo()->standalone->buddyPress->getVueDataSeoPreview() );
		}

		$queriedObject = get_queried_object(); // Don't use the getTerm helper here.
		$templateType  = aioseo()->helpers->getTemplateType();
		if (
			'taxonomy' === $templateType ||
			'single' === $templateType ||
			'page' === $templateType ||
			'attachment' === $templateType
		) {
			$labels = null;

			if ( is_a( $queriedObject, 'WP_Term' ) ) {
				$wpObject              = $queriedObject;
				$labels                = get_taxonomy_labels( get_taxonomy( $queriedObject->taxonomy ) );
				$data['editObjectUrl'] = get_edit_term_link( $queriedObject, $queriedObject->taxonomy );
			} else {
				$wpObject = aioseo()->helpers->getPost();

				if ( is_a( $wpObject, 'WP_Post' ) ) {
					$labels                = get_post_type_labels( get_post_type_object( $wpObject->post_type ) );
					$data['editObjectUrl'] = get_edit_post_link( $wpObject, 'url' );

					if (
						! aioseo()->helpers->isSpecialPage( $wpObject->ID ) &&
						'attachment' !== $templateType
					) {
						$aioseoPost            = Models\Post::getPost( $wpObject->ID );
						$data['page_analysis'] = Models\Post::getPageAnalysisDefaults( $aioseoPost->page_analysis );
						$data['keyphrases']    = Models\Post::getKeyphrasesDefaults( $aioseoPost->keyphrases );
					}
				}
			}

			// At this point if `$wpObject` is not an instance of WP_Term nor WP_Post, then we can't have the URLs.
			if (
				is_object( $wpObject ) &&
				is_object( $labels )
			) {
				$data['editObjectBtnText'] = sprintf(
					// Translators: 1 - A noun for something that's being edited ("Post", "Page", "Article", "Product", etc.).
					esc_html__( 'Edit %1$s', 'all-in-one-seo-pack' ),
					$labels->singular_name
				);
				$data['editGoogleSnippetUrl']   = $this->getEditSnippetUrl( $templateType, 'google', $wpObject );
				$data['editFacebookSnippetUrl'] = $this->getEditSnippetUrl( $templateType, 'facebook', $wpObject );
				$data['editTwitterSnippetUrl']  = $this->getEditSnippetUrl( $templateType, 'twitter', $wpObject );
			}
		}

		if (
			'archive' === $templateType ||
			'author' === $templateType ||
			'date' === $templateType ||
			'search' === $templateType
		) {
			if ( is_a( $queriedObject, 'WP_User' ) ) {
				$data['editObjectUrl']     = get_edit_user_link( $queriedObject->ID );
				$data['editObjectBtnText'] = esc_html__( 'Edit User', 'all-in-one-seo-pack' );
			}

			$data['editGoogleSnippetUrl'] = $this->getEditSnippetUrl( $templateType, 'google' );
		}

		if ( 'dynamic_home' === $templateType ) {
			$data['editGoogleSnippetUrl']   = $this->getEditSnippetUrl( $templateType, 'google' );
			$data['editFacebookSnippetUrl'] = $this->getEditSnippetUrl( $templateType, 'facebook' );
			$data['editTwitterSnippetUrl']  = $this->getEditSnippetUrl( $templateType, 'twitter' );
		}

		return $data;
	}

	/**
	 * Get the URL to the place where the snippet details can be edited.
	 *
	 * @since 4.2.8
	 *
	 * @param  string                 $templateType The WP template type {@see WpContext::getTemplateType}.
	 * @param  string                 $snippet      'google', 'facebook' or 'twitter'.
	 * @param  \WP_Post|\WP_Term|null $object       Post or term object.
	 * @return string                               The URL. Returns an empty string if nothing matches.
	 */
	private function getEditSnippetUrl( $templateType, $snippet, $object = null ) {
		$url = '';

		// Bail if `$snippet` doesn't fit requirements.
		if ( ! in_array( $snippet, [ 'google', 'facebook', 'twitter' ], true ) ) {
			return $url;
		}

		// If we're in a post/page/term (not an attachment) we'll have a URL directly to the meta box.
		if ( in_array( $templateType, [ 'single', 'page', 'attachment', 'taxonomy' ], true ) ) {
			$url = 'taxonomy' === $templateType
				? get_edit_term_link( $object, $object->taxonomy ) . '#aioseo-term-settings-field'
				: get_edit_post_link( $object, 'url' ) . '#aioseo-settings';

			$queryArgs = [ 'aioseo-tab' => 'general' ];
			if ( in_array( $snippet, [ 'facebook', 'twitter' ], true ) ) {
				$queryArgs = [
					'aioseo-tab' => 'social',
					'social-tab' => $snippet
				];
			}

			return add_query_arg( $queryArgs, $url );
		}

		// If we're in any sort of archive let's point to the global archive editing.
		if ( in_array( $templateType, [ 'archive', 'author', 'date', 'search' ], true ) ) {
			return admin_url( 'admin.php?page=aioseo-search-appearance' ) . '#/archives';
		}

		// If homepage is set to show the latest posts let's point to the global home page editing.
		if ( 'dynamic_home' === $templateType ) {
			// Default `$url` for 'google' snippet.
			$url = add_query_arg(
				[ 'aioseo-scroll' => 'home-page-settings' ],
				admin_url( 'admin.php?page=aioseo-search-appearance' ) . '#/global-settings'
			);

			if ( in_array( $snippet, [ 'facebook', 'twitter' ], true ) ) {
				$url = admin_url( 'admin.php?page=aioseo-social-networks' ) . '#/' . $snippet;
			}

			return $url;
		}

		return $url;
	}

	/**
	 * Returns the "SEO Preview" submenu item data ("node" as WP calls it).
	 *
	 * @since 4.2.8
	 *
	 * @return array The admin bar menu item data or an empty array if this feature is disabled.
	 */
	public function getAdminBarMenuItemNode() {
		if ( ! $this->enable ) {
			return [];
		}

		$title = esc_html__( 'SEO Preview', 'all-in-one-seo-pack' );

		// @TODO Remove 'NEW' after a couple months.
		$title .= '<span class="aioseo-menu-new-indicator">';
		$title .= esc_html__( 'NEW', 'all-in-one-seo-pack' ) . '!';
		$title .= '</span>';

		return [
			'id'     => 'aioseo-seo-preview',
			'parent' => 'aioseo-main',
			'title'  => $title,
			'href'   => '#',
		];
	}
}Common/Standalone/SetupWizard.php000064400000015073151536241200013063 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

/**
 * Class that holds our setup wizard.
 *
 * @since 4.0.0
 */
class SetupWizard {
	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		if ( ! is_admin() || wp_doing_cron() || wp_doing_ajax() ) {
			return;
		}

		add_action( 'admin_menu', [ $this, 'addDashboardPage' ] );
		add_action( 'admin_head', [ $this, 'hideDashboardPageFromMenu' ] );
		add_action( 'admin_init', [ $this, 'maybeLoadOnboardingWizard' ] );
		add_action( 'admin_init', [ $this, 'redirect' ], 9999 );
	}

	/**
	 * Onboarding Wizard redirect.
	 *
	 * This function checks if a new install or update has just occurred. If so,
	 * then we redirect the user to the appropriate page.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function redirect() {
		// Check if we should consider redirection.
		if ( ! aioseo()->core->cache->get( 'activation_redirect' ) ) {
			return;
		}

		// If we are redirecting, clear the transient so it only happens once.
		aioseo()->core->cache->delete( 'activation_redirect' );

		// Check option to disable welcome redirect.
		if ( get_option( 'aioseo_activation_redirect', false ) ) {
			return;
		}

		// Only do this for single site installs.
		if ( isset( $_GET['activate-multi'] ) || is_network_admin() ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			return;
		}

		wp_safe_redirect( admin_url( 'index.php?page=aioseo-setup-wizard' ) );
		exit;
	}

	/**
	 * Adds a dashboard page for our setup wizard.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addDashboardPage() {
		add_dashboard_page( '', '', aioseo()->admin->getPageRequiredCapability( 'aioseo-setup-wizard' ), 'aioseo-setup-wizard', '' );
	}

	/**
	 * Hide the dashboard page from the menu.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	public function hideDashboardPageFromMenu() {
		remove_submenu_page( 'index.php', 'aioseo-setup-wizard' );
	}

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

		// Check for wizard-specific parameter
		// Allow plugins to disable the setup wizard
		// Check if current user is allowed to save settings.
		if (
			// phpcs:disable HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			! isset( $_GET['page'] ) ||
			'aioseo-setup-wizard' !== sanitize_text_field( wp_unslash( $_GET['page'] ) ) ||
			// phpcs:enable
			! current_user_can( aioseo()->admin->getPageRequiredCapability( 'aioseo-setup-wizard' ) )
		) {
			return;
		}

		set_current_screen();

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

		// If we are redirecting, clear the transient so it only happens once.
		aioseo()->core->cache->delete( 'activation_redirect' );

		$this->loadOnboardingWizard();
	}

	/**
	 * Load the Onboarding Wizard template.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function loadOnboardingWizard() {
		$this->enqueueScripts();
		$this->setupWizardHeader();
		$this->setupWizardContent();
		$this->setupWizardFooter();
		exit;
	}

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

		aioseo()->core->assets->load( 'src/vue/standalone/setup-wizard/main.js', [], aioseo()->helpers->getVueData( 'setup-wizard' ) );

		aioseo()->main->enqueueTranslations();

		wp_enqueue_style( 'common' );
		wp_enqueue_media();
	}

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

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

	/**
	 * Outputs the simplified footer used for the Onboarding Wizard.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function setupWizardFooter() {
		?>
		<?php
		wp_print_scripts( 'aioseo-vendors' );
		wp_print_scripts( 'aioseo-common' );
		wp_print_scripts( 'aioseo-setup-wizard-script' );
		do_action( 'admin_footer', '' );
		do_action( 'admin_print_footer_scripts' );
		// do_action( 'customize_controls_print_footer_scripts' );
		?>
		</body>
		</html>
		<?php
	}

	/**
	 * Check whether or not the Setup Wizard is completed.
	 *
	 * @since 4.2.0
	 *
	 * @return boolean Whether or not the Setup Wizard is completed.
	 */
	public function isCompleted() {
		$wizard = (string) aioseo()->internalOptions->internal->wizard;
		$wizard = json_decode( $wizard );
		if ( ! $wizard ) {
			return false;
		}

		$totalStageCount   = count( $wizard->stages );
		$currentStageCount = array_search( $wizard->currentStage, $wizard->stages, true );

		// If not found, let's assume it's completed.
		if ( false === $currentStageCount ) {
			return true;
		}

		return $currentStageCount + 1 === $totalStageCount;
	}

	/**
	 * Get the next stage of the wizard.
	 *
	 * @since 4.6.2
	 *
	 * @return string The next stage or empty.
	 */
	public function getNextStage() {
		$wizard    = (string) aioseo()->internalOptions->internal->wizard;
		$wizard    = json_decode( $wizard );
		if ( ! $wizard ) {
			return '';
		}

		// Default to success.
		$nextStage = 'success';

		// Get the next stage of the wizard.
		$currentStageIndex = array_search( $wizard->currentStage, $wizard->stages, true );
		if ( ! empty( $wizard->stages[ $currentStageIndex + 1 ] ) ) {
			$nextStage = $wizard->stages[ $currentStageIndex + 1 ];
		}

		return $nextStage;
	}
}Common/Standalone/Standalone.php000064400000005541151536241200012671 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

use AIOSEO\Plugin\Pro\Standalone as ProStandalone;

/**
 * Registers the standalone components.
 *
 * @since 4.2.0
 */
class Standalone {
	/**
	 * HeadlineAnalyzer class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var HeadlineAnalyzer
	 */
	public $headlineAnalyzer = null;

	/**
	 * FlyoutMenu class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var FlyoutMenu
	 */
	public $flyoutMenu = null;

	/**
	 * SeoPreview class instance.
	 *
	 * @since 4.2.8
	 *
	 * @var SeoPreview
	 */
	public $seoPreview = null;

	/**
	 * SetupWizard class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var SetupWizard
	 */
	public $setupWizard = null;

	/**
	 * PrimaryTerm class instance.
	 *
	 * @since 4.3.6
	 *
	 * @var PrimaryTerm
	 */
	public $primaryTerm = null;

	/**
	 * UserProfileTab class instance.
	 *
	 * @since 4.5.4
	 *
	 * @var UserProfileTab
	 */
	public $userProfileTab = null;

	/**
	 * BuddyPress class instance.
	 *
	 * @since 4.7.6
	 *
	 * @var BuddyPress\BuddyPress
	 */
	public $buddyPress = null;

	/**
	 * BbPress class instance.
	 *
	 * @since 4.8.1
	 *
	 * @var BbPress\BbPress
	 */
	public $bbPress = null;

	/**
	 * List of page builder integration class instances.
	 *
	 * @since 4.2.7
	 *
	 * @var array[Object]
	 */
	public $pageBuilderIntegrations = [];

	/**
	 * List of block class instances.
	 *
	 * @since 4.2.7
	 *
	 * @var array[Object]
	 */
	public $standaloneBlocks = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.2.0
	 */
	public function __construct() {
		$this->headlineAnalyzer = new HeadlineAnalyzer();
		$this->flyoutMenu       = new FlyoutMenu();
		$this->seoPreview       = new SeoPreview();
		$this->setupWizard      = new SetupWizard();
		$this->primaryTerm      = aioseo()->pro ? new ProStandalone\PrimaryTerm() : new PrimaryTerm();
		$this->userProfileTab   = new UserProfileTab();
		$this->buddyPress       = aioseo()->pro ? new ProStandalone\BuddyPress\BuddyPress() : new BuddyPress\BuddyPress();
		$this->bbPress          = aioseo()->pro ? new ProStandalone\BbPress\BbPress() : new BbPress\BbPress();

		aioseo()->pro ? new ProStandalone\DetailsColumn() : new DetailsColumn();

		new AdminBarNoindexWarning();
		new LimitModifiedDate();
		new Notifications();
		new PublishPanel();
		new WpCode();

		$this->pageBuilderIntegrations = [
			'elementor'  => new PageBuilders\Elementor(),
			'divi'       => new PageBuilders\Divi(),
			'seedprod'   => new PageBuilders\SeedProd(),
			'wpbakery'   => new PageBuilders\WPBakery(),
			'avada'      => new PageBuilders\Avada(),
			'siteorigin' => new PageBuilders\SiteOrigin(),
			'thrive'     => new PageBuilders\ThriveArchitect()
		];

		$this->standaloneBlocks = [
			'tocBlock'       => new Blocks\TableOfContents(),
			'faqBlock'       => new Blocks\FaqPage(),
			'keyPointsBlock' => new Blocks\KeyPoints()
		];
	}
}Common/Standalone/UserProfileTab.php000064400000012537151536241200013472 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

use AIOSEO\Plugin\Pro\Standalone as ProStandalone;

/**
 * Registers the standalone components.
 *
 * @since 4.2.2
 */
class UserProfileTab {
	/**
	 * Class constructor.
	 *
	 * @since 4.2.2
	 */
	public function __construct() {
		if ( ! is_admin() ) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScript' ] );
		add_action( 'profile_update', [ $this, 'updateUserSocialProfiles' ] );
	}

	/**
	 * Enqueues the script.
	 *
	 * @since 4.2.2
	 *
	 * @return void
	 */
	public function enqueueScript() {
		if ( apply_filters( 'aioseo_user_profile_tab_disable', false ) ) {
			return;
		}

		$screen = aioseo()->helpers->getCurrentScreen();
		if ( empty( $screen->id ) ) {
			return;
		}

		if ( ! in_array( $screen->id, [ 'user-edit', 'profile' ], true ) ) {
			if ( 'follow-up_page_followup-emails-reports' === $screen->id ) {
				aioseo()->core->assets->load( 'src/vue/standalone/user-profile-tab/follow-up-emails-nav-bar.js' );
			}

			return;
		}

		global $user_id; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if ( ! intval( $user_id ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			return;
		}

		aioseo()->core->assets->load( 'src/vue/standalone/user-profile-tab/main.js', [], $this->getVueData() );
		// Load script again so we can add extra data to localize the strings.
		aioseo()->core->assets->load( 'src/vue/standalone/user-profile-tab/main.js', [], [
			'translations' => aioseo()->helpers->getJedLocaleData( 'aioseo-eeat' )
		], 'eeat' );
	}

	/**
	 * Returns the data Vue requires.
	 *
	 * @since 4.2.2
	 *
	 * @return array
	 */
	public function getVueData() {
		global $user_id; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		$socialProfiles = $this->getSocialProfiles();
		foreach ( $socialProfiles as $platformKey => $v ) {
			$metaName                        = 'aioseo_' . aioseo()->helpers->toSnakeCase( $platformKey );
			$socialProfiles[ $platformKey ] = get_user_meta( $user_id, $metaName, true ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		}

		$sameUsername = get_user_meta( $user_id, 'aioseo_profiles_same_username', true ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if ( empty( $sameUsername ) ) {
			$sameUsername = [
				'enable'   => false,
				'username' => '',
				'included' => [ 'facebookPageUrl', 'twitterUrl', 'tiktokUrl', 'pinterestUrl', 'instagramUrl', 'youtubeUrl', 'linkedinUrl' ] // Same as in Options.php.
			];
		}

		$additionalurls = get_user_meta( $user_id, 'aioseo_profiles_additional_urls', true ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		$extraVueData = [
			'userProfile' => [
				'userData'                          => get_userdata( $user_id )->data, // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				'profiles'                          => [
					'sameUsername'   => $sameUsername,
					'urls'           => $socialProfiles,
					'additionalUrls' => $additionalurls
				],
				'isWooCommerceFollowupEmailsActive' => aioseo()->helpers->isWooCommerceFollowupEmailsActive()
			]
		];

		$vueData = aioseo()->helpers->getVueData();
		$vueData = array_merge( $vueData, $extraVueData );

		return $vueData;
	}

	/**
	 * Updates the user social profile URLs when a user's profile is updated.
	 *
	 * @since 4.2.2
	 *
	 * @param  int  $userId The user ID.
	 * @return void
	 */
	public function updateUserSocialProfiles( $userId ) {
		if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'update-user_' . $userId ) ) {
			return;
		}

		if ( empty( $_POST['aioseo-user-social-profiles'] ) ) {
			return;
		}

		$data = json_decode( sanitize_text_field( wp_unslash( $_POST['aioseo-user-social-profiles'] ) ), true );
		if ( empty( $data ) ) {
			return;
		}

		$sanitizedIncluded = [];
		foreach ( $data['sameUsername']['included'] as $platformKey ) {
			$sanitizedIncluded[] = sanitize_text_field( $platformKey );
		}

		$sanitizedSameUsernameData = [
			'enable'   => (bool) $data['sameUsername']['enable'],
			'username' => sanitize_text_field( $data['sameUsername']['username'] ),
			'included' => array_filter( $sanitizedIncluded )
		];

		update_user_meta( $userId, 'aioseo_profiles_same_username', $sanitizedSameUsernameData );

		foreach ( $data['urls'] as $platformKey => $value ) {
			$value    = sanitize_text_field( $value );
			$metaName = 'aioseo_' . aioseo()->helpers->toSnakeCase( $platformKey );
			update_user_meta( $userId, $metaName, $value );
		}

		$additionalUrls          = sanitize_text_field( $data['additionalUrls'] );
		$sanitizedAdditionalUrls = preg_replace( '/\h/', "\n", (string) $additionalUrls );
		update_user_meta( $userId, 'aioseo_profiles_additional_urls', $sanitizedAdditionalUrls );
	}

	/**
	 * Returns a list of supported social profiles.
	 *
	 * @since 4.2.2
	 *
	 * @return array
	 */
	public function getSocialProfiles() {
		return [
			'facebookPageUrl' => '',
			'twitterUrl'      => '',
			'instagramUrl'    => '',
			'tiktokUrl'       => '',
			'pinterestUrl'    => '',
			'youtubeUrl'      => '',
			'linkedinUrl'     => '',
			'tumblrUrl'       => '',
			'yelpPageUrl'     => '',
			'soundCloudUrl'   => '',
			'wikipediaUrl'    => '',
			'myspaceUrl'      => '',
			'wordPressUrl'    => '',
			'blueskyUrl'      => '',
			'threadsUrl'      => ''
		];
	}
}Common/Standalone/WpCode.php000064400000001354151536241200011760 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

/**
 * Handles registering the AIOSEO username in the WPCode snippets library.
 *
 * @since 4.3.8
 */
class WpCode {
	/**
	 * Class constructor.
	 *
	 * @since 4.3.8
	 */
	public function __construct() {
		if ( ! is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		add_action( 'wpcode_loaded', [ $this, 'registerUsername' ] );
	}

	/**
	 * Enqueues the script.
	 *
	 * @since 4.3.8
	 *
	 * @return void
	 */
	public function registerUsername() {
		if ( ! function_exists( 'wpcode_register_library_username' ) ) {
			return;
		}

		wpcode_register_library_username( 'aioseo', AIOSEO_PLUGIN_SHORT_NAME );
	}
}Common/ThirdParty/ThirdParty.php000064400000000672151536241200012675 0ustar00<?php
namespace AIOSEO\Plugin\Common\ThirdParty;

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

/**
 * Instantiates our third-party classes.
 *
 * @since 4.7.6
 */
class ThirdParty {
	/**
	 * WebStories instance.
	 *
	 * @since 4.7.6
	 *
	 * @var WebStories
	 */
	public $webStories;

	/**
	 * Class constructor.
	 *
	 * @since 4.7.6
	 */
	public function __construct() {
		$this->webStories = new WebStories();
	}
}Common/ThirdParty/WebStories.php000064400000002767151536241200012700 0ustar00<?php
namespace AIOSEO\Plugin\Common\ThirdParty;

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

/**
 * Integrates with Google Web Stories plugin.
 *
 * @since 4.8.3
 */
class WebStories {
	/**
	 * Class constructor.
	 *
	 * @since 4.7.6
	 */
	public function __construct() {
		add_action( 'web_stories_story_head', [ $this, 'stripDefaultTags' ], 0 );
		add_action( 'web_stories_story_head', [ $this, 'outputAioseoTags' ] );
	}

	/**
	 * Strip all meta tags that are added by default by the Web Stories plugin.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	public function stripDefaultTags() {
		add_filter( 'web_stories_enable_metadata', '__return_false' );
		add_filter( 'web_stories_enable_schemaorg_metadata', '__return_false' );
		add_filter( 'web_stories_enable_open_graph_metadata', '__return_false' );
		add_filter( 'web_stories_enable_twitter_metadata', '__return_false' );

		remove_action( 'web_stories_story_head', 'rel_canonical' );
		remove_action( 'web_stories_story_head', 'wp_robots' );

		// This is needed to prevent multiple robots meta tags from being output.
		add_filter( 'wp_robots', '__return_empty_array' );
	}

	/**
	 * Output the AIOSEO tags.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	public function outputAioseoTags() {
		aioseo()->head->wpHead();
	}

	/**
	 * Checks if the plugin is active.
	 *
	 * @since 4.7.6
	 *
	 * @return bool True if the plugin is active.
	 */
	public function isPluginActive() {
		return class_exists( 'Google\Web_Stories\Plugin' );
	}
}Common/Tools/Htaccess.php000064400000004320151536241200011340 0ustar00<?php
namespace AIOSEO\Plugin\Common\Tools;

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

class Htaccess {
	/**
	 * The path to the .htaccess file.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $path = '';

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->path = ABSPATH . '.htaccess';
	}

	/**
	 * Get the contents of the .htaccess file.
	 *
	 * @since 4.0.0
	 *
	 * @return string The contents of the file.
	 */
	public function getContents() {
		$fs = aioseo()->core->fs;
		if ( ! $fs->exists( $this->path ) ) {
			return false;
		}

		$contents = $fs->getContents( $this->path );

		return aioseo()->helpers->encodeOutputHtml( $contents );
	}

	/**
	 * Saves the contents of the .htaccess file.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $contents The contents to write.
	 * @return boolean           True if the file was updated.
	 */
	public function saveContents( $contents ) {
		$fs = aioseo()->core->fs;
		if ( ! $fs->isWritable( $this->path ) ) {
			return [
				'success' => false,
				'reason'  => 'file-not-writable',
				'message' => __( 'We were unable to save the .htaccess file because the file was not writable. Please check the file permissions and try again.', 'all-in-one-seo-pack' )
			];
		}

		$fileExists       = $fs->exists( $this->path );
		$originalContents = $fileExists ? $fs->getContents( $this->path ) : null;
		$fileSaved        = $fs->putContents( $this->path, $contents );
		if ( false === $fileSaved ) {
			return [
				'success' => false,
				'reason'  => 'file-not-saved'
			];
		}

		$response       = wp_remote_get( home_url( '?' . time() ) );
		$isValidRequest = wp_remote_retrieve_response_code( $response );

		if (
			// Add an exception for Windows devs since the request fails in Local.
			! defined( 'AIOSEO_DEV_WINDOWS' ) &&
			( is_wp_error( $response ) || 200 !== $isValidRequest )
		) {
			$fs->putContents( $this->path, $originalContents );

			return [
				'success' => false,
				'reason'  => 'syntax-errors',
				'message' => __( 'We were unable to save the .htaccess file due to syntax errors. Please check the code below and try again.', 'all-in-one-seo-pack' )
			];
		}

		return [
			'success' => true
		];
	}
}Common/Tools/RobotsTxt.php000064400000045145151536241200011565 0ustar00<?php
namespace AIOSEO\Plugin\Common\Tools;

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

use AIOSEO\Plugin\Common\Models;

class RobotsTxt {
	/**
	 * Which directives are allowed to be extracted.
	 *
	 * @since 4.4.2
	 *
	 * @var array
	 */
	private $allowedDirectives = [ 'user-agent', 'allow', 'disallow', 'clean-param', 'crawl-delay' ];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_filter( 'robots_txt', [ $this, 'buildRules' ], 10000 );

		if ( ! is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		add_action( 'init', [ $this, 'checkForPhysicalFiles' ] );
	}

	/**
	 * Build out the robots.txt rules.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $original The original rules to parse.
	 * @return string           The parsed/appended/modified rules.
	 */
	public function buildRules( $original ) {
		// Other plugins might call this too early.
		if ( ! property_exists( aioseo(), 'sitemap' ) ) {
			return $original;
		}

		$searchAppearanceRules = $this->extractSearchAppearanceRules();
		$networkRules          = [];
		if ( is_multisite() ) {
			$searchAppearanceRules = array_merge(
				$searchAppearanceRules,
				$this->extractSearchAppearanceRules( aioseo()->networkOptions->tools->robots->rules )
			);
			$networkRules          = aioseo()->networkOptions->tools->robots->enable ? aioseo()->networkOptions->tools->robots->rules : [];
		}

		$originalRules = $this->extractRules( $original );
		$ruleset       = $this->mergeRules( $originalRules, $this->groupRulesByUserAgent( $searchAppearanceRules ) );
		if ( ! aioseo()->options->tools->robots->enable ) {
			$ruleset = $this->mergeRules( $ruleset, $this->groupRulesByUserAgent( $networkRules ) );
		} else {
			$ruleset = $this->mergeRules(
				$ruleset,
				$this->mergeRules( $this->groupRulesByUserAgent( $networkRules ), $this->groupRulesByUserAgent( aioseo()->options->tools->robots->rules ) ),
				true
			);
		}

		/**
		 * Any plugin can wrongly modify the robots.txt output by hoking into the `do_robots` action hook,
		 * instead of hooking into the `robots_txt` filter hook.
		 * For the first scenario, to make sure our output doesn't conflict with theirs, a new line is necessary.
		 */
		return $this->stringifyRuleset( $ruleset ) . "\n";
	}

	/**
	 * Merges two rulesets.
	 *
	 * @since   4.0.0
	 * @version 4.4.2
	 *
	 * @param  array   $rules1          An array of rules to merge with.
	 * @param  array   $rules2          An array of rules to merge.
	 * @param  boolean $allowOverride   Whether to allow overriding.
	 * @param  boolean $allowDuplicates Whether to allow duplicates.
	 * @return array                    The validated rules.
	 */
	private function mergeRules( $rules1, $rules2, $allowOverride = false, $allowDuplicates = false ) {
		foreach ( $rules2 as $userAgent => $rules ) {
			if ( empty( $userAgent ) ) {
				continue;
			}

			if ( empty( $rules1[ $userAgent ] ) ) {
				$rules1[ $userAgent ] = array_unique( $rules2[ $userAgent ] );

				continue;
			}

			list( $rules1, $rules2 ) = $this->mergeRulesHelper( 'allow', $userAgent, $rules, $rules1, $rules2, $allowDuplicates, $allowOverride );
			list( $rules1, $rules2 ) = $this->mergeRulesHelper( 'disallow', $userAgent, $rules, $rules1, $rules2, $allowDuplicates, $allowOverride );

			$rules1[ $userAgent ] = array_unique( array_merge(
				$rules1[ $userAgent ],
				$rules2[ $userAgent ]
			) );
		}

		return $rules1;
	}

	/**
	 * Helper function for {@see mergeRules()}.
	 *
	 * @since   4.1.2
	 * @version 4.4.2
	 *
	 * @param  string $directive       The directive (allow/disallow).
	 * @param  string $userAgent       The user agent.
	 * @param  array  $rules           The rules.
	 * @param  array  $rules1          The original rules.
	 * @param  array  $rules2          The extra rules.
	 * @param  bool   $allowDuplicates Whether duplicates should be allowed
	 * @param  bool   $allowOverride   Whether the extra rules can override the original ones.
	 * @return array                   The original and extra rules.
	 */
	private function mergeRulesHelper( $directive, $userAgent, $rules, $rules1, $rules2, $allowDuplicates, $allowOverride ) {
		$otherDirective = ( 'allow' === $directive ) ? 'disallow' : 'allow';
		foreach ( $rules as $index1 => $rule ) {
			list( , $ruleValue ) = $this->parseRule( $rule );
			$index2 = array_search( "$otherDirective: $ruleValue", $rules1[ $userAgent ], true );
			if ( false !== $index2 && ! $allowDuplicates ) {
				if ( $allowOverride ) {
					unset( $rules1[ $userAgent ][ $index2 ] );
				} else {
					unset( $rules2[ $userAgent ][ $index1 ] );
				}
			}

			$pattern = str_replace( [ '.', '*', '?', '$' ], [ '\.', '(.*)', '\?', '\$' ], $ruleValue );

			foreach ( $rules1[ $userAgent ] as $rule1 ) {
				$matches = [];
				preg_match( "#^$otherDirective: $pattern$#", (string) $rule1, $matches );
			}

			if ( ! empty( $matches ) && ! $allowDuplicates ) {
				unset( $rules2[ $userAgent ][ $index1 ] );
			}
		}

		return [ $rules1, $rules2 ];
	}

	/**
	 * Parses a rule and extracts the directive and value.
	 *
	 * @since 4.4.2
	 *
	 * @param  string $rule The rule to parse.
	 * @return array        An array containing the parsed directive and value.
	 */
	private function parseRule( $rule ) {
		list( $directive, $value ) = array_map( 'trim', array_pad( explode( ':', $rule, 2 ), 2, '' ) );

		return [ $directive, $value ];
	}

	/**
	 * Stringifies the parsed rules.
	 *
	 * @since   4.0.0
	 * @version 4.4.2
	 *
	 * @param  array  $allRules The rules array.
	 * @return string           The stringified rules.
	 */
	private function stringifyRuleset( $allRules ) {
		$robots = [];
		foreach ( $allRules as $userAgent => $rules ) {
			if ( empty( $userAgent ) ) {
				continue;
			}

			$robots[] = "\r\nUser-agent: $userAgent";
			foreach ( $rules as $rule ) {
				list( $directive, $value ) = $this->parseRule( $rule );
				if ( empty( $directive ) || empty( $value ) ) {
					continue;
				}

				$robots[] = sprintf( '%s: %s', ucfirst( $directive ), $value );
			}
		}

		$robots      = implode( "\r\n", $robots );
		$sitemapUrls = $this->getSitemapRules();
		if ( ! empty( $sitemapUrls ) ) {
			$sitemapUrls = implode( "\r\n", $sitemapUrls );
			$robots      .= "\r\n\r\n$sitemapUrls";
		}

		return trim( $robots );
	}

	/**
	 * Get Sitemap URLs excluding the default ones.
	 *
	 * @since 4.1.7
	 *
	 * @return array An array of the Sitemap URLs.
	 */
	private function getSitemapRules() {
		$defaultSitemaps = $this->extractSitemapUrls( aioseo()->robotsTxt->getDefaultRobotsTxtContent() );
		$sitemapRules    = aioseo()->sitemap->helpers->getSitemapUrlsPrefixed();

		return array_diff( $sitemapRules, $defaultSitemaps );
	}

	/**
	 * Extracts the Search Appearance related rules.
	 *
	 * @since 4.8.1
	 *
	 * @param  array $rules The rules to extract from.
	 * @return array        The Search Appearance related rules.
	 */
	public function extractSearchAppearanceRules( $rules = [] ) {
		$currentRules = $rules ?: aioseo()->options->tools->robots->rules;

		return array_filter( $currentRules, function ( $rule ) {
			$parseRule = json_decode( $rule, true );

			return ! empty( $parseRule['bot'] ) || ! empty( $parseRule['preventCrawling'] );
		} );
	}

	/**
	 * Parses the rules.
	 *
	 * @since   4.0.0
	 * @version 4.4.2
	 *
	 * @param  array $rules An array of rules.
	 * @return array        The rules grouped by user agent.
	 */
	private function groupRulesByUserAgent( $rules ) {
		$groups = [];
		foreach ( $rules as $rule ) {
			$r = is_string( $rule ) ? json_decode( $rule, true ) : $rule;
			if ( empty( $r['userAgent'] ) || empty( $r['fieldValue'] ) ) {
				continue;
			}

			if ( empty( $groups[ $r['userAgent'] ] ) ) {
				$groups[ $r['userAgent'] ] = [];
			}

			$groups[ $r['userAgent'] ][] = "{$r['directive']}: {$r['fieldValue']}";
		}

		return $groups;
	}

	/**
	 * Extract rules from a string.
	 *
	 * @since   4.0.0
	 * @version 4.4.2
	 *
	 * @param  string $lines The lines to extract from.
	 * @return array         An array of extracted rules.
	 */
	public function extractRules( $lines ) {
		$lines              = array_filter( array_map( 'trim', explode( "\n", (string) $lines ) ) );
		$rules              = [];
		$userAgent          = null;
		$prevDirective      = null;
		$prevValue          = null;
		$siblingsUserAgents = [];
		foreach ( $lines as $line ) {
			list( $directive, $value ) = $this->parseRule( $line );
			if ( empty( $directive ) || empty( $value ) ) {
				continue;
			}

			$directive = strtolower( $directive );
			if ( ! in_array( $directive, $this->allowedDirectives, true ) ) {
				continue;
			}

			$value = $this->sanitizeDirectiveValue( $directive, $value );
			if ( ! $value ) {
				continue;
			}

			if ( 'user-agent' === $directive ) {
				if (
					! empty( $prevDirective ) &&
					! empty( $prevValue ) &&
					'user-agent' === $prevDirective
				) {
					$siblingsUserAgents[] = $prevValue;
				}

				$userAgent           = $value;
				$rules[ $userAgent ] = ! empty( $rules[ $userAgent ] ) ? $rules[ $userAgent ] : [];
			} else {
				$rules[ $userAgent ][] = "$directive: $value";
				if ( $siblingsUserAgents ) {
					foreach ( $siblingsUserAgents as $siblingUserAgent ) {
						$rules[ $siblingUserAgent ] = $rules[ $userAgent ];
					}

					$siblingsUserAgents = [];
				}
			}

			$prevDirective = $directive;
			$prevValue     = $value;
		}

		return $rules;
	}

	/**
	 * Extract sitemap URLs from a string.
	 *
	 * @since 4.0.10
	 *
	 * @param  string $lines The lines to extract from.
	 * @return array         An array of sitemap URLs.
	 */
	public function extractSitemapUrls( $lines ) {
		$lines       = array_filter( array_map( 'trim', explode( "\n", (string) $lines ) ) );
		$sitemapUrls = [];
		foreach ( $lines as $line ) {
			$array = array_map( 'trim', explode( 'sitemap:', strtolower( $line ) ) );
			if ( ! empty( $array[1] ) ) {
				$sitemapUrls[] = trim( $line );
			}
		}

		return $sitemapUrls;
	}

	/**
	 * Sanitize the robots.txt rule directive value.
	 *
	 * @since   4.0.0
	 * @version 4.4.2
	 *
	 * @param  string $directive The directive.
	 * @param  string $value     The value.
	 * @return string            The directive value.
	 */
	private function sanitizeDirectiveValue( $directive, $value ) {
		// Percent-encoded characters are stripped from our option values, so we decode.
		$value = rawurldecode( trim( $value ) );
		if ( ! $value ) {
			return $value;
		}

		$value = preg_replace( '/[><]/', '', (string) $value );

		if ( 'user-agent' === $directive ) {
			$value = preg_replace( '/[^a-z0-9\-_*,.\s]/i', '', (string) $value );
		}

		if ( 'allow' === $directive || 'disallow' === $directive ) {
			$value = preg_replace( '/^\/+/', '/', (string) $value );
		}

		return $value;
	}

	/**
	 * Check if a physical robots.txt file exists, and if it does add a notice.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function checkForPhysicalFiles() {
		if ( ! $this->hasPhysicalRobotsTxt() ) {
			return;
		}

		$notification = Models\Notification::getNotificationByName( 'robots-physical-file' );
		if ( $notification->exists() ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'robots-physical-file',
			'title'             => __( 'Physical Robots.txt File Detected', 'all-in-one-seo-pack' ),
			'content'           => sprintf(
				// Translators: 1 - The plugin short name ("AIOSEO"), 2 - The plugin short name ("AIOSEO").
				__( '%1$s has detected a physical robots.txt file in the root folder of your WordPress installation. We recommend removing this file as it could cause conflicts with WordPress\' dynamically generated one. %2$s can import this file and delete it, or you can simply delete it.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				AIOSEO_PLUGIN_SHORT_NAME,
				AIOSEO_PLUGIN_SHORT_NAME
			),
			'type'              => 'error',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Import and Delete', 'all-in-one-seo-pack' ),
			'button1_action'    => 'http://action#tools/import-robots-txt?redirect=aioseo-tools:robots-editor',
			'button2_label'     => __( 'Delete', 'all-in-one-seo-pack' ),
			'button2_action'    => 'http://action#tools/delete-robots-txt?redirect=aioseo-tools:robots-editor',
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Import physical robots.txt file.
	 *
	 * @since   4.0.0
	 * @version 4.4.2
	 *
	 * @param  int|string $blogId The blog ID or 'network'.
	 * @throws \Exception         If request fails or file is not readable.
	 * @return boolean            Whether the file imported correctly.
	 */
	public function importPhysicalRobotsTxt( $blogId ) {
		try {
			$fs = aioseo()->core->fs;
			if ( ! $fs->isWpfsValid() ) {
				$invalid = true;
			}

			$file = trailingslashit( $fs->fs->abspath() ) . 'robots.txt';
			if (
				isset( $invalid ) ||
				! $fs->isReadable( $file )
			) {
				throw new \Exception( esc_html__( 'There was an error importing the static robots.txt file.', 'all-in-one-seo-pack' ) );
			}

			$lines = trim( (string) $fs->getContents( $file ) );
			if ( $lines ) {
				$this->importRobotsTxtFromText( $lines, $blogId );
			}

			return true;
		} catch ( \Exception $e ) {
			throw new \Exception( esc_html( $e->getMessage() ) );
		}
	}

	/**
	 * Import robots.txt from a URL.
	 *
	 * @since 4.4.2
	 *
	 * @param  string     $text   The text to import from.
	 * @param  int|string $blogId The blog ID or 'network'.
	 * @throws \Exception         If no User-agent is found.
	 * @return boolean            Whether the file imported correctly or not.
	 */
	public function importRobotsTxtFromText( $text, $blogId ) {
		$newRules = $this->extractRules( $text );
		if ( ! key( $newRules ) ) {
			throw new \Exception( esc_html__( 'No User-agent found in the content beginning.', 'all-in-one-seo-pack' ) );
		}

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

		$options->tools->robots->rules = array_unique( array_merge( $options->tools->robots->rules, $this->prepareRobotsTxt( $newRules ) ) );

		return true;
	}

	/**
	 * Import robots.txt from a URL.
	 *
	 * @since 4.4.2
	 *
	 * @param  string     $url    The URL to import from.
	 * @param  int|string $blogId The blog ID or 'network'.
	 * @throws \Exception         If request fails.
	 * @return bool               Whether the import was successful or not.
	 */
	public function importRobotsTxtFromUrl( $url, $blogId ) {
		$request          = wp_remote_get( $url, [
			'timeout'   => 10,
			'sslverify' => false
		] );
		$robotsTxtContent = wp_remote_retrieve_body( $request );
		if ( ! $robotsTxtContent ) {
			throw new \Exception( esc_html__( 'There was an error importing the robots.txt content from the URL.', 'all-in-one-seo-pack' ) );
		}

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

		$newRules = $this->extractRules( $robotsTxtContent );

		$options->tools->robots->rules = array_unique( array_merge( $options->tools->robots->rules, $this->prepareRobotsTxt( $newRules ) ) );

		return true;
	}

	/**
	 * Deletes the physical robots.txt file.
	 *
	 * @since 4.4.5
	 *
	 * @throws \Exception If the file is not readable, or it can't be deleted.
	 * @return true       True if the file was successfully deleted.
	 */
	public function deletePhysicalRobotsTxt() {
		try {
			$fs = aioseo()->core->fs;
			if (
				! $fs->isWpfsValid() ||
				! $fs->fs->delete( trailingslashit( $fs->fs->abspath() ) . 'robots.txt' )
			) {
				throw new \Exception( __( 'There was an error deleting the physical robots.txt file.', 'all-in-one-seo-pack' ) );
			}

			Models\Notification::deleteNotificationByName( 'robots-physical-file' );

			return true;
		} catch ( \Exception $e ) {
			throw new \Exception( esc_html( $e->getMessage() ) );
		}
	}

	/**
	 * Prepare robots.txt rules to save.
	 *
	 * @since 4.1.4
	 *
	 * @param  array $allRules Array with the rules.
	 * @return array           The prepared rules array.
	 */
	public function prepareRobotsTxt( $allRules = [] ) {
		$robots = [];
		foreach ( $allRules as $userAgent => $rules ) {
			if ( empty( $userAgent ) ) {
				continue;
			}

			foreach ( $rules as $rule ) {
				list( $directive, $value ) = $this->parseRule( $rule );
				if ( empty( $directive ) || empty( $value ) ) {
					continue;
				}

				if (
					'*' === $userAgent &&
					(
						'allow' === $directive && '/wp-admin/admin-ajax.php' === $value ||
						'disallow' === $directive && '/wp-admin/' === $value
					)
				) {
					continue;
				}

				$robots[] = wp_json_encode( [
					'userAgent'  => $userAgent,
					'directive'  => $directive,
					'fieldValue' => $value
				] );
			}
		}

		return $robots;
	}

	/**
	 * Checks if a physical robots.txt file exists.
	 *
	 * @since 4.0.0
	 *
	 * @return boolean True if it does, false if not.
	 */
	public function hasPhysicalRobotsTxt() {
		$fs = aioseo()->core->fs;
		if ( ! $fs->isWpfsValid() ) {
			return false;
		}

		$accessType = get_filesystem_method();
		if ( 'direct' === $accessType ) {
			$file = trailingslashit( $fs->fs->abspath() ) . 'robots.txt';

			return $fs->exists( $file );
		}

		return false;
	}

	/**
	 * Get the default Robots.txt lines (excluding our own).
	 *
	 * @since   4.1.7
	 * @version 4.4.2
	 *
	 * @return string The robots.txt content rules (excluding our own).
	 */
	public function getDefaultRobotsTxtContent() {
		// First, we need to remove our filter, so that it doesn't run unintentionally.
		remove_filter( 'robots_txt', [ $this, 'buildRules' ], 10000 );

		ob_start();
		do_robots();
		if ( is_admin() ) {
			header( 'Content-Type: text/html; charset=utf-8' );
		}
		$rules = strval( ob_get_clean() );

		// Add the filter back.
		add_filter( 'robots_txt', [ $this, 'buildRules' ], 10000 );

		return $rules;
	}

	/**
	 * A check to see if the rewrite rules are set.
	 * This isn't perfect, but it will help us know in most cases.
	 *
	 * @since 4.0.0
	 *
	 * @return boolean Whether the rewrite rules are set or not.
	 */
	public function rewriteRulesExist() {
		// If we have a physical file, it's almost impossible to tell if the rewrite rules are set.
		// The only scenario is if we still get a 404.
		$response = wp_remote_get( aioseo()->helpers->getSiteUrl() . '/robots.txt' );
		if ( 299 < wp_remote_retrieve_response_code( $response ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Reset the Search Appearance related rules.
	 *
	 * @since 4.8.1
	 *
	 * @return void
	 */
	public function resetSearchAppearanceRules() {
		$currentRules = aioseo()->options->tools->robots->rules;
		$newRules     = [];
		foreach ( ( $currentRules ?? [] ) as $rule ) {
			$parseRule = json_decode( $rule, true );
			if ( empty( $parseRule['bot'] ) && empty( $parseRule['preventCrawling'] ) ) {
				$newRules[] = $rule;
			}
		}

		aioseo()->options->tools->robots->rules = $newRules;
	}
}Common/Tools/SystemStatus.php000064400000032103151536241200012273 0ustar00<?php
namespace AIOSEO\Plugin\Common\Tools;

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

class SystemStatus {
	/**
	 * Get an aggregated list of all system info.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of system information.
	 */
	public static function getSystemStatusInfo() {
		return [
			'wordPress'       => self::getWordPressInfo(),
			'constants'       => self::getConstants(),
			'serverInfo'      => self::getServerInfo(),
			'muPlugins'       => self::mustUsePlugins(),
			'activeTheme'     => self::activeTheme(),
			'activePlugins'   => self::activePlugins(),
			'inactivePlugins' => self::inactivePlugins(),
			'database'        => self::getDatabaseInfo()
		];
	}

	/**
	 * Get an array of system info from WordPress.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of system info.
	 */
	public static function getWordPressInfo() {
		$uploadsDir    = wp_upload_dir();
		$version       = get_bloginfo( 'version' );
		$updates       = get_site_transient( 'update_core' );
		$updateVersion = ! empty( $updates->updates[0]->version ) ? $updates->updates[0]->version : '';
		if ( version_compare( $version, $updateVersion, '<' ) ) {
			$version .= ' (' . __( 'Latest version:', 'all-in-one-seo-pack' ) . ' ' . $updateVersion . ')';
		}

		return [
			'label'   => 'WordPress',
			'results' => [
				[
					'header' => __( 'Version', 'all-in-one-seo-pack' ),
					'value'  => $version
				],
				[
					'header' => __( 'Site Title', 'all-in-one-seo-pack' ),
					'value'  => get_bloginfo( 'name' )
				],
				[
					'header' => __( 'Site Language', 'all-in-one-seo-pack' ),
					'value'  => get_locale() ?: 'en_US'
				],
				[
					'header' => __( 'User Language', 'all-in-one-seo-pack' ),
					'value'  => get_user_locale( get_current_user_id() )
				],
				[
					'header' => __( 'Timezone', 'all-in-one-seo-pack' ),
					'value'  => wp_timezone_string()
				],
				[
					'header' => __( 'Home URL', 'all-in-one-seo-pack' ),
					'value'  => home_url()
				],
				[
					'header' => __( 'Site URL', 'all-in-one-seo-pack' ),
					'value'  => site_url()
				],
				[
					'header' => __( 'Permalink Structure', 'all-in-one-seo-pack' ),
					'value'  => get_option( 'permalink_structure' ) ? get_option( 'permalink_structure' ) : __( 'Default', 'all-in-one-seo-pack' )
				],
				[
					'header' => __( 'Multisite', 'all-in-one-seo-pack' ),
					'value'  => is_multisite() ? __( 'Yes', 'all-in-one-seo-pack' ) : __( 'No', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'HTTPS',
					'value'  => is_ssl() ? __( 'Yes', 'all-in-one-seo-pack' ) : __( 'No', 'all-in-one-seo-pack' )
				],
				[
					'header' => __( 'User Count', 'all-in-one-seo-pack' ),
					'value'  => count_users()['total_users']
				],
				[
					'header' => __( 'Front Page Info', 'all-in-one-seo-pack' ),
					'value'  => 'page' === get_option( 'show_on_front' ) ? get_option( 'show_on_front' ) . ' [ID: ' . get_option( 'page_on_front' ) . ']' : get_option( 'show_on_front' )
				],
				[
					'header' => __( 'Search Engine Visibility', 'all-in-one-seo-pack' ),
					'value'  => get_option( 'blog_public' ) ? __( 'Visible', 'all-in-one-seo-pack' ) : __( 'Hidden', 'all-in-one-seo-pack' )
				],
				[
					'header' => __( 'Upload Directory Info', 'all-in-one-seo-pack' ),
					'value'  =>
						__( 'Path:', 'all-in-one-seo-pack' ) . ' ' . $uploadsDir['path'] . ', ' .
						__( 'Url:', 'all-in-one-seo-pack' ) . ' ' . $uploadsDir['url'] . ', ' .
						__( 'Base Directory:', 'all-in-one-seo-pack' ) . ' ' . $uploadsDir['basedir'] . ', ' .
						__( 'Base URL:', 'all-in-one-seo-pack' ) . ' ' . $uploadsDir['baseurl']
				]
			]
		];
	}

	/**
	 * Get an array of database info from WordPress.
	 *
	 * @since 4.4.5
	 *
	 * @return array An array of database info.
	 */
	public static function getDatabaseInfo() {
		$dbInfo = aioseo()->core->db->getDatabaseInfo();
		if ( empty( $dbInfo['tables'] ) ) {
			return [];
		}

		if ( ! aioseo()->helpers->isDev() ) {
			return [];
		}

		$results = [];
		$tables  = array_merge( $dbInfo['tables']['aioseo'], $dbInfo['tables']['other'] );
		foreach ( $tables as $tableName => $tableData ) {
			$results[] = [
				'header' => $tableName,
				'value'  => sprintf(
					// Translators: %1$s is the data size, %2$s is the index size, %3$s is the engine type.
					__( 'Data: %1$.2f MB / Index: %2$.2f MB / Engine: %3$s / Collation: %4$s', 'all-in-one-seo-pack' ),
					$tableData['data'],
					$tableData['index'],
					$tableData['engine'],
					$tableData['collation']
				)
			];
		}

		return [
			'label'   => __( 'Database', 'all-in-one-seo-pack' ),
			'results' => array_merge( [
				[
					'header' => __( 'Database Size', 'all-in-one-seo-pack' ),
					'value'  => sprintf( '%.2f MB', $dbInfo['size']['data'] + $dbInfo['size']['index'] )
				]
			], $results )
		];
	}

	/**
	 * Get an array of system info from WordPress constants.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of system info.
	 */
	public static function getConstants() {
		return [
			'label'   => __( 'Constants', 'all-in-one-seo-pack' ),
			'results' => [
				[
					'header' => 'ABSPATH',
					'value'  => ABSPATH
				],
				[
					'header' => 'WP_CONTENT_DIR',
					'value'  => defined( 'WP_CONTENT_DIR' ) ? ( WP_CONTENT_DIR ? WP_CONTENT_DIR : __( 'Disabled', 'all-in-one-seo-pack' ) ) : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'WP_CONTENT_URL',
					'value'  => defined( 'WP_CONTENT_URL' ) ? ( WP_CONTENT_URL ? WP_CONTENT_URL : __( 'Disabled', 'all-in-one-seo-pack' ) ) : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'UPLOADS',
					'value'  => defined( 'UPLOADS' ) ? ( UPLOADS ? UPLOADS : __( 'Disabled', 'all-in-one-seo-pack' ) ) : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'WP_DEBUG',
					'value'  => defined( 'WP_DEBUG' ) ? ( WP_DEBUG ? WP_DEBUG : __( 'Disabled', 'all-in-one-seo-pack' ) ) : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'WP_DEBUG_LOG',
					'value'  => defined( 'WP_DEBUG_LOG' ) ? ( WP_DEBUG_LOG ? WP_DEBUG_LOG : __( 'Disabled', 'all-in-one-seo-pack' ) ) : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'WP_DEBUG_DISPLAY',
					'value'  => defined( 'WP_DEBUG_DISPLAY' ) ? ( WP_DEBUG_DISPLAY ? WP_DEBUG_DISPLAY : __( 'Disabled', 'all-in-one-seo-pack' ) ) : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'WP_POST_REVISIONS',
					'value'  => defined( 'WP_POST_REVISIONS' ) ? WP_POST_REVISIONS : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'DISABLE_WP_CRON',
					'value'  => defined( 'DISABLE_WP_CRON' ) ? DISABLE_WP_CRON : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'EMPTY_TRASH_DAYS',
					'value'  => defined( 'EMPTY_TRASH_DAYS' ) ? EMPTY_TRASH_DAYS : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'AUTOSAVE_INTERVAL',
					'value'  => defined( 'AUTOSAVE_INTERVAL' ) ? AUTOSAVE_INTERVAL : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'SCRIPT_DEBUG',
					'value'  => defined( 'SCRIPT_DEBUG' ) ? SCRIPT_DEBUG : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'DB_CHARSET',
					'value'  => defined( 'DB_CHARSET' ) ? ( DB_CHARSET ? DB_CHARSET : __( 'Disabled', 'all-in-one-seo-pack' ) ) : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'DB_COLLATE',
					'value'  => defined( 'DB_COLLATE' ) ? ( DB_COLLATE ? DB_COLLATE : __( 'Disabled', 'all-in-one-seo-pack' ) ) : __( 'Not set', 'all-in-one-seo-pack' )
				]
			]
		];
	}

	/**
	 * Get an array of system info from the server.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of system info.
	 */
	public static function getServerInfo() {
		$sqlMode   = null;
		$mysqlInfo = aioseo()->core->db->db->get_results( "SHOW VARIABLES LIKE 'sql_mode'" );
		if ( ! empty( $mysqlInfo ) && is_array( $mysqlInfo ) ) {
			$sqlMode = $mysqlInfo[0]->Value;
		}

		$dbServerInfo = method_exists( aioseo()->core->db->db, 'db_server_info' )
			? aioseo()->core->db->db->db_server_info()
			: ( function_exists( 'mysqli_get_server_info' )
				? mysqli_get_server_info( aioseo()->core->db->db->dbh ) // phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysqli_get_server_info
				: ''
		);

		return [
			'label'   => __( 'Server Info', 'all-in-one-seo-pack' ),
			'results' => [
				[
					'header' => __( 'Operating System', 'all-in-one-seo-pack' ),
					'value'  => PHP_OS
				],
				[
					'header' => __( 'Web Server', 'all-in-one-seo-pack' ),
					'value'  => ! empty( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : __( 'unknown', 'all-in-one-seo-pack' )
				],
				[
					'header' => __( 'Memory Usage', 'all-in-one-seo-pack' ),
					'value'  => function_exists( 'memory_get_usage' ) ? round( memory_get_usage() / 1024 / 1024, 2 ) . 'M' : __( 'N/A', 'all-in-one-seo-pack' )
				],
				[
					'header' => __( 'Database Powered By', 'all-in-one-seo-pack' ),
					'value'  => stripos( $dbServerInfo, 'mariadb' ) !== false ? 'MariaDB' : 'MySQL'
				],
				[
					'header' => __( 'Database Version', 'all-in-one-seo-pack' ),
					'value'  => aioseo()->core->db->db->db_version()
				],
				[
					'header' => __( 'SQL Mode', 'all-in-one-seo-pack' ),
					'value'  => $sqlMode ?? __( 'Not Set', 'all-in-one-seo-pack' ),
				],
				[
					'header' => __( 'PHP Version', 'all-in-one-seo-pack' ),
					'value'  => PHP_VERSION
				],
				[
					'header' => __( 'PHP Memory Limit', 'all-in-one-seo-pack' ),
					'value'  => ini_get( 'memory_limit' )
				],
				[
					'header' => __( 'PHP Max Upload Size', 'all-in-one-seo-pack' ),
					'value'  => ini_get( 'upload_max_filesize' )
				],
				[
					'header' => __( 'PHP Max Post Size', 'all-in-one-seo-pack' ),
					'value'  => ini_get( 'post_max_size' )
				],
				[
					'header' => __( 'PHP Max Script Execution Time', 'all-in-one-seo-pack' ),
					'value'  => ini_get( 'max_execution_time' )
				],
				[
					'header' => __( 'PHP Exif Support', 'all-in-one-seo-pack' ),
					'value'  => is_callable( 'exif_read_data' ) ? __( 'Yes', 'all-in-one-seo-pack' ) : __( 'No', 'all-in-one-seo-pack' )
				],
				[
					'header' => __( 'PHP IPTC Support', 'all-in-one-seo-pack' ),
					'value'  => is_callable( 'iptcparse' ) ? __( 'Yes', 'all-in-one-seo-pack' ) : __( 'No', 'all-in-one-seo-pack' )
				],
				[
					'header' => __( 'PHP XML Support', 'all-in-one-seo-pack' ),
					'value'  => is_callable( 'xml_parser_create' ) ? __( 'Yes', 'all-in-one-seo-pack' ) : __( 'No', 'all-in-one-seo-pack' )
				]
			]
		];
	}

	/**
	 * Get an array of system info from the active theme.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of system info.
	 */
	public static function activeTheme() {
		$themeData = wp_get_theme();

		return [
			'label'   => __( 'Active Theme', 'all-in-one-seo-pack' ),
			'results' => [
				[
					'header' => $themeData->name,
					'value'  => $themeData->version
				]
			]
		];
	}

	/**
	 * Get an array of system info from must-use plugins.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of system info.
	 */
	public static function mustUsePlugins() {
		$plugins   = [];
		$muPlugins = get_mu_plugins();
		if ( ! empty( $muPlugins ) ) {
			foreach ( $muPlugins as $pluginData ) {
				$plugins[] = [
					'header' => $pluginData['Name'],
					'value'  => $pluginData['Version']
				];
			}
		}

		return [
			'label'   => __( 'Must-Use Plugins', 'all-in-one-seo-pack' ),
			'results' => $plugins
		];
	}

	/**
	 * Get an array of system info from active plugins.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of system info.
	 */
	public static function activePlugins() {
		$plugins       = [];
		$allPlugins    = get_plugins();
		$activePlugins = get_option( 'active_plugins', [] );
		$updates       = get_plugin_updates();
		if ( ! empty( $allPlugins ) ) {
			foreach ( $allPlugins as $pluginPath => $pluginData ) {
				if ( ! in_array( $pluginPath, $activePlugins, true ) ) {
					continue;
				}

				$update    = ( array_key_exists( $pluginPath, $updates ) ) ? ' (' . __( 'needs update', 'all-in-one-seo-pack' ) . ' - ' . $updates[ $pluginPath ]->update->new_version . ')' : '';
				$plugins[] = [
					'header' => $pluginData['Name'],
					'value'  => $pluginData['Version'] . $update
				];
			}
		}

		return [
			'label'   => __( 'Active Plugins', 'all-in-one-seo-pack' ),
			'results' => $plugins
		];
	}

	/**
	 * Get an array of system info from inactive plugins.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of system info.
	 */
	public static function inactivePlugins() {
		$plugins       = [];
		$allPlugins    = get_plugins();
		$activePlugins = get_option( 'active_plugins', [] );
		$updates       = get_plugin_updates();
		if ( ! empty( $allPlugins ) ) {
			foreach ( $allPlugins as $pluginPath => $pluginData ) {
				if ( in_array( $pluginPath, $activePlugins, true ) ) {
					continue;
				}

				$update    = ( array_key_exists( $pluginPath, $updates ) ) ? ' (' . __( 'needs update', 'all-in-one-seo-pack' ) . ' - ' . $updates[ $pluginPath ]->update->new_version . ')' : '';
				$plugins[] = [
					'header' => $pluginData['Name'],
					'value'  => $pluginData['Version'] . $update
				];
			}
		}

		return [
			'label'   => __( 'Inactive Plugins', 'all-in-one-seo-pack' ),
			'results' => $plugins
		];
	}
}Common/Traits/Assets.php000064400000035647151536241200011233 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits;

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

/**
 * Assets trait.
 *
 * @since 4.1.9
 */
trait Assets {
	/**
	 * Whether we should load dev scripts.
	 *
	 * @since 4.1.9
	 *
	 * @var boolean|null
	 */
	private $shouldLoadDevScripts = null;

	/**
	 * Holds the location of the manifest file.
	 *
	 * @since 4.1.9
	 *
	 * @var string
	 */
	private $manifestFile;

	/**
	 * True if we are in a dev environment. This mirrors the global isDev.
	 *
	 * @since 4.1.9
	 *
	 * @var bool
	 */
	private $isDev = false;

	/**
	 * Asset handles that should load as regular JS and not as modern JS module.
	 *
	 * @since 4.1.9
	 *
	 * @var array An array of handles.
	 */
	private $noModuleTag = [];

	/**
	 * Core class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Core\Core
	 */
	protected $core = null;

	/**
	 * The LocalBusiness addon version.
	 *
	 * @since 4.2.7
	 *
	 * @var string
	 */
	protected $version = '';

	/**
	 * The development site domain.
	 *
	 * @since 4.2.7
	 *
	 * @var string
	 */
	protected $domain = '';

	/**
	 * The development server port.
	 *
	 * @since 4.2.7
	 *
	 * @var int
	 */
	protected $port = 0;

	/**
	 * The asset to load.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset        The asset to load.
	 * @param  array  $dependencies An array of dependencies.
	 * @param  mixed  $data         Any data to be localized.
	 * @param  string $objectName   The object name to use when localizing.
	 * @return void
	 */
	public function load( $asset, $dependencies = [], $data = null, $objectName = 'aioseo' ) {
		$this->jsPreloadImports( $asset );
		$this->loadCss( $asset );
		$this->enqueueJs( $asset, $dependencies, $data, $objectName );
	}

	/**
	 * Filter the script loader tag if this is our script.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $tag    The tag that is going to be output.
	 * @param  string $handle The handle for the script.
	 * @return string         The modified tag.
	 */
	public function scriptLoaderTag( $tag, $handle = '', $src = '' ) {
		if ( $this->skipModuleTag( $handle ) ) {
			return $tag;
		}

		$tag = str_replace( $src, $this->normalizeAssetsHost( $src ), $tag );

		// Remove the type and re-add it as module.
		$tag = preg_replace( '/type=[\'"].*?[\'"]/', '', (string) $tag );
		$tag = preg_replace( '/<script/', '<script type="module"', (string) $tag );

		return $tag;
	}

	/**
	 * Preload JS imports.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset The asset to load imports for.
	 * @return void
	 */
	private function jsPreloadImports( $asset ) {
		static $urls = []; // Prevent script from being loaded multiple times.

		$res = '';
		foreach ( $this->importsUrls( $asset ) as $url ) {
			if ( isset( $urls[ $url ] ) ) {
				continue;
			}

			$urls[ $url ] = true;

			$res .= '<link rel="modulepreload" href="' . esc_attr( $url ) . "\">\n";
		}

		$allowedHtml = [
			'link' => [
				'rel'  => [],
				'href' => []
			]
		];

		if ( ! empty( $res ) ) {
			if ( ! function_exists( 'wp_enqueue_script_module' ) ) {
				add_action( 'admin_head', function () use ( &$res, $allowedHtml ) {
					echo wp_kses( $res, $allowedHtml );
				} );
				add_action( 'wp_head', function () use ( &$res, $allowedHtml ) {
					echo wp_kses( $res, $allowedHtml );
				} );
			} else {
				add_action( 'admin_print_footer_scripts', function () use ( &$res, $allowedHtml ) {
					echo wp_kses( $res, $allowedHtml );
				}, 1000 );
			}
		}
	}

	/**
	 * Loads CSS for an asset from the manifest file.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset The script to load CSS for.
	 * @return void
	 */
	public function loadCss( $asset ) {
		if ( $this->shouldLoadDev() ) {
			return;
		}

		foreach ( $this->getCssUrls( $asset ) as $file => $url ) {
			wp_enqueue_style( $this->cssHandle( $file ), $url, [], $this->version );
		}
	}

	/**
	 * Register a CSS asset.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset        The script to load CSS for.
	 * @param  array  $dependencies An array of dependencies.
	 * @return void
	 */
	public function registerCss( $asset, $dependencies = [] ) {
		$handle = $this->cssHandle( $asset );
		if ( wp_style_is( $handle, 'registered' ) ) {
			return;
		}

		$url = $this->shouldLoadDev()
			? $this->getDevUrl() . ltrim( $asset, '/' )
			: $this->assetUrl( $asset );

		if ( ! $url ) {
			return;
		}

		wp_register_style( $handle, $url, $dependencies, $this->version );
	}

	/**
	 * Enqueue css.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset        The css to load.
	 * @param  array  $dependencies An array of dependencies.
	 * @return void
	 */
	public function enqueueCss( $asset, $dependencies = [] ) {
		$this->registerCss( $asset, $dependencies );

		$handle = $this->cssHandle( $asset );
		if ( wp_style_is( $handle, 'enqueued' ) ) {
			return;
		}

		wp_enqueue_style( $handle );
	}

	/**
	 * Register the JS to enqueue.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset        The script to load.
	 * @param  array  $dependencies An array of dependencies.
	 * @param  mixed  $data         Any data to be localized.
	 * @param  string $objectName   The object name to use when localizing.
	 * @return void
	 */
	public function registerJs( $asset, $dependencies = [], $data = null, $objectName = 'aioseo' ) {
		$handle = $this->jsHandle( $asset );
		if ( wp_script_is( $handle, 'registered' ) ) {
			// If it's already registered let's add the data.
			if ( ! empty( $data ) ) {
				wp_localize_script(
					$handle,
					$objectName,
					$data
				);
			}

			return;
		}

		$url = $this->shouldLoadDev()
			? $this->getDevUrl() . ltrim( $asset, '/' )
			: $this->jsUrl( $asset );

		if ( ! $url ) {
			return;
		}

		wp_register_script( $handle, $url, $dependencies, $this->version, true );

		if ( empty( $data ) ) {
			return;
		}

		wp_localize_script(
			$handle,
			$objectName,
			$data
		);
	}

	/**
	 * Register the JS to enqueue.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset        The script to load.
	 * @param  array  $dependencies An array of dependencies.
	 * @param  mixed  $data         Any data to be localized.
	 * @param  string $objectName   The object name to use when localizing.
	 * @return void
	 */
	public function enqueueJs( $asset, $dependencies = [], $data = null, $objectName = 'aioseo' ) {
		$this->registerJs( $asset, $dependencies, $data, $objectName );

		$handle = $this->jsHandle( $asset );
		if ( wp_script_is( $handle, 'enqueued' ) ) {
			return;
		}

		wp_enqueue_script( $handle );
	}

	/**
	 * Return the dev URL.
	 *
	 * @since 4.1.9
	 *
	 * @return string The dev URL.
	 */
	private function getDevUrl() {
		$protocol = is_ssl() ? 'https://' : 'http://';

		return $protocol . $this->domain . ':' . $this->port . '/';
	}

	/**
	 * Get the asset URL.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset The asset to find the URL for.
	 * @return string        The URL for the asset.
	 */
	private function assetUrl( $asset ) {
		$assetManifest = $this->getAssetManifestItem( $asset );

		return ! empty( $assetManifest['file'] )
			? $this->basePath() . $assetManifest['file']
			: $this->basePath() . ltrim( $asset, '/' );
	}

	/**
	 * Get the JS URL.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset The asset to find the URL for.
	 * @return string        The URL for the asset.
	 */
	public function jsUrl( $asset ) {
		$manifestAsset = $this->getManifestItem( $asset );

		return ! empty( $manifestAsset['file'] )
			? $this->basePath() . $manifestAsset['file']
			: $this->basePath() . ltrim( $asset, '/' );
	}

	/**
	 * Get an item from the manifest.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset The asset to find.
	 * @return string        Manifest object.
	 */
	private function getManifestItem( $asset ) {
		$manifest = $this->getManifest();

		$asset = ltrim( $asset, '/' );

		return isset( $manifest[ $asset ] ) ? $manifest[ $asset ] : null;
	}

	/**
	 * Get the CSS asset handle.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset The asset to find the handle for.
	 * @return string        The asset handle.
	 */
	public function cssHandle( $asset ) {
		return "{$this->scriptHandle}/css/$asset";
	}

	/**
	 * Get the JS asset handle.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset The asset to find the handle for.
	 * @return string        The asset handle.
	 */
	public function jsHandle( $asset = '' ) {
		return "{$this->scriptHandle}/js/$asset";
	}

	/**
	 * Get the manifest to load assets from.
	 *
	 * @since 4.1.9
	 *
	 * @return array An array of files.
	 */
	private function getManifest() {
		static $file = null;
		if ( $file ) {
			return $file;
		}

		$manifestJson = ''; // This is set in the view.

		if ( file_exists( $this->manifestFile ) ) {
			require_once $this->manifestFile;
		}

		$file = json_decode( $manifestJson, true );

		return $file;
	}

	/**
	 * Get an item from the asset manifest.
	 *
	 * @since 4.1.9
	 *
	 * @param  string      $item An item to retrieve.
	 * @return string|null       The asset item.
	 */
	private function getAssetManifestItem( $item ) {
		$assetManifest = $this->getManifest();

		return ! empty( $assetManifest[ $item ] ) ? $assetManifest[ $item ] : null;
	}

	/**
	 * Get an asset's array of URLs to import.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset The asset to find imports for.
	 * @return array         An array of imports.
	 */
	private function importsUrls( $asset ) {
		$urls          = [];
		$manifestAsset = $this->getManifestItem( $asset );
		if ( ! empty( $manifestAsset['imports'] ) ) {
			foreach ( $manifestAsset['imports'] as $import ) {
				$importAsset = $this->getManifestItem( $import );
				if ( ! empty( $importAsset['file'] ) ) {
					$urls[] = $this->getPublicUrlBase() . $importAsset['file'];

					// Load the import's CSS if any.
					$this->loadCss( $import );
				}
			}
		}

		return $urls;
	}

	/**
	 * Returns an asset's CSS urls.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset The asset to find CSS URLs for.
	 * @return array         An array of CSS URLs to load.
	 */
	private function getCssUrls( $asset ) {
		$urls          = [];
		$manifestAsset = $this->getManifestItem( $asset );

		if ( ! empty( $manifestAsset['css'] ) ) {
			foreach ( $manifestAsset['css'] as $file ) {
				$urls[ $file ] = $this->getPublicUrlBase() . $file;
			}
		}

		return $urls;
	}

	/**
	 * Check if we should load the dev watcher scripts.
	 *
	 * @since 4.1.9
	 *
	 * @return boolean True if we should load the dev watcher scripts.
	 */
	private function shouldLoadDev() {
		if ( null !== $this->shouldLoadDevScripts ) {
			return $this->shouldLoadDevScripts;
		}

		if (
			! $this->isDev ||
			(
				defined( 'AIOSEO_LOAD_DEV_SCRIPTS' ) &&
				false === AIOSEO_LOAD_DEV_SCRIPTS
			)
		) {
			$this->shouldLoadDevScripts = false;

			return $this->shouldLoadDevScripts;
		}

		if ( ! $this->domain && ! $this->port ) {
			$this->shouldLoadDevScripts = false;

			return $this->shouldLoadDevScripts;
		}

		set_error_handler( function() {} ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
		$connection = fsockopen( $this->domain, $this->port ); // phpcs:ignore WordPress.WP.AlternativeFunctions
		restore_error_handler();

		if ( ! $connection ) {
			$this->shouldLoadDevScripts = false;

			return $this->shouldLoadDevScripts;
		}

		$this->shouldLoadDevScripts = true;

		return $this->shouldLoadDevScripts;
	}

	/**
	 * Get the path for the assets.
	 *
	 * @since 4.1.9
	 *
	 * @param  bool   $maybeDev Whether to try and load dev scripts.
	 * @return string           The path for the assets.
	 */
	public function getAssetsPath( $maybeDev = true ) {
		return $maybeDev && $this->shouldLoadDev()
			? $this->getDevUrl()
			: $this->basePath();
	}

	/**
	 * Finds out if a handle should be loaded as regular JS and not as modern JS module.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $handle The script handle.
	 * @return bool           Should the module tag be skipped.
	 */
	public function skipModuleTag( $handle ) {
		if ( ! aioseo()->helpers->stringContains( $handle, $this->jsHandle( '' ) ) ) {
			return true;
		}

		foreach ( $this->noModuleTag as $tag ) {
			if ( aioseo()->helpers->stringContains( $handle, $tag ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Normalize the assets host. Some sites manually set the WP_PLUGINS_URL
	 * and if that domain has www. and the site_url does not, then it will fail to load
	 * our assets. This doesn't fix the issue 100% because it will still fail on
	 * sub-domains that don't have the proper CORS headers. Those sites will need
	 * manual fixes.
	 *
	 * @since 4.1.10
	 *
	 * @param  string $path The path to normalize.
	 * @return string       The normalized path.
	 */
	public function normalizeAssetsHost( $path ) {
		static $paths = [];
		if ( isset( $paths[ $path ] ) ) {
			return apply_filters( 'aioseo_normalize_assets_host', $paths[ $path ] );
		}

		// We need to verify the domain on the $path attribute matches
		// what's in site_url() for our assets or they won't load.
		$siteUrl        = site_url();
		$siteUrlEscaped = aioseo()->helpers->escapeRegex( $siteUrl );
		if ( preg_match( "/^$siteUrlEscaped/i", (string) $path ) ) {
			$paths[ $path ] = $path;

			return apply_filters( 'aioseo_normalize_assets_host', $paths[ $path ] );
		}

		// We now know that the path doesn't contain the site_url().
		$newPath        = $path;
		$siteUrlParsed  = wp_parse_url( $siteUrl );
		$host           = aioseo()->helpers->escapeRegex( str_replace( 'www.', '', $siteUrlParsed['host'] ) );
		$scheme         = aioseo()->helpers->escapeRegex( $siteUrlParsed['scheme'] );

		$siteUrlHasWww = preg_match( "/^{$scheme}:\/\/www\.$host/", (string) $siteUrl );
		$pathHasWww    = preg_match( "/^{$scheme}:\/\/www\.$host/", (string) $path );

		// Check if the path contains www.
		if ( $pathHasWww && ! $siteUrlHasWww ) {
			// If the path contains www., we want to strip it out.
			$newPath = preg_replace( "/^({$scheme}:\/\/)(www\.)($host)/", '$1$3', (string) $path );
		}

		// Check if the site_url contains www.
		if ( $siteUrlHasWww && ! $pathHasWww ) {
			// If the site_url contains www., we want to add it in to the path.
			$newPath = preg_replace( "/^({$scheme}:\/\/)($host)/", '$1www.$2', (string) $path );
		}

		$paths[ $path ] = $newPath;

		return apply_filters( 'aioseo_normalize_assets_host', $paths[ $path ] );
	}

	/**
	 * Get all the CSS files which a JS asset depends on.
	 * This won't work properly unless you've run `npm run build` first.
	 *
	 * @since 4.3.1
	 *
	 * @param  string $asset The asset to find the CSS dependencies for.
	 * @return array         All the asset's CSS dependencies if any.
	 */
	public function getJsAssetCssQueue( $asset ) {
		$queue = [];

		foreach ( $this->getCssUrls( $asset ) as $file => $url ) {
			$queue[] = [
				'handle' => $this->cssHandle( $file ),
				'url'    => $url
			];
		}

		$manifestAsset = $this->getManifestItem( $asset );
		if ( ! empty( $manifestAsset['imports'] ) ) {
			foreach ( $manifestAsset['imports'] as $subAsset ) {
				foreach ( $this->getCssUrls( $subAsset ) as $file => $url ) {
					$queue[] = [
						'handle' => $this->cssHandle( $file ),
						'url'    => $url
					];
				}
			}
		}

		return $queue;
	}
}Common/Traits/Helpers/Api.php000064400000004752151536241200012075 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

/**
 * Contains Action Scheduler specific helper methods.
 *
 * @since 4.2.4
 */
trait Api {
	/**
	 * Request the remote URL via wp_remote_post and return a json decoded response.
	 *
	 * @since 4.2.4
	 *
	 * @param  array       $body    The content to retrieve from the remote URL.
	 * @param  array       $headers The headers to send to the remote URL.
	 * @return object|null          JSON decoded response on success, false on failure.
	 */
	public function sendRequest( $url, $body = [], $headers = [] ) {
		$body = wp_json_encode( $body );

		// Build the headers of the request.
		$headers = wp_parse_args(
			$headers,
			[
				'Content-Type' => 'application/json'
			]
		);

		// Setup variable for wp_remote_post.
		$requestArgs = [
			'headers' => $headers,
			'body'    => $body,
			'timeout' => 20
		];

		// Perform the query and retrieve the response.
		$response     = $this->wpRemotePost( $url, $requestArgs );
		$responseBody = wp_remote_retrieve_body( $response );

		// Bail out early if there are any errors.
		if ( ! $responseBody ) {
			return null;
		}

		// Return the json decoded content.
		return json_decode( $responseBody );
	}

	/**
	 * Default arguments for wp_remote_get and wp_remote_post.
	 *
	 * @since 4.2.4
	 *
	 * @return array An array of default arguments for the request.
	 */
	private function getWpApiRequestDefaults() {
		return [
			'timeout'    => 10,
			'headers'    => aioseo()->helpers->getApiHeaders(),
			'user-agent' => aioseo()->helpers->getApiUserAgent()
		];
	}

	/**
	 * Sends a request using wp_remote_post.
	 *
	 * @since 4.2.4
	 *
	 * @param  string          $url  The URL to send the request to.
	 * @param  array           $args The args to use in the request.
	 * @return array|\WP_Error      The response as an array or WP_Error on failure.
	 */
	public function wpRemotePost( $url, $args = [] ) {
		return wp_remote_post( $url, array_replace_recursive( $this->getWpApiRequestDefaults(), $args ) );
	}

	/**
	 * Sends a request using wp_remote_get.
	 *
	 * @since 4.2.4
	 *
	 * @param  string          $url  The URL to send the request to.
	 * @param  array           $args The args to use in the request.
	 * @return array|\WP_Error      The response as an array or WP_Error on failure.
	 */
	public function wpRemoteGet( $url, $args = [] ) {
		return wp_remote_get( $url, array_replace_recursive( $this->getWpApiRequestDefaults(), $args ) );
	}
}Common/Traits/Helpers/Arrays.php000064400000020052151536241200012614 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

/**
 * Contains array specific helper methods.
 *
 * @since 4.1.4
 */
trait Arrays {
	/**
	 * Unsets a given value in a given array.
	 * This should only be used if the given value only appears once in the array.
	 *
	 * @since 4.0.0
	 *
	 * @param  array  $array The array.
	 * @param  string $value The value that needs to be removed from the array.
	 * @return array  $array The filtered array.
	 */
	public function unsetValue( $array, $value ) {
		if ( in_array( $value, $array, true ) ) {
			unset( $array[ array_search( $value, $array, true ) ] );
		}

		return $array;
	}

	/**
	 * Compares two multidimensional arrays to see if they're different.
	 *
	 * @since 4.0.0
	 *
	 * @param  array   $array1 The first array.
	 * @param  array   $array2 The second array.
	 * @return boolean         Whether the arrays are different.
	 */
	public function arraysDifferent( $array1, $array2 ) {
		foreach ( $array1 as $key => $value ) {
			// Check for non-existing values.
			if ( ! isset( $array2[ $key ] ) ) {
				return true;
			}
			if ( is_array( $value ) ) {
				if ( $this->arraysDifferent( $value, $array2[ $key ] ) ) {
					return true;
				}
			} else {
				if ( $value !== $array2[ $key ] ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Checks whether the given array is associative.
	 * Arrays that only have consecutive, sequential numeric keys are numeric.
	 * Otherwise they are associative.
	 *
	 * @since 4.1.4
	 *
	 * @param  array $array The array.
	 * @return bool         Whether the array is associative.
	 */
	public function isArrayAssociative( $array ) {
		return 0 < count( array_filter( array_keys( $array ), 'is_string' ) );
	}

	/**
	 * Checks whether the given array is numeric.
	 *
	 * @since 4.1.4
	 *
	 * @param  array $array The array.
	 * @return bool         Whether the array is numeric.
	 */
	public function isArrayNumeric( $array ) {
		return ! $this->isArrayAssociative( $array );
	}

	/**
	 * Recursively replaces the values from one array with the ones from another.
	 * This function should act identical to the built-in array_replace_recursive(), with the exception that it also replaces array values with empty arrays.
	 *
	 * @since 4.2.4
	 *
	 * @param  array $targetArray      The target array
	 * @param  array $replacementArray The array with values to replace in the target array.
	 * @return array                   The modified array.
	 */
	public function arrayReplaceRecursive( $targetArray, $replacementArray ) {
		// In some cases the target array isn't an array yet (due to e.g. race conditions in InternalOptions), so in that case we can just return the replacement array.
		if ( ! is_array( $targetArray ) ) {
			return $replacementArray;
		}

		foreach ( $replacementArray as $k => $v ) {
			// If the key does not exist yet on the target array, add it.
			if ( ! isset( $targetArray[ $k ] ) ) {
				$targetArray[ $k ] = $replacementArray[ $k ];
				continue;
			}

			// If the value is an array, only try to recursively replace it if the value isn't empty.
			// Otherwise empty arrays will be ignored and won't override the existing value of the target array.
			if ( is_array( $v ) && ! empty( $v ) ) {
				$targetArray[ $k ] = $this->arrayReplaceRecursive( $targetArray[ $k ], $v );
				continue;
			}

			// Replace with non-array value or empty array.
			$targetArray[ $k ] = $v;
		}

		return $targetArray;
	}

	/**
	 * Recursively intersects the two given arrays.
	 * You can pass in an optional argument (allowedKey) to restrict the intersect to arrays with a specific key.
	 * This is needed when we are e.g. sanitizing array values before setting/saving them to an option.
	 * This helper method was mainly built to support our complex options architecture.
	 *
	 * @since 4.2.5
	 *
	 * @param  array  $array1     The first array.
	 * @param  array  $array2     The second array.
	 * @param  string $allowedKey The only key the method should run for (optional).
	 * @param  string $parentKey  The parent key.
	 * @return array              The intersected array.
	 */
	public function arrayIntersectRecursive( $array1, $array2, $allowedKey = '', $parentKey = '' ) {
		if ( ! $allowedKey || $allowedKey === $parentKey ) {
			$array1 = $this->arrayIntersectRecursiveHelper( $array1, $array2 );
		}

		if ( empty( $array1 ) ) {
			return [];
		}

		foreach ( $array1 as $k => $v ) {
			if ( is_array( $v ) && isset( $array2[ $k ] ) ) {
				$array1[ $k ] = $this->arrayIntersectRecursive( $array1[ $k ], $array2[ $k ], $allowedKey, $k );
			}
		}

		if ( $this->isArrayNumeric( $array1 ) ) {
			$array1 = array_values( $array1 );
		}

		return $array1;
	}

	/**
	 * Recursively intersects the two given arrays. Supports arrays with a mix of nested arrays and primitive values.
	 * Helper function for arrayIntersectRecursive().
	 *
	 * @since 4.5.4
	 *
	 * @param  array $array1 The first array.
	 * @param  array $array2 The second array.
	 * @return array         The intersected array.
	 */
	private function arrayIntersectRecursiveHelper( $array1, $array2 ) {
		if ( null === $array2 ) {
			$array2 = [];
		}

		if ( is_array( $array1 ) ) {
			// First, check with keys are nested arrays and which are primitive values.
			$arrays     = [];
			$primitives = [];
			foreach ( $array1 as $k => $v ) {
				if ( is_array( $v ) ) {
					$arrays[ $k ] = $v;
				} else {
					$primitives[ $k ] = $v;
				}
			}

			// Then, intersect the primitive values.
			$intersectedPrimitives = array_intersect_assoc( $primitives, $array2 );

			// Finally, recursively intersect the nested arrays.
			$intersectedArrays = [];
			foreach ( $arrays as $k => $v ) {
				if ( isset( $array2[ $k ] ) ) {
					$intersectedArrays[ $k ] = $this->arrayIntersectRecursiveHelper( $v, $array2[ $k ] );
				} else {
					// If the nested array doesn't exist in the second array, we can just unset it.
					unset( $arrays[ $k ] );
				}
			}

			// Merge the intersected arrays and primitive values.
			return array_merge( $intersectedPrimitives, $intersectedArrays );
		}

		return array_intersect_assoc( $array1, $array2 );
	}

	/**
	 * Sorts the keys of an array alphabetically.
	 * The array is passed by reference, so it's not returned the same as in `ksort()`.
	 *
	 * @since 4.4.0.3
	 *
	 * @param array $array The array to sort, passed by reference.
	 */
	public function arrayRecursiveKsort( &$array ) {
		foreach ( $array as &$value ) {
			if ( is_array( $value ) ) {
				$this->arrayRecursiveKsort( $value );
			}
		}

		ksort( $array );
	}

	/**
	 * Creates a multidimensional array from a list of keys and a value.
	 *
	 * @since 4.5.3
	 *
	 * @param  array $keys  The keys to create the array from.
	 * @param  mixed $value The value to assign to the last key.
	 * @param  array $array The array when recursing.
	 * @return array        The multidimensional array.
	 */
	public function createMultidimensionalArray( $keys, $value, $array = [] ) {
		$key = array_shift( $keys );
		if ( empty( $array[ $key ] ) ) {
			$array[ $key ] = null;
		}

		if ( 0 < count( $keys ) ) {
			$array[ $key ] = $this->createMultidimensionalArray( $keys, $value, $array[ $key ] );
		} else {
			$array[ $key ] = $value;
		}

		return $array;
	}

	/**
	 * Sorts an array of arrays by a specific key.
	 *
	 * @since 4.7.4
	 *
	 * @param  array  $arr   The input array.
	 * @param  string $key   The key to sort by.
	 * @param  string $order Designates ascending or descending order. Default 'asc'. Accepts 'asc', 'desc'.
	 * @return void
	 */
	public function usortByKey( &$arr, $key, $order = 'asc' ) {
		if ( empty( $arr ) || ! is_array( $arr ) ) {
			return;
		}

		usort( $arr, function ( $a, $b ) use ( $key, $order ) {
			return 'asc' === $order ? $a[ $key ] <=> $b[ $key ] : $b[ $key ] <=> $a[ $key ];
		} );
	}

	/**
	 * Flattens a multidimensional array.
	 *
	 * @since 4.7.6
	 *
	 * @param  array $arr The input array.
	 * @return array      The flattened array.
	 */
	public function flatten( $arr ) {
		$result = [];
		array_walk_recursive( $arr, function ( $value ) use ( &$result ) {
			$result[] = $value;
		} );

		return $result;
	}
}Common/Traits/Helpers/Buffer.php000064400000000605151536241200012566 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

/**
 * Contains buffer specific helper methods.
 *
 * @since 4.8.3
 */
trait Buffer {
	/**
	 * Clears all output buffers.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	public function clearBuffers() {
		while ( ob_get_level() > 0 ) {
			ob_end_clean();
		}
	}
}Common/Traits/Helpers/Constants.php000064400000025544151536241200013342 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

/**
 * Contains constant specific helper methods.
 *
 * @since 4.0.17
 */
trait Constants {
	/**
	 * Returns the All in One SEO Logo
	 *
	 * @since 4.0.0
	 *
	 * @param  string $width     The width of the image.
	 * @param  string $height    The height of the image.
	 * @param  string $colorCode The color of the image.
	 * @return string            The logo as a string.
	 */
	public function logo( $width, $height, $colorCode ) {
		return '<svg viewBox="0 0 20 20" width="' . $width . '" height="' . $height . '" fill="none" xmlns="http://www.w3.org/2000/svg" class="aioseo-gear"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.98542 19.9708C15.5002 19.9708 19.9708 15.5002 19.9708 9.98542C19.9708 4.47063 15.5002 0 9.98542 0C4.47063 0 0 4.47063 0 9.98542C0 15.5002 4.47063 19.9708 9.98542 19.9708ZM8.39541 3.65464C8.26016 3.4485 8.0096 3.35211 7.77985 3.43327C7.51816 3.52572 7.26218 3.63445 7.01349 3.7588C6.79519 3.86796 6.68566 4.11731 6.73372 4.36049L6.90493 5.22694C6.949 5.44996 6.858 5.6763 6.68522 5.82009C6.41216 6.04734 6.16007 6.30426 5.93421 6.58864C5.79383 6.76539 5.57233 6.85907 5.35361 6.81489L4.50424 6.6433C4.26564 6.5951 4.02157 6.70788 3.91544 6.93121C3.85549 7.05738 3.79889 7.1862 3.74583 7.31758C3.69276 7.44896 3.64397 7.58105 3.59938 7.71369C3.52048 7.94847 3.61579 8.20398 3.81839 8.34133L4.53958 8.83027C4.72529 8.95617 4.81778 9.1819 4.79534 9.40826C4.75925 9.77244 4.76072 10.136 4.79756 10.4936C4.82087 10.7198 4.72915 10.9459 4.54388 11.0724L3.82408 11.5642C3.62205 11.7022 3.52759 11.9579 3.60713 12.1923C3.69774 12.4593 3.8043 12.7205 3.92615 12.9743C4.03313 13.1971 4.27749 13.3088 4.51581 13.2598L5.36495 13.0851C5.5835 13.0401 5.80533 13.133 5.94623 13.3093C6.16893 13.5879 6.42071 13.8451 6.6994 14.0756C6.87261 14.2188 6.96442 14.4448 6.92112 14.668L6.75296 15.5348C6.70572 15.7782 6.81625 16.0273 7.03511 16.1356C7.15876 16.1967 7.285 16.2545 7.41375 16.3086C7.54251 16.3628 7.67196 16.4126 7.80195 16.4581C8.18224 16.5912 8.71449 16.1147 9.108 15.7625C9.30205 15.5888 9.42174 15.343 9.42301 15.0798C9.42301 15.0784 9.42302 15.077 9.42302 15.0756L9.42301 13.6263C9.42301 13.6109 9.4236 13.5957 9.42476 13.5806C8.26248 13.2971 7.39838 12.2301 7.39838 10.9572V9.41823C7.39838 9.30125 7.49131 9.20642 7.60596 9.20642H8.32584V7.6922C8.32584 7.48312 8.49193 7.31364 8.69683 7.31364C8.90171 7.31364 9.06781 7.48312 9.06781 7.6922V9.20642H11.0155V7.6922C11.0155 7.48312 11.1816 7.31364 11.3865 7.31364C11.5914 7.31364 11.7575 7.48312 11.7575 7.6922V9.20642H12.4773C12.592 9.20642 12.6849 9.30125 12.6849 9.41823V10.9572C12.6849 12.2704 11.7653 13.3643 10.5474 13.6051C10.5477 13.6121 10.5478 13.6192 10.5478 13.6263L10.5478 15.0694C10.5478 15.3377 10.6711 15.5879 10.871 15.7622C11.2715 16.1115 11.8129 16.5837 12.191 16.4502C12.4527 16.3577 12.7086 16.249 12.9573 16.1246C13.1756 16.0155 13.2852 15.7661 13.2371 15.5229L13.0659 14.6565C13.0218 14.4334 13.1128 14.2071 13.2856 14.0633C13.5587 13.8361 13.8107 13.5792 14.0366 13.2948C14.177 13.118 14.3985 13.0244 14.6172 13.0685L15.4666 13.2401C15.7052 13.2883 15.9493 13.1756 16.0554 12.9522C16.1153 12.8261 16.1719 12.6972 16.225 12.5659C16.2781 12.4345 16.3269 12.3024 16.3714 12.1698C16.4503 11.935 16.355 11.6795 16.1524 11.5421L15.4312 11.0532C15.2455 10.9273 15.153 10.7015 15.1755 10.4752C15.2116 10.111 15.2101 9.74744 15.1733 9.38986C15.1499 9.16361 15.2417 8.93757 15.4269 8.811L16.1467 8.31927C16.3488 8.18126 16.4432 7.92558 16.3637 7.69115C16.2731 7.42411 16.1665 7.16292 16.0447 6.90915C15.9377 6.68638 15.6933 6.57462 15.455 6.62366L14.6059 6.79837C14.3873 6.84334 14.1655 6.75048 14.0246 6.57418C13.8019 6.29554 13.5501 6.03832 13.2714 5.80784C13.0982 5.6646 13.0064 5.43858 13.0497 5.2154L13.2179 4.34868C13.2651 4.10521 13.1546 3.85616 12.9357 3.74787C12.8121 3.68669 12.6858 3.62895 12.5571 3.5748C12.4283 3.52065 12.2989 3.47086 12.1689 3.42537C11.9388 3.34485 11.6884 3.44211 11.5538 3.64884L11.0746 4.38475C10.9513 4.57425 10.73 4.66862 10.5082 4.64573C10.1513 4.6089 9.79502 4.61039 9.44459 4.64799C9.22286 4.67177 9.00134 4.57818 8.87731 4.38913L8.39541 3.65464Z" fill="' . $colorCode . '" /></svg>'; // phpcs:ignore Generic.Files.LineLength.MaxExceeded
	}

	/**
	 * Returns the country name by code.
	 *
	 * @since 4.0.17
	 *
	 * @param  string $countryCode The country code.
	 * @return string              Country name.
	 */
	public function getCountryName( $countryCode ) {
		return isset( $this->countryList()[ $countryCode ] ) ? $this->countryList()[ $countryCode ] : '';
	}

	/**
	 * Returns a list of countries.
	 *
	 * @since 4.0.17
	 *
	 * @return array A list of countries.
	 */
	public function countryList() {
		return [
			'AF' => 'Afghanistan',
			'AL' => 'Albania',
			'DZ' => 'Algeria',
			'AS' => 'American Samoa',
			'AD' => 'Andorra',
			'AO' => 'Angola',
			'AI' => 'Anguilla',
			'AQ' => 'Antarctica',
			'AG' => 'Antigua and Barbuda',
			'AR' => 'Argentina',
			'AM' => 'Armenia',
			'AW' => 'Aruba',
			'AU' => 'Australia',
			'AT' => 'Austria',
			'AZ' => 'Azerbaijan',
			'BS' => 'Bahamas',
			'BH' => 'Bahrain',
			'BD' => 'Bangladesh',
			'BB' => 'Barbados',
			'BY' => 'Belarus',
			'BE' => 'Belgium',
			'BZ' => 'Belize',
			'BJ' => 'Benin',
			'BM' => 'Bermuda',
			'BT' => 'Bhutan',
			'BO' => 'Bolivia',
			'BQ' => 'Bonaire',
			'BA' => 'Bosnia and Herzegovina',
			'BW' => 'Botswana',
			'BV' => 'Bouvet Island',
			'BR' => 'Brazil',
			'IO' => 'British Indian Ocean Territory',
			'BN' => 'Brunei Darussalam',
			'BG' => 'Bulgaria',
			'BF' => 'Burkina Faso',
			'BI' => 'Burundi',
			'CV' => 'Cabo Verde',
			'KH' => 'Cambodia',
			'CM' => 'Cameroon',
			'CA' => 'Canada',
			'KY' => 'Cayman Islands',
			'CF' => 'Central African Republic',
			'TD' => 'Chad',
			'CL' => 'Chile',
			'CN' => 'China',
			'CX' => 'Christmas Island',
			'CC' => 'Cocos (Keeling) Islands',
			'CO' => 'Colombia',
			'KM' => 'Comoros',
			'CD' => 'Democratic Republic of the Congo',
			'CG' => 'Congo',
			'CK' => 'Cook Islands',
			'CR' => 'Costa Rica',
			'HR' => 'Croatia',
			'CU' => 'Cuba',
			'CW' => 'Curaçao',
			'CY' => 'Cyprus',
			'CZ' => 'Czechia',
			'CI' => 'Côte d\'Ivoire',
			'DK' => 'Denmark',
			'DJ' => 'Djibouti',
			'DM' => 'Dominica',
			'DO' => 'Dominican Republic',
			'EC' => 'Ecuador',
			'EG' => 'Egypt',
			'SV' => 'El Salvador',
			'GQ' => 'Equatorial Guinea',
			'ER' => 'Eritrea',
			'EE' => 'Estonia',
			'SZ' => 'Eswatini',
			'ET' => 'Ethiopia',
			'FK' => 'Falkland Islands',
			'FO' => 'Faroe Islands',
			'FJ' => 'Fiji',
			'FI' => 'Finland',
			'FR' => 'France',
			'GF' => 'French Guiana',
			'PF' => 'French Polynesia',
			'TF' => 'French Southern Territories',
			'GA' => 'Gabon',
			'GM' => 'Gambia',
			'GE' => 'Georgia',
			'DE' => 'Germany',
			'GH' => 'Ghana',
			'GI' => 'Gibraltar',
			'GR' => 'Greece',
			'GL' => 'Greenland',
			'GD' => 'Grenada',
			'GP' => 'Guadeloupe',
			'GU' => 'Guam',
			'GT' => 'Guatemala',
			'GG' => 'Guernsey',
			'GN' => 'Guinea',
			'GW' => 'Guinea-Bissau',
			'GY' => 'Guyana',
			'HT' => 'Haiti',
			'HM' => 'Heard Island and McDonald Islands',
			'VA' => 'Holy See',
			'HN' => 'Honduras',
			'HK' => 'Hong Kong',
			'HU' => 'Hungary',
			'IS' => 'Iceland',
			'IN' => 'India',
			'ID' => 'Indonesia',
			'IR' => 'Iran',
			'IQ' => 'Iraq',
			'IE' => 'Ireland',
			'IM' => 'Isle of Man',
			'IL' => 'Israel',
			'IT' => 'Italy',
			'JM' => 'Jamaica',
			'JP' => 'Japan',
			'JE' => 'Jersey',
			'JO' => 'Jordan',
			'KZ' => 'Kazakhstan',
			'KE' => 'Kenya',
			'KI' => 'Kiribati',
			'KR' => 'South Korea',
			'KW' => 'Kuwait',
			'KG' => 'Kyrgyzstan',
			'LA' => 'Lao People\'s Democratic Republic',
			'LV' => 'Latvia',
			'LB' => 'Lebanon',
			'LS' => 'Lesotho',
			'LR' => 'Liberia',
			'LY' => 'Libya',
			'LI' => 'Liechtenstein',
			'LT' => 'Lithuania',
			'LU' => 'Luxembourg',
			'MO' => 'Macao',
			'MG' => 'Madagascar',
			'MW' => 'Malawi',
			'MY' => 'Malaysia',
			'MV' => 'Maldives',
			'ML' => 'Mali',
			'MT' => 'Malta',
			'MH' => 'Marshall Islands',
			'MQ' => 'Martinique',
			'MR' => 'Mauritania',
			'MU' => 'Mauritius',
			'YT' => 'Mayotte',
			'MX' => 'Mexico',
			'FM' => 'Micronesia',
			'MD' => 'Moldova',
			'MC' => 'Monaco',
			'MN' => 'Mongolia',
			'ME' => 'Montenegro',
			'MS' => 'Montserrat',
			'MA' => 'Morocco',
			'MZ' => 'Mozambique',
			'MM' => 'Myanmar',
			'NA' => 'Namibia',
			'NR' => 'Nauru',
			'NP' => 'Nepal',
			'NL' => 'Netherlands',
			'NC' => 'New Caledonia',
			'NZ' => 'New Zealand',
			'NI' => 'Nicaragua',
			'NE' => 'Niger',
			'NG' => 'Nigeria',
			'NU' => 'Niue',
			'NF' => 'Norfolk Island',
			'MP' => 'Northern Mariana Islands',
			'NO' => 'Norway',
			'OM' => 'Oman',
			'PK' => 'Pakistan',
			'PW' => 'Palau',
			'PS' => 'Palestine, State of',
			'PA' => 'Panama',
			'PG' => 'Papua New Guinea',
			'PY' => 'Paraguay',
			'PE' => 'Peru',
			'PH' => 'Philippines',
			'PN' => 'Pitcairn',
			'PL' => 'Poland',
			'PT' => 'Portugal',
			'PR' => 'Puerto Rico',
			'QA' => 'Qatar',
			'MK' => 'Republic of North Macedonia',
			'RO' => 'Romania',
			'RU' => 'Russian Federation',
			'RW' => 'Rwanda',
			'RE' => 'Réunion',
			'BL' => 'Saint Barthélemy',
			'SH' => 'Saint Helena, Ascension and Tristan da Cunha',
			'KN' => 'Saint Kitts and Nevis',
			'LC' => 'Saint Lucia',
			'MF' => 'Saint Martin',
			'PM' => 'Saint Pierre and Miquelon',
			'VC' => 'Saint Vincent and the Grenadines',
			'WS' => 'Samoa',
			'SM' => 'San Marino',
			'ST' => 'Sao Tome and Principe',
			'SA' => 'Saudi Arabia',
			'SN' => 'Senegal',
			'RS' => 'Serbia',
			'SC' => 'Seychelles',
			'SL' => 'Sierra Leone',
			'SG' => 'Singapore',
			'SX' => 'Sint Maarten',
			'SK' => 'Slovakia',
			'SI' => 'Slovenia',
			'SB' => 'Solomon Islands',
			'SO' => 'Somalia',
			'ZA' => 'South Africa',
			'GS' => 'South Georgia and the South Sandwich Islands',
			'SS' => 'South Sudan',
			'ES' => 'Spain',
			'LK' => 'Sri Lanka',
			'SD' => 'Sudan',
			'SR' => 'Suriname',
			'SJ' => 'Svalbard and Jan Mayen',
			'SE' => 'Sweden',
			'CH' => 'Switzerland',
			'SY' => 'Syrian Arab Republic',
			'TW' => 'Taiwan',
			'TJ' => 'Tajikistan',
			'TZ' => 'Tanzania, United Republic of',
			'TH' => 'Thailand',
			'TL' => 'Timor-Leste',
			'TG' => 'Togo',
			'TK' => 'Tokelau',
			'TO' => 'Tonga',
			'TT' => 'Trinidad and Tobago',
			'TN' => 'Tunisia',
			'TR' => 'Turkey',
			'TM' => 'Turkmenistan',
			'TC' => 'Turks and Caicos Islands',
			'TV' => 'Tuvalu',
			'UG' => 'Uganda',
			'UA' => 'Ukraine',
			'AE' => 'United Arab Emirates',
			'GB' => 'United Kingdom of Great Britain and Northern Ireland',
			'UM' => 'United States Minor Outlying Islands',
			'US' => 'United States of America',
			'UY' => 'Uruguay',
			'UZ' => 'Uzbekistan',
			'VU' => 'Vanuatu',
			'VE' => 'Venezuela',
			'VN' => 'Vietnam',
			'VG' => 'Virgin Islands (British)',
			'VI' => 'Virgin Islands (U.S.)',
			'WF' => 'Wallis and Futuna',
			'EH' => 'Western Sahara',
			'YE' => 'Yemen',
			'ZM' => 'Zambia',
			'ZW' => 'Zimbabwe',
			'AX' => 'Åland Islands'
		];
	}
}Common/Traits/Helpers/DateTime.php000064400000012035151536241200013051 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

/**
 * Contains date/time specific helper methods.
 *
 * @since 4.1.2
 */
trait DateTime {
	/**
	 * Formats a date in ISO8601 format.
	 *
	 * @since 4.1.2
	 *
	 * @param  string $date The date.
	 * @return string       The date formatted in ISO8601 format.
	 */
	public function dateToIso8601( $date ) {
		return date( 'Y-m-d', strtotime( $date ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
	}

	/**
	 * Formats a date & time in ISO8601 format.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $dateTime The date.
	 * @return string           The date formatted in ISO8601 format.
	 */
	public function dateTimeToIso8601( $dateTime ) {
		return date( 'c', strtotime( $dateTime ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
	}

	/**
	 * Formats a date & time in RFC-822 format.
	 *
	 * @since 4.2.1
	 *
	 * @param  string $dateTime The date.
	 * @return string           The date formatted in RFC-822 format.
	 */
	public function dateTimeToRfc822( $dateTime ) {
		return date( 'D, d M Y H:i:s O', strtotime( $dateTime ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
	}

	/**
	 * Retrieves the timezone offset in seconds.
	 *
	 * @since   4.0.0
	 * @version 4.7.2 Returns the actual timezone offset.
	 *
	 * @return int The timezone offset in seconds.
	 */
	public function getTimeZoneOffset() {
		try {
			$timezone = get_option( 'timezone_string' );
			if ( $timezone ) {
				$timezone_object = new \DateTimeZone( $timezone ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName

				return $timezone_object->getOffset( new \DateTime( 'now' ) ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			}
		} catch ( \Exception $e ) {
			// Do nothing.
		}

		return intval( get_option( 'gmt_offset', 0 ) ) * HOUR_IN_SECONDS;
	}

	/**
	 * Formats an amount of days, hours and minutes in ISO8601 duration format.
	 * This is used in our JSON schema to adhere to Google's standards.
	 *
	 * @since 4.2.5
	 *
	 * @param  integer|string $days    The days.
	 * @param  integer|string $hours   The hours.
	 * @param  integer|string $minutes The minutes.
	 * @return string                  The days, hours and minutes formatted in ISO8601 duration format.
	 */
	public function timeToIso8601DurationFormat( $days, $hours, $minutes ) {
		$duration = 'P';
		if ( $days ) {
			$duration .= $days . 'D';
		}

		$duration .= 'T';
		if ( $hours ) {
			$duration .= $hours . 'H';
		}

		if ( $minutes ) {
			$duration .= $minutes . 'M';
		}

		return $duration;
	}

	/**
	 * Returns a MySQL formatted date.
	 *
	 * @since 4.1.5
	 *
	 * @param  int|string   $time Any format accepted by strtotime.
	 * @return false|string       The MySQL formatted string.
	 */
	public function timeToMysql( $time ) {
		$time = is_string( $time ) ? strtotime( $time ) : $time;

		return date( 'Y-m-d H:i:s', $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
	}

	/**
	 * Formats a date in WordPress format.
	 *
	 * @since 4.8.2
	 *
	 * @param  string      $dateTime          Same as you'd pass to `strtotime()`.
	 * @param  string      $dateTimeSeparator The separator between the date and time.
	 * @return string|null                    The date formatted in WordPress format. Null if the passed date is invalid.
	 */
	public function dateToWpFormat( $dateTime, $dateTimeSeparator = ', ' ) {
		static $format = null;
		if ( ! isset( $format ) ) {
			$dateFormat = get_option( 'date_format', 'd M' );
			$timeFormat = get_option( 'time_format', 'H:i' );
			$format     = $dateFormat . $dateTimeSeparator . $timeFormat;
		}

		$timestamp = strtotime( (string) $dateTime );

		return $timestamp && 0 < $timestamp ? date_i18n( $format, $timestamp ) : null;
	}

	/**
	 * Checks if a given string is a valid date.
	 *
	 * @since 4.8.3
	 *
	 * @param  string $date   The date string to check.
	 * @param  string $format The format of the date string.
	 * @return bool           True if the string is a valid date, false otherwise.
	 */
	public function isValidDate( $date, $format = null ) {
		if ( ! $date ) {
			return false;
		}

		if ( $format ) {
			$d = \DateTime::createFromFormat( $format, $date );

			return $d && $d->format( $format ) === $date;
		}

		$timestamp = strtotime( $date );

		return false !== $timestamp;
	}

	/**
	 * Generates a random (yet unique per identifier) time offset based on a site identifier.
	 *
	 * @since 4.7.9
	 *
	 * @param  string $identifier       Data such as the site URL, site ID, or a combination of both to serve as the seed for generating a random time offset.
	 * @param  int    $maxOffsetMinutes The range for the random offset in minutes.
	 * @return int                      The random (yet unique per identifier) time offset in minutes.
	 */
	public function generateRandomTimeOffset( $identifier, $maxOffsetMinutes ) {
		$hash = md5( strval( $identifier ) );

		// Convert part of the hash to an integer.
		$hashInteger = hexdec( substr( $hash, 0, 8 ) );

		return $hashInteger % $maxOffsetMinutes;
	}
}Common/Traits/Helpers/Deprecated.php000064400000006757151536241200013433 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

/**
 * Contains deprecated methods to be removed at a later date..
 *
 * @since 4.1.9
 */
trait Deprecated {
	/**
	 * Helper method to enqueue scripts.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $script The script to enqueue.
	 * @param  string $url    The URL of the script.
	 * @param  bool   $vue    Whether or not this is a vue script.
	 * @return void
	 */
	public function enqueueScript( $script, $url, $vue = true ) {
		if ( ! wp_script_is( $script, 'enqueued' ) ) {
			wp_enqueue_script(
				$script,
				$this->getScriptUrl( $url, $vue ),
				[],
				aioseo()->version,
				true
			);
		}
	}

	/**
	 * Helper method to enqueue stylesheets.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $style The stylesheet to enqueue.
	 * @param  string $url   The URL of the stylesheet.
	 * @param  bool   $vue    Whether or not this is a vue stylesheet.
	 * @return void
	 */
	public function enqueueStyle( $style, $url, $vue = true ) {
		if ( ! wp_style_is( $style, 'enqueued' ) && $this->shouldEnqueue( $url ) ) {
			wp_enqueue_style(
				$style,
				$this->getScriptUrl( $url, $vue ),
				[],
				aioseo()->version
			);
		}
	}

	/**
	 * Whether or not we should enqueue a file.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $url The url to check against.
	 * @return bool        Whether or not we should enqueue.
	 */
	private function shouldEnqueue( $url ) {
		$version  = strtoupper( aioseo()->versionPath );
		$host     = defined( 'AIOSEO_DEV_' . $version ) ? constant( 'AIOSEO_DEV_' . $version ) : false;

		if ( ! $host ) {
			return true;
		}

		if ( false !== strpos( $url, 'chunk-common.css' ) ) {
			// return false;
		}

		return true;
	}

	/**
	 * Retrieve the proper URL for this script or style.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $url The url.
	 * @param  bool   $vue Whether or not this is a vue script.
	 * @return string      The modified url.
	 */
	public function getScriptUrl( $url, $vue = true ) {
		$version  = strtoupper( aioseo()->versionPath );
		$host     = $vue && defined( 'AIOSEO_DEV_' . $version ) ? constant( 'AIOSEO_DEV_' . $version ) : false;
		$localUrl = $url;
		$url      = plugins_url( 'dist/' . aioseo()->versionPath . '/assets/' . $url, AIOSEO_FILE );

		if ( ! $host ) {
			return $url;
		}

		if ( $host && ! self::$connection ) {
			$splitHost        = explode( ':', str_replace( '/', '', str_replace( 'http://', '', str_replace( 'https://', '', $host ) ) ) );
			self::$connection = @fsockopen( $splitHost[0], $splitHost[1] ); // phpcs:ignore WordPress
		}

		if ( ! self::$connection ) {
			return $url;
		}

		return $host . $localUrl;
	}

	/**
	 * Returns the filesystem object if we have access to it.
	 *
	 * @since 4.0.0
	 *
	 * @param  array                    $args The connection args.
	 * @return \WP_Filesystem_Base|bool       The filesystem object.
	 */
	public function wpfs( $args = [] ) {
		require_once ABSPATH . 'wp-admin/includes/file.php';
		WP_Filesystem( $args );

		// phpcs:disable Squiz.NamingConventions.ValidVariableName
		global $wp_filesystem;
		if ( is_object( $wp_filesystem ) ) {
			return $wp_filesystem;
		}
		// phpcs:enable Squiz.NamingConventions.ValidVariableName

		return false;
	}

	/**
	 * Checks whether the current request is an AJAX, CRON or REST request.
	 *
	 * @since 4.1.9.1
	 *
	 * @return bool Whether the current request is an AJAX, CRON or REST request.
	 */
	public function isAjaxCronRest() {
		return $this->isAjaxCronRestRequest();
	}
}Common/Traits/Helpers/Language.php000064400000000744151536241200013104 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

/**
 * Contains i18n and language (code) helper methods.
 *
 * @since 4.1.4
 */
trait Language {
	/**
	 * Returns the language of the current response in BCP 47 format.
	 *
	 * @since 4.1.4
	 *
	 * @return string The language code in BCP 47 format.
	 */
	public function currentLanguageCodeBCP47() {
		return str_replace( '_', '-', determine_locale() );
	}
}Common/Traits/Helpers/Numbers.php000064400000001570151536241210012773 0ustar00<?php

namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

/**
 * Numbers trait.
 *
 * @since 4.7.2
 */
trait Numbers {
	/**
	 * Formats a number to a compact format.
	 *
	 * @since 4.7.2
	 *
	 * @param  float|int|string $number   The number to format.
	 * @param  int              $decimals The number of decimal places to include.
	 * @return string                     Formatted number in string format.
	 */
	public function compactNumber( $number, $decimals = 1 ) {
		$suffixes    = [ '', 'K', 'M', 'B', 'T', 'q', 'Q' ];
		$suffixIndex = 0;

		while ( abs( $number ) >= 1000 && $suffixIndex < count( $suffixes ) - 1 ) {
			$suffixIndex++;
			$number /= 1000;
		}

		// Remove trailing zeros.
		return preg_replace( '/\D0+$/', '', (string) number_format_i18n( $number, $decimals ) ) . $suffixes[ $suffixIndex ];
	}
}Common/Traits/Helpers/PostType.php000064400000001410151536241210013140 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

/**
 * Contains WordPress Post Type helpers.
 *
 * @since 4.2.4
 */
trait PostType {
	/**
	 * Returns a post type feature.
	 *
	 * @since 4.2.4
	 *
	 * @param  string|\WP_Post_Type $postType The post type.
	 * @param  string               $feature  The feature to find.
	 * @return mixed|false                    The post type feature or false if not found.
	 */
	public function getPostTypeFeature( $postType, $feature ) {
		if ( is_string( $postType ) ) {
			$postType = get_post_type_object( $postType );
		}

		if ( ! is_a( $postType, 'WP_Post_Type' ) || ! isset( $postType->$feature ) ) {
			return false;
		}

		return $postType->$feature;
	}
}Common/Traits/Helpers/Request.php000064400000003440151536241210013006 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

/**
 * Parse the current request.
 *
 * @since 4.2.1
 */
trait Request {
	/**
	 * Get the server port.
	 *
	 * @since 4.2.1
	 *
	 * @return string The server port.
	 */
	private function getServerPort() {
		if (
			empty( $_SERVER['SERVER_PORT'] ) ||
			80 === (int) $_SERVER['SERVER_PORT'] ||
			443 === (int) $_SERVER['SERVER_PORT']
		) {
			return '';
		}

		return ':' . (int) $_SERVER['SERVER_PORT'];
	}

	/**
	 * Get the protocol.
	 *
	 * @since 4.2.1
	 *
	 * @return string The protocol.
	 */
	private function getProtocol() {
		return is_ssl() ? 'https' : 'http';
	}

	/**
	 * Get the server name (from $_SERVER['SERVER_NAME]), or use the request name ($_SERVER['HTTP_HOST']) if not present.
	 *
	 * @since 4.2.1
	 *
	 * @return string The server name.
	 */
	private function getServerName() {
		$host = $this->getRequestServerName();

		if ( isset( $_SERVER['SERVER_NAME'] ) ) {
			$host = sanitize_text_field( wp_unslash( $_SERVER['SERVER_NAME'] ) ); // phpcs:ignore HM.Security.ValidatedSanitizedInput.InputNotSanitized
		}

		return $host;
	}

	/**
	 * Get the request server name (from $_SERVER['HTTP_HOST]).
	 *
	 * @since 4.2.1
	 *
	 * @return string The request server name.
	 */
	private function getRequestServerName() {
		$host = '';

		if ( isset( $_SERVER['HTTP_HOST'] ) ) {
			$host = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) );
		}

		return $host;
	}

	/**
	 * Retrieve the request URL.
	 *
	 * @since 4.2.1
	 *
	 * @return string The request URL.
	 */
	public function getRequestUrl() {
		$url = '';

		if ( isset( $_SERVER['REQUEST_URI'] ) ) {
			$url = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
		}

		return rawurldecode( $url );
	}
}Common/Traits/Helpers/Shortcodes.php000064400000014440151536241210013475 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

/**
 * Contains shortcode specific helper methods.
 *
 * @since 4.1.2
 */
trait Shortcodes {
	/**
	 * Shortcodes known to conflict with AIOSEO.
	 * NOTE: This is deprecated and only there for users who already were using the aioseo_conflicting_shortcodes_hook before 4.2.0.
	 *
	 * @since 4.1.2
	 *
	 * @var array
	 */
	private $conflictingShortcodes = [
		'WooCommerce Login'                => 'woocommerce_my_account',
		'WooCommerce Checkout'             => 'woocommerce_checkout',
		'WooCommerce Order Tracking'       => 'woocommerce_order_tracking',
		'WooCommerce Cart'                 => 'woocommerce_cart',
		'WooCommerce Registration'         => 'wwp_registration_form',
		'WISDM Group Registration'         => 'wdm_group_users',
		'WISDM Quiz Reporting'             => 'wdm_quiz_statistics_details',
		'WISDM Course Review'              => 'rrf_course_review',
		'Simple Membership Login'          => 'swpm_login_form',
		'Simple Membership Mini Login'     => 'swpm_mini_login',
		'Simple Membership Payment Button' => 'swpm_payment_button',
		'Simple Membership Thank You Page' => 'swpm_thank_you_page_registration',
		'Simple Membership Registration'   => 'swpm_registration_form',
		'Simple Membership Profile'        => 'swpm_profile_form',
		'Simple Membership Reset'          => 'swpm_reset_form',
		'Simple Membership Update Level'   => 'swpm_update_level_to',
		'Simple Membership Member Info'    => 'swpm_show_member_info',
		'Revslider'                        => 'rev_slider'
	];

	/**
	 * Returns the content with shortcodes replaced.
	 *
	 * @since 4.0.5
	 *
	 * @param  string $content  The post content.
	 * @param  bool   $override Whether shortcodes should be parsed regardless of the context. Needed for ActionScheduler actions.
	 * @param  int    $postId   The post ID (optional).
	 * @return string $content  The post content with shortcodes replaced.
	 */
	public function doShortcodes( $content, $override = false, $postId = 0 ) {
		// NOTE: This is_admin() check can never be removed because themes like Avada will otherwise load the wrong post.
		if ( ! $override && is_admin() ) {
			return $content;
		}

		if ( ! wp_doing_cron() && ! wp_doing_ajax() ) {
			if ( ! $override && apply_filters( 'aioseo_disable_shortcode_parsing', false ) ) {
				return $content;
			}

			if ( ! $override && ! aioseo()->options->searchAppearance->advanced->runShortcodes ) {
				return $this->doAllowedShortcodes( $content, $postId );
			}
		}

		$content = $this->doShortcodesHelper( $content, [], $postId );

		return $content;
	}

	/**
	 * Returns the content with only the allowed shortcodes and wildcards replaced.
	 *
	 * @since   4.1.2
	 * @version 4.6.6 Added the $allowedTags parameter.
	 *
	 * @param  string $content     The content.
	 * @param  int    $postId      The post ID (optional).
	 * @param  array  $allowedTags The shortcode tags to allow (optional).
	 * @return string              The content with shortcodes replaced.
	 */
	public function doAllowedShortcodes( $content, $postId = null, $allowedTags = [] ) {
		// Extract list of shortcodes from the post content.
		$tags = $this->getShortcodeTags( $content );
		if ( ! count( $tags ) ) {
			return $content;
		}

		$allowedTags  = apply_filters( 'aioseo_allowed_shortcode_tags', $allowedTags );
		$tagsToRemove = array_diff( $tags, $allowedTags );

		$content = $this->doShortcodesHelper( $content, $tagsToRemove, $postId );

		return $content;
	}

	/**
	 * Returns the content with only the allowed shortcodes and wildcards replaced.
	 *
	 * @since 4.1.2
	 *
	 * @param  string $content      The content.
	 * @param  array  $tagsToRemove The shortcode tags to remove (optional).
	 * @param  int    $postId       The post ID (optional).
	 * @return string               The content with shortcodes replaced.
	 */
	private function doShortcodesHelper( $content, $tagsToRemove = [], $postId = 0 ) {
		global $shortcode_tags; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$conflictingShortcodes = array_merge( $tagsToRemove, $this->conflictingShortcodes );
		$conflictingShortcodes = apply_filters( 'aioseo_conflicting_shortcodes', $conflictingShortcodes );

		$tagsToRemove = [];
		foreach ( $conflictingShortcodes as $shortcode ) {
			$shortcodeTag = str_replace( [ '[', ']' ], '', $shortcode );
			if ( array_key_exists( $shortcodeTag, $shortcode_tags ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				$tagsToRemove[ $shortcodeTag ] = $shortcode_tags[ $shortcodeTag ]; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			}
		}

		// Remove all conflicting shortcodes before parsing the content.
		foreach ( $tagsToRemove as $shortcodeTag => $shortcodeCallback ) {
			remove_shortcode( $shortcodeTag );
		}

		if ( $postId ) {
			global $post;
			$post = get_post( $postId );
			if ( is_a( $post, 'WP_Post' ) ) {
				// Add the current post to the loop so that shortcodes can use it if needed.
				setup_postdata( $post );
			}
		}

		// Set a flag to indicate Divi that it's processing internal content.

		$default = aioseo()->helpers->setDiviInternalRendering( true );

		$content = do_shortcode( $content );

		// Reset the Divi flag to its default value.
		aioseo()->helpers->setDiviInternalRendering( $default );

		if ( $postId ) {
			wp_reset_postdata();
		}

		// Add back shortcodes as remove_shortcode() disables them site-wide.
		foreach ( $tagsToRemove as $shortcodeTag => $shortcodeCallback ) {
			add_shortcode( $shortcodeTag, $shortcodeCallback );
		}

		return $content;
	}

	/**
	 * Extracts the shortcode tags from the content.
	 *
	 * @since 4.1.2
	 *
	 * @param  string $content The content.
	 * @return array  $tags    The shortcode tags.
	 */
	private function getShortcodeTags( $content ) {
		$tags    = [];
		$pattern = '\\[(\\[?)([^\s]*)(?![\\w-])([^\\]\\/]*(?:\\/(?!\\])[^\\]\\/]*)*?)(?:(\\/)\\]|\\](?:([^\\[]*+(?:\\[(?!\\/\\2\\])[^\\[]*+)*+)\\[\\/\\2\\])?)(\\]?)';
		if ( preg_match_all( "#$pattern#s", (string) $content, $matches ) && array_key_exists( 2, $matches ) ) {
			$tags = array_unique( $matches[2] );
		}

		if ( ! count( $tags ) ) {
			return $tags;
		}

		// Extract nested shortcodes.
		foreach ( $matches[5] as $innerContent ) {
			$tags = array_merge( $tags, $this->getShortcodeTags( $innerContent ) );
		}

		return $tags;
	}
}Common/Traits/Helpers/Strings.php000064400000046236151536241210013021 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

/**
 * Contains string specific helper methods.
 *
 * @since 4.0.13
 */
trait Strings {
	/**
	 * Convert to snake case.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string The string to convert.
	 * @return string         The converted string.
	 */
	public function toSnakeCase( $string ) {
		$string[0] = strtolower( $string[0] );

		return preg_replace_callback( '/([A-Z])/', function ( $value ) {
			return '_' . strtolower( $value[1] );
		}, $string );
	}

	/**
	 * Convert to camel case.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string     The string to convert.
	 * @param  bool   $capitalize Whether or not to capitalize the first letter.
	 * @return string             The converted string.
	 */
	public function toCamelCase( $string, $capitalize = false ) {
		$string[0] = strtolower( $string[0] );
		if ( $capitalize ) {
			$string[0] = strtoupper( $string[0] );
		}

		return preg_replace_callback( '/_([a-z0-9])/', function ( $value ) {
			return strtoupper( $value[1] );
		}, $string );
	}

	/**
	 * Converts kebab case to camel case.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string                   The string to convert.
	 * @param  bool   $capitalizeFirstCharacter Whether to capitalize the first letter.
	 * @return string                           The converted string.
	 */
	public function dashesToCamelCase( $string, $capitalizeFirstCharacter = false ) {
		$string = str_replace( ' ', '', ucwords( str_replace( '-', ' ', $string ) ) );
		if ( ! $capitalizeFirstCharacter ) {
			$string[0] = strtolower( $string[0] );
		}

		return $string;
	}

	/**
	 * Truncates a given string.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $string             The string.
	 * @param  int     $maxCharacters      The max. amount of characters.
	 * @param  boolean $shouldHaveEllipsis Whether the string should have a trailing ellipsis (defaults to true).
	 * @return string                      The string.
	 */
	public function truncate( $string, $maxCharacters, $shouldHaveEllipsis = true ) {
		$length       = strlen( $string );
		$excessLength = $length - $maxCharacters;
		if ( 0 < $excessLength ) {
			// If the string is longer than 65535 characters, we first need to shorten it due to the character limit of the regex pattern quantifier.
			if ( 65535 < $length ) {
				$string = substr( $string, 0, 65534 );
			}
			$string = preg_replace( "#[^\pZ\pP]*.{{$excessLength}}$#", '', (string) $string );
			if ( $shouldHaveEllipsis ) {
				$string = $string . ' ...';
			}
		}

		return $string;
	}

	/**
	 * Escapes special regex characters.
	 *
	 * @since 4.0.5
	 *
	 * @param  string $string    The string.
	 * @param  string $delimiter The delimiter character.
	 * @return string            The escaped string.
	 */
	public function escapeRegex( $string, $delimiter = '/' ) {
		static $escapeRegex = [];
		if ( isset( $escapeRegex[ $string ] ) ) {
			return $escapeRegex[ $string ];
		}
		$escapeRegex[ $string ] = preg_quote( (string) $string, $delimiter );

		return $escapeRegex[ $string ];
	}

	/**
	 * Escapes special regex characters inside the replacement string.
	 *
	 * @since 4.0.7
	 *
	 * @param  string $string The string.
	 * @return string         The escaped string.
	 */
	public function escapeRegexReplacement( $string ) {
		static $escapeRegexReplacement = [];
		if ( isset( $escapeRegexReplacement[ $string ] ) ) {
			return $escapeRegexReplacement[ $string ];
		}

		$escapeRegexReplacement[ $string ] = str_replace( '$', '\$', $string );

		return $escapeRegexReplacement[ $string ];
	}

	/**
	 * preg_replace but with the replacement escaped.
	 *
	 * @since 4.0.10
	 *
	 * @param  string $pattern     The pattern to search for.
	 * @param  string $replacement The replacement string.
	 * @param  string $subject     The subject to search in.
	 * @return string              The subject with matches replaced.
	 */
	public function pregReplace( $pattern, $replacement, $subject ) {
		if ( ! $subject ) {
			return $subject;
		}

		$key = $pattern . $replacement . $subject;

		static $pregReplace = [];
		if ( isset( $pregReplace[ $key ] ) ) {
			return $pregReplace[ $key ];
		}

		// TODO: In the future, we should consider escaping the search pattern as well.
		// We can use the following pattern for this - (?<!\\)([\/.^$*+?|()[{}\]]{1})
		// The pattern above will only escape special characters if they're not escaped yet, which makes it compatible with all our patterns that are already escaped.
		// The caveat is that we'd need to first trim off slash delimiters and add them back later - otherwise they'd be escaped as well.

		$replacement         = $this->escapeRegexReplacement( $replacement );
		$pregReplace[ $key ] = preg_replace( $pattern, $replacement, (string) $subject );

		return $pregReplace[ $key ];
	}

	/**
	 * Returns string after converting it to lowercase.
	 *
	 * @since 4.0.13
	 *
	 * @param  string $string The original string.
	 * @return string         The string converted to lowercase.
	 */
	public function toLowerCase( $string ) {
		static $lowerCased = [];
		if ( isset( $lowerCased[ $string ] ) ) {
			return $lowerCased[ $string ];
		}
		$lowerCased[ $string ] = function_exists( 'mb_strtolower' ) ? mb_strtolower( $string, $this->getCharset() ) : strtolower( $string );

		return $lowerCased[ $string ];
	}

	/**
	 * Returns the index of a substring in a string.
	 *
	 * @since 4.1.6
	 *
	 * @param  string   $stack  The stack.
	 * @param  string   $needle The needle.
	 * @param  int      $offset The offset.
	 * @return int|bool         The index where the string starts or false if it does not exist.
	 */
	public function stringIndex( $stack, $needle, $offset = 0 ) {
		$key = $stack . $needle . $offset;

		static $stringIndex = [];
		if ( isset( $stringIndex[ $key ] ) ) {
			return $stringIndex[ $key ];
		}

		$stringIndex[ $key ] = function_exists( 'mb_strpos' ) ? mb_strpos( $stack, $needle, $offset, $this->getCharset() ) : strpos( $stack, $needle, $offset );

		return $stringIndex[ $key ];
	}

	/**
	 * Checks if the given string contains the given substring.
	 *
	 * @since 4.1.0.2
	 *
	 * @param  string   $stack  The stack.
	 * @param  string   $needle The needle.
	 * @param  int      $offset The offset.
	 * @return bool             Whether the substring occurs in the main string.
	 */
	public function stringContains( $stack, $needle, $offset = 0 ) {
		$key = $stack . $needle . $offset;

		static $stringContains = [];
		if ( isset( $stringContains[ $key ] ) ) {
			return $stringContains[ $key ];
		}

		$stringContains[ $key ] = false !== $this->stringIndex( $stack, $needle, $offset );

		return $stringContains[ $key ];
	}

	/**
	 * Check if a string is JSON encoded or not.
	 *
	 * @since 4.1.2
	 *
	 * @param  mixed $string The string to check.
	 * @return bool          True if it is JSON or false if not.
	 */
	public function isJsonString( $string ) {
		if ( ! is_string( $string ) ) {
			return false;
		}

		json_decode( $string );

		// Return a boolean whether or not the last error matches.
		return json_last_error() === JSON_ERROR_NONE;
	}

	/**
	 * Strips punctuation from a given string.
	 *
	 * @since 4.0.0
	 * @version 4.7.9 Added the $keepSpaces parameter.
	 *
	 * @param  string $string           The string.
	 * @param  array  $charactersToKeep The characters that can't be stripped (optional).
	 * @param  bool   $keepSpaces       Whether to keep spaces.
	 * @return string                   The string without punctuation.
	 */
	public function stripPunctuation( $string, $charactersToKeep = [], $keepSpaces = false ) {
		$characterRegexPattern = '';
		if ( ! empty( $charactersToKeep ) ) {
			$characterString       = implode( '', $charactersToKeep );
			$characterRegexPattern = "(?![$characterString])";
		}

		$string = aioseo()->helpers->decodeHtmlEntities( (string) $string );
		$string = preg_replace( "/{$characterRegexPattern}[\p{P}\d+]/u", '', $string );
		$string = aioseo()->helpers->encodeOutputHtml( $string );

		// Trim both internal and external whitespace.
		return $keepSpaces ? $string : preg_replace( '/\s\s+/u', ' ', trim( $string ) );
	}

	/**
	 * Returns the string after it is encoded with htmlspecialchars().
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string The string to encode.
	 * @return string         The encoded string.
	 */
	public function encodeOutputHtml( $string ) {
		if ( ! is_string( $string ) ) {
			return '';
		}

		return htmlspecialchars( $string, ENT_COMPAT | ENT_HTML401, $this->getCharset(), false );
	}

	/**
	 * Returns the string after all HTML entities have been decoded.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string The string to decode.
	 * @return string         The decoded string.
	 */
	public function decodeHtmlEntities( $string ) {
		static $decodeHtmlEntities = [];
		if ( isset( $decodeHtmlEntities[ $string ] ) ) {
			return $decodeHtmlEntities[ $string ];
		}

		// We must manually decode non-breaking spaces since html_entity_decode doesn't do this.
		$string                        = $this->pregReplace( '/&nbsp;/', ' ', $string );
		$decodeHtmlEntities[ $string ] = html_entity_decode( (string) $string, ENT_QUOTES );

		return $decodeHtmlEntities[ $string ];
	}

	/**
	 * Returns the string with script tags stripped.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string The string.
	 * @return string         The modified string.
	 */
	public function stripScriptTags( $string ) {
		static $stripScriptTags = [];
		if ( isset( $stripScriptTags[ $string ] ) ) {
			return $stripScriptTags[ $string ];
		}

		$stripScriptTags[ $string ] = $this->pregReplace( '/<script(.*?)>(.*?)<\/script>/is', '', $string );

		return $stripScriptTags[ $string ];
	}

	/**
	 * Returns the string with incomplete HTML tags stripped.
	 * Incomplete tags are not unopened/unclosed pairs but rather single tags that aren't properly formed.
	 * e.g. <a href='something'
	 * e.g. href='something' >
	 *
	 * @since 4.1.6
	 *
	 * @param  string $string The string.
	 * @return string         The modified string.
	 */
	public function stripIncompleteHtmlTags( $string ) {
		static $stripIncompleteHtmlTags = [];
		if ( isset( $stripIncompleteHtmlTags[ $string ] ) ) {
			return $stripIncompleteHtmlTags[ $string ];
		}

		$stripIncompleteHtmlTags[ $string ] = $this->pregReplace( '/(^(?!<).*?(\/>)|<[^>]*?(?!\/>)$)/is', '', $string );

		return $stripIncompleteHtmlTags[ $string ];
	}


	/**
	 * Returns the given JSON formatted data tags as a comma separated list with their values instead.
	 *
	 * @since 4.1.0
	 *
	 * @param  string|array $tags The Array or JSON formatted data tags.
	 * @return string             The comma separated values.
	 */
	public function jsonTagsToCommaSeparatedList( $tags ) {
		$tags = is_string( $tags ) ? json_decode( $tags ) : $tags;

		$values = [];
		foreach ( $tags as $k => $tag ) {
			$values[ $k ] = is_object( $tag ) ? $tag->value : $tag['value'];
		}

		return implode( ',', $values );
	}

	/**
	 * Returns the character length of the given string.
	 *
	 * @since 4.1.6
	 *
	 * @param  string $string The string.
	 * @return int            The string length.
	 */
	public function stringLength( $string ) {
		static $stringLength = [];
		if ( isset( $stringLength[ $string ] ) ) {
			return $stringLength[ $string ];
		}

		$stringLength[ $string ] = function_exists( 'mb_strlen' ) ? mb_strlen( $string, $this->getCharset() ) : strlen( $string );

		return $stringLength[ $string ];
	}

	/**
	 * Returns the word count of the given string.
	 *
	 * @since 4.1.6
	 *
	 * @param  string $string The string.
	 * @return int            The word count.
	 */
	public function stringWordCount( $string ) {
		static $stringWordCount = [];
		if ( isset( $stringWordCount[ $string ] ) ) {
			return $stringWordCount[ $string ];
		}

		$stringWordCount[ $string ] = str_word_count( $string );

		return $stringWordCount[ $string ];
	}

	/**
	 * Explodes the given string into an array.
	 *
	 * @since 4.1.6
	 *
	 * @param  string $delimiter The delimiter.
	 * @param  string $string    The string.
	 * @return array             The exploded words.
	 */
	public function explode( $delimiter, $string ) {
		$key = $delimiter . $string;

		static $exploded = [];
		if ( isset( $exploded[ $key ] ) ) {
			return $exploded[ $key ];
		}

		$exploded[ $key ] = explode( $delimiter, $string );

		return $exploded[ $key ];
	}

	/**
	 * Implodes an array into a WHEREIN clause useable string.
	 *
	 * @since 4.1.6
	 *
	 * @param  array  $array       The array.
	 * @param  bool   $outerQuotes Whether outer quotes should be added.
	 * @return string              The imploded array.
	 */
	public function implodeWhereIn( $array, $outerQuotes = false ) {
		// Reset the keys first in case there is no 0 index.
		$array = array_values( $array );

		if ( ! isset( $array[0] ) ) {
			return '';
		}

		if ( is_numeric( $array[0] ) ) {
			return implode( ', ', $array );
		}

		return $outerQuotes ? "'" . implode( "', '", $array ) . "'" : implode( "', '", $array );
	}

	/**
	 * Returns an imploded string of placeholders for usage in a WPDB prepare statement.
	 *
	 * @since 4.1.9
	 *
	 * @param  array  $array       The array.
	 * @param  string $placeholder The placeholder (e.g. "%s" or "%d").
	 * @return string              The imploded string with placeholders.
	 */
	public function implodePlaceholders( $array, $placeholder = '%s' ) {
		return implode( ', ', array_fill( 0, count( $array ), $placeholder ) );
	}

	/**
	 * Verifies that a string is indeed a valid regular expression.
	 *
	 * @since 4.2.1
	 *
	 * @return boolean True if the string is a valid regular expression.
	 */
	public function isValidRegex( $pattern ) {
		// Set a custom error handler to prevent throwing errors on a bad Regular Expression.
		set_error_handler( function() {}, E_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler

		$isValid = true;

		if ( false === preg_match( $pattern, '' ) ) {
			$isValid = false;
		}

		// Restore the error handler.
		restore_error_handler();

		return $isValid;
	}

	/**
	 * Removes the leading slash(es) from a string.
	 *
	 * @since 4.2.3
	 *
	 * @param  string $string The string.
	 * @return string         The modified string.
	 */
	public function unleadingSlashIt( $string ) {
		return ltrim( $string, '/' );
	}

	/**
	 * Convert the case of the given string.
	 *
	 * @since 4.2.4
	 *
	 * @param  string $string The string.
	 * @param  string $type   The casing ("lower", "title", "sentence").
	 * @return string         The converted string.
	 */
	public function convertCase( $string, $type ) {
		switch ( $type ) {
			case 'lower':
				return strtolower( $string );
			case 'title':
				return $this->toTitleCase( $string );
			case 'sentence':
				return $this->toSentenceCase( $string );
			default:
				return $string;
		}
	}

	/**
	 * Converts the given string to title case.
	 *
	 * @since 4.2.4
	 *
	 * @param  string $string The string.
	 * @return string         The converted string.
	 */
	public function toTitleCase( $string ) {
		// List of common English words that aren't typically modified.
		$exceptions = apply_filters( 'aioseo_title_case_exceptions', [
			'of',
			'a',
			'the',
			'and',
			'an',
			'or',
			'nor',
			'but',
			'is',
			'if',
			'then',
			'else',
			'when',
			'at',
			'from',
			'by',
			'on',
			'off',
			'for',
			'in',
			'out',
			'over',
			'to',
			'into',
			'with'
		] );

		$words = explode( ' ', strtolower( $string ) );

		foreach ( $words as $k => $word ) {
			if ( ! in_array( $word, $exceptions, true ) ) {
				$words[ $k ] = ucfirst( $word );
			}
		}

		$string = implode( ' ', $words );

		return $string;
	}

	/**
	 * Converts the given string to sentence case.
	 *
	 * @since 4.2.4
	 *
	 * @param  string $string The string.
	 * @return string         The converted string.
	 */
	public function toSentenceCase( $string ) {
		$phrases = preg_split( '/([.?!]+)/', (string) $string, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE );

		$convertedString = '';
		foreach ( $phrases as $index => $sentence ) {
			$convertedString .= ( $index & 1 ) === 0 ? ucfirst( strtolower( trim( $sentence ) ) ) : $sentence . ' ';
		}

		return trim( $convertedString );
	}

	/**
	 * Returns the substring with a given start index and length.
	 *
	 * @since 4.2.5
	 *
	 * @param  string $string     The string.
	 * @param  int    $startIndex The start index.
	 * @param  int    $length     The length.
	 * @return string             The substring.
	 */
	public function substring( $string, $startIndex, $length ) {
		return function_exists( 'mb_substr' ) ? mb_substr( $string, $startIndex, $length, $this->getCharset() ) : substr( $string, $startIndex, $length );
	}

	/**
	 * Strips emoji characters from a given string.
	 *
	 * @since 4.7.3
	 *
	 * @param  string $string The string.
	 * @return string         The string without emoji characters.
	 */
	public function stripEmoji( $string ) {
		// First, decode HTML entities to convert them to actual Unicode characters.
		$string = $this->decodeHtmlEntities( $string );

		// Pattern to match emoji characters.
		$emojiPattern = '/[\x{1F600}-\x{1F64F}' . // Emoticons
						'\x{1F300}-\x{1F5FF}' . // Misc Symbols and Pictographs
						'\x{1F680}-\x{1F6FF}' . // Transport and Map Symbols
						'\x{1F1E0}-\x{1F1FF}' . // Flags (iOS)
						'\x{2600}-\x{26FF}' . // Misc symbols
						'\x{2700}-\x{27BF}' . // Dingbats
						'\x{FE00}-\x{FE0F}' . // Variation Selectors
						'\x{1F900}-\x{1F9FF}' . // Supplemental Symbols and Pictographs
						']/u';

		$filteredString = preg_replace( $emojiPattern, '', (string) $string );

		// Re-encode special characters to HTML entities.
		return $this->encodeOutputHtml( $filteredString );
	}

	/**
	 * Creates a sha1 hash from the given arguments.
	 *
	 * @since 4.7.8
	 *
	 * @param  mixed  ...$args The arguments to create a sha1 hash from.
	 * @return string          The sha1 hash.
	 */
	public function createHash( ...$args ) {
		return sha1( wp_json_encode( $args ) );
	}

	/**
	 * Extracts URLs from a given string.
	 *
	 * @since 4.8.1
	 *
	 * @param  string $string The string.
	 * @return array          The extracted URLs.
	 */
	public function extractUrls( $string ) {
		$urls = wp_extract_urls( $string );

		if ( empty( $urls ) ) {
			return [];
		}

		$allUrls = [];

		// Attempt to split multiple URLs. Elementor does not always separate them properly.
		foreach ( $urls as $url ) {
			$splitUrls = preg_split( '/(?=https?:\/\/)/', $url, - 1, PREG_SPLIT_NO_EMPTY );
			$allUrls   = array_merge( $allUrls, $splitUrls );
		}

		return $allUrls;
	}

	/**
	 * Determines if a text string contains an emoji or not.
	 *
	 * @since 4.8.0
	 *
	 * @param  string $string The text string to detect emoji in.
	 * @return bool
	 */
	public function hasEmojis( $string ) {
		$emojisRegexPattern = '/[\x{1F600}-\x{1F64F}' . // Emoticons
							'\x{1F300}-\x{1F5FF}' . // Misc Symbols and Pictographs
							'\x{1F680}-\x{1F6FF}' . // Transport and Map Symbols
							'\x{1F1E0}-\x{1F1FF}' . // Flags (iOS)
							'\x{2600}-\x{26FF}' . // Misc symbols
							'\x{2700}-\x{27BF}' . // Dingbats
							'\x{FE00}-\x{FE0F}' . // Variation Selectors
							'\x{1F900}-\x{1F9FF}' . // Supplemental Symbols and Pictographs
							'\x{1F018}-\x{1F270}' . // Various Asian characters
							'\x{238C}-\x{2454}' . // Misc items
							'\x{20D0}-\x{20FF}' . // Combining Diacritical Marks for Symbols
							']/u';

		return preg_match( $emojisRegexPattern, $string );
	}
}Common/Traits/Helpers/Svg.php000064400000002017151536241210012114 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

/**
 * Contains SVG specific helper methods.
 *
 * @since 4.1.4
 */
trait Svg {
	/**
	 * Sanitizes a SVG string.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $svgString The SVG to check.
	 * @return string            The sanitized SVG.
	 */
	public function escSvg( $svgString ) {
		if ( ! is_string( $svgString ) ) {
			return false;
		}

		$ksesDefaults = wp_kses_allowed_html( 'post' );

		$svgArgs = [
			'svg'   => [
				'class'           => true,
				'aria-hidden'     => true,
				'aria-labelledby' => true,
				'role'            => true,
				'xmlns'           => true,
				'width'           => true,
				'height'          => true,
				'viewbox'         => true, // <= Must be lower case!
			],
			'g'     => [ 'fill' => true ],
			'title' => [ 'title' => true ],
			'path'  => [
				'd'    => true,
				'fill' => true,
			]
		];

		return wp_kses( $svgString, array_merge( $ksesDefaults, $svgArgs ) );
	}
}Common/Traits/Helpers/ThirdParty.php000064400000055042151536241210013455 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

/**
 * Contains all third-party related helper methods.
 *
 * @since 4.1.4
 */
trait ThirdParty {
	/**
	 * Checks whether WooCommerce is active.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether WooCommerce is active.
	 */
	public function isWooCommerceActive() {
		return class_exists( 'WooCommerce' );
	}

	/**
	 * Checks if the current page is a special WooCommerce page (Cart, Checkout, ...).
	 *
	 * @since 4.0.0
	 *
	 * @param  int    $postId The post ID.
	 * @return string         The type of page or an empty string if it isn't a WooCommerce page.
	 */
	public function isWooCommercePage( $postId = 0 ) {
		$postId                  = $postId ? (int) $postId : get_the_ID();
		$specialWooCommercePages = $this->getWooCommercePages();

		if ( in_array( $postId, $specialWooCommercePages, true ) ) {
			return array_search( $postId, $specialWooCommercePages, true );
		}

		return '';
	}

	/**
	 * Returns the WooCommerce pages.
	 *
	 * @since 4.7.3
	 *
	 * @return array An associative list of special WooCommerce pages.
	 */
	public function getWooCommercePages() {
		if ( ! $this->isWooCommerceActive() ) {
			$wooCommercePages = [];

			return $wooCommercePages;
		}

		$wooCommercePages = [
			'cart'      => (int) get_option( 'woocommerce_cart_page_id' ),
			'checkout'  => (int) get_option( 'woocommerce_checkout_page_id' ),
			'myAccount' => (int) get_option( 'woocommerce_myaccount_page_id' ),
			'terms'     => (int) get_option( 'woocommerce_terms_page_id' ),
		];

		return $wooCommercePages;
	}

	/**
	 * Checks whether the current page is a special WooCommerce page we shouldn't show our schema settings for.
	 *
	 * @since 4.1.6
	 *
	 * @param  int  $postId The post ID.
	 * @return bool         Whether the current page is a disallowed WooCommerce page.
	 */
	public function isWooCommercePageWithoutSchema( $postId = 0 ) {
		$page = $this->isWooCommercePage( $postId );
		if ( ! $page ) {
			return false;
		}

		$disallowedPages = [ 'cart', 'checkout', 'myAccount' ];

		return in_array( $page, $disallowedPages, true );
	}

	/**
	 * Checks whether the queried object is the WooCommerce shop page.
	 *
	 * @since 4.0.0
	 *
	 * @param  int  $id The post ID to check against (optional).
	 * @return bool     Whether the current page is the WooCommerce shop page.
	 */
	public function isWooCommerceShopPage( $id = 0 ) {
		if ( ! $this->isWooCommerceActive() ) {
			return false;
		}

		if ( ! is_admin() && ! aioseo()->helpers->isAjaxCronRestRequest() && function_exists( 'is_shop' ) ) {
			return is_shop();
		}

		// Prevent non-numeric id.
		$id = is_numeric( $id ) ? (int) $id : 0;

		// phpcs:disable HM.Security.ValidatedSanitizedInput, HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		$id = ! $id && ! empty( $_GET['post'] )
			? (int) sanitize_text_field( wp_unslash( $_GET['post'] ) )
			: $id;
		// phpcs:enable

		return $id && wc_get_page_id( 'shop' ) === $id;
	}

	/**
	 * Checks whether the queried object is the WooCommerce cart page.
	 *
	 * @since 4.1.3
	 *
	 * @param  int  $id The post ID to check against (optional).
	 * @return bool     Whether the current page is the WooCommerce cart page.
	 */
	public function isWooCommerceCartPage( $id = 0 ) {
		if ( ! $this->isWooCommerceActive() ) {
			return false;
		}

		if ( ! is_admin() && ! aioseo()->helpers->isAjaxCronRestRequest() && function_exists( 'is_cart' ) ) {
			return is_cart();
		}

		// phpcs:disable HM.Security.ValidatedSanitizedInput, HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		$id = ! $id && ! empty( $_GET['post'] )
			? (int) sanitize_text_field( wp_unslash( $_GET['post'] ) )
			: (int) $id;
		// phpcs:enable

		return $id && wc_get_page_id( 'cart' ) === $id;
	}

	/**
	 * Checks whether the queried object is the WooCommerce checkout page.
	 *
	 * @since 4.1.3
	 *
	 * @param  int  $id The post ID to check against (optional).
	 * @return bool     Whether the current page is the WooCommerce checkout page.
	 */
	public function isWooCommerceCheckoutPage( $id = 0 ) {
		if ( ! $this->isWooCommerceActive() ) {
			return false;
		}

		if ( ! is_admin() && ! aioseo()->helpers->isAjaxCronRestRequest() && function_exists( 'is_checkout' ) ) {
			return is_checkout();
		}

		// phpcs:disable HM.Security.ValidatedSanitizedInput, HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		$id = ! $id && ! empty( $_GET['post'] )
			? (int) sanitize_text_field( wp_unslash( $_GET['post'] ) )
			: (int) $id;
		// phpcs:enable

		return $id && wc_get_page_id( 'checkout' ) === $id;
	}

	/**
	 * Checks whether the queried object is the WooCommerce account page.
	 *
	 * @since 4.1.3
	 *
	 * @param  int  $id The post ID to check against (optional).
	 * @return bool     Whether the current page is the WooCommerce account page.
	 */
	public function isWooCommerceAccountPage( $id = 0 ) {
		if ( ! $this->isWooCommerceActive() ) {
			return false;
		}

		if ( ! is_admin() && ! aioseo()->helpers->isAjaxCronRestRequest() && function_exists( 'is_account_page' ) ) {
			return is_account_page();
		}

		// phpcs:disable HM.Security.ValidatedSanitizedInput, HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		$id = ! $id && ! empty( $_GET['post'] )
			? (int) sanitize_text_field( wp_unslash( $_GET['post'] ) )
			: (int) $id;
		// phpcs:enable

		return $id && wc_get_page_id( 'myaccount' ) === $id;
	}

	/**
	 * Checks whether the queried object is a WooCommerce product page.
	 *
	 * @since 4.5.5
	 *
	 * @return bool Whether the current page is a WooCommerce product page.
	 */
	public function isWooCommerceProductPage() {
		if (
			! $this->isWooCommerceActive() ||
			! function_exists( 'is_product' )
		) {
			return false;
		}

		return is_product();
	}

	/**
	 * Checks whether the queried object is a WooCommerce taxonomy page.
	 *
	 * @since 4.5.5
	 *
	 * @return bool Whether the current page is a WooCommerce taxonomy page.
	 */
	public function isWooCommerceTaxonomyPage() {
		if (
			! $this->isWooCommerceActive() ||
			! function_exists( 'is_product_taxonomy' )
		) {
			return false;
		}

		return is_product_taxonomy();
	}

	/**
	 * Internationalize.
	 *
	 * @since 4.0.0
	 *
	 * @param $in
	 * @return mixed|void
	 */
	public function internationalize( $in ) {
		if ( function_exists( 'langswitch_filter_langs_with_message' ) ) {
			$in = langswitch_filter_langs_with_message( $in );
		}

		if ( function_exists( 'polyglot_filter' ) ) {
			$in = polyglot_filter( $in );
		}

		if ( function_exists( 'qtrans_useCurrentLanguageIfNotFoundUseDefaultLanguage' ) ) {
			$in = qtrans_useCurrentLanguageIfNotFoundUseDefaultLanguage( $in );
		} elseif ( function_exists( 'ppqtrans_useCurrentLanguageIfNotFoundUseDefaultLanguage' ) ) {
			$in = ppqtrans_useCurrentLanguageIfNotFoundUseDefaultLanguage( $in );
		} elseif ( function_exists( 'qtranxf_useCurrentLanguageIfNotFoundUseDefaultLanguage' ) ) {
			$in = qtranxf_useCurrentLanguageIfNotFoundUseDefaultLanguage( $in );
		}

		return apply_filters( 'localization', $in );
	}

	/**
	 * Checks if WPML is active.
	 *
	 * @since 4.0.0
	 *
	 * @return bool True if it is, false if not.
	 */
	public function isWpmlActive() {
		return class_exists( 'SitePress' );
	}

	/**
	 * Checks if TranslatePress is active.
	 *
	 * @since 4.7.3
	 *
	 * @return bool True if it is, false if not.
	 */
	public function isTranslatePressActive() {
		return class_exists( 'TRP_Translate_Press' );
	}

	/**
	 * Localizes a given URL.
	 *
	 * This is required for compatibility with WPML.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $path The relative path of the URL.
	 * @return string $url  The filtered URL.
	 */
	public function localizedUrl( $path ) {
		$url = apply_filters( 'wpml_home_url', home_url( '/' ) );

		// Remove URL parameters.
		preg_match_all( '/\?[\s\S]+/', (string) $url, $matches );

		// Get the base URL.
		$url  = preg_replace( '/\?[\s\S]+/', '', (string) $url );
		$url  = trailingslashit( $url );
		$url .= preg_replace( '/\//', '', (string) $path, 1 );

		// Readd URL parameters.
		if ( $matches && $matches[0] ) {
			$url .= $matches[0][0];
		}

		return $url;
	}

	/**
	 * Checks whether BuddyPress is active.
	 *
	 * @since 4.0.0
	 *
	 * @return boolean
	 */
	public function isBuddyPressActive() {
		return class_exists( 'BuddyPress' );
	}

	/**
	 * Checks whether the queried object is a buddy press user page.
	 *
	 * @since 4.0.0
	 *
	 * @return boolean
	 */
	public function isBuddyPressUser() {
		return $this->isBuddyPressActive() && function_exists( 'bp_is_user' ) && bp_is_user();
	}

	/**
	 * Returns if the page is a BuddyPress page (Activity, Members, Groups).
	 *
	 * @since 4.0.0
	 *
	 * @param  int  $postId The post ID.
	 * @return bool         If the page is a BuddyPress page or not.
	 */
	public function isBuddyPressPage( $postId = 0 ) {
		$bpPageIds = $this->getBuddyPressPageIds();

		return in_array( $postId, $bpPageIds, true );
	}

	/**
	 * Returns the BuddyPress pages.
	 *
	 * @since 4.7.3
	 *
	 * @return array A list of BuddyPress page IDs.
	 */
	public function getBuddyPressPageIds() {
		if ( ! $this->isBuddyPressActive() ) {
			return [];
		}

		static $bpPageIds = null;
		if ( null === $bpPageIds ) {
			$bpPageIds = (array) get_option( 'bp-pages' );
			$bpPageIds = array_map( 'intval', $bpPageIds );
		}

		return $bpPageIds;
	}

	/**
	 * Returns ACF fields as an array of meta keys and values.
	 *
	 * @since 4.0.6
	 *
	 * @param  \WP_Post|int $post  The post.
	 * @param  array        $types A whitelist of ACF field types.
	 * @return array               An array of meta keys and values.
	 */
	public function getAcfContent( $post = null, $types = [] ) {
		$post = ( $post && is_object( $post ) ) ? $post : $this->getPost( $post );

		if ( ! class_exists( 'ACF' ) || ! function_exists( 'get_field_objects' ) ) {
			return [];
		}

		if ( defined( 'ACF_VERSION' ) && version_compare( ACF_VERSION, '5.7.0', '<' ) ) {
			return [];
		}

		// Set defaults.
		$allowedTypes = [
			'text',
			'textarea',
			'email',
			'url',
			'wysiwyg',
			'image',
			'gallery',
			'link',
		];

		$types        = wp_parse_args( $types, $allowedTypes );
		$fieldObjects = get_field_objects( $post->ID );

		if ( empty( $fieldObjects ) ) {
			return [];
		}

		// Filter out any fields that are not in our allowed types.
		$fields = array_filter( $fieldObjects, function( $object ) use ( $types ) {
			return ! empty( $object['value'] ) && in_array( $object['type'], $types, true );
		});

		// Create an array with the field names and values with added HTML markup.
		$acfFields = [];
		foreach ( $fields as $field ) {
			switch ( $field['type'] ) {
				case 'url':
					$value = make_clickable( $field['value'] ?? '' );
					break;
				case 'image':
					// Image format options are array, URL (string), id (int).
					$imageUrl = is_array( $field['value'] ) ? $field['value']['url'] : $field['value'];
					$imageUrl = is_numeric( $imageUrl ) ? wp_get_attachment_image_url( $imageUrl ) : $imageUrl;

					$value = "<img src='$imageUrl' />"; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
					break;
				case 'gallery':
					$imageUrl = $field['value'];
					// The value of a gallery field should always be an array.
					if ( is_array( $imageUrl ) ) {
						$imageUrl = current( $imageUrl );
					}

					// Image array format.
					if ( is_array( $imageUrl ) && ! empty( $imageUrl['url'] ) ) {
						$imageUrl = $imageUrl['url'];
					}

					// Image ID format.
					$imageUrl = is_numeric( $imageUrl ) ? wp_get_attachment_image_url( $imageUrl ) : $imageUrl;

					$value = ! empty( $imageUrl ) ? "<img src='{$imageUrl}' />" : ''; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
					break;
				case 'link':
					$value = make_clickable( $field['value']['url'] ?? $field['value'] ?? '' );
					break;
				default:
					$value = $field['value'];
					break;
			}

			if ( $value ) {
				$acfFields[ $field['name'] ] = $value;
			}
		}

		return $acfFields;
	}

	/**
	 * Retrieves the ACF Flexible Content field value for a given post.
	 *
	 * @since 4.7.9
	 *
	 * @param  string     $name The name of the field.
	 * @param  int|object $post The post ID or object.
	 * @return string           The field value.
	 */
	public function getAcfFlexibleContentField( $name, $post ) {
		$output = '';
		if ( ! function_exists( 'acf_get_raw_field' ) || ! function_exists( 'acf_get_field' ) ) {
			return $output;
		}

		$parentTrace = [];
		$field       = acf_get_raw_field( $name ) ?? [];
		while ( ! empty( $field['parent'] ) && ! empty( $field['parent_layout'] ) ) {
			$parentField   = acf_get_field( $field['parent'] );
			$parentTrace[] = $parentField['name'] ?? '';
			$field         = $parentField;
		}

		$parentTrace = array_filter( $parentTrace );
		if ( empty( $parentTrace ) ) {
			return $output;
		}

		$parentTrace        = array_reverse( $parentTrace );
		$parentName         = array_shift( $parentTrace );
		$highestParentField = get_field( $parentName, $post );

		for ( $i = 0; $i <= count( $parentTrace ); $i++ ) {
			$values = array_filter( array_column( $highestParentField, $name ), 'is_scalar' );
			if ( $values ) {
				return implode( ' ', $values );
			}

			$highestParentField = $highestParentField[0] ?? '';
			if (
				! is_array( $highestParentField ) ||
				! isset( $parentTrace[ $i ] )
			) {
				break;
			}

			$highestParentField = $highestParentField[ $parentTrace[ $i ] ];
		}

		return $output;
	}

	/**
	 * Checks whether the Smash Balloon Custom Facebook Feed plugin is active.
	 *
	 * @since 4.2.0
	 *
	 * @return bool Whether the SB CFF plugin is active.
	 */
	public function isSbCustomFacebookFeedActive() {
		static $isActive = null;
		if ( null !== $isActive ) {
			return $isActive;
		}

		$isActive = defined( 'CFFVER' ) || is_plugin_active( 'custom-facebook-feed/custom-facebook-feed.php' );

		return $isActive;
	}

	/**
	 * Returns the access token for Facebook from Smash Balloon if there is one.
	 *
	 * @since 4.2.0
	 *
	 * @return string|false The access token or false if there is none.
	 */
	public function getSbAccessToken() {
		static $accessToken = null;
		if ( null !== $accessToken ) {
			return $accessToken;
		}

		if ( ! $this->isSbCustomFacebookFeedActive() ) {
			$accessToken = false;

			return $accessToken;
		}

		$oembedTokenData = get_option( 'cff_oembed_token', [] );
		if ( ! $oembedTokenData || empty( $oembedTokenData['access_token'] ) ) {
			$accessToken = false;

			return $accessToken;
		}

		$sbFacebookDataEncryptionInstance = new \CustomFacebookFeed\SB_Facebook_Data_Encryption();
		$accessToken                      = $sbFacebookDataEncryptionInstance->maybe_decrypt( $oembedTokenData['access_token'] );

		return $accessToken;
	}

	/**
	* Returns the homepage URL for a language code.
	*
	* @since 4.2.1
	*
	* @param  string|int $identifier The language code or the post id to return the url.
	* @return string                 The home URL.
	*/
	public function wpmlHomeUrl( $identifier ) {
		foreach ( $this->wpmlHomePages() as $langCode => $wpmlHomePage ) {
			if (
				( is_string( $identifier ) && $langCode === $identifier ) ||
				( is_numeric( $identifier ) && $wpmlHomePage['id'] === $identifier )
			) {
				return $wpmlHomePage['url'];
			}
		}

		return '';
	}

	/**
	 * Returns the homepage IDs.
	 *
	 * @since 4.2.1
	 *
	 * @return array An array of home page ids.
	 */
	public function wpmlHomePages() {
		global $sitepress;
		static $homePages = [];

		if ( ! $this->isWpmlActive() || empty( $sitepress ) || ! method_exists( $sitepress, 'language_url' ) ) {
			return $homePages;
		}

		if ( empty( $homePages ) ) {
			$languages  = apply_filters( 'wpml_active_languages', [] );
			$homePageId = (int) get_option( 'page_on_front' );
			foreach ( $languages as $language ) {
				$homePages[ $language['code'] ] = [
					'id'  => apply_filters( 'wpml_object_id', $homePageId, 'page', false, $language['code'] ),
					'url' => $sitepress->language_url( $language['code'] )
				];
			}
		}

		return $homePages;
	}

	/**
	 * Returns if the post id os a WPML home page.
	 *
	 * @since 4.2.1
	 *
	 * @param  int  $postId The post ID.
	 * @return bool         Is the post id a home page.
	 */
	public function wpmlIsHomePage( $postId ) {
		foreach ( $this->wpmlHomePages() as $wpmlHomePage ) {
			if ( $wpmlHomePage['id'] === $postId ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Returns the WPML url format.
	 *
	 * @since 4.2.8
	 *
	 * @return string The format.
	 */
	public function getWpmlUrlFormat() {
		global $sitepress;

		if (
			! $this->isWpmlActive() ||
			empty( $sitepress ) ||
			! method_exists( $sitepress, 'get_setting' )
		) {
			return '';
		}

		switch ( $sitepress->get_setting( 'language_negotiation_type' ) ) {
			case WPML_LANGUAGE_NEGOTIATION_TYPE_DIRECTORY:
			case 1:
				return 'directory';
			case WPML_LANGUAGE_NEGOTIATION_TYPE_DOMAIN:
			case 2:
				return 'domain';
			case WPML_LANGUAGE_NEGOTIATION_TYPE_PARAMETER:
			case 3:
				return 'parameter';
			default:
				return '';
		}
	}

	/**
	 * Returns the TranslatePress slugs code and slug.
	 *
	 * @since 4.7.3
	 *
	 * @return array The slugs.
	 */
	public function getTranslatePressUrlSlugs() {
		if ( ! $this->isTranslatePressActive() ) {
			return [];
		}

		$settings = maybe_unserialize( get_option( 'trp_settings', [] ) );

		return isset( $settings['url-slugs'] ) ? $settings['url-slugs'] : [];
	}

	/**
	 * Checks whether the WooCommerce Follow Up Emails plugin is active.
	 *
	 * @since 4.2.2
	 *
	 * @return bool Whether the plugin is active.
	 */
	public function isWooCommerceFollowupEmailsActive() {
		$isActive = defined( 'FUE_VERSION' ) || is_plugin_active( 'woocommerce-follow-up-emails/woocommerce-follow-up-emails.php' );

		return $isActive;
	}

	/**
	 * Checks if the current page is an AMP page.
	 * This function is only effective if called after the `wp` action.
	 *
	 * @since 4.2.3
	 *
	 * @param  string $pluginName The name of the AMP plugin to check for (optional).
	 * @return bool               Whether the current page is an AMP page.
	 */
	public function isAmpPage( $pluginName = '' ) {
		// Official AMP plugin.
		if ( 'amp' === $pluginName ) {
			// If we're checking for the AMP page plugin specifically, return early if it's not active.
			// Otherwise, we'll return true if AMP for WP is enabled because the helper method doesn't distinguish between the two.
			if ( ! defined( 'AMP__VERSION' ) ) {
				return false;
			}

			$options = get_option( 'amp-options' );
			if ( ! empty( $options['theme_support'] ) && 'standard' === strtolower( $options['theme_support'] ) ) {
				return true;
			}
		}

		return $this->isAmpPageHelper();
	}

	/**
	 * Helper function for {@see isAmpPage()}.
	 * Checks if the current page is an AMP page.
	 *
	 * @since 4.2.4
	 *
	 * @return bool Whether the current page is an AMP page.
	 */
	private function isAmpPageHelper() {
		// First check for the existence of any AMP plugin functions. Bail early if none are found, and prevent false positives.
		if (
			! function_exists( 'amp_is_request' ) &&
			! function_exists( 'is_amp_endpoint' ) &&
			! function_exists( 'ampforwp_is_amp_endpoint' ) &&
			! function_exists( 'is_amp_wp' )
		) {
			// If none of the AMP plugin functions are found, return false and allow compatibility with custom implementations.
			return apply_filters( 'aioseo_is_amp_page', false );
		}

		// AMP plugin requires the `wp` action to be called to function properly, otherwise, it will throw warnings.

		if ( did_action( 'wp' ) ) {
			// Check for the "AMP" plugin.
			if ( function_exists( 'amp_is_request' ) ) {
				return (bool) amp_is_request();
			}

			// Check for the "AMP" plugin (`is_amp_endpoint()` is deprecated).
			if ( function_exists( 'is_amp_endpoint' ) ) {
				return (bool) is_amp_endpoint();
			}

			// Check for the "AMP for WP – Accelerated Mobile Pages" plugin.
			if ( function_exists( 'ampforwp_is_amp_endpoint' ) ) {
				return (bool) ampforwp_is_amp_endpoint();
			}

			// Check for the "AMP WP" plugin.
			if ( function_exists( 'is_amp_wp' ) ) {
				return (bool) is_amp_wp();
			}
		}

		return false;
	}

	/**
	 * If we're in a LearnPress lesson page, return the lesson ID.
	 *
	 * @since 4.3.1
	 *
	 * @return int|false
	 */
	public function getLearnPressLesson() {
		// phpcs:disable Squiz.NamingConventions.ValidVariableName
		global $lp_course_item;
		if ( $lp_course_item && method_exists( $lp_course_item, 'get_id' ) ) {
			return $lp_course_item->get_id();
		}
		// phpcs:enable Squiz.NamingConventions.ValidVariableName

		return false;
	}

	/**
	 * Set a flag to indicate Divi whether it is processing internal content or not.
	 *
	 * @since 4.4.3
	 *
	 * @param  null|bool $flag The flag value.
	 * @return null|bool       The previous flag value to reset it later.
	 */
	public function setDiviInternalRendering( $flag ) {
		if ( ! defined( 'ET_BUILDER_VERSION' ) ) {
			return null;
		}
		// phpcs:disable Squiz.NamingConventions.ValidVariableName
		global $et_pb_rendering_column_content;

		$originalValue                  = $et_pb_rendering_column_content;
		$et_pb_rendering_column_content = $flag;
		// phpcs:enable Squiz.NamingConventions.ValidVariableName

		return $originalValue;
	}

	/**
	 * Checks whether the current request is being done by a crawler from Yandex.
	 *
	 * @since 4.4.0
	 *
	 * @return bool Whether the current request is being done by a crawler from Yandex.
	 */
	public function isYandexUserAgent() {
		if ( ! isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
			return false;
		}

		return preg_match( '#.*Yandex.*#', (string) sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) );
	}

	/**
	 * Checks whether the taxonomy is a WooCommerce product attribute.
	 *
	 * @since 4.7.8
	 *
	 * @param  mixed $taxonomy The taxonomy.
	 * @return bool            Whether the taxonomy is a WooCommerce product attribute.
	 */
	public function isWooCommerceProductAttribute( $taxonomy ) {
		$name = is_object( $taxonomy )
			? $taxonomy->name
			: (
				is_array( $taxonomy )
					? $taxonomy['name']
					: $taxonomy
			);

		return ! empty( $name ) && 'pa_' === substr( $name, 0, 3 );
	}

	/**
	 * Returns whether a plugin is active or not using abstraction.
	 *
	 * @since 4.8.1
	 *
	 * @param  string $slug The plugin slug.
	 * @return bool         Whether the plugin is active.
	 */
	public function isPluginActive( $slug ) {
		$mapped = [
			'buddypress' => 'buddypress/bp-loader.php',
			'bbpress'    => 'bbpress/bbpress.php',
			'weglot'     => 'weglot/weglot.php'
		];

		static $output = [];
		if ( isset( $output[ $slug ] ) ) {
			return $output[ $slug ];
		}

		$mapped[ $slug ] = $mapped[ $slug ] ?? $slug;
		$output[ $slug ] = function_exists( 'is_plugin_active' ) && is_plugin_active( $mapped[ $slug ] );

		return $output[ $slug ];
	}
}Common/Traits/Helpers/Url.php000064400000022265151536241210012126 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

/**
 * Contains URL helper methods.
 *
 * @since 4.2.5
 */
trait Url {
	/**
	 * Removes a query string parameter from a URL.
	 *
	 * @since 4.2.5
	 *
	 * @param  string $url        The url.
	 * @param  array  $parameters The parameter keys to remove.
	 * @return string             The url without the parameters removed.
	 */
	public function urlRemoveQueryParameter( $url, $parameters ) {
		$url = wp_parse_url( $url );
		if ( ! empty( $url['query'] ) ) {
			// Take the query string apart.
			parse_str( $url['query'], $queryStringArray );

			// Remove parameters.
			foreach ( $parameters as $parameter ) {
				if ( isset( $queryStringArray[ $parameter ] ) ) {
					unset( $queryStringArray[ $parameter ] );
				}
			}

			// Rebuild the query string.
			$url['query'] = build_query( $queryStringArray );

			// Rebuild the URL from parse_url.
			$url = $this->buildUrl( $url );
		}

		return $url;
	}

	/**
	 * Builds a URL from a parse_url array.
	 *
	 * @since 4.2.5
	 *
	 * @param  array  $params  The params array.
	 * @param  array  $include The keys to include [scheme, user, pass, host, port, path, query, fragment].
	 * @param  array  $exclude The keys to exclude [scheme, user, pass, host, port, path, query, fragment].
	 * @return string          The built url.
	 */
	public function buildUrl( $params, $include = [], $exclude = [] ) {
		if ( ! is_array( $params ) ) {
			return $params;
		}

		if ( ! empty( $include ) ) {
			foreach ( array_keys( $params ) as $includeKey ) {
				if ( ! in_array( $includeKey, $include, true ) ) {
					unset( $params[ $includeKey ] );
				}
			}
		}

		if ( ! empty( $exclude ) ) {
			foreach ( array_keys( $params ) as $excludeKey ) {
				if ( in_array( $excludeKey, $exclude, true ) ) {
					unset( $params[ $excludeKey ] );
				}
			}
		}

		$url = '';
		if ( ! empty( $params['scheme'] ) ) {
			$url .= $params['scheme'] . '://';
		}
		if ( ! empty( $params['user'] ) ) {
			$url .= $params['user'];

			if ( isset( $params['pass'] ) ) {
				$url .= ':' . $params['pass'];
			}

			$url .= '@';
		}

		if ( ! empty( $params['host'] ) ) {
			$url .= $params['host'];
		}

		if ( ! empty( $params['port'] ) ) {
			$url .= ':' . $params['port'];
		}

		if ( ! empty( $params['path'] ) ) {
			$url .= $params['path'];
		}

		if ( ! empty( $params['query'] ) ) {
			$url .= '?' . $params['query'];
		}

		if ( ! empty( $params['fragment'] ) ) {
			$url .= '#' . $params['fragment'];
		}

		return $url;
	}

	/**
	 * Checks if a URL is considered a local one.
	 *
	 * @since 4.5.9
	 *
	 * @param  string $url The URL.
	 * @return bool        Whether the URL is a local one or not.
	 */
	public function isLocalUrl( $url ) {
		$domain = wp_parse_url( $url, PHP_URL_HOST );
		if ( empty( $domain ) ) {
			return false;
		}

		if (
			false !== ip2long( $domain ) &&
			! filter_var( $domain, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE )
		) {
			return true;
		}

		if ( 'localhost' === $domain ) {
			return true;
		}

		if ( ! $this->isValidDomain( $domain ) ) {
			return true;
		}

		$tldsToCheck = [
			'.local',
			'.test',
		];

		foreach ( $tldsToCheck as $tld ) {
			if ( false !== strpos( $this->getTld( $domain ), $tld ) ) {
				return true;
			}
		}

		if ( substr_count( $domain, '.' ) > 1 ) {
			$subdomainsToCheck = [
				'dev',
				'development',
				'staging',
				'stage',
				'test',
				'staging*',
				'*staging',
				'dev*',
				'*dev',
				'test*',
				'*test'
			];

			foreach ( $subdomainsToCheck as $subdomain ) {
				foreach ( $this->getSubdomains( $domain ) as $sd ) {

					$subdomain = str_replace( '.', '(.)', $subdomain );
					$subdomain = str_replace( [ '*', '(.)' ], '(.*)', $subdomain );

					if ( preg_match( '/^(' . $subdomain . ')$/', (string) $sd ) ) {
						return true;
					}
				}
			}
		}

		return false;
	}

	/**
	 * Checks if a domain is valid.
	 *
	 * @since 4.5.9
	 *
	 * @param  string $domain The domain.
	 * @return bool           Whether the domain is valid or not.
	 */
	private function isValidDomain( $domain ) {
		// In case there are unicode characters, convert it into
		// IDNA ASCII URLs
		if ( function_exists( 'idn_to_ascii' ) ) {
			$domain = idn_to_ascii( $domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 );
		}

		if ( ! $domain ) {
			return false;
		}

		$domain = preg_replace( '/^\*\.+/', '', (string) $domain );

		return preg_match( '/^(?!\-)(?:[a-z\d\-]{0,62}[a-z\d]\.){1,126}(?!\d+)[a-z\d]{1,63}$/i', (string) $domain );
	}

	/**
	 * Checks if a domain is valid and optionally contains paths at the end.
	 *
	 * @since 4.7.7
	 *
	 * @param  string $domain The domain.
	 * @return bool           Whether the domain is valid or not.
	 */
	private function isDomainWithPaths( $domain ) {
		// In case there are unicode characters, convert it into IDNA ASCII URLs.
		if ( function_exists( 'idn_to_ascii' ) ) {
			$domain = idn_to_ascii( $domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 );
		}

		if ( ! $domain ) {
			return false;
		}

		$domain = preg_replace( '/^\*\.+/', '', $domain );

		return preg_match( '/^(?!\-)(?:[a-z\d\-]{0,62}[a-z\d]\.){1,126}(?!\d+)[a-z\d]{1,63}(\/[a-z\d\-\/]*)?$/i', $domain );
	}

	/**
	 * Returns a single string of all subdomains associated with this domain.
	 * Example 1: www
	 * Example 2: ww2.www
	 *
	 * @since 4.5.9
	 *
	 * @return array The subdomains associated with this domain.
	 */
	public function getSubdomains( $domain ) {
		// If we can't find a TLD, we won't be able to parse a subdomain.
		if ( empty( $this->getTld( $domain ) ) ) {
			return [];
		}

		// Return any subdomains as an array.
		return array_filter( explode( '.', rtrim( strstr( $domain, $this->getTld( $domain ), true ), '.' ) ) );
	}

	/**
	 * Returns the TLD associated with the given domain.
	 *
	 * @since 4.5.9
	 *
	 * @param  string $domain The domain.
	 * @return string         The TLD.
	 */
	public function getTld( $domain ) {
		if ( preg_match( '/(?P<tld>[a-z0-9][a-z0-9\-]{1,63}\.[a-z\.]{2,6})$/i', (string) $domain, $matches ) ) {
			return $matches['tld'];
		}

		return $domain;
	}

	/**
	 * Returns a decoded URL string.
	 *
	 * @since 4.6.7
	 *
	 * @param  string $url The URL string.
	 * @return string      The decoded URL.
	 */
	public function decodeUrl( $url ) {
		// Ensure input is a string to prevent errors.
		if ( ! is_string( $url ) ) {
			return $url;
		}

		// Set a reasonable iteration limit to prevent infinite loops.
		$maxIterations = 10;
		$iterations    = 0;

		$decodedUrl = rawurldecode( $url );
		while ( $decodedUrl !== $url && $iterations < $maxIterations ) {
			$url        = $decodedUrl;
			$decodedUrl = rawurldecode( $url );
			$iterations++;
		}

		return $decodedUrl;
	}

	/**
	 * Redirects to a specific URL.
	 *
	 * @since 4.8.0
	 *
	 * @param string $url    The URL to redirect to.
	 * @param int    $status The status code to use.
	 * @param string $reason The reason for redirecting.
	 *
	 * @return void
	 */
	public function redirect( $url, $status = 301, $reason = '' ) {
		$redirectBy = 'AIOSEO';
		if ( ! empty( $reason ) ) {
			$redirectBy .= ': ' . $reason;
		}

		wp_safe_redirect( $url, $status, $redirectBy );
		exit;
	}

	/**
	 * Checks if the given URL is external.
	 *
	 * @since 4.8.3
	 *
	 * @param  string $url The URL to check.
	 * @return bool        Whether the URL is external or not.
	 */
	public function isExternalUrl( $url ) {
		$parsedUrl = wp_parse_url( $url );
		if ( ! $parsedUrl ) {
			return false;
		}

		static $parsedSiteUrl = null;
		if ( ! $parsedSiteUrl ) {
			$parsedSiteUrl = wp_parse_url( site_url() );
		}

		return $parsedSiteUrl['host'] !== $parsedUrl['host'];
	}

	/**
	 * Checks if the given URL is relative.
	 *
	 * @since 4.8.3
	 *
	 * @param  string $url The URL to check.
	 * @return bool        Whether the URL is relative or not.
	 */
	public function isRelativeUrl( $url ) {
		$parsedUrl = wp_parse_url( $url );
		if ( ! $parsedUrl ) {
			return false;
		}

		return empty( $parsedUrl['scheme'] ) && empty( $parsedUrl['host'] );
	}

	/**
	 * Makes the given URL relative.
	 *
	 * @since 4.8.3
	 *
	 * @param  string $url The URL to make relative.
	 * @return string      The relative URL.
	 */
	public function makeUrlRelative( $url ) {
		$parsedUrl = wp_parse_url( $url );
		if ( ! $parsedUrl ) {
			return $url;
		}

		static $parsedSiteUrl = null;
		if ( ! $parsedSiteUrl ) {
			$parsedSiteUrl = wp_parse_url( site_url() );
		}

		if ( $parsedSiteUrl['host'] !== $parsedUrl['host'] ) {
			return $url;
		}

		return ! empty( $parsedUrl['path'] ) ? $parsedUrl['path'] : $url;
	}

	/**
	 * Formats a given URL as an absolute URL if it is relative.
	 *
	 * @since   4.0.0
	 * @version 4.8.3 Moved from WpUri trait to Url trait.
	 *
	 * @param  string $url The URL.
	 * @return string      The absolute URL.
	 */
	public function makeUrlAbsolute( $url ) {
		if ( 0 !== strpos( $url, 'http' ) && '/' !== $url ) {
			$url = $this->sanitizeDomain( $url );
			if ( $this->isDomainWithPaths( $url ) ) {
				$scheme = wp_parse_url( site_url(), PHP_URL_SCHEME );
				$url    = $scheme . '://' . $url;
			} elseif ( 0 === strpos( $url, '//' ) ) {
				$scheme = wp_parse_url( site_url(), PHP_URL_SCHEME );
				$url    = $scheme . ':' . $url;
			} else {
				$url = site_url( $url );
			}
		}

		return $url;
	}
}Common/Traits/Helpers/Vue.php000064400000066371151536241210012131 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

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

/**
 * Contains all Vue related helper methods.
 *
 * @since 4.1.4
 */
trait Vue {
	/**
	 * Holds the data for Vue.
	 *
	 * @since 4.4.9
	 *
	 * @var array
	 */
	private $data = [];

	/**
	 * Optional arguments for setting the data.
	 *
	 * @since 4.4.9
	 *
	 * @var array
	 */
	private $args = [];

	/**
	 * Holds the cached data.
	 *
	 * @since 4.5.1
	 *
	 * @var array
	 */
	private $cache = [];

	/**
	 * Returns the data for Vue.
	 *
	 * @since   4.0.0
	 * @version 4.4.9
	 *
	 * @param  string $page         The current page.
	 * @param  int    $staticPostId Data for a specific post.
	 * @param  string $integration  Data for integration (builder).
	 * @return array                The data.
	 */
	public function getVueData( $page = null, $staticPostId = null, $integration = null ) {
		$this->args = compact( 'page', 'staticPostId', 'integration' );
		$hash       = md5( implode( '', array_map( 'strval', $this->args ) ) );
		if ( isset( $this->cache[ $hash ] ) ) {
			return $this->cache[ $hash ];
		}

		// Clear the data so we start fresh.
		$this->data = [];

		$this->setInitialData();
		$this->setMultisiteData();
		$this->setPostData();
		$this->setDashboardData();
		$this->setSearchStatisticsData();
		$this->setSitemapsData();
		$this->setSetupWizardData();
		$this->setSearchAppearanceData();
		$this->setSocialNetworksData();
		$this->setSeoRevisionsData();
		$this->setToolsOrSettingsData();
		$this->setPageBuilderData();
		$this->setWritingAssistantData();
		$this->setBreadcrumbsData();
		$this->setSeoAnalyzerData();

		$this->cache[ $hash ] = $this->data;

		return $this->cache[ $hash ];
	}

	/**
	 * Set Vue initial data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setInitialData() {
		$screen           = aioseo()->helpers->getCurrentScreen();
		$isStaticHomePage = 'page' === get_option( 'show_on_front' );
		$staticHomePage   = intval( get_option( 'page_on_front' ) );

		$this->data = [
			'page'               => $this->args['page'],
			'screen'             => [
				'base'        => isset( $screen->base ) ? $screen->base : '',
				'postType'    => isset( $screen->post_type ) ? $screen->post_type : '',
				'blockEditor' => isset( $screen->is_block_editor ) ? $screen->is_block_editor : false,
				'new'         => isset( $screen->action ) && 'add' === $screen->action
			],
			'internalOptions'    => aioseo()->internalOptions->all(),
			'options'            => aioseo()->options->all(),
			'dynamicOptions'     => aioseo()->dynamicOptions->all(),
			'deprecatedOptions'  => aioseo()->internalOptions->getAllDeprecatedOptions( true ),
			'settings'           => aioseo()->settings->all(),
			'additional_scripts' => apply_filters( 'aioseo_vue_additional_scripts_enabled', true ),
			'tags'               => aioseo()->tags->all( true ),
			'nonce'              => wp_create_nonce( 'wp_rest' ),
			'urls'               => [
				'domain'            => $this->getSiteDomain(),
				'mainSiteUrl'       => $this->getSiteUrl(),
				'siteLogo'          => aioseo()->helpers->getSiteLogoUrl(),
				'home'              => home_url(),
				'restUrl'           => aioseo()->helpers->getRestUrl(),
				'editScreen'        => admin_url( 'edit.php' ),
				'publicPath'        => aioseo()->core->assets->normalizeAssetsHost( plugin_dir_url( AIOSEO_FILE ) ),
				'assetsPath'        => aioseo()->core->assets->getAssetsPath(),
				'generalSitemapUrl' => aioseo()->sitemap->helpers->getUrl( 'general' ),
				'rssSitemapUrl'     => aioseo()->sitemap->helpers->getUrl( 'rss' ),
				'llmsUrl'           => aioseo()->llms->getUrl(),
				'robotsTxtUrl'      => $this->getSiteUrl() . '/robots.txt',
				'marketingSiteUrl'  => $this->getMarketingSiteUrl(),
				'upgradeUrl'        => apply_filters( 'aioseo_upgrade_link', AIOSEO_MARKETING_URL ),
				'staticHomePage'    => 'page' === get_option( 'show_on_front' ) ? get_edit_post_link( get_option( 'page_on_front' ), 'url' ) : null,
				'feeds'             => [
					'rdf'            => get_bloginfo( 'rdf_url' ),
					'rss'            => get_bloginfo( 'rss_url' ),
					'atom'           => get_bloginfo( 'atom_url' ),
					'global'         => get_bloginfo( 'rss2_url' ),
					'globalComments' => get_bloginfo( 'comments_rss2_url' ),
					'staticBlogPage' => $this->getBlogPageId() ? trailingslashit( get_permalink( $this->getBlogPageId() ) ) . 'feed' : ''
				],
				'connect'           => add_query_arg( [
					'siteurl'  => site_url(),
					'homeurl'  => home_url(),
					'redirect' => rawurldecode( base64_encode( admin_url( 'index.php?page=aioseo-connect' ) ) )
				], defined( 'AIOSEO_CONNECT_URL' ) ? AIOSEO_CONNECT_URL : 'https://connect.aioseo.com' ),
				'aio'               => [
					'about'            => is_network_admin() ? network_admin_url( 'admin.php?page=aioseo-about' ) : admin_url( 'admin.php?page=aioseo-about' ),
					'dashboard'        => admin_url( 'admin.php?page=aioseo' ),
					'featureManager'   => admin_url( 'admin.php?page=aioseo-feature-manager' ),
					'linkAssistant'    => admin_url( 'admin.php?page=aioseo-link-assistant' ),
					'localSeo'         => admin_url( 'admin.php?page=aioseo-local-seo' ),
					'monsterinsights'  => admin_url( 'admin.php?page=aioseo-monsterinsights' ),
					'redirects'        => admin_url( 'admin.php?page=aioseo-redirects' ),
					'searchAppearance' => admin_url( 'admin.php?page=aioseo-search-appearance' ),
					'searchStatistics' => admin_url( 'admin.php?page=aioseo-search-statistics' ),
					'seoAnalysis'      => admin_url( 'admin.php?page=aioseo-seo-analysis' ),
					'settings'         => admin_url( 'admin.php?page=aioseo-settings' ),
					'sitemaps'         => admin_url( 'admin.php?page=aioseo-sitemaps' ),
					'socialNetworks'   => admin_url( 'admin.php?page=aioseo-social-networks' ),
					'tools'            => admin_url( 'admin.php?page=aioseo-tools' ),
					'wizard'           => admin_url( 'index.php?page=aioseo-setup-wizard' ),
					'networkSettings'  => is_network_admin() ? network_admin_url( 'admin.php?page=aioseo-settings' ) : '',
					'seoRevisions'     => admin_url( 'admin.php?page=aioseo-seo-revisions' ),
				],
				'admin'             => [
					'widgets'          => admin_url( 'widgets.php' ),
					'optionsReading'   => admin_url( 'options-reading.php' ),
					'scheduledActions' => admin_url( '/tools.php?page=action-scheduler&status=pending&s=aioseo' ),
					'generalSettings'  => admin_url( 'options-general.php' )
				],
				'truSeoWorker'      => aioseo()->core->assets->jsUrl( 'src/app/tru-seo/analyzer/main.js' )
			],
			'backups'            => [],
			'importers'          => [],
			'data'               => [
				'server'                => aioseo()->helpers->getServerName(),
				'robots'                => [
					'defaultRules'      => [],
					'hasPhysicalRobots' => null,
					'rewriteExists'     => null,
					'sitemapUrls'       => []
				],
				'status'                => [],
				'htaccess'              => '',
				'isMultisite'           => is_multisite(),
				'isNetworkAdmin'        => is_network_admin(),
				'currentBlogId'         => get_current_blog_id(),
				'mainSite'              => is_main_site(),
				'subdomain'             => $this->isSubdomain(),
				'isBBPressActive'       => class_exists( 'bbPress' ),
				'isClassicEditorActive' => $this->isClassicEditorActive(),
				'isWooCommerceActive'   => $this->isWooCommerceActive(),
				'staticHomePage'        => $isStaticHomePage ? $staticHomePage : false,
				'staticBlogPage'        => $this->getBlogPageId(),
				'staticBlogPageTitle'   => get_the_title( $this->getBlogPageId() ),
				'isDev'                 => $this->isDev(),
				'isLocal'               => $this->isLocalUrl( site_url() ),
				'isSsl'                 => is_ssl(),
				'hasUrlTrailingSlash'   => '/' === user_trailingslashit( '' ),
				'permalinkStructure'    => get_option( 'permalink_structure' ),
				'usingPermalinks'       => aioseo()->helpers->usingPermalinks(),
				'dateFormat'            => get_option( 'date_format' ),
				'timeFormat'            => get_option( 'time_format' ),
				'siteName'              => aioseo()->helpers->getWebsiteName(),
				'adminEmail'            => get_bloginfo( 'admin_email' ),
				'blocks'                => [
					'toc' => [
						'hashPrefix' => apply_filters( 'aioseo_toc_hash_prefix', 'aioseo-' )
					]
				]
			],
			'user'               => [
				'canManage'      => aioseo()->access->canManage(),
				'capabilities'   => aioseo()->access->getAllCapabilities(),
				'customRoles'    => $this->getCustomRoles(),
				'data'           => wp_get_current_user(),
				'locale'         => function_exists( 'get_user_locale' ) ? get_user_locale() : get_locale(),
				'roles'          => $this->getUserRoles(),
				'unfilteredHtml' => current_user_can( 'unfiltered_html' )
			],
			'plugins'            => $this->getPluginData(),
			'postData'           => [
				'postTypes'    => array_values( $this->getPublicPostTypes( false, false, true ) ),
				'taxonomies'   => array_values( $this->getPublicTaxonomies( false, true ) ),
				'archives'     => array_values( $this->getPublicPostTypes( false, true, true ) ),
				'postStatuses' => array_values( $this->getPublicPostStatuses() )
			],
			'notifications'      => array_merge( Models\Notification::getNotifications( true ), [
				'force' => $this->showNotificationsDrawer()
			] ),
			'addons'             => aioseo()->addons->getAddons(),
			'features'           => aioseo()->features->getFeatures(),
			'version'            => AIOSEO_VERSION,
			'wpVersion'          => get_bloginfo( 'version' ),
			'phpVersion'         => PHP_VERSION,
			'helpPanel'          => aioseo()->help->getDocs(),
			'scheduledActions'   => [
				'sitemaps' => []
			],
			'integration'        => $this->args['integration'],
			'theme'              => [
				'features' => aioseo()->helpers->getThemeFeatures()
			]
		];
	}

	/**
	 * Set Vue multisite data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setMultisiteData() {
		if ( ! is_multisite() ) {
			return;
		}

		$this->data['internalNetworkOptions'] = aioseo()->internalNetworkOptions->all();
		$this->data['networkOptions']         = aioseo()->networkOptions->all();
	}

	/**
	 * Set Vue post data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setPostData() {
		if ( 'post' !== $this->args['page'] ) {
			return;
		}

		$postId         = $this->args['staticPostId'] ?: get_the_ID();
		$postTypeObj    = get_post_type_object( get_post_type( $postId ) );
		$post           = Models\Post::getPost( $postId );
		$wpPost         = get_post( $postId );
		$staticHomePage = intval( get_option( 'page_on_front' ) );

		$this->data['currentPost'] = [
			'context'                        => 'post',
			'tags'                           => aioseo()->tags->getDefaultPostTags( $postId ),
			'id'                             => $postId,
			'priority'                       => isset( $post->priority ) && null !== $post->priority ? (float) $post->priority : 'default',
			'frequency'                      => ! empty( $post->frequency ) ? $post->frequency : 'default',
			'permalink'                      => get_permalink( $postId ),
			'editlink'                       => aioseo()->helpers->getPostEditLink( $postId ),
			'title'                          => ! empty( $post->title ) ? $post->title : aioseo()->meta->title->getPostTypeTitle( $postTypeObj->name ),
			'description'                    => ! empty( $post->description ) ? $post->description : aioseo()->meta->description->getPostTypeDescription( $postTypeObj->name ),
			'descriptionIncludeCustomFields' => apply_filters( 'aioseo_description_include_custom_fields', true, $post ),
			'keywords'                       => ! empty( $post->keywords ) ? $post->keywords : [],
			'keyphrases'                     => Models\Post::getKeyphrasesDefaults( $post->keyphrases ),
			'page_analysis'                  => Models\Post::getPageAnalysisDefaults( $post->page_analysis ),
			'loading'                        => [
				'focus'      => false,
				'additional' => [],
			],
			'type'                           => $postTypeObj->labels->singular_name,
			'postType'                       => 'type' === $postTypeObj->name ? '_aioseo_type' : $postTypeObj->name,
			'postStatus'                     => get_post_status( $postId ),
			'postAuthor'                     => (int) $wpPost->post_author,
			'isSpecialPage'                  => $this->isSpecialPage( $postId ),
			'isTruSeoEligible'               => $this->isTruSeoEligible( $postId ),
			'isStaticPostsPage'              => aioseo()->helpers->isStaticPostsPage(),
			'isHomePage'                     => $postId === $staticHomePage,
			'isWooCommercePageWithoutSchema' => $this->isWooCommercePageWithoutSchema( $postId ),
			'seo_score'                      => (int) $post->seo_score,
			'pillar_content'                 => ( (int) $post->pillar_content ) === 0 ? false : true,
			'canonicalUrl'                   => $post->canonical_url,
			'default'                        => ( (int) $post->robots_default ) === 0 ? false : true,
			'noindex'                        => ( (int) $post->robots_noindex ) === 0 ? false : true,
			'noarchive'                      => ( (int) $post->robots_noarchive ) === 0 ? false : true,
			'nosnippet'                      => ( (int) $post->robots_nosnippet ) === 0 ? false : true,
			'nofollow'                       => ( (int) $post->robots_nofollow ) === 0 ? false : true,
			'noimageindex'                   => ( (int) $post->robots_noimageindex ) === 0 ? false : true,
			'noodp'                          => ( (int) $post->robots_noodp ) === 0 ? false : true,
			'notranslate'                    => ( (int) $post->robots_notranslate ) === 0 ? false : true,
			'maxSnippet'                     => null === $post->robots_max_snippet ? -1 : (int) $post->robots_max_snippet,
			'maxVideoPreview'                => null === $post->robots_max_videopreview ? -1 : (int) $post->robots_max_videopreview,
			'maxImagePreview'                => $post->robots_max_imagepreview,
			'modalOpen'                      => false,
			'generalMobilePrev'              => false,
			'og_object_type'                 => ! empty( $post->og_object_type ) ? $post->og_object_type : 'default',
			'og_title'                       => $post->og_title,
			'og_description'                 => $post->og_description,
			'og_image_custom_url'            => $post->og_image_custom_url,
			'og_image_custom_fields'         => $post->og_image_custom_fields,
			'og_image_type'                  => ! empty( $post->og_image_type ) ? $post->og_image_type : 'default',
			'og_video'                       => ! empty( $post->og_video ) ? $post->og_video : '',
			'og_article_section'             => ! empty( $post->og_article_section ) ? $post->og_article_section : '',
			'og_article_tags'                => ! empty( $post->og_article_tags ) ? $post->og_article_tags : [],
			'twitter_use_og'                 => ( (int) $post->twitter_use_og ) === 0 ? false : true,
			'twitter_card'                   => $post->twitter_card,
			'twitter_image_custom_url'       => $post->twitter_image_custom_url,
			'twitter_image_custom_fields'    => $post->twitter_image_custom_fields,
			'twitter_image_type'             => $post->twitter_image_type,
			'twitter_title'                  => $post->twitter_title,
			'twitter_description'            => $post->twitter_description,
			'ai'                             => Models\Post::getDefaultAiOptions( $post->ai ),
			'schema'                         => Models\Post::getDefaultSchemaOptions( $post->schema, aioseo()->helpers->getPost( $postId ) ),
			'metaDefaults'                   => [
				'title'       => aioseo()->meta->title->getPostTypeTitle( $postTypeObj->name ),
				'description' => aioseo()->meta->description->getPostTypeDescription( $postTypeObj->name )
			],
			'linkAssistant'                  => [
				'modalOpen' => false
			],
			'limit_modified_date'            => ( (int) $post->limit_modified_date ) === 0 ? false : true,
			'redirects'                      => [
				'modalOpen' => false
			],
			'options'                        => $post->options,
			'maxAdditionalKeyphrases'        => 0,
		];

		if ( empty( $this->args['integration'] ) ) {
			$this->data['integration'] = aioseo()->helpers->getPostPageBuilderName( $postId );
		}

		if ( ! $post->exists() ) {
			$oldPostMeta = aioseo()->migration->meta->getMigratedPostMeta( $postId );
			foreach ( $oldPostMeta as $k => $v ) {
				if ( preg_match( '#robots_.*#', (string) $k ) ) {
					$oldPostMeta[ preg_replace( '#robots_#', '', (string) $k ) ] = $v;
					continue;
				}
				if ( 'canonical_url' === $k ) {
					$oldPostMeta['canonicalUrl'] = $v;
				}
			}
			$this->data['currentPost'] = array_merge( $this->data['currentPost'], $oldPostMeta );
		}
	}

	/**
	 * Set Vue dashboard data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setDashboardData() {
		if ( 'dashboard' !== $this->args['page'] ) {
			return;
		}

		$this->data['setupWizard']['isCompleted'] = aioseo()->standalone->setupWizard->isCompleted();
		$this->data['seoOverview']                = aioseo()->postSettings->getPostTypesOverview();
		$this->data['importers']                  = aioseo()->importExport->plugins();
	}

	/**
	 * Set Vue search statistics data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setSearchStatisticsData() {
		$this->data['searchStatistics'] = [
			'isConnected'        => aioseo()->searchStatistics->api->auth->isConnected(),
			'sitemapsWithErrors' => aioseo()->searchStatistics->sitemap->getSitemapsWithErrors(),
		];

		if ( 'post' === $this->args['page'] ) {
			$this->data['keywordRankTracker'] = aioseo()->searchStatistics->keywordRankTracker->getVueDataEdit();
		}

		if ( 'search-statistics' === $this->args['page'] ) {
			$this->data['seoOverview']        = aioseo()->postSettings->getPostTypesOverview();
			$this->data['searchStatistics']   = array_merge( $this->data['searchStatistics'], aioseo()->searchStatistics->getVueData() );
			$this->data['keywordRankTracker'] = aioseo()->searchStatistics->keywordRankTracker->getVueData();
			$this->data['indexStatus']        = aioseo()->searchStatistics->indexStatus->getVueData();
		}
	}

	/**
	 * Set Vue sitemaps data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setSitemapsData() {
		if ( 'sitemaps' !== $this->args['page'] ) {
			return;
		}

		$this->data['data']['sitemapUrls'] = aioseo()->sitemap->helpers->getSitemapUrls();

		try {
			if ( as_next_scheduled_action( 'aioseo_static_sitemap_regeneration' ) ) {
				$this->data['scheduledActions']['sitemap'][] = 'staticSitemapRegeneration';
			}
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Set Vue setup wizard data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setSetupWizardData() {
		if ( 'setup-wizard' !== $this->args['page'] ) {
			return;
		}

		$isStaticHomePage = 'page' === get_option( 'show_on_front' );
		$staticHomePage   = intval( get_option( 'page_on_front' ) );

		$this->data['users']     = $this->getSiteUsers( [ 'administrator', 'editor', 'author' ] );
		$this->data['importers'] = aioseo()->importExport->plugins();
		$this->data['data']      += [
			'staticHomePageTitle'       => $isStaticHomePage ? aioseo()->meta->title->getTitle( $staticHomePage ) : '',
			'staticHomePageDescription' => $isStaticHomePage ? aioseo()->meta->description->getDescription( $staticHomePage ) : '',
		];
	}

	/**
	 * Set Vue search appearance data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setSearchAppearanceData() {
		if ( 'search-appearance' !== $this->args['page'] ) {
			return;
		}

		$isStaticHomePage = 'page' === get_option( 'show_on_front' );
		$staticHomePage   = intval( get_option( 'page_on_front' ) );

		$this->data['users'] = $this->getSiteUsers( [ 'administrator', 'editor', 'author' ] );
		$this->data['data']  += [
			'staticHomePageTitle'       => $isStaticHomePage ? aioseo()->meta->title->getTitle( $staticHomePage ) : '',
			'staticHomePageDescription' => $isStaticHomePage ? aioseo()->meta->description->getDescription( $staticHomePage ) : '',
		];
	}

	/**
	 * Set Vue social networks data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setSocialNetworksData() {
		if ( 'social-networks' !== $this->args['page'] ) {
			return;
		}

		$isStaticHomePage = 'page' === get_option( 'show_on_front' );
		$staticHomePage   = intval( get_option( 'page_on_front' ) );

		$this->data['data'] += [
			'staticHomePageOgTitle'            => $isStaticHomePage ? aioseo()->social->facebook->getTitle( $staticHomePage ) : '',
			'staticHomePageOgDescription'      => $isStaticHomePage ? aioseo()->social->facebook->getDescription( $staticHomePage ) : '',
			'staticHomePageTwitterTitle'       => $isStaticHomePage ? aioseo()->social->twitter->getTitle( $staticHomePage ) : '',
			'staticHomePageTwitterDescription' => $isStaticHomePage ? aioseo()->social->twitter->getDescription( $staticHomePage ) : '',
		];
	}

	/**
	 * Set Vue seo revisions data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setSeoRevisionsData() {
		if ( 'post' === $this->args['page'] ) {
			$this->data['seoRevisions'] = aioseo()->seoRevisions->getVueDataEdit( $this->args['staticPostId'] ?? null );
		}

		if ( 'seo-revisions' === $this->args['page'] ) {
			$this->data['seoRevisions'] = aioseo()->seoRevisions->getVueDataCompare();
		}
	}

	/**
	 * Set Vue tools or settings data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setToolsOrSettingsData() {
		if (
			'tools' !== $this->args['page'] &&
			'settings' !== $this->args['page']
		) {
			return;
		}

		if ( 'tools' === $this->args['page'] ) {
			$this->data['backups']                = array_reverse( aioseo()->backup->all() );
			$this->data['importers']              = aioseo()->importExport->plugins();
			$this->data['data']['robots']         = [
				'defaultRules'      => $this->args['page'] ? aioseo()->robotsTxt->extractRules( aioseo()->robotsTxt->getDefaultRobotsTxtContent() ) : [],
				'hasPhysicalRobots' => aioseo()->robotsTxt->hasPhysicalRobotsTxt(),
				'rewriteExists'     => aioseo()->robotsTxt->rewriteRulesExist(),
				'sitemapUrls'       => array_merge( aioseo()->sitemap->helpers->getSitemapUrlsPrefixed(), aioseo()->sitemap->helpers->extractSitemapUrlsFromRobotsTxt() )
			];
			$this->data['data']['status']         = Tools\SystemStatus::getSystemStatusInfo();
			$this->data['data']['htaccess']       = aioseo()->htaccess->getContents();
			$this->data['data']['v3Options']      = ! empty( get_option( 'aioseop_options' ) );
			$this->data['integrations']['wpcode'] = [
				'snippets'          => WpCodeIntegration::loadWpCodeSnippets(),
				'pluginInstalled'   => WpCodeIntegration::isPluginInstalled(),
				'pluginActive'      => WpCodeIntegration::isPluginActive(),
				'pluginNeedsUpdate' => WpCodeIntegration::pluginNeedsUpdate()
			];
		}

		if ( 'settings' === $this->args['page'] ) {
			$this->data['breadcrumbs']['defaultTemplate'] = aioseo()->helpers->encodeOutputHtml( aioseo()->breadcrumbs->frontend->getDefaultTemplate() );
		}

		if (
			is_multisite() &&
			is_network_admin()
		) {
			$this->data['data']['network'] = [
				'sites'   => aioseo()->helpers->getSites( aioseo()->settings->tablePagination['networkDomains'] ),
				'backups' => []
			];
		}
	}

	/**
	 * Set Vue Page Builder data.
	 *
	 * @since   4.4.9
	 * @version 4.5.2 Renamed.
	 *
	 * @return void
	 */
	private function setPageBuilderData() {
		if ( empty( $this->args['integration'] ) ) {
			return;
		}

		if ( 'divi' === $this->args['integration'] ) {
			// This needs to be dropped in order to prevent JavaScript errors in Divi's visual builder.
			// Some of the data from the site analysis can contain HTML tags, e.g. the search preview, and somehow that causes JSON.parse to fail on our localized Vue data.
			unset( $this->data['internalOptions']['internal']['siteAnalysis'] );
		}
	}

	/**
	 * Returns Jed-formatted localization data. Added for backwards-compatibility.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $domain Translation domain.
	 * @return array          The information of the locale.
	 */
	public function getJedLocaleData( $domain ) {
		$translations = get_translations_for_domain( $domain );

		$locale = [
			'' => [
				'domain' => $domain,
				'lang'   => is_admin() && function_exists( 'get_user_locale' ) ? get_user_locale() : get_locale()
			],
		];

		if ( ! empty( $translations->headers['Plural-Forms'] ) ) {
			$locale['']['plural_forms'] = $translations->headers['Plural-Forms'];
		}

		foreach ( $translations->entries as $entry ) {
			if ( empty( $entry->translations ) || ! is_array( $entry->translations ) ) {
				continue;
			}

			foreach ( $entry->translations as $translation ) {
				// If any of the translated strings contains an HTML line break, we need to ignore it. Otherwise, logging into the admin breaks.

				if ( preg_match( '/<br[\s\/\\\\]*>/', (string) $translation ) ) {
					continue 2;
				}
			}

			// Set the translation data using the singular string as the index. This is how Jed expects it, even for plural strings.
			$locale[ $entry->singular ] = $entry->translations;
		}

		return $locale;
	}

	/**
	 * Set Vue writing assistant data.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	private function setWritingAssistantData() {
		// Settings page or not a post screen.
		if (
			'settings' !== $this->args['page'] &&
			! aioseo()->helpers->isScreenBase( 'post' )
		) {
			return;
		}

		$this->data['writingAssistantSettings'] = aioseo()->writingAssistant->helpers->getSettingsVueData();
	}

	/**
	 * Whether the notifications drawer should be shown or not.
	 *
	 * @since 4.4.9
	 *
	 * @return bool True if it should be shown, false otherwise.
	 */
	private function showNotificationsDrawer() {
		static $showNotificationsDrawer = null;
		if ( null === $showNotificationsDrawer ) {
			$showNotificationsDrawer = (bool) aioseo()->core->cache->get( 'show_notifications_drawer' );

			// If this is set to true, let's disable it now, so it doesn't pop up again.
			if ( $showNotificationsDrawer ) {
				aioseo()->core->cache->delete( 'show_notifications_drawer' );
			}
		}

		return $showNotificationsDrawer;
	}

	/**
	 * Set Vue breadcrumbs data.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	private function setBreadcrumbsData() {
		$isPostOrTermPage              = aioseo()->helpers->isScreenBase( 'post' ) || aioseo()->helpers->isScreenBase( 'term' );
		$isCurrentPageUsingPageBuilder = 'post' === $this->args['page'] && ! empty( $this->args['integration'] );
		$isSettingsPage                = ! empty( $this->args['page'] ) && 'settings' === $this->args['page'];
		if ( ! $isSettingsPage && ! $isCurrentPageUsingPageBuilder && ! $isPostOrTermPage ) {
			return;
		}

		$this->data['breadcrumbs']['defaultTemplate'] = aioseo()->helpers->encodeOutputHtml( aioseo()->breadcrumbs->frontend->getDefaultTemplate() );
	}

	/**
	 * Set Vue SEO Analyzer data.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	private function setSeoAnalyzerData() {
		if ( 'seo-analysis' !== $this->args['page'] ) {
			return;
		}

		$this->data['analyzer']['homeResults'] = Models\SeoAnalyzerResult::getResults();
		$this->data['analyzer']['competitors'] = Models\SeoAnalyzerResult::getCompetitorsResults();
	}

	/**
	 * Returns the marketing site URL.
	 *
	 * @since 4.8.4
	 *
	 * @return string The marketing site URL.
	 */
	private function getMarketingSiteUrl() {
		if ( defined( 'AIOSEO_MARKETING_SITE_URL' ) && AIOSEO_MARKETING_SITE_URL ) {
			return AIOSEO_MARKETING_SITE_URL;
		}

		return 'https://aioseo.com/';
	}
}Common/Traits/Helpers/Wp.php000064400000067033151536241210011754 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

use AIOSEO\Plugin\Common\Utils;

/**
 * Contains all WP related helper methods.
 *
 * @since 4.1.4
 */
trait Wp {
	/**
	 * Whether or not we have a local connection.
	 *
	 * @since 4.0.0
	 *
	 * @var bool
	 */
	private static $connection = false;

	/**
	 * Returns user roles in the current WP install.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of user roles.
	 */
	public function getUserRoles() {
		global $wp_roles; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		$wpRoles = $wp_roles; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if ( ! is_object( $wpRoles ) ) {
			// Don't assign this to the global because otherwise WordPress won't override it.
			$wpRoles = new \WP_Roles();
		}

		$roleNames = $wpRoles->get_names();
		asort( $roleNames );

		return $roleNames;
	}

	/**
	 * Returns the custom roles in the current WP install.
	 *
	 * @since 4.1.3
	 *
	 * @return array An array of custom roles.
	 */
	public function getCustomRoles() {
		$allRoles = $this->getUserRoles();

		$toSkip = array_merge(
			// Default WordPress roles.
			[ 'superadmin', 'administrator', 'editor', 'author', 'contributor' ],
			// Default AIOSEO roles.
			[ 'aioseo_manager', 'aioseo_editor' ],
			// Filterable roles.
			apply_filters( 'aioseo_access_control_excluded_roles', array_merge( [
				'subscriber'
			], aioseo()->helpers->isWooCommerceActive() ? [ 'customer' ] : [] ) )
		);

		// Remove empty entries.
		$toSkip = array_filter( $toSkip );

		$customRoles = [];
		foreach ( $allRoles as $roleName => $role ) {
			// Skip specific roles.
			if ( in_array( $roleName, $toSkip, true ) ) {
				continue;
			}

			$customRoles[ $roleName ] = $role;
		}

		return $customRoles;
	}

	/**
	 * Returns an array of plugins with the active status.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of plugins with active status.
	 */
	public function getPluginData() {
		$pluginUpgrader   = new Utils\PluginUpgraderSilentAjax();
		$installedPlugins = array_keys( get_plugins() );

		$plugins = [];
		foreach ( $pluginUpgrader->pluginSlugs as $key => $slug ) {
			$adminUrl        = admin_url( $pluginUpgrader->pluginAdminUrls[ $key ] );
			$networkAdminUrl = null;
			if (
				is_multisite() &&
				is_network_admin() &&
				! empty( $pluginUpgrader->hasNetworkAdmin[ $key ] )
			) {
				$networkAdminUrl = network_admin_url( $pluginUpgrader->hasNetworkAdmin[ $key ] );
				if ( aioseo()->helpers->isPluginNetworkActivated( $pluginUpgrader->pluginSlugs[ $key ] ) ) {
					$adminUrl = $networkAdminUrl;
				}
			}

			$plugins[ $key ] = [
				'basename'        => $slug,
				'installed'       => in_array( $slug, $installedPlugins, true ),
				'activated'       => is_plugin_active( $slug ),
				'adminUrl'        => $adminUrl,
				'networkAdminUrl' => $networkAdminUrl,
				'canInstall'      => aioseo()->addons->canInstall(),
				'canActivate'     => aioseo()->addons->canActivate(),
				'canUpdate'       => aioseo()->addons->canUpdate(),
				'wpLink'          => ! empty( $pluginUpgrader->wpPluginLinks[ $key ] ) ? $pluginUpgrader->wpPluginLinks[ $key ] : null
			];
		}

		return $plugins;
	}

	/**
	 * Returns all registered Post Statuses.
	 *
	 * @since 4.1.6
	 *
	 * @param  boolean $statusesOnly Whether or not to only return statuses.
	 * @return array              An array of post statuses.
	 */
	public function getPublicPostStatuses( $statusesOnly = false ) {
		$allStatuses = get_post_stati( [ 'show_in_admin_all_list' => true ], 'objects' );

		$postStatuses = [];
		foreach ( $allStatuses as $status => $data ) {
			if (
				! $data->public &&
				! $data->protected &&
				! $data->private
			) {
				continue;
			}

			if ( $statusesOnly ) {
				$postStatuses[] = $status;
				continue;
			}

			$postStatuses[] = [
				'label'  => $data->label,
				'status' => $status
			];
		}

		return $postStatuses;
	}

	/**
	 * Returns a list of public post types objects or names.
	 *
	 * @since 4.0.0
	 *
	 * @param  bool  $namesOnly       Whether only the names should be returned.
	 * @param  bool  $hasArchivesOnly Whether to only include post types which have archives.
	 * @param  bool  $rewriteType     Whether to rewrite the type slugs.
	 * @param  array $args            Additional arguments.
	 * @return array                  List of public post types.
	 */
	public function getPublicPostTypes( $namesOnly = false, $hasArchivesOnly = false, $rewriteType = false, $args = [] ) {
		$args = array_merge( [
			'include' => [] // Post types to include.
		], $args );

		$postTypes   = [];
		$postTypeObjects = get_post_types( [], 'objects' );
		foreach ( $postTypeObjects as $postTypeObject ) {
			if ( ! is_post_type_viewable( $postTypeObject ) ) {
				continue;
			}

			$postTypeArray = $this->getPostType( $postTypeObject, $namesOnly, $hasArchivesOnly, $rewriteType );
			if ( ! empty( $postTypeArray ) ) {
				$postTypes[] = $postTypeArray;
			}
		}

		if ( isset( aioseo()->standalone->buddyPress ) ) {
			aioseo()->standalone->buddyPress->maybeAddPostTypes( $postTypes, $namesOnly, $hasArchivesOnly, $args );
		}

		return apply_filters( 'aioseo_public_post_types', $postTypes, $namesOnly, $hasArchivesOnly, $args );
	}

	/**
	 * Returns the data for the given post type.
	 *
	 * @since 4.2.2
	 *
	 * @param  \WP_Post_Type $postTypeObject  The post type object.
	 * @param  bool          $namesOnly       Whether only the names should be returned.
	 * @param  bool          $hasArchivesOnly Whether to only include post types which have archives.
	 * @param  bool          $rewriteType     Whether to rewrite the type slugs.
	 * @return mixed                          Data for the post type.
	 */
	public function getPostType( $postTypeObject, $namesOnly = false, $hasArchivesOnly = false, $rewriteType = false ) {
		if ( empty( $postTypeObject->label ) ) {
			return $namesOnly ? null : [];
		}

		// We don't want to include archives for the WooCommerce shop page.
		if (
			$hasArchivesOnly &&
			(
				! $postTypeObject->has_archive ||
				( 'product' === $postTypeObject->name && $this->isWooCommerceActive() )
			)
		) {
			return $namesOnly ? null : [];
		}

		if ( $namesOnly ) {
			return $postTypeObject->name;
		}

		if ( 'attachment' === $postTypeObject->name ) {
			// We have to check if the 'init' action has been fired to avoid a PHP notice
			// in WP 6.7+ due to loading translations too early.
			if ( did_action( 'init' ) ) {
				$postTypeObject->label = __( 'Attachments', 'all-in-one-seo-pack' );
			}
		}

		if ( 'product' === $postTypeObject->name && $this->isWooCommerceActive() ) {
			$postTypeObject->menu_icon = 'dashicons-products';
		}

		$name = $postTypeObject->name;
		if ( 'type' === $postTypeObject->name && $rewriteType ) {
			$name = '_aioseo_type';
		}

		return [
			'name'         => $name,
			'label'        => ucwords( $postTypeObject->label ),
			'singular'     => ucwords( $postTypeObject->labels->singular_name ),
			'icon'         => $postTypeObject->menu_icon,
			'hasArchive'   => $postTypeObject->has_archive,
			'hierarchical' => $postTypeObject->hierarchical,
			'taxonomies'   => get_object_taxonomies( $name ),
			'slug'         => isset( $postTypeObject->rewrite['slug'] ) ? $postTypeObject->rewrite['slug'] : $name,
			'supports'     => get_all_post_type_supports( $name )
		];
	}

	/**
	 * Returns a list of public taxonomies objects or names.
	 *
	 * @since 4.0.0
	 *
	 * @param  bool  $namesOnly   Whether only the names should be returned.
	 * @param  bool  $rewriteType Whether to rewrite the type slugs.
	 * @return array              List of public taxonomies.
	 */
	public function getPublicTaxonomies( $namesOnly = false, $rewriteType = false ) {
		$taxonomies = [];
		if ( count( $taxonomies ) ) {
			return $taxonomies;
		}

		$taxObjects = get_taxonomies( [], 'objects' );
		foreach ( $taxObjects as $taxObject ) {
			if (
				empty( $taxObject->label ) ||
				! is_taxonomy_viewable( $taxObject ) ||
				aioseo()->helpers->isWooCommerceProductAttribute( $taxObject->name )
			) {
				continue;
			}

			if ( in_array( $taxObject->name, [
				'product_shipping_class',
				'post_format'
			], true ) ) {
				continue;
			}

			if ( $namesOnly ) {
				$taxonomies[] = $taxObject->name;
				continue;
			}

			$name = $taxObject->name;
			if ( 'type' === $taxObject->name && $rewriteType ) {
				$name = '_aioseo_type';
			}

			global $wp_taxonomies; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			$taxonomyPostTypes = ! empty( $wp_taxonomies[ $name ] ) // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				? $wp_taxonomies[ $name ]->object_type // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				: [];

			$taxonomies[] = [
				'name'               => $name,
				'label'              => ucwords( $taxObject->label ),
				'singular'           => ucwords( $taxObject->labels->singular_name ),
				'icon'               => strpos( $taxObject->label, 'categor' ) !== false ? 'dashicons-category' : 'dashicons-tag',
				'hierarchical'       => $taxObject->hierarchical,
				'slug'               => isset( $taxObject->rewrite['slug'] ) ? $taxObject->rewrite['slug'] : '',
				'primaryTermSupport' => (bool) $taxObject->hierarchical,
				'restBase'           => ( $taxObject->rest_base ) ? $taxObject->rest_base : $taxObject->name,
				'postTypes'          => $taxonomyPostTypes
			];
		}

		if ( $this->isWooCommerceActive() ) {
			// We inject a fake one for WooCommerce product attributes so that we can show a single set of settings
			// instead of having to duplicate them for each attribute.
			if ( $namesOnly ) {
				$taxonomies[] = 'product_attributes';
			} else {
				$taxonomies[] = [
					'name'               => 'product_attributes',
					'label'              => __( 'Product Attributes', 'all-in-one-seo-pack' ),
					'singular'           => __( 'Product Attribute', 'all-in-one-seo-pack' ),
					'icon'               => 'dashicons-products',
					'hierarchical'       => true,
					'slug'               => 'product_attributes',
					'primaryTermSupport' => true,
					'restBase'           => 'product_attributes_class',
					'postTypes'          => [ 'product' ]
				];
			}
		}

		return apply_filters( 'aioseo_public_taxonomies', $taxonomies, $namesOnly );
	}

	/**
	 * Retrieve a list of users that match passed in roles.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of user data.
	 */
	public function getSiteUsers( $roles ) {
		static $users = [];

		if ( ! empty( $users ) ) {
			return $users;
		}

		$rolesWhere = [];
		foreach ( $roles as $role ) {
			$rolesWhere[] = '(um.meta_key = \'' . aioseo()->core->db->db->prefix . 'capabilities\' AND um.meta_value LIKE \'%\"' . $role . '\"%\')';
		}
		// We get the table name from WPDB since multisites share the same table.
		$usersTableName    = aioseo()->core->db->db->users;
		$usermetaTableName = aioseo()->core->db->db->usermeta;
		$dbUsers           = aioseo()->core->db->start( "$usersTableName as u", true )
			->select( 'u.ID, u.display_name, u.user_nicename, u.user_email' )
			->join( "$usermetaTableName as um", 'u.ID = um.user_id', '', true )
			->whereRaw( '(' . implode( ' OR ', $rolesWhere ) . ')' )
			->orderBy( 'u.user_nicename' )
			->run()
			->result();

		foreach ( $dbUsers as $dbUser ) {
			$users[] = [
				'id'          => (int) $dbUser->ID,
				'displayName' => $dbUser->display_name,
				'niceName'    => $dbUser->user_nicename,
				'email'       => $dbUser->user_email,
				'gravatar'    => get_avatar_url( $dbUser->user_email )
			];
		}

		return $users;
	}

	/**
	 * Returns the ID of the site logo if it exists.
	 *
	 * @since 4.0.0
	 *
	 * @return int
	 */
	public function getSiteLogoId() {
		if ( ! get_theme_support( 'custom-logo' ) ) {
			return false;
		}

		return get_theme_mod( 'custom_logo' );
	}

	/**
	 * Returns the URL of the site logo if it exists.
	 *
	 * @since 4.0.0
	 *
	 * @return string
	 */
	public function getSiteLogoUrl() {
		$id = $this->getSiteLogoId();
		if ( ! $id ) {
			return false;
		}

		$image = wp_get_attachment_image_src( $id, 'full' );
		if ( empty( $image ) ) {
			return false;
		}

		return $image[0];
	}

	/**
	 * Returns noindexed post types.
	 *
	 * @since 4.0.0
	 *
	 * @return array A list of noindexed post types.
	 */
	public function getNoindexedPostTypes() {
		return $this->getNoindexedObjects( 'postTypes' );
	}

	/**
	 * Checks whether a given post type is noindexed.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $postType The post type.
	 * @return bool              Whether the post type is noindexed.
	 */
	public function isPostTypeNoindexed( $postType ) {
		$noindexedPostTypes = $this->getNoindexedPostTypes();

		return in_array( $postType, $noindexedPostTypes, true );
	}

	/**
	 * Checks whether a given post type is public.
	 *
	 * @since 4.2.2
	 *
	 * @param  string  $postType The post type.
	 * @return bool              Whether the post type is public.
	 */
	public function isPostTypePublic( $postType ) {
		$publicPostTypes = $this->getPublicPostTypes( true );

		return in_array( $postType, $publicPostTypes, true );
	}

	/**
	 * Returns noindexed taxonomies.
	 *
	 * @since 4.0.0
	 *
	 * @return array A list of noindexed taxonomies.
	 */
	public function getNoindexedTaxonomies() {
		return $this->getNoindexedObjects( 'taxonomies' );
	}

	/**
	 * Checks whether a given post type is noindexed.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $taxonomy The taxonomy.
	 * @return bool              Whether the taxonomy is noindexed.
	 */
	public function isTaxonomyNoindexed( $taxonomy ) {
		$noindexedTaxonomies = $this->getNoindexedTaxonomies();

		return in_array( $taxonomy, $noindexedTaxonomies, true );
	}

	/**
	 * Checks whether a given taxonomy is public.
	 *
	 * @since 4.2.2
	 *
	 * @param  string  $taxonomy The taxonomy.
	 * @return bool              Whether the taxonomy is public.
	 */
	public function isTaxonomyPublic( $taxonomy ) {
		$publicTaxonomies = $this->getPublicTaxonomies( true );

		return in_array( $taxonomy, $publicTaxonomies, true );
	}

	/**
	 * Returns noindexed object types of a given parent type.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $type The parent object type ("postTypes", "archives", "taxonomies").
	 * @return array        A list of noindexed objects types.
	 */
	public function getNoindexedObjects( $type ) {
		$noindexed = [];
		foreach ( aioseo()->dynamicOptions->searchAppearance->$type->all() as $name => $object ) {
			if (
				! $object['show'] ||
				( $object['advanced']['robotsMeta'] && ! $object['advanced']['robotsMeta']['default'] && $object['advanced']['robotsMeta']['noindex'] )
			) {
				$noindexed[] = $name;
			}
		}

		return $noindexed;
	}

	/**
	 * Returns all categories for a post.
	 *
	 * @since 4.1.4
	 *
	 * @param  int   $postId The post ID.
	 * @return array         The category names.
	 */
	public function getAllCategories( $postId = 0 ) {
		$names      = [];
		$categories = get_the_category( $postId );
		if ( $categories && count( $categories ) ) {
			foreach ( $categories as $category ) {
				$names[] = aioseo()->helpers->internationalize( $category->name );
			}
		}

		return $names;
	}

	/**
	 * Returns all tags for a post.
	 *
	 * @since 4.1.4
	 *
	 * @param  int   $postId The post ID.
	 * @return array $names  The tag names.
	 */
	public function getAllTags( $postId = 0 ) {
		$names = [];

		$tags = get_the_tags( $postId );
		if ( ! empty( $tags ) && ! is_wp_error( $tags ) ) {
			foreach ( $tags as $tag ) {
				if ( ! empty( $tag->name ) ) {
					$names[] = aioseo()->helpers->internationalize( $tag->name );
				}
			}
		}

		return $names;
	}

	/**
	 * Loads the translations for a given domain.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function loadTextDomain( $domain ) {
		if ( ! is_user_logged_in() ) {
			return;
		}

		// Unload the domain in case WordPress has enqueued the translations for the site language instead of profile language.
		// Reloading the text domain will otherwise not override the existing loaded translations.
		unload_textdomain( $domain );

		$mofile = $domain . '-' . get_user_locale() . '.mo';
		load_textdomain( $domain, WP_LANG_DIR . '/plugins/' . $mofile );
	}

	/**
	 * Get the page builder the given Post ID was built with.
	 *
	 * @since 4.1.7
	 *
	 * @param  int         $postId The Post ID.
	 * @return bool|string         The page builder or false if not built with page builders.
	 */
	public function getPostPageBuilderName( $postId ) {
		foreach ( aioseo()->standalone->pageBuilderIntegrations as $integration => $pageBuilder ) {
			if ( $pageBuilder->isBuiltWith( $postId ) ) {
				return $integration;
			}
		}

		return false;
	}

	/**
	 * Get the edit link for the given Post ID.
	 *
	 * @since 4.3.1
	 *
	 * @param  int         $postId The Post ID.
	 * @return bool|string         The edit link or false if not built with page builders.
	 */
	public function getPostEditLink( $postId ) {
		$pageBuilder = $this->getPostPageBuilderName( $postId );
		if ( ! empty( $pageBuilder ) ) {
			return aioseo()->standalone->pageBuilderIntegrations[ $pageBuilder ]->getEditUrl( $postId );
		}

		return get_edit_post_link( $postId );
	}

	/**
	 * Checks if the current user can edit posts of the given post type.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $postType The name of the post type.
	 * @return bool             Whether the user can edit posts of the given post type.
	 */
	public function canEditPostType( $postType ) {
		$capabilities = $this->getPostTypeCapabilities( $postType );

		return current_user_can( $capabilities['edit_posts'] );
	}

	/**
	 * Returns a list of capabilities for the given post type.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $postType The name of the post type.
	 * @return array            The capabilities.
	 */
	public function getPostTypeCapabilities( $postType ) {
		static $capabilities = [];
		if ( isset( $capabilities[ $postType ] ) ) {
			return $capabilities[ $postType ];
		}

		$postTypeObject = get_post_type_object( $postType );
		if ( ! is_a( $postTypeObject, 'WP_Post_Type' ) ) {
			$capabilities[ $postType ] = [];

			return $capabilities[ $postType ];
		}

		$capabilityType = $postTypeObject->capability_type;
		if ( ! is_array( $capabilityType ) ) {
			$capabilityType = [
				$capabilityType,
				$capabilityType . 's'
			];
		}

		// Singular base for meta capabilities, plural base for primitive capabilities.
		list( $singularBase, $pluralBase ) = $capabilityType;

		$capabilities[ $postType ] = [
			'edit_post'          => 'edit_' . $singularBase,
			'read_post'          => 'read_' . $singularBase,
			'delete_post'        => 'delete_' . $singularBase,
			'edit_posts'         => 'edit_' . $pluralBase,
			'edit_others_posts'  => 'edit_others_' . $pluralBase,
			'delete_posts'       => 'delete_' . $pluralBase,
			'publish_posts'      => 'publish_' . $pluralBase,
			'read_private_posts' => 'read_private_' . $pluralBase,
		];

		return $capabilities[ $postType ];
	}

	/**
	 * Checks if the current user can edit terms of the given taxonomy.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $taxonomy The name of the taxonomy.
	 * @return bool             Whether the user can edit posts of the given taxonomy.
	 */
	public function canEditTaxonomy( $taxonomy ) {
		$capabilities = $this->getTaxonomyCapabilities( $taxonomy );

		return current_user_can( $capabilities['edit_terms'] );
	}

	/**
	 * Returns a list of capabilities for the given taxonomy.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $taxonomy The name of the taxonomy.
	 * @return array            The capabilities.
	 */
	public function getTaxonomyCapabilities( $taxonomy ) {
		static $capabilities = [];
		if ( isset( $capabilities[ $taxonomy ] ) ) {
			return $capabilities[ $taxonomy ];
		}

		$taxonomyObject = get_taxonomy( $taxonomy );
		if ( ! is_a( $taxonomyObject, 'WP_Taxonomy' ) ) {
			$capabilities[ $taxonomy ] = [];

			return $capabilities[ $taxonomy ];
		}

		$capabilities[ $taxonomy ] = (array) $taxonomyObject->cap;

		return $capabilities[ $taxonomy ];
	}

	/**
	 * Returns the charset for the site.
	 *
	 * @since 4.2.3
	 *
	 * @return string The name of the charset.
	 */
	public function getCharset() {
		static $charset = null;
		if ( null !== $charset ) {
			return $charset;
		}

		$charset = get_option( 'blog_charset' );
		$charset = $charset ? $charset : 'UTF-8';

		return $charset;
	}

	/**
	 * Returns the given data as JSON.
	 * We temporarily change the floating point precision in order to prevent rounding errors.
	 * Otherwise e.g. 4.9 could be output as 4.90000004.
	 *
	 * @since 4.2.7
	 *
	 * @param  mixed  $data  The data.
	 * @param  int    $flags The flags.
	 * @return string        The JSON output.
	 */
	public function wpJsonEncode( $data, $flags = 0 ) {
		$originalPrecision          = false;
		$originalSerializePrecision = false;
		if ( version_compare( PHP_VERSION, '7.1', '>=' ) ) {
			$originalPrecision          = ini_get( 'precision' );
			$originalSerializePrecision = ini_get( 'serialize_precision' );
			ini_set( 'precision', 17 );
			ini_set( 'serialize_precision', -1 );
		}

		$json = wp_json_encode( $data, $flags );

		if ( version_compare( PHP_VERSION, '7.1', '>=' ) ) {
			ini_set( 'precision', $originalPrecision );
			ini_set( 'serialize_precision', $originalSerializePrecision );
		}

		return $json;
	}

	/**
	 * Returns the post title or a placeholder if there isn't one.
	 *
	 * @since 4.3.0
	 *
	 * @param  int    $postId The post ID.
	 * @return string         The post title.
	 */
	public function getPostTitle( $postId ) {
		static $titles = [];
		if ( isset( $titles[ $postId ] ) ) {
			return $titles[ $postId ];
		}

		$post = aioseo()->helpers->getPost( $postId );
		if ( ! is_a( $post, 'WP_Post' ) ) {
			$titles[ $postId ] = __( '(no title)', 'default' ); // phpcs:ignore AIOSEO.Wp.I18n.TextDomainMismatch, WordPress.WP.I18n.TextDomainMismatch

			return $titles[ $postId ];
		}

		$title = $post->post_title;
		$title = $title ? $title : __( '(no title)', 'default' ); // phpcs:ignore AIOSEO.Wp.I18n.TextDomainMismatch, WordPress.WP.I18n.TextDomainMismatch

		$titles[ $postId ] = aioseo()->helpers->decodeHtmlEntities( $title );

		return $titles[ $postId ];
	}

	/**
	 * Checks whether the post status should be considered viewable.
	 * This function is a copy of the WordPress core function is_post_status_viewable() which was introduced in WP 5.7.
	 *
	 * @since 4.5.0
	 *
	 * @param  string|\stdClass $postStatus The post status name or object.
	 * @return bool                         Whether the post status is viewable.
	 */
	public function isPostStatusViewable( $postStatus ) {
		if ( is_scalar( $postStatus ) ) {
			$postStatus = get_post_status_object( $postStatus );

			if ( ! $postStatus ) {
				return false;
			}
		}

		if (
			! is_object( $postStatus ) ||
			$postStatus->internal ||
			$postStatus->protected
		) {
			return false;
		}

		return $postStatus->publicly_queryable || ( $postStatus->_builtin && $postStatus->public );
	}

	/**
	 * Checks whether the given post is publicly viewable.
	 * This function is a copy of the WordPress core function is_post_publicly_viewable() which was introduced in WP 5.7.
	 *
	 * @since 4.5.0
	 *
	 * @param  int|\WP_Post  $post Optional. Post ID or post object. Defaults to global $post.
	 * @return boolean                      Whether the post is publicly viewable or not.
	 */
	public function isPostPubliclyViewable( $post = null ) {
		$post = get_post( $post );
		if ( empty( $post ) ) {
			return false;
		}

		$postType   = get_post_type( $post );
		$postStatus = get_post_status( $post );

		return is_post_type_viewable( $postType ) && $this->isPostStatusViewable( $postStatus );
	}

	/**
	 * Only register a legacy widget if the WP version is lower than 5.8 or the widget is being used.
	 * The "Block-based Widgets Editor" was released in WP 5.8, so for WP versions below 5.8 it's okay to register them.
	 * The main purpose here is to avoid blocks and widgets with the same name to be displayed on the Customizer,
	 * like e.g. the "Breadcrumbs" Block and Widget.
	 *
	 * @since 4.3.9
	 *
	 * @param string $idBase The base ID of a widget created by extending WP_Widget.
	 * @return bool          Whether the legacy widget can be registered.
	 */
	public function canRegisterLegacyWidget( $idBase ) {
		global $wp_version; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if (
			version_compare( $wp_version, '5.8', '<' ) || // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			is_active_widget( false, false, $idBase ) ||
			aioseo()->standalone->pageBuilderIntegrations['elementor']->isPluginActive()
		) {
			return true;
		}

		return false;
	}

	/**
	 * Parses blocks for a given post.
	 *
	 * @since 4.6.8
	 *
	 * @param  \WP_Post|int $post          The post or post ID.
	 * @param  bool         $flattenBlocks Whether to flatten the blocks.
	 * @return array                       The parsed blocks.
	 */
	public function parseBlocks( $post, $flattenBlocks = true ) {
		if ( ! is_a( $post, 'WP_Post' ) ) {
			$post = aioseo()->helpers->getPost( $post );
		}

		static $parsedBlocks = [];
		if ( isset( $parsedBlocks[ $post->ID ] ) ) {
			return $parsedBlocks[ $post->ID ];
		}

		$parsedBlocks = parse_blocks( $post->post_content );

		if ( $flattenBlocks ) {
			$parsedBlocks = $this->flattenBlocks( $parsedBlocks );
		}

		$parsedBlocks[ $post->ID ] = $parsedBlocks;

		return $parsedBlocks[ $post->ID ];
	}

	/**
	 * Flattens the given blocks.
	 *
	 * @since 4.6.8
	 *
	 * @param  array $blocks The blocks.
	 * @return array         The flattened blocks.
	 */
	public function flattenBlocks( $blocks ) {
		$flattenedBlocks = [];

		foreach ( $blocks as $block ) {
			if ( ! empty( $block['innerBlocks'] ) ) {
				// Flatten inner blocks first.
				$innerBlocks = $this->flattenBlocks( $block['innerBlocks'] );
				unset( $block['innerBlocks'] );

				// Add the current block to the result.
				$flattenedBlocks[] = $block;

				// Add the flattened inner blocks to the result.
				$flattenedBlocks = array_merge( $flattenedBlocks, $innerBlocks );
			} else {
				// If no inner blocks, just add the block to the result.
				$flattenedBlocks[] = $block;
			}
		}

		return $flattenedBlocks;
	}

	/**
	 * Checks if the Classic eEditor is active and if the Block Editor is disabled in its settings.
	 *
	 * @since 4.7.3
	 *
	 * @return bool Whether the Classic Editor is active.
	 */
	public function isClassicEditorActive() {
		include_once ABSPATH . 'wp-admin/includes/plugin.php';

		if ( ! is_plugin_active( 'classic-editor/classic-editor.php' ) ) {
			return false;
		}

		return 'classic' === get_option( 'classic-editor-replace' );
	}

	/**
	 * Redirects to a 404 Not Found page if the sitemap is disabled.
	 *
	 * @since 4.0.0
	 * @version 4.8.0 Moved from the Sitemap class.
	 *
	 * @return void
	 */
	public function notFoundPage() {
		global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$wp_query->set_404(); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		status_header( 404 );
		include_once get_404_template();
		exit;
	}

	/**
	 * Retrieves the post type labels for the given post type.
	 *
	 * @since 4.8.2
	 *
	 * @param  string $postType The name of a registered post type.
	 * @return object           Object with all the labels as member variables.
	 */
	public function getPostTypeLabels( $postType ) {
		static $postTypeLabels = [];
		if ( ! isset( $postTypeLabels[ $postType ] ) ) {
			$postTypeObject = get_post_type_object( $postType );
			if ( ! is_a( $postTypeObject, 'WP_Post_Type' ) ) {
				return null;
			}

			$postTypeLabels[ $postType ] = get_post_type_labels( $postTypeObject );
		}

		return $postTypeLabels[ $postType ];
	}

	/**
	 * Cleans the slug of the current request before we use it.
	 *
	 * @since 4.8.4
	 *
	 * @param  string $slug The slug.
	 * @return string       The cleaned slug.
	 */
	public function cleanSlug( $slug ) {
		$slug = strtolower( $slug );
		$slug = aioseo()->helpers->unleadingSlashIt( $slug );
		$slug = untrailingslashit( $slug );

		return $slug;
	}
}Common/Traits/Helpers/WpContext.php000064400000070077151536241210013323 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

/**
 * Contains all context related helper methods.
 * This includes methods to check the context of the current request, but also get WP objects.
 *
 * @since 4.1.4
 */
trait WpContext {
	/**
	 * The original main query.
	 *
	 * @since 4.3.0
	 *
	 * @var \WP_Query
	 */
	public $originalQuery;

	/**
	 * The original main post variable.
	 *
	 * @since 4.3.0
	 *
	 * @var \WP_Post
	 */
	public $originalPost;

	/**
	 * Get the home page object.
	 *
	 * @since 4.1.1
	 *
	 * @return \WP_Post|null The home page.
	 */
	public function getHomePage() {
		$homePageId = $this->getHomePageId();

		return $homePageId ? get_post( $homePageId ) : null;
	}

	/**
	 * Get the ID of the home page.
	 *
	 * @since 4.0.0
	 *
	 * @return int|false The home page ID.
	 */
	public function getHomePageId() {
		static $homeId = null;
		if ( null !== $homeId ) {
			return $homeId;
		}

		$pageShowOnFront = ( 'page' === get_option( 'show_on_front' ) );
		$pageOnFrontId   = get_option( 'page_on_front' );

		$homeId = $pageShowOnFront && $pageOnFrontId ? (int) $pageOnFrontId : false;

		return $homeId;
	}

	/**
	 * Returns the blog page.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_Post|null The blog page.
	 */
	public function getBlogPage() {
		$blogPageId = $this->getBlogPageId();

		return $blogPageId ? get_post( $blogPageId ) : null;
	}

	/**
	 * Gets the current blog page id if it's configured.
	 *
	 * @since 4.1.1
	 *
	 * @return int|null
	 */
	public function getBlogPageId() {
		$pageShowOnFront = ( 'page' === get_option( 'show_on_front' ) );
		$blogPageId      = (int) get_option( 'page_for_posts' );

		return $pageShowOnFront && $blogPageId ? $blogPageId : null;
	}

	/**
	 * Checks whether the current page is a taxonomy term archive.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether the current page is a taxonomy term archive.
	 */
	public function isTaxTerm() {
		$object = get_queried_object();

		return $object instanceof \WP_Term;
	}

	/**
	 * Checks whether the current page is a static one.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether the current page is a static one.
	 */
	public function isStaticPage() {
		return $this->isStaticHomePage() || $this->isStaticPostsPage() || $this->isWooCommerceShopPage();
	}

	/**
	 * Checks whether the current page is the static homepage.
	 *
	 * @since 4.0.0
	 *
	 * @param  mixed $post Pass in an optional post to check if its the static home page.
	 * @return bool        Whether the current page is the static homepage.
	 */
	public function isStaticHomePage( $post = null ) {
		static $isHomePage = null;
		if ( null !== $isHomePage ) {
			return $isHomePage;
		}

		$post = aioseo()->helpers->getPost( $post );

		$isHomePage = ( 'page' === get_option( 'show_on_front' ) && ! empty( $post->ID ) && (int) get_option( 'page_on_front' ) === $post->ID );

		return $isHomePage;
	}

	/**
	 * Checks whether the current page is the dynamic homepage.
	 *
	 * @since 4.2.3
	 *
	 * @return bool Whether the current page is the dynamic homepage.
	 */
	public function isDynamicHomePage() {
		return is_front_page() && is_home();
	}

	/**
	 * Checks whether the current page is the static posts page.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether the current page is the static posts page.
	 */
	public function isStaticPostsPage( $post = null ) {
		static $isStaticPostsPage = null;
		if ( null !== $isStaticPostsPage ) {
			return $isStaticPostsPage;
		}

		$post = aioseo()->helpers->getPost( $post );

		$isStaticPostsPage = (
			( is_home() && ( 0 !== (int) get_option( 'page_for_posts' ) ) ) ||
			( ! empty( $post->ID ) && (int) get_option( 'page_for_posts' ) === $post->ID )
		);

		return $isStaticPostsPage;
	}

	/**
	 * Checks whether current page supports meta.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether the current page supports meta.
	 */
	public function supportsMeta() {
		return ! is_date() && ! is_author() && ! is_search() && ! is_404();
	}

	/**
	 * Returns the current post object.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|int|bool $postId The post ID.
	 * @return \WP_Post|null             The post object.
	 */
	public function getPost( $postId = false ) {
		$postId = is_a( $postId, 'WP_Post' ) ? $postId->ID : $postId;

		if ( aioseo()->helpers->isWooCommerceShopPage( $postId ) ) {
			return get_post( wc_get_page_id( 'shop' ) );
		}

		if ( is_front_page() || is_home() ) {
			$showOnFront = 'page' === get_option( 'show_on_front' );
			if ( $showOnFront ) {
				if ( is_front_page() ) {
					$pageOnFront = (int) get_option( 'page_on_front' );

					return get_post( $pageOnFront );
				} elseif ( is_home() ) {
					$pageForPosts = (int) get_option( 'page_for_posts' );

					return get_post( $pageForPosts );
				}
			}
		}

		// Learnpress lessons load the course. So here we need to switch to the lesson.
		$learnPressLesson = aioseo()->helpers->getLearnPressLesson();
		if ( ! $postId && $learnPressLesson ) {
			$postId = $learnPressLesson;
		}

		// Allow other plugins to filter the post ID e.g. for a special archive page.
		$postId = apply_filters( 'aioseo_get_post_id', $postId );

		// We need to check these conditions and cannot always return get_post() because we'll return the first post on archive pages (dynamic homepage, term pages, etc.).

		if (
			$this->isScreenBase( 'post' ) ||
			$postId ||
			is_singular()
		) {
			return get_post( $postId );
		}

		return null;
	}

	/**
	 * Returns the term object for the given ID or the one from the main query.
	 *
	 * @since 4.7.8
	 *
	 * @param  int    $termId   The term ID.
	 * @param  string $taxonomy The taxonomy.
	 * @return \WP_Term         The term object.
	 */
	public function getTerm( $termId = 0, $taxonomy = '' ) {
		$term = null;
		if ( $termId ) {
			$term = get_term( $termId, $taxonomy );
		} else {
			$term = get_queried_object();
		}

		// If the term is a Product Attribute, set its parent taxonomy to our fake
		// "product_attributes" taxonomy so we can use the default settings.
		if ( is_a( $term, 'WP_Term' ) && $this->isWooCommerceProductAttribute( $term->taxonomy ) ) {
			$term           = clone $term;
			$term->taxonomy = 'product_attributes';
		}

		return $term;
	}

	/**
	 * Returns the current post ID.
	 *
	 * @since 4.3.1
	 *
	 * @return int|null The post ID.
	 */
	public function getPostId() {
		$post = $this->getPost();

		return is_object( $post ) && property_exists( $post, 'ID' ) ? $post->ID : null;
	}

	/**
	 * Returns the post content after parsing it.
	 *
	 * @since 4.1.5
	 *
	 * @param  \WP_Post|int $post The post (optional).
	 * @return string             The post content.
	 */
	public function getPostContent( $post = null ) {
		$post = is_a( $post, 'WP_Post' ) ? $post : $this->getPost( $post );

		static $content = [];
		if ( isset( $content[ $post->ID ] ) ) {
			return $content[ $post->ID ];
		}

		// We need to process the content for page builders.
		$postContent = $post->post_content;
		$pageBuilder = aioseo()->helpers->getPostPageBuilderName( $post->ID );
		if ( ! empty( $pageBuilder ) ) {
			$postContent = aioseo()->standalone->pageBuilderIntegrations[ $pageBuilder ]->processContent( $post->ID, $postContent );
		}

		$postContent = is_string( $postContent ) ? $postContent : '';

		$content[ $post->ID ] = $this->theContent( $postContent );

		if ( apply_filters( 'aioseo_description_include_custom_fields', true, $post ) ) {
			$content[ $post->ID ] .= $this->theContent( $this->getPostCustomFieldsContent( $post ) );
		}

		return $content[ $post->ID ];
	}

	/**
	 * Gets the content from configured custom fields.
	 *
	 * @since 4.2.7
	 *
	 * @param  \WP_Post|int $post A post object or ID.
	 * @return string             The content.
	 */
	public function getPostCustomFieldsContent( $post = null ) {
		$post = is_a( $post, 'WP_Post' ) ? $post : $this->getPost( $post );

		if ( ! aioseo()->dynamicOptions->searchAppearance->postTypes->has( $post->post_type ) ) {
			return '';
		}

		$customFieldKeys = aioseo()->dynamicOptions->searchAppearance->postTypes->{$post->post_type}->customFields;
		if ( empty( $customFieldKeys ) ) {
			return '';
		}

		$customFieldKeys = explode( ' ', sanitize_text_field( $customFieldKeys ) );

		return aioseo()->helpers->getCustomFieldsContent( $post, $customFieldKeys );
	}

	/**
	 * Returns the post content after parsing shortcodes and blocks.
	 * We avoid using the "the_content" hook because it breaks stuff if we call it outside the loop or main query.
	 * See https://developer.wordpress.org/reference/hooks/the_content/
	 *
	 * @since 4.1.5.2
	 *
	 * @param  string $postContent The post content.
	 * @return string              The parsed post content.
	 */
	public function theContent( $postContent ) {
		if ( ! aioseo()->options->searchAppearance->advanced->runShortcodes ) {
			return $postContent;
		}

		// Because do_blocks() and do_shortcodes() can trigger conflicts, we need to clone these objects and restore them afterwards.
		// We need to clone deep to sever pointers/references because these have nested object properties.
		global $wp_query, $post; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$this->originalQuery = $this->deepClone( $wp_query ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$this->originalPost  = is_a( $post, 'WP_Post' ) ? $this->deepClone( $post ) : null;

		// The order of the function calls below is intentional and should NOT change.
		$postContent = do_blocks( $postContent );
		$postContent = wpautop( $postContent );
		$postContent = $this->doShortcodes( $postContent );

		$this->restoreWpQuery();

		return $postContent;
	}

	/**
	 * Returns the description based on the post content.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|int $post The post (optional).
	 * @return string             The description.
	 */
	public function getDescriptionFromContent( $post = null ) {
		$post = is_a( $post, 'WP_Post' ) ? $post : $this->getPost( $post );

		static $content = [];
		if ( isset( $content[ $post->ID ] ) ) {
			return $content[ $post->ID ];
		}

		$content[ $post->ID ] = '';
		if ( ! empty( $post->post_password ) ) {
			return $content[ $post->ID ];
		}

		$postContent = $this->getPostContent( $post );

		// Strip images, captions and WP oembed wrappers (e.g. YouTube URLs) from the post content.
		$postContent          = preg_replace( '/(<figure.*?\/figure>|<img.*?\/>|<div.*?class="wp-block-embed__wrapper".*?>.*?<\/div>)/s', '', (string) $postContent );
		$postContent          = str_replace( ']]>', ']]&gt;', (string) $postContent );
		$postContent          = trim( wp_strip_all_tags( strip_shortcodes( (string) $postContent ) ) );
		$content[ $post->ID ] = wp_trim_words( (string) $postContent, 55, '' );

		return $content[ $post->ID ];
	}

	/**
	 * Returns custom fields as a string.
	 *
	 * @since 4.0.6
	 *
	 * @param  \WP_Post|int $post The post.
	 * @param  array        $keys The post meta_keys to check for values.
	 * @return string             The custom field content.
	 */
	public function getCustomFieldsContent( $post = null, $keys = [] ) {
		$post = is_a( $post, 'WP_Post' ) ? $post : $this->getPost( $post );

		$customFieldContent = '';

		$acfFields = $this->getAcfContent( $post );
		foreach ( $keys as $key ) {
			// Try ACF.
			if ( isset( $acfFields[ $key ] ) && is_scalar( $acfFields[ $key ] ) ) {
				$customFieldContent .= "$acfFields[$key] ";
				continue;
			}

			// Fallback to post meta.
			$value = get_post_meta( $post->ID, $key, true );
			if ( $value && is_scalar( $value ) ) {
				$customFieldContent .= $value . ' ';
			}
		}

		return $customFieldContent;
	}

	/**
	 * Returns if the page is a special type (WooCommerce pages, Privacy page).
	 *
	 * @since 4.0.0
	 *
	 * @param  int  $postId The post ID.
	 * @return bool         If the page is special or not.
	 */
	public function isSpecialPage( $postId = 0 ) {
		$specialPages = $this->getSpecialPageIds();

		return in_array( (int) $postId, $specialPages, true );
	}

	/**
	 * Returns the ID of all special pages (e.g. homepage, blog page, WooCommerce, BuddyPress, etc.).
	 * This cannot be cached because the plugins need to be loaded first.
	 *
	 * @since 4.7.3
	 *
	 * @return array The IDs of all special pages.
	 */
	public function getSpecialPageIds() {
		$pageForPostsId         = (int) get_option( 'page_for_posts' );
		$pageForPrivacyPolicyId = (int) get_option( 'wp_page_for_privacy_policy' );
		$buddyPressPageIds      = $this->getBuddyPressPageIds();
		$wooCommercePageIds     = array_values( $this->getWooCommercePages() );

		$specialPageIds = array_merge(
			[
				$pageForPostsId,
				$pageForPrivacyPolicyId,
			],
			$buddyPressPageIds,
			$wooCommercePageIds
		);

		// Ensure all values are integers.
		$specialPageIds = array_map( 'intval', $specialPageIds );

		return $specialPageIds;
	}

	/**
	 * Returns whether a post is eligible for being analyzed by TruSEO.
	 *
	 * @since   4.6.1
	 * @version 4.7.3 Renamed from "isPageAnalysisEligible" to "isTruSeoEligible" to make it more clear.
	 *
	 * @param  int  $postId Post ID.
	 * @return bool         Whether a post is eligible for being analyzed by TruSEO.
	 */
	public function isTruSeoEligible( $postId ) {
		static $isTruSeoEnabled = null;
		if ( null === $isTruSeoEnabled ) {
			$isTruSeoEnabled = aioseo()->options->advanced->truSeo;
		}

		if ( ! $isTruSeoEnabled ) {
			return false;
		}

		static $isPostEligible = [];
		if ( isset( $isPostEligible[ $postId ] ) ) {
			return $isPostEligible[ $postId ];
		}

		// Set the default to true.
		$isPostEligible[ $postId ] = true;

		$wpPost = $this->getPost( $postId );
		if ( ! is_a( $wpPost, 'WP_Post' ) ) {
			$isPostEligible[ $postId ] = false;

			return false;
		}

		$eligiblePostTypes = $this->getTruSeoEligiblePostTypes();
		if (
			! in_array( $wpPost->post_type, $eligiblePostTypes, true ) ||
			$this->isSpecialPage( $wpPost->ID )
		) {
			$isPostEligible[ $postId ] = false;
		}

		return $isPostEligible[ $postId ];
	}

	/**
	 * Returns the post types that are eligible for TruSEO analysis.
	 *
	 * @since 4.7.3
	 *
	 * @return array The post types that are eligible for TruSEO analysis.
	 */
	public function getTruSeoEligiblePostTypes() {
		$allowedPostTypes  = aioseo()->helpers->getPublicPostTypes( true );
		$excludedPostTypes = [ 'attachment', 'aioseo-location', 'web-story' ];
		if ( class_exists( 'bbPress' ) ) {
			$excludedPostTypes = array_merge( $excludedPostTypes, [ 'forum', 'topic', 'reply' ] );
		}

		// Remove the excluded post types from the allowed ones.
		$allowedPostTypes = array_diff( $allowedPostTypes, $excludedPostTypes );

		// Now, check if the metabox is enabled and that the post type is public for each of these.
		foreach ( $allowedPostTypes as $postType ) {
			$postObjectType = get_post_type_object( $postType );
			if ( is_a( $postObjectType, 'WP_Post_Type' ) && ! $postObjectType->public ) {
				unset( $allowedPostTypes[ $postType ] );
			}

			$dynamicOptions = aioseo()->dynamicOptions->noConflict();
			if ( ! $dynamicOptions->searchAppearance->postTypes->has( $postType, false ) || ! $dynamicOptions->{$postType}->advanced->showMetaBox ) {
				// If not, unset it.
				unset( $allowedPostTypes[ $postType ] );
			}
		}

		// Considering post types get registered during various stages of the WP load process, we should not cache this.
		return $allowedPostTypes;
	}

	/**
	 * Returns the page number of the current page.
	 *
	 * @since 4.0.0
	 *
	 * @return int The page number.
	 */
	public function getPageNumber() {
		$page = get_query_var( 'page' );
		if ( ! empty( $page ) ) {
			return (int) $page;
		}

		$paged = get_query_var( 'paged' );
		if ( ! empty( $paged ) ) {
			return (int) $paged;
		}

		return 1;
	}


	/**
	 * Returns the page number for the comment page.
	 *
	 * @since 4.2.1
	 *
	 * @return int|false The page number or false if we're not on a comment page.
	 */
	public function getCommentPageNumber() {
		$cpage = get_query_var( 'cpage', null );
		if ( $this->isBlockTheme() ) {
			global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

			// For block themes we can't rely on `get_query_var()` because of {@see build_comment_query_vars_from_block()},
			// so we need to check the query directly.
			$cpage = $wp_query->query['cpage'] ?? null; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		}

		return isset( $cpage ) ? (int) $cpage : false;
	}

	/**
	 * Check if the post passed in is a valid post, not a revision or autosave.
	 *
	 * @since 4.0.5
	 *
	 * @param  \WP_Post $post                The Post object to check.
	 * @param  array    $allowedPostStatuses Allowed post statuses.
	 * @return bool                          True if valid, false if not.
	 */
	public function isValidPost( $post, $allowedPostStatuses = [ 'publish' ] ) {
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return false;
		}

		if ( ! is_object( $post ) ) {
			$post = get_post( $post );
		}

		// No post, no go.
		if ( empty( $post ) ) {
			return false;
		}

		// In order to prevent recursion, we are skipping scheduled-action posts and revisions.
		if (
			'scheduled-action' === $post->post_type ||
			'revision' === $post->post_type
		) {
			return false;
		}

		// Ensure this post has the proper post status.
		if (
			! in_array( $post->post_status, $allowedPostStatuses, true ) &&
			! in_array( 'all', $allowedPostStatuses, true )
		) {
			return false;
		}

		return true;
	}

	/**
	 * Checks whether the given URL is a valid attachment.
	 *
	 * @since 4.0.13
	 *
	 * @param  string $url The URL.
	 * @return bool        Whether the URL is a valid attachment.
	 */
	public function isValidAttachment( $url ) {
		$uploadDirUrl = aioseo()->helpers->escapeRegex( $this->getWpContentUrl() );

		return preg_match( "/$uploadDirUrl.*/", (string) $url );
	}

	/**
	 * Tries to convert an attachment URL into a post ID.
	 *
	 * This our own optimized version of attachment_url_to_postid().
	 *
	 * @since 4.0.13
	 *
	 * @param  string   $url The attachment URL.
	 * @return int|bool      The attachment ID or false if no attachment could be found.
	 */
	public function attachmentUrlToPostId( $url ) {
		$cacheName = 'attachment_url_to_post_id_' . sha1( "aioseo_attachment_url_to_post_id_$url" );

		$cachedId = aioseo()->core->cache->get( $cacheName );
		if ( $cachedId ) {
			return 'none' !== $cachedId && is_numeric( $cachedId ) ? (int) $cachedId : false;
		}

		$path          = $url;
		$uploadDirInfo = wp_get_upload_dir();

		$siteUrl   = wp_parse_url( $uploadDirInfo['url'] );
		$imagePath = wp_parse_url( $path );

		// Force the protocols to match if needed.
		if ( isset( $imagePath['scheme'] ) && ( $imagePath['scheme'] !== $siteUrl['scheme'] ) ) {
			$path = str_replace( $imagePath['scheme'], $siteUrl['scheme'], $path );
		}

		if ( ! $this->isValidAttachment( $path ) ) {
			aioseo()->core->cache->update( $cacheName, 'none' );

			return false;
		}

		if ( 0 === strpos( $path, $uploadDirInfo['baseurl'] . '/' ) ) {
			$path = substr( $path, strlen( $uploadDirInfo['baseurl'] . '/' ) );
		}

		$results = aioseo()->core->db->start( 'postmeta' )
			->select( 'post_id' )
			->where( 'meta_key', '_wp_attached_file' )
			->where( 'meta_value', $path )
			->limit( 1 )
			->run()
			->result();

		if ( empty( $results[0]->post_id ) ) {
			aioseo()->core->cache->update( $cacheName, 'none' );

			return false;
		}

		aioseo()->core->cache->update( $cacheName, $results[0]->post_id );

		return $results[0]->post_id;
	}

	/**
	 * Returns true if the request is a non-legacy REST API request.
	 * This function was copied from WooCommerce and improved.
	 *
	 * @since 4.1.2
	 *
	 * @return bool True if this is a REST API request.
	 */
	public function isRestApiRequest() {
		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return true;
		}

		global $wp_rewrite; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		if ( empty( $wp_rewrite ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			return false;
		}

		if ( empty( $_SERVER['REQUEST_URI'] ) ) {
			return false;
		}

		$restUrl = wp_parse_url( get_rest_url() );
		$restUrl = $restUrl['path'] . ( ! empty( $restUrl['query'] ) ? '?' . $restUrl['query'] : '' );

		$isRestApiRequest = ( 0 === strpos( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ), $restUrl ) );

		return apply_filters( 'aioseo_is_rest_api_request', $isRestApiRequest );
	}

	/**
	 * Checks whether the current request is an AJAX, CRON or REST request.
	 *
	 * @since 4.1.3
	 *
	 * @return bool Whether the request is an AJAX, CRON or REST request.
	 */
	public function isAjaxCronRestRequest() {
		return wp_doing_ajax() || wp_doing_cron() || $this->isRestApiRequest();
	}

	/**
	 * Check if we are in the middle of a WP-CLI call.
	 *
	 * @since 4.2.8
	 *
	 * @return bool True if we are in the WP_CLI context.
	 */
	public function isDoingWpCli() {
		return defined( 'WP_CLI' ) && WP_CLI;
	}

	/**
	 * Checks whether we're on the given screen.
	 *
	 * @since   4.0.7
	 * @version 4.3.1
	 *
	 * @param  string $screenName The screen name.
	 * @param  string $comparison Check as a prefix.
	 * @return bool               Whether we're on the given screen.
	 */
	public function isScreenBase( $screenName, $comparison = '' ) {
		$screen = $this->getCurrentScreen();
		if ( ! $screen || ! isset( $screen->base ) ) {
			return false;
		}

		if ( 'prefix' === $comparison ) {
			return 0 === stripos( $screen->base, $screenName );
		}

		return $screen->base === $screenName;
	}

	/**
	 * Returns if current screen is of a post type
	 *
	 * @since 4.0.17
	 *
	 * @param  string $postType Post type slug
	 * @return bool             True if the current screen is a post type screen.
	 */
	public function isScreenPostType( $postType ) {
		$screen = $this->getCurrentScreen();
		if ( ! $screen || ! isset( $screen->post_type ) ) {
			return false;
		}

		return $screen->post_type === $postType;
	}

	/**
	 * Returns if current screen is a post list, optionaly of a post type.
	 *
	 * @since 4.2.4
	 *
	 * @param  string $postType Post type slug.
	 * @return bool             Is a post list.
	 */
	public function isScreenPostList( $postType = '' ) {
		$screen = $this->getCurrentScreen();
		if (
			! $this->isScreenBase( 'edit' ) ||
			empty( $screen->post_type )
		) {
			return false;
		}

		if ( ! empty( $postType ) && $screen->post_type !== $postType ) {
			return false;
		}

		return true;
	}

	/**
	 * Returns if current screen is a post edit screen, optionaly of a post type.
	 *
	 * @since 4.2.4
	 *
	 * @param  string $postType Post type slug.
	 * @return bool             Is a post editing screen.
	 */
	public function isScreenPostEdit( $postType = '' ) {
		$screen = $this->getCurrentScreen();
		if (
			! $this->isScreenBase( 'post' ) ||
			empty( $screen->post_type )
		) {
			return false;
		}

		if ( ! empty( $postType ) && $screen->post_type !== $postType ) {
			return false;
		}

		return true;
	}

	/**
	 * Gets current admin screen.
	 *
	 * @since 4.0.17
	 *
	 * @return false|\WP_Screen|null
	 */
	public function getCurrentScreen() {
		if ( ! is_admin() || ! function_exists( 'get_current_screen' ) ) {
			return false;
		}

		return get_current_screen();
	}

	/**
	 * Checks whether the current site is a multisite subdomain.
	 *
	 * @since 4.1.9
	 *
	 * @return bool Whether the current site is a subdomain.
	 */
	public function isSubdomain() {
		if ( ! is_multisite() ) {
			return false;
		}

		return apply_filters( 'aioseo_multisite_subdomain', is_subdomain_install() );
	}

	/**
	 * Returns if the current page is the login or register page.
	 *
	 * @since 4.2.1
	 *
	 * @return bool Login or register page.
	 */
	public function isWpLoginPage() {
		// We can't sanitize the filename using sanitize_file_name() here because it will cause issues with custom login pages and certain plugins/themes where this function is not defined.
		$self = ! empty( $_SERVER['PHP_SELF'] ) ? sanitize_text_field( wp_unslash( $_SERVER['PHP_SELF'] ) ) : ''; // phpcs:ignore HM.Security.ValidatedSanitizedInput.InputNotSanitized
		if ( preg_match( '/wp-login\.php$|wp-register\.php$/', (string) $self ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Returns which type of WordPress page we're seeing.
	 * It will only work if {@see \WP_Query::$queried_object} has been set.
	 *
	 * @link https://developer.wordpress.org/themes/basics/template-hierarchy/#filter-hierarchy
	 *
	 * @since 4.2.8
	 *
	 * @return string|null The template type or `null` if no match.
	 */
	public function getTemplateType() {
		static $type = null;

		if ( ! empty( $type ) ) {
			return $type;
		}

		if ( is_attachment() ) {
			$type = 'attachment';
		} elseif ( is_single() ) {
			$type = 'single';
		} elseif (
			is_page() ||
			$this->isStaticPostsPage() ||
			$this->isWooCommerceShopPage()
		) {
			$type = 'page';
		} elseif ( is_author() ) { // An author page is an archive page, so it needs to be checked before `is_archive()`.
			$type = 'author';
		} elseif (
			is_tax() ||
			is_category() ||
			is_tag()
		) { // A taxonomy term page is an archive page, so it needs to be checked before `is_archive()`.
			$type = 'taxonomy';
		} elseif ( is_date() ) { // A date page is an archive page, so it needs to be checked before `is_archive()`.
			$type = 'date';
		} elseif ( is_archive() ) {
			$type = 'archive';
		} elseif ( is_home() && is_front_page() ) {
			$type = 'dynamic_home';
		} elseif ( is_search() ) {
			$type = 'search';
		}

		return $type;
	}

	/**
	 * Sets the given post as the queried object of the main query.
	 *
	 * @since 4.3.0
	 *
	 * @param  \WP_Post|int $wpPost The post object or ID.
	 * @return void
	 */
	public function setWpQueryPost( $wpPost ) {
		$wpPost = is_a( $wpPost, 'WP_Post' ) ? $wpPost : get_post( $wpPost );
		// phpcs:disable Squiz.NamingConventions.ValidVariableName
		global $wp_query, $post;
		$this->originalQuery = $this->deepClone( $wp_query );
		$this->originalPost  = is_a( $post, 'WP_Post' ) ? $this->deepClone( $post ) : null;

		$wp_query->posts                 = [ $wpPost ];
		$wp_query->post                  = $wpPost;
		$wp_query->post_count            = 1;
		$wp_query->get_queried_object_id = (int) $wpPost->ID;
		$wp_query->queried_object        = $wpPost;
		$wp_query->is_single             = true;
		$wp_query->is_singular           = true;

		if ( 'page' === $wpPost->post_type ) {
			$wp_query->is_page = true;
		}
		// phpcs:enable Squiz.NamingConventions.ValidVariableName

		$post = $wpPost;
	}

	/**
	 * Restores the main query back to the original query.
	 *
	 * @since 4.3.0
	 *
	 * @return void
	 */
	public function restoreWpQuery() {
		global $wp_query, $post; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if ( is_a( $this->originalQuery, 'WP_Query' ) ) {
			// Loop over all properties and replace the ones that have changed.
			// We want to avoid replacing the entire object because it can cause issues with other plugins.
			foreach ( $this->originalQuery as $key => $value ) {
				if ( $value !== $wp_query->{$key} ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
					$wp_query->{$key} = $value; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				}
			}
		}

		if ( is_a( $this->originalPost, 'WP_Post' ) ) {
			foreach ( $this->originalPost as $key => $value ) {
				if ( $value !== $post->{$key} ) {
					$post->{$key} = $value;
				}
			}
		}

		$this->originalQuery = null;
		$this->originalPost  = null;
	}

	/**
	 * Gets the list of theme features.
	 *
	 * @since 4.4.9
	 *
	 * @return array List of theme features.
	 */
	public function getThemeFeatures() {
		global $_wp_theme_features; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		return isset( $_wp_theme_features ) && is_array( $_wp_theme_features ) ? $_wp_theme_features : []; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
	}

	/**
	 * Returns whether the active theme is a block-based theme or not.
	 *
	 * @since 4.5.3
	 *
	 * @return bool Whether the active theme is a block-based theme or not.
	 */
	public function isBlockTheme() {
		if ( function_exists( 'wp_is_block_theme' ) ) {
			return wp_is_block_theme(); // phpcs:ignore AIOSEO.WpFunctionUse.NewFunctions.wp_is_block_themeFound
		}

		return false;
	}

	/**
	 * Retrieves the website name.
	 *
	 * @since 4.6.1
	 *
	 * @return string The website name.
	 */
	public function getWebsiteName() {
		return aioseo()->options->searchAppearance->global->schema->websiteName
			? aioseo()->tags->replaceTags( aioseo()->options->searchAppearance->global->schema->websiteName )
			: aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) );
	}
}Common/Traits/Helpers/WpMultisite.php000064400000017264151536241210013655 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

/**
 * Contains methods related to multisite.
 *
 * @since 4.2.5
 */
trait WpMultisite {
	/**
	 * Returns the ID of the network's main site.
	 *
	 * @since 4.2.5
	 *
	 * @return int The ID of the network's main site.
	 */
	public function getNetworkId() {
		if ( is_multisite() ) {
			return get_network()->site_id;
		}

		return get_current_blog_id();
	}

	/**
	 * Get a site (with aliases) by it's blog ID.
	 *
	 * @since 4.2.5
	 *
	 * @param  int          $blogId The blog ID.
	 * @return \WP_Site|null         The site.
	 */
	public function getSiteByBlogId( $blogId ) {
		$sites = $this->getSites();
		foreach ( $sites['sites'] as $site ) {
			if ( $site->blog_id === $blogId ) {
				return $site;
			}
		}

		return null;
	}

	/**
	 * Get the current site.
	 *
	 * @since 4.2.5
	 *
	 * @return \WP_Site|object A WP_Site instance of the current site or an object representing the same.
	 */
	public function getSite() {
		if ( is_multisite() ) {
			return get_site();
		}

		return (object) [
			'domain' => $this->getSiteDomain( true ),
			'path'   => $this->getHomePath( true )
		];
	}

	/**
	 * Get all sites in the multisite network.
	 *
	 * @since 4.2.5
	 *
	 * @param  int|string  $limit      The number of sites to get or 'all'.
	 * @param  int         $offset     The offset to start at.
	 * @param  null|string $searchTerm The search term to look for.
	 * @param  null|string $filter     A filter to look up sites by.
	 * @param  null|string $orderBy    The column to order results by. Defaults to null.
	 * @param  string      $orderDir   The direction to order results by. Defaults to 'DESC'.
	 * @return array                   An array of sites.
	 */
	public function getSites( $limit = 'all', $offset = 0, $searchTerm = null, $filter = 'all', $orderBy = null, $orderDir = 'DESC' ) {
		$countSites = $this->countSites();
		$sites      = get_sites( [
			'network_id' => get_current_network_id(),
			'number'     => $countSites['public'],
			'public'     => 1
		] );

		$allSites = [];
		foreach ( $sites as $site ) {
			$clonedSite           = clone $site;
			$clonedSite->adminUrl = get_admin_url( $site->blog_id );
			$clonedSite->homeUrl  = get_home_url( $site->blog_id );

			if ( $this->includeSite( $clonedSite, $filter ) ) {
				$allSites[] = $clonedSite;
			}

			// We need to look up aliases for Mercator, this checks to see if it's even enabled.
			if ( ! class_exists( '\Mercator\Mapping' ) ) {
				continue;
			}

			$aliases = $this->getSiteAliases( $site );
			foreach ( $aliases as $alias ) {
				$aliasSite               = clone $clonedSite;
				$aliasSite->domain       = $alias['domain'];
				$aliasSite->path         = '/';
				$aliasSite->alias        = $alias;
				$aliasSite->parentDomain = $site->domain;
				$aliasSite->parentPath   = $site->path;

				if ( $this->includeSite( $aliasSite, $filter ) ) {
					$allSites[] = $aliasSite;
				}
			}
		}

		// If we have a search term, let's filter down these results.
		if ( ! empty( $searchTerm ) ) {
			foreach ( $allSites as $key => $site ) {
				$keep = false;
				if (
					false !== stripos( $site->domain, $searchTerm ) ||
					false !== stripos( $site->path, $searchTerm ) ||
					false !== stripos( $site->parentDomain, $searchTerm ) ||
					false !== stripos( $site->parentPath, $searchTerm )
				) {
					$keep = true;
				}

				if ( ! $keep ) {
					unset( $allSites[ $key ] );
				}
			}
		}

		// Ordering the sites.
		if ( ! empty( $orderBy ) ) {
			usort( $allSites, function( $site1, $site2 ) use ( $orderBy, $orderDir ) {
				if ( empty( $site1->{ $orderBy } ) ) {
					return 0;
				}

				return 'ASC' === strtoupper( $orderDir )
					? ( $site1->{ $orderBy } > $site2->{ $orderBy } ? 1 : 0 )
					: ( $site1->{ $orderBy } < $site2->{ $orderBy } ? 1 : 0 );
			} );
		}

		return [
			'total' => count( $allSites ),
			'limit' => $limit,
			'sites' => 'all' === $limit ? $allSites : array_slice( $allSites, $offset, $limit )
		];
	}

	/**
	 * Count the number of sites in the network. A clone of wp_count_sites. We use this because
	 * we don't yet support WordPress 5.3. Once we do, we can revert to wp_count_sites.
	 *
	 * @since 4.4.5
	 *
	 * @return array          An array of aliases.
	 */
	private function countSites() {
		$networkId = get_current_network_id();

		$counts = [];
		$args   = [
			'network_id'    => $networkId,
			'number'        => 1,
			'fields'        => 'ids',
			'no_found_rows' => false,
		];

		$q             = new \WP_Site_Query( $args );
		$counts['all'] = $q->found_sites;

		$_args    = $args;
		$statuses = [ 'public', 'archived', 'mature', 'spam', 'deleted' ];

		foreach ( $statuses as $status ) {
			$_args            = $args;
			$_args[ $status ] = 1;

			$q                 = new \WP_Site_Query( $_args );
			$counts[ $status ] = $q->found_sites;
		}

		return $counts;
	}

	/**
	 * Filter sites based on a passed in filter. Options include 'all', 'activated' or 'deactivated'.
	 *
	 * @since 4.2.5
	 *
	 * @param  Object $site   The site object.
	 * @param  string $filter The filter to use.
	 * @return bool           The site if allowed or null if not.
	 */
	private function includeSite( $site, $filter ) {
		if ( 'all' === $filter ) {
			return true;
		}

		$siteIsActive = aioseo()->networkLicense->isSiteActive( $site );
		if (
			( 'deactivated' === $filter && ! $siteIsActive ) ||
			( 'activated' === $filter && $siteIsActive )
		) {
			return true;
		}

		return false;
	}

	/**
	 * Get an array of aliases for a WP_Site.
	 *
	 * @since 4.2.5
	 *
	 * @param  \WP_Site $site The Site.
	 * @return array          An array of aliases.
	 */
	public function getSiteAliases( $site ) {
		// We need to look up aliases for Mercator, this checks to see if it's even enabled.
		if ( ! class_exists( '\Mercator\Mapping' ) ) {
			return [];
		}

		$aliases = \Mercator\Mapping::get_by_site( $site->blog_id );
		if ( empty( $aliases ) ) {
			return [];
		}

		$aliasData = [];
		foreach ( $aliases as $alias ) {
			$aliasData[] = [
				'alias_id' => $alias->get_id(),
				'domain'   => $alias->get_domain(),
				'active'   => $alias->is_active()
			];
		}

		return $aliasData;
	}

	/**
	 * Wrapper for switch_to_blog especially for non-multisite setups.
	 *
	 * @since 4.2.5
	 *
	 * @param  int  $blogId The blog ID to switch to.
	 * @return bool         Whether the blog was switched to or not.
	 */
	public function switchToBlog( $blogId ) {
		if ( ! is_multisite() ) {
			return false;
		}

		switch_to_blog( $blogId );

		aioseo()->core->db->init();

		return true;
	}

	/**
	 * Wrapper for restore_current_blog especially for non-multisite setups.
	 *
	 * @since 4.2.5
	 *
	 * @return bool Whether the blog was restored or not.
	 */
	public function restoreCurrentBlog() {
		if ( ! is_multisite() ) {
			return false;
		}

		restore_current_blog();

		aioseo()->core->db->init();

		return true;
	}

	/**
	 * Checks if the current plugin is network activated.
	 *
	 * @since 4.2.8
	 *
	 * @param  string|null $plugin The plugin to check for network activation.
	 * @return bool                True if network activated, false if not.
	 */
	public function isPluginNetworkActivated( $plugin = null ) {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		if ( ! is_multisite() ) {
			return false;
		}

		$plugin = $plugin ? $plugin : plugin_basename( AIOSEO_FILE );

		// If the plugin is not network activated, then no it's not network licensed.
		if ( ! is_plugin_active_for_network( $plugin ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Returns the current site domain.
	 *
	 * @since 4.7.7
	 *
	 * @return string The site domain.
	 */
	public function getMultiSiteDomain() {
		$site = aioseo()->helpers->getSite();

		return $site->domain . $site->path;
	}
}Common/Traits/Helpers/WpUri.php000064400000040107151536241210012425 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Contains all WordPress related URL, URI, path, slug, etc. related helper methods.
 *
 * @since 4.1.4
 */
trait WpUri {
	/**
	 * Returns the site domain.
	 *
	 * @since 4.0.0
	 *
	 * @param  bool   $unfiltered Whether to get the unfiltered value.
	 * @return string             The site's domain.
	 */
	public function getSiteDomain( $unfiltered = false ) {
		return wp_parse_url( $this->getHomeUrl( $unfiltered ), PHP_URL_HOST );
	}

	/**
	 * Returns the site URL.
	 * NOTE: For multisites inside a sub-directory, this returns the URL for the main site.
	 * This is intentional.
	 *
	 * @since 4.0.0
	 *
	 * @param  bool   $unfiltered Whether to get the unfiltered value.
	 * @return string             The site's domain.
	 */
	public function getSiteUrl( $unfiltered = false ) {
		$homeUrl = $this->getHomeUrl( $unfiltered );

		return wp_parse_url( $homeUrl, PHP_URL_SCHEME ) . '://' . wp_parse_url( $homeUrl, PHP_URL_HOST );
	}

	/**
	 * Returns the current URL.
	 *
	 * @since 4.0.0
	 *
	 * @param  boolean $canonical Whether or not to get the canonical URL.
	 * @return string             The URL.
	 */
	public function getUrl( $canonical = false ) {
		$url = '';
		if ( is_singular() ) {
			$objectId = aioseo()->helpers->getPostId();

			if ( $canonical ) {
				$url = aioseo()->helpers->wpGetCanonicalUrl( $objectId );
			}

			if ( ! $url ) {
				// wp_get_canonical_url() returns false if the post isn't published.
				// Therefore, we must to fall back to the permalink if the post isn't published, e.g. draft post or attachment (inherit).
				$url = get_permalink( $objectId );
			}
		}

		if ( $url ) {
			return $url;
		}

		global $wp;
		// Permalink url without the query string.
		$url = user_trailingslashit( home_url( $wp->request ) );

		// If permalinks are not being used we need to append the query string to the home url.
		if ( ! $this->usingPermalinks() ) {
			$url = home_url( ! empty( $wp->query_string ) ? '?' . $wp->query_string : '' );
		}

		return $url;
	}

	/**
	 * Gets the canonical URL for the current page/post.
	 *
	 * @since 4.0.0
	 *
	 * @return string $url The canonical URL.
	 */
	public function canonicalUrl() {
		$queriedObject = get_queried_object(); // Don't use our getTerm helper here.
		$hash          = md5( wp_json_encode( $queriedObject ?? [] ) );

		static $url = [];
		if ( isset( $url[ $hash ] ) ) {
			return $url[ $hash ];
		}

		if ( is_404() || is_search() ) {
			$url[ $hash ] = apply_filters( 'aioseo_canonical_url', '' );

			return $url[ $hash ];
		}

		$metaData = [];
		$post     = $this->getPost();
		if ( $post ) {
			$metaData = aioseo()->meta->metaData->getMetaData( $post );
		}

		if ( is_category() || is_tag() || is_tax() ) {
			$metaData     = aioseo()->meta->metaData->getMetaData( $queriedObject );
			$url[ $hash ] = get_term_link( $queriedObject, $queriedObject->taxonomy ?? '' );

			// If the term link is a WP_Error, set it to an empty string.
			if ( ! is_string( $url[ $hash ] ) ) {
				$url[ $hash ] = '';
			}

			// Add pagination to the URL. We need to do this here because get_term_link() doesn't handle pagination.
			// We'll strip it further down if no pagination for canonical is enabled.
			if ( $this->getPageNumber() > 1 ) {
				$url[ $hash ] = user_trailingslashit( rtrim( $url[ $hash ], '/' ) . '/page/' . $this->getPageNumber() );
			}
		}

		if ( $metaData && ! empty( $metaData->canonical_url ) ) {
			$url[ $hash ] = apply_filters( 'aioseo_canonical_url', $this->makeUrlAbsolute( $metaData->canonical_url ) );

			return $url[ $hash ];
		}

		if ( BuddyPressIntegration::isComponentPage() ) {
			$url[ $hash ] = aioseo()->standalone->buddyPress->component->getMeta( 'canonical' );
		}

		if ( empty( $url[ $hash ] ) || is_wp_error( $url[ $hash ] ) ) {
			$url[ $hash ] = $this->getUrl( true );
		}

		$pageNumber = $this->getPageNumber();
		if (
			in_array( 'noPaginationForCanonical', aioseo()->internalOptions->deprecatedOptions, true ) &&
			aioseo()->options->deprecated->searchAppearance->advanced->noPaginationForCanonical
		) {
			if ( 1 < $pageNumber ) {
				if ( $this->usingPermalinks() ) {
					// Replace /page/3 and /page/3/.
					$url[ $hash ] = preg_replace( "@(?<=/)page/$pageNumber(/|)$@", '', (string) $url[ $hash ] );
					// Replace /3 and /3/.
					$url[ $hash ] = preg_replace( "@(?<=/)$pageNumber(/|)$@", '', (string) $url[ $hash ] );
				} else {
					// Replace /?page_id=457&paged=1 and /?page_id=457&page=1.
					$url[ $hash ] = aioseo()->helpers->urlRemoveQueryParameter( $url[ $hash ], [ 'page', 'paged' ] );
				}
			}

			// Comment pages.
			$url[ $hash ] = preg_replace( '/(?<=\/)comment-page-\d+\/*(#comments)*$/', '', (string) $url[ $hash ] );
		}

		$url[ $hash ] = $this->maybeRemoveTrailingSlash( $url[ $hash ] );

		// Get rid of /amp at the end of the URL.
		if (
			aioseo()->helpers->isAmpPage() &&
			! apply_filters( 'aioseo_disable_canonical_url_amp', false )
		) {
			$url[ $hash ] = preg_replace( '/\/amp$/', '', (string) $url[ $hash ] );
			$url[ $hash ] = preg_replace( '/\/amp\/$/', '/', (string) $url[ $hash ] );
		}

		$url[ $hash ] = apply_filters( 'aioseo_canonical_url', $url[ $hash ] );

		return $url[ $hash ];
	}

	/**
	 * Sanitizes a given domain.
	 *
	 * @since 4.0.0
	 *
	 * @param  string       $domain The domain to sanitize.
	 * @return mixed|string         The sanitized domain.
	 */
	public function sanitizeDomain( $domain ) {
		$domain = trim( $domain );
		$domain = strtolower( $domain );
		if ( 0 === strpos( $domain, 'http://' ) ) {
			$domain = substr( $domain, 7 );
		} elseif ( 0 === strpos( $domain, 'https://' ) ) {
			$domain = substr( $domain, 8 );
		}
		$domain = untrailingslashit( $domain );

		return $domain;
	}

	/**
	 * Remove trailing slashes if not set in the permalink structure.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $url The original URL.
	 * @return string      The adjusted URL.
	 */
	public function maybeRemoveTrailingSlash( $url ) {
		$permalinks = get_option( 'permalink_structure' );
		if ( $permalinks && ( ! is_home() || ! is_front_page() ) ) {
			$trailing = substr( $permalinks, -1 );
			if ( '/' !== $trailing ) {
				$url = untrailingslashit( $url );
			}
		}

		// Don't slash urls with query args.
		if ( false !== strpos( $url, '?' ) ) {
			$url = untrailingslashit( $url );
		}

		return $url;
	}

	/**
	 * Removes image dimensions from the slug of a URL.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $url The image URL.
	 * @return string      The formatted image URL.
	 */
	public function removeImageDimensions( $url ) {
		return $this->isValidAttachment( $url ) ? preg_replace( '#(-[0-9]*x[0-9]*|-scaled)#', '', (string) $url ) : $url;
	}

	/**
	 * Returns the URL for the WP content folder.
	 *
	 * @since 4.0.5
	 *
	 * @return string The URL.
	 */
	public function getWpContentUrl() {
		$info = wp_get_upload_dir();

		return isset( $info['baseurl'] ) ? $info['baseurl'] : '';
	}

	/**
	* Retrieves a post by its given path.
	* Based on the built-in get_page_by_path() function, but only checks ancestry if the post type is actually hierarchical.
	*
	* @since 4.1.4
	*
	* @param  string       $path     The path.
	* @param  string       $output   The output type. OBJECT, ARRAY_A, or ARRAY_N.
	* @param  string|array $postType The post type(s) to check against.
	* @return object|false           The post or false on failure.
	*/
	public function getPostByPath( $path, $output = OBJECT, $postType = 'page' ) {
		$lastChanged = wp_cache_get_last_changed( 'aioseo_posts_by_path' );
		$hash        = md5( $path . serialize( $postType ) );
		$cacheKey    = "get_page_by_path:$hash:$lastChanged";
		$cached      = wp_cache_get( $cacheKey, 'aioseo_posts_by_path' );

		if ( false !== $cached ) {
			// Special case: '0' is a bad `$path`.
			if ( '0' === $cached || 0 === $cached ) {
				return false;
			}

			return get_post( $cached, $output );
		}

		$path          = rawurlencode( urldecode( $path ) );
		$path          = str_replace( '%2F', '/', $path );
		$path          = str_replace( '%20', ' ', $path );
		$parts         = explode( '/', trim( $path, '/' ) );
		$reversedParts = array_reverse( $parts );
		$postNames     = "'" . implode( "','", $parts ) . "'";

		$postTypes = is_array( $postType ) ? $postType : [ $postType, 'attachment' ];
		$postTypes = "'" . implode( "','", $postTypes ) . "'";

		$posts = aioseo()->core->db->start( 'posts' )
			->select( 'ID, post_name, post_parent, post_type' )
			->whereRaw( "post_name in ( $postNames )" )
			->whereRaw( "post_type in ( $postTypes )" )
			->run()
			->result();

		$foundId = 0;
		foreach ( $posts as $post ) {
			if ( $post->post_name === $reversedParts[0] ) {
				$count = 0;
				$p     = $post;

				// Loop through the given path parts from right to left, ensuring each matches the post ancestry.
				while ( 0 !== (int) $p->post_parent && isset( $posts[ $p->post_parent ] ) ) {
					$count++;
					$parent = $posts[ $p->post_parent ];
					if ( ! isset( $reversedParts[ $count ] ) || $parent->post_name !== $reversedParts[ $count ] ) {
						break;
					}
					$p = $parent;
				}

				if (
					0 === (int) $p->post_parent &&
					( ! is_post_type_hierarchical( $p->post_type ) || count( $reversedParts ) === $count + 1 ) &&
					$p->post_name === $reversedParts[ $count ]
				) {
					$foundId = $post->ID;
					if ( $post->post_type === $postType ) {
						break;
					}
				}
			}
		}

		// We cache misses as well as hits.
		wp_cache_set( $cacheKey, $foundId, 'aioseo_posts_by_path' );

		return $foundId ? get_post( $foundId, $output ) : false;
	}

	/**
	 * Validates a URL.
	 *
	 * @since 4.1.2
	 *
	 * @param  string $url The url.
	 * @return bool        Is it a valid/safe url.
	 */
	public function isUrl( $url ) {
		return esc_url_raw( $url ) === $url;
	}

	/**
	 * Retrieves the parameters for a given URL.
	 *
	 * @since 4.1.5
	 *
	 * @param  string $url          The url.
	 * @return array                The parameters.
	 */
	public function getParametersFromUrl( $url ) {
		$parsedUrl  = wp_parse_url( wp_unslash( $url ) );
		$parameters = [];

		if ( empty( $parsedUrl['query'] ) ) {
			return [];
		}

		wp_parse_str( $parsedUrl['query'], $parameters );

		return $parameters;
	}

	/**
	 * Adds a leading slash to an url.
	 *
	 * @since 4.1.8
	 *
	 * @param  string $url The url.
	 * @return string      The url with a leading slash.
	 */
	public function leadingSlashIt( $url ) {
		return '/' . ltrim( $url, '/' );
	}

	/**
	 * Returns the path from a permalink.
	 * This function will help get the correct path from WP installations in subfolders.
	 *
	 * @since 4.1.8
	 *
	 * @param  string $permalink A permalink from get_permalink().
	 * @return string            The path without the home_url().
	 */
	public function getPermalinkPath( $permalink ) {
		// We want to get this value straight from the DB to prevent plugins like WPML from filtering it.
		// This will otherwise mess with things like license activation requests and redirects.
		$homeUrl = $this->getHomeUrl( true );

		return $this->leadingSlashIt( str_replace( $homeUrl, '', $permalink ) );
	}

	/**
	 * Changed if permalinks are different and the before wasn't
	 * the site url (we don't want to redirect the site URL).
	 *
	 * @since 4.2.3
	 *
	 * @param  string  $before The URL before the change.
	 * @param  string  $after  The URL after the change.
	 * @return boolean         True if the permalink has changed.
	 */
	public function hasPermalinkChanged( $before, $after ) {
		// Check it's not redirecting from the root.
		if ( $this->getHomePath() === $before || '/' === $before ) {
			return false;
		}

		// Are the URLs the same?
		return ( $before !== $after );
	}

	/**
	 * Retrieve the home path.
	 *
	 * @since 4.2.3
	 *
	 * @param  bool   $unfiltered Whether to get the unfiltered value.
	 * @return string              The home path.
	 */
	public function getHomePath( $unfiltered = false ) {
		$path = wp_parse_url( $this->getHomeUrl( $unfiltered ), PHP_URL_PATH );

		return $path ? trailingslashit( $path ) : '/';
	}

	/**
	 * Returns the home URL.
	 *
	 * @since 4.7.3
	 *
	 * @param  bool   $unfiltered Whether to get the unfiltered value.
	 * @return string             The home URL.
	 */
	private function getHomeUrl( $unfiltered = false ) {
		$homeUrl = home_url();
		if ( $unfiltered ) {
			// We want to get this value straight from the DB to prevent plugins like WPML from filtering it.
			// This will otherwise mess with things like license activation requests and redirects.
			$homeUrl = get_option( 'home' );
		}

		return $homeUrl;
	}

	/**
	 * Checks if the given URL is an internal URL for the current site.
	 *
	 * @since 4.2.6
	 *
	 * @param  string $urlToCheck The URL to check.
	 * @return bool               Whether the given URL is an internal one.
	 */
	public function isInternalUrl( $urlToCheck ) {
		$parsedHomeUrl    = wp_parse_url( home_url() );
		$parsedUrlToCheck = wp_parse_url( $urlToCheck );

		return ! empty( $parsedHomeUrl['host'] ) && ! empty( $parsedUrlToCheck['host'] )
			? $parsedHomeUrl['host'] === $parsedUrlToCheck['host']
			: false;
	}

	/**
	 * Helper for the rest url.
	 *
	 * @since 4.4.9
	 *
	 * @return string
	 */
	public function getRestUrl() {
		$restUrl = get_rest_url();

		if ( aioseo()->helpers->isWpmlActive() ) {
			global $sitepress;

			// Replace the rest url 'all' language prefix so our rest calls don't fail.
			if (
				is_object( $sitepress ) &&
				method_exists( $sitepress, 'get_current_language' ) &&
				method_exists( $sitepress, 'get_default_language' ) &&
				'all' === $sitepress->get_current_language()
			) {
				$restUrl = str_replace(
					get_home_url( null, '/all/' ),
					get_home_url( null, '/' . $sitepress->get_default_language() . '/' ),
					$restUrl
				);
			}
		}

		return $restUrl;
	}

	/**
	 * Exclude the home path from a full path.
	 *
	 * @since   1.2.3 Moved from aioseo-redirects.
	 * @version 4.5.8
	 *
	 * @param  string $path The original path.
	 * @return string       The path without WP's home path.
	 */
	public function excludeHomePath( $path ) {
		return preg_replace( '@^' . $this->getHomePath() . '@', '/', (string) $path );
	}

	/**
	 * Get the canonical URL for a post.
	 * This is a duplicate of wp_get_canonical_url() with a fix for issue #6372 where
	 * posts with paginated comment pages return the wrong canonical URL due to how WordPress sets the cpage var.
	 * We can remove this once trac ticket 60806 is resolved.
	 *
	 * @since 4.6.9
	 *
	 * @param  \WP_Post|int|null $post The post object or ID.
	 * @return string|false            The post's canonical URL, or false if the post is not published.
	 */
	public function wpGetCanonicalUrl( $post = null ) {
		$post = get_post( $post );

		if ( ! $post ) {
			return false;
		}

		if ( 'publish' !== $post->post_status ) {
			return false;
		}

		$canonical_url = get_permalink( $post ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		// If a canonical is being generated for the current page, make sure it has pagination if needed.
		if ( get_queried_object_id() === $post->ID ) {
			$page = get_query_var( 'page', 0 );
			if ( $page >= 2 ) {
				if ( ! get_option( 'permalink_structure' ) ) {
					$canonical_url = add_query_arg( 'page', $page, $canonical_url ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				} else {
					$canonical_url = trailingslashit( $canonical_url ) . user_trailingslashit( $page, 'single_paged' ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				}
			}

			$cpage = aioseo()->helpers->getCommentPageNumber(); // We're calling our own function here to get the correct cpage number.
			if ( $cpage ) {
				$canonical_url = get_comments_pagenum_link( $cpage ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			}
		}

		return apply_filters( 'get_canonical_url', $canonical_url, $post ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
	}

	/**
	 * Checks if permalinks are enabled.
	 *
	 * @since 4.8.3
	 *
	 * @return bool Whether permalinks are enabled.
	 */
	public function usingPermalinks() {
		global $wp_rewrite; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		return $wp_rewrite->using_permalinks();  // phpcs:ignore Squiz.NamingConventions.ValidVariableName
	}
}Common/Traits/NetworkOptions.php000064400000003700151536241210012760 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits;

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

/**
 * Options trait.
 *
 * @since 4.2.5
 */
trait NetworkOptions {
	/**
	 * Initializes the options.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	protected function init() {
		if ( ! is_multisite() ) {
			return;
		}

		aioseo()->helpers->switchToBlog( $this->helpers->getNetworkId() );

		$dbOptions = json_decode( get_option( $this->optionsName ), true );
		if ( empty( $dbOptions ) ) {
			$dbOptions = [];
		}

		$this->defaultsMerged = aioseo()->helpers->arrayReplaceRecursive( $this->defaults, $this->defaultsMerged );

		$options = aioseo()->helpers->arrayReplaceRecursive(
			$this->defaultsMerged,
			$this->addValueToValuesArray( $this->defaultsMerged, $dbOptions )
		);

		aioseo()->core->optionsCache->setOptions( $this->optionsName, $options );

		aioseo()->helpers->restoreCurrentBlog();
	}

	/**
	 * Sanitizes, then saves the options to the database.
	 *
	 * @since 4.2.5
	 *
	 * @param  array $newOptions The new options to sanitize, then save.
	 * @return void
	 */
	public function sanitizeAndSave( $newOptions ) {
		if ( ! is_multisite() ) {
			return;
		}

		if ( ! is_array( $newOptions ) ) {
			return;
		}

		$this->init();

		aioseo()->helpers->switchToBlog( $this->helpers->getNetworkId() );

		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$dbOptions     = aioseo()->helpers->arrayReplaceRecursive(
			$cachedOptions,
			$this->addValueToValuesArray( $cachedOptions, $newOptions, [], true )
		);

		// Tools.
		if ( ! empty( $newOptions['tools'] ) ) {
			if ( isset( $newOptions['tools']['robots']['rules'] ) ) {
				$dbOptions['tools']['robots']['rules']['value'] = $this->sanitizeField( $newOptions['tools']['robots']['rules'], 'array' );
			}
		}

		aioseo()->core->optionsCache->setOptions( $this->optionsName, $dbOptions );
		$this->save( true );

		aioseo()->helpers->restoreCurrentBlog();
	}
}Common/Traits/Options.php000064400000067142151536241210011420 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits;

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

/**
 * Options trait.
 *
 * @since 4.0.0
 */
trait Options {
	/**
	 * Whether or not this instance is a clone.
	 *
	 * @since 4.1.4
	 *
	 * @var boolean
	 */
	public $isClone = false;

	/**
	 * Whether or not the options need to be saved to the DB.
	 *
	 * @since 4.1.4
	 *
	 * @var string
	 */
	public $shouldSave = false;

	/**
	 * The name to lookup the options with.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	public $optionsName = '';

	/**
	 * Holds the localized options.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $localized = [];

	/**
	 * The group key we are working with.
	 *
	 * @since 4.0.0
	 *
	 * @var string|null
	 */
	protected $groupKey = null;

	/**
	 * Allows us to create unlimited number of sub groups.
	 * Like so: options->breadcrumbs->templates->taxonomies->tags->template
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $subGroups = [];

	/**
	 * Any arguments associated with a dynamic method.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $arguments = [];

	/**
	 * The value to set on an option.
	 *
	 * @since 4.0.0
	 *
	 * @var mixed
	 */
	protected $value = null;

	/**
	 * Holds all the defaults after they have been merged.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $defaultsMerged = [];

	/**
	 * Holds a redirect link or slug.
	 *
	 * @since 4.0.17
	 *
	 * @var string
	 */
	protected $screenRedirection = '';

	/**
	 * Retrieve an option or null if missing.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name      The name of the property that is missing on the class.
	 * @param  array  $arguments The arguments passed into the method.
	 * @return mixed             The value from the options or default/null.
	 */
	public function __call( $name, $arguments = [] ) {
		if ( $this->setGroupKey( $name, $arguments ) ) {
			return $this;
		}

		// If we need to set a sub-group, do that now.
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$defaults      = $cachedOptions[ $this->groupKey ];
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				$defaults = $defaults[ $subGroup ];
			}
		}

		if ( ! isset( $defaults[ $name ] ) ) {
			$this->resetGroups();

			return ! empty( $this->arguments[0] )
				? $this->arguments[0]
				: $this->getDefault( $name, false );
		}

		if ( empty( $defaults[ $name ]['type'] ) ) {
			return $this->setSubGroup( $name );
		}

		$value = isset( $cachedOptions[ $this->groupKey ][ $name ]['value'] )
			? $cachedOptions[ $this->groupKey ][ $name ]['value']
			: (
				! empty( $this->arguments[0] )
					? $this->arguments[0]
					: $this->getDefault( $name, false )
			);

		$this->resetGroups();

		return $value;
	}

	/**
	 * Retrieve an option or null if missing.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name The name of the property that is missing on the class.
	 * @return mixed        The value from the options or default/null.
	 */
	public function __get( $name ) {
		if ( 'type' === $name ) {
			$name = '_aioseo_type';
		}

		if ( $this->setGroupKey( $name ) ) {
			return $this;
		}

		// If we need to set a sub-group, do that now.
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$defaults      = $cachedOptions[ $this->groupKey ];
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				$defaults = $defaults[ $subGroup ];
			}
		}

		if ( ! isset( $defaults[ $name ] ) ) {
			$default = $this->getDefault( $name, false );
			$this->resetGroups();

			return $default;
		}

		if ( ! isset( $defaults[ $name ]['type'] ) ) {
			return $this->setSubGroup( $name );
		}

		$value = $this->getDefault( $name, false );

		if ( isset( $defaults[ $name ]['value'] ) ) {
			$preserveHtml = ! empty( $defaults[ $name ]['preserveHtml'] );
			if ( $preserveHtml ) {
				if ( is_array( $defaults[ $name ]['value'] ) ) {
					foreach ( $defaults[ $name ]['value'] as $k => $v ) {
						$defaults[ $name ]['value'][ $k ] = html_entity_decode( $v, ENT_NOQUOTES );
					}
				} else {
					$defaults[ $name ]['value'] = html_entity_decode( $defaults[ $name ]['value'], ENT_NOQUOTES );
				}
			}
			$value = $defaults[ $name ]['value'];

			// Localized value.
			if ( isset( $defaults[ $name ]['localized'] ) ) {
				$localizedKey = $this->groupKey;
				if ( ! empty( $this->subGroups ) ) {
					foreach ( $this->subGroups as $subGroup ) {
						$localizedKey .= '_' . $subGroup;
					}
				}

				$localizedKey .= '_' . $name;

				if ( ! empty( $this->localized[ $localizedKey ] ) ) {
					$value = $this->localized[ $localizedKey ];
					// We need to rebuild the keywords as a json string.
					if ( 'keywords' === $name ) {
						$keywords = explode( ',', $value );
						foreach ( $keywords as $k => $keyword ) {
							$keywords[ $k ] = [
								'label' => $keyword,
								'value' => $keyword
							];
						}

						$value = wp_json_encode( $keywords );
					}
				}
			}
		}

		$this->resetGroups();

		return $value;
	}

	/**
	 * Sets the option value and saves to the database.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name  The name of the option.
	 * @param  mixed  $value The value to set.
	 * @return void
	 */
	public function __set( $name, $value ) {
		if ( $this->setGroupKey( $name, null, $value ) ) {
			return $this;
		}

		// If we need to set a sub-group, do that now.
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$defaults      = json_decode( wp_json_encode( $cachedOptions[ $this->groupKey ] ), true );
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				$defaults = &$defaults[ $subGroup ];
			}
		}

		if ( ! isset( $defaults[ $name ] ) ) {
			$default = $this->getDefault( $name, false );
			$this->resetGroups();

			return $default;
		}

		if ( empty( $defaults[ $name ]['type'] ) ) {
			return $this->setSubGroup( $name );
		}

		$preserveHtml               = ! empty( $defaults[ $name ]['preserveHtml'] );
		$localized                  = ! empty( $defaults[ $name ]['localized'] );
		$defaults[ $name ]['value'] = $this->sanitizeField( $this->value, $defaults[ $name ]['type'], $preserveHtml );

		if ( $localized ) {
			$localizedKey = $this->groupKey;
			if ( ! empty( $this->subGroups ) ) {
				foreach ( $this->subGroups as $subGroup ) {
					$localizedKey .= '_' . $subGroup;
				}
			}

			$localizedKey  .= '_' . $name;
			$localizedValue = $defaults[ $name ]['value'];

			if ( 'keywords' === $name ) {
				$keywords = json_decode( $localizedValue ) ? json_decode( $localizedValue ) : [];
				foreach ( $keywords as $k => $keyword ) {
					$keywords[ $k ] = $keyword->value;
				}

				$localizedValue = implode( ',', $keywords );
			}

			$this->localized[ $localizedKey ] = $localizedValue;
			update_option( $this->optionsName . '_localized', $this->localized );
		}

		$originalDefaults = json_decode( wp_json_encode( $cachedOptions[ $this->groupKey ] ), true );
		$pointer          = &$originalDefaults; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		foreach ( $this->subGroups as $subGroup ) {
			$pointer = &$pointer[ $subGroup ];
		}
		$pointer = $defaults;

		$cachedOptions[ $this->groupKey ] = $originalDefaults;
		aioseo()->core->optionsCache->setOptions( $this->optionsName, $cachedOptions );

		$this->resetGroups();

		$this->update();
	}

	/**
	 * Checks if an option is set or returns null if not.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name The name of the option.
	 * @return mixed        True or null.
	 */
	public function __isset( $name ) {
		if ( $this->setGroupKey( $name ) ) {
			return $this;
		}

		// If we need to set a sub-group, do that now.
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$defaults      = $cachedOptions[ $this->groupKey ];
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				$defaults = &$defaults[ $subGroup ];
			}
		}

		if ( ! isset( $defaults[ $name ] ) ) {
			$this->resetGroups();

			return false;
		}

		if ( empty( $defaults[ $name ]['type'] ) ) {
			return $this->setSubGroup( $name );
		}

		$value = isset( $defaults[ $name ]['value'] )
			? false === empty( $defaults[ $name ]['value'] )
			: false;

			$this->resetGroups();

		return $value;
	}

	/**
	 * Unsets the option value and saves to the database.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name  The name of the option.
	 * @return void
	 */
	public function __unset( $name ) {
		if ( $this->setGroupKey( $name ) ) {
			return $this;
		}

		// If we need to set a sub-group, do that now.
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$defaults      = json_decode( wp_json_encode( $cachedOptions[ $this->groupKey ] ), true );
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				$defaults = &$defaults[ $subGroup ];
			}
		}

		if ( ! isset( $defaults[ $name ] ) ) {
			$this->groupKey  = null;
			$this->subGroups = [];

			return;
		}

		if ( empty( $defaults[ $name ]['type'] ) ) {
			return $this->setSubGroup( $name );
		}

		if ( ! isset( $defaults[ $name ]['value'] ) ) {
			return;
		}

		unset( $defaults[ $name ]['value'] );

		$cachedOptions[ $this->groupKey ] = $defaults;
		aioseo()->core->optionsCache->setOptions( $this->optionsName, $cachedOptions );

		$this->resetGroups();

		$this->update();
	}

	/**
	 * Retrieves all options.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $include Keys to include.
	 * @param  array $exclude Keys to exclude.
	 * @return array          An array of options.
	 */
	public function all( $include = [], $exclude = [] ) {
		$originalGroupKey  = $this->groupKey;
		$originalSubGroups = $this->subGroups;

		// Make sure our dynamic options have loaded.
		$this->init();

		// Refactor options.
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$refactored    = $this->convertOptionsToValues( $cachedOptions );

		$this->groupKey = null;

		if ( ! $originalGroupKey ) {
			return $this->allFiltered( $refactored, $include, $exclude );
		}

		if ( empty( $originalSubGroups ) ) {
			$all = $refactored[ $originalGroupKey ];

			return $this->allFiltered( $all, $include, $exclude );
		}

		$returnable = &$refactored[ $originalGroupKey ]; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		foreach ( $originalSubGroups as $subGroup ) {
			$returnable = &$returnable[ $subGroup ];
		}

		$this->resetGroups();

		return $this->allFiltered( $returnable, $include, $exclude );
	}

	/**
	 * Reset the current option to the defaults.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $include Keys to include.
	 * @param  array $exclude Keys to exclude.
	 * @return void
	 */
	public function reset( $include = [], $exclude = [] ) {
		$originalGroupKey  = $this->groupKey;
		$originalSubGroups = $this->subGroups;

		// Make sure our dynamic options have loaded.
		$this->init();

		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );

		// If we don't have a group key set, it means we want to reset everything.
		if ( empty( $originalGroupKey ) ) {
			$groupKeys = array_keys( $cachedOptions );
			foreach ( $groupKeys as $groupKey ) {
				$this->groupKey = $groupKey;
				$this->reset();
			}

			// Since we just finished resetting everything, we can return early.
			return;
		}

		// If we need to set a sub-group, do that now.
		$keys     = array_merge( [ $originalGroupKey ], $originalSubGroups );
		$defaults = json_decode( wp_json_encode( $cachedOptions[ $originalGroupKey ] ), true );
		if ( ! empty( $originalSubGroups ) ) {
			foreach ( $originalSubGroups as $subGroup ) {
				$defaults = $defaults[ $subGroup ];
			}
		}

		// Refactor options.
		$resetValues = $this->resetValues( $defaults, $this->defaultsMerged, $keys, $include, $exclude );
		// We need to call our helper method instead of the built-in array_replace_recursive() function here because we want values to be replaced with empty arrays.
		$defaults = aioseo()->helpers->arrayReplaceRecursive( $defaults, $resetValues );

		$originalDefaults = json_decode( wp_json_encode( $cachedOptions[ $originalGroupKey ] ), true );
		$pointer          = &$originalDefaults; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		foreach ( $originalSubGroups as $subGroup ) {
			$pointer = &$pointer[ $subGroup ];
		}
		$pointer = $defaults;

		$cachedOptions[ $originalGroupKey ] = $originalDefaults;
		aioseo()->core->optionsCache->setOptions( $this->optionsName, $cachedOptions );

		$this->resetGroups();

		$this->update();
	}

	/**
	 * Resets all values in a group.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $defaults The defaults array we are currently working with.
	 * @param  array $values   The values to adjust.
	 * @param  array $keys     Parent keys for the current group we are parsing.
	 * @param  array $include  Keys to include.
	 * @param  array $exclude  Keys to exclude.
	 * @return array           The modified values.
	 */
	protected function resetValues( $values, $defaults, $keys = [], $include = [], $exclude = [] ) {
		$values = $this->allFiltered( $values, $include, $exclude );
		foreach ( $values as $key => $value ) {
			$option = $this->isAnOption( $key, $defaults, $keys );
			if ( $option ) {
				$values[ $key ]['value'] = isset( $values[ $key ]['default'] ) ? $values[ $key ]['default'] : null;
				continue;
			}

			$keys[]         = $key;
			$values[ $key ] = $this->resetValues( $value, $defaults, $keys );
			array_pop( $keys );
		}

		return $values;
	}

	/**
	 * Checks if the current group has an option or group.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $optionOrGroup The option or group to look for.
	 * @param  bool   $resetGroups   Whether or not to reset the groups after.
	 * @return bool                  True if it does, false if not.
	 */
	public function has( $optionOrGroup = '', $resetGroups = true ) {
		if ( 'type' === $optionOrGroup ) {
			$optionOrGroup = '_aioseo_type';
		}

		$originalGroupKey  = $this->groupKey;
		$originalSubGroups = $this->subGroups;

		static $hasInitialized = false;
		if ( ! $hasInitialized ) {
			$hasInitialized = true;
			$this->init();
		}

		// If we need to set a sub-group, do that now.
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$defaults      = $originalGroupKey ? $cachedOptions[ $originalGroupKey ] : $cachedOptions;
		if ( ! empty( $originalSubGroups ) ) {
			foreach ( $originalSubGroups as $subGroup ) {
				$defaults = $defaults[ $subGroup ];
			}
		}

		if ( $resetGroups ) {
			$this->resetGroups();
		}

		if ( ! empty( $defaults[ $optionOrGroup ] ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Filters the results based on passed in array.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $all     All the options to filter.
	 * @param  array $include Keys to include.
	 * @param  array $exclude Keys to exclude.
	 * @return array          The filtered options.
	 */
	private function allFiltered( $all, $include, $exclude ) {
		if ( ! empty( $include ) ) {
			return array_intersect_ukey( $all, $include, function ( $key1, $key2 ) use ( $include ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
				if ( in_array( $key1, $include, true ) ) {
					return 0;
				}

				return -1;
			} );
		}

		if ( ! empty( $exclude ) ) {
			return array_diff_ukey( $all, $exclude, function ( $key1, $key2 ) use ( $exclude ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
				if ( ! in_array( $key1, $exclude, true ) ) {
					return 0;
				}

				return -1;
			} );
		}

		return $all;
	}

	/**
	 * Gets the default value for an option.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name The option name.
	 * @return mixed        The default value.
	 */
	public function getDefault( $name, $resetGroups = true ) {
		$defaults = $this->defaultsMerged[ $this->groupKey ];
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				if ( empty( $defaults[ $subGroup ] ) ) {
					return null;
				}
				$defaults = $defaults[ $subGroup ];
			}
		}

		if ( $resetGroups ) {
			$this->resetGroups();
		}

		if ( ! isset( $defaults[ $name ] ) ) {
			return null;
		}

		if ( empty( $defaults[ $name ]['type'] ) ) {
			return $this->setSubGroup( $name );
		}

		return isset( $defaults[ $name ]['default'] )
			? $defaults[ $name ]['default']
			: null;
	}

	/**
	 * Gets the defaults options.
	 *
	 * @since 4.1.3
	 *
	 * @return array An array of dafults.
	 */
	public function getDefaults() {
		return $this->defaults;
	}

	/**
	 * Updates the options in the database.
	 *
	 * @since 4.0.0
	 *
	 * @param  string     $optionsName An optional option name to update.
	 * @param  string     $defaults    The defaults to filter the options by.
	 * @param  array|null $options     An optional options array.
	 * @return void
	 */
	public function update( $optionsName = null, $defaults = null, $options = null ) {
		$optionsName = empty( $optionsName ) ? $this->optionsName : $optionsName;
		$defaults    = empty( $defaults ) ? $this->defaults : $defaults;

		// First, we need to filter our options.
		$options = $this->filterOptions( $defaults, $options );

		// Refactor options.
		$refactored = $this->convertOptionsToValues( $options );

		$this->resetGroups();

		// The following needs to happen here (possibly a clone) as well as in the main instance.
		$originalInstance = $this->getOriginalInstance();

		// Update the DB options.
		aioseo()->core->optionsCache->setDb( $optionsName, $refactored );

		// Force a save here and in the main class.
		$this->shouldSave             = true;
		$originalInstance->shouldSave = true;
	}

	/**
	 * Updates the options in the database.
	 *
	 * @since 4.1.4
	 *
	 * @param  boolean $force       Whether or not to force an immediate save.
	 * @param  string  $optionsName An optional option name to update.
	 * @param  string  $defaults    The defaults to filter the options by.
	 * @return void
	 */
	public function save( $force = false, $optionsName = null, $defaults = null ) {
		if ( ! $this->shouldSave && ! $force ) {
			return;
		}

		$optionsName = empty( $optionsName ) ? $this->optionsName : $optionsName;
		$defaults    = empty( $defaults ) ? $this->defaults : $defaults;

		$this->update( $optionsName );

		// First, we need to filter our options.
		$options = $this->filterOptions( $defaults );

		// Refactor options.
		$refactored = $this->convertOptionsToValues( $options );

		$this->resetGroups();

		update_option( $optionsName, wp_json_encode( $refactored ) );
	}

	/**
	 * Filter options to match our defaults.
	 *
	 * @since 4.0.0
	 *
	 * @param  array      $defaults The defaults to use in filtering.
	 * @param  array|null $options  An optional options array.
	 * @return array                An array of filtered options.
	 */
	public function filterOptions( $defaults, $options = null ) {
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$options       = ! empty( $options ) ? $options : json_decode( wp_json_encode( $cachedOptions ), true );

		return $this->filterRecursively( $options, $defaults );
	}

	/**
	 * Filters options in a loop.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $options  An array of options to filter.
	 * @param  array $defaults An array of defaults to filter against.
	 * @return array           A filtered array of options.
	 */
	public function filterRecursively( $options, $defaults ) {
		if ( ! is_array( $options ) ) {
			return $options;
		}

		foreach ( $options as $key => $value ) {
			if ( ! isset( $defaults[ $key ] ) ) {
				unset( $options[ $key ] );
				continue;
			}

			if ( ! isset( $value['type'] ) ) {
				$options[ $key ] = $this->filterRecursively( $options[ $key ], $defaults[ $key ] );
				continue;
			}
		}

		return $options;
	}

	/**
	 * Sanitizes the value before allowing it to be saved.
	 *
	 * @since 4.0.0
	 *
	 * @param  mixed  $value The value to sanitize.
	 * @param  string $type  The type of sanitization to do.
	 * @return mixed         The sanitized value.
	 */
	public function sanitizeField( $value, $type, $preserveHtml = false ) {
		switch ( $type ) {
			case 'boolean':
				return (bool) $value;
			case 'html':
				return sanitize_textarea_field( $value );
			case 'string':
				return sanitize_text_field( $value );
			case 'number':
				return intval( $value );
			case 'array':
				$array = [];
				foreach ( (array) $value as $k => $v ) {
					if ( is_array( $v ) ) {
						$array[ $k ] = $this->sanitizeField( $v, 'array' );
						continue;
					}

					$array[ $k ] = sanitize_text_field( $preserveHtml ? htmlspecialchars( $v, ENT_NOQUOTES, 'UTF-8' ) : $v );
				}

				return $array;
			case 'float':
				return floatval( $value );
		}
	}

	/**
	 * Checks to see if we need to set the group key. If so, will return true.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $name      The name of the option to set.
	 * @param  array   $arguments Any arguments needed if this was a method called.
	 * @param  mixed   $value     The value if we are setting an option.
	 * @return boolean            Whether or not we need to set the group key.
	 */
	private function setGroupKey( $name, $arguments = null, $value = null ) {
		$this->arguments = $arguments;
		$this->value     = $value;

		if ( empty( $this->groupKey ) ) {
			$groups = array_keys( $this->defaultsMerged );
			if ( in_array( $name, $groups, true ) ) {
				$this->groupKey = $name;

				return true;
			}

			$this->groupKey = $groups[0];
		}

		return false;
	}

	/**
	 * Sets the sub group key. Will set and return the instance.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $name      The name of the option to set.
	 * @param  array   $arguments Any arguments needed if this was a method called.
	 * @param  mixed   $value     The value if we are setting an option.
	 * @return object             The options object.
	 */
	private function setSubGroup( $name, $arguments = null, $value = null ) {
		if ( ! is_null( $arguments ) ) {
			$this->arguments = $arguments;
		}
		if ( ! is_null( $value ) ) {
			$this->value = $value;
		}

		$defaults = $this->defaultsMerged[ $this->groupKey ];
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				$defaults = $defaults[ $subGroup ];
			}
		}

		$groups = array_keys( $defaults );
		if ( in_array( $name, $groups, true ) ) {
			$this->subGroups[] = $name;
		}

		return $this;
	}

	/**
	 * Reset groups.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function resetGroups() {
		$this->groupKey  = null;
		$this->subGroups = [];
	}

	/**
	 * Converts an associative array of values into a structure
	 * that works with our defaults.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $defaults The defaults array we are currently working with.
	 * @param  array $values   The values to adjust.
	 * @param  array $keys     Parent keys for the current group we are parsing.
	 * @param  bool  $sanitize Whether or not we should sanitize the value.
	 * @return array           The modified values.
	 */
	protected function addValueToValuesArray( $defaults, $values, $keys = [], $sanitize = false ) {
		foreach ( $values as $key => $value ) {
			$option = $this->isAnOption( $key, $defaults, $keys );
			if ( $option ) {
				$preserveHtml   = ! empty( $option['preserveHtml'] );
				$newValue       = $sanitize ? $this->sanitizeField( $value, $option['type'], $preserveHtml ) : $value;
				$values[ $key ] = [
					'value' => $newValue
				];

				// If this is a localized string, let's save it to our localized options.
				if ( $sanitize && ! empty( $option['localized'] ) ) {
					$localizedKey = '';
					foreach ( $keys as $k ) {
						$localizedKey .= $k . '_';
					}

					$localizedKey  .= $key;
					$localizedValue = $newValue;
					if ( 'keywords' === $key ) {
						$keywords = json_decode( $localizedValue ) ? json_decode( $localizedValue ) : [];
						foreach ( $keywords as $k => $keyword ) {
							$keywords[ $k ] = $keyword->value;
						}

						$localizedValue = implode( ',', $keywords );
					}

					$this->localized[ $localizedKey ] = $localizedValue;
				}
				continue;
			}

			if ( ! is_array( $value ) ) {
				continue;
			}

			$keys[]         = $key;
			$values[ $key ] = $this->addValueToValuesArray( $defaults, $value, $keys, $sanitize );
			array_pop( $keys );
		}

		return $values;
	}

	/**
	 * Our options array has values (or defaults).
	 * This method converts them to how we would store them
	 * in the DB.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $options The options array.
	 * @return array           The converted options array.
	 */
	public function convertOptionsToValues( $options, $optionKey = 'type' ) {
		foreach ( $options as $key => $value ) {
			if ( ! is_array( $value ) ) {
				continue;
			}

			if ( ! isset( $value[ $optionKey ] ) ) {
				$options[ $key ] = $this->convertOptionsToValues( $value, $optionKey );
				continue;
			}

			$options[ $key ] = null;

			if ( isset( $value['value'] ) ) {
				$preserveHtml = ! empty( $value['preserveHtml'] );
				if ( $preserveHtml ) {
					if ( is_array( $value['value'] ) ) {
						foreach ( $value['value'] as $k => $v ) {
							$value['value'][ $k ] = html_entity_decode( $v, ENT_NOQUOTES );
						}
					} else {
						$value['value'] = html_entity_decode( $value['value'], ENT_NOQUOTES );
					}
				}
				$options[ $key ] = $value['value'];
				continue;
			}

			if ( isset( $value['default'] ) ) {
				$options[ $key ] = $value['default'];
			}
		}

		return $options;
	}

	/**
	 * This checks to see if the current array/option is really an option
	 * and not just another parent with a subgroup.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $key      The current array key we are working with.
	 * @param  array  $defaults The defaults array to check against.
	 * @param  array  $keys     The parent keys to loop through.
	 * @return bool             Whether or not this is an option.
	 */
	private function isAnOption( $key, $defaults, $keys ) {
		if ( ! empty( $keys ) ) {
			foreach ( $keys as $k ) {
				$defaults = isset( $defaults[ $k ] ) ? $defaults[ $k ] : [];
			}
		}

		if ( isset( $defaults[ $key ]['type'] ) ) {
			return $defaults[ $key ];
		}

		return false;
	}

	/**
	 * Refreshes the options from the database.
	 *
	 * We need this during the migration to update through clones.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function refresh() {
		// Reset DB options to clear the cache.
		aioseo()->core->optionsCache->resetDb();
		$this->init();
	}

	/**
	 * Returns the DB options.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $optionsName The options name.
	 * @return array               The options.
	 */
	public function getDbOptions( $optionsName ) {
		$cache = aioseo()->core->optionsCache->getDb( $optionsName );
		if ( empty( $cache ) ) {
			$options = json_decode( get_option( $optionsName ), true );
			$options = ! empty( $options ) ? $options : [];

			// Set the cache.
			aioseo()->core->optionsCache->setDb( $optionsName, $options );
		}

		return aioseo()->core->optionsCache->getDb( $optionsName );
	}

	/**
	 * In order to not have a conflict, we need to return a clone.
	 *
	 * @since 4.0.0
	 *
	 * @param  bool   $reInitialize Whether to reinitialize on the clone.
	 * @return object               The cloned Options object.
	 */
	public function noConflict( $reInitialize = false ) {
		$class          = clone $this;
		$class->isClone = true;

		if ( $reInitialize ) {
			$class->init();
		}

		return $class;
	}

	/**
	 * Get original instance. Since this could be a cloned object, let's get the original instance.
	 *
	 * @since 4.1.4
	 *
	 * @return self
	 */
	public function getOriginalInstance() {
		if ( ! $this->isClone ) {
			return $this;
		}

		$class      = new \ReflectionClass( get_called_class() );
		$optionName = aioseo()->helpers->toCamelCase( $class->getShortName() );

		if ( isset( aioseo()->{ $optionName } ) ) {
			return aioseo()->{ $optionName };
		}

		return $this;
	}
}Common/Traits/SocialProfiles.php000064400000011740151536241210012674 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits;

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

/**
 * Trait that handles the social profiles.
 *
 * @since 4.2.2
 */
trait SocialProfiles {
	/**
	 * List of base URLs.
	 *
	 * @since 4.2.2
	 *
	 * @var array
	 */
	private $baseUrls = [
		'facebookPageUrl' => 'https://facebook.com/',
		'twitterUrl'      => 'https://x.com/',
		'instagramUrl'    => 'https://instagram.com/',
		'tiktokUrl'       => 'https://tiktok.com/@',
		'pinterestUrl'    => 'https://pinterest.com/',
		'youtubeUrl'      => 'https://youtube.com/',
		'linkedinUrl'     => 'https://linkedin.com/in/',
		'tumblrUrl'       => 'https://tumblr.com/',
		'yelpPageUrl'     => 'https://yelp.com/biz/',
		'soundCloudUrl'   => 'https://soundcloud.com/',
		'wikipediaUrl'    => 'https://en.wikipedia.org/wiki/',
		'myspaceUrl'      => 'https://myspace.com/',
		'wordPressUrl'    => 'https://profiles.wordpress.org/',
		'blueskyUrl'      => 'https://bsky.app/profile/',
		'threadsUrl'      => 'https://threads.com/@',
	];

	/**
	 * Returns the profiles of the organization, set under Social Networks.
	 *
	 * @since 4.2.2
	 *
	 * @return array List of social profiles.
	 */
	protected function getOrganizationProfiles() {
		$socialProfiles = [
			'facebookPageUrl' => aioseo()->options->social->profiles->urls->facebookPageUrl,
			'twitterUrl'      => aioseo()->options->social->profiles->urls->twitterUrl,
			'instagramUrl'    => aioseo()->options->social->profiles->urls->instagramUrl,
			'tiktokUrl'       => aioseo()->options->social->profiles->urls->tiktokUrl,
			'pinterestUrl'    => aioseo()->options->social->profiles->urls->pinterestUrl,
			'youtubeUrl'      => aioseo()->options->social->profiles->urls->youtubeUrl,
			'linkedinUrl'     => aioseo()->options->social->profiles->urls->linkedinUrl,
			'tumblrUrl'       => aioseo()->options->social->profiles->urls->tumblrUrl,
			'yelpPageUrl'     => aioseo()->options->social->profiles->urls->yelpPageUrl,
			'soundCloudUrl'   => aioseo()->options->social->profiles->urls->soundCloudUrl,
			'wikipediaUrl'    => aioseo()->options->social->profiles->urls->wikipediaUrl,
			'myspaceUrl'      => aioseo()->options->social->profiles->urls->myspaceUrl,
			'wordPressUrl'    => aioseo()->options->social->profiles->urls->wordPressUrl,
			'blueskyUrl'      => aioseo()->options->social->profiles->urls->blueskyUrl,
			'threadsUrl'      => aioseo()->options->social->profiles->urls->threadsUrl,
		];

		if ( aioseo()->options->social->profiles->sameUsername->enable ) {
			$username          = aioseo()->options->social->profiles->sameUsername->username;
			$includedPlatforms = aioseo()->options->social->profiles->sameUsername->included;

			foreach ( $this->baseUrls as $platformKey => $baseUrl ) {
				if ( ! in_array( $platformKey, $includedPlatforms, true ) ) {
					continue;
				}

				$socialProfiles[ $platformKey ] = $baseUrl . $username;
			}
		}

		if ( aioseo()->options->social->profiles->additionalUrls ) {
			$additionalUrls = preg_split( '/\n|\r|\r\n/', (string) aioseo()->options->social->profiles->additionalUrls );
			$socialProfiles = array_merge( $socialProfiles, $additionalUrls );
		}

		if ( ! aioseo()->options->social->facebook->general->showAuthor ) {
			unset( $socialProfiles['facebookPageUrl'] );
		}

		if ( ! aioseo()->options->social->twitter->general->showAuthor ) {
			unset( $socialProfiles['twitterUrl'] );
		}

		return array_filter( $socialProfiles );
	}

	/**
	 * Returns the profiles of the given user, set under the User Profile.
	 *
	 * @since 4.2.2
	 *
	 * @param  int   $userId The user ID.
	 * @return array         List of social profiles.
	 */
	protected function getUserProfiles( $userId ) {
		$socialProfiles = $this->baseUrls;
		foreach ( $socialProfiles as $platformKey => $v ) {
			$metaName                       = 'aioseo_' . aioseo()->helpers->toSnakeCase( $platformKey );
			$socialProfiles[ $platformKey ] = get_user_meta( $userId, $metaName, true );
		}

		$sameUsernameData = get_user_meta( $userId, 'aioseo_profiles_same_username', true );
		if ( is_array( $sameUsernameData ) && (bool) $sameUsernameData['enable'] ) {
			foreach ( $this->baseUrls as $platform => $baseUrl ) {
				if ( ! in_array( $platform, $sameUsernameData['included'], true ) ) {
					continue;
				}

				$socialProfiles[ $platform ] = $baseUrl . $sameUsernameData['username'];
			}
		}

		$additionalUrls = get_user_meta( $userId, 'aioseo_profiles_additional_urls', true );
		if ( $additionalUrls ) {
			$additionalUrls = preg_split( '/\n|\r|\r\n/', (string) $additionalUrls );
			foreach ( $additionalUrls as $additionalUrl ) {
				// We need to set a random key because otherwise we'll override the ones from the organization.
				$socialProfiles[ uniqid() ] = $additionalUrl;
			}
		}

		if ( ! aioseo()->options->social->facebook->general->showAuthor ) {
			unset( $socialProfiles['facebookPageUrl'] );
		}

		if ( ! aioseo()->options->social->twitter->general->showAuthor ) {
			unset( $socialProfiles['twitterUrl'] );
		}

		return array_filter( $socialProfiles );
	}
}Common/Utils/Access.php000064400000020005151536241210011003 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

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

class Access {
	/**
	 * Capabilities for our users.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $capabilities = [
		'aioseo_about_us_page',
		'aioseo_dashboard',
		'aioseo_feature_manager_settings',
		'aioseo_general_settings',
		'aioseo_link_assistant_settings',
		'aioseo_local_seo_settings',
		'aioseo_page_advanced_settings',
		'aioseo_page_ai_content_settings',
		'aioseo_page_analysis',
		'aioseo_page_general_settings',
		'aioseo_page_link_assistant_settings',
		'aioseo_page_local_seo_settings',
		'aioseo_page_redirects_manage',
		'aioseo_page_schema_settings',
		'aioseo_page_seo_revisions_settings',
		'aioseo_page_social_settings',
		'aioseo_page_writing_assistant_settings',
		'aioseo_redirects_manage',
		'aioseo_redirects_settings',
		'aioseo_search_appearance_settings',
		'aioseo_search_statistics_settings',
		'aioseo_seo_analysis_settings',
		'aioseo_setup_wizard',
		'aioseo_sitemap_settings',
		'aioseo_social_networks_settings',
		'aioseo_tools_settings'
	];

	/**
	 * Whether we're already updating the roles during this request.
	 *
	 * @since 4.2.7
	 *
	 * @var bool
	 */
	protected $isUpdatingRoles = false;

	/**
	 * Roles we check capabilities against.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $roles = [
		'superadmin'    => 'superadmin',
		'administrator' => 'administrator',
		'editor'        => 'editor',
		'author'        => 'author',
		'contributor'   => 'contributor'
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		// First load the roles so that we can pull the roles from the other plugins.
		add_action( 'plugins_loaded', [ $this, 'setRoles' ], 999 );

		// Load later again so that we can pull the roles lately registered.
		// This needs to run before 1000 so that our update migrations and other hook callbacks can pull the roles.
		add_action( 'init', [ $this, 'setRoles' ], 999 );
	}

	/**
	 * Sets the roles on the instance.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	public function setRoles() {
		$adminRoles = [];
		$allRoles   = aioseo()->helpers->getUserRoles();
		foreach ( $allRoles as $roleName => $wpRole ) {
			$role = get_role( $roleName );
			if ( $this->isAdmin( $roleName ) || $role->has_cap( 'publish_posts' ) ) {
				$adminRoles[ $roleName ] = $roleName;
			}
		}

		$this->roles = array_merge( $this->roles, $adminRoles );
	}

	/**
	 * Adds capabilities into WordPress for the current user.
	 * Only on activation or settings saved.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addCapabilities() {
		$this->isUpdatingRoles = true;

		foreach ( $this->roles as $wpRole => $role ) {
			$roleObject = get_role( $wpRole );
			if ( ! is_object( $roleObject ) ) {
				continue;
			}

			if ( $this->isAdmin( $role ) ) {
				$roleObject->add_cap( 'aioseo_manage_seo' );
			}

			if ( $roleObject->has_cap( 'edit_posts' ) ) {
				$postCapabilities = [
					'aioseo_page_advanced_settings',
					'aioseo_page_ai_content_settings',
					'aioseo_page_analysis',
					'aioseo_page_general_settings',
					'aioseo_page_schema_settings',
					'aioseo_page_social_settings'
				];

				foreach ( $postCapabilities as $capability ) {
					$roleObject->add_cap( $capability );
				}
			}
		}
	}

	/**
	 * Removes capabilities for any unknown role.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function removeCapabilities() {
		$this->isUpdatingRoles = true;

		// Clear out capabilities for unknown roles.
		$wpRoles  = wp_roles();
		$allRoles = $wpRoles->roles;
		foreach ( $allRoles as $key => $wpRole ) {
			$checkRole = is_multisite() ? 'superadmin' : 'administrator';
			if ( $checkRole === $key ) {
				continue;
			}

			if ( array_key_exists( $key, $this->roles ) ) {
				continue;
			}

			$role = get_role( $key );
			if ( ! is_a( $role, 'WP_Role' ) || ! is_array( $role->capabilities ) ) {
				continue;
			}

			// We don't need to remove the capabilities for administrators.
			if ( $this->isAdmin( $key ) ) {
				continue;
			}

			foreach ( $this->capabilities as $capability ) {
				if ( $role->has_cap( $capability ) ) {
					$role->remove_cap( $capability );
				}
			}

			$role->remove_cap( 'aioseo_manage_seo' );
		}
	}

	/**
	 * Checks if the current user has the capability.
	 *
	 * @since 4.0.0
	 *
	 * @param  string|array $capability The capability to check against.
	 * @param  string|null  $checkRole  A role to check against.
	 * @return bool                     Whether or not the user has this capability.
	 */
	public function hasCapability( $capability, $checkRole = null ) {
		if ( $this->isAdmin( $checkRole ) ) {
			return true;
		}

		$canPublishOrEdit = $this->can( 'publish_posts', $checkRole ) || $this->can( 'edit_posts', $checkRole );
		if ( ! $canPublishOrEdit ) {
			return false;
		}

		if ( is_array( $capability ) ) {
			foreach ( $capability as $cap ) {
				if ( false !== strpos( $cap, 'aioseo_page_' ) ) {
					return true;
				}
			}

			return false;
		}

		return false !== strpos( $capability, 'aioseo_page_' );
	}

	/**
	 * Gets all the capabilities for the current user.
	 *
	 * @since 4.0.0
	 *
	 * @param  string|null $role A role to check against.
	 * @return array             An array of capabilities.
	 */
	public function getAllCapabilities( $role = null ) {
		$capabilities = [];
		foreach ( $this->getCapabilityList() as $capability ) {
			$capabilities[ $capability ] = $this->hasCapability( $capability, $role );
		}

		$capabilities['aioseo_admin']         = $this->isAdmin( $role );
		$capabilities['aioseo_manage_seo']    = $this->isAdmin( $role );
		$capabilities['aioseo_about_us_page'] = $this->canManage( $role );

		return $capabilities;
	}

	/**
	 * Returns the capability list.
	 *
	 * @return 4.1.3
	 *
	 * @return array An array of capabilities.
	 */
	public function getCapabilityList() {
		return $this->capabilities;
	}

	/**
	 * If the current user is an admin, or superadmin, they have access to all caps regardless.
	 *
	 * @since 4.0.0
	 *
	 * @param  string|null $role The role to check admin privileges if we have one.
	 * @return bool              Whether not the user/role is an admin.
	 */
	public function isAdmin( $role = null ) {
		if ( $role ) {
			if ( ( is_multisite() && 'superadmin' === $role ) || 'administrator' === $role ) {
				return true;
			}

			return false;
		}

		if ( ! function_exists( 'wp_get_current_user' ) ) {
			return false;
		}

		if ( ( is_multisite() && current_user_can( 'superadmin' ) ) || current_user_can( 'administrator' ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Check if the passed in role can publish posts.
	 *
	 * @since 4.0.9
	 *
	 * @param  string  $capability The capability to check against.
	 * @param  string  $role       The role to check.
	 * @return boolean             True if the role can publish.
	 */
	protected function can( $capability, $role ) {
		if ( empty( $role ) ) {
			return current_user_can( $capability );
		}

		$wpRoles  = wp_roles();
		$allRoles = $wpRoles->roles;
		foreach ( $allRoles as $key => $wpRole ) {
			if ( $key === $role ) {
				$r = get_role( $key );
				if ( $r->has_cap( $capability ) ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Checks if the current user can manage AIOSEO.
	 *
	 * @since 4.0.0
	 *
	 * @param  string|null $checkRole A role to check against.
	 * @return bool                   Whether or not the user can manage AIOSEO.
	 */
	public function canManage( $checkRole = null ) {
		return $this->isAdmin( $checkRole );
	}

	/**
	 * Gets all options that the user does not have access to manage.
	 *
	 * @since 4.1.3
	 *
	 * @return array An array with the option names.
	 */
	public function getNotAllowedOptions() {
		return [];
	}

	/**
	 * Gets all page fields that the user does not have access to manage.
	 *
	 * @since 4.1.3
	 *
	 * @return array An array with the field names.
	 */
	public function getNotAllowedPageFields() {
		return [];
	}

	/**
	 * Returns Roles.
	 *
	 * @since 4.0.17
	 *
	 * @return array An array of role names.
	 */
	public function getRoles() {
		return $this->roles;
	}
}Common/Utils/ActionScheduler.php000064400000021010151536241210012653 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

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

/**
 * Handles all Action Scheduler related tasks.
 *
 * @since 4.0.0
 */
class ActionScheduler {
	/**
	 * The Action Scheduler group.
	 *
	 * @since   4.1.5
	 * @version 4.2.7
	 *
	 * @var string
	 */
	private $actionSchedulerGroup = 'aioseo';

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'action_scheduler_after_execute', [ $this, 'cleanup' ], 1000, 2 );

		// Note: \ActionScheduler is first loaded on `plugins_loaded` action hook.
		add_action( 'plugins_loaded', [ $this, 'maybeRecreateTables' ] );
	}

	/**
	 * Maybe register the `{$table_prefix}_actionscheduler_{$suffix}` tables with WordPress and create them if needed.
	 * Hooked into `plugins_loaded` action hook.
	 *
	 * @since 4.2.7
	 *
	 * @return void
	 */
	public function maybeRecreateTables() {
		if ( ! is_admin() ) {
			return;
		}

		if ( ! apply_filters( 'action_scheduler_enable_recreate_data_store', true ) ) {
			return;
		}

		if (
			! class_exists( 'ActionScheduler' ) ||
			! class_exists( 'ActionScheduler_HybridStore' ) ||
			! class_exists( 'ActionScheduler_StoreSchema' ) ||
			! class_exists( 'ActionScheduler_LoggerSchema' )
		) {
			return;
		}

		$store = \ActionScheduler::store();

		if ( ! is_a( $store, 'ActionScheduler_HybridStore' ) ) {
			$store = new \ActionScheduler_HybridStore();
		}

		$tableList = [
			'actionscheduler_actions',
			'actionscheduler_logs',
			'actionscheduler_groups',
			'actionscheduler_claims',
		];

		foreach ( $tableList as $tableName ) {
			if ( ! aioseo()->core->db->tableExists( $tableName ) ) {
				add_action( 'action_scheduler/created_table', [ $store, 'set_autoincrement' ], 10, 2 );

				$storeSchema  = new \ActionScheduler_StoreSchema();
				$loggerSchema = new \ActionScheduler_LoggerSchema();
				$storeSchema->register_tables( true );
				$loggerSchema->register_tables( true );

				remove_action( 'action_scheduler/created_table', [ $store, 'set_autoincrement' ] );

				break;
			}
		}
	}

	/**
	 * Cleans up the Action Scheduler tables after one of our actions completes.
	 * Hooked into `action_scheduler_after_execute` action hook.
	 *
	 * @since 4.0.10
	 *
	 * @param  int                     $actionId The action ID processed.
	 * @param  \ActionScheduler_Action $action   Class instance.
	 * @return void
	 */
	public function cleanup( $actionId, $action = null ) {
		if (
			// Bail if this isn't one of our actions or if we're in a dev environment.
			'aioseo' !== $action->get_group() ||
			( defined( 'WP_ENVIRONMENT_TYPE' ) && 'development' === WP_ENVIRONMENT_TYPE ) ||
			// Bail if the tables don't exist.
			! aioseo()->core->db->tableExists( 'actionscheduler_actions' ) ||
			! aioseo()->core->db->tableExists( 'actionscheduler_groups' ) ||
			// Bail if it hasn't been long enough since the last cleanup.
			aioseo()->core->cache->get( 'action_scheduler_log_cleanup' )
		) {
			return;
		}

		$prefix = aioseo()->core->db->db->prefix;

		// Clean up logs associated with entries in the actions table.
		aioseo()->core->db->execute(
			"DELETE al FROM {$prefix}actionscheduler_logs as al
			JOIN {$prefix}actionscheduler_actions as aa on `aa`.`action_id` = `al`.`action_id`
			LEFT JOIN {$prefix}actionscheduler_groups as ag on `ag`.`group_id` = `aa`.`group_id`
			WHERE (
				(`ag`.`slug` = '{$this->actionSchedulerGroup}' AND `aa`.`status` IN ('complete', 'failed', 'canceled'))
				OR
				(`aa`.`hook` LIKE 'aioseo_%' AND `aa`.`group_id` = 0 AND `aa`.`status` IN ('complete', 'failed', 'canceled'))
			);"
		);

		// Clean up actions.
		aioseo()->core->db->execute(
			"DELETE aa FROM {$prefix}actionscheduler_actions as aa
			LEFT JOIN {$prefix}actionscheduler_groups as ag on `ag`.`group_id` = `aa`.`group_id`
			WHERE (
				(`ag`.`slug` = '{$this->actionSchedulerGroup}' AND `aa`.`status` IN ('complete', 'failed', 'canceled'))
				OR
				(`aa`.`hook` LIKE 'aioseo_%' AND `aa`.`group_id` = 0 AND `aa`.`status` IN ('complete', 'failed', 'canceled'))
			);"
		);

		// Set a transient to prevent this from running again for a while.
		aioseo()->core->cache->update( 'action_scheduler_log_cleanup', true, DAY_IN_SECONDS );
	}

	/**
	 * Schedules a single action at a specific time in the future.
	 *
	 * @since   4.0.13
	 * @version 4.2.7
	 *
	 * @param  string  $actionName    The action name.
	 * @param  int     $time          The time to add to the current time.
	 * @param  array   $args          Args passed down to the action.
	 * @param  bool    $forceSchedule Whether we should schedule a new action regardless of whether one is already set.
	 * @return boolean                Whether the action was scheduled.
	 */
	public function scheduleSingle( $actionName, $time = 0, $args = [], $forceSchedule = false ) {
		try {
			if ( $forceSchedule || ! $this->isScheduled( $actionName, $args ) ) {
				as_schedule_single_action( time() + $time, $actionName, $args, $this->actionSchedulerGroup );

				return true;
			}
		} catch ( \RuntimeException $e ) {
			// Nothing needs to happen.
		}

		return false;
	}

	/**
	 * Checks if a given action is already scheduled.
	 *
	 * @since   4.0.13
	 * @version 4.2.7
	 *
	 * @param  string  $actionName The action name.
	 * @param  array   $args       Args passed down to the action.
	 * @return boolean             Whether the action is already scheduled.
	 */
	public function isScheduled( $actionName, $args = [] ) {
		$scheduledActions = $this->getScheduledActions();

		$hooks = [];
		foreach ( $scheduledActions as $action ) {
			$hooks[] = $action->hook;
		}

		$isScheduled = in_array( $actionName, array_filter( $hooks ), true );
		if ( empty( $args ) ) {
			return $isScheduled;
		}

		// If there are arguments, we need to check if the action is scheduled with the same arguments.
		if ( $isScheduled ) {
			foreach ( $scheduledActions as $action ) {
				if ( $action->hook === $actionName ) {
					foreach ( $args as $k => $v ) {
						if ( ! isset( $action->args[ $k ] ) || $action->args[ $k ] !== $v ) {
							continue;
						}

						return true;
					}
				}
			}
		}

		return false;
	}

	/**
	 * Returns all AIOSEO scheduled actions.
	 *
	 * @since 4.7.7
	 *
	 * @return array The scheduled actions.
	 */
	private function getScheduledActions() {
		static $scheduledActions = null;
		if ( null !== $scheduledActions ) {
			return $scheduledActions;
		}

		$scheduledActions = aioseo()->core->db->start( 'actionscheduler_actions as aa' )
			->select( 'aa.hook, aa.args' )
			->join( 'actionscheduler_groups as ag', 'ag.group_id', 'aa.group_id' )
			->where( 'ag.slug', $this->actionSchedulerGroup )
			->whereIn( 'status', [ 'pending', 'in-progress', 'past-due' ] )
			->run()
			->result();

		// Decode the args.
		foreach ( $scheduledActions as $key => $action ) {
			$scheduledActions[ $key ]->args = json_decode( $action->args, true );
		}

		return $scheduledActions;
	}

	/**
	 * Unschedule an action.
	 *
	 * @since   4.1.4
	 * @version 4.2.7
	 *
	 * @param  string $actionName The action name to unschedule.
	 * @param  array  $args       Args passed down to the action.
	 * @return void
	 */
	public function unschedule( $actionName, $args = [] ) {
		try {
			if ( as_next_scheduled_action( $actionName, $args ) ) {
				as_unschedule_action( $actionName, $args, $this->actionSchedulerGroup );
			}
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Schedules a recurring action.
	 *
	 * @since   4.1.5
	 * @version 4.2.7
	 *
	 * @param  string  $actionName The action name.
	 * @param  int     $time       The seconds to add to the current time.
	 * @param  int     $interval   The interval in seconds.
	 * @param  array   $args       Args passed down to the action.
	 * @return boolean             Whether the action was scheduled.
	 */
	public function scheduleRecurrent( $actionName, $time, $interval = 60, $args = [] ) {
		try {
			if ( ! $this->isScheduled( $actionName, $args ) ) {
				as_schedule_recurring_action( time() + $time, $interval, $actionName, $args, $this->actionSchedulerGroup );

				return true;
			}
		} catch ( \RuntimeException $e ) {
			// Nothing needs to happen.
		}

		return false;
	}

	/**
	 * Schedule a single async action.
	 *
	 * @since   4.1.6
	 * @version 4.2.7
	 *
	 * @param  string $actionName The name of the action.
	 * @param  array  $args       Any relevant arguments.
	 * @return void
	 */
	public function scheduleAsync( $actionName, $args = [] ) {
		try {
			// Run the task immediately using an async action.
			as_enqueue_async_action( $actionName, $args, $this->actionSchedulerGroup );
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}
}Common/Utils/Addons.php000064400000071455151536241210011031 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

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

use AIOSEO\Plugin\Common\Utils;

/**
 * Contains helper methods specific to the addons.
 *
 * @since 4.0.0
 */
class Addons {
	/**
	 * Holds our list of loaded addons.
	 *
	 * @since 4.1.0
	 *
	 * @var array
	 */
	protected $loadedAddons = [];

	/**
	 * The addons URL.
	 *
	 * @since 4.1.8
	 *
	 * @var string
	 */
	protected $addonsUrl = 'https://licensing-cdn.aioseo.com/keys/lite/all-in-one-seo-pack-pro.json';

	/**
	 * The main Image SEO addon class.
	 *
	 * @since 4.4.2
	 *
	 * @var \AIOSEO\Plugin\Addon\ImageSeo\ImageSeo
	 */
	private $imageSeo = null;

	/**
	 * The main Index Now addon class.
	 *
	 * @since 4.4.2
	 *
	 * @var \AIOSEO\Plugin\Addon\IndexNow\IndexNow
	 */
	private $indexNow = null;

	/**
	 * The main Local Business addon class.
	 *
	 * @since 4.4.2
	 *
	 * @var \AIOSEO\Plugin\Addon\LocalBusiness\LocalBusiness
	 */
	private $localBusiness = null;

	/**
	 * The main News Sitemap addon class.
	 *
	 * @since 4.4.2
	 *
	 * @var \AIOSEO\Plugin\Addon\NewsSitemap\NewsSitemap
	 */
	private $newsSitemap = null;

	/**
	 * The main Redirects addon class.
	 *
	 * @since 4.4.2
	 *
	 * @var \AIOSEO\Plugin\Addon\Redirects\Redirects
	 */
	private $redirects = null;

	/**
	 * The main REST API addon class.
	 *
	 * @since 4.4.2
	 *
	 * @var \AIOSEO\Plugin\Addon\RestApi\RestApi
	 */
	private $restApi = null;

	/**
	 * The main Video Sitemap addon class.
	 *
	 * @since 4.4.2
	 *
	 * @var \AIOSEO\Plugin\Addon\VideoSitemap\VideoSitemap
	 */
	private $videoSitemap = null;

	/**
	 * The main Link Assistant addon class.
	 *
	 * @since 4.4.2
	 *
	 * @var \AIOSEO\Plugin\Addon\LinkAssistant\LinkAssistant
	 */
	private $linkAssistant = null;

	/**
	 * The main EEAT addon class.
	 *
	 * @since 4.5.4
	 *
	 * @var \AIOSEO\Plugin\Addon\LinkAssistant\LinkAssistant
	 */
	private $eeat = null;

	/**
	 * Returns our addons.
	 *
	 * @since 4.0.0
	 *
	 * @param  boolean $flushCache Whether or not to flush the cache.
	 * @return array               An array of addon data.
	 */
	public function getAddons( $flushCache = false ) {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		$addons        = aioseo()->core->cache->get( 'addons' );
		$defaultAddons = $this->getDefaultAddons();
		if ( null === $addons || $flushCache ) {
			$response = aioseo()->helpers->wpRemoteGet( $this->getAddonsUrl() );
			if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
				$addons = json_decode( wp_remote_retrieve_body( $response ), true );
			}

			if ( ! $addons || ! empty( $addons->error ) ) {
				$addons = $defaultAddons;
			}

			aioseo()->core->cache->update( 'addons', $addons );
		}

		// Convert the addons array to objects using JSON. This is essential because we have lots of addons that rely on this to be an object, and changing it to an array would break them.

		$addons = json_decode( wp_json_encode( $addons ) );

		$installedPlugins = array_keys( get_plugins() );
		foreach ( $addons as $key => $addon ) {
			if ( ! is_object( $addon ) ) {
				continue;
			}

			$addons[ $key ]->basename          = $this->getAddonBasename( $addon->sku );
			$addons[ $key ]->installed         = in_array( $this->getAddonBasename( $addon->sku ), $installedPlugins, true );
			$addons[ $key ]->isActive          = is_plugin_active( $addons[ $key ]->basename );
			$addons[ $key ]->canInstall        = $this->canInstall();
			$addons[ $key ]->canActivate       = $this->canActivate();
			$addons[ $key ]->canUpdate         = $this->canUpdate();
			$addons[ $key ]->capability        = $this->getManageCapability( $addon->sku );
			$addons[ $key ]->minimumVersion    = '0.0.0';
			$addons[ $key ]->hasMinimumVersion = false;
			$addons[ $key ]->featured          = $this->setFeatured( $addon );
		}

		return $this->sortAddons( $addons );
	}

	/**
	 * Set the featured status for an addon.
	 *
	 * @since 4.6.9
	 *
	 * @param  object $addon The addon.
	 * @return bool          The featured status.
	 */
	protected function setFeatured( $addon ) {
		$defaultAddons = $this->getDefaultAddons();
		$featured      = false;

		// Find the addon in the default addons list and get the featured status.
		foreach ( $defaultAddons as $defaultAddon ) {
			if ( $addon->sku !== $defaultAddon['sku'] ) {
				continue;
			}

			$featured = ! empty( $addon->featured )
				? $addon->featured
				: (
					! empty( $defaultAddon['featured'] )
						? $defaultAddon['featured']
						: $featured
					);
			break;
		}

		return $featured;
	}

	/**
	 * Sort the addons by moving the featured ones to the top.
	 *
	 * @since 4.6.9
	 *
	 * @param  array $addons The addons to sort.
	 * @return array         The sorted addons.
	 */
	protected function sortAddons( $addons ) {
		if ( ! is_array( $addons ) ) {
			return $addons;
		}

		// Sort the addons by moving the featured ones to the top.
		usort( $addons, function( $a, $b ) {
			// Sort by featured value. It can be false, or numerical. If it's false, it will be moved to the bottom.
			// If it's numerical, it will be moved to the top. Numbers will be sorted in descending order.
			$featuredA = ! empty( $a->featured ) ? $a->featured : 0;
			$featuredB = ! empty( $b->featured ) ? $b->featured : 0;

			if ( $featuredA === $featuredB ) {
				return 0;
			}

			return $featuredA > $featuredB ? -1 : 1;
		} );

		return $addons;
	}

	/**
	 * Returns the required capability to manage the addon.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $sku The addon sku.
	 * @return string      The required capability.
	 */
	protected function getManageCapability( $sku ) {
		$capability = apply_filters( 'aioseo_manage_seo', 'aioseo_manage_seo' );

		switch ( $sku ) {
			case 'aioseo-image-seo':
				$capability = 'aioseo_search_appearance_settings';
				break;
			case 'aioseo-video-sitemap':
			case 'aioseo-news-sitemap':
				$capability = 'aioseo_sitemap_settings';
				break;
			case 'aioseo-redirects':
				$capability = 'aioseo_redirects_settings';
				break;
			case 'aioseo-local-business':
				$capability = 'aioseo_local_seo_settings';
				break;
			case 'aioseo-index-now':
				$capability = 'aioseo_general_settings';
				break;
		}

		return $capability;
	}

	/**
	 * Check to see if there are unlicensed addons installed and activated.
	 *
	 * @since 4.1.3
	 *
	 * @return boolean True if there are unlicensed addons, false if not.
	 */
	public function unlicensedAddons() {
		$unlicensed = [
			'addons'  => [],
			// Translators: 1 - Opening bold tag, 2 - Plugin short name ("AIOSEO"), 3 - "Pro", 4 - Closing bold tag.
			'message' => sprintf(
				// Translators: 1 - Opening HTML strong tag, 2 - The short plugin name ("AIOSEO"), 3 - "Pro", 4 - Closing HTML strong tag.
				__( 'The following addons cannot be used, because they require %1$s%2$s %3$s%4$s to work:', 'all-in-one-seo-pack' ),
				'<strong>',
				AIOSEO_PLUGIN_SHORT_NAME,
				'Pro',
				'</strong>'
			)
		];

		$addons = $this->getAddons();
		foreach ( $addons as $addon ) {
			if ( ! is_object( $addon ) ) {
				continue;
			}

			if ( $addon->isActive ) {
				$unlicensed['addons'][] = $addon;
			}
		}

		return $unlicensed;
	}

	/**
	 * Get the data for a specific addon.
	 *
	 * We need this function to refresh the data of a given addon because installation links expire after one hour.
	 *
	 * @since 4.0.0
	 *
	 * @param  string      $sku        The addon sku.
	 * @param  bool        $flushCache Whether or not to flush the cache.
	 * @return null|object             The addon.
	 */
	public function getAddon( $sku, $flushCache = false ) {
		$addon     = null;
		$allAddons = $this->getAddons( $flushCache );
		foreach ( $allAddons as $a ) {
			if ( $sku === $a->sku ) {
				$addon = $a;
			}
		}

		if ( ! $addon || ! empty( $addon->error ) ) {
			$addon = $this->getDefaultAddon( $sku );
			aioseo()->core->cache->update( 'addon_' . $sku, $addon, 10 * MINUTE_IN_SECONDS );
		}

		return $addon;
	}

	/**
	 * Checks if the specified addon is activated.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $sku The sku to check.
	 * @return string      The addon basename.
	 */
	public function getAddonBasename( $sku ) {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';
		$plugins = get_plugins();

		$keys = array_keys( $plugins );
		foreach ( $keys as $key ) {
			if ( preg_match( '|^' . $sku . '|', (string) $key ) ) {
				return $key;
			}
		}

		return $sku;
	}

	/**
	 * Returns an array of levels connected to an addon.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $addonName The addon name.
	 * @return array             The array of levels.
	 */
	public function getAddonLevels( $addonName ) {
		$addons = $this->getAddons();
		foreach ( $addons as $addon ) {
			if ( $addonName !== $addon->sku ) {
				continue;
			}

			if ( ! isset( $addon->levels ) ) {
				return [];
			}

			return $addon->levels;
		}

		return [];
	}

	/**
	 * Returns a list of addon SKUs.
	 *
	 * @since 4.5.6
	 *
	 * @return array The addon SKUs.
	 */
	public function getAddonSkus() {
		$addons = $this->getAddons();
		if ( empty( $addons ) ) {
			return [];
		}

		return array_map( function( $addon ) {
			return $addon->sku;
		}, $addons );
	}

	/**
	 * Get the URL to get addons.
	 *
	 * @since 4.1.8
	 *
	 * @return string The URL.
	 */
	protected function getAddonsUrl() {
		$url = $this->addonsUrl;
		if ( defined( 'AIOSEO_ADDONS_URL' ) ) {
			$url = AIOSEO_ADDONS_URL;
		}

		if ( defined( 'AIOSEO_INTERNAL_ADDONS' ) && AIOSEO_INTERNAL_ADDONS ) {
			$url = add_query_arg( 'internal', true, $url );
		}

		return $url;
	}

	/**
	 * Installs and activates a given addon or plugin.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name    The addon name/sku.
	 * @param  bool   $network Whether or not we are in a network environment.
	 * @return bool            Whether or not the installation was succesful.
	 */
	public function installAddon( $name, $network = false ) {
		if ( ! $this->canInstall() ) {
			return false;
		}

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

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

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

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

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

		// Activate the plugin silently.
		$pluginUrl = ! empty( $installer->pluginSlugs[ $name ] ) ? $installer->pluginSlugs[ $name ] : $name;
		$activated = activate_plugin( $pluginUrl, '', $network );

		if ( ! is_wp_error( $activated ) ) {
			return $name;
		}

		// Using output buffering to prevent the FTP form from being displayed in the screen.
		ob_start();
		$creds = request_filesystem_credentials( $url, '', false, false, null );
		ob_end_clean();

		// Check for file system permissions.
		$fs = aioseo()->core->fs->noConflict();
		$fs->init( $creds );
		if ( false === $creds || ! $fs->isWpfsValid() ) {
			return false;
		}

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

		$installLink = ! empty( $installer->pluginLinks[ $name ] ) ? $installer->pluginLinks[ $name ] : null;

		// Check if this is an addon and if we have a download link.
		if ( empty( $installLink ) ) {
			$downloadUrl = aioseo()->addons->getDownloadUrl( $name );
			if ( empty( $downloadUrl ) ) {
				return false;
			}

			$installLink = $downloadUrl;
		}

		$installer->install( $installLink );

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

		$pluginBasename = $installer->plugin_info();
		if ( ! $pluginBasename ) {
			return false;
		}

		// Activate the plugin silently.
		$activated = activate_plugin( $pluginBasename, '', $network );

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

		return $pluginBasename;
	}

	/**
	 * Determine if addons/plugins can be installed.
	 *
	 * @since 4.0.0
	 *
	 * @return bool True if yes, false if not.
	 */
	public function canInstall() {
		if (
			function_exists( 'wp_get_current_user' ) &&
			is_user_logged_in() &&
			! current_user_can( 'install_plugins' ) &&
			! aioseo()->helpers->isDoingWpCli()
		) {
			return false;
		}

		// Determine whether file modifications are allowed.
		if ( ! wp_is_file_mod_allowed( 'aioseo_can_install' ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Determine if addons/plugins can be updated.
	 *
	 * @since 4.1.6
	 *
	 * @return bool True if yes, false if not.
	 */
	public function canUpdate() {
		if (
			function_exists( 'wp_get_current_user' ) &&
			is_user_logged_in() &&
			! current_user_can( 'update_plugins' ) &&
			! aioseo()->helpers->isDoingWpCli()
		) {
			return false;
		}

		// Determine whether file modifications are allowed.
		if ( ! wp_is_file_mod_allowed( 'aioseo_can_update' ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Determine if addons/plugins can be activated.
	 *
	 * @since 4.1.3
	 *
	 * @return bool True if yes, false if not.
	 */
	public function canActivate() {
		if (
			function_exists( 'wp_get_current_user' ) &&
			is_user_logged_in() &&
			! current_user_can( 'activate_plugins' ) &&
			! aioseo()->helpers->isDoingWpCli()
		) {
			return false;
		}

		return true;
	}

	/**
	 * Load an addon into aioseo.
	 *
	 * @since 4.1.0
	 *
	 * @param  string $slug
	 * @param  object $addon Addon class instance.
	 * @return void
	 */
	public function loadAddon( $slug, $addon ) {
		$this->{$slug}        = $addon;
		$this->loadedAddons[] = $slug;
	}

	/**
	 * Return a loaded addon.
	 *
	 * @since 4.1.0
	 *
	 * @param  string $slug
	 * @return object|null
	 */
	public function getLoadedAddon( $slug ) {
		return isset( $this->{$slug} ) ? $this->{$slug} : null;
	}

	/**
	 * Returns loaded addons
	 *
	 * @since 4.1.0
	 *
	 * @return array
	 */
	public function getLoadedAddons() {
		$loadedAddonsList = [];
		if ( ! empty( $this->loadedAddons ) ) {
			foreach ( $this->loadedAddons as $addonSlug ) {
				$loadedAddonsList[ $addonSlug ] = $this->{$addonSlug};
			}
		}

		return $loadedAddonsList;
	}

	/**
	 * Run a function through all addons that support it.
	 *
	 * @since 4.2.3
	 *
	 * @param  string $class    The class name.
	 * @param  string $function The function name.
	 * @param  array  $args     The args for the function.
	 * @return array            The response from each addon.
	 */
	public function doAddonFunction( $class, $function, $args = [] ) {
		$addonResponses = [];

		foreach ( $this->getLoadedAddons() as $addonSlug => $addon ) {
			if ( isset( $addon->$class ) && method_exists( $addon->$class, $function ) ) {
				$addonResponses[ $addonSlug ] = call_user_func_array( [ $addon->$class, $function ], $args );
			}
		}

		return $addonResponses;
	}

	/**
	 * Merges the data for Vue.
	 *
	 * @since 4.4.1
	 *
	 * @param  array  $data The data to merge.
	 * @param  string $page The current page.
	 * @return array        The data.
	 */
	public function getVueData( $data = [], $page = null ) {
		foreach ( $this->getLoadedAddons() as $addon ) {
			if ( isset( $addon->helpers ) && method_exists( $addon->helpers, 'getVueData' ) ) {
				$data = array_merge( $data, $addon->helpers->getVueData( $data, $page ) );
			}
		}

		return $data;
	}

	/**
	 * Retrieves a default addon with whatever information is needed if the API cannot be reached.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $sku The sku of the addon.
	 * @return array       An array of addon data.
	 */
	public function getDefaultAddon( $sku ) {
		$addons = $this->getDefaultAddons();
		$addon  = [];
		foreach ( $addons as $a ) {
			if ( $a['sku'] === $sku ) {
				$addon = $a;
			}
		}

		return $addon;
	}

	/**
	 * Retrieves a default list of addons if the API cannot be reached.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of addons.
	 */
	protected function getDefaultAddons() {
		return json_decode( wp_json_encode( [
			[
				'sku'                => 'aioseo-eeat',
				'name'               => 'Author SEO (E-E-A-T)',
				'version'            => '1.0.0',
				'image'              => null,
				'icon'               => 'svg-eeat',
				'levels'             => [
					'plus',
					'pro',
					'elite',
				],
				'currentLevels'      => [
					'plus',
					'pro',
					'elite'
				],
				'requiresUpgrade'    => true,
				'description'        => '<p>Optimize your site for Google\'s E-E-A-T ranking factor by proving your writer\'s expertise through author schema markup and new UI elements.</p>',
				'descriptionVersion' => 0,
				'productUrl'         => 'https://aioseo.com/author-seo-eeat/',
				'learnMoreUrl'       => 'https://aioseo.com/author-seo-eeat/',
				'manageUrl'          => 'https://route#aioseo-search-appearance:author-seo',
				'basename'           => 'aioseo-eeat/aioseo-eeat.php',
				'installed'          => false,
				'isActive'           => false,
				'canInstall'         => false,
				'canActivate'        => false,
				'canUpdate'          => false,
				'capability'         => $this->getManageCapability( 'aioseo-eeat' ),
				'minimumVersion'     => '0.0.0',
				'hasMinimumVersion'  => false,
				'featured'           => 300
			],
			[
				'sku'                => 'aioseo-redirects',
				'name'               => 'Redirection Manager',
				'version'            => '1.0.0',
				'image'              => null,
				'icon'               => 'svg-redirect',
				'levels'             => [
					'agency',
					'business',
					'pro',
					'elite'
				],
				'currentLevels'      => [
					'pro',
					'elite'
				],
				'requiresUpgrade'    => true,
				'description'        => '<p>Our Redirection Manager allows you to easily create and manage redirects for your broken links to avoid confusing search engines and users, as well as losing valuable backlinks. It even automatically sends users and search engines from your old URLs to your new ones.</p>', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				'descriptionVersion' => 0,
				'productUrl'         => 'https://aioseo.com/features/redirection-manager/',
				'learnMoreUrl'       => 'https://aioseo.com/features/redirection-manager/',
				'manageUrl'          => 'https://route#aioseo-redirects:redirects',
				'basename'           => 'aioseo-redirects/aioseo-redirects.php',
				'installed'          => false,
				'isActive'           => false,
				'canInstall'         => false,
				'canActivate'        => false,
				'canUpdate'          => false,
				'capability'         => $this->getManageCapability( 'aioseo-redirects' ),
				'minimumVersion'     => '0.0.0',
				'hasMinimumVersion'  => false,
				'featured'           => 200
			],
			[
				'sku'                => 'aioseo-link-assistant',
				'name'               => 'Link Assistant',
				'version'            => '1.0.0',
				'image'              => null,
				'icon'               => 'svg-link-assistant',
				'levels'             => [
					'agency',
					'pro',
					'elite'
				],
				'currentLevels'      => [
					'pro',
					'elite'
				],
				'requiresUpgrade'    => true,
				'description'        => '<p>Super-charge your SEO with Link Assistant! Get relevant suggestions for adding internal links to older content as well as finding any orphaned posts that have no internal links. Use our reporting feature to see all link suggestions or add them directly from any page or post.</p>', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				'descriptionVersion' => 0,
				'productUrl'         => 'https://aioseo.com/feature/internal-link-assistant/',
				'learnMoreUrl'       => 'https://aioseo.com/feature/internal-link-assistant/',
				'manageUrl'          => 'https://route#aioseo-link-assistant:overview',
				'basename'           => 'aioseo-link-assistant/aioseo-link-assistant.php',
				'installed'          => false,
				'isActive'           => false,
				'canInstall'         => false,
				'canActivate'        => false,
				'canUpdate'          => false,
				'capability'         => $this->getManageCapability( 'aioseo-link-assistant' ),
				'minimumVersion'     => '0.0.0',
				'hasMinimumVersion'  => false,
				'featured'           => 100
			],
			[
				'sku'                => 'aioseo-video-sitemap',
				'name'               => 'Video Sitemap',
				'version'            => '1.0.0',
				'image'              => null,
				'icon'               => 'svg-sitemaps-pro',
				'levels'             => [
					'individual',
					'business',
					'agency',
					'pro',
					'elite'
				],
				'currentLevels'      => [
					'pro',
					'elite'
				],
				'requiresUpgrade'    => true,
				'description'        => '<p>The Video Sitemap works in much the same way as the XML Sitemap module, it generates an XML Sitemap specifically for video content on your site. Search engines use this information to display rich snippet information in search results.</p>', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				'descriptionVersion' => 0,
				'productUrl'         => 'https://aioseo.com/video-sitemap',
				'learnMoreUrl'       => 'https://aioseo.com/video-sitemap',
				'manageUrl'          => 'https://route#aioseo-sitemaps:video-sitemap',
				'basename'           => 'aioseo-video-sitemap/aioseo-video-sitemap.php',
				'installed'          => false,
				'isActive'           => false,
				'canInstall'         => false,
				'canActivate'        => false,
				'canUpdate'          => false,
				'capability'         => $this->getManageCapability( 'aioseo-video-sitemap' ),
				'minimumVersion'     => '0.0.0',
				'hasMinimumVersion'  => false
			],
			[
				'sku'                => 'aioseo-local-business',
				'name'               => 'Local Business SEO',
				'version'            => '1.0.0',
				'image'              => null,
				'icon'               => 'svg-local-business',
				'levels'             => [
					'business',
					'agency',
					'plus',
					'pro',
					'elite'
				],
				'currentLevels'      => [
					'plus',
					'pro',
					'elite'
				],
				'requiresUpgrade'    => true,
				'description'        => '<p>Local Business schema markup enables you to tell Google about your business, including your business name, address and phone number, opening hours and price range. This information may be displayed as a Knowledge Graph card or business carousel.</p>', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				'descriptionVersion' => 0,
				'productUrl'         => 'https://aioseo.com/local-business',
				'learnMoreUrl'       => 'https://aioseo.com/local-business',
				'manageUrl'          => 'https://route#aioseo-local-seo:locations',
				'basename'           => 'aioseo-local-business/aioseo-local-business.php',
				'installed'          => false,
				'isActive'           => false,
				'canInstall'         => false,
				'canActivate'        => false,
				'canUpdate'          => false,
				'capability'         => $this->getManageCapability( 'aioseo-local-business' ),
				'minimumVersion'     => '0.0.0',
				'hasMinimumVersion'  => false
			],
			[
				'sku'                => 'aioseo-news-sitemap',
				'name'               => 'News Sitemap',
				'version'            => '1.0.0',
				'image'              => null,
				'icon'               => 'svg-sitemaps-pro',
				'levels'             => [
					'business',
					'agency',
					'pro',
					'elite'
				],
				'currentLevels'      => [
					'pro',
					'elite'
				],
				'requiresUpgrade'    => true,
				'description'        => '<p>Our Google News Sitemap lets you control which content you submit to Google News and only contains articles that were published in the last 48 hours. In order to submit a News Sitemap to Google, you must have added your site to Google’s Publisher Center and had it approved.</p>', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				'descriptionVersion' => 0,
				'productUrl'         => 'https://aioseo.com/news-sitemap',
				'learnMoreUrl'       => 'https://aioseo.com/news-sitemap',
				'manageUrl'          => 'https://route#aioseo-sitemaps:news-sitemap',
				'basename'           => 'aioseo-news-sitemap/aioseo-news-sitemap.php',
				'installed'          => false,
				'isActive'           => false,
				'canInstall'         => false,
				'canActivate'        => false,
				'canUpdate'          => false,
				'capability'         => $this->getManageCapability( 'aioseo-news-sitemap' ),
				'minimumVersion'     => '0.0.0',
				'hasMinimumVersion'  => false
			],
			[
				'sku'                => 'aioseo-index-now',
				'name'               => 'IndexNow',
				'version'            => '1.0.0',
				'image'              => null,
				'icon'               => 'svg-sitemaps-pro',
				'levels'             => [
					'agency',
					'business',
					'basic',
					'plus',
					'pro',
					'elite'
				],
				'currentLevels'      => [
					'basic',
					'plus',
					'pro',
					'elite'
				],
				'requiresUpgrade'    => true,
				'description'        => '<p>Add IndexNow support to instantly notify search engines when your content has changed. This helps the search engines to prioritize the changes on your website and helps you rank faster.</p>', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				'descriptionVersion' => 0,
				'downloadUrl'        => '',
				'productUrl'         => 'https://aioseo.com/index-now/',
				'learnMoreUrl'       => 'https://aioseo.com/index-now/',
				'manageUrl'          => 'https://route#aioseo-settings:webmaster-tools',
				'basename'           => 'aioseo-index-now/aioseo-index-now.php',
				'installed'          => false,
				'isActive'           => false,
				'canInstall'         => false,
				'canActivate'        => false,
				'canUpdate'          => false,
				'capability'         => $this->getManageCapability( 'aioseo-index-now' ),
				'minimumVersion'     => '0.0.0',
				'hasMinimumVersion'  => false
			],
			[
				'sku'                => 'aioseo-rest-api',
				'name'               => 'REST API',
				'version'            => '1.0.0',
				'image'              => null,
				'icon'               => 'svg-code',
				'levels'             => [
					'plus',
					'pro',
					'elite'
				],
				'currentLevels'      => [
					'plus',
					'pro',
					'elite'
				],
				'requiresUpgrade'    => true,
				'description'        => '<p>Manage your post and term SEO meta via the WordPress REST API. This addon also works seamlessly with headless WordPress installs.</p>', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				'descriptionVersion' => 0,
				'downloadUrl'        => '',
				'productUrl'         => 'https://aioseo.com/feature/rest-api/',
				'learnMoreUrl'       => 'https://aioseo.com/feature/rest-api/',
				'manageUrl'          => null,
				'basename'           => 'aioseo-rest-api/aioseo-rest-api.php',
				'installed'          => false,
				'isActive'           => false,
				'canInstall'         => false,
				'canActivate'        => false,
				'canUpdate'          => false,
				'capability'         => null,
				'minimumVersion'     => '0.0.0',
				'hasMinimumVersion'  => false
			],
			[
				'sku'                => 'aioseo-image-seo',
				'name'               => 'Image SEO',
				'version'            => '1.0.0',
				'image'              => null,
				'icon'               => 'svg-image-seo',
				'levels'             => [
					'individual',
					'business',
					'agency',
					'plus',
					'pro',
					'elite',
				],
				'currentLevels'      => [
					'plus',
					'pro',
					'elite'
				],
				'requiresUpgrade'    => true,
				'description'        => '<p>Globally control the Title attribute and Alt text for images in your content. These attributes are essential for both accessibility and SEO.</p>',
				'descriptionVersion' => 0,
				'productUrl'         => 'https://aioseo.com/image-seo',
				'learnMoreUrl'       => 'https://aioseo.com/image-seo',
				'manageUrl'          => 'https://route#aioseo-search-appearance:media',
				'basename'           => 'aioseo-image-seo/aioseo-image-seo.php',
				'installed'          => false,
				'isActive'           => false,
				'canInstall'         => false,
				'canActivate'        => false,
				'canUpdate'          => false,
				'capability'         => $this->getManageCapability( 'aioseo-image-seo' ),
				'minimumVersion'     => '0.0.0',
				'hasMinimumVersion'  => false
			]
		] ), true );
	}

	/**
	 * Check for updates for all addons.
	 *
	 * @since 4.2.4
	 *
	 * @return void
	 */
	public function registerUpdateCheck() {}

	/**
	 * Updates a given addon or plugin.
	 *
	 * @since 4.4.3
	 *
	 * @param  string $name    The addon name/sku.
	 * @param  bool   $network Whether we are in a network environment.
	 * @return bool            Whether the installation was succesful.
	 */
	public function upgradeAddon( $name, $network ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return false;
	}

	/**
	 * Get the download URL for the given addon.
	 *
	 * @since 4.4.3
	 *
	 * @param  string $sku The addon sku.
	 * @return string      The download url for the addon.
	 */
	public function getDownloadUrl( $sku ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return '';
	}
}Common/Utils/Assets.php000064400000004125151536241210011051 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

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

use AIOSEO\Plugin\Common\Traits;

/**
 * Load file assets.
 *
 * @since 4.1.9
 */
class Assets {
	use Traits\Assets;

	/**
	 * Get the script handle to use for asset enqueuing.
	 *
	 * @since 4.1.9
	 *
	 * @var string
	 */
	private $scriptHandle = 'aioseo';

	/**
	 * Class constructor.
	 *
	 * @since 4.1.9
	 *
	 * @param \AIOSEO\Plugin\Common\Core\Core $core The AIOSEO Core class.
	 */
	public function __construct( $core ) {
		$this->core         = $core;
		$this->version      = aioseo()->version;
		$this->manifestFile = AIOSEO_DIR . '/dist/' . aioseo()->versionPath . '/manifest.php';
		$this->isDev        = aioseo()->isDev;

		if ( $this->isDev ) {
			$this->domain = getenv( 'VITE_AIOSEO_DOMAIN' );
			$this->port   = getenv( 'VITE_AIOSEO_DEV_PORT' );
		}

		add_filter( 'script_loader_tag', [ $this, 'scriptLoaderTag' ], 10, 3 );
		add_action( 'admin_head', [ $this, 'devRefreshRuntime' ] );
		add_action( 'wp_head', [ $this, 'devRefreshRuntime' ] );
	}

	/**
	 * Get the public URL base.
	 *
	 * @since 4.1.9
	 *
	 * @return string The URL base.
	 */
	private function getPublicUrlBase() {
		return $this->shouldLoadDev() ? $this->getDevUrl() . 'dist/' . aioseo()->versionPath . '/assets/' : $this->basePath();
	}

	/**
	 * Get the base path URL.
	 *
	 * @since 4.1.9
	 *
	 * @return string The base path URL.
	 */
	private function basePath() {
		return $this->normalizeAssetsHost( plugins_url( 'dist/' . aioseo()->versionPath . '/assets/', AIOSEO_FILE ) );
	}

	/**
	 * Adds the RefreshRuntime.
	 *
	 * @since 4.1.9
	 *
	 * @return void
	 */
	public function devRefreshRuntime() {
		if ( $this->shouldLoadDev() ) {
			echo sprintf( '<script type="module">
			import RefreshRuntime from "%1$s@react-refresh"
			RefreshRuntime.injectIntoGlobalHook(window)
			window.$RefreshReg$ = () => {}
			window.$RefreshSig$ = () => (type) => type
			window.__vite_plugin_react_preamble_installed__ = true
			</script>', $this->getDevUrl() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}
	}
}Common/Utils/Backup.php000064400000004413151536241210011014 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

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

/**
 * Backup for AIOSEO Settings.
 *
 * @since 4.0.0
 */
class Backup {
	/**
	 * A the name of the option to save backups with.
	 *
	 * @since 4.00
	 *
	 * @var string
	 */
	private $optionsName = 'aioseo_settings_backup';

	/**
	 * Get all backups.
	 *
	 * @return array An array of backups.
	 */
	public function all() {
		$backups = json_decode( get_option( $this->optionsName ), true );
		if ( empty( $backups ) ) {
			$backups = [];
		}

		return $backups;
	}

	/**
	 * Creates a backup of the settings state.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function create() {
		$backupTime = time();
		$options    = $this->getOptions();

		update_option( $this->optionsName . '_' . $backupTime, wp_json_encode( $options ), 'no' );

		$backups = $this->all();

		$backups[] = $backupTime;

		update_option( $this->optionsName, wp_json_encode( $backups ), 'no' );
	}

	/**
	 * Deletes a backup of the settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function delete( $backupTime ) {
		delete_option( $this->optionsName . '_' . $backupTime );

		$backups = $this->all();

		foreach ( $backups as $key => $backup ) {
			if ( $backup === $backupTime ) {
				unset( $backups[ $key ] );
			}
		}

		update_option( $this->optionsName, wp_json_encode( array_values( $backups ) ), 'no' );
	}

	/**
	 * Restores a backup of the settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function restore( $backupTime ) {
		$backup = json_decode( get_option( $this->optionsName . '_' . $backupTime ), true );
		if ( ! empty( $backup['options']['tools']['robots']['rules'] ) ) {
			$backup['options']['tools']['robots']['rules'] = array_merge(
				aioseo()->robotsTxt->extractSearchAppearanceRules(),
				$backup['options']['tools']['robots']['rules']
			);
		}

		aioseo()->options->sanitizeAndSave( $backup['options'] );
		aioseo()->internalOptions->sanitizeAndSave( $backup['internalOptions'] );
	}

	/**
	 * Get the options to save.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of options to save.
	 */
	private function getOptions() {
		return [
			'options'         => aioseo()->options->all(),
			'internalOptions' => aioseo()->internalOptions->all()
		];
	}
}Common/Utils/Blocks.php000064400000010366151536241210011030 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

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

/**
 * Block helpers.
 *
 * @since 4.1.1
 */
class Blocks {
	/**
	 * Class constructor.
	 *
	 * @since 4.1.1
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'init' ] );
	}

	/**
	 * Initializes our blocks.
	 *
	 * @since 4.1.1
	 *
	 * @return void
	 */
	public function init() {
		add_action( 'enqueue_block_editor_assets', [ $this, 'registerBlockEditorAssets' ] );
	}

	/**
	 * Registers the block type with WordPress.
	 *
	 * @since 4.2.1
	 *
	 * @param  string               $slug Block type name including namespace.
	 * @param  array                $args Array of block type arguments with additional 'wp_min_version' arg.
	 * @return \WP_Block_Type|false       The registered block type on success, or false on failure.
	 */
	public function registerBlock( $slug = '', $args = [] ) {
		global $wp_version; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		if ( ! strpos( $slug, '/' ) ) {
			$slug = 'aioseo/' . $slug;
		}

		if ( ! $this->isBlockEditorActive() ) {
			return false;
		}

		// Check if the block requires a minimum WP version.
		if ( ! empty( $args['wp_min_version'] ) && version_compare( $wp_version, $args['wp_min_version'], '>' ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			return false;
		}

		// Checking whether block is registered to ensure it isn't registered twice.
		if ( $this->isRegistered( $slug ) ) {
			return false;
		}

		$defaults = [
			'render_callback' => null,
			'editor_script'   => aioseo()->core->assets->jsHandle( 'src/vue/standalone/blocks/main.js' ),
			'editor_style'    => aioseo()->core->assets->cssHandle( 'src/vue/assets/scss/blocks-editor.scss' ),
			'attributes'      => null,
			'supports'        => null
		];

		$args = wp_parse_args( $args, $defaults );

		return register_block_type( $slug, $args );
	}

	/**
	 * Registers Gutenberg editor assets.
	 *
	 * @since 4.2.1
	 *
	 * @return void
	 */
	public function registerBlockEditorAssets() {
		$postSettingJsAsset = 'src/vue/standalone/post-settings/main.js';
		if (
			aioseo()->helpers->isScreenBase( 'widgets' ) ||
			aioseo()->helpers->isScreenBase( 'customize' )
		) {
			/**
			 * Make sure the post settings JS asset is registered before adding it as a dependency below.
			 * This is needed because this asset is not loaded on widgets and customizer screens,
			 * {@see \AIOSEO\Plugin\Common\Admin\PostSettings::enqueuePostSettingsAssets}.
			 *

			 */
			aioseo()->core->assets->load( $postSettingJsAsset, [], aioseo()->helpers->getVueData() );
		}

		aioseo()->core->assets->loadCss( 'src/vue/standalone/blocks/main.js' );

		$dependencies = [
			'wp-annotations',
			'wp-block-editor',
			'wp-blocks',
			'wp-components',
			'wp-element',
			'wp-i18n',
			'wp-data',
			'wp-url',
			'wp-polyfill',
			aioseo()->core->assets->jsHandle( $postSettingJsAsset )
		];

		aioseo()->core->assets->enqueueJs( 'src/vue/standalone/blocks/main.js', $dependencies );
		aioseo()->core->assets->registerCss( 'src/vue/assets/scss/blocks-editor.scss' );
	}

	/**
	 * Check if a block is already registered.
	 *
	 * @since 4.2.1
	 *
	 * @param string $slug Name of block to check.
	 *
	 * @return bool
	 */
	public function isRegistered( $slug ) {
		if ( ! class_exists( 'WP_Block_Type_Registry' ) ) {
			return false;
		}

		return \WP_Block_Type_Registry::get_instance()->is_registered( $slug );
	}

	/**
	 * Helper function to determine if we're rendering the block inside Gutenberg.
	 *
	 * @since 4.1.1
	 *
	 * @return bool In gutenberg.
	 */
	public function isRenderingBlockInEditor() {
		// phpcs:disable HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) {
			return false;
		}

		$context = isset( $_REQUEST['context'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['context'] ) ) : '';
		// phpcs:enable HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended

		return 'edit' === $context;
	}

	/**
	 * Helper function to determine if we can register blocks.
	 *
	 * @since 4.1.1
	 *
	 * @return bool Can register block.
	 */
	public function isBlockEditorActive() {
		return function_exists( 'register_block_type' );
	}
}Common/Utils/Cache.php000064400000021126151536241210010612 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

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

/**
 * Handles our cache.
 *
 * @since 4.1.5
 */
class Cache {
	/**
	 * Our cache table.
	 *
	 * @since 4.1.5
	 *
	 * @var string
	 */
	private $table = 'aioseo_cache';

	/**
	 * Our cached cache.
	 *
	 * @since 4.1.5
	 *
	 * @var array
	 */
	private static $cache = [];

	/**
	 * The Cache Prune class.
	 *
	 * @since 4.1.5
	 *
	 * @var CachePrune
	 */
	public $prune;

	/**
	 * Prefix for this cache.
	 *
	 * @since 4.1.5
	 *
	 * @var string
	 */
	protected $prefix = '';

	/**
	 * Class constructor.
	 *
	 * @since 4.7.7.1
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'checkIfTableExists' ] ); // This needs to run on init because the DB
		// class gets instantiated along with the cache class.
	}

	/**
	 * Checks if the cache table exists and creates it if it doesn't.
	 *
	 * @since 4.7.7.1
	 *
	 * @return void
	 */
	public function checkIfTableExists() {
		if ( ! aioseo()->core->db->tableExists( $this->table ) ) {
			aioseo()->preUpdates->createCacheTable();
		}
	}

	/**
	 * Returns the cache value for a key if it exists and is not expired.
	 *
	 * @since 4.1.5
	 *
	 * @param  string     $key            The cache key name. Use a '%' for a like query.
	 * @param  bool|array $allowedClasses Whether to allow objects to be returned.
	 * @return mixed                      The value or null if the cache does not exist.
	 */
	public function get( $key, $allowedClasses = false ) {
		$key = $this->prepareKey( $key );
		if ( isset( self::$cache[ $key ] ) ) {
			return self::$cache[ $key ];
		}

		// Are we searching for a group of keys?
		$isLikeGet = preg_match( '/%/', (string) $key );

		$result = aioseo()->core->db
			->start( $this->table )
			->select( '`key`, `value`' )
			->whereRaw( '( `expiration` IS NULL OR `expiration` > \'' . aioseo()->helpers->timeToMysql( time() ) . '\' )' );

		$isLikeGet ?
			$result->whereRaw( '`key` LIKE \'' . $key . '\'' ) :
			$result->where( 'key', $key );

		$result->output( ARRAY_A )->run();

		// If we have nothing in the cache let's return a hard null.
		$values = $result->nullSet() ? null : $result->result();

		// If we have something let's normalize it.
		if ( $values ) {
			foreach ( $values as &$value ) {
				$value['value'] = aioseo()->helpers->maybeUnserialize( $value['value'], $allowedClasses );
			}
			// Return only the single cache value.
			if ( ! $isLikeGet ) {
				$values = $values[0]['value'];
			}
		}

		// Return values without a static cache.
		// This is here because clearing the like cache is not simple.
		if ( $isLikeGet ) {
			return $values;
		}

		self::$cache[ $key ] = $values;

		return self::$cache[ $key ];
	}

	/**
	 * Updates the given cache or creates it if it doesn't exist.
	 *
	 * @since 4.1.5
	 *
	 * @param  string $key        The cache key name.
	 * @param  mixed  $value      The value.
	 * @param  int    $expiration The expiration time in seconds. Defaults to 24 hours. 0 to no expiration.
	 * @return void
	 */
	public function update( $key, $value, $expiration = DAY_IN_SECONDS ) {
		// If the value is null we'll convert it and give it a shorter expiration.
		if ( null === $value ) {
			$value      = false;
			$expiration = 10 * MINUTE_IN_SECONDS;
		}

		$serializedValue = serialize( $value );
		$expiration      = 0 < $expiration ? aioseo()->helpers->timeToMysql( time() + $expiration ) : null;

		aioseo()->core->db->insert( $this->table )
			->set( [
				'key'        => $this->prepareKey( $key ),
				'value'      => $serializedValue,
				'expiration' => $expiration,
				'created'    => aioseo()->helpers->timeToMysql( time() ),
				'updated'    => aioseo()->helpers->timeToMysql( time() )
			] )
			->onDuplicate( [
				'value'      => $serializedValue,
				'expiration' => $expiration,
				'updated'    => aioseo()->helpers->timeToMysql( time() )
			] )
			->run();

		$this->updateStatic( $key, $value );
	}

	/**
	 * Deletes the given cache key.
	 *
	 * @since 4.1.5
	 *
	 * @param  string $key The cache key.
	 * @return void
	 */
	public function delete( $key ) {
		$key = $this->prepareKey( $key );

		aioseo()->core->db->delete( $this->table )
			->where( 'key', $key )
			->run();

		$this->clearStatic( $key );
	}

	/**
	 * Prepares the key before using the cache.
	 *
	 * @since 4.1.5
	 *
	 * @param  string $key The key to prepare.
	 * @return string      The prepared key.
	 */
	private function prepareKey( $key ) {
		$key = trim( $key );
		$key = $this->prefix && 0 !== strpos( $key, $this->prefix ) ? $this->prefix . $key : $key;

		if ( aioseo()->helpers->isDev() && 80 < mb_strlen( $key, 'UTF-8' ) ) {
			throw new \Exception( 'You are using a cache key that is too large, shorten your key and try again: [' . esc_html( $key ) . ']' );
		}

		return $key;
	}

	/**
	 * Clears all of our cache.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	public function clear() {
		// Bust the tableExists and columnExists cache.
		aioseo()->internalOptions->database->installedTables = '';

		if ( $this->prefix ) {
			$this->clearPrefix( '' );

			return;
		}

		// Try to acquire the lock.
		if ( ! aioseo()->core->db->acquireLock( 'aioseo_cache_clear_lock', 0 ) ) {
			// If we couldn't acquire the lock, exit early without doing anything.
			// This means another process is already clearing the cache.
			return;
		}

		// If we find the activation redirect, we'll need to reset it after clearing.
		$activationRedirect = $this->get( 'activation_redirect' );

		// Create a temporary table with the same structure.
		$table    = aioseo()->core->db->prefix . $this->table;
		$newTable = aioseo()->core->db->prefix . $this->table . '_new';
		$oldTable = aioseo()->core->db->prefix . $this->table . '_old';

		try {
			// Drop the temp table if it exists from a previous failed attempt.
			if ( false === aioseo()->core->db->execute( "DROP TABLE IF EXISTS {$newTable}" ) ) {
				throw new \Exception( 'Failed to drop temporary table' );
			}

			// Create the new empty table with the same structure.
			if ( false === aioseo()->core->db->execute( "CREATE TABLE {$newTable} LIKE {$table}" ) ) {
				throw new \Exception( 'Failed to create temporary table' );
			}

			// Rename tables (atomic operation in MySQL).
			if ( false === aioseo()->core->db->execute( "RENAME TABLE {$table} TO {$oldTable}, {$newTable} TO {$table}" ) ) {
				throw new \Exception( 'Failed to rename tables' );
			}

			// Drop the old table.
			if ( false === aioseo()->core->db->execute( "DROP TABLE {$oldTable}" ) ) {
				throw new \Exception( 'Failed to drop old table' );
			}
		} catch ( \Exception $e ) {
			// If something fails, ensure we clean up any temporary tables.
			aioseo()->core->db->execute( "DROP TABLE IF EXISTS {$newTable}" );
			aioseo()->core->db->execute( "DROP TABLE IF EXISTS {$oldTable}" );

			// Truncate table to clear the cache.
			aioseo()->core->db->truncate( $this->table )->run();
		}

		$this->clearStatic();

		if ( $activationRedirect ) {
			$this->update( 'activation_redirect', $activationRedirect, 30 );
		}
	}

	/**
	 * Clears all of our cache under a certain prefix.
	 *
	 * @since 4.1.5
	 *
	 * @param  string $prefix A prefix to clear or empty to clear everything.
	 * @return void
	 */
	public function clearPrefix( $prefix ) {
		$prefix = $this->prepareKey( $prefix );

		aioseo()->core->db->delete( $this->table )
			->whereRaw( "`key` LIKE '$prefix%'" )
			->run();

		$this->clearStaticPrefix( $prefix );
	}

	/**
	 * Clears all of our static in-memory cache of a prefix.
	 *
	 * @since 4.1.5
	 *
	 * @param  string $prefix A prefix to clear.
	 * @return void
	 */
	private function clearStaticPrefix( $prefix ) {
		$prefix = $this->prepareKey( $prefix );
		foreach ( array_keys( self::$cache ) as $key ) {
			if ( 0 === strpos( $key, $prefix ) ) {
				unset( self::$cache[ $key ] );
			}
		}
	}

	/**
	 * Clears all of our static in-memory cache.
	 *
	 * @since 4.1.5
	 *
	 * @param  string $key A key to clear.
	 * @return void
	 */
	private function clearStatic( $key = null ) {
		if ( empty( $key ) ) {
			self::$cache = [];

			return;
		}

		unset( self::$cache[ $this->prepareKey( $key ) ] );
	}

	/**
	 * Clears all of our static in-memory cache or the cache for a single given key.
	 *
	 * @since 4.7.1
	 *
	 * @param  string $key   A key to clear (optional).
	 * @param  string $value A value to update (optional).
	 * @return void
	 */
	private function updateStatic( $key = null, $value = null ) {
		if ( empty( $key ) ) {
			$this->clearStatic( $key );

			return;
		}

		self::$cache[ $this->prepareKey( $key ) ] = $value;
	}

	/**
	 * Returns the cache table name.
	 *
	 * @since 4.1.5
	 *
	 * @return string
	 */
	public function getTableName() {
		return $this->table;
	}
}Common/Utils/CachePrune.php000064400000004074151536241210011627 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

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

/**
 * Handles our cache pruning.
 *
 * @since 4.1.5
 */
class CachePrune {
	/**
	 * The action for the scheduled cache prune.
	 *
	 * @since 4.1.5
	 *
	 * @var string
	 */
	private $pruneAction = 'aioseo_cache_prune';

	/**
	 * The action for the scheduled old cache clean.
	 *
	 * @since 4.1.5
	 *
	 * @var string
	 */
	private $optionCacheCleanAction = 'aioseo_old_cache_clean';

	/**
	 * Class constructor.
	 *
	 * @since 4.1.5
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'init' ] );
	}

	/**
	 * Inits our class.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	public function init() {
		add_action( $this->pruneAction, [ $this, 'prune' ] );
		add_action( $this->optionCacheCleanAction, [ $this, 'optionCacheClean' ] );

		if ( ! is_admin() ) {
			return;
		}

		if ( ! aioseo()->actionScheduler->isScheduled( $this->pruneAction ) ) {
			aioseo()->actionScheduler->scheduleRecurrent( $this->pruneAction, 0, DAY_IN_SECONDS );
		}
	}

	/**
	 * Prunes our expired cache.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	public function prune() {
		aioseo()->core->db->delete( aioseo()->core->cache->getTableName() )
			->whereRaw( '( `expiration` IS NOT NULL AND expiration <= \'' . aioseo()->helpers->timeToMysql( time() ) . '\' )' )
			->run();
	}

	/**
	 * Cleans our old options cache.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	public function optionCacheClean() {
		$optionCache = aioseo()->core->db->delete( aioseo()->core->db->db->options, true )
			->whereRaw( "option_name LIKE '\_aioseo\_cache\_%'" )
			->limit( 10000 )
			->run();

		// Schedule a new run if we're not done cleaning.
		if ( 0 !== $optionCache->db->rows_affected ) {
			aioseo()->actionScheduler->scheduleSingle( $this->optionCacheCleanAction, MINUTE_IN_SECONDS, [], true );
		}
	}

	/**
	 * Returns the action name for the old cache clean.
	 *
	 * @since 4.1.5
	 *
	 * @return string
	 */
	public function getOptionCacheCleanAction() {
		return $this->optionCacheCleanAction;
	}
}Common/Utils/Database.php000064400000136636151536241210011330 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

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

/**
 * Database utility class for AIOSEO.
 *
 * @since 4.0.0
 */
class Database {
	/**
	 * List of custom tables we support.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $customTables = [
		'aioseo_cache',
		'aioseo_crawl_cleanup_blocked_args',
		'aioseo_crawl_cleanup_logs',
		'aioseo_links',
		'aioseo_links_suggestions',
		'aioseo_notifications',
		'aioseo_posts',
		'aioseo_redirects',
		'aioseo_redirects_404',
		'aioseo_redirects_404_logs',
		'aioseo_redirects_hits',
		'aioseo_redirects_logs',
		'aioseo_terms',
		'aioseo_search_statistics_objects',
		'aioseo_revisions'
	];

	/**
	 * Holds $wpdb instance.
	 *
	 * @since 4.0.0
	 *
	 * @var \wpdb
	 */
	public $db = null;

	/**
	 * Holds $wpdb prefix.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	public $prefix = '';

	/**
	 * The database table in use by this query.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	public $table = '';

	/**
	 * The sql statement (SELECT, INSERT, UPDATE, DELETE, etc.).
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $statement = '';

	/**
	 * The limit clause for the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var string|int
	 */
	private $limit = '';

	/**
	 * The group clause for the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $group = [];

	/**
	 * The order by clause for the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $order = [];

	/**
	 * The select clause for the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $select = [];

	/**
	 * The set clause for the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $set = [];

	/**
	 * Duplicate clause for the INSERT query.
	 *
	 * @since 4.1.5
	 *
	 * @var array
	 */
	private $onDuplicate = [];

	/**
	 * Ignore clause for the INSERT query.
	 *
	 * @since 4.1.6
	 *
	 * @var array
	 */
	private $ignore = false;

	/**
	 * The where clause for the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $where = [];

	/**
	 * The union clause for the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $union = [];

	/**
	 * The join clause for the SQL query.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $join = [];

	/**
	 * Determines whether the select statement should be distinct.
	 *
	 * @since 4.0.0
	 *
	 * @var bool
	 */
	private $distinct = false;

	/**
	 * The order by direction for the query.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $orderDirection = 'ASC';

	/**
	 * The query string is populated after the __toString function is run.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $query = '';

	/**
	 * The sql query results are stored here.
	 *
	 * @since 4.0.0
	 *
	 * @var mixed
	 */
	private $result;

	/**
	 * The method in which $wpdb will output results.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $output = 'OBJECT';

	/**
	 * Whether or not to strip tags.
	 *
	 * @since 4.0.0
	 *
	 * @var bool
	 */
	private $stripTags = false;

	/**
	 * Set which option to use to escape the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var int
	 */
	protected $escapeOptions = 0;

	/**
	 * A cache of all queries and their results.
	 *
	 * @var array
	 */
	private $cache = [];

	/**
	 * Whether or not to reset the cached results.
	 *
	 * @var bool
	 */
	private $shouldResetCache = false;

	/**
	 * Constant for escape options.
	 *
	 * @since 4.0.0
	 *
	 * @var int
	 */
	const ESCAPE_FORCE = 2;

	/**
	 * Constant for escape options.
	 *
	 * @since 4.0.0
	 *
	 * @var int
	 */
	const ESCAPE_STRIP_HTML = 4;

	/**
	 * Constant for escape options.
	 *
	 * @since 4.0.0
	 *
	 * @var int
	 */
	const ESCAPE_QUOTE = 8;

	/**
	 * List of model class instances.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $models = [];

	/**
	 * The last query that ran, stringified.
	 *
	 * @since 4.3.0
	 */
	public $lastQuery = '';

	/**
	 * Prepares the database class for use.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->init();
	}

	/**
	 * Initializes the DB class.
	 * This needs to be called after the class is instantiated or when switching between sites in a multisite environment.
	 * The latter is important because the prefix otherwise isn't updated.
	 *
	 * @since 4.6.1
	 *
	 * @return void
	 */
	public function init() {
		global $wpdb;
		$this->db            = $wpdb;
		$this->prefix        = $wpdb->prefix;
		$this->escapeOptions = self::ESCAPE_STRIP_HTML | self::ESCAPE_QUOTE;
	}

	/**
	 * If this is a clone, lets reset all the data.
	 *
	 * @since 4.0.0
	 */
	public function __clone() {
		// We need to reset the result separately as well since it is not in the default array.
		$this->reset( [ 'result' ] );
		$this->reset();
	}

	/**
	 * Gets all AIOSEO installed tables.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of custom AIOSEO tables.
	 */
	public function getInstalledTables() {
		$results = $this->db->get_results( 'SHOW TABLES', 'ARRAY_N' );

		return ! empty( $results ) ? wp_list_pluck( $results, 0 ) : [];
	}

	/**
	 * Get all the database info such as data size, index size, table list.
	 *
	 * @since 4.4.5
	 *
	 * @return array An array of the database info.
	 */
	public function getDatabaseInfo() {
		$tables       = [];
		$databaseSize = [];

		if ( defined( 'DB_NAME' ) ) {
			$databaseTableInformation = $this->db->get_results(
				$this->db->prepare(
					"SELECT
						table_name AS 'name',
						table_collation AS 'collation',
						engine AS 'engine',
						round( ( data_length / 1024 / 1024 ), 2 ) 'data',
						round( ( index_length / 1024 / 1024 ), 2 ) 'index'
					FROM information_schema.TABLES
					WHERE table_schema = %s
					ORDER BY name ASC;",
					DB_NAME
				)
			);

			$databaseSize = [
				'data'  => 0,
				'index' => 0,
			];

			$siteTablesPrefix = $this->db->get_blog_prefix( get_current_blog_id() );
			$globalTables     = $this->db->tables( 'global', true );
			foreach ( $databaseTableInformation as $table ) {
				// Only include tables matching the prefix of the current site, this is to prevent displaying all tables on a MS install not relating to the current.
				if ( is_multisite() && 0 !== strpos( $table->name, $siteTablesPrefix ) && ! in_array( $table->name, $globalTables, true ) ) {
					continue;
				}

				$tableType = ( 0 === strpos( $table->name, aioseo()->core->db->prefix . 'aioseo' ) ) ? 'aioseo' : 'other';

				$tables[ $tableType ][ $table->name ] = [
					'data'      => $table->data,
					'index'     => $table->index,
					'engine'    => $table->engine,
					'collation' => $table->collation
				];

				$databaseSize['data']  += $table->data;
				$databaseSize['index'] += $table->index;
			}
		}

		return [
			'tables' => $tables,
			'size'   => $databaseSize,
		];
	}

	/**
	 * Gets all columns from a table.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $table The name of the table to lookup columns for.
	 * @return array         An array of custom AIOSEO tables.
	 */
	public function getColumns( $table ) {
		if ( ! $this->tableExists( $table ) ) {
			return [];
		}

		$table           = $this->prefix . $table;
		$installedTables = json_decode( aioseo()->internalOptions->database->installedTables, true );

		if ( empty( $installedTables[ $table ] ) ) {
			$installedTables[ $table ]                           = $this->db->get_col( 'SHOW COLUMNS FROM `' . $table . '`' );
			aioseo()->internalOptions->database->installedTables = wp_json_encode( $installedTables );
		}

		return $installedTables[ $table ];
	}

	/**
	 * Checks if a table exists.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $table The name of the table.
	 * @return bool          Whether or not the table exists.
	 */
	public function tableExists( $table ) {
		$table           = $this->prefix . $table;
		$installedTables = json_decode( aioseo()->internalOptions->database->installedTables ?? '[]', true ) ?: [];
		if ( isset( $installedTables[ $table ] ) ) {
			return true;
		}

		$results = $this->db->get_results( "SHOW TABLES LIKE '" . $table . "'" );
		if ( empty( $results ) ) {
			return false;
		}

		$installedTables[ $table ]                           = [];
		aioseo()->internalOptions->database->installedTables = wp_json_encode( $installedTables );

		return true;
	}

	/**
	 * Checks if a column exists on a given table.
	 *
	 * @since 4.0.5
	 *
	 * @param  string $table  The name of the table.
	 * @param  string $column The name of the column.
	 * @return bool           Whether or not the column exists.
	 */
	public function columnExists( $table, $column ) {
		if ( ! $this->tableExists( $table ) ) {
			return false;
		}

		$columns = $this->getColumns( $table );

		return in_array( $column, $columns, true );
	}

	/**
	 * Gets the size of a table in bytes.
	 *
	 * @since 4.1.0
	 *
	 * @param  string $table The table to check.
	 * @return int           The size of the table in bytes.
	 */
	public function getTableSize( $table ) {
		$this->db->query( 'ANALYZE TABLE ' . $this->prefix . $table );
		$results = $this->db->get_results( '
			SELECT
				TABLE_NAME AS `table`,
				ROUND(SUM(DATA_LENGTH + INDEX_LENGTH)) AS `size`
			FROM information_schema.TABLES
			WHERE TABLE_SCHEMA = "' . $this->db->dbname . '"
			AND TABLE_NAME = "' . $this->prefix . $table . '"
			ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC;
		' );

		return ! empty( $results ) ? $results[0]->size : 0;
	}

	/**
	 * The query string in all its glory.
	 *
	 * @since 4.0.0
	 *
	 * @return string The actual query string.
	 */
	public function __toString() {
		switch ( strtoupper( $this->statement ) ) {
			case 'INSERT':
				$insert = 'INSERT ';
				if ( $this->ignore ) {
					$insert .= 'IGNORE ';
				}
				$insert   .= 'INTO ' . $this->table;
				$clauses   = [];
				$clauses[] = $insert;
				$clauses[] = 'SET ' . implode( ', ', $this->set );
				if ( ! empty( $this->onDuplicate ) ) {
					$clauses[] = 'ON DUPLICATE KEY UPDATE ' . implode( ', ', $this->onDuplicate );
				}

				break;
			case 'REPLACE':
				$clauses   = [];
				$clauses[] = "REPLACE INTO $this->table";
				$clauses[] = 'SET ' . implode( ', ', $this->set );

				break;
			case 'UPDATE':
				$clauses   = [];
				$clauses[] = "UPDATE $this->table";

				if ( count( $this->join ) > 0 ) {
					foreach ( (array) $this->join as $join ) {
						if ( is_array( $join[1] ) ) {
							$join_on = []; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
							foreach ( (array) $join[1] as $left => $right ) {
								$join_on[] = "$this->table.`$left` = `{$join[0]}`.`$right`"; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
							}
							// phpcs:disable Squiz.NamingConventions.ValidVariableName
							$clauses[] = "\t" . ( ( 'LEFT' === $join[2] || 'RIGHT' === $join[2] ) ? $join[2] . ' JOIN ' : 'JOIN ' ) . $join[0] . ' ON ' . implode( ' AND ', $join_on );
							// phpcs:enable Squiz.NamingConventions.ValidVariableName
						} else {
							$clauses[] = "\t" . ( ( 'LEFT' === $join[2] || 'RIGHT' === $join[2] ) ? $join[2] . ' JOIN ' : 'JOIN ' ) . "{$join[0]} ON {$join[1]}";
						}
					}
				}

				$clauses[] = 'SET ' . implode( ', ', $this->set );

				if ( count( $this->where ) > 0 ) {
					$clauses[] = "WHERE 1 = 1 AND\n\t" . implode( "\n\tAND ", $this->where );
				}

				if ( count( $this->order ) > 0 ) {
					$clauses[] = 'ORDER BY ' . implode( ', ', $this->order );
				}

				if ( $this->limit ) {
					$clauses[] = 'LIMIT ' . $this->limit;
				}

				break;

			case 'TRUNCATE':
				$clauses   = [];
				$clauses[] = "TRUNCATE TABLE $this->table";
				break;

			case 'DELETE':
				$clauses   = [];
				$clauses[] = "DELETE FROM $this->table";

				if ( count( $this->where ) > 0 ) {
					$clauses[] = "WHERE 1 = 1 AND\n\t" . implode( "\n\tAND ", $this->where );
				}

				if ( count( $this->order ) > 0 ) {
					$clauses[] = 'ORDER BY ' . implode( ', ', $this->order );
				}

				if ( $this->limit ) {
					$clauses[] = 'LIMIT ' . $this->limit;
				}

				break;
			case 'SELECT':
			case 'SELECT DISTINCT':
			default:
				// Select fields.
				$clauses   = [];
				$distinct  = ( $this->distinct || stripos( $this->statement, 'DISTINCT' ) !== false ) ? 'DISTINCT ' : '';
				$select    = ( count( $this->select ) > 0 ) ? implode( ",\n\t", $this->select ) : '*';
				$clauses[] = "SELECT {$distinct}\n\t{$select}";

				// Select table.
				$clauses[] = "FROM $this->table";

				// Select joins.
				if ( ! empty( $this->join ) && count( $this->join ) > 0 ) {
					foreach ( (array) $this->join as $join ) {
						if ( is_array( $join[1] ) ) {
							$join_on = []; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
							foreach ( (array) $join[1] as $left => $right ) {
								$join_on[] = "$this->table.`$left` = `{$join[0]}`.`$right`"; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
							}
							// phpcs:disable Squiz.NamingConventions.ValidVariableName
							$clauses[] = "\t" . ( ( 'LEFT' === $join[2] || 'RIGHT' === $join[2] ) ? $join[2] . ' JOIN ' : 'JOIN ' ) . $join[0] . ' ON ' . implode( ' AND ', $join_on );
							// phpcs:enable Squiz.NamingConventions.ValidVariableName
						} else {
							$clauses[] = "\t" . ( ( 'LEFT' === $join[2] || 'RIGHT' === $join[2] ) ? $join[2] . ' JOIN ' : 'JOIN ' ) . "{$join[0]} ON {$join[1]}";
						}
					}
				}

				// Select conditions.
				if ( count( $this->where ) > 0 ) {
					$clauses[] = "WHERE 1 = 1 AND\n\t" . implode( "\n\tAND ", $this->where );
				}

				// Union queries.
				if ( count( $this->union ) > 0 ) {
					foreach ( $this->union as $union ) {
						$keyword   = ( $union[1] ) ? 'UNION' : 'UNION ALL';
						$clauses[] = "\n$keyword\n\n$union[0]";
					}

					$clauses[] = '';
				}

				// Select groups.
				if ( count( $this->group ) > 0 ) {
					$clauses[] = 'GROUP BY ' . implode( ', ', $this->escapeColNames( $this->group ) );
				}

				// Select order.
				if ( count( $this->order ) > 0 ) {
					$orderFragments = [];
					foreach ( $this->escapeColNames( $this->order ) as $col ) {
						$orderFragments[] = ( preg_match( '/ (ASC|DESC|RAND\(\))$/i', (string) $col ) ) ? $col : "$col $this->orderDirection";
					}

					$clauses[] = 'ORDER BY ' . implode( ', ', $orderFragments );
				}

				// Select limit.
				if ( $this->limit ) {
					$clauses[] = 'LIMIT ' . $this->limit;
				}

				break;
		}

		// @HACK for wpdb::prepare.
		$clauses[] = '/* %d = %d */';

		$this->query = str_replace( '%%d = %%d', '%d = %d', str_replace( '%', '%%', implode( "\n", $clauses ) ) );

		// Flag queries with double quotes down, but not if the double quotes are contained within a string value (like JSON).
		if ( aioseo()->isDev && preg_match( '/\{[^}]*\}(*SKIP)(*FAIL)|\[[^]]*\](*SKIP)(*FAIL)|\'[^\']*\'(*SKIP)(*FAIL)|\\"(*SKIP)(*FAIL)|"/i', (string) $this->query ) ) {
			// phpcs:disable WordPress.PHP.DevelopmentFunctions
			error_log(
				"Query with double quotes detected - this may cause isues when ANSI_QUOTES is enabled:\r\n" .
				$this->query . "\r\n" . wp_debug_backtrace_summary()
			);
			// phpcs:enable WordPress.PHP.DevelopmentFunctions
		}

		$this->lastQuery = $this->query;

		return $this->query;
	}

	/**
	 * Shortcut method to return the query string.
	 *
	 * @since 4.0.0
	 *
	 * @return string The query string.
	 */
	public function query() {
		return $this->__toString();
	}

	/**
	 * Start a new Database Query.
	 *
	 * @since 4.0.0
	 *
	 * @param  string   $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  bool     $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @param  string   $statement      The MySQL statement for the query.
	 * @return Database                 Returns the Database class which can then be method chained for building the query.
	 */
	public function start( $table = '', $includesPrefix = false, $statement = 'SELECT' ) {
		// Always reset everything when starting a new query.
		$this->reset();
		$this->table     = $includesPrefix ? $table : $this->prefix . $table;
		$this->statement = $statement;

		return $this;
	}

	/**
	 * Shortcut method for start with INSERT as the statement.
	 *
	 * @since 4.0.0
	 *
	 * @param  string   $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  bool     $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                 Returns the Database class which can then be method chained for building the query.
	 */
	public function insert( $table = '', $includesPrefix = false ) {
		return $this->start( $table, $includesPrefix, 'INSERT' );
	}

	/**
	 * Shortcut method for start with INSERT IGNORE as the statement.
	 *
	 * @since 4.1.6
	 *
	 * @param  string   $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  bool     $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                 Returns the Database class which can then be method chained for building the query.
	 */
	public function insertIgnore( $table = '', $includesPrefix = false ) {
		$this->ignore = true;

		return $this->start( $table, $includesPrefix, 'INSERT' );
	}

	/**
	 * Shortcut method for start with UPDATE as the statement.
	 *
	 * @since 4.0.0
	 *
	 * @param  string   $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  bool     $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                 Returns the Database class which can then be method chained for building the query.
	 */
	public function update( $table = '', $includesPrefix = false ) {
		return $this->start( $table, $includesPrefix, 'UPDATE' );
	}

	/**
	 * Shortcut method for start with REPLACE as the statement.
	 *
	 * @since 4.0.0
	 *
	 * @param  string   $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  bool     $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                 Returns the Database class which can then be method chained for building the query.
	 */
	public function replace( $table = '', $includesPrefix = false ) {
		return $this->start( $table, $includesPrefix, 'REPLACE' );
	}

	/**
	 * Shortcut method for start with TRUNCATE as the statement.
	 *
	 * @since 4.0.0
	 *
	 * @param  string   $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  bool     $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                 Returns the Database class which can then be method chained for building the query.
	 */
	public function truncate( $table = '', $includesPrefix = false ) {
		return $this->start( $table, $includesPrefix, 'TRUNCATE' );
	}

	/**
	 * Shortcut method for start with DELETE as the statement.
	 *
	 * @since 4.0.0
	 *
	 * @param  string   $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  bool     $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                 Returns the Database class which can then be method chained for building the query.
	 */
	public function delete( $table = '', $includesPrefix = false ) {
		return $this->start( $table, $includesPrefix, 'DELETE' );
	}

	/**
	 * Adds a SELECT clause.
	 *
	 * @since 4.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function select() {
		$args = (array) func_get_args();
		if ( count( $args ) === 1 && is_array( $args[0] ) ) {
			$args = $args[0];
		}

		$this->select = array_merge( $this->select, $this->escapeColNames( $args ) );

		return $this;
	}

	/**
	 * Adds a WHERE clause.
	 *
	 * @since 4.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function where() {
		$criteria = $this->prepArgs( func_get_args() );

		foreach ( (array) $criteria as $field => $value ) {
			if ( ! preg_match( '/[\(\)<=>!]+/', (string) $field ) && false === stripos( $field, ' IS ' ) ) {
				$operator = ( is_null( $value ) ) ? 'IS' : '=';
				$escaped  = $this->escapeColNames( $field );
				$field    = array_pop( $escaped ) . ' ' . $operator;
			}

			if ( is_null( $value ) && false !== stripos( $field, ' IS ' ) ) {
				// WHERE `field` IS NOT NULL.
				$this->where[] = "$field NULL";
				continue;
			}

			if ( is_null( $value ) ) {
				// WHERE `field` IS NULL.
				$this->where[] = "$field NULL";
				continue;
			}

			if ( is_array( $value ) ) {
				$wheres = [];
				foreach ( (array) $value as $val ) {
					$wheres[] = sprintf( "$field %s", $this->escape( $val, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
				}

				$this->where[] = '(' . implode( ' OR ', $wheres ) . ')';
				continue;
			}

			$this->where[] = sprintf( "$field %s", $this->escape( $value, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
		}

		return $this;
	}

	/**
	 * Adds a complex WHERE clause.
	 *
	 * @since 4.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function whereRaw() {
		$criteria = $this->prepArgs( func_get_args() );

		foreach ( (array) $criteria as $clause ) {
			$this->where[] = $clause;
		}

		return $this;
	}

	/**
	 * Adds a WHERE clause with all arguments sent separated by OR instead of AND inside a subclause.
	 * @example [ 'a' => 1, 'b' => 2 ] becomes "AND (a = 1 OR b = 2)"
	 *
	 * @since 4.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function whereOr() {
		$criteria = $this->prepArgs( func_get_args() );

		$or = [];
		foreach ( (array) $criteria as $field => $value ) {
			if ( ! preg_match( '/[\(\)<=>!]+/', (string) $field ) && false === stripos( $field, ' IS ' ) ) {
				$operator = ( is_null( $value ) ) ? 'IS' : '=';
				$field    = $this->escapeColNames( $field );
				$field    = array_pop( $field ) . ' ' . $operator;
			}

			if ( is_null( $value ) && false !== stripos( $field, ' IS ' ) ) {
				// WHERE `field` IS NOT NULL.
				$or[] = "$field NULL";
				continue;
			}

			if ( is_null( $value ) ) {
				// WHERE `field` IS NULL.
				$or[] = "$field NULL";
			}

			$or[] = sprintf( "$field %s", $this->escape( $value, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
		}

		// Create our subclause, and add it to the WHERE array.
		$this->where[] = '(' . implode( ' OR ', $or ) . ')';

		return $this;
	}

	/**
	 * Adds a WHERE IN() clause.
	 *
	 * @since 4.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function whereIn() {
		$criteria = $this->prepArgs( func_get_args() );

		foreach ( (array) $criteria as $field => $values ) {
			if ( ! is_array( $values ) ) {
				$values = [ $values ];
			}

			if ( count( $values ) === 0 ) {
				continue;
			}

			foreach ( $values as &$value ) {
				// Note: We can no longer check for `is_numeric` because a value like `61021e6242255` returns true and breaks the query.
				if ( is_int( $value ) || is_float( $value ) ) {
					// No change.
					continue;
				}

				if ( is_null( $value ) || 'null' === strtolower( $value ) ) {
					// Change to a true NULL value.
					$value = null;
					continue;
				}

				$value = sprintf( '%s', $this->escape( $value, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
			}

			$values = implode( ',', $values );
			$this->whereRaw( "$field IN ($values)" );
		}

		return $this;
	}

	/**
	 * Adds a WHERE NOT IN() clause.
	 *
	 * @since 4.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function whereNotIn() {
		$criteria = $this->prepArgs( func_get_args() );

		foreach ( (array) $criteria as $field => $values ) {
			if ( ! is_array( $values ) ) {
				$values = [ $values ];
			}

			if ( count( $values ) === 0 ) {
				continue;
			}

			foreach ( $values as &$value ) {
				if ( is_numeric( $value ) ) {
					// No change.
					continue;
				}

				if ( is_null( $value ) || false !== stristr( $value, 'NULL' ) ) {
					// Change to a true NULL value.
					$value = null;
					continue;
				}

				$value = sprintf( '%s', $this->escape( $value, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
			}

			$values = implode( ',', $values );
			$this->whereRaw( "$field NOT IN($values)" );
		}

		return $this;
	}

	/**
	 * Adds a WHERE BETWEEN clause.
	 *
	 * @since 4.3.0
	 *
	 * @return Database  Returns the Database class which can be method chained for more query building.
	 */
	public function whereBetween() {
		$criteria = $this->prepArgs( func_get_args() );

		foreach ( (array) $criteria as $field => $values ) {
			if ( ! is_array( $values ) ) {
				$values = [ $values ];
			}

			if ( count( $values ) === 0 ) {
				continue;
			}

			foreach ( $values as &$value ) {
				// Note: We can no longer check for `is_numeric` because a value like `61021e6242255` returns true and breaks the query.
				if ( is_int( $value ) || is_float( $value ) ) {
					// No change.
					continue;
				}

				if ( is_null( $value ) || false !== stristr( $value, 'NULL' ) ) {
					// Change to a true NULL value.
					$value = null;
					continue;
				}

				$value = sprintf( '%s', $this->escape( $value, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
			}

			$values = implode( ' AND ', $values );
			$this->whereRaw( "$field BETWEEN $values" );
		}

		return $this;
	}

	/**
	 * Adds a LEFT JOIN clause.
	 *
	 * @since 4.0.0
	 *
	 * @param  string       $table          The name of the table to join to this query.
	 * @param  string|array $conditions     The conditions of the join clause.
	 * @param  bool         $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                     Returns the Database class which can be method chained for more query building.
	 */
	public function leftJoin( $table = '', $conditions = '', $includesPrefix = false ) {
		return $this->join( $table, $conditions, 'LEFT', $includesPrefix );
	}

	/**
	 * Adds a JOIN clause.
	 *
	 * @since 4.0.0
	 *
	 * @param  string       $table          The name of the table to join to this query.
	 * @param  string|array $conditions     The conditions of the join clause.
	 * @param  string       $direction      This can take 'LEFT' or 'RIGHT' as arguments.
	 * @param  bool         $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                     Returns the Database class which can be method chained for more query building.
	 */
	public function join( $table = '', $conditions = '', $direction = '', $includesPrefix = false ) {
		$this->join[] = [ $includesPrefix ? $table : $this->prefix . $table, $conditions, $direction ];

		return $this;
	}

	/**
	 * Add a UNION query.
	 *
	 * @since 4.0.0
	 *
	 * @param  Database|string $query    The query (Database object or query string) to be joined with.
	 * @param  bool            $distinct Set whether this union should be distinct or not.
	 * @return Database                  Returns the Database class which can be method chained for more query building.
	 */
	public function union( $query, $distinct = true ) {
		$this->union[] = [ $query, $distinct ];

		return $this;
	}

	/**
	 * Adds am GROUP BY clause.
	 *
	 * @since 4.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function groupBy() {
		$args = (array) func_get_args();
		if ( count( $args ) === 1 && is_array( $args[0] ) ) {
			$args = $args[0];
		}

		$this->group = array_merge( $this->group, $args );

		return $this;
	}

	/**
	 * Adds am ORDER BY clause.
	 *
	 * @since   4.0.0
	 * @version 4.8.2 Hardened against SQL injection.
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function orderBy() {
		// Normalize arguments.
		$args = (array) func_get_args();
		if ( count( $args ) === 1 && is_array( $args[0] ) ) {
			$args = $args[0];
		}

		$orderBy = [];
		// Separate commas to account for multiple orders.
		foreach ( $args as $argComma ) {
			$orderBy = array_map( 'trim', array_merge( $orderBy, explode( ',', $argComma ) ) );
		}

		// Validate and sanitize column names and sort directions.
		$sanitizedOrderBy = [];
		foreach ( $orderBy as $ordBy ) {
			$parts     = explode( ' ', $ordBy );
			$column    = str_replace( '`', '', $parts[0] ); // Strip existing ticks first.
			$column    = preg_replace( '/[^a-zA-Z0-9_.]/', '', $column ); // Strip invalid characters from the column name.
			$column    = $this->escapeColNames( $column )[0];
			$direction = isset( $parts[1] ) ? strtoupper( $parts[1] ) : 'ASC';

			// Validate the order direction.
			if ( ! in_array( $direction, [ 'ASC', 'DESC' ], true ) ) {
				$direction = 'ASC';
			}

			$sanitizedOrderBy[] = "$column $direction";
		}

		if ( ! empty( $sanitizedOrderBy ) ) {
			if ( ! empty( $args[0] ) && true !== $args[0] ) {
				$this->order = array_merge( $this->order, $sanitizedOrderBy );
			} else {
				// This allows for overwriting a preexisting order-by setting.
				array_shift( $sanitizedOrderBy );
				$this->order = $sanitizedOrderBy;
			}
		}

		return $this;
	}

	/**
	 * Adds a raw ORDER BY clause.
	 *
	 * @since 4.8.2
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function orderByRaw() {
		$args = (array) func_get_args();
		if ( count( $args ) === 1 && is_array( $args[0] ) ) {
			$args = $args[0];
		}

		$this->order = array_merge( $this->order, $args );

		return $this;
	}

	/**
	 * Sets the sort direction for ORDER BY clauses.
	 *
	 * @since 4.0.0
	 *
	 * @param  string    $direction This sets the direction of the order by clause, default is 'ASC'.
	 * @return Database             Returns the Database class which can be method chained for more query building.
	 */
	public function orderDirection( $direction = 'ASC' ) {
		$this->orderDirection = $direction;

		return $this;
	}

	/**
	 * Adds a LIMIT clause.
	 *
	 * @since 4.0.0
	 *
	 * @param  int      $limit  The amount of rows to limit the query to.
	 * @param  int      $offset The amount of rows the result of the query should be ofset with.
	 * @return Database         Returns the Database class which can be method chained for more query building.
	 */
	public function limit( $limit, $offset = -1 ) {
		if ( ! is_numeric( $limit ) || $limit <= 0 ) {
			return $this;
		}

		if ( ! is_numeric( $offset ) ) {
			$offset = -1;
		}

		$this->limit = ( -1 === $offset )
			? intval( $limit )
			: intval( $offset ) . ', ' . intval( $limit );

		return $this;
	}

	/**
	 * Converts associative arrays to a SET argument.
	 *
	 * @since 4.1.5
	 *
	 * @param  array $args The arguments.
	 * @return array       The prepared arguments.
	 */
	private function prepareSet( $args ) {
		$args = $this->prepArgs( $args );

		$preparedSet = [];
		foreach ( (array) $args as $field => $value ) {
			if ( is_null( $value ) ) {
				$preparedSet[] = "`$field` = NULL";
				continue;
			}

			if ( is_array( $value ) ) {
				throw new \Exception( 'Cannot save an unserialized array in the database. Data passed was: ' . wp_json_encode( $value ) );
			}

			if ( is_object( $value ) ) {
				throw new \Exception( 'Cannot save an unserialized object in the database. Data passed was: ' . esc_html( $value ) );
			}

			$preparedSet[] = sprintf( "`$field` = %s", $this->escape( $value, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
		}

		return $preparedSet;
	}

	/**
	 * Adds a SET clause.
	 *
	 * @since 4.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function set() {
		$this->set = array_merge( $this->set, $this->prepareSet( func_get_args() ) );

		return $this;
	}

	/**
	 * Adds an ON DUPLICATE clause.
	 *
	 * @since 4.1.5
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function onDuplicate() {
		$this->onDuplicate = array_merge( $this->onDuplicate, $this->prepareSet( func_get_args() ) );

		return $this;
	}

	/**
	 * Set the output for the query.
	 *
	 * @since 4.0.0
	 *
	 * @param  string   $output This can be one of the following: ARRAY_A | ARRAY_N | OBJECT | OBJECT_K.
	 * @return Database         Returns the Database class which can be method chained for more query building.
	 */
	public function output( $output = 'OBJECT' ) {
		if ( ! $output ) {
			$output = 'OBJECT';
		}

		$this->output = $output;

		return $this;
	}

	/**
	 * Reset the cache so we make sure the query gets to the DB.
	 *
	 * @since 4.1.6
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function resetCache() {
		$this->shouldResetCache = true;

		return $this;
	}

	/**
	 * Run this query.
	 *
	 * @since 4.0.0
	 *
	 * @param  bool     $reset  Whether to reset the results/query.
	 * @param  string   $return Determine which method to call on the $wpdb object
	 * @param  array    $params Optional extra parameters to pass to the db method call
	 * @return Database         Returns the Database class which can be method chained for more query building.
	 */
	public function run( $reset = true, $return = 'results', $params = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		if ( ! in_array( $return, [ 'results', 'col', 'var', 'row' ], true ) ) {
			$return = 'results';
		}

		$prepare        = $this->db->prepare( $this->query(), 1, 1 );
		$queryHash      = sha1( $this->query() );
		$cacheTableName = $this->getCacheTableName();

		// Pull the result from the in-memory cache if everything checks out.
		if (
			! $this->shouldResetCache &&
			! in_array( $this->statement, [ 'INSERT', 'REPLACE', 'UPDATE', 'DELETE' ], true ) &&
			isset( $this->cache[ $cacheTableName ][ $queryHash ][ $return ] ) &&
			empty( $this->join )
		) {
			$this->result = $this->cache[ $cacheTableName ][ $queryHash ][ $return ];

			return $this;
		}

		switch ( $return ) {
			case 'col':
				$this->result = $this->db->get_col( $prepare );
				break;
			case 'var':
				$this->result = $this->db->get_var( $prepare );
				break;
			case 'row':
				$this->result = $this->db->get_row( $prepare );
				break;
			default:
				$this->result = $this->db->get_results( $prepare, $this->output );
		}

		if ( $reset ) {
			$this->reset();
		}

		$this->cache[ $cacheTableName ][ $queryHash ][ $return ] = $this->result;

		// Reset the cache trigger for the next run.
		$this->shouldResetCache = false;

		return $this;
	}

	/**
	 * Inject a count select statement and return the result.
	 *
	 * @since 4.1.0
	 *
	 * @param  string $countColumn The column to count with. Defaults to '*' all.
	 * @return int                 The number of rows that were found.
	 */
	public function count( $countColumn = '*' ) {
		$usingGroup = ! empty( $this->group );
		$results    = $this->reset( [ 'select', 'order', 'limit' ] )
			->select( 'count(' . $countColumn . ') as count' )
			->run()
			->result();

		return 1 === $this->numRows() && ! $usingGroup
			? (int) $results[0]->count
			: $this->numRows();
	}

	/**
	 * Inject a count group select statement and return the result.
	 *
	 * @since 4.6.1
	 *
	 * @param  string $countDistinctColumn The column to count with. Defaults to '*' all.
	 * @return int                         The number of rows that were found.
	 */
	public function countDistinct( $countDistinctColumn = '*' ) {
		$countDistinctColumn = '*' !== $countDistinctColumn ? 'distinct( ' . $countDistinctColumn . ' )' : $countDistinctColumn;

		return $this->reset( [ 'select', 'order', 'limit' ] )
			->select( 'count(' . $countDistinctColumn . ') as count' )
			->run( true, 'var' )
			->result();
	}

	/**
	 * Returns the query results based on the value of the output property.
	 *
	 * @since 4.0.0
	 *
	 * @return mixed This depends on what was set in the output property.
	 */
	public function result() {
		return $this->result;
	}

	/**
	 * Return a model model from a row.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $class The name of the model class to call.
	 * @return object        The model class instance.
	 */
	public function model( $class ) {
		$result = $this->result();

		return ! empty( $result )
			? ( is_array( $result )
				? new $class( (array) current( $result ) )
				: $result )
			: new $class();
	}

	/**
	 * Return an array of model class instancnes from the result.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $class  The name of the model class to call.
	 * @param  string $id     The ID of the index to use.
	 * @param  bool   $toJson The index if necessary.
	 * @return array          An array of model class instances.
	 */
	public function models( $class, $id = null, $toJson = false ) {
		if ( ! empty( $this->models ) ) {
			return $this->models;
		}

		$i      = 0;
		$models = [];
		foreach ( $this->result() as $row ) {
			$var   = ( null === $id ) ? $row : $row[ $id ];
			$class = new $class( $var );
			// Lets add the class to the array using the class ID.
			$models[ $class->id ] = $toJson ? $class->jsonSerialize() : $class;
			$i++;
		}

		$this->models = $models;

		return $this->models;
	}

	/**
	 * Returns the last error reported by MySQL.
	 *
	 * @since 4.0.0
	 *
	 * @return string The last error message.
	 */
	public function lastError() {
		return $this->db->last_error;
	}

	/**
	 * Return the $wpdb insert_id from the last query.
	 *
	 * @since 4.0.0
	 *
	 * @return int The ID of the most recent INSERT query.
	 */
	public function insertId() {
		return $this->db->insert_id;
	}

	/**
	 * Return the $wpdb rows_affected from the last query.
	 *
	 * @since 4.0.0
	 *
	 * @return int The number of rows affected.
	 */
	public function rowsAffected() {
		return $this->db->rows_affected;
	}

	/**
	 * Return the $wpdb num_rows from the last query.
	 *
	 * @since 4.0.0
	 *
	 * @return int The count for the number of rows in the last query.
	 */
	public function numRows() {
		return $this->db->num_rows;
	}

	/**
	 * Check if the last query had any rows.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether there were any rows retrived by the last query.
	 */
	public function nullSet() {
		return ( $this->numRows() < 1 );
	}

	/**
	 * This will start a MySQL transaction. Be sure to commit or rollback!
	 *
	 * @since 4.0.0
	 */
	public function startTransaction() {
		$this->db->query( 'START TRANSACTION' );
	}

	/**
	 * This will commit a MySQL transaction. Used in conjunction with startTransaction.
	 *
	 * @since 4.0.0
	 */
	public function commit() {
		$this->db->query( 'COMMIT' );
	}

	/**
	 * This will rollback a MySQL transaction. Used in conjunction with startTransaction.
	 *
	 * @since 4.0.0
	 */
	public function rollback() {
		$this->db->query( 'ROLLBACK' );
	}

	/**
	 * Fast way to execute raw queries.
	 * NOTE: When using this method, all arguments must be sanitized manually!
	 *
	 * @since 4.0.0
	 *
	 * @param  string $sql      The sql query to execute.
	 * @param  bool   $results  Whether to return the results or not.
	 * @param  bool   $useCache Whether to use the cache or not.
	 * @return mixed            Could be an array or object depending on the result set.
	 */
	public function execute( $sql, $results = false, $useCache = false ) {
		$this->lastQuery = $sql;
		$queryHash       = sha1( $sql );
		$cacheTableName  = $this->getCacheTableName();

		// Pull the result from the in-memory cache if everything checks out.
		if (
			$useCache &&
			! $this->shouldResetCache &&
			isset( $this->cache[ $cacheTableName ][ $queryHash ] )
		) {
			if ( $results ) {
				$this->result = $this->cache[ $cacheTableName ][ $queryHash ];
			}

			return $this;
		}

		if ( $results ) {
			$this->result = $this->db->get_results( $sql, $this->output );

			if ( $useCache ) {
				$this->cache[ $cacheTableName ][ $queryHash ] = $this->result;

				// Reset the cache trigger for the next run.
				$this->shouldResetCache = false;
			}

			return $this;
		}

		return $this->db->query( $sql );
	}

	/**
	 * Escape a value for safe use in SQL queries.
	 *
	 * @param string   $value   The value to be escaped.
	 * @param int|null $options The escape options.
	 * @return string           The escaped SQL value.
	 */
	public function escape( $value, $options = null ) {
		if ( is_array( $value ) ) {
			foreach ( $value as &$val ) {
				$val = $this->escape( $val, $options );
			}

			return $value;
		}

		$options = ( is_null( $options ) ) ? $this->getEscapeOptions() : $options;
		if ( ( $options & self::ESCAPE_STRIP_HTML ) !== 0 && isset( $this->stripTags ) && true === $this->stripTags ) {
			$value = wp_strip_all_tags( $value );
		}

		if (
			( ( $options & self::ESCAPE_FORCE ) !== 0 || php_sapi_name() === 'cli' ) ||
			( ( $options & self::ESCAPE_QUOTE ) !== 0 && ! is_int( $value ) )
		) {
			$value = esc_sql( $value );
			if ( ! is_int( $value ) ) {
				$value = "'$value'";
			}
		}

		return $value;
	}

	/**
	 * Returns the current escape options value.
	 *
	 * @since 4.0.0
	 *
	 * @return int The current escape options value.
	 */
	public function getEscapeOptions() {
		return $this->escapeOptions;
	}


	/**
	 * Sets the current escape options value.
	 *
	 * @since 4.0.0
	 *
	 * @param int $options The escape options value.
	 */
	public function setEscapeOptions( $options ) {
		$this->escapeOptions = $options;
	}

	/**
	 * Backtick-escapes an array of column and/or table names.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $cols An array of column names to be escaped.
	 * @return array       An array of escaped column names.
	 */
	private function escapeColNames( $cols ) {
		if ( ! is_array( $cols ) ) {
			$cols = [ $cols ];
		}

		foreach ( $cols as &$col ) {
			if ( false === stripos( $col, '(' ) && false === stripos( $col, ' ' ) && false === stripos( $col, '*' ) ) {
				if ( stripos( $col, '.' ) ) {
					list( $table, $c ) = explode( '.', $col );
					$col = "`$table`.`$c`";
					continue;
				}

				$col = "`$col`";
			}
		}

		return $cols;
	}

	/**
	 * Gets a variable list of function arguments and reformats them as needed for many of the functions of this class.
	 *
	 * @since 4.0.0
	 *
	 * @param  mixed $values This could be anything, but if used properly it usually is a string or an array.
	 * @return mixed         If the preparation was successful, it will return an array of arguments. Otherwise it could be anything.
	 */
	private function prepArgs( $values ) {
		$values = (array) $values;
		if ( ! is_array( $values[0] ) && count( $values ) === 2 ) {
			$values = [ $values[0] => $values[1] ];
		} elseif ( is_array( $values[0] ) && count( $values ) === 1 ) {
			$values = $values[0];
		}

		return $values;
	}

	/**
	 * Resets all the variables that make up the query.
	 *
	 * @since 4.0.0
	 *
	 * @param  array    $what Set which properties you want to reset. All are selected by default.
	 * @return Database       Returns the Database instance.
	 */
	public function reset(
		$what = [
			'table',
			'statement',
			'limit',
			'group',
			'order',
			'select',
			'set',
			'onDuplicate',
			'ignore',
			'where',
			'union',
			'distinct',
			'orderDirection',
			'query',
			'output',
			'stripTags',
			'models',
			'join'
		]
	) {
		// If we are not running a select query, let's bust the cache for this table.
		$selectStatements = [ 'SELECT', 'SELECT DISTINCT' ];
		if (
			! empty( $this->statement ) &&
			! in_array( $this->statement, $selectStatements, true )
		) {
			$this->bustCache( $this->getCacheTableName() );
		}

		foreach ( (array) $what as $var ) {
			switch ( $var ) {
				case 'group':
				case 'order':
				case 'select':
				case 'set':
				case 'onDuplicate':
				case 'where':
				case 'union':
				case 'join':
					$this->$var = [];
					break;
				case 'orderDirection':
					$this->$var = 'ASC';
					break;
				case 'ignore':
				case 'stripTags':
					$this->$var = false;
					break;
				case 'output':
					$this->$var = 'OBJECT';
					break;
				default:
					if ( isset( $this->$var ) ) {
						$this->$var = null;
					}
					break;
			}
		}

		return $this;
	}

	/**
	 * Returns the current value of one or more query properties.
	 *
	 * @since 4.0.0
	 *
	 * @param  string|array  $what You can pass in an array of options to retrieve. By default it selects all if them.
	 * @return string|array       Returns the value of whichever variables are passed in.
	 */
	public function getQueryProperty(
		$what = [
			'table',
			'statement',
			'limit',
			'group',
			'order',
			'select',
			'set',
			'onDuplicate',
			'where',
			'union',
			'distinct',
			'orderDirection',
			'query',
			'output',
			'result'
		]
	) {
		if ( is_array( $what ) ) {
			$return = [];
			foreach ( (array) $what as $which ) {
				$return[ $which ] = $this->$which;
			}

			return $return;
		}

		return $this->$what;
	}

	/**
	 * Get a table name for the cache key.
	 *
	 * @since 4.1.6
	 *
	 * @param  string $cacheTableName The table name to check against.
	 * @return string                 The cache key table name.
	 */
	private function getCacheTableName( $cacheTableName = '' ) {
		$cacheTableName = empty( $cacheTableName ) ? $this->table : $cacheTableName;

		foreach ( $this->customTables as $tableName ) {
			if ( false !== stripos( (string) $cacheTableName, $this->prefix . $tableName ) ) {
				$cacheTableName = $tableName;
				break;
			}
		}

		return $cacheTableName;
	}

	/**
	 * Busts the cache for the given table name.
	 *
	 * @since 4.1.6
	 *
	 * @param  string $tableName The table name.
	 * @return void
	 */
	public function bustCache( $tableName = '' ) {
		if ( ! $tableName ) {
			// Bust all the cache.
			$this->cache = [];

			return;
		}

		unset( $this->cache[ $tableName ] );
	}

	/**
	 * In order to not have a conflict, we need to return a clone.
	 *
	 * @since 4.1.0
	 *
	 * @return Database The cloned Database instance.
	 */
	public function noConflict() {
		return clone $this;
	}

	/**
	 * Checks whether the given index exists on the given table.
	 *
	 * @since 4.4.8
	 *
	 * @param  string $tableName      The table name.
	 * @param  string $indexName      The index name.
	 * @param  bool   $includesPrefix Whether the table name includes the WordPress prefix or not.
	 * @return bool                   Whether the index exists or not.
	 */
	public function indexExists( $tableName, $indexName, $includesPrefix = false ) {
		$prefix    = $includesPrefix ? '' : $this->prefix;
		$tableName = strtolower( $prefix . $tableName );
		$indexName = strtolower( $indexName );

		$indexes = $this->db->get_results( "SHOW INDEX FROM `$tableName`" );
		foreach ( $indexes as $index ) {
			if ( empty( $index->Key_name ) ) {
				continue;
			}

			if ( strtolower( $index->Key_name ) === $indexName ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Acquires a database lock with the given name.
	 *
	 * @since 4.8.3
	 *
	 * @param  string  $lockName The name of the lock to acquire.
	 * @param  integer $timeout  The timeout in seconds. Default is 0 which means it will return immediately if the lock cannot be acquired.
	 * @return boolean           Whether the lock was acquired.
	 */
	public function acquireLock( $lockName, $timeout = 0 ) {
		$lockResult = $this->db->get_var( $this->db->prepare( 'SELECT GET_LOCK(%s, %d)', $lockName, $timeout ) );
		$acquired   = '1' === $lockResult;

		if ( $acquired ) {
			// Register a shutdown function to always release the lock even if a fatal error occurs.
			register_shutdown_function( function () use ( $lockName ) {
				$this->releaseLock( $lockName );
			} );
		}

		return $acquired;
	}

	/**
	 * Releases a database lock with the given name.
	 *
	 * @since 4.8.3
	 *
	 * @param  string  $lockName The name of the lock to release.
	 * @return boolean           Whether the lock was released.
	 */
	public function releaseLock( $lockName ) {
		$releaseResult = $this->db->query( $this->db->prepare( 'SELECT RELEASE_LOCK(%s)', $lockName ) );

		return false !== $releaseResult;
	}
}Common/Utils/Features.php000064400000012002151536241210011356 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

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

/**
 * Contains helper methods specific to the addons.
 *
 * @since 4.3.0
 */
class Features {
	/**
	 * The features URL.
	 *
	 * @since 4.3.0
	 *
	 * @var string
	 */
	protected $featuresUrl = 'https://licensing-cdn.aioseo.com/keys/lite/all-in-one-seo-pack-pro-features.json';

	/**
	 * Returns our features.
	 *
	 * @since 4.3.0
	 *
	 * @param  boolean $flushCache Whether or not to flush the cache.
	 * @return array               An array of addon data.
	 */
	public function getFeatures( $flushCache = false ) {
		$features = aioseo()->core->networkCache->get( 'license_features' );
		if ( null === $features || $flushCache ) {
			$response = aioseo()->helpers->wpRemoteGet( $this->getFeaturesUrl() );
			if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
				$features = json_decode( wp_remote_retrieve_body( $response ), true );
			}

			if ( ! $features || ! empty( $features->error ) ) {
				$features = $this->getDefaultFeatures();
			}

			aioseo()->core->networkCache->update( 'license_features', $features );
		}

		// Convert the features array to objects using JSON. This is essential because we have lots of features that rely on this to be an object, and changing it to an array would break them.

		$features = json_decode( wp_json_encode( $features ) );

		return $features;
	}

	/**
	 * Get the URL to get features.
	 *
	 * @since 4.1.8
	 *
	 * @return string The URL.
	 */
	protected function getFeaturesUrl() {
		$url = $this->featuresUrl;
		if ( defined( 'AIOSEO_FEATURES_URL' ) ) {
			$url = AIOSEO_FEATURES_URL;
		}

		return $url;
	}

	/**
	 * Retrieves a default list of all external saas features available for the current user if the API cannot be reached.
	 *
	 * @since 4.3.0
	 *
	 * @return array An array of features.
	 */
	protected function getDefaultFeatures() {
		return json_decode( wp_json_encode( [
			[
				'license_level' => 'pro',
				'section'       => 'schema',
				'feature'       => 'event'
			],
			[
				'license_level' => 'elite',
				'section'       => 'schema',
				'feature'       => 'event'
			],
			[
				'license_level' => 'elite',
				'section'       => 'schema',
				'feature'       => 'job-posting'
			],
			[
				'license_level' => 'elite',
				'section'       => 'tools',
				'feature'       => 'network-tools-site-activation'
			],
			[
				'license_level' => 'elite',
				'section'       => 'tools',
				'feature'       => 'network-tools-database'
			],
			[
				'license_level' => 'elite',
				'section'       => 'tools',
				'feature'       => 'network-tools-import-export'
			],
			[
				'license_level' => 'elite',
				'section'       => 'tools',
				'feature'       => 'network-tools-robots'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'seo-statistics'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'keyword-rankings'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'keyword-rankings-pages'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'content-rankings'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'post-detail'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'post-detail-page-speed'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'post-detail-seo-statistics'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'post-detail-keywords'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'post-detail-focus-keyword-trend'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'keyword-tracking'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'post-detail-keyword-tracking'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'index-status'
			]
		] ), true );
	}

	/**
	 * Get the plans for a given feature.
	 *
	 * @since 4.3.0
	 *
	 * @param  string $sectionSlug The section name.
	 * @param  string $feature     The feature name.
	 * @return array               The plans for the feature.
	 */
	public function getPlansForFeature( $sectionSlug, $feature = '' ) {
		$plans = [];

		// Loop through all the features and find the plans that have access to the feature.
		foreach ( $this->getFeatures() as $featureArray ) {
			if ( $featureArray->section !== $sectionSlug ) {
				continue;
			}

			if ( ! empty( $feature ) && $featureArray->feature !== $feature ) {
				continue;
			}

			$plans[] = ucfirst( $featureArray->license_level );
		}

		return array_unique( $plans );
	}
}Common/Utils/Filesystem.php000064400000014234151536241210011735 0ustar00<?php
// phpcs:disable WordPress.WP.AlternativeFunctions

namespace AIOSEO\Plugin\Common\Utils;

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

/**
 * Load our manifest to use throughout the app.
 *
 * @since 4.1.9
 */
class Filesystem {
	/**
	 * Holds the WordPress filesystem object.
	 *
	 * @since 4.1.9
	 *
	 * @var \WP_Filesystem_Base
	 */
	public $fs = null;

	/**
	 * Core class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Core\Core
	 */
	private $core = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.1.9
	 *
	 * @param \AIOSEO\Plugin\Common\Core\Core $core The AIOSEO Core class.
	 * @param array                           $args Any arguments needed to construct the class with.
	 */
	public function __construct( $core, $args = [] ) {
		$this->core = $core;
		$this->init( $args );
	}

	/**
	 * Initialize the filesystem.
	 *
	 * @since 4.1.9
	 *
	 * @param  array $args An array of arguments for the WP_Filesystem
	 * @return void
	 */
	public function init( $args = [] ) {
		require_once ABSPATH . 'wp-admin/includes/file.php';
		WP_Filesystem( $args );

		global $wp_filesystem; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if ( is_object( $wp_filesystem ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			$this->fs = $wp_filesystem; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		}
	}

	/**
	 * Wrapper method to check if a file exists.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $filename The filename to check if it exists.
	 * @return bool             Returns true if the file or directory specified by filename exists; false otherwise.
	 */
	public function exists( $filename ) {
		if ( ! $this->isWpfsValid() ) {
			return @file_exists( $filename );
		}

		return $this->fs->exists( $filename );
	}

	/**
	 * Retrieve the contents of a file.
	 *
	 * @since 4.1.9
	 *
	 * @param  string      $filename The filename to get the contents for.
	 * @return string|bool           The function returns the read data or false on failure.
	 */
	public function getContents( $filename ) {
		if ( ! $this->exists( $filename ) ) {
			return false;
		}

		if ( ! $this->isWpfsValid() ) {
			return @file_get_contents( $filename );
		}

		return $this->fs->get_contents( $filename );
	}

	/**
	 * Reads entire file into an array.
	 *
	 * @since 4.1.9
	 *
	 * @param  string     $file Path to the file.
	 * @return array|bool       File contents in an array on success, false on failure.
	 */
	public function getContentsArray( $file ) {
		if ( ! $this->exists( $file ) ) {
			return false;
		}

		if ( ! $this->isWpfsValid() ) {
			return @file( $file );
		}

		return $this->fs->get_contents_array( $file );
	}

	/**
	 * Sets the access and modification times of a file.
	 * Note: If $file doesn't exist, it will be created.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $file  Path to file.
	 * @param  int    $time  Optional. Modified time to set for file. Default 0.
	 * @param  int    $atime Optional. Access time to set for file. Default 0.
	 * @return bool          True on success, false on failure.
	 */
	public function touch( $file, $time = 0, $atime = 0 ) {
		if ( 0 === $time ) {
			$time = time();
		}

		if ( 0 === $atime ) {
			$atime = time();
		}

		if ( ! $this->isWpfsValid() ) {
			return @touch( $file, $time, $atime );
		}

		return $this->fs->touch( $file, $time, $atime );
	}

	/**
	 * Writes a string to a file.
	 *
	 * @since 4.1.9
	 *
	 * @param  string    $file     Remote path to the file where to write the data.
	 * @param  string    $contents The data to write.
	 * @param  int|false $mode     Optional. The file permissions as octal number, usually 0644. Default false.
	 * @return int|bool            True on success, false on failure.
	 */
	public function putContents( $file, $contents, $mode = false ) {
		if ( ! $this->isWpfsValid() ) {
			return @file_put_contents( $file, $contents );
		}

		return $this->fs->put_contents( $file, $contents, $mode );
	}

	/**
	 * Checks if a file or directory is writable.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $file Path to file or directory.
	 * @return bool         Whether $file is writable.
	 */
	public function isWritable( $file ) {
		if ( ! $this->isWpfsValid() ) {
			return @is_writable( $file );
		}

		return $this->fs->is_writable( $file );
	}

	/**
	 * Checks if a file is readable.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $file Path to file.
	 * @return bool         Whether $file is readable.
	 */
	public function isReadable( $file ) {
		if ( ! $this->isWpfsValid() ) {
			return @is_readable( $file );
		}

		return $this->fs->is_readable( $file );
	}

	/**
	 * Gets the file size (in bytes).
	 *
	 * @since 4.1.9
	 *
	 * @param  string   $file Path to file.
	 * @return int|bool       Size of the file in bytes on success, false on failure.
	 */
	public function size( $file ) {
		if ( ! $this->isWpfsValid() ) {
			return @filesize( $file );
		}

		return $this->fs->size( $file );
	}

	/**
	 * Checks if resource is a file.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $file File path.
	 * @return bool         Whether $file is a file.
	 */
	public function isFile( $file ) {
		if ( ! $this->isWpfsValid() ) {
			return @is_file( $file );
		}

		return $this->fs->is_file( $file );
	}

	/**
	 * Checks if resource is a directory.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $path Directory path.
	 * @return bool         Whether $path is a directory.
	 */
	public function isDir( $path ) {
		if ( ! $this->isWpfsValid() ) {
			return @is_dir( $path );
		}

		return $this->fs->is_dir( $path );
	}

	/**
	 * A simple check to ensure that the WP_Filesystem is valid.
	 *
	 * @since 4.1.9
	 *
	 * @return bool True if valid, false if not.
	 */
	public function isWpfsValid() {
		if (
			! is_a( $this->fs, 'WP_Filesystem_Base' ) ||
			(
				// Errors is a WP_Error object.
				! empty( $this->fs->errors ) &&
				// We directly check if the errors array is empty for compatibility with WP < 5.1.
				! empty( $this->fs->errors->errors )
			)
		) {
			return false;
		}

		return true;
	}

	/**
	 * In order to not have a conflict, we need to return a clone.
	 *
	 * @since 4.1.9
	 *
	 * @return Filesystem The cloned Filesystem object.
	 */
	public function noConflict() {
		return clone $this;
	}
}Common/Utils/Helpers.php000064400000024524151536241210011216 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

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

use AIOSEO\Plugin\Common\Traits\Helpers as TraitHelpers;

/**
 * Contains helper functions
 *
 * @since 4.0.0
 */
class Helpers {
	use TraitHelpers\Api;
	use TraitHelpers\Arrays;
	use TraitHelpers\Buffer;
	use TraitHelpers\Constants;
	use TraitHelpers\Deprecated;
	use TraitHelpers\DateTime;
	use TraitHelpers\Language;
	use TraitHelpers\Numbers;
	use TraitHelpers\PostType;
	use TraitHelpers\Request;
	use TraitHelpers\Shortcodes;
	use TraitHelpers\Strings;
	use TraitHelpers\Svg;
	use TraitHelpers\ThirdParty;
	use TraitHelpers\Url;
	use TraitHelpers\Vue;
	use TraitHelpers\Wp;
	use TraitHelpers\WpContext;
	use TraitHelpers\WpMultisite;
	use TraitHelpers\WpUri;

	/**
	 * Generate a UTM URL from the url and medium/content passed in.
	 *
	 * @since 4.0.0
	 *
	 * @param  string      $url     The URL to parse.
	 * @param  string      $medium  The UTM medium parameter.
	 * @param  string|null $content The UTM content parameter or null.
	 * @param  boolean     $esc     Whether or not to escape the URL.
	 * @return string               The new URL.
	 */
	public function utmUrl( $url, $medium, $content = null, $esc = true ) {
		// First, remove any existing utm parameters on the URL.
		$url = remove_query_arg( [
			'utm_source',
			'utm_medium',
			'utm_campaign',
			'utm_content'
		], $url );

		// Generate the new arguments.
		$args = [
			'utm_source'   => 'WordPress',
			'utm_campaign' => aioseo()->pro ? 'proplugin' : 'liteplugin',
			'utm_medium'   => $medium
		];

		// Content is not used by default.
		if ( $content ) {
			$args['utm_content'] = $content;
		}

		// Return the new URL.
		$url = add_query_arg( $args, $url );

		return $esc ? esc_url( $url ) : $url;
	}

	/**
	 * Checks if we are in a dev environment or not.
	 *
	 * @since 4.1.0
	 *
	 * @return boolean True if we are, false if not.
	 */
	public function isDev() {
		return aioseo()->isDev || isset( $_REQUEST['aioseo-dev'] ); // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
	}

	/**
	 * Checks if the server is running on Apache.
	 *
	 * @since 4.0.0
	 *
	 * @return boolean Whether or not it is on apache.
	 */
	public function isApache() {
		if ( ! isset( $_SERVER['SERVER_SOFTWARE'] ) ) {
			return false;
		}

		return stripos( sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ), 'apache' ) !== false;
	}

	/**
	 * Checks if the server is running on nginx.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether or not it is on nginx.
	 */
	public function isNginx() {
		if ( ! isset( $_SERVER['SERVER_SOFTWARE'] ) ) {
			return false;
		}

		$server = sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) );

		if (
			false !== stripos( $server, 'Flywheel' ) ||
			false !== stripos( $server, 'nginx' )
		) {
			return true;
		}

		return false;
	}

	/**
	 * Checks if the server is running on LiteSpeed.
	 *
	 * @since 4.5.3
	 *
	 * @return bool Whether it is on LiteSpeed.
	 */
	public function isLiteSpeed() {
		if ( ! isset( $_SERVER['SERVER_SOFTWARE'] ) ) {
			return false;
		}

		$server = strtolower( sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) );

		return false !== stripos( $server, 'litespeed' );
	}

	/**
	 * Returns the server name: Apache, nginx or LiteSpeed.
	 *
	 * @since 4.5.3
	 *
	 * @return string The server name. An empty string if it's unknown.
	 */
	public function getServerName() {
		if ( aioseo()->helpers->isApache() ) {
			return 'apache';
		}

		if ( aioseo()->helpers->isNginx() ) {
			return 'nginx';
		}

		if ( aioseo()->helpers->isLiteSpeed() ) {
			return 'litespeed';
		}

		return '';
	}

	/**
	 * Validate IP addresses.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $ip The IP address to validate.
	 * @return boolean     If the IP address is valid or not.
	 */
	public function validateIp( $ip ) {
		if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
			return true;
		}

		if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
			return true;
		}

		// Doesn't seem to be a valid IP.
		return false;
	}

	/**
	 * Convert bytes to readable format.
	 *
	 * @since 4.0.0
	 *
	 * @param  integer $bytes The size of the file.
	 * @return array          The original and readable file size.
	 */
	public function convertFileSize( $bytes ) {
		if ( empty( $bytes ) ) {
			return [
				'original' => 0,
				'readable' => '0 B'
			];
		}
		$i = floor( log( $bytes ) / log( 1024 ) );
		$sizes = [ 'B', 'KB', 'MB', 'GB', 'TB' ];

		return [
			'original' => $bytes,
			'readable' => sprintf( '%.02F', $bytes / pow( 1024, $i ) ) * 1 . ' ' . $sizes[ $i ]
		];
	}

	/**
	 * Sanitizes a given option value before we store it in the DB.
	 *
	 * Used by the migration and importer classes.
	 *
	 * @since 4.0.0
	 *
	 * @param  mixed $value The value.
	 * @return mixed $value The sanitized value.
	 */
	public function sanitizeOption( $value ) {
		switch ( gettype( $value ) ) {
			case 'boolean':
				return (bool) $value;
			case 'string':
				$value = aioseo()->helpers->decodeHtmlEntities( $value );

				return aioseo()->helpers->encodeOutputHtml( wp_strip_all_tags( wp_check_invalid_utf8( trim( $value ) ) ) );
			case 'integer':
				return intval( $value );
			case 'double':
				return floatval( $value );
			case 'array':
				$sanitized = [];
				foreach ( (array) $value as $child ) {
					$sanitized[] = aioseo()->helpers->sanitizeOption( $child );
				}

				return $sanitized;
			case 'object':
				$sanitized = [];
				foreach ( (array) $value as $key => $child ) {
					$sanitized[ $key ] = aioseo()->helpers->sanitizeOption( $child );
				}

				return $sanitized;
			default:
				return false;
		}
	}

	/**
	 * Checks if the given string is serialized, and if so, unserializes it.
	 * If the serialized string contains an object, we abort to prevent PHP object injection.
	 *
	 * @since 4.1.0.2
	 *
	 * @param  string        $string         The string.
	 * @param  array|boolean $allowedClasses The allowed classes for unserialize.
	 * @return string|array                  The string or unserialized data.
	 */
	public function maybeUnserialize( $string, $allowedClasses = false ) {
		if ( ! is_string( $string ) ) {
			return $string;
		}

		$string = trim( $string );
		if ( is_serialized( $string ) ) {
			return @unserialize( $string, [ 'allowed_classes' => $allowedClasses ] ); // phpcs:disable PHPCompatibility.FunctionUse.NewFunctionParameters.unserialize_optionsFound
		}

		return $string;
	}

	/**
	 * Returns a deep clone of the given object.
	 * The built-in PHP clone KW provides a shallow clone. This method returns a deep clone that also clones nested object properties.
	 * You can use this method to sever the reference to nested objects.
	 *
	 * @since 4.4.7
	 *
	 * @return object The cloned object.
	 */
	public function deepClone( $object ) {
		return unserialize( serialize( $object ) );
	}

	/**
	 * Sanitizes a given variable
	 *
	 * @since 4.5.6
	 *
	 * @param  mixed $variable             The variable.
	 * @param  bool  $preserveHtml         Whether or not to preserve HTML for ALL fields.
	 * @param  array $fieldsToPreserveHtml Specific fields to preserve HTML for.
	 * @param  string $fieldName           The name of the current field (when looping over a list).
	 * @return mixed                       The sanitized variable.
	 */
	public function sanitize( $variable, $preserveHtml = false, $fieldsToPreserveHtml = [], $fieldName = '' ) {
		$type = gettype( $variable );
		switch ( $type ) {
			case 'boolean':
				return (bool) $variable;
			case 'string':
				if ( $preserveHtml || in_array( $fieldName, $fieldsToPreserveHtml, true ) ) {
					return aioseo()->helpers->decodeHtmlEntities( sanitize_text_field( htmlspecialchars( $variable, ENT_NOQUOTES, 'UTF-8' ) ) );
				}

				return sanitize_text_field( $variable );
			case 'integer':
				return intval( $variable );
			case 'float':
			case 'double':
				return floatval( $variable );
			case 'array':
				$array = [];
				foreach ( (array) $variable as $k => $v ) {
					$array[ $k ] = $this->sanitize( $v, $preserveHtml, $fieldsToPreserveHtml, $k );
				}

				return $array;
			default:
				return false;
		}
	}

	/**
	 * Return the version number with a filter to enable users to hide the version.
	 *
	 * @since 4.3.7
	 *
	 * @return string The current version or empty if the filter is active. Using ?aioseo-dev will override the filter.
	 */
	public function getAioseoVersion() {
		$version = aioseo()->version;

		if ( ! $this->isDev() && apply_filters( 'aioseo_hide_version_number', false ) ) {
			$version = '';
		}

		return $version;
	}

	/**
	 * Retrieves the marketing site articles.
	 *
	 * @since 4.7.2
	 *
	 * @param  bool  $fetchImage Whether to fetch the article image.
	 * @return array             The articles or an empty array on failure.
	 */
	public function fetchAioseoArticles( $fetchImage = false ) {
		$items = aioseo()->core->networkCache->get( 'rss_feed' );
		if ( null !== $items ) {
			return $items;
		}

		$options  = [
			'timeout'   => 10,
			'sslverify' => false,
		];
		$response = wp_remote_get( 'https://aioseo.com/wp-json/wp/v2/posts?per_page=4', $options );
		$body     = wp_remote_retrieve_body( $response );
		if ( ! $body ) {
			return [];
		}

		$cached = [];
		$items  = json_decode( $body, true );
		foreach ( $items as $k => $item ) {
			$cached[ $k ] = [
				'url'     => $item['link'],
				'title'   => $item['title']['rendered'],
				'date'    => date( get_option( 'date_format' ), strtotime( $item['date'] ) ),
				'content' => wp_html_excerpt( $item['content']['rendered'], 128, '&hellip;' ),
			];

			if ( $fetchImage ) {
				$response = wp_remote_get( $item['_links']['wp:featuredmedia'][0]['href'] ?? '', $options );
				$body     = wp_remote_retrieve_body( $response );
				if ( ! $body ) {
					continue;
				}

				$image = json_decode( $body, true );

				$cached[ $k ]['image'] = [
					'url'   => $image['source_url'] ?? '',
					'alt'   => $image['alt_text'] ?? '',
					'sizes' => $image['media_details']['sizes'] ?? ''
				];
			}
		}

		aioseo()->core->networkCache->update( 'rss_feed', $cached, 24 * HOUR_IN_SECONDS );

		return $cached;
	}

	/**
	 * Returns if the admin bar is enabled.
	 *
	 * @since 4.8.1
	 *
	 * @return bool Whether the admin bar is enabled.
	 */
	public function isAdminBarEnabled() {
		$showAdminBarMenu = aioseo()->options->advanced->adminBarMenu;

		return is_admin_bar_showing() && ( $showAdminBarMenu ?? true );
	}
}Common/Utils/NetworkCache.php000064400000005416151536241210012170 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

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

/**
 * Handles our network cache.
 *
 * @since 4.2.5
 */
class NetworkCache extends Cache {
	/**
	 * Returns the cache value for a key if it exists and is not expired.
	 *
	 * @since 4.2.5
	 *
	 * @param  string     $key            The cache key name. Use a '%' for a like query.
	 * @param  bool|array $allowedClasses Whether to allow objects to be returned.
	 * @return mixed                      The value or null if the cache does not exist.
	 */
	public function get( $key, $allowedClasses = false ) {
		if ( ! aioseo()->helpers->isPluginNetworkActivated() ) {
			return parent::get( $key, $allowedClasses );
		}

		aioseo()->helpers->switchToBlog( aioseo()->helpers->getNetworkId() );
		$value = parent::get( $key, $allowedClasses );
		aioseo()->helpers->restoreCurrentBlog();

		return $value;
	}

	/**
	 * Updates the given cache or creates it if it doesn't exist.
	 *
	 * @since 4.2.5
	 *
	 * @param  string $key        The cache key name.
	 * @param  mixed  $value      The value.
	 * @param  int    $expiration The expiration time in seconds. Defaults to 24 hours. 0 to no expiration.
	 * @return void
	 */
	public function update( $key, $value, $expiration = DAY_IN_SECONDS ) {
		if ( ! aioseo()->helpers->isPluginNetworkActivated() ) {
			parent::update( $key, $value, $expiration );

			return;
		}

		aioseo()->helpers->switchToBlog( aioseo()->helpers->getNetworkId() );
		parent::update( $key, $value, $expiration );
		aioseo()->helpers->restoreCurrentBlog();
	}

	/**
	 * Deletes the given cache key.
	 *
	 * @since 4.2.5
	 *
	 * @param  string $key The cache key.
	 * @return void
	 */
	public function delete( $key ) {
		if ( ! aioseo()->helpers->isPluginNetworkActivated() ) {
			parent::delete( $key );

			return;
		}

		aioseo()->helpers->switchToBlog( aioseo()->helpers->getNetworkId() );
		parent::delete( $key );
		aioseo()->helpers->restoreCurrentBlog();
	}

	/**
	 * Clears all of our cache.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	public function clear() {
		if ( ! aioseo()->helpers->isPluginNetworkActivated() ) {
			parent::clear();

			return;
		}

		aioseo()->helpers->switchToBlog( aioseo()->helpers->getNetworkId() );
		parent::clear();
		aioseo()->helpers->restoreCurrentBlog();
	}

	/**
	 * Clears all of our cache under a certain prefix.
	 *
	 * @since 4.2.5
	 *
	 * @param  string $prefix A prefix to clear or empty to clear everything.
	 * @return void
	 */
	public function clearPrefix( $prefix ) {
		if ( ! aioseo()->helpers->isPluginNetworkActivated() ) {
			parent::clearPrefix( $prefix );

			return;
		}

		aioseo()->helpers->switchToBlog( aioseo()->helpers->getNetworkId() );
		parent::clearPrefix( $prefix );
		aioseo()->helpers->restoreCurrentBlog();
	}
}Common/Utils/PluginUpgraderSilentAjax.php000064400000023674151536241210014534 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

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

use WP_Error;

/** \WP_Upgrader class */
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';

/** \Plugin_Upgrader class */
require_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php';

/**
 * In WP 5.3 a PHP 5.6 splat operator (...$args) was added to \WP_Upgrader_Skin::feedback().
 * We need to remove all calls to *Skin::feedback() method, as we can't override it in own Skins
 * without breaking support for PHP 5.3-5.5.
 *
 * @internal Please do not use this class outside of core AIOSEO development. May be removed at any time.
 *
 * @since 1.5.6.1
 */
class PluginUpgraderSilentAjax extends \Plugin_Upgrader {
	/**
	 * An array of links to install the plugins from.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $pluginLinks = [
		'brokenLinkChecker'    => 'https://downloads.wordpress.org/plugin/broken-link-checker-seo.zip',
		'optinMonster'         => 'https://downloads.wordpress.org/plugin/optinmonster.zip',
		'wpForms'              => 'https://downloads.wordpress.org/plugin/wpforms-lite.zip',
		'miLite'               => 'https://downloads.wordpress.org/plugin/google-analytics-for-wordpress.zip',
		'emLite'               => 'https://downloads.wordpress.org/plugin/google-analytics-dashboard-for-wp.zip',
		'wpMail'               => 'https://downloads.wordpress.org/plugin/wp-mail-smtp.zip',
		'rafflePress'          => 'https://downloads.wordpress.org/plugin/rafflepress.zip',
		'seedProd'             => 'https://downloads.wordpress.org/plugin/coming-soon.zip',
		'trustPulse'           => 'https://downloads.wordpress.org/plugin/trustpulse-api.zip',
		'instagramFeed'        => 'https://downloads.wordpress.org/plugin/instagram-feed.zip',
		'facebookFeed'         => 'https://downloads.wordpress.org/plugin/custom-facebook-feed.zip',
		'twitterFeed'          => 'https://downloads.wordpress.org/plugin/custom-twitter-feeds.zip',
		'youTubeFeed'          => 'https://downloads.wordpress.org/plugin/feeds-for-youtube.zip',
		'pushEngage'           => 'https://downloads.wordpress.org/plugins/pushengage.zip',
		'sugarCalendar'        => 'https://downloads.wordpress.org/plugins/sugar-calendar-lite.zip',
		'wpSimplePay'          => 'https://downloads.wordpress.org/plugins/stripe.zip',
		'easyDigitalDownloads' => 'https://downloads.wordpress.org/plugins/easy-digital-downloads.zip',
		'wpcode'               => 'https://downloads.wordpress.org/plugin/insert-headers-and-footers.zip',
		'searchWp'             => '',
		'affiliateWp'          => '',
		'charitable'           => 'https://downloads.wordpress.org/plugin/charitable.zip',
		'duplicator'           => 'https://downloads.wordpress.org/plugin/duplicator.zip'
	];

	/**
	 * An array of links to install the plugins from wordpress.org.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $wpPluginLinks = [
		'brokenLinkChecker' => 'https://wordpress.org/plugins/broken-link-checker-seo/',
		'optinMonster'      => 'https://wordpress.org/plugin/optinmonster/',
		'wpForms'           => 'https://wordpress.org/plugin/wpforms-lite/',
		'miLite'            => 'https://wordpress.org/plugin/google-analytics-for-wordpress/',
		'emLite'            => 'https://wordpress.org/plugin/google-analytics-dashboard-for-wp/',
		'wpMail'            => 'https://wordpress.org/plugin/wp-mail-smtp/',
		'rafflePress'       => 'https://wordpress.org/plugin/rafflepress/',
		'seedProd'          => 'https://wordpress.org/plugin/coming-soon/',
		'trustPulse'        => 'https://wordpress.org/plugin/trustpulse-api/',
		'instagramFeed'     => 'https://wordpress.org/plugin/instagram-feed/',
		'facebookFeed'      => 'https://wordpress.org/plugin/custom-facebook-feed/',
		'twitterFeed'       => 'https://wordpress.org/plugin/custom-twitter-feeds/',
		'youTubeFeed'       => 'https://wordpress.org/plugin/feeds-for-youtube/',
		'pushEngage'        => 'https://wordpress.org/plugins/pushengage/',
		'sugarCalendar'     => 'https://wordpress.org/plugins/sugar-calendar-lite/',
		'wpSimplePay'       => 'https://wordpress.org/plugins/stripe/',
		'searchWp'          => 'https://searchwp.com/',
		'affiliateWp'       => 'https://affiliatewp.com/',
		'wpcode'            => 'https://wordpress.org/plugins/insert-headers-and-footers/',
		'charitable'        => 'https://wordpress.org/plugins/charitable/',
		'duplicator'        => 'https://wordpress.org/plugins/duplicator/'
	];

	/**
	 * An array of slugs to check if plugins are activated.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $pluginSlugs = [
		'brokenLinkChecker'       => 'broken-link-checker-seo/aioseo-broken-link-checker.php',
		'optinMonster'            => 'optinmonster/optin-monster-wp-api.php',
		'wpForms'                 => 'wpforms-lite/wpforms.php',
		'wpFormsPro'              => 'wpforms/wpforms.php',
		'miLite'                  => 'google-analytics-for-wordpress/googleanalytics.php',
		'miPro'                   => 'google-analytics-premium/googleanalytics-premium.php',
		'emLite'                  => 'google-analytics-dashboard-for-wp/gadwp.php',
		'emPro'                   => 'exactmetrics-premium/exactmetrics-premium.php',
		'wpMail'                  => 'wp-mail-smtp/wp_mail_smtp.php',
		'wpMailPro'               => 'wp-mail-smtp-pro/wp_mail_smtp.php',
		'rafflePress'             => 'rafflepress/rafflepress.php',
		'rafflePressPro'          => 'rafflepress-pro/rafflepress-pro.php',
		'seedProd'                => 'coming-soon/coming-soon.php',
		'seedProdPro'             => 'seedprod-coming-soon-pro-5/seedprod-coming-soon-pro-5.php',
		'trustPulse'              => 'trustpulse-api/trustpulse.php',
		'instagramFeed'           => 'instagram-feed/instagram-feed.php',
		'instagramFeedPro'        => 'instagram-feed-pro/instagram-feed.php',
		'facebookFeed'            => 'custom-facebook-feed/custom-facebook-feed.php',
		'facebookFeedPro'         => 'custom-facebook-feed-pro/custom-facebook-feed.php',
		'twitterFeed'             => 'custom-twitter-feeds/custom-twitter-feed.php',
		'twitterFeedPro'          => 'custom-twitter-feeds-pro/custom-twitter-feed.php',
		'youTubeFeed'             => 'feeds-for-youtube/youtube-feed.php',
		'youTubeFeedPro'          => 'youtube-feed-pro/youtube-feed.php',
		'pushEngage'              => 'pushengage/main.php',
		'sugarCalendar'           => 'sugar-calendar-lite/sugar-calendar-lite.php',
		'sugarCalendarPro'        => 'sugar-calendar/sugar-calendar.php',
		'wpSimplePay'             => 'stripe/stripe-checkout.php',
		'wpSimplePayPro'          => 'wp-simple-pay-pro-3/simple-pay.php',
		'easyDigitalDownloads'    => 'easy-digital-downloads/easy-digital-downloads.php',
		'easyDigitalDownloadsPro' => 'easy-digital-downloads-pro/easy-digital-downloads.php',
		'searchWp'                => 'searchwp/index.php',
		'affiliateWp'             => 'affiliate-wp/affiliate-wp.php',
		'wpcode'                  => 'insert-headers-and-footers/ihaf.php',
		'wpcodePro'               => 'wpcode-premium/wpcode.php',
		'charitable'              => 'charitable/charitable.php',
		'duplicator'              => 'duplicator/duplicator.php'
	];

	/**
	 * An array of links for admin settings.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $pluginAdminUrls = [
		'brokenLinkChecker'       => 'admin.php?page=broken-link-checker#/settings',
		'optinMonster'            => 'admin.php?page=optin-monster-api-settings',
		'wpForms'                 => 'admin.php?page=wpforms-settings',
		'wpFormsPro'              => 'admin.php?page=wpforms-settings',
		'miLite'                  => 'admin.php?page=monsterinsights_settings#/',
		'miPro'                   => 'admin.php?page=monsterinsights_settings#/',
		'emLite'                  => 'admin.php?page=exactmetrics_settings#/',
		'emPro'                   => 'admin.php?page=exactmetrics_settings#/',
		'wpMail'                  => 'admin.php?page=wp-mail-smtp',
		'wpMailPro'               => 'admin.php?page=wp-mail-smtp',
		'seedProd'                => 'admin.php?page=seedprod_lite',
		'seedProdPro'             => 'admin.php?page=seedprod_pro',
		'rafflePress'             => 'admin.php?page=rafflepress_lite#/settings',
		'rafflePressPro'          => 'admin.php?page=rafflepress_pro#/settings',
		'trustPulse'              => 'admin.php?page=trustpulse',
		'instagramFeed'           => 'admin.php?page=sb-instagram-feed',
		'instagramFeedPro'        => 'admin.php?page=sb-instagram-feed',
		'facebookFeed'            => 'admin.php?page=cff-top',
		'facebookFeedPro'         => 'admin.php?page=cff-top',
		'twitterFeed'             => 'admin.php?page=ctf-settings',
		'twitterFeedPro'          => 'admin.php?page=ctf-settings',
		'youTubeFeed'             => 'admin.php?page=youtube-feed-settings',
		'youTubeFeedPro'          => 'admin.php?page=youtube-feed-settings',
		'pushEngage'              => 'admin.php?page=pushengage',
		'sugarCalendar'           => 'admin.php?page=sugar-calendar',
		'sugarCalendarPro'        => 'admin.php?page=sugar-calendar',
		'wpSimplePay'             => 'edit.php?post_type=simple-pay',
		'wpSimplePayPro'          => 'edit.php?post_type=simple-pay',
		'easyDigitalDownloads'    => 'edit.php?post_type=download&page=edd-settings',
		'easyDigitalDownloadsPro' => 'edit.php?post_type=download&page=edd-settings',
		'searchWp'                => 'options-general.php?page=searchwp',
		'affiliateWp'             => 'admin.php?page=affiliate-wp',
		'wpcode'                  => 'admin.php?page=wpcode',
		'wpcodePro'               => 'admin.php?page=wpcode',
		'charitable'              => 'admin.php?page=charitable-settings',
		'duplicator'              => 'admin.php?page=duplicator-settings'
	];

	/**
	 * An array of slugs that work in the network admin.
	 *
	 * @since 4.2.8
	 *
	 * @var array
	 */
	public $hasNetworkAdmin = [
		'miLite'    => 'admin.php?page=monsterinsights_network',
		'miPro'     => 'admin.php?page=monsterinsights_network',
		'emLite'    => 'admin.php?page=exactmetrics_network',
		'emPro'     => 'admin.php?page=exactmetrics_network',
		'wpMail'    => 'admin.php?page=wp-mail-smtp',
		'wpMailPro' => 'admin.php?page=wp-mail-smtp',
	];
}Common/Utils/PluginUpgraderSkin.php000064400000003257151536241210013371 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

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

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

/**
 * Class PluginSilentUpgraderSkin.
 *
 * @internal Please do not use this class outside of core All in One SEO development. May be removed at any time.
 *
 * @since 4.0.0
 */
class PluginUpgraderSkin extends \WP_Upgrader_Skin {
	/**
	 * Empty out the header of its HTML content and only check to see if it has
	 * been performed or not.
	 *
	 * @since 4.0.0
	 */
	public function header() {}

	/**
	 * Empty out the footer of its HTML contents.
	 *
	 * @since 4.0.0
	 */
	public function footer() {}

	/**
	 * Instead of outputting HTML for errors, just return them.
	 * Ajax request will just ignore it.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $errors Array of errors with the install process.
	 * @return void
	 */
	public function error( $errors ) {
		if ( ! empty( $errors ) ) {
			wp_send_json_error( $errors );
		}
	}

	/**
	 * Empty out JavaScript output that calls function to decrement the update counts.
	 *
	 * @since 4.0.0
	 *
	 * @param string $type Type of update count to decrement.
	 */
	public function decrement_update_count( $type ) {} // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable, PSR1.Methods.CamelCapsMethodName.NotCamelCaps

	/**
	 * @since 4.2.5
	 *
	 * @param  string $feedback Message data.
	 * @param  mixed  ...$args  Optional text replacements.
	 * @return void
	 */
	public function feedback( $feedback, ...$args ) {} // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
}Common/Utils/Tags.php000064400000124307151536241210010512 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

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

/**
 * Class to replace tag values with their data counterparts.
 *
 * @since 4.0.0
 */
class Tags {
	/**
	 * An array of tag values that we support.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $tags = [];

	/**
	 * Specifies the denotation character for the tags.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	public $denotationChar = '#';

	/**
	 * An array of contexts to separate tags.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $context = [
		'authorDescription'   => [
			'author_bio',
			'author_first_name',
			'author_last_name',
			'author_name',
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'separator_sa',
			'site_title',
			'tagline'
		],
		'authorTitle'         => [
			'author_first_name',
			'author_last_name',
			'author_name',
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'separator_sa',
			'site_title',
			'tagline'
		],
		'descriptionFormat'   => [
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'description',
			'post_date',
			'post_month',
			'post_title',
			'post_year',
			'separator_sa',
			'site_title',
			'tagline'
		],
		'dateDescription'     => [
			'archive_date',
			'archive_title',
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'post_day',
			'post_month',
			'post_year',
			'separator_sa',
			'site_title',
			'tagline'
		],
		'dateTitle'           => [
			'archive_date',
			'archive_title',
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'post_day',
			'post_month',
			'post_year',
			'separator_sa',
			'site_title',
			'tagline'
		],
		'homePage'            => [
			'author_first_name',
			'author_last_name',
			'author_name',
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'post_date',
			'post_day',
			'post_excerpt_only',
			'post_excerpt',
			'post_month',
			'post_title',
			'post_year',
			'separator_sa',
			'site_title',
			'tagline'
		],
		'knowledgeGraph'      => [
			'separator_sa',
			'site_title',
			'tagline'
		],
		'pagedFormat'         => [
			'page_number',
			'separator_sa'
		],
		'postDescription'     => [
			'author_first_name',
			'author_last_name',
			'author_name',
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'permalink',
			'post_content',
			'post_date',
			'post_day',
			'post_excerpt_only',
			'post_excerpt',
			'post_month',
			'post_title',
			'post_year',
			'separator_sa',
			'site_title',
			'tagline',
			'tax_name',
			'taxonomy_title'
		],
		'postTitle'           => [
			'author_first_name',
			'author_last_name',
			'author_name',
			'categories',
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'permalink',
			'post_content',
			'post_date',
			'post_day',
			'post_excerpt_only',
			'post_excerpt',
			'post_month',
			'post_title',
			'post_year',
			'separator_sa',
			'site_title',
			'tagline',
			'tax_name',
			'taxonomy_title'
		],
		'rss'                 => [
			'author_link',
			'author_link_alt',
			'author_name',
			'featured_image',
			'post_date',
			'post_link',
			'post_link_alt',
			'post_title',
			'site_link',
			'site_link_alt',
			'site_title',
			'taxonomy_title'
		],
		'schema'              => [
			'author_first_name',
			'author_last_name',
			'author_name',
			'author_url',
			'categories',
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'permalink',
			'post_content',
			'post_date',
			'post_day',
			'post_excerpt_only',
			'post_excerpt',
			'post_month',
			'post_title',
			'post_year',
			'separator_sa',
			'site_title',
			'tagline',
			'tax_name',
			'taxonomy_title'
		],
		'searchDescription'   => [
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'search_term',
			'separator_sa',
			'site_title',
			'tagline'
		],
		'searchTitle'         => [
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'search_term',
			'separator_sa',
			'site_title',
			'tagline'
		],
		'siteDescription'     => [
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'permalink',
			'post_date',
			'post_day',
			'post_month',
			'post_year',
			'search_term',
			'separator_sa',
			'tagline'
		],
		'siteTitle'           => [
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'permalink',
			'post_date',
			'post_day',
			'post_month',
			'post_year',
			'search_term',
			'separator_sa',
			'tagline'
		],
		'taxonomyDescription' => [
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'permalink',
			'separator_sa',
			'site_title',
			'tagline',
			'taxonomy_description',
			'taxonomy_title'
		],
		'taxonomyTitle'       => [
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'permalink',
			'separator_sa',
			'site_title',
			'tagline',
			'tax_parent_name',
			'taxonomy_description',
			'taxonomy_title'
		]
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		// Tags need to be registered on wp_loaded instead of init to ensure these are available during block rendering.
		add_action( 'wp_loaded', [ $this, 'registerTags' ] );
	}

	/**
	 * Register the tags.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	public function registerTags() {
		$this->tags = array_merge( $this->tags, [
			[
				'id'          => 'alt_tag',
				'name'        => __( 'Image Alt Tag', 'all-in-one-seo-pack' ),
				'description' => __( 'Your image\'s alt tag attribute.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'attachment_caption',
				'name'        => __( 'Media Caption', 'all-in-one-seo-pack' ),
				'description' => __( 'Caption for the current media file.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'attachment_description',
				'name'        => __( 'Media Description', 'all-in-one-seo-pack' ),
				'description' => __( 'Description for the current media file.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'archive_date',
				'name'        => __( 'Archive Date', 'all-in-one-seo-pack' ),
				'description' => __( 'The date of the current archive, localized.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'author_link',
				'name'        => __( 'Author Link', 'all-in-one-seo-pack' ),
				'description' => __( 'Author archive link (name as text).', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'author_link_alt',
				'name'        => __( 'Author Link (Alt)', 'all-in-one-seo-pack' ),
				'description' => __( 'Author archive link (link as text).', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'author_bio',
				'name'        => __( 'Author Biography', 'all-in-one-seo-pack' ),
				'description' => __( 'The biography of the author.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'author_name',
				'name'        => __( 'Author Name', 'all-in-one-seo-pack' ),
				'description' => __( 'The display name of the post author.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'author_first_name',
				'name'        => __( 'Author First Name', 'all-in-one-seo-pack' ),
				'description' => __( 'The first name of the post author.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'author_last_name',
				'name'        => __( 'Author Last Name', 'all-in-one-seo-pack' ),
				'description' => __( 'The last name of the post author.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'author_url',
				'name'        => __( 'Author URL', 'all-in-one-seo-pack' ),
				'description' => __( 'The URL of the author page.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'archive_title',
				'name'        => __( 'Archive Title', 'all-in-one-seo-pack' ),
				'description' => __( 'The title of the current archive.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'blog_link',
				'name'        => __( 'Site Link', 'all-in-one-seo-pack' ),
				'description' => __( 'Site link (link as text).', 'all-in-one-seo-pack' ),
				'html'        => true
			],
			[
				'id'          => 'blog_title',
				'name'        => __( 'Site Title', 'all-in-one-seo-pack' ),
				'description' => __( 'Your site title.', 'all-in-one-seo-pack' ),
				'deprecated'  => true
			],
			[
				'id'          => 'category',
				'name'        => __( 'Category', 'all-in-one-seo-pack' ),
				'description' => __( 'Current or first category title.', 'all-in-one-seo-pack' ),
				'deprecated'  => true
			],
			[
				'id'          => 'categories',
				'name'        => __( 'Categories', 'all-in-one-seo-pack' ),
				'description' => __( 'All categories that are assigned to the current post, comma-separated.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'category_link',
				// Translators: 1 - The type of page (Post, Page, Category, Tag, etc.).
				'name'        => sprintf( __( '%1$s Link', 'all-in-one-seo-pack' ), 'Category' ),
				'description' => __( 'Current or first term link (name as text).', 'all-in-one-seo-pack' ),
				'html'        => true
			],
			[
				'id'          => 'category_link_alt',
				// Translators: 1 - The type of page (Post, Page, Category, Tag, etc.).
				'name'        => sprintf( __( '%1$s Link (Alt)', 'all-in-one-seo-pack' ), 'Category' ),
				'description' => __( 'Current or first term link (link as text).', 'all-in-one-seo-pack' ),
				'html'        => true
			],
			[
				'id'          => 'current_date',
				'name'        => __( 'Current Date', 'all-in-one-seo-pack' ),
				'description' => __( 'The current date, localized.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'current_day',
				'name'        => __( 'Current Day', 'all-in-one-seo-pack' ),
				'description' => __( 'The current day of the month, localized.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'current_month',
				'name'        => __( 'Current Month', 'all-in-one-seo-pack' ),
				'description' => __( 'The current month, localized.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'current_year',
				'name'        => __( 'Current Year', 'all-in-one-seo-pack' ),
				'description' => __( 'The current year, localized.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'custom_field',
				'name'        => __( 'Custom Field', 'all-in-one-seo-pack' ),
				'description' => __( 'A custom field from the current page/post.', 'all-in-one-seo-pack' ),
				'custom'      => true
			],
			[
				'id'          => 'description',
				'name'        => __( 'Description', 'all-in-one-seo-pack' ),
				'description' => __( 'The meta description for the current page/post.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'featured_image',
				'name'        => __( 'Featured Image', 'all-in-one-seo-pack' ),
				'description' => __( 'The featured image of the current page/post.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'page_number',
				'name'        => __( 'Page Number', 'all-in-one-seo-pack' ),
				'description' => __( 'The page number for the current paginated page.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'parent_title',
				'name'        => __( 'Parent Title', 'all-in-one-seo-pack' ),
				'description' => __( 'The title of the parent post of the current page/post.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'permalink',
				'name'        => __( 'Permalink', 'all-in-one-seo-pack' ),
				'description' => __( 'The permalink for the current page/post.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'post_content',
				// Translators: 1 - The singular name of the post type.
				'name'        => sprintf( __( '%1$s Content', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'The content of your page/post.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'post_date',
				// Translators: 1 - The singular name of the post type.
				'name'        => sprintf( __( '%1$s Date', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'The date when the page/post was published, localized.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'post_day',
				// Translators: 1 - The singular name of the post type.
				'name'        => sprintf( __( '%1$s Day', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'The day of the month when the page/post was published, localized.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'post_excerpt',
				// Translators: 1 - The singular name of the post type.
				'name'        => sprintf( __( '%1$s Excerpt', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'The excerpt defined on your page/post.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'post_excerpt_only',
				// Translators: 1 - The singular name of the post type.
				'name'        => sprintf( __( '%1$s Excerpt Only', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'The excerpt defined on your page/post. Will not fall back to the post content.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'post_month',
				// Translators: 1 - The singular name of the post type.
				'name'        => sprintf( __( '%1$s Month', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'The month when the page/post was published, localized.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'post_year',
				// Translators: 1 - The singular name of the post type.
				'name'        => sprintf( __( '%1$s Year', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'The year when the page/post was published, localized.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'post_link',
				// Translators: 1 - The type of page (Post, Page, Category, Tag, etc.).
				'name'        => sprintf( __( '%1$s Link', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'Post link (name as anchor text).', 'all-in-one-seo-pack' ),
				'html'        => true
			],
			[
				'id'          => 'post_link_alt',
				// Translators: 1 - The type of page (Post, Page, Category, Tag, etc.).
				'name'        => sprintf( __( '%1$s Link (Alt)', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'Post link (link as anchor text).', 'all-in-one-seo-pack' ),
				'html'        => true
			],
			[
				'id'          => 'post_title',
				// Translators: 1 - The type of page (Post, Page, Category, Tag, etc.).
				'name'        => sprintf( __( '%1$s Title', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'The original title of the current post.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'search_term',
				'name'        => __( 'Search Term', 'all-in-one-seo-pack' ),
				'description' => __( 'The term the user is searching for.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'separator_sa',
				'name'        => __( 'Separator', 'all-in-one-seo-pack' ),
				'description' => __( 'The separator defined in the search appearance settings.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'site_description',
				'name'        => __( 'Site Description', 'all-in-one-seo-pack' ),
				'description' => __( 'The description for your site.', 'all-in-one-seo-pack' ),
				'deprecated'  => true
			],
			[
				'id'          => 'site_link',
				'name'        => __( 'Site Link', 'all-in-one-seo-pack' ),
				'description' => __( 'Site link (name as text).', 'all-in-one-seo-pack' ),
				'html'        => true
			],
			[
				'id'          => 'site_link_alt',
				'name'        => __( 'Site Link (Alt)', 'all-in-one-seo-pack' ),
				'description' => __( 'Site link (link as text).', 'all-in-one-seo-pack' ),
				'html'        => true
			],
			[
				'id'          => 'site_title',
				'name'        => __( 'Site Title', 'all-in-one-seo-pack' ),
				'description' => __( 'Your site title.', 'all-in-one-seo-pack' ),
				'html'        => true
			],
			[
				'id'          => 'tagline',
				'name'        => __( 'Tagline', 'all-in-one-seo-pack' ),
				'description' => __( 'The tagline for your site, set in the general settings.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'tax_name',
				'name'        => __( 'Taxonomy Name', 'all-in-one-seo-pack' ),
				'description' => __( 'The name of the first term of a given taxonomy that is assigned to the current page/post.', 'all-in-one-seo-pack' ),
				'custom'      => true
			],
			[
				'id'          => 'tax_parent_name',
				'name'        => __( 'Parent Term', 'all-in-one-seo-pack' ),
				'description' => __( 'The name of the parent term of the current term.', 'all-in-one-seo-pack' ),
			],
			[
				'id'          => 'taxonomy_description',
				// Translators: 1 - The singular name of the current taxonomy.
				'name'        => sprintf( __( '%1$s Description', 'all-in-one-seo-pack' ), 'Category' ),
				'description' => __( 'The description of the primary term, first assigned term or the current term.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'taxonomy_title',
				// Translators: 1 - The type of page (Post, Page, Category, Tag, etc.).
				'name'        => sprintf( __( '%1$s Title', 'all-in-one-seo-pack' ), 'Category' ),
				'description' => __( 'The title of the primary term, first assigned term or the current term.', 'all-in-one-seo-pack' )
			]
		] );
	}

	/**
	 * Returns all the tags.
	 *
	 * @since 4.0.0
	 *
	 * @param  bool  $sampleData Whether or not to fill empty values with sample data.
	 * @return array             An array of tags.
	 */
	public function all( $sampleData = false ) {
		$tags = $this->tags;
		foreach ( $tags as $key => $tag ) {
			$tags[ $key ]['value'] = ( $tag['instance'] ?? null )
				? $tag['instance']->getTagValue( $tag, null, $sampleData )
				: $this->getTagValue( $tag, null, $sampleData );
		}

		usort( $tags, function ( $a, $b ) {
			return $a['name'] < $b['name']
				? -1
				: ( $a['name'] > $b['name'] ? 1 : 0 );
		} );

		return [
			'tags'    => $tags,
			'context' => $this->getContext()
		];
	}

	/**
	 * Add the context for all the post/page types.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of contextual data.
	 */
	public function getContext() {
		$context = $this->context;

		// Post types including CPT's.
		foreach ( aioseo()->helpers->getPublicPostTypes() as $postType ) {
			if (
				'post' === $postType['name'] ||
				! empty( $postType['buddyPress'] )
			) {
				continue;
			}

			if ( $postType['hasArchive'] ) {
				$context[ $postType['name'] . 'ArchiveTitle' ]       = $context['dateTitle'];
				$context[ $postType['name'] . 'ArchiveDescription' ] = $context['dateDescription'];
			}

			$context[ $postType['name'] . 'Title' ]       = $context['postTitle'];
			$context[ $postType['name'] . 'Description' ] = $context['postDescription'];

			// Check if the post type has an excerpt.
			if ( empty( $postType['supports']['excerpt'] ) ) {
				$phpTitleKey = array_search( 'post_excerpt', $context[ $postType['name'] . 'Title' ], true );
				if ( false !== $phpTitleKey ) {
					unset( $context[ $postType['name'] . 'Title' ][ $phpTitleKey ] );
				}

				$phpTitleKey = array_search( 'post_excerpt_only', $context[ $postType['name'] . 'Title' ], true );
				if ( false !== $phpTitleKey ) {
					unset( $context[ $postType['name'] . 'Title' ][ $phpTitleKey ] );
				}

				$phpDescriptionKey = array_search( 'post_excerpt', $context[ $postType['name'] . 'Description' ], true );
				if ( false !== $phpDescriptionKey ) {
					unset( $context[ $postType['name'] . 'Description' ][ $phpDescriptionKey ] );
				}

				$phpDescriptionKey = array_search( 'post_excerpt_only', $context[ $postType['name'] . 'Description' ], true );
				if ( false !== $phpDescriptionKey ) {
					unset( $context[ $postType['name'] . 'Description' ][ $phpDescriptionKey ] );
				}

				asort( $context[ $postType['name'] . 'Title' ] );
				$context[ $postType['name'] . 'Title' ] = array_values( $context[ $postType['name'] . 'Title' ] );
				asort( $context[ $postType['name'] . 'Description' ] );
				$context[ $postType['name'] . 'Description' ] = array_values( $context[ $postType['name'] . 'Description' ] );
			}

			if ( 'page' === $postType['name'] ) {
				$phpTitleKey = array_search( 'taxonomy_title', $context['pageTitle'], true );
				if ( false !== $phpTitleKey ) {
					unset( $context['pageTitle'][ $phpTitleKey ] );
				}

				$phpTitleKey = array_search( 'category', $context['pageTitle'], true );
				if ( false !== $phpTitleKey ) {
					unset( $context['pageTitle'][ $phpTitleKey ] );
				}

				$phpDescriptionKey = array_search( 'taxonomy_title', $context['pageDescription'], true );
				if ( false !== $phpDescriptionKey ) {
					unset( $context['pageDescription'][ $phpDescriptionKey ] );
				}

				$phpDescriptionKey = array_search( 'category', $context['pageDescription'], true );
				if ( false !== $phpDescriptionKey ) {
					unset( $context['pageDescription'][ $phpDescriptionKey ] );
				}

				$context['pageTitle']       = array_values( $context['pageTitle'] );
				$context['pageDescription'] = array_values( $context['pageDescription'] );

				asort( $context['pageTitle'] );
				$context['pageTitle'] = array_values( $context['pageTitle'] );
				asort( $context['pageDescription'] );
				$context['pageDescription'] = array_values( $context['pageDescription'] );
			}

			if ( 'attachment' === $postType['name'] ) {
				$context['attachmentTitle'][] = 'alt_tag';
				asort( $context['attachmentTitle'] );
				$context['attachmentTitle'] = array_values( $context['attachmentTitle'] );
				$context['attachmentDescription'][] = 'alt_tag';
				asort( $context['attachmentDescription'] );
				$context['attachmentDescription'] = array_values( $context['attachmentDescription'] );

				$phpTitleKey = array_search( 'taxonomy_title', $context['attachmentTitle'], true );
				if ( false !== $phpTitleKey ) {
					unset( $context['attachmentTitle'][ $phpTitleKey ] );
				}

				$phpTitleKey = array_search( 'post_content', $context['attachmentTitle'], true );
				if ( false !== $phpTitleKey ) {
					unset( $context['attachmentTitle'][ $phpTitleKey ] );
				}

				$phpTitleKey = array_search( 'post_excerpt', $context['attachmentTitle'], true );
				if ( false !== $phpTitleKey ) {
					unset( $context['attachmentTitle'][ $phpTitleKey ] );
				}

				$phpTitleKey = array_search( 'post_excerpt_only', $context['attachmentTitle'], true );
				if ( false !== $phpTitleKey ) {
					unset( $context['attachmentTitle'][ $phpTitleKey ] );
				}

				$phpDescriptionKey = array_search( 'taxonomy_title', $context['attachmentDescription'], true );
				if ( false !== $phpDescriptionKey ) {
					unset( $context['attachmentDescription'][ $phpDescriptionKey ] );
				}

				$phpDescriptionKey = array_search( 'post_content', $context['attachmentDescription'], true );
				if ( false !== $phpDescriptionKey ) {
					unset( $context['attachmentDescription'][ $phpDescriptionKey ] );
				}

				$phpDescriptionKey = array_search( 'post_excerpt', $context['attachmentDescription'], true );
				if ( false !== $phpDescriptionKey ) {
					unset( $context['attachmentDescription'][ $phpDescriptionKey ] );
				}

				$phpDescriptionKey = array_search( 'post_excerpt_only', $context['attachmentDescription'], true );
				if ( false !== $phpDescriptionKey ) {
					unset( $context['attachmentDescription'][ $phpDescriptionKey ] );
				}

				$context['attachmentTitle']       = array_merge( $context['attachmentTitle'], [ 'attachment_caption', 'attachment_description' ] );
				$context['attachmentDescription'] = array_merge( $context['attachmentDescription'], [ 'attachment_caption', 'attachment_description' ] );

				asort( $context['attachmentTitle'] );
				$context['attachmentTitle'] = array_values( $context['attachmentTitle'] );
				asort( $context['attachmentDescription'] );
				$context['attachmentDescription'] = array_values( $context['attachmentDescription'] );
			}

			if ( ! in_array( 'category', get_object_taxonomies( $postType['name'] ), true ) ) {
				$phpTitleKey = array_search( 'categories', $context[ $postType['name'] . 'Title' ], true );
				if ( false !== $phpTitleKey ) {
					unset( $context[ $postType['name'] . 'Title' ][ $phpTitleKey ] );
				}

				$phpTitleKey = array_search( 'categories', $context[ $postType['name'] . 'Description' ], true );
				if ( false !== $phpTitleKey ) {
					unset( $context[ $postType['name'] . 'Description' ][ $phpTitleKey ] );
				}

				asort( $context[ $postType['name'] . 'Title' ] );
				$context[ $postType['name'] . 'Title' ] = array_values( $context[ $postType['name'] . 'Title' ] );
				asort( $context[ $postType['name'] . 'Description' ] );
				$context[ $postType['name'] . 'Description' ] = array_values( $context[ $postType['name'] . 'Description' ] );
			}

			if ( $postType['hierarchical'] ) {
				$context[ $postType['name'] . 'Title' ][] = 'parent_title';
			}
		}

		// Taxonomies including from CPT's.
		foreach ( aioseo()->helpers->getPublicTaxonomies() as $taxonomy ) {
			$context[ $taxonomy['name'] . 'Title' ]       = $context['taxonomyTitle'];
			$context[ $taxonomy['name'] . 'Description' ] = $context['taxonomyDescription'];
		}

		return $context;
	}

	/**
	 * Replace the tags in the string provided.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string The string to look for tags in.
	 * @param  int    $id     The page or post ID.
	 * @return string         The string with tags replaced.
	 */
	public function replaceTags( $string, $id = 0 ) {
		if ( ! $string || ! preg_match( '/' . $this->denotationChar . '/', (string) $string ) ) {
			return $string;
		}

		foreach ( $this->tags as $tag ) {
			if ( 'custom_field' === $tag['id'] || 'tax_name' === $tag['id'] ) {
				continue;
			}

			$tagId = $this->denotationChar . $tag['id'];
			// Pattern explained: Exact match of tag, not followed by any additional letter, number or underscore.
			// This allows us to have tags like: #post_link and #post_link_alt
			// and it will always replace the correct one.
			$pattern = "/$tagId(?![a-zA-Z0-9_])/im";
			if ( preg_match( $pattern, (string) $string ) ) {
				$tagValue = $this->getTagValue( $tag, $id );
				$string   = preg_replace( $pattern, '%|%' . aioseo()->helpers->escapeRegexReplacement( $tagValue ), (string) $string );
			}
		}

		$string = $this->parseTaxonomyNames( $string, $id );

		// Custom fields are parsed separately.
		$string = $this->parseCustomFields( $string, $id );

		return preg_replace( '/%\|%/im', '', (string) $string );
	}

	/**
	 * Get the value of the tag to replace.
	 *
	 * @since 4.0.0
	 *
	 * @param  array    $tag        The tag to look for.
	 * @param  int|null $id         The post ID.
	 * @param  bool     $sampleData Whether or not to fill empty values with sample data.
	 * @return mixed                The value of the tag.
	 */
	public function getTagValue( $tag, $id, $sampleData = false ) {
		$author   = new \WP_User();
		$post     = aioseo()->helpers->getPost( $id );
		$postId   = null;
		$category = null;
		if ( $post ) {
			$author   = new \WP_User( $post->post_author );
			$postId   = empty( $id ) ? $post->ID : $id;
			$category = get_the_category( $postId );
		} elseif ( is_author() && is_a( get_queried_object(), 'WP_User' ) ) {
			$author = get_queried_object();
		}

		switch ( $tag['id'] ) {
			case 'alt_tag':
				return empty( $id )
					? ( $sampleData ? __( 'A sample alt tag for your image', 'all-in-one-seo-pack' ) : '' )
					: get_post_meta( $id, '_wp_attachment_image_alt', true );
			case 'archive_date':
				$date = null;
				if ( is_year() ) {
					$date = get_the_date( 'Y' );
				}
				if ( is_month() ) {
					$date = get_the_date( 'F, Y' );
				}
				if ( is_day() ) {
					$date = get_the_date();
				}
				if ( $sampleData ) {
					$date = $this->formatDateAsI18n( date_i18n( 'U' ) );
				}
				if ( ! empty( $date ) ) {
					return $date;
				}

				break;
			case 'archive_title':
				$title = is_post_type_archive() ? post_type_archive_title( '', false ) : get_the_archive_title();

				return $sampleData ? __( 'Sample Archive Title', 'all-in-one-seo-pack' ) : wp_strip_all_tags( $title );
			case 'author_bio':
				$bio = get_the_author_meta( 'description', $author->ID );

				return empty( $bio ) && $sampleData ? __( 'Sample author biography', 'all-in-one-seo-pack' ) : $bio;
			case 'author_first_name':
				$name = $author->first_name;

				return empty( $name ) && $sampleData ? wp_get_current_user()->first_name : $author->first_name;
			case 'author_last_name':
				$name = $author->last_name;

				return empty( $name ) && $sampleData ? wp_get_current_user()->last_name : $author->last_name;
			case 'author_link':
				return '<a href="' . esc_url( get_author_posts_url( $author->ID ) ) . '">' . esc_html( $author->display_name ) . '</a>';
			case 'author_link_alt':
				return '<a href="' . esc_url( get_author_posts_url( $author->ID ) ) . '">' . esc_url( get_author_posts_url( $author->ID ) ) . '</a>';
			case 'author_name':
				$name = $author->display_name;

				return empty( $name ) && $sampleData ? wp_get_current_user()->display_name : $author->display_name;
			case 'author_url':
				$authorUrl = get_author_posts_url( $author->ID );

				return ! empty( $authorUrl ) ? $authorUrl : '';
			case 'attachment_caption':
				$caption = wp_get_attachment_caption( $postId );

				return empty( $caption ) && $sampleData ? __( 'Sample caption for media.', 'all-in-one-seo-pack' ) : $caption;
			case 'attachment_description':
				$description = ! empty( $post->post_content ) ? $post->post_content : '';

				return empty( $description ) && $sampleData ? __( 'Sample description for media.', 'all-in-one-seo-pack' ) : $description;
			case 'categories':
				if ( ! is_object( $post ) || 'post' !== $post->post_type ) {
					return ! is_object( $post ) && $sampleData ? __( 'Sample Category 1, Sample Category 2', 'all-in-one-seo-pack' ) : '';
				}
				$categories = get_the_terms( $post->ID, 'category' );

				$names = [];
				if ( ! is_array( $categories ) ) {
					return '';
				}

				foreach ( $categories as $category ) {
					$names[] = $category->name;
				}

				return implode( ', ', $names );
			case 'category_link':
				return '<a href="' . esc_url( get_category_link( $category ) ) . '">' . ( $category ? $category[0]->name : '' ) . '</a>';
			case 'category_link_alt':
				return '<a href="' . esc_url( get_category_link( $category ) ) . '">' . esc_url( get_category_link( $category ) ) . '</a>';
			case 'current_date':
				return $this->formatDateAsI18n( date_i18n( 'U' ) );
			case 'current_day':
				return date_i18n( 'd' );
			case 'current_month':
				return date_i18n( 'F' );
			case 'current_year':
				return date_i18n( 'Y' );
			case 'custom_field':
				return $sampleData ? __( 'Sample Custom Field Value', 'all-in-one-seo-pack' ) : '';
			case 'featured_image':
				if ( ! has_post_thumbnail( $postId ) ) {
					return $sampleData ? __( 'Sample featured image', 'all-in-one-seo-pack' ) : '';
				}

				$imageId = get_post_thumbnail_id( $postId );
				$image   = (array) wp_get_attachment_image_src( $imageId, 'full' );
				$image   = isset( $image[0] ) ? '<img src="' . $image[0] . '" style="display: block; margin: 1em auto">' : ''; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage

				return $sampleData ? __( 'Sample featured image', 'all-in-one-seo-pack' ) : $image;
			case 'page_number':
				return aioseo()->helpers->getPageNumber();
			case 'parent_title':
				if ( ! is_object( $post ) || ! $post->post_parent ) {
					return ! is_object( $post ) && $sampleData ? __( 'Sample Parent', 'all-in-one-seo-pack' ) : '';
				}
				$parent = get_post( $post->post_parent );

				return $parent ? $parent->post_title : '';
			case 'permalink':
				return aioseo()->helpers->getUrl();
			case 'post_date':
				$date = $this->formatDateAsI18n( get_the_date( 'U' ) );

				return empty( $date ) && $sampleData ? $this->formatDateAsI18n( date_i18n( 'U' ) ) : $date;
			case 'post_day':
				$day = get_the_date( 'd', $post );

				return empty( $day ) && $sampleData ? date_i18n( 'd' ) : $day;
			case 'post_excerpt_only':
				return empty( $postId ) ? ( $sampleData ? __( 'Sample excerpt from a page/post.', 'all-in-one-seo-pack' ) : '' ) : $post->post_excerpt;
			case 'post_excerpt':
				if ( empty( $postId ) ) {
					return $sampleData ? __( 'Sample excerpt from a page/post.', 'all-in-one-seo-pack' ) : '';
				}

				if ( $post->post_excerpt ) {
					return $post->post_excerpt;
				}

				// Fall through if the post doesn't have an excerpt set. In that case getDescriptionFromContent() will generate it for us.
			case 'post_content':
				return empty( $postId ) ? ( $sampleData ? __( 'An example of content from your page/post.', 'all-in-one-seo-pack' ) : '' ) : aioseo()->helpers->getDescriptionFromContent( $post );
			case 'post_link':
				return '<a href="' . esc_url( get_permalink( $post ) ) . '">' . esc_html( get_the_title( $post ) ) . '</a>';
			case 'post_link_alt':
				return '<a href="' . esc_url( get_permalink( $post ) ) . '">' . esc_url( get_permalink( $post ) ) . '</a>';
			case 'post_month':
				$month = get_the_date( 'F', $post );

				return empty( $month ) && $sampleData ? date_i18n( 'F' ) : $month;
			case 'post_title':
				$title = esc_html( get_the_title( $post ) );

				return empty( $title ) && $sampleData ? __( 'Sample Post', 'all-in-one-seo-pack' ) : $title;
			case 'post_year':
				$year = get_the_date( 'Y', $post );

				return empty( $year ) && $sampleData ? date_i18n( 'Y' ) : $year;
			case 'search_term':
				$search = get_search_query();

				return empty( $search ) && $sampleData ? __( 'Example search string', 'all-in-one-seo-pack' ) : esc_attr( stripslashes( $search ) );
			case 'separator_sa':
				return aioseo()->helpers->decodeHtmlEntities( aioseo()->options->searchAppearance->global->separator );
			case 'site_link':
			case 'blog_link':
				return '<a href="' . esc_url( get_bloginfo( 'url' ) ) . '">' . esc_html( get_bloginfo( 'name' ) ) . '</a>';
			case 'site_link_alt':
				return '<a href="' . esc_url( get_bloginfo( 'url' ) ) . '">' . esc_url( get_bloginfo( 'url' ) ) . '</a>';
			case 'tag':
				return single_term_title( '', false );
			case 'tax_name':
				return $sampleData ? __( 'Sample Taxonomy Name Value', 'all-in-one-seo-pack' ) : '';
			case 'tax_parent_name':
				$termObject       = get_term( $id ); // Don't use the getTerm() helper here. We need the actual Product Attribute tax.
				$parentTermObject = ! empty( $termObject->parent ) ? aioseo()->helpers->getTerm( $termObject->parent ) : '';
				$name             = $parentTermObject->name ?? '';

				if (
					is_a( $termObject, 'WP_Term' ) &&
					empty( $parentTermObject ) &&
					aioseo()->helpers->isWooCommerceProductAttribute( $termObject->taxonomy )
				) {
					$wcAttributeTaxonomiesTable = aioseo()->core->db->prefix . 'woocommerce_attribute_taxonomies';
					$attributeName              = str_replace( 'pa_', '', $termObject->taxonomy );

					$result = aioseo()->core->db->db->get_row(
						aioseo()->core->db->db->prepare(
							"SELECT attribute_label FROM $wcAttributeTaxonomiesTable WHERE attribute_name = %s",
							$attributeName
						)
					);

					return $result->attribute_label ?? '';
				}

				return $sampleData ? __( 'Sample Parent Term Name', 'all-in-one-seo-pack' ) : $name;
			case 'taxonomy_description':
				$description = term_description();

				return empty( $description ) && $sampleData ? __( 'Sample taxonomy description', 'all-in-one-seo-pack' ) : $description;
			case 'taxonomy_title':
			case 'category':
				$title = $this->getTaxonomyTitle( $postId );

				return ! $title && $sampleData ? __( 'Sample Taxonomy Title', 'all-in-one-seo-pack' ) : $title;
			case 'site_description':
			case 'blog_description':
			case 'tagline':
				return aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'description' ) );
			case 'site_title':
			case 'blog_title':
				return aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) );
			default:
				return '';
		}
	}

	/**
	 * Get the category title.
	 *
	 * @since 4.0.0
	 *
	 * @param  integer $postId The post ID if set.
	 * @return string          The category title.
	 */
	private function getTaxonomyTitle( $postId = null ) {
		$isWcActive = aioseo()->helpers->isWooCommerceActive();
		$title      = '';
		if ( $isWcActive && is_product_category() ) {
			$title = single_cat_title( '', false );
		} elseif ( is_category() ) {
			$title = single_cat_title( '', false );
		} elseif ( is_tag() ) {
			$title = single_tag_title( '', false );
		} elseif ( is_author() ) {
			$title = get_the_author();
		} elseif ( is_tax() ) {
			$title = single_term_title( '', false );
		} elseif ( is_post_type_archive() ) {
			$title = post_type_archive_title( '', false );
		} elseif ( is_archive() ) {
			$title = get_the_archive_title();
		}

		if ( $postId ) {
			$currentScreen  = aioseo()->helpers->getCurrentScreen();
			$isProduct      = $isWcActive && ( is_product() || 'product' === ( $currentScreen->post_type ?? '' ) );
			$post           = aioseo()->helpers->getPost( $postId );
			$postTaxonomies = get_object_taxonomies( $post, 'objects' );
			$postTerms      = [];
			foreach ( $postTaxonomies as $taxonomySlug => $taxonomy ) {
				if ( ! $taxonomy->hierarchical ) {
					continue;
				}

				$taxonomySlug = $isProduct ? 'product_cat' : $taxonomySlug;
				$primaryTerm  = aioseo()->standalone->primaryTerm->getPrimaryTerm( $postId, $taxonomySlug );
				if ( $primaryTerm ) {
					$postTerms[] = aioseo()->helpers->getTerm( $primaryTerm, $taxonomySlug );
					break;
				}

				$postTaxonomyTerms = get_the_terms( $postId, $taxonomySlug );
				if ( is_array( $postTaxonomyTerms ) ) {
					$postTerms = array_merge( $postTerms, $postTaxonomyTerms );
					break;
				}
			}

			$title = $postTerms ? $postTerms[0]->name : '';
		}

		return wp_strip_all_tags( (string) $title );
	}

	/**
	 * Formatted Date
	 *
	 * Get formatted date based on WP options.
	 *
	 * @since 4.0.0
	 *
	 * @param  null|int    $date   Date in UNIX timestamp format. Otherwise, current time.
	 * @return string              Date internationalized.
	 */
	public function formatDateAsI18n( $date = null ) {
		if ( ! $date ) {
			$date = time();
		}

		$format        = get_option( 'date_format' );
		$formattedDate = date_i18n( $format, $date );

		return apply_filters(
			'aioseo_format_date',
			$formattedDate,
			[
				$date,
				$format
			]
		);
	}

	/**
	 * Parses custom taxonomy tags by replacing them with the name of the first assigned term of the given taxonomy.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string The string to parse.
	 * @return mixed          The new title.
	 */
	private function parseTaxonomyNames( $string, $id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$pattern = '/' . $this->denotationChar . 'tax_name-([a-zA-Z0-9_-]+)/im';
		$string  = preg_replace_callback( $pattern, [ $this, 'replaceTaxonomyName' ], $string );
		$pattern = '/' . $this->denotationChar . 'tax_name(?![a-zA-Z0-9_-])/im';

		return preg_replace( $pattern, '', (string) $string );
	}

	/**
	 * Adds support for using #custom_field-[custom_field_title] for using
	 * custom fields / Advanced Custom Fields in titles / descriptions etc.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string The string to parse customs fields out of.
	 * @param  int    $postId The page or post ID.
	 * @return string         The new title.
	 */
	public function parseCustomFields( $string, $postId = 0 ) {
		$pattern = '/' . $this->denotationChar . 'custom_field-([a-zA-Z0-9_-]+)/im';
		$matches = [];
		preg_match_all( $pattern, (string) $string, $matches, PREG_SET_ORDER );

		$string  = $this->replaceCustomField( $string, $matches, $postId );
		$pattern = '/' . $this->denotationChar . 'custom_field(?![a-zA-Z0-9_-])/im';

		return preg_replace( $pattern, '', (string) $string );
	}

	/**
	 * Add context to our internal context.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $context A context array to append.
	 * @return void
	 */
	public function addContext( $context ) {
		$this->context = array_merge( $this->context, $context );
	}

	/**
	 * Add tags to our internal tags.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $tags A tags array to append.
	 * @return void
	 */
	public function addTags( $tags ) {
		$this->tags = array_merge( $this->tags, $tags );
	}

	/**
	 * Replaces a taxonomy name tag with its respective value.
	 *
	 * @since 4.0.0
	 *
	 * @param  array  $matches The matches.
	 * @return string          The replaced matches.
	 */
	private function replaceTaxonomyName( $matches ) {
		$termName = '';
		$post     = aioseo()->helpers->getPost();
		if ( ! empty( $matches[1] ) && $post ) {
			$taxonomy = get_taxonomy( $matches[1] );
			if ( ! $taxonomy ) {
				return '';
			}

			$term = aioseo()->standalone->primaryTerm->getPrimaryTerm( $post->ID, $taxonomy->name );
			if ( ! $term ) {
				$terms = get_the_terms( $post->ID, $taxonomy->name );
				if ( ! $terms || is_wp_error( $terms ) ) {
					return '';
				}

				$term = array_shift( $terms );
			}

			$termName = $term->name;
		}

		return '%|%' . $termName;
	}

	/**
	 * (ACF) Custom Field Replace.
	 *
	 * @since 4.0.0
	 *
	 * @param  string      $string  The string to parse customs fields out of.
	 * @param  array       $matches Array of matched values.
	 * @param  int         $postId  The page or post ID.
	 * @return bool|string          New title/text.
	 */
	private function replaceCustomField( $string, $matches, $postId ) {
		if ( empty( $matches ) ) {
			return $string;
		}

		$postId = get_queried_object() ?? $postId;

		foreach ( $matches as $match ) {
			$value = '';
			if ( ! empty( $match[1] ) ) {
				if ( function_exists( 'get_field' ) ) {
					$value = get_field( $match[1], $postId );
					if ( ! empty( $value['url'] ) && ! empty( $value['title'] ) ) {
						$value = "<a href='{$value['url']}'>{$value['title']}</a>";
					}
					if ( empty( $value ) ) {
						$value = aioseo()->helpers->getAcfFlexibleContentField( $match[1], $postId );
					}
				}

				if ( empty( $value ) ) {
					global $post;
					if ( ! empty( $post ) ) {
						$value = get_post_meta( $post->ID, $match[1], true );
					}
				}
			}

			$value  = is_scalar( $value ) ? wp_strip_all_tags( $value ) : '';
			$string = str_replace( $match[0], '%|%' . $value, $string );
		}

		return $string;
	}

	/**
	 * Get the default tags for the current post.
	 *
	 * @since 4.0.0
	 *
	 * @param  integer $postId The Post ID.
	 * @return array           An array of tags.
	 */
	public function getDefaultPostTags( $postId ) {
		$post = get_post( $postId );

		$title       = aioseo()->meta->title->getTitle( $post, true );
		$description = aioseo()->meta->description->getDescription( $post, true );

		return [
			'title'       => empty( $title ) ? '' : $title,
			'description' => empty( $description ) ? '' : $description
		];
	}
}Common/Utils/Templates.php000064400000005504151536241210011547 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

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

/**
 * Class Templates
 *
 * @since 4.0.17
 *
 * @package AIOSEO\Plugin\Common\Utils
 */
class Templates {
	/**
	 * This plugin absolute path.
	 *
	 * @since 4.0.17
	 *
	 * @var string
	 */
	protected $pluginPath = AIOSEO_DIR;

	/**
	 * Paths were our template files are located.
	 *
	 * @since 4.0.17
	 *
	 * @var string Array of paths.
	 */
	protected $paths = [
		'app/Common/Views'
	];

	/**
	 *
	 * The theme folder.
	 *
	 * @since 4.0.17
	 *
	 * @var string
	 */
	private $themeTemplatePath = 'aioseo/';

	/**
	 *
	 * A theme subfolder.
	 *
	 * @since 4.0.17
	 *
	 * @var string
	 */
	protected $themeTemplateSubpath = '';

	/**
	 * Locate a template file in the theme or our plugin paths.
	 *
	 * @since 4.0.17
	 *
	 * @param  string $templateName The template name.
	 * @return string               The template absolute path.
	 */
	public function locateTemplate( $templateName ) {
		// Try to find template file in the theme.
		$template = locate_template(
			[
				trailingslashit( $this->getThemeTemplatePath() ) . trailingslashit( $this->getThemeTemplateSubpath() ) . $templateName
			]
		);

		if ( ! $template ) {
			// Try paths, in order.
			foreach ( $this->paths as $path ) {
				$template = trailingslashit( $this->addPluginPath( $path ) ) . $templateName;
				if ( aioseo()->core->fs->exists( $template ) ) {
					break;
				}
			}
		}

		return apply_filters( 'aioseo_locate_template', $template, $templateName );
	}

	/**
	 * Includes a template if the file exists.
	 *
	 * @param  string $templateName The template path/name.php to be included.
	 * @param  null   $data         Data passed down to the template.
	 * @return void
	 */
	public function getTemplate( $templateName, $data = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$template = $this->locateTemplate( $templateName );
		if ( ! empty( $template ) and aioseo()->core->fs->exists( $template ) ) {
			include $template;
		}
	}

	/**
	 * Add this plugin path when trying the paths.
	 *
	 * @since 4.0.17
	 *
	 * @param  string $path A path.
	 * @return string       A path with the plugin absolute path.
	 */
	protected function addPluginPath( $path ) {
		return trailingslashit( $this->pluginPath ) . $path;
	}

	/**
	 * Returns the theme folder for templates.
	 *
	 * @since 4.0.17
	 *
	 * @return string The theme folder for templates.
	 */
	public function getThemeTemplatePath() {
		return apply_filters( 'aioseo_template_path', $this->themeTemplatePath );
	}

	/**
	 *
	 * Returns the theme subfolder for templates.
	 *
	 * @since 4.0.17
	 *
	 * @return string The theme subfolder for templates.
	 */
	public function getThemeTemplateSubpath() {
		return apply_filters( 'aioseo_template_subpath', $this->themeTemplateSubpath );
	}
}Common/Utils/VueSettings.php000064400000023223151536241210012067 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

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

/**
 * Vue Settings for the user.
 *
 * @since 4.0.0
 */
class VueSettings {
	/**
	 * The name to lookup the settings with.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $settingsName = '';

	/**
	 * The settings array.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $settings = [];

	/**
	 * All the default settings.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $defaults = [
		'showUpgradeBar'  => true,
		'showSetupWizard' => true,
		'toggledCards'    => [
			'dashboardOverview'            => true,
			'dashboardSeoSetup'            => true,
			'dashboardSeoSiteScore'        => true,
			'dashboardNotifications'       => true,
			'dashboardSupport'             => true,
			'license'                      => true,
			'webmasterTools'               => true,
			'enableBreadcrumbs'            => true,
			'breadcrumbSettings'           => true,
			'breadcrumbTemplates'          => true,
			'advanced'                     => true,
			'accessControl'                => true,
			'rssContent'                   => true,
			'generalSitemap'               => true,
			'generalSitemapSettings'       => true,
			'imageSitemap'                 => true,
			'videoSitemap'                 => true,
			'newsSitemap'                  => true,
			'rssSitemap'                   => true,
			'rssSitemapSettings'           => true,
			'rssAdditionalPages'           => true,
			'rssAdvancedSettings'          => true,
			'additionalPages'              => true,
			'advancedSettings'             => true,
			'videoSitemapSettings'         => true,
			'videoAdditionalPages'         => true,
			'videoAdvancedSettings'        => true,
			'videoEmbedSettings'           => true,
			'newsSitemapSettings'          => true,
			'newsAdditionalPages'          => true,
			'newsAdvancedSettings'         => true,
			'newsEmbedSettings'            => true,
			'socialProfiles'               => true,
			'facebook'                     => true,
			'facebookHomePageSettings'     => true,
			'facebookAdvancedSettings'     => true,
			'twitter'                      => true,
			'twitterHomePageSettings'      => true,
			'pinterest'                    => true,
			'searchTitleSeparator'         => true,
			'searchHomePage'               => true,
			'searchSchema'                 => true,
			'searchMediaAttachments'       => true,
			'searchAdvanced'               => true,
			'searchAdvancedCrawlCleanup'   => true,
			'searchCleanup'                => true,
			'authorArchives'               => true,
			'dateArchives'                 => true,
			'searchArchives'               => true,
			'imageSeo'                     => true,
			'completeSeoChecklist'         => true,
			'localBusinessInfo'            => true,
			'localBusinessOpeningHours'    => true,
			'locationsSettings'            => true,
			'advancedLocationsSettings'    => true,
			'localBusinessMapsApiKey'      => true,
			'localBusinessMapsSettings'    => true,
			'robotsEditor'                 => true,
			'databaseTools'                => true,
			'htaccessEditor'               => true,
			'databaseToolsLogs'            => true,
			'systemStatusInfo'             => true,
			'addNewRedirection'            => true,
			'redirectSettings'             => true,
			'debug'                        => true,
			'fullSiteRedirectsRelocate'    => true,
			'fullSiteRedirectsAliases'     => true,
			'fullSiteRedirectsCanonical'   => true,
			'fullSiteRedirectsHttpHeaders' => true,
			'htmlSitemap'                  => true,
			'htmlSitemapSettings'          => true,
			'htmlSitemapAdvancedSettings'  => true,
			'linkAssistantSettings'        => true,
			'domainActivations'            => true,
			'404Settings'                  => true,
			'userProfiles'                 => true,
			'queryArgLogs'                 => true,
			'aiContentSettings'            => true,
			'writingAssistantSettings'     => true,
			'writingAssistantCta'          => true
		],
		'toggledRadio'    => [
			'breadcrumbsShowMoreSeparators' => false,
			'searchShowMoreSeparators'      => false,
			'overviewPostType'              => 'post',
		],
		'dismissedAlerts' => [
			'searchStatisticsContentRankings' => false,
			'searchConsoleNotConnected'       => false,
			'searchConsoleSitemapErrors'      => false
		],
		'internalTabs'    => [
			'authorArchives'    => 'title-description',
			'dateArchives'      => 'title-description',
			'searchArchives'    => 'title-description',
			'seoAuditChecklist' => 'all-items'
		],
		'tablePagination' => [
			'networkDomains'                         => 20,
			'redirects'                              => 20,
			'redirectLogs'                           => 20,
			'redirect404Logs'                        => 20,
			'sitemapAdditionalPages'                 => 20,
			'linkAssistantLinksReport'               => 20,
			'linkAssistantPostsReport'               => 20,
			'linkAssistantDomainsReport'             => 20,
			'searchStatisticsSeoStatistics'          => 20,
			'searchStatisticsKeywordRankings'        => 20,
			'searchStatisticsContentRankings'        => 20,
			'searchStatisticsPostDetailKeywords'     => 20,
			'searchStatisticsKrtKeywords'            => 20,
			'searchStatisticsKrtGroups'              => 20,
			'searchStatisticsKrtGroupsTableKeywords' => 10,
			'searchStatisticsIndexStatus'            => 20,
			'queryArgs'                              => 20
		],
		'semrushCountry'  => 'US'
	];

	/**
	 * The Construct method.
	 *
	 * @since 4.0.0
	 *
	 * @param string $settings An array of settings.
	 */
	public function __construct( $settings = '_aioseo_settings' ) {
		$this->addDynamicDefaults();

		$this->settingsName = $settings;

		$dbSettings     = get_user_meta( get_current_user_id(), $settings, true );
		$this->settings = $dbSettings
			? array_replace_recursive( $this->defaults, $dbSettings )
			: $this->defaults;
	}

	/**
	 * Adds some defaults that are dynamically generated.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function addDynamicDefaults() {
		$postTypes = aioseo()->helpers->getPublicPostTypes( false, false, true, [ 'include' => [ 'buddypress' ] ] );
		foreach ( $postTypes as $postType ) {
			$this->defaults['toggledCards'][ $postType['name'] . 'SA' ] = true;
			$this->defaults['internalTabs'][ $postType['name'] . 'SA' ] = 'title-description';
		}

		$taxonomies = aioseo()->helpers->getPublicTaxonomies( false, true );
		foreach ( $taxonomies as $taxonomy ) {
			$this->defaults['toggledCards'][ $taxonomy['name'] . 'SA' ] = true;
			$this->defaults['internalTabs'][ $taxonomy['name'] . 'SA' ] = 'title-description';
		}

		$postTypes = aioseo()->helpers->getPublicPostTypes( false, true, true, [ 'include' => [ 'buddypress' ] ] );
		foreach ( $postTypes as $postType ) {
			$this->defaults['toggledCards'][ $postType['name'] . 'ArchiveArchives' ] = true;
			$this->defaults['internalTabs'][ $postType['name'] . 'ArchiveArchives' ] = 'title-description';
		}

		// Check any addons for defaults.
		$addonsDefaults = array_filter( aioseo()->addons->doAddonFunction( 'vueSettings', 'addDynamicDefaults' ) );
		foreach ( $addonsDefaults as $addonDefaults ) {
			$this->defaults = array_merge_recursive( $this->defaults, $addonDefaults );
		}
	}

	/**
	 * Retrieves all settings.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of settings.
	 */
	public function all() {
		return array_replace_recursive( $this->defaults, $this->settings );
	}

	/**
	 * Retrieve a setting or null if missing.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name      The name of the property that is missing on the class.
	 * @param  array  $arguments The arguments passed into the method.
	 * @return mixed             The value from the settings or default/null.
	 */
	public function __call( $name, $arguments = [] ) {
		$value = isset( $this->settings[ $name ] ) ? $this->settings[ $name ] : ( ! empty( $arguments[0] ) ? $arguments[0] : $this->getDefault( $name ) );

		return $value;
	}

	/**
	 * Retrieve a setting or null if missing.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name The name of the property that is missing on the class.
	 * @return mixed        The value from the settings or default/null.
	 */
	public function __get( $name ) {
		$value = isset( $this->settings[ $name ] ) ? $this->settings[ $name ] : $this->getDefault( $name );

		return $value;
	}

	/**
	 * Sets the settings value and saves to the database.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name  The name of the settings.
	 * @param  mixed  $value The value to set.
	 * @return void
	 */
	public function __set( $name, $value ) {
		$this->settings[ $name ] = $value;

		$this->update();
	}

	/**
	 * Checks if an settings is set or returns null if not.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name The name of the settings.
	 * @return mixed        True or null.
	 */
	public function __isset( $name ) {
		return isset( $this->settings[ $name ] ) ? false === empty( $this->settings[ $name ] ) : null;
	}

	/**
	 * Unsets the settings value and saves to the database.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name  The name of the settings.
	 * @return void
	 */
	public function __unset( $name ) {
		if ( ! isset( $this->settings[ $name ] ) ) {
			return;
		}

		unset( $this->settings[ $name ] );

		$this->update();
	}

	/**
	 * Gets the default value for a setting.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name The settings name.
	 * @return mixed        The default value.
	 */
	public function getDefault( $name ) {
		return isset( $this->defaults[ $name ] ) ? $this->defaults[ $name ] : null;
	}

	/**
	 * Updates the settings in the database.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function update() {
		update_user_meta( get_current_user_id(), $this->settingsName, $this->settings );
	}
}Common/Views/admin/posts/columns.php000064400000000637151536241210013530 0ustar00<?php
/**
 * This is the output for the columns on the page/post editor.
 *
 * @since 4.0.0
 */

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

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
?>

<div id="<?php echo esc_attr( $columnName ); ?>-<?php echo esc_attr( $postId ); ?>">
	<?php require AIOSEO_DIR . '/app/Common/Views/parts/loader.php'; ?>
</div>Common/Views/admin/settings-page.php000064400000030033151536241210013443 0ustar00<?php
/**
 * This is the error page HTML.
 *
 * @since 4.1.9
 */

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

// phpcs:disable Generic.Files.LineLength.MaxExceeded
$logoImage = 'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTMyIDI2IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSJhaW9zZW8tbG9nbyI+Cgk8cGF0aAoJCWZpbGwtcnVsZT0iZXZlbm9kZCIKCQljbGlwLXJ1bGU9ImV2ZW5vZGQiCgkJZD0iTTExOS4wMzggMjUuOTI0MUMxMjYuMTk3IDI1LjkyNDEgMTMyIDIwLjEyMDggMTMyIDEyLjk2MkMxMzIgNS44MDMzIDEyNi4xOTcgMCAxMTkuMDM4IDBDMTExLjg3OSAwIDEwNi4wNzYgNS44MDMzIDEwNi4wNzYgMTIuOTYyQzEwNi4wNzYgMjAuMTIwOCAxMTEuODc5IDI1LjkyNDEgMTE5LjAzOCAyNS45MjQxWk0xMTYuOTc0IDQuNzQ0MDhDMTE2Ljc5OCA0LjQ3NjQ4IDExNi40NzMgNC4zNTEzNiAxMTYuMTc1IDQuNDU2NzJDMTE1LjgzNSA0LjU3NjczIDExNS41MDMgNC43MTc4NyAxMTUuMTggNC44NzkyOUMxMTQuODk3IDUuMDIwOTggMTE0Ljc1NSA1LjM0NDY2IDExNC44MTcgNS42NjAzM0wxMTUuMDM5IDYuNzg1MDdDMTE1LjA5NiA3LjA3NDU3IDExNC45NzggNy4zNjgzOSAxMTQuNzU0IDcuNTU1MDRDMTE0LjQgNy44NTAwMyAxMTQuMDcyIDguMTgzNTQgMTEzLjc3OSA4LjU1MjY4QzExMy41OTcgOC43ODIxMiAxMTMuMzA5IDguOTAzNzMgMTEzLjAyNSA4Ljg0NjM4TDExMS45MjMgOC42MjM2NEMxMTEuNjEzIDguNTYxMDcgMTExLjI5NiA4LjcwNzQ3IDExMS4xNTkgOC45OTczOEMxMTEuMDgxIDkuMTYxMTYgMTExLjAwNyA5LjMyODM3IDExMC45MzggOS40OTg5MkMxMTAuODY5IDkuNjY5NDcgMTEwLjgwNiA5Ljg0MDkzIDExMC43NDggMTAuMDEzMUMxMTAuNjQ2IDEwLjMxNzkgMTEwLjc3IDEwLjY0OTYgMTExLjAzMyAxMC44Mjc5TDExMS45NjkgMTEuNDYyNUMxMTIuMjEgMTEuNjI2IDExMi4zMyAxMS45MTkgMTEyLjMwMSAxMi4yMTI4QzExMi4yNTQgMTIuNjg1NiAxMTIuMjU2IDEzLjE1NzUgMTEyLjMwNCAxMy42MjE3QzExMi4zMzQgMTMuOTE1NCAxMTIuMjE1IDE0LjIwODggMTExLjk3NCAxNC4zNzMxTDExMS4wNCAxNS4wMTE0QzExMC43NzggMTUuMTkwNiAxMTAuNjU1IDE1LjUyMjQgMTEwLjc1OCAxNS44MjY4QzExMC44NzYgMTYuMTczNCAxMTEuMDE0IDE2LjUxMjUgMTExLjE3MiAxNi44NDE5QzExMS4zMTEgMTcuMTMxMSAxMTEuNjI5IDE3LjI3NjEgMTExLjkzOCAxNy4yMTI1TDExMy4wNCAxNi45ODU3QzExMy4zMjQgMTYuOTI3MyAxMTMuNjEyIDE3LjA0NzggMTEzLjc5NSAxNy4yNzY3QzExNC4wODQgMTcuNjM4NCAxMTQuNDExIDE3Ljk3MjMgMTE0Ljc3MiAxOC4yNzE1QzExNC45OTcgMTguNDU3NCAxMTUuMTE2IDE4Ljc1MDggMTE1LjA2IDE5LjA0MDVMMTE0Ljg0MiAyMC4xNjU2QzExNC43ODEgMjAuNDgxNyAxMTQuOTI0IDIwLjgwNDkgMTE1LjIwOCAyMC45NDU1QzExNS4zNjkgMjEuMDI0OSAxMTUuNTMzIDIxLjA5OTkgMTE1LjcgMjEuMTcwMkMxMTUuODY3IDIxLjI0MDUgMTE2LjAzNSAyMS4zMDUxIDExNi4yMDQgMjEuMzY0MkMxMTYuNjk3IDIxLjUzNjkgMTE3LjM4OCAyMC45MTg1IDExNy44OTkgMjAuNDYxM0MxMTguMTUxIDIwLjIzNTggMTE4LjMwNiAxOS45MTY3IDExOC4zMDggMTkuNTc1MUMxMTguMzA4IDE5LjU3MzIgMTE4LjMwOCAxOS41NzE0IDExOC4zMDggMTkuNTY5NkwxMTguMzA4IDE3LjY4ODJDMTE4LjMwOCAxNy42NjgyIDExOC4zMDkgMTcuNjQ4NSAxMTguMzEgMTcuNjI4OUMxMTYuODAxIDE3LjI2MDkgMTE1LjY4IDE1Ljg3NTkgMTE1LjY4IDE0LjIyMzZWMTIuMjI1OEMxMTUuNjggMTIuMDczOSAxMTUuOCAxMS45NTA4IDExNS45NDkgMTEuOTUwOEgxMTYuODg0VjkuOTg1MjFDMTE2Ljg4NCA5LjcxMzgxIDExNy4wOTkgOS40OTM4MSAxMTcuMzY1IDkuNDkzODFDMTE3LjYzMSA5LjQ5MzgxIDExNy44NDcgOS43MTM4MSAxMTcuODQ3IDkuOTg1MjFWMTEuOTUwOEgxMjAuMzc1VjkuOTg1MjFDMTIwLjM3NSA5LjcxMzgxIDEyMC41OTEgOS40OTM4MSAxMjAuODU3IDkuNDkzODFDMTIxLjEyMyA5LjQ5MzgxIDEyMS4zMzggOS43MTM4MSAxMjEuMzM4IDkuOTg1MjFWMTEuOTUwOEgxMjIuMjczQzEyMi40MjIgMTEuOTUwOCAxMjIuNTQyIDEyLjA3MzkgMTIyLjU0MiAxMi4yMjU4VjE0LjIyMzZDMTIyLjU0MiAxNS45MjgxIDEyMS4zNDggMTcuMzQ4MiAxMTkuNzY4IDE3LjY2MDhDMTE5Ljc2OCAxNy42Njk5IDExOS43NjggMTcuNjc5IDExOS43NjggMTcuNjg4MkwxMTkuNzY4IDE5LjU2MTVDMTE5Ljc2OCAxOS45MDk3IDExOS45MjggMjAuMjM0NiAxMjAuMTg3IDIwLjQ2MDlDMTIwLjcwNyAyMC45MTQzIDEyMS40MSAyMS41MjczIDEyMS45MDEgMjEuMzUzOUMxMjIuMjQxIDIxLjIzMzkgMTIyLjU3MyAyMS4wOTI3IDEyMi44OTYgMjAuOTMxM0MxMjMuMTc5IDIwLjc4OTYgMTIzLjMyMSAyMC40NjU5IDEyMy4yNTkgMjAuMTUwM0wxMjMuMDM3IDE5LjAyNTVDMTIyLjk4IDE4LjczNiAxMjMuMDk4IDE4LjQ0MjIgMTIzLjMyMiAxOC4yNTU1QzEyMy42NzYgMTcuOTYwNiAxMjQuMDA0IDE3LjYyNzEgMTI0LjI5NyAxNy4yNTc5QzEyNC40NzkgMTcuMDI4NSAxMjQuNzY3IDE2LjkwNjkgMTI1LjA1IDE2Ljk2NDJMMTI2LjE1MyAxNy4xODdDMTI2LjQ2MyAxNy4yNDk1IDEyNi43OCAxNy4xMDMxIDEyNi45MTcgMTYuODEzMkMxMjYuOTk1IDE2LjY0OTQgMTI3LjA2OSAxNi40ODIyIDEyNy4xMzggMTYuMzExN0MxMjcuMjA2IDE2LjE0MTIgMTI3LjI3IDE1Ljk2OTcgMTI3LjMyOCAxNS43OTc1QzEyNy40MyAxNS40OTI3IDEyNy4zMDYgMTUuMTYxMSAxMjcuMDQzIDE0Ljk4MjhMMTI2LjEwNyAxNC4zNDgxQzEyNS44NjYgMTQuMTg0NiAxMjUuNzQ2IDEzLjg5MTYgMTI1Ljc3NSAxMy41OTc4QzEyNS44MjIgMTMuMTI1IDEyNS44MiAxMi42NTMxIDEyNS43NzIgMTIuMTg4OUMxMjUuNzQyIDExLjg5NTIgMTI1Ljg2MSAxMS42MDE4IDEyNi4xMDIgMTEuNDM3NUwxMjcuMDM2IDEwLjc5OTJDMTI3LjI5OCAxMC42MjAxIDEyNy40MjEgMTAuMjg4MiAxMjcuMzE4IDkuOTgzODVDMTI3LjIgOS42MzcyMSAxMjcuMDYyIDkuMjk4MTUgMTI2LjkwMyA4Ljk2ODc0QzEyNi43NjUgOC42Nzk1NyAxMjYuNDQ3IDguNTM0NSAxMjYuMTM4IDguNTk4MTRMMTI1LjAzNiA4LjgyNDk0QzEyNC43NTIgOC44ODMzMSAxMjQuNDY0IDguNzYyNzcgMTI0LjI4MSA4LjUzMzkxQzEyMy45OTIgOC4xNzIyMiAxMjMuNjY1IDcuODM4MzIgMTIzLjMwNCA3LjUzOTE0QzEyMy4wNzkgNy4zNTMxOSAxMjIuOTU5IDcuMDU5NzkgMTIzLjAxNiA2Ljc3MDA5TDEyMy4yMzQgNS42NDUwMUMxMjMuMjk1IDUuMzI4OTYgMTIzLjE1MiA1LjAwNTY3IDEyMi44NjggNC44NjUxQzEyMi43MDcgNC43ODU2OCAxMjIuNTQzIDQuNzEwNzIgMTIyLjM3NiA0LjY0MDQzQzEyMi4yMDkgNC41NzAxNCAxMjIuMDQxIDQuNTA1NTEgMTIxLjg3MiA0LjQ0NjQ2QzEyMS41NzQgNC4zNDE5NCAxMjEuMjQ5IDQuNDY4MTkgMTIxLjA3NCA0LjczNjU0TDEyMC40NTIgNS42OTE4M0MxMjAuMjkyIDUuOTM3ODEgMTIwLjAwNSA2LjA2MDMyIDExOS43MTcgNi4wMzA2QzExOS4yNTMgNS45ODI3OSAxMTguNzkxIDUuOTg0NzMgMTE4LjMzNiA2LjAzMzUzQzExOC4wNDggNi4wNjQ0MSAxMTcuNzYxIDUuOTQyOTIgMTE3LjYgNS42OTc1MUwxMTYuOTc0IDQuNzQ0MDhaIgoJCWZpbGw9IiMwMDVBRTAiCgkvPgoJPHBhdGgKCQlmaWxsLXJ1bGU9ImV2ZW5vZGQiCgkJY2xpcC1ydWxlPSJldmVub2RkIgoJCWQ9Ik0xMDUuNTEzIDEuMDUzMzdIODguMjk0MVYyNS4xMDY4SDEwNS42MTVDMTA0LjgyMSAyMy40NDcyIDEwNC4xODUgMjEuNjk3OCAxMDMuNzI2IDE5Ljg3NzhIOTQuNDk2OFYxNS41NTAzSDEwMi45ODlDMTAyLjkxMiAxNC43MDEyIDEwMi44NzIgMTMuODQxMiAxMDIuODcyIDEyLjk3MkMxMDIuODcyIDEyLjA2NTggMTAyLjkxNSAxMS4xNjk2IDEwMi45OTkgMTAuMjg1M0g5NC40OTY4VjYuMjgyMzdIMTAzLjY3MkMxMDQuMTE1IDQuNDYzNzUgMTA0LjczNSAyLjcxNDM1IDEwNS41MTMgMS4wNTMzN1pNNzUuMzY3OSAyNS41Mzk1QzcwLjQ5OTUgMjUuNTM5NSA2Ny4xMDk2IDI0LjAyNDkgNjQuNjkzNSAyMS43MTY5TDY3Ljk3NTEgMTcuMDY0OUM2OS43MDYxIDE4Ljc5NTkgNzIuMzc0NyAyMC4yMzg0IDc1LjY1NjQgMjAuMjM4NEM3Ny43ODQgMjAuMjM4NCA3OS4wODIzIDE5LjMzNjggNzkuMDgyMyAxOC4xODI5Qzc5LjA4MjMgMTYuODEyNSA3Ny41MzE2IDE2LjI3MTYgNzQuOTcxMiAxNS43MzA2TDc0Ljc2NzQgMTUuNjg5OUM3MC44MTcgMTQuOTAxNiA2NS40NTA4IDEzLjgzMDYgNjUuNDUwOCA4LjIyOTczQzY1LjQ1MDggNC4xOTA3NyA2OC44NzY3IDAuNjkyNzQ5IDc1LjA0MzMgMC42OTI3NDlDNzguOTAxOSAwLjY5Mjc0OSA4Mi4yNTU3IDEuODQ2NzQgODQuODE2MSA0LjA0NjUyTDgxLjQyNjMgOC40ODIxNkM3OS40MDY4IDYuODIzMyA3Ni43NzQzIDUuOTkzODggNzQuNjQ2NiA1Ljk5Mzg4QzcyLjU5MTEgNS45OTM4OCA3MS43OTc3IDYuODIzMyA3MS43OTc3IDcuODY5MTFDNzEuNzk3NyA5LjEzMTI4IDczLjI3NjMgOS41NjQwMiA3NS45NDQ5IDEwLjA2ODlDNzkuOTExNyAxMC44OTgzIDg1LjM5MzEgMTIuMDUyMyA4NS4zOTMxIDE3LjQ5NzdDODUuMzkzMSAyMi4zMyA4MS44MjMgMjUuNTM5NSA3NS4zNjc5IDI1LjUzOTVaIgoJCWZpbGw9IiMwMDVBRTAiCgkvPgoJPHBhdGgKCQlkPSJNMTguNjY0NiAyNS4xMTg2SDI1LjIyNTNMMTYuMzg0MiAxLjcxNzU5SDguODA2MDZMMCAyNS4xMTg2SDYuNTYwNjlMNy43NTM1NSAyMS41NzUxSDE3LjQ3MThMMTguNjY0NiAyNS4xMTg2Wk0xMi41OTUxIDYuOTgwMThMMTUuODkzIDE2LjQ4NzlIOS4zMzIzMkwxMi41OTUxIDYuOTgwMThaIgoJCWZpbGw9IiMxNDFCMzgiCgkvPgoJPHBhdGgKCQlkPSJNMjcuOTk5IDI1LjExODZIMzQuMDMzNVYxLjcxNzU5SDI3Ljk5OVYyNS4xMTg2WiIKCQlmaWxsPSIjMTQxQjM4IgoJLz4KCTxwYXRoCgkJZD0iTTM3LjA1MDQgMTMuNDM1NkMzNy4wNTA0IDIwLjU1NzcgNDIuNDE4MyAyNS41Mzk2IDQ5LjU3NTQgMjUuNTM5NkM1Ni43MzI1IDI1LjUzOTYgNjIuMDY1MyAyMC41NTc3IDYyLjA2NTMgMTMuNDM1NkM2Mi4wNjUzIDYuMzEzNTggNTYuNzMyNSAxLjMzMTY3IDQ5LjU3NTQgMS4zMzE2N0M0Mi40MTgzIDEuMzMxNjcgMzcuMDUwNCA2LjMxMzU4IDM3LjA1MDQgMTMuNDM1NlpNNTUuOTI1NiAxMy40MzU2QzU1LjkyNTYgMTcuMjI0NyA1My40MzQ2IDIwLjIwNjggNDkuNTc1NCAyMC4yMDY4QzQ1LjY4MTEgMjAuMjA2OCA0My4xOTAxIDE3LjIyNDcgNDMuMTkwMSAxMy40MzU2QzQzLjE5MDEgOS42MTE0NyA0NS42ODExIDYuNjY0NDIgNDkuNTc1NCA2LjY2NDQyQzUzLjQzNDYgNi42NjQ0MiA1NS45MjU2IDkuNjExNDcgNTUuOTI1NiAxMy40MzU2WiIKCQlmaWxsPSIjMTQxQjM4IgoJLz4KPC9zdmc+';
$medium    = false !== strpos( AIOSEO_PHP_VERSION_DIR, 'pro' ) ? 'proplugin' : 'liteplugin';
?>
<style type="text/css">
	#aioseo-settings-area {
		visibility: hidden;
		margin: auto;
		width: 750px;
		max-width: 100%;
		animation: loadAioseoSettingsNoJSView 0s 5s forwards;
	}

	#aioseo-settings-error-loading-area {
		text-align: center;
		background-color: #fff;
		border: 1px solid #D6E2EC;
		padding: 15px 50px 30px;
		color: #141B38;
		margin: 82px 0;
	}

	#aioseo-settings-logo {
		max-width: 100%;
		width: 240px;
		padding: 30px 0 15px;
	}

	.aioseo-settings-button,
	.aioseo-settings-button:focus {
		margin-left: auto;
		background-color: #005ae0;
		border-color: #3380BC;
		border-bottom-width: 2px;
		color: #fff;
		border-radius: 3px;
		font-weight: 600;
		transition: all 0.1s ease-in-out;
		transition-duration: 0.2s;
		padding: 14px 35px;
		font-size: 16px;
		margin-top: 10px;
		margin-bottom: 20px;
		text-decoration: none;
		display: inline-block;
	}

	.aioseo-settings-button:hover {
		color: #fff;
		background-color: #1a82ea;
	}

	#aioseo-alert-message {
		position: relative;
		border-radius: 3px;
		padding: 12px 20px;
		font-size: 14px;
		color: #141B38;
		line-height: 1.4;
		border: 1px solid #DF2A4A;
		background-color: #FBE9EC;
	}

	#aioseo-settings-area h3 {
		font-size: 20px;
		color: #434343;
		font-weight: 500;
		line-height:1.4;
	}

	#aioseo-settings-area p {
		line-height: 1.5;
		margin: 1em 0;
		font-size: 16px;
		color: #434343;
		padding: 5px 20px 20px;
	}

	@keyframes loadAioseoSettingsNoJSView{
		to { visibility: visible; }
	}
</style>
<!--[if IE]>
	<style>
		#aioseo-settings-area{
			visibility: visible !important;
		}
	</style>
<![endif]-->

<script type="text/javascript">
	var ua   = window.navigator.userAgent;
	var msie = ua.indexOf( 'MSIE ' );
	if (0 < msie) {
		document.addEventListener('DOMContentLoaded', () => {
			var browserError = document.getElementById( 'aioseo-error-browser' ),
				jsError      = document.getElementById( 'aioseo-error-js' );

			jsError.style.display      = 'none';
			browserError.style.display = 'block';
		})
	} else {
		window.onerror = function myErrorHandler( errorMsg, url, lineNumber ) {
			/* Don't try to put error in container that no longer exists post-vue loading */
			var messageContainer = document.getElementById( 'aioseo-nojs-error-message' );
			if ( ! messageContainer ) {
				return false;
			}
			var message                    = document.getElementById( 'aioseo-alert-message' );
			message.innerHTML              = errorMsg;
			messageContainer.style.display = 'block';
			return false;
		}
	}
</script>

<div id="aioseo-settings-area">
	<div id="aioseo-settings-error-loading-area">
		<img
			id="aioseo-settings-logo"
			src="<?php echo esc_attr( $logoImage ); // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage ?>"
			alt="<?php echo esc_attr( AIOSEO_PLUGIN_NAME ); ?>"
		>

		<div id="aioseo-error-js">
			<h3><?php esc_html_e( 'Ooops! It Appears JavaScript Didn’t Load', 'all-in-one-seo-pack' ); ?></h3>

			<p>
				<?php
				printf(
					// Translators: 1 - Line break HTML tag, 2 - "AIOSEO".
					esc_html__( 'There seems to be an issue running JavaScript on your website. %1$s%2$s is built with JavaScript to give you the best experience possible.', 'all-in-one-seo-pack' ),
					'<br>',
					esc_attr( AIOSEO_PLUGIN_SHORT_NAME )
				);
				?>
			</p>

			<div style="display: none;" id="aioseo-nojs-error-message">
				<div id="aioseo-alert-message"></div>

				<p style="margin-top: 5px; font-size: 14px; color: #141B38;">
					<?php
					printf(
						// Translators: 1 - "AIOSEO".
						esc_html__( 'Copy the error message above and paste it in a message to the %1$s support team.', 'all-in-one-seo-pack' ),
						esc_attr( AIOSEO_PLUGIN_SHORT_NAME )
					);
					?>
				</p>
			</div>

			<a href="https://aioseo.com/docs/how-to-fix-javascript-errors/?utm_source=WordPress&utm_medium=<?php echo esc_attr( $medium ); ?>&utm_campaign=javascript-errors" class="aioseo-settings-button" target="_blank">
				<?php esc_html_e( 'Resolve This Issue', 'all-in-one-seo-pack' ); ?>
			</a>
		</div>

		<div id="aioseo-error-browser" style="display: none">
			<h3><?php esc_html_e( 'Your browser version is not supported', 'all-in-one-seo-pack' ); ?></h3>

			<p>
				<?php
				printf(
					// Translators: 1 - "AIOSEO".
					esc_html__( 'You are using a browser which is no longer supported by %1$s. Please update or use another browser in order to access the plugin settings.', 'all-in-one-seo-pack' ),
					esc_attr( AIOSEO_PLUGIN_SHORT_NAME )
				);
				?>
			</p>

			<a href="https://www.aioseo.com/docs/browser-support-policy/?utm_source=WordPress&utm_medium=<?php echo esc_attr( $medium ); ?>&utm_campaign=javascript-errors" class="aioseo-settings-button" target="_blank">
				<?php esc_html_e( 'View supported browsers', 'all-in-one-seo-pack' ); ?>
			</a>
		</div>
	</div>
</div>Common/Views/admin/terms/columns.php000064400000000636151536241210013511 0ustar00<?php
/**
 * This is the output for the columns on the taxonomy screen.
 *
 * @since 4.0.0
 */

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

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
?>

<div id="<?php echo esc_attr( $columnName ); ?>-<?php echo esc_attr( $termId ); ?>">
	<?php require AIOSEO_DIR . '/app/Common/Views/parts/loader.php'; ?>
</div>Common/Views/main/clarity.php000064400000001402151536241210012172 0ustar00<?php
/**
 * This is the output for Microsoft Clarity on the page.
 *
 * @since 4.1.9
 */

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

$projectId = aioseo()->options->webmasterTools->microsoftClarityProjectId;

if ( empty( $projectId ) || aioseo()->helpers->isAmpPage() ) {
	return;
}
?>
		<script type="text/javascript">
			(function(c,l,a,r,i,t,y){
			c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};t=l.createElement(r);t.async=1;
			t.src="https://www.clarity.ms/tag/"+i+"?ref=aioseo";y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
		})(window, document, "clarity", "script", "<?php echo esc_js( $projectId ); ?>");
		</script>
<?php
// Leave this comment to allow for a line break after the closing script tag.Common/Views/main/meta.php000064400000005727151536241210011467 0ustar00<?php
/**
 * This is the output for meta on the page.
 *
 * @since 4.0.0
 */

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

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
// phpcs:disable Generic.WhiteSpace.ScopeIndent.Incorrect
// phpcs:disable Generic.WhiteSpace.ScopeIndent.IncorrectExact
$description = aioseo()->helpers->encodeOutputHtml( aioseo()->meta->description->getDescription() );
$robots      = aioseo()->meta->robots->meta();
$keywords    = $this->keywords->getKeywords();
$canonical   = aioseo()->helpers->canonicalUrl();
$links       = $this->links->getLinks();
$postType    = get_post_type();
$post        = aioseo()->helpers->getPost();
?>
<?php if ( $description ) : ?>
	<meta name="description" content="<?php echo esc_attr( $description ); ?>" />
<?php endif; ?>
<?php if ( $robots ) : ?>
	<meta name="robots" content="<?php echo esc_html( $robots ); ?>" />
<?php
endif;
if (
	apply_filters( 'aioseo_author_meta', true ) &&
	! is_page() &&
	post_type_supports( $postType, 'author' ) &&
	! empty( $post->post_author ) &&
	! empty( get_the_author_meta( 'display_name', $post->post_author ) )
) :
	?>
	<meta name="author" content="<?php echo esc_attr( get_the_author_meta( 'display_name', $post->post_author ) ); ?>"/>
<?php
endif;
?>
<?php // Adds the site verification meta for webmaster tools. ?>
<?php foreach ( $this->verification->meta() as $metaName => $value ) : ?>
	<meta name="<?php echo esc_attr( $metaName ); ?>" content="<?php echo esc_attr( trim( wp_strip_all_tags( $value ) ) ); ?>" />
<?php endforeach; ?>
<?php if ( ! empty( $keywords ) ) : ?>
	<meta name="keywords" content="<?php echo esc_attr( $keywords ); ?>" />
<?php endif; ?>
<?php if ( ! empty( $canonical ) && ! aioseo()->helpers->isAmpPage( 'amp' ) ) : ?>
	<link rel="canonical" href="<?php echo esc_url( $canonical ); ?>" />
<?php endif; ?>
<?php if ( ! empty( $links['prev'] ) ) : ?>
	<link rel="prev" href="<?php echo esc_url( $links['prev'] ); ?>" />
<?php endif; ?>
<?php if ( ! empty( $links['next'] ) ) : ?>
	<link rel="next" href="<?php echo esc_url( $links['next'] ); ?>" />
<?php endif; ?>
<?php // Add our generator output. ?>
	<meta name="generator" content="<?php echo trim( sprintf( '%1$s (%2$s) %3$s', esc_html( AIOSEO_PLUGIN_NAME ), esc_html( AIOSEO_PLUGIN_SHORT_NAME ), aioseo()->helpers->getAioseoVersion() ) ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped, Generic.Files.LineLength.MaxExceeded ?>" />
<?php

// This adds the miscellaneous verification to the head tag inside our comments.
// @TODO: [V4+] Maybe move this out of meta? Better idea would be to have a global wp_head where meta gets
// attached as well as other things like this:
$miscellaneous = aioseo()->helpers->decodeHtmlEntities( aioseo()->options->webmasterTools->miscellaneousVerification );
$miscellaneous = trim( $miscellaneous );
if ( ! empty( $miscellaneous ) ) {
	echo "\n\t\t$miscellaneous\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}Common/Views/main/schema.php000064400000001254151536241210011770 0ustar00<?php
/**
 * This is the output for structured data/schema on the page.
 *
 * @since 4.0.0
 */

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

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
// phpcs:disable Generic.WhiteSpace.ScopeIndent.Incorrect
// phpcs:disable Generic.WhiteSpace.ScopeIndent.IncorrectExact
// phpcs:disable Generic.Files.EndFileNoNewline.Found

$schema = aioseo()->schema->get();
?>
<?php if ( ! empty( $schema ) ) : ?>
		<script type="application/ld+json" class="aioseo-schema">
			<?php echo $schema . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
		</script>
<?php
endif;
Common/Views/main/social.php000064400000001563151536241210012005 0ustar00<?php
/**
 * This is the output for social meta on the page.
 *
 * @since 4.0.0
 */

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

// phpcs:disable Generic.WhiteSpace.ScopeIndent

// Set context for meta class to social meta.
$facebookMeta = aioseo()->social->output->getFacebookMeta();
foreach ( $facebookMeta as $key => $meta ) :
	// Each article tag needs to be output in a separate meta tag so we cast and loop over each key.
	if ( ! is_array( $meta ) ) {
		$meta = [ $meta ];
	}
	foreach ( $meta as $m ) :
	?>
		<meta property="<?php echo esc_attr( $key ); ?>" content="<?php echo esc_attr( $m ); ?>" />
<?php
	endforeach;
endforeach;

$twitterMeta = aioseo()->social->output->getTwitterMeta();
foreach ( $twitterMeta as $key => $meta ) :
?>
		<meta name="<?php echo esc_attr( $key ); ?>" content="<?php echo esc_attr( $meta ); ?>" />
<?php
endforeach;Common/Views/parts/loader.php000064400000002454151536241210012206 0ustar00<?php
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<style>
.aioseo-loading-spinner {
	width: 35px;
	height: 35px;
	position: absolute;
}

.aioseo-loading-spinner .double-bounce1,
.aioseo-loading-spinner .double-bounce2 {
	width: 100%;
	height: 100%;
	border-radius: 50%;
	background-color: #fff;
	opacity: 0.6;
	position: absolute;
	top: 0;
	left: 0;

	-webkit-animation: aioseo-sk-bounce 1.3s infinite ease-in-out;
	animation: aioseo-sk-bounce 1.3s infinite ease-in-out;
}

.aioseo-loading-spinner.dark .double-bounce1,
.aioseo-loading-spinner.dark .double-bounce2 {
	background-color: #8C8F9A;
}

.aioseo-loading-spinner .double-bounce2 {
	-webkit-animation-delay: -0.65s;
	animation-delay: -0.65s;
}

.aioseo-loading-spinner {}
.aioseo-loading-spinner {}

@-webkit-keyframes aioseo-sk-bounce {
	0%, 100% { -webkit-transform: scale(0.0) }
	50% { -webkit-transform: scale(1.0) }
}

@keyframes aioseo-sk-bounce {
	0%, 100% {
		transform: scale(0.0);
		-webkit-transform: scale(0.0);
	} 50% {
		transform: scale(1.0);
		-webkit-transform: scale(1.0);
	}
}
</style>
<div style="height:50px; position:relative;">
	<div class="aioseo-loading-spinner dark" style="top:calc( 50% - 17px);left:calc( 50% - 17px);">
		<div class="double-bounce1"></div>
		<div class="double-bounce2"></div>
	</div>
</div>Common/Views/report/summary.php000064400000164475151536241210012633 0ustar00<?php
/**
 * Summary report view.
 *
 * @since 4.7.2
 */

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

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
// phpcs:disable Generic.Files.LineLength.MaxExceeded
?>
<div style="background-color: #f3f4f5; color: #141b38; font-family: Helvetica, Roboto, Arial, sans-serif; font-size: 14px; line-height: 22px; margin: 0; padding: 0;">
	<span style="display: none !important; visibility: hidden; opacity: 0; height: 0; width: 0;"><?php echo $preHeader ?? '' ?></span>

	<div style="margin: 0 auto; padding: 70px 0; width: 100%; max-width: 680px;">
		<div style="background-color: #ffffff; border: 1px solid #e8e8eb;">
			<div style="padding-left: 20px; padding-right: 20px; padding-bottom: 20px;">
				<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
					<thead>
					<tr>
						<th style="padding: 0; width: 60%; line-height: 1;"></th>
						<th style="padding: 0; width: 40%; line-height: 1;"></th>
					</tr>
					</thead>

					<tbody>
					<tr>
						<td style="padding: 0;">
							<div style="padding-top: 20px;">
								<img
										style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none;"
										width="100"
										height="20"
										src="https://static.aioseo.io/report/ste/text-logo.jpg"
										alt="<?php echo esc_attr( AIOSEO_PLUGIN_SHORT_NAME ) ?>"
								/>
							</div>
						</td>

						<td style="padding: 0; word-break: break-word;">
							<div style="padding-top: 20px; font-size: 12px; text-align: right; line-height: 15px;"><?php echo $dateRange['range'] ?? ''; ?></div>
						</td>
					</tr>

					<tr>
						<td style="padding: 0; word-break: break-word;">
							<div style="padding-top: 10px;">
								<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;"><?php echo $heading ?? ''; ?></p>
							</div>
						</td>

						<td style="padding: 0; word-break: break-word;">
							<div style="padding-top: 10px; font-size: 12px; text-align: right; line-height: 15px;">
								<a
										href="<?php echo site_url(); ?>"
										style="color: #005ae0; font-weight: normal; text-decoration: none;"
								><?php echo site_url(); ?></a>
							</div>
						</td>
					</tr>
					</tbody>
				</table>
			</div>

			<div style="background-color: #004f9d; padding-bottom: 20px;">
				<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
					<thead>
					<tr>
						<th style="padding: 0; width: 70%; line-height: 1;"></th>
						<th style="padding: 0; width: 30%; line-height: 1;"></th>
					</tr>
					</thead>

					<tbody>
					<tr>
						<td style="padding: 0; word-break: break-word;">
							<div style="padding-right: 20px; padding-left: 20px; padding-top: 20px; line-height: 1;">
								<span style="color: #ffffff; margin-right: 3px; font-weight: 700; font-size: 28px; vertical-align: middle;"><?php esc_html_e( 'Hi there!', 'all-in-one-seo-pack' ); ?></span>

								<img
										style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
										width="28"
										height="28"
										src="https://static.aioseo.io/report/ste/emoji-1f44b.png"
										alt="Waving Hand Sign"
								/>
							</div>

							<div style="color: #ffffff; padding-right: 20px; padding-left: 20px; padding-top: 20px; font-size: 20px; line-height: 26px;  font-weight: 400;">
								<?php echo $subheading ?? ''; ?>
							</div>
						</td>

						<td style="padding: 0; text-align: right; word-break: break-word;">
							<img
									style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; padding-top: 20px;"
									width="142"
									height="140"
									src="<?php echo esc_attr( 'https://static.aioseo.io/report/ste/' . ( $iconCalendar ?? '' ) . '.png' ) ?>"
									alt=""
							/>
						</td>
					</tr>
					</tbody>
				</table>
			</div>
		</div>

		<?php if ( aioseo()->siteHealth->shouldUpdate() ) { ?>
			<div style="margin-top: 20px;">
				<div style="text-align: center; font-size: 14px; border-radius: 4px; margin: 0; padding: 8px 12px; background-color: #fffbeb; border: 1px solid #f18200;">
					<?php
					printf(
						// Translators: 1 - The plugin short name ("AIOSEO"), 2 - Opening link tag, 3 - HTML arrow, 4 - Closing link tag.
						__( 'An update is available for %1$s. %2$sUpgrade to the latest version%3$s%4$s', 'all-in-one-seo-pack' ),
						AIOSEO_PLUGIN_SHORT_NAME,
						'<a href="' . ( $links['update'] ?? '#' ) . '" style="color: #005ae0; font-weight: normal; text-decoration: underline;">',
						'&nbsp;&rarr;',
						'</a>'
					)
					?>
				</div>
			</div>
		<?php } ?>

		<div style="background-color: #ffffff; border: 1px solid #e8e8eb; margin-top: 20px;">
			<div style="border-bottom: 1px solid #e5e5e5; padding: 15px 20px;">
				<img
						style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-right: 6px;"
						width="35"
						height="35"
						src="https://static.aioseo.io/report/ste/icon-report.png"
						alt=""
				/>

				<h2 style="font-size: 20px; font-weight: 700; line-height: 24px; margin-bottom: 0; margin-top: 0; vertical-align: middle; display: inline-block;"><?php esc_html_e( 'SEO Report', 'all-in-one-seo-pack' ); ?></h2>
			</div>

			<div style="padding: 20px;">
				<?php if ( ! empty( $statisticsReport['posts']['winning'] ) ) { ?>
					<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
						<tbody>
						<tr>
							<td style="padding: 0; word-break: break-word;">
								<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;"><?php esc_html_e( 'Top Winning Posts', 'all-in-one-seo-pack' ); ?></p>
							</td>

							<td style="text-align: right; word-break: break-word;">
								<?php if ( ! empty( $statisticsReport['posts']['winning']['url'] ) ) { ?>
									<a
											href="<?php echo esc_attr( $statisticsReport['posts']['winning']['url'] ) ?>"
											style="color: #005ae0; font-weight: 700; text-decoration: underline;"
									>
										<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
									</a>
								<?php } ?>
							</td>
						</tr>
						</tbody>
					</table>

					<div style="margin-top: 16px; overflow-x: auto;">
						<table style="min-width: 460px; table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
							<thead>
							<tr style="border-color: #ffffff; border-bottom-width: 6px; border-bottom-style: solid;">
								<th style="width: 59%; background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; border-top-left-radius: 2px; border-bottom-left-radius: 2px; line-height: 1;">
									<?php esc_html_e( 'Post', 'all-in-one-seo-pack' ); ?>
								</th>

								<th style="width: <?php echo $statisticsReport['posts']['winning']['show_tru_seo'] ? '17%' : '0' ?>; text-align: center; background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
									<?php if ( $statisticsReport['posts']['winning']['show_tru_seo'] ) { ?>
										TruSEO
									<?php } ?>
								</th>

								<th style="width: 12%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960;  line-height: 1;">
									<?php esc_html_e( 'Clicks', 'all-in-one-seo-pack' ); ?>
								</th>

								<th style="width: 12%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; border-top-right-radius: 2px; border-bottom-right-radius: 2px; line-height: 1;">
									<?php esc_html_e( 'Diff', 'all-in-one-seo-pack' ); ?>
								</th>
							</tr>
							</thead>

							<tbody>
							<?php foreach ( ( $statisticsReport['posts']['winning']['items'] ?? [] ) as $i => $item ) { ?>
								<tr style="<?php echo 0 === $i % 2 ? 'background-color: #f3f4f5;' : '' ?>">
									<td style="padding: 0; word-break: break-word;">
										<div style="padding: 6px; font-size: 14px;">
											<a style="color: #141b38; font-weight: normal; text-decoration: none;"<?php echo ! empty( $item['url'] ) ? ' href="' . esc_attr( $item['url'] ) . '"' : '' ?>>
												<?php echo $item['title']; ?>
											</a>
										</div>
									</td>

									<td style="padding: 0; word-break: break-word;">
										<?php if ( ! empty( $item['tru_seo'] ) ) { ?>
											<div style="padding: 6px;">
												<div style="width: 45px; padding: 6px; margin-left: auto; margin-right: auto; font-size: 12px; text-align: center; line-height: 1; border-radius: 4px; border-width: 1px; border-style: solid; <?php echo "color: {$item['tru_seo']['color']}"; ?>">
													<?php echo $item['tru_seo']['text']; ?>
												</div>
											</div>
										<?php } ?>
									</td>

									<td style="padding: 0; word-break: break-word;">
										<div style="padding: 6px; font-size: 14px;">
											<?php echo $item['clicks']; ?>
										</div>
									</td>

									<td style="padding: 0; word-break: break-word;">
										<div style="padding: 6px; font-size: 0; <?php echo "color: {$item['difference']['clicks']['color']}"; ?>">
											<?php if ( '#00aa63' === $item['difference']['clicks']['color'] ) { ?>
												<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-bottom-color: #00aa63; border-bottom-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-top-width: 0;"></div>
											<?php } ?>

											<?php if ( '#df2a4a' === $item['difference']['clicks']['color'] ) { ?>
												<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-top-color: #df2a4a; border-top-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-bottom-width: 0;"></div>
											<?php } ?>

											<span style="display: inline-block; vertical-align: middle; font-size: 14px;"><?php echo $item['difference']['clicks']['text']; ?></span>
										</div>
									</td>
								</tr>
							<?php } ?>
							</tbody>
						</table>
					</div>
				<?php } ?>

				<?php if ( ! empty( $statisticsReport['posts']['losing'] ) ) { ?>
					<?php if ( ! empty( $statisticsReport['posts']['winning'] ) ) { ?>
						<div style="margin-top: 20px; margin-bottom: 20px; border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
					<?php } ?>

					<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
						<tbody>
						<tr>
							<td style="padding: 0; word-break: break-word;">
								<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;"><?php esc_html_e( 'Top Losing Posts', 'all-in-one-seo-pack' ); ?></p>
							</td>

							<td style="text-align: right; word-break: break-word;">
								<?php if ( ! empty( $statisticsReport['posts']['losing']['url'] ) ) { ?>
									<a
											href="<?php echo esc_attr( $statisticsReport['posts']['losing']['url'] ) ?>"
											style="color: #005ae0; font-weight: 700; text-decoration: underline;"
									>
										<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
									</a>
								<?php } ?>
							</td>
						</tr>
						</tbody>
					</table>

					<div style="margin-top: 16px; overflow-x: auto;">
						<table style="table-layout: fixed; min-width: 460px; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
							<thead>
							<tr style="border-color: #ffffff; border-bottom-width: 6px; border-bottom-style: solid;">
								<th style="width: 59%; background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; border-top-left-radius: 2px; border-bottom-left-radius: 2px; line-height: 1;">
									<?php esc_html_e( 'Post', 'all-in-one-seo-pack' ); ?>
								</th>

								<th style="width: <?php echo $statisticsReport['posts']['losing']['show_tru_seo'] ? '17%' : '0' ?>; text-align: center; background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
									<?php if ( $statisticsReport['posts']['losing']['show_tru_seo'] ) { ?>
										TruSEO
									<?php } ?>
								</th>

								<th style="width: 12%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
									<?php esc_html_e( 'Clicks', 'all-in-one-seo-pack' ); ?>
								</th>

								<th style="width: 12%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; border-top-right-radius: 2px; border-bottom-right-radius: 2px; line-height: 1;">
									<?php esc_html_e( 'Diff', 'all-in-one-seo-pack' ); ?>
								</th>
							</tr>
							</thead>

							<tbody>
							<?php foreach ( ( $statisticsReport['posts']['losing']['items'] ?? [] ) as $i => $item ) { ?>
								<tr style="<?php echo 0 === $i % 2 ? 'background-color: #f3f4f5;' : '' ?>">
									<td style="padding: 0; word-break: break-word;">
										<div style="padding: 6px; font-size: 14px;">
											<a style="color: #141b38; font-weight: normal; text-decoration: none;"<?php echo ! empty( $item['url'] ) ? ' href="' . esc_attr( $item['url'] ) . '"' : '' ?>>
												<?php echo $item['title']; ?>
											</a>
										</div>
									</td>

									<td style="padding: 0; word-break: break-word;">
										<?php if ( ! empty( $item['tru_seo'] ) ) { ?>
											<div style="padding: 6px;">
												<div style="width: 45px; padding: 6px; margin-left: auto; margin-right: auto; font-size: 12px; text-align: center; line-height: 1; border-radius: 4px; border-width: 1px; border-style: solid; <?php echo "color: {$item['tru_seo']['color']}"; ?>">
													<?php echo $item['tru_seo']['text']; ?>
												</div>
											</div>
										<?php } ?>
									</td>

									<td style="padding: 0; word-break: break-word;">
										<div style="padding: 6px; font-size: 14px;">
											<?php echo $item['clicks']; ?>
										</div>
									</td>

									<td style="padding: 0; word-break: break-word;">
										<div style="padding: 6px; font-size: 0; <?php echo "color: {$item['difference']['clicks']['color']}"; ?>">
											<?php if ( '#00aa63' === $item['difference']['clicks']['color'] ) { ?>
												<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-bottom-color: #00aa63; border-bottom-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-top-width: 0;"></div>
											<?php } ?>

											<?php if ( '#df2a4a' === $item['difference']['clicks']['color'] ) { ?>
												<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-top-color: #df2a4a; border-top-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-bottom-width: 0;"></div>
											<?php } ?>

											<span style="display: inline-block; vertical-align: middle; font-size: 14px;"><?php echo $item['difference']['clicks']['text']; ?></span>
										</div>
									</td>
								</tr>
							<?php } ?>
							</tbody>
						</table>
					</div>
				<?php } ?>

				<?php if ( ! empty( $statisticsReport['keywords']['winning'] ) || ! empty( $statisticsReport['keywords']['losing'] ) ) { ?>
					<?php if ( ! empty( $statisticsReport['posts']['winning'] ) || ! empty( $statisticsReport['posts']['losing'] ) ) { ?>
						<div style="margin-top: 20px; margin-bottom: 20px; border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
					<?php } ?>
					<div style="overflow-x: auto;">
						<table style="min-width: 600px; table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
							<thead>
							<tr>
								<th style="width: 47%; line-height: 1;"></th>
								<th style="width: 6%; line-height: 1;"></th>
								<th style="width: 47%; line-height: 1;"></th>
							</tr>
							</thead>

							<tbody>
							<tr style="height: 1px;">
								<td style="vertical-align: top; word-break: break-word;">
									<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
										<tbody>
										<tr>
											<td style="padding: 0; word-break: break-word;">
												<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;">
													<?php esc_html_e( 'Top Winning Keywords', 'all-in-one-seo-pack' ); ?>
												</p>
											</td>

											<td style="text-align: right; word-break: break-word;">
												<?php if ( ! empty( $statisticsReport['keywords']['winning']['url'] ) ) { ?>
													<a
															href="<?php echo esc_attr( $statisticsReport['keywords']['winning']['url'] ) ?>"
															style="color: #005ae0; font-weight: 700; text-decoration: underline;"
													>
														<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
													</a>
												<?php } ?>
											</td>
										</tr>
										</tbody>
									</table>

									<table style="margin-top: 16px; table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
										<thead>
										<tr style="border-color: #ffffff; border-bottom-width: 6px; border-bottom-style: solid;">
											<th style="width: 64%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; border-top-left-radius: 2px; border-bottom-left-radius: 2px; line-height: 1;">
												<?php esc_html_e( 'Keyword', 'all-in-one-seo-pack' ); ?>
											</th>
											<th style="width: 16%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
												<?php esc_html_e( 'Clicks', 'all-in-one-seo-pack' ); ?>
											</th>
											<th style="width: 20%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; border-top-right-radius: 2px; border-bottom-right-radius: 2px; line-height: 1;">
												<?php esc_html_e( 'Diff', 'all-in-one-seo-pack' ); ?>
											</th>
										</tr>
										</thead>

										<tbody>
										<?php foreach ( ( $statisticsReport['keywords']['winning']['items'] ?? [] ) as $i => $item ) { ?>
											<tr style="<?php echo 0 === $i % 2 ? 'background-color: #f3f4f5;' : '' ?>">
												<td style="padding: 0; word-break: break-word;">
													<div style="padding: 6px; font-size: 14px;">
														<?php echo $item['title']; ?>
													</div>
												</td>

												<td style="padding: 0; word-break: break-word;">
													<div style="padding: 6px; font-size: 14px;">
														<?php echo $item['clicks']; ?>
													</div>
												</td>

												<td style="padding: 0; word-break: break-word;">
													<div style="padding: 6px; font-size: 0; <?php echo "color: {$item['difference']['clicks']['color']}"; ?>">
														<?php if ( '#00aa63' === $item['difference']['clicks']['color'] ) { ?>
															<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-bottom-color: #00aa63; border-bottom-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-top-width: 0;"></div>
														<?php } ?>

														<?php if ( '#df2a4a' === $item['difference']['clicks']['color'] ) { ?>
															<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-top-color: #df2a4a; border-top-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-bottom-width: 0;"></div>
														<?php } ?>

														<span style="display: inline-block; vertical-align: middle; font-size: 14px;"><?php echo $item['difference']['clicks']['text']; ?></span>
													</div>
												</td>
											</tr>
										<?php } ?>
										</tbody>
									</table>
								</td>

								<td style="height: inherit; padding: 0; text-align: center; vertical-align: baseline; overflow: hidden; word-break: break-word;">
									<div style="width: 1px; margin-left: auto; margin-right: auto; background-color: #e5e5e5; height: 100%;"></div>
								</td>

								<td style="vertical-align: top; word-break: break-word;">
									<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
										<tbody>
										<tr>
											<td style="padding: 0; word-break: break-word;">
												<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;">
													<?php esc_html_e( 'Top Losing Keywords', 'all-in-one-seo-pack' ); ?>
												</p>
											</td>

											<td style="text-align: right; word-break: break-word;">
												<?php if ( ! empty( $statisticsReport['keywords']['losing']['url'] ) ) { ?>
													<a
															href="<?php echo esc_attr( $statisticsReport['keywords']['losing']['url'] ) ?>"
															style="color: #005ae0; font-weight: 700; text-decoration: underline;"
													>
														<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
													</a>
												<?php } ?>
											</td>
										</tr>
										</tbody>
									</table>

									<table style="margin-top: 16px; table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
										<thead>
										<tr style="border-color: #ffffff; border-bottom-width: 6px; border-bottom-style: solid;">
											<th style="width: 64%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; border-top-left-radius: 2px; border-bottom-left-radius: 2px; line-height: 1;">
												<?php esc_html_e( 'Keyword', 'all-in-one-seo-pack' ); ?>
											</th>
											<th style="width: 16%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
												<?php esc_html_e( 'Clicks', 'all-in-one-seo-pack' ); ?>
											</th>
											<th style="width: 20%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; border-top-right-radius: 2px; border-bottom-right-radius: 2px; line-height: 1;">
												<?php esc_html_e( 'Diff', 'all-in-one-seo-pack' ); ?>
											</th>
										</tr>
										</thead>

										<tbody>
										<?php foreach ( ( $statisticsReport['keywords']['losing']['items'] ?? [] ) as $i => $item ) { ?>
											<tr style="<?php echo 0 === $i % 2 ? 'background-color: #f3f4f5;' : '' ?>">
												<td style="padding: 0; word-break: break-word;">
													<div style="padding: 6px; font-size: 14px;">
														<?php echo $item['title']; ?>
													</div>
												</td>

												<td style="padding: 0; word-break: break-word;">
													<div style="padding: 6px; font-size: 14px;">
														<?php echo $item['clicks']; ?>
													</div>
												</td>

												<td style="padding: 0; word-break: break-word;">
													<div style="padding: 6px; font-size: 0; <?php echo "color: {$item['difference']['clicks']['color']}"; ?>">
														<?php if ( '#00aa63' === $item['difference']['clicks']['color'] ) { ?>
															<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-bottom-color: #00aa63; border-bottom-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-top-width: 0;"></div>
														<?php } ?>

														<?php if ( '#df2a4a' === $item['difference']['clicks']['color'] ) { ?>
															<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-top-color: #df2a4a; border-top-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-bottom-width: 0;"></div>
														<?php } ?>

														<span style="display: inline-block; vertical-align: middle; font-size: 14px;"><?php echo $item['difference']['clicks']['text']; ?></span>
													</div>
												</td>
											</tr>
										<?php } ?>
										</tbody>
									</table>
								</td>
							</tr>
							</tbody>
						</table>
					</div>
				<?php } ?>

				<?php if ( ! empty( $upsell['search-statistics'] ) ) { ?>
					<div>
						<img
								style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
								width="638"
								height="146"
								src="https://static.aioseo.io/report/ste/banner-search-statistics-cta-upsell.jpg"
								alt=""
						/>

						<p style="font-size: 16px; margin-bottom: 0; margin-top: 20px; text-align: center;">
							<?php esc_html_e( 'Connect your site to Google Search Console to receive insights on how content is being discovered. Identify areas for improvement and drive traffic to your website.', 'all-in-one-seo-pack' ); ?>
						</p>

						<div style="width: 475px; max-width: 96%; margin-top: 20px; margin-left: auto; margin-right: auto;">
							<div style="width: 210px; padding: 6px; display: inline-block; vertical-align: middle;">
								<img
										style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-right: 3px;"
										width="17"
										height="17"
										src="https://static.aioseo.io/report/ste/icon-check-circle-out.png"
										alt="&#10003;"
								/>

								<span style="display: inline-block; vertical-align: middle; line-height: 20px; max-width: 185px;"><?php esc_html_e( 'Search traffic insights', 'all-in-one-seo-pack' ); ?></span>
							</div>

							<div style="width: 210px; padding: 6px; display: inline-block; vertical-align: middle;">
								<img
										style="margin-right: 3px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
										width="17"
										height="17"
										src="https://static.aioseo.io/report/ste/icon-check-circle-out.png"
										alt="&#10003;"
								/>

								<span style="display: inline-block; vertical-align: middle; line-height: 20px; max-width: 185px;"><?php esc_html_e( 'Track page rankings', 'all-in-one-seo-pack' ); ?></span>
							</div>

							<div style="width: 210px; padding: 6px; display: inline-block; vertical-align: middle;">
								<img
										style="margin-right: 3px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
										width="17"
										height="17"
										src="https://static.aioseo.io/report/ste/icon-check-circle-out.png"
										alt="&#10003;"
								/>

								<span style="display: inline-block; vertical-align: middle; line-height: 20px; max-width: 185px;"><?php esc_html_e( 'Track keyword rankings', 'all-in-one-seo-pack' ); ?></span>
							</div>

							<div style="width: 210px; padding: 6px; display: inline-block; vertical-align: middle;">
								<img
										style="margin-right: 3px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
										width="17"
										height="17"
										src="https://static.aioseo.io/report/ste/icon-check-circle-out.png"
										alt="&#10003;"
								/>

								<span style="display: inline-block; vertical-align: middle; line-height: 20px; max-width: 185px;"><?php esc_html_e( 'Speed tests for individual pages/posts', 'all-in-one-seo-pack' ); ?></span>
							</div>
						</div>

						<div style="margin-top: 20px; text-align: center;">
							<a
									href="<?php echo esc_attr( $upsell['search-statistics']['cta']['url'] ) ?>"
									style="border-radius: 4px; border: none; display: inline-block; font-size: 14px; font-style: normal; font-weight: 700; text-align: center; text-decoration: none; user-select: none; vertical-align: middle; background-color: #00aa63; color: #ffffff; padding: 8px 20px;"
							>
								<?php echo $upsell['search-statistics']['cta']['text']; ?>
							</a>
						</div>
					</div>
				<?php } ?>

				<?php if ( empty( $upsell['search-statistics'] ) && ! empty( $statisticsReport['cta'] ) ) { ?>
					<div style="margin-top: 20px; margin-bottom: 20px; border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>

					<div style="text-align: center;">
						<a
								href="<?php echo esc_attr( $statisticsReport['cta']['url'] ) ?>"
								style="border-radius: 4px; border: none; display: inline-block; font-size: 14px; font-style: normal; font-weight: 700; text-align: center; text-decoration: none; user-select: none; vertical-align: middle; background-color: #005ae0; color: #ffffff; padding: 8px 20px;"
						>
							<?php echo $statisticsReport['cta']['text']; ?>
						</a>
					</div>
				<?php } ?>
			</div>
		</div>

		<?php if ( ! empty( $posts ) ) { ?>
			<div style="background-color: #ffffff; border: 1px solid #e8e8eb; margin-top: 20px;">
				<div style="border-bottom: 1px solid #e5e5e5; padding: 15px 20px;">
					<img
							style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-right: 6px;"
							width="35"
							height="35"
							src="https://static.aioseo.io/report/ste/icon-summary.png"
							alt=""
					/>

					<h2 style="font-size: 20px; font-weight: 700; line-height: 24px; margin-bottom: 0; margin-top: 0; vertical-align: middle; display: inline-block;"><?php esc_html_e( 'Content Summary', 'all-in-one-seo-pack' ); ?></h2>
				</div>

				<div style="padding: 20px;">
					<?php if ( ! empty( $posts['publish']['items'] ) ) { ?>
						<div>
							<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
								<tbody>
								<tr>
									<td style="padding: 0; word-break: break-word;">
										<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;">
											<?php esc_html_e( 'Most Recent Published', 'all-in-one-seo-pack' ); ?>
										</p>
									</td>

									<td style="text-align: right; word-break: break-word;">
										<a
												href="<?php echo esc_attr( $posts['publish']['url'] ) ?>"
												style="color: #005ae0; font-weight: 700; text-decoration: underline;"
										>
											<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
										</a>
									</td>
								</tr>
								</tbody>
							</table>

							<div style="margin-top: 16px; overflow-x: auto;">
								<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
									<thead>
									<tr>
										<th style="background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; border-top-left-radius: 2px; border-bottom-left-radius: 2px; width: 210px; line-height: 1;">
											<?php esc_html_e( 'Post', 'all-in-one-seo-pack' ); ?>
										</th>

										<th style="text-align: center; background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1; width: <?php echo $posts['publish']['show_tru_seo'] ? '115px' : '0' ?>">
											<?php if ( $posts['publish']['show_tru_seo'] ) { ?>
												TruSEO
											<?php } ?>
										</th>

										<th style="background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; border-top-right-radius: 2px; border-bottom-right-radius: 2px; line-height: 1; width: <?php echo $posts['publish']['show_stats'] ? '135px' : '0' ?>">
											<?php if ( $posts['publish']['show_stats'] ) { ?>
												<?php esc_html_e( 'Stats', 'all-in-one-seo-pack' ); ?>
											<?php } ?>
										</th>
									</tr>
									</thead>

									<tbody>
									<?php foreach ( $posts['publish']['items'] as $i => $item ) { ?>
										<?php if ( $i > 0 ) { ?>
											<tr>
												<td
														colspan="3"
														style="padding: 0; word-break: break-word;"
												>
													<div style="border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
												</td>
											</tr>
										<?php } ?>
										<tr>
											<td style="padding: 12px; word-break: break-word;">
												<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
													<thead>
													<tr>
														<th style="width: 35%; padding: 0; line-height: 1;"></th>
														<th style="width: 65%; padding: 0; line-height: 1;"></th>
													</tr>
													</thead>

													<tbody>
													<tr>
														<td style="padding: 0; word-break: break-word;">
															<div style="width: 100%; height: 65px; position: relative; overflow: hidden;">
																<a
																		style="color: #005ae0; font-weight: normal; text-decoration: underline;"
																		href="<?php echo esc_attr( $item['url'] ?: '#' ) ?>"
																>
																	<img
																			src="<?php echo esc_attr( $item['image_url'] ); ?>"
																			alt="<?php echo esc_attr( aioseo()->helpers->stripEmoji( $item['title'] ) ); ?>"
																			style="box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; position: absolute; width: 100%; height: 100%; top: 0; right: 0; bottom: 0; left: 0; object-position: center; object-fit: cover; border-width: 1px; border-style: solid; border-color: #e5e5e5;"
																	/>
																</a>
															</div>
														</td>

														<td style="padding: 0; word-break: break-word;">
															<div style="padding: 6px; display: inline-block; vertical-align: middle; font-size: 14px;">
																<a
																		style="color: #141b38; font-weight: normal; text-decoration: none;"
																		href="<?php echo esc_attr( $item['url'] ?: '#' ) ?>"
																>
																	<?php echo $item['title']; ?>
																</a>
															</div>
														</td>
													</tr>
													</tbody>
												</table>
											</td>

											<td style="padding: 0; word-break: break-word;">
												<?php if ( ! empty( $item['tru_seo'] ) ) { ?>
													<div style="padding: 12px;">
														<div style="width: 45px; padding: 6px; margin-left: auto; margin-right: auto; font-size: 12px; text-align: center; line-height: 1; border-radius: 4px; border-width: 1px; border-style: solid; <?php echo "color: {$item['tru_seo']['color']}"; ?>">
															<?php echo $item['tru_seo']['text']; ?>
														</div>
													</div>
												<?php } ?>
											</td>

											<td style="padding: 0; word-break: break-word;">
												<?php if ( ! empty( $item['stats'] ) ) { ?>
													<div style="padding: 12px; font-size: 14px;">
														<?php foreach ( $item['stats'] as $k => $stat ) { ?>
															<div style="line-height: 1;<?php echo $k > 0 ? ' margin-top: 6px;' : '' ?>">
																<img
																		style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
																		width="19"
																		height="19"
																		src="<?php echo esc_attr( 'https://static.aioseo.io/report/ste/icon-' . $stat['icon'] . '.png' ) ?>"
																		alt=""
																/>

																<span style="vertical-align: middle;"><?php echo $stat['label']; ?>:</span>

																<span style="vertical-align: middle; font-weight: 700;"><?php echo $stat['value']; ?></span>
															</div>
														<?php } ?>
													</div>
												<?php } ?>
											</td>
										</tr>
									<?php } ?>
									</tbody>
								</table>
							</div>
						</div>
					<?php } ?>

					<?php if ( ! empty( $posts['optimize']['items'] ) ) { ?>
						<?php if ( ! empty( $posts['publish']['items'] ) ) { ?>
							<div style="margin-top: 30px; margin-bottom: 30px; border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
						<?php } ?>
						<div>
							<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
								<tbody>
								<tr>
									<td style="padding: 0; word-break: break-word;">
										<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;">
											<?php esc_html_e( 'Posts to Optimize', 'all-in-one-seo-pack' ); ?>
										</p>
									</td>

									<td style="text-align: right; word-break: break-word;">
										<a
												href="<?php echo esc_attr( $posts['optimize']['url'] ) ?>"
												style="color: #005ae0; font-weight: 700; text-decoration: underline;"
										>
											<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
										</a>
									</td>
								</tr>
								</tbody>
							</table>

							<div style="margin-top: 16px; overflow-x: auto;">
								<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
									<thead>
									<tr>
										<th style="width: 319px; background-color: #f0f6ff; padding: 0; font-size: 12px; font-weight: 400; color: #434960; border-top-left-radius: 2px; border-bottom-left-radius: 2px; line-height: 1;">
											<div style="padding: 12px;"><?php esc_html_e( 'Post', 'all-in-one-seo-pack' ); ?></div>
										</th>

										<th style="width: <?php echo $posts['optimize']['show_tru_seo'] ? '159px' : '0' ?>; text-align: center; background-color: #f0f6ff; padding: 0; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
											<?php if ( $posts['optimize']['show_tru_seo'] ) { ?>
												<div style="padding: 12px;">TruSEO</div>
											<?php } ?>
										</th>

										<th style="width: 159px; text-align: center; background-color: #f0f6ff; padding: 0; font-size: 12px; font-weight: 400; color: #434960; border-top-right-radius: 2px; border-bottom-right-radius: 2px; line-height: 1;">
											<div style="padding: 12px;"><?php esc_html_e( 'Content Drop', 'all-in-one-seo-pack' ); ?></div>
										</th>
									</tr>
									</thead>

									<tbody>
									<?php foreach ( $posts['optimize']['items'] as $item ) { ?>
										<tr>
											<td
													colspan="3"
													style="padding-bottom: 8px; padding-top: 8px; word-break: break-word;"
											></td>
										</tr>

										<tr style="border-width: 1px; border-style: solid; border-color: #e5e5e5;">
											<td
													colspan="3"
													style="padding: 12px; word-break: break-word;"
											>
												<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
													<thead>
													<tr>
														<th style="width: 311px; padding: 0; line-height: 1;"></th>
														<th style="width: <?php echo $posts['optimize']['show_tru_seo'] ? '151px' : '0' ?>; padding: 0; line-height: 1;"></th>
														<th style="width: 151px; padding: 0; line-height: 1;"></th>
													</tr>
													</thead>

													<tbody>
													<tr>
														<td style="padding: 0; background-color: #f3f4f5; word-break: break-word;">
															<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
																<thead>
																<tr>
																	<th style="width: 35%; padding: 0; line-height: 1;"></th>
																	<th style="width: 65%; padding: 0; line-height: 1;"></th>
																</tr>
																</thead>

																<tbody>
																<tr>
																	<td style="padding: 0; word-break: break-word;">
																		<div style="width: 100%; height: 65px; position: relative; overflow: hidden;">
																			<a style="color: #005ae0; font-weight: normal; text-decoration: underline;"<?php echo ! empty( $item['url'] ) ? ' href="' . esc_attr( $item['url'] ) . '"' : '' ?>>
																				<img
																						src="<?php echo esc_attr( $item['image_url'] ); ?>"
																						alt="<?php echo esc_attr( aioseo()->helpers->stripEmoji( $item['title'] ) ); ?>"
																						style="box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; position: absolute; width: 100%; height: 100%; top: 0; right: 0; bottom: 0; left: 0; object-position: center; object-fit: cover; border-width: 1px; border-style: solid; border-color: #e5e5e5;"
																				/>
																			</a>
																		</div>
																	</td>

																	<td style="padding: 0; word-break: break-word;">
																		<div style="padding: 6px; display: inline-block; vertical-align: middle; font-size: 14px;">
																			<a style="color: #141b38; font-weight: normal; text-decoration: none;"<?php echo ! empty( $item['url'] ) ? ' href="' . esc_attr( $item['url'] ) . '"' : '' ?>>
																				<?php echo $item['title']; ?>
																			</a>
																		</div>
																	</td>
																</tr>
																</tbody>
															</table>
														</td>

														<td style="padding: 0; background-color: #f3f4f5; word-break: break-word;">
															<?php if ( ! empty( $item['tru_seo'] ) ) { ?>
																<div style="width: 45px; padding: 6px; margin-left: auto; margin-right: auto; font-size: 12px; text-align: center; line-height: 1; border-radius: 4px; border-width: 1px; border-style: solid; <?php echo "color: {$item['tru_seo']['color']}"; ?>">
																	<?php echo $item['tru_seo']['text']; ?>
																</div>
															<?php } ?>
														</td>

														<td style="padding: 0; background-color: #f3f4f5; word-break: break-word;">
															<div style="width: 50px; padding: 6px; margin-left: auto; margin-right: auto; font-size: 0; text-align: center; line-height: 1; border-radius: 4px; border-width: 1px; border-style: solid; <?php echo "color: {$item['decay_percent']['color']}"; ?>">
																<?php if ( '#00aa63' === $item['decay_percent']['color'] ) { ?>
																	<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-bottom-color: #00aa63; border-bottom-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-top-width: 0;"></div>
																<?php } ?>

																<?php if ( '#df2a4a' === $item['decay_percent']['color'] ) { ?>
																	<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-top-color: #df2a4a; border-top-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-bottom-width: 0;"></div>
																<?php } ?>

																<span style="display: inline-block; vertical-align: middle; font-size: 12px;"><?php echo $item['decay_percent']['text']; ?></span>
															</div>
														</td>
													</tr>

													<?php if ( ! empty( $item['issues']['items'] ) ) { ?>
														<tr>
															<td
																	colspan="2"
																	style="padding: 10px 0 0; word-break: break-word;"
															>
																<div style="font-size: 14px; font-weight: 700;"><?php esc_html_e( 'Issues', 'all-in-one-seo-pack' ); ?></div>
															</td>

															<td style="padding: 10px 0 0; text-align: right; word-break: break-word;">
																<?php if ( ! empty( $item['issues']['url'] ) ) { ?>
																	<a
																			href="<?php echo esc_attr( $item['issues']['url'] ) ?>"
																			style="color: #005ae0; font-weight: normal; text-decoration: underline; font-size: 12px;"
																	>
																		<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
																	</a>
																<?php } ?>
															</td>
														</tr>

														<tr>
															<td
																	colspan="3"
																	style="padding: 0; word-break: break-word;"
															>
																<?php foreach ( $item['issues']['items'] as $issue ) { ?>
																	<div style="padding-top: 0; padding-left: 0; padding-right: 0; pt-6 font-size: 14px;">
																		<img
																				style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-right: 3px;"
																				width="15"
																				height="15"
																				src="https://static.aioseo.io/report/ste/icon-remove.png"
																				alt="x"
																		/>

																		<span style="width: 94%; vertical-align: middle; display: inline-block;"><?php echo $issue['text'] ?></span>
																	</div>
																<?php } ?>
															</td>
														</tr>
													<?php } ?>
													</tbody>
												</table>
											</td>
										</tr>
									<?php } ?>
									</tbody>
								</table>
							</div>
						</div>
					<?php } ?>

					<?php if ( empty( $posts['publish']['items'] ) && empty( $posts['optimize']['items'] ) ) { ?>
						<div style="font-size: 16px; font-weight: 400; text-align: center;">
							<?php echo esc_html__( 'It seems there is no content yet to be displayed.', 'all-in-one-seo-pack' ) ?>
						</div>
					<?php } ?>

					<div style="margin-top: 20px; text-align: center;">
						<a
								href="<?php echo esc_attr( $posts['cta']['url'] ); ?>"
								style="border-radius: 4px; border: none; display: inline-block; font-size: 14px; font-style: normal; font-weight: 700; text-align: center; text-decoration: none; user-select: none; vertical-align: middle; background-color: #005ae0; color: #ffffff; padding: 8px 20px;"
						>
							<?php echo $posts['cta']['text']; ?>
						</a>
					</div>
				</div>
			</div>
		<?php } ?>

		<?php if ( ! empty( $statisticsReport['milestones'] ) ) { ?>
			<div style="background-color: #ffffff; border: 1px solid #e8e8eb; margin-top: 20px;">
				<div style="border-bottom: 1px solid #e5e5e5; padding: 15px 20px;">
					<img
							style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-right: 6px;"
							width="35"
							height="35"
							src="https://static.aioseo.io/report/ste/icon-flag.png"
							alt=""
					/>

					<h2 style="font-size: 20px; font-weight: 700; line-height: 24px; margin-bottom: 0; margin-top: 0; vertical-align: middle; display: inline-block;"><?php esc_html_e( 'SEO Milestones', 'all-in-one-seo-pack' ); ?></h2>
				</div>

				<div style="padding: 10px; overflow-x: auto;">
					<table style="min-width: 400px; width: 100%;">
						<tbody>
						<?php for ( $i = 0; $i < count( $statisticsReport['milestones'] ) / 2; $i++ ) { ?>
							<tr>
								<?php for ( $j = $i; $j < ( 2 + $i ); $j++ ) { ?>
									<?php $milestone = $statisticsReport['milestones'][ $i + $j ] ?? null ?>
									<?php if ( ! $milestone ) { ?>
										<?php continue; ?>
									<?php } ?>
									<td style="padding: 16px; word-break: break-word; vertical-align: top; border: 10px solid #ffff; text-align: center; border-radius: 4px; background-color: <?php echo $milestone['background'] ?>; color: <?php echo $milestone['color'] ?>; width: <?php echo max( 50, 100 / count( $statisticsReport['milestones'] ) ), '%' ?>">
										<img
												style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-left: auto; margin-right: auto;"
												width="35"
												height="35"
												src="<?php echo 'https://static.aioseo.io/report/ste/', $milestone['icon'], '.png' ?>"
												alt=""
										/>

										<p style="font-size: 16px; margin-bottom: 0; margin-top: 12px;">
											<?php echo $milestone['message']; ?>
										</p>
									</td>
								<?php } ?>
							</tr>
						<?php } ?>
						</tbody>
					</table>

					<?php if ( ! empty( $statisticsReport['cta'] ) ) { ?>
						<div style="margin: 10px 10px 20px; border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>

						<div style="padding-bottom: 10px; text-align: center;">
							<a
									href="<?php echo esc_attr( $statisticsReport['cta']['url'] ) ?>"
									style="border-radius: 4px; border: none; display: inline-block; font-size: 14px; font-style: normal; font-weight: 700; text-align: center; text-decoration: none; user-select: none; vertical-align: middle; background-color: #005ae0; color: #ffffff; padding: 8px 20px;"
							>
								<?php echo $statisticsReport['cta']['text']; ?>
							</a>
						</div>
					<?php } ?>
				</div>
			</div>
		<?php } ?>

		<?php if ( ! empty( $resources['posts'] ) ) { ?>
			<div style="background-color: #ffffff; border: 1px solid #e8e8eb; margin-top: 20px;">
				<div style="border-bottom: 1px solid #e5e5e5; padding: 15px 20px;">
					<img
							style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-right: 6px;"
							width="35"
							height="35"
							src="https://static.aioseo.io/report/ste/icon-star.png"
							alt=""
					/>

					<h2 style="font-size: 20px; font-weight: 700; line-height: 24px; margin-bottom: 0; margin-top: 0; vertical-align: middle; display: inline-block;"><?php esc_html_e( "What's New", 'all-in-one-seo-pack' ); ?></h2>
				</div>

				<div style="padding: 20px;">
					<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
						<tbody>
						<?php foreach ( $resources['posts'] as $k => $post ) { ?>
							<?php if ( $k > 0 ) { ?>
								<tr>
									<td
											colspan="3"
											style="padding-bottom: 8px; padding-top: 8px; word-break: break-word;"
									>
										<div style="border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
									</td>
								</tr>
							<?php } ?>

							<tr>
								<td style="padding: 0; word-break: break-word;">
									<div style="width: 147px; height: 82px; margin-top: 6px; margin-bottom: 6px; margin-right: 6px; display: inline-block; vertical-align: top; position: relative; overflow: hidden;">
										<a
												style="color: #005ae0; font-weight: normal; text-decoration: underline;"
												href="<?php echo esc_attr( $post['url'] ?: '#' ); ?>"
										>
											<img
													src="<?php echo esc_attr( $post['image']['url'] ) ?>"
													alt="<?php echo esc_attr( aioseo()->helpers->stripEmoji( $post['title'] ) ) ?>"
													style="box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; position: absolute; width: 100%; height: 100%; top: 0; right: 0; bottom: 0; left: 0; object-position: center; object-fit: cover; border-width: 1px; border-style: solid; border-color: #e5e5e5;"
											/>
										</a>
									</div>

									<div style="max-width: 448px; padding-bottom: 3px; padding-top: 3px; display: inline-block; vertical-align: top;">
										<a
												style="color: #141b38; font-weight: 700; text-decoration: none; font-size: 16px;"
												href="<?php echo esc_attr( $post['url'] ?: '#' ); ?>"
										>
											<?php echo $post['title']; ?>
										</a>

										<div style="margin-top: 6px; font-size: 14px;">
											<span style="display: block;"><?php echo $post['content'] ?? ''; ?></span>

											<a
													style="color: #005ae0; font-weight: normal; text-decoration: underline;"
													href="<?php echo esc_attr( $post['url'] ?: '#' ); ?>"
											>
												<?php esc_html_e( 'Continue Reading', 'all-in-one-seo-pack' ) ?>
											</a>
										</div>
									</div>
								</td>
							</tr>
						<?php } ?>
						</tbody>
					</table>

					<div style="padding-top: 8px;">
						<div style="border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
					</div>

					<div style="padding-top: 20px; text-align: center;">
						<a
								href="<?php echo esc_attr( $resources['cta']['url'] ); ?>"
								style="border-radius: 4px; border: none; display: inline-block; font-size: 14px; font-style: normal; font-weight: 700; text-align: center; text-decoration: none; user-select: none; vertical-align: middle; background-color: #005ae0; color: #ffffff; padding: 8px 20px;"
						>
							<?php echo $resources['cta']['text']; ?>
						</a>
					</div>
				</div>
			</div>
		<?php } ?>

		<div style="width: 600px; max-width: 90%; margin-top: 20px; margin-left: auto; margin-right: auto;">
			<div style="text-align: center;">
				<img
						style="border-radius: 9999px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-left: auto; margin-right: auto;"
						src="https://static.aioseo.io/report/ste/danny-circle.jpg"
						width="50"
						height="50"
						alt="<?php echo esc_attr( AIOSEO_PLUGIN_SHORT_NAME ) ?>"
				/>

				<p style="font-size: 14px; margin-bottom: 0; margin-top: 20px; text-align: center; color: #434960;">
					<?php
					// Translators: 1 - The plugin short name ("AIOSEO").
					printf( esc_html__( 'This email was auto-generated and sent from %1$s.', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME )
					?>
				</p>

				<p style="font-size: 14px; margin-bottom: 0; margin-top: 0; text-align: center; color: #434960;">
					<?php
					printf(
						// Translators: 1 - Opening link tag, 2 - Closing link tag.
						esc_html__( 'Learn how to %1$sdisable%2$s it.', 'all-in-one-seo-pack' ),
						'<a href="' . ( $links['disable'] ?? '#' ) . '" style="color: #141b38; font-weight: normal; text-decoration: underline;">', '</a>'
					)
					?>
				</p>
			</div>

			<div style="margin-top: 20px;">
				<div style="border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
			</div>

			<div style="margin-top: 20px;">
				<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
					<tbody>
					<tr>
						<td style="line-height: 1; word-break: break-word;">
							<a
									style="color: #005ae0; font-weight: normal; text-decoration: none; display: inline-block;"
									href="<?php echo esc_attr( $links['marketing-site'] ?? '#' ) ?>"
							>
								<img
										style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
										width="82"
										height="17"
										src="https://static.aioseo.io/report/ste/text-logo.png"
										alt="<?php echo esc_attr( AIOSEO_PLUGIN_SHORT_NAME ) ?>"
								/>
							</a>
						</td>

						<td style="line-height: 1; text-align: right; word-break: break-word;">
							<a
									style="margin-right: 6px; color: #005ae0; font-weight: normal; text-decoration: none; display: inline-block;"
									href="<?php echo esc_attr( $links['facebook'] ?? '#' ) ?>"
							>
								<img
										style="border-radius: 2px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; height: 20px; width: 20px;"
										src="https://static.aioseo.io/report/ste/facebook.jpg"
										alt="Fb"
										width="20"
										height="20"
								/>
							</a>

							<a
									style="margin-right: 6px; color: #005ae0; font-weight: normal; text-decoration: none; display: inline-block;"
									href="<?php echo esc_attr( $links['linkedin'] ?? '#' ) ?>"
							>
								<img
										style="border-radius: 2px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; height: 20px; width: 20px;"
										src="https://static.aioseo.io/report/ste/linkedin.jpg"
										alt="In"
										width="20"
										height="20"
								/>
							</a>

							<a
									style="margin-right: 6px; color: #005ae0; font-weight: normal; text-decoration: none; display: inline-block;"
									href="<?php echo esc_attr( $links['youtube'] ?? '#' ) ?>"
							>
								<img
										style="border-radius: 2px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; height: 20px; width: 20px;"
										src="https://static.aioseo.io/report/ste/youtube.jpg"
										alt="Yt"
										width="20"
										height="20"
								/>
							</a>

							<a
									style="color: #005ae0; font-weight: normal; text-decoration: none; display: inline-block;"
									href="<?php echo esc_attr( $links['twitter'] ?? '#' ) ?>"
							>
								<img
										style="border-radius: 2px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; height: 20px; width: 20px;"
										src="https://static.aioseo.io/report/ste/x.jpg"
										alt="Tw"
										width="20"
										height="20"
								/>
							</a>
						</td>
					</tr>
					</tbody>
				</table>
			</div>
		</div>
	</div>
</div>Common/Views/sitemap/htaccess-rewrite-rules.php000064400000000735151536241210015655 0ustar00<?php
/**
 * Htaccess rewrite rules for sites using plain permalinks.
 *
 * @since 4.2.5
 */

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

// phpcs:disable
?>


# START: All in One SEO Sitemap Rewrite Rules
# Do not make edits to these rules!
<IfModule mod_rewrite.c>
	RewriteEngine On

	RewriteRule sitemap(|[0-9]+)\.xml$ /index.php [L]
	RewriteRule (default|video)-sitemap\.xsl /index.php [L]
</IfModule>
# END: All in One SEO Sitemap Rewrite RulesCommon/Views/sitemap/html/compact-archive.php000064400000001202151536241210015250 0ustar00<?php
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
// phpcs:disable Generic.ControlStructures.InlineControlStructure.NotAllowed

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<div class="aioseo-html-sitemap">
	<div class="aioseo-html-sitemap-compact-archive">
		<?php if ( empty( $data['dateArchives'] ) ) esc_html_e( 'No date archives could be found.', 'all-in-one-seo-pack' ); ?>

		<?php if ( ! empty( $data['lines'] ) ) : ?>
			<ul>
				<?php echo $data['lines']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
			</ul>
		<?php endif; ?>

	</div>
</div>Common/Views/sitemap/html/widget-options.php000064400000015531151536241210015171 0ustar00<?php
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable

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

<div class="aioseo-html-sitemap">
	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" class="aioseo-title">
			<?php esc_html_e( 'Title', 'all-in-one-seo-pack' ); ?>
		</label>
		<input
			type="text"
			id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
			name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
			value="<?php echo esc_attr( $instance['title'] ); ?>"
			class="widefat"
		/>
	</p>
	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'archives' ) ); ?>">
			<input
				type="checkbox"
				id="<?php echo esc_attr( $this->get_field_id( 'archives' ) ); ?>"
				name="<?php echo esc_attr( $this->get_field_name( 'archives' ) ); ?>"
				<?php
				if ( 'on' === $instance['archives'] ) {
					echo 'checked="checked"';
				}
				?>
				class="widefat"
			/>
			<?php esc_html_e( 'Compact Archives', 'all-in-one-seo-pack' ); ?>
		</label>
	</p>
	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'show_label' ) ); ?>">
			<input
				type="checkbox"
				id="<?php echo esc_attr( $this->get_field_id( 'show_label' ) ); ?>"
				name="<?php echo esc_attr( $this->get_field_name( 'show_label' ) ); ?>"
				<?php
				if ( 'on' === $instance['show_label'] ) {
					echo 'checked="checked"';
				}
				?>
				class="widefat"
			/>
			<?php esc_html_e( 'Show Labels', 'all-in-one-seo-pack' ); ?>
		</label>
	</p>
	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'publication_date' ) ); ?>">
			<input
				type="checkbox"
				id="<?php echo esc_attr( $this->get_field_id( 'publication_date' ) ); ?>"
				name="<?php echo esc_attr( $this->get_field_name( 'publication_date' ) ); ?>"
				<?php
				if ( 'on' === $instance['publication_date'] ) {
					echo 'checked="checked"';
				}
				?>
				class="widefat"
			/>
			<?php esc_html_e( 'Show Publication Date', 'all-in-one-seo-pack' ); ?>
		</label>
	</p>

	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'post_types' ) ); ?>" class="aioseo-title">
		<?php esc_html_e( 'Post Types', 'all-in-one-seo-pack' ); ?>
		</label>

		<div class="aioseo-columns">
			<?php foreach ( $postTypeObjects as $i => $postTypeObject ) : ?>
			<div>
				<label>
					<input
						type="checkbox"
						name="<?php echo esc_attr( $this->get_field_name( 'post_types' ) ); ?>[]"
						id="<?php echo esc_attr( $this->get_field_id( 'post_types' . $i ) ); ?>"
						<?php checked( in_array( $postTypeObject['name'], $instance['post_types'], true ) ); ?>
						value="<?php echo esc_html( $i ); ?>"
					/>
					<?php echo esc_html( $postTypeObject['label'] ); ?>
				</label>
			</div>
			<?php endforeach ?>
		</div>
	</p>

	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'taxonomies' ) ); ?>" class="aioseo-title">
		<?php esc_html_e( 'Taxonomies', 'all-in-one-seo-pack' ); ?>
		</label>

		<div class="aioseo-columns">
			<?php foreach ( $taxonomyObjects as $i => $taxonomyObject ) : ?>
			<div>
				<label>
					<input
						type="checkbox"
						name="<?php echo esc_attr( $this->get_field_name( 'taxonomies' ) ); ?>[]"
						id="<?php echo esc_attr( $this->get_field_id( 'taxonomies' . $i ) ); ?>"
						<?php checked( in_array( $taxonomyObject['name'], $instance['taxonomies'], true ) ); ?>
						value="<?php echo esc_html( $i ); ?>"
					/>
					<?php echo esc_html( $taxonomyObject['label'] ); ?>
				</label>
			</div>
			<?php endforeach ?>
		</div>
	</p>

	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'order_by' ) ); ?>" class="aioseo-title">
			<?php esc_html_e( 'Sort Order', 'all-in-one-seo-pack' ); ?>
		</label>
		<select name="<?php echo esc_attr( $this->get_field_name( 'order_by' ) ); ?>" id="<?php echo esc_attr( $this->get_field_id( 'order_by' ) ); ?>" class="widefat">
			<option value="publish_date"<?php selected( 'publish_date', $instance['order_by'], true ); ?>>
				<?php esc_html_e( 'Publish Date', 'all-in-one-seo-pack' ); ?>
			</option>
			<option value="last_updated"<?php selected( 'last_updated', $instance['order_by'], true ); ?>>
				<?php esc_html_e( 'Last Updated', 'all-in-one-seo-pack' ); ?>
			</option>
			<option value="alphabetical"<?php selected( 'alphabetical', $instance['order_by'], true ); ?>>
				<?php esc_html_e( 'Alphabetical', 'all-in-one-seo-pack' ); ?>
			</option>
			<option value="id"<?php selected( 'id', $instance['order_by'], true ); ?>>
				<?php esc_html_e( 'ID', 'all-in-one-seo-pack' ); ?>
			</option>
		</select>
	</p>
	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'order' ) ); ?>" class="aioseo-title">
			<?php esc_html_e( 'Sort Direction', 'all-in-one-seo-pack' ); ?>
		</label>
		<select name="<?php echo esc_attr( $this->get_field_name( 'order' ) ); ?>" id="<?php echo esc_attr( $this->get_field_id( 'order' ) ); ?>" class="widefat">
			<option value="asc"<?php echo ( 'asc' === $instance['order'] ) ? ' selected="selected"' : '' ?>><?php esc_html_e( 'Ascending', 'all-in-one-seo-pack' ); ?></option>
			<option value="desc"<?php echo ( 'desc' === $instance['order'] ) ? ' selected="selected"' : '' ?>"><?php esc_html_e( 'Descending', 'all-in-one-seo-pack' ); ?></option>
		</select>
	</p>

	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'excluded_posts' ) ); ?>" class="aioseo-title">
			<?php esc_html_e( 'Exclude Posts / Pages', 'all-in-one-seo-pack' ); ?>
		</label>
		<input
			type="text"
			value="<?php echo esc_attr( $instance['excluded_posts'] ); ?>"
			name="<?php echo esc_attr( $this->get_field_name( 'excluded_posts' ) ); ?>"
			id="<?php echo esc_attr( $this->get_field_id( 'excluded_posts' ) ); ?>"
			class="widefat"
		/>
		<p class="aioseo-description"><?php esc_html_e( 'Enter a comma-separated list of post IDs.', 'all-in-one-seo-pack' ); ?></p>
	</p>

	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'excluded_terms' ) ); ?>" class="aioseo-title">
			<?php esc_html_e( 'Exclude Terms', 'all-in-one-seo-pack' ); ?>
		</label>
		<input
			type="text"
			value="<?php echo esc_attr( $instance['excluded_terms'] ); ?>"
			name="<?php echo esc_attr( $this->get_field_name( 'excluded_terms' ) ); ?>"
			id="<?php echo esc_attr( $this->get_field_id( 'excluded_terms' ) ); ?>"
			class="widefat"
		/>
		<p class="aioseo-description"><?php esc_html_e( 'Enter a comma-separated list of term IDs.', 'all-in-one-seo-pack' ); ?></p>
	</p>
</div>

<style>
	.aioseo-html-sitemap label.aioseo-title,
	.aioseo-html-sitemap label.aioseo-title select {
		color: #141B38 !important;
		font-weight: bold !important;
	}
	.aioseo-html-sitemap .aioseo-description {
		margin-top: -5px;
		font-style: italic;
		font-size: 13px;
	}
	.aioseo-html-sitemap select, .aioseo-html-sitemap input[type=text] {
		margin-top: 8px;
	}
	.aioseo-html-sitemap .aioseo-columns {
		display: flex;
		flex-wrap: wrap;
	}
	.aioseo-html-sitemap .aioseo-columns div {
		flex: 0 0 50%;
	}
</style>Common/Views/sitemap/xml/default.php000064400000003620151536241210013471 0ustar00<?php
/**
 * XML template for our sitemap index pages.
 *
 * @since 4.0.0
 */

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

// phpcs:disable
?>
<urlset
	xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
	xmlns:xhtml="http://www.w3.org/1999/xhtml"
<?php if ( ! aioseo()->sitemap->helpers->excludeImages() ): ?>
	xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
<?php endif; ?>
>
<?php foreach ( $entries as $entry ) {
	if ( empty( $entry['loc'] ) ) {
		continue;
	}
	?>
	<url>
		<loc><?php aioseo()->sitemap->output->escapeAndEcho( $entry['loc'] ); ?></loc><?php
	if ( ! empty( $entry['lastmod'] ) ) {
			?>

		<lastmod><?php aioseo()->sitemap->output->escapeAndEcho( $entry['lastmod'] ); ?></lastmod><?php
	}
	if ( ! empty( $entry['changefreq'] ) ) {
			?>

		<changefreq><?php aioseo()->sitemap->output->escapeAndEcho( $entry['changefreq'] ); ?></changefreq><?php
	}
	if ( isset( $entry['priority'] ) ) {
			?>

		<priority><?php aioseo()->sitemap->output->escapeAndEcho( $entry['priority'] ); ?></priority><?php
	}
	if ( ! empty( $entry['languages'] ) ) {
		foreach ( $entry['languages'] as $subentry ) {
			if ( empty( $subentry['language'] ) || empty( $subentry['location'] ) ) {
				continue;
			}
		?>

		<xhtml:link rel="alternate" hreflang="<?php echo esc_attr( $subentry['language'] ); ?>" href="<?php echo esc_url( $subentry['location'] ); ?>" /><?php
		}
	}
	if ( ! aioseo()->sitemap->helpers->excludeImages() && ! empty( $entry['images'] ) ) {
			foreach ( $entry['images'] as $image ) {
				$image = (array) $image;
			?>

		<image:image>
			<image:loc>
				<?php
				if ( aioseo()->helpers->isRelativeUrl( $image['image:loc'] ) ) {
					$image['image:loc'] = aioseo()->helpers->makeUrlAbsolute( $image['image:loc'] );
				}

				aioseo()->sitemap->output->escapeAndEcho( $image['image:loc'] );
				?>
			</image:loc>
		</image:image><?php
		}
	}
	?>

	</url>
<?php } ?>
</urlset>
Common/Views/sitemap/xml/root.php000064400000001147151536241210013032 0ustar00<?php
/**
 * XML template for our root index page.
 *
 * @since 4.0.0
 */

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

 // phpcs:disable
?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<?php foreach ( $entries as $entry ) {
	if ( empty( $entry['loc'] ) ) {
		continue;
	}
	?>
	<sitemap>
		<loc><?php aioseo()->sitemap->output->escapeAndEcho( $entry['loc'] ); ?></loc><?php
	if ( ! empty( $entry['lastmod'] ) ) {
			?>

		<lastmod><?php aioseo()->sitemap->output->escapeAndEcho( $entry['lastmod'] ); ?></lastmod><?php
		}
	?>

	</sitemap>
<?php } ?>
</sitemapindex>
Common/Views/sitemap/xml/rss.php000064400000003306151536241210012655 0ustar00<?php
/**
 * XML template for the RSS Sitemap.
 *
 * @since 4.0.0
 */

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

// phpcs:disable
?>

<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel><?php
	// Yandex doesn't support some tags so we need to check the user agent.
	if ( ! aioseo()->helpers->isYandexUserAgent() ) {
		?>

		<title><?php aioseo()->sitemap->output->escapeAndEcho( $title, false ); ?></title>
		<link><?php aioseo()->sitemap->output->escapeAndEcho( $link ); ?></link>
		<?php if ( $description ) {
		?><description><?php aioseo()->sitemap->output->escapeAndEcho( $description ); ?></description>
		<?php }
		?><?php if ( ! empty( $entries[0]['pubDate'] ) ) {
		?><lastBuildDate><?php aioseo()->sitemap->output->escapeAndEcho( $entries[0]['pubDate'] ); ?></lastBuildDate>
		<?php }
		?><docs>https://validator.w3.org/feed/docs/rss2.html</docs>
		<atom:link href="<?php echo aioseo()->sitemap->helpers->getUrl( 'rss' ); ?>" rel="self" type="application/rss+xml" />
		<ttl><?php aioseo()->sitemap->output->escapeAndEcho( $ttl ); ?></ttl>

<?php }
foreach ( $entries as $entry ) {
		if ( empty( $entry['guid'] ) ) {
			continue;
			}?>
		<item>
			<guid><?php aioseo()->sitemap->output->escapeAndEcho( $entry['guid'] ); ?></guid>
			<link><?php aioseo()->sitemap->output->escapeAndEcho( $entry['guid'] ); ?></link><?php
			if ( ! empty( $entry['title'] ) ) {
				?>

			<title><?php aioseo()->sitemap->output->escapeAndEcho( $entry['title'], false ); ?></title><?php
			}
			if ( ! empty( $entry['pubDate'] ) ) {
				?>

			<pubDate><?php aioseo()->sitemap->output->escapeAndEcho( $entry['pubDate'] ); ?></pubDate><?php
			}
			?>

		</item>
			<?php } ?>
	</channel>
</rss>
Common/Views/sitemap/xsl/default.php000064400000031502151536241210013477 0ustar00<?php
/**
 * XSL stylesheet for the sitemap.
 *
 * @since 4.0.0
 */

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

// phpcs:disable
$utmMedium = 'xml-sitemap';
if ( '/sitemap.rss' === $sitemapPath ) {
	$utmMedium = 'rss-sitemap';
}
?>
<xsl:stylesheet
	version="2.0"
	xmlns:html="http://www.w3.org/TR/html40"
	xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9"
	xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
	<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>

	<xsl:template match="/">
		<xsl:variable name="fileType">
			<xsl:choose>
				<xsl:when test="//channel">RSS</xsl:when>
				<xsl:when test="//sitemap:url">Sitemap</xsl:when>
				<xsl:otherwise>SitemapIndex</xsl:otherwise>
			</xsl:choose>
		</xsl:variable>
		<html xmlns="http://www.w3.org/1999/xhtml">
			<head>
				<title>
					<xsl:choose>
						<xsl:when test="$fileType='Sitemap' or $fileType='RSS'"><?php echo $title; ?></xsl:when>
						<xsl:otherwise><?php _e( 'Sitemap Index', 'all-in-one-seo-pack' ); ?></xsl:otherwise>
					</xsl:choose>
				</title>
				<meta name="viewport" content="width=device-width, initial-scale=1" />
				<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
				<?php aioseo()->templates->getTemplate( 'sitemap/xsl/styles.php' ); ?>
			</head>
			<body>
				<xsl:variable name="amountOfURLs">
					<xsl:choose>
						<xsl:when test="$fileType='RSS'">
							<xsl:value-of select="count(//channel/item)"></xsl:value-of>
						</xsl:when>
						<xsl:when test="$fileType='Sitemap'">
							<xsl:value-of select="count(sitemap:urlset/sitemap:url)"></xsl:value-of>
						</xsl:when>
						<xsl:otherwise>
							<xsl:value-of select="count(sitemap:sitemapindex/sitemap:sitemap)"></xsl:value-of>
						</xsl:otherwise>
					</xsl:choose>
				</xsl:variable>

				<xsl:call-template name="Header">
					<xsl:with-param name="title"><?php echo $title; ?></xsl:with-param>
					<xsl:with-param name="amountOfURLs" select="$amountOfURLs"/>
					<xsl:with-param name="fileType" select="$fileType"/>
				</xsl:call-template>

				<div class="content">
					<div class="container">
						<xsl:choose>
							<xsl:when test="$amountOfURLs = 0"><xsl:call-template name="emptySitemap"/></xsl:when>
							<xsl:when test="$fileType='Sitemap'"><xsl:call-template name="sitemapTable"/></xsl:when>
							<xsl:when test="$fileType='RSS'"><xsl:call-template name="sitemapRSS"/></xsl:when>
							<xsl:otherwise><xsl:call-template name="siteindexTable"/></xsl:otherwise>
						</xsl:choose>
					</div>
				</div>
			</body>
		</html>
	</xsl:template>

	<xsl:template name="siteindexTable">
		<?php
		$sitemapIndex = aioseo()->sitemap->helpers->filename( 'general' );
		$sitemapIndex = $sitemapIndex ? $sitemapIndex : 'sitemap';
		aioseo()->templates->getTemplate(
			'sitemap/xsl/partials/breadcrumb.php',
			[
				'items' => [
					[ 'title' => __( 'Sitemap Index', 'all-in-one-seo-pack' ), 'url' => $sitemapUrl ],
				]
			]
		);
		?>
		<div class="table-wrapper">
			<table cellpadding="3">
				<thead>
				<tr>
					<th class="left">
						<?php _e( 'URL', 'all-in-one-seo-pack' ); ?>
					</th>
					<th><?php _e( 'URL Count', 'all-in-one-seo-pack' ); ?></th>
					<th>
						<?php
						aioseo()->templates->getTemplate(
							'sitemap/xsl/partials/sortable-column.php',
							[
								'parameters' => $sitemapParams,
								'sitemapUrl' => $sitemapUrl,
								'column'     => 'date',
								'title'      => __( 'Last Updated', 'all-in-one-seo-pack' )
							]
						);
						?>
					</th>
				</tr>
				</thead>
				<tbody>
				<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'"/>
				<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
				<xsl:for-each select="sitemap:sitemapindex/sitemap:sitemap">
					<?php
					aioseo()->templates->getTemplate(
						'sitemap/xsl/partials/xsl-sort.php',
						[
							'parameters' => $sitemapParams,
							'node'       => 'sitemap:lastmod',
						]
					);
					?>
					<tr>
						<xsl:if test="position() mod 2 != 0">
							<xsl:attribute name="class">stripe</xsl:attribute>
						</xsl:if>
						<td class="left">
							<a>
								<xsl:attribute name="href">
									<xsl:value-of select="sitemap:loc" />
								</xsl:attribute>
								<xsl:value-of select="sitemap:loc"/>
							</a>
						</td>
						<td>
							<?php if ( ! empty( $xslParams['counts'] ) ) : ?>
							<div class="item-count">
							<xsl:choose>
								<?php foreach ( $xslParams['counts'] as $slug => $count ) : ?>
									<xsl:when test="contains(sitemap:loc, '<?php echo $slug; ?>')"><?php echo $count; ?></xsl:when>
								<?php endforeach; ?>
								<xsl:otherwise><?php echo $linksPerIndex; ?></xsl:otherwise>
							</xsl:choose>
							</div>
							<?php endif; ?>
						</td>
						<td class="datetime">
							<?php
							if ( ! empty( $xslParams['datetime'] ) ) {
								aioseo()->templates->getTemplate(
									'sitemap/xsl/partials/date-time.php',
									[
										'datetime' => $xslParams['datetime'],
										'node'     => 'sitemap:loc'
									]
								);
							}
							?>
						</td>
					</tr>
				</xsl:for-each>
				</tbody>
			</table>
		</div>
	</xsl:template>

	<xsl:template name="sitemapRSS">
		<?php
		aioseo()->templates->getTemplate(
			'sitemap/xsl/partials/breadcrumb.php',
			[
				'items' => [
					[ 'title' => $title, 'url' => $sitemapUrl ],
				]
			]
		);
		?>
		<div class="table-wrapper">
			<table cellpadding="3">
				<thead>
					<tr>
						<th class="left"><?php _e( 'URL', 'all-in-one-seo-pack' ); ?></th>
						<th>
							<?php
							aioseo()->templates->getTemplate(
								'sitemap/xsl/partials/sortable-column.php',
								[
									'parameters' => $sitemapParams,
									'sitemapUrl' => $sitemapUrl,
									'column'     => 'date',
									'title'      => __( 'Publication Date', 'all-in-one-seo-pack' )
								]
							);
							?>
						</th>
					</tr>
				</thead>
				<tbody>
				<xsl:for-each select="//channel/item">
					<?php
					if ( ! empty( $sitemapParams['sitemap-order'] ) ) {
						aioseo()->templates->getTemplate(
							'sitemap/xsl/partials/xsl-sort.php',
							[
								'parameters' => $sitemapParams,
								'node'       => 'pubDate',
							]
						);
					}
					?>
					<tr>
						<xsl:if test="position() mod 2 != 0">
							<xsl:attribute name="class">stripe</xsl:attribute>
						</xsl:if>
						<td class="left">
							<a>
								<xsl:attribute name="href">
									<xsl:value-of select="link" />
								</xsl:attribute>
								<xsl:value-of select="link"/>
							</a>
						</td>
						<td class="datetime">
							<?php
							if ( ! empty( $xslParams['datetime'] ) ) {
								aioseo()->templates->getTemplate(
									'sitemap/xsl/partials/date-time.php',
									[
										'datetime' => $xslParams['datetime'],
										'node'     => 'link'
									]
								);
							}
							?>
						</td>
					</tr>
				</xsl:for-each>
				</tbody>
			</table>
		</div>
	</xsl:template>

	<xsl:template name="sitemapTable">
		<?php
		$sitemapIndex  = aioseo()->sitemap->helpers->filename( 'general' );
		$sitemapIndex  = $sitemapIndex ?: 'sitemap';
		$excludeImages = isset( $excludeImages ) ? $excludeImages : aioseo()->sitemap->helpers->excludeImages();
		aioseo()->templates->getTemplate(
			'sitemap/xsl/partials/breadcrumb.php',
			[
				'items' => [
					[ 'title' => __( 'Sitemap Index', 'all-in-one-seo-pack' ), 'url' => home_url( "/$sitemapIndex.xml" ) ],
					[ 'title' => $title, 'url' => $sitemapUrl ],
				]
			]
		);
		?>
		<div class="table-wrapper">
			<table cellpadding="3">
				<thead>
					<tr>
						<th class="left">
							<?php _e( 'URL', 'all-in-one-seo-pack' ); ?>
						</th>
						<?php if ( ! $excludeImages ) : ?>
							<th>
								<?php
								aioseo()->templates->getTemplate(
									'sitemap/xsl/partials/sortable-column.php',
									[
										'parameters' => $sitemapParams,
										'sitemapUrl' => $sitemapUrl,
										'column'     => 'image',
										'title'      => __( 'Images', 'all-in-one-seo-pack' )
									]
								);
								?>
							</th>
						<?php endif; ?>
						<th>
							<?php
							aioseo()->templates->getTemplate(
								'sitemap/xsl/partials/sortable-column.php',
								[
									'parameters' => $sitemapParams,
									'sitemapUrl' => $sitemapUrl,
									'column'     => 'changefreq',
									'title'      => __( 'Change Frequency', 'all-in-one-seo-pack' )
								]
							);
							?>
						</th>
						<th>
							<?php
							aioseo()->templates->getTemplate(
								'sitemap/xsl/partials/sortable-column.php',
								[
									'parameters' => $sitemapParams,
									'sitemapUrl' => $sitemapUrl,
									'column'     => 'priority',
									'title'      => __( 'Priority', 'all-in-one-seo-pack' )
								]
							);
							?>
						</th>
						<th>
							<?php
							aioseo()->templates->getTemplate(
								'sitemap/xsl/partials/sortable-column.php',
								[
									'parameters' => $sitemapParams,
									'sitemapUrl' => $sitemapUrl,
									'column'     => 'date',
									'title'      => __( 'Last Updated', 'all-in-one-seo-pack' )
								]
							);
							?>
						</th>
					</tr>
				</thead>
				<tbody>
				<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'"/>
				<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
				<xsl:for-each select="sitemap:urlset/sitemap:url">
					<?php
					if ( ! empty( $sitemapParams['sitemap-order'] ) ) {
						switch ( $sitemapParams['sitemap-order'] ) {
							case 'image':
								$node = 'count(image:image)';
								break;
							case 'date':
								$node = 'sitemap:lastmod';
								break;
							default:
								$node = 'sitemap:' . $sitemapParams['sitemap-order'];
								break;
						}
						aioseo()->templates->getTemplate(
							'sitemap/xsl/partials/xsl-sort.php',
							[
								'parameters' => $sitemapParams,
								'node'       => $node,
							]
						);
					}
					?>
					<tr>
						<xsl:if test="position() mod 2 != 0">
							<xsl:attribute name="class">stripe</xsl:attribute>
						</xsl:if>

						<td class="left">
							<xsl:variable name="itemURL">
								<xsl:value-of select="sitemap:loc"/>
							</xsl:variable>

							<xsl:choose>
								<xsl:when test="count(./*[@rel='alternate']) > 0">
									<xsl:for-each select="./*[@rel='alternate']">
										<xsl:if test="position() = last()">
											<a href="{current()/@href}" class="localized">
												<xsl:value-of select="@href"/>
											</a> &#160;&#8594; <xsl:value-of select="@hreflang"/>
										</xsl:if>
									</xsl:for-each>
								</xsl:when>
								<xsl:otherwise>
									<a href="{$itemURL}">
										<xsl:value-of select="sitemap:loc"/>
									</a>
								</xsl:otherwise>
							</xsl:choose>

							<xsl:for-each select="./*[@rel='alternate']">
								<br />
								<xsl:if test="position() != last()">
									<a href="{current()/@href}" class="localized">
										<xsl:value-of select="@href"/>
									</a> &#160;&#8594; <xsl:value-of select="@hreflang"/>
								</xsl:if>
							</xsl:for-each>
						</td>
						<?php if ( ! $excludeImages ) : ?>
						<td>
							<div class="item-count">
								<xsl:value-of select="count(image:image)"/>
							</div>
						</td>
						<?php endif; ?>
						<td>
							<xsl:value-of select="concat(translate(substring(sitemap:changefreq, 1, 1),concat($lower, $upper),concat($upper, $lower)),substring(sitemap:changefreq, 2))"/>
						</td>
						<td>
							<xsl:if test="string(number(sitemap:priority))!='NaN'">
								<xsl:call-template name="formatPriority">
									<xsl:with-param name="priority" select="sitemap:priority"/>
								</xsl:call-template>
							</xsl:if>
						</td>
						<td class="datetime">
							<?php
							if ( ! empty( $xslParams['datetime'] ) ) {
								aioseo()->templates->getTemplate(
									'sitemap/xsl/partials/date-time.php',
									[
										'datetime' => $xslParams['datetime'],
										'node'     => 'sitemap:loc'
									]
								);
							}
							?>
						</td>
					</tr>
				</xsl:for-each>
				</tbody>
			</table>
		</div>
		<?php
		if ( ! empty( $xslParams['pagination'] ) ) {
			aioseo()->templates->getTemplate(
				'sitemap/xsl/partials/pagination.php',
				[
					'sitemapUrl'    => $sitemapUrl,
					'currentPage'   => $currentPage,
					'linksPerIndex' => $linksPerIndex,
					'total'         => $xslParams['pagination']['total'],
					'showing'       => $xslParams['pagination']['showing']
				]
			);
		}
		?>
	</xsl:template>

	<?php aioseo()->templates->getTemplate( 'sitemap/xsl/templates/header.php', [ 'utmMedium' => $utmMedium ] ); ?>
	<?php aioseo()->templates->getTemplate( 'sitemap/xsl/templates/format-priority.php' ); ?>
	<?php
	aioseo()->templates->getTemplate( 'sitemap/xsl/templates/empty-sitemap.php', [
		'utmMedium' => $utmMedium,
		'items'     => [
			[ 'title' => __( 'Sitemap Index', 'all-in-one-seo-pack' ), 'url' => $sitemapUrl ]
		]
	] );
	?>
</xsl:stylesheet>
Common/Views/sitemap/xsl/partials/breadcrumb.php000064400000002372151536241210016003 0ustar00<?php
/**
 * XSL Breadcrumb partial for the sitemap.
 *
 * @since 4.1.5
 */

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

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
if ( empty( $data['items'] ) ) {
	return;
}

$sitemapIndex = aioseo()->sitemap->helpers->filename( 'general' );
$sitemapIndex = $sitemapIndex ? $sitemapIndex : 'sitemap';
?>
<div class="breadcrumb">
	<svg class="back" width="6" height="9" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5.274 7.56L2.22 4.5l3.054-3.06-.94-.94-4 4 4 4 .94-.94z" fill="#141B38"/></svg>

	<a href="<?php echo esc_attr( home_url() ); ?>"><span><?php esc_attr_e( 'Home', 'all-in-one-seo-pack' ); ?></span></a>

	<?php
	foreach ( $data['items'] as $key => $item ) {
		if ( empty( $item ) ) {
			continue;
		}
		?>
		<svg width="6" height="8" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M.727 7.06L3.78 4 .727.94l.94-.94 4 4-4 4-.94-.94z" fill="#141B38"/></svg>

		<?php if ( count( $data['items'] ) === $key + 1 ) : ?>
			<span><?php echo esc_html( $item['title'] ); ?></span>
		<?php else : ?>
			<a href="<?php echo esc_attr( $item['url'] ) ?>"><span><?php echo esc_html( $item['title'] ); ?></span></a>
		<?php endif; ?>
		<?php
	}
	?>
</div>Common/Views/sitemap/xsl/partials/date-time.php000064400000001605151536241210015544 0ustar00<?php
/**
 * XSL Breadcrumb partial for the sitemap.
 *
 * @since 4.1.5
 */

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

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
if ( empty( $data['datetime'] ) || empty( $data['node'] ) ) {
	return;
}

?>
<div class="date">
	<xsl:choose>
		<?php foreach ( $data['datetime'] as $slug => $datetime ) : ?>
			<xsl:when test="<?php echo esc_attr( $data['node'] ); ?> = '<?php echo esc_attr( $slug ); ?>'"><?php echo esc_html( $datetime['date'] ); ?></xsl:when>
		<?php endforeach; ?>
	</xsl:choose>
</div>
<div class="time">
	<xsl:choose>
		<?php foreach ( $data['datetime'] as $slug => $datetime ) : ?>
			<xsl:when test="<?php echo esc_attr( $data['node'] ); ?> = '<?php echo esc_attr( $slug ); ?>'"><?php echo esc_html( $datetime['time'] ); ?></xsl:when>
		<?php endforeach; ?>
	</xsl:choose>
</div>Common/Views/sitemap/xsl/partials/pagination.php000064400000003645151536241210016032 0ustar00<?php
/**
 * XSL Pagination partial for the sitemap.
 *
 * @since 4.1.5
 */

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

// Don't allow pagination for now.
return;

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable

// Check if requires pagination.
if ( $data['showing'] === $data['total'] ) {
	return;
}

$currentPage   = (int) $data['currentPage'];
$totalLinks    = (int) $data['total'];
$showing       = (int) $data['showing'];
$linksPerIndex = (int) $data['linksPerIndex'];
$totalPages    = ceil( $totalLinks / $linksPerIndex );
$start         = ( ( $currentPage - 1 ) * $linksPerIndex ) + 1;
$end           = ( ( $currentPage - 1 ) * $linksPerIndex ) + $showing;

$hasNextPage = $totalPages > $currentPage;
$hasPrevPage = $currentPage > 1;
$nextPageUri = $hasNextPage ? preg_replace( '/sitemap([0-9]*)\.xml/', 'sitemap' . ( $currentPage + 1 ) . '.xml', (string) $data['sitemapUrl'] ) : '#';
$prevPageUri = $hasPrevPage ? preg_replace( '/sitemap([0-9]*)\.xml/', 'sitemap' . ( $currentPage - 1 ) . '.xml', (string) $data['sitemapUrl'] ) : '#';
?>
<div class="pagination">
	<div class="label">
		<?php
		echo esc_html(
			sprintf(
				// Translators: 1 - The "start-end" pagination results, 2 - Total items.
				__( 'Showing %1$s of %2$s', 'all-in-one-seo-pack' ),
				"$start-$end",
				$totalLinks
			)
		);
		?>
	</div>

	<a href="<?php echo esc_attr( $prevPageUri ); ?>" class="<?php echo $hasPrevPage ? '' : 'disabled'; ?>">
		<svg width="7" height="10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.842 8.825L3.025 5l3.817-3.825L5.667 0l-5 5 5 5 1.175-1.175z" fill="#141B38"/></svg>
	</a>

	<a href="<?php echo esc_attr( $nextPageUri ); ?>" class="<?php echo $hasNextPage ? '' : 'disabled'; ?>">
		<svg width="7" height="10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M.158 8.825L3.975 5 .158 1.175 1.333 0l5 5-5 5L.158 8.825z" fill="#141B38"/></svg>
	</a>
</div>Common/Views/sitemap/xsl/partials/sortable-column.php000064400000001673151536241210017006 0ustar00<?php
/**
 * XSL sortableColumn partial for the sitemap.
 *
 * @since 4.1.5
 */

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

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable

// Just print out the title for now.
echo esc_html( $data['title'] );

/*$orderBy = 'ascending';
if ( ! empty( $data['parameters']['sitemap-orderby'] ) ) {
	$orderBy = $data['parameters']['sitemap-orderby'];
}

$isOrdering = false;
if ( ! empty( $data['parameters']['sitemap-order'] ) ) {
	$isOrdering = $data['column'] === $data['parameters']['sitemap-order'];
}

$link = add_query_arg( [
	'sitemap-order'   => $data['column'],
	'sitemap-orderby' => 'ascending' === $orderBy ? 'descending' : 'ascending'
], $data['sitemapUrl'] );
?>
<a href="<?php echo esc_url( $link ); ?>" class="sortable <?php echo esc_attr( ( $isOrdering ? 'active' : '' ) . ' ' . $orderBy ); ?>">
	<?php echo esc_html( $data['title'] ); ?>
</a>*/Common/Views/sitemap/xsl/partials/xsl-sort.php000064400000001176151536241210015471 0ustar00<?php
/**
 * XSL XSLSort partial for the sitemap.
 *
 * @since 4.1.5
 */

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

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
if ( empty( $data['node'] ) ) {
	return;
}

$orderBy = '';
if ( ! empty( $data['parameters']['sitemap-orderby'] ) && in_array( $data['parameters']['sitemap-orderby'], [ 'ascending', 'descending' ], true ) ) {
	$orderBy = $data['parameters']['sitemap-orderby'];
}

if ( empty( $orderBy ) ) {
	return;
}
?>

<xsl:sort select="<?php echo esc_attr( $data['node'] ); ?>" order="<?php echo esc_attr( $orderBy ) ?>"/>Common/Views/sitemap/xsl/styles.php000064400000003512151536241210013376 0ustar00<?php
/**
 * Styles for the sitemap.
 *
 * @since 4.1.5
 */

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

// phpcs:disable
?>
<style type="text/css">
	body {
		margin: 0;
		font-family: Helvetica, Arial, sans-serif;
		font-size: 68.5%;
	}
	#content-head {
		background-color: #141B38;
		padding: 20px 40px;
	}
	#content-head h1,
	#content-head p,
	#content-head a {
		color: #fff;
		font-size: 1.2em;
	}
	#content-head h1 {
		font-size: 2em;
	}
	table {
		margin: 20px 40px;
		border: none;
		border-collapse: collapse;
		font-size: 1em;
		width: 75%;
	}
	th {
		border-bottom: 1px solid #ccc;
		text-align: left;
		padding: 15px 5px;
		font-size: 14px;
	}
	td {
		padding: 10px 5px;
		border-left: 3px solid #fff;
	}
	tr.stripe {
		background-color: #f7f7f7;
	}
	table td a:not(.localized) {
		display: block;
	}
	table td a img {
		max-height: 30px;
		margin: 6px 3px;
	}
	.empty-sitemap {
		margin: 20px 40px;
		width: 75%;
	}
	.empty-sitemap__title {
		font-size: 18px;
		line-height: 125%;
		margin: 12px 0;
	}
	.empty-sitemap svg {
		width: 140px;
		height: 140px;
	}
	.empty-sitemap__buttons {
		margin-bottom: 30px;
	}
	.empty-sitemap__buttons .button {
		margin-right: 5px;
	}
	.breadcrumb {
		margin: 20px 40px;
		width: 75%;

		display: flex;
		align-items: center;
		font-size: 12px;
		font-weight: 600;
	}
	.breadcrumb a {
		color: #141B38;
		text-decoration: none;
	}
	.breadcrumb svg {
		margin: 0 10px;
	}
	@media (max-width: 1023px) {
		.breadcrumb svg:not(.back),
		.breadcrumb a:not(:last-of-type),
		.breadcrumb span {
			display: none;
		}
		.breadcrumb a:last-of-type::before {
			content: '<?php _e( 'Back', 'all-in-one-seo-pack' ); ?>'
		}
	}
	@media (min-width: 1024px) {
		.breadcrumb {
			font-size: 14px;
		}
		.breadcrumb a {
			font-weight: 400;
		}
		.breadcrumb svg.back {
			display: none;
		}
	}
</style>
Common/Views/sitemap/xsl/templates/empty-sitemap.php000064400000004177151536241210016657 0ustar00<?php
/**
 * XSL emptySitemap template for the sitemap.
 *
 * @since 4.1.5
 */

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

$canManageSitemap = is_user_logged_in() && aioseo()->access->hasCapability( 'aioseo_sitemap_settings' );
$adminUrl         = admin_url( 'admin.php?page=aioseo-sitemaps' );

// phpcs:disable
if ( 'xml-sitemap' !== $data['utmMedium'] ) {
	$adminUrl .= '#/' . str_replace( 'aioseo-', '', $data['utmMedium'] );
}
?>
<xsl:template name="emptySitemap">
	<?php
	if ( ! empty( $data['items'] ) ) {
		aioseo()->templates->getTemplate(
			'sitemap/xsl/partials/breadcrumb.php',
			[ 'items' => $data['items'] ]
		);
	}
	?>
	<div class="empty-sitemap">
		<h2 class="empty-sitemap__title">
			<?php _e( 'Whoops!', 'all-in-one-seo-pack' ); ?>
			<br />
			<?php _e( 'There are no posts here', 'all-in-one-seo-pack' ); ?>
		</h2>
		<div class="empty-sitemap__buttons">
			<a href="<?php echo esc_attr( home_url() ); ?>" class="button"><?php _e( 'Back to Homepage', 'all-in-one-seo-pack' ); ?></a>
			<?php if ( $canManageSitemap ) : ?>
				<a href="<?php echo esc_attr( esc_url( $adminUrl ) ); ?>" class="button"><?php _e( 'Configure Sitemap', 'all-in-one-seo-pack' ); ?></a>
			<?php endif; ?>
		</div>

		<?php if ( $canManageSitemap ) : ?>
			<div class="aioseo-alert yellow">
				<?php
					echo sprintf(
						// Translators: 1 - Opening HTML link tag, 2 - Closing HTML link tag.
						__( 'Didn\'t expect to see this? Make sure your sitemap is enabled and your content is set to be indexed. %1$sLearn More →%2$s', 'all-in-one-seo-pack' ),
						'<a target="_blank" href="' . aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'docs/how-to-fix-a-404-error-when-viewing-your-sitemap/', $data['utmMedium'], 'learn-more' ) . '">',
						'</a>'
					);
				?>
			</div>
		<?php endif; ?>
	</div>
	<style>
		.hand-magnifier{
			animation: hand-magnifier .8s infinite ease-in-out;
			transform-origin: center 90%;
			transform-box: fill-box;
		}
		@keyframes hand-magnifier {
			0% {
				transform: rotate(0deg);
			}
			50% {
				transform: rotate(-12deg);
			}
			100% {
				transform: rotate(0deg);
			}
		}
	</style>
</xsl:template>
Common/Views/sitemap/xsl/templates/format-priority.php000064400000002127151536241210017221 0ustar00<?php
/**
 * XSL formatPriority template for the sitemap.
 *
 * @since 4.1.5
 */

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

// phpcs:disable
?>
<xsl:template name="formatPriority">
	<xsl:param name="priority"/>

	<xsl:variable name="priorityLevel">
		<xsl:choose>
			<xsl:when test="$priority &lt;= 0.5">low</xsl:when>
			<xsl:when test="$priority &gt;= 0.6 and $priority &lt;= 0.8">medium</xsl:when>
			<xsl:when test="$priority &gt;= 0.9">high</xsl:when>
		</xsl:choose>
	</xsl:variable>

	<xsl:variable name="priorityLabel">
		<xsl:choose>
			<xsl:when test="$priorityLevel = 'low'"><?php _e( 'Low', 'all-in-one-seo-pack' ); ?></xsl:when>
			<xsl:when test="$priorityLevel = 'medium'"><?php _e( 'Medium', 'all-in-one-seo-pack' ); ?></xsl:when>
			<xsl:when test="$priorityLevel = 'high'"><?php _e( 'High', 'all-in-one-seo-pack' ); ?></xsl:when>
		</xsl:choose>
	</xsl:variable>

	<div>
		<xsl:attribute name="class">
			<xsl:value-of select="concat('priority priority--', $priorityLevel)" />
		</xsl:attribute>
		<xsl:value-of select="$priorityLabel" />
	</div>
</xsl:template>
Common/Views/sitemap/xsl/templates/header.php000064400000006135151536241210015305 0ustar00<?php
/**
 * XSL Header template for the sitemap.
 *
 * @since 4.1.5
 */

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

// phpcs:disable
?>
<xsl:template name="Header">
	<xsl:param name="title"/>
	<xsl:param name="amountOfURLs"/>
	<xsl:param name="fileType"/>

	<div id="content-head">
		<h1><xsl:value-of select="$title"/></h1>
		<xsl:choose>
			<xsl:when test="$fileType='RSS'">
				<p><?php echo __( 'Generated by', 'all-in-one-seo-pack' ); ?> <a href="<?php echo aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL, $data['utmMedium'] ); ?>" target="_blank" rel="noreferrer noopener"><?php echo AIOSEO_PLUGIN_NAME; ?></a>, <?php echo __( 'this is an RSS Sitemap, meant to be consumed by search engines like Google or Bing.', 'all-in-one-seo-pack' ) ?></p>
				<p>
					<?php
						// Translators: 1 - Opening HTML link tag, 2 - Closing HTML link tag.
						printf( __( 'You can find more information about RSS Sitemaps at %1$ssitemaps.org%2$s.', 'all-in-one-seo-pack' ), '<a href="https://www.sitemaps.org/" target="_blank" rel="noreferrer noopener">', '</a>');
					?>
				</p>
			</xsl:when>
			<xsl:otherwise>
				<p><?php echo __( 'Generated by', 'all-in-one-seo-pack' ); ?> <a href="<?php echo aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL, $data['utmMedium'] ); ?>" target="_blank" rel="noreferrer noopener"><?php echo AIOSEO_PLUGIN_NAME; ?></a>, <?php echo __( 'this is an XML Sitemap, meant to be consumed by search engines like Google or Bing.', 'all-in-one-seo-pack' ) ?></p>
				<p>
					<?php
						// Translators: 1 - Opening HTML link tag, 2 - Closing HTML link tag.
						printf( __( 'You can find more information about XML Sitemaps at %1$ssitemaps.org%2$s.', 'all-in-one-seo-pack' ), '<a href="https://www.sitemaps.org/" target="_blank" rel="noreferrer noopener">', '</a>');
					?>
				</p>
			</xsl:otherwise>
		</xsl:choose>
		<xsl:if test="$amountOfURLs &gt; 0">
			<p>
				<xsl:choose>
					<xsl:when test="$fileType='Sitemap' or $fileType='RSS'">
						<?php echo __( 'This sitemap contains', 'all-in-one-seo-pack' ); ?>
						<xsl:value-of select="$amountOfURLs"/>
						<xsl:choose>
							<xsl:when test="$amountOfURLs = 1">
								<?php _e( 'URL', 'all-in-one-seo-pack' ); ?>
							</xsl:when>
							<xsl:otherwise>
								<?php _e( 'URLs', 'all-in-one-seo-pack' ); ?>
							</xsl:otherwise>
						</xsl:choose>
					</xsl:when>
					<xsl:otherwise>
						<?php echo __( 'This sitemap index contains', 'all-in-one-seo-pack' ); ?>
						<xsl:value-of select="$amountOfURLs"/>
						<xsl:choose>
							<xsl:when test="$amountOfURLs = 1">
								<?php _e( 'sitemap', 'all-in-one-seo-pack' ); ?>
							</xsl:when>
							<xsl:otherwise>
								<?php _e( 'sitemaps', 'all-in-one-seo-pack' ); ?>
							</xsl:otherwise>
						</xsl:choose>
					</xsl:otherwise>
				</xsl:choose>
				<?php 
					echo sprintf(
						// Translators: 1 - The generated date, 2 - The generated time.
						__( 'and was generated on %1$s at %2$s', 'all-in-one-seo-pack' ),
						date_i18n( get_option( 'date_format' ) ),
						date_i18n( get_option( 'time_format' ) )
					); 
				?>
			</p>
		</xsl:if>
	</div>
</xsl:template>
Common/WritingAssistant/SeoBoost/SeoBoost.php000064400000020236151536241210015317 0ustar00<?php
namespace AIOSEO\Plugin\Common\WritingAssistant\SeoBoost;

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

/**
 * Handles the connection with SEOBoost.
 *
 * @since 4.7.4
 */
class SeoBoost {
	/**
	 * URL of the login page.
	 *
	 * @since 4.7.4
	 */
	private $loginUrl = 'https://app.seoboost.com/login/';

	/**
	 * URL of the Create Account page.
	 *
	 * @since 4.7.4
	 */
	private $createAccountUrl = 'https://seoboost.com/checkout/';

	/**
	 * The service.
	 *
	 * @since 4.7.4
	 *
	 * @var Service
	 */
	public $service;

	/**
	 * Class constructor.
	 *
	 * @since 4.7.4
	 */
	public function __construct() {
		$this->service = new Service();

		$returnParam = isset( $_GET['aioseo-writing-assistant'] ) // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			? sanitize_text_field( wp_unslash( $_GET['aioseo-writing-assistant'] ) ) // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			: null;

		if ( 'auth_return' === $returnParam ) {
			add_action( 'init', [ $this, 'checkToken' ], 50 );
		}

		if ( 'ms_logged_in' === $returnParam ) {
			add_action( 'init', [ $this, 'marketingSiteCallback' ], 50 );
		}

		add_action( 'init', [ $this, 'migrateUserData' ], 10 );
		add_action( 'init', [ $this, 'refreshUserOptionsAfterError' ] );
	}

	/**
	 * Returns if the user has an access key.
	 *
	 * @since 4.7.4
	 *
	 * @return bool
	 */
	public function isLoggedIn() {
		return $this->getAccessToken() !== '';
	}

	/**
	 * Gets the login URL.
	 *
	 * @since 4.7.4
	 *
	 * @return string The login URL.
	 */
	public function getLoginUrl() {
		$url = $this->loginUrl;
		if ( defined( 'AIOSEO_WRITING_ASSISTANT_LOGIN_URL' ) ) {
			$url = AIOSEO_WRITING_ASSISTANT_LOGIN_URL;
		}

		$params = [
			'oauth'    => true,
			'redirect' => get_site_url() . '?' . build_query( [ 'aioseo-writing-assistant' => 'auth_return' ] ),
			'domain'   => aioseo()->helpers->getMultiSiteDomain()
		];

		return trailingslashit( $url ) . '?' . build_query( $params );
	}

	/**
	 * Gets the login URL.
	 *
	 * @since 4.7.4
	 *
	 * @return string The login URL.
	 */
	public function getCreateAccountUrl() {
		$url = $this->createAccountUrl;
		if ( defined( 'AIOSEO_WRITING_ASSISTANT_CREATE_ACCOUNT_URL' ) ) {
			$url = AIOSEO_WRITING_ASSISTANT_CREATE_ACCOUNT_URL;
		}

		$params = [
			'url'                        => base64_encode( get_site_url() . '?' . build_query( [ 'aioseo-writing-assistant' => 'ms_logged_in' ] ) ),
			'writing-assistant-checkout' => true
		];

		return trailingslashit( $url ) . '?' . build_query( $params );
	}

	/**
	 * Gets the user's access token.
	 *
	 * @since 4.7.4
	 *
	 * @return string The access token.
	 */
	public function getAccessToken() {
		$metaKey = 'seoboost_access_token_' . get_current_blog_id();

		return get_user_meta( get_current_user_id(), $metaKey, true );
	}

	/**
	 * Sets the user's access token.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	public function setAccessToken( $accessToken ) {
		$metaKey = 'seoboost_access_token_' . get_current_blog_id();
		update_user_meta( get_current_user_id(), $metaKey, $accessToken );

		$this->refreshUserOptions();
	}

	/**
	 * Refreshes user options from SEOBoost.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	public function refreshUserOptions() {
		$userOptions = $this->service->getUserOptions();
		if ( is_wp_error( $userOptions ) || ! empty( $userOptions['error'] ) ) {
			$userOptions = $this->getDefaultUserOptions();

			aioseo()->cache->update( 'seoboost_get_user_options_error', time() + DAY_IN_SECONDS, MONTH_IN_SECONDS );
		}

		$this->setUserOptions( $userOptions );
	}

	/**
	 * Gets the user options.
	 *
	 * @since 4.7.4
	 *
	 * @param  bool  $refresh Whether to refresh the user options.
	 * @return array          The user options.
	 */
	public function getUserOptions( $refresh = false ) {
		if ( ! $refresh ) {
			$metaKey     = 'seoboost_user_options_' . get_current_blog_id();
			$userOptions = get_user_meta( get_current_user_id(), $metaKey, true );

			if ( ! empty( $userOptions ) ) {
				return json_decode( (string) $userOptions, true ) ?? [];
			}
		}

		// If there are no options or we need to refresh them, get them from SEOBoost.
		$this->refreshUserOptions();

		$userOptions = $this->getUserOptions();
		if ( empty( $userOptions ) ) {
			return $this->getDefaultUserOptions();
		}

		return $userOptions;
	}

	/**
	 * Gets the user options.
	 *
	 * @since 4.7.4
	 *
	 * @param  array $options The user options.
	 * @return void
	 */
	public function setUserOptions( $options ) {
		if ( ! is_array( $options ) ) {
			return;
		}

		$metaKey     = 'seoboost_user_options_' . get_current_blog_id();
		$userOptions = array_intersect_key( $options, $this->getDefaultUserOptions() );

		update_user_meta( get_current_user_id(), $metaKey, wp_json_encode( $userOptions ) );
	}

	/**
	 * Gets the user info from SEOBoost.
	 *
	 * @since 4.7.4
	 *
	 * @return array|\WP_Error The user info or a WP_Error.
	 */
	public function getUserInfo() {
		return $this->service->getUserInfo();
	}

	/**
	 * Checks the token.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	public function checkToken() {
		$authToken = isset( $_GET['token'] ) // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			? sanitize_key( wp_unslash( $_GET['token'] ) ) // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			: null;

		if ( $authToken ) {
			$accessToken = $this->service->getAccessToken( $authToken );

			if ( ! is_wp_error( $accessToken ) && ! empty( $accessToken['token'] ) ) {
				$this->setAccessToken( $accessToken['token'] );
				?>
				<script>
					// Send message to parent window.
					window.opener.postMessage('seoboost-authenticated', '*');
				</script>
				<?php
			}
		}
		?>
		<script>
			// Close window.
			window.close();
		</script>
		<?php
		die;
	}

	/**
	 * Handles the callback from the marketing site after completing authentication.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	public function marketingSiteCallback() {
		?>
		<script>
			// Send message to parent window.
			window.opener.postMessage('seoboost-ms-logged-in', '*');
			window.close();
		</script>
		<?php
	}

	/**
	 * Resets the logins.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	public function resetLogins() {
		// Delete access token and user options from the database.
		aioseo()->core->db->delete( 'usermeta' )->whereRaw( 'meta_key LIKE \'seoboost_access_token%\'' )->run();
		aioseo()->core->db->delete( 'usermeta' )->where( 'meta_key', 'seoboost_user_options' )->run();
	}

	/**
	 * Gets the report history.
	 *
	 * @since 4.7.4
	 *
	 * @return array|\WP_Error The report history.
	 */
	public function getReportHistory() {
		return $this->service->getReportHistory();
	}

	/**
	 * Migrate Writing Assistant access tokens.
	 * This handles the fix for multisites where subsites all used the same workspace/account.
	 *
	 * @since 4.7.7
	 *
	 * @return void
	 */
	public function migrateUserData() {
		$userToken = get_user_meta( get_current_user_id(), 'seoboost_access_token', true );
		if ( ! empty( $userToken ) ) {
			$this->setAccessToken( $userToken );
			delete_user_meta( get_current_user_id(), 'seoboost_access_token' );
		}

		$userOptions = get_user_meta( get_current_user_id(), 'seoboost_user_options', true );
		if ( ! empty( $userOptions ) ) {
			$this->setUserOptions( $userOptions );
			delete_user_meta( get_current_user_id(), 'seoboost_user_options' );
		}
	}

	/**
	 * Refreshes user options after an error.
	 * This needs to run on init since service class is not available in the constructor.
	 *
	 * @since 4.7.7.2
	 *
	 * @return void
	 */
	public function refreshUserOptionsAfterError() {
		$userOptionsFetchError = aioseo()->cache->get( 'seoboost_get_user_options_error' );
		if ( $userOptionsFetchError && time() > $userOptionsFetchError ) {
			aioseo()->cache->delete( 'seoboost_get_user_options_error' );

			$this->refreshUserOptions();
		}
	}

	/**
	 * Returns the default user options.
	 *
	 * @since 4.7.7.1
	 *
	 * @return array The default user options.
	 */
	private function getDefaultUserOptions() {
		return [
			'language' => 'en',
			'country'  => 'US'
		];
	}
}Common/WritingAssistant/SeoBoost/Service.php000064400000014005151536241210015157 0ustar00<?php
namespace AIOSEO\Plugin\Common\WritingAssistant\SeoBoost;

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

/**
 * Service class for SeoBoost.
 *
 * @since 4.7.4
 */
class Service {
	/**
	 * The base URL for the SeoBoost microservice.
	 *
	 * @since 4.7.4
	 *
	 * @var string
	 */
	private $baseUrl = 'https://app.seoboost.com/api/';

	/**
	 * Sends the keyword to be processed.
	 *
	 * @since 4.7.4
	 *
	 * @param  string          $keyword  The keyword.
	 * @param  string          $country  The country code.
	 * @param  string          $language The language code.
	 * @return array|\WP_Error           The response.
	 */
	public function processKeyword( $keyword, $country = 'US', $language = 'en' ) {
		if ( empty( $keyword ) || empty( $country ) || empty( $language ) ) {
			return new \WP_Error( 'service-error', __( 'Missing parameters', 'all-in-one-seo-pack' ) );
		}

		$reportRequest = $this->doRequest( 'waAddNewReport', [
			'params' => [
				'keyword'  => $keyword,
				'country'  => $country,
				'language' => $language
			]
		] );

		if ( is_wp_error( $reportRequest ) ) {
			return $reportRequest;
		}

		if ( empty( $reportRequest ) || empty( $reportRequest['status'] ) ) {
			return new \WP_Error( 'service-error', __( 'Empty response from service', 'all-in-one-seo-pack' ) );
		}

		if ( 'success' !== $reportRequest['status'] ) {
			return new \WP_Error( 'service-error', $reportRequest['msg'] );
		}

		return $reportRequest;
	}

	/**
	 * Sends a post content to be analyzed.
	 *
	 * @since 4.7.4
	 *
	 * @param  string          $title       The title.
	 * @param  string          $description The description.
	 * @param  string          $content     The content.
	 * @param  string          $reportSlug  The report slug.
	 * @return array|\WP_Error              The response.
	 */
	public function getContentAnalysis( $title, $description, $content, $reportSlug ) {
		return $this->doRequest( 'waAnalyzeContent', [
			'title'       => $title,
			'description' => $description,
			'content'     => $content,
			'slug'        => $reportSlug
		] );
	}

	/**
	 * Gets the progress for a keyword.
	 *
	 * @since 4.7.4
	 *
	 * @param  string          $uuid The uuid.
	 * @return array|\WP_Error       The progress.
	 */
	public function getProgressAndResult( $uuid ) {
		$response = $this->doRequest( 'waGetReport', [ 'slug' => $uuid ] );

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		if ( empty( $response ) ) {
			return new \WP_Error( 'empty-progress-and-result', __( 'Empty progress and result.', 'all-in-one-seo-pack' ) );
		}

		return $response;
	}

	/**
	 * Gets the user options.
	 *
	 * @since 4.7.4
	 *
	 * @return array|\WP_Error The user options.
	 */
	public function getUserOptions() {
		return $this->doRequest( 'waGetUserOptions' );
	}

	/**
	 * Gets the user information.
	 *
	 * @since 4.7.4
	 *
	 * @return array|\WP_Error The user information.
	 */
	public function getUserInfo() {
		return $this->doRequest( 'waGetUserInfo' );
	}

	/**
	 * Gets the access token.
	 *
	 * @since 4.7.4
	 *
	 * @param  string          $authToken The auth token.
	 * @return array|\WP_Error            The response.
	 */
	public function getAccessToken( $authToken ) {
		return $this->doRequest( 'oauthaccess', [ 'token' => $authToken ] );
	}

	/**
	 * Refreshes the access token.
	 *
	 * @since 4.7.4
	 *
	 * @return bool Was the token refreshed?
	 */
	private function refreshAccessToken() {
		$newAccessToken = $this->doRequest( 'waRefreshAccessToken' );
		if (
			is_wp_error( $newAccessToken ) ||
			'success' !== $newAccessToken['status']
		) {
			aioseo()->writingAssistant->seoBoost->setAccessToken( '' );

			return false;
		}

		aioseo()->writingAssistant->seoBoost->setAccessToken( $newAccessToken['token'] );

		return true;
	}

	/**
	 * Sends a POST request to the microservice.
	 *
	 * @since 4.7.4
	 *
	 * @param  string          $path        The path.
	 * @param  array           $requestBody The request body.
	 * @return array|\WP_Error              Returns the response body or WP_Error if the request failed.
	 */
	private function doRequest( $path, $requestBody = [] ) {
		// Prevent API requests if no access token is present.
		if (
			'oauthaccess' !== $path && // Except if we're getting the access token.
			empty( aioseo()->writingAssistant->seoBoost->getAccessToken() )
		) {
			return new \WP_Error( 'service-error', __( 'Missing access token', 'all-in-one-seo-pack' ) );
		}

		$requestData = [
			'headers' => [
				'X-SeoBoost-Access-Token' => aioseo()->writingAssistant->seoBoost->getAccessToken(),
				'X-SeoBoost-Domain'       => aioseo()->helpers->getMultiSiteDomain(),
				'Content-Type'            => 'application/json'
			],
			'timeout' => 60,
			'method'  => 'GET'
		];

		if ( ! empty( $requestBody ) ) {
			$requestData['method'] = 'POST';
			$requestData['body']   = wp_json_encode( $requestBody );
		}

		$path         = trailingslashit( $this->getUrl() ) . trailingslashit( $path );
		$response     = wp_remote_request( $path, $requestData );
		$responseBody = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( ! $responseBody ) {
			$response = new \WP_Error( 'service-failed', __( 'Error in the SeoBoost service. Please contact support.', 'all-in-one-seo-pack' ) );
		}

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		// Refresh access token if expired and redo the request.
		if (
			isset( $responseBody['error'] ) &&
			'invalid-access-token' === $responseBody['error']
		) {
			if ( $this->refreshAccessToken() ) {
				return $this->doRequest( $path, $requestBody );
			}
		}

		return $responseBody;
	}

	/**
	 * Returns the URL for the Writing Assistant service.
	 *
	 * @since 4.7.4
	 *
	 * @return string The URL.
	 */
	public function getUrl() {
		$url = $this->baseUrl;
		if ( defined( 'AIOSEO_WRITING_ASSISTANT_SERVICE_URL' ) ) {
			$url = AIOSEO_WRITING_ASSISTANT_SERVICE_URL;
		}

		return $url;
	}

	/**
	 * Gets the report history.
	 *
	 * @since 4.7.4
	 *
	 * @return array|\WP_Error
	 */
	public function getReportHistory() {
		return $this->doRequest( 'waGetReportHistory' );
	}
}Common/WritingAssistant/Utils/Helpers.php000064400000043000151536241210014521 0ustar00<?php
namespace AIOSEO\Plugin\Common\WritingAssistant\Utils;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Helper functions.
 *
 * @since 4.7.4
 */
class Helpers {
	/**
	 * Gets the data for vue.
	 *
	 * @since 4.7.4
	 *
	 * @return array An array of data.
	 */
	public function getStandaloneVueData() {
		$keyword = Models\WritingAssistantPost::getKeyword( get_the_ID() );

		return [
			'postId'          => get_the_ID(),
			'report'          => $keyword,
			'keywordText'     => ! empty( $keyword->keyword ) ? $keyword->keyword : '',
			'contentAnalysis' => Models\WritingAssistantPost::getContentAnalysis( get_the_ID() ),
			'seoBoost'        => [
				'isLoggedIn'       => aioseo()->writingAssistant->seoBoost->isLoggedIn(),
				'loginUrl'         => aioseo()->writingAssistant->seoBoost->getLoginUrl(),
				'createAccountUrl' => aioseo()->writingAssistant->seoBoost->getCreateAccountUrl(),
				'userOptions'      => aioseo()->writingAssistant->seoBoost->getUserOptions()
			]
		];
	}

	/**
	 * Gets the data for vue.
	 *
	 * @since 4.7.4
	 *
	 * @return array An array of data.
	 */
	public function getSettingsVueData() {
		return [
			'seoBoost' => [
				'isLoggedIn'       => aioseo()->writingAssistant->seoBoost->isLoggedIn(),
				'loginUrl'         => aioseo()->writingAssistant->seoBoost->getLoginUrl(),
				'createAccountUrl' => aioseo()->writingAssistant->seoBoost->getCreateAccountUrl(),
				'userOptions'      => aioseo()->writingAssistant->seoBoost->getUserOptions(),
				'countries'        => $this->getCountries(),
				'languages'        => $this->getLanguages(),
				'searchEngines'    => $this->getSearchEngines()
			]
		];
	}

	/**
	 * Returns the list of countries.
	 *
	 * @since 4.7.7.1
	 * @version 4.8.3 Moved from SeoBoost/SeoBoost.php
	 *
	 * @return array The list of countries.
	 */
	private function getCountries() {
		$countries = [
			'AF' => __( 'Afghanistan', 'all-in-one-seo-pack' ),
			'AL' => __( 'Albania', 'all-in-one-seo-pack' ),
			'DZ' => __( 'Algeria', 'all-in-one-seo-pack' ),
			'AS' => __( 'American Samoa', 'all-in-one-seo-pack' ),
			'AD' => __( 'Andorra', 'all-in-one-seo-pack' ),
			'AO' => __( 'Angola', 'all-in-one-seo-pack' ),
			'AI' => __( 'Anguilla', 'all-in-one-seo-pack' ),
			'AG' => __( 'Antigua & Barbuda', 'all-in-one-seo-pack' ),
			'AR' => __( 'Argentina', 'all-in-one-seo-pack' ),
			'AM' => __( 'Armenia', 'all-in-one-seo-pack' ),
			'AU' => __( 'Australia', 'all-in-one-seo-pack' ),
			'AT' => __( 'Austria', 'all-in-one-seo-pack' ),
			'AZ' => __( 'Azerbaijan', 'all-in-one-seo-pack' ),
			'BS' => __( 'Bahamas', 'all-in-one-seo-pack' ),
			'BH' => __( 'Bahrain', 'all-in-one-seo-pack' ),
			'BD' => __( 'Bangladesh', 'all-in-one-seo-pack' ),
			'BY' => __( 'Belarus', 'all-in-one-seo-pack' ),
			'BE' => __( 'Belgium', 'all-in-one-seo-pack' ),
			'BZ' => __( 'Belize', 'all-in-one-seo-pack' ),
			'BJ' => __( 'Benin', 'all-in-one-seo-pack' ),
			'BT' => __( 'Bhutan', 'all-in-one-seo-pack' ),
			'BO' => __( 'Bolivia', 'all-in-one-seo-pack' ),
			'BA' => __( 'Bosnia & Herzegovina', 'all-in-one-seo-pack' ),
			'BW' => __( 'Botswana', 'all-in-one-seo-pack' ),
			'BR' => __( 'Brazil', 'all-in-one-seo-pack' ),
			'VG' => __( 'British Virgin Islands', 'all-in-one-seo-pack' ),
			'BN' => __( 'Brunei', 'all-in-one-seo-pack' ),
			'BG' => __( 'Bulgaria', 'all-in-one-seo-pack' ),
			'BF' => __( 'Burkina Faso', 'all-in-one-seo-pack' ),
			'BI' => __( 'Burundi', 'all-in-one-seo-pack' ),
			'KH' => __( 'Cambodia', 'all-in-one-seo-pack' ),
			'CM' => __( 'Cameroon', 'all-in-one-seo-pack' ),
			'CA' => __( 'Canada', 'all-in-one-seo-pack' ),
			'CV' => __( 'Cape Verde', 'all-in-one-seo-pack' ),
			'CF' => __( 'Central African Republic', 'all-in-one-seo-pack' ),
			'TD' => __( 'Chad', 'all-in-one-seo-pack' ),
			'CL' => __( 'Chile', 'all-in-one-seo-pack' ),
			'CO' => __( 'Colombia', 'all-in-one-seo-pack' ),
			'CG' => __( 'Congo - Brazzaville', 'all-in-one-seo-pack' ),
			'CD' => __( 'Congo - Kinshasa', 'all-in-one-seo-pack' ),
			'CK' => __( 'Cook Islands', 'all-in-one-seo-pack' ),
			'CR' => __( 'Costa Rica', 'all-in-one-seo-pack' ),
			'CI' => __( 'Côte d’Ivoire', 'all-in-one-seo-pack' ),
			'HR' => __( 'Croatia', 'all-in-one-seo-pack' ),
			'CU' => __( 'Cuba', 'all-in-one-seo-pack' ),
			'CY' => __( 'Cyprus', 'all-in-one-seo-pack' ),
			'CZ' => __( 'Czechia', 'all-in-one-seo-pack' ),
			'DK' => __( 'Denmark', 'all-in-one-seo-pack' ),
			'DJ' => __( 'Djibouti', 'all-in-one-seo-pack' ),
			'DM' => __( 'Dominica', 'all-in-one-seo-pack' ),
			'DO' => __( 'Dominican Republic', 'all-in-one-seo-pack' ),
			'EC' => __( 'Ecuador', 'all-in-one-seo-pack' ),
			'EG' => __( 'Egypt', 'all-in-one-seo-pack' ),
			'SV' => __( 'El Salvador', 'all-in-one-seo-pack' ),
			'EE' => __( 'Estonia', 'all-in-one-seo-pack' ),
			'ET' => __( 'Ethiopia', 'all-in-one-seo-pack' ),
			'FJ' => __( 'Fiji', 'all-in-one-seo-pack' ),
			'FI' => __( 'Finland', 'all-in-one-seo-pack' ),
			'FR' => __( 'France', 'all-in-one-seo-pack' ),
			'GA' => __( 'Gabon', 'all-in-one-seo-pack' ),
			'GM' => __( 'Gambia', 'all-in-one-seo-pack' ),
			'GE' => __( 'Georgia', 'all-in-one-seo-pack' ),
			'DE' => __( 'Germany', 'all-in-one-seo-pack' ),
			'GH' => __( 'Ghana', 'all-in-one-seo-pack' ),
			'GI' => __( 'Gibraltar', 'all-in-one-seo-pack' ),
			'GR' => __( 'Greece', 'all-in-one-seo-pack' ),
			'GL' => __( 'Greenland', 'all-in-one-seo-pack' ),
			'GT' => __( 'Guatemala', 'all-in-one-seo-pack' ),
			'GG' => __( 'Guernsey', 'all-in-one-seo-pack' ),
			'GY' => __( 'Guyana', 'all-in-one-seo-pack' ),
			'HT' => __( 'Haiti', 'all-in-one-seo-pack' ),
			'HN' => __( 'Honduras', 'all-in-one-seo-pack' ),
			'HK' => __( 'Hong Kong', 'all-in-one-seo-pack' ),
			'HU' => __( 'Hungary', 'all-in-one-seo-pack' ),
			'IS' => __( 'Iceland', 'all-in-one-seo-pack' ),
			'IN' => __( 'India', 'all-in-one-seo-pack' ),
			'ID' => __( 'Indonesia', 'all-in-one-seo-pack' ),
			'IQ' => __( 'Iraq', 'all-in-one-seo-pack' ),
			'IE' => __( 'Ireland', 'all-in-one-seo-pack' ),
			'IM' => __( 'Isle of Man', 'all-in-one-seo-pack' ),
			'IL' => __( 'Israel', 'all-in-one-seo-pack' ),
			'IT' => __( 'Italy', 'all-in-one-seo-pack' ),
			'JM' => __( 'Jamaica', 'all-in-one-seo-pack' ),
			'JP' => __( 'Japan', 'all-in-one-seo-pack' ),
			'JE' => __( 'Jersey', 'all-in-one-seo-pack' ),
			'JO' => __( 'Jordan', 'all-in-one-seo-pack' ),
			'KZ' => __( 'Kazakhstan', 'all-in-one-seo-pack' ),
			'KE' => __( 'Kenya', 'all-in-one-seo-pack' ),
			'KI' => __( 'Kiribati', 'all-in-one-seo-pack' ),
			'KW' => __( 'Kuwait', 'all-in-one-seo-pack' ),
			'KG' => __( 'Kyrgyzstan', 'all-in-one-seo-pack' ),
			'LA' => __( 'Laos', 'all-in-one-seo-pack' ),
			'LV' => __( 'Latvia', 'all-in-one-seo-pack' ),
			'LB' => __( 'Lebanon', 'all-in-one-seo-pack' ),
			'LS' => __( 'Lesotho', 'all-in-one-seo-pack' ),
			'LY' => __( 'Libya', 'all-in-one-seo-pack' ),
			'LI' => __( 'Liechtenstein', 'all-in-one-seo-pack' ),
			'LT' => __( 'Lithuania', 'all-in-one-seo-pack' ),
			'LU' => __( 'Luxembourg', 'all-in-one-seo-pack' ),
			'MG' => __( 'Madagascar', 'all-in-one-seo-pack' ),
			'MW' => __( 'Malawi', 'all-in-one-seo-pack' ),
			'MY' => __( 'Malaysia', 'all-in-one-seo-pack' ),
			'MV' => __( 'Maldives', 'all-in-one-seo-pack' ),
			'ML' => __( 'Mali', 'all-in-one-seo-pack' ),
			'MT' => __( 'Malta', 'all-in-one-seo-pack' ),
			'MU' => __( 'Mauritius', 'all-in-one-seo-pack' ),
			'MX' => __( 'Mexico', 'all-in-one-seo-pack' ),
			'FM' => __( 'Micronesia', 'all-in-one-seo-pack' ),
			'MD' => __( 'Moldova', 'all-in-one-seo-pack' ),
			'MN' => __( 'Mongolia', 'all-in-one-seo-pack' ),
			'ME' => __( 'Montenegro', 'all-in-one-seo-pack' ),
			'MS' => __( 'Montserrat', 'all-in-one-seo-pack' ),
			'MA' => __( 'Morocco', 'all-in-one-seo-pack' ),
			'MZ' => __( 'Mozambique', 'all-in-one-seo-pack' ),
			'MM' => __( 'Myanmar (Burma)', 'all-in-one-seo-pack' ),
			'NA' => __( 'Namibia', 'all-in-one-seo-pack' ),
			'NR' => __( 'Nauru', 'all-in-one-seo-pack' ),
			'NP' => __( 'Nepal', 'all-in-one-seo-pack' ),
			'NL' => __( 'Netherlands', 'all-in-one-seo-pack' ),
			'NZ' => __( 'New Zealand', 'all-in-one-seo-pack' ),
			'NI' => __( 'Nicaragua', 'all-in-one-seo-pack' ),
			'NE' => __( 'Niger', 'all-in-one-seo-pack' ),
			'NG' => __( 'Nigeria', 'all-in-one-seo-pack' ),
			'NU' => __( 'Niue', 'all-in-one-seo-pack' ),
			'MK' => __( 'North Macedonia', 'all-in-one-seo-pack' ),
			'NO' => __( 'Norway', 'all-in-one-seo-pack' ),
			'OM' => __( 'Oman', 'all-in-one-seo-pack' ),
			'PK' => __( 'Pakistan', 'all-in-one-seo-pack' ),
			'PS' => __( 'Palestine', 'all-in-one-seo-pack' ),
			'PA' => __( 'Panama', 'all-in-one-seo-pack' ),
			'PG' => __( 'Papua New Guinea', 'all-in-one-seo-pack' ),
			'PY' => __( 'Paraguay', 'all-in-one-seo-pack' ),
			'PE' => __( 'Peru', 'all-in-one-seo-pack' ),
			'PH' => __( 'Philippines', 'all-in-one-seo-pack' ),
			'PN' => __( 'Pitcairn Islands', 'all-in-one-seo-pack' ),
			'PL' => __( 'Poland', 'all-in-one-seo-pack' ),
			'PT' => __( 'Portugal', 'all-in-one-seo-pack' ),
			'PR' => __( 'Puerto Rico', 'all-in-one-seo-pack' ),
			'QA' => __( 'Qatar', 'all-in-one-seo-pack' ),
			'RO' => __( 'Romania', 'all-in-one-seo-pack' ),
			'RU' => __( 'Russia', 'all-in-one-seo-pack' ),
			'RW' => __( 'Rwanda', 'all-in-one-seo-pack' ),
			'WS' => __( 'Samoa', 'all-in-one-seo-pack' ),
			'SM' => __( 'San Marino', 'all-in-one-seo-pack' ),
			'ST' => __( 'São Tomé & Príncipe', 'all-in-one-seo-pack' ),
			'SA' => __( 'Saudi Arabia', 'all-in-one-seo-pack' ),
			'SN' => __( 'Senegal', 'all-in-one-seo-pack' ),
			'RS' => __( 'Serbia', 'all-in-one-seo-pack' ),
			'SC' => __( 'Seychelles', 'all-in-one-seo-pack' ),
			'SL' => __( 'Sierra Leone', 'all-in-one-seo-pack' ),
			'SG' => __( 'Singapore', 'all-in-one-seo-pack' ),
			'SK' => __( 'Slovakia', 'all-in-one-seo-pack' ),
			'SI' => __( 'Slovenia', 'all-in-one-seo-pack' ),
			'SB' => __( 'Solomon Islands', 'all-in-one-seo-pack' ),
			'SO' => __( 'Somalia', 'all-in-one-seo-pack' ),
			'ZA' => __( 'South Africa', 'all-in-one-seo-pack' ),
			'KR' => __( 'South Korea', 'all-in-one-seo-pack' ),
			'ES' => __( 'Spain', 'all-in-one-seo-pack' ),
			'LK' => __( 'Sri Lanka', 'all-in-one-seo-pack' ),
			'SH' => __( 'St. Helena', 'all-in-one-seo-pack' ),
			'VC' => __( 'St. Vincent & Grenadines', 'all-in-one-seo-pack' ),
			'SR' => __( 'Suriname', 'all-in-one-seo-pack' ),
			'SE' => __( 'Sweden', 'all-in-one-seo-pack' ),
			'CH' => __( 'Switzerland', 'all-in-one-seo-pack' ),
			'TW' => __( 'Taiwan', 'all-in-one-seo-pack' ),
			'TJ' => __( 'Tajikistan', 'all-in-one-seo-pack' ),
			'TZ' => __( 'Tanzania', 'all-in-one-seo-pack' ),
			'TH' => __( 'Thailand', 'all-in-one-seo-pack' ),
			'TL' => __( 'Timor-Leste', 'all-in-one-seo-pack' ),
			'TG' => __( 'Togo', 'all-in-one-seo-pack' ),
			'TO' => __( 'Tonga', 'all-in-one-seo-pack' ),
			'TT' => __( 'Trinidad & Tobago', 'all-in-one-seo-pack' ),
			'TN' => __( 'Tunisia', 'all-in-one-seo-pack' ),
			'TR' => __( 'Turkey', 'all-in-one-seo-pack' ),
			'TM' => __( 'Turkmenistan', 'all-in-one-seo-pack' ),
			'VI' => __( 'U.S. Virgin Islands', 'all-in-one-seo-pack' ),
			'UG' => __( 'Uganda', 'all-in-one-seo-pack' ),
			'UA' => __( 'Ukraine', 'all-in-one-seo-pack' ),
			'AE' => __( 'United Arab Emirates', 'all-in-one-seo-pack' ),
			'GB' => __( 'United Kingdom', 'all-in-one-seo-pack' ),
			'US' => __( 'United States', 'all-in-one-seo-pack' ),
			'UY' => __( 'Uruguay', 'all-in-one-seo-pack' ),
			'UZ' => __( 'Uzbekistan', 'all-in-one-seo-pack' ),
			'VU' => __( 'Vanuatu', 'all-in-one-seo-pack' ),
			'VE' => __( 'Venezuela', 'all-in-one-seo-pack' ),
			'VN' => __( 'Vietnam', 'all-in-one-seo-pack' ),
			'ZM' => __( 'Zambia', 'all-in-one-seo-pack' ),
			'ZW' => __( 'Zimbabwe', 'all-in-one-seo-pack' )
		];

		return $countries;
	}

	/**
	 * Returns the list of languages.
	 *
	 * @since 4.7.7.1
	 * @version 4.8.3 Moved from SeoBoost/SeoBoost.php
	 *
	 * @return array The list of languages.
	 */
	private function getLanguages() {
		$languages = [
			'ca' => __( 'Catalan', 'all-in-one-seo-pack' ),
			'da' => __( 'Danish', 'all-in-one-seo-pack' ),
			'nl' => __( 'Dutch', 'all-in-one-seo-pack' ),
			'en' => __( 'English', 'all-in-one-seo-pack' ),
			'fr' => __( 'French', 'all-in-one-seo-pack' ),
			'de' => __( 'German', 'all-in-one-seo-pack' ),
			'id' => __( 'Indonesian', 'all-in-one-seo-pack' ),
			'it' => __( 'Italian', 'all-in-one-seo-pack' ),
			'no' => __( 'Norwegian', 'all-in-one-seo-pack' ),
			'pt' => __( 'Portuguese', 'all-in-one-seo-pack' ),
			'ro' => __( 'Romanian', 'all-in-one-seo-pack' ),
			'es' => __( 'Spanish', 'all-in-one-seo-pack' ),
			'sv' => __( 'Swedish', 'all-in-one-seo-pack' ),
			'tr' => __( 'Turkish', 'all-in-one-seo-pack' )
		];

		return $languages;
	}

	/**
	 * Returns the list of search engines.
	 *
	 * @since 4.7.7.1
	 * @version 4.8.3 Moved from SeoBoost/SeoBoost.php
	 *
	 * @return array The list of search engines.
	 */
	private function getSearchEngines() {
		$searchEngines = [
			'AF' => 'google.com.af',
			'AL' => 'google.al',
			'DZ' => 'google.dz',
			'AS' => 'google.as',
			'AD' => 'google.ad',
			'AO' => 'google.it.ao',
			'AI' => 'google.com.ai',
			'AG' => 'google.com.ag',
			'AR' => 'google.com.ar',
			'AM' => 'google.am',
			'AU' => 'google.com.au',
			'AT' => 'google.at',
			'AZ' => 'google.az',
			'BS' => 'google.bs',
			'BH' => 'google.com.bh',
			'BD' => 'google.com.bd',
			'BY' => 'google.com.by',
			'BE' => 'google.be',
			'BZ' => 'google.com.bz',
			'BJ' => 'google.bj',
			'BT' => 'google.bt',
			'BO' => 'google.com.bo',
			'BA' => 'google.ba',
			'BW' => 'google.co.bw',
			'BR' => 'google.com.br',
			'VG' => 'google.vg',
			'BN' => 'google.com.bn',
			'BG' => 'google.bg',
			'BF' => 'google.bf',
			'BI' => 'google.bi',
			'KH' => 'google.com.kh',
			'CM' => 'google.cm',
			'CA' => 'google.ca',
			'CV' => 'google.cv',
			'CF' => 'google.cf',
			'TD' => 'google.td',
			'CL' => 'google.cl',
			'CO' => 'google.com.co',
			'CG' => 'google.cg',
			'CD' => 'google.cd',
			'CK' => 'google.co.ck',
			'CR' => 'google.co.cr',
			'CI' => 'google.ci',
			'HR' => 'google.hr',
			'CU' => 'google.com.cu',
			'CY' => 'google.com.cy',
			'CZ' => 'google.cz',
			'DK' => 'google.dk',
			'DJ' => 'google.dj',
			'DM' => 'google.dm',
			'DO' => 'google.com.do',
			'EC' => 'google.com.ec',
			'EG' => 'google.com.eg',
			'SV' => 'google.com.sv',
			'EE' => 'google.ee',
			'ET' => 'google.com.et',
			'FJ' => 'google.com.fj',
			'FI' => 'google.fi',
			'FR' => 'google.fr',
			'GA' => 'google.ga',
			'GM' => 'google.gm',
			'GE' => 'google.ge',
			'DE' => 'google.de',
			'GH' => 'google.com.gh',
			'GI' => 'google.com.gi',
			'GR' => 'google.gr',
			'GL' => 'google.gl',
			'GT' => 'google.com.gt',
			'GG' => 'google.gg',
			'GY' => 'google.gy',
			'HT' => 'google.ht',
			'HN' => 'google.hn',
			'HK' => 'google.com.hk',
			'HU' => 'google.hu',
			'IS' => 'google.is',
			'IN' => 'google.co.in',
			'ID' => 'google.co.id',
			'IQ' => 'google.iq',
			'IE' => 'google.ie',
			'IM' => 'google.co.im',
			'IL' => 'google.co.il',
			'IT' => 'google.it',
			'JM' => 'google.com.jm',
			'JP' => 'google.co.jp',
			'JE' => 'google.co.je',
			'JO' => 'google.jo',
			'KZ' => 'google.kz',
			'KE' => 'google.co.ke',
			'KI' => 'google.ki',
			'KW' => 'google.com.kw',
			'KG' => 'google.com.kg',
			'LA' => 'google.la',
			'LV' => 'google.lv',
			'LB' => 'google.com.lb',
			'LS' => 'google.co.ls',
			'LY' => 'google.com.ly',
			'LI' => 'google.li',
			'LT' => 'google.lt',
			'LU' => 'google.lu',
			'MG' => 'google.mg',
			'MW' => 'google.mw',
			'MY' => 'google.com.my',
			'MV' => 'google.mv',
			'ML' => 'google.ml',
			'MT' => 'google.com.mt',
			'MU' => 'google.mu',
			'MX' => 'google.com.mx',
			'FM' => 'google.fm',
			'MD' => 'google.md',
			'MN' => 'google.mn',
			'ME' => 'google.me',
			'MS' => 'google.ms',
			'MA' => 'google.co.ma',
			'MZ' => 'google.co.mz',
			'MM' => 'google.com.mm',
			'NA' => 'google.com.na',
			'NR' => 'google.nr',
			'NP' => 'google.com.np',
			'NL' => 'google.nl',
			'NZ' => 'google.co.nz',
			'NI' => 'google.com.ni',
			'NE' => 'google.ne',
			'NG' => 'google.com.ng',
			'NU' => 'google.nu',
			'MK' => 'google.mk',
			'NO' => 'google.no',
			'OM' => 'google.com.om',
			'PK' => 'google.com.pk',
			'PS' => 'google.ps',
			'PA' => 'google.com.pa',
			'PG' => 'google.com.pg',
			'PY' => 'google.com.py',
			'PE' => 'google.com.pe',
			'PH' => 'google.com.ph',
			'PN' => 'google.pn',
			'PL' => 'google.pl',
			'PT' => 'google.pt',
			'PR' => 'google.com.pr',
			'QA' => 'google.com.qa',
			'RO' => 'google.ro',
			'RU' => 'google.ru',
			'RW' => 'google.rw',
			'WS' => 'google.as',
			'SM' => 'google.sm',
			'ST' => 'google.st',
			'SA' => 'google.com.sa',
			'SN' => 'google.sn',
			'RS' => 'google.rs',
			'SC' => 'google.sc',
			'SL' => 'google.com.sl',
			'SG' => 'google.com.sg',
			'SK' => 'google.sk',
			'SI' => 'google.si',
			'SB' => 'google.com.sb',
			'SO' => 'google.so',
			'ZA' => 'google.co.za',
			'KR' => 'google.co.kr',
			'ES' => 'google.es',
			'LK' => 'google.lk',
			'SH' => 'google.sh',
			'VC' => 'google.com.vc',
			'SR' => 'google.sr',
			'SE' => 'google.se',
			'CH' => 'google.ch',
			'TW' => 'google.com.tw',
			'TJ' => 'google.com.tj',
			'TZ' => 'google.co.tz',
			'TH' => 'google.co.th',
			'TL' => 'google.tl',
			'TG' => 'google.tg',
			'TO' => 'google.to',
			'TT' => 'google.tt',
			'TN' => 'google.tn',
			'TR' => 'google.com.tr',
			'TM' => 'google.tm',
			'VI' => 'google.co.vi',
			'UG' => 'google.co.ug',
			'UA' => 'google.com.ua',
			'AE' => 'google.ae',
			'GB' => 'google.co.uk',
			'US' => 'google.com',
			'UY' => 'google.com.uy',
			'UZ' => 'google.co.uz',
			'VU' => 'google.vu',
			'VE' => 'google.co.ve',
			'VN' => 'google.com.vn',
			'ZM' => 'google.co.zm',
			'ZW' => 'google.co.zw'
		];

		return $searchEngines;
	}
}Common/WritingAssistant/WritingAssistant.php000064400000001102151536241210015331 0ustar00<?php
namespace AIOSEO\Plugin\Common\WritingAssistant;

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

/**
 * Main class.
 *
 * @since 4.7.4
 */
class WritingAssistant {
	/**
	 * Helpers.
	 *
	 * @since 4.7.4
	 *
	 * @var Utils\Helpers
	 */
	public $helpers;

	/**
	 * SeoBoost.
	 *
	 * @since 4.7.4
	 *
	 * @var SeoBoost\SeoBoost
	 */
	public $seoBoost;

	/**
	 * Load our classes.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	public function __construct() {
		$this->helpers  = new Utils\Helpers();
		$this->seoBoost = new SeoBoost\SeoBoost();
	}
}Lite/Admin/Admin.php000064400000005132151536241210010233 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Admin;

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

use AIOSEO\Plugin\Common\Admin as CommonAdmin;

/**
 * Abstract class that Pro and Lite both extend.
 *
 * @since 4.0.0
 */
class Admin extends CommonAdmin\Admin {
	/**
	 * Connect class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Connect
	 */
	public $connect = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		if ( ! wp_doing_cron() ) {
			parent::__construct();
		}

		$this->connect = new Connect();
	}

	/**
	 * Actually adds the menu items to the admin bar.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function addAdminBarMenuItems() {
		// Add an upsell to Pro.
		if ( current_user_can( $this->getPageRequiredCapability( '' ) ) ) {
			$this->adminBarMenuItems['aioseo-pro-upgrade'] = [
				'parent' => 'aioseo-main',
				'title'  => '<span class="aioseo-menu-highlight lite">' . __( 'Upgrade to Pro', 'all-in-one-seo-pack' ) . '</span>',
				'id'     => 'aioseo-pro-upgrade',
				'href'   => apply_filters(
					'aioseo_upgrade_link',
					esc_url( admin_url( 'admin.php?page=aioseo-tools&aioseo-redirect-upgrade=1' ) )
				),
				'meta'   => [ 'target' => '_blank' ],
			];
		}

		parent::addAdminBarMenuItems();
	}

	/**
	 * Add the menu inside of WordPress.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addMenu() {
		parent::addMenu();

		$capability = $this->getPageRequiredCapability( '' );

		// We use the global submenu, because we are adding an external link here.
		if ( current_user_can( $capability ) ) {
			global $submenu;
			$submenu[ $this->pageSlug ][] = [
				'<span class="aioseo-menu-highlight lite">' . esc_html__( 'Upgrade to Pro', 'all-in-one-seo-pack' ) . '</span>',
				$capability,
				apply_filters(
					'aioseo_upgrade_link',
					esc_url( admin_url( 'admin.php?page=aioseo-tools&aioseo-redirect-upgrade=1' ) )
				)
			];
		}
	}

	/**
	 * Check the query args to see if we need to redirect to an external URL.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	protected function checkForRedirects() {
		$mappedUrls = [
			// Added to resolve an issue with the open_basedir in the IIS.

			'aioseo-redirect-upgrade' => apply_filters(
				'aioseo_upgrade_link',
				aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'lite-upgrade/', 'admin-bar', null, false )
			)
		];

		foreach ( $mappedUrls as $queryArg => $redirectUrl ) {
			if ( isset( $_GET[ $queryArg ] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
				wp_redirect( $redirectUrl );
			}
		}
	}
}Lite/Admin/Connect.php000064400000027050151536241210010577 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Admin;

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

use AIOSEO\Plugin\Common\Utils;

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

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

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

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

		// Check for connect-specific parameter.
		// phpcs:disable HM.Security.ValidatedSanitizedInput.InputNotSanitized, HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended, Generic.Files.LineLength.MaxExceeded
		if ( ! isset( $_GET['page'] ) ) {
			return;
		}

		$page = sanitize_text_field( wp_unslash( $_GET['page'] ) );
		// phpcs:enable

		// Check if we're on the right page and if current user is allowed to save settings.
		if (
			( 'aioseo-connect-pro' !== $page && 'aioseo-connect' !== $page ) ||
			! current_user_can( 'aioseo_manage_seo' )
		) {
			return;
		}

		set_current_screen();

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

		if ( 'aioseo-connect-pro' === $page ) {
			$this->loadConnectPro();

			return;
		}

		$this->loadConnect();
		// phpcs:enable
	}

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

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

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

		aioseo()->core->assets->load( 'src/vue/standalone/connect/main.js', [], aioseo()->helpers->getVueData() );
	}

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

		aioseo()->core->assets->load( 'src/vue/standalone/connect-pro/main.js', [], aioseo()->helpers->getVueData() );
	}

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

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

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

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

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

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

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

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

		// Generate a hash that can be compared after the user is redirected back.
		$oth       = hash( 'sha512', wp_rand() );
		$hashedOth = hash_hmac( 'sha512', $oth, wp_salt() );

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

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

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

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

	/**
	 * Process AIOSEO Connect.
	 *
	 * @since 1.0.0
	 *
	 * @return array An array containing a valid response or an error message.
	 */
	public function process() {
		// phpcs:disable HM.Security.NonceVerification.Missing, WordPress.Security.NonceVerification
		$hashedOth   = ! empty( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '';
		$downloadUrl = ! empty( $_POST['file'] ) ? esc_url_raw( wp_unslash( $_POST['file'] ) ) : '';
		// phpcs:enable

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

		$success = esc_html__( 'Plugin installed & activated.', 'all-in-one-seo-pack' );

		// Check if all required params are present.
		if ( empty( $downloadUrl ) || empty( $hashedOth ) ) {
			wp_send_json_error( $error );
		}

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

		// Check if the stored hash matches the salted one that is sent back from the server.
		if ( hash_hmac( 'sha512', $oth, wp_salt() ) !== $hashedOth ) {
			wp_send_json_error( $error );
		}

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

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

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

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

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

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

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

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

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

			wp_send_json_success( $success );
		}

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

		$fs = aioseo()->core->fs->noConflict();
		$fs->init( $creds );
		if ( ! $fs->isWpfsValid() ) {
			wp_send_json_error( $error );
		}

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

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

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

		$installer->install( $downloadUrl );

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

		$pluginBasename = $installer->plugin_info();

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

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

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

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

		wp_send_json_success( $success );
	}
}Lite/Admin/Notices/Notices.php000064400000004706151536241210012221 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Admin\Notices;

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

use AIOSEO\Plugin\Common\Admin\Notices as CommonNotices;
use AIOSEO\Plugin\Common\Models;

/**
 * Lite version of the notices class.
 *
 * @since 4.0.0
 */
class Notices extends CommonNotices\Notices {
	/**
	 * Initialize the internal notices.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function initInternalNotices() {
		parent::initInternalNotices();

		$this->wooUpsellNotice();
	}

	/**
	 * Validates the notification type.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $type The notification type we are targeting.
	 * @return boolean       True if yes, false if no.
	 */
	public function validateType( $type ) {
		$validated = parent::validateType( $type );

		// Any lite notification should pass here.
		if ( 'lite' === $type ) {
			$validated = true;
		}

		return $validated;
	}

	/**
	 * Add a notice if WooCommerce is detected and not licensed or running Lite.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function wooUpsellNotice() {
		$notification = Models\Notification::getNotificationByName( 'woo-upsell' );

		if (
			! class_exists( 'WooCommerce' )
		) {
			if ( $notification->exists() ) {
				Models\Notification::deleteNotificationByName( 'woo-upsell' );
			}

			return;
		}

		if ( $notification->exists() ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'woo-upsell',
			// Translators: 1 - "WooCommerce".
			'title'             => sprintf( __( 'Advanced %1$s Support', 'all-in-one-seo-pack' ), 'WooCommerce' ),
			// Translators: 1 - "WooCommerce", 2 - The plugin short name ("AIOSEO").
			'content'           => sprintf( __( 'We have detected you are running %1$s. Upgrade to %2$s to unlock our advanced eCommerce SEO features, including SEO for Product Categories and more.', 'all-in-one-seo-pack' ), 'WooCommerce', AIOSEO_PLUGIN_SHORT_NAME . ' Pro' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'type'              => 'info',
			'level'             => [ 'all' ],
			// Translators: 1 - "Pro".
			'button1_label'     => sprintf( __( 'Upgrade to %1$s', 'all-in-one-seo-pack' ), 'Pro' ),
			'button1_action'    => html_entity_decode( apply_filters( 'aioseo_upgrade_link', aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'lite-upgrade/', 'woo-notification-upsell', false ) ) ),
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}
}Lite/Admin/PostSettings.php000064400000003366151536241210011660 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Admin;

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

use AIOSEO\Plugin\Common\Admin as CommonAdmin;

/**
 * Abstract class that Pro and Lite both extend.
 *
 * @since 4.0.0
 */
class PostSettings extends CommonAdmin\PostSettings {
	/**
	 * Holds a list of page builder integration class instances.
	 * This prop exists for backwards compatibility with pre-4.2.0 versions (see backwardsCompatibilityLoad() in AIOSEO.php).
	 *
	 * @since 4.4.2
	 *
	 * @var object[]
	 */
	public $integrations = null;

	/**
	 * Initialize the admin.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function __construct() {
		parent::__construct();
	}

	/**
	 * Add upsell to terms.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function init() {
		if ( is_admin() ) {
			// We don't call getPublicTaxonomies() here because we want to show the CTA for Product Attributes as well.
			$taxonomies = get_taxonomies( [], 'objects' );
			foreach ( $taxonomies as $taxObject ) {
				if (
					empty( $taxObject->label ) ||
					! is_taxonomy_viewable( $taxObject )
				) {
					unset( $taxonomies[ $taxObject->name ] );
				}
			}

			foreach ( $taxonomies as $taxonomy ) {
				add_action( $taxonomy->name . '_edit_form', [ $this, 'addTaxonomyUpsell' ] );
				add_action( 'after-' . $taxonomy->name . '-table', [ $this, 'addTaxonomyUpsell' ] );
			}
		}
	}

	/**
	 * Add Taxonomy Upsell
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addTaxonomyUpsell() {
		$screen = aioseo()->helpers->getCurrentScreen();
		if (
			! isset( $screen->parent_base ) ||
			'edit' !== $screen->parent_base ||
			empty( $screen->taxonomy )
		) {
			return;
		}

		include_once AIOSEO_DIR . '/app/Lite/Views/taxonomy-upsell.php';
	}
}Lite/Admin/Usage.php000064400000001177151536241210010254 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Admin;

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

use AIOSEO\Plugin\Common\Admin as CommonAdmin;

/**
 * Usage tracking class.
 *
 * @since 4.0.0
 */
class Usage extends CommonAdmin\Usage {
	/**
	 * Class Constructor
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		parent::__construct();

		$this->enabled = apply_filters( 'aioseo_usage_tracking_enable', aioseo()->options->advanced->usageTracking );
	}

	/**
	 * Get the type for the request.
	 *
	 * @since 4.0.0
	 *
	 * @return string The install type.
	 */
	public function getType() {
		return 'lite';
	}
}Lite/Api/Api.php000064400000001351151536241210007374 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 );
	}
}Lite/Api/Wizard.php000064400000002124151536241210010122 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;
	}
}Lite/Main/Filters.php000064400000005774151536241210010463 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Main;

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

use AIOSEO\Plugin\Common\Main as CommonMain;

/**
 * Filters class with methods that are called.
 *
 * @since 4.0.0
 */
class Filters extends CommonMain\Filters {
	/**
	 * Registers our row meta for the plugins page.
	 *
	 * @since 4.0.0
	 *
	 * @param  array  $actions    List of existing actions.
	 * @param  string $pluginFile The plugin file.
	 * @return array              List of action links.
	 */
	public function pluginRowMeta( $actions, $pluginFile = '' ) {
		$reviewLabel = str_repeat( '<span class="dashicons dashicons-star-filled" style="font-size: 18px; width:16px; height: 16px; color: #ffb900;"></span>', 5 );

		$actionLinks = [
			'suggest-feature' => [
				// Translators: This is an action link users can click to open a feature request.
				'label' => __( 'Suggest a Feature', 'all-in-one-seo-pack' ),
				'url'   => aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'suggest-a-feature/', 'plugin-row-meta', 'feature' ),
			],
			'review'          => [
				'label' => $reviewLabel,
				'url'   => aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'review-aioseo', 'plugin-row-meta', 'review' ),
				'title' => sprintf(
					// Translators: 1 - The plugin short name ("AIOSEO").
					__( 'Rate %1$s', 'all-in-one-seo-pack' ),
					'AIOSEO'
				)
			]
		];

		return $this->parseActionLinks( $actions, $pluginFile, $actionLinks );
	}

	/**
	 * Registers our action links for the plugins page.
	 *
	 * @since 4.0.0
	 *
	 * @param  array  $actions    List of existing actions.
	 * @param  string $pluginFile The plugin file.
	 * @return array              List of action links.
	 */
	public function pluginActionLinks( $actions, $pluginFile = '' ) {
		$actionLinks = [
			'settings'   => [
				'label' => __( 'SEO Settings', 'all-in-one-seo-pack' ),
				'url'   => get_admin_url( null, 'admin.php?page=aioseo-settings' ),
			],
			'support'    => [
				// Translators: This is an action link users can click to open our premium support.
				'label' => __( 'Support', 'all-in-one-seo-pack' ),
				'url'   => aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'contact/', 'plugin-action-links', 'Support' ),
			],
			'docs'       => [
				// Translators: This is an action link users can click to open our general documentation page.
				'label' => __( 'Documentation', 'all-in-one-seo-pack' ),
				'url'   => aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'docs/', 'plugin-action-links', 'Documentation' ),
			],
			'proupgrade' => [
				// Translators: This is an action link users can click to purchase a license for All in One SEO Pro.
				'label' => __( 'Upgrade to Pro', 'all-in-one-seo-pack' ),
				'url'   => apply_filters( 'aioseo_upgrade_link', aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'lite-upgrade/', 'plugin-action-links', 'Upgrade', false ) ),
			]
		];

		if ( isset( $actions['edit'] ) ) {
			unset( $actions['edit'] );
		}

		return $this->parseActionLinks( $actions, $pluginFile, $actionLinks, 'before' );
	}
}Lite/Options/InternalOptions.php000064400000002071151536241210012735 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Options;

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

use AIOSEO\Plugin\Common\Options as CommonOptions;
use AIOSEO\Plugin\Lite\Traits;

/**
 * Class that holds all internal options for AIOSEO.
 *
 * @since 4.0.0
 */
class InternalOptions extends CommonOptions\InternalOptions {
	use Traits\Options;

	/**
	 * Defaults options for Lite.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $liteDefaults = [
		// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
		'internal' => [
			'activated'      => [ 'type' => 'number', 'default' => 0 ],
			'firstActivated' => [ 'type' => 'number', 'default' => 0 ],
			'installed'      => [ 'type' => 'number', 'default' => 0 ],
			'connect'        => [
				'key'     => [ 'type' => 'string' ],
				'time'    => [ 'type' => 'number', 'default' => 0 ],
				'network' => [ 'type' => 'boolean', 'default' => false ],
				'token'   => [ 'type' => 'string' ]
			]
		]
		// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	];
}Lite/Options/Options.php000064400000002371151536241210011243 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Options;

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

use AIOSEO\Plugin\Common\Options as CommonOptions;
use AIOSEO\Plugin\Lite\Traits;

/**
 * Class that holds all options for AIOSEO.
 *
 * @since 4.0.0
 */
class Options extends CommonOptions\Options {
	use Traits\Options;

	/**
	 * Defaults options for Lite.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $liteDefaults = [
		// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
		'advanced' => [
			'usageTracking' => [ 'type' => 'boolean', 'default' => false ]
		]
		// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	];

	/**
	 * Sanitizes, then saves the options to the database.
	 *
	 * @since 4.7.2
	 *
	 * @param  array $options An array of options to sanitize, then save.
	 * @return void
	 */
	public function sanitizeAndSave( $options ) {
		if ( isset( $options['advanced']['emailSummary']['recipients'] ) ) {
			$options['advanced']['emailSummary']['recipients']                 = [ array_shift( $options['advanced']['emailSummary']['recipients'] ) ];
			$options['advanced']['emailSummary']['recipients'][0]['frequency'] = 'monthly';
		}

		parent::sanitizeAndSave( $options );
	}
}Lite/Traits/Options.php000064400000005230151536241210011053 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Traits;

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

/**
 * Options trait.
 *
 * @since 4.0.0
 */
trait Options {
	/**
	 * Initialize the options.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function init() {
		parent::init();

		$dbOptions = $this->getDbOptions( $this->optionsName . '_lite' );

		// Refactor options.
		$this->defaultsMerged = array_replace_recursive( $this->defaults, $this->liteDefaults );

		$mergedDefaults = array_replace_recursive(
			$this->liteDefaults,
			$this->addValueToValuesArray( $this->liteDefaults, $dbOptions )
		);

		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$dbOptions     = array_replace_recursive(
			$cachedOptions,
			$mergedDefaults
		);

		aioseo()->core->optionsCache->setOptions( $this->optionsName, $dbOptions );
	}

	/**
	 * Merge defaults with liteDefaults.
	 *
	 * @since 4.1.4
	 *
	 * @return array An array of dafults.
	 */
	public function getDefaults() {
		return array_replace_recursive( parent::getDefaults(), $this->liteDefaults );
	}

	/**
	 * Updates the options in the database.
	 *
	 * @since 4.1.4
	 *
	 * @param  string     $optionsName An optional option name to update.
	 * @param  string     $defaults    The defaults to filter the options by.
	 * @param  array|null $options     An optional options array.
	 * @return void
	 */
	public function update( $optionsName = null, $defaults = null, $options = null ) {
		$optionsName = empty( $optionsName ) ? $this->optionsName . '_lite' : $optionsName;
		$defaults    = empty( $defaults ) ? $this->liteDefaults : $defaults;

		// We're creating a new array here because it was setting it by reference.
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$optionsBefore = json_decode( wp_json_encode( $cachedOptions ), true );

		parent::update( $this->optionsName, $options );
		parent::update( $optionsName, $defaults, $optionsBefore );
	}

	/**
	 * Updates the options in the database.
	 *
	 * @since 4.1.4
	 *
	 * @param  boolean $force       Whether or not to force an immediate save.
	 * @param  string  $optionsName An optional option name to update.
	 * @param  string  $defaults    The defaults to filter the options by.
	 * @return void
	 */
	public function save( $force = false, $optionsName = null, $defaults = null ) {
		if ( ! $this->shouldSave && ! $force ) {
			return;
		}

		$optionsName = empty( $optionsName ) ? $this->optionsName . '_lite' : $optionsName;
		$defaults    = empty( $defaults ) ? $this->liteDefaults : $defaults;

		parent::save( $force, $this->optionsName );
		parent::save( $force, $optionsName, $defaults );
	}
}Lite/Utils/Helpers.php000064400000001314151536241210010653 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Utils;

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

use AIOSEO\Plugin\Common\Utils as CommonUtils;

/**
 * Contains helper functions.
 *
 * @since 4.2.4
 */
class Helpers extends CommonUtils\Helpers {
	/**
	 * Get the headers for internal API requests.
	 *
	 * @since 4.2.4
	 *
	 * @return array An array of headers.
	 */
	public function getApiHeaders() {
		return [];
	}

	/**
	 * Get the User Agent for internal API requests.
	 *
	 * @since 4.2.4
	 *
	 * @return string The User Agent.
	 */
	public function getApiUserAgent() {
		return 'WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' ) . '; AIOSEO/Lite/' . AIOSEO_VERSION;
	}
}Lite/Views/taxonomy-upsell.php000064400000133476151536241210012445 0ustar00<?php
// phpcs:disable Generic.Files.LineLength.MaxExceeded

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<style>
	#poststuff.aioseo-taxonomy-upsell {
		min-width: auto;
		overflow: hidden;
	}
</style>
<div id="poststuff" class="aioseo-taxonomy-upsell" style="margin-top:30px;max-width: 800px;">
	<div id="advanced-sortables" class="meta-box-sortables">
		<div id="aioseo-tabbed" class="postbox ">
			<h2 class="hndle">
				<span><?php esc_html_e( 'AIOSEO Settings', 'all-in-one-seo-pack' ); ?></span>
			</h2>
			<div>
				<div class="aioseo-app aioseo-post-settings">
					<div class="aioseo-blur">
						<div class="aioseo-tabs internal">
							<div class="tabs-scroller">
								<div class="var-tabs var--box var-tabs--item-horizontal var-tabs--layout-horizontal-padding">
									<div class="var-tabs__tab-wrap var-tabs--layout-horizontal-scrollable var-tabs--layout-horizontal">
										<div class="var-tab var--box var-tab--active var-tab--horizontal">
											<span class="label">General</span>
										</div>
										<div class="var-tab var--box var-tab--inactive var-tab--horizontal">
											<span class="label">Social</span>
										</div>
										<div class="var-tab var--box var-tab--inactive var-tab--horizontal">
											<span class="label">Redirects</span>
										</div>
										<div class="var-tab var--box var-tab--inactive var-tab--horizontal">
											<span class="label">SEO Revisions</span>
										</div>
										<div class="var-tab var--box var-tab--inactive var-tab--horizontal">
											<span class="label">Advanced</span>
										</div>
									<div class="var-tabs__indicator var-tabs--layout-horizontal-indicator" style="width: 102px; transform: translateX(0px);"><div class="var-tabs__indicator-inner var-tabs--layout-horizontal-indicator-inner"></div>
									</div>
								</div>
							</div>
						</div>
						<div class="tabs-extra"></div>
					</div>
						<div class="aioseo-tab-content aioseo-post-general">
							<div class="aioseo-settings-row mobile-radio-buttons aioseo-row ">
								<div class="aioseo-col col-xs-12 col-md-3 text-xs-left">
									<div class="settings-name">
										<div class="name"> </div>
									</div>
								</div>
								<div class="aioseo-col col-xs-12 col-md-9 text-xs-left">
									<div class="settings-content">
										<div class="aioseo-radio-toggle circle">
											<div><input id="id_previewGeneralIsMobile_0" name="previewGeneralIsMobile"
													type="radio"><label for="id_previewGeneralIsMobile_0" class="dark"><svg
														width="20" height="18" viewBox="0 0 20 18" fill="none"
														xmlns="http://www.w3.org/2000/svg" class="aioseo-desktop">
														<path fill-rule="evenodd" clip-rule="evenodd"
															d="M2.50004 0.666504H17.5C18.4167 0.666504 19.1667 1.4165 19.1667 2.33317V12.3332C19.1667 13.2498 18.4167 13.9998 17.5 13.9998H11.6667V15.6665H13.3334V17.3332H6.66671V15.6665H8.33337V13.9998H2.50004C1.58337 13.9998 0.833374 13.2498 0.833374 12.3332V2.33317C0.833374 1.4165 1.58337 0.666504 2.50004 0.666504ZM2.50004 12.3332H17.5V2.33317H2.50004V12.3332Z"
															fill="currentColor"></path>
													</svg></label></div>
											<div><input id="id_previewGeneralIsMobile_1" name="previewGeneralIsMobile"
													type="radio"><label for="id_previewGeneralIsMobile_1" class=""><svg
														width="12" height="20" viewBox="0 0 12 20" fill="none"
														xmlns="http://www.w3.org/2000/svg" class="aioseo-mobile">
														<path fill-rule="evenodd" clip-rule="evenodd"
															d="M1.72767 0.833496L10.061 0.841829C10.9777 0.841829 11.7277 1.5835 11.7277 2.50016V17.5002C11.7277 18.4168 10.9777 19.1668 10.061 19.1668H1.72767C0.811003 19.1668 0.0693359 18.4168 0.0693359 17.5002V2.50016C0.0693359 1.5835 0.811003 0.833496 1.72767 0.833496ZM1.72763 15.8335H10.061V4.16683H1.72763V15.8335Z"
															fill="currentColor"></path>
													</svg>
												</label>
											</div>
										</div>
									</div>
								</div>
							</div>
							<div class="aioseo-settings-row snippet-preview-row aioseo-row ">
								<div class="aioseo-col col-xs-12 col-md-3 text-xs-left">
									<div class="settings-name">
										<div class="name"> Snippet Preview </div>
									</div>
								</div>
								<div class="aioseo-col col-xs-12 col-md-9 text-xs-left">
									<div class="settings-content">
										<div class="aioseo-google-search-preview">
											<div class="domain"> https://aioseo.com/category/uncategorized/ </div>
											<div class="site-title">Taxonomy Title | aioseo.com</div>
											<div class="meta-description">Sample taxonomy description</div>
										</div>
									</div>
								</div>
							</div>
							<div class="aioseo-settings-row snippet-title-row aioseo-row ">
								<div class="aioseo-col col-xs-12 col-md-3 text-xs-left">
									<div class="settings-name">
										<div class="name"> Category Title </div>
									</div>
								</div>
								<div class="aioseo-col col-xs-12 col-md-9 text-xs-left">
									<div class="settings-content">
										<div class="aioseo-html-tags-editor">

											<div>
												<div class="aioseo-description tags-description"> Click on the tags below to
													insert variables into your title. </div>
												<div class="add-tags">
													<div class="aioseo-add-template-tag"><svg viewBox="0 0 10 11"
															fill="none" xmlns="http://www.w3.org/2000/svg"
															class="aioseo-plus">
															<path
																d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																fill="currentColor"></path>
														</svg> Category Title </div>
													<div class="aioseo-add-template-tag"><svg viewBox="0 0 10 11"
															fill="none" xmlns="http://www.w3.org/2000/svg"
															class="aioseo-plus">
															<path
																d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																fill="currentColor"></path>
														</svg> Separator </div>
													<div class="aioseo-add-template-tag"><svg viewBox="0 0 10 11"
															fill="none" xmlns="http://www.w3.org/2000/svg"
															class="aioseo-plus">
															<path
																d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																fill="currentColor"></path>
														</svg> Site Title </div><a href="#" class="aioseo-view-all-tags">
														View all tags&nbsp;→ </a>
												</div>
											</div>
											<div class="aioseo-editor">
												<div class="ql-toolbar ql-snow"><span class="ql-formats"></span></div>
												<div class="aioseo-editor-single ql-container ql-snow">
													<div class="ql-editor" data-gramm="false" contenteditable="true"></div>
													<div class="ql-clipboard" contenteditable="true" tabindex="-1"></div>
													<div class="ql-tooltip ql-hidden"><a class="ql-preview"
															rel="noopener noreferrer" target="_blank"
															href="about:blank"></a><input type="text" data-formula="e=mc^2"
															data-link="https://quilljs.com" data-video="Embed URL"><a
															class="ql-action"></a><a class="ql-remove"></a></div>
													<div class="ql-mention-list-container"
														style="display: none; position: absolute;">
														<div class="aioseo-tag-custom">
															<div data-v-3f0a80a7="" class="aioseo-input">

																<input data-v-3f0a80a7="" type="text"
																	placeholder="Enter a custom field name..."
																	spellcheck="true" class="small">
															</div>
														</div>
														<div class="aioseo-tag-search">
															<div data-v-3f0a80a7="" class="aioseo-input">
																<div data-v-3f0a80a7="" class="prepend-icon medium"><svg
																		data-v-3f0a80a7="" viewBox="0 0 15 16"
																		xmlns="http://www.w3.org/2000/svg"
																		class="aioseo-search">
																		<path
																			d="M14.8828 14.6152L11.3379 11.0703C11.25 11.0117 11.1621 10.9531 11.0742 10.9531H10.6934C11.6016 9.89844 12.1875 8.49219 12.1875 6.96875C12.1875 3.62891 9.43359 0.875 6.09375 0.875C2.72461 0.875 0 3.62891 0 6.96875C0 10.3379 2.72461 13.0625 6.09375 13.0625C7.61719 13.0625 8.99414 12.5059 10.0781 11.5977V11.9785C10.0781 12.0664 10.1074 12.1543 10.166 12.2422L13.7109 15.7871C13.8574 15.9336 14.0918 15.9336 14.209 15.7871L14.8828 15.1133C15.0293 14.9961 15.0293 14.7617 14.8828 14.6152ZM6.09375 11.6562C3.48633 11.6562 1.40625 9.57617 1.40625 6.96875C1.40625 4.39062 3.48633 2.28125 6.09375 2.28125C8.67188 2.28125 10.7812 4.39062 10.7812 6.96875C10.7812 9.57617 8.67188 11.6562 6.09375 11.6562Z"
																			fill="currentColor"></path>
																	</svg></div>
																<input data-v-3f0a80a7="" type="text"
																	placeholder="Search for an item..." spellcheck="true"
																	class="medium prepend">
															</div>
														</div>
														<ul class="ql-mention-list"></ul>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Category Description</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Category Description </div>
															<div class="aioseo-tag-description"> Current or first category
																description. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Category Title</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Category Title </div>
															<div class="aioseo-tag-description"> Current or first category
																title. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Current Date</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Current Date </div>
															<div class="aioseo-tag-description"> The current date,
																localized. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Current Day</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Current Day </div>
															<div class="aioseo-tag-description"> The current day of the
																month, localized. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Current Month</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Current Month </div>
															<div class="aioseo-tag-description"> The current month,
																localized. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Current Year</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Current Year </div>
															<div class="aioseo-tag-description"> The current year,
																localized. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Custom Field</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Custom Field </div>
															<div class="aioseo-tag-description"> A custom field from the
																current page/post. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Permalink</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Permalink </div>
															<div class="aioseo-tag-description"> The permalink for the
																current page/post. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Separator</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Separator </div>
															<div class="aioseo-tag-description"> The separator defined in
																the search appearance settings. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Site Title</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Site Title </div>
															<div class="aioseo-tag-description"> Your site title. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Tagline</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Tagline </div>
															<div class="aioseo-tag-description"> The tagline for your site,
																set in the general settings. </div>
														</div>
													</div>
												</div>
												<div style="display: none;">
													<div data-v-3f0a80a7="" class="aioseo-input">
														<div data-v-3f0a80a7="" class="prepend-icon medium"><svg
																data-v-3f0a80a7="" viewBox="0 0 15 16"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-search">
																<path
																	d="M14.8828 14.6152L11.3379 11.0703C11.25 11.0117 11.1621 10.9531 11.0742 10.9531H10.6934C11.6016 9.89844 12.1875 8.49219 12.1875 6.96875C12.1875 3.62891 9.43359 0.875 6.09375 0.875C2.72461 0.875 0 3.62891 0 6.96875C0 10.3379 2.72461 13.0625 6.09375 13.0625C7.61719 13.0625 8.99414 12.5059 10.0781 11.5977V11.9785C10.0781 12.0664 10.1074 12.1543 10.166 12.2422L13.7109 15.7871C13.8574 15.9336 14.0918 15.9336 14.209 15.7871L14.8828 15.1133C15.0293 14.9961 15.0293 14.7617 14.8828 14.6152ZM6.09375 11.6562C3.48633 11.6562 1.40625 9.57617 1.40625 6.96875C1.40625 4.39062 3.48633 2.28125 6.09375 2.28125C8.67188 2.28125 10.7812 4.39062 10.7812 6.96875C10.7812 9.57617 8.67188 11.6562 6.09375 11.6562Z"
																	fill="currentColor"></path>
															</svg></div>
														<input data-v-3f0a80a7="" type="text"
															placeholder="Search for an item..." spellcheck="true"
															class="medium prepend">


													</div>
												</div>
												<div style="display: none;">
													<div data-v-3f0a80a7="" class="aioseo-input">

														<input data-v-3f0a80a7="" type="text"
															placeholder="Enter a custom field name..." spellcheck="true"
															class="small">


													</div>
												</div>
											</div>
										</div>
										<div class="max-recommended-count"><strong>32</strong> out of <strong>60</strong>
											max recommended characters.</div>
									</div>
								</div>
							</div>
							<div class="aioseo-settings-row snippet-description-row aioseo-row ">
								<div class="aioseo-col col-xs-12 col-md-3 text-xs-left">
									<div class="settings-name">
										<div class="name"> Meta Description </div>
										<!---->
									</div>
								</div>
								<div class="aioseo-col col-xs-12 col-md-9 text-xs-left">
									<div class="settings-content">
										<div class="aioseo-html-tags-editor">
											<!---->
											<div>
												<div class="aioseo-description tags-description"> Click on the tags below to
													insert variables into your meta description. </div>
												<div class="add-tags">
													<div class="aioseo-add-template-tag"><svg viewBox="0 0 10 11"
															fill="none" xmlns="http://www.w3.org/2000/svg"
															class="aioseo-plus">
															<path
																d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																fill="currentColor"></path>
														</svg> Category Title </div>
													<div class="aioseo-add-template-tag"><svg viewBox="0 0 10 11"
															fill="none" xmlns="http://www.w3.org/2000/svg"
															class="aioseo-plus">
															<path
																d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																fill="currentColor"></path>
														</svg> Separator </div>
													<div class="aioseo-add-template-tag"><svg viewBox="0 0 10 11"
															fill="none" xmlns="http://www.w3.org/2000/svg"
															class="aioseo-plus">
															<path
																d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																fill="currentColor"></path>
														</svg> Category Description </div><a href="#"
														class="aioseo-view-all-tags"> View all tags&nbsp;→ </a>
												</div>
											</div>
											<div class="aioseo-editor">
												<div class="ql-toolbar ql-snow"><span class="ql-formats"></span></div>
												<div class="aioseo-editor-description ql-container ql-snow">
													<div class="ql-editor" data-gramm="false" contenteditable="true"></div>
													<div class="ql-clipboard" contenteditable="true" tabindex="-1"></div>
													<div class="ql-tooltip ql-hidden"><a class="ql-preview"
															rel="noopener noreferrer" target="_blank"
															href="about:blank"></a><input type="text" data-formula="e=mc^2"
															data-link="https://quilljs.com" data-video="Embed URL"><a
															class="ql-action"></a><a class="ql-remove"></a></div>
													<div class="ql-mention-list-container"
														style="display: none; position: absolute;">
														<div class="aioseo-tag-custom">
															<div data-v-3f0a80a7="" class="aioseo-input">

																<input data-v-3f0a80a7="" type="text"
																	placeholder="Enter a custom field name..."
																	spellcheck="true" class="small">
															</div>
														</div>
														<div class="aioseo-tag-search">
															<div data-v-3f0a80a7="" class="aioseo-input">
																<div data-v-3f0a80a7="" class="prepend-icon medium"><svg
																		data-v-3f0a80a7="" viewBox="0 0 15 16"
																		xmlns="http://www.w3.org/2000/svg"
																		class="aioseo-search">
																		<path
																			d="M14.8828 14.6152L11.3379 11.0703C11.25 11.0117 11.1621 10.9531 11.0742 10.9531H10.6934C11.6016 9.89844 12.1875 8.49219 12.1875 6.96875C12.1875 3.62891 9.43359 0.875 6.09375 0.875C2.72461 0.875 0 3.62891 0 6.96875C0 10.3379 2.72461 13.0625 6.09375 13.0625C7.61719 13.0625 8.99414 12.5059 10.0781 11.5977V11.9785C10.0781 12.0664 10.1074 12.1543 10.166 12.2422L13.7109 15.7871C13.8574 15.9336 14.0918 15.9336 14.209 15.7871L14.8828 15.1133C15.0293 14.9961 15.0293 14.7617 14.8828 14.6152ZM6.09375 11.6562C3.48633 11.6562 1.40625 9.57617 1.40625 6.96875C1.40625 4.39062 3.48633 2.28125 6.09375 2.28125C8.67188 2.28125 10.7812 4.39062 10.7812 6.96875C10.7812 9.57617 8.67188 11.6562 6.09375 11.6562Z"
																			fill="currentColor"></path>
																	</svg></div>
																<input data-v-3f0a80a7="" type="text"
																	placeholder="Search for an item..." spellcheck="true"
																	class="medium prepend">
															</div>
														</div>
														<ul class="ql-mention-list"></ul>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Category Description</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Category Description </div>
															<div class="aioseo-tag-description"> Current or first category
																description. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Category Title</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Category Title </div>
															<div class="aioseo-tag-description"> Current or first category
																title. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Current Date</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Current Date </div>
															<div class="aioseo-tag-description"> The current date,
																localized. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Current Day</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Current Day </div>
															<div class="aioseo-tag-description"> The current day of the
																month, localized. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Current Month</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Current Month </div>
															<div class="aioseo-tag-description"> The current month,
																localized. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Current Year</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Current Year </div>
															<div class="aioseo-tag-description"> The current year,
																localized. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Custom Field</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Custom Field </div>
															<div class="aioseo-tag-description"> A custom field from the
																current page/post. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Permalink</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Permalink </div>
															<div class="aioseo-tag-description"> The permalink for the
																current page/post. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Separator</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Separator </div>
															<div class="aioseo-tag-description"> The separator defined in
																the search appearance settings. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Site Title</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Site Title </div>
															<div class="aioseo-tag-description"> Your site title. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Tagline</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Tagline </div>
															<div class="aioseo-tag-description"> The tagline for your site,
																set in the general settings. </div>
														</div>
													</div>
												</div>
												<div style="display: none;">
													<div data-v-3f0a80a7="" class="aioseo-input">
														<div data-v-3f0a80a7="" class="prepend-icon medium"><svg
																data-v-3f0a80a7="" viewBox="0 0 15 16"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-search">
																<path
																	d="M14.8828 14.6152L11.3379 11.0703C11.25 11.0117 11.1621 10.9531 11.0742 10.9531H10.6934C11.6016 9.89844 12.1875 8.49219 12.1875 6.96875C12.1875 3.62891 9.43359 0.875 6.09375 0.875C2.72461 0.875 0 3.62891 0 6.96875C0 10.3379 2.72461 13.0625 6.09375 13.0625C7.61719 13.0625 8.99414 12.5059 10.0781 11.5977V11.9785C10.0781 12.0664 10.1074 12.1543 10.166 12.2422L13.7109 15.7871C13.8574 15.9336 14.0918 15.9336 14.209 15.7871L14.8828 15.1133C15.0293 14.9961 15.0293 14.7617 14.8828 14.6152ZM6.09375 11.6562C3.48633 11.6562 1.40625 9.57617 1.40625 6.96875C1.40625 4.39062 3.48633 2.28125 6.09375 2.28125C8.67188 2.28125 10.7812 4.39062 10.7812 6.96875C10.7812 9.57617 8.67188 11.6562 6.09375 11.6562Z"
																	fill="currentColor"></path>
															</svg></div>
														<input data-v-3f0a80a7="" type="text"
															placeholder="Search for an item..." spellcheck="true"
															class="medium prepend">
													</div>
												</div>
												<div style="display: none;">
													<div data-v-3f0a80a7="" class="aioseo-input">

														<input data-v-3f0a80a7="" type="text"
															placeholder="Enter a custom field name..." spellcheck="true"
															class="small">
													</div>
												</div>
											</div>
										</div>
										<div class="max-recommended-count"><strong>27</strong> out of <strong>160</strong>
											max recommended characters.</div>
									</div>
								</div>
							</div>
						</div>
					</div>

					<div class="aioseo-cta floating" style="max-width: 630px;">
						<div class="aioseo-cta-background">
							<div class="type-1">
								<div class="header-text"><?php esc_html_e( 'Custom Taxonomies are a PRO Feature', 'all-in-one-seo-pack' ); ?></div>
								<div class="description"><?php esc_html_e( 'Set custom SEO meta, social meta and more for individual terms.', 'all-in-one-seo-pack' ); ?></div>
								<div class="feature-list aioseo-row ">
									<div class="aioseo-col col-xs-12 col-md-6 text-xs-left">
										<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="aioseo-circle-check">
											<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM10 14.17L16.59 7.58L18 9L10 17L6 13L7.41 11.59L10 14.17Z" fill="currentColor"></path>
										</svg> SEO Title/Description
									</div>
									<div class="aioseo-col col-xs-12 col-md-6 text-xs-left">
										<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="aioseo-circle-check">
											<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM10 14.17L16.59 7.58L18 9L10 17L6 13L7.41 11.59L10 14.17Z" fill="currentColor"></path>
										</svg> Social Meta
									</div>
									<div class="aioseo-col col-xs-12 col-md-6 text-xs-left">
										<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="aioseo-circle-check">
											<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM10 14.17L16.59 7.58L18 9L10 17L6 13L7.41 11.59L10 14.17Z" fill="currentColor"></path>
										</svg> SEO Revisions
									</div>
									<div class="aioseo-col col-xs-12 col-md-6 text-xs-left">
										<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="aioseo-circle-check">
											<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM10 14.17L16.59 7.58L18 9L10 17L6 13L7.41 11.59L10 14.17Z" fill="currentColor"></path>
										</svg> Import/Export
									</div>
								</div>
								<div class="actions">
									<a type="" to="" class="aioseo-button green" href="<?php echo esc_attr( aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'lite-upgrade/', 'taxonomies-upsell', 'features=[]=taxonomies', false ) ); ?>" target="_blank"><?php esc_html_e( 'Unlock Custom Taxonomies', 'all-in-one-seo-pack' ); ?></a>
									<a href="https://aioseo.com/?utm_source=WordPress&amp;utm_campaign=liteplugin&amp;utm_medium=taxonomies-upsell&amp;features[]=taxonomies" target="_blank" class="learn-more"><?php esc_html_e( 'Learn more about all features', 'all-in-one-seo-pack' ); ?></a>
								</div>


								<div class="aioseo-alert yellow medium bonus-alert"> 🎁 <span>
									<strong><?php esc_html_e( 'Bonus:', 'all-in-one-seo-pack' ); ?></strong>
									<?php esc_html_e( 'You can upgrade to the Pro plan today and ', 'all-in-one-seo-pack' ); ?>
									<strong><?php esc_html_e( 'save 60% off', 'all-in-one-seo-pack' ); ?></strong>
									<?php esc_html_e( '(discount auto-applied)', 'all-in-one-seo-pack' ); ?>.</span>
								</div>
							</div>
						</div>
					</div>
				</div>
			</div>
		</div>
	</div>
</div>init/activation.php000064400000003744151536241210010371 0ustar00<?php
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! function_exists( 'aioseo_lite_just_activated' ) ) {
	/**
	 * Store temporarily that the Lite version of the plugin was activated.
	 * This is needed because WP does a redirect after activation and
	 * we need to preserve this state to know whether user activated Lite or not.
	 *
	 * @since 4.0.0
	 */
	function aioseo_lite_just_activated() {
		aioseo()->core->cache->update( 'lite_just_activated', true );
	}
}

if ( ! function_exists( 'aioseo_lite_just_deactivated' ) ) {
	/**
	 * Store temporarily that Lite plugin was deactivated.
	 * Convert temporary "activated" value to a global variable,
	 * so it is available through the request. Remove from the storage.
	 *
	 * @since 4.0.0
	 */
	function aioseo_lite_just_deactivated() {
		global $aioseoLiteJustActivated, $aioseoLiteJustDeactivated;

		$aioseoLiteJustActivated   = (bool) aioseo()->core->cache->get( 'lite_just_activated' );
		$aioseoLiteJustDeactivated = true;

		aioseo()->core->cache->delete( 'lite_just_activated' );
	}
}

if ( ! function_exists( 'aioseo_pro_just_activated' ) ) {
	/**
	 * Store temporarily that the Pro version of the plugin was activated.
	 * This is needed because when we activate the Pro version on top
	 * of the Lite version, it does not trigger the activation hook in Pro.
	 *
	 * @since 4.0.0
	 */
	function aioseo_pro_just_activated() {
		$liteActivated = is_plugin_active( 'all-in-one-seo-pack/all_in_one_seo_pack.php' );
		if ( $liteActivated ) {
			// Add capabilities for the current user on upgrade so that the menu is visible on the first request.
			aioseo()->activate->addCapabilitiesOnUpgrade();

			aioseo()->core->cache->update( 'pro_just_deactivated_lite', true );
		}
	}
}

// If we detect that V3 is active, let's deactivate it now.
if ( defined( 'AIOSEOP_VERSION' ) && defined( 'AIOSEO_PLUGIN_FILE' ) ) {
	require_once ABSPATH . 'wp-admin/includes/plugin.php';
	deactivate_plugins( plugin_basename( AIOSEO_PLUGIN_FILE ) );
}init/blocks.js000064400000002525151536241210007326 0ustar00/**
 * Since we dynamically load our blocks, wordpress.org cannot pick them up properly.
 * This file solely exists to let WordPress know what blocks we are currently using.
 *
 * @since 4.2.4
 */

/* eslint-disable no-undef */

registerBlockType('aioseo/breadcrumbs', {
	title : 'AIOSEO - Breadcrumbs'
})
registerBlockType('aioseo/html-sitemap', {
	title : 'AIOSEO - HTML Sitemap'
})
registerBlockType('aioseo/faq', {
	title : 'AIOSEO - FAQ with JSON Schema'
})
registerBlockType('aioseo/table-of-contents', {
	title : 'AIOSEO - Table of Contents'
})
registerBlockType('aioseo/businessinfo', {
	title : 'AIOSEO - Local Business Info'
})
registerBlockType('aioseo/locationcategories', {
	title : 'AIOSEO - Local Business Location Categories'
})
registerBlockType('aioseo/locations', {
	title : 'AIOSEO - Local Business Locations'
})
registerBlockType('aioseo/locationmap', {
	title : 'AIOSEO - Local Business Google Map'
})
registerBlockType('aioseo/openinghours', {
	title : 'AIOSEO - Local Business Opening Hours'
})
registerBlockType('aioseo/openinghours', {
	title : 'AIOSEO - Author Bio (E-E-A-T)'
})
registerBlockType('aioseo/openinghours', {
	title : 'AIOSEO - Author Name (E-E-A-T)'
})
registerBlockType('aioseo/openinghours', {
	title : 'AIOSEO - Reviewer Name (E-E-A-T)'
})
registerBlockType('aioseo/key-points', {
	title : 'AIOSEO - Key Points (TLDR)'
})index.asset.php000064400000001515151547157720007522 0ustar00<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wc-admin-layout', 'wc-components', 'wc-csv', 'wc-currency', 'wc-customer-effort-score', 'wc-date', 'wc-experimental', 'wc-explat', 'wc-navigation', 'wc-notices', 'wc-number', 'wc-product-editor', 'wc-settings', 'wc-store-data', 'wc-tracks', 'wp-a11y', 'wp-api-fetch', 'wp-blob', 'wp-block-editor', 'wp-block-library', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-data-controls', 'wp-date', 'wp-deprecated', 'wp-dom', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-keyboard-shortcuts', 'wp-keycodes', 'wp-media-utils', 'wp-notices', 'wp-plugins', 'wp-preferences', 'wp-primitives', 'wp-private-apis', 'wp-router', 'wp-url', 'wp-viewport', 'wp-warning'), 'version' => '33c7c1418889bad5328b12d2577421d1');index.js000064400002057007151547157720006242 0ustar00/*! For license information please see index.js.LICENSE.txt */
(()=>{var e,t,n,o,r={75283:(e,t,n)=>{"use strict";n.d(t,{Z:()=>i});var o=n(99196),r=n(45229);function a(e,t){let n,r,i=[];for(let o=0;o<e.length;o++){const a=e[o];if("string"!==a.type){if(void 0===t[a.value])throw new Error(`Invalid interpolation, missing component node: \`${a.value}\``);if("object"!=typeof t[a.value])throw new Error(`Invalid interpolation, component node must be a ReactElement or null: \`${a.value}\``);if("componentClose"===a.type)throw new Error(`Missing opening component token: \`${a.value}\``);if("componentOpen"===a.type){n=t[a.value],r=o;break}i.push(t[a.value])}else i.push(a.value)}if(n){const s=function(e,t){const n=t[e];let o=0;for(let r=e+1;r<t.length;r++){const e=t[r];if(e.value===n.value){if("componentOpen"===e.type){o++;continue}if("componentClose"===e.type){if(0===o)return r;o--}}}throw new Error("Missing closing component token `"+n.value+"`")}(r,e),c=a(e.slice(r+1,s),t),l=(0,o.cloneElement)(n,{},c);if(i.push(l),s<e.length-1){const n=a(e.slice(s+1),t);i=i.concat(n)}}return i=i.filter(Boolean),0===i.length?null:1===i.length?i[0]:(0,o.createElement)(o.Fragment,null,...i)}function i(e){const{mixedString:t,components:n,throwErrors:o}=e;if(!n)return t;if("object"!=typeof n){if(o)throw new Error(`Interpolation Error: unable to process \`${t}\` because components is not an object`);return t}const i=(0,r.Z)(t);try{return a(i,n)}catch(e){if(o)throw new Error(`Interpolation Error: unable to process \`${t}\` because of error \`${e.message}\``);return t}}},45229:(e,t,n)=>{"use strict";function o(e){return e.startsWith("{{/")?{type:"componentClose",value:e.replace(/\W/g,"")}:e.endsWith("/}}")?{type:"componentSelfClosing",value:e.replace(/\W/g,"")}:e.startsWith("{{")?{type:"componentOpen",value:e.replace(/\W/g,"")}:{type:"string",value:e}}function r(e){return e.split(/(\{\{\/?\s*\w+\s*\/?\}\})/g).map(o)}n.d(t,{Z:()=>r})},23374:(e,t,n)=>{"use strict";n.d(t,{Z:()=>r});var o=n(69307);const r=function(e){let{icon:t,size:n=24,...r}=e;return(0,o.cloneElement)(t,{width:n,height:n,...r})}},34200:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var o=n(69307),r=n(70444);const a=(0,o.createElement)(r.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,o.createElement)(r.Path,{fillRule:"evenodd",d:"M5 5.5h14a.5.5 0 01.5.5v1.5a.5.5 0 01-.5.5H5a.5.5 0 01-.5-.5V6a.5.5 0 01.5-.5zM4 9.232A2 2 0 013 7.5V6a2 2 0 012-2h14a2 2 0 012 2v1.5a2 2 0 01-1 1.732V18a2 2 0 01-2 2H6a2 2 0 01-2-2V9.232zm1.5.268V18a.5.5 0 00.5.5h12a.5.5 0 00.5-.5V9.5h-13z",clipRule:"evenodd"}))},47642:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var o=n(69307),r=n(70444);const a=(0,o.createElement)(r.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,o.createElement)(r.Path,{d:"M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"}))},70048:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var o=n(69307),r=n(70444);const a=(0,o.createElement)(r.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,o.createElement)(r.Path,{d:"M14.6 7l-1.2-1L8 12l5.4 6 1.2-1-4.6-5z"}))},28601:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var o=n(69307),r=n(70444);const a=(0,o.createElement)(r.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,o.createElement)(r.Path,{d:"M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"}))},70261:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var o=n(69307),r=n(70444);const a=(0,o.createElement)(r.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,o.createElement)(r.Path,{d:"M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"}))},12532:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var o=n(69307),r=n(70444);const a=(0,o.createElement)(r.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,o.createElement)(r.Path,{d:"M18.2 17c0 .7-.6 1.2-1.2 1.2H7c-.7 0-1.2-.6-1.2-1.2V7c0-.7.6-1.2 1.2-1.2h3.2V4.2H7C5.5 4.2 4.2 5.5 4.2 7v10c0 1.5 1.2 2.8 2.8 2.8h10c1.5 0 2.8-1.2 2.8-2.8v-3.6h-1.5V17zM14.9 3v1.5h3.7l-6.4 6.4 1.1 1.1 6.4-6.4v3.7h1.5V3h-6.3z"}))},91250:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var o=n(69307),r=n(70444);const a=(0,o.createElement)(r.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,o.createElement)(r.Path,{d:"M12 4.75a7.25 7.25 0 100 14.5 7.25 7.25 0 000-14.5zM3.25 12a8.75 8.75 0 1117.5 0 8.75 8.75 0 01-17.5 0zM12 8.75a1.5 1.5 0 01.167 2.99c-.465.052-.917.44-.917 1.01V14h1.5v-.845A3 3 0 109 10.25h1.5a1.5 1.5 0 011.5-1.5zM11.25 15v1.5h1.5V15h-1.5z"}))},89015:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var o=n(69307),r=n(70444);const a=(0,o.createElement)(r.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,o.createElement)(r.Path,{d:"M7 5.5h10a.5.5 0 01.5.5v12a.5.5 0 01-.5.5H7a.5.5 0 01-.5-.5V6a.5.5 0 01.5-.5zM17 4H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V6a2 2 0 00-2-2zm-1 3.75H8v1.5h8v-1.5zM8 11h8v1.5H8V11zm6 3.25H8v1.5h6v-1.5z"}))},5060:(e,t,n)=>{"use strict";n.d(t,{Amex:()=>m,ApplePay:()=>u,CB:()=>C,GooglePay:()=>j,JCB:()=>y,Loader:()=>te,MasterCard:()=>l,SetupRequired:()=>R,Sofort:()=>H,UnionPay:()=>_,Visa:()=>c,WCPayBanner:()=>x,WCPayBannerBody:()=>N,WCPayBannerFooter:()=>S,WCPayBannerImageCut:()=>b,WCPayBenefits:()=>P,WooOnboardingTask:()=>W,WooOnboardingTaskListHeader:()=>F,WooOnboardingTaskListItem:()=>B,WooPaymentGatewayConfigure:()=>Z,WooPaymentGatewaySetup:()=>D,findCountryOption:()=>q,getCountry:()=>J});var o=n(55609),r=n(14812),a=n(69307),i=n(86020),s=n(65736);const c=()=>(0,a.createElement)("svg",{width:"51",height:"35",viewBox:"0 0 51 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("rect",{x:"0.5",y:"0.5",width:"50",height:"34",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),(0,a.createElement)("path",{d:"M22.6435 24.004H19.248L21.3718 11.7534H24.7671L22.6435 24.004Z",fill:"#15195A"}),(0,a.createElement)("path",{d:"M34.952 12.0528C34.2823 11.8049 33.22 11.5312 31.9066 11.5312C28.5534 11.5312 26.1922 13.1993 26.1777 15.5842C26.1499 17.3437 27.8683 18.321 29.1536 18.9077C30.4672 19.5072 30.9138 19.8985 30.9138 20.4329C30.9004 21.2536 29.8522 21.6319 28.8747 21.6319C27.5191 21.6319 26.7927 21.4369 25.6889 20.9803L25.2417 20.7845L24.7666 23.5345C25.563 23.873 27.0302 24.1733 28.5534 24.1865C32.1162 24.1865 34.4356 22.5442 34.4631 20.0028C34.4767 18.6082 33.5693 17.5396 31.613 16.6665C30.4254 16.1059 29.6981 15.728 29.6981 15.1544C29.7121 14.6331 30.3133 14.099 31.6539 14.099C32.7577 14.0729 33.5687 14.3204 34.1831 14.5681L34.4902 14.6982L34.952 12.0528Z",fill:"#15195A"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M41.0301 11.7534H43.6565L46.3957 24.0039H43.2519C43.2519 24.0039 42.9442 22.5963 42.8467 22.1662H38.4873C38.3612 22.4919 37.7747 24.0039 37.7747 24.0039H34.2119L39.2554 12.7699C39.6049 11.9748 40.2202 11.7534 41.0301 11.7534ZM40.8208 16.2365C40.8208 16.2365 39.7448 18.9603 39.4652 19.6641H42.2875C42.1478 19.0516 41.5048 16.1192 41.5048 16.1192L41.2676 15.0636C41.1676 15.3355 41.0231 15.7092 40.9256 15.9612C40.8596 16.1321 40.8151 16.2471 40.8208 16.2365Z",fill:"#15195A"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M4.53636 11.7534H9.99929C10.7398 11.7792 11.3406 12.0008 11.5361 12.7832L12.7233 18.4113C12.7234 18.4118 12.7236 18.4124 12.7238 18.4129L13.0871 20.1072L16.4124 11.7534H20.0028L14.6657 23.991H11.0752L8.04881 13.3464C7.00461 12.7769 5.81289 12.3188 4.48047 12.0009L4.53636 11.7534Z",fill:"#15195A"})),l=()=>(0,a.createElement)("svg",{width:"51",height:"35",viewBox:"0 0 51 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("rect",{x:"0.5",y:"0.5",width:"50",height:"34",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M18.6846 27.0292V28.3215V29.6137H18.1154V29.2999C17.9349 29.5327 17.661 29.6787 17.2886 29.6787C16.5546 29.6787 15.9791 29.1112 15.9791 28.3215C15.9791 27.5324 16.5546 26.9642 17.2886 26.9642C17.661 26.9642 17.9349 27.1103 18.1154 27.343V27.0292H18.6846ZM17.3594 27.494C16.8667 27.494 16.5652 27.8672 16.5652 28.3215C16.5652 28.7757 16.8667 29.1489 17.3594 29.1489C17.8302 29.1489 18.148 28.7918 18.148 28.3215C18.148 27.8511 17.8302 27.494 17.3594 27.494ZM37.9186 28.3215C37.9186 27.8672 38.2201 27.494 38.7128 27.494C39.1842 27.494 39.5014 27.8511 39.5014 28.3215C39.5014 28.7918 39.1842 29.1489 38.7128 29.1489C38.2201 29.1489 37.9186 28.7757 37.9186 28.3215ZM40.0386 25.9913V28.3215V29.6137H39.4688V29.2999C39.2882 29.5327 39.0143 29.6787 38.642 29.6787C37.9079 29.6787 37.3325 29.1112 37.3325 28.3215C37.3325 27.5324 37.9079 26.9642 38.642 26.9642C39.0143 26.9642 39.2882 27.1103 39.4688 27.343V25.9913H40.0386ZM25.7496 27.4674C26.1163 27.4674 26.352 27.6945 26.4122 28.0943H25.0538C25.1146 27.7211 25.3441 27.4674 25.7496 27.4674ZM24.4571 28.3215C24.4571 27.5157 24.9937 26.9642 25.7609 26.9642C26.4943 26.9642 26.9983 27.5157 27.0039 28.3215C27.0039 28.397 26.9983 28.4675 26.9926 28.5375L25.0488 28.5375C25.1309 29.0029 25.465 29.1706 25.8317 29.1706C26.0944 29.1706 26.374 29.0728 26.5933 28.9001L26.8723 29.3167C26.5545 29.5815 26.1934 29.6787 25.7991 29.6787C25.0156 29.6787 24.4571 29.1434 24.4571 28.3215ZM32.6337 28.3215C32.6337 27.8672 32.9353 27.494 33.4279 27.494C33.8987 27.494 34.2165 27.8511 34.2165 28.3215C34.2165 28.7918 33.8987 29.1489 33.4279 29.1489C32.9353 29.1489 32.6337 28.7757 32.6337 28.3215ZM34.7529 27.0292V28.3215V29.6137H34.1837V29.2999C34.0026 29.5327 33.7293 29.6787 33.3569 29.6787C32.6229 29.6787 32.0475 29.1112 32.0475 28.3215C32.0475 27.5324 32.6229 26.9642 33.3569 26.9642C33.7293 26.9642 34.0026 27.1103 34.1837 27.343V27.0292H34.7529ZM29.4191 28.3215C29.4191 29.1056 29.972 29.6787 30.8157 29.6787C31.21 29.6787 31.4726 29.5921 31.7572 29.3705L31.4839 28.9162C31.2701 29.0679 31.0457 29.1489 30.7988 29.1489C30.3443 29.1434 30.0102 28.8191 30.0102 28.3215C30.0102 27.8239 30.3443 27.4996 30.7988 27.494C31.0457 27.494 31.2701 27.5751 31.4839 27.7267L31.7572 27.2724C31.4726 27.0509 31.21 26.9642 30.8157 26.9642C29.972 26.9642 29.4191 27.5373 29.4191 28.3215ZM36.0674 27.3431C36.2153 27.1159 36.4291 26.9643 36.7575 26.9643C36.8729 26.9643 37.0371 26.986 37.1631 27.0349L36.9876 27.5646C36.8672 27.5157 36.7469 27.4997 36.6315 27.4997C36.2592 27.4997 36.073 27.7373 36.073 28.165V29.6138H35.5032V27.0293H36.0674V27.3431ZM21.4996 27.2347C21.2257 27.0564 20.8483 26.9642 20.4321 26.9642C19.7689 26.9642 19.342 27.278 19.342 27.7917C19.342 28.2132 19.6599 28.4731 20.2453 28.5542L20.5142 28.5919C20.8264 28.6352 20.9737 28.7163 20.9737 28.8624C20.9737 29.0623 20.7656 29.1762 20.377 29.1762C19.9827 29.1762 19.6981 29.0518 19.5063 28.9057L19.238 29.3433C19.5502 29.5704 19.9444 29.6787 20.3713 29.6787C21.1273 29.6787 21.5654 29.3272 21.5654 28.8352C21.5654 28.3809 21.2207 28.1432 20.6509 28.0621L20.3826 28.0238C20.1363 27.9916 19.9388 27.9433 19.9388 27.77C19.9388 27.5806 20.125 27.4674 20.4371 27.4674C20.7712 27.4674 21.0947 27.5918 21.2533 27.689L21.4996 27.2347ZM28.1542 27.3431C28.3015 27.1159 28.5152 26.9643 28.8437 26.9643C28.959 26.9643 29.1233 26.986 29.2493 27.0349L29.0738 27.5646C28.9534 27.5157 28.833 27.4997 28.7177 27.4997C28.3454 27.4997 28.1592 27.7373 28.1592 28.165V29.6138H27.59V27.0293L28.1542 27.0293V27.3431ZM23.9862 27.0292H23.0553V26.2451H22.4799V27.0292H21.949V27.5429H22.4799V28.7219C22.4799 29.3216 22.7156 29.6787 23.3888 29.6787C23.6358 29.6787 23.9204 29.6032 24.1009 29.4788L23.9367 28.9973C23.7668 29.0945 23.5806 29.1434 23.4327 29.1434C23.1481 29.1434 23.0553 28.9701 23.0553 28.7108V27.5429H23.9862V27.0292ZM15.4758 27.9917V29.6138H14.9003V28.1755C14.9003 27.7373 14.7142 27.4941 14.3255 27.4941C13.9475 27.4941 13.6849 27.7324 13.6849 28.1811V29.6138H13.1095V28.1755C13.1095 27.7373 12.9183 27.4941 12.5403 27.4941C12.151 27.4941 11.899 27.7324 11.899 28.1811V29.6138H11.3242V27.0293H11.894V27.348C12.1078 27.0454 12.3811 26.9643 12.6606 26.9643C13.0606 26.9643 13.3451 27.1376 13.5257 27.4242C13.767 27.0615 14.1118 26.9587 14.4459 26.9643C15.0815 26.9699 15.4758 27.3808 15.4758 27.9917Z",fill:"#231F20"}),(0,a.createElement)("path",{d:"M29.9381 22.6376H21.3115V7.33105H29.9381V22.6376Z",fill:"#FF5F00"}),(0,a.createElement)("path",{d:"M21.8586 14.9846C21.8586 11.8796 23.331 9.11372 25.624 7.33129C23.9472 6.02789 21.831 5.24994 19.5311 5.24994C14.0864 5.24994 9.67285 9.60822 9.67285 14.9846C9.67285 20.361 14.0864 24.7192 19.5311 24.7192C21.831 24.7192 23.9472 23.9413 25.624 22.6379C23.331 20.8555 21.8586 18.0896 21.8586 14.9846Z",fill:"#EB001B"}),(0,a.createElement)("path",{d:"M41.5758 14.9846C41.5758 20.361 37.1622 24.7192 31.7175 24.7192C29.4177 24.7192 27.3014 23.9413 25.624 22.6379C27.9176 20.8555 29.3901 18.0896 29.3901 14.9846C29.3901 11.8796 27.9176 9.11372 25.624 7.33129C27.3014 6.02789 29.4177 5.24994 31.7175 5.24994C37.1622 5.24994 41.5758 9.60822 41.5758 14.9846Z",fill:"#F79E1B"})),m=()=>(0,a.createElement)("svg",{width:"52",height:"35",viewBox:"0 0 52 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("rect",{x:"1.18945",y:"0.5",width:"50",height:"34",rx:"3.5",fill:"#006FCF",stroke:"#F3F3F3"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M11.1205 25.2823V18.0771H19.3189L20.1985 19.1441L21.1072 18.0771H50.8653V24.7854C50.8653 24.7854 50.0871 25.2751 49.187 25.2823H32.7093L31.7176 24.1465V25.2823H28.4679V23.3435C28.4679 23.3435 28.0239 23.6141 27.0642 23.6141H25.9581V25.2823H21.0376L20.1593 24.1924L19.2675 25.2823H11.1205ZM1.56836 12.6465L3.41294 8.63574H6.60294L7.64976 10.8824V8.63574H11.6152L12.2384 10.2596L12.8425 8.63574H30.6434V9.4521C30.6434 9.4521 31.5792 8.63574 33.1171 8.63574L38.8928 8.65457L39.9215 10.8718V8.63574H43.24L44.1534 9.90939V8.63574H47.5023V15.841H44.1534L43.2781 14.5632V15.841H38.4025L37.9121 14.7052H36.6014L36.1191 15.841H32.8126C31.4893 15.841 30.6434 15.0413 30.6434 15.0413V15.841H25.658L24.6685 14.7052V15.841H6.13036L5.64039 14.7052H4.33383L3.84732 15.841H1.56836V12.6465ZM1.5779 14.9189L4.06583 9.52391H5.95199L8.43755 14.9189H6.7821L6.32542 13.8386H3.65672L3.19767 14.9189H1.5779ZM5.79982 12.6674L4.98636 10.7795L4.17053 12.6674H5.79982ZM8.60869 14.9182V9.52317L10.9105 9.53115L12.2493 13.0095L13.556 9.52317H15.8394V14.9182H14.3933V10.9429L12.8603 14.9182H11.592L10.0548 10.9429V14.9182H8.60869ZM16.8289 14.9182V9.52317H21.5479V10.73H18.2902V11.6528H21.4717V12.7886H18.2902V13.7469H21.5479V14.9182H16.8289ZM22.3851 14.9189V9.52391H25.6033C26.6696 9.52391 27.625 10.1389 27.625 11.2742C27.625 12.2447 26.8195 12.8698 26.0385 12.9313L27.9413 14.9189H26.1741L24.4402 13.0023H23.8313V14.9189H22.3851ZM25.4843 10.7306H23.8313V11.8664H25.5057C25.7956 11.8664 26.1694 11.6569 26.1694 11.2985C26.1694 11.0199 25.8809 10.7306 25.4843 10.7306ZM29.692 14.9182H28.2154V9.52317H29.692V14.9182ZM33.1931 14.9182H32.8744C31.3323 14.9182 30.396 13.7851 30.396 12.2429C30.396 10.6626 31.3218 9.52317 33.2692 9.52317H34.8676V10.8009H33.2108C32.4202 10.8009 31.8611 11.3763 31.8611 12.2562C31.8611 13.301 32.5004 13.7398 33.4215 13.7398H33.802L33.1931 14.9182ZM33.8521 14.9189L36.34 9.52391H38.2262L40.7117 14.9189H39.0563L38.5996 13.8386H35.9309L35.4719 14.9189H33.8521ZM38.074 12.6674L37.2605 10.7795L36.4447 12.6674H38.074ZM40.8805 14.9182V9.52317H42.7191L45.0667 12.9128V9.52317H46.5128V14.9182H44.7337L42.3267 11.4398V14.9182H40.8805ZM12.1099 24.3594V18.9643H16.8289V20.1711H13.5713V21.0939H16.7528V22.2297H13.5713V23.1881H16.8289V24.3594H12.1099ZM35.2329 24.3594V18.9643H39.9519V20.1711H36.6943V21.0939H39.8606V22.2297H36.6943V23.1881H39.9519V24.3594H35.2329ZM17.0121 24.3594L19.3097 21.6951L16.9574 18.9643H18.7793L20.1803 20.6525L21.586 18.9643H23.3366L21.0151 21.6618L23.317 24.3594H21.4953L20.1351 22.6978L18.8079 24.3594H17.0121ZM23.4887 24.3603V18.9653H26.6831C27.9938 18.9653 28.7595 19.7531 28.7595 20.7799C28.7595 22.0193 27.7832 22.6566 26.4952 22.6566H24.9729V24.3603H23.4887ZM26.5761 20.1853H24.973V21.4276H26.5714C26.9937 21.4276 27.2897 21.1665 27.2897 20.8064C27.2897 20.4232 26.9922 20.1853 26.5761 20.1853ZM29.3875 24.3594V18.9643H32.6056C33.672 18.9643 34.6274 19.5793 34.6274 20.7146C34.6274 21.6851 33.8218 22.3102 33.0409 22.3717L34.9437 24.3594H33.1765L31.4426 22.4427H30.8337V24.3594H29.3875ZM32.4867 20.171H30.8337V21.3068H32.5082C32.798 21.3068 33.1718 21.0974 33.1718 20.7389C33.1718 20.4603 32.8833 20.171 32.4867 20.171ZM40.6217 24.3594V23.1881H43.5159C43.9441 23.1881 44.1295 22.9722 44.1295 22.7355C44.1295 22.5087 43.9447 22.2794 43.5159 22.2794H42.208C41.0712 22.2794 40.4381 21.6334 40.4381 20.6636C40.4381 19.7985 41.0178 18.9643 42.7072 18.9643H45.5233L44.9144 20.1782H42.4788C42.0132 20.1782 41.8699 20.4061 41.8699 20.6237C41.8699 20.8473 42.047 21.0939 42.4027 21.0939H43.7727C45.04 21.0939 45.5899 21.7644 45.5899 22.6424C45.5899 23.5863 44.9772 24.3594 43.7038 24.3594H40.6217ZM45.7176 24.3594V23.1881H48.6118C49.04 23.1881 49.2254 22.9722 49.2254 22.7355C49.2254 22.5087 49.0406 22.2794 48.6118 22.2794H47.3039C46.1671 22.2794 45.534 21.6334 45.534 20.6636C45.534 19.7985 46.1138 18.9643 47.8031 18.9643H50.6192L50.0103 20.1782H47.5747C47.1092 20.1782 46.9658 20.4061 46.9658 20.6237C46.9658 20.8473 47.1429 21.0939 47.4986 21.0939H48.8687C50.1359 21.0939 50.6858 21.7644 50.6858 22.6424C50.6858 23.5863 50.0731 24.3594 48.7997 24.3594H45.7176Z",fill:"white"})),u=()=>(0,a.createElement)("svg",{width:"52",height:"35",viewBox:"0 0 52 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("rect",{x:"0.878906",y:"0.5",width:"50",height:"34",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M15.8352 13.0607C15.4642 13.5024 14.8707 13.8507 14.2771 13.8009C14.2029 13.2038 14.4935 12.5693 14.8336 12.1774C15.2045 11.7233 15.8537 11.3999 16.3792 11.375C16.4411 11.997 16.1999 12.6066 15.8352 13.0607ZM16.373 13.9192C15.8501 13.8889 15.373 14.0774 14.9876 14.2297C14.7396 14.3277 14.5296 14.4106 14.3698 14.4106C14.1905 14.4106 13.9718 14.3232 13.7263 14.2251C13.4046 14.0965 13.0367 13.9495 12.651 13.9565C11.7669 13.969 10.9446 14.4728 10.4933 15.2753C9.56588 16.8801 10.2522 19.2563 11.1486 20.5626C11.5876 21.2095 12.1131 21.9186 12.8056 21.8937C13.1102 21.8822 13.3294 21.7886 13.5562 21.6918C13.8173 21.5803 14.0885 21.4645 14.512 21.4645C14.9208 21.4645 15.1802 21.5773 15.4292 21.6856C15.6659 21.7885 15.8933 21.8874 16.2308 21.8813C16.948 21.8689 17.3993 21.2344 17.8383 20.5875C18.312 19.8931 18.5202 19.2155 18.5518 19.1127L18.5555 19.1008C18.5547 19.1 18.5488 19.0973 18.5385 19.0926C18.3802 19.0196 17.1698 18.4621 17.1582 16.9672C17.1465 15.7124 18.1182 15.0767 18.2712 14.9766L18.2712 14.9766C18.2805 14.9705 18.2868 14.9664 18.2896 14.9642C17.6713 14.0436 16.7068 13.9441 16.373 13.9192ZM21.3377 21.8128V12.1153H24.9546C26.8217 12.1153 28.1263 13.4091 28.1263 15.3001C28.1263 17.1911 26.797 18.4974 24.9051 18.4974H22.8339V21.8128H21.3377ZM22.8339 13.3841H24.5589C25.8572 13.3841 26.5991 14.0808 26.5991 15.3062C26.5991 16.5317 25.8572 17.2346 24.5527 17.2346H22.8339V13.3841ZM33.0661 20.6496C32.6704 21.4085 31.7986 21.8874 30.8589 21.8874C29.4678 21.8874 28.4971 21.0539 28.4971 19.7974C28.4971 18.5533 29.4368 17.838 31.1742 17.7322L33.0413 17.6203V17.0853C33.0413 16.2953 32.5282 15.8661 31.6131 15.8661C30.8589 15.8661 30.3086 16.258 30.1973 16.8552H28.8495C28.8928 15.5986 30.0675 14.6842 31.6564 14.6842C33.369 14.6842 34.4819 15.5862 34.4819 16.9858V21.8128H33.097V20.6496H33.0661ZM31.2609 20.7368C30.4633 20.7368 29.9563 20.3511 29.9563 19.7602C29.9563 19.1506 30.4448 18.796 31.3784 18.74L33.0415 18.6343V19.1817C33.0415 20.0898 32.2748 20.7368 31.2609 20.7368ZM39.0756 22.1922C38.4759 23.8903 37.7897 24.4502 36.3306 24.4502C36.2193 24.4502 35.8483 24.4377 35.7617 24.4129V23.2496C35.8545 23.2621 36.0832 23.2745 36.2007 23.2745C36.8623 23.2745 37.2332 22.9946 37.462 22.2668L37.598 21.8376L35.0631 14.7775H36.6273L38.3894 20.5065H38.4203L40.1823 14.7775H41.7033L39.0756 22.1922Z",fill:"black"})),d=()=>(0,a.createElement)("svg",{width:"52",height:"35",viewBox:"0 0 52 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("rect",{x:"1.18945",y:"0.5",width:"50",height:"34",rx:"3.5",fill:"#006FCF",stroke:"#F3F3F3"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M11.1205 25.2823V18.0771H19.3189L20.1985 19.1441L21.1072 18.0771H50.8653V24.7854C50.8653 24.7854 50.0871 25.2751 49.187 25.2823H32.7093L31.7176 24.1465V25.2823H28.4679V23.3435C28.4679 23.3435 28.0239 23.6141 27.0642 23.6141H25.9581V25.2823H21.0376L20.1593 24.1924L19.2675 25.2823H11.1205ZM1.56836 12.6465L3.41294 8.63574H6.60294L7.64976 10.8824V8.63574H11.6152L12.2384 10.2596L12.8425 8.63574H30.6434V9.4521C30.6434 9.4521 31.5792 8.63574 33.1171 8.63574L38.8928 8.65457L39.9215 10.8718V8.63574H43.24L44.1534 9.90939V8.63574H47.5023V15.841H44.1534L43.2781 14.5632V15.841H38.4025L37.9121 14.7052H36.6014L36.1191 15.841H32.8126C31.4893 15.841 30.6434 15.0413 30.6434 15.0413V15.841H25.658L24.6685 14.7052V15.841H6.13036L5.64039 14.7052H4.33383L3.84732 15.841H1.56836V12.6465ZM1.5779 14.9189L4.06583 9.52391H5.95199L8.43755 14.9189H6.7821L6.32542 13.8386H3.65672L3.19767 14.9189H1.5779ZM5.79982 12.6674L4.98636 10.7795L4.17053 12.6674H5.79982ZM8.60869 14.9182V9.52317L10.9105 9.53115L12.2493 13.0095L13.556 9.52317H15.8394V14.9182H14.3933V10.9429L12.8603 14.9182H11.592L10.0548 10.9429V14.9182H8.60869ZM16.8289 14.9182V9.52317H21.5479V10.73H18.2902V11.6528H21.4717V12.7886H18.2902V13.7469H21.5479V14.9182H16.8289ZM22.3851 14.9189V9.52391H25.6033C26.6696 9.52391 27.625 10.1389 27.625 11.2742C27.625 12.2447 26.8195 12.8698 26.0385 12.9313L27.9413 14.9189H26.1741L24.4402 13.0023H23.8313V14.9189H22.3851ZM25.4843 10.7306H23.8313V11.8664H25.5057C25.7956 11.8664 26.1694 11.6569 26.1694 11.2985C26.1694 11.0199 25.8809 10.7306 25.4843 10.7306ZM29.692 14.9182H28.2154V9.52317H29.692V14.9182ZM33.1931 14.9182H32.8744C31.3323 14.9182 30.396 13.7851 30.396 12.2429C30.396 10.6626 31.3218 9.52317 33.2692 9.52317H34.8676V10.8009H33.2108C32.4202 10.8009 31.8611 11.3763 31.8611 12.2562C31.8611 13.301 32.5004 13.7398 33.4215 13.7398H33.802L33.1931 14.9182ZM33.8521 14.9189L36.34 9.52391H38.2262L40.7117 14.9189H39.0563L38.5996 13.8386H35.9309L35.4719 14.9189H33.8521ZM38.074 12.6674L37.2605 10.7795L36.4447 12.6674H38.074ZM40.8805 14.9182V9.52317H42.7191L45.0667 12.9128V9.52317H46.5128V14.9182H44.7337L42.3267 11.4398V14.9182H40.8805ZM12.1099 24.3594V18.9643H16.8289V20.1711H13.5713V21.0939H16.7528V22.2297H13.5713V23.1881H16.8289V24.3594H12.1099ZM35.2329 24.3594V18.9643H39.9519V20.1711H36.6943V21.0939H39.8606V22.2297H36.6943V23.1881H39.9519V24.3594H35.2329ZM17.0121 24.3594L19.3097 21.6951L16.9574 18.9643H18.7793L20.1803 20.6525L21.586 18.9643H23.3366L21.0151 21.6618L23.317 24.3594H21.4953L20.1351 22.6978L18.8079 24.3594H17.0121ZM23.4887 24.3603V18.9653H26.6831C27.9938 18.9653 28.7595 19.7531 28.7595 20.7799C28.7595 22.0193 27.7832 22.6566 26.4952 22.6566H24.9729V24.3603H23.4887ZM26.5761 20.1853H24.973V21.4276H26.5714C26.9937 21.4276 27.2897 21.1665 27.2897 20.8064C27.2897 20.4232 26.9922 20.1853 26.5761 20.1853ZM29.3875 24.3594V18.9643H32.6056C33.672 18.9643 34.6274 19.5793 34.6274 20.7146C34.6274 21.6851 33.8218 22.3102 33.0409 22.3717L34.9437 24.3594H33.1765L31.4426 22.4427H30.8337V24.3594H29.3875ZM32.4867 20.171H30.8337V21.3068H32.5082C32.798 21.3068 33.1718 21.0974 33.1718 20.7389C33.1718 20.4603 32.8833 20.171 32.4867 20.171ZM40.6217 24.3594V23.1881H43.5159C43.9441 23.1881 44.1295 22.9722 44.1295 22.7355C44.1295 22.5087 43.9447 22.2794 43.5159 22.2794H42.208C41.0712 22.2794 40.4381 21.6334 40.4381 20.6636C40.4381 19.7985 41.0178 18.9643 42.7072 18.9643H45.5233L44.9144 20.1782H42.4788C42.0132 20.1782 41.8699 20.4061 41.8699 20.6237C41.8699 20.8473 42.047 21.0939 42.4027 21.0939H43.7727C45.04 21.0939 45.5899 21.7644 45.5899 22.6424C45.5899 23.5863 44.9772 24.3594 43.7038 24.3594H40.6217ZM45.7176 24.3594V23.1881H48.6118C49.04 23.1881 49.2254 22.9722 49.2254 22.7355C49.2254 22.5087 49.0406 22.2794 48.6118 22.2794H47.3039C46.1671 22.2794 45.534 21.6334 45.534 20.6636C45.534 19.7985 46.1138 18.9643 47.8031 18.9643H50.6192L50.0103 20.1782H47.5747C47.1092 20.1782 46.9658 20.4061 46.9658 20.6237C46.9658 20.8473 47.1429 21.0939 47.4986 21.0939H48.8687C50.1359 21.0939 50.6858 21.7644 50.6858 22.6424C50.6858 23.5863 50.0731 24.3594 48.7997 24.3594H45.7176Z",fill:"white"})),p=()=>(0,a.createElement)("svg",{width:"52",height:"35",viewBox:"0 0 52 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("rect",{x:"0.878906",y:"0.5",width:"50",height:"34",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M15.8352 13.0607C15.4642 13.5024 14.8707 13.8507 14.2771 13.8009C14.2029 13.2038 14.4935 12.5693 14.8336 12.1774C15.2045 11.7233 15.8537 11.3999 16.3792 11.375C16.4411 11.997 16.1999 12.6066 15.8352 13.0607ZM16.373 13.9192C15.8501 13.8889 15.373 14.0774 14.9876 14.2297C14.7396 14.3277 14.5296 14.4106 14.3698 14.4106C14.1905 14.4106 13.9718 14.3232 13.7263 14.2251C13.4046 14.0965 13.0367 13.9495 12.651 13.9565C11.7669 13.969 10.9446 14.4728 10.4933 15.2753C9.56588 16.8801 10.2522 19.2563 11.1486 20.5626C11.5876 21.2095 12.1131 21.9186 12.8056 21.8937C13.1102 21.8822 13.3294 21.7886 13.5562 21.6918C13.8173 21.5803 14.0885 21.4645 14.512 21.4645C14.9208 21.4645 15.1802 21.5773 15.4292 21.6856C15.6659 21.7885 15.8933 21.8874 16.2308 21.8813C16.948 21.8689 17.3993 21.2344 17.8383 20.5875C18.312 19.8931 18.5202 19.2155 18.5518 19.1127L18.5555 19.1008C18.5547 19.1 18.5488 19.0973 18.5385 19.0926C18.3802 19.0196 17.1698 18.4621 17.1582 16.9672C17.1465 15.7124 18.1182 15.0767 18.2712 14.9766L18.2712 14.9766C18.2805 14.9705 18.2868 14.9664 18.2896 14.9642C17.6713 14.0436 16.7068 13.9441 16.373 13.9192ZM21.3377 21.8128V12.1153H24.9546C26.8217 12.1153 28.1263 13.4091 28.1263 15.3001C28.1263 17.1911 26.797 18.4974 24.9051 18.4974H22.8339V21.8128H21.3377ZM22.8339 13.3841H24.5589C25.8572 13.3841 26.5991 14.0808 26.5991 15.3062C26.5991 16.5317 25.8572 17.2346 24.5527 17.2346H22.8339V13.3841ZM33.0661 20.6496C32.6704 21.4085 31.7986 21.8874 30.8589 21.8874C29.4678 21.8874 28.4971 21.0539 28.4971 19.7974C28.4971 18.5533 29.4368 17.838 31.1742 17.7322L33.0413 17.6203V17.0853C33.0413 16.2953 32.5282 15.8661 31.6131 15.8661C30.8589 15.8661 30.3086 16.258 30.1973 16.8552H28.8495C28.8928 15.5986 30.0675 14.6842 31.6564 14.6842C33.369 14.6842 34.4819 15.5862 34.4819 16.9858V21.8128H33.097V20.6496H33.0661ZM31.2609 20.7368C30.4633 20.7368 29.9563 20.3511 29.9563 19.7602C29.9563 19.1506 30.4448 18.796 31.3784 18.74L33.0415 18.6343V19.1817C33.0415 20.0898 32.2748 20.7368 31.2609 20.7368ZM39.0756 22.1922C38.4759 23.8903 37.7897 24.4502 36.3306 24.4502C36.2193 24.4502 35.8483 24.4377 35.7617 24.4129V23.2496C35.8545 23.2621 36.0832 23.2745 36.2007 23.2745C36.8623 23.2745 37.2332 22.9946 37.462 22.2668L37.598 21.8376L35.0631 14.7775H36.6273L38.3894 20.5065H38.4203L40.1823 14.7775H41.7033L39.0756 22.1922Z",fill:"black"})),C=()=>(0,a.createElement)("svg",{width:"52",height:"35",viewBox:"0 0 52 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("rect",{x:"1.18945",y:"0.5",width:"49.6897",height:"34",rx:"2.5",fill:"url(#paint0_linear)",stroke:"#F1F1F1"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M19.7636 16.9816H26.6269C26.5657 15.4963 26.2201 13.9649 25.1717 12.9813C23.9229 11.8096 21.7355 11.375 19.781 11.375C17.7466 11.375 15.4968 11.8517 14.2414 13.1088C13.1588 14.1918 12.9248 15.9341 12.9248 17.4997C12.9248 19.1395 13.3827 21.0469 14.5571 22.1456C15.8059 23.3147 17.8294 23.625 19.781 23.625C21.6767 23.625 23.7302 23.2746 24.9718 22.1647C26.2099 21.0561 26.6377 19.1888 26.6377 17.4997V17.4918H19.7636V16.9816ZM27.0876 17.4921V23.3511H36.6352V23.3432C38.0322 23.267 39.1436 22.0059 39.1436 20.4575C39.1436 18.9084 38.0322 17.5664 36.6352 17.4895V17.4921H27.0876ZM36.5263 11.6203C37.8879 11.6203 38.9687 12.8032 38.9687 14.2957C38.9687 15.7087 37.9762 16.8626 36.7135 16.9816H27.0873V11.6118H36.2251C36.2813 11.6049 36.3468 11.6097 36.4108 11.6144C36.4508 11.6174 36.4901 11.6203 36.5263 11.6203Z",fill:"#FEFEFE"}),(0,a.createElement)("defs",null,(0,a.createElement)("linearGradient",{id:"paint0_linear",x1:"14.4385",y1:"-4.43215",x2:"2.09335",y2:"33.4202",gradientUnits:"userSpaceOnUse"},(0,a.createElement)("stop",{stopColor:"#222E72"}),(0,a.createElement)("stop",{offset:"0.591647",stopColor:"#40CBFF"}),(0,a.createElement)("stop",{offset:"1",stopColor:"#3CB792"})))),h=()=>(0,a.createElement)("svg",{width:"52",height:"35",viewBox:"0 0 52 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("rect",{x:"0.878906",y:"0.5",width:"50",height:"34",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M17.1461 17.7823C17.1461 19.9191 18.8322 21.576 21.0021 21.576C21.6154 21.576 22.141 21.456 22.7888 21.1524V19.4832C22.2192 20.0506 21.7145 20.2795 21.0685 20.2795C19.6332 20.2795 18.6146 19.2439 18.6146 17.7717C18.6146 16.376 19.6654 15.2751 21.0021 15.2751C21.6818 15.2751 22.1963 15.5163 22.7888 16.0929V14.4246C22.1633 14.1089 21.649 13.978 21.0357 13.978C18.8768 13.978 17.1461 15.6685 17.1461 17.7823ZM13.4892 16.0168C13.4892 16.4097 13.7401 16.6173 14.5953 16.9321C16.2163 17.5222 16.6967 18.0449 16.6967 19.2001C16.6967 20.6072 15.6577 21.5872 14.177 21.5872C13.0926 21.5872 12.304 21.1622 11.6475 20.2031L12.5682 19.3209C12.8962 19.9523 13.4437 20.2907 14.1234 20.2907C14.7593 20.2907 15.2298 19.8542 15.2298 19.2653C15.2298 18.96 15.0873 18.6979 14.8025 18.5128C14.6594 18.4252 14.3754 18.2949 13.8174 18.0988C12.479 17.6197 12.0201 17.1068 12.0201 16.1053C12.0201 14.9155 13.006 14.0224 14.2987 14.0224C15.0997 14.0224 15.8327 14.2948 16.4455 14.8282L15.6998 15.7997C15.3286 15.3857 14.9775 15.211 14.5507 15.211C13.9366 15.211 13.4892 15.559 13.4892 16.0168ZM9.68583 21.4123H11.1109V14.1424H9.68583V21.4123ZM6.77288 19.6035C6.32524 20.006 5.74353 20.1815 4.82283 20.1815H4.44039V15.374H4.82283C5.74353 15.374 6.30238 15.538 6.77288 15.9621C7.26569 16.3986 7.56205 17.0755 7.56205 17.7717C7.56205 18.4697 7.26569 19.1671 6.77288 19.6035ZM5.10834 14.1424H3.0166V21.4121H5.09733C6.20374 21.4121 7.0025 21.1525 7.70389 20.5728C8.53737 19.8867 9.03017 18.8523 9.03017 17.7824C9.03017 15.6369 7.41938 14.1424 5.10834 14.1424ZM32.1394 14.1424L34.0875 19.0255L36.061 14.1424H37.6057L34.4496 21.5988H33.6828L30.5826 14.1424H32.1394ZM38.2501 21.4122H42.2913V20.1815H39.6741V18.2191H42.1951V16.9878H39.6741V15.3742H42.2913V14.1424H38.2501V21.4122ZM44.6585 17.4893H45.0748C45.9851 17.4893 46.4674 17.0958 46.4674 16.365C46.4674 15.6575 45.9851 15.2876 45.0974 15.2876H44.6585V17.4893ZM45.3485 14.1422C46.9918 14.1422 47.9339 14.9275 47.9339 16.2886C47.9339 17.4016 47.3429 18.1325 46.2695 18.3496L48.5695 21.4121H46.817L44.8447 18.4917H44.6587V21.4121H43.2353V14.1422H45.3485Z",fill:"#1D1D1B"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M30.415 19.8859C31.5716 18.0862 31.0433 15.6953 29.235 14.5445C27.4267 13.3937 25.0236 13.9191 23.867 15.7188C22.7107 17.518 23.2391 19.9096 25.0474 21.0604C26.8557 22.2112 29.2587 21.6851 30.415 19.8859Z",fill:"url(#paint0_linear)"}),(0,a.createElement)("defs",null,(0,a.createElement)("linearGradient",{id:"paint0_linear",x1:"32.5088",y1:"16.6279",x2:"25.9795",y2:"12.4317",gradientUnits:"userSpaceOnUse"},(0,a.createElement)("stop",{stopColor:"#F6A000"}),(0,a.createElement)("stop",{offset:"0.623918",stopColor:"#E47E02"}),(0,a.createElement)("stop",{offset:"1",stopColor:"#D36002"})))),g=()=>(0,a.createElement)("svg",{width:"64",height:"40",viewBox:"0 0 64 40",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("path",{d:"M0 4C0 1.79086 1.79086 0 4 0H60C62.2091 0 64 1.79086 64 4V36C64 38.2091 62.2091 40 60 40H4C1.79086 40 0 38.2091 0 36V4Z",fill:"white"}),(0,a.createElement)("path",{d:"M12 11C12 8.79086 13.7909 7 16 7H48.8571C51.0663 7 52.8571 8.79086 52.8571 11V29C52.8571 31.2091 51.0663 33 48.8571 33H16C13.7909 33 12 31.2091 12 29V11Z",fill:"white"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M48.8571 9H16C14.8954 9 14 9.89543 14 11V29C14 30.1046 14.8954 31 16 31H48.8571C49.9617 31 50.8571 30.1046 50.8571 29V11C50.8571 9.89543 49.9617 9 48.8571 9ZM16 7C13.7909 7 12 8.79086 12 11V29C12 31.2091 13.7909 33 16 33H48.8571C51.0663 33 52.8571 31.2091 52.8571 29V11C52.8571 8.79086 51.0663 7 48.8571 7H16Z",fill:"#0F3365"}),(0,a.createElement)("path",{d:"M32.4285 7H48.857C51.0662 7 52.857 8.79086 52.857 11V29C52.857 31.2091 51.0662 33 48.857 33H32.4285V7Z",fill:"#0F3365"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M35.2142 28.1058H39.4694V22.1896H39.5169C40.3211 23.4859 41.9291 23.966 43.4183 23.966C47.083 23.966 49.0452 21.2899 49.0452 18.0698C49.0452 15.4361 47.1788 12.5715 43.7737 12.5715C41.8349 12.5715 40.0373 13.2615 39.1865 14.7878H39.139V12.8223H35.2142V28.1058ZM44.648 18.1539C44.648 19.8896 43.6784 21.0808 42.0716 21.0808C40.6524 21.0808 39.4691 19.8896 39.4691 18.2999C39.4691 16.6696 40.5103 15.4567 42.0716 15.4567C43.7259 15.4567 44.648 16.7105 44.648 18.1539Z",fill:"#FFFFFE"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M29.6351 12.8223H25.7804V14.7871H25.7341C24.8353 13.4496 23.369 12.5715 21.549 12.5715C17.7192 12.5715 15.9929 14.9962 15.9929 18.2372C15.9929 21.4584 18.0972 23.7145 21.4785 23.7145C23.1807 23.7145 24.5991 23.1307 25.615 21.8537H25.6629V22.4404C25.6629 24.573 24.3395 25.5971 21.8802 25.5971C20.1068 25.5971 19.0193 25.2629 17.7192 24.6984L17.5059 27.6676C18.4993 27.98 20.1781 28.3572 22.2111 28.3572C27.1762 28.3572 29.6351 26.9139 29.6351 22.4404V12.8223ZM20.3904 18.1742C20.3904 16.6902 21.218 15.4567 22.8022 15.4567C24.7169 15.4567 25.521 16.8146 25.521 18.0278C25.521 19.7012 24.3148 20.83 22.8022 20.83C21.5255 20.83 20.3904 19.8672 20.3904 18.1742Z",fill:"#D8232A"}),(0,a.createElement)("path",{d:"M4 1H60V-1H4V1ZM63 4V36H65V4H63ZM60 39H4V41H60V39ZM1 36V4H-1V36H1ZM4 39C2.34315 39 1 37.6569 1 36H-1C-1 38.7614 1.23858 41 4 41V39ZM63 36C63 37.6569 61.6569 39 60 39V41C62.7614 41 65 38.7614 65 36H63ZM60 1C61.6569 1 63 2.34315 63 4H65C65 1.23858 62.7614 -1 60 -1V1ZM4 -1C1.23858 -1 -1 1.23858 -1 4H1C1 2.34315 2.34315 1 4 1V-1Z",fill:"#DDDDDD"})),f=()=>(0,a.createElement)("svg",{width:"84",height:"58",viewBox:"0 0 84 58",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("rect",{x:"0.5",y:"0.5",width:"83",height:"57",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M40.4322 34.9714V28.4303V28.429H43.9665C45.4236 28.4299 46.6482 27.9715 47.6403 27.0538C48.6429 26.1828 49.2035 24.9306 49.175 23.6253C49.1948 22.3273 48.6353 21.0844 47.6403 20.2162C46.6569 19.2935 45.3343 18.7901 43.9665 18.8178H38.293V34.9714H40.4322ZM40.4322 26.4454V20.8065V20.8052H44.0196C44.8192 20.7834 45.59 21.0956 46.1362 21.6624C46.6859 22.1807 46.996 22.8927 46.996 23.6363C46.996 24.3799 46.6859 25.0919 46.1362 25.6102C45.5833 26.1651 44.8149 26.4683 44.0196 26.4454H40.4322Z",fill:"#5F6368"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M57.8487 24.7879C56.9337 23.9682 55.6856 23.5583 54.1045 23.5583C52.0733 23.5583 50.5396 24.2801 49.5032 25.7236L51.3873 26.8746C52.0835 25.8968 53.0291 25.4079 54.2241 25.4079C54.9859 25.3992 55.7231 25.6693 56.2888 26.1644C56.8529 26.6168 57.1773 27.2912 57.1724 28.0011V28.4754C56.3504 28.0243 55.3043 27.7987 54.034 27.7987C52.5459 27.8005 51.3563 28.139 50.4652 28.8144C49.5741 29.4898 49.1285 30.3993 49.1285 31.543C49.1091 32.5844 49.5748 33.5787 50.3961 34.2496C51.2411 34.9714 52.2912 35.3323 53.5464 35.3323C55.0168 35.3323 56.1949 34.7007 57.0807 33.4376H57.1738V34.9714H59.2199V28.1596C59.2208 26.7315 58.7638 25.6076 57.8487 24.7879ZM52.0464 32.9414C51.5983 32.6281 51.334 32.1239 51.3369 31.5881C51.3369 30.9862 51.6279 30.4848 52.2138 30.0723C52.794 29.6659 53.5186 29.4627 54.3876 29.4627C55.5808 29.4627 56.5108 29.7205 57.1778 30.236C57.1778 31.1073 56.8235 31.8665 56.1149 32.5135C55.4766 33.1327 54.6113 33.4813 53.7086 33.4827C53.1068 33.4937 52.5195 33.3024 52.0464 32.9414Z",fill:"#5F6368"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M70.9654 23.9192L63.8223 39.8434H61.6141L64.2648 34.2716L59.5679 23.9192H61.8931L65.2879 31.8588H65.3344L68.6362 23.9192H70.9654Z",fill:"#5F6368"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M31.7877 27.0203C31.7885 26.3879 31.7333 25.7566 31.6229 25.1333H22.6011V28.7074H27.7684C27.5547 29.8609 26.8645 30.8797 25.8577 31.5275V33.8475H28.9416C30.7473 32.2325 31.7877 29.8442 31.7877 27.0203Z",fill:"#4285F4"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M22.6012 36.0889C25.1828 36.0889 27.3566 35.2666 28.9417 33.8488L25.8578 31.5288C24.9995 32.0933 23.894 32.4156 22.6012 32.4156C20.1059 32.4156 17.988 30.7838 17.2306 28.585H14.0537V30.9759C15.6776 34.1104 18.9848 36.0887 22.6012 36.0889Z",fill:"#34A853"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M17.2307 28.585C16.8303 27.4328 16.8303 26.185 17.2307 25.0328V22.6419H14.0538C12.6956 25.2637 12.6956 28.3541 14.0538 30.9759L17.2307 28.585Z",fill:"#FBBC04"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M22.6012 21.2022C23.9655 21.1806 25.2837 21.6806 26.271 22.5942L29.0015 19.9456C27.2701 18.368 24.9764 17.502 22.6012 17.5289C18.9848 17.529 15.6776 19.5074 14.0537 22.6419L17.2306 25.0328C17.988 22.834 20.1059 21.2022 22.6012 21.2022Z",fill:"#EA4335"})),y=()=>(0,a.createElement)("svg",{width:"51",height:"35",viewBox:"0 0 51 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("rect",{x:"0.878906",y:"0.5",width:"49",height:"34",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M33.473 17.5973H33.494H33.5346H33.5752H33.6158H33.6564H33.697H33.7376H33.7782H33.8188H33.8594H33.9H33.9406H33.9812H34.0218H34.0624H34.103H34.1436H34.1842H34.2248H34.2654H34.306H34.3466H34.3872H34.4278H34.4684H34.509H34.5496H34.5902H34.6308H34.6714H34.712H34.7527H34.7932H34.8338H34.8745H34.915H34.9556H34.9963H35.0368H35.0774H35.1181H35.1586H35.1992H35.2399H35.2805H35.321H35.3617H35.4023H35.4428H35.4835H35.5241H35.5646H35.6053H35.6459H35.6864H35.7271H35.7677H35.8083H35.8489H35.8895H35.9233L35.9301 17.5975C35.9426 17.5978 35.9563 17.5984 35.9707 17.5991C35.9837 17.5999 35.9973 17.6008 36.0113 17.6018C36.0246 17.6028 36.0383 17.604 36.0519 17.6052C36.0656 17.6065 36.0792 17.6079 36.0925 17.6093C36.1064 17.6109 36.1201 17.6126 36.1331 17.6143C36.1476 17.6163 36.1613 17.6183 36.1737 17.6205C36.1826 17.622 36.1909 17.6235 36.1982 17.6251L36.2143 17.6288C36.2279 17.632 36.2414 17.6354 36.2549 17.6392C36.2685 17.643 36.282 17.647 36.2955 17.6513C36.3091 17.6557 36.3227 17.6603 36.3361 17.6653C36.3497 17.6702 36.3632 17.6755 36.3767 17.681C36.3903 17.6866 36.4039 17.6926 36.4173 17.6987C36.431 17.705 36.4445 17.7116 36.4579 17.7185C36.4716 17.7254 36.4851 17.7328 36.4985 17.7403C36.5122 17.7481 36.5257 17.7562 36.5391 17.7646C36.5528 17.7732 36.5664 17.7821 36.5797 17.7913C36.5934 17.8008 36.607 17.8105 36.6203 17.8206C36.634 17.8311 36.6476 17.8419 36.6609 17.8531C36.6747 17.8646 36.6882 17.8766 36.7015 17.8889C36.7154 17.9017 36.7289 17.9148 36.7421 17.9284C36.7561 17.9426 36.7695 17.9574 36.7827 17.9725C36.7967 17.9885 36.8102 18.005 36.8233 18.0219C36.8374 18.0401 36.8509 18.0587 36.8639 18.0779C36.8781 18.0989 36.8917 18.1205 36.9045 18.1426C36.919 18.1675 36.9325 18.1932 36.9451 18.2196C36.96 18.2508 36.9736 18.283 36.9857 18.3161C37.002 18.3606 37.0156 18.4067 37.0263 18.4544C37.0449 18.5369 37.0549 18.6238 37.0549 18.7145C37.0549 18.8057 37.0449 18.893 37.0263 18.9758C37.0156 19.0235 37.002 19.0698 36.9857 19.1143C36.9736 19.1476 36.96 19.1798 36.9451 19.2111C36.9325 19.2375 36.919 19.2632 36.9045 19.2882C36.8917 19.3103 36.8782 19.3319 36.8639 19.3529C36.8509 19.3721 36.8374 19.3908 36.8233 19.409C36.8102 19.4259 36.7967 19.4423 36.7827 19.4584C36.7695 19.4734 36.756 19.4881 36.7421 19.5024C36.7289 19.516 36.7153 19.5291 36.7015 19.5419C36.6882 19.5541 36.6747 19.566 36.6609 19.5775C36.6476 19.5886 36.6341 19.5995 36.6203 19.6099C36.607 19.62 36.5934 19.6297 36.5797 19.6391C36.5664 19.6483 36.5528 19.6572 36.5391 19.6657C36.5257 19.674 36.5122 19.6821 36.4985 19.6898C36.4851 19.6974 36.4716 19.7046 36.4579 19.7116C36.4445 19.7184 36.431 19.7249 36.4173 19.7312C36.4039 19.7373 36.3903 19.7432 36.3767 19.7488C36.3632 19.7542 36.3497 19.7595 36.3361 19.7644C36.3227 19.7692 36.3091 19.7738 36.2955 19.7781C36.282 19.7824 36.2685 19.7863 36.2549 19.7901C36.2414 19.7937 36.2279 19.7971 36.2143 19.8003L36.1982 19.8039C36.1909 19.8056 36.1826 19.8073 36.1737 19.8089C36.1613 19.8111 36.1476 19.8132 36.1331 19.8152C36.1201 19.8169 36.1064 19.8186 36.0925 19.8202C36.0792 19.8217 36.0656 19.823 36.0519 19.8243C36.0383 19.8255 36.0246 19.8266 36.0113 19.8276C35.9973 19.8286 35.9837 19.8295 35.9707 19.8301C35.9563 19.8309 35.9426 19.8314 35.9301 19.8317L35.9083 19.832H35.8895H35.8489H35.8083H35.7677H35.7271H35.6864H35.6459H35.6053H35.5646H35.5241H35.4835H35.4428H35.4023H35.3617H35.321H35.2805H35.2399H35.1992H35.1586H35.1181H35.0774H35.0368H34.9963H34.9556H34.915H34.8745H34.8338H34.7932H34.7527H34.712H34.6714H34.6308H34.5902H34.5496H34.509H34.4684H34.4278H34.3872H34.3466H34.306H34.2654H34.2248H34.1842H34.1436H34.103H34.0624H34.0218H33.9812H33.9406H33.9H33.8594H33.8188H33.7782H33.7376H33.697H33.6564H33.6158H33.5752H33.5346H33.494H33.473V17.5973ZM36.7423 15.0681C36.7586 15.1431 36.7673 15.2226 36.7673 15.3062C36.7673 15.3898 36.7586 15.4691 36.7423 15.5441C36.7318 15.5923 36.7181 15.6385 36.7016 15.6829C36.6896 15.7153 36.676 15.7467 36.661 15.7769C36.6485 15.8023 36.6349 15.8269 36.6204 15.8508C36.6076 15.8718 36.594 15.8923 36.5798 15.9122C36.5668 15.9303 36.5533 15.9478 36.5392 15.9649C36.5261 15.9807 36.5126 15.9961 36.4986 16.011C36.4855 16.0251 36.472 16.0387 36.4581 16.0518C36.4448 16.0644 36.4313 16.0765 36.4174 16.0883C36.4042 16.0996 36.3906 16.1104 36.3768 16.121C36.3636 16.1311 36.35 16.1409 36.3363 16.1504C36.3229 16.1596 36.3094 16.1684 36.2956 16.1769C36.2823 16.1852 36.2687 16.1931 36.255 16.2007C36.2417 16.2082 36.2281 16.2154 36.2145 16.2222C36.2011 16.2289 36.1875 16.2353 36.1738 16.2415C36.1604 16.2475 36.1469 16.2532 36.1332 16.2586C36.1198 16.2639 36.1063 16.269 36.0926 16.2737C36.0792 16.2784 36.0657 16.2829 36.0521 16.287C36.0386 16.2911 36.0251 16.295 36.0114 16.2985C35.998 16.3021 35.9845 16.3053 35.9708 16.3083C35.9574 16.3113 35.9439 16.3139 35.9303 16.3164L35.9204 16.3182C35.9125 16.3195 35.902 16.3211 35.8896 16.3227C35.8777 16.3242 35.8639 16.3258 35.849 16.3273C35.8362 16.3286 35.8225 16.3299 35.8085 16.3311C35.7952 16.3322 35.7815 16.3332 35.7678 16.334C35.7542 16.3349 35.7405 16.3357 35.7272 16.3362C35.7132 16.3368 35.6995 16.3372 35.6867 16.3372L35.6802 16.3373H35.6461H35.6054H35.5648H35.5243H35.4836H35.443H35.4025H35.3619H35.3212H35.2807H35.2401H35.1994H35.1589H35.1183H35.0776H35.037H34.9965H34.9559H34.9152H34.8747H34.8341H34.7934H34.7529H34.7123H34.6716H34.6311H34.5905H34.5499H34.5092H34.4687H34.4281H34.3874H34.3469H34.3063H34.2657H34.2251H34.1845H34.1439H34.1033H34.0627H34.0221H33.9814H33.9409H33.9003H33.8597H33.8191H33.7785H33.7379H33.6973H33.6567H33.6161H33.5754H33.5349H33.4943H33.473V14.2751H33.4943H33.5349H33.5754H33.6161H33.6567H33.6973H33.7379H33.7785H33.8191H33.8597H33.9003H33.9409H33.9814H34.0221H34.0627H34.1033H34.1439H34.1845H34.2251H34.2657H34.3063H34.3469H34.3874H34.4281H34.4687H34.5092H34.5499H34.5905H34.6311H34.6716H34.7123H34.7529H34.7934H34.8341H34.8747H34.9152H34.9559H34.9965H35.037H35.0776H35.1183H35.1589H35.1994H35.2401H35.2807H35.3212H35.3619H35.4025H35.443H35.4836H35.5243H35.5648H35.6054H35.6461H35.6867H35.7037L35.7272 14.2759C35.7405 14.2764 35.7542 14.2772 35.7678 14.2781C35.7815 14.279 35.7952 14.28 35.8085 14.2811C35.8225 14.2822 35.8362 14.2835 35.849 14.2848C35.8639 14.2862 35.8777 14.2877 35.8896 14.2891C35.902 14.2905 35.9125 14.2919 35.9204 14.293L35.9303 14.2948C35.9439 14.2972 35.9574 14.3 35.9708 14.303C35.9845 14.306 35.998 14.3093 36.0114 14.3129C36.0251 14.3165 36.0386 14.3203 36.0521 14.3245C36.0657 14.3287 36.0792 14.3331 36.0926 14.3378C36.1063 14.3427 36.1198 14.3477 36.1332 14.3531C36.1469 14.3585 36.1604 14.3643 36.1738 14.3703C36.1875 14.3765 36.2011 14.3829 36.2145 14.3896C36.2281 14.3965 36.2417 14.4036 36.255 14.4111C36.2687 14.4187 36.2823 14.4267 36.2956 14.435C36.3094 14.4435 36.3229 14.4523 36.3363 14.4615C36.35 14.4709 36.3636 14.4808 36.3768 14.491C36.3906 14.5015 36.4042 14.5124 36.4174 14.5237C36.4313 14.5355 36.4448 14.5476 36.4581 14.5602C36.472 14.5733 36.4855 14.5869 36.4986 14.601C36.5126 14.6159 36.5261 14.6313 36.5392 14.6471C36.5533 14.6642 36.5668 14.6818 36.5798 14.6999C36.594 14.7197 36.6076 14.7402 36.6204 14.7613C36.6349 14.7851 36.6485 14.8097 36.661 14.8351C36.676 14.8654 36.6896 14.8968 36.7016 14.9292C36.7181 14.9736 36.7318 15.0199 36.7423 15.0681ZM41.7774 4.375H41.7941L41.794 25.5542C41.794 25.6807 41.7882 25.8059 41.7774 25.9296C41.7679 26.0385 41.7543 26.1462 41.7368 26.2526C41.725 26.324 41.7115 26.3948 41.6962 26.465C41.6838 26.5221 41.6703 26.5789 41.6556 26.6351C41.6429 26.6836 41.6293 26.7318 41.615 26.7796C41.602 26.8228 41.5886 26.8658 41.5744 26.9085C41.5614 26.9472 41.5478 26.9855 41.5338 27.0237C41.5207 27.0593 41.5071 27.0946 41.4932 27.1297C41.48 27.1627 41.4665 27.1954 41.4526 27.228C41.4394 27.2588 41.4258 27.2895 41.4119 27.3199C41.3987 27.3489 41.3852 27.3778 41.3714 27.4064C41.3581 27.4339 41.3446 27.4612 41.3307 27.4883C41.3175 27.5143 41.3039 27.5402 41.2901 27.5659C41.2769 27.5906 41.2632 27.615 41.2495 27.6394C41.2362 27.663 41.2227 27.6865 41.2089 27.7098C41.1956 27.7325 41.182 27.755 41.1683 27.7775C41.1549 27.7993 41.1414 27.8211 41.1277 27.8427C41.1144 27.8635 41.1007 27.8842 41.0871 27.9049C41.0737 27.9249 41.0601 27.9448 41.0465 27.9647C41.0331 27.9841 41.0195 28.0036 41.0058 28.0228C40.9924 28.0416 40.979 28.0603 40.9653 28.0789C40.9519 28.097 40.9383 28.1148 40.9246 28.1327C40.9112 28.1503 40.8977 28.1678 40.884 28.1852C40.8706 28.2022 40.8571 28.2191 40.8434 28.2359C40.83 28.2523 40.8164 28.2686 40.8028 28.2849C40.7893 28.3009 40.7759 28.3171 40.7622 28.3329C40.7488 28.3484 40.7352 28.3637 40.7216 28.379C40.7082 28.3941 40.6946 28.4091 40.681 28.424C40.6675 28.4387 40.6541 28.4534 40.6404 28.4679C40.627 28.4821 40.6134 28.4961 40.5998 28.5102C40.5863 28.5241 40.5729 28.5382 40.5592 28.5519C40.5458 28.5654 40.5321 28.5786 40.5186 28.5919C40.5051 28.605 40.4916 28.6182 40.4779 28.6313C40.4645 28.644 40.451 28.6567 40.4374 28.6693C40.4239 28.6818 40.4103 28.6942 40.3967 28.7065C40.3833 28.7187 40.3698 28.7308 40.3561 28.7428C40.3427 28.7546 40.3291 28.7664 40.3155 28.7781C40.302 28.7896 40.2886 28.8012 40.2749 28.8126C40.2615 28.8239 40.2479 28.8349 40.2343 28.846C40.2208 28.857 40.2073 28.8681 40.1937 28.8789C40.1803 28.8896 40.1667 28.9001 40.1531 28.9106C40.1396 28.9211 40.1261 28.9316 40.1125 28.942C40.0991 28.9521 40.0854 28.9621 40.0719 28.9721C40.0583 28.9821 40.0449 28.9921 40.0313 29.002C40.0178 29.0117 40.0042 29.0211 39.9907 29.0306C39.9771 29.0401 39.9637 29.0496 39.95 29.059C39.9366 29.0682 39.923 29.0772 39.9095 29.0862C39.896 29.0953 39.8825 29.1043 39.8688 29.1132C39.8554 29.122 39.8418 29.1306 39.8282 29.1392C39.8147 29.1477 39.8012 29.1563 39.7876 29.1647C39.7742 29.1731 39.7606 29.1813 39.747 29.1895L39.7064 29.2137C39.6929 29.2216 39.6794 29.2295 39.6658 29.2373C39.6524 29.2451 39.6388 29.2526 39.6252 29.2602C39.6117 29.2678 39.5982 29.2754 39.5846 29.2828C39.5711 29.2901 39.5575 29.2972 39.544 29.3044C39.5304 29.3115 39.517 29.3189 39.5034 29.326C39.4899 29.3329 39.4763 29.3395 39.4628 29.3463C39.4492 29.3531 39.4358 29.36 39.4221 29.3666C39.4087 29.3732 39.3951 29.3796 39.3816 29.3861L39.3409 29.4052L39.3003 29.4237C39.2868 29.4298 39.2733 29.4357 39.2597 29.4416C39.2462 29.4475 39.2327 29.4536 39.2191 29.4593C39.2057 29.4651 39.1921 29.4704 39.1785 29.476C39.165 29.4816 39.1515 29.4873 39.1379 29.4927C39.1244 29.4981 39.1109 29.5032 39.0973 29.5085C39.0838 29.5137 39.0703 29.5189 39.0567 29.524L39.0161 29.5391C39.0026 29.544 38.989 29.5487 38.9755 29.5535C38.9619 29.5583 38.9485 29.5632 38.9349 29.5679C38.9214 29.5724 38.9078 29.5767 38.8942 29.5811L38.8537 29.5944C38.8402 29.5987 38.8266 29.6028 38.813 29.607L38.7724 29.6192C38.7589 29.6232 38.7454 29.6273 38.7318 29.6311C38.7184 29.635 38.7048 29.6385 38.6912 29.6422C38.6777 29.6459 38.6642 29.6497 38.6506 29.6533L38.61 29.6636L38.5694 29.6735L38.5288 29.6833C38.5153 29.6864 38.5017 29.6892 38.4882 29.6922C38.4746 29.6952 38.4611 29.6983 38.4476 29.7011L38.407 29.7093L38.3663 29.7171C38.3528 29.7196 38.3393 29.7224 38.3257 29.7248C38.3123 29.7272 38.2987 29.7293 38.2851 29.7316L38.2445 29.7383L38.2039 29.7445L38.1633 29.7502C38.1498 29.7521 38.1363 29.7542 38.1227 29.7559C38.1092 29.7577 38.0956 29.759 38.0821 29.7606C38.0686 29.7622 38.0551 29.7638 38.0415 29.7653C38.028 29.7667 38.0145 29.7682 38.0009 29.7695C37.9874 29.7709 37.9738 29.772 37.9603 29.7732C37.9467 29.7744 37.9332 29.7757 37.9197 29.7768C37.9062 29.7778 37.8926 29.7787 37.8791 29.7796L37.8384 29.7822C37.8249 29.783 37.8114 29.784 37.7978 29.7847C37.7844 29.7854 37.7708 29.7857 37.7572 29.7863L37.7166 29.7878L37.676 29.7889C37.6625 29.7892 37.649 29.7892 37.6354 29.7894L37.5948 29.7899L37.5864 29.79H37.5542H37.5136H37.473H37.4324H37.3917H37.3512H37.3105H37.2699H37.2293H37.1887H37.1481H37.1075H37.0669H37.0263H36.9857H36.9451H36.9045H36.8638H36.8233H36.7826H36.742H36.7014H36.6608H36.6202H36.5796H36.539H36.4984H36.4578H36.4172H36.3766H36.3359H36.2954H36.2547H36.2141H36.1735H36.1329H36.0923H36.0517H36.0111H35.9705H35.9299H35.8893H35.8487H35.808H35.7675H35.7268H35.6862H35.6456H35.605H35.5644H35.5238H35.4832H35.4426H35.402H35.3614H35.3208H35.2801H35.2396H35.1989H35.1583H35.1177H35.0771H35.0365H34.9959H34.9553H34.9147H34.8741H34.8335H34.7929H34.7522H34.7117H34.671H34.6304H34.5898H34.5492H34.5086H34.468H34.4274H34.3868H34.3462H34.3056H34.265H34.2243H34.1838H34.1431H34.1025H34.0619H34.0213H33.9807H33.9401H33.8995H33.8589H33.8183H33.7777H33.7371H33.6964H33.6559H33.6152H33.5746H33.534H33.4934H33.4528H33.4122H33.3716H33.331H33.2904H33.2498H33.2092H33.1685H33.128H33.0873H33.0467H33.0061H32.9655H32.9249H32.8843H32.8437H32.8031H32.7625H32.7219H32.6813H32.6406H32.6H32.5594H32.5188H32.4782H32.4376H32.397H32.3564H32.3158H32.2752H32.2346H32.194H32.1534H32.1127H32.0721H32.0315H31.9909H31.9503H31.9097H31.8691H31.8285H31.7879H31.7473H31.7067H31.666H31.6255H31.5848H31.5442H31.5036H31.416V21.1545H31.5036H31.5442H31.5848H31.6255H31.666H31.7067H31.7473H31.7879H31.8285H31.8691H31.9097H31.9503H31.9909H32.0315H32.0721H32.1127H32.1534H32.194H32.2346H32.2752H32.3158H32.3564H32.397H32.4376H32.4782H32.5188H32.5594H32.6H32.6406H32.6813H32.7219H32.7625H32.8031H32.8437H32.8843H32.9249H32.9655H33.0061H33.0467H33.0873H33.128H33.1685H33.2092H33.2498H33.2904H33.331H33.3716H33.4122H33.4528H33.4934H33.534H33.5746H33.6152H33.6559H33.6964H33.7371H33.7777H33.8183H33.8589H33.8995H33.9401H33.9807H34.0213H34.0619H34.1025H34.1431H34.1838H34.2243H34.265H34.3056H34.3462H34.3868H34.4274H34.468H34.5086H34.5492H34.5898H34.6304H34.671H34.7117H34.7522H34.7929H34.8335H34.8741H34.9147H34.9553H34.9959H35.0365H35.0771H35.1177H35.1583H35.1989H35.2396H35.2801H35.3208H35.3614H35.402H35.4426H35.4832H35.5238H35.5644H35.605H35.6456H35.6862H35.7268H35.7675H35.808H35.8487H35.8893H35.9299H35.9705H36.0111H36.0517H36.0923H36.1329H36.1735H36.2141H36.2547H36.2954H36.3359H36.3766H36.4172H36.4578H36.4984H36.539H36.5796H36.6202H36.6608H36.7014H36.742H36.7826H36.8233H36.8638H36.9045H36.9451H36.9857H37.0263H37.0669H37.1075H37.1481H37.1887H37.2293H37.2699H37.3105H37.3512H37.3917H37.4324H37.473H37.5136H37.5542H37.5948H37.6354H37.676H37.7166H37.7572H37.7978H37.8384H37.8791H37.9197H37.9412L37.9603 21.1543L38.0009 21.154L38.0415 21.1532C38.0551 21.1528 38.0686 21.1526 38.0821 21.1521C38.0957 21.1517 38.1092 21.151 38.1227 21.1504L38.1633 21.1485L38.2039 21.146L38.2445 21.1432L38.2851 21.14L38.3257 21.1363C38.3393 21.135 38.3529 21.1337 38.3663 21.1323C38.3799 21.1309 38.3934 21.1293 38.407 21.1277L38.4476 21.1227C38.4611 21.1209 38.4747 21.1192 38.4882 21.1173C38.5018 21.1154 38.5153 21.1133 38.5288 21.1113C38.5424 21.1092 38.5559 21.1072 38.5694 21.105L38.61 21.0981L38.6506 21.0908C38.6642 21.0882 38.6778 21.0858 38.6912 21.0831C38.7048 21.0804 38.7183 21.0775 38.7318 21.0746L38.7724 21.0658C38.786 21.0627 38.7996 21.0597 38.813 21.0565C38.8267 21.0533 38.8402 21.0498 38.8537 21.0465C38.8672 21.0431 38.8808 21.0396 38.8942 21.0361C38.9078 21.0325 38.9214 21.0288 38.9349 21.0251C38.9485 21.0213 38.962 21.0174 38.9755 21.0134C38.9891 21.0095 39.0026 21.0054 39.0161 21.0013L39.0567 20.9885C39.0703 20.9841 39.0838 20.9796 39.0973 20.975C39.1109 20.9704 39.1245 20.9658 39.1379 20.961C39.1515 20.9562 39.1651 20.9514 39.1785 20.9464C39.1922 20.9413 39.2056 20.9361 39.2191 20.9308C39.2327 20.9256 39.2463 20.9202 39.2597 20.9147C39.2733 20.9092 39.2869 20.9037 39.3003 20.898C39.314 20.8923 39.3275 20.8864 39.3409 20.8804C39.3546 20.8744 39.3681 20.8682 39.3816 20.862C39.3951 20.8557 39.4087 20.8493 39.4221 20.8429C39.4357 20.8363 39.4493 20.8297 39.4628 20.823C39.4764 20.8161 39.4899 20.8091 39.5034 20.802C39.517 20.7949 39.5305 20.7875 39.544 20.7801C39.5576 20.7727 39.5712 20.7651 39.5846 20.7574C39.5982 20.7496 39.6118 20.7417 39.6252 20.7337C39.6388 20.7256 39.6524 20.7174 39.6658 20.709C39.6795 20.7004 39.693 20.6917 39.7064 20.683C39.7201 20.674 39.7336 20.6649 39.747 20.6558C39.7607 20.6464 39.7742 20.6369 39.7876 20.6274C39.8013 20.6176 39.8148 20.6077 39.8282 20.5976C39.8419 20.5874 39.8555 20.577 39.8688 20.5665C39.8825 20.5558 39.8961 20.5449 39.9095 20.5339C39.9232 20.5226 39.9367 20.5112 39.95 20.4996C39.9638 20.4878 39.9773 20.4757 39.9907 20.4636C40.0044 20.451 40.0179 20.4383 40.0313 20.4255C40.045 20.4123 40.0586 20.3989 40.0719 20.3854C40.0857 20.3714 40.0992 20.3572 40.1125 20.3428C40.1263 20.328 40.1398 20.3129 40.1531 20.2977C40.1669 20.2819 40.1805 20.2658 40.1937 20.2496C40.2075 20.2327 40.2211 20.2156 40.2343 20.1983C40.2481 20.1801 40.2618 20.1618 40.2749 20.1432C40.2889 20.1235 40.3023 20.1034 40.3155 20.0831C40.3295 20.0616 40.343 20.0399 40.3561 20.0179C40.3702 19.9942 40.3837 19.9701 40.3967 19.9458C40.4109 19.9193 40.4245 19.8926 40.4374 19.8654C40.4517 19.835 40.4652 19.8041 40.4779 19.7728C40.4926 19.7368 40.5061 19.7004 40.5186 19.6634C40.5338 19.6177 40.5473 19.5711 40.5592 19.5237C40.5765 19.4544 40.5901 19.3833 40.5998 19.3104C40.6116 19.222 40.6178 19.1312 40.6178 19.0378C40.6178 18.9469 40.6116 18.8586 40.5998 18.7728C40.5901 18.7024 40.5765 18.6337 40.5592 18.5668C40.5473 18.5213 40.5338 18.4766 40.5186 18.4327C40.5061 18.3968 40.4926 18.3613 40.4779 18.3266C40.4653 18.2964 40.4517 18.2667 40.4374 18.2375C40.4245 18.2112 40.4109 18.1853 40.3967 18.1598C40.3837 18.1363 40.3702 18.113 40.3561 18.0901C40.3431 18.0689 40.3295 18.0479 40.3155 18.0273C40.3024 18.0078 40.2889 17.9884 40.2749 17.9694C40.2618 17.9514 40.2482 17.9337 40.2343 17.9163C40.2211 17.8995 40.2075 17.883 40.1937 17.8667C40.1804 17.851 40.1669 17.8355 40.1531 17.8202C40.1398 17.8055 40.1263 17.791 40.1125 17.7767C40.0992 17.7628 40.0856 17.7492 40.0719 17.7357C40.0586 17.7226 40.045 17.7097 40.0313 17.697C40.0179 17.6846 40.0044 17.6724 39.9907 17.6604C39.9773 17.6486 39.9638 17.637 39.95 17.6255C39.9367 17.6143 39.9232 17.6033 39.9095 17.5924C39.8961 17.5817 39.8825 17.5712 39.8688 17.5608C39.8554 17.5507 39.8419 17.5407 39.8282 17.5307C39.8148 17.5211 39.8013 17.5115 39.7876 17.502C39.7742 17.4928 39.7607 17.4836 39.747 17.4745C39.7336 17.4657 39.7201 17.4569 39.7064 17.4482C39.693 17.4398 39.6795 17.4314 39.6658 17.4231C39.6524 17.415 39.6389 17.407 39.6252 17.3991C39.6118 17.3913 39.5982 17.3837 39.5846 17.3761C39.5712 17.3687 39.5576 17.3614 39.544 17.3541C39.5305 17.347 39.517 17.3399 39.5034 17.333C39.4899 17.3261 39.4764 17.3193 39.4628 17.3126C39.4493 17.3061 39.4358 17.2995 39.4221 17.2931C39.4087 17.2868 39.3951 17.2807 39.3816 17.2746C39.3681 17.2685 39.3546 17.2626 39.3409 17.2567C39.3275 17.251 39.3139 17.2453 39.3003 17.2396C39.2868 17.2341 39.2734 17.2285 39.2597 17.2231C39.2463 17.2178 39.2327 17.2126 39.2191 17.2074C39.2057 17.2023 39.1921 17.1974 39.1785 17.1925C39.165 17.1876 39.1515 17.1828 39.1379 17.1781C39.1244 17.1734 39.1109 17.1688 39.0973 17.1642C39.0838 17.1598 39.0703 17.1554 39.0567 17.1511C39.0432 17.1468 39.0296 17.1427 39.0161 17.1386L38.9755 17.1266C38.962 17.1227 38.9485 17.1188 38.9349 17.1151C38.9214 17.1114 38.9078 17.1078 38.8942 17.1043C38.8808 17.1008 38.8672 17.0974 38.8537 17.094C38.8402 17.0906 38.8266 17.0873 38.813 17.0841C38.7996 17.0809 38.786 17.0779 38.7724 17.0748C38.759 17.0718 38.7454 17.069 38.7318 17.0661C38.7183 17.0633 38.7048 17.0604 38.6912 17.0577L38.6506 17.0499L38.61 17.0426C38.5965 17.0403 38.583 17.0379 38.5694 17.0357L38.5288 17.0292C38.5153 17.0272 38.5017 17.0252 38.4882 17.0233L38.4476 17.0178L38.407 17.0126L38.3663 17.008L38.3257 17.0037L38.2851 16.9999L38.2445 16.9965L38.207 16.9936L38.207 16.9446L38.2445 16.9387L38.2851 16.932C38.2987 16.9296 38.3123 16.9273 38.3257 16.9248L38.3663 16.9169C38.38 16.9142 38.3935 16.9114 38.407 16.9085L38.4476 16.8996C38.4611 16.8965 38.4747 16.8934 38.4882 16.8902C38.5018 16.8869 38.5153 16.8834 38.5288 16.8799C38.5424 16.8764 38.5559 16.8728 38.5694 16.8691C38.583 16.8654 38.5965 16.8616 38.61 16.8577C38.6236 16.8538 38.6372 16.8498 38.6506 16.8457C38.6642 16.8416 38.6778 16.8373 38.6912 16.833C38.7049 16.8286 38.7184 16.824 38.7318 16.8195C38.7455 16.8148 38.759 16.81 38.7724 16.8052C38.7861 16.8003 38.7996 16.7954 38.813 16.7903C38.8267 16.7852 38.8402 16.7799 38.8537 16.7746C38.8673 16.7692 38.8808 16.7637 38.8942 16.7581C38.9079 16.7524 38.9214 16.7467 38.9349 16.7408C38.9485 16.7348 38.962 16.7287 38.9755 16.7225C38.9891 16.7162 39.0026 16.7098 39.0161 16.7033C39.0297 16.6967 39.0433 16.6899 39.0567 16.6831C39.0703 16.6761 39.0839 16.6691 39.0973 16.662C39.111 16.6547 39.1245 16.6472 39.1379 16.6398C39.1516 16.6321 39.1651 16.6243 39.1785 16.6165C39.1922 16.6084 39.2057 16.6003 39.2191 16.592C39.2328 16.5836 39.2463 16.575 39.2597 16.5664C39.2734 16.5575 39.2869 16.5485 39.3003 16.5394C39.314 16.53 39.3276 16.5206 39.3409 16.511C39.3547 16.5012 39.3682 16.4912 39.3816 16.4812C39.3953 16.4708 39.4088 16.4604 39.4221 16.4498C39.4359 16.4389 39.4494 16.4279 39.4628 16.4167C39.4765 16.4052 39.49 16.3935 39.5034 16.3817C39.5172 16.3695 39.5307 16.3571 39.544 16.3446C39.5577 16.3317 39.5713 16.3186 39.5846 16.3054C39.5984 16.2917 39.6119 16.2779 39.6252 16.2639C39.639 16.2493 39.6526 16.2345 39.6658 16.2195C39.6796 16.2039 39.6932 16.1881 39.7064 16.1721C39.7203 16.1554 39.7338 16.1385 39.747 16.1214C39.7609 16.1033 39.7745 16.085 39.7876 16.0665C39.8016 16.0469 39.8151 16.027 39.8282 16.007C39.8422 15.9856 39.8558 15.9639 39.8688 15.942C39.8829 15.9183 39.8965 15.8942 39.9095 15.8699C39.9237 15.8432 39.9372 15.8162 39.95 15.7888C39.9645 15.7581 39.978 15.7268 39.9907 15.6953C40.0054 15.6586 40.0189 15.6214 40.0313 15.5836C40.0467 15.5362 40.0603 15.488 40.0719 15.4393C40.0906 15.3601 40.1042 15.2793 40.1125 15.1971C40.1191 15.1322 40.1225 15.0665 40.1225 15.0001C40.1225 14.93 40.1191 14.8616 40.1125 14.795C40.1041 14.7098 40.0905 14.6275 40.0719 14.5481C40.0603 14.4985 40.0467 14.45 40.0313 14.4027C40.0189 14.3648 40.0054 14.3277 39.9907 14.2913C39.978 14.26 39.9644 14.2292 39.95 14.199C39.9372 14.172 39.9236 14.1455 39.9095 14.1194C39.8965 14.0955 39.8829 14.072 39.8688 14.0488C39.8557 14.0273 39.8422 14.0061 39.8282 13.9852C39.8151 13.9656 39.8015 13.9464 39.7876 13.9274C39.7745 13.9095 39.7609 13.8917 39.747 13.8743C39.7338 13.8576 39.7203 13.8411 39.7064 13.8249C39.6932 13.8095 39.6796 13.7943 39.6658 13.7793C39.6526 13.7649 39.639 13.7506 39.6252 13.7366C39.6119 13.723 39.5984 13.7097 39.5846 13.6965C39.5713 13.6837 39.5577 13.6711 39.544 13.6587C39.5307 13.6466 39.5171 13.6347 39.5034 13.623C39.49 13.6116 39.4765 13.6004 39.4628 13.5893C39.4494 13.5785 39.4359 13.5678 39.4221 13.5574C39.4088 13.5471 39.3952 13.5371 39.3816 13.5272C39.3682 13.5174 39.3546 13.5079 39.3409 13.4984C39.3275 13.4892 39.314 13.4801 39.3003 13.4711C39.2869 13.4623 39.2734 13.4537 39.2597 13.4451C39.2463 13.4367 39.2328 13.4285 39.2191 13.4204C39.2057 13.4124 39.1922 13.4045 39.1785 13.3968C39.1651 13.3891 39.1516 13.3816 39.1379 13.3742C39.1245 13.3669 39.111 13.3598 39.0973 13.3527C39.0839 13.3458 39.0703 13.339 39.0567 13.3322C39.0432 13.3256 39.0297 13.319 39.0161 13.3126C39.0026 13.3062 38.9891 13.2999 38.9755 13.2938C38.962 13.2878 38.9485 13.282 38.9349 13.2761C38.9214 13.2704 38.9078 13.2647 38.8942 13.2591C38.8808 13.2536 38.8672 13.2482 38.8537 13.2429C38.8402 13.2376 38.8267 13.2324 38.813 13.2273C38.7996 13.2223 38.7861 13.2174 38.7724 13.2126C38.759 13.2078 38.7454 13.2031 38.7318 13.1985C38.7184 13.194 38.7048 13.1895 38.6912 13.1852C38.6778 13.1808 38.6642 13.1766 38.6506 13.1724C38.6371 13.1683 38.6236 13.1641 38.61 13.1602C38.5965 13.1563 38.583 13.1525 38.5694 13.1488C38.5559 13.1451 38.5424 13.1415 38.5288 13.1379L38.4882 13.1275C38.4747 13.1242 38.4612 13.1209 38.4476 13.1177C38.4341 13.1145 38.4206 13.1115 38.407 13.1085L38.3663 13.0998C38.3528 13.097 38.3394 13.0941 38.3257 13.0915C38.3123 13.0888 38.2987 13.0864 38.2851 13.0839L38.2445 13.0767C38.231 13.0744 38.2175 13.0721 38.2039 13.0699L38.1633 13.0637L38.1227 13.0579L38.0821 13.0525L38.0415 13.0477L38.0009 13.0433L37.9603 13.0392L37.9197 13.0356L37.8791 13.0324L37.8384 13.0295L37.7978 13.0272L37.7817 13.0263L37.7572 13.0247L37.7166 13.0223L37.676 13.0201L37.6354 13.0179L37.5948 13.016L37.5542 13.0142L37.5136 13.0127L37.473 13.0114L37.4324 13.0105L37.3917 13.01L37.3694 13.0099H37.3512H37.3105H37.2699H37.2293H37.1887H37.1481H37.1075H37.0669H37.0263H36.9857H36.9451H36.9045H36.8638H36.8233H36.7826H36.742H36.7014H36.6608H36.6202H36.5796H36.539H36.4984H36.4578H36.4172H36.3766H36.3359H36.2954H36.2547H36.2141H36.1735H36.1329H36.0923H36.0517H36.0111H35.9705H35.9299H35.8893H35.8487H35.808H35.7675H35.7268H35.6862H35.6456H35.605H35.5644H35.5238H35.4832H35.4426H35.402H35.3614H35.3208H35.2801H35.2396H35.1989H35.1583H35.1177H35.0771H35.0365H34.9959H34.9553H34.9147H34.8741H34.8335H34.7929H34.7522H34.7117H34.671H34.6304H34.5898H34.5492H34.5086H34.468H34.4274H34.3868H34.3462H34.3056H34.265H34.2243H34.1838H34.1431H34.1025H34.0619H34.0213H33.9807H33.9401H33.8995H33.8589H33.8183H33.7777H33.7371H33.6964H33.6559H33.6152H33.5746H33.534H33.4934H33.4528H33.4122H33.3716H33.331H33.2904H33.2498H33.2092H33.1685H33.128H33.0873H33.0467H33.0061H32.9655H32.9249H32.8843H32.8437H32.8031H32.7625H32.7219H32.6813H32.6406H32.6H32.5594H32.5188H32.4782H32.4376H32.397H32.3564H32.3158H32.2752H32.2346H32.194H32.1534H32.1127H32.0721H32.0315H31.9909H31.9503H31.9097H31.8691H31.8285H31.7879H31.7473H31.7067H31.666H31.6255H31.5848H31.5442H31.5036H31.416V8.61119C31.416 8.31526 31.4463 8.02649 31.5036 7.74768C31.5159 7.68797 31.5295 7.62877 31.5442 7.57005C31.5569 7.5198 31.5705 7.46992 31.5848 7.42041C31.5977 7.37619 31.6112 7.33227 31.6255 7.28866C31.6385 7.24881 31.6519 7.20917 31.666 7.16986C31.6791 7.1334 31.6926 7.09714 31.7067 7.06115C31.7198 7.02754 31.7333 6.99409 31.7473 6.96092C31.7605 6.92961 31.774 6.89843 31.7879 6.8675C31.8011 6.83812 31.8147 6.80894 31.8285 6.77994C31.8417 6.75225 31.8553 6.72477 31.8691 6.69742C31.8824 6.67113 31.896 6.64504 31.9097 6.61905C31.923 6.594 31.9365 6.56913 31.9503 6.5444C31.9636 6.5204 31.9772 6.49662 31.9909 6.47293C32.0043 6.44996 32.0178 6.42708 32.0315 6.40441C32.0449 6.38242 32.0584 6.3606 32.0721 6.33888C32.0854 6.31784 32.0991 6.297 32.1127 6.27622C32.1261 6.25589 32.1396 6.23563 32.1534 6.21554C32.1668 6.19585 32.1802 6.17616 32.194 6.15671C32.2073 6.13787 32.2209 6.11927 32.2346 6.10066C32.2479 6.08236 32.2615 6.0642 32.2752 6.04614C32.2887 6.02835 32.3021 6.01049 32.3158 5.99294C32.3291 5.97586 32.3428 5.95905 32.3564 5.94218C32.3698 5.92554 32.3833 5.90904 32.397 5.89264C32.4105 5.87644 32.4239 5.86024 32.4376 5.84425C32.451 5.82866 32.4646 5.81335 32.4782 5.79796C32.4917 5.78271 32.5051 5.76743 32.5188 5.75239C32.5322 5.73765 32.5458 5.72304 32.5594 5.70847C32.5729 5.6941 32.5864 5.67984 32.6 5.66564C32.6135 5.65164 32.627 5.63765 32.6406 5.62382C32.6541 5.61024 32.6677 5.59685 32.6813 5.58347C32.6948 5.57015 32.7082 5.5567 32.7219 5.54358C32.7352 5.53074 32.7489 5.5182 32.7625 5.50553C32.776 5.49292 32.7894 5.48028 32.8031 5.46785C32.8165 5.45561 32.8301 5.44362 32.8437 5.43159C32.8572 5.41963 32.8707 5.40767 32.8843 5.39587C32.8978 5.38425 32.9113 5.3728 32.9249 5.36134C32.9384 5.34999 32.9519 5.33868 32.9655 5.32749C32.979 5.31641 32.9925 5.30547 33.0061 5.29459L33.0467 5.26243L33.0873 5.23112C33.1008 5.22085 33.1144 5.21066 33.128 5.20056C33.1414 5.19056 33.1549 5.1806 33.1685 5.17074L33.2092 5.1417C33.2226 5.13218 33.2362 5.12272 33.2498 5.11337L33.2904 5.08568L33.331 5.05875C33.3445 5.0499 33.358 5.04109 33.3716 5.03242L33.4122 5.00687C33.4257 4.99843 33.4392 4.98999 33.4528 4.98172C33.4663 4.97352 33.4799 4.96556 33.4934 4.95753C33.507 4.9495 33.5204 4.9414 33.534 4.93354C33.5474 4.92578 33.5611 4.91829 33.5746 4.91067C33.5882 4.90304 33.6016 4.89528 33.6152 4.88783C33.6287 4.88047 33.6423 4.87336 33.6559 4.86614C33.6694 4.85892 33.6828 4.85167 33.6964 4.84459C33.7099 4.83757 33.7235 4.83076 33.7371 4.82392C33.7506 4.81711 33.7641 4.81026 33.7777 4.80359L33.8183 4.78383C33.8318 4.77736 33.8453 4.77106 33.8589 4.76472C33.8724 4.75842 33.8859 4.75201 33.8995 4.74585C33.913 4.73975 33.9266 4.73392 33.9401 4.72795C33.9536 4.72199 33.9671 4.71592 33.9807 4.7101C33.9942 4.70434 34.0078 4.69885 34.0213 4.69322C34.0348 4.68763 34.0483 4.68197 34.0619 4.67651L34.1025 4.66042L34.1431 4.64487C34.1567 4.63972 34.1702 4.6345 34.1838 4.62952C34.1972 4.62457 34.2108 4.61993 34.2243 4.61511C34.2379 4.6103 34.2514 4.60539 34.265 4.60071C34.2784 4.59607 34.292 4.5917 34.3056 4.58719L34.3462 4.57394C34.3597 4.56961 34.3732 4.56523 34.3868 4.56103C34.4003 4.55686 34.4139 4.55293 34.4274 4.5489C34.4409 4.54484 34.4544 4.54063 34.468 4.53674C34.4815 4.53287 34.4951 4.52928 34.5086 4.52555C34.5221 4.52183 34.5356 4.51807 34.5492 4.51447L34.5898 4.5039C34.6033 4.50048 34.6169 4.49723 34.6304 4.49394C34.644 4.49065 34.6574 4.48713 34.671 4.48398C34.6845 4.48086 34.6981 4.47801 34.7117 4.475L34.7522 4.46608L34.7929 4.45765C34.8063 4.45494 34.8199 4.45239 34.8335 4.44982C34.847 4.44721 34.8605 4.44443 34.8741 4.44196C34.8876 4.43948 34.9012 4.43738 34.9147 4.43508L34.9553 4.4283C34.9688 4.42613 34.9824 4.4239 34.9959 4.42186C35.0094 4.41983 35.023 4.41803 35.0365 4.41614C35.0501 4.41424 35.0635 4.41217 35.0771 4.41041C35.0906 4.40865 35.1042 4.40709 35.1177 4.40546L35.1583 4.40079C35.1719 4.3993 35.1854 4.39764 35.1989 4.39628C35.2124 4.39492 35.226 4.39387 35.2396 4.39265L35.2801 4.38903L35.3208 4.38595C35.3343 4.38496 35.3478 4.38418 35.3614 4.38334C35.3749 4.38252 35.3884 4.38147 35.402 4.38076C35.4155 4.38005 35.4291 4.37964 35.4426 4.37907L35.4832 4.37754C35.4968 4.3771 35.5103 4.37652 35.5238 4.37622C35.5373 4.37591 35.5509 4.37588 35.5644 4.37568C35.5779 4.37547 35.5915 4.37524 35.605 4.37517L35.6152 4.37503H35.6456H35.6862H35.7268H35.7675H35.808H35.8487H35.8893H35.9299H35.9705H36.0111H36.0517H36.0923H36.1329H36.1735H36.2141H36.2547H36.2954H36.3359H36.3766H36.4172H36.4578H36.4984H36.539H36.5796H36.6202H36.6608H36.7014H36.742H36.7826H36.8233H36.8638H36.9045H36.9451H36.9857H37.0263H37.0669H37.1075H37.1481H37.1887H37.2293H37.2699H37.3105H37.3512H37.3917H37.4324H37.473H37.5136H37.5542H37.5948H37.6354H37.676H37.7166H37.7572H37.7978H37.8384H37.8791H37.9197H37.9603H38.0009H38.0415H38.0821H38.1227H38.1633H38.2039H38.2445H38.2851H38.3257H38.3663H38.407H38.4476H38.4882H38.5288H38.5694H38.61H38.6506H38.6912H38.7318H38.7724H38.813H38.8537H38.8942H38.9349H38.9755H39.0161H39.0567H39.0973H39.1379H39.1785H39.2191H39.2597H39.3003H39.3409H39.3816H39.4221H39.4628H39.5034H39.544H39.5846H39.6252H39.6658H39.7064H39.747H39.7876H39.8282H39.8688H39.9095H39.95H39.9907H40.0313H40.0719H40.1125H40.1531H40.1937H40.2343H40.2749H40.3155H40.3561H40.3967H40.4374H40.4779H40.5186H40.5592H40.5998H40.6404H40.681H40.7216H40.7622H40.8028H40.8434H40.884H40.9246H40.9653H41.0058H41.0465H41.0871H41.1277H41.1683H41.2089H41.2495H41.2901H41.3307H41.3714H41.4119H41.4526H41.4932H41.5338H41.5744H41.615H41.6556H41.6962H41.7368H41.7774V4.375Z",fill:"#54B230"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M8.1377 19.0723V8.61115C8.1377 8.31608 8.16772 8.02805 8.22476 7.74998C8.23702 7.69027 8.25061 7.63111 8.26532 7.57235C8.27791 7.52203 8.29147 7.47212 8.30584 7.42254C8.31867 7.37836 8.3322 7.33451 8.34637 7.29093C8.35936 7.25105 8.37279 7.21133 8.38692 7.17196C8.40001 7.13553 8.41341 7.09931 8.42744 7.06336C8.44054 7.02974 8.45407 6.99623 8.468 6.96302C8.48116 6.93171 8.49466 6.9006 8.50852 6.8697C8.52172 6.84032 8.53525 6.81118 8.54904 6.78217C8.56224 6.75445 8.57583 6.72694 8.5896 6.69956C8.60283 6.67326 8.61636 6.64717 8.63012 6.62121C8.64338 6.59617 8.65691 6.5713 8.67064 6.54656C8.68394 6.52261 8.69744 6.49879 8.7112 6.47513C8.72453 6.45216 8.73796 6.42925 8.75172 6.40655C8.76505 6.38459 8.77858 6.3628 8.79228 6.34112C8.80554 6.32011 8.81917 6.29923 8.8328 6.27846C8.84616 6.25813 8.85963 6.23787 8.87332 6.21777C8.88679 6.19805 8.90011 6.17836 8.91388 6.15888C8.92717 6.14007 8.94081 6.1215 8.9544 6.1029C8.9678 6.0846 8.98129 6.06644 8.99496 6.04838C9.00839 6.03062 9.02178 6.01273 9.03548 5.99518C9.04881 5.97813 9.06244 5.96136 9.076 5.94452C9.0894 5.92788 9.10289 5.91131 9.11656 5.89487C9.12999 5.87871 9.14342 5.86258 9.15708 5.84662C9.17044 5.831 9.18404 5.81565 9.19764 5.80023C9.21107 5.78495 9.2245 5.7697 9.23816 5.75466C9.25152 5.73992 9.26512 5.72538 9.27868 5.71084C9.29211 5.69644 9.30561 5.68217 9.31924 5.66798C9.33267 5.65398 9.34613 5.63999 9.35976 5.62616C9.37316 5.61257 9.38675 5.59919 9.40032 5.5858C9.41381 5.57249 9.42717 5.55907 9.44084 5.54595C9.4542 5.53311 9.46783 5.52057 9.48136 5.5079C9.49486 5.49526 9.50825 5.48259 9.52192 5.47015C9.53531 5.45795 9.54891 5.44599 9.56244 5.43396C9.57594 5.422 9.58936 5.41 9.603 5.39821C9.61642 5.38659 9.62995 5.37517 9.64352 5.36372C9.65695 5.35237 9.67044 5.34101 9.68404 5.32983C9.69747 5.31878 9.71103 5.30784 9.7246 5.29693C9.73806 5.28612 9.75152 5.27541 9.76512 5.26477C9.77855 5.25423 9.79211 5.24383 9.80567 5.23346C9.81914 5.22319 9.83263 5.21296 9.8462 5.20286L9.88672 5.17311C9.90018 5.16332 9.91368 5.15359 9.92728 5.14397C9.9407 5.13445 9.95423 5.12503 9.9678 5.11567C9.98126 5.10635 9.99476 5.0971 10.0083 5.08795C10.0218 5.07891 10.0353 5.07 10.0489 5.06108C10.0623 5.05224 10.0758 5.04333 10.0894 5.03462C10.1028 5.02601 10.1164 5.01761 10.13 5.00914C10.1435 5.0007 10.1569 4.99216 10.1705 4.98389C10.1839 4.97573 10.1975 4.9678 10.211 4.95977C10.2245 4.95173 10.2379 4.94357 10.2516 4.93567C10.265 4.92791 10.2786 4.92049 10.2921 4.91287C10.3056 4.90524 10.319 4.89752 10.3326 4.89003C10.3461 4.88264 10.3596 4.87553 10.3732 4.86831C10.3867 4.86109 10.4001 4.85384 10.4137 4.84676C10.4271 4.83974 10.4407 4.83286 10.4542 4.82598L10.4948 4.80572C10.5083 4.79905 10.5217 4.79237 10.5353 4.78586C10.5488 4.77939 10.5623 4.77312 10.5758 4.76679C10.5894 4.76045 10.6028 4.75398 10.6164 4.74781C10.6298 4.74168 10.6434 4.73592 10.6569 4.72995C10.6704 4.72399 10.6839 4.71792 10.6974 4.71209C10.7109 4.70633 10.7245 4.70078 10.738 4.69515L10.7785 4.67848L10.819 4.66228L10.8596 4.64676C10.8731 4.64161 10.8865 4.63629 10.9001 4.63128C10.9135 4.62633 10.9272 4.62172 10.9407 4.61691C10.9542 4.6121 10.9676 4.60722 10.9812 4.60254L11.0217 4.58889L11.0623 4.57564C11.0758 4.5713 11.0892 4.56686 11.1028 4.56266C11.1162 4.55849 11.1298 4.55453 11.1433 4.55049C11.1568 4.54643 11.1703 4.54229 11.1839 4.53836C11.1973 4.53447 11.2109 4.53084 11.2244 4.52708L11.265 4.516C11.2784 4.51241 11.2919 4.50875 11.3055 4.50529C11.3189 4.50183 11.3325 4.49868 11.346 4.49536C11.3595 4.49204 11.373 4.48858 11.3866 4.4854C11.4 4.48225 11.4136 4.4793 11.4271 4.47628L11.4676 4.46741C11.4811 4.46449 11.4946 4.46154 11.5082 4.4588C11.5216 4.45609 11.5352 4.45358 11.5487 4.45097C11.5622 4.44836 11.5757 4.44565 11.5892 4.44318C11.6027 4.4407 11.6162 4.43847 11.6298 4.43613L11.6703 4.42935C11.6838 4.42718 11.6973 4.42485 11.7108 4.42278C11.7243 4.42071 11.7379 4.41898 11.7514 4.41705C11.7648 4.41515 11.7783 4.41312 11.7919 4.41133L11.8324 4.40621L11.873 4.40157C11.8865 4.40004 11.8999 4.39828 11.9135 4.39689C11.927 4.3955 11.9405 4.39448 11.954 4.39323L11.9946 4.38964C12.0081 4.38849 12.0216 4.38737 12.0351 4.38639C12.0486 4.3854 12.0621 4.38462 12.0757 4.38378C12.0892 4.38293 12.1027 4.38195 12.1162 4.3812C12.1297 4.38046 12.1432 4.37995 12.1567 4.37934L12.1973 4.37778C12.2108 4.3773 12.2242 4.37659 12.2378 4.37625C12.2513 4.37591 12.2648 4.37595 12.2783 4.37575C12.2918 4.37554 12.3053 4.3753 12.3189 4.37524L12.3594 4.375H12.3999H12.4405H12.481H12.5215H12.5621H12.6026H12.6431H12.6837H12.7242H12.7647H12.8053H12.8458H12.8863H12.9269H12.9674H13.008H13.0485H13.089H13.1296H13.1701H13.2107H13.2512H13.2917H13.3323H13.3728H13.4133H13.4539H13.4944H13.5349H13.5755H13.616H13.6565H13.6971H13.7376H13.7781H13.8187H13.8592H13.8997H13.9403H13.9808H14.0213H14.0619H14.1024H14.143H14.1835H14.224H14.2646H14.3051H14.3457H14.3862H14.4267H14.4673H14.5078H14.5483H14.5889H14.6294H14.6699H14.7105H14.751H14.7915H14.8321H14.8726H14.9131H14.9537H14.9942H15.0347H15.0753H15.1158H15.1563H15.1969H15.2374H15.278H15.3185H15.359H15.3996H15.4401H15.4807H15.5212H15.5617H15.6023H15.6428H15.6833H15.7239H15.7644H15.8049H15.8455H15.886H15.9265H15.9671H16.0076H16.0481H16.0887H16.1292H16.1697H16.2103H16.2508H16.2914H16.3319H16.3724H16.413H16.4535H16.494H16.5346H16.5751H16.6157H16.6562H16.6967H16.7373H16.7778H16.8183H16.8589H16.8994H16.9399H16.9805H17.021H17.0615H17.1021H17.1426H17.1831H17.2237H17.2642H17.3047H17.3453H17.3858H17.4264H17.4669H17.5074H17.548H17.5885H17.629H17.6696H17.7101H17.7506H17.7912H17.8317H17.8722H17.9128H17.9533H17.9938H18.0344H18.0749H18.1154H18.156H18.1965H18.237H18.2776H18.3181H18.3586H18.3992H18.4397H18.4803H18.5147V25.5542C18.5147 25.7384 18.5029 25.9199 18.4803 26.098C18.4694 26.1837 18.4556 26.2685 18.4397 26.3525C18.4276 26.4165 18.4141 26.4799 18.3992 26.5427C18.3866 26.5957 18.3732 26.6483 18.3586 26.7005C18.3459 26.7463 18.3323 26.7917 18.3181 26.8368C18.3052 26.8777 18.2917 26.9182 18.2776 26.9586C18.2646 26.9959 18.2511 27.033 18.237 27.0698C18.224 27.1042 18.2105 27.1384 18.1965 27.1723C18.1834 27.2043 18.1699 27.2361 18.156 27.2676C18.1428 27.2976 18.1293 27.3273 18.1154 27.357C18.1023 27.3852 18.0887 27.4132 18.0749 27.4411C18.0617 27.4679 18.0482 27.4945 18.0344 27.5209C18.0211 27.5463 18.0076 27.5717 17.9938 27.5968C17.9805 27.6211 17.9671 27.6454 17.9533 27.6694C17.94 27.6926 17.9265 27.7156 17.9128 27.7385C17.8995 27.7607 17.8859 27.7826 17.8722 27.8045C17.8589 27.8259 17.8454 27.8471 17.8317 27.8682C17.8183 27.8889 17.8049 27.9095 17.7912 27.9298C17.7778 27.9496 17.7643 27.9692 17.7506 27.9887C17.7373 28.0078 17.7238 28.0267 17.7101 28.0456C17.6967 28.0641 17.6832 28.0826 17.6696 28.1009C17.6562 28.1188 17.6427 28.1365 17.629 28.1542C17.6157 28.1714 17.6021 28.1885 17.5885 28.2056C17.5751 28.2224 17.5616 28.2392 17.548 28.2558C17.5346 28.2721 17.521 28.2881 17.5074 28.3041C17.494 28.3199 17.4805 28.3356 17.4669 28.3512C17.4534 28.3665 17.44 28.382 17.4264 28.3972C17.413 28.412 17.3994 28.4266 17.3858 28.4413C17.3723 28.4558 17.359 28.4704 17.3453 28.4847C17.3319 28.4988 17.3183 28.5126 17.3047 28.5265C17.2913 28.5402 17.2778 28.5539 17.2642 28.5674C17.2508 28.5808 17.2373 28.5941 17.2237 28.6073C17.2103 28.6203 17.1967 28.6331 17.1831 28.6459C17.1697 28.6586 17.1562 28.6713 17.1426 28.6839C17.1292 28.6962 17.1156 28.7082 17.1021 28.7204C17.0886 28.7325 17.0752 28.7447 17.0615 28.7566C17.0482 28.7682 17.0345 28.7796 17.021 28.7911C17.0075 28.8026 16.9941 28.8142 16.9805 28.8255C16.9671 28.8366 16.9535 28.8475 16.9399 28.8584C16.9264 28.8693 16.913 28.8803 16.8994 28.8911C16.886 28.9016 16.8724 28.9119 16.8589 28.9223C16.8454 28.9327 16.832 28.9431 16.8183 28.9533C16.8049 28.9634 16.7913 28.9732 16.7778 28.9831C16.7643 28.993 16.7509 29.003 16.7373 29.0127C16.7239 29.0222 16.7103 29.0316 16.6967 29.041C16.6832 29.0504 16.6698 29.0599 16.6562 29.0692C16.6428 29.0782 16.6292 29.0871 16.6157 29.096C16.6021 29.105 16.5887 29.1141 16.5751 29.1229C16.5617 29.1315 16.5481 29.1398 16.5346 29.1484C16.5211 29.1569 16.5076 29.1655 16.494 29.1738C16.4806 29.1821 16.467 29.1901 16.4535 29.1981C16.44 29.2062 16.4266 29.2143 16.413 29.2223C16.3995 29.2301 16.386 29.2377 16.3724 29.2454C16.359 29.2531 16.3455 29.2608 16.3319 29.2682C16.3184 29.2757 16.3049 29.2831 16.2914 29.2904L16.2508 29.3119C16.2373 29.319 16.2239 29.3261 16.2103 29.333C16.1968 29.3399 16.1833 29.3466 16.1697 29.3533C16.1562 29.36 16.1428 29.367 16.1292 29.3735C16.1158 29.38 16.1022 29.3862 16.0887 29.3926C16.0752 29.399 16.0617 29.4054 16.0481 29.4117C16.0347 29.4178 16.0211 29.4237 16.0076 29.4298L15.9671 29.4476L15.9265 29.4649C15.9131 29.4706 15.8995 29.4761 15.886 29.4816C15.8725 29.4871 15.859 29.4928 15.8455 29.4982C15.832 29.5035 15.8185 29.5085 15.8049 29.5136C15.7914 29.5188 15.778 29.5241 15.7644 29.5292C15.7509 29.5342 15.7374 29.539 15.7239 29.5438L15.6833 29.5582L15.6428 29.5722C15.6293 29.5767 15.6158 29.5811 15.6023 29.5854C15.5887 29.5899 15.5753 29.5944 15.5617 29.5987C15.5483 29.6029 15.5347 29.6068 15.5212 29.6109L15.4807 29.6231L15.4401 29.6347C15.4266 29.6385 15.4131 29.6421 15.3996 29.6457C15.386 29.6494 15.3726 29.6533 15.359 29.6567C15.3456 29.6602 15.332 29.6633 15.3185 29.6667C15.305 29.6701 15.2915 29.6734 15.278 29.6767L15.2374 29.6861L15.1969 29.695C15.1834 29.6979 15.1699 29.7011 15.1563 29.7039C15.1429 29.7067 15.1293 29.709 15.1158 29.7117C15.1023 29.7143 15.0888 29.717 15.0753 29.7195L15.0347 29.7269C15.0213 29.7292 15.0077 29.7314 14.9942 29.7336C14.9807 29.7358 14.9672 29.7383 14.9537 29.7404C14.9402 29.7425 14.9267 29.7442 14.9131 29.7462L14.8726 29.7519C14.8591 29.7538 14.8456 29.7556 14.8321 29.7573C14.8186 29.759 14.805 29.7604 14.7915 29.762C14.778 29.7635 14.7645 29.7652 14.751 29.7667C14.7375 29.7681 14.724 29.7693 14.7105 29.7706L14.6699 29.7742C14.6564 29.7754 14.6429 29.7768 14.6294 29.7778C14.6159 29.7788 14.6024 29.7795 14.5889 29.7804L14.5483 29.783L14.5078 29.7851C14.4943 29.7858 14.4808 29.7862 14.4673 29.7867C14.4537 29.7872 14.4402 29.7878 14.4267 29.7883C14.4132 29.7886 14.3997 29.7888 14.3862 29.789L14.3457 29.7895C14.3331 29.7896 14.3207 29.79 14.3081 29.79H14.2646H14.224H14.1835H14.143H14.1024H14.0619H14.0213H13.9808H13.9403H13.8997H13.8592H13.8187H13.7781H13.7376H13.6971H13.6565H13.616H13.5755H13.5349H13.4944H13.4539H13.4133H13.3728H13.3323H13.2917H13.2512H13.2107H13.1701H13.1296H13.089H13.0485H13.008H12.9674H12.9269H12.8863H12.8458H12.8053H12.7647H12.7242H12.6837H12.6431H12.6026H12.5621H12.5215H12.481H12.4405H12.3999H12.3594H12.3189H12.2783H12.2378H12.1973H12.1567H12.1162H12.0757H12.0351H11.9946H11.954H11.9135H11.873H11.8324H11.7919H11.7514H11.7108H11.6703H11.6298H11.5892H11.5487H11.5082H11.4676H11.4271H11.3866H11.346H11.3055H11.265H11.2244H11.1839H11.1433H11.1028H11.0623H11.0217H10.9812H10.9407H10.9001H10.8596H10.819H10.7785H10.738H10.6974H10.6569H10.6164H10.5758H10.5353H10.4948H10.4542H10.4137H10.3732H10.3326H10.2921H10.2516H10.211H10.1705H10.13H10.0894H10.0489H10.0083H9.9678H9.92728H9.88672H9.8462H9.80567H9.76512H9.7246H9.68404H9.64352H9.603H9.56244H9.52192H9.48136H9.44084H9.40032H9.35976H9.31924H9.27868H9.23816H9.19764H9.15708H9.11656H9.076H9.03548H8.99496H8.9544H8.91388H8.87332H8.8328H8.79228H8.75172H8.7112H8.67064H8.63012H8.5896H8.54904H8.50852H8.468H8.42744H8.38692H8.34637H8.30584H8.26532H8.22476H8.1377V20.8632C8.1377 20.8632 8.16765 20.8715 8.22476 20.886L8.26532 20.8962L8.30584 20.9062L8.34637 20.916L8.38692 20.9256L8.42744 20.9351L8.468 20.9445L8.50852 20.9537L8.54904 20.9628L8.5896 20.9717L8.63012 20.9805L8.67064 20.9893L8.7112 20.9979L8.75172 21.0064L8.79228 21.0147L8.8328 21.023L8.87332 21.0311L8.91388 21.0392L8.9544 21.0471L8.99496 21.055L9.03548 21.0628L9.076 21.0704L9.11656 21.078L9.15708 21.0854L9.19764 21.0928L9.23816 21.1001L9.27868 21.1072L9.31924 21.1143L9.35976 21.1212L9.40032 21.1281L9.44084 21.1349L9.48136 21.1416L9.52192 21.1482L9.56244 21.1547L9.603 21.1612L9.64352 21.1675L9.68404 21.1738L9.7246 21.1799L9.76512 21.186L9.80567 21.1919L9.8462 21.1978L9.88672 21.2036L9.92728 21.2093L9.9678 21.2149L10.0083 21.2205L10.0489 21.2259L10.0894 21.2313L10.13 21.2366L10.1705 21.2417L10.211 21.2468L10.2516 21.2518L10.2921 21.2568L10.3326 21.2615L10.3732 21.2663L10.4137 21.2709L10.4542 21.2755L10.4948 21.28L10.5353 21.2844L10.5758 21.2887L10.6164 21.2929L10.6569 21.297L10.6974 21.301L10.738 21.305L10.7785 21.3089L10.819 21.3126L10.8596 21.3164L10.9001 21.3199L10.9407 21.3234L10.9812 21.3269L11.0217 21.3302L11.0623 21.3335L11.1028 21.3366L11.1433 21.3397L11.1839 21.3426L11.2244 21.3455L11.265 21.3483L11.3055 21.351L11.346 21.3536L11.3866 21.3562L11.4271 21.3586L11.4676 21.361L11.5082 21.3632L11.5487 21.3654L11.5892 21.3675L11.6298 21.3694L11.6703 21.3713L11.7108 21.3731L11.7514 21.3748L11.7919 21.3765L11.8324 21.3779L11.873 21.3793L11.9135 21.3807L11.954 21.3819L11.9946 21.3831L12.0351 21.3841L12.0757 21.385L12.1162 21.386L12.1567 21.3867L12.1973 21.3873L12.2378 21.388L12.2783 21.3884L12.3189 21.3888L12.3594 21.3891L12.3999 21.3892L12.4405 21.3893L12.481 21.3893L12.5215 21.389L12.5621 21.3888L12.6026 21.3885L12.6431 21.3883L12.6837 21.3875L12.7242 21.3868L12.7647 21.3861L12.8053 21.3853L12.8458 21.3843L12.8863 21.383L12.9269 21.3818L12.9674 21.3805L13.008 21.3791L13.0485 21.3773L13.089 21.3756L13.1296 21.3737L13.1701 21.3719L13.2107 21.3697L13.2512 21.3673L13.2917 21.3649L13.3323 21.3626L13.3728 21.3599L13.4133 21.3569L13.4539 21.354L13.4944 21.3511L13.5349 21.3478L13.5755 21.3443L13.616 21.3408L13.6565 21.3372L13.6971 21.3334L13.7376 21.3292L13.7781 21.3251L13.8187 21.3209L13.8592 21.3164L13.8997 21.3116L13.9403 21.3068L13.9808 21.302L14.0213 21.2967L14.0619 21.2912L14.1024 21.2858L14.143 21.2802C14.1566 21.2782 14.1699 21.2761 14.1835 21.2741L14.224 21.2679L14.2646 21.2618L14.3051 21.2553C14.3187 21.2531 14.3321 21.2507 14.3457 21.2484L14.3862 21.2416C14.3996 21.2393 14.4134 21.2371 14.4267 21.2347C14.4404 21.2323 14.4537 21.2296 14.4673 21.2271L14.5078 21.2195L14.5483 21.2119L14.5889 21.2039C14.6025 21.2012 14.6159 21.1983 14.6294 21.1955L14.6699 21.1871L14.7105 21.1785C14.7242 21.1756 14.7374 21.1724 14.751 21.1694L14.7915 21.1601C14.805 21.157 14.8187 21.1541 14.8321 21.151C14.8458 21.1477 14.8591 21.1443 14.8726 21.141L14.9131 21.1309C14.9266 21.1276 14.9403 21.1243 14.9537 21.1209C14.9673 21.1174 14.9807 21.1138 14.9942 21.1102L15.0347 21.0993C15.0482 21.0956 15.0619 21.0921 15.0753 21.0883C15.0889 21.0846 15.1023 21.0806 15.1158 21.0768L15.1563 21.065C15.1698 21.061 15.1835 21.0571 15.1969 21.0531L15.2374 21.0406L15.278 21.0278C15.2914 21.0235 15.3052 21.0193 15.3185 21.015L15.359 21.0014L15.3996 20.9876C15.413 20.9829 15.4268 20.9784 15.4401 20.9737C15.4538 20.9689 15.4671 20.9638 15.4807 20.9589L15.5212 20.9441C15.5347 20.939 15.5483 20.9341 15.5617 20.9289C15.5754 20.9237 15.5887 20.9183 15.6023 20.913C15.6158 20.9077 15.6294 20.9024 15.6428 20.897L15.6833 20.8804L15.7239 20.8632C15.7373 20.8575 15.7511 20.852 15.7644 20.8461C15.7781 20.8401 15.7914 20.8339 15.8049 20.8278L15.8455 20.8094L15.886 20.7904C15.8996 20.784 15.9131 20.7774 15.9265 20.7708C15.94 20.7642 15.9538 20.7577 15.9671 20.751C15.9808 20.7441 15.9941 20.737 16.0076 20.73C16.0211 20.723 16.0348 20.7161 16.0481 20.7091C16.0618 20.7018 16.0752 20.6944 16.0887 20.687C16.1023 20.6796 16.1158 20.6721 16.1292 20.6646C16.1428 20.6569 16.1563 20.6492 16.1697 20.6415C16.1834 20.6336 16.1969 20.6255 16.2103 20.6175C16.2239 20.6094 16.2375 20.6013 16.2508 20.5931C16.2645 20.5847 16.2779 20.5761 16.2914 20.5676C16.3049 20.559 16.3186 20.5504 16.3319 20.5417C16.3456 20.5328 16.359 20.5236 16.3724 20.5145C16.386 20.5053 16.3996 20.4963 16.413 20.487C16.4267 20.4774 16.44 20.4677 16.4535 20.458C16.4671 20.4482 16.4807 20.4384 16.494 20.4286C16.5077 20.4184 16.5211 20.408 16.5346 20.3977C16.5482 20.3872 16.5618 20.3767 16.5751 20.3661C16.5888 20.3552 16.6023 20.3442 16.6157 20.3331C16.6293 20.3218 16.6428 20.3104 16.6562 20.299C16.6698 20.2873 16.6834 20.2757 16.6967 20.2639C16.7105 20.2517 16.7238 20.2393 16.7373 20.2269C16.7508 20.2144 16.7645 20.2019 16.7778 20.1892C16.7915 20.176 16.805 20.1627 16.8183 20.1493C16.832 20.1356 16.8455 20.1218 16.8589 20.108C16.8725 20.0938 16.8861 20.0797 16.8994 20.0653C16.9132 20.0503 16.9265 20.0351 16.9399 20.0199C16.9537 20.0043 16.9672 19.9885 16.9805 19.9727C16.9942 19.9564 17.0078 19.9401 17.021 19.9236C17.0347 19.9064 17.0483 19.8892 17.0615 19.8718C17.0754 19.8536 17.0888 19.8352 17.1021 19.8168C17.1159 19.7976 17.1293 19.7783 17.1426 19.7589C17.1564 19.7386 17.1699 19.7182 17.1831 19.6976C17.197 19.6761 17.2105 19.6543 17.2237 19.6324C17.2375 19.6093 17.2511 19.5861 17.2642 19.5627C17.2781 19.5378 17.2916 19.5128 17.3047 19.4876C17.3187 19.4606 17.3322 19.4335 17.3453 19.4061C17.3594 19.3767 17.3728 19.3471 17.3858 19.3172C17.3999 19.2847 17.4135 19.2521 17.4264 19.2191C17.4406 19.1827 17.4541 19.1461 17.4669 19.1091C17.4814 19.0672 17.4948 19.0248 17.5074 18.982C17.5222 18.9321 17.5356 18.8816 17.548 18.8307C17.5632 18.7675 17.5768 18.7036 17.5885 18.639C17.6059 18.5428 17.6193 18.4451 17.629 18.346C17.6426 18.208 17.6493 18.0674 17.6493 17.9246V13.01H17.5885H17.548H17.5074H17.4669H17.4264H17.3858H17.3453H17.3047H17.2642H17.2237H17.1831H17.1426H17.1021H17.0615H17.021H16.9805H16.9399H16.8994H16.8589H16.8183H16.7778H16.7373H16.6967H16.6562H16.6157H16.5751H16.5346H16.494H16.4535H16.413H16.3724H16.3319H16.2914H16.2508H16.2103H16.1697H16.1292H16.0887H16.0481H16.0076H15.9671H15.9265H15.886H15.8455H15.8049H15.7644H15.7239H15.6833H15.6428H15.6023H15.5617H15.5212H15.4807H15.4401H15.3996H15.359H15.3185H15.278H15.2374H15.1969H15.1563H15.1158H15.0753H15.0347H14.9942H14.9537H14.9131H14.8726H14.8321H14.7915H14.751H14.7105H14.6699H14.6294H14.5889H14.5483H14.5078H14.4673H14.4267H14.3862H14.3457H14.3051H14.2646H14.224H14.1835H14.143H14.1024H14.0619L14.0312 17.9246C14.0312 18.0016 14.0278 18.0773 14.0213 18.1515C14.0131 18.2461 13.9995 18.3383 13.9808 18.4279C13.9693 18.4833 13.9557 18.5377 13.9403 18.591C13.928 18.6335 13.9145 18.6753 13.8997 18.7164C13.8872 18.7514 13.8736 18.7858 13.8592 18.8198C13.8464 18.8501 13.8329 18.8799 13.8187 18.9093C13.8057 18.9361 13.7923 18.9626 13.7781 18.9885C13.7652 19.0125 13.7516 19.0359 13.7376 19.0592C13.7245 19.081 13.711 19.1026 13.6971 19.1238C13.6839 19.1438 13.6704 19.1634 13.6565 19.1828C13.6433 19.2013 13.6299 19.2196 13.616 19.2376C13.6028 19.2547 13.5893 19.2714 13.5755 19.288C13.5622 19.3039 13.5487 19.3196 13.5349 19.3351C13.5217 19.35 13.5082 19.3647 13.4944 19.3792C13.4811 19.3931 13.4676 19.407 13.4539 19.4206C13.4406 19.4337 13.4271 19.4467 13.4133 19.4594C13.4 19.4718 13.3865 19.4839 13.3728 19.4958C13.3595 19.5075 13.346 19.519 13.3323 19.5302C13.3189 19.5412 13.3054 19.552 13.2917 19.5627C13.2784 19.5731 13.2648 19.5833 13.2512 19.5934C13.2378 19.6032 13.2243 19.6129 13.2107 19.6224C13.1973 19.6318 13.1838 19.6409 13.1701 19.65C13.1567 19.6588 13.1432 19.6674 13.1296 19.676C13.1162 19.6843 13.1027 19.6925 13.089 19.7006C13.0757 19.7085 13.0621 19.7162 13.0485 19.7239C13.0351 19.7314 13.0216 19.7386 13.008 19.7459C12.9946 19.753 12.9811 19.76 12.9674 19.7668C12.954 19.7735 12.9405 19.7801 12.9269 19.7866C12.9135 19.793 12.9 19.7993 12.8863 19.8055C12.8729 19.8115 12.8595 19.8175 12.8458 19.8232C12.8325 19.8289 12.8189 19.8344 12.8053 19.8399C12.7919 19.8453 12.7783 19.8505 12.7647 19.8557C12.7513 19.8608 12.7379 19.8657 12.7242 19.8706C12.7108 19.8754 12.6973 19.8802 12.6837 19.8848C12.6703 19.8893 12.6567 19.8937 12.6431 19.898C12.6298 19.9022 12.6162 19.9063 12.6026 19.9103C12.5892 19.9143 12.5757 19.9183 12.5621 19.922C12.5486 19.9258 12.5351 19.9295 12.5215 19.933C12.5081 19.9365 12.4946 19.9399 12.481 19.9432C12.4676 19.9464 12.4541 19.9495 12.4405 19.9526C12.427 19.9556 12.4135 19.9586 12.3999 19.9614C12.3865 19.9642 12.373 19.967 12.3594 19.9696C12.346 19.9722 12.3324 19.9746 12.3189 19.977C12.3054 19.9794 12.2919 19.9817 12.2783 19.9839L12.2378 19.9902C12.2244 19.9921 12.2108 19.9939 12.1973 19.9957C12.1838 19.9975 12.1703 19.9993 12.1567 20.0009L12.1162 20.0054C12.1028 20.0068 12.0892 20.008 12.0757 20.0093C12.0622 20.0105 12.0487 20.0118 12.0351 20.0128C12.0217 20.0139 12.0082 20.0148 11.9946 20.0157C11.9811 20.0165 11.9676 20.0173 11.954 20.018C11.9405 20.0187 11.9271 20.0194 11.9135 20.02C11.9001 20.0205 11.8865 20.0208 11.873 20.0212L11.8324 20.0222C11.819 20.0224 11.8055 20.0225 11.7919 20.0226L11.7514 20.0226L11.7108 20.0223L11.6703 20.0218L11.6298 20.0209L11.5892 20.0198L11.5487 20.0185L11.5082 20.0168L11.4676 20.0149L11.4271 20.0129L11.3866 20.0104L11.346 20.0078L11.3055 20.0049L11.265 20.0017L11.2244 19.9983L11.1839 19.9946L11.1433 19.9907L11.1028 19.9865L11.0623 19.9821L11.0217 19.9775L10.9812 19.9725L10.9407 19.9674L10.9001 19.962L10.8596 19.9563L10.819 19.9504L10.7785 19.9443L10.738 19.9379L10.6974 19.9313L10.6569 19.9244L10.6164 19.9173L10.5758 19.91L10.5353 19.9024L10.4948 19.8946L10.4542 19.8866L10.4137 19.8783L10.3732 19.8698L10.3326 19.8611L10.2921 19.8521L10.2516 19.8429L10.211 19.8335L10.1705 19.8238L10.13 19.8139L10.0894 19.8038L10.0489 19.7936L10.0083 19.7829L9.9678 19.7722L9.92728 19.7612L9.88672 19.75L9.8462 19.7386L9.80567 19.7269L9.76512 19.7151L9.7246 19.703L9.68404 19.6907L9.64352 19.6782L9.603 19.6654L9.56244 19.6525L9.52192 19.6394L9.48136 19.6261L9.44084 19.6125L9.40032 19.5987L9.35976 19.5847L9.31924 19.5706L9.27868 19.5562L9.23816 19.5416L9.19764 19.5269L9.15708 19.5118L9.11656 19.4967L9.076 19.4813L9.03548 19.4657L8.99496 19.4499L8.9544 19.434L8.91388 19.4178L8.87332 19.4014L8.8328 19.3849L8.79228 19.3682L8.75172 19.3513L8.7112 19.3341L8.67064 19.3168L8.63012 19.2993L8.5896 19.2817L8.54904 19.2637L8.50852 19.2457L8.468 19.2275L8.42744 19.2091L8.38692 19.1905L8.34637 19.1717L8.30584 19.1528L8.26532 19.1336L8.22476 19.1144C8.19572 19.1004 8.16671 19.0864 8.1377 19.0723Z",fill:"#006CB9"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M19.8644 14.0838C19.8348 14.1079 19.8057 14.1321 19.7773 14.1567V8.61115C19.7773 8.31625 19.8074 8.02839 19.8644 7.75042C19.8766 7.69075 19.8902 7.63162 19.9049 7.57289C19.9175 7.52251 19.9311 7.47253 19.9455 7.42288C19.9583 7.37866 19.9718 7.33475 19.986 7.29114C19.999 7.25129 20.0124 7.21164 20.0265 7.17233C20.0396 7.1359 20.0531 7.09965 20.0671 7.06369C20.0802 7.03008 20.0937 6.99663 20.1076 6.96343C20.1208 6.93212 20.1343 6.90101 20.1481 6.87007C20.1613 6.84069 20.1749 6.81152 20.1887 6.78251C20.2019 6.75479 20.2154 6.72734 20.2292 6.69997C20.2424 6.67367 20.256 6.64758 20.2698 6.62159C20.283 6.59655 20.2966 6.57167 20.3103 6.54694C20.3236 6.52298 20.3371 6.49912 20.3508 6.47547C20.3642 6.4525 20.3776 6.42963 20.3914 6.40692C20.4047 6.385 20.4182 6.36321 20.4319 6.34156C20.4452 6.32048 20.4588 6.29964 20.4725 6.27883C20.4858 6.25847 20.4993 6.23824 20.513 6.21814C20.5264 6.19842 20.5398 6.1787 20.5535 6.15925C20.5668 6.14041 20.5805 6.12188 20.5941 6.10331C20.6074 6.08501 20.6209 6.06681 20.6346 6.04875C20.648 6.03096 20.6614 6.01314 20.6751 5.99558C20.6884 5.97854 20.7021 5.96173 20.7157 5.94489C20.7291 5.92825 20.7426 5.91168 20.7562 5.89528C20.7696 5.87908 20.7831 5.86295 20.7968 5.84699C20.8101 5.83141 20.8237 5.81602 20.8373 5.80064C20.8507 5.78539 20.8642 5.77007 20.8778 5.75499C20.8912 5.74025 20.9048 5.72568 20.9183 5.71115C20.9317 5.69678 20.9453 5.68251 20.9589 5.66832C20.9723 5.65432 20.9858 5.64033 20.9994 5.6265C21.0128 5.61295 21.0264 5.59956 21.0399 5.58618C21.0534 5.57286 21.0668 5.55944 21.0805 5.54629C21.0938 5.53345 21.1075 5.52091 21.121 5.50824C21.1345 5.4956 21.1479 5.48293 21.1616 5.47049C21.175 5.45829 21.1886 5.4463 21.2021 5.43427C21.2156 5.42227 21.229 5.41031 21.2426 5.39852C21.2561 5.38689 21.2696 5.37548 21.2832 5.36402C21.2966 5.35267 21.3101 5.34132 21.3237 5.3301C21.3372 5.31906 21.3507 5.30811 21.3642 5.29723C21.3777 5.28642 21.3912 5.27568 21.4048 5.26504C21.4182 5.25454 21.4318 5.24413 21.4453 5.23377C21.4588 5.2235 21.4723 5.21326 21.4858 5.20317C21.4993 5.19314 21.5128 5.18324 21.5264 5.17338C21.5399 5.16362 21.5534 5.15386 21.5669 5.14424C21.5804 5.13472 21.5939 5.12533 21.6075 5.11594C21.6209 5.10663 21.6344 5.09734 21.648 5.08819C21.6614 5.07914 21.675 5.07023 21.6886 5.06132C21.702 5.05244 21.7155 5.04356 21.7291 5.03486C21.7425 5.02625 21.7561 5.01785 21.7696 5.00937C21.7831 5.00094 21.7966 4.9924 21.8102 4.98413C21.8236 4.97593 21.8372 4.968 21.8507 4.95997C21.8642 4.95194 21.8776 4.94374 21.8912 4.93588C21.9046 4.92812 21.9183 4.92066 21.9318 4.91304C21.9453 4.90541 21.9587 4.89772 21.9723 4.89027C21.9857 4.88288 21.9993 4.87576 22.0128 4.86851C22.0263 4.86129 22.0398 4.85404 22.0534 4.84696L22.0939 4.82619L22.1345 4.80589C22.1479 4.79925 22.1614 4.79254 22.175 4.78603C22.1885 4.77956 22.202 4.77329 22.2156 4.76696C22.2291 4.76065 22.2425 4.75415 22.2561 4.74798C22.2695 4.74185 22.2831 4.73609 22.2966 4.73012C22.3101 4.72416 22.3236 4.71809 22.3372 4.71226C22.3506 4.7065 22.3641 4.70091 22.3777 4.69529L22.4182 4.67862C22.4317 4.67316 22.4452 4.6677 22.4588 4.66238C22.4722 4.6571 22.4858 4.65202 22.4993 4.64686C22.5128 4.64171 22.5262 4.63639 22.5398 4.63138C22.5533 4.62643 22.5669 4.62182 22.5804 4.61701C22.5939 4.6122 22.6073 4.60732 22.6209 4.60264C22.6344 4.59797 22.6479 4.59349 22.6615 4.58899L22.702 4.57574C22.7155 4.5714 22.729 4.56696 22.7425 4.56273C22.756 4.55856 22.7696 4.55463 22.7831 4.5506C22.7966 4.54653 22.8101 4.54236 22.8236 4.53843C22.8371 4.53453 22.8506 4.53091 22.8641 4.52715L22.9047 4.51607C22.9182 4.51247 22.9317 4.50881 22.9452 4.50536C22.9587 4.50194 22.9722 4.49872 22.9857 4.4954C22.9992 4.49208 23.0127 4.48862 23.0263 4.48543C23.0398 4.48228 23.0533 4.47933 23.0668 4.47632L23.1074 4.46744C23.1209 4.46456 23.1343 4.46158 23.1479 4.45883C23.1614 4.45609 23.1749 4.45361 23.1885 4.45101C23.202 4.4484 23.2154 4.44565 23.229 4.44318L23.2695 4.43613L23.3101 4.42939C23.3236 4.42718 23.337 4.42485 23.3506 4.42278C23.3641 4.42071 23.3776 4.41898 23.3911 4.41705C23.4046 4.41515 23.4181 4.41316 23.4317 4.41136L23.4722 4.40624L23.5127 4.40157C23.5263 4.40004 23.5397 4.39828 23.5533 4.39689C23.5668 4.3955 23.5803 4.39448 23.5938 4.39323L23.6344 4.38964C23.6479 4.38849 23.6614 4.38737 23.6749 4.38639C23.6884 4.3854 23.7019 4.38462 23.7154 4.38378C23.7289 4.38293 23.7424 4.38195 23.756 4.3812C23.7695 4.38046 23.783 4.37995 23.7965 4.37934L23.837 4.37778C23.8506 4.3773 23.864 4.37659 23.8776 4.37625C23.8911 4.37591 23.9046 4.37595 23.9181 4.37575C23.9317 4.37554 23.9452 4.3753 23.9587 4.37524L23.9766 4.375H23.9992H24.0398H24.0803H24.1208H24.1614H24.2019H24.2424H24.283H24.3235H24.364H24.4046H24.4451H24.4856H24.5262H24.5667H24.6073H24.6478H24.6884H24.7289H24.7694H24.81H24.8505H24.891H24.9316H24.9721H25.0127H25.0532H25.0937H25.1343H25.1748H25.2153H25.2559H25.2964H25.3369H25.3775H25.418H25.4585H25.4991H25.5396H25.5802H25.6207H25.6613H25.7018H25.7423H25.7829H25.8234H25.8639H25.9045H25.945H25.9855H26.0261H26.0666H26.1072H26.1477H26.1882H26.2288H26.2693H26.3099H26.3504H26.3909H26.4315H26.472H26.5125H26.5531H26.5936H26.6342H26.6747H26.7152H26.7558H26.7963H26.8368H26.8774H26.9179H26.9584H26.999H27.0395H27.0801H27.1206H27.1612H27.2017H27.2422H27.2828H27.3233H27.3638H27.4044H27.4449H27.4854H27.526H27.5665H27.6071H27.6476H27.6882H27.7287H27.7692H27.8098H27.8503H27.8908H27.9314H27.9719H28.0124H28.053H28.0935H28.1341H28.1746H28.2151H28.2557H28.2962H28.3367H28.3773H28.4178H28.4583H28.4989H28.5394H28.58H28.6205H28.661H28.7016H28.7421H28.7827H28.8232H28.8637H28.9043H28.9448H28.9853H29.0259H29.0664H29.107H29.1475H29.1881H29.2286H29.2691H29.3097H29.3502H29.3907H29.4313H29.4718H29.5123H29.5529H29.5934H29.6339H29.6745H29.715H29.7556H29.7961H29.8366H29.8772H29.9177H29.9582H29.9988H30.0393H30.0799H30.1204H30.1548V25.5542C30.1548 25.7384 30.1431 25.9197 30.1204 26.0977C30.1095 26.1834 30.0958 26.2682 30.0799 26.3522C30.0678 26.4161 30.0543 26.4796 30.0393 26.5425C30.0267 26.5954 30.0133 26.6481 29.9988 26.7003C29.986 26.746 29.9725 26.7915 29.9582 26.8366C29.9454 26.8775 29.9318 26.918 29.9177 26.9583C29.9047 26.9956 29.8912 27.0327 29.8772 27.0696C29.8641 27.1039 29.8506 27.1381 29.8366 27.172C29.8235 27.204 29.81 27.2358 29.7961 27.2674C29.7829 27.2973 29.7694 27.3271 29.7556 27.3567C29.7423 27.3849 29.7288 27.413 29.715 27.4409C29.7018 27.4676 29.6883 27.4942 29.6745 27.5207C29.6612 27.5461 29.6477 27.5715 29.6339 27.5966C29.6206 27.621 29.6072 27.6452 29.5934 27.6692C29.5801 27.6924 29.5666 27.7154 29.5529 27.7382C29.5396 27.7604 29.526 27.7824 29.5123 27.8043C29.499 27.8256 29.4855 27.8469 29.4718 27.868C29.4584 27.8886 29.445 27.9093 29.4313 27.9296C29.4179 27.9494 29.4044 27.969 29.3907 27.9885C29.3774 28.0076 29.3638 28.0265 29.3502 28.0453C29.3368 28.0639 29.3233 28.0824 29.3097 28.1007C29.2963 28.1185 29.2828 28.1362 29.2691 28.1539C29.2558 28.1712 29.2422 28.1883 29.2286 28.2053C29.2151 28.2222 29.2017 28.239 29.1881 28.2556C29.1747 28.2718 29.1611 28.2879 29.1475 28.3039C29.1341 28.3197 29.1206 28.3353 29.107 28.3509C29.0935 28.3663 29.0801 28.3817 29.0664 28.3969C29.0531 28.4118 29.0394 28.4264 29.0259 28.441C29.0124 28.4556 28.999 28.4702 28.9853 28.4845C28.9719 28.4986 28.9584 28.5124 28.9448 28.5263C28.9314 28.54 28.9179 28.5536 28.9043 28.5672C28.8909 28.5806 28.8774 28.5939 28.8637 28.6071C28.8503 28.62 28.8368 28.6329 28.8232 28.6457C28.8097 28.6584 28.7963 28.6712 28.7827 28.6837C28.7693 28.696 28.7557 28.7081 28.7421 28.7202C28.7286 28.7323 28.7152 28.7445 28.7016 28.7564C28.6882 28.7681 28.6746 28.7795 28.661 28.791C28.6475 28.8025 28.6341 28.814 28.6205 28.8253C28.6071 28.8364 28.5935 28.8472 28.58 28.8582C28.5665 28.8691 28.5531 28.8801 28.5394 28.8909C28.526 28.9014 28.5124 28.9117 28.4989 28.9221C28.4854 28.9325 28.472 28.9429 28.4583 28.9532C28.445 28.9632 28.4314 28.9731 28.4178 28.983C28.4043 28.9928 28.3909 29.0028 28.3773 29.0125C28.3639 29.0221 28.3503 29.0314 28.3367 29.0408C28.3232 29.0502 28.3098 29.0597 28.2962 29.069C28.2828 29.0781 28.2692 29.0869 28.2557 29.0959C28.2422 29.1048 28.2288 29.1139 28.2151 29.1227C28.2017 29.1314 28.1881 29.1397 28.1746 29.1482C28.1611 29.1567 28.1477 29.1653 28.1341 29.1737C28.1206 29.1819 28.107 29.1899 28.0935 29.198C28.08 29.206 28.0666 29.2142 28.053 29.2221C28.0395 29.2299 28.026 29.2376 28.0124 29.2453L27.9719 29.2681L27.9314 29.2902L27.8908 29.3118C27.8773 29.3188 27.8639 29.326 27.8503 29.3329C27.8368 29.3398 27.8233 29.3465 27.8098 29.3532C27.7963 29.3599 27.7828 29.3668 27.7692 29.3734C27.7558 29.3799 27.7422 29.3861 27.7287 29.3925C27.7152 29.3988 27.7017 29.4053 27.6882 29.4115C27.6747 29.4177 27.6611 29.4236 27.6476 29.4297L27.6071 29.4475L27.5665 29.4648L27.526 29.4815C27.5125 29.487 27.499 29.4927 27.4854 29.4981C27.472 29.5034 27.4584 29.5084 27.4449 29.5135C27.4314 29.5187 27.418 29.524 27.4044 29.5291C27.3909 29.5341 27.3774 29.5389 27.3638 29.5437L27.3233 29.5581L27.2828 29.5721C27.2693 29.5766 27.2557 29.5809 27.2422 29.5853C27.2287 29.5898 27.2152 29.5944 27.2017 29.5986C27.1883 29.6028 27.1747 29.6067 27.1612 29.6108L27.1206 29.623L27.0801 29.6346C27.0666 29.6384 27.0531 29.642 27.0395 29.6456C27.026 29.6493 27.0126 29.6532 26.999 29.6567C26.9855 29.6602 26.972 29.6633 26.9584 29.6666C26.945 29.67 26.9315 29.6734 26.9179 29.6766L26.8774 29.686L26.8368 29.6949C26.8233 29.6978 26.8099 29.701 26.7963 29.7038C26.7829 29.7066 26.7693 29.709 26.7558 29.7116L26.7152 29.7194L26.6747 29.7268C26.6612 29.7292 26.6477 29.7313 26.6342 29.7336C26.6206 29.7358 26.6072 29.7382 26.5936 29.7403C26.5802 29.7424 26.5666 29.7442 26.5531 29.7462L26.5125 29.7519C26.499 29.7537 26.4855 29.7556 26.472 29.7573C26.4585 29.759 26.445 29.7604 26.4315 29.762C26.418 29.7635 26.4045 29.7652 26.3909 29.7666C26.3775 29.7681 26.3639 29.7693 26.3504 29.7706L26.3099 29.7742C26.2963 29.7754 26.2829 29.7768 26.2693 29.7778C26.2559 29.7788 26.2423 29.7795 26.2288 29.7804L26.1882 29.783L26.1477 29.7851C26.1342 29.7858 26.1207 29.7862 26.1072 29.7867C26.0936 29.7872 26.0802 29.7878 26.0666 29.7882C26.0532 29.7886 26.0396 29.7887 26.0261 29.789L25.9855 29.7895C25.9729 29.7896 25.9604 29.79 25.9477 29.79H25.945H25.9045H25.8639H25.8234H25.7829H25.7423H25.7018H25.6613H25.6207H25.5802H25.5396H25.4991H25.4585H25.418H25.3775H25.3369H25.2964H25.2559H25.2153H25.1748H25.1343H25.0937H25.0532H25.0127H24.9721H24.9316H24.891H24.8505H24.81H24.7694H24.7289H24.6884H24.6478H24.6073H24.5667H24.5262H24.4856H24.4451H24.4046H24.364H24.3235H24.283H24.2424H24.2019H24.1614H24.1208H24.0803H24.0398H23.9992H23.9587H23.9181H23.8776H23.837H23.7965H23.756H23.7154H23.6749H23.6344H23.5938H23.5533H23.5127H23.4722H23.4317H23.3911H23.3506H23.3101H23.2695H23.229H23.1885H23.1479H23.1074H23.0668H23.0263H22.9857H22.9452H22.9047H22.8641H22.8236H22.7831H22.7425H22.702H22.6615H22.6209H22.5804H22.5398H22.4993H22.4588H22.4182H22.3777H22.3372H22.2966H22.2561H22.2156H22.175H22.1345H22.0939H22.0534H22.0128H21.9723H21.9318H21.8912H21.8507H21.8102H21.7696H21.7291H21.6886H21.648H21.6075H21.5669H21.5264H21.4858H21.4453H21.4048H21.3642H21.3237H21.2832H21.2426H21.2021H21.1616H21.121H21.0805H21.0399H20.9994H20.9589H20.9183H20.8778H20.8373H20.7968H20.7562H20.7157H20.6751H20.6346H20.5941H20.5535H20.513H20.4725H20.4319H20.3914H20.3508H20.3103H20.2698H20.2292H20.1887H20.1481H20.1076H20.0671H20.0265H19.986H19.9455H19.9049H19.8644H19.7773V20.01C19.8057 20.0345 19.8348 20.0588 19.8644 20.0828C19.8778 20.0937 19.8913 20.1046 19.9049 20.1153C19.9183 20.126 19.9318 20.1366 19.9455 20.1471C19.9589 20.1575 19.9723 20.1679 19.986 20.1782C19.9994 20.1882 20.013 20.1982 20.0265 20.2081C20.0399 20.218 20.0535 20.2277 20.0671 20.2375C20.0805 20.2471 20.094 20.2566 20.1076 20.2662L20.1481 20.2941C20.1615 20.3032 20.1751 20.3123 20.1887 20.3213L20.2292 20.3479C20.2426 20.3567 20.2561 20.3654 20.2698 20.374C20.2831 20.3825 20.2967 20.3909 20.3103 20.3993L20.3508 20.4241C20.3643 20.4323 20.3778 20.4404 20.3914 20.4484C20.4048 20.4564 20.4183 20.4642 20.4319 20.472L20.4725 20.4952L20.513 20.5178L20.5535 20.5399C20.567 20.5471 20.5804 20.5545 20.5941 20.5617C20.6074 20.5687 20.621 20.5757 20.6346 20.5827L20.6751 20.6034L20.7157 20.6237L20.7562 20.6434C20.7697 20.6499 20.7831 20.6565 20.7968 20.6629L20.8373 20.6818L20.8778 20.7004L20.9183 20.7185L20.9589 20.7363L20.9994 20.7538L21.0399 20.7708L21.0805 20.7875L21.121 20.8038L21.1616 20.8199L21.2021 20.8354C21.2156 20.8406 21.229 20.8458 21.2426 20.8509L21.2832 20.8657C21.2967 20.8707 21.3101 20.8757 21.3237 20.8806C21.3371 20.8854 21.3507 20.8901 21.3642 20.8948L21.4048 20.909L21.4453 20.9227C21.4588 20.9272 21.4722 20.9319 21.4858 20.9364C21.4992 20.9408 21.5129 20.9451 21.5264 20.9494L21.5669 20.9625L21.6075 20.9751L21.648 20.9876L21.6886 20.9996L21.7291 21.0115L21.7696 21.0231L21.8102 21.0345L21.8507 21.0456L21.8912 21.0565L21.9318 21.0672L21.9723 21.0776L22.0128 21.0879L22.0534 21.0978L22.0939 21.1077L22.1345 21.1171L22.175 21.1264L22.2156 21.1355L22.2561 21.1444L22.2966 21.1532L22.3372 21.1616L22.3777 21.17L22.4182 21.178L22.4588 21.186L22.4993 21.1937L22.5398 21.2012L22.5804 21.2087L22.6209 21.2158L22.6615 21.2228L22.702 21.2296L22.7425 21.2363L22.7831 21.2429L22.8236 21.2491L22.8641 21.2553L22.9047 21.2613L22.9452 21.2671L22.9857 21.2728L23.0263 21.2782L23.0668 21.2836L23.1074 21.2888L23.1479 21.2938L23.1885 21.2988L23.229 21.3035L23.2695 21.3081L23.3101 21.3126L23.3506 21.3168L23.3911 21.321L23.4317 21.3251L23.4722 21.3289L23.5127 21.3328L23.5533 21.3363L23.5938 21.3398L23.6344 21.3433L23.6749 21.3464L23.7154 21.3495L23.756 21.3526L23.7965 21.3554L23.837 21.3581L23.8776 21.3608L23.9181 21.3632L23.9587 21.3656L23.9992 21.3679L24.0398 21.3699L24.0803 21.3719L24.1208 21.3739L24.1614 21.3756L24.2019 21.3773L24.2424 21.3789L24.283 21.3803L24.3235 21.3816L24.364 21.3829L24.4046 21.384L24.4451 21.385L24.4856 21.386L24.5262 21.3867L24.5667 21.3874L24.6073 21.3881L24.6478 21.3885L24.6884 21.3889L24.7289 21.3892L24.7694 21.3894L24.81 21.3895L24.8505 21.3895L24.891 21.3894L24.9316 21.3892L24.9721 21.389L25.0127 21.3886L25.0532 21.3881L25.0937 21.3876L25.1343 21.387L25.1748 21.3862L25.2153 21.3853L25.2559 21.3845L25.2964 21.3834L25.3369 21.3823L25.3775 21.3812L25.418 21.38L25.4585 21.3785L25.4991 21.3771L25.5396 21.3757L25.5802 21.374L25.6207 21.3723L25.6613 21.3706L25.6811 21.3698L25.7018 21.3688L25.7423 21.3668L25.7829 21.3646L25.8234 21.3624L25.8639 21.3599L25.9045 21.3574L25.945 21.3547L25.9855 21.3519L26.0261 21.3489L26.0666 21.3458L26.1072 21.3426L26.1477 21.3393L26.1882 21.3358L26.2288 21.3322L26.2693 21.3285L26.3099 21.3246L26.3504 21.3207L26.3909 21.3166L26.4315 21.3125L26.472 21.3082L26.5125 21.3038L26.5531 21.2993L26.5936 21.2947L26.6342 21.29L26.6747 21.2852L26.7152 21.2803L26.7558 21.2753L26.7963 21.2701L26.8368 21.2649L26.8774 21.2597L26.9179 21.2542L26.9584 21.2487L26.999 21.2431L27.0395 21.2374L27.0801 21.2317L27.1206 21.2258L27.1612 21.2198L27.2017 21.2137L27.2422 21.2076L27.2828 21.2014L27.3233 21.1951L27.3638 21.1887L27.4044 21.1822L27.4449 21.1756L27.4854 21.169L27.526 21.1622L27.5665 21.1554L27.6071 21.1485L27.6476 21.1415L27.6882 21.1344L27.7287 21.1273L27.7692 21.12L27.8098 21.1127L27.8503 21.1053L27.8908 21.0979L27.9314 21.0903L27.9719 21.0826L28.0124 21.0749L28.053 21.0671L28.0935 21.0593L28.1341 21.0513L28.1746 21.0433L28.2151 21.0352L28.2557 21.027L28.2962 21.0187L28.3367 21.0103L28.3773 21.0019L28.4178 20.9934L28.4583 20.9848L28.4989 20.976L28.5394 20.9673L28.58 20.9584L28.6205 20.9495L28.661 20.9404L28.7016 20.9313L28.7421 20.9221L28.7827 20.9127L28.8232 20.9033L28.8637 20.8937L28.9043 20.884L28.9448 20.8741L28.9851 20.8641V19.0723L28.9448 19.0925L28.9043 19.1127L28.8637 19.1326L28.8232 19.1524L28.7827 19.1721L28.7421 19.1915L28.7016 19.2107L28.661 19.2297L28.6205 19.2485L28.58 19.2672L28.5394 19.2856L28.4989 19.3038L28.4583 19.3217L28.4178 19.3395L28.3773 19.3571L28.3367 19.3744L28.2962 19.3916L28.2557 19.4085L28.2151 19.4252L28.1746 19.4417L28.1341 19.4579L28.0935 19.474L28.053 19.4897L28.0124 19.5053L27.9719 19.5207L27.9314 19.5358L27.8908 19.5506L27.8503 19.5653L27.8098 19.5797L27.7692 19.5939L27.7287 19.6079L27.6882 19.6216L27.6476 19.6351L27.6071 19.6484L27.5665 19.6614L27.526 19.6742L27.4854 19.6868L27.4449 19.6991L27.4044 19.7112L27.3638 19.7231L27.3233 19.7347L27.2828 19.7461L27.2422 19.7573L27.2017 19.7682L27.1612 19.7789L27.1206 19.7894L27.0801 19.7996L27.0395 19.8096L26.999 19.8195L26.9584 19.829L26.9179 19.8383L26.8774 19.8474L26.8368 19.8563L26.7963 19.8649L26.7558 19.8733L26.7152 19.8815L26.6747 19.8895L26.6342 19.8972L26.5936 19.9047L26.5531 19.912L26.5125 19.919L26.472 19.9258L26.4315 19.9324L26.3909 19.9389L26.3504 19.945L26.3099 19.9509L26.2693 19.9567L26.2288 19.9622L26.1882 19.9674L26.1477 19.9725L26.1072 19.9774L26.0666 19.982L26.0261 19.9864L25.9855 19.9906L25.945 19.9946L25.9045 19.9984L25.8639 20.002L25.8234 20.0053L25.7988 20.0072L25.7829 20.0083L25.7423 20.0111L25.7018 20.0138L25.6613 20.016L25.6207 20.0181L25.5802 20.02L25.5396 20.0213L25.4991 20.0226L25.4585 20.0235L25.418 20.0241L25.3775 20.0246C25.3639 20.0247 25.3505 20.0245 25.3369 20.0245L25.2964 20.0243L25.2559 20.0237L25.2153 20.0228L25.1748 20.0218C25.1612 20.0213 25.1478 20.0207 25.1343 20.0201C25.1208 20.0196 25.1072 20.0191 25.0937 20.0185L25.0532 20.0162L25.0127 20.0138L24.9721 20.0109L24.9316 20.0077C24.9181 20.0066 24.9045 20.0056 24.891 20.0044C24.8774 20.0031 24.864 20.0016 24.8505 20.0003L24.81 19.9962L24.7694 19.9914L24.7289 19.9865L24.6884 19.9811L24.6478 19.9753C24.6343 19.9733 24.6207 19.9714 24.6073 19.9692C24.5937 19.9671 24.5802 19.9648 24.5667 19.9625C24.5532 19.9602 24.5396 19.9581 24.5262 19.9557C24.5126 19.9533 24.4992 19.9506 24.4856 19.9481C24.4721 19.9455 24.4585 19.9431 24.4451 19.9404C24.4315 19.9377 24.4181 19.9347 24.4046 19.9319C24.3911 19.929 24.3775 19.9263 24.364 19.9233C24.3504 19.9203 24.337 19.9171 24.3235 19.9139L24.283 19.9043C24.2694 19.901 24.2559 19.8975 24.2424 19.894C24.2289 19.8905 24.2153 19.8871 24.2019 19.8835C24.1883 19.8798 24.1748 19.876 24.1614 19.8722C24.1478 19.8684 24.1342 19.8645 24.1208 19.8605C24.1072 19.8565 24.0937 19.8523 24.0803 19.8482C24.0667 19.844 24.0532 19.8398 24.0398 19.8354C24.0262 19.831 24.0127 19.8265 23.9992 19.8219C23.9857 19.8174 23.9721 19.8128 23.9587 19.8081C23.9451 19.8033 23.9316 19.7983 23.9181 19.7934C23.9046 19.7884 23.891 19.7835 23.8776 19.7784C23.864 19.7731 23.8505 19.7677 23.837 19.7623C23.8235 19.7569 23.8099 19.7516 23.7965 19.7461C23.7829 19.7404 23.7695 19.7345 23.756 19.7287C23.7424 19.7228 23.7288 19.717 23.7154 19.711C23.7018 19.7049 23.6884 19.6985 23.6749 19.6922C23.6613 19.6858 23.6478 19.6794 23.6344 19.6729C23.6208 19.6663 23.6072 19.6595 23.5938 19.6527C23.5802 19.6458 23.5667 19.6388 23.5533 19.6317C23.5397 19.6246 23.5261 19.6175 23.5127 19.6101C23.4991 19.6027 23.4857 19.5949 23.4722 19.5872C23.4587 19.5795 23.4451 19.5718 23.4317 19.5638C23.418 19.5557 23.4046 19.5474 23.3911 19.5392C23.3775 19.5307 23.364 19.5221 23.3506 19.5135C23.337 19.5048 23.3234 19.4962 23.3101 19.4872C23.2964 19.4781 23.283 19.4686 23.2695 19.4592C23.2559 19.4498 23.2424 19.4401 23.229 19.4304C23.2154 19.4206 23.2018 19.4107 23.1885 19.4006C23.1748 19.3902 23.1613 19.3796 23.1479 19.369C23.1343 19.3582 23.1208 19.3474 23.1074 19.3364C23.0938 19.3252 23.0802 19.314 23.0668 19.3025C23.0531 19.2907 23.0397 19.2787 23.0263 19.2666C23.0126 19.2543 22.9991 19.2419 22.9857 19.2293C22.9721 19.2164 22.9585 19.2035 22.9452 19.1904C22.9316 19.1769 22.918 19.1635 22.9047 19.1498C22.8909 19.1356 22.8775 19.1212 22.8641 19.1067C22.8504 19.0918 22.8369 19.0767 22.8236 19.0615C22.8099 19.0458 22.7964 19.03 22.7831 19.014C22.7694 18.9976 22.7558 18.9809 22.7425 18.9641C22.7288 18.9467 22.7152 18.929 22.702 18.9112C22.6883 18.8928 22.6747 18.8742 22.6615 18.8553C22.6477 18.8357 22.6341 18.8159 22.6209 18.7958C22.6071 18.7748 22.5936 18.7536 22.5804 18.7321C22.5665 18.7096 22.553 18.6868 22.5398 18.6638C22.5259 18.6396 22.5125 18.615 22.4993 18.5902C22.4853 18.5638 22.4719 18.5371 22.4588 18.5101C22.4448 18.4814 22.4313 18.4523 22.4182 18.4228C22.4042 18.3911 22.3906 18.3592 22.3777 18.3266C22.3634 18.2907 22.3501 18.254 22.3372 18.217C22.3228 18.176 22.3093 18.1344 22.2966 18.0921C22.282 18.0431 22.2684 17.9933 22.2561 17.9426C22.2407 17.8795 22.2273 17.8151 22.2156 17.7495C22.1981 17.6522 22.1845 17.5523 22.175 17.4496C22.1641 17.331 22.1585 17.2089 22.1585 17.0832C22.1585 16.9574 22.1641 16.8352 22.175 16.7165C22.1845 16.6138 22.1981 16.5138 22.2156 16.4165C22.2273 16.3509 22.2407 16.2864 22.2561 16.2233C22.2684 16.1726 22.282 16.1228 22.2966 16.0737C22.3093 16.0314 22.3228 15.9898 22.3372 15.9487C22.3501 15.9117 22.3634 15.8751 22.3777 15.8391C22.3906 15.8065 22.4042 15.7745 22.4182 15.7428C22.4313 15.7133 22.4448 15.6842 22.4588 15.6555C22.4719 15.6285 22.4853 15.6017 22.4993 15.5754C22.5125 15.5506 22.5259 15.526 22.5398 15.5017C22.553 15.4787 22.5665 15.4559 22.5804 15.4334C22.5936 15.4119 22.6071 15.3907 22.6209 15.3697C22.6341 15.3496 22.6477 15.3298 22.6615 15.3101C22.6747 15.2912 22.6883 15.2726 22.702 15.2542C22.7152 15.2363 22.7288 15.2188 22.7425 15.2013C22.7558 15.1845 22.7694 15.1678 22.7831 15.1514C22.7964 15.1353 22.8099 15.1195 22.8236 15.1039C22.8369 15.0886 22.8504 15.0735 22.8641 15.0586C22.8775 15.0441 22.8909 15.0296 22.9047 15.0155C22.918 15.0018 22.9316 14.9883 22.9452 14.9749C22.9585 14.9618 22.9721 14.9488 22.9857 14.936C22.9991 14.9234 23.0126 14.9109 23.0263 14.8986C23.0397 14.8865 23.0531 14.8745 23.0668 14.8627C23.0802 14.8512 23.0938 14.8401 23.1074 14.8288C23.1208 14.8178 23.1343 14.8069 23.1479 14.7962C23.1613 14.7855 23.1748 14.7749 23.1885 14.7646C23.2018 14.7545 23.2154 14.7446 23.229 14.7347C23.2424 14.725 23.2559 14.7154 23.2695 14.7059C23.283 14.6965 23.2964 14.6871 23.3101 14.6779C23.3234 14.669 23.337 14.6603 23.3506 14.6516C23.364 14.6429 23.3775 14.6344 23.3911 14.626C23.4046 14.6177 23.418 14.6094 23.4317 14.6013C23.4451 14.5934 23.4587 14.5856 23.4722 14.5779C23.4857 14.5702 23.4991 14.5625 23.5127 14.555C23.5261 14.5476 23.5397 14.5405 23.5533 14.5334C23.5667 14.5263 23.5802 14.5193 23.5938 14.5123C23.6072 14.5055 23.6208 14.4988 23.6344 14.4921C23.6478 14.4856 23.6613 14.4792 23.6749 14.4729C23.6884 14.4666 23.7018 14.4602 23.7154 14.4541C23.7288 14.448 23.7424 14.4422 23.756 14.4364C23.7695 14.4305 23.7829 14.4246 23.7965 14.419C23.8099 14.4134 23.8235 14.4081 23.837 14.4027C23.8505 14.3973 23.864 14.3919 23.8776 14.3867C23.891 14.3815 23.9046 14.3766 23.9181 14.3717C23.9316 14.3667 23.9451 14.3617 23.9587 14.3569C23.9721 14.3522 23.9857 14.3476 23.9992 14.3431C24.0127 14.3385 24.0262 14.334 24.0398 14.3296C24.0532 14.3252 24.0667 14.321 24.0803 14.3168C24.0937 14.3127 24.1072 14.3085 24.1208 14.3045L24.1614 14.2928C24.1748 14.289 24.1883 14.2852 24.2019 14.2815C24.2153 14.2779 24.2289 14.2744 24.2424 14.2709C24.2559 14.2675 24.2694 14.264 24.283 14.2606L24.3235 14.2511C24.337 14.2479 24.3504 14.2447 24.364 14.2417C24.3775 14.2387 24.3911 14.2359 24.4046 14.2331C24.4181 14.2302 24.4315 14.2272 24.4451 14.2245C24.4585 14.2218 24.4721 14.2194 24.4856 14.2169C24.4992 14.2144 24.5126 14.2117 24.5262 14.2092C24.5396 14.2068 24.5532 14.2047 24.5667 14.2025C24.5802 14.2002 24.5937 14.1979 24.6073 14.1957L24.6478 14.1897L24.6884 14.1838L24.7289 14.1784L24.7694 14.1735L24.81 14.1687L24.8505 14.1646C24.864 14.1633 24.8774 14.1618 24.891 14.1605C24.9045 14.1593 24.9181 14.1583 24.9316 14.1572L24.9721 14.154L25.0127 14.1512L25.0532 14.1487L25.0937 14.1464C25.1072 14.1458 25.1208 14.1453 25.1343 14.1448C25.1478 14.1442 25.1612 14.1436 25.1748 14.1431L25.2153 14.1421L25.2559 14.1412L25.2964 14.1406L25.3369 14.1405C25.3505 14.1404 25.3639 14.1402 25.3775 14.1403L25.418 14.1408L25.4585 14.1414L25.4991 14.1423L25.5396 14.1436L25.5802 14.145L25.6207 14.1469L25.6613 14.1489L25.7018 14.1511L25.7423 14.1538L25.7829 14.1566L25.7988 14.1577L25.8234 14.1596L25.8639 14.1629L25.9045 14.1664L25.945 14.1701L25.9855 14.174L26.0261 14.1782L26.0666 14.1826L26.1072 14.1871L26.1477 14.1919L26.1882 14.1969L26.2288 14.2021L26.2693 14.2075L26.3099 14.2132L26.3504 14.219L26.3909 14.2251L26.4315 14.2314L26.472 14.238L26.5125 14.2447L26.5531 14.2517L26.5936 14.2589L26.6342 14.2663L26.6747 14.274L26.7152 14.2819L26.7558 14.29L26.7963 14.2983L26.8368 14.3069L26.8774 14.3157L26.9179 14.3247L26.9584 14.3339L26.999 14.3434L27.0395 14.3532L27.0801 14.3631L27.1206 14.3733L27.1612 14.3837L27.2017 14.3944L27.2422 14.4053L27.2828 14.4164L27.3233 14.4278L27.3638 14.4394L27.4044 14.4513L27.4449 14.4634L27.4854 14.4757L27.526 14.4883L27.5665 14.501L27.6071 14.5141L27.6476 14.5274L27.6882 14.5409L27.7287 14.5546L27.7692 14.5686L27.8098 14.5828L27.8503 14.5973L27.8908 14.612L27.9314 14.6269L27.9719 14.6421L28.0124 14.6575L28.053 14.6732L28.0935 14.689L28.1341 14.7051L28.1746 14.7215L28.2151 14.738L28.2557 14.7548L28.2962 14.7719L28.3367 14.7891L28.3773 14.8066L28.4178 14.8243L28.4583 14.8422L28.4989 14.8603L28.5394 14.8787L28.58 14.8973L28.6205 14.916L28.661 14.935L28.7016 14.9542L28.7421 14.9736L28.7827 14.9932L28.8232 15.013L28.8637 15.033L28.9043 15.0531L28.9448 15.0735L28.9851 15.0939V13.3008L28.9448 13.2908L28.9043 13.2809L28.8637 13.2712L28.8232 13.2617L28.7827 13.2522L28.7421 13.2429L28.7016 13.2336L28.661 13.2245L28.6205 13.2155L28.58 13.2065L28.5394 13.1976L28.4989 13.1889L28.4583 13.1802L28.4178 13.1716L28.3773 13.1631L28.3367 13.1546L28.2962 13.1463L28.2557 13.138L28.2151 13.1298L28.1746 13.1217L28.1341 13.1137L28.0935 13.1057L28.053 13.0979L28.0124 13.0901L27.9719 13.0824L27.9314 13.0747L27.8908 13.0672L27.8503 13.0597L27.8098 13.0523L27.7692 13.045L27.7287 13.0378L27.6882 13.0307L27.6476 13.0236L27.6071 13.0166L27.5665 13.0097L27.526 13.0029L27.4854 12.9962L27.4449 12.9895L27.4044 12.983L27.3638 12.9765L27.3233 12.9701L27.2828 12.9638L27.2422 12.9576L27.2017 12.9514L27.1612 12.9454L27.1206 12.9394L27.0801 12.9336L27.0395 12.9278L26.999 12.9222L26.9584 12.9166L26.9179 12.9111L26.8774 12.9057L26.8368 12.9004L26.7963 12.8952L26.7558 12.8901L26.7152 12.8851L26.6747 12.8802L26.6342 12.8754L26.5936 12.8707L26.5531 12.8661L26.5125 12.8616L26.472 12.8573L26.4315 12.853L26.3909 12.8489L26.3504 12.8448L26.3099 12.8409L26.2693 12.837L26.2288 12.8334L26.1882 12.8298L26.1477 12.8263L26.1072 12.823L26.0666 12.8198L26.0261 12.8168L25.9855 12.8138L25.945 12.811L25.9045 12.8083L25.8639 12.8058L25.8234 12.8034L25.7829 12.8011L25.7423 12.799L25.7018 12.797L25.6811 12.796L25.6613 12.7952L25.6207 12.7935L25.5802 12.7918L25.5396 12.7901L25.4991 12.7887L25.4585 12.7873L25.418 12.7859L25.3775 12.7846L25.3369 12.7835L25.2964 12.7824L25.2559 12.7813L25.2153 12.7805L25.1748 12.7797L25.1343 12.7788L25.0937 12.7783L25.0532 12.7777L25.0127 12.7772L24.9721 12.7769L24.9316 12.7767L24.891 12.7764L24.8505 12.7763L24.81 12.7764L24.7694 12.7764L24.7289 12.7766L24.6884 12.777L24.6478 12.7774L24.6073 12.7778L24.5667 12.7785L24.5262 12.7792L24.4856 12.7799L24.4451 12.7809L24.4046 12.7819L24.364 12.7829L24.3235 12.7842L24.283 12.7856L24.2424 12.787L24.2019 12.7886L24.1614 12.7903L24.1208 12.792L24.0803 12.794L24.0398 12.796L23.9992 12.798L23.9587 12.8003L23.9181 12.8027L23.8776 12.8051L23.837 12.8078L23.7965 12.8105L23.756 12.8133L23.7154 12.8164L23.6749 12.8195L23.6344 12.8227L23.5938 12.8261L23.5533 12.8295L23.5127 12.8332L23.4722 12.837L23.4317 12.8408L23.3911 12.8449L23.3506 12.8491L23.3101 12.8533L23.2695 12.8579L23.229 12.8625L23.1885 12.8672L23.1479 12.8722L23.1074 12.8772L23.0668 12.8824L23.0263 12.8878L22.9857 12.8932L22.9452 12.8989L22.9047 12.9047L22.8641 12.9107L22.8236 12.9169L22.7831 12.9231L22.7425 12.9297L22.702 12.9364L22.6615 12.9432L22.6209 12.9502L22.5804 12.9573L22.5398 12.9648L22.4993 12.9723L22.4588 12.9801L22.4182 12.988L22.3777 12.996L22.3372 13.0045L22.2966 13.0129L22.2561 13.0216L22.2156 13.0306L22.175 13.0396L22.1345 13.049L22.0939 13.0584L22.0534 13.0683L22.0128 13.0782L21.9723 13.0885L21.9318 13.0989L21.8912 13.1096L21.8507 13.1205L21.8102 13.1316L21.7696 13.1431L21.7291 13.1546L21.6886 13.1666L21.648 13.1786L21.6075 13.1911L21.5669 13.2037L21.5264 13.2168C21.5129 13.2212 21.4992 13.2254 21.4858 13.2299L21.4453 13.2435C21.4318 13.2481 21.4182 13.2525 21.4048 13.2572L21.3642 13.2714L21.3237 13.2857C21.3101 13.2905 21.2967 13.2956 21.2832 13.3005C21.2696 13.3055 21.256 13.3104 21.2426 13.3154L21.2021 13.3309C21.1886 13.3361 21.175 13.3412 21.1616 13.3464L21.121 13.3625L21.0805 13.3788L21.0399 13.3956L20.9994 13.4126L20.9589 13.43L20.9183 13.4478L20.8778 13.4659L20.8373 13.4846L20.7968 13.5034C20.7831 13.5099 20.7697 13.5164 20.7562 13.523L20.7157 13.5427L20.6751 13.563L20.6346 13.5837C20.621 13.5908 20.6074 13.5977 20.5941 13.6048C20.5804 13.612 20.567 13.6193 20.5535 13.6266L20.513 13.6487L20.4725 13.6713C20.4588 13.679 20.4454 13.6867 20.4319 13.6945C20.4183 13.7023 20.4048 13.7101 20.3914 13.7181C20.3778 13.7261 20.3643 13.7342 20.3508 13.7424C20.3372 13.7506 20.3237 13.7589 20.3103 13.7672C20.2967 13.7756 20.2831 13.784 20.2698 13.7925C20.2561 13.8012 20.2426 13.8099 20.2292 13.8186L20.1887 13.8453C20.1751 13.8543 20.1615 13.8633 20.1481 13.8724L20.1076 13.9004C20.094 13.91 20.0805 13.9195 20.0671 13.9291C20.0535 13.9389 20.0399 13.9487 20.0265 13.9585C20.013 13.9685 19.9994 13.9784 19.986 13.9885C19.9723 13.9988 19.9589 14.0091 19.9455 14.0195C19.9318 14.0301 19.9183 14.0407 19.9049 14.0513C19.8913 14.0621 19.8778 14.0729 19.8644 14.0839V14.0838Z",fill:"#E10238"})),w=()=>(0,a.createElement)("svg",{width:"51",height:"35",viewBox:"0 0 51 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("rect",{x:"0.5",y:"0.5",width:"50",height:"34",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M18.6846 27.0292V28.3215V29.6137H18.1154V29.2999C17.9349 29.5327 17.661 29.6787 17.2886 29.6787C16.5546 29.6787 15.9791 29.1112 15.9791 28.3215C15.9791 27.5324 16.5546 26.9642 17.2886 26.9642C17.661 26.9642 17.9349 27.1103 18.1154 27.343V27.0292H18.6846ZM17.3594 27.494C16.8667 27.494 16.5652 27.8672 16.5652 28.3215C16.5652 28.7757 16.8667 29.1489 17.3594 29.1489C17.8302 29.1489 18.148 28.7918 18.148 28.3215C18.148 27.8511 17.8302 27.494 17.3594 27.494ZM37.9186 28.3215C37.9186 27.8672 38.2201 27.494 38.7128 27.494C39.1842 27.494 39.5014 27.8511 39.5014 28.3215C39.5014 28.7918 39.1842 29.1489 38.7128 29.1489C38.2201 29.1489 37.9186 28.7757 37.9186 28.3215ZM40.0386 25.9913V28.3215V29.6137H39.4688V29.2999C39.2882 29.5327 39.0143 29.6787 38.642 29.6787C37.9079 29.6787 37.3325 29.1112 37.3325 28.3215C37.3325 27.5324 37.9079 26.9642 38.642 26.9642C39.0143 26.9642 39.2882 27.1103 39.4688 27.343V25.9913H40.0386ZM25.7496 27.4674C26.1163 27.4674 26.352 27.6945 26.4122 28.0943H25.0538C25.1146 27.7211 25.3441 27.4674 25.7496 27.4674ZM24.4571 28.3215C24.4571 27.5157 24.9937 26.9642 25.7609 26.9642C26.4943 26.9642 26.9983 27.5157 27.0039 28.3215C27.0039 28.397 26.9983 28.4675 26.9926 28.5375L25.0488 28.5375C25.1309 29.0029 25.465 29.1706 25.8317 29.1706C26.0944 29.1706 26.374 29.0728 26.5933 28.9001L26.8723 29.3167C26.5545 29.5815 26.1934 29.6787 25.7991 29.6787C25.0156 29.6787 24.4571 29.1434 24.4571 28.3215ZM32.6337 28.3215C32.6337 27.8672 32.9353 27.494 33.4279 27.494C33.8987 27.494 34.2165 27.8511 34.2165 28.3215C34.2165 28.7918 33.8987 29.1489 33.4279 29.1489C32.9353 29.1489 32.6337 28.7757 32.6337 28.3215ZM34.7529 27.0292V28.3215V29.6137H34.1837V29.2999C34.0026 29.5327 33.7293 29.6787 33.3569 29.6787C32.6229 29.6787 32.0475 29.1112 32.0475 28.3215C32.0475 27.5324 32.6229 26.9642 33.3569 26.9642C33.7293 26.9642 34.0026 27.1103 34.1837 27.343V27.0292H34.7529ZM29.4191 28.3215C29.4191 29.1056 29.972 29.6787 30.8157 29.6787C31.21 29.6787 31.4726 29.5921 31.7572 29.3705L31.4839 28.9162C31.2701 29.0679 31.0457 29.1489 30.7988 29.1489C30.3443 29.1434 30.0102 28.8191 30.0102 28.3215C30.0102 27.8239 30.3443 27.4996 30.7988 27.494C31.0457 27.494 31.2701 27.5751 31.4839 27.7267L31.7572 27.2724C31.4726 27.0509 31.21 26.9642 30.8157 26.9642C29.972 26.9642 29.4191 27.5373 29.4191 28.3215ZM36.0674 27.3431C36.2153 27.1159 36.4291 26.9643 36.7575 26.9643C36.8729 26.9643 37.0371 26.986 37.1631 27.0349L36.9876 27.5646C36.8672 27.5157 36.7469 27.4997 36.6315 27.4997C36.2592 27.4997 36.073 27.7373 36.073 28.165V29.6138H35.5032V27.0293H36.0674V27.3431ZM21.4996 27.2347C21.2257 27.0564 20.8483 26.9642 20.4321 26.9642C19.7689 26.9642 19.342 27.278 19.342 27.7917C19.342 28.2132 19.6599 28.4731 20.2453 28.5542L20.5142 28.5919C20.8264 28.6352 20.9737 28.7163 20.9737 28.8624C20.9737 29.0623 20.7656 29.1762 20.377 29.1762C19.9827 29.1762 19.6981 29.0518 19.5063 28.9057L19.238 29.3433C19.5502 29.5704 19.9444 29.6787 20.3713 29.6787C21.1273 29.6787 21.5654 29.3272 21.5654 28.8352C21.5654 28.3809 21.2207 28.1432 20.6509 28.0621L20.3826 28.0238C20.1363 27.9916 19.9388 27.9433 19.9388 27.77C19.9388 27.5806 20.125 27.4674 20.4371 27.4674C20.7712 27.4674 21.0947 27.5918 21.2533 27.689L21.4996 27.2347ZM28.1542 27.3431C28.3015 27.1159 28.5152 26.9643 28.8437 26.9643C28.959 26.9643 29.1233 26.986 29.2493 27.0349L29.0738 27.5646C28.9534 27.5157 28.833 27.4997 28.7177 27.4997C28.3454 27.4997 28.1592 27.7373 28.1592 28.165V29.6138H27.59V27.0293L28.1542 27.0293V27.3431ZM23.9862 27.0292H23.0553V26.2451H22.4799V27.0292H21.949V27.5429H22.4799V28.7219C22.4799 29.3216 22.7156 29.6787 23.3888 29.6787C23.6358 29.6787 23.9204 29.6032 24.1009 29.4788L23.9367 28.9973C23.7668 29.0945 23.5806 29.1434 23.4327 29.1434C23.1481 29.1434 23.0553 28.9701 23.0553 28.7108V27.5429H23.9862V27.0292ZM15.4758 27.9917V29.6138H14.9003V28.1755C14.9003 27.7373 14.7142 27.4941 14.3255 27.4941C13.9475 27.4941 13.6849 27.7324 13.6849 28.1811V29.6138H13.1095V28.1755C13.1095 27.7373 12.9183 27.4941 12.5403 27.4941C12.151 27.4941 11.899 27.7324 11.899 28.1811V29.6138H11.3242V27.0293H11.894V27.348C12.1078 27.0454 12.3811 26.9643 12.6606 26.9643C13.0606 26.9643 13.3451 27.1376 13.5257 27.4242C13.767 27.0615 14.1118 26.9587 14.4459 26.9643C15.0815 26.9699 15.4758 27.3808 15.4758 27.9917Z",fill:"#231F20"}),(0,a.createElement)("path",{d:"M29.9381 22.6376H21.3115V7.33105H29.9381V22.6376Z",fill:"#FF5F00"}),(0,a.createElement)("path",{d:"M21.8586 14.9846C21.8586 11.8796 23.331 9.11372 25.624 7.33129C23.9472 6.02789 21.831 5.24994 19.5311 5.24994C14.0864 5.24994 9.67285 9.60822 9.67285 14.9846C9.67285 20.361 14.0864 24.7192 19.5311 24.7192C21.831 24.7192 23.9472 23.9413 25.624 22.6379C23.331 20.8555 21.8586 18.0896 21.8586 14.9846Z",fill:"#EB001B"}),(0,a.createElement)("path",{d:"M41.5758 14.9846C41.5758 20.361 37.1622 24.7192 31.7175 24.7192C29.4177 24.7192 27.3014 23.9413 25.624 22.6379C27.9176 20.8555 29.3901 18.0896 29.3901 14.9846C29.3901 11.8796 27.9176 9.11372 25.624 7.33129C27.3014 6.02789 29.4177 5.24994 31.7175 5.24994C37.1622 5.24994 41.5758 9.60822 41.5758 14.9846Z",fill:"#F79E1B"})),H=()=>(0,a.createElement)("svg",{width:"64",height:"40",viewBox:"0 0 64 40",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("path",{d:"M0 4C0 1.79086 1.79086 0 4 0H60C62.2091 0 64 1.79086 64 4V36C64 38.2091 62.2091 40 60 40H4C1.79086 40 0 38.2091 0 36V4Z",fill:"#EF809F"}),(0,a.createElement)("path",{d:"M29.333 25.0919C29.8266 25.5762 29.836 26.368 29.3516 26.8617L29.2585 26.9641C28.7741 27.4578 27.9824 27.4671 27.4887 26.9828L24.6943 24.2163C23.9212 23.4432 23.8187 22.4931 24.3031 21.9901L24.3962 21.8877C24.7968 21.4685 25.4581 21.4313 25.9145 21.7852C26.1287 21.9529 26.4454 21.9156 26.6224 21.7014C26.7901 21.4871 26.7528 21.1705 26.5386 20.9935C25.6723 20.3135 24.4335 20.3973 23.6697 21.1798L23.5766 21.2822C22.673 22.2137 22.85 23.7878 23.9771 24.9149L26.7808 27.7093C27.6843 28.5755 29.1094 28.5569 29.985 27.6627L30.0874 27.5603C30.9444 26.6661 30.9258 25.2502 30.0409 24.3747C29.8453 24.1791 29.5286 24.1791 29.333 24.384C29.128 24.5796 29.128 24.8963 29.333 25.0919ZM27.6563 10.0861L28.802 19.4938C28.83 19.764 29.0815 19.9689 29.3609 19.9316C29.631 19.9037 29.836 19.6522 29.7987 19.3727L28.653 9.92776L28.6437 9.87187C28.4481 9.08944 28.9324 8.21387 29.5845 8.07415C29.9477 7.99964 30.3296 8.06484 30.637 8.26976C30.9444 8.47468 31.1586 8.79138 31.2331 9.15465L33.2265 17.6682C33.2917 17.9383 33.5618 18.106 33.8319 18.0408C34.102 17.9756 34.2697 17.7054 34.2045 17.4353L32.2205 8.94972C32.0994 8.33496 31.7268 7.7854 31.1959 7.43145C30.665 7.07749 30.0036 6.9564 29.3795 7.08681C28.15 7.34762 27.349 8.78206 27.6563 10.0861Z",fill:"white"}),(0,a.createElement)("path",{d:"M38.1632 21.1332L40.4266 9.86258C40.6874 8.65168 39.9702 7.43147 38.7686 7.07752C37.4273 6.7422 36.0767 7.52462 35.732 8.84729L33.2264 17.426C33.1519 17.6961 33.3009 17.9756 33.5711 18.0501C33.8412 18.1246 34.1206 17.9756 34.1951 17.7055L36.7008 9.11741C36.915 8.32567 37.7161 7.85063 38.4985 8.04624C39.1691 8.25116 39.5883 8.94975 39.4393 9.65766L37.1758 20.9376C37.1199 21.2077 37.2969 21.4779 37.567 21.5338C37.8465 21.5803 38.1166 21.4033 38.1632 21.1332ZM32.4719 31.994C28.8858 31.994 25.8959 29.3673 25.8959 25.6322C25.8959 25.3527 25.6723 25.1292 25.3929 25.1292C25.1134 25.1292 24.8899 25.3527 24.8899 25.6322C24.8899 29.9541 28.3642 33 32.4719 33C32.7514 33 32.9749 32.7765 32.9749 32.497C32.9749 32.2176 32.7514 31.994 32.4719 31.994ZM30.7022 23.2104C30.6277 23.4898 30.4507 23.7413 30.2085 23.8997L30.0967 23.9649C29.5192 24.3561 28.7368 24.207 28.3456 23.6295L26.1473 20.3601C25.5512 19.4473 25.6257 18.4972 26.2032 18.0967L26.3057 18.0222C27.0509 17.5937 27.6749 17.9197 28.3176 18.8605L30.1526 21.599C30.311 21.8318 30.6183 21.8877 30.8512 21.7387C31.0841 21.5896 31.14 21.2729 30.9909 21.0401L29.156 18.3016C28.2431 16.9789 27.1254 16.3921 25.7748 17.1745L25.635 17.2677C24.5546 18.0035 24.4241 19.587 25.309 20.919L27.5073 24.1977C28.2152 25.2316 29.6124 25.5018 30.6463 24.8032L30.7581 24.7287C31.1958 24.4306 31.5125 23.9835 31.6616 23.4712C31.7361 23.2011 31.5777 22.9309 31.3076 22.8564C31.0468 22.7819 30.7767 22.9403 30.7022 23.2104Z",fill:"white"}),(0,a.createElement)("path",{d:"M34.0273 24.8684C32.9841 25.4552 32.3321 26.545 32.2576 28.2123C32.2389 28.4918 32.4532 28.7246 32.7326 28.7433C33.012 28.7526 33.2449 28.5476 33.2635 28.2682C33.3381 26.7592 33.9249 25.9489 34.8656 25.5949C35.3686 25.4086 35.9182 25.3714 36.4026 25.4179L36.5236 25.4366C37.1663 25.5577 37.3713 24.5983 36.7286 24.4492L32.863 23.583C31.056 23.0893 30.7765 22.4187 31.1119 21.3009C31.2609 20.8165 31.5497 20.5651 32.0061 20.4626C32.3041 20.3974 32.6301 20.4067 32.9468 20.4626C33.04 20.4812 33.1052 20.4905 33.1331 20.4998L39.0013 21.8691L39.1131 21.9064C39.2062 21.9436 39.3087 22.0181 39.4112 22.1206C39.8024 22.5397 40.0259 23.3315 39.9141 24.6355C39.5323 29.1997 37.4085 32.0313 32.36 31.9847C32.0806 31.9847 31.857 32.2083 31.857 32.4784C31.857 32.7578 32.0806 32.9814 32.3507 32.9814C38.014 33.028 40.4917 29.7399 40.9108 24.71C41.0412 23.1452 40.7525 22.074 40.147 21.4313C39.8117 21.0774 39.4577 20.9097 39.1876 20.8724L33.3846 19.5218C33.3474 19.5125 33.2542 19.4846 33.1238 19.4659C32.686 19.3914 32.2296 19.3821 31.7825 19.4752C31.0001 19.6522 30.404 20.1459 30.1525 21.0028C29.6681 22.6143 30.1804 23.881 32.6208 24.5424L34.0273 24.8684Z",fill:"white"}),(0,a.createElement)("path",{d:"M4 1H60V-1H4V1ZM63 4V36H65V4H63ZM60 39H4V41H60V39ZM1 36V4H-1V36H1ZM4 39C2.34315 39 1 37.6569 1 36H-1C-1 38.7614 1.23858 41 4 41V39ZM63 36C63 37.6569 61.6569 39 60 39V41C62.7614 41 65 38.7614 65 36H63ZM60 1C61.6569 1 63 2.34315 63 4H65C65 1.23858 62.7614 -1 60 -1V1ZM4 -1C1.23858 -1 -1 1.23858 -1 4H1C1 2.34315 2.34315 1 4 1V-1Z",fill:"#DDDDDD"})),_=()=>(0,a.createElement)("svg",{width:"52",height:"35",viewBox:"0 0 52 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("rect",{x:"0.878906",y:"0.5",width:"50",height:"34",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M44.0545 5.25735L34.3353 5.25488C34.3341 5.25488 34.3328 5.25488 34.3328 5.25488C34.3253 5.25488 34.3179 5.2562 34.3106 5.2562C32.9754 5.29641 31.3124 6.34915 31.0096 7.64726L26.4132 27.6401C26.1104 28.9503 26.9343 30.0165 28.2599 30.0361H38.4703C39.7756 29.9726 41.044 28.932 41.3417 27.6486L45.9382 7.65564C46.2459 6.33208 45.402 5.25735 44.0545 5.25735Z",fill:"#01798A"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M26.4134 27.6401L31.0097 7.64729C31.3126 6.34917 32.9755 5.29643 34.3107 5.25622L30.4464 5.25376L23.484 5.25244C22.1451 5.27936 20.4605 6.33949 20.1577 7.64729L15.5601 27.6401C15.2561 28.9503 16.0813 30.0165 17.4059 30.0361H28.26C26.9345 30.0165 26.1105 28.9503 26.4134 27.6401",fill:"#024381"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M15.5602 27.64L20.1578 7.64714C20.4606 6.33934 22.1452 5.27922 23.4841 5.2523L14.5649 5.25C13.2185 5.25 11.4923 6.32227 11.1846 7.64714L6.58694 27.64C6.55896 27.762 6.54344 27.8815 6.53418 27.9986V28.3695C6.62418 29.3246 7.36619 30.0201 8.43278 30.036H17.406C16.0814 30.0163 15.2562 28.9502 15.5602 27.64Z",fill:"#DD0228"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M23.6716 19.8205H23.8404C23.9955 19.8205 24.0999 19.7693 24.1488 19.668L24.5874 19.0227H25.762L25.5171 19.4472H26.9254L26.7467 20.0975H25.0709C24.8779 20.3829 24.6403 20.5171 24.3547 20.5012H23.4818L23.6716 19.8205H23.6716ZM23.4788 20.7527H26.5643L26.3676 21.4591H25.1268L24.9374 22.1409H26.1449L25.9482 22.8473H24.7407L24.4602 23.8548C24.3908 24.0232 24.4821 24.099 24.7327 24.0818H25.7168L25.5345 24.7382H23.6451C23.287 24.7382 23.1641 24.5368 23.2765 24.1331L23.6351 22.8473H22.8633L23.0593 22.1409H23.8313L24.0205 21.4591H23.2827L23.4788 20.7527H23.4788ZM28.4035 19.018L28.355 19.4315C28.355 19.4315 28.937 19.002 29.4656 19.002H31.4189L30.6719 21.6601C30.61 21.964 30.3443 22.1151 29.8752 22.1151H27.6612L27.1426 23.9817C27.1128 24.0817 27.155 24.133 27.2667 24.133H27.7023L27.5422 24.7124H26.4347C26.0096 24.7124 25.8328 24.5867 25.903 24.3343L27.3684 19.018H28.4035H28.4035ZM30.0576 19.7693H28.3141L28.1056 20.4866C28.1056 20.4866 28.3959 20.2805 28.8811 20.2731C29.365 20.2657 29.9173 20.2731 29.9173 20.2731L30.0576 19.7693ZM29.4261 21.4333C29.555 21.4504 29.6271 21.4003 29.6358 21.282L29.7425 20.9039H27.9964L27.85 21.4333H29.4261ZM28.2483 22.2921H29.2547L29.236 22.7203H29.504C29.6394 22.7203 29.7065 22.6776 29.7065 22.5935L29.7858 22.3166H30.6223L30.5106 22.7203C30.4161 23.057 30.1656 23.2327 29.7586 23.2499H29.2225L29.22 23.9817C29.2101 24.0989 29.318 24.1587 29.54 24.1587H30.0439L29.8813 24.7381H28.6727C28.3339 24.754 28.1678 24.5953 28.1713 24.2587L28.2483 22.2921V22.2921Z",fill:"white"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M16.0529 15.4764C15.9164 16.1339 15.6 16.639 15.1091 16.9976C14.6227 17.3502 13.9954 17.527 13.2273 17.527C12.5044 17.527 11.9745 17.3465 11.6364 16.9841C11.4018 16.7267 11.2852 16.3998 11.2852 16.0045C11.2852 15.8411 11.3051 15.6654 11.3448 15.4764L12.1631 11.5972H13.3991L12.5919 15.4325C12.5671 15.5386 12.5571 15.6374 12.5584 15.7265C12.5571 15.9229 12.6068 16.0839 12.7073 16.2095C12.8537 16.3962 13.0914 16.4889 13.4221 16.4889C13.8024 16.4889 14.1158 16.3974 14.359 16.2132C14.6022 16.0302 14.761 15.7704 14.8324 15.4325L15.6422 11.5972H16.8719L16.0529 15.4764Z",fill:"white"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M21.2436 13.9502H22.2116L21.4534 17.4123H20.4873L21.2436 13.9502ZM21.5482 12.689H22.5248L22.3424 13.5293H21.3659L21.5482 12.689Z",fill:"white"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M23.0688 17.1487C22.8156 16.9109 22.6878 16.59 22.6865 16.1826C22.6865 16.113 22.6908 16.0338 22.7002 15.9471C22.7095 15.8592 22.7214 15.7739 22.738 15.6946C22.8528 15.1323 23.0973 14.6858 23.4739 14.3564C23.8499 14.0258 24.3036 13.8599 24.8347 13.8599C25.2696 13.8599 25.6145 13.9794 25.8672 14.2185C26.1196 14.4589 26.2462 14.7833 26.2462 15.1957C26.2462 15.2664 26.2407 15.3481 26.2313 15.436C26.2201 15.525 26.2066 15.6104 26.1909 15.6946C26.0787 16.2484 25.8349 16.69 25.4583 17.0134C25.0816 17.3391 24.6293 17.5012 24.1019 17.5012C23.6651 17.5012 23.3213 17.3841 23.0688 17.1487M24.9136 16.4631C25.0843 16.2814 25.2065 16.0056 25.2809 15.6385C25.2921 15.5812 25.302 15.5214 25.3082 15.4616C25.3143 15.403 25.3168 15.3482 25.3168 15.2981C25.3168 15.0846 25.2616 14.9188 25.1506 14.8016C25.0402 14.6833 24.8832 14.6248 24.6804 14.6248C24.4122 14.6248 24.1939 14.7174 24.0227 14.9029C23.8501 15.0884 23.7279 15.3689 23.6509 15.7422C23.6404 15.7995 23.6317 15.8569 23.6237 15.913C23.6175 15.9703 23.6157 16.024 23.6168 16.0728C23.6168 16.285 23.6721 16.4485 23.7831 16.5644C23.8935 16.6803 24.0498 16.7376 24.2553 16.7376C24.5246 16.7376 24.743 16.6461 24.9136 16.4631Z",fill:"white"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M32.5262 19.8496L32.7596 19.0421H33.9397L33.8888 19.3385C33.8888 19.3385 34.4918 19.0421 34.9261 19.0421C35.3606 19.0421 36.3854 19.0421 36.3854 19.0421L36.1535 19.8496H35.9239L34.8231 23.6582H35.0527L34.8343 24.4146H34.6047L34.5092 24.7427H33.3664L33.4617 24.4146H31.207L31.4268 23.6582H31.6527L32.7544 19.8496H32.5262H32.5262ZM33.7993 19.8498L33.4989 20.8805C33.4989 20.8805 34.0128 20.6866 34.4558 20.6318C34.5536 20.2718 34.6815 19.8498 34.6815 19.8498H33.7993V19.8498ZM33.3598 21.3637L33.0585 22.4433C33.0585 22.4433 33.628 22.1676 34.0188 22.1444C34.1317 21.7271 34.2447 21.3637 34.2447 21.3637H33.3598V21.3637ZM33.5808 23.6583L33.8067 22.8751H32.9258L32.6987 23.6583H33.5808ZM36.4352 18.9922H37.5447L37.5918 19.3946C37.5844 19.4971 37.6463 19.546 37.7779 19.546H37.9739L37.7756 20.2279H36.9601C36.6487 20.2437 36.4886 20.1267 36.4738 19.8741L36.4352 18.9922ZM36.1102 20.4548H39.7039L39.493 21.1868H38.3488L38.1526 21.8673H39.2957L39.0835 22.5981H37.8104L37.5224 23.0264H38.1455L38.2894 23.8839C38.3066 23.9693 38.3836 24.0108 38.5151 24.0108H38.7086L38.5053 24.717H37.8202C37.4653 24.7342 37.2818 24.6171 37.2667 24.3646L37.1016 23.5814L36.5346 24.4146C36.4005 24.65 36.1945 24.7599 35.9167 24.7427H34.8705L35.074 24.0363H35.4004C35.5345 24.0363 35.646 23.9778 35.7465 23.8595L36.634 22.5981H35.4898L35.7018 21.8673H36.9428L37.1402 21.1868H35.898L36.1102 20.4548Z",fill:"white"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M17.1915 13.9492H18.0645L17.9647 14.4493L18.0899 14.3066C18.3729 14.009 18.7166 13.8613 19.1224 13.8613C19.4898 13.8613 19.7547 13.9663 19.921 14.1773C20.0847 14.3884 20.1294 14.6799 20.0519 15.0544L19.571 17.4137H18.6738L19.1081 15.2752C19.1529 15.0544 19.1405 14.8897 19.0715 14.7836C19.0033 14.6774 18.873 14.625 18.685 14.625C18.4542 14.625 18.26 14.6957 18.1017 14.8361C17.9429 14.9776 17.8381 15.174 17.7865 15.424L17.3863 17.4137H16.4873L17.1915 13.9492Z",fill:"white"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M27.2021 13.9492H28.0758L27.9767 14.4493L28.1006 14.3066C28.3837 14.009 28.7287 13.8613 29.1332 13.8613C29.5005 13.8613 29.766 13.9663 29.931 14.1773C30.0937 14.3884 30.1408 14.6799 30.0614 15.0544L29.5823 17.4137H28.6839L29.1184 15.2752C29.1629 15.0544 29.1506 14.8897 29.0823 14.7836C29.0115 14.6774 28.8836 14.625 28.6964 14.625C28.4655 14.625 28.272 14.6957 28.1119 14.8361C27.953 14.9776 27.8476 15.174 27.798 15.424L27.396 17.4137H26.498L27.2021 13.9492",fill:"white"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M31.5212 11.8018H34.0577C34.5454 11.8018 34.9225 11.9104 35.1818 12.1238C35.44 12.3398 35.5692 12.6497 35.5692 13.0534V13.0656C35.5692 13.1424 35.564 13.229 35.5567 13.3229C35.5441 13.4157 35.5279 13.5095 35.5071 13.6072C35.3954 14.1415 35.1359 14.571 34.7352 14.8967C34.333 15.2211 33.8567 15.3846 33.3082 15.3846H31.9479L31.5274 17.4133H30.3496L31.5212 11.8018M32.1554 14.4087H33.2835C33.5776 14.4087 33.8108 14.3415 33.9809 14.2086C34.1497 14.0744 34.2614 13.8695 34.3234 13.5914C34.3332 13.54 34.3394 13.4937 34.3469 13.451C34.3508 13.4108 34.3556 13.3704 34.3556 13.3315C34.3556 13.1326 34.2838 12.9887 34.1397 12.8984C33.9958 12.8068 33.7701 12.763 33.4572 12.763H32.4991L32.1554 14.4087",fill:"white"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M40.8406 18.0833C40.4683 18.8615 40.1135 19.3152 39.9051 19.5263C39.6964 19.735 39.2833 20.2205 38.2881 20.1839L38.3737 19.5898C39.2112 19.3361 39.6642 18.1929 39.9223 17.6867L39.6146 13.9587L40.2624 13.9502H40.8059L40.8643 16.2888L41.8829 13.9502H42.9143L40.8406 18.0833Z",fill:"white"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M37.9561 14.232L37.5464 14.509C37.1183 14.1796 36.7274 13.9759 35.9731 14.3199C34.9454 14.7883 34.0868 18.381 36.9161 17.1976L37.0774 17.3855L38.1905 17.4135L38.9215 14.1491L37.9561 14.232M37.3233 16.0168C37.1445 16.5353 36.7451 16.8781 36.4324 16.7805C36.1196 16.6853 36.008 16.1851 36.1891 15.6655C36.3678 15.1458 36.7698 14.8042 37.08 14.9018C37.3927 14.997 37.5056 15.4971 37.3233 16.0168Z",fill:"white"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M34.3328 5.26107L30.4463 5.25342L34.3106 5.26981C34.318 5.26981 34.3253 5.26107 34.3328 5.26107",fill:"#E02F41"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M30.4467 5.27406L23.5378 5.25C23.5204 5.25 23.5024 5.25765 23.4844 5.26531L30.4467 5.27406",fill:"#2E4F7D"})),v=()=>(0,a.createElement)("svg",{width:"51",height:"35",viewBox:"0 0 51 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("rect",{x:"0.5",y:"0.5",width:"50",height:"34",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),(0,a.createElement)("path",{d:"M22.6435 24.004H19.248L21.3718 11.7534H24.7671L22.6435 24.004Z",fill:"#15195A"}),(0,a.createElement)("path",{d:"M34.952 12.0528C34.2823 11.8049 33.22 11.5312 31.9066 11.5312C28.5534 11.5312 26.1922 13.1993 26.1777 15.5842C26.1499 17.3437 27.8683 18.321 29.1536 18.9077C30.4672 19.5072 30.9138 19.8985 30.9138 20.4329C30.9004 21.2536 29.8522 21.6319 28.8747 21.6319C27.5191 21.6319 26.7927 21.4369 25.6889 20.9803L25.2417 20.7845L24.7666 23.5345C25.563 23.873 27.0302 24.1733 28.5534 24.1865C32.1162 24.1865 34.4356 22.5442 34.4631 20.0028C34.4767 18.6082 33.5693 17.5396 31.613 16.6665C30.4254 16.1059 29.6981 15.728 29.6981 15.1544C29.7121 14.6331 30.3133 14.099 31.6539 14.099C32.7577 14.0729 33.5687 14.3204 34.1831 14.5681L34.4902 14.6982L34.952 12.0528Z",fill:"#15195A"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M41.0301 11.7534H43.6565L46.3957 24.0039H43.2519C43.2519 24.0039 42.9442 22.5963 42.8467 22.1662H38.4873C38.3612 22.4919 37.7747 24.0039 37.7747 24.0039H34.2119L39.2554 12.7699C39.6049 11.9748 40.2202 11.7534 41.0301 11.7534ZM40.8208 16.2365C40.8208 16.2365 39.7448 18.9603 39.4652 19.6641H42.2875C42.1478 19.0516 41.5048 16.1192 41.5048 16.1192L41.2676 15.0636C41.1676 15.3355 41.0231 15.7092 40.9256 15.9612C40.8596 16.1321 40.8151 16.2471 40.8208 16.2365Z",fill:"#15195A"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M4.53636 11.7534H9.99929C10.7398 11.7792 11.3406 12.0008 11.5361 12.7832L12.7233 18.4113C12.7234 18.4118 12.7236 18.4124 12.7238 18.4129L13.0871 20.1072L16.4124 11.7534H20.0028L14.6657 23.991H11.0752L8.04881 13.3464C7.00461 12.7769 5.81289 12.3188 4.48047 12.0009L4.53636 11.7534Z",fill:"#15195A"})),E=()=>(0,a.createElement)("svg",{width:"64",height:"40",viewBox:"0 0 64 40",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("rect",{x:"0.5",y:"0.5",width:"63",height:"39",rx:"3.5",fill:"#7F54B3"}),(0,a.createElement)("g",{clipPath:"url(#clip0_1424_5139)"},(0,a.createElement)("path",{d:"M38.0384 16.1863H40.6738C41.604 16.1863 42.3496 16.4077 42.9106 16.8507C43.4716 17.2936 43.7521 17.8768 43.7521 18.6002C43.7521 19.3606 43.4642 19.9585 42.8958 20.3867C42.3274 20.8148 41.5375 21.0363 40.5262 21.0363H40.1571V24.0112H38.0237V16.1863H38.0384ZM40.1645 17.6848V19.5303C40.6443 19.523 40.9986 19.4491 41.2201 19.3015C41.449 19.1539 41.5597 18.925 41.5597 18.615C41.5597 18.2754 41.449 18.0392 41.2275 17.9063C41.0134 17.766 40.6591 17.6922 40.1645 17.6848Z",fill:"white"}),(0,a.createElement)("path",{d:"M50.3739 24.0039H48.1593L47.7975 22.6972H45.546L45.1769 24.0039H43.0066L45.4943 16.1789H47.8861L50.3739 24.0039ZM47.3915 21.2947C47.0741 20.1283 46.8305 19.021 46.6755 17.9727H46.6459C46.4762 19.0579 46.2399 20.1652 45.9373 21.2947H47.3915Z",fill:"white"}),(0,a.createElement)("path",{d:"M49.0967 16.1863H51.4885C52.02 17.3083 52.4038 18.1794 52.6401 18.7848H52.6696C52.9058 18.1573 53.2897 17.2936 53.8064 16.1863H56.2499L53.7326 20.4679V24.0039H51.577V20.4679L49.0967 16.1863Z",fill:"white"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M33.0704 13.071H10.3559C8.91636 13.071 7.75 14.2374 7.75 15.6695V24.336C7.75 25.7682 8.91636 26.9345 10.3485 26.9345H21.1115L26.0353 29.6733L24.9206 26.9345H33.0704C34.5025 26.9345 35.6689 25.7682 35.6689 24.336V15.6695C35.6689 14.2374 34.5025 13.071 33.0704 13.071ZM9.87596 15.0716C9.59544 15.079 9.33707 15.2118 9.1599 15.4259C8.99012 15.6474 8.92368 15.9279 8.98274 16.2084C9.6545 20.49 10.282 23.3764 10.8652 24.8675C11.0792 25.4138 11.345 25.6722 11.655 25.65C12.1275 25.6131 12.7033 24.9561 13.3751 23.6643C13.5241 23.3661 13.7058 23.0039 13.9191 22.5789L13.9199 22.5772L13.9204 22.5763L13.9207 22.5757L13.9208 22.5755C14.2247 21.9699 14.5925 21.2368 15.0212 20.3793C15.6413 22.5274 16.4829 24.1441 17.5459 25.2293C17.8486 25.5319 18.1439 25.6722 18.4465 25.65C18.7049 25.6353 18.9411 25.4729 19.0518 25.2366C19.1626 25.0078 19.2142 24.7494 19.1847 24.4911C19.1109 23.4502 19.2216 21.9959 19.5021 20.143C19.79 18.2237 20.1591 16.8506 20.6021 16.0239C20.6907 15.8615 20.7276 15.6695 20.7128 15.4776C20.698 15.2192 20.5725 14.9756 20.3658 14.8206C20.1665 14.6508 19.9082 14.5696 19.6498 14.5917C19.325 14.6065 19.0371 14.7984 18.8968 15.0937C18.1955 16.3856 17.6935 18.4821 17.3909 21.3758C16.911 20.1135 16.5493 18.8069 16.3131 17.4781C16.1876 16.799 15.8776 16.4815 15.3756 16.5185C15.0286 16.5406 14.7481 16.7694 14.5193 17.1976L12.0168 21.9516C11.6107 20.2907 11.2269 18.268 10.8725 15.891C10.784 15.3078 10.4518 15.0347 9.87596 15.0716ZM26.53 17.2197C26.1461 16.5332 25.4817 16.046 24.7066 15.8984C24.4999 15.8541 24.2932 15.8319 24.0865 15.8319C22.994 15.8319 22.1008 16.4003 21.4068 17.5372C20.8163 18.5042 20.5062 19.6189 20.521 20.7484C20.521 21.6268 20.7056 22.3798 21.0673 23.0073C21.4511 23.6938 22.1155 24.181 22.8906 24.3287C23.0973 24.3729 23.304 24.3951 23.5107 24.3951C24.6106 24.3951 25.5039 23.8267 26.1904 22.6898C26.781 21.7154 27.091 20.6007 27.0763 19.4565C27.0763 18.578 26.8917 17.8325 26.53 17.2197ZM25.0905 20.3866C24.9355 21.1322 24.6476 21.6933 24.2194 22.0771C23.8872 22.3798 23.5772 22.4979 23.2966 22.4462C23.0161 22.3946 22.7947 22.1436 22.6249 21.708C22.4994 21.3758 22.4256 21.0289 22.4256 20.6672C22.4256 20.3866 22.4551 20.1061 22.5068 19.833C22.6175 19.3458 22.8168 18.8881 23.1121 18.4747C23.4886 17.921 23.8872 17.6848 24.3006 17.7734C24.5811 17.8325 24.8026 18.0761 24.9724 18.5116C25.0979 18.8438 25.1717 19.1908 25.1717 19.5451C25.1717 19.8256 25.1495 20.1061 25.0905 20.3866ZM31.8524 15.8984C32.6275 16.046 33.2919 16.5332 33.6757 17.2197C34.0374 17.8325 34.222 18.578 34.222 19.4565C34.2367 20.6007 33.9267 21.7154 33.3361 22.6898C32.6496 23.8267 31.7564 24.3951 30.6565 24.3951C30.4498 24.3951 30.2431 24.3729 30.0364 24.3287C29.2613 24.181 28.5969 23.6938 28.213 23.0073C27.8513 22.3798 27.6667 21.6268 27.6667 20.7484C27.652 19.6189 27.962 18.5042 28.5526 17.5372C29.2465 16.4003 30.1397 15.8319 31.2323 15.8319C31.439 15.8319 31.6457 15.8541 31.8524 15.8984ZM31.3651 22.0771C31.7933 21.6933 32.0812 21.1322 32.2362 20.3866C32.2953 20.1061 32.3174 19.8256 32.3174 19.5451C32.3174 19.1908 32.2436 18.8438 32.1181 18.5116C31.9483 18.0761 31.7269 17.8325 31.4463 17.7734C31.0329 17.6848 30.6343 17.921 30.2578 18.4747C29.9626 18.8881 29.7632 19.3458 29.6525 19.833C29.6008 20.1061 29.5713 20.3866 29.5713 20.6672C29.5713 21.0289 29.6451 21.3758 29.7706 21.708C29.9404 22.1436 30.1619 22.3946 30.4424 22.4462C30.7229 22.4979 31.0329 22.3798 31.3651 22.0771Z",fill:"white"})),(0,a.createElement)("defs",null,(0,a.createElement)("clipPath",{id:"clip0_1424_5139"},(0,a.createElement)("rect",{width:"48.5",height:"30.9953",fill:"white",transform:"translate(7.75 4.50232)"})))),L=()=>(0,a.createElement)("svg",{width:"294",height:"275",viewBox:"0 0 294 275",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("g",{clipPath:"url(#clip0_255_63852)"},(0,a.createElement)("path",{d:"M0.5 10C0.5 4.47716 4.97715 0 10.5 0H725.5C731.023 0 735.5 4.47715 735.5 10V332H0.5V10Z",fill:"#F6F7F7"}),(0,a.createElement)("circle",{cx:"17.5",cy:"14",r:"5",fill:"#D94F4F"}),(0,a.createElement)("circle",{cx:"35.5",cy:"14",r:"5",fill:"#F0B849"}),(0,a.createElement)("circle",{cx:"53.5",cy:"14",r:"5",fill:"#4AB866"}),(0,a.createElement)("rect",{x:"3.5",y:"28",width:"287",height:"304",fill:"white"}),(0,a.createElement)("line",{x1:"27.5",y1:"95.5",x2:"270.5",y2:"95.5",stroke:"#DCDCDE"}),(0,a.createElement)("line",{x1:"49.5",y1:"157.5",x2:"253.5",y2:"157.5",stroke:"#DCDCDE"}),(0,a.createElement)("line",{x1:"49.5",y1:"198.5",x2:"253.5",y2:"198.5",stroke:"#DCDCDE"}),(0,a.createElement)("rect",{x:"28",y:"116.5",width:"242",height:"127",rx:"9.5",stroke:"#DCDCDE"}),(0,a.createElement)("path",{d:"M50.3984 135.954V143H52.8154C54.9102 143 56.1406 141.696 56.1406 139.475C56.1406 137.258 54.9053 135.954 52.8154 135.954H50.3984ZM51.2773 136.745H52.7568C54.3193 136.745 55.2422 137.761 55.2422 139.484C55.2422 141.203 54.3291 142.209 52.7568 142.209H51.2773V136.745ZM61.043 141.638C60.8232 142.102 60.3643 142.351 59.6855 142.351C58.792 142.351 58.2109 141.691 58.167 140.651V140.612H61.9512V140.29C61.9512 138.654 61.0869 137.644 59.666 137.644C58.2207 137.644 57.293 138.718 57.293 140.373C57.293 142.038 58.2061 143.093 59.666 143.093C60.8184 143.093 61.6387 142.536 61.8828 141.638H61.043ZM59.6562 138.386C60.4912 138.386 61.0479 139.001 61.0674 139.934H58.167C58.2305 139.001 58.8164 138.386 59.6562 138.386ZM65.833 137.644C65.1152 137.644 64.4854 138.01 64.1484 138.615H64.0703V137.736H63.2695V144.758H64.1094V142.209H64.1875C64.4756 142.766 65.0811 143.093 65.833 143.093C67.1709 143.093 68.0449 142.014 68.0449 140.368C68.0449 138.713 67.1758 137.644 65.833 137.644ZM65.6328 142.336C64.6855 142.336 64.085 141.574 64.085 140.368C64.085 139.157 64.6855 138.4 65.6377 138.4C66.5996 138.4 67.1758 139.138 67.1758 140.368C67.1758 141.599 66.5996 142.336 65.6328 142.336ZM71.5264 143.093C73.0254 143.093 73.9531 142.058 73.9531 140.368C73.9531 138.674 73.0254 137.644 71.5264 137.644C70.0273 137.644 69.0996 138.674 69.0996 140.368C69.0996 142.058 70.0273 143.093 71.5264 143.093ZM71.5264 142.336C70.5303 142.336 69.9688 141.613 69.9688 140.368C69.9688 139.118 70.5303 138.4 71.5264 138.4C72.5225 138.4 73.084 139.118 73.084 140.368C73.084 141.613 72.5225 142.336 71.5264 142.336ZM75.1494 139.177C75.1494 139.938 75.5986 140.363 76.585 140.603L77.4883 140.822C78.0498 140.959 78.3232 141.203 78.3232 141.564C78.3232 142.048 77.8154 142.385 77.1074 142.385C76.4336 142.385 76.0137 142.102 75.8721 141.657H75.0078C75.1006 142.531 75.9062 143.093 77.0781 143.093C78.2744 143.093 79.1875 142.443 79.1875 141.501C79.1875 140.744 78.709 140.314 77.7178 140.075L76.9072 139.88C76.2871 139.729 75.9941 139.504 75.9941 139.143C75.9941 138.674 76.4824 138.356 77.1074 138.356C77.7422 138.356 78.1523 138.635 78.2646 139.055H79.0947C78.9824 138.19 78.2158 137.644 77.1123 137.644C75.9941 137.644 75.1494 138.303 75.1494 139.177ZM80.9453 136.721C81.2676 136.721 81.5312 136.457 81.5312 136.135C81.5312 135.812 81.2676 135.549 80.9453 135.549C80.623 135.549 80.3594 135.812 80.3594 136.135C80.3594 136.457 80.623 136.721 80.9453 136.721ZM80.5254 143H81.3652V137.736H80.5254V143ZM83.3184 136.374V137.736H82.4688V138.439H83.3184V141.633C83.3184 142.639 83.7529 143.039 84.8369 143.039C85.0029 143.039 85.1641 143.02 85.3301 142.99V142.282C85.1738 142.297 85.0908 142.302 84.9395 142.302C84.3926 142.302 84.1582 142.038 84.1582 141.418V138.439H85.3301V137.736H84.1582V136.374H83.3184Z",fill:"#2C3338"}),(0,a.createElement)("path",{d:"M126.459 143.708H127.045V142.863C128.197 142.775 129.154 142.136 129.154 140.959C129.154 139.934 128.451 139.416 127.196 139.113L127.045 139.079V136.857C127.704 136.94 128.173 137.326 128.236 137.985H129.086C129.052 136.906 128.173 136.193 127.045 136.091V135.246H126.459V136.096C125.302 136.198 124.477 136.892 124.477 137.941C124.477 138.903 125.131 139.445 126.347 139.738L126.459 139.768V142.097C125.614 142.004 125.238 141.54 125.15 140.93H124.301C124.335 142.067 125.297 142.756 126.459 142.858V143.708ZM125.326 137.883C125.326 137.409 125.717 136.96 126.459 136.862V138.938C125.619 138.732 125.326 138.361 125.326 137.883ZM127.074 139.914C128.041 140.148 128.305 140.49 128.305 141.027C128.305 141.589 127.953 142.033 127.045 142.102V139.909L127.074 139.914ZM132.25 139.753H133.124C134.115 139.753 134.726 140.261 134.726 141.076C134.726 141.867 134.066 142.399 133.139 142.399C132.23 142.399 131.596 141.926 131.518 141.184H130.668C130.731 142.38 131.718 143.166 133.148 143.166C134.55 143.166 135.629 142.282 135.629 141.101C135.629 140.119 135.023 139.499 134.11 139.353V139.274C134.833 139.064 135.36 138.503 135.365 137.653C135.37 136.667 134.569 135.788 133.178 135.788C131.752 135.788 130.878 136.633 130.79 137.78H131.645C131.718 136.989 132.27 136.555 133.119 136.555C133.969 136.555 134.472 137.092 134.472 137.751C134.472 138.483 133.896 139.001 133.085 139.001H132.25V139.753ZM139.384 143.166C140.824 143.166 141.806 142.17 141.806 140.72C141.806 139.323 140.829 138.396 139.647 138.396C138.944 138.396 138.422 138.61 138.08 139.035H138.002L138.222 136.74H141.444V135.954H137.533L137.128 139.982H137.953C138.231 139.46 138.759 139.152 139.428 139.152C140.316 139.152 140.927 139.792 140.927 140.744C140.927 141.73 140.316 142.395 139.394 142.395C138.563 142.395 137.958 141.892 137.88 141.14H137.016C137.094 142.346 138.051 143.166 139.384 143.166ZM143.188 137.941H144.037C144.037 137.106 144.599 136.555 145.443 136.555C146.273 136.555 146.776 137.111 146.776 137.858C146.776 138.479 146.513 138.845 145.614 139.816L143.231 142.395V143H147.792V142.209H144.452V142.131L146.054 140.476C147.323 139.162 147.675 138.576 147.675 137.766C147.675 136.623 146.732 135.788 145.502 135.788C144.154 135.788 143.188 136.687 143.188 137.941ZM149.828 143.049C150.18 143.049 150.463 142.761 150.463 142.414C150.463 142.062 150.18 141.779 149.828 141.779C149.481 141.779 149.193 142.062 149.193 142.414C149.193 142.761 149.481 143.049 149.828 143.049ZM153.959 143.161C155.683 143.161 156.669 141.774 156.669 139.353C156.669 136.955 155.536 135.788 154.018 135.788C152.572 135.788 151.547 136.799 151.547 138.215C151.547 139.582 152.519 140.573 153.861 140.573C154.701 140.573 155.39 140.173 155.736 139.489H155.814C155.766 141.364 155.116 142.38 153.969 142.38C153.275 142.38 152.738 141.994 152.553 141.359H151.649C151.869 142.468 152.763 143.161 153.959 143.161ZM154.018 139.812C153.085 139.812 152.436 139.147 152.436 138.186C152.436 137.263 153.124 136.56 154.027 136.56C154.931 136.56 155.619 137.272 155.619 138.215C155.619 139.147 154.95 139.812 154.018 139.812ZM160.336 143.161C162.06 143.161 163.046 141.774 163.046 139.353C163.046 136.955 161.913 135.788 160.395 135.788C158.949 135.788 157.924 136.799 157.924 138.215C157.924 139.582 158.896 140.573 160.238 140.573C161.078 140.573 161.767 140.173 162.113 139.489H162.191C162.143 141.364 161.493 142.38 160.346 142.38C159.652 142.38 159.115 141.994 158.93 141.359H158.026C158.246 142.468 159.14 143.161 160.336 143.161ZM160.395 139.812C159.462 139.812 158.812 139.147 158.812 138.186C158.812 137.263 159.501 136.56 160.404 136.56C161.308 136.56 161.996 137.272 161.996 138.215C161.996 139.147 161.327 139.812 160.395 139.812Z",fill:"#2C3338"}),(0,a.createElement)("rect",{x:"196",y:"132",width:"43",height:"14",rx:"7",fill:"#B8E6BF"}),(0,a.createElement)("path",{d:"M209.719 136.363V142H210.422V139.988H211.84C212.898 139.988 213.652 139.242 213.652 138.188C213.652 137.113 212.914 136.363 211.848 136.363H209.719ZM210.422 136.988H211.664C212.48 136.988 212.93 137.414 212.93 138.188C212.93 138.934 212.465 139.363 211.664 139.363H210.422V136.988ZM215.734 142.074C216.297 142.074 216.758 141.828 217.031 141.379H217.094V142H217.734V139.117C217.734 138.242 217.16 137.715 216.133 137.715C215.234 137.715 214.57 138.16 214.48 138.836H215.16C215.254 138.504 215.605 138.312 216.109 138.312C216.738 138.312 217.062 138.598 217.062 139.117V139.5L215.848 139.574C214.867 139.633 214.312 140.066 214.312 140.82C214.312 141.59 214.918 142.074 215.734 142.074ZM215.859 141.484C215.371 141.484 215.008 141.234 215.008 140.805C215.008 140.383 215.289 140.16 215.93 140.117L217.062 140.043V140.43C217.062 141.031 216.551 141.484 215.859 141.484ZM219.328 136.977C219.586 136.977 219.797 136.766 219.797 136.508C219.797 136.25 219.586 136.039 219.328 136.039C219.07 136.039 218.859 136.25 218.859 136.508C218.859 136.766 219.07 136.977 219.328 136.977ZM218.992 142H219.664V137.789H218.992V142ZM222.504 142.074C223.086 142.074 223.586 141.797 223.852 141.328H223.914V142H224.555V136.117H223.883V138.453H223.824C223.586 137.992 223.09 137.715 222.504 137.715C221.434 137.715 220.734 138.574 220.734 139.895C220.734 141.219 221.426 142.074 222.504 142.074ZM222.66 138.32C223.422 138.32 223.898 138.93 223.898 139.895C223.898 140.867 223.426 141.469 222.66 141.469C221.891 141.469 221.43 140.879 221.43 139.895C221.43 138.914 221.895 138.32 222.66 138.32Z",fill:"#00450C"}),(0,a.createElement)("rect",{x:"192.5",y:"173",width:"50",height:"10",rx:"5",fill:"#B8E6BF"}),(0,a.createElement)("rect",{x:"49.5",y:"174",width:"50",height:"8",rx:"2",fill:"#DCDCDE"}),(0,a.createElement)("rect",{x:"132.5",y:"170",width:"22.7796",height:"16",rx:"4",fill:"#F6F7F7"}),(0,a.createElement)("path",{d:"M143.507 182.856C144.227 182.856 144.869 182.747 145.435 182.528C146.004 182.305 146.451 181.981 146.774 181.558C147.103 181.129 147.267 180.612 147.267 180.006V179.992C147.267 179.24 147.025 178.659 146.542 178.249C146.063 177.839 145.357 177.529 144.423 177.319L143.404 177.094C142.853 176.966 142.454 176.809 142.208 176.622C141.962 176.435 141.839 176.201 141.839 175.918V175.904C141.839 175.695 141.903 175.508 142.03 175.344C142.158 175.18 142.347 175.05 142.598 174.954C142.848 174.858 143.158 174.811 143.527 174.811C143.855 174.811 144.147 174.861 144.402 174.961C144.658 175.061 144.865 175.209 145.024 175.405C145.184 175.597 145.28 175.836 145.312 176.123L145.318 176.137L147.13 176.13L147.137 176.123C147.114 175.544 146.948 175.043 146.638 174.619C146.332 174.191 145.911 173.86 145.373 173.628C144.84 173.396 144.215 173.279 143.5 173.279C142.816 173.279 142.206 173.391 141.668 173.614C141.13 173.833 140.706 174.15 140.396 174.564C140.087 174.979 139.932 175.476 139.932 176.055V176.068C139.932 176.802 140.169 177.381 140.643 177.805C141.117 178.229 141.8 178.541 142.693 178.741L143.712 178.974C144.332 179.11 144.76 179.27 144.997 179.452C145.239 179.63 145.359 179.878 145.359 180.197V180.211C145.359 180.439 145.291 180.637 145.154 180.806C145.022 180.974 144.822 181.104 144.553 181.195C144.284 181.282 143.944 181.325 143.534 181.325C143.142 181.325 142.812 181.275 142.543 181.175C142.274 181.075 142.062 180.931 141.907 180.744C141.757 180.553 141.659 180.327 141.613 180.067V180.061H139.788L139.781 180.067C139.804 180.664 139.977 181.17 140.301 181.585C140.629 182 141.069 182.316 141.62 182.535C142.176 182.749 142.805 182.856 143.507 182.856ZM143.151 183.971H143.938V172.145H143.151V183.971Z",fill:"#C3C4C7"}),(0,a.createElement)("rect",{x:"192.5",y:"214",width:"50",height:"10",rx:"5",fill:"#B8E6BF"}),(0,a.createElement)("rect",{x:"49.5",y:"215",width:"50",height:"8",rx:"2",fill:"#DCDCDE"}),(0,a.createElement)("rect",{x:"132.5",y:"211",width:"22.7796",height:"16",rx:"4",fill:"#F6F7F7"}),(0,a.createElement)("path",{d:"M145.879 223.856C146.143 223.856 146.403 223.847 146.658 223.829C146.913 223.806 147.13 223.774 147.308 223.733V222.072C147.139 222.104 146.927 222.129 146.672 222.147C146.417 222.161 146.177 222.168 145.954 222.168C145.444 222.168 144.983 222.106 144.573 221.983C144.168 221.856 143.819 221.664 143.527 221.409C143.24 221.149 143.019 220.826 142.864 220.438C142.709 220.051 142.632 219.595 142.632 219.071V219.064C142.632 218.545 142.707 218.091 142.857 217.704C143.012 217.317 143.233 216.995 143.521 216.74C143.812 216.48 144.159 216.287 144.56 216.159C144.961 216.027 145.41 215.961 145.906 215.961C146.148 215.961 146.392 215.972 146.638 215.995C146.884 216.018 147.098 216.048 147.28 216.084V214.437C147.103 214.386 146.879 214.348 146.61 214.32C146.341 214.293 146.082 214.279 145.831 214.279C145.065 214.279 144.364 214.38 143.726 214.58C143.092 214.781 142.543 215.081 142.078 215.482C141.613 215.883 141.253 216.382 140.998 216.979C140.747 217.576 140.622 218.269 140.622 219.058V219.064C140.622 219.857 140.747 220.555 140.998 221.156C141.253 221.753 141.613 222.252 142.078 222.653C142.548 223.05 143.104 223.351 143.746 223.556C144.389 223.756 145.1 223.856 145.879 223.856ZM139.46 220.425H146.542V219.639H139.46V220.425ZM139.46 218.531H146.542V217.745H139.46V218.531Z",fill:"#C3C4C7"}),(0,a.createElement)("path",{fill:"#7F54B3",fillRule:"evenodd",d:"M33.24 50.42H62.2a3.32 3.32 0 0 1 3.31 3.31v11.05a3.32 3.32 0 0 1-3.3 3.3H51.8l1.43 3.5-6.28-3.5H33.23a3.32 3.32 0 0 1-3.3-3.3V53.73c0-1.82 1.48-3.3 3.31-3.3Z",clipRule:"evenodd"}),(0,a.createElement)("path",{fill:"#fff",d:"M31.72 53.42c.23-.27.55-.44.91-.45.74-.05 1.16.3 1.27 1.04.45 3.03.94 5.61 1.46 7.73l3.19-6.06c.3-.55.65-.84 1.1-.87.63-.04 1.03.36 1.19 1.23.3 1.7.76 3.36 1.37 4.96.39-3.68 1.02-6.36 1.92-8 .18-.38.54-.62.96-.64.33-.03.66.07.91.29.26.2.42.5.44.84.02.24-.03.49-.14.7-.56 1.05-1.03 2.8-1.4 5.24-.36 2.36-.5 4.22-.4 5.54.03.33-.03.66-.17.95-.14.3-.45.51-.78.53-.38.03-.76-.15-1.14-.53-1.36-1.39-2.43-3.45-3.22-6.19l-2.1 4.2c-.85 1.64-1.59 2.47-2.2 2.52-.39.03-.72-.3-1-1-.74-1.9-1.54-5.57-2.4-11.03-.07-.35.01-.72.23-1ZM62.96 55.7a3.4 3.4 0 0 0-2.32-1.68 3.77 3.77 0 0 0-.8-.08c-1.38 0-2.52.72-3.4 2.17a7.67 7.67 0 0 0-1.13 4.1c0 1.11.23 2.07.7 2.87a3.4 3.4 0 0 0 2.32 1.69c.26.05.52.08.79.08 1.4 0 2.54-.72 3.41-2.17a7.74 7.74 0 0 0 1.13-4.12c0-1.11-.24-2.06-.7-2.85Zm-1.83 4.04c-.2.95-.57 1.67-1.11 2.16-.42.38-.82.53-1.18.47-.35-.08-.64-.39-.85-.94a3.74 3.74 0 0 1-.26-1.33 4.78 4.78 0 0 1 .87-2.8c.49-.7 1-1 1.52-.89.36.08.64.39.86.94.16.43.25.87.25 1.32 0 .36-.03.72-.1 1.07ZM53.86 55.7a3.4 3.4 0 0 0-2.33-1.68 3.76 3.76 0 0 0-.79-.08c-1.39 0-2.53.72-3.41 2.17a7.67 7.67 0 0 0-1.13 4.1c0 1.11.23 2.07.7 2.87a3.4 3.4 0 0 0 2.32 1.69c.26.05.52.08.79.08 1.4 0 2.54-.72 3.41-2.17a7.84 7.84 0 0 0 1.14-4.12c0-1.12-.23-2.07-.7-2.86Zm-1.84 4.04a3.8 3.8 0 0 1-1.1 2.16c-.43.38-.83.53-1.18.47-.36-.08-.64-.39-.86-.94a3.74 3.74 0 0 1-.25-1.33c0-.36.03-.71.1-1.06.14-.62.4-1.2.77-1.73.48-.7.99-1 1.51-.9.36.08.64.39.86.94.16.43.26.87.26 1.32 0 .36-.04.72-.1 1.07Z"}),(0,a.createElement)("path",{fill:"#000",d:"M68.52 54.4h3.36c1.19 0 2.14.27 2.85.84a2.7 2.7 0 0 1 1.07 2.23 2.7 2.7 0 0 1-1.09 2.27c-.72.55-1.73.83-3.02.83h-.47v3.8H68.5v-9.98h.02Zm2.71 1.9v2.35c.61 0 1.07-.1 1.35-.29.29-.19.43-.48.43-.87 0-.44-.14-.74-.42-.9a2.64 2.64 0 0 0-1.36-.29ZM84.24 64.36h-2.82l-.46-1.66h-2.87l-.47 1.66h-2.77l3.17-9.97h3.05l3.17 9.97Zm-3.8-3.46c-.4-1.48-.71-2.9-.91-4.23h-.04c-.21 1.38-.52 2.8-.9 4.23h1.85ZM82.62 54.4h3.04a55.3 55.3 0 0 1 1.47 3.3h.04c.3-.8.79-1.9 1.45-3.3h3.11l-3.2 5.45v4.5h-2.75v-4.5l-3.16-5.46ZM92.58 54.4h3.43c.5 2.15.88 3.99 1.16 5.54l.3-1.54c.11-.6.24-1.19.38-1.76l.53-2.24h3.5l1.36 9.97h-2.7l-.72-6.86-1.67 6.86H96.3l-1.75-6.86-.7 6.86h-2.57l1.29-9.98ZM104.06 54.4h5.5v2.1h-2.78v1.84h2.4v2.07h-2.4v1.82h2.79v2.12h-5.51V54.4ZM110.58 54.4h2.73l3.46 5.74-.1-1.07v-4.68h2.72v9.97h-2.78l-3.38-5.66.07 1.24v4.42h-2.71V54.4h-.01ZM120.1 54.4h6.49v2.27h-1.87v7.7h-2.75v-7.7h-1.85v-2.28h-.01ZM127.7 64.18v-2.33c.86.42 1.58.63 2.17.63.34 0 .61-.07.81-.23.2-.16.3-.37.3-.62 0-.2-.08-.39-.24-.57a4.05 4.05 0 0 0-.82-.63 9 9 0 0 1-2.04-1.66 2.65 2.65 0 0 1-.63-1.73c0-.86.33-1.56.98-2.1a3.8 3.8 0 0 1 2.53-.82c.76 0 1.52.14 2.27.41v2.23a5.08 5.08 0 0 0-1.96-.55c-.34 0-.6.06-.8.2-.19.14-.28.33-.28.57 0 .41.45.88 1.35 1.4.54.32 1.08.75 1.64 1.32a2.68 2.68 0 0 1-.26 4.08c-.72.56-1.63.85-2.74.85-.77-.01-1.53-.16-2.28-.45Z"})),(0,a.createElement)("defs",null,(0,a.createElement)("clipPath",{id:"clip0_255_63852"},(0,a.createElement)("path",{d:"M0.5 0H283.5C289.023 0 293.5 4.47715 293.5 10V275H0.5V0Z",fill:"white"})),(0,a.createElement)("clipPath",{id:"clip1_255_63852"},(0,a.createElement)("rect",{width:"120",height:"39.7959",fill:"white",transform:"translate(27.5 45)"})))),b=()=>(0,a.createElement)("svg",{width:"310",height:"282",viewBox:"0 0 310 282",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("g",{filter:"url(#filter0_d_5851_4204)"},(0,a.createElement)("path",{d:"M16 30C16 24.4772 20.4772 20 26 20H309V48H16V30Z",fill:"#F2EDFF"}),(0,a.createElement)("path",{d:"M16 48H309V260H26C20.4771 260 16 255.523 16 250V48Z",fill:"white"})),(0,a.createElement)("path",{d:"M43.5 119C43.5 113.753 47.7533 109.5 53 109.5H309.5V236.5H53C47.7533 236.5 43.5 232.247 43.5 227V119Z",fill:"white",stroke:"#DCDCDE"}),(0,a.createElement)("path",{d:"M65.8545 134V126.954H68.6279C70.7227 126.954 71.9336 128.219 71.9336 130.426V130.436C71.9336 132.711 70.7373 134 68.6279 134H65.8545ZM67.3291 132.784H68.3936C69.6973 132.784 70.4297 131.964 70.4297 130.46V130.45C70.4297 129 69.6729 128.17 68.3936 128.17H67.3291V132.784ZM75.5566 134.112C73.9209 134.112 72.9297 133.043 72.9297 131.319V131.314C72.9297 129.605 73.9307 128.507 75.498 128.507C77.0654 128.507 78.042 129.581 78.042 131.207V131.651H74.3408C74.3555 132.535 74.8291 133.048 75.5859 133.048C76.2109 133.048 76.5576 132.716 76.6553 132.511L76.6699 132.481H77.998L77.9883 132.53C77.8223 133.204 77.1289 134.112 75.5566 134.112ZM75.5225 129.576C74.9023 129.576 74.4434 129.996 74.3555 130.758H76.6748C76.5869 129.977 76.1426 129.576 75.5225 129.576ZM79.209 135.782V128.624H80.6348V129.454H80.7227C81.001 128.878 81.5723 128.536 82.334 128.536C83.6768 128.536 84.5166 129.591 84.5166 131.305V131.314C84.5166 133.043 83.6963 134.083 82.334 134.083C81.5967 134.083 80.9961 133.727 80.7227 133.141H80.6348V135.782H79.209ZM81.8457 132.901C82.5977 132.901 83.0664 132.301 83.0664 131.314V131.305C83.0664 130.318 82.6025 129.718 81.8457 129.718C81.0938 129.718 80.6201 130.318 80.6201 131.305V131.314C80.6201 132.301 81.0986 132.901 81.8457 132.901ZM88.0713 134.112C86.416 134.112 85.4199 133.062 85.4199 131.314V131.305C85.4199 129.571 86.4307 128.507 88.0713 128.507C89.7119 128.507 90.7227 129.566 90.7227 131.305V131.314C90.7227 133.067 89.7266 134.112 88.0713 134.112ZM88.0713 132.994C88.8281 132.994 89.2627 132.374 89.2627 131.314V131.305C89.2627 130.255 88.8232 129.625 88.0713 129.625C87.3145 129.625 86.8799 130.255 86.8799 131.305V131.314C86.8799 132.374 87.3096 132.994 88.0713 132.994ZM94.0234 134.112C92.5635 134.112 91.7334 133.458 91.6309 132.535L91.626 132.516H93.0273L93.0371 132.535C93.1494 132.867 93.4766 133.106 94.0381 133.106C94.585 133.106 94.9561 132.872 94.9561 132.516V132.506C94.9561 132.218 94.7412 132.037 94.1943 131.915L93.2959 131.725C92.2705 131.505 91.7627 131.002 91.7627 130.196V130.191C91.7627 129.181 92.6611 128.507 94.0039 128.507C95.415 128.507 96.1963 129.21 96.2549 130.074V130.094H94.9316L94.9268 130.069C94.8486 129.781 94.5312 129.513 93.999 129.513C93.5059 129.513 93.1543 129.742 93.1543 130.104V130.108C93.1543 130.396 93.3545 130.562 93.9111 130.685L94.8047 130.875C95.8789 131.104 96.3818 131.554 96.3818 132.35V132.354C96.3818 133.419 95.4004 134.112 94.0234 134.112ZM98.2764 127.799C97.8369 127.799 97.4707 127.447 97.4707 127.008C97.4707 126.563 97.8369 126.212 98.2764 126.212C98.7158 126.212 99.0771 126.563 99.0771 127.008C99.0771 127.447 98.7158 127.799 98.2764 127.799ZM97.5586 134V128.624H98.9844V134H97.5586ZM102.651 134.02C101.343 134.02 100.815 133.595 100.815 132.55V129.698H99.9854V128.624H100.815V127.34H102.256V128.624H103.364V129.698H102.256V132.223C102.256 132.706 102.461 132.921 102.944 132.921C103.13 132.921 103.228 132.911 103.364 132.896V133.956C103.188 133.99 102.925 134.02 102.651 134.02Z",fill:"#2C3338"}),(0,a.createElement)("path",{d:"M142.11 134.693V133.893C140.738 133.814 139.742 133.121 139.703 131.905L139.708 131.9H141.012V131.905C141.095 132.403 141.441 132.726 142.11 132.789V131.036L141.783 130.958C140.509 130.67 139.811 130.099 139.811 129.049V129.039C139.811 127.877 140.768 127.145 142.11 127.066V126.246H142.672V127.066C144.024 127.169 144.908 127.94 144.957 129.088L144.952 129.093L143.658 129.098L143.653 129.088C143.585 128.565 143.219 128.253 142.672 128.17V129.869L143.019 129.942C144.352 130.245 145.05 130.782 145.05 131.852V131.861C145.05 133.077 144.059 133.795 142.672 133.888V134.693H142.11ZM141.173 128.941C141.173 129.317 141.451 129.562 142.11 129.737V128.165C141.495 128.233 141.173 128.536 141.173 128.932V128.941ZM143.688 131.998C143.688 131.573 143.429 131.354 142.672 131.163V132.794C143.351 132.73 143.688 132.447 143.688 132.008V131.998ZM149.063 134.156C147.472 134.156 146.422 133.302 146.324 132.042L146.319 131.979H147.73L147.735 132.032C147.799 132.579 148.302 132.97 149.063 132.97C149.815 132.97 150.309 132.55 150.309 131.954V131.944C150.309 131.271 149.806 130.899 148.951 130.899H148.146V129.85H148.932C149.674 129.85 150.152 129.459 150.152 128.888V128.878C150.152 128.292 149.747 127.931 149.054 127.931C148.36 127.931 147.896 128.307 147.838 128.893L147.833 128.937H146.476L146.48 128.878C146.583 127.618 147.574 126.798 149.054 126.798C150.567 126.798 151.544 127.56 151.544 128.683V128.692C151.544 129.571 150.899 130.143 150.074 130.313V130.343C151.124 130.44 151.808 131.046 151.808 132.003V132.013C151.808 133.292 150.709 134.156 149.063 134.156ZM155.792 134.181C154.283 134.181 153.287 133.282 153.204 132.105L153.199 132.037H154.537L154.547 132.071C154.649 132.589 155.133 133.004 155.797 133.004C156.554 133.004 157.081 132.472 157.081 131.715V131.705C157.081 130.953 156.549 130.431 155.802 130.431C155.445 130.431 155.143 130.533 154.908 130.733C154.776 130.841 154.659 130.973 154.581 131.124H153.316L153.644 126.954H158.048V128.126H154.791L154.615 130.133H154.703C154.986 129.659 155.533 129.366 156.207 129.366C157.521 129.366 158.468 130.333 158.468 131.676V131.686C158.468 133.17 157.374 134.181 155.792 134.181ZM159.762 134V133.009L162.032 130.768C163.023 129.806 163.292 129.449 163.292 128.956V128.941C163.292 128.341 162.882 127.921 162.208 127.921C161.52 127.921 161.046 128.38 161.046 129.049V129.073H159.684L159.679 129.054C159.679 127.701 160.733 126.773 162.276 126.773C163.727 126.773 164.742 127.604 164.742 128.819V128.834C164.742 129.635 164.327 130.299 163.043 131.485L161.739 132.701V132.813H164.845V134H159.762ZM167.145 134.122C166.646 134.122 166.285 133.756 166.285 133.272C166.285 132.789 166.646 132.428 167.145 132.428C167.647 132.428 168.004 132.789 168.004 133.272C168.004 133.756 167.647 134.122 167.145 134.122ZM171.783 126.769C173.414 126.769 174.693 127.931 174.693 130.406V130.416C174.693 132.774 173.58 134.176 171.773 134.176C170.411 134.176 169.4 133.37 169.181 132.267L169.171 132.223H170.616L170.631 132.267C170.802 132.706 171.202 133.004 171.769 133.004C172.789 133.004 173.238 132.003 173.292 130.719C173.292 130.67 173.292 130.616 173.292 130.562H173.194C172.94 131.104 172.354 131.593 171.378 131.593C170.006 131.593 169.083 130.616 169.083 129.283V129.273C169.083 127.833 170.196 126.769 171.783 126.769ZM171.783 130.504C172.511 130.504 173.072 129.986 173.072 129.259V129.249C173.072 128.512 172.511 127.94 171.793 127.94C171.08 127.94 170.509 128.502 170.509 129.22V129.229C170.509 129.972 171.046 130.504 171.783 130.504ZM178.619 126.769C180.25 126.769 181.529 127.931 181.529 130.406V130.416C181.529 132.774 180.416 134.176 178.609 134.176C177.247 134.176 176.236 133.37 176.017 132.267L176.007 132.223H177.452L177.467 132.267C177.638 132.706 178.038 133.004 178.604 133.004C179.625 133.004 180.074 132.003 180.128 130.719C180.128 130.67 180.128 130.616 180.128 130.562H180.03C179.776 131.104 179.19 131.593 178.214 131.593C176.842 131.593 175.919 130.616 175.919 129.283V129.273C175.919 127.833 177.032 126.769 178.619 126.769ZM178.619 130.504C179.347 130.504 179.908 129.986 179.908 129.259V129.249C179.908 128.512 179.347 127.94 178.629 127.94C177.916 127.94 177.345 128.502 177.345 129.22V129.229C177.345 129.972 177.882 130.504 178.619 130.504Z",fill:"#2C3338"}),(0,a.createElement)("rect",{x:"211.5",y:"123",width:"44",height:"14",rx:"7",fill:"#B8E6BF"}),(0,a.createElement)("path",{d:"M225.184 133V127.363H227.508C228.695 127.363 229.488 128.129 229.488 129.289V129.297C229.488 130.457 228.695 131.223 227.508 131.223H226.363V133H225.184ZM227.219 128.297H226.363V130.301H227.219C227.895 130.301 228.293 129.941 228.293 129.301V129.293C228.293 128.652 227.895 128.297 227.219 128.297ZM231.477 133.066C230.656 133.066 230.074 132.559 230.074 131.777V131.77C230.074 130.996 230.672 130.543 231.742 130.48L232.781 130.418V130.062C232.781 129.691 232.527 129.48 232.059 129.48C231.648 129.48 231.387 129.625 231.297 129.879L231.289 129.895H230.234L230.238 129.859C230.328 129.105 231.039 128.605 232.121 128.605C233.262 128.605 233.906 129.145 233.906 130.062V133H232.781V132.43H232.711C232.492 132.828 232.039 133.066 231.477 133.066ZM231.188 131.707C231.188 132.035 231.465 132.234 231.859 132.234C232.387 132.234 232.781 131.887 232.781 131.434V131.117L231.914 131.172C231.426 131.203 231.188 131.387 231.188 131.699V131.707ZM235.605 128.039C235.254 128.039 234.961 127.758 234.961 127.406C234.961 127.051 235.254 126.77 235.605 126.77C235.957 126.77 236.246 127.051 236.246 127.406C236.246 127.758 235.957 128.039 235.605 128.039ZM235.031 133V128.699H236.172V133H235.031ZM238.855 133.066C237.781 133.066 237.109 132.227 237.109 130.852V130.844C237.109 129.461 237.766 128.629 238.855 128.629C239.445 128.629 239.926 128.918 240.141 129.387H240.211V127.055H241.352V133H240.211V132.332H240.141C239.918 132.797 239.461 133.066 238.855 133.066ZM239.242 132.121C239.848 132.121 240.223 131.645 240.223 130.855V130.848C240.223 130.059 239.844 129.574 239.242 129.574C238.641 129.574 238.27 130.059 238.27 130.844V130.852C238.27 131.645 238.641 132.121 239.242 132.121Z",fill:"#00450C"}),(0,a.createElement)("rect",{x:"208",y:"165",width:"50",height:"8",rx:"4",fill:"#B8E6BF"}),(0,a.createElement)("rect",{x:"65",y:"165",width:"50",height:"8",rx:"4",fill:"#DCDCDE"}),(0,a.createElement)("rect",{x:"148",y:"161",width:"22.7796",height:"16",rx:"4",fill:"#F2EDFF"}),(0,a.createElement)("path",{d:"M159.007 173.856C158.305 173.856 157.676 173.749 157.12 173.535C156.569 173.316 156.129 173 155.801 172.585C155.477 172.17 155.304 171.664 155.281 171.067L155.288 171.061H157.113V171.067C157.159 171.327 157.257 171.553 157.407 171.744C157.562 171.931 157.774 172.075 158.043 172.175C158.312 172.275 158.642 172.325 159.034 172.325C159.444 172.325 159.784 172.282 160.053 172.195C160.322 172.104 160.522 171.974 160.654 171.806C160.791 171.637 160.859 171.439 160.859 171.211V171.197C160.859 170.878 160.739 170.63 160.497 170.452C160.26 170.27 159.832 170.11 159.212 169.974L158.193 169.741C157.601 169.609 157.1 169.427 156.689 169.194C156.279 168.962 155.967 168.673 155.753 168.326C155.539 167.975 155.432 167.556 155.432 167.068V167.055C155.432 166.476 155.587 165.979 155.896 165.564C156.206 165.15 156.63 164.833 157.168 164.614C157.706 164.391 158.316 164.279 159 164.279C159.715 164.279 160.34 164.396 160.873 164.628C161.411 164.86 161.832 165.191 162.138 165.619C162.448 166.043 162.614 166.544 162.637 167.123L162.63 167.13L160.818 167.137L160.812 167.123C160.78 166.836 160.684 166.597 160.524 166.405C160.365 166.209 160.158 166.061 159.902 165.961C159.647 165.861 159.355 165.811 159.027 165.811C158.658 165.811 158.348 165.858 158.098 165.954C157.847 166.05 157.658 166.18 157.53 166.344C157.403 166.508 157.339 166.695 157.339 166.904V166.918C157.339 167.105 157.394 167.271 157.503 167.417C157.612 167.563 157.781 167.69 158.009 167.8C158.237 167.909 158.535 168.007 158.904 168.094L159.923 168.319C160.857 168.529 161.563 168.839 162.042 169.249C162.525 169.659 162.767 170.24 162.767 170.992V171.006C162.767 171.612 162.603 172.129 162.274 172.558C161.951 172.981 161.504 173.305 160.935 173.528C160.369 173.747 159.727 173.856 159.007 173.856ZM158.651 174.971V163.145H159.438V174.971H158.651Z",fill:"#674399"}),(0,a.createElement)("rect",{x:"65",y:"206",width:"50",height:"8",rx:"4",fill:"#DCDCDE"}),(0,a.createElement)("rect",{x:"208",y:"206",width:"50",height:"8",rx:"4",fill:"#B8E6BF"}),(0,a.createElement)("rect",{x:"148",y:"202",width:"22.7796",height:"16",rx:"4",fill:"#F2EDFF"}),(0,a.createElement)("path",{d:"M161.379 214.856C160.6 214.856 159.889 214.756 159.246 214.556C158.604 214.351 158.048 214.05 157.578 213.653C157.113 213.252 156.753 212.753 156.498 212.156C156.247 211.555 156.122 210.857 156.122 210.064V210.058C156.122 209.269 156.247 208.576 156.498 207.979C156.753 207.382 157.113 206.883 157.578 206.482C158.043 206.081 158.592 205.781 159.226 205.58C159.864 205.38 160.565 205.279 161.331 205.279C161.582 205.279 161.841 205.293 162.11 205.32C162.379 205.348 162.603 205.386 162.78 205.437V207.084C162.598 207.048 162.384 207.018 162.138 206.995C161.892 206.972 161.648 206.961 161.406 206.961C160.91 206.961 160.461 207.027 160.06 207.159C159.659 207.287 159.312 207.48 159.021 207.74C158.733 207.995 158.512 208.317 158.357 208.704C158.207 209.091 158.132 209.545 158.132 210.064V210.071C158.132 210.595 158.209 211.051 158.364 211.438C158.519 211.826 158.74 212.149 159.027 212.409C159.319 212.664 159.668 212.856 160.073 212.983C160.483 213.106 160.944 213.168 161.454 213.168C161.677 213.168 161.917 213.161 162.172 213.147C162.427 213.129 162.639 213.104 162.808 213.072V214.733C162.63 214.774 162.413 214.806 162.158 214.829C161.903 214.847 161.643 214.856 161.379 214.856ZM154.96 211.425V210.639H162.042V211.425H154.96ZM154.96 209.531V208.745H162.042V209.531H154.96Z",fill:"#674399"}),(0,a.createElement)("path",{d:"M65 148.5H309",stroke:"#DCDCDE"}),(0,a.createElement)("path",{d:"M65 189.5H309",stroke:"#DCDCDE"}),(0,a.createElement)("path",{d:"M84.1484 75.0801H87.5495C88.7499 75.0801 89.7121 75.3659 90.4362 75.9375C91.1602 76.5091 91.5222 77.2617 91.5222 78.1954C91.5222 79.1766 91.1507 79.9483 90.4171 80.5008C89.6835 81.0534 88.6642 81.3392 87.359 81.3392H86.8827V85.1785H84.1294V75.0801H84.1484ZM86.8922 77.014V79.3957C87.5114 79.3862 87.9687 79.2909 88.2545 79.1004C88.5499 78.9099 88.6928 78.6145 88.6928 78.2144C88.6928 77.7762 88.5499 77.4713 88.264 77.2998C87.9878 77.1188 87.5305 77.0235 86.8922 77.014Z",fill:"black"}),(0,a.createElement)("path",{d:"M100.068 85.1785H97.2098L96.743 83.4923H93.8373L93.361 85.1785H90.5601L93.7706 75.0801H96.8573L100.068 85.1785ZM96.219 81.6727C95.8094 80.1674 95.495 78.7384 95.2949 77.3856H95.2568C95.0377 78.786 94.7328 80.215 94.3422 81.6727H96.219Z",fill:"black"}),(0,a.createElement)("path",{d:"M98.4194 75.0801H101.506C102.192 76.5282 102.687 77.6523 102.992 78.4335H103.03C103.335 77.6237 103.831 76.5091 104.498 75.0801H107.651L104.402 80.6057V85.169H101.62V80.6057L98.4194 75.0801Z",fill:"black"}),(0,a.createElement)("path",{d:"M108.508 75.0801H111.986C112.481 77.2617 112.872 79.129 113.157 80.7009L113.453 79.1385C113.577 78.5288 113.7 77.9381 113.843 77.357L114.377 75.0896H117.921L119.302 85.1881H116.578L115.844 78.243L114.148 85.1881H112.29L110.509 78.2335L109.804 85.1881H107.203L108.508 75.0801Z",fill:"black"}),(0,a.createElement)("path",{d:"M120.131 75.0801H125.695V77.2236H122.885V79.0814H125.323V81.1773H122.885V83.0159H125.714V85.169H120.131V75.0801Z",fill:"black"}),(0,a.createElement)("path",{d:"M126.733 75.0801H129.506L133.002 80.901L132.907 79.8149V75.0801H135.66V85.1785H132.84L129.42 79.4434L129.496 80.7009V85.1785H126.743V75.0801H126.733Z",fill:"black"}),(0,a.createElement)("path",{d:"M136.384 75.0801H142.948V77.3856H141.052V85.1785H138.271V77.3856H136.394V75.0801H136.384Z",fill:"black"}),(0,a.createElement)("path",{d:"M144.072 84.9879V82.6348C144.939 83.054 145.673 83.2731 146.273 83.2731C146.616 83.2731 146.892 83.1969 147.092 83.0349C147.292 82.873 147.387 82.6634 147.387 82.4061C147.387 82.2061 147.311 82.0155 147.149 81.8345C146.987 81.6535 146.711 81.4439 146.32 81.1962C145.368 80.5865 144.682 80.0244 144.253 79.51C143.824 78.986 143.615 78.4049 143.615 77.7666C143.615 76.8901 143.948 76.1851 144.606 75.6325C145.263 75.08 146.12 74.8037 147.168 74.8037C147.94 74.8037 148.712 74.9466 149.474 75.2229V77.4808C148.654 77.1092 147.997 76.9187 147.483 76.9187C147.14 76.9187 146.873 76.9854 146.682 77.1283C146.482 77.2712 146.387 77.4617 146.387 77.6999C146.387 78.1191 146.844 78.5954 147.759 79.1289C148.302 79.4433 148.855 79.8815 149.417 80.4531C149.979 81.0247 150.255 81.6726 150.255 82.4252C150.255 83.3017 149.893 84.0162 149.159 84.5878C148.426 85.1594 147.502 85.4452 146.378 85.4452C145.596 85.4357 144.834 85.2832 144.072 84.9879Z",fill:"black"}),(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M48.4225 71.0597H77.7367C79.5849 71.0597 81.0901 72.5649 81.0901 74.4131V85.5977C81.0901 87.4459 79.5849 88.9511 77.7367 88.9511H67.219L68.6576 92.4856L62.3032 88.9511H48.413C46.5648 88.9511 45.0596 87.4459 45.0596 85.5977V74.4131C45.0596 72.5649 46.5648 71.0597 48.4225 71.0597Z",fill:"#7F54B3"}),(0,a.createElement)("path",{d:"M46.8795 74.0987C47.1081 73.8224 47.4415 73.651 47.8036 73.6414C48.5467 73.5938 48.9754 73.9463 49.0897 74.6989C49.547 77.7666 50.0424 80.3769 50.5664 82.5205L53.796 76.3852C54.0913 75.8326 54.4533 75.5373 54.9011 75.5087C55.5489 75.4611 55.949 75.8707 56.111 76.7472C56.4158 78.462 56.8827 80.1483 57.5019 81.7774C57.8925 78.0428 58.5403 75.3372 59.4454 73.67C59.6264 73.2889 59.9979 73.0412 60.4171 73.0222C60.7506 72.9936 61.084 73.0984 61.3412 73.3175C61.608 73.5176 61.7699 73.832 61.789 74.1654C61.808 74.4131 61.7604 74.6608 61.6461 74.8704C61.0745 75.9374 60.5981 77.7094 60.2266 80.1864C59.8646 82.5776 59.7217 84.4544 59.8169 85.7977C59.855 86.1311 59.7883 86.4646 59.6454 86.7599C59.5025 87.0648 59.1977 87.2744 58.8642 87.2934C58.4736 87.322 58.0926 87.141 57.702 86.7504C56.3301 85.3499 55.244 83.2636 54.4438 80.4912C53.5006 82.3776 52.7861 83.7971 52.3193 84.7307C51.4524 86.3979 50.7093 87.2458 50.0995 87.2934C49.6994 87.322 49.3564 86.9886 49.0802 86.2836C48.3275 84.3591 47.5178 80.6341 46.6508 75.1086C46.5746 74.7466 46.6603 74.3845 46.8795 74.0987Z",fill:"white"}),(0,a.createElement)("path",{d:"M69.296 76.4139C68.8006 75.5279 67.9432 74.8991 66.9429 74.7085C66.6762 74.6514 66.4094 74.6228 66.1426 74.6228C64.7327 74.6228 63.5799 75.3564 62.6844 76.8235C61.9223 78.0715 61.5221 79.5101 61.5412 80.9677C61.5412 82.1014 61.7794 83.0731 62.2462 83.8829C62.7416 84.7689 63.599 85.3977 64.5993 85.5882C64.8661 85.6454 65.1328 85.6739 65.3996 85.6739C66.8191 85.6739 67.9718 84.9404 68.8578 83.4732C69.6199 82.2157 70.0201 80.7771 70.001 79.3005C70.001 78.1668 69.7629 77.2046 69.296 76.4139ZM67.4383 80.5009C67.2382 81.4631 66.8667 82.1871 66.3141 82.6825C65.8854 83.0731 65.4853 83.2255 65.1233 83.1589C64.7613 83.0922 64.4754 82.7683 64.2563 82.2062C64.0944 81.7775 63.9991 81.3297 63.9991 80.8629C63.9991 80.5009 64.0372 80.1388 64.1039 79.7864C64.2468 79.1576 64.504 78.5669 64.8851 78.0334C65.371 77.3189 65.8854 77.014 66.4189 77.1284C66.7809 77.2046 67.0667 77.519 67.2859 78.081C67.4478 78.5098 67.5431 78.9575 67.5431 79.4148C67.5431 79.7768 67.5145 80.1388 67.4383 80.5009Z",fill:"white"}),(0,a.createElement)("path",{d:"M78.5182 76.4139C78.0228 75.5279 77.1654 74.8991 76.1651 74.7085C75.8983 74.6514 75.6316 74.6228 75.3648 74.6228C73.9548 74.6228 72.8021 75.3564 71.9066 76.8235C71.1444 78.0715 70.7443 79.5101 70.7633 80.9677C70.7633 82.1014 71.0015 83.0731 71.4683 83.8829C71.9637 84.7689 72.8211 85.3977 73.8215 85.5882C74.0882 85.6454 74.355 85.6739 74.6217 85.6739C76.0412 85.6739 77.194 84.9404 78.08 83.4732C78.8421 82.2157 79.2422 80.7771 79.2232 79.3005C79.2232 78.1668 78.985 77.2046 78.5182 76.4139ZM76.6605 80.5009C76.4604 81.4631 76.0889 82.1871 75.5363 82.6825C75.1076 83.0731 74.7075 83.2255 74.3454 83.1589C73.9834 83.0922 73.6976 82.7683 73.4785 82.2062C73.3165 81.7775 73.2213 81.3297 73.2213 80.8629C73.2213 80.5009 73.2594 80.1388 73.3261 79.7864C73.469 79.1576 73.7262 78.5669 74.1073 78.0334C74.5931 77.3189 75.1076 77.014 75.6411 77.1284C76.0031 77.2046 76.2889 77.519 76.508 78.081C76.67 78.5098 76.7653 78.9575 76.7653 79.4148C76.7653 79.7768 76.7367 80.1388 76.6605 80.5009Z",fill:"white"}),(0,a.createElement)("circle",{cx:"34.2526",cy:"34.2526",r:"2.25264",fill:"#DFD1FB"}),(0,a.createElement)("circle",{cx:"42.758",cy:"34.2526",r:"2.25264",fill:"#DFD1FB"}),(0,a.createElement)("circle",{cx:"51.2634",cy:"34.2526",r:"2.25264",fill:"#DFD1FB"}),(0,a.createElement)("defs",null,(0,a.createElement)("filter",{id:"filter0_d_5851_4204",x:"0",y:"4",width:"325",height:"272",filterUnits:"userSpaceOnUse",colorInterpolationFilters:"sRGB"},(0,a.createElement)("feFlood",{floodOpacity:"0",result:"BackgroundImageFix"}),(0,a.createElement)("feColorMatrix",{in:"SourceAlpha",type:"matrix",values:"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0",result:"hardAlpha"}),(0,a.createElement)("feOffset",null),(0,a.createElement)("feGaussianBlur",{stdDeviation:"8"}),(0,a.createElement)("feComposite",{in2:"hardAlpha",operator:"out"}),(0,a.createElement)("feColorMatrix",{type:"matrix",values:"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0"}),(0,a.createElement)("feBlend",{mode:"normal",in2:"BackgroundImageFix",result:"effect1_dropShadow_5851_4204"}),(0,a.createElement)("feBlend",{mode:"normal",in:"SourceGraphic",in2:"effect1_dropShadow_5851_4204",result:"shape"})))),k=e=>{let{isWooPayEligible:t=!1}=e;return(0,a.createElement)("div",{className:"woocommerce-recommended-payments-banner__footer_icon_container"},(0,a.createElement)(v,null),(0,a.createElement)(w,null),(0,a.createElement)(d,null),t&&(0,a.createElement)(E,null),(0,a.createElement)(p,null),(0,a.createElement)(f,null),(0,a.createElement)(C,null),(0,a.createElement)(g,null),(0,a.createElement)(h,null),(0,a.createElement)(_,null),(0,a.createElement)(y,null),(0,a.createElement)(H,null),(0,a.createElement)(r.Text,{variant:"caption",as:"p",size:"12",lineHeight:"16px"},(0,s.__)("& more.","woocommerce")))},S=e=>{let{isWooPayEligible:t}=e;return(0,a.createElement)(o.CardFooter,{className:"woocommerce-recommended-payments-banner__footer"},(0,a.createElement)("div",null,(0,a.createElement)(r.Text,{variant:"caption",as:"p",size:"12",lineHeight:"16px"},(0,s.__)("WooPayments is pre-integrated with popular payment options:","woocommerce"))),(0,a.createElement)(k,{isWooPayEligible:t}))},M=e=>{let{actionButton:t,isWooPayEligible:n}=e;const o={tosLink:(0,a.createElement)(i.Link,{href:"https://wordpress.com/tos/",type:"external",target:"_blank"},(0,a.createElement)(a.Fragment,null)),privacyLink:(0,a.createElement)(i.Link,{href:"https://automattic.com/privacy/",type:"external",target:"_blank"},(0,a.createElement)(a.Fragment,null)),woopayMerchantTerms:(0,a.createElement)(i.Link,{href:"https://wordpress.com/tos/#more-woopay-specifically",type:"external",target:"_blank"},(0,a.createElement)(a.Fragment,null))};return(0,a.createElement)("div",{className:"woocommerce-recommended-payments-banner__text_container"},(0,a.createElement)(r.Text,{className:"woocommerce-recommended-payments__header-title",variant:"title.small",as:"p",size:"24",lineHeight:"28px",padding:"0 20px 0 0"},(0,s.__)("Payments made simple, designed exclusively for WooCommerce stores.","woocommerce")),(0,a.createElement)(r.Text,{className:"woocommerce-recommended-payments__header-heading",variant:"caption",as:"p",size:"12",lineHeight:"16px"},n?(0,a.createInterpolateElement)((0,s.__)("By using WooPayments you agree to the <tosLink>Terms of Service</tosLink> (including WooPay <woopayMerchantTerms>merchant terms</woopayMerchantTerms>) and acknowledge that you have read our <privacyLink>Privacy Policy</privacyLink>.","woocommerce"),o):(0,a.createInterpolateElement)((0,s.__)("By using WooPayments you agree to the <tosLink>Terms of Service</tosLink> and acknowledge that you have read our <privacyLink>Privacy Policy</privacyLink>.","woocommerce"),o)),t)},N=e=>{let{actionButton:t,textPosition:n,bannerImage:r=(0,a.createElement)(L,null),isWooPayEligible:i}=e;return(0,a.createElement)(o.CardBody,{className:"woocommerce-recommended-payments-banner__body"},"left"===n?(0,a.createElement)(a.Fragment,null,(0,a.createElement)(M,{actionButton:t,isWooPayEligible:i}),(0,a.createElement)("div",{className:"woocommerce-recommended-payments-banner__image_container"},r)):(0,a.createElement)(a.Fragment,null,(0,a.createElement)("div",{className:"woocommerce-recommended-payments-banner__image_container"},r),(0,a.createElement)(M,{actionButton:t,isWooPayEligible:i})))},x=e=>{let{children:t}=e;return(0,a.createElement)(o.Card,{size:"medium",className:"woocommerce-recommended-payments-banner"},t)},T=()=>(0,a.createElement)("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("mask",{id:"mask0_5908_1374",style:{maskType:"luminance"},maskUnits:"userSpaceOnUse",x:"2",y:"4",width:"20",height:"16"},(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M20 4H4C2.895 4 2 4.895 2 6V18C2 19.105 2.895 20 4 20H20C21.105 20 22 19.105 22 18V6C22 4.895 21.105 4 20 4ZM20 6V8H4V6H20ZM4 12V18H20V12H4ZM6 14H13V16H6V14ZM18 14H15V16H18V14Z",fill:"white"})),(0,a.createElement)("g",{mask:"url(#mask0_5908_1374)"},(0,a.createElement)("rect",{width:"24",height:"24",fill:"#674399"}))),I=()=>(0,a.createElement)("svg",{width:"25",height:"24",viewBox:"0 0 25 24",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("mask",{id:"mask0_5908_1702",style:{maskType:"luminance"},maskUnits:"userSpaceOnUse",x:"2",y:"2",width:"21",height:"20"},(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M2.3335 12C2.3335 6.477 6.8105 2 12.3335 2C17.8565 2 22.3335 6.477 22.3335 12C22.3335 17.523 17.8565 22 12.3335 22C6.8105 22 2.3335 17.523 2.3335 12ZM15.3335 17L14.3335 18L12.3335 20C15.5725 20 18.3625 18.061 19.6195 15.286L19.3335 15H17.3335L16.3335 14V12L18.3335 10H20.0695C19.7675 8.834 19.2155 7.767 18.4635 6.87L15.3335 10L14.3335 9V7L15.3335 6V4.589C14.4065 4.212 13.3945 4 12.3335 4C11.2725 4 10.2605 4.212 9.3335 4.589V5L10.3335 6H12.3335V8L9.3335 11H7.3335V13H5.3335L4.3335 12C4.3335 16.072 7.3935 19.436 11.3335 19.931V18L9.3335 16V13H12.3335L13.3335 14V15H15.3335V17Z",fill:"white"})),(0,a.createElement)("g",{mask:"url(#mask0_5908_1702)"},(0,a.createElement)("rect",{x:"0.333496",width:"24",height:"24",fill:"#674399"}))),A=()=>(0,a.createElement)("svg",{width:"25",height:"24",viewBox:"0 0 25 24",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("mask",{id:"mask0_5908_1139",style:{maskType:"luminance"},maskUnits:"userSpaceOnUse",x:"3",y:"2",width:"19",height:"19"},(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M21.6665 6V19C21.6665 20.104 20.7705 21 19.6665 21H5.6665C4.5625 21 3.6665 20.104 3.6665 19V6C3.6665 4.896 4.5625 4 5.6665 4H6.6665V2H8.6665V4H16.6665V2H18.6665V4H19.6665C20.7705 4 21.6665 4.896 21.6665 6ZM11.1645 18.001L7.4595 14.296L8.8745 12.881L11.1685 15.175L16.4615 9.882L17.8765 11.297L11.1645 18.001ZM5.6665 8H19.6665V19H5.6665V8Z",fill:"white"})),(0,a.createElement)("g",{mask:"url(#mask0_5908_1139)"},(0,a.createElement)("rect",{x:"0.666504",width:"24",height:"24",fill:"#674399"}))),O=()=>(0,a.createElement)("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("mask",{id:"mask0_5908_3242",style:{maskType:"luminance"},maskUnits:"userSpaceOnUse",x:"3",y:"4",width:"18",height:"18"},(0,a.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M18.192 4H5.79578C4.24399 4 2.98777 5.26853 3.00009 6.80801V16.168C3.00009 17.7198 4.2563 18.9761 5.8081 18.9761H11.1676L16.4757 21.9319L15.2687 18.9761H18.192C19.7438 18.9761 21 17.7198 21 16.168V6.80801C21 5.25622 19.7438 4 18.192 4ZM7.53039 7.19611C7.2535 7.21589 7.04582 7.31478 6.90738 7.50268C6.76893 7.68068 6.71948 7.90813 6.74915 8.16525C7.33261 11.8737 7.87651 14.3756 8.38086 15.6711C8.57864 16.1458 8.80609 16.3732 9.0731 16.3535C9.48844 16.3238 9.9829 15.7502 10.5664 14.6327C10.8729 13.9998 11.3476 13.0505 11.9904 11.7847C12.5244 13.6537 13.2562 15.058 14.1759 15.9974C14.433 16.2645 14.7 16.3831 14.9571 16.3633C15.1846 16.3436 15.3626 16.2249 15.4813 16.0073C15.5801 15.8194 15.6197 15.6019 15.5999 15.3547C15.5406 14.4547 15.6296 13.1988 15.8768 11.5869C16.1339 9.92551 16.4504 8.72893 16.8361 8.01691C16.9152 7.86857 16.9448 7.72024 16.935 7.54223C16.9152 7.31478 16.8163 7.12689 16.6284 6.97855C16.4405 6.83021 16.2328 6.76099 16.0054 6.78077C15.7186 6.80055 15.501 6.939 15.3527 7.21589C14.7396 8.33336 14.3045 10.1431 14.0473 12.6549C13.6715 11.7056 13.3551 10.5881 13.1079 9.27283C12.9991 8.68937 12.7321 8.41248 12.297 8.44214C12.0003 8.46192 11.7531 8.65971 11.5553 9.03549L9.38955 13.1593C9.03354 11.7253 8.69731 9.97496 8.39075 7.90813C8.32152 7.3939 8.03474 7.15656 7.53039 7.19611Z",fill:"white"})),(0,a.createElement)("g",{mask:"url(#mask0_5908_3242)"},(0,a.createElement)("rect",{width:"24",height:"24",fill:"#674399"}))),P=e=>{let{isWooPayEligible:t=!1}=e;return(0,a.createElement)(o.Flex,{className:"woocommerce-wcpay-benefits",align:"top"},(0,a.createElement)(o.Flex,{className:"woocommerce-wcpay-benefits-benefit"},(0,a.createElement)(o.Flex,{className:"woocommerce-wcpay-benefits-benefit-icon-container"},(0,a.createElement)(T,null)),(0,a.createElement)(r.Text,{as:"p"},(0,s.__)("Offer your customers card payments, Sofort, iDeal, and the ability to sell in-person with Woo mobile app.","woocommerce"))),(0,a.createElement)(o.Flex,{className:"woocommerce-wcpay-benefits-benefit"},(0,a.createElement)(o.Flex,{className:"woocommerce-wcpay-benefits-benefit-icon-container"},(0,a.createElement)(I,null)),(0,a.createElement)(r.Text,{as:"p"},(0,s.__)("Sell to international markets and accept more than 135 currencies with local payment methods.","woocommerce"))),(0,a.createElement)(o.Flex,{className:"woocommerce-wcpay-benefits-benefit"},(0,a.createElement)(o.Flex,{className:"woocommerce-wcpay-benefits-benefit-icon-container"},(0,a.createElement)(A,null)),(0,a.createElement)(r.Text,{as:"p"},(0,s.__)("Earn and manage recurring revenue and get automatic deposits into your nominated bank account.","woocommerce"))),t&&(0,a.createElement)(o.Flex,{className:"woocommerce-wcpay-benefits-benefit"},(0,a.createElement)(o.Flex,{className:"woocommerce-wcpay-benefits-benefit-icon-container"},(0,a.createElement)(O,null)),(0,a.createElement)(r.Text,{as:"p"},(0,s.__)("Boost conversions with WooPay, a new express checkout feature included in WooPayments.","woocommerce"))))};var V=n(59838);const R=()=>(0,a.createElement)("span",{className:"woocommerce-task-payment__setup_required"},(0,a.createElement)(V.Z,null),(0,a.createElement)(r.Text,{variant:"small",size:"14",lineHeight:"20px"},(0,s.__)("Setup required","woocommerce"))),j=()=>(0,a.createElement)("svg",{width:"36",height:"25",viewBox:"0 0 36 25",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("rect",{x:"1.41431",y:"1",width:"33.7586",height:"23",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),(0,a.createElement)("path",{d:"M17.645 14.9708V12.2642V12.2636H19.1074C19.7104 12.264 20.2171 12.0743 20.6276 11.6946C21.0425 11.3342 21.2745 10.816 21.2627 10.2759C21.2709 9.73881 21.0393 9.22448 20.6276 8.86525C20.2207 8.48344 19.6734 8.27511 19.1074 8.28658H16.7598V14.9708H17.645ZM17.6451 11.4427V9.10938V9.10885H19.1295C19.4604 9.09983 19.7793 9.22898 20.0054 9.46351C20.2328 9.678 20.3611 9.97262 20.3611 10.2803C20.3611 10.588 20.2328 10.8826 20.0054 11.0971C19.7766 11.3267 19.4586 11.4522 19.1295 11.4427H17.6451Z",fill:"#5F6368"}),(0,a.createElement)("path",{d:"M24.8518 10.7568C24.4731 10.4176 23.9567 10.248 23.3024 10.248C22.462 10.248 21.8273 10.5467 21.3985 11.144L22.1781 11.6203C22.4662 11.2157 22.8574 11.0134 23.3519 11.0134C23.6672 11.0098 23.9722 11.1216 24.2063 11.3264C24.4397 11.5136 24.5739 11.7927 24.5719 12.0864V12.2827C24.2318 12.096 23.7989 12.0027 23.2733 12.0027C22.6575 12.0034 22.1652 12.1435 21.7965 12.423C21.4278 12.7024 21.2434 13.0788 21.2434 13.552C21.2354 13.983 21.4281 14.3944 21.7679 14.672C22.1176 14.9707 22.5521 15.12 23.0715 15.12C23.68 15.12 24.1674 14.8587 24.534 14.336H24.5725V14.9707H25.4192V12.152C25.4195 11.5611 25.2304 11.096 24.8518 10.7568ZM22.4508 14.1307C22.2654 14.0011 22.156 13.7924 22.1572 13.5707C22.1572 13.3216 22.2776 13.1142 22.5201 12.9435C22.7602 12.7753 23.06 12.6912 23.4196 12.6912C23.9133 12.6912 24.2982 12.7979 24.5742 13.0112C24.5742 13.3718 24.4276 13.6859 24.1343 13.9536C23.8702 14.2099 23.5122 14.3541 23.1386 14.3547C22.8896 14.3592 22.6466 14.2801 22.4508 14.1307Z",fill:"#5F6368"}),(0,a.createElement)("path",{d:"M30.2792 10.3975L27.3235 16.9868H26.4097L27.5065 14.6812L25.563 10.3975H26.5251L27.9299 13.6828H27.9491L29.3154 10.3975H30.2792Z",fill:"#5F6368"}),(0,a.createElement)("path",{d:"M14.0677 11.6812C14.068 11.4195 14.0452 11.1583 13.9995 10.9004H10.2664V12.3793H12.4045C12.3161 12.8566 12.0305 13.2782 11.6139 13.5463V14.5063H12.89C13.6372 13.838 14.0677 12.8497 14.0677 11.6812Z",fill:"#4285F4"}),(0,a.createElement)("path",{d:"M10.2666 15.4332C11.3349 15.4332 12.2344 15.0929 12.8903 14.5063L11.6142 13.5463C11.259 13.7799 10.8016 13.9132 10.2666 13.9132C9.23409 13.9132 8.35771 13.238 8.04432 12.3281H6.72974V13.3175C7.40168 14.6145 8.77018 15.4331 10.2666 15.4332Z",fill:"#34A853"}),(0,a.createElement)("path",{d:"M8.04421 12.3283C7.87853 11.8516 7.87853 11.3353 8.04421 10.8585V9.86914H6.72962C6.1676 10.954 6.1676 12.2328 6.72962 13.3177L8.04421 12.3283Z",fill:"#FBBC04"}),(0,a.createElement)("path",{d:"M10.2666 9.27318C10.8312 9.26424 11.3766 9.47114 11.7852 9.84918L12.915 8.75318C12.1986 8.10042 11.2495 7.74205 10.2666 7.75318C8.77018 7.75325 7.40168 8.57187 6.72974 9.86892L8.04432 10.8582C8.35771 9.94838 9.23409 9.27318 10.2666 9.27318Z",fill:"#EA4335"})),D=e=>{let{id:t,...n}=e;return(0,a.createElement)(o.Fill,{name:"woocommerce_payment_gateway_setup_"+t,...n})};D.Slot=e=>{let{id:t,fillProps:n}=e;return(0,a.createElement)(o.Slot,{name:"woocommerce_payment_gateway_setup_"+t,fillProps:n})};const Z=e=>{let{id:t,...n}=e;return(0,a.createElement)(o.Fill,{name:"woocommerce_payment_gateway_configure_"+t,...n})};Z.Slot=e=>{let{id:t,fillProps:n}=e;return(0,a.createElement)(o.Slot,{name:"woocommerce_payment_gateway_configure_"+t,fillProps:n})};const B=e=>{let{id:t,...n}=e;return(0,a.createElement)(o.Fill,{name:"woocommerce_onboarding_task_list_item_"+t,...n})};B.Slot=e=>{let{id:t,fillProps:n}=e;return(0,a.createElement)(o.Slot,{name:"woocommerce_onboarding_task_list_item_"+t,fillProps:n})};const F=e=>{let{id:t,...n}=e;return(0,a.createElement)(o.Fill,{name:"woocommerce_onboarding_task_list_header_"+t,...n})};F.Slot=e=>{let{id:t,fillProps:n}=e;return(0,a.createElement)(o.Slot,{name:"woocommerce_onboarding_task_list_header_"+t,fillProps:n})};var z=n(14599);const W=e=>{let{id:t,...n}=e;return(0,a.createElement)(o.Fill,{name:"woocommerce_onboarding_task_"+t,...n})};W.Slot=e=>{let{id:t,fillProps:n}=e;return(0,a.useEffect)((()=>{(async(e,t)=>{const n=wp.data.select("wc/admin/plugins").getActivePlugins(),o=wp.data.select("wc/admin/plugins").getInstalledPlugins(),r=wp.data.select("wc/admin/plugins").isJetpackConnected()||!1;(0,z.recordEvent)("task_view",{task_name:e,variant:void 0,wcs_installed:o.includes("woocommerce-services"),wcs_active:n.includes("woocommerce-services"),jetpack_installed:o.includes("jetpack"),jetpack_active:n.includes("jetpack"),jetpack_connected:r})})(t)}),[t]),(0,a.createElement)(o.Slot,{name:"woocommerce_onboarding_task_"+t,fillProps:n})};var U=n(61055),G=n.n(U);const Y={PH:{"National Capital Region":(0,s.__)("Metro Manila","woocommerce")},IT:{Rome:(0,s.__)("Roma","woocommerce")}},Q=e=>{let{country_short:t,region:n="",city:o=""}=e;if(!t)return null;const r=Y[t];if(!r)return null;const a=r[n];if(a)return a;return r[o]||null},q=function(e,t){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:.7;if(!t)return null;let o=null,r=n;const a=Q(t);for(const n of e){if(n.key===t.country_short)return n;if(n.key.split(":")[0]===t.country_short&&n.label.includes("—")){const e=n.label.split("—")[1].trim();if(a===e)return n;if(0===e.localeCompare(t.region||"","en",{sensitivity:"base"})||0===e.localeCompare(t.city||"","en",{sensitivity:"base"}))return n;const i=Math.max(G().compareTwoStrings(e,t.region||""),G().compareTwoStrings(e,t.city||""));i>=r&&(o=n,r=i)}}return o},J=e=>{var t;return null!==(t=null==e?void 0:e.split(":")[0])&&void 0!==t?t:void 0};var K=n(83849),X=n.n(K),$=n(99196);const ee=e=>{let{className:t="",percent:n=0,color:o="#674399",bgcolor:r="var(--wp-admin-theme-color)"}=e;const a={backgroundColor:r},i={backgroundColor:o,width:`${n}%`,display:0===n?"none":"inherit"};return(0,$.createElement)("div",{className:`woocommerce-onboarding-progress-bar ${t}`},(0,$.createElement)("div",{className:"woocommerce-onboarding-progress-bar__container",style:a},(0,$.createElement)("div",{className:"woocommerce-onboarding-progress-bar__filler",style:i})))},te=e=>{let{children:t,className:n}=e;return(0,a.createElement)("div",{className:X()("woocommerce-onboarding-loader",n)},t)};te.Layout=e=>{let{children:t,className:n}=e;return(0,a.createElement)("div",{className:X()("woocommerce-onboarding-loader-wrapper",n)},(0,a.createElement)("div",{className:X()("woocommerce-onboarding-loader-container",n)},t))},te.Illustration=e=>{let{children:t}=e;return(0,a.createElement)(a.Fragment,null,t)},te.Title=e=>{let{children:t,className:n}=e;return(0,a.createElement)("h1",{className:X()("woocommerce-onboarding-loader__title",n)},t)},te.ProgressBar=e=>{let{progress:t,className:n}=e;return(0,a.createElement)(ee,{className:X()("progress-bar",n),percent:null!=t?t:0,color:"var(--wp-admin-theme-color)",bgcolor:"#E0E0E0"})},te.Subtext=e=>{let{children:t,className:n}=e;return(0,a.createElement)("p",{className:X()("woocommerce-onboarding-loader__paragraph",n)},t)},te.Sequence=e=>{let{interval:t,children:n}=e;const[o,r]=(0,a.useState)(0);(0,a.useEffect)((()=>{const e=setInterval((()=>{r((e=>(e+1)%a.Children.count(n)))}),t);return()=>clearInterval(e)}),[t,n]);const i=a.Children.toArray(n)[o];return(0,a.createElement)(a.Fragment,null,i)}},92342:(e,t,n)=>{"use strict";n.d(t,{z:()=>m,T:()=>p});var o=n(69307),r=n(55609),a=n(9818),i=n(65736),s=n(67221),c=n(14599);const l=()=>(0,o.createElement)(o.Fragment,null,(0,o.createElement)("svg",{className:"woocommerce-layout__activity-panel-tab-icon",width:"24",height:"24",viewBox:"3 3 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,o.createElement)("path",{d:"M13.8053 15.3982C13.8053 15.7965 13.4867 16.1947 13.0089 16.1947H6.79646C6.55752 16.1947 6.39823 16.115 6.23894 15.9558C6.07965 15.7965 6 15.6372 6 15.3982V6.79646C6 6.63717 6.15929 6.39823 6.23894 6.23894C6.39823 6.07965 6.55752 6 6.79646 6H13.0089C13.4071 6 13.8053 6.31858 13.8053 6.79646V15.3982Z",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"}),(0,o.createElement)("path",{d:"M23.9203 10.6195C23.9203 11.0177 23.6017 11.4159 23.1238 11.4159H16.9115C16.6725 11.4159 16.5132 11.3363 16.3539 11.177C16.1946 11.0177 16.115 10.8584 16.115 10.6195V6.79646C16.115 6.39823 16.4336 6 16.9115 6H23.1238C23.5221 6 23.9203 6.31858 23.9203 6.79646V10.6195Z",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"}),(0,o.createElement)("path",{d:"M13.8053 23.2035C13.8053 23.4424 13.7257 23.6017 13.5664 23.761C13.4071 23.9203 13.2478 23.9999 13.0089 23.9999H6.79646C6.39823 23.9999 6 23.6813 6 23.2035V19.3804C6 19.1415 6.07965 18.9822 6.23894 18.8229C6.39823 18.6636 6.55752 18.584 6.79646 18.584H13.0089C13.4071 18.584 13.8053 18.9026 13.8053 19.3804V23.2035Z",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"}),(0,o.createElement)("path",{d:"M16.9912 23.9999C16.7522 23.9999 16.5929 23.9202 16.4336 23.7609C16.2743 23.6016 16.1947 23.4423 16.1947 23.2034V14.6016C16.1947 14.3627 16.2743 14.2034 16.4336 14.0441C16.5929 13.8848 16.7522 13.8052 16.9912 13.8052H23.2036C23.4425 13.8052 23.6018 13.8848 23.7611 14.0441C23.9204 14.2034 24 14.3627 24 14.6016V23.2034C24 23.6016 23.6814 23.9999 23.2036 23.9999H16.9912Z",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})),(0,i.__)("Display","woocommerce")),{Fill:m,Slot:u}=(0,r.createSlotFill)("DisplayOptions");m.Slot=u;const d=[{value:"single_column",label:(0,o.createElement)(o.Fragment,null,(0,o.createElement)((()=>(0,o.createElement)("svg",{className:"woocommerce-layout__activity-panel-tab-icon",width:"12",height:"14",viewBox:"0 0 12 14",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,o.createElement)("rect",{x:"0.5",y:"0.5",width:"11",height:"13",strokeWidth:"1"}))),null),(0,i.__)("Single column","woocommerce"))},{value:"two_columns",label:(0,o.createElement)(o.Fragment,null,(0,o.createElement)((()=>(0,o.createElement)("svg",{className:"woocommerce-layout__activity-panel-tab-icon",width:"18",height:"14",viewBox:"0 0 18 14",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,o.createElement)("rect",{x:"0.5",y:"0.5",width:"7",height:"13",strokeWidth:"1"}),(0,o.createElement)("rect",{x:"9.5",y:"0.5",width:"7",height:"13",strokeWidth:"1"}))),null),(0,i.__)("Two columns","woocommerce"))}],p=()=>{const{defaultHomescreenLayout:e,taskListComplete:t,isTaskListHidden:n}=(0,a.useSelect)((e=>{const{getOption:t}=e(s.OPTIONS_STORE_NAME),{getTaskList:n}=e(s.ONBOARDING_STORE_NAME),o=n("setup");return{defaultHomescreenLayout:t("woocommerce_default_homepage_layout")||"single_column",taskListComplete:null==o?void 0:o.isComplete,isTaskListHidden:null==o?void 0:o.isHidden}})),{updateUserPreferences:m,homepage_layout:p}=(0,s.useUserPreferences)(),C=t||n||window.wcAdminFeatures.analytics;return(0,o.createElement)(u,null,(t=>0!==t.length||C?(0,o.createElement)(r.DropdownMenu,{icon:(0,o.createElement)(l,null),label:(0,i.__)("Display options","woocommerce"),toggleProps:{className:"woocommerce-layout__activity-panel-tab display-options",onClick:()=>(0,c.recordEvent)("homescreen_display_click")},popoverProps:{className:"woocommerce-layout__activity-panel-popover"}},(n=>{let{onClose:a}=n;return(0,o.createElement)(o.Fragment,null,t,C?(0,o.createElement)(r.MenuGroup,{className:"woocommerce-layout__homescreen-display-options",label:(0,i.__)("Layout","woocommerce")},(0,o.createElement)(r.MenuItemsChoice,{choices:d,onSelect:e=>{m({homepage_layout:e}),a(),(0,c.recordEvent)("homescreen_display_option",{display_option:e})},value:p||e})):null)})):null))}},30554:(e,t,n)=>{"use strict";n.d(t,{$Q:()=>w,vn:()=>H});var o=n(69307),r=n(65736),a=n(14812),i=n(14599),s=n(86020),c=n(9818),l=n(67221),m=n(70444);const u=(0,o.createElement)(m.SVG,{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},(0,o.createElement)(m.Path,{d:"M18 4H6c-1.1 0-2 .9-2 2v12.9c0 .6.5 1.1 1.1 1.1.3 0 .5-.1.8-.3L8.5 17H18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm.5 11c0 .3-.2.5-.5.5H7.9l-2.4 2.4V6c0-.3.2-.5.5-.5h12c.3 0 .5.2.5.5v9z"}));var d=n(89015),p=n(34200),C=n(55609),h=n(10431),g=n(22983),f=n(14138);const y=()=>(0,o.createElement)("svg",{width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},(0,o.createElement)("path",{d:"M0 0h24v24H0z",fill:"none"}),(0,o.createElement)("path",{d:"M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2zm-2 1H8v-6c0-2.48 1.51-4.5 4-4.5s4 2.02 4 4.5v6z"})),w="AbbreviatedNotification",H=e=>{let{thingsToDoNextCount:t}=e;const{ordersToProcessCount:n,reviewsToModerateCount:m,stockNoticesCount:H,isSetupTaskListHidden:_,isExtendedTaskListHidden:v}=(0,c.useSelect)((e=>{var t,n;const{getTaskList:o}=e(l.ONBOARDING_STORE_NAME),r=(0,g.sg)(e);return{ordersToProcessCount:(0,g.xX)(e,r),reviewsToModerateCount:(0,f.Vh)(e),stockNoticesCount:(0,g.ae)(e),isSetupTaskListHidden:null===(t=o("setup"))||void 0===t?void 0:t.isHidden,isExtendedTaskListHidden:null===(n=o("extended"))||void 0===n?void 0:n.isHidden}})),E=e=>{(0,i.recordEvent)("activity_panel_click",{task:e})},{Slot:L}=(0,C.createSlotFill)(w),b=(0,h.isWCAdmin)();return(0,o.createElement)("div",{className:"woocommerce-abbreviated-notifications"},t>0&&!v&&(0,o.createElement)(s.AbbreviatedCard,{className:"woocommerce-abbreviated-notification",icon:(0,o.createElement)(y,null),href:"admin.php?page=wc-admin#extended_task_list",onClick:()=>E("thingsToDoNext"),type:b?"wc-admin":"wp-admin"},(0,o.createElement)(a.Text,{as:"h3"},(0,r.__)("Things to do next","woocommerce")),(0,o.createElement)(a.Text,{as:"p"},(0,r.sprintf)((0,r._n)("You have %d new thing to do","You have %d new things to do",t,"woocommerce"),t))),n>0&&_&&(0,o.createElement)(s.AbbreviatedCard,{className:"woocommerce-abbreviated-notification",icon:d.Z,href:"admin.php?page=wc-admin&opened_panel=orders-panel",onClick:()=>E("ordersToProcess"),type:b?"wc-admin":"wp-admin"},(0,o.createElement)(a.Text,{as:"h3"},(0,r.__)("Orders to fulfill","woocommerce")),(0,o.createElement)(a.Text,null,(0,r.sprintf)((0,r._n)("You have %d order to fulfill","You have %d orders to fulfill",n,"woocommerce"),n))),m>0&&_&&(0,o.createElement)(s.AbbreviatedCard,{className:"woocommerce-abbreviated-notification",icon:u,href:"admin.php?page=wc-admin&opened_panel=reviews-panel",onClick:()=>E("reviewsToModerate"),type:b?"wc-admin":"wp-admin"},(0,o.createElement)(a.Text,{as:"h3"},(0,r.__)("Reviews to moderate","woocommerce")),(0,o.createElement)(a.Text,null,(0,r.sprintf)((0,r._n)("You have %d review to moderate","You have %d reviews to moderate",m,"woocommerce"),m))),H>0&&_&&(0,o.createElement)(s.AbbreviatedCard,{className:"woocommerce-abbreviated-notification",icon:p.Z,href:"admin.php?page=wc-admin&opened_panel=stock-panel",onClick:()=>E("stockNotices"),type:b?"wc-admin":"wp-admin"},(0,o.createElement)(a.Text,{as:"h3"},(0,r.__)("Inventory to review","woocommerce")),(0,o.createElement)(a.Text,null,(0,r.__)("You have inventory to review and update","woocommerce"))),!v&&(0,o.createElement)(L,null))}},18210:(e,t,n)=>{"use strict";n.d(t,{Z:()=>y});var o=n(65736),r=n(92694),a=n(69307),i=n(73463);const s=(0,a.lazy)((()=>Promise.all([n.e(5838),n.e(3994)]).then(n.bind(n,38304)))),c=(0,a.lazy)((()=>Promise.all([n.e(5838),n.e(8758),n.e(6824)]).then(n.bind(n,57267)))),l=(0,a.lazy)((()=>Promise.all([n.e(5838),n.e(8758),n.e(9456)]).then(n.bind(n,62365)))),m=(0,a.lazy)((()=>Promise.all([n.e(5838),n.e(9616),n.e(3576)]).then(n.bind(n,9033)))),u=(0,a.lazy)((()=>Promise.all([n.e(5838),n.e(8758),n.e(185)]).then(n.bind(n,95084)))),d=(0,a.lazy)((()=>Promise.all([n.e(5838),n.e(4011)]).then(n.bind(n,15523)))),p=(0,a.lazy)((()=>Promise.all([n.e(5838),n.e(9792)]).then(n.bind(n,88414)))),C=(0,a.lazy)((()=>Promise.all([n.e(5838),n.e(4854)]).then(n.bind(n,54536)))),h=(0,a.lazy)((()=>Promise.all([n.e(5838),n.e(2502)]).then(n.bind(n,51734)))),g=(0,a.lazy)((()=>Promise.all([n.e(5838),n.e(3700)]).then(n.bind(n,60455)))),f=(0,i.O3)("manageStock","no"),y=()=>{const e=[{report:"revenue",title:(0,o.__)("Revenue","woocommerce"),component:s,navArgs:{id:"woocommerce-analytics-revenue"}},{report:"products",title:(0,o.__)("Products","woocommerce"),component:c,navArgs:{id:"woocommerce-analytics-products"}},{report:"variations",title:(0,o.__)("Variations","woocommerce"),component:l,navArgs:{id:"woocommerce-analytics-variations"}},{report:"orders",title:(0,o.__)("Orders","woocommerce"),component:m,navArgs:{id:"woocommerce-analytics-orders"}},{report:"categories",title:(0,o.__)("Categories","woocommerce"),component:u,navArgs:{id:"woocommerce-analytics-categories"}},{report:"coupons",title:(0,o.__)("Coupons","woocommerce"),component:d,navArgs:{id:"woocommerce-analytics-coupons"}},{report:"taxes",title:(0,o.__)("Taxes","woocommerce"),component:p,navArgs:{id:"woocommerce-analytics-taxes"}},"yes"===f?{report:"stock",title:(0,o.__)("Stock","woocommerce"),component:h,navArgs:{id:"woocommerce-analytics-stock"}}:null,{report:"customers",title:(0,o.__)("Customers","woocommerce"),component:g},{report:"downloads",title:(0,o.__)("Downloads","woocommerce"),component:C,navArgs:{id:"woocommerce-analytics-downloads"}}].filter(Boolean);return(0,r.applyFilters)("woocommerce_admin_reports_list",e)}},49850:(e,t,n)=>{"use strict";n.d(t,{ex:()=>u,vc:()=>g});var o=n(69307),r=n(65736),a=n(92694),i=n(75283),s=n(86020),c=n(67221),l=n(81921);var m=n(73463);const u=["processing","on-hold"],d=["completed","processing","refunded","cancelled","failed","pending","on-hold"],p=Object.keys(m.rq).filter((e=>"refunded"!==e)).map((e=>({value:e,label:m.rq[e],description:(0,r.sprintf)((0,r.__)("Exclude the %s status from reports","woocommerce"),m.rq[e])}))),C=(0,m.O3)("unregisteredOrderStatuses",{}),h=[{key:"defaultStatuses",options:p.filter((e=>d.includes(e.value)))},{key:"customStatuses",label:(0,r.__)("Custom Statuses","woocommerce"),options:p.filter((e=>!d.includes(e.value)))},{key:"unregisteredStatuses",label:(0,r.__)("Unregistered Statuses","woocommerce"),options:Object.keys(C).map((e=>({value:e,label:e,description:(0,r.sprintf)((0,r.__)("Exclude the %s status from reports","woocommerce"),e)})))}],g=(0,a.applyFilters)("woocommerce_admin_analytics_settings",{woocommerce_excluded_report_order_statuses:{label:(0,r.__)("Excluded statuses:","woocommerce"),inputType:"checkboxGroup",options:h,helpText:(0,i.Z)({mixedString:(0,r.__)("Orders with these statuses are excluded from the totals in your reports. The {{strong}}Refunded{{/strong}} status can not be excluded.","woocommerce"),components:{strong:(0,o.createElement)("strong",null)}}),defaultValue:["pending","cancelled","failed"]},woocommerce_actionable_order_statuses:{label:(0,r.__)("Actionable statuses:","woocommerce"),inputType:"checkboxGroup",options:h,helpText:(0,r.__)("Orders with these statuses require action on behalf of the store admin. These orders will show up in the Home Screen - Orders task.","woocommerce"),defaultValue:u},woocommerce_default_date_range:{name:"woocommerce_default_date_range",label:(0,r.__)("Default date range:","woocommerce"),inputType:"component",component:e=>{let{value:t,onChange:n}=e;const{wcAdminSettings:r}=(0,c.useSettings)("wc_admin",["wcAdminSettings"]),{woocommerce_default_date_range:a}=r,i=Object.fromEntries(new URLSearchParams(t.replace(/&amp;/g,"&"))),{period:m,compare:u,before:d,after:p}=(0,l.getDateParamsFromQuery)(i,a),{primary:C,secondary:h}=(0,l.getCurrentDates)(i,a),g={period:m,compare:u,before:d,after:p,primaryDate:C,secondaryDate:h};return(0,o.createElement)(s.DateRangeFilterPicker,{query:i,onRangeSelect:e=>{n({target:{name:"woocommerce_default_date_range",value:new URLSearchParams(e).toString()}})},dateQuery:g,isoDateFormat:l.isoDateFormat})},helpText:(0,r.__)("Select a default date range. When no range is selected, reports will be viewed by the default date range.","woocommerce"),defaultValue:"period=month&compare=previous_year"},woocommerce_date_type:{name:"woocommerce_date_type",label:(0,r.__)("Date type:","woocommerce"),inputType:"select",options:[{label:(0,r.__)("Select a date type","woocommerce"),value:"",disabled:!0},{label:(0,r.__)("Date created","woocommerce"),value:"date_created",key:"date_created"},{label:(0,r.__)("Date paid","woocommerce"),value:"date_paid",key:"date_paid"},{label:(0,r.__)("Date completed","woocommerce"),value:"date_completed",key:"date_completed"}],helpText:(0,r.__)("Database date field considered for Revenue and Orders reports","woocommerce")}})},64214:(e,t,n)=>{"use strict";n.d(t,{Z:()=>d});var o=n(69307),r=n(65736),a=n(55609),i=n(94333),s=n(7862),c=n.n(s),l=n(9818),m=n(67221);class u extends o.Component{constructor(e){super(e),this.state={isConnecting:!1},this.connectJetpack=this.connectJetpack.bind(this),e.setIsPending(!0)}componentDidUpdate(e){const{createNotice:t,error:n,isRequesting:o,onError:r,setIsPending:a}=this.props;e.isRequesting&&!o&&a(!1),n&&n!==e.error&&(r&&r(),t("error",n))}async connectJetpack(){const{jetpackConnectUrl:e,onConnect:t}=this.props;this.setState({isConnecting:!0},(()=>{t&&t(),window.location=e}))}render(){const{hasErrors:e,isRequesting:t,onSkip:n,skipText:i,onAbort:s,abortText:c}=this.props;return(0,o.createElement)(o.Fragment,null,e?(0,o.createElement)(a.Button,{isPrimary:!0,onClick:()=>window.location.reload()},(0,r.__)("Retry","woocommerce")):(0,o.createElement)(a.Button,{disabled:t,isBusy:this.state.isConnecting,isPrimary:!0,onClick:this.connectJetpack},(0,r.__)("Connect","woocommerce")),n&&(0,o.createElement)(a.Button,{onClick:n},i||(0,r.__)("No thanks","woocommerce")),s&&(0,o.createElement)(a.Button,{onClick:s},c||(0,r.__)("Abort","woocommerce")))}}u.propTypes={createNotice:c().func.isRequired,error:c().string,hasErrors:c().bool,isRequesting:c().bool,jetpackConnectUrl:c().string,onConnect:c().func,onError:c().func,onSkip:c().func,redirectUrl:c().string,skipText:c().string,setIsPending:c().func,onAbort:c().func,abortText:c().string},u.defaultProps={setIsPending:()=>{}};const d=(0,i.compose)((0,l.withSelect)(((e,t)=>{const{getJetpackConnectUrl:n,isPluginsRequesting:o,getPluginsError:r}=e(m.PLUGINS_STORE_NAME),a={redirect_url:t.redirectUrl||window.location.href},i=o("getJetpackConnectUrl");return{error:r("getJetpackConnectUrl")||"",isRequesting:i,jetpackConnectUrl:n(a)}})),(0,l.withDispatch)((e=>{const{createNotice:t}=e("core/notices");return{createNotice:t}})))(u)},11891:(e,t,n)=>{"use strict";n.d(t,{IO:()=>g,Yt:()=>p});var o=n(5267),r=n(69307),a=n(65736),i=n(67221),s=n(22629),c=n(92819),l=n(86020),m=n(55609),u=n(9818);const d=["addressLine1","addressLine2","city","countryState","postCode"];function p(){return e=>{const t={};return e.countryState.trim().length||(t.countryState=(0,a.__)("Please select a country / region","woocommerce")),t}}const C=e=>e.replace(/\s/g,"").toLowerCase(),h=(e,t)=>n=>{const o=e?n.key.split(":"):n.label.split("—");if(o.length<=1)return!1;const r=o[1];if(r.includes("/")){const e=r.split("/");return C(e[0])===t||C(e[1])===t}if(r.includes("(")&&r.includes(")")){const e=r.replace(")","").split("(");return C(e[0])===t||C(e[1])===t}return C(r)===t};function g(e){var t,n,p,g,f,y;let{getInputProps:w,setValue:H}=e;const _=w("countryState").value,{locale:v,hasFinishedResolution:E,countries:L,loadingCountries:b}=(0,u.useSelect)((e=>{const{getLocale:t,getCountries:n,hasFinishedResolution:o}=e(i.COUNTRIES_STORE_NAME);return{locale:t(_),countries:n(),loadingCountries:!o("getCountries"),hasFinishedResolution:o("getLocales")}})),k=(0,r.useMemo)((()=>function(e){return e.reduce(((e,t)=>{if(!t.states.length)return e.push({key:t.code,label:(0,s.decodeEntities)(t.name)}),e;const n=t.states.map((e=>({key:t.code+":"+e.code,label:(0,s.decodeEntities)(t.name)+" — "+(0,s.decodeEntities)(e.name)})));return e.push(...n),e}),[])}(L)),[L]),S=function(e,t,n){const[o,a]=(0,r.useState)(""),[i,s]=(0,r.useState)(""),l=(0,r.useRef)();return(0,r.useEffect)((()=>{if(!l.current){const n=e.find((e=>e.key===t)),r=n?n.label.split(/\u2013|\u2014|\-/):[],c=(r[0]||"").trim(),l=(r[1]||"").trim();c===o&&l===i||(a(c),s(l))}l.current=!1}),[t,e]),(0,r.useEffect)((()=>{if(void 0===l.current)return;if(!o&&!i&&t)return l.current=!0,void n("countryState","");const r=new RegExp((0,c.escapeRegExp)(o),"i"),a=o.length<3,s=i.length<3&&!!i.match(/^[\w]+$/);let m=[];o.length&&i.length?(m=e.filter((e=>r.test(a?e.key:e.label))),m.length||(m=[...e]),m.length>1&&(m=m.filter(h(s,C(i))))):o.length?m=e.filter((e=>r.test(a?e.key:e.label))):i.length&&(m=e.filter(h(s,C(i)))),1===m.length&&t!==m[0].key&&(l.current=!0,n("countryState",m[0].key))}),[o,i,e,n]),(0,r.createElement)(r.Fragment,null,(0,r.createElement)("input",{onChange:e=>a(e.target.value),value:o,name:"country",type:"text",className:"woocommerce-select-control__autofill-input",tabIndex:-1,autoComplete:"country"}),(0,r.createElement)("input",{onChange:e=>s(e.target.value),value:i,name:"state",type:"text",className:"woocommerce-select-control__autofill-input",tabIndex:-1,autoComplete:"address-level1"}))}(k,_,H);return(0,r.useEffect)((()=>{v&&d.forEach((e=>{var t,n;const o=e.replace(/(address)Line([0-9])/,"$1$2").toLowerCase(),r=w(e);var a;a=o,v.hasOwnProperty(a)&&null!==(t=v[o])&&void 0!==t&&t.hidden&&(null===(n=r.value)||void 0===n?void 0:n.length)>0&&H(e,"")}))}),[_,v]),!E||b?(0,r.createElement)(m.Spinner,null):(0,r.createElement)("div",{className:"woocommerce-store-address-fields"},(0,r.createElement)(l.SelectControl,(0,o.Z)({label:(0,a.__)("Country / Region","woocommerce")+" *",required:!0,autoComplete:"new-password",getSearchExpression:e=>new RegExp("(^"+e+"| — ("+e+"))","i"),options:k,excludeSelectedOptions:!1,showAllOnFocus:!0,isSearchable:!0},w("countryState"),{controlClassName:w("countryState").className}),S),!(null!=v&&null!==(t=v.address_1)&&void 0!==t&&t.hidden)&&(0,r.createElement)(l.TextControl,(0,o.Z)({label:(null==v||null===(n=v.address_1)||void 0===n?void 0:n.label)||(0,a.__)("Address","woocommerce"),autoComplete:"address-line1"},w("addressLine1"))),!(null!=v&&null!==(p=v.postcode)&&void 0!==p&&p.hidden)&&(0,r.createElement)(l.TextControl,(0,o.Z)({label:(null==v||null===(g=v.postcode)||void 0===g?void 0:g.label)||(0,a.__)("Post code","woocommerce"),autoComplete:"postal-code"},w("postCode"))),!(null!=v&&null!==(f=v.city)&&void 0!==f&&f.hidden)&&(0,r.createElement)(l.TextControl,(0,o.Z)({label:(null==v||null===(y=v.city)||void 0===y?void 0:y.label)||(0,a.__)("City","woocommerce")},w("city"),{autoComplete:"address-level2"})))}},82580:(e,t,n)=>{"use strict";n.d(t,{jt:()=>c,so:()=>a,w:()=>i}),n(22629);var o=n(92819),r=n(73463);function a(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";return e?e.split(":")[0]:null}function i(e){let t=a(e);const n=(0,o.without)((0,r.O3)("onboarding",{euCountries:[]}).euCountries,"GB");return null!==t&&n.includes(t)&&(t="EU"),t}function s(e){let t=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=arguments.length>2?arguments[2]:void 0,o=arguments.length>3?arguments[3]:void 0;const r=[];if(!o)return r;const a=e.product_types||[];return a.forEach((e=>{o[e]&&o[e].product&&(t||!n.includes(o[e].slug))&&r.push(o[e])})),r}function c(e,t){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2],o=arguments.length>3?arguments[3]:void 0;const r=s(t,n,o,e),a=r.map((e=>e.id||e.product));return a}},22983:(e,t,n)=>{"use strict";n.d(t,{Ox:()=>s,ae:()=>c,sg:()=>i,xX:()=>a});var o=n(67221),r=n(49850);function a(e,t){const{getItemsTotalCount:n,getItemsError:r,isResolving:a}=e(o.ITEMS_STORE_NAME);if(!t.length)return 0;const i={page:1,per_page:1,status:t,_fields:["id"]},s=n("orders",i,null),c=Boolean(r("orders",i)),l=a("getItemsTotalCount",["orders",i,null]);return c||l?null:s}function i(e){const{getSetting:t}=e(o.SETTINGS_STORE_NAME),{woocommerce_actionable_order_statuses:n=r.ex}=t("wc_admin","wcAdminSettings",{});return n}const s={page:1,per_page:1,low_in_stock:!0,status:"publish",_fields:["id"]};function c(e){const{getItemsTotalCount:t,getItemsError:n,isResolving:r}=e(o.ITEMS_STORE_NAME),a=null,i=t("products/low-in-stock",s,a),c=Boolean(n("products/low-in-stock",s)),l=r("getItemsTotalCount",["products/low-in-stock",s,a]);return c||l&&i===a?null:i}},14138:(e,t,n)=>{"use strict";n.d(t,{Vh:()=>i,X6:()=>r,wY:()=>a});var o=n(67221);const r=5,a={page:1,per_page:1,status:"hold",_embed:1,_fields:["id"]};function i(e){const{getReviewsTotalCount:t,getReviewsError:n,isResolving:r}=e(o.REVIEWS_STORE_NAME),i=t(a),s=Boolean(n(a)),c=r("getReviewsTotalCount",[a]);return s||c&&void 0===i?null:i}},23322:(e,t,n)=>{"use strict";n.d(t,{fs:()=>i,kS:()=>s,r7:()=>m});var o=n(92819),r=n(49248),a=n.n(r);function i(e,t){return(0,o.filter)(e,(e=>{const{is_deleted:n,date_created_gmt:o,status:r}=e;if(!n)return(!t||!o||new Date(o+"Z").getTime()>t)&&"unactioned"===r})).length}function s(e){return(0,o.filter)(e,(e=>{const{is_deleted:t}=e;return!t})).length>0}const c=function(e,t){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:" ",o=e.slice(0,t);if(e.indexOf(n,t)!==t){const e=o.lastIndexOf(n);e>-1&&(o=o.slice(0,e))}return o.join("")},l=(e,t)=>{const n=document.createElement("div"),o=Array.from(e.childNodes),r=new(a());let i=0;for(let e=0;e<o.length;e++){let a=o[e].cloneNode(!0);const s=r.splitGraphemes(a.textContent);if(i+s.length<=t){n.appendChild(a),i+=s.length;continue}const m=t-i;a.hasChildNodes()?a=l(a,m):a.textContent=c(s,m),n.appendChild(a);break}return n},m=(e,t)=>{const n=document.createElement("div"),o=new(a());return n.innerHTML=e,o.splitGraphemes(n.textContent).length>t?l(n,t).innerHTML+"...":e}},74816:(e,t,n)=>{"use strict";n.d(t,{E:()=>s});var o=n(69307),r=n(65736),a=n(55609),i=n(14812);const s=()=>(0,o.createElement)("div",{className:"woocommerce-layout__no-match"},(0,o.createElement)(a.Card,null,(0,o.createElement)(a.CardBody,null,(0,o.createElement)(i.Text,null,(0,r.__)("Sorry, you are not allowed to access this page.","woocommerce")))))},25753:(e,t,n)=>{"use strict";n.d(t,{C:()=>a});var o=n(96483),r=n(74617);const a=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{pathname:n,search:a}=window.location,i=(0,r.getSetting)("connectNonce","");return t={"wccom-site":(0,r.getSetting)("siteUrl"),"wccom-back":n+a,"wccom-woo-version":(0,r.getSetting)("wcVersion"),"wccom-connect-nonce":i,...t},(0,o.addQueryArgs)(e,t)}},34704:(e,t,n)=>{"use strict";n.d(t,{a:()=>r});var o=n(9818);function r(e){const{createNotice:t}=(0,o.dispatch)("core/notices");e.error_data&&e.errors&&Object.keys(e.errors).length?Object.keys(e.errors).forEach((n=>{t("error",e.errors[n].join(" "))})):e.message&&t(e.code?"error":"success",e.message)}},32008:(e,t,n)=>{"use strict";n.d(t,{ZP:()=>i});var o=n(74944);const r=["a","b","em","i","strong","p","br"],a=["target","href","rel","name","download"],i=e=>({__html:(0,o.sanitize)(e,{ALLOWED_TAGS:r,ALLOWED_ATTR:a})})},90523:(e,t,n)=>{"use strict";n.d(t,{j:()=>o});const o=e=>e&&1===e.filter((e=>0===e.id.indexOf("woocommerce_payments"))).length},74693:(e,t,n)=>{"use strict";n.d(t,{A:()=>r});var o=n(69307);const r=()=>(0,o.createElement)("svg",{width:"16",height:"17",viewBox:"0 0 16 17",xmlns:"http://www.w3.org/2000/svg"},(0,o.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M2.68822 12.625L1.5 13.8145L1.5 1.5L14.5 1.5L14.5 12.625L2.68822 12.625ZM3.31 14.125L15 14.125C15.5523 14.125 16 13.6773 16 13.125L16 1C16 0.447717 15.5523 0 15 0H1C0.447717 0 0 0.447716 0 1V15.5247C0 15.8173 0.161234 16.086 0.419354 16.2237C0.727111 16.3878 1.10601 16.3313 1.35252 16.0845L3.31 14.125ZM12 5.99997H4V4.49997H12V5.99997ZM4 9.99997H9V8.49997H4V9.99997Z",fill:"currentColor"}))},3816:(e,t,n)=>{"use strict";n.d(t,{Z:()=>p});var o=n(69307),r=n(65736),a=n(94333),i=n(9818),s=n(75283),c=n(55609),l=n(86020),m=n(67221),u=n(73516);class d extends o.Component{constructor(e){super(e),this.state={isLoadingScripts:!1,isRequestStarted:!1,selectedAction:null}}async componentDidUpdate(e,t){const{hasErrors:n,isRequesting:o,onClose:a,onContinue:i,createNotice:s}=this.props,{isLoadingScripts:c,isRequestStarted:l}=this.state;if(!l)return;const m=!o&&!c&&(e.isRequesting||t.isLoadingScripts)&&!n,u=!o&&e.isRequesting&&n;m&&(a(),i()),u&&(s("error",(0,r.__)("There was a problem updating your preferences","woocommerce")),a())}updateTracking(e){let{allowTracking:t}=e;const{updateOptions:n}=this.props;t&&"function"==typeof window.wcTracks.enable?(this.setState({isLoadingScripts:!0}),window.wcTracks.enable((()=>{this._isMounted&&((0,u.initializeExPlat)(),this.setState({isLoadingScripts:!1}))}))):t||(window.wcTracks.isEnabled=!1);const o=t?"yes":"no";this.setState({isRequestStarted:!0}),n({woocommerce_allow_tracking:o})}componentDidMount(){this._isMounted=!0}componentWillUnmount(){this._isMounted=!1}render(){const{allowTracking:e,isResolving:t,onClose:n,onContinue:a}=this.props;if(t)return null;if(e)return n(),a(),null;const{isRequesting:i,title:m=(0,r.__)("Build a better WooCommerce","woocommerce"),message:u=(0,s.Z)({mixedString:(0,r.__)("Get improved features and faster fixes by sharing non-sensitive data via {{link}}usage tracking{{/link}} that shows us how WooCommerce is used. No personal data is tracked or stored.","woocommerce"),components:{link:(0,o.createElement)(l.Link,{href:"https://woocommerce.com/usage-tracking?utm_medium=product",target:"_blank",type:"external"})}}),dismissActionText:d=(0,r.__)("No thanks","woocommerce"),acceptActionText:p=(0,r.__)("Yes, count me in!","woocommerce")}=this.props,{isRequestStarted:C,selectedAction:h}=this.state,g=C&&i;return(0,o.createElement)(c.Modal,{title:m,isDismissible:this.props.isDismissible,onRequestClose:()=>this.props.onClose(),className:"woocommerce-usage-modal"},(0,o.createElement)("div",{className:"woocommerce-usage-modal__wrapper"},(0,o.createElement)("div",{className:"woocommerce-usage-modal__message"},u),(0,o.createElement)("div",{className:"woocommerce-usage-modal__actions"},(0,o.createElement)(c.Button,{isSecondary:!0,isBusy:g&&"dismiss"===h,disabled:g&&"accept"===h,onClick:()=>{this.setState({selectedAction:"dismiss"}),this.updateTracking({allowTracking:!1})}},d),(0,o.createElement)(c.Button,{isPrimary:!0,isBusy:g&&"accept"===h,disabled:g&&"dismiss"===h,onClick:()=>{this.setState({selectedAction:"accept"}),this.updateTracking({allowTracking:!0})}},p))))}}const p=(0,a.compose)((0,i.withSelect)((e=>{const{getOption:t,getOptionsUpdatingError:n,isOptionsUpdating:o,hasFinishedResolution:r}=e(m.OPTIONS_STORE_NAME);return{allowTracking:"yes"===t("woocommerce_allow_tracking"),isRequesting:Boolean(o()),isResolving:!r("getOption",["woocommerce_allow_tracking"])||void 0===t("woocommerce_allow_tracking"),hasErrors:Boolean(n())}})),(0,i.withDispatch)((e=>{const{createNotice:t}=e("core/notices"),{updateOptions:n}=e(m.OPTIONS_STORE_NAME);return{createNotice:t,updateOptions:n}})))(d)},52260:(e,t,n)=>{"use strict";n.r(t),n.d(t,{UsageModal:()=>l,default:()=>m});var o=n(69307),r=n(65736),a=n(10431),i=n(75283),s=n(86020),c=n(3816);const l=()=>{const e="1"===(0,a.getQuery)()["wcpay-connection-success"],[t,n]=(0,o.useState)(e);if(!t)return null;const l=()=>{n(!1),(0,a.updateQueryString)({"wcpay-connection-success":void 0})},m=(0,r.__)("Help us build a better WooPayments experience","woocommerce"),u=(0,i.Z)({mixedString:(0,r.__)("By agreeing to share non-sensitive {{link}}usage data{{/link}}, you’ll help us improve features and optimize the WooPayments experience. You can opt out at any time.","woocommerce"),components:{link:(0,o.createElement)(s.Link,{href:"https://woocommerce.com/usage-tracking?utm_medium=product",target:"_blank",type:"external"})}});return(0,o.createElement)(c.Z,{isDismissible:!1,title:m,message:u,acceptActionText:(0,r.__)("I agree","woocommerce"),dismissActionText:(0,r.__)("No thanks","woocommerce"),onContinue:l,onClose:l})},m=l},67097:(e,t,n)=>{"use strict";n.d(t,{L:()=>i});var o=n(69307),r=n(83849),a=n.n(r);const i=e=>{let{logo:t,description:n,layout:r="single",features:i,children:s}=e;return(0,o.createElement)("div",{className:a()("woocommerce-task-shipping-recommendation__plugins-install",r)},t&&(0,o.createElement)("div",{className:"plugins-install__plugin-banner-image"},(0,o.createElement)("img",{src:t.image,alt:null==t?void 0:t.alt})),n&&(0,o.createElement)("p",null,n),(0,o.createElement)("div",{className:"plugins-install__list"},i.map(((e,t)=>(0,o.createElement)("div",{className:"plugins-install__list-item",key:t},(0,o.createElement)("div",{className:"plugins-install__list-icon"},(0,o.createElement)("img",{src:e.icon,alt:""})),(0,o.createElement)("div",null,e.title&&(0,o.createElement)("div",null,(0,o.createElement)("strong",null,e.title)),(0,o.createElement)("div",null,e.description)))))),s)}},58670:(e,t,n)=>{"use strict";n.d(t,{Z:()=>u,z:()=>m});var o=n(69307),r=n(65736),a=n(55609),i=n(67221),s=n(86020),c=n(9818),l=n(11891);const m=e=>(0,l.Yt)()(e),u=e=>{let{onComplete:t,createNotice:n,isSettingsRequesting:u,updateAndPersistSettingsForGroup:d,settings:p,buttonText:C=(0,r.__)("Continue","woocommerce"),validate:h=m}=e;const{hasFinishedResolution:g}=(0,c.useSelect)((e=>{const t=e(i.COUNTRIES_STORE_NAME);return t.getCountries(),{getLocale:t.getLocale,locales:t.getLocales(),hasFinishedResolution:t.hasFinishedResolution("getLocales")&&t.hasFinishedResolution("getCountries")}})),[f,y]=(0,o.useState)(!1);return u||!g?(0,o.createElement)(s.Spinner,null):(0,o.createElement)(s.Form,{initialValues:{addressLine1:(null==p?void 0:p.woocommerce_store_address)||"",addressLine2:(null==p?void 0:p.woocommerce_store_address_2)||"",city:(null==p?void 0:p.woocommerce_store_city)||"",countryState:(null==p?void 0:p.woocommerce_default_country)||"",postCode:(null==p?void 0:p.woocommerce_store_postcode)||""},onSubmit:async e=>{y(!0);try{await d("general",{general:{...p,woocommerce_store_address:e.addressLine1,woocommerce_store_address_2:e.addressLine2,woocommerce_default_country:e.countryState,woocommerce_store_city:e.city,woocommerce_store_postcode:e.postCode}}),y(!1),t(e)}catch(e){y(!1),n("error",(0,r.__)("There was a problem saving your store location","woocommerce"))}},validate:h},(e=>{let{getInputProps:t,handleSubmit:n,setValue:r}=e;return(0,o.createElement)(o.Fragment,null,(0,o.createElement)(l.IO,{getInputProps:t,setValue:r}),(0,o.createElement)(a.Button,{isPrimary:!0,onClick:n,isBusy:f},C))}))}},33414:(e,t,n)=>{"use strict";n.d(t,{AO:()=>a,_9:()=>i,c7:()=>r});var o=n(74617);const r=["jetpack","woocommerce-services"],a=e=>{const{woocommerce_store_address:t,woocommerce_default_country:n,woocommerce_store_postcode:o}=e;return Boolean(t&&n&&o)},i=()=>{window.location.href=(0,o.getAdminLink)("admin.php?page=wc-settings&tab=tax&section=standard&wc_onboarding_active_task=tax")}},70123:(e,t,n)=>{"use strict";n.r(t),n.d(t,{ProgressTitle:()=>Nt,TaskLists:()=>_t,TasksPlaceholder:()=>yt,TasksReminderBar:()=>bt,useActiveSetupTasklist:()=>xt});var o=n(73463);const r=(0,o.O3)("onboarding");var a=n(69307),i=n(65736),s=n(9818),c=n(67221),l=n(14599),m=n(98817),u=n(5060),d=n(10431),p=n(55609),C=n(72672),h=n(83849),g=n.n(h),f=n(86020),y=n(14812),w=n(34374);const H=e=>{let{hasSetup:t=!1,needsSetup:n=!0,id:o,isEnabled:r=!1,isLoading:s=!1,isInstalled:c=!1,isRecommended:m=!1,hasPlugins:u,manageUrl:C=null,markConfigured:h,onSetUp:g=(()=>{}),onSetupCallback:f,setupButtonText:y=(0,i.__)("Get started","woocommerce"),externalLink:H=null}=e;const[_,v]=(0,a.useState)(!1),E="woocommerce-task-payment__action";if(s)return(0,a.createElement)(p.Spinner,null);const L=async()=>{if(g(o),(0,l.recordEvent)("tasklist_payment_setup",{selected:(0,w.br)(o)}),u||!H)return f?(v(!0),void await new Promise(f).then((()=>{v(!1)})).catch((()=>{v(!1)}))):void(0,d.updateQueryString)({id:o});window.location.href=H},b=()=>(0,a.createElement)(p.Button,{className:E,isSecondary:!0,role:"button",href:C,onClick:()=>(0,l.recordEvent)("tasklist_payment_manage",{id:o})},(0,i.__)("Manage","woocommerce")),k=()=>(0,a.createElement)(p.Button,{className:E,isPrimary:m,isSecondary:!m,isBusy:_,disabled:_,onClick:()=>L()},y),S=()=>(0,a.createElement)(p.Button,{className:E,isSecondary:!0,onClick:()=>h(o)},(0,i.__)("Enable","woocommerce"));return t?u?n?c&&u?(0,a.createElement)(p.Button,{className:E,isPrimary:m,isSecondary:!m,isBusy:_,disabled:_,onClick:()=>L()},(0,i.__)("Finish setup","woocommerce")):(0,a.createElement)(k,null):r?(0,a.createElement)(b,null):(0,a.createElement)(S,null):r?(0,a.createElement)(b,null):(0,a.createElement)(k,null):r?(0,a.createElement)(b,null):(0,a.createElement)(S,null)};var _=n(67630);const v=e=>{var t,n;let{isRecommended:o,markConfigured:r,paymentGateway:s}=e;const{image_72x72:c,content:l,id:m,plugins:d=[],title:C,loading:h,enabled:w=!1,installed:v=!1,needsSetup:E=!0,requiredSettings:L,settingsUrl:b,is_local_partner:k,external_link:S,transaction_processors:M}=s,N=(0,y.useSlot)(`woocommerce_payment_gateway_configure_${m}`),x=(0,y.useSlot)(`woocommerce_payment_gateway_setup_${m}`),T=Boolean(null==N||null===(t=N.fills)||void 0===t?void 0:t.length)||Boolean(null==x||null===(n=x.fills)||void 0===n?void 0:n.length),I=Boolean(d.length||L.length||T||S),A=o&&E,O=g()("woocommerce-task-payment","woocommerce-task-card",E&&"woocommerce-task-payment-not-configured","woocommerce-task-payment-"+m);return(0,a.createElement)(a.Fragment,{key:m},(0,a.createElement)(p.CardBody,{style:{paddingLeft:0,marginBottom:0},className:O},(0,a.createElement)(p.CardMedia,{isBorderless:!0},(0,a.createElement)("img",{src:c,alt:C,onError:e=>e.currentTarget.src=_})),(0,a.createElement)("div",{className:"woocommerce-task-payment__description"},(0,a.createElement)(y.Text,{as:"h3",className:"woocommerce-task-payment__title"},(0,a.createElement)("span",null,C),A&&(0,a.createElement)(f.Pill,{className:!k&&"pill-green"},k?(0,i.__)("Local Partner","woocommerce"):(0,i.__)("Recommended","woocommerce")),v&&E&&!!d.length&&(0,a.createElement)(u.SetupRequired,null)),(0,a.createElement)("div",{className:"woocommerce-task-payment__content"},l),M&&(0,a.createElement)("div",{className:"woocommerce-task-payment__transaction-processors_images"},Object.keys(M).map((e=>(0,a.createElement)("img",{src:M[e],alt:e,key:e}))))),(0,a.createElement)("div",{className:"woocommerce-task-payment__footer"},(0,a.createElement)(H,{manageUrl:b,id:m,hasSetup:I,needsSetup:E,isEnabled:w,isInstalled:v,hasPlugins:Boolean(d.length),isRecommended:o,isLoading:h,markConfigured:r,externalLink:S}))),(0,a.createElement)(p.CardDivider,null))},E=e=>{let{heading:t,headingDescription:n,markConfigured:o,recommendation:r,paymentGateways:i,footerLink:s}=e;return(0,a.createElement)(p.Card,null,t&&(0,a.createElement)(p.CardHeader,{as:"h2"},t,n&&(0,a.createElement)("p",{className:"woocommerce-task-payment-header__description"},n)),i.map((e=>{const{id:t}=e;return(0,a.createElement)(v,{key:t,isRecommended:r===t,markConfigured:o,paymentGateway:e})})),s&&(0,a.createElement)(p.CardFooter,{isBorderless:!0},s))},L=()=>{const e=g()("woocommerce-task-payment","woocommerce-task-card");return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(p.CardBody,{style:{paddingLeft:0,marginBottom:0},className:e},(0,a.createElement)(p.CardMedia,{isBorderless:!0},(0,a.createElement)("span",{className:"is-placeholder"})),(0,a.createElement)("div",{className:"woocommerce-task-payment__description"},(0,a.createElement)(y.Text,{as:"h3",className:"woocommerce-task-payment__title"},(0,a.createElement)("span",{className:"is-placeholder"})),(0,a.createElement)("div",{className:"woocommerce-task-payment__content"},(0,a.createElement)("span",{className:"is-placeholder"}))),(0,a.createElement)("div",{className:"woocommerce-task-payment__footer"},(0,a.createElement)("span",{className:"is-placeholder"}))),(0,a.createElement)(p.CardDivider,null))},b=()=>(0,a.createElement)(p.Card,{"aria-hidden":"true",className:"is-loading woocommerce-payment-gateway-suggestions-list-placeholder"},(0,a.createElement)(p.CardHeader,{as:"h2"},(0,a.createElement)("span",{className:"is-placeholder"})),(0,a.createElement)(L,null),(0,a.createElement)(L,null),(0,a.createElement)(L,null));var k=n(34704),S=n(32008);const M=e=>{var t;let{markConfigured:n,paymentGateway:o}=e;const{id:r,connectionUrl:m,setupHelpText:d,settingsUrl:C,title:h,requiredSettings:g}=o,{createNotice:w}=(0,s.useDispatch)("core/notices"),{updatePaymentGateway:H}=(0,s.useDispatch)(c.PAYMENT_GATEWAYS_STORE_NAME),_=(0,y.useSlot)(`woocommerce_payment_gateway_configure_${r}`),v=Boolean(null==_||null===(t=_.fills)||void 0===t?void 0:t.length),{isUpdating:E}=(0,s.useSelect)((e=>{const{isPaymentGatewayUpdating:t}=e(c.PAYMENT_GATEWAYS_STORE_NAME);return{isUpdating:t()}})),L=e=>{H(r,{enabled:!0,settings:e}).then((e=>{e&&e.id===r&&(n(r),w("success",(0,i.sprintf)((0,i.__)("%s configured successfully","woocommerce"),h)))})).catch((()=>{w("error",(0,i.__)("There was a problem saving your payment settings","woocommerce"))}))},b=d&&(0,a.createElement)("p",{dangerouslySetInnerHTML:(0,S.ZP)(d)}),k=(0,a.createElement)(f.DynamicForm,{fields:g,isBusy:E,onSubmit:L,submitLabel:(0,i.__)("Continue","woocommerce"),validate:e=>((e,t)=>{const n={},o=e=>t.find((t=>t.id===e));for(const[t,r]of Object.entries(e)){const e=o(t),a=e.label.replace(/([A-Z][a-z]+)/g,(e=>e.toLowerCase()));r||"checkbox"===e.type||(n[t]=`Please enter your ${a}`)}return n})(e,g)});return v?(0,a.createElement)(u.WooPaymentGatewayConfigure.Slot,{fillProps:{defaultForm:k,defaultSubmit:L,defaultFields:g,markConfigured:()=>n(r),paymentGateway:o},id:r}):m?(0,a.createElement)(a.Fragment,null,b,(0,a.createElement)(p.Button,{isPrimary:!0,onClick:()=>(0,l.recordEvent)("tasklist_payment_connect_start",{payment_method:r}),href:m},(0,i.__)("Connect","woocommerce"))):g.length?(0,a.createElement)(a.Fragment,null,b,k):(0,a.createElement)(a.Fragment,null,b||(0,a.createElement)("p",null,(0,i.__)("You can manage this payment gateway's settings by clicking the button below","woocommerce")),(0,a.createElement)(p.Button,{isPrimary:!0,href:C},(0,i.__)("Get started","woocommerce")))},N=e=>{var t;let{markConfigured:n,paymentGateway:o}=e;const{id:r,plugins:m=[],title:d,postInstallScripts:C,installed:h}=o,g=(0,y.useSlot)(`woocommerce_payment_gateway_setup_${r}`),w=Boolean(null==g||null===(t=g.fills)||void 0===t?void 0:t.length),[H,_]=(0,a.useState)(!1);(0,a.useEffect)((()=>{(0,l.recordEvent)("payments_task_stepper_view",{payment_method:r})}),[]);const{invalidateResolutionForStoreSelector:v}=(0,s.useDispatch)(c.PAYMENT_GATEWAYS_STORE_NAME),{isOptionUpdating:E,isPaymentGatewayResolving:L,needsPluginInstall:b}=(0,s.useSelect)((e=>{const{isOptionsUpdating:t}=e(c.OPTIONS_STORE_NAME),{isResolving:n}=e(c.PAYMENT_GATEWAYS_STORE_NAME),o=e(c.PLUGINS_STORE_NAME).getActivePlugins(),r=m.filter((e=>!o.includes(e)));return{isOptionUpdating:t(),isPaymentGatewayResolving:n("getPaymentGateways"),needsPluginInstall:!!r.length}}));(0,a.useEffect)((()=>{if(!b)if(C&&C.length){const e=C.map((e=>function(e){return new Promise(((t,n)=>{document.querySelector(`#${e.handle}-js`)&&t();const o=document.createElement("script");o.src=e.src,o.id=`${e.handle}-js`,o.async=!0,o.onload=t,o.onerror=n,document.body.appendChild(o)}))}(e)));Promise.all(e).then((()=>{_(!0)}))}else _(!0)}),[C,b]);const S=(0,a.useMemo)((()=>m&&m.length?{key:"install",label:(0,i.sprintf)((0,i.__)("Install %s","woocommerce"),d),content:(0,a.createElement)(f.Plugins,{onComplete:(e,t)=>{(0,k.a)(t),v("getPaymentGateways"),(0,l.recordEvent)("tasklist_payment_install_method",{plugins:m})},onError:(e,t)=>(0,k.a)(t),autoInstall:!0,pluginSlugs:m})}:null),[]),N=(0,a.useMemo)((()=>({key:"configure",label:(0,i.sprintf)((0,i.__)("Configure your %(title)s account","woocommerce"),{title:d}),content:h?(0,a.createElement)(M,{markConfigured:n,paymentGateway:o}):null})),[h]),x=b||E||L||!H,T=(0,a.createElement)(f.Stepper,{isVertical:!0,isPending:x,currentStep:b?"install":"configure",steps:[S,N].filter(Boolean)});return(0,a.createElement)(p.Card,{className:"woocommerce-task-payment-method woocommerce-task-card"},(0,a.createElement)(p.CardBody,null,w?(0,a.createElement)(u.WooPaymentGatewaySetup.Slot,{fillProps:{defaultStepper:T,defaultInstallStep:S,defaultConfigureStep:N,markConfigured:()=>n(r),paymentGateway:o},id:r}):T))},x=()=>{const e=g()("is-loading","woocommerce-task-payment-method","woocommerce-task-card");return(0,a.createElement)(p.Card,{"aria-hidden":"true",className:e},(0,a.createElement)(p.CardBody,null,(0,a.createElement)(f.Stepper,{isVertical:!0,currentStep:"none",steps:[{key:"first",label:""},{key:"second",label:""}]})))};var T=n(86989),I=n.n(T);function A(e,t){const n=(0,i.__)("There was an error connecting to WooPayments. Please try again or connect later in store settings.","woocommerce");I()({path:c.WC_ADMIN_NAMESPACE+"/plugins/connect-wcpay",method:"POST"}).then((e=>{window.location=e.connectUrl})).catch((()=>{e("error",n),"function"==typeof t&&t()}))}function O(e,t,n){n(["woocommerce-payments"]).then((()=>{(0,l.recordEvent)("woocommerce_payments_install",{context:"tasklist"}),A(t,(()=>{e()}))})).catch((t=>{(0,k.a)(t),e()}))}const P=e=>{let{paymentGateway:t,onSetupCallback:n=null}=e;const{id:r,needsSetup:c,installed:l,enabled:m,installed:d}=t,p=(0,o.O3)("isWooPayEligible"),{createNotice:C}=(0,s.useDispatch)("core/notices");return l&&null===n&&(n=()=>{A(C)}),(0,a.createElement)("div",{className:"woocommerce-wcpay-suggestion"},(0,a.createElement)(u.WCPayBanner,null,(0,a.createElement)(u.WCPayBannerBody,{textPosition:"left",actionButton:(0,a.createElement)(H,{id:r,hasSetup:!0,needsSetup:c,isEnabled:m,isRecommended:!0,isInstalled:d,hasPlugins:!0,setupButtonText:(0,i.__)("Get started","woocommerce"),onSetupCallback:n}),bannerImage:(0,a.createElement)(u.WCPayBannerImageCut,null),isWooPayEligible:p}),(0,a.createElement)(u.WCPayBenefits,{isWooPayEligible:p}),(0,a.createElement)(u.WCPayBannerFooter,{isWooPayEligible:p})))};n(52260);var V=n(82580);const R=(e,t)=>e.recommendation_priority-t.recommendation_priority,j=e=>{var t;return 1===(null===(t=e.plugins)||void 0===t?void 0:t.length)&&"woocommerce-payments"===e.plugins[0]},D=(e,t)=>e.category_other&&-1!==e.category_other.indexOf(t);var Z=n(5267);const B={account_name:"",account_number:"",bank_name:"",sort_code:"",iban:"",bic:""};(0,m.registerPlugin)("wc-admin-payment-gateway-setup-bacs",{render:()=>{const e=(0,s.useSelect)((e=>e(c.OPTIONS_STORE_NAME).isOptionsUpdating())),{createNotice:t}=(0,s.useDispatch)("core/notices"),{updateOptions:n}=(0,s.useDispatch)(c.OPTIONS_STORE_NAME),o=e=>{const t={};return e.account_number||e.iban||(t.account_number=t.iban=(0,i.__)("Please enter an account number or IBAN","woocommerce")),t};return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(u.WooPaymentGatewaySetup,{id:"bacs"},(r=>{let{markConfigured:s}=r;return(0,a.createElement)(f.Form,{initialValues:B,onSubmit:e=>(async(e,o)=>{if((await n({woocommerce_bacs_settings:{enabled:"yes"},woocommerce_bacs_accounts:[e]})).success)return o(),void t("success",(0,i.__)("Direct bank transfer details added successfully","woocommerce"));t("error",(0,i.__)("There was a problem saving your payment settings","woocommerce"))})(e,s),validate:o},(t=>{let{getInputProps:n,handleSubmit:o}=t;return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(f.H,null,(0,i.__)("Add your bank details","woocommerce")),(0,a.createElement)("p",null,(0,i.__)("These details are required to receive payments via bank transfer","woocommerce")),(0,a.createElement)("div",{className:"woocommerce-task-payment-method__fields"},(0,a.createElement)(f.TextControl,(0,Z.Z)({label:(0,i.__)("Account name","woocommerce"),required:!0},n("account_name"))),(0,a.createElement)(f.TextControl,(0,Z.Z)({label:(0,i.__)("Account number","woocommerce"),required:!0},n("account_number"))),(0,a.createElement)(f.TextControl,(0,Z.Z)({label:(0,i.__)("Bank name","woocommerce"),required:!0},n("bank_name"))),(0,a.createElement)(f.TextControl,(0,Z.Z)({label:(0,i.__)("Sort code","woocommerce"),required:!0},n("sort_code"))),(0,a.createElement)(f.TextControl,(0,Z.Z)({label:(0,i.__)("IBAN","woocommerce"),required:!0},n("iban"))),(0,a.createElement)(f.TextControl,(0,Z.Z)({label:(0,i.__)("BIC / Swift","woocommerce"),required:!0},n("bic")))),(0,a.createElement)(p.Button,{isPrimary:!0,isBusy:e,onClick:o},(0,i.__)("Save","woocommerce")))}))})))},scope:"woocommerce-tasks"});const F=e=>{let{onComplete:t,query:n}=e;const{updatePaymentGateway:o}=(0,s.useDispatch)(c.PAYMENT_GATEWAYS_STORE_NAME),{getPaymentGateway:r,paymentGatewaySuggestions:m,installedPaymentGateways:u,isResolving:h,countryCode:g}=(0,s.useSelect)((e=>{const{getSettings:t}=e(c.SETTINGS_STORE_NAME),{general:n={}}=t("general");return{getPaymentGateway:e(c.PAYMENT_GATEWAYS_STORE_NAME).getPaymentGateway,getOption:e(c.OPTIONS_STORE_NAME).getOption,installedPaymentGateways:e(c.PAYMENT_GATEWAYS_STORE_NAME).getPaymentGateways(),isResolving:e(c.ONBOARDING_STORE_NAME).isResolving("getPaymentGatewaySuggestions"),paymentGatewaySuggestions:e(c.ONBOARDING_STORE_NAME).getPaymentGatewaySuggestions(!0),countryCode:(0,V.so)(n.woocommerce_default_country)}}),[]),f=(0,a.useMemo)((()=>((e,t)=>{const n=e.reduce(((e,t)=>(e[t.id]=t,e)),{});return t.reduce(((e,t)=>{const o=(0,w.V7)(t.id),r=n[o]?n[o]:{},a={installed:!!n[o],postInstallScripts:r.post_install_scripts,hasPlugins:!(!t.plugins||!t.plugins.length),enabled:r.enabled||!1,needsSetup:r.needs_setup,settingsUrl:r.settings_url,connectionUrl:r.connection_url,setupHelpText:r.setup_help_text,title:r.title,requiredSettings:r.required_settings_keys?r.required_settings_keys.map((e=>r.settings[e])).filter(Boolean):[],...t};return e.set(t.id,a),e}),new Map)})(u,m)),[u,m]);(0,a.useEffect)((()=>{f.size&&(0,l.recordEvent)("tasklist_payments_options",{options:Array.from(f.values()).map((e=>e.id))})}),[f]);const y=(0,a.useCallback)((async e=>{if(!f.get(e))throw`Payment gateway ${e} not found in available gateways list`;(0,l.recordEvent)("tasklist_payment_connect_method",{payment_method:e}),(e=>{e&&r(e)&&o(e,{enabled:!0}).then((()=>{var n;t(null!==(n=f.get(e))&&void 0!==n&&n.hasPlugins?{}:{redirectPath:(0,d.getNewPath)({task:"payments"},{},"/")})}))})(e)}),[f]),H=(0,a.useMemo)((()=>Array.from(f.values()).filter((e=>e.recommendation_priority)).sort(R).map((e=>e.id)).shift()),[f]),_=(0,a.useMemo)((()=>{if(!n.id||h||!f.size)return null;const e=f.get(n.id);if(!e)throw`Current gateway ${n.id} not found in available gateways list`;return e}),[h,n,f]),v=(0,a.useMemo)((()=>((e,t)=>{for(const[,n]of e.entries())if(n.installed&&!n.needsSetup){if(j(n))return!0;if(D(n,t))return!0}return!1})(f,g)),[g,f]),L=-1!==Array.from(f.values()).findIndex(j),[k,S,M]=(0,a.useMemo)((()=>((e,t,n,o)=>Array.from(e.values()).sort(R).reduce(((e,r)=>{const[a,i,s]=e;return j(r)?!n||r.installed&&!r.needsSetup||a.push(r):r.is_offline?i.push(r):r.enabled||(o?((e,t)=>e.category_additional&&-1!==e.category_additional.indexOf(t))(r,t)&&s.push(r):D(r,t)&&s.push(r)),e}),[[],[],[]]))(f,g,L,v)),[f,g,L,v]);if(n.id&&!_)return(0,a.createElement)(x,null);if(_)return(0,a.createElement)(N,{paymentGateway:_,markConfigured:y});let T=(0,i.__)("Choose a payment provider","woocommerce"),I=(0,i.__)("To start accepting online payments","woocommerce");v?(T=(0,i.__)("Additional payment options","woocommerce"),I=(0,i.__)("Give your customers additional choices in ways to pay.","woocommerce")):L&&(T=(0,i.__)("Other payment providers","woocommerce"),I=(0,i.__)("Try one of the alternative payment providers.","woocommerce"));const A=!!M.length&&(0,a.createElement)(E,{heading:T,headingDescription:I,recommendation:H,paymentGateways:M,markConfigured:y,footerLink:(0,a.createElement)(p.Button,{href:"https://woocommerce.com/product-category/woocommerce-extensions/payment-gateways/?utm_source=payments_recommendations",target:"_blank",onClick:()=>{(0,l.recordEvent)("tasklist_payment_see_more",{})},isTertiary:!0},(0,i.__)("See more","woocommerce"),(0,a.createElement)(C.Z,{size:18}))}),O=!!S.length&&(0,a.createElement)(E,{heading:(0,i.__)("Offline payment methods","woocommerce"),recommendation:H,paymentGateways:S,markConfigured:y});return(0,a.createElement)("div",{className:"woocommerce-task-payments"},!f.size&&(0,a.createElement)(b,null),k.length?(0,a.createElement)(a.Fragment,null,(0,a.createElement)(P,{paymentGateway:k[0]}),A,O):(0,a.createElement)(a.Fragment,null,A,O))};(0,m.registerPlugin)("wc-admin-onboarding-task-payments",{scope:"woocommerce-tasks",render:()=>(0,a.createElement)(u.WooOnboardingTask,{id:"payments"},(e=>{let{onComplete:t,query:n}=e;return(0,a.createElement)(F,{onComplete:t,query:n})}))});var z=n(94333),W=n(92819),U=n(75283),G=n(74617),Y=n(64214),Q=n(58670),q=n(7862),J=n.n(q),K=n(23374),X=n(70444);const $=(0,a.createElement)(X.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,a.createElement)(X.Path,{d:"M12 3.3c-4.8 0-8.8 3.9-8.8 8.8 0 4.8 3.9 8.8 8.8 8.8 4.8 0 8.8-3.9 8.8-8.8s-4-8.8-8.8-8.8zm6.5 5.5h-2.6C15.4 7.3 14.8 6 14 5c2 .6 3.6 2 4.5 3.8zm.7 3.2c0 .6-.1 1.2-.2 1.8h-2.9c.1-.6.1-1.2.1-1.8s-.1-1.2-.1-1.8H19c.2.6.2 1.2.2 1.8zM12 18.7c-1-.7-1.8-1.9-2.3-3.5h4.6c-.5 1.6-1.3 2.9-2.3 3.5zm-2.6-4.9c-.1-.6-.1-1.1-.1-1.8 0-.6.1-1.2.1-1.8h5.2c.1.6.1 1.1.1 1.8s-.1 1.2-.1 1.8H9.4zM4.8 12c0-.6.1-1.2.2-1.8h2.9c-.1.6-.1 1.2-.1 1.8 0 .6.1 1.2.1 1.8H5c-.2-.6-.2-1.2-.2-1.8zM12 5.3c1 .7 1.8 1.9 2.3 3.5H9.7c.5-1.6 1.3-2.9 2.3-3.5zM10 5c-.8 1-1.4 2.3-1.8 3.8H5.5C6.4 7 8 5.6 10 5zM5.5 15.3h2.6c.4 1.5 1 2.8 1.8 3.7-1.8-.6-3.5-2-4.4-3.7zM14 19c.8-1 1.4-2.2 1.8-3.7h2.6C17.6 17 16 18.4 14 19z"}));var ee=n(17844);const te=e=>{let{zone:t}=e;return(0,a.createElement)("div",{className:"woocommerce-shipping-rate__icon"},t.locations?t.locations.map((e=>(0,a.createElement)(f.Flag,{size:24,code:e.code,key:e.code}))):(0,a.createElement)(K.Z,{icon:$}))},ne=e=>{let{zone:t,getInputProps:n}=e;return(0,a.createElement)("label",{htmlFor:`woocommerce-shipping-rate__toggle-${t.id}`,className:"woocommerce-shipping-rate__name"},t.name,(0,a.createElement)(p.FormToggle,(0,Z.Z)({id:`woocommerce-shipping-rate__toggle-${t.id}`},n(`${t.id}_enabled`))))},oe=e=>{let{zone:t,values:n,setTouched:o,setValue:r,getFormattedRate:s,renderInputPrefix:c,renderInputSuffix:l,inputProps:{className:m,...u}}=e;const d=g()("muriel-input-text","woocommerce-shipping-rate__control-wrapper",m);return(0,a.createElement)(a.Fragment,null,!t.toggleable&&(0,a.createElement)("div",{className:"woocommerce-shipping-rate__name"},t.name),(!t.toggleable||n[`${t.id}_enabled`])&&(0,a.createElement)(f.TextControlWithAffixes,(0,Z.Z)({label:(0,i.__)("Shipping cost","woocommerce"),required:!0,className:d},u,{onBlur:()=>{o(`${t.id}_rate`),r(`${t.id}_rate`,s(n[`${t.id}_rate`]))},prefix:c(),suffix:l(n[`${t.id}_rate`])})))};class re extends a.Component{constructor(){super(...arguments),this.updateShippingZones=this.updateShippingZones.bind(this),this.getFormattedRate=this.getFormattedRate.bind(this),this.renderInputPrefix=this.renderInputPrefix.bind(this),this.renderInputSuffix=this.renderInputSuffix.bind(this)}getShippingMethods(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;return e&&e.methods&&Array.isArray(e.methods)?t?e.methods?e.methods.filter((e=>e.method_id===t)):[]:e.methods:[]}disableShippingMethods(e,t){t.length&&t.forEach((t=>{I()({method:"POST",path:`/wc/v3/shipping/zones/${e.id}/methods/${t.instance_id}`,data:{enabled:!1}})}))}async updateShippingZones(e){const{createNotice:t,shippingZones:n}=this.props;let o=!1,r=!1;n.forEach((t=>{0===t.id?o=t.toggleable&&e[`${t.id}_enabled`]:r=""!==e[`${t.id}_rate`]&&parseFloat(e[`${t.id}_rate`])!==parseFloat(0);const n=this.getShippingMethods(t),a=parseFloat(e[`${t.id}_rate`])===parseFloat(0)?"free_shipping":"flat_rate",i=this.getShippingMethods(t,a).length?this.getShippingMethods(t,a)[0]:null;if(!t.toggleable||e[`${t.id}_enabled`]){if(i){const e=n.filter((e=>e.instance_id!==i.instance_id));this.disableShippingMethods(t,e)}I()({method:"POST",path:i?`/wc/v3/shipping/zones/${t.id}/methods/${i.instance_id}`:`/wc/v3/shipping/zones/${t.id}/methods`,data:{method_id:a,enabled:!0,settings:{cost:e[`${t.id}_rate`]}}})}else this.disableShippingMethods(t,n)})),(0,l.recordEvent)("tasklist_shipping_set_costs",{shipping_cost:r,rest_world:o}),t("success",(0,i.__)("Your shipping rates have been updated","woocommerce")),this.props.onComplete()}renderInputPrefix(){const{symbolPosition:e,symbol:t}=this.context.getCurrencyConfig();return 0===e.indexOf("right")?null:(0,a.createElement)("span",{className:"woocommerce-shipping-rate__control-prefix"},t)}renderInputSuffix(e){const{symbolPosition:t,symbol:n}=this.context.getCurrencyConfig();return 0===t.indexOf("right")?(0,a.createElement)("span",{className:"woocommerce-shipping-rate__control-suffix"},n):parseFloat(e)===parseFloat(0)?(0,a.createElement)("span",{className:"woocommerce-shipping-rate__control-suffix"},(0,i.__)("Free shipping","woocommerce")):null}getFormattedRate(e){const{formatDecimalString:t}=this.context,n=t(e);return e.length&&n.length?t(e):t(0)}getInitialValues(){const{formatDecimalString:e}=this.context,t={};return this.props.shippingZones.forEach((n=>{const o=this.getShippingMethods(n),r=o.length&&o[0].settings.cost?this.getFormattedRate(o[0].settings.cost.value):e(0);t[`${n.id}_rate`]=r,o.length&&o[0].enabled?t[`${n.id}_enabled`]=!0:t[`${n.id}_enabled`]=!1})),t}validate(e){const t={};return Object.keys(e).filter((e=>e.endsWith("_rate"))).forEach((n=>{e[n]<0&&(t[n]=(0,i.__)("Shipping rates can not be negative numbers.","woocommerce"))})),t}render(){const{buttonText:e,shippingZones:t}=this.props;return t.length?(0,a.createElement)(f.Form,{initialValues:this.getInitialValues(),onSubmit:this.updateShippingZones,validate:this.validate},(n=>{let{getInputProps:o,handleSubmit:r,setTouched:s,setValue:c,values:l}=n;return(0,a.createElement)(a.Fragment,null,(0,a.createElement)("div",{className:"woocommerce-shipping-rates"},t.map((e=>(0,a.createElement)("div",{className:"woocommerce-shipping-rate",key:e.id},(0,a.createElement)(te,{zone:e}),(0,a.createElement)("div",{className:"woocommerce-shipping-rate__main"},e.toggleable&&(0,a.createElement)(ne,{zone:e,getInputProps:o}),(0,a.createElement)(oe,{zone:e,values:l,inputProps:o(`${e.id}_rate`),setTouched:s,setValue:c,getFormattedRate:this.getFormattedRate,renderInputPrefix:this.renderInputPrefix,renderInputSuffix:this.renderInputSuffix})))))),(0,a.createElement)(p.Button,{isPrimary:!0,onClick:r},e||(0,i.__)("Update","woocommerce")))})):null}}re.propTypes={buttonText:J().string,onComplete:J().func.isRequired,createNotice:J().func.isRequired,shippingZones:J().array},re.defaultProps={shippingZones:[]},re.contextType=ee.CurrencyContext;const ae=re;var ie=n(67097);const se=e=>{var t,n;let{shippingMethod:o}=e;return(0,a.createElement)(ie.L,{features:(null===(t=o.layout_column)||void 0===t?void 0:t.features)||[],logo:{image:(null===(n=o.layout_column)||void 0===n?void 0:n.image)||""}})},ce=e=>{var t,n;let{shippingMethod:o,children:r}=e;return(0,a.createElement)(ie.L,{layout:"dual",features:(null===(t=o.layout_row)||void 0===t?void 0:t.features)||[],logo:{image:(null===(n=o.layout_row)||void 0===n?void 0:n.image)||""},description:o.description},r)};class le extends a.Component{constructor(e){super(e),this.initialState={isPending:!1,step:"store_location",shippingZones:[]},this.activePlugins=e.activePlugins,this.state=this.initialState,this.completeStep=this.completeStep.bind(this),this.shippingSmartDefaultsEnabled=window.wcAdminFeatures&&window.wcAdminFeatures["shipping-smart-defaults"],this.storeLocationCompleted=!1,this.shippingPartners=e.shippingPartners}componentDidMount(){this.reset()}reset(){this.setState(this.initialState)}async fetchShippingZones(){const{countryCode:e,countryName:t}=this.props,n=[],o=await I()({path:"/wc/v3/shipping/zones"});let r=!1;if(await Promise.all(o.map((async t=>{if(0===t.id)return t.methods=await I()({path:`/wc/v3/shipping/zones/${t.id}/methods`}),t.name=(0,i.__)("Rest of the world","woocommerce"),t.toggleable=!0,void n.push(t);t.locations=await I()({path:`/wc/v3/shipping/zones/${t.id}/locations`}),t.locations.find((t=>e===t.code))&&(t.methods=await I()({path:`/wc/v3/shipping/zones/${t.id}/methods`}),n.push(t),r=!0)}))),!r){const o=await I()({method:"POST",path:"/wc/v3/shipping/zones",data:{name:t}});o.locations=await I()({method:"POST",path:`/wc/v3/shipping/zones/${o.id}/locations`,data:[{code:e,type:"country"}]}),n.push(o)}n.reverse(),this.setState({isPending:!1,shippingZones:n})}componentDidUpdate(e,t){const{countryCode:n,countryName:o,settings:r}=this.props,{woocommerce_store_address:a,woocommerce_default_country:i,woocommerce_store_postcode:s}=r,{step:c}=this.state;"rates"!==c||e.countryCode===n&&e.countryName===o&&"rates"===t.step||(this.setState({isPending:!0}),o&&this.fetchShippingZones());const l=Boolean(a&&i&&s);"store_location"===c&&l&&(this.shippingSmartDefaultsEnabled&&!this.storeLocationCompleted?(this.completeStep(),this.storeLocationCompleted=!0):this.shippingSmartDefaultsEnabled||this.completeStep())}completeStep(){const{createNotice:e,onComplete:t}=this.props,{step:n}=this.state,o=this.getSteps(),r=o.findIndex((e=>e.key===n)),a=o[r+1];a?this.setState({step:a.key}):(e("success",(0,i.__)("📦 Shipping is done! Don't worry, you can always change it later","woocommerce")),t())}getSteps(){const{countryCode:e,createNotice:t,invalidateResolutionForStoreSelector:n,isJetpackConnected:o,onComplete:r,optimisticallyCompleteTask:s,settings:c,task:m,updateAndPersistSettingsForGroup:u,shippingPartners:C}=this.props,h=C,w=h.map((e=>e.slug));w.includes("woocommerce-services")&&w.push("jetpack");const H=()=>{(0,l.recordEvent)("tasklist_shipping_label_printing",{install:!1,plugins_to_activate:w}),(0,d.getHistory)().push((0,d.getNewPath)({},"/",{})),r()},_=!o&&"US"===e;let v=[{key:"store_location",label:(0,i.__)("Set store location","woocommerce"),description:(0,i.__)("The address from which your business operates","woocommerce"),content:(0,a.createElement)(Q.Z,{createNotice:t,updateAndPersistSettingsForGroup:u,settings:c,onComplete:e=>{const t=(0,V.so)(e.countryState);(0,l.recordEvent)("tasklist_shipping_set_location",{country:t}),this.shippingSmartDefaultsEnabled&&this.completeStep()}}),visible:!0},{key:"rates",label:(0,i.__)("Set shipping costs","woocommerce"),description:(0,i.__)("Define how much customers pay to ship to different destinations","woocommerce"),content:(0,a.createElement)(ae,{buttonText:w.length||_?(0,i.__)("Continue","woocommerce"):(0,i.__)("Complete task","woocommerce"),shippingZones:this.state.shippingZones,onComplete:()=>{const{id:e}=m;s(e),n(),this.completeStep()},createNotice:t}),visible:"disabled"!==c.woocommerce_ship_to_countries},{key:"label_printing",label:(0,i.__)("Enable shipping label printing","woocommerce"),description:w.includes("woocommerce-shipstation-integration")?(0,U.Z)({mixedString:(0,i.__)("We recommend using ShipStation to save time at the post office by printing your shipping labels at home. Try ShipStation free for 30 days. {{link}}Learn more{{/link}}.","woocommerce"),components:{link:(0,a.createElement)(f.Link,{href:"https://woocommerce.com/products/shipstation-integration?utm_medium=product",target:"_blank",type:"external"})}}):(0,i.__)("With WooCommerce Shipping you can save time by printing your USPS and DHL Express shipping labels at home","woocommerce"),content:(0,a.createElement)(f.Plugins,{onComplete:(e,t)=>{(0,k.a)(t),(0,l.recordEvent)("tasklist_shipping_label_printing",{install:!0,plugins_to_activate:w}),this.completeStep()},onError:(e,t)=>(0,k.a)(t),onSkip:()=>{(0,l.recordEvent)("tasklist_shipping_label_printing",{install:!1,plugins_to_activate:w}),n(),(0,d.getHistory)().push((0,d.getNewPath)({},"/",{})),r()},pluginSlugs:w}),visible:w.length},{key:"connect",label:(0,i.__)("Connect your store","woocommerce"),description:(0,i.__)("Connect your store to WordPress.com to enable label printing","woocommerce"),content:(0,a.createElement)(Y.Z,{redirectUrl:(0,G.getAdminLink)("admin.php?page=wc-admin"),completeStep:this.completeStep,onConnect:()=>{(0,l.recordEvent)("tasklist_shipping_connect_store")}}),visible:_}];if(this.shippingSmartDefaultsEnabled){const e=w.includes("woocommerce-services")?(0,i.__)("By installing Jetpack and WooCommerce Shipping you agree to the {{link}}Terms of Service{{/link}}.","woocommerce"):(0,i.__)("By installing Jetpack you agree to the {{link}}Terms of Service{{/link}}.","woocommerce"),r={rates:{label:(0,i.__)("Review your shipping options","woocommerce"),description:(0,i.__)("We recommend the following shipping options based on your location. You can manage your shipping options again at any time in WooCommerce Shipping settings.","woocommerce"),onClick:"rates"!==this.state.step?()=>{this.setState({step:"rates"})}:void 0,content:(0,a.createElement)(ae,{buttonText:(0,i.__)("Save shipping options","woocommerce"),shippingZones:this.state.shippingZones,onComplete:()=>{const{id:e}=m;s(e),n(),this.completeStep()},createNotice:t})},label_printing:{label:(0,i.__)("Enable shipping label printing and discounted rates","woocommerce"),description:1===h.length?(E=h[0].name,L=h[0].learn_more_link,(0,U.Z)({mixedString:(0,i.sprintf)((0,i.__)("Save time and money by printing your shipping labels right from your computer with %1$s. Try %2$s for free. {{link}}Learn more{{/link}}","woocommerce"),E,E),components:{link:(0,a.createElement)(f.Link,{href:L,target:"_blank",type:"external"})}})):(0,i.__)("Save time and money by printing your shipping labels right from your computer with one of these shipping solutions.","woocommerce"),content:(0,a.createElement)(a.Fragment,null,1===h.length?(0,a.createElement)(se,{shippingMethod:h[0]}):(0,a.createElement)("div",{className:"woocommerce-task-shipping-recommendation_plugins-install-container"},h.map((e=>{var t;const o=[null==e?void 0:e.slug,...null!==(t=null==e?void 0:e.dependencies)&&void 0!==t?t:[]].filter((e=>void 0!==e));return(0,a.createElement)(ce,{shippingMethod:e,key:e.name},(0,a.createElement)("div",{className:"woocommerce-task-shipping-recommendations_plugins-buttons"},(0,a.createElement)(f.Plugins,{onComplete:e=>{(0,k.a)(e),(0,l.recordEvent)("tasklist_shipping_label_printing",{install:!0,plugins_to_activate:o}),n(),this.completeStep()},onError:(e,t)=>(0,k.a)(t),installText:(0,i.__)("Install and enable","woocommerce"),learnMoreLink:e.learn_more_link,onLearnMore:()=>{(0,l.recordEvent)("tasklist_shipping_label_printing_learn_more",{plugin:e.slug})},pluginSlugs:o,installButtonVariant:"secondary"})))}))),1===h.length&&void 0===h[0].slug&&(0,a.createElement)("a",{href:h[0].learn_more_link,target:"_blank",rel:"noreferrer"},(0,a.createElement)(p.Button,{variant:"primary"},(0,i.__)("Download","woocommerce"))),1===h.length&&h[0].slug?(0,a.createElement)(f.Plugins,{onComplete:(e,t)=>{(0,k.a)(t),(0,l.recordEvent)("tasklist_shipping_label_printing",{install:!0,plugins_to_activate:w}),n(),this.completeStep()},onError:(e,t)=>(0,k.a)(t),onSkip:H,pluginSlugs:w,installText:(0,i.__)("Install and enable","woocommerce")}):(0,a.createElement)(p.Button,{isTertiary:!0,onClick:H,className:g()("woocommerce-task-shipping-recommendations_skip-button",2===h.length?"dual":"")},(0,i.__)("No Thanks","woocommerce")),!o&&w.includes("woocommerce-services")&&(0,a.createElement)(y.Text,{variant:"caption",className:"woocommerce-task__caption",size:"12",lineHeight:"16px",style:{display:"block"}},(0,U.Z)({mixedString:e,components:{link:(0,a.createElement)(f.Link,{href:"https://wordpress.com/tos/",target:"_blank",type:"external"},(0,a.createElement)(a.Fragment,null))}})))},store_location:{label:(0,i.__)("Set your store location","woocommerce"),description:(0,i.__)("Add your store location to help us calculate shipping rates and the best shipping options for you. You can manage your store location again at any time in WooCommerce Settings General.","woocommerce"),onClick:"store_location"!==this.state.step?()=>{this.setState({step:"store_location"})}:void 0,buttonText:(0,i.__)("Save store location","woocommerce")}};v=v.map((e=>(r.hasOwnProperty(e.key)&&(e={...e,...r[e.key]}),e.key!==this.state.step&&(e.description=""),e)))}var E,L;return(0,W.filter)(v,(e=>e.visible))}render(){const{isPending:e,step:t}=this.state,{isUpdateSettingsRequesting:n}=this.props,o=this.getSteps();return(0,a.createElement)("div",{className:"woocommerce-task-shipping"},(0,a.createElement)(p.Card,{className:"woocommerce-task-card"},(0,a.createElement)(p.CardBody,null,(0,a.createElement)(f.Stepper,{isPending:e||n,isVertical:!0,currentStep:t,steps:o}))))}}const me=(0,z.compose)((0,s.withSelect)((e=>{const{getSettings:t,isUpdateSettingsRequesting:n}=e(c.SETTINGS_STORE_NAME),{getActivePlugins:o,isJetpackConnected:r}=e(c.PLUGINS_STORE_NAME),{getCountry:a}=e(c.COUNTRIES_STORE_NAME),{general:i={}}=t("general"),s=(0,V.so)(i.woocommerce_default_country),l=e(c.SHIPPING_METHODS_STORE_NAME).getShippingMethods(!0),m=s?a(s):null,u=m?m.name:null,d=o();return{countryCode:s,countryName:u,isUpdateSettingsRequesting:n("general"),settings:i,activePlugins:d,isJetpackConnected:r(),shippingPartners:l}})),(0,s.withDispatch)((e=>{const{createNotice:t}=e("core/notices"),{updateAndPersistSettingsForGroup:n}=e(c.SETTINGS_STORE_NAME),{invalidateResolutionForStoreSelector:o,optimisticallyCompleteTask:r}=e(c.ONBOARDING_STORE_NAME);return{createNotice:t,invalidateResolutionForStoreSelector:o,optimisticallyCompleteTask:r,updateAndPersistSettingsForGroup:n}})))(le);(0,m.registerPlugin)("wc-admin-onboarding-task-shipping",{scope:"woocommerce-tasks",render:()=>(0,a.createElement)(u.WooOnboardingTask,{id:"shipping"},(e=>{let{onComplete:t,task:n}=e;return(0,a.createElement)(me,{onComplete:t,task:n})}))});const ue=e=>{let{description:t,imageUrl:n,installAndActivate:o=(()=>{}),onManage:r=(()=>{}),isActive:s,isBusy:c,isBuiltByWC:m,isDisabled:u,isInstalled:d,manageUrl:C,name:h,slug:g}=e;return(0,a.createElement)("div",{className:"woocommerce-plugin-list__plugin"},n&&(0,a.createElement)("div",{className:"woocommerce-plugin-list__plugin-logo"},(0,a.createElement)("img",{src:n,alt:(0,i.sprintf)((0,i.__)("%s logo","woocommerce"),h)})),(0,a.createElement)("div",{className:"woocommerce-plugin-list__plugin-text"},(0,a.createElement)(y.Text,{variant:"subtitle.small",as:"h4"},h,m&&(0,a.createElement)(f.Pill,null,(0,i.__)("Built by WooCommerce","woocommerce"))),(0,a.createElement)(y.Text,{variant:"subtitle.small"},t)),(0,a.createElement)("div",{className:"woocommerce-plugin-list__plugin-action"},s&&C&&(0,a.createElement)(p.Button,{disabled:u,isBusy:c,isSecondary:!0,href:(0,G.getAdminLink)(C),onClick:()=>{(0,l.recordEvent)("marketing_manage",{extension_name:g}),r(g)}},(0,i.__)("Manage","woocommerce")),d&&!s&&(0,a.createElement)(p.Button,{disabled:u,isBusy:c,isSecondary:!0,onClick:()=>o(g)},(0,i.__)("Activate","woocommerce")),!d&&(0,a.createElement)(p.Button,{disabled:u,isBusy:c,isSecondary:!0,onClick:()=>o(g)},(0,i.__)("Get started","woocommerce"))))},de=e=>{let{currentPlugin:t,installAndActivate:n=(()=>{}),onManage:o=(()=>{}),plugins:r=[],title:i}=e;return(0,a.createElement)("div",{className:"woocommerce-plugin-list"},i&&(0,a.createElement)("div",{className:"woocommerce-plugin-list__title"},(0,a.createElement)(y.Text,{variant:"sectionheading",as:"h3"},i)),r.map((e=>{const{description:r,imageUrl:i,isActive:s,isBuiltByWC:c,isInstalled:l,manageUrl:m,slug:u,name:d}=e;return(0,a.createElement)(ue,{key:u,description:r,manageUrl:m,name:d,imageUrl:i,installAndActivate:n,onManage:o,isActive:s,isBuiltByWC:c,isBusy:t===u,isDisabled:!!t,isInstalled:l,slug:u})})))},pe=["task-list/grow","task-list/reach"],Ce=e=>{let{onComplete:t}=e;const[n,o]=(0,a.useState)(null),{actionTask:r}=(0,s.useDispatch)(c.ONBOARDING_STORE_NAME),{installAndActivatePlugins:m}=(0,s.useDispatch)(c.PLUGINS_STORE_NAME),{activePlugins:u,freeExtensions:C,installedPlugins:h,isResolving:g}=(0,s.useSelect)((e=>{const{getActivePlugins:t,getInstalledPlugins:n}=e(c.PLUGINS_STORE_NAME),{getFreeExtensions:o,hasFinishedResolution:r}=e(c.ONBOARDING_STORE_NAME);return{activePlugins:t(),freeExtensions:o(),installedPlugins:n(),isResolving:!r("getFreeExtensions")}})),[f,H]=(0,a.useMemo)((()=>((e,t,n)=>{const o=[],r=[];return e.sort(((e,t)=>pe.indexOf(e.key)-pe.indexOf(t.key))).forEach((e=>{if(!pe.includes(e.key))return;const a=[];if(e.plugins.forEach((e=>{const r=((e,t,n)=>{const{description:o,image_url:r,is_built_by_wc:a,key:i,manage_url:s,name:c}=e,l=(0,w.V7)(i);return{description:o,slug:l,imageUrl:r,isActive:t.includes(l),isInstalled:n.includes(l),isBuiltByWC:a,manageUrl:s,name:c}})(e,t,n);r.isInstalled?o.push(r):a.push(r)})),!a.length)return;const i={...e,plugins:a};r.push(i)})),[o,r]})(C,u,h)),[h,u,C]),_=e=>{o(e),r("marketing"),m([e]).then((n=>{(0,l.recordEvent)("tasklist_marketing_install",{selected_extension:e,installed_extensions:f.map((e=>e.slug)),section_order:H.map((e=>e.key)).join(", ")}),(0,k.a)(n),o(null),t({redirectPath:(0,d.getNewPath)({task:"marketing"})})})).catch((e=>{(0,k.a)(e),o(null)}))},v=()=>{r("marketing")};return g?(0,a.createElement)(p.Spinner,null):(0,a.createElement)("div",{className:"woocommerce-task-marketing"},!!f.length&&(0,a.createElement)(p.Card,{className:"woocommerce-task-card"},(0,a.createElement)(p.CardHeader,null,(0,a.createElement)(y.Text,{variant:"title.small",as:"h2",className:"woocommerce-task-card__title"},(0,i.__)("Installed marketing extensions","woocommerce"))),(0,a.createElement)(de,{currentPlugin:n,installAndActivate:_,onManage:v,plugins:f})),!!H.length&&(0,a.createElement)(p.Card,{className:"woocommerce-task-card"},(0,a.createElement)(p.CardHeader,null,(0,a.createElement)(y.Text,{variant:"title.small",as:"h2",className:"woocommerce-task-card__title"},(0,i.__)("Recommended marketing extensions","woocommerce")),(0,a.createElement)(y.Text,{as:"span"},(0,i.__)('We recommend adding one of the following marketing tools for your store. The extension will be installed and activated for you when you click "Get started".',"woocommerce"))),H.map((e=>{const{key:t,title:o,plugins:r}=e;return(0,a.createElement)(de,{currentPlugin:n,installAndActivate:_,onManage:v,key:t,plugins:r,title:o})}))))};(0,m.registerPlugin)("wc-admin-onboarding-task-marketing",{scope:"woocommerce-tasks",render:()=>(0,a.createElement)(u.WooOnboardingTask,{id:"marketing"},(e=>{let{onComplete:t}=e;return(0,a.createElement)(Ce,{onComplete:t})}))});var he=n(99196);const ge=()=>{const{onClick:e}={onClick:()=>{window.location=(0,G.getAdminLink)("theme-install.php?browse=block-themes")}};return(0,a.createElement)(u.WooOnboardingTaskListItem,{id:"appearance"},(t=>{let{defaultTaskItem:n}=t;return(0,a.createElement)(n,{onClick:e})}))};(0,m.registerPlugin)("wc-admin-onboarding-task-appearance",{scope:"woocommerce-tasks",render:()=>(0,a.createElement)(ge,null)});var fe=n(33414);const ye=()=>(0,a.createElement)("svg",{width:"13",height:"10",viewBox:"0 0 13 10",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)("path",{d:"M12.1883 1.1814L4.7091 8.66062L1.48438 5.4359",stroke:"#4AB866",strokeWidth:"1.5"})),we=e=>{let{name:t,logo:n,description:o,benefits:r,terms:i,actionText:s,onClick:c,isBusy:l}=e;return(0,a.createElement)("div",{className:"woocommerce-tax-partner-card"},(0,a.createElement)("div",{className:"woocommerce-tax-partner-card__logo"},(0,a.createElement)("img",{src:n,alt:t})),(0,a.createElement)("div",{className:"woocommerce-tax-partner-card__description"},o),(0,a.createElement)("ul",{className:"woocommerce-tax-partner-card__benefits"},r.map(((e,t)=>(0,a.createElement)("li",{className:"woocommerce-tax-partner-card__benefit",key:t},(0,a.createElement)("span",{className:"woocommerce-tax-partner-card__benefit-bullet"},(0,a.createElement)(ye,null)),(0,a.createElement)("span",{className:"woocommerce-tax-partner-card__benefit-text"},e))))),(0,a.createElement)("div",{className:"woocommerce-tax-partner-card__action"},(0,a.createElement)("div",{className:"woocommerce-tax-partner-card__terms"},i),(0,a.createElement)(p.Button,{isSecondary:!0,onClick:c,isBusy:l,disabled:l},s)))};var He=n(2732);const _e=()=>(0,a.createElement)(we,{name:(0,i.__)("WooCommerce Tax","woocommerce"),logo:He,description:(0,i.__)("Best for new stores","woocommerce"),benefits:[(0,i.__)("Real-time sales tax calculation","woocommerce"),(0,U.Z)({mixedString:(0,i.__)("{{strong}}Single{{/strong}} economic nexus compliance","woocommerce"),components:{strong:(0,a.createElement)("strong",null)}}),(0,U.Z)({mixedString:(0,i.__)("Powered by {{link}}Jetpack{{/link}}","woocommerce"),components:{link:(0,a.createElement)(f.Link,{type:"external",href:"https://woocommerce.com/products/jetpack/?utm_medium=product",target:"_blank"},(0,a.createElement)(a.Fragment,null))}}),(0,i.__)("100% free","woocommerce")],terms:(0,U.Z)({mixedString:(0,i.__)("By installing WooCommerce Tax and Jetpack you agree to the {{link}}Terms of Service{{/link}}.","woocommerce"),components:{link:(0,a.createElement)(f.Link,{href:"https://wordpress.com/tos/",target:"_blank",type:"external"},(0,a.createElement)(a.Fragment,null))}}),actionText:(0,i.__)("Continue setup","woocommerce"),onClick:()=>{(0,l.recordEvent)("tasklist_tax_select_option",{selected_option:"woocommerce-tax"}),(0,d.updateQueryString)({partner:"woocommerce-tax"})}}),ve=e=>{let{isPending:t,onManual:n}=e;const{generalSettings:o}=(0,s.useSelect)((e=>{var t;const{getSettings:n}=e(c.SETTINGS_STORE_NAME);return{generalSettings:null===(t=n("general"))||void 0===t?void 0:t.general}}));return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(p.Button,{isPrimary:!0,disabled:t,isBusy:t,onClick:()=>{(0,l.recordEvent)("tasklist_tax_config_rates",{}),n()}},(0,i.__)("Configure","woocommerce")),(0,a.createElement)("p",null,"yes"!==(null==o?void 0:o.woocommerce_calc_taxes)&&(0,U.Z)({mixedString:(0,i.__)('By clicking "Configure" you\'re enabling tax rates and calculations. More info {{link}}here{{/link}}.',"woocommerce"),components:{link:(0,a.createElement)(f.Link,{href:"https://woocommerce.com/document/setting-up-taxes-in-woocommerce/?utm_medium=product#section-1",target:"_blank",type:"external"},(0,a.createElement)(a.Fragment,null))}})))},Ee=e=>{const t=(0,Q.z)(e);return e.addressLine1.trim().length||(t.addressLine1=(0,i.__)("Please enter an address","woocommerce")),e.postCode.trim().length||(t.postCode=(0,i.__)("Please enter a post code","woocommerce")),e.city.trim().length||(t.city=(0,i.__)("Please enter a city","woocommerce")),t},Le=e=>{let{nextStep:t}=e;const{createNotice:n}=(0,s.useDispatch)("core/notices"),{updateAndPersistSettingsForGroup:o}=(0,s.useDispatch)(c.SETTINGS_STORE_NAME),{generalSettings:r,isResolving:i,isUpdating:m}=(0,s.useSelect)((e=>{var t;const{getSettings:n,hasFinishedResolution:o,isUpdateSettingsRequesting:r}=e(c.SETTINGS_STORE_NAME);return{generalSettings:null===(t=n("general"))||void 0===t?void 0:t.general,isResolving:!o("getSettings",["general"]),isUpdating:r("general")}}));return(0,a.useEffect)((()=>{i||m||!(0,fe.AO)(r||{})||t()}),[i,r,m]),i?null:(0,a.createElement)(Q.Z,{validate:Ee,onComplete:e=>{const t=(0,V.so)(e.countryState);(0,l.recordEvent)("tasklist_tax_set_location",{country:t})},isSettingsRequesting:!1,settings:r,updateAndPersistSettingsForGroup:o,createNotice:n})},be=e=>{let{isPending:t,onDisable:n,onAutomate:o,onManual:r}=e;const[s,c]=(0,a.useState)(0),l={isPending:t,onAutomate:o,onDisable:n,nextStep:()=>{c(s+1)},onManual:r},m=[{key:"store_location",label:(0,i.__)("Set store location","woocommerce"),description:(0,i.__)("The address from which your business operates","woocommerce"),content:(0,a.createElement)(Le,l)},{key:"manual_configuration",label:(0,i.__)("Configure tax rates","woocommerce"),description:(0,i.__)("Head over to the tax rate settings screen to configure your tax rates","woocommerce"),content:(0,a.createElement)(ve,l)}],u=m[s];return(0,a.createElement)(f.Stepper,{isVertical:!0,currentStep:u.key,steps:m})},ke=e=>{let{children:t,isPending:n,onManual:o,onDisable:r}=e;const s=g()("woocommerce-task-card","woocommerce-tax-partners",`woocommerce-tax-partners__partners-count-${a.Children.count(t)}`);return(0,a.createElement)(p.Card,{className:s},(0,a.createElement)(p.CardHeader,null,(0,i.__)("Choose a tax partner","woocommerce")),(0,a.createElement)(p.CardBody,null,(0,a.createElement)("div",{className:"woocommerce-tax-partners__partners"},t),(0,a.createElement)("ul",{className:"woocommerce-tax-partners__other-actions"},(0,a.createElement)("li",null,(0,a.createElement)(p.Button,{isTertiary:!0,disabled:n,isBusy:n,onClick:()=>{o()}},(0,i.__)("Set up taxes manually","woocommerce"))),(0,a.createElement)("li",null,(0,a.createElement)(p.Button,{isTertiary:!0,disabled:n,isBusy:n,onClick:()=>{r()}},(0,i.__)("I don't charge sales tax","woocommerce"))))))},Se=e=>{let{isPending:t,onAutomate:n,onManual:o,onDisable:r}=e;return(0,a.createElement)("div",{className:"woocommerce-task-tax__success"},(0,a.createElement)("span",{className:"woocommerce-task-tax__success-icon",role:"img","aria-labelledby":"woocommerce-task-tax__success-message"},"🎊"),(0,a.createElement)(f.H,{id:"woocommerce-task-tax__success-message"},(0,i.__)("Good news!","woocommerce")),(0,a.createElement)("p",null,(0,U.Z)({mixedString:(0,i.__)("{{strong}}Jetpack{{/strong}} and {{strong}}WooCommerce Tax{{/strong}} can automate your sales tax calculations for you.","woocommerce"),components:{strong:(0,a.createElement)("strong",null)}})),(0,a.createElement)(p.Button,{isPrimary:!0,isBusy:t,onClick:()=>{(0,l.recordEvent)("tasklist_tax_setup_automated_proceed",{setup_automatically:!0}),n()}},(0,i.__)("Yes please","woocommerce")),(0,a.createElement)(p.Button,{disabled:t,isTertiary:!0,onClick:()=>{(0,l.recordEvent)("tasklist_tax_setup_automated_proceed",{setup_automatically:!1}),o()}},(0,i.__)("No thanks, I'll set up manually","woocommerce")),(0,a.createElement)(p.Button,{disabled:t,isTertiary:!0,onClick:r},(0,i.__)("I don't charge sales tax","woocommerce")))},Me=e=>{let{onDisable:t,onManual:n}=e;return(0,a.createElement)(Y.Z,{onConnect:()=>{(0,l.recordEvent)("tasklist_tax_connect_store",{connect:!0,no_tax:!1})},onSkip:()=>{(0,l.queueRecordEvent)("tasklist_tax_connect_store",{connect:!1,no_tax:!1}),n()},skipText:(0,i.__)("Set up tax rates manually","woocommerce"),onAbort:()=>t(),abortText:(0,i.__)("My business doesn't charge sales tax","woocommerce")})},Ne=e=>"object"==typeof e&&null!==e,xe=e=>{let{nextStep:t,onDisable:n,onManual:o,pluginsToActivate:r}=e;const{updateOptions:m}=(0,s.useDispatch)(c.OPTIONS_STORE_NAME),{isResolving:u,tosAccepted:d}=(0,s.useSelect)((e=>{const{getOption:t,hasFinishedResolution:n}=e(c.OPTIONS_STORE_NAME),o=t("wc_connect_options");return{isResolving:!n("getOption",["woocommerce_setup_jetpack_opted_in"])||!n("getOption",["wc_connect_options"]),tosAccepted:Ne(o)&&(null==o?void 0:o.tos_accepted)||"1"===t("woocommerce_setup_jetpack_opted_in")}}));(0,a.useEffect)((()=>{d&&!r.length&&t()}),[u]);const p=r.includes("woocommerce-services")?(0,i.__)("By installing Jetpack and WooCommerce Tax you agree to the {{link}}Terms of Service{{/link}}.","woocommerce"):(0,i.__)("By installing Jetpack you agree to the {{link}}Terms of Service{{/link}}.","woocommerce");return u?null:(0,a.createElement)(a.Fragment,null,(0,a.createElement)(f.Plugins,{onComplete:(e,n)=>{(0,k.a)(n),(0,l.recordEvent)("tasklist_tax_install_extensions",{install_extensions:!0}),m({woocommerce_setup_jetpack_opted_in:!0}),t()},onError:(e,t)=>(0,k.a)(t),onSkip:()=>{(0,l.queueRecordEvent)("tasklist_tax_install_extensions",{install_extensions:!1}),o()},skipText:(0,i.__)("Set up manually","woocommerce"),onAbort:()=>n(),abortText:(0,i.__)("I don't charge sales tax","woocommerce")}),!d&&(0,a.createElement)(y.Text,{variant:"caption",className:"woocommerce-task__caption",size:"12",lineHeight:"16px"},(0,U.Z)({mixedString:p,components:{link:(0,a.createElement)(f.Link,{href:"https://wordpress.com/tos/",target:"_blank",type:"external"},(0,a.createElement)(a.Fragment,null))}})))},Te=e=>{let{isPending:t,onDisable:n,onAutomate:o,onManual:r}=e;const[l,m]=(0,a.useState)([]),{activePlugins:u,isResolving:d}=(0,s.useSelect)((e=>{var t;const{getSettings:n}=e(c.SETTINGS_STORE_NAME),{hasFinishedResolution:o}=e(c.OPTIONS_STORE_NAME),{getActivePlugins:r}=e(c.PLUGINS_STORE_NAME);return{activePlugins:r(),generalSettings:null===(t=n("general"))||void 0===t?void 0:t.general,isResolving:!o("getOption",["woocommerce_setup_jetpack_opted_in"])||!o("getOption",["wc_connect_options"])}})),[p,C]=(0,a.useState)(0);(0,a.useEffect)((()=>{const e=(0,W.difference)(fe.c7,u);e.length<=l.length||m(e)}),[u]);const h={isPending:t,isResolving:d,onAutomate:o,onDisable:n,nextStep:()=>{C(p+1)},onManual:r,pluginsToActivate:l},g=[{key:"store_location",label:(0,i.__)("Set store location","woocommerce"),description:(0,i.__)("The address from which your business operates","woocommerce"),content:(0,a.createElement)(Le,h)},{key:"plugins",label:l.includes("woocommerce-services")?(0,i.__)("Install Jetpack and WooCommerce Tax","woocommerce"):(0,i.__)("Install Jetpack","woocommerce"),description:(0,i.__)("Jetpack and WooCommerce Tax allow you to automate sales tax calculations","woocommerce"),content:(0,a.createElement)(xe,h)},{key:"connect",label:(0,i.__)("Connect your store","woocommerce"),description:(0,i.__)("Connect your store to WordPress.com to enable automated sales tax calculations","woocommerce"),content:(0,a.createElement)(Me,h)}],y=g[p];return(0,a.createElement)(f.Stepper,{isPending:d,isVertical:!0,currentStep:y.key,steps:g})},Ie=e=>{let{isPending:t,onAutomate:n,onManual:o,onDisable:r}=e;const{generalSettings:i,isJetpackConnected:l,isResolving:m,pluginsToActivate:u}=(0,s.useSelect)((e=>{const{getSettings:t}=e(c.SETTINGS_STORE_NAME),{getActivePlugins:n,hasFinishedResolution:o}=e(c.PLUGINS_STORE_NAME),r=n();return{generalSettings:t("general").general,isJetpackConnected:e(c.PLUGINS_STORE_NAME).isJetpackConnected(),isResolving:!o("isJetpackConnected")||!e(c.SETTINGS_STORE_NAME).hasFinishedResolution("getSettings",["general"])||!o("getActivePlugins"),pluginsToActivate:(0,W.difference)(fe.c7,r)}}));if(m)return(0,a.createElement)(f.Spinner,null);const d={isPending:t,onAutomate:n,onManual:o,onDisable:r};return(0,fe.AO)(i||{})&&!u.length&&l?(0,a.createElement)(Se,d):(0,a.createElement)(Te,d)},Ae=e=>{let{children:t}=e;return(0,a.createElement)(p.Card,{className:"woocommerce-task-card"},(0,a.createElement)(p.CardBody,null,t))},Oe=e=>{let{onComplete:t,query:n,task:o}=e;const[r,m]=(0,a.useState)(!1),{updateOptions:u}=(0,s.useDispatch)(c.OPTIONS_STORE_NAME),{createNotice:d}=(0,s.useDispatch)("core/notices"),{updateAndPersistSettingsForGroup:C}=(0,s.useDispatch)(c.SETTINGS_STORE_NAME),{generalSettings:h,isResolving:g,taxSettings:f}=(0,s.useSelect)((e=>{const{getSettings:t,hasFinishedResolution:n}=e(c.SETTINGS_STORE_NAME);return{generalSettings:t("general").general,isResolving:!n("getSettings",["general"]),taxSettings:t("tax").tax||{}}})),y=(0,a.useCallback)((async()=>{m(!0),"yes"!==(null==h?void 0:h.woocommerce_calc_taxes)?(C("tax",{tax:{...f,wc_connect_taxes_enabled:"no"}}),C("general",{general:{...h,woocommerce_calc_taxes:"yes"}}).then((()=>(0,fe._9)())).catch((e=>{m(!1),(0,k.a)(e)}))):(0,fe._9)()}),[]),w=(0,a.useCallback)((async()=>{m(!0);try{await Promise.all([C("tax",{tax:{...f,wc_connect_taxes_enabled:"yes"}}),C("general",{general:{...h,woocommerce_calc_taxes:"yes"}})])}catch(e){return m(!1),void d("error",(0,i.__)("There was a problem setting up automated taxes. Please try again.","woocommerce"))}d("success",(0,i.__)("You're awesome! One less item on your to-do list ✅","woocommerce")),t()}),[]),H=(0,a.useCallback)((()=>{m(!0),(0,l.queueRecordEvent)("tasklist_tax_connect_store",{connect:!1,no_tax:!0}),u({woocommerce_no_sales_tax:!0,woocommerce_calc_taxes:"no"}).then((()=>{window.location.href=(0,G.getAdminLink)("admin.php?page=wc-admin")}))}),[]),_=(()=>{const e=(0,V.so)(null==h?void 0:h.woocommerce_default_country)||"",{additionalData:{woocommerceTaxCountries:t=[],taxJarActivated:n}={}}=o;return[{id:"woocommerce-tax",card:_e,component:Ie,isVisible:!n&&t.includes(e)}].filter((e=>e.isVisible))})();(0,a.useEffect)((()=>{const{auto:e}=n;"true"!==e?n.partner||(0,l.recordEvent)("tasklist_tax_view_options",{options:_.map((e=>e.id))}):w()}),[]);const v={isPending:r,onAutomate:w,onManual:y,onDisable:H,task:o};if(g)return(0,a.createElement)(p.Spinner,null);const E=n.partner&&_.find((e=>e.id===n.partner))||null;return _.length?E?(0,a.createElement)(Ae,null,E.component&&(0,a.createElement)(E.component,v)):(0,a.createElement)(ke,v,_.map((e=>e.card&&(0,a.createElement)(e.card,{key:e.id,...v})))):(0,a.createElement)(Ae,null,(0,a.createElement)(be,v))};(0,m.registerPlugin)("wc-admin-onboarding-task-tax",{scope:"woocommerce-tasks",render:()=>(0,a.createElement)(u.WooOnboardingTask,{id:"tax"},(e=>{let{onComplete:t,query:n,task:o}=e;return(0,a.createElement)(Oe,{onComplete:t,query:n,task:o})}))}),(0,m.registerPlugin)("woocommerce-admin-task-wcpay",{scope:"woocommerce-tasks",render:()=>{const{installAndActivatePlugins:e}=(0,s.useDispatch)(c.PLUGINS_STORE_NAME),{createNotice:t}=(0,s.useDispatch)("core/notices");return(0,a.createElement)(u.WooOnboardingTaskListItem,{id:"woocommerce-payments"},(n=>{let{defaultTaskItem:o}=n;return(0,a.createElement)(o,{onClick:()=>new Promise(((n,o)=>O(o,t,e)))})}))}});const Pe=()=>{const{installAndActivatePlugins:e}=(0,s.useDispatch)(c.PLUGINS_STORE_NAME),{createNotice:t}=(0,s.useDispatch)("core/notices");return(0,he.useEffect)((()=>{new Promise(((n,o)=>O(o,t,e)))}),[t,e]),(0,a.createElement)("div",{style:{height:"70vh",display:"flex",flexDirection:"column",justifyContent:"center",alignItems:"center"}},(0,a.createElement)(f.Spinner,null),(0,a.createElement)("div",{style:{marginTop:"1rem"}},"Preparing payment settings..."))};(0,m.registerPlugin)("woocommerce-admin-task-wcpay-page",{scope:"woocommerce-tasks",render:()=>(0,a.createElement)(u.WooOnboardingTask,{id:"woocommerce-payments"},(0,a.createElement)(Pe,null))});const Ve=()=>{const[e,t]=(0,a.useState)([]),{isResolving:n,taskLists:o}=(0,s.useSelect)((e=>({isResolving:e(c.ONBOARDING_STORE_NAME).isResolving("getTaskLists"),taskLists:e(c.ONBOARDING_STORE_NAME).getTaskLists()})));return(0,a.useEffect)((()=>{if(o&&o.length>0){const e=[];for(const t of o)for(const n of t.tasks)n.isDeprecated&&n.container&&e.push(n);t(e)}}),[o]),n?null:(0,a.createElement)(a.Fragment,null,e.map((e=>(0,a.createElement)(u.WooOnboardingTask,{id:e.id,key:e.id},(()=>e.container)))))};(0,m.registerPlugin)("wc-admin-deprecated-task-container",{scope:"woocommerce-tasks",render:()=>(0,a.createElement)(Ve,null)}),(0,m.registerPlugin)("woocommerce-admin-task-customize-store",{scope:"woocommerce-tasks",render:()=>(0,a.createElement)(u.WooOnboardingTaskListItem,{id:"customize-store"},(e=>{let{defaultTaskItem:t}=e;return(0,a.createElement)(t,{onClick:()=>{window.location.href=(0,G.getAdminLink)("admin.php?page=wc-admin&path=%2Fcustomize-store")}})}))}),(async()=>{var e,t,o,a;(null===(e=window)||void 0===e||null===(t=e.wcAdminFeatures)||void 0===t?void 0:t["import-products-task"])&&(null==r||null===(o=r.profile)||void 0===o?void 0:o.selling_venues)&&"no"!==(null==r||null===(a=r.profile)||void 0===a?void 0:a.selling_venues)?Promise.all([n.e(6412),n.e(4891)]).then(n.bind(n,14891)):Promise.all([n.e(6412),n.e(5792)]).then(n.bind(n,65792))})(),window.wcAdminFeatures&&window.wcAdminFeatures["shipping-smart-defaults"]&&n.e(8994).then(n.bind(n,8994));var Re=n(47642),je=n(92342),De=n(46530),Ze=n(26016);var Be=n(22629),Fe=n(25753);class ze extends a.Component{constructor(e){super(e),this.state={purchaseNowButtonBusy:!1,purchaseLaterButtonBusy:!1}}onClickPurchaseNow(){const{productIds:e,onClickPurchaseNow:t}=this.props;if(this.setState({purchaseNowButtonBusy:!0}),!e.length)return;(0,l.recordEvent)("tasklist_modal_proceed_checkout",{product_ids:e,purchase_install:!0});const n=(0,Fe.C)("https://woocommerce.com/cart?utm_medium=product",{"wccom-replace-with":e.join(",")});t?t(n):window.location=n}onClickPurchaseLater(){const{productIds:e}=this.props;(0,l.recordEvent)("tasklist_modal_proceed_checkout",{product_ids:e,purchase_install:!1}),this.setState({purchaseLaterButtonBusy:!0}),this.props.onClickPurchaseLater()}onClose(){const{onClose:e,productIds:t}=this.props;(0,l.recordEvent)("tasklist_modal_proceed_checkout",{product_ids:t,purchase_install:!1}),e()}renderProducts(){const{productIds:e,productTypes:t}=this.props,{themes:n=[]}=(0,o.O3)("onboarding",{}),r=[];return e.forEach((e=>{const o=(0,W.find)(t,(t=>t.product===e));o&&r.push({title:o.label,content:o.description});const s=(0,W.find)(n,(t=>t.id===e));s&&r.push({title:(0,i.sprintf)((0,i.__)("%s — %s per year","woocommerce"),s.title,(0,Be.decodeEntities)(s.price)),content:(0,a.createElement)("span",{dangerouslySetInnerHTML:(0,S.ZP)(s.excerpt)})})})),(0,a.createElement)(f.List,{items:r})}render(){const{purchaseNowButtonBusy:e,purchaseLaterButtonBusy:t}=this.state;return(0,a.createElement)(p.Modal,{title:(0,i.__)("Would you like to add the following paid features to your store now?","woocommerce"),onRequestClose:()=>this.onClose(),className:"woocommerce-cart-modal"},this.renderProducts(),(0,a.createElement)("p",{className:"woocommerce-cart-modal__help-text"},(0,i.__)("You won't have access to this functionality until the extensions have been purchased and installed.","woocommerce")),(0,a.createElement)("div",{className:"woocommerce-cart-modal__actions"},(0,a.createElement)(p.Button,{isLink:!0,isBusy:t,onClick:()=>this.onClickPurchaseLater()},(0,i.__)("I'll do it later","woocommerce")),(0,a.createElement)(p.Button,{isPrimary:!0,isBusy:e,onClick:()=>this.onClickPurchaseNow()},(0,i.__)("Buy now","woocommerce"))))}}const We=(0,z.compose)((0,s.withSelect)((e=>{const{getInstalledPlugins:t}=e(c.PLUGINS_STORE_NAME),{getProductTypes:n,getProfileItems:o}=e(c.ONBOARDING_STORE_NAME),r=o(),a=t(),i=n();return{profileItems:r,productIds:(0,V.jt)(i,r,!1,a),productTypes:i}})))(ze),Ue={store_details:e=>{let{task:t,goToTask:n}=e;return(0,a.createElement)("div",{className:"woocommerce-task-header__contents-container"},(0,a.createElement)("img",{alt:(0,i.__)("Store location illustration","woocommerce"),src:o.vm+"images/task_list/store-details-illustration.png",className:"svg-background"}),(0,a.createElement)("div",{className:"woocommerce-task-header__contents"},(0,a.createElement)("h1",null,(0,i.__)("First, tell us about your store","woocommerce")),(0,a.createElement)("p",null,(0,i.__)("Get your store up and running in no time. Add your store's address to set up shipping, tax and payments faster.","woocommerce")),(0,a.createElement)(p.Button,{isSecondary:t.isComplete,isPrimary:!t.isComplete,onClick:n},(0,i.__)("Add details","woocommerce")),(0,a.createElement)("p",{className:"woocommerce-task-header__timer"},(0,a.createElement)("img",{src:Ze,alt:"Timer"})," ",(0,a.createElement)("span",null,(0,i.__)("2 minutes","woocommerce")))))},"customize-store":e=>{let{task:t}=e;return(0,a.createElement)("div",{className:`woocommerce-task-header__contents-container woocommerce-task-header__${t.id}`},(0,a.createElement)("img",{alt:(0,i.__)("Customize your store illustration","woocommerce"),src:o.vm+"images/task_list/customize-store-illustration.svg",className:"svg-background"}),(0,a.createElement)("div",{className:"woocommerce-task-header__contents"},(0,a.createElement)("h1",null,(0,i.__)("Start customizing your store","woocommerce")),(0,a.createElement)("p",null,(0,i.__)("Use our built-in AI tools to design your store and populate it with content, or select a pre-built theme and customize it to fit your brand.","woocommerce")),(0,a.createElement)(p.Button,{isSecondary:t.isComplete,isPrimary:!t.isComplete,onClick:()=>{window.location.href=(0,G.getAdminLink)("admin.php?page=wc-admin&path=%2Fcustomize-store")}},(0,i.__)("Start customizing","woocommerce"))))},tax:e=>{let{task:t,goToTask:n}=e;return(0,a.createElement)("div",{className:"woocommerce-task-header__contents-container"},(0,a.createElement)("img",{alt:(0,i.__)("Tax illustration","woocommerce"),src:o.vm+"images/task_list/tax-illustration.png",className:"svg-background"}),(0,a.createElement)("div",{className:"woocommerce-task-header__contents"},(0,a.createElement)("h1",null,(0,i.__)("Add your tax rates","woocommerce")),(0,a.createElement)("p",null,(0,i.__)("Set up tax rates manually or use WooCommerce and Jetpack to automate your sales tax calculations for you.","woocommerce")),(0,a.createElement)(p.Button,{isSecondary:t.isComplete,isPrimary:!t.isComplete,onClick:n},(0,i.__)("Yes, please","woocommerce")),(0,a.createElement)("p",{className:"woocommerce-task-header__timer"},(0,a.createElement)("img",{src:Ze,alt:"Timer"})," ",(0,a.createElement)("span",null,t.time))))},shipping:e=>{let{task:t,goToTask:n}=e;return(0,a.createElement)("div",{className:"woocommerce-task-header__contents-container"},(0,a.createElement)("img",{alt:(0,i.__)("Shipping illustration","woocommerce"),src:o.vm+"images/task_list/shipping-illustration.png",className:"svg-background"}),(0,a.createElement)("div",{className:"woocommerce-task-header__contents"},(0,a.createElement)("h1",null,(0,i.__)("Set up shipping for your store","woocommerce")),(0,a.createElement)("p",null,(0,i.__)("Choose where and how you will ship your products, select shipping methods, and add fixed or calculated rates.","woocommerce")),(0,a.createElement)(p.Button,{isSecondary:t.isComplete,isPrimary:!t.isComplete,onClick:n},(0,i.__)("Add shipping zones","woocommerce")),(0,a.createElement)("p",{className:"woocommerce-task-header__timer"},(0,a.createElement)("img",{src:Ze,alt:"Timer"})," ",(0,a.createElement)("span",null,t.time))))},marketing:e=>{let{task:t,goToTask:n}=e;return(0,a.createElement)("div",{className:"woocommerce-task-header__contents-container"},(0,a.createElement)("img",{alt:(0,i.__)("Marketing illustration","woocommerce"),src:o.vm+"images/task_list/sales-illustration.png",className:"svg-background"}),(0,a.createElement)("div",{className:"woocommerce-task-header__contents"},(0,a.createElement)("h1",null,(0,i.__)("Get more sales","woocommerce")),(0,a.createElement)("p",null,(0,i.__)("Give your business a boost by enabling social channels such as newsletter, Facebook, Google, in-person selling, and more.","woocommerce")),(0,a.createElement)(p.Button,{isSecondary:t.isComplete,isPrimary:!t.isComplete,onClick:n},(0,i.__)("Add sales channels","woocommerce")),(0,a.createElement)("p",{className:"woocommerce-task-header__timer"},(0,a.createElement)("img",{src:Ze,alt:"Timer"})," ",(0,a.createElement)("span",null,t.time))))},appearance:e=>{let{task:t,goToTask:n}=e;return(0,a.createElement)("div",{className:"woocommerce-task-header__contents-container"},(0,a.createElement)("img",{alt:(0,i.__)("Appearance illustration","woocommerce"),src:o.vm+"images/task_list/expand-section-illustration.png",className:"svg-background"}),(0,a.createElement)("div",{className:"woocommerce-task-header__contents"},(0,a.createElement)("h1",null,(0,i.__)("Make your store stand out","woocommerce")),(0,a.createElement)("p",null,(0,i.__)("Add your logo, create a homepage, and start designing your store.","woocommerce")),(0,a.createElement)(p.Button,{isSecondary:t.isComplete,isPrimary:!t.isComplete,onClick:n},t.isComplete?(0,i.__)("Modify choices","woocommerce"):(0,i.__)("Personalize","woocommerce")),(0,a.createElement)("p",{className:"woocommerce-task-header__timer"},(0,a.createElement)("img",{src:Ze,alt:"Timer"})," ",(0,a.createElement)("span",null,t.time))))},payments:e=>{let{task:t,goToTask:n}=e;return(0,a.createElement)("div",{className:"woocommerce-task-header__contents-container"},(0,a.createElement)("img",{alt:(0,i.__)("Payment illustration","woocommerce"),src:o.vm+"images/task_list/payment-illustration.png",className:"svg-background"}),(0,a.createElement)("div",{className:"woocommerce-task-header__contents"},(0,a.createElement)("h1",null,(0,i.__)("Add a way to get paid","woocommerce")),(0,a.createElement)("p",null,(0,i.__)("Choose from fast & secure online and offline payment methods to make it easy for your customers to pay in your store.","woocommerce")),(0,a.createElement)(p.Button,{isSecondary:t.isComplete,isPrimary:!t.isComplete,onClick:n},(0,i.__)("View options","woocommerce")),(0,a.createElement)("p",{className:"woocommerce-task-header__timer"},(0,a.createElement)("img",{src:Ze,alt:"Timer"})," ",(0,a.createElement)("span",null,t.time))))},products:e=>{let{task:t,goToTask:n}=e;return(0,a.createElement)("div",{className:"woocommerce-task-header__contents-container"},(0,a.createElement)("img",{alt:(0,i.__)("Products illustration","woocommerce"),src:o.vm+"images/task_list/sales-section-illustration.png",className:"svg-background"}),(0,a.createElement)("div",{className:"woocommerce-task-header__contents"},(0,a.createElement)("h1",null,(0,i.__)("Add products to sell","woocommerce")),(0,a.createElement)("p",null,(0,i.__)("Build your catalog by adding what you want to sell. You can add products manually or import them from a different store.","woocommerce")),(0,a.createElement)(p.Button,{isSecondary:t.isComplete,isPrimary:!t.isComplete,onClick:n},(0,i.__)("Add products","woocommerce")),(0,a.createElement)("p",{className:"woocommerce-task-header__timer"},(0,a.createElement)("img",{src:Ze,alt:"Timer"})," ",(0,a.createElement)("span",null,(0,i.__)("2 minutes","woocommerce")))))},purchase:e=>{let{task:t}=e;const[n,r]=(0,a.useState)(!1),s=(0,a.useCallback)((()=>{n||(0,l.recordEvent)("tasklist_purchase_extensions"),r(!n)}),[n]);return(0,a.createElement)("div",{className:"woocommerce-task-header__contents-container"},(0,a.createElement)("img",{alt:(0,i.__)("Purchase illustration","woocommerce"),src:o.vm+"images/task_list/purchase-illustration.png",className:"svg-background"}),(0,a.createElement)("div",{className:"woocommerce-task-header__contents"},(0,a.createElement)("h1",null,t.title),(0,a.createElement)("p",null,(0,i.__)("Good choice! You chose to add amazing new features to your store. Continue to checkout to complete your purchase.","woocommerce")),(0,a.createElement)(p.Button,{isSecondary:t.isComplete,isPrimary:!t.isComplete,onClick:s},(0,i.__)("Continue","woocommerce")),(0,a.createElement)("p",{className:"woocommerce-task-header__timer"},(0,a.createElement)("img",{src:Ze,alt:"Timer"})," ",(0,a.createElement)("span",null,t.time))),n&&(0,a.createElement)(We,{onClose:()=>s(),onClickPurchaseLater:()=>s()}))},"woocommerce-payments":e=>{var t;let{task:n,trackClick:r}=e;const l=(0,o.O3)("wcpayWelcomePageIncentive")||(null===(t=window.wcpaySettings)||void 0===t?void 0:t.connectIncentive),{createNotice:m}=(0,s.useDispatch)("core/notices"),[u,d]=(0,a.useState)(!1);return(0,a.createElement)("div",{className:"woocommerce-task-header__contents-container"},(0,a.createElement)("img",{alt:(0,i.__)("Payment illustration","woocommerce"),src:o.vm+"images/task_list/payment-illustration.png",className:"svg-background"}),(0,a.createElement)("div",{className:"woocommerce-task-header__contents"},(0,a.createElement)("h1",null,(0,i.__)("It's time to get paid","woocommerce")),null!=l&&l.task_header_content?(0,a.createElement)("p",{dangerouslySetInnerHTML:(0,S.ZP)(l.task_header_content)}):(0,a.createElement)("p",null,(0,i.__)("You're only one step away from getting paid. Verify your business details to start managing transactions with WooPayments.","woocommerce")),(0,a.createElement)("p",null,(0,U.Z)({mixedString:(0,i.__)('By clicking "Verify Details", you agree to the {{link}}Terms of Service{{/link}}',"woocommerce"),components:{link:(0,a.createElement)(f.Link,{href:"https://wordpress.com/tos/",target:"_blank",type:"external",rel:"noreferrer"})}})),(0,a.createElement)(p.Button,{isSecondary:n.isComplete,isPrimary:!n.isComplete,isBusy:u,disabled:u,onClick:()=>{r(),((e,t)=>{const n=(0,i.__)("There was an error connecting to WooPayments. Please try again or connect later in store settings.","woocommerce");t(!0),I()({path:c.WC_ADMIN_NAMESPACE+"/plugins/connect-wcpay",method:"POST"}).then((e=>{window.location=e.connectUrl})).catch((()=>{e("error",n),t(!1)}))})(m,d)}},(0,i.__)("Verify details","woocommerce")),(0,a.createElement)("p",{className:"woocommerce-task-header__timer"},(0,a.createElement)("img",{src:Ze,alt:"Timer"})," ",(0,a.createElement)("span",null,(0,i.__)("2 minutes","woocommerce")))))}},Ge=e=>{let{showDismissModal:t,setShowDismissModal:n,hideTasks:o}=e;const r=(0,i.__)("Hide store setup tasks","woocommerce"),s=(0,i.__)("Are you sure? These tasks are required for all stores.","woocommerce"),c=(0,i.__)("Cancel","woocommerce"),l=(0,i.__)("Yes, hide store setup tasks","woocommerce");return(0,a.createElement)(a.Fragment,null,t&&(0,a.createElement)(p.Modal,{title:r,className:"woocommerce-task-dismiss-modal",onRequestClose:()=>n(!1)},(0,a.createElement)("div",{className:"woocommerce-task-dismiss-modal__wrapper"},(0,a.createElement)("div",{className:"woocommerce-usage-modal__message"},s),(0,a.createElement)("div",{className:"woocommerce-usage-modal__actions"},(0,a.createElement)(p.Button,{onClick:()=>n(!1)},c),(0,a.createElement)(p.Button,{isPrimary:!0,onClick:()=>{o("remove_card"),n(!1)}},l)))))};var Ye=n(21079);const Qe=e=>{let{hideTasks:t,keepTasks:n}=e;return(0,a.createElement)(a.Fragment,null,(0,a.createElement)("div",{className:g()("woocommerce-task-dashboard__container setup-task-list")},(0,a.createElement)(p.Card,{size:"large",className:"woocommerce-task-card woocommerce-homescreen-card completed"},(0,a.createElement)(p.CardHeader,{size:"medium"},(0,a.createElement)("div",{className:"wooocommerce-task-card__header"},(0,a.createElement)("img",{src:Ye,alt:"Completed"}),(0,a.createElement)("h2",null,(0,i.__)("You've completed store setup","woocommerce")),(0,a.createElement)(p.Button,{isSecondary:!0,onClick:n},(0,i.__)("Keep list","woocommerce")),(0,a.createElement)(p.Button,{isPrimary:!0,onClick:t},(0,i.__)("Hide this list","woocommerce")))))))},qe="woocommerce_tasklist_experimental_progress_header_item",Je=e=>{let{children:t,order:n=1}=e;return(0,a.createElement)(p.Fill,{name:qe},(e=>(0,f.createOrderedChildren)(t,n,e)))};Je.Slot=e=>{let{fillProps:t}=e;return(0,a.createElement)(p.Slot,{name:qe,fillProps:t},f.sortFillsByOrder)};const Ke=e=>{let{id:t,hideTaskListText:n}=e;const{hideTaskList:o}=(0,s.useDispatch)(c.ONBOARDING_STORE_NAME);return(0,a.createElement)("div",{className:"woocommerce-card__menu woocommerce-card__header-item"},(0,a.createElement)(f.EllipsisMenu,{label:(0,i.__)("Task List Options","woocommerce"),renderContent:()=>(0,a.createElement)("div",{className:"woocommerce-task-card__section-controls"},(0,a.createElement)(p.Button,{onClick:()=>o(t)},n||(0,i.__)("Hide this","woocommerce")))}))},Xe=e=>{let{taskListId:t}=e;const{loading:n,tasksCount:o,completedCount:r}=(0,s.useSelect)((e=>{const n=e(c.ONBOARDING_STORE_NAME).getTaskList(t),o=e(c.ONBOARDING_STORE_NAME).hasFinishedResolution("getTaskList",[t]),r=(0,c.getVisibleTasks)((null==n?void 0:n.tasks)||[]);return{loading:!o,tasksCount:null==r?void 0:r.length,completedCount:null==r?void 0:r.filter((e=>e.isComplete)).length}}));return n?null:(0,a.createElement)("div",{className:"woocommerce-task-progress-header"},(0,a.createElement)(Ke,{id:t,hideTaskListText:(0,i.__)("Hide setup list","woocommerce")}),(0,a.createElement)("div",{className:"woocommerce-task-progress-header__contents"},r!==o?(0,a.createElement)(a.Fragment,null,(0,a.createElement)("p",null,(0,i.sprintf)((0,i.__)("Follow these steps to start selling quickly. %1$d out of %2$d complete.","woocommerce"),r,o)),(0,a.createElement)("progress",{className:"woocommerce-task-progress-header__progress-bar",max:o,value:r||.25})):null))},$e=e=>{var t;let{taskListId:n}=e;const o=(0,y.useSlot)(qe);return Boolean(null==o||null===(t=o.fills)||void 0===t?void 0:t.length)?(0,a.createElement)(Je.Slot,{fillProps:{taskListId:n}}):(0,a.createElement)(Xe,{taskListId:n})},et=e=>{var t;let{task:n,activeTaskId:o,taskIndex:r,goToTask:l,trackClick:m}=e;const{createNotice:d}=(0,s.useDispatch)("core/notices"),{dismissTask:p,undoDismissTask:C}=(0,s.useDispatch)(c.ONBOARDING_STORE_NAME),{id:h,title:f,badge:w,content:H,time:_,actionLabel:v,isComplete:E,additionalInfo:L,isDismissable:b}=n,k=(0,y.useSlot)(`woocommerce_onboarding_task_list_item_${h}`),S=Boolean(null==k||null===(t=k.fills)||void 0===t?void 0:t.length),M=(0,a.useCallback)((e=>{const t=g()("woocommerce-task-list__item index-"+r,{complete:E,"is-active":h===o});return(0,a.createElement)(y.TaskItem,{key:h,className:t,title:f,badge:w,completed:E,additionalInfo:L,content:H,onClick:t=>{"A"!==t.target.tagName&&(()=>{if(e.onClick)return m(),e.onClick();l()})()},onDismiss:b?()=>(p(h),void d("success",(0,i.__)("Task dismissed","woocommerce"),{actions:[{label:(0,i.__)("Undo","woocommerce"),onClick:()=>C(h)}]})):void 0,action:()=>{},actionLabel:v})}),[h,f,w,H,_,v,E,o]);return S?(0,a.createElement)(u.WooOnboardingTaskListItem.Slot,{id:h,fillProps:{defaultTaskItem:M,isComplete:E}}):(0,a.createElement)(M,null)};var tt=n(75606),nt=n(5196);const ot="store_setup";function rt(e){if(0===e)return null;const t=Date.now()-1e3*e;return Math.round(t/c.WEEK)}const at=e=>{let{hideTasks:t,keepTasks:n,customerEffortScore:o}=e;const{updateOptions:r}=(0,s.useDispatch)(c.OPTIONS_STORE_NAME),[m,u]=(0,a.useState)(!1),[d,C]=(0,a.useState)(!1),[h,w]=(0,a.useState)(NaN),[H,_]=(0,a.useState)(!1),{storeAgeInWeeks:v,cesShownForActions:E,canShowCustomerEffortScore:L}=(0,s.useSelect)((e=>{const{getOption:t,hasFinishedResolution:n}=e(c.OPTIONS_STORE_NAME);if(o){const e=t(tt.ALLOW_TRACKING_OPTION_NAME),o=t(tt.ADMIN_INSTALL_TIMESTAMP_OPTION_NAME)||0,r=t(tt.SHOWN_FOR_ACTIONS_OPTION_NAME),a=!n("getOption",[tt.SHOWN_FOR_ACTIONS_OPTION_NAME])||!n("getOption",[tt.ADMIN_INSTALL_TIMESTAMP_OPTION_NAME]);return{storeAgeInWeeks:rt(o),cesShownForActions:r,canShowCustomerEffortScore:!a&&e&&!(r||[]).includes("store_setup"),loading:a}}return{}}));(0,a.useEffect)((()=>{d&&setTimeout((()=>{_(!0)}),1200)}),[d]);const b=e=>{let{firstScore:t,secondScore:n,comments:o}=e;(0,l.recordEvent)("ces_feedback",{action:ot,score:t,score_second_question:null!=n?n:null,score_combined:t+(null!=n?n:0),comments:o||"",store_age:v}),r({[tt.SHOWN_FOR_ACTIONS_OPTION_NAME]:[ot,...E||[]]}),C(!0)};return(0,a.createElement)(a.Fragment,null,(0,a.createElement)("div",{className:g()("woocommerce-task-dashboard__container setup-task-list")},(0,a.createElement)(p.Card,{size:"large",className:"woocommerce-task-card woocommerce-homescreen-card completed"},(0,a.createElement)(p.CardHeader,{size:"medium"},(0,a.createElement)("div",{className:"wooocommerce-task-card__header"},(0,a.createElement)("img",{src:nt,alt:"Completed",className:"wooocommerce-task-card__finished-header-image"}),(0,a.createElement)(y.Text,{size:"title",as:"h2",lineHeight:1.4},(0,i.__)("You've completed store setup","woocommerce")),(0,a.createElement)(y.Text,{variant:"subtitle.small",as:"p",size:"13",lineHeight:"16px",className:"wooocommerce-task-card__header-subtitle"},(0,i.__)("Congratulations! Take a moment to celebrate and look out for the first sale.","woocommerce")),(0,a.createElement)("div",{className:"woocommerce-task-card__header-menu"},(0,a.createElement)(f.EllipsisMenu,{label:(0,i.__)("Task List Options","woocommerce"),renderContent:()=>(0,a.createElement)("div",{className:"woocommerce-task-card__section-controls"},(0,a.createElement)(p.Button,{onClick:()=>n()},(0,i.__)("Show setup task list","woocommerce")),(0,a.createElement)(p.Button,{onClick:()=>t()},(0,i.__)("Hide this","woocommerce")))})))),L&&!H&&!d&&(0,a.createElement)(tt.CustomerFeedbackSimple,{label:(0,i.__)("How was your experience?","woocommerce"),onSelect:e=>{e>2?(w(e),b({firstScore:e})):(w(e),u(!0),(0,l.recordEvent)("ces_view",{action:ot,store_age:v}))}}),d&&!H&&(0,a.createElement)("div",{className:"wooocommerce-task-card__header-ces-feedback"},(0,a.createElement)(y.Text,{variant:"subtitle.small",as:"p",size:"13",lineHeight:"16px"},"🙌"," ",(0,i.__)("We appreciate your feedback!","woocommerce"))))),m?(0,a.createElement)(tt.CustomerFeedbackModal,{title:(0,i.__)("How was your experience?","woocommerce"),firstQuestion:(0,i.__)("The store setup is easy to complete.","woocommerce"),secondQuestion:(0,i.__)("The store setup process meets my needs.","woocommerce"),defaultScore:h,recordScoreCallback:(e,t,n)=>{u(!1),b({firstScore:e,secondScore:t,comments:n})},onCloseModal:()=>{w(NaN),u(!1)}}):null)},it="experimental_woocommerce_tasklist_footer_item",st=e=>{let{children:t,order:n=1}=e;return(0,a.createElement)(p.Fill,{name:it},(e=>(0,f.createOrderedChildren)(t,n,e)))};st.Slot=e=>{let{fillProps:t}=e;return(0,a.createElement)(p.Slot,{name:it,fillProps:t},f.sortFillsByOrder)};const ct=()=>{var e;const t=(0,y.useSlot)(it);return Boolean(null==t||null===(e=t.fills)||void 0===e?void 0:e.length)?(0,a.createElement)("div",{className:"woocommerce-tasklist__footer"},(0,a.createElement)(st.Slot,null)):null},lt="woocommerce_experimental_task_list_completion",mt=e=>{let{children:t,order:n=1}=e;return(0,a.createElement)(p.Fill,{name:lt},(e=>(0,f.createOrderedChildren)(t,n,e)))};mt.Slot=e=>{let{fillProps:t}=e;return(0,a.createElement)(p.Slot,{name:lt,fillProps:t},f.sortFillsByOrder)};const ut=e=>{var t;let{className:n,fillProps:o}=e;const r=(0,y.useSlot)(lt);return Boolean(null==r||null===(t=r.fills)||void 0===t?void 0:t.length)?(0,a.createElement)("div",{className:g()("woocommerce-tasklist-completion-slot",n)},(0,a.createElement)(mt.Slot,{fillProps:o})):null},dt=e=>{var t,n,o,r,m,C;let{query:h,id:w,eventName:H,eventPrefix:_,tasks:v,keepCompletedTaskList:E,isComplete:L,displayProgressHeader:b,cesHeader:k=!0}=e;const S=H?H+"_":_,{profileItems:M}=(0,s.useSelect)((e=>{const{getProfileItems:t}=e(c.ONBOARDING_STORE_NAME);return{profileItems:t()}})),{hideTaskList:N,visitedTask:x,keepCompletedTaskList:T}=(0,s.useDispatch)(c.ONBOARDING_STORE_NAME),I=(0,c.useUserPreferences)(),[A,O]=(0,a.useState)({}),[P,V]=(0,a.useState)(""),[R,j]=(0,a.useState)(!1),{layoutString:D}=(0,De.useLayoutContext)(),Z=(0,a.useRef)(h),B=(0,c.getVisibleTasks)(v);(0,a.useEffect)((()=>{h.task||(0,l.recordEvent)(`${S}view`,{number_tasks:B.length,store_connected:M.wccom_connected,context:D})}),[]);const F=(0,y.useSlot)(lt);(0,a.useEffect)((()=>{const{task:e}=Z.current,{task:t}=h;e!==t&&(window.document.documentElement.scrollTop=0,Z.current=h)}),[h]);const z=v.filter((e=>!e.isComplete&&!e.isDismissed)),W=()=>{N(w)},U=()=>{T(w)};let G=B.find((e=>!1===e.isComplete));G||(G=B[B.length-1]);const Y=(0,y.useSlot)(`woocommerce_onboarding_task_list_header_${null!==(t=null==A||null===(n=A.task)||void 0===n?void 0:n.id)&&void 0!==t?t:null===(o=G)||void 0===o?void 0:o.id}`),Q=Boolean(null==Y||null===(r=Y.fills)||void 0===r?void 0:r.length),q=e=>{(0,l.recordEvent)(`${S}click`,{task_name:e.id,context:D})},J=e=>{q(e),e.isComplete||(e=>{const t=(e=>{const t=I.task_list_tracked_started_tasks;return t&&t[e]?t[e]:0})(e)+1,n=I.task_list_tracked_started_tasks||{};x(e),I.updateUserPreferences({task_list_tracked_started_tasks:{...n||{},[e]:t}})})(e.id),e.actionUrl?(0,d.navigateTo)({url:e.actionUrl}):(0,d.navigateTo)({url:(0,d.getNewPath)({task:e.id},"/",{})})};if((0,a.useEffect)((()=>{var e;G&&(Ue[(e=G).id]||Q)&&(O({task:e,goToTask:()=>J(e),trackClick:()=>q(e)}),V(e.id))}),[G]),!B.length)return(0,a.createElement)("div",{className:"woocommerce-task-dashboard__container"});const K=Boolean(null==F||null===(m=F.fills)||void 0===m?void 0:m.length);return L&&"yes"!==E?K?(0,a.createElement)(ut,{fillProps:{hideTasks:W,keepTasks:U,customerEffortScore:k}}):(0,a.createElement)(a.Fragment,null,k?(0,a.createElement)(at,{hideTasks:W,keepTasks:U,customerEffortScore:!0}):(0,a.createElement)(Qe,{hideTasks:W,keepTasks:U})):(0,a.createElement)(a.Fragment,null,R&&(0,a.createElement)(Ge,{showDismissModal:R,setShowDismissModal:j,hideTasks:W}),b?(0,a.createElement)($e,{taskListId:w}):null,(0,a.createElement)("div",{className:g()(`woocommerce-task-dashboard__container woocommerce-task-list__${w} setup-task-list`)},(0,a.createElement)(p.Card,{size:"large",className:"woocommerce-task-card woocommerce-homescreen-card"},(0,a.createElement)("div",{className:"wooocommerce-task-card__header-container"},(0,a.createElement)("div",{className:"wooocommerce-task-card__header"},Q?(0,a.createElement)(u.WooOnboardingTaskListHeader.Slot,{id:null===(C=G)||void 0===C?void 0:C.id,fillProps:A}):(null==A?void 0:A.task)&&(0,a.createElement)(Ue[A.task.id],A)),!b&&(0,a.createElement)("div",{className:"woocommerce-card__menu woocommerce-card__header-item"},(0,a.createElement)(f.EllipsisMenu,{className:w,label:(0,i.__)("Task List Options","woocommerce"),renderContent:e=>{let{onToggle:t}=e;return(0,a.createElement)("div",{className:"woocommerce-task-card__section-controls"},(0,a.createElement)(p.Button,{onClick:()=>{z.length>0?(j(!0),t()):W()}},(0,i.__)("Hide this","woocommerce")))}}))),(0,a.createElement)(y.List,{animation:"custom"},B.map(((e,t)=>(0,a.createElement)(et,{key:e.id,taskIndex:++t,activeTaskId:P,task:e,goToTask:()=>J(e),trackClick:()=>q(e)})))),(0,a.createElement)(ct,null))))},pt=e=>{const{numTasks:t=5}=e;return(0,a.createElement)("div",{className:g()("woocommerce-task-dashboard__container setup-task-list")},(0,a.createElement)("div",{className:"components-card is-size-large woocommerce-task-card woocommerce-homescreen-card is-loading"},(0,a.createElement)("div",{className:"components-card__header is-size-medium"},(0,a.createElement)("div",{className:"wooocommerce-task-card__header"},(0,a.createElement)("div",{className:"is-placeholder"}," "))),(0,a.createElement)("ul",{className:"woocommerce-experimental-list"},Array.from(new Array(t)).map(((e,t)=>(0,a.createElement)("li",{tabIndex:t,key:t,className:"woocommerce-experimental-list__item woocommerce-task-list__item"},(0,a.createElement)("div",{className:"woocommerce-task-list__item-before"},(0,a.createElement)("div",{className:"is-placeholder"})),(0,a.createElement)("div",{className:"woocommerce-task-list__item-text"},(0,a.createElement)("div",{className:"components-truncate components-text is-placeholder"}))))))))};var Ct=n(70048),ht=n(39630);const gt=e=>{let{title:t}=e;const n=(0,i.__)("WooCommerce Home","woocommerce"),o=()=>{(0,l.recordEvent)("topbar_back_button",{page_name:t}),(0,d.updateQueryString)({},(0,d.getHistory)().location.pathname,{})};return(0,a.createElement)(p.Tooltip,{text:n},(0,a.createElement)("div",{tabIndex:0,role:"button","data-testid":"header-back-button",className:"woocommerce-layout__header-back-button",onKeyDown:e=>{let{keyCode:t}=e;t!==ht.ENTER&&t!==ht.SPACE||o()}},(0,a.createElement)(K.Z,{icon:Ct.Z,onClick:o})))},ft=e=>{let{query:t,task:n}=e;const o=t.task||"";o||console.warn("No task id provided");const{invalidateResolutionForStoreSelector:r,optimisticallyCompleteTask:i}=(0,s.useDispatch)(c.ONBOARDING_STORE_NAME),l=(0,a.useCallback)((()=>{const e=Array.from(document.querySelectorAll("#adminmenu .woocommerce-task-list-remaining-tasks-badge"));null!=e&&e.length&&e.forEach((e=>{const t=Number(e.innerText);1===t?e.remove():e.innerHTML=String(t-1)}))}),[]),m=(0,a.useCallback)((e=>{i(o),(0,d.getHistory)().push(e&&e.redirectPath?e.redirectPath:(0,d.getNewPath)({},"/",{})),r("getTaskLists"),l()}),[o]);return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(De.WooHeaderNavigationItem,null,(0,a.createElement)(gt,{title:n.title})),(0,a.createElement)(De.WooHeaderPageTitle,null,n.title),(0,a.createElement)(u.WooOnboardingTask.Slot,{id:o,fillProps:{onComplete:m,query:t,task:n}}))},yt=e=>{let{numTasks:t=5,query:n}=e;return Boolean(n.task)?null:(0,a.createElement)("div",{className:"woocommerce-task-dashboard__container"},(0,a.createElement)("div",{className:"woocommerce-card woocommerce-task-card is-loading","aria-hidden":!0},(0,a.createElement)("div",{className:"woocommerce-card__header"},(0,a.createElement)("div",{className:"woocommerce-card__title-wrapper"},(0,a.createElement)("div",{className:"woocommerce-card__title woocommerce-card__header-item"},(0,a.createElement)("span",{className:"is-placeholder"})))),(0,a.createElement)("div",{className:"woocommerce-card__body"},(0,a.createElement)("div",{className:"woocommerce-list"},Array.from(new Array(t)).map(((e,t)=>(0,a.createElement)("div",{key:t,className:"woocommerce-list__item has-action"},(0,a.createElement)("div",{className:"woocommerce-list__item-inner"},(0,a.createElement)("div",{className:"woocommerce-list__item-before"},(0,a.createElement)("span",{className:"is-placeholder"})),(0,a.createElement)("div",{className:"woocommerce-list__item-text"},(0,a.createElement)("div",{className:"woocommerce-list__item-title"},(0,a.createElement)("span",{className:"is-placeholder"}))),(0,a.createElement)("div",{className:"woocommerce-list__item-after"},(0,a.createElement)("span",{className:"is-placeholder"}))))))))))},wt=e=>{var t;let{isExpandable:n=!1,isExpanded:o=!1,setExpandedTask:r,task:m}=e;const{createNotice:p}=(0,s.useDispatch)("core/notices"),{layoutString:C}=(0,De.useLayoutContext)(),{dismissTask:h,snoozeTask:g,undoDismissTask:f,undoSnoozeTask:w,visitedTask:H}=(0,s.useDispatch)(c.ONBOARDING_STORE_NAME),_=(0,c.useUserPreferences)(),{actionLabel:v,actionUrl:E,content:L,id:b,isComplete:k,isDismissable:S,isSnoozeable:M,time:N,title:x,badge:T,level:I,additionalInfo:A,recordViewEvent:O}=m;(0,a.useEffect)((()=>{O&&(0,l.recordEvent)("tasklist_item_view",{task_name:b,is_complete:k,context:C})}),[]);const P=(0,y.useSlot)(`woocommerce_onboarding_task_list_item_${b}`),V=Boolean(null==P||null===(t=P.fills)||void 0===t?void 0:t.length),R=(0,a.useCallback)((()=>{h(b),p("success",(0,i.__)("Task dismissed","woocommerce"),{actions:[{label:(0,i.__)("Undo","woocommerce"),onClick:()=>f(b)}]})}),[b]),j=(0,a.useCallback)((()=>{g(b),p("success",(0,i.__)("Task postponed until tomorrow","woocommerce"),{actions:[{label:(0,i.__)("Undo","woocommerce"),onClick:()=>w(b)}]})}),[b]),D=(0,a.useCallback)((()=>{E?(0,d.navigateTo)({url:E}):(0,d.navigateTo)({url:(0,d.getNewPath)({task:b},"/",{})})}),[b,k,E]),B={expandable:n,expanded:n&&o,completed:k,onSnooze:M&&j,onDismiss:S&&R},F=(0,a.useCallback)((e=>{const t=()=>((0,l.recordEvent)("tasklist_click",{task_name:b,context:C}),k||(()=>{const e=(()=>{const e=_.task_list_tracked_started_tasks;return e&&e[b]?e[b]:0})()+1,t=_.task_list_tracked_started_tasks||{};H(b),_.updateUserPreferences({task_list_tracked_started_tasks:{...t||{},[b]:e}})})(),e.onClick?e.onClick():D());return(0,a.createElement)(y.TaskItem,(0,Z.Z)({key:b,title:x,badge:T,content:L,additionalInfo:A,time:N,action:t,level:I,actionLabel:v},B,e,{onClick:!n||k?t:()=>r(b)}))}),[b,x,T,L,N,v,n,k]);return V?(0,a.createElement)(u.WooOnboardingTaskListItem.Slot,{id:b,fillProps:{defaultTaskItem:F,isComplete:k,...B}}):(0,a.createElement)(F,{onClick:m.onClick})},Ht=e=>{var t;let{id:n,eventPrefix:o,tasks:r,title:m,isCollapsible:u=!1,isExpandable:d=!1,displayProgressHeader:C=!1,query:h}=e;const{profileItems:g}=(0,s.useSelect)((e=>{const{getProfileItems:t}=e(c.ONBOARDING_STORE_NAME);return{profileItems:t()}})),w=(0,a.useRef)(h),H=(0,c.getVisibleTasks)(r),{layoutString:_}=(0,De.useLayoutContext)(),v=r.filter((e=>!e.isComplete&&!e.isDismissed)),[E,L]=(0,a.useState)(null===(t=v[0])||void 0===t?void 0:t.id);if((0,a.useEffect)((()=>{(0,l.recordEvent)(o+"view",{number_tasks:H.length,store_connected:g.wccom_connected,context:_})}),[]),(0,a.useEffect)((()=>{const{task:e}=w.current,{task:t}=h;e!==t&&(window.document.documentElement.scrollTop=0,w.current=h)}),[h]),!H.length)return(0,a.createElement)("div",{className:"woocommerce-task-dashboard__container"});const b=(0,i.sprintf)((0,i._n)("Show %d more task.","Show %d more tasks.",H.length-2,"woocommerce"),H.length-2),k=(0,i.__)("Show less","woocommerce"),S=H.map((e=>(0,a.createElement)(wt,{key:e.id,isExpanded:E===e.id,isExpandable:d,task:e,setExpandedTask:L})));return(0,a.createElement)(a.Fragment,null,(0,a.createElement)("div",{className:"woocommerce-task-dashboard__container woocommerce-task-list__"+n},C?(0,a.createElement)($e,{taskListId:n}):null,(0,a.createElement)(p.Card,{size:"large",className:"woocommerce-task-card woocommerce-homescreen-card"},(0,a.createElement)(p.CardHeader,{size:"medium"},(0,a.createElement)("div",{className:"wooocommerce-task-card__header"},(0,a.createElement)(y.Text,{size:"20",lineHeight:"28px",variant:"title.small"},m),(0,a.createElement)(f.Badge,{count:v.length})),(0,a.createElement)(Ke,{id:n})),u?(0,a.createElement)(y.CollapsibleList,{animation:"custom",collapseLabel:k,expandLabel:b,show:2,onCollapse:()=>(0,l.recordEvent)(o+"collapse",{}),onExpand:()=>(0,l.recordEvent)(o+"expand",{})},S):(0,a.createElement)(y.List,{animation:"custom"},S))))},_t=e=>{let{query:t}=e;const{task:n}=t,{hideTaskList:r}=(0,s.useDispatch)(c.ONBOARDING_STORE_NAME),{updateOptions:m}=(0,s.useDispatch)(c.OPTIONS_STORE_NAME),{isResolving:u,taskLists:d}=(0,s.useSelect)((e=>({isResolving:!e(c.ONBOARDING_STORE_NAME).hasFinishedResolution("getTaskLists"),taskLists:e(c.ONBOARDING_STORE_NAME).getTaskLists()})));(0,a.useEffect)((()=>{m({woocommerce_task_list_prompt_shown:!0})}),[d,u]);const C=(()=>{if(!n)return null;return d.reduce(((e,t)=>[...e,...t.tasks]),[]).find((e=>e.id===n))||null})();if(n&&!C)return null;if(C)return(0,a.createElement)("div",{className:"woocommerce-task-dashboard__container"},(0,a.createElement)(ft,{query:t,task:C}));const h="setup"===(0,o.O3)("visibleTaskListIds",[])[0]?pt:yt;return u?(0,a.createElement)(h,{query:t}):(0,a.createElement)(a.Fragment,null,d.filter((e=>{let{isVisible:t}=e;return t})).map((e=>{const{id:n,isHidden:o,isToggleable:s}=e,c="setup"===n?dt:Ht;return(0,a.createElement)(a.Fragment,{key:n},(0,a.createElement)(c,(0,Z.Z)({isExpandable:!1,query:t},e)),s&&(0,a.createElement)(je.z,null,(0,a.createElement)(p.MenuGroup,{className:"woocommerce-layout__homescreen-display-options",label:(0,i.__)("Display","woocommerce")},(0,a.createElement)(p.MenuItem,{className:"woocommerce-layout__homescreen-extension-tasklist-toggle",icon:o?void 0:Re.Z,isSelected:!o,role:"menuitemcheckbox",onClick:()=>(e=>{const{id:t,eventPrefix:n,isHidden:o}=e,a=!o;(0,l.recordEvent)(a?`${n}hide`:`${n}show`,{}),r(t)})(e)},(0,i.__)("Show things to do next","woocommerce")))))})))};var vt=n(70261);const Et="woocommerce_task_list_reminder_bar_hidden",Lt=e=>{let{remainingCount:t,tracksProps:n}=e;const o=1===t?(0,i.__)("🎉 Almost there. Only {{strongText}}%1$d step left{{/strongText}} get your store up and running. {{setupLink}}Finish setup{{/setupLink}}","woocommerce"):(0,i.__)("🚀 You're doing great! {{strongText}}%1$d steps left{{/strongText}} to get your store up and running. {{setupLink}}Continue setup{{/setupLink}}","woocommerce");return(0,a.createElement)("p",null,(0,U.Z)({mixedString:(0,i.sprintf)(o,t),components:{strongText:(0,a.createElement)("strong",null),setupLink:(0,a.createElement)(f.Link,{href:(0,G.getAdminLink)("admin.php?page=wc-admin"),onClick:()=>(0,l.recordEvent)("tasklist_reminder_bar_continue",n),type:"wp-admin"},(0,a.createElement)(a.Fragment,null))}}))},bt=e=>{let{taskListId:t,updateBodyMargin:n}=e;const{updateOptions:o}=(0,s.useDispatch)(c.OPTIONS_STORE_NAME),{remainingCount:r,loading:i,taskListHidden:m,taskListComplete:u,reminderBarHidden:C,completedTasksCount:h}=(0,s.useSelect)((e=>{const{getTaskList:n,hasFinishedResolution:o}=e(c.ONBOARDING_STORE_NAME),{getOption:r,hasFinishedResolution:a}=e(c.OPTIONS_STORE_NAME),i=r(Et),s=n(t),l=o("getTaskList",[t]),m=a("getOption",[Et]),u=(0,c.getVisibleTasks)((null==s?void 0:s.tasks)||[]),d=u.filter((e=>e.isComplete))||[],p=l&&m;return{reminderBarHidden:"yes"===i,taskListHidden:!!p&&(null==s?void 0:s.isHidden),taskListComplete:!!p&&(null==s?void 0:s.isComplete),loading:!p,completedTasksCount:d.length,remainingCount:p?(null==u?void 0:u.length)-d.length:null}})),g=(0,d.getQuery)(),f=g.page&&"wc-admin"===g.page&&!g.path,y=Boolean(g.wc_onboarding_active_task),w=i||m||u||C||0===h||f||y;(0,a.useEffect)((()=>{n()}),[w,n]);const H={completed:h,is_homescreen:!!f,is_active_task_page:y};return(0,a.useEffect)((()=>{i||w||(0,l.recordEvent)("tasklist_reminder_bar_view",H)}),[w,i]),w?null:(0,a.createElement)("div",{className:"woocommerce-layout__header-tasks-reminder-bar"},(0,a.createElement)(Lt,{remainingCount:r,tracksProps:H}),(0,a.createElement)(p.Button,{isSmall:!0,onClick:()=>{o({[Et]:"yes"}),(0,l.recordEvent)("tasklist_reminder_bar_close",H)},icon:vt.Z}))},kt="woocommerce_tasklist_experimental_progress_title_item",St=e=>{let{children:t,order:n=1}=e;return(0,a.createElement)(p.Fill,{name:kt},(e=>(0,f.createOrderedChildren)(t,n,e)))};St.Slot=e=>{let{fillProps:t}=e;return(0,a.createElement)(p.Slot,{name:kt,fillProps:t},f.sortFillsByOrder)};const Mt=e=>{let{taskListId:t}=e;const{loading:n,tasksCount:o,completedCount:r,hasVisitedTasks:l}=(0,s.useSelect)((e=>{const n=e(c.ONBOARDING_STORE_NAME).getTaskList(t),o=e(c.ONBOARDING_STORE_NAME).hasFinishedResolution("getTaskList",[t]),r=(0,c.getVisibleTasks)((null==n?void 0:n.tasks)||[]);return{loading:!o,tasksCount:null==r?void 0:r.length,completedCount:null==r?void 0:r.filter((e=>e.isComplete)).length,hasVisitedTasks:(null==r?void 0:r.filter((e=>e.isVisited&&"store_details"!==e.id)).length)>0}})),m=(0,a.useMemo)((()=>{if(!l||r===o){const e=(0,G.getSetting)("siteTitle");return e?(0,i.sprintf)((0,i.__)("Welcome to %s","woocommerce"),e):(0,i.__)("Welcome to your store","woocommerce")}return r<=3?(0,i.__)("Let's get you started","woocommerce")+"   🚀":r>3&&r<6?(0,i.__)("You are on the right track","woocommerce"):(0,i.__)("You are almost there","woocommerce")}),[r,l,o]);return n?null:(0,a.createElement)("h1",{className:"woocommerce-task-progress-header__title",dangerouslySetInnerHTML:(0,S.ZP)(m)})},Nt=e=>{var t;let{taskListId:n}=e;const o=(0,y.useSlot)(kt);return Boolean(null==o||null===(t=o.fills)||void 0===t?void 0:t.length)?(0,a.createElement)(St.Slot,{fillProps:{taskListId:n}}):(0,a.createElement)(Mt,{taskListId:n})},xt=()=>{const{activeSetuplist:e}=(0,s.useSelect)((e=>{const t=e(c.ONBOARDING_STORE_NAME).getTaskLists().filter((e=>"setup"===e.id&&e.isVisible));return{activeSetuplist:t.length?t[0].id:null}}));return e}},73463:(e,t,n)=>{"use strict";n.d(t,{MV:()=>m,O3:()=>c,SX:()=>l,rq:()=>d,vm:()=>u});var o=n(65736),r=n(74617);const a=["wcAdminSettings","preloadSettings"],i=(0,r.getSetting)("admin",{}),s=Object.keys(i).reduce(((e,t)=>(a.includes(t)||(e[t]=i[t]),e)),{});function c(e){let t=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:e=>e;if(a.includes(e))throw new Error((0,o.__)("Mutable settings should be accessed via data store.","woocommerce"));const r=s.hasOwnProperty(e)?s[e]:t;return n(r,t)}const l=(0,r.getSetting)("adminUrl"),m=((0,r.getSetting)("countries"),(0,r.getSetting)("currency"),(0,r.getSetting)("locale")),u=((0,r.getSetting)("siteTitle"),(0,r.getSetting)("wcAssetUrl")),d=c("orderStatuses")},34374:(e,t,n)=>{"use strict";n.d(t,{V7:()=>r,br:()=>a,GG:()=>s,Jm:()=>l,DP:()=>i,p0:()=>m});var o=n(69307);function r(e){return(e||"").split(":",1)[0]}function a(e){const t=r(e);return/^woocommerce(-|_)payments$/.test(t)?"wcpay":`${t.replace(/-/g,"_")}`.split(":",1)[0]}function i(e){return e?e.substr(1).split("&").reduce(((e,t)=>{const n=t.split("="),o=n[0];let r=decodeURIComponent(n[1]);return r=isNaN(Number(r))?r:Number(r),e[o]=r,e}),{}):{}}function s(){let e="";const{page:t,path:n,post_type:o}=i(window.location.search);if(t){const o="wc-admin"===t?"home_screen":t;e=n?n.replace(/\//g,"_").substring(1):o}else o&&(e=o);return e}n(99196);const c=[{name:"0-2s",max:2},{name:"2-5s",max:5},{name:"5-10s",max:10},{name:"10-15s",max:15},{name:"15-20s",max:20},{name:"20-30s",max:30},{name:"30-60s",max:60},{name:">60s"}],l=e=>{for(const t of c){if(!t.max)return t.name;if(e<1e3*t.max)return t.name}},m=e=>{(0,o.useEffect)((()=>{const t=document.documentElement.classList.contains("wp-toolbar");return document.body.classList.remove("woocommerce-admin-is-loading"),document.body.classList.add(e),document.body.classList.add("woocommerce-admin-full-screen"),document.body.classList.add("is-wp-toolbar-disabled"),t&&document.documentElement.classList.remove("wp-toolbar"),()=>{document.body.classList.remove(e),document.body.classList.remove("woocommerce-admin-full-screen"),document.body.classList.remove("is-wp-toolbar-disabled"),t&&document.documentElement.classList.add("wp-toolbar")}}))}},2864:(e,t,n)=>{"use strict";var o=n(28487),r=n(25592),a=r(o("String.prototype.indexOf"));e.exports=function(e,t){var n=o(e,!!t);return"function"==typeof n&&a(e,".prototype.")>-1?r(n):n}},25592:(e,t,n)=>{"use strict";var o=n(22698),r=n(28487),a=r("%Function.prototype.apply%"),i=r("%Function.prototype.call%"),s=r("%Reflect.apply%",!0)||o.call(i,a),c=r("%Object.getOwnPropertyDescriptor%",!0),l=r("%Object.defineProperty%",!0),m=r("%Math.max%");if(l)try{l({},"a",{value:1})}catch(e){l=null}e.exports=function(e){var t=s(o,i,arguments);if(c&&l){var n=c(t,"length");n.configurable&&l(t,"length",{value:1+m(0,e.length-(arguments.length-1))})}return t};var u=function(){return s(o,a,arguments)};l?l(e.exports,"apply",{value:u}):e.exports.apply=u},83849:(e,t)=>{var n;!function(){"use strict";var o={}.hasOwnProperty;function r(){for(var e=[],t=0;t<arguments.length;t++){var n=arguments[t];if(n){var a=typeof n;if("string"===a||"number"===a)e.push(n);else if(Array.isArray(n)){if(n.length){var i=r.apply(null,n);i&&e.push(i)}}else if("object"===a)if(n.toString===Object.prototype.toString)for(var s in n)o.call(n,s)&&n[s]&&e.push(s);else e.push(n.toString())}}return e.join(" ")}e.exports?(r.default=r,e.exports=r):void 0===(n=function(){return r}.apply(t,[]))||(e.exports=n)}()},74944:function(e){e.exports=function(){"use strict";var e=Object.hasOwnProperty,t=Object.setPrototypeOf,n=Object.isFrozen,o=Object.getPrototypeOf,r=Object.getOwnPropertyDescriptor,a=Object.freeze,i=Object.seal,s=Object.create,c="undefined"!=typeof Reflect&&Reflect,l=c.apply,m=c.construct;l||(l=function(e,t,n){return e.apply(t,n)}),a||(a=function(e){return e}),i||(i=function(e){return e}),m||(m=function(e,t){return new(Function.prototype.bind.apply(e,[null].concat(function(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t<e.length;t++)n[t]=e[t];return n}return Array.from(e)}(t))))});var u,d=v(Array.prototype.forEach),p=v(Array.prototype.pop),C=v(Array.prototype.push),h=v(String.prototype.toLowerCase),g=v(String.prototype.match),f=v(String.prototype.replace),y=v(String.prototype.indexOf),w=v(String.prototype.trim),H=v(RegExp.prototype.test),_=(u=TypeError,function(){for(var e=arguments.length,t=Array(e),n=0;n<e;n++)t[n]=arguments[n];return m(u,t)});function v(e){return function(t){for(var n=arguments.length,o=Array(n>1?n-1:0),r=1;r<n;r++)o[r-1]=arguments[r];return l(e,t,o)}}function E(e,o){t&&t(e,null);for(var r=o.length;r--;){var a=o[r];if("string"==typeof a){var i=h(a);i!==a&&(n(o)||(o[r]=i),a=i)}e[a]=!0}return e}function L(t){var n=s(null),o=void 0;for(o in t)l(e,t,[o])&&(n[o]=t[o]);return n}function b(e,t){for(;null!==e;){var n=r(e,t);if(n){if(n.get)return v(n.get);if("function"==typeof n.value)return v(n.value)}e=o(e)}return function(e){return console.warn("fallback value for",e),null}}var k=a(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","section","select","shadow","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),S=a(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","filter","font","g","glyph","glyphref","hkern","image","line","lineargradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),M=a(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),N=a(["animate","color-profile","cursor","discard","fedropshadow","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),x=a(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover"]),T=a(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),I=a(["#text"]),A=a(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","face","for","headers","height","hidden","high","href","hreflang","id","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","pattern","placeholder","playsinline","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","xmlns","slot"]),O=a(["accent-height","accumulate","additive","alignment-baseline","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),P=a(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),V=a(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),R=i(/\{\{[\s\S]*|[\s\S]*\}\}/gm),j=i(/<%[\s\S]*|[\s\S]*%>/gm),D=i(/^data-[\-\w.\u00B7-\uFFFF]/),Z=i(/^aria-[\-\w]+$/),B=i(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),F=i(/^(?:\w+script|data):/i),z=i(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),W=i(/^html$/i),U="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};function G(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t<e.length;t++)n[t]=e[t];return n}return Array.from(e)}var Y=function(){return"undefined"==typeof window?null:window},Q=function(e,t){if("object"!==(void 0===e?"undefined":U(e))||"function"!=typeof e.createPolicy)return null;var n=null,o="data-tt-policy-suffix";t.currentScript&&t.currentScript.hasAttribute(o)&&(n=t.currentScript.getAttribute(o));var r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:function(e){return e}})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}};return function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:Y(),n=function(t){return e(t)};if(n.version="2.3.6",n.removed=[],!t||!t.document||9!==t.document.nodeType)return n.isSupported=!1,n;var o=t.document,r=t.document,i=t.DocumentFragment,s=t.HTMLTemplateElement,c=t.Node,l=t.Element,m=t.NodeFilter,u=t.NamedNodeMap,v=void 0===u?t.NamedNodeMap||t.MozNamedAttrMap:u,q=t.HTMLFormElement,J=t.DOMParser,K=t.trustedTypes,X=l.prototype,$=b(X,"cloneNode"),ee=b(X,"nextSibling"),te=b(X,"childNodes"),ne=b(X,"parentNode");if("function"==typeof s){var oe=r.createElement("template");oe.content&&oe.content.ownerDocument&&(r=oe.content.ownerDocument)}var re=Q(K,o),ae=re?re.createHTML(""):"",ie=r,se=ie.implementation,ce=ie.createNodeIterator,le=ie.createDocumentFragment,me=ie.getElementsByTagName,ue=o.importNode,de={};try{de=L(r).documentMode?r.documentMode:{}}catch(e){}var pe={};n.isSupported="function"==typeof ne&&se&&void 0!==se.createHTMLDocument&&9!==de;var Ce=R,he=j,ge=D,fe=Z,ye=F,we=z,He=B,_e=null,ve=E({},[].concat(G(k),G(S),G(M),G(x),G(I))),Ee=null,Le=E({},[].concat(G(A),G(O),G(P),G(V))),be=Object.seal(Object.create(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ke=null,Se=null,Me=!0,Ne=!0,xe=!1,Te=!1,Ie=!1,Ae=!1,Oe=!1,Pe=!1,Ve=!1,Re=!1,je=!0,De=!0,Ze=!1,Be={},Fe=null,ze=E({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),We=null,Ue=E({},["audio","video","img","source","image","track"]),Ge=null,Ye=E({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Qe="http://www.w3.org/1998/Math/MathML",qe="http://www.w3.org/2000/svg",Je="http://www.w3.org/1999/xhtml",Ke=Je,Xe=!1,$e=void 0,et=["application/xhtml+xml","text/html"],tt="text/html",nt=void 0,ot=null,rt=r.createElement("form"),at=function(e){return e instanceof RegExp||e instanceof Function},it=function(e){ot&&ot===e||(e&&"object"===(void 0===e?"undefined":U(e))||(e={}),e=L(e),_e="ALLOWED_TAGS"in e?E({},e.ALLOWED_TAGS):ve,Ee="ALLOWED_ATTR"in e?E({},e.ALLOWED_ATTR):Le,Ge="ADD_URI_SAFE_ATTR"in e?E(L(Ye),e.ADD_URI_SAFE_ATTR):Ye,We="ADD_DATA_URI_TAGS"in e?E(L(Ue),e.ADD_DATA_URI_TAGS):Ue,Fe="FORBID_CONTENTS"in e?E({},e.FORBID_CONTENTS):ze,ke="FORBID_TAGS"in e?E({},e.FORBID_TAGS):{},Se="FORBID_ATTR"in e?E({},e.FORBID_ATTR):{},Be="USE_PROFILES"in e&&e.USE_PROFILES,Me=!1!==e.ALLOW_ARIA_ATTR,Ne=!1!==e.ALLOW_DATA_ATTR,xe=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Te=e.SAFE_FOR_TEMPLATES||!1,Ie=e.WHOLE_DOCUMENT||!1,Pe=e.RETURN_DOM||!1,Ve=e.RETURN_DOM_FRAGMENT||!1,Re=e.RETURN_TRUSTED_TYPE||!1,Oe=e.FORCE_BODY||!1,je=!1!==e.SANITIZE_DOM,De=!1!==e.KEEP_CONTENT,Ze=e.IN_PLACE||!1,He=e.ALLOWED_URI_REGEXP||He,Ke=e.NAMESPACE||Je,e.CUSTOM_ELEMENT_HANDLING&&at(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(be.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&at(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(be.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(be.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),$e=$e=-1===et.indexOf(e.PARSER_MEDIA_TYPE)?tt:e.PARSER_MEDIA_TYPE,nt="application/xhtml+xml"===$e?function(e){return e}:h,Te&&(Ne=!1),Ve&&(Pe=!0),Be&&(_e=E({},[].concat(G(I))),Ee=[],!0===Be.html&&(E(_e,k),E(Ee,A)),!0===Be.svg&&(E(_e,S),E(Ee,O),E(Ee,V)),!0===Be.svgFilters&&(E(_e,M),E(Ee,O),E(Ee,V)),!0===Be.mathMl&&(E(_e,x),E(Ee,P),E(Ee,V))),e.ADD_TAGS&&(_e===ve&&(_e=L(_e)),E(_e,e.ADD_TAGS)),e.ADD_ATTR&&(Ee===Le&&(Ee=L(Ee)),E(Ee,e.ADD_ATTR)),e.ADD_URI_SAFE_ATTR&&E(Ge,e.ADD_URI_SAFE_ATTR),e.FORBID_CONTENTS&&(Fe===ze&&(Fe=L(Fe)),E(Fe,e.FORBID_CONTENTS)),De&&(_e["#text"]=!0),Ie&&E(_e,["html","head","body"]),_e.table&&(E(_e,["tbody"]),delete ke.tbody),a&&a(e),ot=e)},st=E({},["mi","mo","mn","ms","mtext"]),ct=E({},["foreignobject","desc","title","annotation-xml"]),lt=E({},S);E(lt,M),E(lt,N);var mt=E({},x);E(mt,T);var ut=function(e){var t=ne(e);t&&t.tagName||(t={namespaceURI:Je,tagName:"template"});var n=h(e.tagName),o=h(t.tagName);if(e.namespaceURI===qe)return t.namespaceURI===Je?"svg"===n:t.namespaceURI===Qe?"svg"===n&&("annotation-xml"===o||st[o]):Boolean(lt[n]);if(e.namespaceURI===Qe)return t.namespaceURI===Je?"math"===n:t.namespaceURI===qe?"math"===n&&ct[o]:Boolean(mt[n]);if(e.namespaceURI===Je){if(t.namespaceURI===qe&&!ct[o])return!1;if(t.namespaceURI===Qe&&!st[o])return!1;var r=E({},["title","style","font","a","script"]);return!mt[n]&&(r[n]||!lt[n])}return!1},dt=function(e){C(n.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){try{e.outerHTML=ae}catch(t){e.remove()}}},pt=function(e,t){try{C(n.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){C(n.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Ee[e])if(Pe||Ve)try{dt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},Ct=function(e){var t=void 0,n=void 0;if(Oe)e="<remove></remove>"+e;else{var o=g(e,/^[\r\n\t ]+/);n=o&&o[0]}"application/xhtml+xml"===$e&&(e='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+e+"</body></html>");var a=re?re.createHTML(e):e;if(Ke===Je)try{t=(new J).parseFromString(a,$e)}catch(e){}if(!t||!t.documentElement){t=se.createDocument(Ke,"template",null);try{t.documentElement.innerHTML=Xe?"":a}catch(e){}}var i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),Ke===Je?me.call(t,Ie?"html":"body")[0]:Ie?t.documentElement:i},ht=function(e){return ce.call(e.ownerDocument||e,e,m.SHOW_ELEMENT|m.SHOW_COMMENT|m.SHOW_TEXT,null,!1)},gt=function(e){return e instanceof q&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof v)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore)},ft=function(e){return"object"===(void 0===c?"undefined":U(c))?e instanceof c:e&&"object"===(void 0===e?"undefined":U(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},yt=function(e,t,o){pe[e]&&d(pe[e],(function(e){e.call(n,t,o,ot)}))},wt=function(e){var t=void 0;if(yt("beforeSanitizeElements",e,null),gt(e))return dt(e),!0;if(g(e.nodeName,/[\u0080-\uFFFF]/))return dt(e),!0;var o=nt(e.nodeName);if(yt("uponSanitizeElement",e,{tagName:o,allowedTags:_e}),!ft(e.firstElementChild)&&(!ft(e.content)||!ft(e.content.firstElementChild))&&H(/<[/\w]/g,e.innerHTML)&&H(/<[/\w]/g,e.textContent))return dt(e),!0;if("select"===o&&H(/<template/i,e.innerHTML))return dt(e),!0;if(!_e[o]||ke[o]){if(!ke[o]&&_t(o)){if(be.tagNameCheck instanceof RegExp&&H(be.tagNameCheck,o))return!1;if(be.tagNameCheck instanceof Function&&be.tagNameCheck(o))return!1}if(De&&!Fe[o]){var r=ne(e)||e.parentNode,a=te(e)||e.childNodes;if(a&&r)for(var i=a.length-1;i>=0;--i)r.insertBefore($(a[i],!0),ee(e))}return dt(e),!0}return e instanceof l&&!ut(e)?(dt(e),!0):"noscript"!==o&&"noembed"!==o||!H(/<\/no(script|embed)/i,e.innerHTML)?(Te&&3===e.nodeType&&(t=e.textContent,t=f(t,Ce," "),t=f(t,he," "),e.textContent!==t&&(C(n.removed,{element:e.cloneNode()}),e.textContent=t)),yt("afterSanitizeElements",e,null),!1):(dt(e),!0)},Ht=function(e,t,n){if(je&&("id"===t||"name"===t)&&(n in r||n in rt))return!1;if(Ne&&!Se[t]&&H(ge,t));else if(Me&&H(fe,t));else if(!Ee[t]||Se[t]){if(!(_t(e)&&(be.tagNameCheck instanceof RegExp&&H(be.tagNameCheck,e)||be.tagNameCheck instanceof Function&&be.tagNameCheck(e))&&(be.attributeNameCheck instanceof RegExp&&H(be.attributeNameCheck,t)||be.attributeNameCheck instanceof Function&&be.attributeNameCheck(t))||"is"===t&&be.allowCustomizedBuiltInElements&&(be.tagNameCheck instanceof RegExp&&H(be.tagNameCheck,n)||be.tagNameCheck instanceof Function&&be.tagNameCheck(n))))return!1}else if(Ge[t]);else if(H(He,f(n,we,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==y(n,"data:")||!We[e])if(xe&&!H(ye,f(n,we,"")));else if(n)return!1;return!0},_t=function(e){return e.indexOf("-")>0},vt=function(e){var t=void 0,o=void 0,r=void 0,a=void 0;yt("beforeSanitizeAttributes",e,null);var i=e.attributes;if(i){var s={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Ee};for(a=i.length;a--;){var c=t=i[a],l=c.name,m=c.namespaceURI;if(o=w(t.value),r=nt(l),s.attrName=r,s.attrValue=o,s.keepAttr=!0,s.forceKeepAttr=void 0,yt("uponSanitizeAttribute",e,s),o=s.attrValue,!s.forceKeepAttr&&(pt(l,e),s.keepAttr))if(H(/\/>/i,o))pt(l,e);else{Te&&(o=f(o,Ce," "),o=f(o,he," "));var u=nt(e.nodeName);if(Ht(u,r,o))try{m?e.setAttributeNS(m,l,o):e.setAttribute(l,o),p(n.removed)}catch(e){}}}yt("afterSanitizeAttributes",e,null)}},Et=function e(t){var n=void 0,o=ht(t);for(yt("beforeSanitizeShadowDOM",t,null);n=o.nextNode();)yt("uponSanitizeShadowNode",n,null),wt(n)||(n.content instanceof i&&e(n.content),vt(n));yt("afterSanitizeShadowDOM",t,null)};return n.sanitize=function(e,r){var a=void 0,s=void 0,l=void 0,m=void 0,u=void 0;if((Xe=!e)&&(e="\x3c!--\x3e"),"string"!=typeof e&&!ft(e)){if("function"!=typeof e.toString)throw _("toString is not a function");if("string"!=typeof(e=e.toString()))throw _("dirty is not a string, aborting")}if(!n.isSupported){if("object"===U(t.toStaticHTML)||"function"==typeof t.toStaticHTML){if("string"==typeof e)return t.toStaticHTML(e);if(ft(e))return t.toStaticHTML(e.outerHTML)}return e}if(Ae||it(r),n.removed=[],"string"==typeof e&&(Ze=!1),Ze){if(e.nodeName){var d=nt(e.nodeName);if(!_e[d]||ke[d])throw _("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof c)1===(s=(a=Ct("\x3c!----\x3e")).ownerDocument.importNode(e,!0)).nodeType&&"BODY"===s.nodeName||"HTML"===s.nodeName?a=s:a.appendChild(s);else{if(!Pe&&!Te&&!Ie&&-1===e.indexOf("<"))return re&&Re?re.createHTML(e):e;if(!(a=Ct(e)))return Pe?null:Re?ae:""}a&&Oe&&dt(a.firstChild);for(var p=ht(Ze?e:a);l=p.nextNode();)3===l.nodeType&&l===m||wt(l)||(l.content instanceof i&&Et(l.content),vt(l),m=l);if(m=null,Ze)return e;if(Pe){if(Ve)for(u=le.call(a.ownerDocument);a.firstChild;)u.appendChild(a.firstChild);else u=a;return Ee.shadowroot&&(u=ue.call(o,u,!0)),u}var C=Ie?a.outerHTML:a.innerHTML;return Ie&&_e["!doctype"]&&a.ownerDocument&&a.ownerDocument.doctype&&a.ownerDocument.doctype.name&&H(W,a.ownerDocument.doctype.name)&&(C="<!DOCTYPE "+a.ownerDocument.doctype.name+">\n"+C),Te&&(C=f(C,Ce," "),C=f(C,he," ")),re&&Re?re.createHTML(C):C},n.setConfig=function(e){it(e),Ae=!0},n.clearConfig=function(){ot=null,Ae=!1},n.isValidAttribute=function(e,t,n){ot||it({});var o=nt(e),r=nt(t);return Ht(o,r,n)},n.addHook=function(e,t){"function"==typeof t&&(pe[e]=pe[e]||[],C(pe[e],t))},n.removeHook=function(e){pe[e]&&p(pe[e])},n.removeHooks=function(e){pe[e]&&(pe[e]=[])},n.removeAllHooks=function(){pe={}},n}()}()},59748:e=>{"use strict";var t="Function.prototype.bind called on incompatible ",n=Array.prototype.slice,o=Object.prototype.toString,r="[object Function]";e.exports=function(e){var a=this;if("function"!=typeof a||o.call(a)!==r)throw new TypeError(t+a);for(var i,s=n.call(arguments,1),c=function(){if(this instanceof i){var t=a.apply(this,s.concat(n.call(arguments)));return Object(t)===t?t:this}return a.apply(e,s.concat(n.call(arguments)))},l=Math.max(0,a.length-s.length),m=[],u=0;u<l;u++)m.push("$"+u);if(i=Function("binder","return function ("+m.join(",")+"){ return binder.apply(this,arguments); }")(c),a.prototype){var d=function(){};d.prototype=a.prototype,i.prototype=new d,d.prototype=null}return i}},22698:(e,t,n)=>{"use strict";var o=n(59748);e.exports=Function.prototype.bind||o},28487:(e,t,n)=>{"use strict";var o,r=SyntaxError,a=Function,i=TypeError,s=function(e){try{return a('"use strict"; return ('+e+").constructor;")()}catch(e){}},c=Object.getOwnPropertyDescriptor;if(c)try{c({},"")}catch(e){c=null}var l=function(){throw new i},m=c?function(){try{return l}catch(e){try{return c(arguments,"callee").get}catch(e){return l}}}():l,u=n(72770)(),d=Object.getPrototypeOf||function(e){return e.__proto__},p={},C="undefined"==typeof Uint8Array?o:d(Uint8Array),h={"%AggregateError%":"undefined"==typeof AggregateError?o:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"==typeof ArrayBuffer?o:ArrayBuffer,"%ArrayIteratorPrototype%":u?d([][Symbol.iterator]()):o,"%AsyncFromSyncIteratorPrototype%":o,"%AsyncFunction%":p,"%AsyncGenerator%":p,"%AsyncGeneratorFunction%":p,"%AsyncIteratorPrototype%":p,"%Atomics%":"undefined"==typeof Atomics?o:Atomics,"%BigInt%":"undefined"==typeof BigInt?o:BigInt,"%Boolean%":Boolean,"%DataView%":"undefined"==typeof DataView?o:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":Error,"%eval%":eval,"%EvalError%":EvalError,"%Float32Array%":"undefined"==typeof Float32Array?o:Float32Array,"%Float64Array%":"undefined"==typeof Float64Array?o:Float64Array,"%FinalizationRegistry%":"undefined"==typeof FinalizationRegistry?o:FinalizationRegistry,"%Function%":a,"%GeneratorFunction%":p,"%Int8Array%":"undefined"==typeof Int8Array?o:Int8Array,"%Int16Array%":"undefined"==typeof Int16Array?o:Int16Array,"%Int32Array%":"undefined"==typeof Int32Array?o:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":u?d(d([][Symbol.iterator]())):o,"%JSON%":"object"==typeof JSON?JSON:o,"%Map%":"undefined"==typeof Map?o:Map,"%MapIteratorPrototype%":"undefined"!=typeof Map&&u?d((new Map)[Symbol.iterator]()):o,"%Math%":Math,"%Number%":Number,"%Object%":Object,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"==typeof Promise?o:Promise,"%Proxy%":"undefined"==typeof Proxy?o:Proxy,"%RangeError%":RangeError,"%ReferenceError%":ReferenceError,"%Reflect%":"undefined"==typeof Reflect?o:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"==typeof Set?o:Set,"%SetIteratorPrototype%":"undefined"!=typeof Set&&u?d((new Set)[Symbol.iterator]()):o,"%SharedArrayBuffer%":"undefined"==typeof SharedArrayBuffer?o:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":u?d(""[Symbol.iterator]()):o,"%Symbol%":u?Symbol:o,"%SyntaxError%":r,"%ThrowTypeError%":m,"%TypedArray%":C,"%TypeError%":i,"%Uint8Array%":"undefined"==typeof Uint8Array?o:Uint8Array,"%Uint8ClampedArray%":"undefined"==typeof Uint8ClampedArray?o:Uint8ClampedArray,"%Uint16Array%":"undefined"==typeof Uint16Array?o:Uint16Array,"%Uint32Array%":"undefined"==typeof Uint32Array?o:Uint32Array,"%URIError%":URIError,"%WeakMap%":"undefined"==typeof WeakMap?o:WeakMap,"%WeakRef%":"undefined"==typeof WeakRef?o:WeakRef,"%WeakSet%":"undefined"==typeof WeakSet?o:WeakSet},g=function e(t){var n;if("%AsyncFunction%"===t)n=s("async function () {}");else if("%GeneratorFunction%"===t)n=s("function* () {}");else if("%AsyncGeneratorFunction%"===t)n=s("async function* () {}");else if("%AsyncGenerator%"===t){var o=e("%AsyncGeneratorFunction%");o&&(n=o.prototype)}else if("%AsyncIteratorPrototype%"===t){var r=e("%AsyncGenerator%");r&&(n=d(r.prototype))}return h[t]=n,n},f={"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},y=n(22698),w=n(22786),H=y.call(Function.call,Array.prototype.concat),_=y.call(Function.apply,Array.prototype.splice),v=y.call(Function.call,String.prototype.replace),E=y.call(Function.call,String.prototype.slice),L=y.call(Function.call,RegExp.prototype.exec),b=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,k=/\\(\\)?/g,S=function(e){var t=E(e,0,1),n=E(e,-1);if("%"===t&&"%"!==n)throw new r("invalid intrinsic syntax, expected closing `%`");if("%"===n&&"%"!==t)throw new r("invalid intrinsic syntax, expected opening `%`");var o=[];return v(e,b,(function(e,t,n,r){o[o.length]=n?v(r,k,"$1"):t||e})),o},M=function(e,t){var n,o=e;if(w(f,o)&&(o="%"+(n=f[o])[0]+"%"),w(h,o)){var a=h[o];if(a===p&&(a=g(o)),void 0===a&&!t)throw new i("intrinsic "+e+" exists, but is not available. Please file an issue!");return{alias:n,name:o,value:a}}throw new r("intrinsic "+e+" does not exist!")};e.exports=function(e,t){if("string"!=typeof e||0===e.length)throw new i("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof t)throw new i('"allowMissing" argument must be a boolean');if(null===L(/^%?[^%]*%?$/,e))throw new r("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var n=S(e),o=n.length>0?n[0]:"",a=M("%"+o+"%",t),s=a.name,l=a.value,m=!1,u=a.alias;u&&(o=u[0],_(n,H([0,1],u)));for(var d=1,p=!0;d<n.length;d+=1){var C=n[d],g=E(C,0,1),f=E(C,-1);if(('"'===g||"'"===g||"`"===g||'"'===f||"'"===f||"`"===f)&&g!==f)throw new r("property names with quotes must have matching quotes");if("constructor"!==C&&p||(m=!0),w(h,s="%"+(o+="."+C)+"%"))l=h[s];else if(null!=l){if(!(C in l)){if(!t)throw new i("base intrinsic for "+e+" exists, but the property is not available.");return}if(c&&d+1>=n.length){var y=c(l,C);l=(p=!!y)&&"get"in y&&!("originalValue"in y.get)?y.get:l[C]}else p=w(l,C),l=l[C];p&&!m&&(h[s]=l)}}return l}},49248:e=>{e.exports&&(e.exports=function(){var e=3,t=4,n=12,o=13,r=16,a=17;function i(e,t){void 0===t&&(t=0);var n=e.charCodeAt(t);if(55296<=n&&n<=56319&&t<e.length-1){var o=n;return 56320<=(r=e.charCodeAt(t+1))&&r<=57343?1024*(o-55296)+(r-56320)+65536:o}if(56320<=n&&n<=57343&&t>=1){var r=n;return 55296<=(o=e.charCodeAt(t-1))&&o<=56319?1024*(o-55296)+(r-56320)+65536:r}return n}function s(i,s,c){var l=[i].concat(s).concat([c]),m=l[l.length-2],u=c,d=l.lastIndexOf(14);if(d>1&&l.slice(1,d).every((function(t){return t==e}))&&-1==[e,o,a].indexOf(i))return 2;var p=l.lastIndexOf(t);if(p>0&&l.slice(1,p).every((function(e){return e==t}))&&-1==[n,t].indexOf(m))return l.filter((function(e){return e==t})).length%2==1?3:4;if(0==m&&1==u)return 0;if(2==m||0==m||1==m)return 14==u&&s.every((function(t){return t==e}))?2:1;if(2==u||0==u||1==u)return 1;if(6==m&&(6==u||7==u||9==u||10==u))return 0;if(!(9!=m&&7!=m||7!=u&&8!=u))return 0;if((10==m||8==m)&&8==u)return 0;if(u==e||15==u)return 0;if(5==u)return 0;if(m==n)return 0;var C=-1!=l.indexOf(e)?l.lastIndexOf(e)-1:l.length-2;return-1!=[o,a].indexOf(l[C])&&l.slice(C+1,-1).every((function(t){return t==e}))&&14==u||15==m&&-1!=[r,a].indexOf(u)?0:-1!=s.indexOf(t)?2:m==t&&u==t?0:1}function c(i){return 1536<=i&&i<=1541||1757==i||1807==i||2274==i||3406==i||69821==i||70082<=i&&i<=70083||72250==i||72326<=i&&i<=72329||73030==i?n:13==i?0:10==i?1:0<=i&&i<=9||11<=i&&i<=12||14<=i&&i<=31||127<=i&&i<=159||173==i||1564==i||6158==i||8203==i||8206<=i&&i<=8207||8232==i||8233==i||8234<=i&&i<=8238||8288<=i&&i<=8292||8293==i||8294<=i&&i<=8303||55296<=i&&i<=57343||65279==i||65520<=i&&i<=65528||65529<=i&&i<=65531||113824<=i&&i<=113827||119155<=i&&i<=119162||917504==i||917505==i||917506<=i&&i<=917535||917632<=i&&i<=917759||918e3<=i&&i<=921599?2:768<=i&&i<=879||1155<=i&&i<=1159||1160<=i&&i<=1161||1425<=i&&i<=1469||1471==i||1473<=i&&i<=1474||1476<=i&&i<=1477||1479==i||1552<=i&&i<=1562||1611<=i&&i<=1631||1648==i||1750<=i&&i<=1756||1759<=i&&i<=1764||1767<=i&&i<=1768||1770<=i&&i<=1773||1809==i||1840<=i&&i<=1866||1958<=i&&i<=1968||2027<=i&&i<=2035||2070<=i&&i<=2073||2075<=i&&i<=2083||2085<=i&&i<=2087||2089<=i&&i<=2093||2137<=i&&i<=2139||2260<=i&&i<=2273||2275<=i&&i<=2306||2362==i||2364==i||2369<=i&&i<=2376||2381==i||2385<=i&&i<=2391||2402<=i&&i<=2403||2433==i||2492==i||2494==i||2497<=i&&i<=2500||2509==i||2519==i||2530<=i&&i<=2531||2561<=i&&i<=2562||2620==i||2625<=i&&i<=2626||2631<=i&&i<=2632||2635<=i&&i<=2637||2641==i||2672<=i&&i<=2673||2677==i||2689<=i&&i<=2690||2748==i||2753<=i&&i<=2757||2759<=i&&i<=2760||2765==i||2786<=i&&i<=2787||2810<=i&&i<=2815||2817==i||2876==i||2878==i||2879==i||2881<=i&&i<=2884||2893==i||2902==i||2903==i||2914<=i&&i<=2915||2946==i||3006==i||3008==i||3021==i||3031==i||3072==i||3134<=i&&i<=3136||3142<=i&&i<=3144||3146<=i&&i<=3149||3157<=i&&i<=3158||3170<=i&&i<=3171||3201==i||3260==i||3263==i||3266==i||3270==i||3276<=i&&i<=3277||3285<=i&&i<=3286||3298<=i&&i<=3299||3328<=i&&i<=3329||3387<=i&&i<=3388||3390==i||3393<=i&&i<=3396||3405==i||3415==i||3426<=i&&i<=3427||3530==i||3535==i||3538<=i&&i<=3540||3542==i||3551==i||3633==i||3636<=i&&i<=3642||3655<=i&&i<=3662||3761==i||3764<=i&&i<=3769||3771<=i&&i<=3772||3784<=i&&i<=3789||3864<=i&&i<=3865||3893==i||3895==i||3897==i||3953<=i&&i<=3966||3968<=i&&i<=3972||3974<=i&&i<=3975||3981<=i&&i<=3991||3993<=i&&i<=4028||4038==i||4141<=i&&i<=4144||4146<=i&&i<=4151||4153<=i&&i<=4154||4157<=i&&i<=4158||4184<=i&&i<=4185||4190<=i&&i<=4192||4209<=i&&i<=4212||4226==i||4229<=i&&i<=4230||4237==i||4253==i||4957<=i&&i<=4959||5906<=i&&i<=5908||5938<=i&&i<=5940||5970<=i&&i<=5971||6002<=i&&i<=6003||6068<=i&&i<=6069||6071<=i&&i<=6077||6086==i||6089<=i&&i<=6099||6109==i||6155<=i&&i<=6157||6277<=i&&i<=6278||6313==i||6432<=i&&i<=6434||6439<=i&&i<=6440||6450==i||6457<=i&&i<=6459||6679<=i&&i<=6680||6683==i||6742==i||6744<=i&&i<=6750||6752==i||6754==i||6757<=i&&i<=6764||6771<=i&&i<=6780||6783==i||6832<=i&&i<=6845||6846==i||6912<=i&&i<=6915||6964==i||6966<=i&&i<=6970||6972==i||6978==i||7019<=i&&i<=7027||7040<=i&&i<=7041||7074<=i&&i<=7077||7080<=i&&i<=7081||7083<=i&&i<=7085||7142==i||7144<=i&&i<=7145||7149==i||7151<=i&&i<=7153||7212<=i&&i<=7219||7222<=i&&i<=7223||7376<=i&&i<=7378||7380<=i&&i<=7392||7394<=i&&i<=7400||7405==i||7412==i||7416<=i&&i<=7417||7616<=i&&i<=7673||7675<=i&&i<=7679||8204==i||8400<=i&&i<=8412||8413<=i&&i<=8416||8417==i||8418<=i&&i<=8420||8421<=i&&i<=8432||11503<=i&&i<=11505||11647==i||11744<=i&&i<=11775||12330<=i&&i<=12333||12334<=i&&i<=12335||12441<=i&&i<=12442||42607==i||42608<=i&&i<=42610||42612<=i&&i<=42621||42654<=i&&i<=42655||42736<=i&&i<=42737||43010==i||43014==i||43019==i||43045<=i&&i<=43046||43204<=i&&i<=43205||43232<=i&&i<=43249||43302<=i&&i<=43309||43335<=i&&i<=43345||43392<=i&&i<=43394||43443==i||43446<=i&&i<=43449||43452==i||43493==i||43561<=i&&i<=43566||43569<=i&&i<=43570||43573<=i&&i<=43574||43587==i||43596==i||43644==i||43696==i||43698<=i&&i<=43700||43703<=i&&i<=43704||43710<=i&&i<=43711||43713==i||43756<=i&&i<=43757||43766==i||44005==i||44008==i||44013==i||64286==i||65024<=i&&i<=65039||65056<=i&&i<=65071||65438<=i&&i<=65439||66045==i||66272==i||66422<=i&&i<=66426||68097<=i&&i<=68099||68101<=i&&i<=68102||68108<=i&&i<=68111||68152<=i&&i<=68154||68159==i||68325<=i&&i<=68326||69633==i||69688<=i&&i<=69702||69759<=i&&i<=69761||69811<=i&&i<=69814||69817<=i&&i<=69818||69888<=i&&i<=69890||69927<=i&&i<=69931||69933<=i&&i<=69940||70003==i||70016<=i&&i<=70017||70070<=i&&i<=70078||70090<=i&&i<=70092||70191<=i&&i<=70193||70196==i||70198<=i&&i<=70199||70206==i||70367==i||70371<=i&&i<=70378||70400<=i&&i<=70401||70460==i||70462==i||70464==i||70487==i||70502<=i&&i<=70508||70512<=i&&i<=70516||70712<=i&&i<=70719||70722<=i&&i<=70724||70726==i||70832==i||70835<=i&&i<=70840||70842==i||70845==i||70847<=i&&i<=70848||70850<=i&&i<=70851||71087==i||71090<=i&&i<=71093||71100<=i&&i<=71101||71103<=i&&i<=71104||71132<=i&&i<=71133||71219<=i&&i<=71226||71229==i||71231<=i&&i<=71232||71339==i||71341==i||71344<=i&&i<=71349||71351==i||71453<=i&&i<=71455||71458<=i&&i<=71461||71463<=i&&i<=71467||72193<=i&&i<=72198||72201<=i&&i<=72202||72243<=i&&i<=72248||72251<=i&&i<=72254||72263==i||72273<=i&&i<=72278||72281<=i&&i<=72283||72330<=i&&i<=72342||72344<=i&&i<=72345||72752<=i&&i<=72758||72760<=i&&i<=72765||72767==i||72850<=i&&i<=72871||72874<=i&&i<=72880||72882<=i&&i<=72883||72885<=i&&i<=72886||73009<=i&&i<=73014||73018==i||73020<=i&&i<=73021||73023<=i&&i<=73029||73031==i||92912<=i&&i<=92916||92976<=i&&i<=92982||94095<=i&&i<=94098||113821<=i&&i<=113822||119141==i||119143<=i&&i<=119145||119150<=i&&i<=119154||119163<=i&&i<=119170||119173<=i&&i<=119179||119210<=i&&i<=119213||119362<=i&&i<=119364||121344<=i&&i<=121398||121403<=i&&i<=121452||121461==i||121476==i||121499<=i&&i<=121503||121505<=i&&i<=121519||122880<=i&&i<=122886||122888<=i&&i<=122904||122907<=i&&i<=122913||122915<=i&&i<=122916||122918<=i&&i<=122922||125136<=i&&i<=125142||125252<=i&&i<=125258||917536<=i&&i<=917631||917760<=i&&i<=917999?e:127462<=i&&i<=127487?t:2307==i||2363==i||2366<=i&&i<=2368||2377<=i&&i<=2380||2382<=i&&i<=2383||2434<=i&&i<=2435||2495<=i&&i<=2496||2503<=i&&i<=2504||2507<=i&&i<=2508||2563==i||2622<=i&&i<=2624||2691==i||2750<=i&&i<=2752||2761==i||2763<=i&&i<=2764||2818<=i&&i<=2819||2880==i||2887<=i&&i<=2888||2891<=i&&i<=2892||3007==i||3009<=i&&i<=3010||3014<=i&&i<=3016||3018<=i&&i<=3020||3073<=i&&i<=3075||3137<=i&&i<=3140||3202<=i&&i<=3203||3262==i||3264<=i&&i<=3265||3267<=i&&i<=3268||3271<=i&&i<=3272||3274<=i&&i<=3275||3330<=i&&i<=3331||3391<=i&&i<=3392||3398<=i&&i<=3400||3402<=i&&i<=3404||3458<=i&&i<=3459||3536<=i&&i<=3537||3544<=i&&i<=3550||3570<=i&&i<=3571||3635==i||3763==i||3902<=i&&i<=3903||3967==i||4145==i||4155<=i&&i<=4156||4182<=i&&i<=4183||4228==i||6070==i||6078<=i&&i<=6085||6087<=i&&i<=6088||6435<=i&&i<=6438||6441<=i&&i<=6443||6448<=i&&i<=6449||6451<=i&&i<=6456||6681<=i&&i<=6682||6741==i||6743==i||6765<=i&&i<=6770||6916==i||6965==i||6971==i||6973<=i&&i<=6977||6979<=i&&i<=6980||7042==i||7073==i||7078<=i&&i<=7079||7082==i||7143==i||7146<=i&&i<=7148||7150==i||7154<=i&&i<=7155||7204<=i&&i<=7211||7220<=i&&i<=7221||7393==i||7410<=i&&i<=7411||7415==i||43043<=i&&i<=43044||43047==i||43136<=i&&i<=43137||43188<=i&&i<=43203||43346<=i&&i<=43347||43395==i||43444<=i&&i<=43445||43450<=i&&i<=43451||43453<=i&&i<=43456||43567<=i&&i<=43568||43571<=i&&i<=43572||43597==i||43755==i||43758<=i&&i<=43759||43765==i||44003<=i&&i<=44004||44006<=i&&i<=44007||44009<=i&&i<=44010||44012==i||69632==i||69634==i||69762==i||69808<=i&&i<=69810||69815<=i&&i<=69816||69932==i||70018==i||70067<=i&&i<=70069||70079<=i&&i<=70080||70188<=i&&i<=70190||70194<=i&&i<=70195||70197==i||70368<=i&&i<=70370||70402<=i&&i<=70403||70463==i||70465<=i&&i<=70468||70471<=i&&i<=70472||70475<=i&&i<=70477||70498<=i&&i<=70499||70709<=i&&i<=70711||70720<=i&&i<=70721||70725==i||70833<=i&&i<=70834||70841==i||70843<=i&&i<=70844||70846==i||70849==i||71088<=i&&i<=71089||71096<=i&&i<=71099||71102==i||71216<=i&&i<=71218||71227<=i&&i<=71228||71230==i||71340==i||71342<=i&&i<=71343||71350==i||71456<=i&&i<=71457||71462==i||72199<=i&&i<=72200||72249==i||72279<=i&&i<=72280||72343==i||72751==i||72766==i||72873==i||72881==i||72884==i||94033<=i&&i<=94078||119142==i||119149==i?5:4352<=i&&i<=4447||43360<=i&&i<=43388?6:4448<=i&&i<=4519||55216<=i&&i<=55238?7:4520<=i&&i<=4607||55243<=i&&i<=55291?8:44032==i||44060==i||44088==i||44116==i||44144==i||44172==i||44200==i||44228==i||44256==i||44284==i||44312==i||44340==i||44368==i||44396==i||44424==i||44452==i||44480==i||44508==i||44536==i||44564==i||44592==i||44620==i||44648==i||44676==i||44704==i||44732==i||44760==i||44788==i||44816==i||44844==i||44872==i||44900==i||44928==i||44956==i||44984==i||45012==i||45040==i||45068==i||45096==i||45124==i||45152==i||45180==i||45208==i||45236==i||45264==i||45292==i||45320==i||45348==i||45376==i||45404==i||45432==i||45460==i||45488==i||45516==i||45544==i||45572==i||45600==i||45628==i||45656==i||45684==i||45712==i||45740==i||45768==i||45796==i||45824==i||45852==i||45880==i||45908==i||45936==i||45964==i||45992==i||46020==i||46048==i||46076==i||46104==i||46132==i||46160==i||46188==i||46216==i||46244==i||46272==i||46300==i||46328==i||46356==i||46384==i||46412==i||46440==i||46468==i||46496==i||46524==i||46552==i||46580==i||46608==i||46636==i||46664==i||46692==i||46720==i||46748==i||46776==i||46804==i||46832==i||46860==i||46888==i||46916==i||46944==i||46972==i||47e3==i||47028==i||47056==i||47084==i||47112==i||47140==i||47168==i||47196==i||47224==i||47252==i||47280==i||47308==i||47336==i||47364==i||47392==i||47420==i||47448==i||47476==i||47504==i||47532==i||47560==i||47588==i||47616==i||47644==i||47672==i||47700==i||47728==i||47756==i||47784==i||47812==i||47840==i||47868==i||47896==i||47924==i||47952==i||47980==i||48008==i||48036==i||48064==i||48092==i||48120==i||48148==i||48176==i||48204==i||48232==i||48260==i||48288==i||48316==i||48344==i||48372==i||48400==i||48428==i||48456==i||48484==i||48512==i||48540==i||48568==i||48596==i||48624==i||48652==i||48680==i||48708==i||48736==i||48764==i||48792==i||48820==i||48848==i||48876==i||48904==i||48932==i||48960==i||48988==i||49016==i||49044==i||49072==i||49100==i||49128==i||49156==i||49184==i||49212==i||49240==i||49268==i||49296==i||49324==i||49352==i||49380==i||49408==i||49436==i||49464==i||49492==i||49520==i||49548==i||49576==i||49604==i||49632==i||49660==i||49688==i||49716==i||49744==i||49772==i||49800==i||49828==i||49856==i||49884==i||49912==i||49940==i||49968==i||49996==i||50024==i||50052==i||50080==i||50108==i||50136==i||50164==i||50192==i||50220==i||50248==i||50276==i||50304==i||50332==i||50360==i||50388==i||50416==i||50444==i||50472==i||50500==i||50528==i||50556==i||50584==i||50612==i||50640==i||50668==i||50696==i||50724==i||50752==i||50780==i||50808==i||50836==i||50864==i||50892==i||50920==i||50948==i||50976==i||51004==i||51032==i||51060==i||51088==i||51116==i||51144==i||51172==i||51200==i||51228==i||51256==i||51284==i||51312==i||51340==i||51368==i||51396==i||51424==i||51452==i||51480==i||51508==i||51536==i||51564==i||51592==i||51620==i||51648==i||51676==i||51704==i||51732==i||51760==i||51788==i||51816==i||51844==i||51872==i||51900==i||51928==i||51956==i||51984==i||52012==i||52040==i||52068==i||52096==i||52124==i||52152==i||52180==i||52208==i||52236==i||52264==i||52292==i||52320==i||52348==i||52376==i||52404==i||52432==i||52460==i||52488==i||52516==i||52544==i||52572==i||52600==i||52628==i||52656==i||52684==i||52712==i||52740==i||52768==i||52796==i||52824==i||52852==i||52880==i||52908==i||52936==i||52964==i||52992==i||53020==i||53048==i||53076==i||53104==i||53132==i||53160==i||53188==i||53216==i||53244==i||53272==i||53300==i||53328==i||53356==i||53384==i||53412==i||53440==i||53468==i||53496==i||53524==i||53552==i||53580==i||53608==i||53636==i||53664==i||53692==i||53720==i||53748==i||53776==i||53804==i||53832==i||53860==i||53888==i||53916==i||53944==i||53972==i||54e3==i||54028==i||54056==i||54084==i||54112==i||54140==i||54168==i||54196==i||54224==i||54252==i||54280==i||54308==i||54336==i||54364==i||54392==i||54420==i||54448==i||54476==i||54504==i||54532==i||54560==i||54588==i||54616==i||54644==i||54672==i||54700==i||54728==i||54756==i||54784==i||54812==i||54840==i||54868==i||54896==i||54924==i||54952==i||54980==i||55008==i||55036==i||55064==i||55092==i||55120==i||55148==i||55176==i?9:44033<=i&&i<=44059||44061<=i&&i<=44087||44089<=i&&i<=44115||44117<=i&&i<=44143||44145<=i&&i<=44171||44173<=i&&i<=44199||44201<=i&&i<=44227||44229<=i&&i<=44255||44257<=i&&i<=44283||44285<=i&&i<=44311||44313<=i&&i<=44339||44341<=i&&i<=44367||44369<=i&&i<=44395||44397<=i&&i<=44423||44425<=i&&i<=44451||44453<=i&&i<=44479||44481<=i&&i<=44507||44509<=i&&i<=44535||44537<=i&&i<=44563||44565<=i&&i<=44591||44593<=i&&i<=44619||44621<=i&&i<=44647||44649<=i&&i<=44675||44677<=i&&i<=44703||44705<=i&&i<=44731||44733<=i&&i<=44759||44761<=i&&i<=44787||44789<=i&&i<=44815||44817<=i&&i<=44843||44845<=i&&i<=44871||44873<=i&&i<=44899||44901<=i&&i<=44927||44929<=i&&i<=44955||44957<=i&&i<=44983||44985<=i&&i<=45011||45013<=i&&i<=45039||45041<=i&&i<=45067||45069<=i&&i<=45095||45097<=i&&i<=45123||45125<=i&&i<=45151||45153<=i&&i<=45179||45181<=i&&i<=45207||45209<=i&&i<=45235||45237<=i&&i<=45263||45265<=i&&i<=45291||45293<=i&&i<=45319||45321<=i&&i<=45347||45349<=i&&i<=45375||45377<=i&&i<=45403||45405<=i&&i<=45431||45433<=i&&i<=45459||45461<=i&&i<=45487||45489<=i&&i<=45515||45517<=i&&i<=45543||45545<=i&&i<=45571||45573<=i&&i<=45599||45601<=i&&i<=45627||45629<=i&&i<=45655||45657<=i&&i<=45683||45685<=i&&i<=45711||45713<=i&&i<=45739||45741<=i&&i<=45767||45769<=i&&i<=45795||45797<=i&&i<=45823||45825<=i&&i<=45851||45853<=i&&i<=45879||45881<=i&&i<=45907||45909<=i&&i<=45935||45937<=i&&i<=45963||45965<=i&&i<=45991||45993<=i&&i<=46019||46021<=i&&i<=46047||46049<=i&&i<=46075||46077<=i&&i<=46103||46105<=i&&i<=46131||46133<=i&&i<=46159||46161<=i&&i<=46187||46189<=i&&i<=46215||46217<=i&&i<=46243||46245<=i&&i<=46271||46273<=i&&i<=46299||46301<=i&&i<=46327||46329<=i&&i<=46355||46357<=i&&i<=46383||46385<=i&&i<=46411||46413<=i&&i<=46439||46441<=i&&i<=46467||46469<=i&&i<=46495||46497<=i&&i<=46523||46525<=i&&i<=46551||46553<=i&&i<=46579||46581<=i&&i<=46607||46609<=i&&i<=46635||46637<=i&&i<=46663||46665<=i&&i<=46691||46693<=i&&i<=46719||46721<=i&&i<=46747||46749<=i&&i<=46775||46777<=i&&i<=46803||46805<=i&&i<=46831||46833<=i&&i<=46859||46861<=i&&i<=46887||46889<=i&&i<=46915||46917<=i&&i<=46943||46945<=i&&i<=46971||46973<=i&&i<=46999||47001<=i&&i<=47027||47029<=i&&i<=47055||47057<=i&&i<=47083||47085<=i&&i<=47111||47113<=i&&i<=47139||47141<=i&&i<=47167||47169<=i&&i<=47195||47197<=i&&i<=47223||47225<=i&&i<=47251||47253<=i&&i<=47279||47281<=i&&i<=47307||47309<=i&&i<=47335||47337<=i&&i<=47363||47365<=i&&i<=47391||47393<=i&&i<=47419||47421<=i&&i<=47447||47449<=i&&i<=47475||47477<=i&&i<=47503||47505<=i&&i<=47531||47533<=i&&i<=47559||47561<=i&&i<=47587||47589<=i&&i<=47615||47617<=i&&i<=47643||47645<=i&&i<=47671||47673<=i&&i<=47699||47701<=i&&i<=47727||47729<=i&&i<=47755||47757<=i&&i<=47783||47785<=i&&i<=47811||47813<=i&&i<=47839||47841<=i&&i<=47867||47869<=i&&i<=47895||47897<=i&&i<=47923||47925<=i&&i<=47951||47953<=i&&i<=47979||47981<=i&&i<=48007||48009<=i&&i<=48035||48037<=i&&i<=48063||48065<=i&&i<=48091||48093<=i&&i<=48119||48121<=i&&i<=48147||48149<=i&&i<=48175||48177<=i&&i<=48203||48205<=i&&i<=48231||48233<=i&&i<=48259||48261<=i&&i<=48287||48289<=i&&i<=48315||48317<=i&&i<=48343||48345<=i&&i<=48371||48373<=i&&i<=48399||48401<=i&&i<=48427||48429<=i&&i<=48455||48457<=i&&i<=48483||48485<=i&&i<=48511||48513<=i&&i<=48539||48541<=i&&i<=48567||48569<=i&&i<=48595||48597<=i&&i<=48623||48625<=i&&i<=48651||48653<=i&&i<=48679||48681<=i&&i<=48707||48709<=i&&i<=48735||48737<=i&&i<=48763||48765<=i&&i<=48791||48793<=i&&i<=48819||48821<=i&&i<=48847||48849<=i&&i<=48875||48877<=i&&i<=48903||48905<=i&&i<=48931||48933<=i&&i<=48959||48961<=i&&i<=48987||48989<=i&&i<=49015||49017<=i&&i<=49043||49045<=i&&i<=49071||49073<=i&&i<=49099||49101<=i&&i<=49127||49129<=i&&i<=49155||49157<=i&&i<=49183||49185<=i&&i<=49211||49213<=i&&i<=49239||49241<=i&&i<=49267||49269<=i&&i<=49295||49297<=i&&i<=49323||49325<=i&&i<=49351||49353<=i&&i<=49379||49381<=i&&i<=49407||49409<=i&&i<=49435||49437<=i&&i<=49463||49465<=i&&i<=49491||49493<=i&&i<=49519||49521<=i&&i<=49547||49549<=i&&i<=49575||49577<=i&&i<=49603||49605<=i&&i<=49631||49633<=i&&i<=49659||49661<=i&&i<=49687||49689<=i&&i<=49715||49717<=i&&i<=49743||49745<=i&&i<=49771||49773<=i&&i<=49799||49801<=i&&i<=49827||49829<=i&&i<=49855||49857<=i&&i<=49883||49885<=i&&i<=49911||49913<=i&&i<=49939||49941<=i&&i<=49967||49969<=i&&i<=49995||49997<=i&&i<=50023||50025<=i&&i<=50051||50053<=i&&i<=50079||50081<=i&&i<=50107||50109<=i&&i<=50135||50137<=i&&i<=50163||50165<=i&&i<=50191||50193<=i&&i<=50219||50221<=i&&i<=50247||50249<=i&&i<=50275||50277<=i&&i<=50303||50305<=i&&i<=50331||50333<=i&&i<=50359||50361<=i&&i<=50387||50389<=i&&i<=50415||50417<=i&&i<=50443||50445<=i&&i<=50471||50473<=i&&i<=50499||50501<=i&&i<=50527||50529<=i&&i<=50555||50557<=i&&i<=50583||50585<=i&&i<=50611||50613<=i&&i<=50639||50641<=i&&i<=50667||50669<=i&&i<=50695||50697<=i&&i<=50723||50725<=i&&i<=50751||50753<=i&&i<=50779||50781<=i&&i<=50807||50809<=i&&i<=50835||50837<=i&&i<=50863||50865<=i&&i<=50891||50893<=i&&i<=50919||50921<=i&&i<=50947||50949<=i&&i<=50975||50977<=i&&i<=51003||51005<=i&&i<=51031||51033<=i&&i<=51059||51061<=i&&i<=51087||51089<=i&&i<=51115||51117<=i&&i<=51143||51145<=i&&i<=51171||51173<=i&&i<=51199||51201<=i&&i<=51227||51229<=i&&i<=51255||51257<=i&&i<=51283||51285<=i&&i<=51311||51313<=i&&i<=51339||51341<=i&&i<=51367||51369<=i&&i<=51395||51397<=i&&i<=51423||51425<=i&&i<=51451||51453<=i&&i<=51479||51481<=i&&i<=51507||51509<=i&&i<=51535||51537<=i&&i<=51563||51565<=i&&i<=51591||51593<=i&&i<=51619||51621<=i&&i<=51647||51649<=i&&i<=51675||51677<=i&&i<=51703||51705<=i&&i<=51731||51733<=i&&i<=51759||51761<=i&&i<=51787||51789<=i&&i<=51815||51817<=i&&i<=51843||51845<=i&&i<=51871||51873<=i&&i<=51899||51901<=i&&i<=51927||51929<=i&&i<=51955||51957<=i&&i<=51983||51985<=i&&i<=52011||52013<=i&&i<=52039||52041<=i&&i<=52067||52069<=i&&i<=52095||52097<=i&&i<=52123||52125<=i&&i<=52151||52153<=i&&i<=52179||52181<=i&&i<=52207||52209<=i&&i<=52235||52237<=i&&i<=52263||52265<=i&&i<=52291||52293<=i&&i<=52319||52321<=i&&i<=52347||52349<=i&&i<=52375||52377<=i&&i<=52403||52405<=i&&i<=52431||52433<=i&&i<=52459||52461<=i&&i<=52487||52489<=i&&i<=52515||52517<=i&&i<=52543||52545<=i&&i<=52571||52573<=i&&i<=52599||52601<=i&&i<=52627||52629<=i&&i<=52655||52657<=i&&i<=52683||52685<=i&&i<=52711||52713<=i&&i<=52739||52741<=i&&i<=52767||52769<=i&&i<=52795||52797<=i&&i<=52823||52825<=i&&i<=52851||52853<=i&&i<=52879||52881<=i&&i<=52907||52909<=i&&i<=52935||52937<=i&&i<=52963||52965<=i&&i<=52991||52993<=i&&i<=53019||53021<=i&&i<=53047||53049<=i&&i<=53075||53077<=i&&i<=53103||53105<=i&&i<=53131||53133<=i&&i<=53159||53161<=i&&i<=53187||53189<=i&&i<=53215||53217<=i&&i<=53243||53245<=i&&i<=53271||53273<=i&&i<=53299||53301<=i&&i<=53327||53329<=i&&i<=53355||53357<=i&&i<=53383||53385<=i&&i<=53411||53413<=i&&i<=53439||53441<=i&&i<=53467||53469<=i&&i<=53495||53497<=i&&i<=53523||53525<=i&&i<=53551||53553<=i&&i<=53579||53581<=i&&i<=53607||53609<=i&&i<=53635||53637<=i&&i<=53663||53665<=i&&i<=53691||53693<=i&&i<=53719||53721<=i&&i<=53747||53749<=i&&i<=53775||53777<=i&&i<=53803||53805<=i&&i<=53831||53833<=i&&i<=53859||53861<=i&&i<=53887||53889<=i&&i<=53915||53917<=i&&i<=53943||53945<=i&&i<=53971||53973<=i&&i<=53999||54001<=i&&i<=54027||54029<=i&&i<=54055||54057<=i&&i<=54083||54085<=i&&i<=54111||54113<=i&&i<=54139||54141<=i&&i<=54167||54169<=i&&i<=54195||54197<=i&&i<=54223||54225<=i&&i<=54251||54253<=i&&i<=54279||54281<=i&&i<=54307||54309<=i&&i<=54335||54337<=i&&i<=54363||54365<=i&&i<=54391||54393<=i&&i<=54419||54421<=i&&i<=54447||54449<=i&&i<=54475||54477<=i&&i<=54503||54505<=i&&i<=54531||54533<=i&&i<=54559||54561<=i&&i<=54587||54589<=i&&i<=54615||54617<=i&&i<=54643||54645<=i&&i<=54671||54673<=i&&i<=54699||54701<=i&&i<=54727||54729<=i&&i<=54755||54757<=i&&i<=54783||54785<=i&&i<=54811||54813<=i&&i<=54839||54841<=i&&i<=54867||54869<=i&&i<=54895||54897<=i&&i<=54923||54925<=i&&i<=54951||54953<=i&&i<=54979||54981<=i&&i<=55007||55009<=i&&i<=55035||55037<=i&&i<=55063||55065<=i&&i<=55091||55093<=i&&i<=55119||55121<=i&&i<=55147||55149<=i&&i<=55175||55177<=i&&i<=55203?10:9757==i||9977==i||9994<=i&&i<=9997||127877==i||127938<=i&&i<=127940||127943==i||127946<=i&&i<=127948||128066<=i&&i<=128067||128070<=i&&i<=128080||128110==i||128112<=i&&i<=128120||128124==i||128129<=i&&i<=128131||128133<=i&&i<=128135||128170==i||128372<=i&&i<=128373||128378==i||128400==i||128405<=i&&i<=128406||128581<=i&&i<=128583||128587<=i&&i<=128591||128675==i||128692<=i&&i<=128694||128704==i||128716==i||129304<=i&&i<=129308||129310<=i&&i<=129311||129318==i||129328<=i&&i<=129337||129341<=i&&i<=129342||129489<=i&&i<=129501?o:127995<=i&&i<=127999?14:8205==i?15:9792==i||9794==i||9877<=i&&i<=9878||9992==i||10084==i||127752==i||127806==i||127859==i||127891==i||127908==i||127912==i||127979==i||127981==i||128139==i||128187<=i&&i<=128188||128295==i||128300==i||128488==i||128640==i||128658==i?r:128102<=i&&i<=128105?a:11}return this.nextBreak=function(e,t){if(void 0===t&&(t=0),t<0)return 0;if(t>=e.length-1)return e.length;for(var n,o,r=c(i(e,t)),a=[],l=t+1;l<e.length;l++)if(o=l-1,!(55296<=(n=e).charCodeAt(o)&&n.charCodeAt(o)<=56319&&56320<=n.charCodeAt(o+1)&&n.charCodeAt(o+1)<=57343)){var m=c(i(e,l));if(s(r,a,m))return l;a.push(m)}return e.length},this.splitGraphemes=function(e){for(var t,n=[],o=0;(t=this.nextBreak(e,o))<e.length;)n.push(e.slice(o,t)),o=t;return o<e.length&&n.push(e.slice(o)),n},this.iterateGraphemes=function(e){var t=0,n={next:function(){var n,o;return(o=this.nextBreak(e,t))<e.length?(n=e.slice(t,o),t=o,{value:n,done:!1}):t<e.length?(n=e.slice(t),t=e.length,{value:n,done:!1}):{value:void 0,done:!0}}.bind(this)};return"undefined"!=typeof Symbol&&Symbol.iterator&&(n[Symbol.iterator]=function(){return n}),n},this.countGraphemes=function(e){for(var t,n=0,o=0;(t=this.nextBreak(e,o))<e.length;)o=t,n++;return o<e.length&&n++,n},this})},46290:(e,t,n)=>{"use strict";t.Z=function(e){var t=e.size,n=void 0===t?24:t,o=e.onClick,s=(e.icon,e.className),c=function(e,t){if(null==e)return{};var n,o,r=function(e,t){if(null==e)return{};var n,o,r={},a=Object.keys(e);for(o=0;o<a.length;o++)n=a[o],0<=t.indexOf(n)||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(o=0;o<a.length;o++)n=a[o],0<=t.indexOf(n)||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}(e,a),l=["gridicon","gridicons-cross-small",s,!1,!1,!1].filter(Boolean).join(" ");return r.default.createElement("svg",i({className:l,height:n,width:n,onClick:o},c,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"}),r.default.createElement("g",null,r.default.createElement("path",{d:"M17.705 7.705l-1.41-1.41L12 10.59 7.705 6.295l-1.41 1.41L10.59 12l-4.295 4.295 1.41 1.41L12 13.41l4.295 4.295 1.41-1.41L13.41 12l4.295-4.295z"})))};var o,r=(o=n(99196))&&o.__esModule?o:{default:o},a=["size","onClick","icon","className"];function i(){return i=Object.assign||function(e){for(var t,n=1;n<arguments.length;n++)for(var o in t=arguments[n])Object.prototype.hasOwnProperty.call(t,o)&&(e[o]=t[o]);return e},i.apply(this,arguments)}},72672:(e,t,n)=>{"use strict";t.Z=function(e){var t=e.size,n=void 0===t?24:t,o=e.onClick,s=(e.icon,e.className),c=function(e,t){if(null==e)return{};var n,o,r=function(e,t){if(null==e)return{};var n,o,r={},a=Object.keys(e);for(o=0;o<a.length;o++)n=a[o],0<=t.indexOf(n)||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(o=0;o<a.length;o++)n=a[o],0<=t.indexOf(n)||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}(e,a),l=["gridicon","gridicons-external",s,!!function(e){return 0==e%18}(n)&&"needs-offset",!1,!1].filter(Boolean).join(" ");return r.default.createElement("svg",i({className:l,height:n,width:n,onClick:o},c,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"}),r.default.createElement("g",null,r.default.createElement("path",{d:"M19 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h6v2H5v12h12v-6h2zM13 3v2h4.586l-7.793 7.793 1.414 1.414L19 6.414V11h2V3h-8z"})))};var o,r=(o=n(99196))&&o.__esModule?o:{default:o},a=["size","onClick","icon","className"];function i(){return i=Object.assign||function(e){for(var t,n=1;n<arguments.length;n++)for(var o in t=arguments[n])Object.prototype.hasOwnProperty.call(t,o)&&(e[o]=t[o]);return e},i.apply(this,arguments)}},59838:(e,t,n)=>{"use strict";t.Z=function(e){var t=e.size,n=void 0===t?24:t,o=e.onClick,s=(e.icon,e.className),c=function(e,t){if(null==e)return{};var n,o,r=function(e,t){if(null==e)return{};var n,o,r={},a=Object.keys(e);for(o=0;o<a.length;o++)n=a[o],0<=t.indexOf(n)||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(o=0;o<a.length;o++)n=a[o],0<=t.indexOf(n)||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}(e,a),l=["gridicon","gridicons-notice-outline",s,!!function(e){return 0==e%18}(n)&&"needs-offset",!1,!1].filter(Boolean).join(" ");return r.default.createElement("svg",i({className:l,height:n,width:n,onClick:o},c,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"}),r.default.createElement("g",null,r.default.createElement("path",{d:"M12 4c4.411 0 8 3.589 8 8s-3.589 8-8 8-8-3.589-8-8 3.589-8 8-8m0-2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm1 13h-2v2h2v-2zm-2-2h2l.5-6h-3l.5 6z"})))};var o,r=(o=n(99196))&&o.__esModule?o:{default:o},a=["size","onClick","icon","className"];function i(){return i=Object.assign||function(e){for(var t,n=1;n<arguments.length;n++)for(var o in t=arguments[n])Object.prototype.hasOwnProperty.call(t,o)&&(e[o]=t[o]);return e},i.apply(this,arguments)}},72770:(e,t,n)=>{"use strict";var o="undefined"!=typeof Symbol&&Symbol,r=n(69578);e.exports=function(){return"function"==typeof o&&"function"==typeof Symbol&&"symbol"==typeof o("foo")&&"symbol"==typeof Symbol("bar")&&r()}},69578:e=>{"use strict";e.exports=function(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var e={},t=Symbol("test"),n=Object(t);if("string"==typeof t)return!1;if("[object Symbol]"!==Object.prototype.toString.call(t))return!1;if("[object Symbol]"!==Object.prototype.toString.call(n))return!1;for(t in e[t]=42,e)return!1;if("function"==typeof Object.keys&&0!==Object.keys(e).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(e).length)return!1;var o=Object.getOwnPropertySymbols(e);if(1!==o.length||o[0]!==t)return!1;if(!Object.prototype.propertyIsEnumerable.call(e,t))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var r=Object.getOwnPropertyDescriptor(e,t);if(42!==r.value||!0!==r.enumerable)return!1}return!0}},22786:(e,t,n)=>{"use strict";var o=n(22698);e.exports=o.call(Function.call,Object.prototype.hasOwnProperty)},31741:(e,t,n)=>{var o="function"==typeof Map&&Map.prototype,r=Object.getOwnPropertyDescriptor&&o?Object.getOwnPropertyDescriptor(Map.prototype,"size"):null,a=o&&r&&"function"==typeof r.get?r.get:null,i=o&&Map.prototype.forEach,s="function"==typeof Set&&Set.prototype,c=Object.getOwnPropertyDescriptor&&s?Object.getOwnPropertyDescriptor(Set.prototype,"size"):null,l=s&&c&&"function"==typeof c.get?c.get:null,m=s&&Set.prototype.forEach,u="function"==typeof WeakMap&&WeakMap.prototype?WeakMap.prototype.has:null,d="function"==typeof WeakSet&&WeakSet.prototype?WeakSet.prototype.has:null,p="function"==typeof WeakRef&&WeakRef.prototype?WeakRef.prototype.deref:null,C=Boolean.prototype.valueOf,h=Object.prototype.toString,g=Function.prototype.toString,f=String.prototype.match,y=String.prototype.slice,w=String.prototype.replace,H=String.prototype.toUpperCase,_=String.prototype.toLowerCase,v=RegExp.prototype.test,E=Array.prototype.concat,L=Array.prototype.join,b=Array.prototype.slice,k=Math.floor,S="function"==typeof BigInt?BigInt.prototype.valueOf:null,M=Object.getOwnPropertySymbols,N="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?Symbol.prototype.toString:null,x="function"==typeof Symbol&&"object"==typeof Symbol.iterator,T="function"==typeof Symbol&&Symbol.toStringTag&&(Symbol.toStringTag,1)?Symbol.toStringTag:null,I=Object.prototype.propertyIsEnumerable,A=("function"==typeof Reflect?Reflect.getPrototypeOf:Object.getPrototypeOf)||([].__proto__===Array.prototype?function(e){return e.__proto__}:null);function O(e,t){if(e===1/0||e===-1/0||e!=e||e&&e>-1e3&&e<1e3||v.call(/e/,t))return t;var n=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if("number"==typeof e){var o=e<0?-k(-e):k(e);if(o!==e){var r=String(o),a=y.call(t,r.length+1);return w.call(r,n,"$&_")+"."+w.call(w.call(a,/([0-9]{3})/g,"$&_"),/_$/,"")}}return w.call(t,n,"$&_")}var P=n(50189),V=P.custom,R=F(V)?V:null;function j(e,t,n){var o="double"===(n.quoteStyle||t)?'"':"'";return o+e+o}function D(e){return w.call(String(e),/"/g,"&quot;")}function Z(e){return!("[object Array]"!==U(e)||T&&"object"==typeof e&&T in e)}function B(e){return!("[object RegExp]"!==U(e)||T&&"object"==typeof e&&T in e)}function F(e){if(x)return e&&"object"==typeof e&&e instanceof Symbol;if("symbol"==typeof e)return!0;if(!e||"object"!=typeof e||!N)return!1;try{return N.call(e),!0}catch(e){}return!1}e.exports=function e(t,n,o,r){var s=n||{};if(W(s,"quoteStyle")&&"single"!==s.quoteStyle&&"double"!==s.quoteStyle)throw new TypeError('option "quoteStyle" must be "single" or "double"');if(W(s,"maxStringLength")&&("number"==typeof s.maxStringLength?s.maxStringLength<0&&s.maxStringLength!==1/0:null!==s.maxStringLength))throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var c=!W(s,"customInspect")||s.customInspect;if("boolean"!=typeof c&&"symbol"!==c)throw new TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if(W(s,"indent")&&null!==s.indent&&"\t"!==s.indent&&!(parseInt(s.indent,10)===s.indent&&s.indent>0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(W(s,"numericSeparator")&&"boolean"!=typeof s.numericSeparator)throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var h=s.numericSeparator;if(void 0===t)return"undefined";if(null===t)return"null";if("boolean"==typeof t)return t?"true":"false";if("string"==typeof t)return Y(t,s);if("number"==typeof t){if(0===t)return 1/0/t>0?"0":"-0";var H=String(t);return h?O(t,H):H}if("bigint"==typeof t){var v=String(t)+"n";return h?O(t,v):v}var k=void 0===s.depth?5:s.depth;if(void 0===o&&(o=0),o>=k&&k>0&&"object"==typeof t)return Z(t)?"[Array]":"[Object]";var M,V=function(e,t){var n;if("\t"===e.indent)n="\t";else{if(!("number"==typeof e.indent&&e.indent>0))return null;n=L.call(Array(e.indent+1)," ")}return{base:n,prev:L.call(Array(t+1),n)}}(s,o);if(void 0===r)r=[];else if(G(r,t)>=0)return"[Circular]";function z(t,n,a){if(n&&(r=b.call(r)).push(n),a){var i={depth:s.depth};return W(s,"quoteStyle")&&(i.quoteStyle=s.quoteStyle),e(t,i,o+1,r)}return e(t,s,o+1,r)}if("function"==typeof t&&!B(t)){var Q=function(e){if(e.name)return e.name;var t=f.call(g.call(e),/^function\s*([\w$]+)/);return t?t[1]:null}(t),ee=$(t,z);return"[Function"+(Q?": "+Q:" (anonymous)")+"]"+(ee.length>0?" { "+L.call(ee,", ")+" }":"")}if(F(t)){var te=x?w.call(String(t),/^(Symbol\(.*\))_[^)]*$/,"$1"):N.call(t);return"object"!=typeof t||x?te:q(te)}if((M=t)&&"object"==typeof M&&("undefined"!=typeof HTMLElement&&M instanceof HTMLElement||"string"==typeof M.nodeName&&"function"==typeof M.getAttribute)){for(var ne="<"+_.call(String(t.nodeName)),oe=t.attributes||[],re=0;re<oe.length;re++)ne+=" "+oe[re].name+"="+j(D(oe[re].value),"double",s);return ne+=">",t.childNodes&&t.childNodes.length&&(ne+="..."),ne+"</"+_.call(String(t.nodeName))+">"}if(Z(t)){if(0===t.length)return"[]";var ae=$(t,z);return V&&!function(e){for(var t=0;t<e.length;t++)if(G(e[t],"\n")>=0)return!1;return!0}(ae)?"["+X(ae,V)+"]":"[ "+L.call(ae,", ")+" ]"}if(function(e){return!("[object Error]"!==U(e)||T&&"object"==typeof e&&T in e)}(t)){var ie=$(t,z);return"cause"in Error.prototype||!("cause"in t)||I.call(t,"cause")?0===ie.length?"["+String(t)+"]":"{ ["+String(t)+"] "+L.call(ie,", ")+" }":"{ ["+String(t)+"] "+L.call(E.call("[cause]: "+z(t.cause),ie),", ")+" }"}if("object"==typeof t&&c){if(R&&"function"==typeof t[R]&&P)return P(t,{depth:k-o});if("symbol"!==c&&"function"==typeof t.inspect)return t.inspect()}if(function(e){if(!a||!e||"object"!=typeof e)return!1;try{a.call(e);try{l.call(e)}catch(e){return!0}return e instanceof Map}catch(e){}return!1}(t)){var se=[];return i.call(t,(function(e,n){se.push(z(n,t,!0)+" => "+z(e,t))})),K("Map",a.call(t),se,V)}if(function(e){if(!l||!e||"object"!=typeof e)return!1;try{l.call(e);try{a.call(e)}catch(e){return!0}return e instanceof Set}catch(e){}return!1}(t)){var ce=[];return m.call(t,(function(e){ce.push(z(e,t))})),K("Set",l.call(t),ce,V)}if(function(e){if(!u||!e||"object"!=typeof e)return!1;try{u.call(e,u);try{d.call(e,d)}catch(e){return!0}return e instanceof WeakMap}catch(e){}return!1}(t))return J("WeakMap");if(function(e){if(!d||!e||"object"!=typeof e)return!1;try{d.call(e,d);try{u.call(e,u)}catch(e){return!0}return e instanceof WeakSet}catch(e){}return!1}(t))return J("WeakSet");if(function(e){if(!p||!e||"object"!=typeof e)return!1;try{return p.call(e),!0}catch(e){}return!1}(t))return J("WeakRef");if(function(e){return!("[object Number]"!==U(e)||T&&"object"==typeof e&&T in e)}(t))return q(z(Number(t)));if(function(e){if(!e||"object"!=typeof e||!S)return!1;try{return S.call(e),!0}catch(e){}return!1}(t))return q(z(S.call(t)));if(function(e){return!("[object Boolean]"!==U(e)||T&&"object"==typeof e&&T in e)}(t))return q(C.call(t));if(function(e){return!("[object String]"!==U(e)||T&&"object"==typeof e&&T in e)}(t))return q(z(String(t)));if(!function(e){return!("[object Date]"!==U(e)||T&&"object"==typeof e&&T in e)}(t)&&!B(t)){var le=$(t,z),me=A?A(t)===Object.prototype:t instanceof Object||t.constructor===Object,ue=t instanceof Object?"":"null prototype",de=!me&&T&&Object(t)===t&&T in t?y.call(U(t),8,-1):ue?"Object":"",pe=(me||"function"!=typeof t.constructor?"":t.constructor.name?t.constructor.name+" ":"")+(de||ue?"["+L.call(E.call([],de||[],ue||[]),": ")+"] ":"");return 0===le.length?pe+"{}":V?pe+"{"+X(le,V)+"}":pe+"{ "+L.call(le,", ")+" }"}return String(t)};var z=Object.prototype.hasOwnProperty||function(e){return e in this};function W(e,t){return z.call(e,t)}function U(e){return h.call(e)}function G(e,t){if(e.indexOf)return e.indexOf(t);for(var n=0,o=e.length;n<o;n++)if(e[n]===t)return n;return-1}function Y(e,t){if(e.length>t.maxStringLength){var n=e.length-t.maxStringLength,o="... "+n+" more character"+(n>1?"s":"");return Y(y.call(e,0,t.maxStringLength),t)+o}return j(w.call(w.call(e,/(['\\])/g,"\\$1"),/[\x00-\x1f]/g,Q),"single",t)}function Q(e){var t=e.charCodeAt(0),n={8:"b",9:"t",10:"n",12:"f",13:"r"}[t];return n?"\\"+n:"\\x"+(t<16?"0":"")+H.call(t.toString(16))}function q(e){return"Object("+e+")"}function J(e){return e+" { ? }"}function K(e,t,n,o){return e+" ("+t+") {"+(o?X(n,o):L.call(n,", "))+"}"}function X(e,t){if(0===e.length)return"";var n="\n"+t.prev+t.base;return n+L.call(e,","+n)+"\n"+t.prev}function $(e,t){var n=Z(e),o=[];if(n){o.length=e.length;for(var r=0;r<e.length;r++)o[r]=W(e,r)?t(e[r],e):""}var a,i="function"==typeof M?M(e):[];if(x){a={};for(var s=0;s<i.length;s++)a["$"+i[s]]=i[s]}for(var c in e)W(e,c)&&(n&&String(Number(c))===c&&c<e.length||x&&a["$"+c]instanceof Symbol||(v.call(/[^\w$]/,c)?o.push(t(c,e)+": "+t(e[c],e)):o.push(c+": "+t(e[c],e))));if("function"==typeof M)for(var l=0;l<i.length;l++)I.call(e,i[l])&&o.push("["+t(i[l])+"]: "+t(e[i[l]],e));return o}},31772:(e,t,n)=>{"use strict";var o=n(25148);function r(){}function a(){}a.resetWarningCache=r,e.exports=function(){function e(e,t,n,r,a,i){if(i!==o){var s=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw s.name="Invariant Violation",s}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:a,resetWarningCache:r};return n.PropTypes=n,n}},7862:(e,t,n)=>{e.exports=n(31772)()},25148:e=>{"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},38243:e=>{"use strict";var t=String.prototype.replace,n=/%20/g,o="RFC3986";e.exports={default:o,formatters:{RFC1738:function(e){return t.call(e,n,"+")},RFC3986:function(e){return String(e)}},RFC1738:"RFC1738",RFC3986:o}},79119:(e,t,n)=>{"use strict";var o=n(67576),r=n(93649),a=n(38243);e.exports={formats:a,parse:r,stringify:o}},93649:(e,t,n)=>{"use strict";var o=n(7173),r=Object.prototype.hasOwnProperty,a=Array.isArray,i={allowDots:!1,allowPrototypes:!1,allowSparse:!1,arrayLimit:20,charset:"utf-8",charsetSentinel:!1,comma:!1,decoder:o.decode,delimiter:"&",depth:5,ignoreQueryPrefix:!1,interpretNumericEntities:!1,parameterLimit:1e3,parseArrays:!0,plainObjects:!1,strictNullHandling:!1},s=function(e){return e.replace(/&#(\d+);/g,(function(e,t){return String.fromCharCode(parseInt(t,10))}))},c=function(e,t){return e&&"string"==typeof e&&t.comma&&e.indexOf(",")>-1?e.split(","):e},l=function(e,t,n,o){if(e){var a=n.allowDots?e.replace(/\.([^.[]+)/g,"[$1]"):e,i=/(\[[^[\]]*])/g,s=n.depth>0&&/(\[[^[\]]*])/.exec(a),l=s?a.slice(0,s.index):a,m=[];if(l){if(!n.plainObjects&&r.call(Object.prototype,l)&&!n.allowPrototypes)return;m.push(l)}for(var u=0;n.depth>0&&null!==(s=i.exec(a))&&u<n.depth;){if(u+=1,!n.plainObjects&&r.call(Object.prototype,s[1].slice(1,-1))&&!n.allowPrototypes)return;m.push(s[1])}return s&&m.push("["+a.slice(s.index)+"]"),function(e,t,n,o){for(var r=o?t:c(t,n),a=e.length-1;a>=0;--a){var i,s=e[a];if("[]"===s&&n.parseArrays)i=[].concat(r);else{i=n.plainObjects?Object.create(null):{};var l="["===s.charAt(0)&&"]"===s.charAt(s.length-1)?s.slice(1,-1):s,m=parseInt(l,10);n.parseArrays||""!==l?!isNaN(m)&&s!==l&&String(m)===l&&m>=0&&n.parseArrays&&m<=n.arrayLimit?(i=[])[m]=r:"__proto__"!==l&&(i[l]=r):i={0:r}}r=i}return r}(m,t,n,o)}};e.exports=function(e,t){var n=function(e){if(!e)return i;if(null!==e.decoder&&void 0!==e.decoder&&"function"!=typeof e.decoder)throw new TypeError("Decoder has to be a function.");if(void 0!==e.charset&&"utf-8"!==e.charset&&"iso-8859-1"!==e.charset)throw new TypeError("The charset option must be either utf-8, iso-8859-1, or undefined");var t=void 0===e.charset?i.charset:e.charset;return{allowDots:void 0===e.allowDots?i.allowDots:!!e.allowDots,allowPrototypes:"boolean"==typeof e.allowPrototypes?e.allowPrototypes:i.allowPrototypes,allowSparse:"boolean"==typeof e.allowSparse?e.allowSparse:i.allowSparse,arrayLimit:"number"==typeof e.arrayLimit?e.arrayLimit:i.arrayLimit,charset:t,charsetSentinel:"boolean"==typeof e.charsetSentinel?e.charsetSentinel:i.charsetSentinel,comma:"boolean"==typeof e.comma?e.comma:i.comma,decoder:"function"==typeof e.decoder?e.decoder:i.decoder,delimiter:"string"==typeof e.delimiter||o.isRegExp(e.delimiter)?e.delimiter:i.delimiter,depth:"number"==typeof e.depth||!1===e.depth?+e.depth:i.depth,ignoreQueryPrefix:!0===e.ignoreQueryPrefix,interpretNumericEntities:"boolean"==typeof e.interpretNumericEntities?e.interpretNumericEntities:i.interpretNumericEntities,parameterLimit:"number"==typeof e.parameterLimit?e.parameterLimit:i.parameterLimit,parseArrays:!1!==e.parseArrays,plainObjects:"boolean"==typeof e.plainObjects?e.plainObjects:i.plainObjects,strictNullHandling:"boolean"==typeof e.strictNullHandling?e.strictNullHandling:i.strictNullHandling}}(t);if(""===e||null==e)return n.plainObjects?Object.create(null):{};for(var m="string"==typeof e?function(e,t){var n,l={},m=t.ignoreQueryPrefix?e.replace(/^\?/,""):e,u=t.parameterLimit===1/0?void 0:t.parameterLimit,d=m.split(t.delimiter,u),p=-1,C=t.charset;if(t.charsetSentinel)for(n=0;n<d.length;++n)0===d[n].indexOf("utf8=")&&("utf8=%E2%9C%93"===d[n]?C="utf-8":"utf8=%26%2310003%3B"===d[n]&&(C="iso-8859-1"),p=n,n=d.length);for(n=0;n<d.length;++n)if(n!==p){var h,g,f=d[n],y=f.indexOf("]="),w=-1===y?f.indexOf("="):y+1;-1===w?(h=t.decoder(f,i.decoder,C,"key"),g=t.strictNullHandling?null:""):(h=t.decoder(f.slice(0,w),i.decoder,C,"key"),g=o.maybeMap(c(f.slice(w+1),t),(function(e){return t.decoder(e,i.decoder,C,"value")}))),g&&t.interpretNumericEntities&&"iso-8859-1"===C&&(g=s(g)),f.indexOf("[]=")>-1&&(g=a(g)?[g]:g),r.call(l,h)?l[h]=o.combine(l[h],g):l[h]=g}return l}(e,n):e,u=n.plainObjects?Object.create(null):{},d=Object.keys(m),p=0;p<d.length;++p){var C=d[p],h=l(C,m[C],n,"string"==typeof e);u=o.merge(u,h,n)}return!0===n.allowSparse?u:o.compact(u)}},67576:(e,t,n)=>{"use strict";var o=n(44852),r=n(7173),a=n(38243),i=Object.prototype.hasOwnProperty,s={brackets:function(e){return e+"[]"},comma:"comma",indices:function(e,t){return e+"["+t+"]"},repeat:function(e){return e}},c=Array.isArray,l=String.prototype.split,m=Array.prototype.push,u=function(e,t){m.apply(e,c(t)?t:[t])},d=Date.prototype.toISOString,p=a.default,C={addQueryPrefix:!1,allowDots:!1,charset:"utf-8",charsetSentinel:!1,delimiter:"&",encode:!0,encoder:r.encode,encodeValuesOnly:!1,format:p,formatter:a.formatters[p],indices:!1,serializeDate:function(e){return d.call(e)},skipNulls:!1,strictNullHandling:!1},h={},g=function e(t,n,a,i,s,m,d,p,g,f,y,w,H,_,v){for(var E,L=t,b=v,k=0,S=!1;void 0!==(b=b.get(h))&&!S;){var M=b.get(t);if(k+=1,void 0!==M){if(M===k)throw new RangeError("Cyclic object value");S=!0}void 0===b.get(h)&&(k=0)}if("function"==typeof d?L=d(n,L):L instanceof Date?L=f(L):"comma"===a&&c(L)&&(L=r.maybeMap(L,(function(e){return e instanceof Date?f(e):e}))),null===L){if(i)return m&&!H?m(n,C.encoder,_,"key",y):n;L=""}if("string"==typeof(E=L)||"number"==typeof E||"boolean"==typeof E||"symbol"==typeof E||"bigint"==typeof E||r.isBuffer(L)){if(m){var N=H?n:m(n,C.encoder,_,"key",y);if("comma"===a&&H){for(var x=l.call(String(L),","),T="",I=0;I<x.length;++I)T+=(0===I?"":",")+w(m(x[I],C.encoder,_,"value",y));return[w(N)+"="+T]}return[w(N)+"="+w(m(L,C.encoder,_,"value",y))]}return[w(n)+"="+w(String(L))]}var A,O=[];if(void 0===L)return O;if("comma"===a&&c(L))A=[{value:L.length>0?L.join(",")||null:void 0}];else if(c(d))A=d;else{var P=Object.keys(L);A=p?P.sort(p):P}for(var V=0;V<A.length;++V){var R=A[V],j="object"==typeof R&&void 0!==R.value?R.value:L[R];if(!s||null!==j){var D=c(L)?"function"==typeof a?a(n,R):n:n+(g?"."+R:"["+R+"]");v.set(t,k);var Z=o();Z.set(h,v),u(O,e(j,D,a,i,s,m,d,p,g,f,y,w,H,_,Z))}}return O};e.exports=function(e,t){var n,r=e,l=function(e){if(!e)return C;if(null!==e.encoder&&void 0!==e.encoder&&"function"!=typeof e.encoder)throw new TypeError("Encoder has to be a function.");var t=e.charset||C.charset;if(void 0!==e.charset&&"utf-8"!==e.charset&&"iso-8859-1"!==e.charset)throw new TypeError("The charset option must be either utf-8, iso-8859-1, or undefined");var n=a.default;if(void 0!==e.format){if(!i.call(a.formatters,e.format))throw new TypeError("Unknown format option provided.");n=e.format}var o=a.formatters[n],r=C.filter;return("function"==typeof e.filter||c(e.filter))&&(r=e.filter),{addQueryPrefix:"boolean"==typeof e.addQueryPrefix?e.addQueryPrefix:C.addQueryPrefix,allowDots:void 0===e.allowDots?C.allowDots:!!e.allowDots,charset:t,charsetSentinel:"boolean"==typeof e.charsetSentinel?e.charsetSentinel:C.charsetSentinel,delimiter:void 0===e.delimiter?C.delimiter:e.delimiter,encode:"boolean"==typeof e.encode?e.encode:C.encode,encoder:"function"==typeof e.encoder?e.encoder:C.encoder,encodeValuesOnly:"boolean"==typeof e.encodeValuesOnly?e.encodeValuesOnly:C.encodeValuesOnly,filter:r,format:n,formatter:o,serializeDate:"function"==typeof e.serializeDate?e.serializeDate:C.serializeDate,skipNulls:"boolean"==typeof e.skipNulls?e.skipNulls:C.skipNulls,sort:"function"==typeof e.sort?e.sort:null,strictNullHandling:"boolean"==typeof e.strictNullHandling?e.strictNullHandling:C.strictNullHandling}}(t);"function"==typeof l.filter?r=(0,l.filter)("",r):c(l.filter)&&(n=l.filter);var m,d=[];if("object"!=typeof r||null===r)return"";m=t&&t.arrayFormat in s?t.arrayFormat:t&&"indices"in t?t.indices?"indices":"repeat":"indices";var p=s[m];n||(n=Object.keys(r)),l.sort&&n.sort(l.sort);for(var h=o(),f=0;f<n.length;++f){var y=n[f];l.skipNulls&&null===r[y]||u(d,g(r[y],y,p,l.strictNullHandling,l.skipNulls,l.encode?l.encoder:null,l.filter,l.sort,l.allowDots,l.serializeDate,l.format,l.formatter,l.encodeValuesOnly,l.charset,h))}var w=d.join(l.delimiter),H=!0===l.addQueryPrefix?"?":"";return l.charsetSentinel&&("iso-8859-1"===l.charset?H+="utf8=%26%2310003%3B&":H+="utf8=%E2%9C%93&"),w.length>0?H+w:""}},7173:(e,t,n)=>{"use strict";var o=n(38243),r=Object.prototype.hasOwnProperty,a=Array.isArray,i=function(){for(var e=[],t=0;t<256;++t)e.push("%"+((t<16?"0":"")+t.toString(16)).toUpperCase());return e}(),s=function(e,t){for(var n=t&&t.plainObjects?Object.create(null):{},o=0;o<e.length;++o)void 0!==e[o]&&(n[o]=e[o]);return n};e.exports={arrayToObject:s,assign:function(e,t){return Object.keys(t).reduce((function(e,n){return e[n]=t[n],e}),e)},combine:function(e,t){return[].concat(e,t)},compact:function(e){for(var t=[{obj:{o:e},prop:"o"}],n=[],o=0;o<t.length;++o)for(var r=t[o],i=r.obj[r.prop],s=Object.keys(i),c=0;c<s.length;++c){var l=s[c],m=i[l];"object"==typeof m&&null!==m&&-1===n.indexOf(m)&&(t.push({obj:i,prop:l}),n.push(m))}return function(e){for(;e.length>1;){var t=e.pop(),n=t.obj[t.prop];if(a(n)){for(var o=[],r=0;r<n.length;++r)void 0!==n[r]&&o.push(n[r]);t.obj[t.prop]=o}}}(t),e},decode:function(e,t,n){var o=e.replace(/\+/g," ");if("iso-8859-1"===n)return o.replace(/%[0-9a-f]{2}/gi,unescape);try{return decodeURIComponent(o)}catch(e){return o}},encode:function(e,t,n,r,a){if(0===e.length)return e;var s=e;if("symbol"==typeof e?s=Symbol.prototype.toString.call(e):"string"!=typeof e&&(s=String(e)),"iso-8859-1"===n)return escape(s).replace(/%u[0-9a-f]{4}/gi,(function(e){return"%26%23"+parseInt(e.slice(2),16)+"%3B"}));for(var c="",l=0;l<s.length;++l){var m=s.charCodeAt(l);45===m||46===m||95===m||126===m||m>=48&&m<=57||m>=65&&m<=90||m>=97&&m<=122||a===o.RFC1738&&(40===m||41===m)?c+=s.charAt(l):m<128?c+=i[m]:m<2048?c+=i[192|m>>6]+i[128|63&m]:m<55296||m>=57344?c+=i[224|m>>12]+i[128|m>>6&63]+i[128|63&m]:(l+=1,m=65536+((1023&m)<<10|1023&s.charCodeAt(l)),c+=i[240|m>>18]+i[128|m>>12&63]+i[128|m>>6&63]+i[128|63&m])}return c},isBuffer:function(e){return!(!e||"object"!=typeof e||!(e.constructor&&e.constructor.isBuffer&&e.constructor.isBuffer(e)))},isRegExp:function(e){return"[object RegExp]"===Object.prototype.toString.call(e)},maybeMap:function(e,t){if(a(e)){for(var n=[],o=0;o<e.length;o+=1)n.push(t(e[o]));return n}return t(e)},merge:function e(t,n,o){if(!n)return t;if("object"!=typeof n){if(a(t))t.push(n);else{if(!t||"object"!=typeof t)return[t,n];(o&&(o.plainObjects||o.allowPrototypes)||!r.call(Object.prototype,n))&&(t[n]=!0)}return t}if(!t||"object"!=typeof t)return[t].concat(n);var i=t;return a(t)&&!a(n)&&(i=s(t,o)),a(t)&&a(n)?(n.forEach((function(n,a){if(r.call(t,a)){var i=t[a];i&&"object"==typeof i&&n&&"object"==typeof n?t[a]=e(i,n,o):t.push(n)}else t[a]=n})),t):Object.keys(n).reduce((function(t,a){var i=n[a];return r.call(t,a)?t[a]=e(t[a],i,o):t[a]=i,t}),i)}}},67027:(e,t,n)=>{"use strict";n.d(t,{M:()=>a,lr:()=>i});var o=n(99196),r=n(85597);function a(e){let{basename:t,children:n,history:a}=e;const[i,s]=(0,o.useState)({action:a.action,location:a.location});return(0,o.useLayoutEffect)((()=>a.listen(s)),[a]),(0,o.createElement)(r.F0,{basename:t,children:n,location:i.location,navigationType:i.action,navigator:a})}function i(e){let t=(0,o.useRef)(s(e)),n=(0,r.TH)(),a=(0,o.useMemo)((()=>{let e=s(n.search);for(let n of t.current.keys())e.has(n)||t.current.getAll(n).forEach((t=>{e.append(n,t)}));return e}),[n.search]),i=(0,r.s0)();return[a,(0,o.useCallback)(((e,t)=>{i("?"+s(e),t)}),[i])]}function s(e){return void 0===e&&(e=""),new URLSearchParams("string"==typeof e||Array.isArray(e)||e instanceof URLSearchParams?e:Object.keys(e).reduce(((t,n)=>{let o=e[n];return t.concat(Array.isArray(o)?o.map((e=>[n,e])):[[n,o]])}),[]))}},85597:(e,t,n)=>{"use strict";var o;function r(e){var t={};if(e){var n=e.indexOf("#");n>=0&&(t.hash=e.substr(n),e=e.substr(0,n));var o=e.indexOf("?");o>=0&&(t.search=e.substr(o),e=e.substr(0,o)),e&&(t.pathname=e)}return t}n.d(t,{AW:()=>S,F0:()=>M,Z5:()=>N,TH:()=>E,bS:()=>L,s0:()=>b,UO:()=>k}),function(e){e.Pop="POP",e.Push="PUSH",e.Replace="REPLACE"}(o||(o={}));var a=n(99196);const i=(0,a.createContext)(null),s=(0,a.createContext)(null),c=(0,a.createContext)({outlet:null,matches:[]});function l(e,t){if(!e)throw new Error(t)}function m(e,t,n){void 0===n&&(n="/");let o=f(("string"==typeof t?r(t):t).pathname||"/",n);if(null==o)return null;let a=u(e);!function(e){e.sort(((e,t)=>e.score!==t.score?t.score-e.score:function(e,t){return e.length===t.length&&e.slice(0,-1).every(((e,n)=>e===t[n]))?e[e.length-1]-t[t.length-1]:0}(e.routesMeta.map((e=>e.childrenIndex)),t.routesMeta.map((e=>e.childrenIndex)))))}(a);let i=null;for(let e=0;null==i&&e<a.length;++e)i=h(a[e],o);return i}function u(e,t,n,o){return void 0===t&&(t=[]),void 0===n&&(n=[]),void 0===o&&(o=""),e.forEach(((e,r)=>{let a={relativePath:e.path||"",caseSensitive:!0===e.caseSensitive,childrenIndex:r,route:e};a.relativePath.startsWith("/")&&(a.relativePath.startsWith(o)||l(!1),a.relativePath=a.relativePath.slice(o.length));let i=y([o,a.relativePath]),s=n.concat(a);e.children&&e.children.length>0&&(!0===e.index&&l(!1),u(e.children,t,s,i)),(null!=e.path||e.index)&&t.push({path:i,score:C(i,e.index),routesMeta:s})})),t}const d=/^:\w+$/,p=e=>"*"===e;function C(e,t){let n=e.split("/"),o=n.length;return n.some(p)&&(o+=-2),t&&(o+=2),n.filter((e=>!p(e))).reduce(((e,t)=>e+(d.test(t)?3:""===t?1:10)),o)}function h(e,t){let{routesMeta:n}=e,o={},r="/",a=[];for(let e=0;e<n.length;++e){let i=n[e],s=e===n.length-1,c="/"===r?t:t.slice(r.length)||"/",l=g({path:i.relativePath,caseSensitive:i.caseSensitive,end:s},c);if(!l)return null;Object.assign(o,l.params);let m=i.route;a.push({params:o,pathname:y([r,l.pathname]),pathnameBase:w(y([r,l.pathnameBase])),route:m}),"/"!==l.pathnameBase&&(r=y([r,l.pathnameBase]))}return a}function g(e,t){"string"==typeof e&&(e={path:e,caseSensitive:!1,end:!0});let[n,o]=function(e,t,n){void 0===t&&(t=!1),void 0===n&&(n=!0);let o=[],r="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^$?{}|()[\]]/g,"\\$&").replace(/:(\w+)/g,((e,t)=>(o.push(t),"([^\\/]+)")));return e.endsWith("*")?(o.push("*"),r+="*"===e||"/*"===e?"(.*)$":"(?:\\/(.+)|\\/*)$"):r+=n?"\\/*$":"(?:(?=[.~-]|%[0-9A-F]{2})|\\b|\\/|$)",[new RegExp(r,t?void 0:"i"),o]}(e.path,e.caseSensitive,e.end),r=t.match(n);if(!r)return null;let a=r[0],i=a.replace(/(.)\/+$/,"$1"),s=r.slice(1);return{params:o.reduce(((e,t,n)=>{if("*"===t){let e=s[n]||"";i=a.slice(0,a.length-e.length).replace(/(.)\/+$/,"$1")}return e[t]=function(e,t){try{return decodeURIComponent(e)}catch(t){return e}}(s[n]||""),e}),{}),pathname:a,pathnameBase:i,pattern:e}}function f(e,t){if("/"===t)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=e.charAt(t.length);return n&&"/"!==n?null:e.slice(t.length)||"/"}const y=e=>e.join("/").replace(/\/\/+/g,"/"),w=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),H=e=>e&&"?"!==e?e.startsWith("?")?e:"?"+e:"",_=e=>e&&"#"!==e?e.startsWith("#")?e:"#"+e:"";function v(){return null!=(0,a.useContext)(s)}function E(){return v()||l(!1),(0,a.useContext)(s).location}function L(e){v()||l(!1);let{pathname:t}=E();return(0,a.useMemo)((()=>g(e,t)),[t,e])}function b(){v()||l(!1);let{basename:e,navigator:t}=(0,a.useContext)(i),{matches:n}=(0,a.useContext)(c),{pathname:o}=E(),s=JSON.stringify(n.map((e=>e.pathnameBase))),m=(0,a.useRef)(!1);return(0,a.useEffect)((()=>{m.current=!0})),(0,a.useCallback)((function(n,a){if(void 0===a&&(a={}),!m.current)return;if("number"==typeof n)return void t.go(n);let i=function(e,t,n){let o,a="string"==typeof e?r(e):e,i=""===e||""===a.pathname?"/":a.pathname;if(null==i)o=n;else{let e=t.length-1;if(i.startsWith("..")){let t=i.split("/");for(;".."===t[0];)t.shift(),e-=1;a.pathname=t.join("/")}o=e>=0?t[e]:"/"}let s=function(e,t){void 0===t&&(t="/");let{pathname:n,search:o="",hash:a=""}="string"==typeof e?r(e):e,i=n?n.startsWith("/")?n:function(e,t){let n=t.replace(/\/+$/,"").split("/");return e.split("/").forEach((e=>{".."===e?n.length>1&&n.pop():"."!==e&&n.push(e)})),n.length>1?n.join("/"):"/"}(n,t):t;return{pathname:i,search:H(o),hash:_(a)}}(a,o);return i&&"/"!==i&&i.endsWith("/")&&!s.pathname.endsWith("/")&&(s.pathname+="/"),s}(n,JSON.parse(s),o);"/"!==e&&(i.pathname=y([e,i.pathname])),(a.replace?t.replace:t.push)(i,a.state)}),[e,t,s,o])}function k(){let{matches:e}=(0,a.useContext)(c),t=e[e.length-1];return t?t.params:{}}function S(e){l(!1)}function M(e){let{basename:t="/",children:n=null,location:c,navigationType:m=o.Pop,navigator:u,static:d=!1}=e;v()&&l(!1);let p=w(t),C=(0,a.useMemo)((()=>({basename:p,navigator:u,static:d})),[p,u,d]);"string"==typeof c&&(c=r(c));let{pathname:h="/",search:g="",hash:y="",state:H=null,key:_="default"}=c,E=(0,a.useMemo)((()=>{let e=f(h,p);return null==e?null:{pathname:e,search:g,hash:y,state:H,key:_}}),[p,h,g,y,H,_]);return null==E?null:(0,a.createElement)(i.Provider,{value:C},(0,a.createElement)(s.Provider,{children:n,value:{location:E,navigationType:m}}))}function N(e){let{children:t,location:n}=e;return function(e,t){v()||l(!1);let{matches:n}=(0,a.useContext)(c),o=n[n.length-1],i=o?o.params:{},s=(o&&o.pathname,o?o.pathnameBase:"/");o&&o.route;let u,d=E();if(t){var p;let e="string"==typeof t?r(t):t;"/"===s||(null==(p=e.pathname)?void 0:p.startsWith(s))||l(!1),u=e}else u=d;let C=u.pathname||"/",h=m(e,{pathname:"/"===s?C:C.slice(s.length)||"/"});return function(e,t){return void 0===t&&(t=[]),null==e?null:e.reduceRight(((n,o,r)=>(0,a.createElement)(c.Provider,{children:void 0!==o.route.element?o.route.element:n,value:{outlet:n,matches:t.concat(e.slice(0,r+1))}})),null)}(h&&h.map((e=>Object.assign({},e,{params:Object.assign({},i,e.params),pathname:y([s,e.pathname]),pathnameBase:"/"===e.pathnameBase?s:y([s,e.pathnameBase])}))),n)}(x(t),n)}function x(e){let t=[];return a.Children.forEach(e,(e=>{if(!(0,a.isValidElement)(e))return;if(e.type===a.Fragment)return void t.push.apply(t,x(e.props.children));e.type!==S&&l(!1);let n={caseSensitive:e.props.caseSensitive,element:e.props.element,index:e.props.index,path:e.props.path};e.props.children&&(n.children=x(e.props.children)),t.push(n)})),t}},44852:(e,t,n)=>{"use strict";var o=n(28487),r=n(2864),a=n(31741),i=o("%TypeError%"),s=o("%WeakMap%",!0),c=o("%Map%",!0),l=r("WeakMap.prototype.get",!0),m=r("WeakMap.prototype.set",!0),u=r("WeakMap.prototype.has",!0),d=r("Map.prototype.get",!0),p=r("Map.prototype.set",!0),C=r("Map.prototype.has",!0),h=function(e,t){for(var n,o=e;null!==(n=o.next);o=n)if(n.key===t)return o.next=n.next,n.next=e.next,e.next=n,n};e.exports=function(){var e,t,n,o={assert:function(e){if(!o.has(e))throw new i("Side channel does not contain "+a(e))},get:function(o){if(s&&o&&("object"==typeof o||"function"==typeof o)){if(e)return l(e,o)}else if(c){if(t)return d(t,o)}else if(n)return function(e,t){var n=h(e,t);return n&&n.value}(n,o)},has:function(o){if(s&&o&&("object"==typeof o||"function"==typeof o)){if(e)return u(e,o)}else if(c){if(t)return C(t,o)}else if(n)return function(e,t){return!!h(e,t)}(n,o);return!1},set:function(o,r){s&&o&&("object"==typeof o||"function"==typeof o)?(e||(e=new s),m(e,o,r)):c?(t||(t=new c),p(t,o,r)):(n||(n={key:{},next:null}),function(e,t,n){var o=h(e,t);o?o.value=n:e.next={key:t,next:e.next,value:n}}(n,o,r))}};return o}},61055:e=>{function t(e,t){if((e=e.replace(/\s+/g,""))===(t=t.replace(/\s+/g,"")))return 1;if(e.length<2||t.length<2)return 0;let n=new Map;for(let t=0;t<e.length-1;t++){const o=e.substring(t,t+2),r=n.has(o)?n.get(o)+1:1;n.set(o,r)}let o=0;for(let e=0;e<t.length-1;e++){const r=t.substring(e,e+2),a=n.has(r)?n.get(r):0;a>0&&(n.set(r,a-1),o++)}return 2*o/(e.length+t.length-2)}e.exports={compareTwoStrings:t,findBestMatch:function(e,n){if(!function(e,t){return"string"==typeof e&&!!Array.isArray(t)&&!!t.length&&!t.find((function(e){return"string"!=typeof e}))}(e,n))throw new Error("Bad arguments: First argument should be a string, second should be an array of strings");const o=[];let r=0;for(let a=0;a<n.length;a++){const i=n[a],s=t(e,i);o.push({target:i,rating:s}),s>o[r].rating&&(r=a)}return{ratings:o,bestMatch:o[r],bestMatchIndex:r}}}},31863:e=>{"use strict";e.exports="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTguNTc0NjUgMy4yMTYzNUwxLjUxNjMyIDE0Ljk5OTdDMS4zNzA3OSAxNS4yNTE3IDEuMjkzNzkgMTUuNTM3NCAxLjI5Mjk4IDE1LjgyODRDMS4yOTIxNiAxNi4xMTk1IDEuMzY3NTYgMTYuNDA1NiAxLjUxMTY3IDE2LjY1ODVDMS42NTU3OSAxNi45MTEzIDEuODYzNTkgMTcuMTIyIDIuMTE0NDEgMTcuMjY5NkMyLjM2NTIzIDE3LjQxNzEgMi42NTAzMiAxNy40OTY1IDIuOTQxMzIgMTcuNDk5N0gxNy4wNThDMTcuMzQ5IDE3LjQ5NjUgMTcuNjM0MSAxNy40MTcxIDE3Ljg4NDkgMTcuMjY5NkMxOC4xMzU3IDE3LjEyMiAxOC4zNDM1IDE2LjkxMTMgMTguNDg3NiAxNi42NTg1QzE4LjYzMTcgMTYuNDA1NiAxOC43MDcxIDE2LjExOTUgMTguNzA2MyAxNS44Mjg0QzE4LjcwNTUgMTUuNTM3NCAxOC42Mjg1IDE1LjI1MTcgMTguNDgzIDE0Ljk5OTdMMTEuNDI0NyAzLjIxNjM1QzExLjI3NjEgMi45NzE0NCAxMS4wNjY5IDIuNzY4OTUgMTAuODE3MyAyLjYyODQyQzEwLjU2NzcgMi40ODc4OSAxMC4yODYxIDIuNDE0MDYgOS45OTk2NSAyLjQxNDA2QzkuNzEzMjEgMi40MTQwNiA5LjQzMTU5IDIuNDg3ODkgOS4xODE5OSAyLjYyODQyQzguOTMyMzggMi43Njg5NSA4LjcyMzIxIDIuOTcxNDQgOC41NzQ2NSAzLjIxNjM1VjMuMjE2MzVaIiBzdHJva2U9IiNFNjUwNTQiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik0xMCA3LjVWMTAuODMzMyIgc3Ryb2tlPSIjRTY1MDU0IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cGF0aCBkPSJNMTAgMTQuMTY4SDEwLjAwODMiIHN0cm9rZT0iI0U2NTA1NCIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg=="},67630:e=>{"use strict";e.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAMAAABiM0N1AAAATlBMVEUAAADz9/f0+/v29/f29vb29/f19vb09vb09PT29/fz9vb2+Pj29vb2+Pj19vb3+fn2+Pj09vb29/fDxMfj5OXW19nx8vPp6uvJys3d3t9on+F5AAAAEnRSTlMAIBDfoM/vkGDeMO+PX+6fj493pkuNAAAA9klEQVRYw+3Yyw6CMBAF0On0QUHxcXmo//+j6koaIbFwE0PSs2RxM52QNjPyofHsA34WvGtVvpkqIJ8zktILVjqm5VisZidFNRYb2IZQT1KTWmxkVd6SPm/ouAHB+3AOBJWIguGgEkERxYHCiQeFlwCKg4CkBJWgXQSNj2HemBl07xbc84Ju3aJbVlDfdf3S978FDf2MIT9oQQn6KWj//1EJ2l0Q4WIjX7UYh3mPcffvWgkqQTloIwRtqOGNWREUrSinSSpSUU5GHNcpJV1JK41aOUuW2nDWPnXDWUS96pnQanWfVVLGId/paOSbts6HjBB/jpNqnnilXjdVFKs9AAAAAElFTkSuQmCC"},2732:e=>{"use strict";e.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANsAAABACAYAAACTIgLAAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAABhuSURBVHgB7V0LfBXFuf9m9uSBhEopWrVUsfahUOm14KNWW23VViABCQG11av21l5vK1ev2oeobARbtC1eab3e2nLr20oSIBDAWgu2vbW+qqKAj+sDreILECGQhJwzc7//2bPJ7uzsnoQkGHD/P4acnZ2Znd2db77nzBKlSJEiRYoUKVKkSJEiRYoUKVKkSJEiRYoUKfYgiODB3FOXlW0uzx6jHHG8lnQQ5USG9mQItUGTfESWqJXu3VUbKEWKPkQHsV1zRuNH27PyJqVoohBhItzjoWm1kPoCt67qfylFij5CnqjcmqX7KVILhKYv0AcX27WQE6+uG/cHSpGiDyDxn9bq4l1FaBr/hH6Myfwh/rmD+g/2klpd705cOJhSpOgDSHA1HvhnBzOF0O1a0Lt5wuhlaMqdU1tXdWRtXeUXRI5O1UK/R/0EfLMjVWnmG5QiRR9Aks6N4lG2n5/Bg7+B2nP7Xl1XOSTTLj/HOW9TL4Fl1gdm1k+8zT92F1atEFr+nvoRpKYTKUWKPgCLkWK/cIbzPXfRaZvx+6rGcU8zgfyWeglK0xwzT5BuT66llZd6gm60oekTtHOASN5d6+2ebe3tf8A7cuh9QoYpQJIMGB9HPrqB6joP+fQzLFZSjyH02u0DylYEs9wzFg9lUjveLMpXa5NC38qEuFg7pWvadyhdInOfISGr+ewEremjVPx6SmjRyJbV27WSq1guzpU4uUO48VO4fW5HfNJWjcXnEuo6vsRpPCfoux8n70Vu5bSW0485PW6UL+N0OidwzyM5fYgT9NbnOf2J0zJOT8VcC/2qKvw18SKnRwPHGFQTCtczARfH/Zb8LxbuwQQmwyWFfvrA/R5EdkBSebfwGwPrVPLusxie5fQkpxqyEwQmy2ZOr3N6xuhPHPCsKsl7T8dy2pe8Z7OpcK3LOO3NaQx1DaCMHO0kIjNrZs1oDOTX/WM2nqwVogueACHWKpWb50hxLBPDaZwhQ+e1ePBnt39tW6hKu1OpSR1otPQCX3PKjLqqJ0WYyl/hdJ9bXX+tkGW/5mt8Jb4r9Bb/OdOtr1xhnEIbK9ya+ddpVX4TF5xCO4ejOF3L6Tiyc6fDODVRmNj+idNdnA4lirhWwE2/zqmW09WcfsapzSgDQrib7MTWyGkSeQPSb29+TN9A2J+x5ON+vmjJb+WEickfE+j7bPIGsA2YRB4r/AbR/IrTMCqOa8gjAKgZ5Qnl0B+8x9s53chpc0y5kzndwOlTFH0OeJajyLuPiZx+QsWBsbiIekBs0szYIcSng8fOpkFPFxPBeHD/Tbwz8HMzGybMoZGPTxEkl0TKkK4LHruuK5VW5wfzmKusEkIez+08IWLYqdsw+SUaWDEO+h/ZsSlL6svsM1sRc57cuimbahuqprI78W7qPs4hjwudQF0XA8G9V5JHhEkzVymnWWQnFIj7cVwXxB+0osZNAsDHYvL3jcnHwD/CKHcYxeMA6lugP5gs8Jye4HS4cR5j+krypAT0s9+I6hFicxR9PnjsPnBiK4tjrye0sV0JMZ3LZfPlXVcxbd5llNlSLgY8HMxoXzN6fx52Afatm7kz57t1496kInBvObGVynacRZ5Y0Qkh0Neps+omPEddQEboy7gP3YkcgQg4j5JnXhNDyZutu+NSgOgz3cjbP6E8CDEoIXw5oexATqYYjgngYwl1jgv8xgDeJ6HscNp1GM5pOXncywfEd0gH/U4fjhAbU8qoSJ7Uz8Y1wBzofjli3J/CrarV/PaCIuPqH9adHDLxO9ncKRR4IJr0dW5d5SNm+3NqHhxw6Vm/H2jmu3dWv8a1bgv1RVO92zA+oo8gDG12zR/2NvOvqKt8nfv5O+oaIP79kizPjIGJBuIZuNffydOLfHHjIrIPwBfIE90WWc5h8F9IYS5xMMUD5X1ug2f6aUrGocYxiG+vhPJB7nEkJeOTVBwvc1ptpKRJ9qeF9KLlHCYJXx2AavHvMW1sJ0+XxjtaQ55uHYdNlv6toR4iMnCE5aUKbwDZoKi9fYbrCkPM3PECy4BvddQX9OdITccJGkZelSLza7PIVTWNJ2/RGx8d2LLjtSsnLzo70i+VuT1wuJkpdoZZxq1uHLVhYPbvrbr1xatqmqZrCKtB5PSd5L2IYric00cs+dAfYPDAgMTLBreGTgAiquA02VIHutzRnH7IifXbPEGaYjOuFdQpixHQsYW/0I8+V6Ss2dZhRcp/ljrv/ZQulC2G88h7XsH0y4TykJS+Xyj3gOX8GYW/15Nd6gCBHVfo21cKf/GOnic7Vlj6h9RKPYBtlh4GThDMYI0txkIm5rOb4Ekzl3WiHTxyOkQ5NvmvDp6fXzPfYZWs4wVz2d+b4qNbs2gE6363wdHMh4Mdcn7Jho0hoQuNOvURJuS85Yv/LmOu9lKojeqmTygpF7FuNpLb+YhQ+vIrpy4Pm/ZHPQ5uup6SAT2l2pLP3JXGckJMZdA6hpeCmRP3aJvp55I3e/rARGMT1WGE8CcH06iRNY6PDZQLSgI2fdvUq2zujuD9HBioMzKhHBBnpewNtJBnDDKBieAYshM6xueZ5Ol3wQltC9mfTZ8hQmzcm33e3it3YDhPvWSpu11Q9hcUj0e8urpd5yhUfy0N3IetiR1ytsyIWyL9UM4NIWc70aCcLj89WAYclZ/WS96N6JsjbQiqZdGyk1ML2kvm2s8Kt+HCB/cqJQOzuU3MmkueaBIHiGumGRtc9ElLXpOlPgwTvtncNGBAXA76KEEEeJ9HG+XupShM66BNXzPFW0wcIMqg7gid+cEutNWbKLXkQWzHpGdjHj+iZBF1l8FGbAMzpEOzlyNLXuGBuiWYx4N4kVs/8cG4hjMi15AvR+I9p6I9LGtLMZzzfbHkNfd340PtuDULj+KKJ5ltOiQj8ZvCE1c3qdaQn4nbaGSOos6MlpdHRPPEJkqGzQ8D0/ydRep9ypKH5/iKJd+mj2CyAbGBux1gKR90LUAagfhoEhuI2OSCJrc1iQ/Sgin6Q5c/1shjS3Ve/yKjH8VM/dCrfh1IEBGTnM0YK4eQJ1Zfbjl/H3muFRN4rzsTWD7K6B/SUdRDRCw2Ij8enZBYsbFZvDukQvMMLo7xcvIBxDOTGv7oh4c9+/qm9Ru5vbdn3HbaO25Au1K53CG+bMTi30qzrtYlZ9jCMpkFfZiihTlbPOUuqdoezs78M0w7FEXEwaqjg9HEhy15MPhsTK5Ggyx5eHZbLPk2f1F5IWGwmf0GQYBrBYkLA8LUv8ABMSEFOY5Z5hDjGPdlvhfoPOZLAUHG+Rlfo3hMNI4f4jQnoTyMWHiWNic9VAD42+6xnIMEsTN+sU9TVK/Fs36EegDbYGQRTodm/18sH9tGWnaIS8zVbnEbqmItlMB3bh7TLrVcwm/nddNnJoU4rLMt8X/R2srqsJZCRxRUFhXLhCXqQgg12tYG96Ut2oawvcQgbLMuiGZnXqQiu66QTbj2oZZ8iEamZIF7PtAoA6unOSnA+lgRODYHFjgCojSCcbGQdkzuAYJ8g6I4hHoXcJ/Y3hG4KlQLqBK2sRzn8H5fYCU2po0jXXdlxsh7wvvFXE3mvf0hXDl5ScRayFT7Bya4iOLP+trhnb9VJOpf28UvGGqihKlpqJAyJJMXDDxDbW3wMI8ODq3tZTvRbMmD7jAguZrVehUXQ2kbTJikMDnYjCzgVjBCBU3YZqjT44U2bAYgX/cC5zRF1PWFeg8F8vCMglEjeG+YgG3cvbcd2zaOgj4iSuQvheN3LWX2o34EK7HxtDusdE1LSHSSWfqLzi8KkHe5dVUhgwKsftzQvB/XzA87Oz80aIGQOmKtZHbWIdPnLGvaWLS0chrhiD9G8kgcZEa4vFzRLNlfaHU8M2cL6SIXXzx/gChuQbMZUGAZ3b9IvXWWPNybTby0xQ+Cw0A8tulAENMwkQXdMqbj/I8J/Rhe+AvCMMfBPwp/l1N8HyFNYJKzGc9sMZZBwJj1o0BCoECSZfC/KMr5QUhBn98Llnrg9N0JQPDxlNE/pKeph7BzNjaStOTCosW2TPk6iZlMiUgcmRbqEv6TaRfl5wXzEemhqWWe5RIdMx8P/mgIkhbrLJ3aQC0qbEiZ2oBolyEs9oba2Hvk2jYmwreiTdDWEiFCVr+K9eWfpeIz8Z8teeBO30uuFjEeABiwtsFoM1uDyCEK2fqHQY4B+BDZAc70t4R++Dqcbfb39a1HKR6ryLOG2iaiYj5BhMnNDqTfUDKxwaF8h5GHsZsfd4XjJyz1MLF9l7qP543+zaZ4n1yXIeNOODIbsvzNrjuJlXoxy104LnTRvNVPiG/it9ay0myHfW4hEexX5z9WwmLkPp0dcKKOYi0ig1srdZ1pBKFc2dfz50iEOAzM+VrrVZE2NN14ed3Yd4J5QjmwsBUL7UFbtgH7LfICiOMAsdcUJSF6mlY9uBVOsNSHmAYx0gw0gDTg30dcDCjErOcCv034xGbzsfkEhHteR3b4UUPgriahFHOSdxeYVG6yXAc65AmF341kJ1gEDBSLetkliCU2HsFHBqMtmFPoGXXj7okWkxfwf3nxggf4aLd6aeJyhTfeXb+/COoVQkWUf61zt4VFQ3Hv0O0lc4NloFOyqHiO1zc12nV16F5kNoeZMEjoK8qkMyfchiulVt+m4oBehOh10xoHIllKnmUWVkFwIBgooNtgwK2jzkEZxGXUOSBBfD+msGHDh+/rMh3aGwN9gZhuMwRgpvddGu9Yzn8ypm2/fSrUf9hyHsT+18Lvdgo76AGoIEmxoAid289Ie1MywGXNZ4lJEpwLoiJE2lst9SCCw5J4KXnEiWthghmf0McSS/+QurD8JR6xxMYWus9fW3N/SI8wrYruxOXD+ZUHI/fLFeX+lZIu6DghLsQE+jX3G8tC16ltqHqAudXVWLbDh7PKhDx7GiyiQazacpIoGFIUiRG0ZlnI6YvIFqnz4VBP8UXmlomW002uRqtHf4knlK7OwiD2VZZ8PMMryFPUIddjkMNKB+LD87LNyHjZDxcS7vFCS7uwBoKQYYgxxc6glRAOXZu+sjamvI8Rhb8240swOPuvlvPrKEzAtmDuJH0WRPG0kS6mZOBZ4h2Y1mSs2/N9YD8n+8QC/RqxlX8rXAt67mKKJ7ZTLP1DKqUeIFZ84js7pDXbhpccu0eIzmSxlCFoQgZBTmXicd07x1r9LKpdfSQ8P4jBoj2HqI4bA23gwdYWUgRuzcoKTVt/7s/t3NwgroK9Q34eLDejofLGYLuR/muazheL5+5hIFQIcY5Yq2V7SZgNh1jyQTCYWcca+TCSxDlKIXpeUrgmOJ75noKDG08BCzbHWK7rA5wHboqgpRIzPgaPSRRtRvv3W+pCdA0aLCCmmhIKiNji1slj7y7mmQCBYMILPjeMpu+Qp1dDtzuzUM5mKfb9loCmeAyw1NfUV5wNzu2cjPeauxMXDudCVZZTFdSWN5hYoYSKRPDrHH3bjMdMgtLbLmbWOyKYl1P64ismLuuyf6e2uukCW5RKESBqA+bmtd2og0HJDvZEY0MQGOyIkvAtgTbjiDmRwUgSHDwQe4O+R+i6JvfBJAlxz1xug3LBFRuIdjF1vr8Yx7aImBHU+4CEYFvoicgSn9gxOWAh7TrqZ0ic1R2pJ8ad0xnnu/BxWc+RPs+tWWIlVBbtIkSlBI3aODB3HXUB7qRFk1lHc818nhw+lslkf9IVoq2d0niikokRC0kAZwPBQU/r6lo4lIMhBUtq2mLKgFhAzDA2XR/It1kuTQugv6zHB0TQoB4H4jFFSRAbxHDTQAV9rSVwDEINThTQ154t0h+iGF9pLwDW5NVGHjj/pYFjSBJf5YTY3RbqJxDupMXnaCl+az0paNuHqGWf/6ibEuqwO6lhmJaleKEVsQ1rvZIOH3SK654Y8o+4NYsma+3U2euIeaSl6y6IiqB5g8iabT9g0ryMy8WKHNznhSzRfd+t+/oL0TbYiLJm6b+w/Dhb20OwQLWra+sqD6euAfd/fCFBZ4SohVAsiDMQZWwBsCg3jrw4RoijGLxwtEP5h05hDg6IPaZ5HpzLdCYfRJ1iDgjEJC6IuKYvD/40GzGvM47xrILPHBbIYBD0YIqK1ngOEF+HU9cA0RnPC2KzyQQwkTQXuR4mMVs0CyYTLIGCz23fQHtw+CN+t4K6vrAXk8pOrxQQbnXjN7WQt8cVYAPG5KsbqhoCx8Kd0tTE8/DYYo1LEtNm1I8PrQxwq5eMZRvn0oRqLLLo+dyn+zOkN7HFcQCLjGzdE5M0qZGRvU2sd0UbWOtbkiXVlMk4b7arXKZEOcfxc6pSWhyVtL26IP2wW191DKVI0cvg8czUmqD2sbECjts8sWEdWm1NE/SxooQGMKH8p1u9TLsNYwMLA9UnikivrKOIi5gzXuQFHvqd64Z+yuItlz7XIXmuzmq+SQnRNl+/6N5FWj5OKVL0ATJUxub1dv1C3NZujBNqJzdd0irkbWtV9lta6Gu6YZSRWuRmcv2ytpLyO2Rr6xAW5C6BGbC/QoncSkqRog/gfVhjytJpSqk5ImlNkWY9QCTuU1HsQttYKStlKbQ7+zLuYuj7hGitNqNeUqToDeTluRF6240saP0msWQPCA3AotR+Tmhvi1J5QUpoKfoKHfIgQpfoqdGXa6nZUS165CnfrSDgeWALZrb039yFX+u17xqk6Dnw0RetdFc2UO2EUJtr66uKRaO8L4goX+4Zdw1V7RUI3BzlaLlL9t5j6+ShbMDAIsD462lazdbNrm471yXkhF4vs5mV7qJT11GKfgf39IXDdTbzcrcqabGutmH8wdQPERnc7t1nwgexnKJrmfoM4Kpi9ZibmMXMYivkidZCggZrsf2n2LmLUqTYDdEvdo31driiB9npfJJ6eslUdjcggNjcLHaYyg3AHov3UIoPBnKDN0jaGgpsVySPYvblr5vEJjNX8HHnqgNB/Vbn7lFgZV8hvwqgLTeN/XQXBXbhykelzGio/CqW+1CKDyTcyU2ns8rhf6MhK5RzsC3iCJh++qKPc4kvOeQg6DpLMrta7tAP+59EA649r3FQy3vCW9uXySoa8N6r7i3ndqxBdGvmV5Aq71wpP2jjuuD57qCrEe+7FO6dY7e49ZWz+MGMEvltxET+5pQQx0yftOQISpEiAUyQx15Vs+SBTNZ5NUPyDkF6NqefMWHeqzKZl66qabrAX6vZsrlkbxLiMS3EMzpX8pzYus+5He3kP/4yYG7+HBKJebRt4E5Lg/2S2Hy4iyesZ6I7X2h1BD+ZJn5gZSVCpJ/hTRELd9KyYUxIK1n2+TKO+TdiTTf7i5EF4jy1uqG2Zml+VwFwRZaYOhYQK6Guw546+YOnj5zM5X3i28zj71s9cQ31a2LzgW3zmOgqBT6sJ+gAc7FpihQdGPXIelYzFvA4eVNoeYnOiS9kHGcMD/TxQniLf/l8CWk9zV/dv1m2LuLjwpeXRIUWep5b1XiAFqpjFzlWaaYX276xGHYLYvPh1lfd98aQ/c9es6N5G6VIYQGMbe2Z7PdLcvIYt2HcnJkLK1ddec/YF2fUVy0n6e2VA7DSf3D5mvvzu5xdXzelRej2H7ApwF+3d4IuldiZrBDCKO51Plv539RD7HbfdL755jHtlCJFAkraBrRlMzsunVG9+OhC5NOrWjr12W3qMadjnbaWrdnmzqCOBdWvudWNP2C7wM3CW6XtLUbV9Jqg1u9Gv9TUfexWnC1FimLI62zOjke1psvY8IGlWWOwPEsodZcsT3YbuQ0T7pCRz5uJ/8l/7bYXkBJbij0Kysl17FSGbwSygeSHQuivspHtQtbZBibVZcPISVrrk0OZQp/nTl1abB/MLmG3EyNTpIjDhacuK5OUG+U7YR0lL5uxYJy/MHqFW9N4ryZp3YTIPWflYLW1ea4oLE5mUfIV7a1+H0a53Cwmwqlx33nvKlLOlmKPQaaiWWod2PhIGF+UdUrjv1bU3PxT5nzetoaaHso5NIGP/+EdipramqazqIdIiS3FHoO8VTGwHXtO5H5RO2Vx9dU1C0fjM9Eql6231fO2BilYKtkawtztGzPvqVyVVXoaFbbsYwq+Ph+R0gOkxJZiz4KQtWwYWZf/SWJ/pUR9Tmcek+TcKs3vqRNWuawcqoW8hq2TeTsli4oz/c9FZw5/fDE7shcWig5xss6d5teduoNUZ0uxe0G0PytUyS3eb63YSB/6sCS+ze6etuBocpzvMS/BFuOlXHC9IrVENGduFXupG/IFHbGZBm1sVVtLT5MkCt9L0G/SiHGzO9pin51bs3SaUBorYQYIXG/1e9ii8UFKkSJFihQpUqRIkSJFihQpUqRIkSJFihQpUnzQ8f8TN8TrgNW1mAAAAABJRU5ErkJggg=="},5196:e=>{"use strict";e.exports="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDUzIiBoZWlnaHQ9IjczIiB2aWV3Qm94PSIwIDAgNDUzIDczIiBmaWxsPSJub25lIgogICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxyZWN0IHdpZHRoPSIzLjg4NTYxIiBoZWlnaHQ9IjEyLjIxMTkiIHJ4PSIxLjk0MjgxIiB0cmFuc2Zvcm09Im1hdHJpeCgtMC44MjU5NDMgMC41NjM3NTQgMC41NjM3NTUgMC44MjU5NDIgNDA0LjA0NyA0OC41OTQpIiBmaWxsPSIjNjRDQTQzIi8+CiAgICA8cmVjdCB3aWR0aD0iMy4wNTI2NCIgaGVpZ2h0PSI5LjU5NDAxIiByeD0iMS41MjYzMiIgdHJhbnNmb3JtPSJtYXRyaXgoLTAuNjcyMTc3IDAuNzQwMzkxIDAuNzQwMzkxIDAuNjcyMTc2IDQxNy45NyAxMy40OTk4KSIgZmlsbD0iI0ZGMkQ1NSIvPgogICAgPHJlY3Qgd2lkdGg9IjMuNjQyNzYiIGhlaWdodD0iMTEuNDQ4NyIgcng9IjEuODIxMzgiIHRyYW5zZm9ybT0ibWF0cml4KC0wLjYzODIgLTAuNzY5ODcxIC0wLjc2OTg3MiAwLjYzODE5OCAzOTEuNzk1IDI0Ljk1MDQpIiBmaWxsPSIjMTE3QUM5Ii8+CiAgICA8cmVjdCB3aWR0aD0iMy44ODU2MSIgaGVpZ2h0PSIxMi4yMTE5IiByeD0iMS45NDI4IiB0cmFuc2Zvcm09Im1hdHJpeCgtMC40MDQzNzIgLTAuOTE0NTk1IC0wLjkxNDU5NSAwLjQwNDM3MSAxNzAuOTQgMTQuNjkxNCkiIGZpbGw9IiNGRjgwODUiLz4KICAgIDxyZWN0IHdpZHRoPSI1LjM0MjcxIiBoZWlnaHQ9IjE2Ljc5MTQiIHJ4PSIyLjY3MTM2IiB0cmFuc2Zvcm09Im1hdHJpeCgwLjM5MjY0IDAuOTE5NjkyIDAuOTE5NjkyIC0wLjM5MjY0MiAzMjguMTE5IDUwLjYwNTUpIiBmaWxsPSIjRkY4MDg1Ii8+CiAgICA8Y2lyY2xlIHI9IjMuNDM0MjIiIHRyYW5zZm9ybT0ibWF0cml4KC0wLjk0OTE5MyAtMC4zMTQ2OTQgLTAuMzE0Njk0IDAuOTQ5MTkzIDQzMy42OTQgNjAuNTQ0KSIgZmlsbD0iI0YwQjg0OSIvPgogICAgPGVsbGlwc2Ugcng9IjIuMjg5NDgiIHJ5PSIyLjI4OTQ4IiB0cmFuc2Zvcm09Im1hdHJpeCgtMC45NDkxOTMgLTAuMzE0Njk1IC0wLjMxNDY5MyAwLjk0OTE5NCA0NDkuOTkzIDQ0LjAwMDgpIiBmaWxsPSIjQkY1QUYyIi8+CiAgICA8ZWxsaXBzZSByeD0iMS41MjYzMiIgcnk9IjEuNTI2MzIiIHRyYW5zZm9ybT0ibWF0cml4KC0wLjk0OTE5NCAtMC4zMTQ2OTIgLTAuMzE0Njk1IDAuOTQ5MTkzIDM3My4zMzkgNjMuMzEpIiBmaWxsPSIjQkY1QUYyIi8+CiAgICA8ZWxsaXBzZSByeD0iMi4yODk0OCIgcnk9IjIuMjg5NDgiIHRyYW5zZm9ybT0ibWF0cml4KC0wLjk0OTE5NCAtMC4zMTQ2OTIgLTAuMzE0Njk1IDAuOTQ5MTkzIDE2MC43MTMgNTQuMDk3KSIgZmlsbD0iIzA5QjU4NSIvPgogICAgPHJlY3QgeD0iMzE0LjI3MyIgeT0iMTcuMjE5MiIgd2lkdGg9IjUuMzQyNzEiIGhlaWdodD0iMTYuNzkxNCIgcng9IjIuNjcxMzYiIHRyYW5zZm9ybT0icm90YXRlKC01MS43OTU4IDMxNC4yNzMgMTcuMjE5MikiIGZpbGw9IiM5ODRBOUMiLz4KICAgIDxyZWN0IHdpZHRoPSIzLjg4NTYxIiBoZWlnaHQ9IjEyLjIxMTkiIHJ4PSIxLjk0MjgiIHRyYW5zZm9ybT0ibWF0cml4KDAuNjE4NDY1IC0wLjc4NTgxMiAwLjc4NTgxIDAuNjE4NDY3IDI3LjA2MSAzNC43NDEpIiBmaWxsPSIjNjRDQTQzIi8+CiAgICA8cmVjdCB3aWR0aD0iMy42NDI3NiIgaGVpZ2h0PSIxMS40NDg3IiByeD0iMS44MjEzOCIgdHJhbnNmb3JtPSJtYXRyaXgoLTAuOTg4ODgxIC0wLjE0ODcxMSAwLjE0ODcxNCAtMC45ODg4OCAyNjcuNjAyIDI3Ljg2MykiIGZpbGw9IiNFN0MwMzciLz4KICAgIDxyZWN0IHdpZHRoPSIzLjAwNjgyIiBoZWlnaHQ9IjkuNDUiIHJ4PSIxLjUwMzQxIiB0cmFuc2Zvcm09Im1hdHJpeCgwLjIyNjk3MSAwLjk3MzkwMiAtMC45NzM5MDIgMC4yMjY5NjggMjEyLjIwNCA1MSkiIGZpbGw9IiNFN0MwMzciLz4KICAgIDxyZWN0IHdpZHRoPSIzLjg4NTYxIiBoZWlnaHQ9IjEyLjIxMTkiIHJ4PSIxLjk0MjgiIHRyYW5zZm9ybT0ibWF0cml4KDAuNzg1ODEgMC42MTg0NjggLTAuNjE4NDY1IDAuNzg1ODEyIDI2OS4zOTYgNTYuODc4OSkiIGZpbGw9IiMzMzYxQ0MiLz4KICAgIDxjaXJjbGUgY3g9IjkwLjUyNyIgY3k9IjQ1LjY5MjYiIHI9IjMuNDM0MjIiIHRyYW5zZm9ybT0icm90YXRlKC0xLjc5NTc4IDkwLjUyNyA0NS42OTI2KSIgZmlsbD0iI0YwQjg0OSIvPgogICAgPGNpcmNsZSBjeD0iNTkuODU5NiIgY3k9IjI3LjExNTgiIHI9IjIuMjg5NDgiIHRyYW5zZm9ybT0icm90YXRlKC0xLjc5NTc2IDU5Ljg1OTYgMjcuMTE1OCkiIGZpbGw9IiNCRjVBRjIiLz4KICAgIDxjaXJjbGUgY3g9IjMwNy4xMDkiIGN5PSI2MC43NjYzIiByPSIxLjUyNjMyIiB0cmFuc2Zvcm09InJvdGF0ZSgtMS43OTU3NCAzMDcuMTA5IDYwLjc2NjMpIiBmaWxsPSIjRjBDOTMwIi8+CiAgICA8Y2lyY2xlIGN4PSIzNTcuMzExIiBjeT0iMjguNTQ0NCIgcj0iMS41MjYzMiIgdHJhbnNmb3JtPSJyb3RhdGUoLTEuNzk1NzQgMzU3LjMxMSAyOC41NDQ0KSIgZmlsbD0iI0YwQzkzMCIvPgogICAgPGVsbGlwc2UgY3g9IjIzNy4yNDgiIGN5PSI0Ny4zNjc0IiByeD0iMS41MjYzMiIgcnk9IjEuNTI2MzIiIHRyYW5zZm9ybT0icm90YXRlKC0xLjc5NTc4IDIzNy4yNDggNDcuMzY3NCkiIGZpbGw9IiMzMzYxQ0MiLz4KICAgIDxjaXJjbGUgY3g9IjI5MC44NjkiIGN5PSIzOS45MzI5IiByPSIxLjkwNzkiIHRyYW5zZm9ybT0icm90YXRlKC0xLjc5NTc3IDI5MC44NjkgMzkuOTMyOSkiIGZpbGw9IiMzN0U2ODgiLz4KICAgIDxyZWN0IHdpZHRoPSIzLjg4NTYxIiBoZWlnaHQ9IjEyLjIxMTkiIHJ4PSIxLjk0MjgiIHRyYW5zZm9ybT0ibWF0cml4KDAuMzM2NzM1IC0wLjk0MTU5OSAwLjk0MTU5OSAwLjMzNjczNyAxMDguNjg0IDYwLjc1MSkiIGZpbGw9IiM2NENBNDMiLz4KICAgIDxyZWN0IHg9IjEzMS4yNTIiIHk9IjI1LjEyODIiIHdpZHRoPSIzLjg4NTYxIiBoZWlnaHQ9IjEyLjIxMTkiIHJ4PSIxLjk0MjgiIHRyYW5zZm9ybT0icm90YXRlKDUuODE4NjkgMTMxLjI1MiAyNS4xMjgyKSIgZmlsbD0iIzMzNjFDQyIvPgogICAgPGVsbGlwc2Ugcng9IjMuNDM0MjIiIHJ5PSIzLjQzNDIyIiB0cmFuc2Zvcm09Im1hdHJpeCgwLjgyNzI2MiAtMC41NjE4MTYgMC41NjE4MTEgMC44MjcyNjYgMjEuNDU2OSA2Ny43NzUxKSIgZmlsbD0iI0YwQjg0OSIvPgogICAgPGNpcmNsZSBjeD0iMTk1LjgxOSIgY3k9IjMzLjE2NTQiIHI9IjIuMjg5NDgiIHRyYW5zZm9ybT0icm90YXRlKC0zNC4xODEzIDE5NS44MTkgMzMuMTY1NCkiIGZpbGw9IiNCRjVBRjIiLz4KICAgIDxjaXJjbGUgcj0iMS41MjYzMiIgdHJhbnNmb3JtPSJtYXRyaXgoMC44MjcyNjYgLTAuNTYxODEgMC41NjE4MTggMC44MjcyNjEgNjQuMjU0IDY1Ljk3NDUpIiBmaWxsPSIjMzM2MUNDIi8+CiAgICA8ZWxsaXBzZSByeD0iMS45MDc5IiByeT0iMS45MDc5IiB0cmFuc2Zvcm09Im1hdHJpeCgwLjgyNzI2NSAtMC41NjE4MTIgMC41NjE4MTUgMC44MjcyNjMgMi41ODcyNCA0OC4zMDMxKSIgZmlsbD0iIzM3RTY4OCIvPgogICAgPGVsbGlwc2Ugcng9IjEuOTA3OSIgcnk9IjEuOTA3OSIgdHJhbnNmb3JtPSJtYXRyaXgoMC44MjcyNjUgLTAuNTYxODEyIDAuNTYxODE1IDAuODI3MjYzIDI3Ljk3NjkgMTUuNjQ5MykiIGZpbGw9IiNGMEM5MzAiLz4KICAgIDxlbGxpcHNlIGN4PSIyMzEuMzY3IiBjeT0iMjEuMzM2IiByeD0iMi4yODk0OCIgcnk9IjIuMjg5NDgiIHRyYW5zZm9ybT0icm90YXRlKC0zNC4xODEzIDIzMS4zNjcgMjEuMzM2KSIgZmlsbD0iIzA5QjU4NSIvPgogICAgPGVsbGlwc2Ugcng9IjIuMjg5NDgiIHJ5PSIyLjI4OTQ4IiB0cmFuc2Zvcm09Im1hdHJpeCgwLjgyNzI2NyAtMC41NjE4MDkgMC41NjE4MTkgMC44MjcyNiAxMDAuMTY0IDE1LjQyNzEpIiBmaWxsPSIjRkYzQjMwIi8+Cjwvc3ZnPgo="},21079:(e,t,n)=>{"use strict";e.exports=n.p+"dbfe730286a89feb7ce0.svg"},26016:e=>{"use strict";e.exports="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPG1hc2sgaWQ9Im1hc2swIiBzdHlsZT0ibWFzay10eXBlOmFscGhhIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4PSIyIiB5PSIxIiB3aWR0aD0iMTIiIGhlaWdodD0iMTQiPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTYgMS4wMDI5M0gxMFYyLjMzNjI2SDZWMS4wMDI5M1pNNy4zMzMzMyA5LjY2OTZWNS42Njk2SDguNjY2NjdWOS42Njk2SDcuMzMzMzNaTTEyLjY4NjcgNS4yNTYyNkwxMy42MzMzIDQuMzA5NkMxMy4zNDY3IDMuOTY5NiAxMy4wMzMzIDMuNjQ5NiAxMi42OTMzIDMuMzY5NkwxMS43NDY3IDQuMzE2MjZDMTAuNzEzMyAzLjQ4OTYgOS40MTMzMyAyLjk5NjI2IDggMi45OTYyNkM0LjY4NjY3IDIuOTk2MjYgMiA1LjY4MjkzIDIgOC45OTYyNkMyIDEyLjMwOTYgNC42OCAxNC45OTYzIDggMTQuOTk2M0MxMS4zMiAxNC45OTYzIDE0IDEyLjMwOTYgMTQgOC45OTYyNkMxNCA3LjU4OTYgMTMuNTA2NyA2LjI4OTYgMTIuNjg2NyA1LjI1NjI2Wk0zLjMzMzMzIDkuMDAyOTNDMy4zMzMzMyAxMS41ODI5IDUuNDIgMTMuNjY5NiA4IDEzLjY2OTZDMTAuNTggMTMuNjY5NiAxMi42NjY3IDExLjU4MjkgMTIuNjY2NyA5LjAwMjkzQzEyLjY2NjcgNi40MjI5MyAxMC41OCA0LjMzNjI2IDggNC4zMzYyNkM1LjQyIDQuMzM2MjYgMy4zMzMzMyA2LjQyMjkzIDMuMzMzMzMgOS4wMDI5M1oiIGZpbGw9IndoaXRlIi8+CjwvbWFzaz4KPGcgbWFzaz0idXJsKCNtYXNrMCkiPgo8cmVjdCB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIGZpbGw9IiM3ODdDODIiLz4KPC9nPgo8L3N2Zz4K"},99196:e=>{"use strict";e.exports=window.React},91850:e=>{"use strict";e.exports=window.ReactDOM},92819:e=>{"use strict";e.exports=window.lodash},76292:e=>{"use strict";e.exports=window.moment},46530:e=>{"use strict";e.exports=window.wc.adminLayout},86020:e=>{"use strict";e.exports=window.wc.components},49742:e=>{"use strict";e.exports=window.wc.csvExport},17844:e=>{"use strict";e.exports=window.wc.currency},75606:e=>{"use strict";e.exports=window.wc.customerEffortScore},67221:e=>{"use strict";e.exports=window.wc.data},81921:e=>{"use strict";e.exports=window.wc.date},14812:e=>{"use strict";e.exports=window.wc.experimental},73516:e=>{"use strict";e.exports=window.wc.explat},10431:e=>{"use strict";e.exports=window.wc.navigation},81595:e=>{"use strict";e.exports=window.wc.number},51393:e=>{"use strict";e.exports=window.wc.productEditor},14599:e=>{"use strict";e.exports=window.wc.tracks},74617:e=>{"use strict";e.exports=window.wc.wcSettings},25158:e=>{"use strict";e.exports=window.wp.a11y},86989:e=>{"use strict";e.exports=window.wp.apiFetch},11313:e=>{"use strict";e.exports=window.wp.blob},52175:e=>{"use strict";e.exports=window.wp.blockEditor},97800:e=>{"use strict";e.exports=window.wp.blockLibrary},4981:e=>{"use strict";e.exports=window.wp.blocks},55609:e=>{"use strict";e.exports=window.wp.components},94333:e=>{"use strict";e.exports=window.wp.compose},37798:e=>{"use strict";e.exports=window.wp.coreData},9818:e=>{"use strict";e.exports=window.wp.data},23418:e=>{"use strict";e.exports=window.wp.dataControls},69771:e=>{"use strict";e.exports=window.wp.date},37180:e=>{"use strict";e.exports=window.wp.deprecated},45904:e=>{"use strict";e.exports=window.wp.dom},12238:e=>{"use strict";e.exports=window.wp.editor},69307:e=>{"use strict";e.exports=window.wp.element},92694:e=>{"use strict";e.exports=window.wp.hooks},22629:e=>{"use strict";e.exports=window.wp.htmlEntities},65736:e=>{"use strict";e.exports=window.wp.i18n},24705:e=>{"use strict";e.exports=window.wp.keyboardShortcuts},39630:e=>{"use strict";e.exports=window.wp.keycodes},58141:e=>{"use strict";e.exports=window.wp.mediaUtils},53961:e=>{"use strict";e.exports=window.wp.notices},98817:e=>{"use strict";e.exports=window.wp.plugins},55815:e=>{"use strict";e.exports=window.wp.preferences},70444:e=>{"use strict";e.exports=window.wp.primitives},74776:e=>{"use strict";e.exports=window.wp.privateApis},86802:e=>{"use strict";e.exports=window.wp.router},96483:e=>{"use strict";e.exports=window.wp.url},90162:e=>{"use strict";e.exports=window.wp.viewport},12560:e=>{"use strict";e.exports=window.wp.warning},50189:()=>{},5267:(e,t,n)=>{"use strict";function o(){return o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var o in n)Object.prototype.hasOwnProperty.call(n,o)&&(e[o]=n[o])}return e},o.apply(this,arguments)}n.d(t,{Z:()=>o})}},a={};function i(e){var t=a[e];if(void 0!==t)return t.exports;var n=a[e]={id:e,loaded:!1,exports:{}};return r[e].call(n.exports,n,n.exports,i),n.loaded=!0,n.exports}i.m=r,i.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return i.d(t,{a:t}),t},i.d=(e,t)=>{for(var n in t)i.o(t,n)&&!i.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},i.f={},i.e=e=>Promise.all(Object.keys(i.f).reduce(((t,n)=>(i.f[n](e,t),t)),[])),i.u=e=>"chunks/"+({185:"analytics-report-categories",727:"leaderboards",925:"customize-store",1828:"core-profiler",2397:"dashboard-charts",2502:"analytics-report-stock",2624:"store-performance",2953:"analytics-settings",3307:"edit-product-page",3576:"analytics-report-orders",3700:"analytics-report-customers",3994:"analytics-report-revenue",4011:"analytics-report-coupons",4854:"analytics-report-downloads",4882:"multichannel-marketing",5396:"payment-recommendations",5502:"homescreen",6125:"shipping-recommendations",6732:"activity-panels-help",6824:"analytics-report-products",7387:"product-page",7708:"profile-wizard",7844:"activity-panels-setup",8544:"analytics-report",8597:"store-alerts",8851:"activity-panels-inbox",8960:"customizable-dashboard",9360:"wcpay-payment-welcome-page",9456:"analytics-report-variations",9669:"marketplace",9792:"analytics-report-taxes",9966:"dashboard"}[e]||e)+".js",i.miniCssF=e=>"chunks/"+e+".style.css",i.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),i.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),e={},t="__wcAdmin_webpackJsonp:",i.l=(n,o,r,a)=>{if(e[n])e[n].push(o);else{var s,c;if(void 0!==r)for(var l=document.getElementsByTagName("script"),m=0;m<l.length;m++){var u=l[m];if(u.getAttribute("src")==n||u.getAttribute("data-webpack")==t+r){s=u;break}}s||(c=!0,(s=document.createElement("script")).charset="utf-8",s.timeout=120,i.nc&&s.setAttribute("nonce",i.nc),s.setAttribute("data-webpack",t+r),s.src=n),e[n]=[o];var d=(t,o)=>{s.onerror=s.onload=null,clearTimeout(p);var r=e[n];if(delete e[n],s.parentNode&&s.parentNode.removeChild(s),r&&r.forEach((e=>e(o))),t)return t(o)},p=setTimeout(d.bind(null,void 0,{type:"timeout",target:s}),12e4);s.onerror=d.bind(null,s.onerror),s.onload=d.bind(null,s.onload),c&&document.head.appendChild(s)}},i.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),(()=>{var e;i.g.importScripts&&(e=i.g.location+"");var t=i.g.document;if(!e&&t&&(t.currentScript&&(e=t.currentScript.src),!e)){var n=t.getElementsByTagName("script");n.length&&(e=n[n.length-1].src)}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),i.p=e+"../"})(),n=e=>new Promise(((t,n)=>{var o=i.miniCssF(e),r=i.p+o;if(((e,t)=>{for(var n=document.getElementsByTagName("link"),o=0;o<n.length;o++){var r=(i=n[o]).getAttribute("data-href")||i.getAttribute("href");if("stylesheet"===i.rel&&(r===e||r===t))return i}var a=document.getElementsByTagName("style");for(o=0;o<a.length;o++){var i;if((r=(i=a[o]).getAttribute("data-href"))===e||r===t)return i}})(o,r))return t();((e,t,n,o)=>{var r=document.createElement("link");r.rel="stylesheet",r.type="text/css",r.onerror=r.onload=a=>{if(r.onerror=r.onload=null,"load"===a.type)n();else{var i=a&&("load"===a.type?"missing":a.type),s=a&&a.target&&a.target.href||t,c=new Error("Loading CSS chunk "+e+" failed.\n("+s+")");c.code="CSS_CHUNK_LOAD_FAILED",c.type=i,c.request=s,r.parentNode.removeChild(r),o(c)}},r.href=t,document.head.appendChild(r)})(e,r,t,n)})),o={2143:0,2343:0},i.f.miniCss=(e,t)=>{o[e]?t.push(o[e]):0!==o[e]&&{185:1,727:1,925:1,1828:1,2397:1,2502:1,2624:1,2953:1,3307:1,3576:1,3700:1,3994:1,4011:1,4854:1,4882:1,4891:1,5009:1,5502:1,5792:1,6125:1,6732:1,6824:1,7387:1,7708:1,8544:1,8597:1,8851:1,9360:1,9456:1,9669:1,9792:1,9966:1}[e]&&t.push(o[e]=n(e).then((()=>{o[e]=0}),(t=>{throw delete o[e],t})))},(()=>{var e={2143:0,2343:0};i.f.j=(t,n)=>{var o=i.o(e,t)?e[t]:void 0;if(0!==o)if(o)n.push(o[2]);else if(5009!=t){var r=new Promise(((n,r)=>o=e[t]=[n,r]));n.push(o[2]=r);var a=i.p+i.u(t),s=new Error;i.l(a,(n=>{if(i.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var r=n&&("load"===n.type?"missing":n.type),a=n&&n.target&&n.target.src;s.message="Loading chunk "+t+" failed.\n("+r+": "+a+")",s.name="ChunkLoadError",s.type=r,s.request=a,o[1](s)}}),"chunk-"+t,t)}else e[t]=0};var t=(t,n)=>{var o,r,[a,s,c]=n,l=0;if(a.some((t=>0!==e[t]))){for(o in s)i.o(s,o)&&(i.m[o]=s[o]);c&&c(i)}for(t&&t(n);l<a.length;l++)r=a[l],i.o(e,r)&&e[r]&&e[r][0](),e[r]=0},n=globalThis.webpackChunk_wcAdmin_webpackJsonp=globalThis.webpackChunk_wcAdmin_webpackJsonp||[];n.forEach(t.bind(null,0)),n.push=t.bind(null,n.push.bind(n))})();var s={};(()=>{"use strict";i.r(s);var e=i(69307),t=i(53961),n=i(75606),o=i(67221),r=i(73463),a=i(55609),c=i(94333),l=i(9818),m=i(67027),u=i(85597),d=i(99196),p=i(7862),C=i.n(p),h=i(92819),g=i(10431),f=i(14599);window.wc.notices;var y=i(98817),w=i(46530),H=i(79119),_=i(92694),v=i(65736),E=i(86020),L=i(18210);function b(e){const t=function(e){const t=(0,r.O3)("features");return t&&t[e]}(e);return Boolean(null==t?void 0:t.is_enabled)}var k=i(74816);const S=(0,e.lazy)((()=>Promise.all([i.e(3727),i.e(5009),i.e(3307)]).then(i.bind(i,33284)))),M=(0,e.lazy)((()=>Promise.all([i.e(3727),i.e(5009),i.e(3307)]).then(i.bind(i,54159)))),N=(0,e.lazy)((()=>Promise.all([i.e(5009),i.e(7387)]).then(i.bind(i,30985)))),x=(0,e.lazy)((()=>i.e(8544).then(i.bind(i,13369)))),T=(0,e.lazy)((()=>i.e(2953).then(i.bind(i,68266)))),I=(0,e.lazy)((()=>i.e(9966).then(i.bind(i,55144)))),A=(0,e.lazy)((()=>Promise.all([i.e(7846),i.e(9529),i.e(5502)]).then(i.bind(i,60108)))),O=(0,e.lazy)((()=>i.e(4882).then(i.bind(i,32901)))),P=(0,e.lazy)((()=>i.e(9669).then(i.bind(i,67049)))),V=(0,e.lazy)((()=>i.e(7708).then(i.bind(i,22173)))),R=(0,e.lazy)((()=>Promise.all([i.e(4284),i.e(1828)]).then(i.bind(i,77405)))),j=(0,e.lazy)((()=>i.e(7708).then(i.bind(i,38447)))),D=(0,e.lazy)((()=>i.e(9360).then(i.bind(i,10990)))),Z=(0,e.lazy)((()=>Promise.all([i.e(4284),i.e(1061),i.e(925)]).then(i.bind(i,16801)))),B=()=>{const e=[],t=[["",(0,r.O3)("woocommerceTranslation")]];if(e.push({container:A,path:"/",breadcrumbs:[...t,(0,v.__)("Home","woocommerce")],wpOpenMenu:"toplevel_page_woocommerce",navArgs:{id:"woocommerce-home"},capability:"manage_woocommerce"}),window.wcAdminFeatures.analytics&&(e.push({container:I,path:"/analytics/overview",breadcrumbs:[...t,["/analytics/overview",(0,v.__)("Analytics","woocommerce")],(0,v.__)("Overview","woocommerce")],wpOpenMenu:"toplevel_page_wc-admin-path--analytics-overview",navArgs:{id:"woocommerce-analytics-overview"},capability:"view_woocommerce_reports"}),e.push({container:T,path:"/analytics/settings",breadcrumbs:[...t,["/analytics/revenue",(0,v.__)("Analytics","woocommerce")],(0,v.__)("Settings","woocommerce")],wpOpenMenu:"toplevel_page_wc-admin-path--analytics-overview",navArgs:{id:"woocommerce-analytics-settings"},capability:"view_woocommerce_reports"}),e.push({container:x,path:"/customers",breadcrumbs:[...t,(0,v.__)("Customers","woocommerce")],wpOpenMenu:"toplevel_page_woocommerce",navArgs:{id:"woocommerce-analytics-customers"},capability:"view_woocommerce_reports"}),e.push({container:x,path:"/analytics/:report",breadcrumbs:e=>{let{match:n}=e;const o=(0,h.find)((0,L.Z)(),{report:n.params.report});return o?[...t,["/analytics/revenue",(0,v.__)("Analytics","woocommerce")],o.title]:[]},wpOpenMenu:"toplevel_page_wc-admin-path--analytics-overview",capability:"view_woocommerce_reports"})),window.wcAdminFeatures.marketing&&e.push({container:O,path:"/marketing",breadcrumbs:[...t,["/marketing",(0,v.__)("Marketing","woocommerce")],(0,v.__)("Overview","woocommerce")],wpOpenMenu:"toplevel_page_woocommerce-marketing",navArgs:{id:"woocommerce-marketing-overview"},capability:"view_woocommerce_reports"}),b("marketplace")&&e.push({container:P,layout:{header:!1},path:"/extensions",breadcrumbs:[["/extensions",(0,v.__)("Extensions","woocommerce")],(0,v.__)("Extensions","woocommerce")],wpOpenMenu:"toplevel_page_woocommerce",capability:"manage_woocommerce",navArgs:{id:"woocommerce-marketplace"}}),b("product_block_editor")){const t={container:N,layout:{header:!1},wpOpenMenu:"menu-posts-product",capability:"manage_woocommerce"};e.push({...t,path:"/add-product",breadcrumbs:[["/add-product",(0,v.__)("Product","woocommerce")],(0,v.__)("Add New Product","woocommerce")],navArgs:{id:"woocommerce-add-product"}}),e.push({...t,path:"/product/:productId",breadcrumbs:[["/edit-product",(0,v.__)("Product","woocommerce")],(0,v.__)("Edit Product","woocommerce")],navArgs:{id:"woocommerce-edit-product"}})}else window.wcAdminFeatures["new-product-management-experience"]&&(e.push({container:S,path:"/add-product",breadcrumbs:[["/add-product",(0,v.__)("Product","woocommerce")],(0,v.__)("Add New Product","woocommerce")],navArgs:{id:"woocommerce-add-product"},wpOpenMenu:"menu-posts-product",capability:"manage_woocommerce"}),e.push({container:M,path:"/product/:productId",breadcrumbs:[["/edit-product",(0,v.__)("Product","woocommerce")],(0,v.__)("Edit Product","woocommerce")],navArgs:{id:"woocommerce-edit-product"},wpOpenMenu:"menu-posts-product",capability:"manage_woocommerce"}));window.wcAdminFeatures["product-variation-management"]&&e.push({container:M,path:"/product/:productId/variation/:variationId",breadcrumbs:[["/edit-product",(0,v.__)("Product","woocommerce")],(0,v.__)("Edit Product Variation","woocommerce")],navArgs:{id:"woocommerce-edit-product"},wpOpenMenu:"menu-posts-product",capability:"edit_products"}),window.wcAdminFeatures.onboarding&&(window.wcAdminFeatures["core-profiler"]?e.push({container:R,path:"/setup-wizard",breadcrumbs:[...t,(0,v.__)("Profiler","woocommerce")],capability:"manage_woocommerce"}):e.push({container:V,path:"/setup-wizard",breadcrumbs:[...t,(0,v.__)("Setup Wizard","woocommerce")],capability:"manage_woocommerce"})),window.wcAdminFeatures["core-profiler"]&&e.push({container:R,path:"/profiler",breadcrumbs:[...t,(0,v.__)("Profiler","woocommerce")],capability:"manage_woocommerce"}),window.wcAdminFeatures["customize-store"]&&e.push({container:Z,path:"/customize-store/*",breadcrumbs:[...t,(0,v.__)("Customize Your Store","woocommerce")],capability:"manage_woocommerce"}),window.wcAdminFeatures.settings&&e.push({container:j,path:"/settings/:page",breadcrumbs:e=>{let{match:n}=e;const o=(0,r.O3)("settingsPages"),a=o[n.params.page];return a?[...t,[o.general?"/settings/general":`/settings/${Object.keys(o)[0]}`,(0,v.__)("Settings","woocommerce")],a]:[]},wpOpenMenu:"toplevel_page_woocommerce",capability:"manage_woocommerce"}),window.wcAdminFeatures["wc-pay-welcome-page"]&&e.push({container:D,path:"/wc-pay-welcome-page",breadcrumbs:[["/wc-pay-welcome-page",(0,v.__)("WooPayments","woocommerce")],(0,v.__)("WooPayments","woocommerce")],navArgs:{id:"woocommerce-wc-pay-welcome-page"},wpOpenMenu:"toplevel_page_woocommerce-wc-pay-welcome-page",capability:"manage_woocommerce"});const n=(0,_.applyFilters)("woocommerce_admin_pages_list",e);return n.push({container:k.E,path:"*",breadcrumbs:[...t,(0,v.__)("Not allowed","woocommerce")],wpOpenMenu:"toplevel_page_woocommerce"}),n},F=t=>{let{...n}=t;const o=function(e){const t=(0,d.useRef)();return(0,d.useEffect)((()=>{t.current=e}),[e]),t.current}(n);(0,d.useEffect)((()=>{window.document.documentElement.scrollTop=0,window.document.body.classList.remove("woocommerce-admin-is-loading")}),[]),(0,d.useEffect)((()=>{if(o){const e=(0,h.omit)(o.query,"chartType","filter","paged"),t=(0,h.omit)(n.query,"chartType","filter","paged");o.query.paged>1&&!(0,h.isEqual)(e,t)&&(0,g.getHistory)().replace((0,g.getNewPath)({paged:1})),o.match.url!==n.match.url&&(window.document.documentElement.scrollTop=0)}}),[n,o]);const{page:r,match:a,query:i}=n,{url:s,params:c}=a;return window.wpNavMenuUrlUpdate(i),window.wpNavMenuClassChange(r,s),(0,e.createElement)(e.Suspense,{fallback:(0,e.createElement)(E.Spinner,null)},(0,e.createElement)(r.container,{params:c,path:s,pathMatch:r.path,query:i}))};window.wpNavMenuUrlUpdate=function(e){const t=(0,g.getPersistedQuery)(e),n=(0,g.getQueryExcludedScreens)(),o=document.querySelectorAll("#adminmenu a");Array.from(o).forEach((e=>function(e,t,n){if((0,g.isWCAdmin)(e.href)){const o=(0,h.last)(e.href.split("?")),r=(0,H.parse)(o),a=r.path||"homescreen",i=(0,g.getScreenFromPath)(a),s=n.includes(i),c="admin.php?"+(0,H.stringify)(Object.assign(r,s?{}:t));e.href=c,e.onclick=e=>{e.preventDefault(),(0,g.getHistory)().push(c)}}}(e,t,n)))},window.wpNavMenuClassChange=function(e,t){var n;const o=document.querySelector("#adminmenu");Array.from(o.getElementsByClassName("current")).forEach((function(e){e.classList.remove("current")})),Array.from(o.querySelectorAll(".wp-has-current-submenu")).forEach((function(e){e.classList.remove("wp-has-current-submenu"),e.classList.remove("wp-menu-open"),e.classList.remove("selected"),e.classList.add("wp-not-current-submenu"),e.classList.add("menu-top")}));const r="/"===t?"admin.php?page=wc-admin":"admin.php?page=wc-admin&path="+encodeURIComponent(t);let a="/"===t?`li > a[href$="${r}"], li > a[href*="${r}?"]`:`li > a[href*="${r}"]`;const i=null===(n=e.navArgs)||void 0===n?void 0:n.parentPath;i&&(a+=`, li > a[href*="${"/"===i?"admin.php?page=wc-admin":"admin.php?page=wc-admin&path="+encodeURIComponent(i)}"]`);const s=o.querySelectorAll(a);if(Array.from(s).forEach((function(e){e.parentElement.classList.add("current")})),e.wpOpenMenu){const t=o.querySelector("#"+e.wpOpenMenu);t&&(t.classList.remove("wp-not-current-submenu"),t.classList.add("wp-has-current-submenu"),t.classList.add("wp-menu-open"),t.classList.add("current"))}document.querySelector("#wpwrap").classList.remove("wp-responsive-open")};var z=i(83849),W=i.n(z),U=i(22629),G=i(74617),Y=i(14812);function Q(){const[t,n]=(0,e.useState)(!1),o=(0,e.useRef)(null);return(0,e.useEffect)((()=>{const e=()=>{n(window.pageYOffset>20)},t=()=>{o.current=window.requestAnimationFrame(e)};return window.addEventListener("scroll",t),()=>{window.removeEventListener("scroll",t),window.cancelAnimationFrame(o.current)}}),[]),t}var q=i(70123);const J=t=>{var n;let{sections:o,isEmbedded:r=!1,query:a}=t;const i=(0,e.useRef)(null),s=(0,q.useActiveSetupTasklist)(),c=(0,G.getSetting)("siteTitle",""),l=o.slice(-1)[0],m=Q();let u=null;const d=W()("woocommerce-layout__header",{"is-scrolled":m}),p=(0,Y.useSlot)(w.WC_HEADER_PAGE_TITLE_SLOT_NAME),C=Boolean(null==p||null===(n=p.fills)||void 0===n?void 0:n.length),h=(0,Y.useSlot)(w.WC_HEADER_SLOT_NAME),g=null==h?void 0:h.fills,f=()=>{clearTimeout(u),u=setTimeout((function(){const e=document.querySelector("#wpbody");e&&i.current&&(e.style.marginTop=`${i.current.offsetHeight}px`)}),200)};return(0,e.useLayoutEffect)((()=>(f(),window.addEventListener("resize",f),()=>{window.removeEventListener("resize",f);const e=document.querySelector("#wpbody");e&&(e.style.marginTop=null)})),[g]),(0,e.useEffect)((()=>{if(!r){const e=o.map((e=>Array.isArray(e)?e[1]:e)).reverse().join(" &lsaquo; "),t=(0,U.decodeEntities)((0,v.sprintf)((0,v.__)("%1$s &lsaquo; %2$s &#8212; WooCommerce","woocommerce"),e,c));document.title!==t&&(document.title=t)}}),[r,o,c]),(0,e.createElement)("div",{className:d,ref:i},s&&(0,e.createElement)(q.TasksReminderBar,{updateBodyMargin:f,taskListId:s}),(0,e.createElement)("div",{className:"woocommerce-layout__header-wrapper"},(0,e.createElement)(w.WooHeaderNavigationItem.Slot,{fillProps:{isEmbedded:r,query:a}}),(0,e.createElement)(Y.Text,{className:"woocommerce-layout__header-heading",as:"h1"},(0,U.decodeEntities)(C?(0,e.createElement)(w.WooHeaderPageTitle.Slot,{fillProps:{isEmbedded:r,query:a}}):l)),(0,e.createElement)(w.WooHeaderItem.Slot,{fillProps:{isEmbedded:r,query:a}})))},K=()=>{var t;const n=(0,Y.useSlot)(w.WC_FOOTER_SLOT_NAME);return Boolean(null==n||null===(t=n.fills)||void 0===t?void 0:t.length)?(0,e.createElement)("div",{className:"woocommerce-layout__footer"},(0,e.createElement)(w.WooFooterItem.Slot,null)):null};class X extends e.Component{render(){return(0,e.createElement)("div",{id:"woocommerce-layout__notice-list",className:"woocommerce-layout__notice-list"})}}const $=X;var ee=i(5267);let te=fe();const ne=e=>pe(e,te);let oe=fe();ne.write=e=>pe(e,oe);let re=fe();ne.onStart=e=>pe(e,re);let ae=fe();ne.onFrame=e=>pe(e,ae);let ie=fe();ne.onFinish=e=>pe(e,ie);let se=[];ne.setTimeout=(e,t)=>{let n=ne.now()+t,o=()=>{let e=se.findIndex((e=>e.cancel==o));~e&&se.splice(e,1),ue-=~e?1:0},r={time:n,handler:e,cancel:o};return se.splice(ce(n),0,r),ue+=1,Ce(),r};let ce=e=>~(~se.findIndex((t=>t.time>e))||~se.length);ne.cancel=e=>{re.delete(e),ae.delete(e),te.delete(e),oe.delete(e),ie.delete(e)},ne.sync=e=>{de=!0,ne.batchedUpdates(e),de=!1},ne.throttle=e=>{let t;function n(){try{e(...t)}finally{t=null}}function o(...e){t=e,ne.onStart(n)}return o.handler=e,o.cancel=()=>{re.delete(n),t=null},o};let le="undefined"!=typeof window?window.requestAnimationFrame:()=>{};ne.use=e=>le=e,ne.now="undefined"!=typeof performance?()=>performance.now():Date.now,ne.batchedUpdates=e=>e(),ne.catch=console.error,ne.frameLoop="always",ne.advance=()=>{"demand"!==ne.frameLoop?console.warn("Cannot call the manual advancement of rafz whilst frameLoop is not set as demand"):ge()};let me=-1,ue=0,de=!1;function pe(e,t){de?(t.delete(e),e(0)):(t.add(e),Ce())}function Ce(){me<0&&(me=0,"demand"!==ne.frameLoop&&le(he))}function he(){~me&&(le(he),ne.batchedUpdates(ge))}function ge(){let e=me;me=ne.now();let t=ce(me);t&&(ye(se.splice(0,t),(e=>e.handler())),ue-=t),re.flush(),te.flush(e?Math.min(64,me-e):16.667),ae.flush(),oe.flush(),ie.flush(),ue||(me=-1)}function fe(){let e=new Set,t=e;return{add(n){ue+=t!=e||e.has(n)?0:1,e.add(n)},delete:n=>(ue-=t==e&&e.has(n)?1:0,e.delete(n)),flush(n){t.size&&(e=new Set,ue-=t.size,ye(t,(t=>t(n)&&e.add(t))),ue+=e.size,t=e)}}}function ye(e,t){e.forEach((e=>{try{t(e)}catch(e){ne.catch(e)}}))}function we(){}const He={arr:Array.isArray,obj:e=>!!e&&"Object"===e.constructor.name,fun:e=>"function"==typeof e,str:e=>"string"==typeof e,num:e=>"number"==typeof e,und:e=>void 0===e};function _e(e,t){if(He.arr(e)){if(!He.arr(t)||e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}return e===t}const ve=(e,t)=>e.forEach(t);function Ee(e,t,n){if(He.arr(e))for(let o=0;o<e.length;o++)t.call(n,e[o],`${o}`);else for(const o in e)e.hasOwnProperty(o)&&t.call(n,e[o],o)}const Le=e=>He.und(e)?[]:He.arr(e)?e:[e];function be(e,t){if(e.size){const n=Array.from(e);e.clear(),ve(n,t)}}const ke=(e,...t)=>be(e,(e=>e(...t))),Se=()=>"undefined"==typeof window||!window.navigator||/ServerSideRendering|^Deno\//.test(window.navigator.userAgent);let Me,Ne,xe=null,Te=!1,Ie=we;var Ae=Object.freeze({__proto__:null,get createStringInterpolator(){return Me},get to(){return Ne},get colors(){return xe},get skipAnimation(){return Te},get willAdvance(){return Ie},assign:e=>{e.to&&(Ne=e.to),e.now&&(ne.now=e.now),void 0!==e.colors&&(xe=e.colors),null!=e.skipAnimation&&(Te=e.skipAnimation),e.createStringInterpolator&&(Me=e.createStringInterpolator),e.requestAnimationFrame&&ne.use(e.requestAnimationFrame),e.batchedUpdates&&(ne.batchedUpdates=e.batchedUpdates),e.willAdvance&&(Ie=e.willAdvance),e.frameLoop&&(ne.frameLoop=e.frameLoop)}});const Oe=new Set;let Pe=[],Ve=[],Re=0;const je={get idle(){return!Oe.size&&!Pe.length},start(e){Re>e.priority?(Oe.add(e),ne.onStart(De)):(Ze(e),ne(Fe))},advance:Fe,sort(e){if(Re)ne.onFrame((()=>je.sort(e)));else{const t=Pe.indexOf(e);~t&&(Pe.splice(t,1),Be(e))}},clear(){Pe=[],Oe.clear()}};function De(){Oe.forEach(Ze),Oe.clear(),ne(Fe)}function Ze(e){Pe.includes(e)||Be(e)}function Be(e){Pe.splice(function(t,n){const o=t.findIndex((t=>t.priority>e.priority));return o<0?t.length:o}(Pe),0,e)}function Fe(e){const t=Ve;for(let n=0;n<Pe.length;n++){const o=Pe[n];Re=o.priority,o.idle||(Ie(o),o.advance(e),o.idle||t.push(o))}return Re=0,Ve=Pe,Ve.length=0,Pe=t,Pe.length>0}const ze="[-+]?\\d*\\.?\\d+",We=ze+"%";function Ue(...e){return"\\(\\s*("+e.join(")\\s*,\\s*(")+")\\s*\\)"}const Ge=new RegExp("rgb"+Ue(ze,ze,ze)),Ye=new RegExp("rgba"+Ue(ze,ze,ze,ze)),Qe=new RegExp("hsl"+Ue(ze,We,We)),qe=new RegExp("hsla"+Ue(ze,We,We,ze)),Je=/^#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,Ke=/^#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,Xe=/^#([0-9a-fA-F]{6})$/,$e=/^#([0-9a-fA-F]{8})$/;function et(e,t,n){return n<0&&(n+=1),n>1&&(n-=1),n<1/6?e+6*(t-e)*n:n<.5?t:n<2/3?e+(t-e)*(2/3-n)*6:e}function tt(e,t,n){const o=n<.5?n*(1+t):n+t-n*t,r=2*n-o,a=et(r,o,e+1/3),i=et(r,o,e),s=et(r,o,e-1/3);return Math.round(255*a)<<24|Math.round(255*i)<<16|Math.round(255*s)<<8}function nt(e){const t=parseInt(e,10);return t<0?0:t>255?255:t}function ot(e){return(parseFloat(e)%360+360)%360/360}function rt(e){const t=parseFloat(e);return t<0?0:t>1?255:Math.round(255*t)}function at(e){const t=parseFloat(e);return t<0?0:t>100?1:t/100}function it(e){let t=function(e){let t;return"number"==typeof e?e>>>0===e&&e>=0&&e<=4294967295?e:null:(t=Xe.exec(e))?parseInt(t[1]+"ff",16)>>>0:xe&&void 0!==xe[e]?xe[e]:(t=Ge.exec(e))?(nt(t[1])<<24|nt(t[2])<<16|nt(t[3])<<8|255)>>>0:(t=Ye.exec(e))?(nt(t[1])<<24|nt(t[2])<<16|nt(t[3])<<8|rt(t[4]))>>>0:(t=Je.exec(e))?parseInt(t[1]+t[1]+t[2]+t[2]+t[3]+t[3]+"ff",16)>>>0:(t=$e.exec(e))?parseInt(t[1],16)>>>0:(t=Ke.exec(e))?parseInt(t[1]+t[1]+t[2]+t[2]+t[3]+t[3]+t[4]+t[4],16)>>>0:(t=Qe.exec(e))?(255|tt(ot(t[1]),at(t[2]),at(t[3])))>>>0:(t=qe.exec(e))?(tt(ot(t[1]),at(t[2]),at(t[3]))|rt(t[4]))>>>0:null}(e);return null===t?e:(t=t||0,`rgba(${(4278190080&t)>>>24}, ${(16711680&t)>>>16}, ${(65280&t)>>>8}, ${(255&t)/255})`)}const st=(e,t,n)=>{if(He.fun(e))return e;if(He.arr(e))return st({range:e,output:t,extrapolate:n});if(He.str(e.output[0]))return Me(e);const o=e,r=o.output,a=o.range||[0,1],i=o.extrapolateLeft||o.extrapolate||"extend",s=o.extrapolateRight||o.extrapolate||"extend",c=o.easing||(e=>e);return e=>{const t=function(e,t){for(var n=1;n<t.length-1&&!(t[n]>=e);++n);return n-1}(e,a);return function(e,t,n,o,r,a,i,s,c){let l=c?c(e):e;if(l<t){if("identity"===i)return l;"clamp"===i&&(l=t)}if(l>n){if("identity"===s)return l;"clamp"===s&&(l=n)}return o===r?o:t===n?e<=t?o:r:(t===-1/0?l=-l:n===1/0?l-=t:l=(l-t)/(n-t),l=a(l),o===-1/0?l=-l:r===1/0?l+=o:l=l*(r-o)+o,l)}(e,a[t],a[t+1],r[t],r[t+1],c,i,s,o.map)}};function ct(){return ct=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var o in n)Object.prototype.hasOwnProperty.call(n,o)&&(e[o]=n[o])}return e},ct.apply(this,arguments)}const lt=Symbol.for("FluidValue.get"),mt=Symbol.for("FluidValue.observers"),ut=e=>Boolean(e&&e[lt]),dt=e=>e&&e[lt]?e[lt]():e,pt=e=>e[mt]||null;function Ct(e,t){let n=e[mt];n&&n.forEach((e=>{!function(e,t){e.eventObserved?e.eventObserved(t):e(t)}(e,t)}))}class ht{constructor(e){if(this[lt]=void 0,this[mt]=void 0,!e&&!(e=this.get))throw Error("Unknown getter");gt(this,e)}}const gt=(e,t)=>wt(e,lt,t);function ft(e,t){if(e[lt]){let n=e[mt];n||wt(e,mt,n=new Set),n.has(t)||(n.add(t),e.observerAdded&&e.observerAdded(n.size,t))}return t}function yt(e,t){let n=e[mt];if(n&&n.has(t)){const o=n.size-1;o?n.delete(t):e[mt]=null,e.observerRemoved&&e.observerRemoved(o,t)}}const wt=(e,t,n)=>Object.defineProperty(e,t,{value:n,writable:!0,configurable:!0}),Ht=/[+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,_t=/(#(?:[0-9a-f]{2}){2,4}|(#[0-9a-f]{3})|(rgb|hsl)a?\((-?\d+%?[,\s]+){2,3}\s*[\d\.]+%?\))/gi,vt=new RegExp(`(${Ht.source})(%|[a-z]+)`,"i"),Et=/rgba\(([0-9\.-]+), ([0-9\.-]+), ([0-9\.-]+), ([0-9\.-]+)\)/gi,Lt=/var\((--[a-zA-Z0-9-_]+),? ?([a-zA-Z0-9 ()%#.,-]+)?\)/,bt=e=>{const[t,n]=kt(e);if(!t||Se())return e;const o=window.getComputedStyle(document.documentElement).getPropertyValue(t);if(o)return o.trim();if(n&&n.startsWith("--")){return window.getComputedStyle(document.documentElement).getPropertyValue(n)||e}return n&&Lt.test(n)?bt(n):n||e},kt=e=>{const t=Lt.exec(e);if(!t)return[,];const[,n,o]=t;return[n,o]};let St;const Mt=(e,t,n,o,r)=>`rgba(${Math.round(t)}, ${Math.round(n)}, ${Math.round(o)}, ${r})`,Nt=e=>{St||(St=xe?new RegExp(`(${Object.keys(xe).join("|")})(?!\\w)`,"g"):/^\b$/);const t=e.output.map((e=>dt(e).replace(Lt,bt).replace(_t,it).replace(St,it))),n=t.map((e=>e.match(Ht).map(Number))),o=n[0].map(((e,t)=>n.map((e=>{if(!(t in e))throw Error('The arity of each "output" value must be equal');return e[t]})))).map((t=>st(ct({},e,{output:t}))));return e=>{var n;const r=!vt.test(t[0])&&(null==(n=t.find((e=>vt.test(e))))?void 0:n.replace(Ht,""));let a=0;return t[0].replace(Ht,(()=>`${o[a++](e)}${r||""}`)).replace(Et,Mt)}},xt="react-spring: ",Tt=e=>{const t=e;let n=!1;if("function"!=typeof t)throw new TypeError(`${xt}once requires a function parameter`);return(...e)=>{n||(t(...e),n=!0)}},It=Tt(console.warn),At=Tt(console.warn);function Ot(e){return He.str(e)&&("#"==e[0]||/\d/.test(e)||!Se()&&Lt.test(e)||e in(xe||{}))}const Pt=e=>(0,d.useEffect)(e,Vt),Vt=[];function Rt(){const e=(0,d.useState)()[1],t=(0,d.useState)(jt)[0];return Pt(t.unmount),()=>{t.current&&e({})}}function jt(){const e={current:!0,unmount:()=>()=>{e.current=!1}};return e}function Dt(e){const t=(0,d.useRef)();return(0,d.useEffect)((()=>{t.current=e})),t.current}const Zt="undefined"!=typeof window&&window.document&&window.document.createElement?d.useLayoutEffect:d.useEffect,Bt=Symbol.for("Animated:node"),Ft=e=>e&&e[Bt],zt=(e,t)=>{return n=e,o=Bt,r=t,Object.defineProperty(n,o,{value:r,writable:!0,configurable:!0});var n,o,r},Wt=e=>e&&e[Bt]&&e[Bt].getPayload();class Ut{constructor(){this.payload=void 0,zt(this,this)}getPayload(){return this.payload||[]}}class Gt extends Ut{constructor(e){super(),this.done=!0,this.elapsedTime=void 0,this.lastPosition=void 0,this.lastVelocity=void 0,this.v0=void 0,this.durationProgress=0,this._value=e,He.num(this._value)&&(this.lastPosition=this._value)}static create(e){return new Gt(e)}getPayload(){return[this]}getValue(){return this._value}setValue(e,t){return He.num(e)&&(this.lastPosition=e,t&&(e=Math.round(e/t)*t,this.done&&(this.lastPosition=e))),this._value!==e&&(this._value=e,!0)}reset(){const{done:e}=this;this.done=!1,He.num(this._value)&&(this.elapsedTime=0,this.durationProgress=0,this.lastPosition=this._value,e&&(this.lastVelocity=null),this.v0=null)}}class Yt extends Gt{constructor(e){super(0),this._string=null,this._toString=void 0,this._toString=st({output:[e,e]})}static create(e){return new Yt(e)}getValue(){let e=this._string;return null==e?this._string=this._toString(this._value):e}setValue(e){if(He.str(e)){if(e==this._string)return!1;this._string=e,this._value=1}else{if(!super.setValue(e))return!1;this._string=null}return!0}reset(e){e&&(this._toString=st({output:[this.getValue(),e]})),this._value=0,super.reset()}}const Qt={dependencies:null};class qt extends Ut{constructor(e){super(),this.source=e,this.setValue(e)}getValue(e){const t={};return Ee(this.source,((n,o)=>{var r;(r=n)&&r[Bt]===r?t[o]=n.getValue(e):ut(n)?t[o]=dt(n):e||(t[o]=n)})),t}setValue(e){this.source=e,this.payload=this._makePayload(e)}reset(){this.payload&&ve(this.payload,(e=>e.reset()))}_makePayload(e){if(e){const t=new Set;return Ee(e,this._addToPayload,t),Array.from(t)}}_addToPayload(e){Qt.dependencies&&ut(e)&&Qt.dependencies.add(e);const t=Wt(e);t&&ve(t,(e=>this.add(e)))}}class Jt extends qt{constructor(e){super(e)}static create(e){return new Jt(e)}getValue(){return this.source.map((e=>e.getValue()))}setValue(e){const t=this.getPayload();return e.length==t.length?t.map(((t,n)=>t.setValue(e[n]))).some(Boolean):(super.setValue(e.map(Kt)),!0)}}function Kt(e){return(Ot(e)?Yt:Gt).create(e)}function Xt(e){const t=Ft(e);return t?t.constructor:He.arr(e)?Jt:Ot(e)?Yt:Gt}function $t(){return $t=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var o in n)Object.prototype.hasOwnProperty.call(n,o)&&(e[o]=n[o])}return e},$t.apply(this,arguments)}const en=(e,t)=>{const n=!He.fun(e)||e.prototype&&e.prototype.isReactComponent;return(0,d.forwardRef)(((o,r)=>{const a=(0,d.useRef)(null),i=n&&(0,d.useCallback)((e=>{a.current=function(e,t){return e&&(He.fun(e)?e(t):e.current=t),t}(r,e)}),[r]),[s,c]=function(e,t){const n=new Set;return Qt.dependencies=n,e.style&&(e=$t({},e,{style:t.createAnimatedStyle(e.style)})),e=new qt(e),Qt.dependencies=null,[e,n]}(o,t),l=Rt(),m=()=>{const e=a.current;n&&!e||!1===(!!e&&t.applyAnimatedValues(e,s.getValue(!0)))&&l()},u=new tn(m,c),p=(0,d.useRef)();Zt((()=>{const e=p.current;p.current=u,ve(c,(e=>ft(e,u))),e&&(ve(e.deps,(t=>yt(t,e))),ne.cancel(e.update))})),(0,d.useEffect)(m,[]),Pt((()=>()=>{const e=p.current;ve(e.deps,(t=>yt(t,e)))}));const C=t.getComponentProps(s.getValue());return d.createElement(e,$t({},C,{ref:i}))}))};class tn{constructor(e,t){this.update=e,this.deps=t}eventObserved(e){"change"==e.type&&ne.write(this.update)}}const nn=Symbol.for("AnimatedComponent"),on=e=>He.str(e)?e:e&&He.str(e.displayName)?e.displayName:He.fun(e)&&e.name||null;function rn(){return rn=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var o in n)Object.prototype.hasOwnProperty.call(n,o)&&(e[o]=n[o])}return e},rn.apply(this,arguments)}function an(e,...t){return He.fun(e)?e(...t):e}const sn=(e,t)=>!0===e||!!(t&&e&&(He.fun(e)?e(t):Le(e).includes(t))),cn=(e,t)=>He.obj(e)?t&&e[t]:e,ln=(e,t)=>!0===e.default?e[t]:e.default?e.default[t]:void 0,mn=e=>e,un=(e,t=mn)=>{let n=dn;e.default&&!0!==e.default&&(e=e.default,n=Object.keys(e));const o={};for(const r of n){const n=t(e[r],r);He.und(n)||(o[r]=n)}return o},dn=["config","onProps","onStart","onChange","onPause","onResume","onRest"],pn={config:1,from:1,to:1,ref:1,loop:1,reset:1,pause:1,cancel:1,reverse:1,immediate:1,default:1,delay:1,onProps:1,onStart:1,onChange:1,onPause:1,onResume:1,onRest:1,onResolve:1,items:1,trail:1,sort:1,expires:1,initial:1,enter:1,update:1,leave:1,children:1,onDestroyed:1,keys:1,callId:1,parentId:1};function Cn(e){const t=function(e){const t={};let n=0;if(Ee(e,((e,o)=>{pn[o]||(t[o]=e,n++)})),n)return t}(e);if(t){const n={to:t};return Ee(e,((e,o)=>o in t||(n[o]=e))),n}return rn({},e)}function hn(e){return e=dt(e),He.arr(e)?e.map(hn):Ot(e)?Ae.createStringInterpolator({range:[0,1],output:[e,e]})(1):e}function gn(e){for(const t in e)return!0;return!1}function fn(e){return He.fun(e)||He.arr(e)&&He.obj(e[0])}function yn(e,t){var n;null==(n=e.ref)||n.delete(e),null==t||t.delete(e)}function wn(e,t){var n;t&&e.ref!==t&&(null==(n=e.ref)||n.delete(e),t.add(e),e.ref=t)}Math.PI,Math.PI;const Hn=rn({},{tension:170,friction:26},{mass:1,damping:1,easing:e=>e,clamp:!1});class _n{constructor(){this.tension=void 0,this.friction=void 0,this.frequency=void 0,this.damping=void 0,this.mass=void 0,this.velocity=0,this.restVelocity=void 0,this.precision=void 0,this.progress=void 0,this.duration=void 0,this.easing=void 0,this.clamp=void 0,this.bounce=void 0,this.decay=void 0,this.round=void 0,Object.assign(this,Hn)}}function vn(e,t){if(He.und(t.decay)){const n=!He.und(t.tension)||!He.und(t.friction);!n&&He.und(t.frequency)&&He.und(t.damping)&&He.und(t.mass)||(e.duration=void 0,e.decay=void 0),n&&(e.frequency=void 0)}else e.duration=void 0}const En=[];class Ln{constructor(){this.changed=!1,this.values=En,this.toValues=null,this.fromValues=En,this.to=void 0,this.from=void 0,this.config=new _n,this.immediate=!1}}function bn(e,{key:t,props:n,defaultProps:o,state:r,actions:a}){return new Promise(((i,s)=>{var c;let l,m,u=sn(null!=(c=n.cancel)?c:null==o?void 0:o.cancel,t);if(u)C();else{He.und(n.pause)||(r.paused=sn(n.pause,t));let e=null==o?void 0:o.pause;!0!==e&&(e=r.paused||sn(e,t)),l=an(n.delay||0,t),e?(r.resumeQueue.add(p),a.pause()):(a.resume(),p())}function d(){r.resumeQueue.add(p),r.timeouts.delete(m),m.cancel(),l=m.time-ne.now()}function p(){l>0&&!Ae.skipAnimation?(r.delayed=!0,m=ne.setTimeout(C,l),r.pauseQueue.add(d),r.timeouts.add(m)):C()}function C(){r.delayed&&(r.delayed=!1),r.pauseQueue.delete(d),r.timeouts.delete(m),e<=(r.cancelId||0)&&(u=!0);try{a.start(rn({},n,{callId:e,cancel:u}),i)}catch(e){s(e)}}}))}const kn=(e,t)=>1==t.length?t[0]:t.some((e=>e.cancelled))?Nn(e.get()):t.every((e=>e.noop))?Sn(e.get()):Mn(e.get(),t.every((e=>e.finished))),Sn=e=>({value:e,noop:!0,finished:!0,cancelled:!1}),Mn=(e,t,n=!1)=>({value:e,finished:t,cancelled:n}),Nn=e=>({value:e,cancelled:!0,finished:!1});function xn(e,t,n,o){const{callId:r,parentId:a,onRest:i}=t,{asyncTo:s,promise:c}=n;return a||e!==s||t.reset?n.promise=(async()=>{n.asyncId=r,n.asyncTo=e;const l=un(t,((e,t)=>"onRest"===t?void 0:e));let m,u;const d=new Promise(((e,t)=>(m=e,u=t))),p=e=>{const t=r<=(n.cancelId||0)&&Nn(o)||r!==n.asyncId&&Mn(o,!1);if(t)throw e.result=t,u(e),e},C=(e,t)=>{const a=new In,i=new An;return(async()=>{if(Ae.skipAnimation)throw Tn(n),i.result=Mn(o,!1),u(i),i;p(a);const s=He.obj(e)?rn({},e):rn({},t,{to:e});s.parentId=r,Ee(l,((e,t)=>{He.und(s[t])&&(s[t]=e)}));const c=await o.start(s);return p(a),n.paused&&await new Promise((e=>{n.resumeQueue.add(e)})),c})()};let h;if(Ae.skipAnimation)return Tn(n),Mn(o,!1);try{let t;t=He.arr(e)?(async e=>{for(const t of e)await C(t)})(e):Promise.resolve(e(C,o.stop.bind(o))),await Promise.all([t.then(m),d]),h=Mn(o.get(),!0,!1)}catch(e){if(e instanceof In)h=e.result;else{if(!(e instanceof An))throw e;h=e.result}}finally{r==n.asyncId&&(n.asyncId=a,n.asyncTo=a?s:void 0,n.promise=a?c:void 0)}return He.fun(i)&&ne.batchedUpdates((()=>{i(h,o,o.item)})),h})():c}function Tn(e,t){be(e.timeouts,(e=>e.cancel())),e.pauseQueue.clear(),e.resumeQueue.clear(),e.asyncId=e.asyncTo=e.promise=void 0,t&&(e.cancelId=t)}class In extends Error{constructor(){super("An async animation has been interrupted. You see this error because you forgot to use `await` or `.catch(...)` on its returned promise."),this.result=void 0}}class An extends Error{constructor(){super("SkipAnimationSignal"),this.result=void 0}}const On=e=>e instanceof Vn;let Pn=1;class Vn extends ht{constructor(...e){super(...e),this.id=Pn++,this.key=void 0,this._priority=0}get priority(){return this._priority}set priority(e){this._priority!=e&&(this._priority=e,this._onPriorityChange(e))}get(){const e=Ft(this);return e&&e.getValue()}to(...e){return Ae.to(this,e)}interpolate(...e){return It(`${xt}The "interpolate" function is deprecated in v9 (use "to" instead)`),Ae.to(this,e)}toJSON(){return this.get()}observerAdded(e){1==e&&this._attach()}observerRemoved(e){0==e&&this._detach()}_attach(){}_detach(){}_onChange(e,t=!1){Ct(this,{type:"change",parent:this,value:e,idle:t})}_onPriorityChange(e){this.idle||je.sort(this),Ct(this,{type:"priority",parent:this,priority:e})}}const Rn=Symbol.for("SpringPhase"),jn=e=>(1&e[Rn])>0,Dn=e=>(2&e[Rn])>0,Zn=e=>(4&e[Rn])>0,Bn=(e,t)=>t?e[Rn]|=3:e[Rn]&=-3,Fn=(e,t)=>t?e[Rn]|=4:e[Rn]&=-5;class zn extends Vn{constructor(e,t){if(super(),this.key=void 0,this.animation=new Ln,this.queue=void 0,this.defaultProps={},this._state={paused:!1,delayed:!1,pauseQueue:new Set,resumeQueue:new Set,timeouts:new Set},this._pendingCalls=new Set,this._lastCallId=0,this._lastToId=0,this._memoizedDuration=0,!He.und(e)||!He.und(t)){const n=He.obj(e)?rn({},e):rn({},t,{from:e});He.und(n.default)&&(n.default=!0),this.start(n)}}get idle(){return!(Dn(this)||this._state.asyncTo)||Zn(this)}get goal(){return dt(this.animation.to)}get velocity(){const e=Ft(this);return e instanceof Gt?e.lastVelocity||0:e.getPayload().map((e=>e.lastVelocity||0))}get hasAnimated(){return jn(this)}get isAnimating(){return Dn(this)}get isPaused(){return Zn(this)}get isDelayed(){return this._state.delayed}advance(e){let t=!0,n=!1;const o=this.animation;let{config:r,toValues:a}=o;const i=Wt(o.to);!i&&ut(o.to)&&(a=Le(dt(o.to))),o.values.forEach(((s,c)=>{if(s.done)return;const l=s.constructor==Yt?1:i?i[c].lastPosition:a[c];let m=o.immediate,u=l;if(!m){if(u=s.lastPosition,r.tension<=0)return void(s.done=!0);let t=s.elapsedTime+=e;const n=o.fromValues[c],a=null!=s.v0?s.v0:s.v0=He.arr(r.velocity)?r.velocity[c]:r.velocity;let i;if(He.und(r.duration))if(r.decay){const e=!0===r.decay?.998:r.decay,o=Math.exp(-(1-e)*t);u=n+a/(1-e)*(1-o),m=Math.abs(s.lastPosition-u)<.1,i=a*o}else{i=null==s.lastVelocity?a:s.lastVelocity;const t=r.precision||(n==l?.005:Math.min(1,.001*Math.abs(l-n))),o=r.restVelocity||t/10,c=r.clamp?0:r.bounce,d=!He.und(c),p=n==l?s.v0>0:n<l;let C,h=!1;const g=1,f=Math.ceil(e/g);for(let e=0;e<f&&(C=Math.abs(i)>o,C||(m=Math.abs(l-u)<=t,!m));++e)d&&(h=u==l||u>l==p,h&&(i=-i*c,u=l)),i+=(1e-6*-r.tension*(u-l)+.001*-r.friction*i)/r.mass*g,u+=i*g}else{let o=1;r.duration>0&&(this._memoizedDuration!==r.duration&&(this._memoizedDuration=r.duration,s.durationProgress>0&&(s.elapsedTime=r.duration*s.durationProgress,t=s.elapsedTime+=e)),o=(r.progress||0)+t/this._memoizedDuration,o=o>1?1:o<0?0:o,s.durationProgress=o),u=n+r.easing(o)*(l-n),i=(u-s.lastPosition)/e,m=1==o}s.lastVelocity=i,Number.isNaN(u)&&(console.warn("Got NaN while animating:",this),m=!0)}i&&!i[c].done&&(m=!1),m?s.done=!0:t=!1,s.setValue(u,r.round)&&(n=!0)}));const s=Ft(this),c=s.getValue();if(t){const e=dt(o.to);c===e&&!n||r.decay?n&&r.decay&&this._onChange(c):(s.setValue(e),this._onChange(e)),this._stop()}else n&&this._onChange(c)}set(e){return ne.batchedUpdates((()=>{this._stop(),this._focus(e),this._set(e)})),this}pause(){this._update({pause:!0})}resume(){this._update({pause:!1})}finish(){if(Dn(this)){const{to:e,config:t}=this.animation;ne.batchedUpdates((()=>{this._onStart(),t.decay||this._set(e,!1),this._stop()}))}return this}update(e){return(this.queue||(this.queue=[])).push(e),this}start(e,t){let n;return He.und(e)?(n=this.queue||[],this.queue=[]):n=[He.obj(e)?e:rn({},t,{to:e})],Promise.all(n.map((e=>this._update(e)))).then((e=>kn(this,e)))}stop(e){const{to:t}=this.animation;return this._focus(this.get()),Tn(this._state,e&&this._lastCallId),ne.batchedUpdates((()=>this._stop(t,e))),this}reset(){this._update({reset:!0})}eventObserved(e){"change"==e.type?this._start():"priority"==e.type&&(this.priority=e.priority+1)}_prepareNode(e){const t=this.key||"";let{to:n,from:o}=e;n=He.obj(n)?n[t]:n,(null==n||fn(n))&&(n=void 0),o=He.obj(o)?o[t]:o,null==o&&(o=void 0);const r={to:n,from:o};return jn(this)||(e.reverse&&([n,o]=[o,n]),o=dt(o),He.und(o)?Ft(this)||this._set(n):this._set(o)),r}_update(e,t){let n=rn({},e);const{key:o,defaultProps:r}=this;n.default&&Object.assign(r,un(n,((e,t)=>/^on/.test(t)?cn(e,o):e))),qn(this,n,"onProps"),Jn(this,"onProps",n,this);const a=this._prepareNode(n);if(Object.isFrozen(this))throw Error("Cannot animate a `SpringValue` object that is frozen. Did you forget to pass your component to `animated(...)` before animating its props?");const i=this._state;return bn(++this._lastCallId,{key:o,props:n,defaultProps:r,state:i,actions:{pause:()=>{Zn(this)||(Fn(this,!0),ke(i.pauseQueue),Jn(this,"onPause",Mn(this,Wn(this,this.animation.to)),this))},resume:()=>{Zn(this)&&(Fn(this,!1),Dn(this)&&this._resume(),ke(i.resumeQueue),Jn(this,"onResume",Mn(this,Wn(this,this.animation.to)),this))},start:this._merge.bind(this,a)}}).then((e=>{if(n.loop&&e.finished&&(!t||!e.noop)){const e=Un(n);if(e)return this._update(e,!0)}return e}))}_merge(e,t,n){if(t.cancel)return this.stop(!0),n(Nn(this));const o=!He.und(e.to),r=!He.und(e.from);if(o||r){if(!(t.callId>this._lastToId))return n(Nn(this));this._lastToId=t.callId}const{key:a,defaultProps:i,animation:s}=this,{to:c,from:l}=s;let{to:m=c,from:u=l}=e;!r||o||t.default&&!He.und(m)||(m=u),t.reverse&&([m,u]=[u,m]);const d=!_e(u,l);d&&(s.from=u),u=dt(u);const p=!_e(m,c);p&&this._focus(m);const C=fn(t.to),{config:h}=s,{decay:g,velocity:f}=h;(o||r)&&(h.velocity=0),t.config&&!C&&function(e,t,n){n&&(vn(n=rn({},n),t),t=rn({},n,t)),vn(e,t),Object.assign(e,t);for(const t in Hn)null==e[t]&&(e[t]=Hn[t]);let{mass:o,frequency:r,damping:a}=e;He.und(r)||(r<.01&&(r=.01),a<0&&(a=0),e.tension=Math.pow(2*Math.PI/r,2)*o,e.friction=4*Math.PI*a*o/r)}(h,an(t.config,a),t.config!==i.config?an(i.config,a):void 0);let y=Ft(this);if(!y||He.und(m))return n(Mn(this,!0));const w=He.und(t.reset)?r&&!t.default:!He.und(u)&&sn(t.reset,a),H=w?u:this.get(),_=hn(m),v=He.num(_)||He.arr(_)||Ot(_),E=!C&&(!v||sn(i.immediate||t.immediate,a));if(p){const e=Xt(m);if(e!==y.constructor){if(!E)throw Error(`Cannot animate between ${y.constructor.name} and ${e.name}, as the "to" prop suggests`);y=this._set(_)}}const L=y.constructor;let b=ut(m),k=!1;if(!b){const e=w||!jn(this)&&d;(p||e)&&(k=_e(hn(H),_),b=!k),(_e(s.immediate,E)||E)&&_e(h.decay,g)&&_e(h.velocity,f)||(b=!0)}if(k&&Dn(this)&&(s.changed&&!w?b=!0:b||this._stop(c)),!C&&((b||ut(c))&&(s.values=y.getPayload(),s.toValues=ut(m)?null:L==Yt?[1]:Le(_)),s.immediate!=E&&(s.immediate=E,E||w||this._set(c)),b)){const{onRest:e}=s;ve(Qn,(e=>qn(this,t,e)));const o=Mn(this,Wn(this,c));ke(this._pendingCalls,o),this._pendingCalls.add(n),s.changed&&ne.batchedUpdates((()=>{s.changed=!w,null==e||e(o,this),w?an(i.onRest,o):null==s.onStart||s.onStart(o,this)}))}w&&this._set(H),C?n(xn(t.to,t,this._state,this)):b?this._start():Dn(this)&&!p?this._pendingCalls.add(n):n(Sn(H))}_focus(e){const t=this.animation;e!==t.to&&(pt(this)&&this._detach(),t.to=e,pt(this)&&this._attach())}_attach(){let e=0;const{to:t}=this.animation;ut(t)&&(ft(t,this),On(t)&&(e=t.priority+1)),this.priority=e}_detach(){const{to:e}=this.animation;ut(e)&&yt(e,this)}_set(e,t=!0){const n=dt(e);if(!He.und(n)){const e=Ft(this);if(!e||!_e(n,e.getValue())){const o=Xt(n);e&&e.constructor==o?e.setValue(n):zt(this,o.create(n)),e&&ne.batchedUpdates((()=>{this._onChange(n,t)}))}}return Ft(this)}_onStart(){const e=this.animation;e.changed||(e.changed=!0,Jn(this,"onStart",Mn(this,Wn(this,e.to)),this))}_onChange(e,t){t||(this._onStart(),an(this.animation.onChange,e,this)),an(this.defaultProps.onChange,e,this),super._onChange(e,t)}_start(){const e=this.animation;Ft(this).reset(dt(e.to)),e.immediate||(e.fromValues=e.values.map((e=>e.lastPosition))),Dn(this)||(Bn(this,!0),Zn(this)||this._resume())}_resume(){Ae.skipAnimation?this.finish():je.start(this)}_stop(e,t){if(Dn(this)){Bn(this,!1);const n=this.animation;ve(n.values,(e=>{e.done=!0})),n.toValues&&(n.onChange=n.onPause=n.onResume=void 0),Ct(this,{type:"idle",parent:this});const o=t?Nn(this.get()):Mn(this.get(),Wn(this,null!=e?e:n.to));ke(this._pendingCalls,o),n.changed&&(n.changed=!1,Jn(this,"onRest",o,this))}}}function Wn(e,t){const n=hn(t);return _e(hn(e.get()),n)}function Un(e,t=e.loop,n=e.to){let o=an(t);if(o){const r=!0!==o&&Cn(o),a=(r||e).reverse,i=!r||r.reset;return Gn(rn({},e,{loop:t,default:!1,pause:void 0,to:!a||fn(n)?n:void 0,from:i?e.from:void 0,reset:i},r))}}function Gn(e){const{to:t,from:n}=e=Cn(e),o=new Set;return He.obj(t)&&Yn(t,o),He.obj(n)&&Yn(n,o),e.keys=o.size?Array.from(o):null,e}function Yn(e,t){Ee(e,((e,n)=>null!=e&&t.add(n)))}const Qn=["onStart","onRest","onChange","onPause","onResume"];function qn(e,t,n){e.animation[n]=t[n]!==ln(t,n)?cn(t[n],e.key):void 0}function Jn(e,t,...n){var o,r,a,i;null==(o=(r=e.animation)[t])||o.call(r,...n),null==(a=(i=e.defaultProps)[t])||a.call(i,...n)}const Kn=["onStart","onChange","onRest"];let Xn=1;class $n{constructor(e,t){this.id=Xn++,this.springs={},this.queue=[],this.ref=void 0,this._flush=void 0,this._initialProps=void 0,this._lastAsyncId=0,this._active=new Set,this._changed=new Set,this._started=!1,this._item=void 0,this._state={paused:!1,pauseQueue:new Set,resumeQueue:new Set,timeouts:new Set},this._events={onStart:new Map,onChange:new Map,onRest:new Map},this._onFrame=this._onFrame.bind(this),t&&(this._flush=t),e&&this.start(rn({default:!0},e))}get idle(){return!this._state.asyncTo&&Object.values(this.springs).every((e=>e.idle&&!e.isDelayed&&!e.isPaused))}get item(){return this._item}set item(e){this._item=e}get(){const e={};return this.each(((t,n)=>e[n]=t.get())),e}set(e){for(const t in e){const n=e[t];He.und(n)||this.springs[t].set(n)}}update(e){return e&&this.queue.push(Gn(e)),this}start(e){let{queue:t}=this;return e?t=Le(e).map(Gn):this.queue=[],this._flush?this._flush(this,t):(ro(this,t),function(e,t){return Promise.all(t.map((t=>eo(e,t)))).then((t=>kn(e,t)))}(this,t))}stop(e,t){if(e!==!!e&&(t=e),t){const n=this.springs;ve(Le(t),(t=>n[t].stop(!!e)))}else Tn(this._state,this._lastAsyncId),this.each((t=>t.stop(!!e)));return this}pause(e){if(He.und(e))this.start({pause:!0});else{const t=this.springs;ve(Le(e),(e=>t[e].pause()))}return this}resume(e){if(He.und(e))this.start({pause:!1});else{const t=this.springs;ve(Le(e),(e=>t[e].resume()))}return this}each(e){Ee(this.springs,e)}_onFrame(){const{onStart:e,onChange:t,onRest:n}=this._events,o=this._active.size>0,r=this._changed.size>0;(o&&!this._started||r&&!this._started)&&(this._started=!0,be(e,(([e,t])=>{t.value=this.get(),e(t,this,this._item)})));const a=!o&&this._started,i=r||a&&n.size?this.get():null;r&&t.size&&be(t,(([e,t])=>{t.value=i,e(t,this,this._item)})),a&&(this._started=!1,be(n,(([e,t])=>{t.value=i,e(t,this,this._item)})))}eventObserved(e){if("change"==e.type)this._changed.add(e.parent),e.idle||this._active.add(e.parent);else{if("idle"!=e.type)return;this._active.delete(e.parent)}ne.onFrame(this._onFrame)}}async function eo(e,t,n){const{keys:o,to:r,from:a,loop:i,onRest:s,onResolve:c}=t,l=He.obj(t.default)&&t.default;i&&(t.loop=!1),!1===r&&(t.to=null),!1===a&&(t.from=null);const m=He.arr(r)||He.fun(r)?r:void 0;m?(t.to=void 0,t.onRest=void 0,l&&(l.onRest=void 0)):ve(Kn,(n=>{const o=t[n];if(He.fun(o)){const r=e._events[n];t[n]=({finished:e,cancelled:t})=>{const n=r.get(o);n?(e||(n.finished=!1),t&&(n.cancelled=!0)):r.set(o,{value:null,finished:e||!1,cancelled:t||!1})},l&&(l[n]=t[n])}}));const u=e._state;t.pause===!u.paused?(u.paused=t.pause,ke(t.pause?u.pauseQueue:u.resumeQueue)):u.paused&&(t.pause=!0);const d=(o||Object.keys(e.springs)).map((n=>e.springs[n].start(t))),p=!0===t.cancel||!0===ln(t,"cancel");(m||p&&u.asyncId)&&d.push(bn(++e._lastAsyncId,{props:t,state:u,actions:{pause:we,resume:we,start(t,n){p?(Tn(u,e._lastAsyncId),n(Nn(e))):(t.onRest=s,n(xn(m,t,u,e)))}}})),u.paused&&await new Promise((e=>{u.resumeQueue.add(e)}));const C=kn(e,await Promise.all(d));if(i&&C.finished&&(!n||!C.noop)){const n=Un(t,i,r);if(n)return ro(e,[n]),eo(e,n,!0)}return c&&ne.batchedUpdates((()=>c(C,e,e.item))),C}function to(e,t){const n=rn({},e.springs);return t&&ve(Le(t),(e=>{He.und(e.keys)&&(e=Gn(e)),He.obj(e.to)||(e=rn({},e,{to:void 0})),oo(n,e,(e=>no(e)))})),function(e,t){Ee(t,((t,n)=>{e.springs[n]||(e.springs[n]=t,ft(t,e))}))}(e,n),n}function no(e,t){const n=new zn;return n.key=e,t&&ft(n,t),n}function oo(e,t,n){t.keys&&ve(t.keys,(o=>{(e[o]||(e[o]=n(o)))._prepareNode(t)}))}function ro(e,t){ve(t,(t=>{oo(e.springs,t,(t=>no(t,e)))}))}const ao=["children"],io=e=>{let{children:t}=e,n=function(e,t){if(null==e)return{};var n,o,r={},a=Object.keys(e);for(o=0;o<a.length;o++)n=a[o],t.indexOf(n)>=0||(r[n]=e[n]);return r}(e,ao);const o=(0,d.useContext)(so),r=n.pause||!!o.pause,a=n.immediate||!!o.immediate;n=function(e,t){const[n]=(0,d.useState)((()=>({inputs:t,result:e()}))),o=(0,d.useRef)(),r=o.current;let a=r;return a?Boolean(t&&a.inputs&&function(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}(t,a.inputs))||(a={inputs:t,result:e()}):a=n,(0,d.useEffect)((()=>{o.current=a,r==n&&(n.inputs=n.result=void 0)}),[a]),a.result}((()=>({pause:r,immediate:a})),[r,a]);const{Provider:i}=so;return d.createElement(i,{value:n},t)},so=(co=io,lo={},Object.assign(co,d.createContext(lo)),co.Provider._context=co,co.Consumer._context=co,co);var co,lo;io.Provider=so.Provider,io.Consumer=so.Consumer;const mo=()=>{const e=[],t=function(t){At(`${xt}Directly calling start instead of using the api object is deprecated in v9 (use ".start" instead), this will be removed in later 0.X.0 versions`);const o=[];return ve(e,((e,r)=>{if(He.und(t))o.push(e.start());else{const a=n(t,e,r);a&&o.push(e.start(a))}})),o};t.current=e,t.add=function(t){e.includes(t)||e.push(t)},t.delete=function(t){const n=e.indexOf(t);~n&&e.splice(n,1)},t.pause=function(){return ve(e,(e=>e.pause(...arguments))),this},t.resume=function(){return ve(e,(e=>e.resume(...arguments))),this},t.set=function(t){ve(e,(e=>e.set(t)))},t.start=function(t){const n=[];return ve(e,((e,o)=>{if(He.und(t))n.push(e.start());else{const r=this._getProps(t,e,o);r&&n.push(e.start(r))}})),n},t.stop=function(){return ve(e,(e=>e.stop(...arguments))),this},t.update=function(t){return ve(e,((e,n)=>e.update(this._getProps(t,e,n)))),this};const n=function(e,t,n){return He.fun(e)?e(n,t):e};return t._getProps=n,t};let uo;!function(e){e.MOUNT="mount",e.ENTER="enter",e.UPDATE="update",e.LEAVE="leave"}(uo||(uo={}));let po=1;function Co(e,{key:t,keys:n=t},o){if(null===n){const t=new Set;return e.map((e=>{const n=o&&o.find((n=>n.item===e&&n.phase!==uo.LEAVE&&!t.has(n)));return n?(t.add(n),n.key):po++}))}return He.und(n)?e:He.fun(n)?e.map(n):Le(n)}class ho extends Vn{constructor(e,t){super(),this.key=void 0,this.idle=!0,this.calc=void 0,this._active=new Set,this.source=e,this.calc=st(...t);const n=this._get(),o=Xt(n);zt(this,o.create(n))}advance(e){const t=this._get();_e(t,this.get())||(Ft(this).setValue(t),this._onChange(t,this.idle)),!this.idle&&fo(this._active)&&yo(this)}_get(){const e=He.arr(this.source)?this.source.map(dt):Le(dt(this.source));return this.calc(...e)}_start(){this.idle&&!fo(this._active)&&(this.idle=!1,ve(Wt(this),(e=>{e.done=!1})),Ae.skipAnimation?(ne.batchedUpdates((()=>this.advance())),yo(this)):je.start(this))}_attach(){let e=1;ve(Le(this.source),(t=>{ut(t)&&ft(t,this),On(t)&&(t.idle||this._active.add(t),e=Math.max(e,t.priority+1))})),this.priority=e,this._start()}_detach(){ve(Le(this.source),(e=>{ut(e)&&yt(e,this)})),this._active.clear(),yo(this)}eventObserved(e){"change"==e.type?e.idle?this.advance():(this._active.add(e.parent),this._start()):"idle"==e.type?this._active.delete(e.parent):"priority"==e.type&&(this.priority=Le(this.source).reduce(((e,t)=>Math.max(e,(On(t)?t.priority:0)+1)),0))}}function go(e){return!1!==e.idle}function fo(e){return!e.size||Array.from(e).every(go)}function yo(e){e.idle||(e.idle=!0,ve(Wt(e),(e=>{e.done=!0})),Ct(e,{type:"idle",parent:e}))}Ae.assign({createStringInterpolator:Nt,to:(e,t)=>new ho(e,t)}),je.advance;var wo=i(91850);function Ho(e,t){if(null==e)return{};var n,o,r={},a=Object.keys(e);for(o=0;o<a.length;o++)n=a[o],t.indexOf(n)>=0||(r[n]=e[n]);return r}const _o=["style","children","scrollTop","scrollLeft"],vo=/^--/;function Eo(e,t){return null==t||"boolean"==typeof t||""===t?"":"number"!=typeof t||0===t||vo.test(e)||bo.hasOwnProperty(e)&&bo[e]?(""+t).trim():t+"px"}const Lo={};let bo={animationIterationCount:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0};const ko=["Webkit","Ms","Moz","O"];bo=Object.keys(bo).reduce(((e,t)=>(ko.forEach((n=>e[((e,t)=>e+t.charAt(0).toUpperCase()+t.substring(1))(n,t)]=e[t])),e)),bo);const So=["x","y","z"],Mo=/^(matrix|translate|scale|rotate|skew)/,No=/^(translate)/,xo=/^(rotate|skew)/,To=(e,t)=>He.num(e)&&0!==e?e+t:e,Io=(e,t)=>He.arr(e)?e.every((e=>Io(e,t))):He.num(e)?e===t:parseFloat(e)===t;class Ao extends qt{constructor(e){let{x:t,y:n,z:o}=e,r=Ho(e,So);const a=[],i=[];(t||n||o)&&(a.push([t||0,n||0,o||0]),i.push((e=>[`translate3d(${e.map((e=>To(e,"px"))).join(",")})`,Io(e,0)]))),Ee(r,((e,t)=>{if("transform"===t)a.push([e||""]),i.push((e=>[e,""===e]));else if(Mo.test(t)){if(delete r[t],He.und(e))return;const n=No.test(t)?"px":xo.test(t)?"deg":"";a.push(Le(e)),i.push("rotate3d"===t?([e,t,o,r])=>[`rotate3d(${e},${t},${o},${To(r,n)})`,Io(r,0)]:e=>[`${t}(${e.map((e=>To(e,n))).join(",")})`,Io(e,t.startsWith("scale")?1:0)])}})),a.length&&(r.transform=new Oo(a,i)),super(r)}}class Oo extends ht{constructor(e,t){super(),this._value=null,this.inputs=e,this.transforms=t}get(){return this._value||(this._value=this._get())}_get(){let e="",t=!0;return ve(this.inputs,((n,o)=>{const r=dt(n[0]),[a,i]=this.transforms[o](He.arr(r)?r:n.map(dt));e+=" "+a,t=t&&i})),t?"none":e}observerAdded(e){1==e&&ve(this.inputs,(e=>ve(e,(e=>ut(e)&&ft(e,this)))))}observerRemoved(e){0==e&&ve(this.inputs,(e=>ve(e,(e=>ut(e)&&yt(e,this)))))}eventObserved(e){"change"==e.type&&(this._value=null),Ct(this,e)}}const Po=["scrollTop","scrollLeft"];Ae.assign({batchedUpdates:wo.unstable_batchedUpdates,createStringInterpolator:Nt,colors:{transparent:0,aliceblue:4042850303,antiquewhite:4209760255,aqua:16777215,aquamarine:2147472639,azure:4043309055,beige:4126530815,bisque:4293182719,black:255,blanchedalmond:4293643775,blue:65535,blueviolet:2318131967,brown:2771004159,burlywood:3736635391,burntsienna:3934150143,cadetblue:1604231423,chartreuse:2147418367,chocolate:3530104575,coral:4286533887,cornflowerblue:1687547391,cornsilk:4294499583,crimson:3692313855,cyan:16777215,darkblue:35839,darkcyan:9145343,darkgoldenrod:3095792639,darkgray:2846468607,darkgreen:6553855,darkgrey:2846468607,darkkhaki:3182914559,darkmagenta:2332068863,darkolivegreen:1433087999,darkorange:4287365375,darkorchid:2570243327,darkred:2332033279,darksalmon:3918953215,darkseagreen:2411499519,darkslateblue:1211993087,darkslategray:793726975,darkslategrey:793726975,darkturquoise:13554175,darkviolet:2483082239,deeppink:4279538687,deepskyblue:12582911,dimgray:1768516095,dimgrey:1768516095,dodgerblue:512819199,firebrick:2988581631,floralwhite:4294635775,forestgreen:579543807,fuchsia:4278255615,gainsboro:3705462015,ghostwhite:4177068031,gold:4292280575,goldenrod:3668254975,gray:2155905279,green:8388863,greenyellow:2919182335,grey:2155905279,honeydew:4043305215,hotpink:4285117695,indianred:3445382399,indigo:1258324735,ivory:4294963455,khaki:4041641215,lavender:3873897215,lavenderblush:4293981695,lawngreen:2096890111,lemonchiffon:4294626815,lightblue:2916673279,lightcoral:4034953471,lightcyan:3774873599,lightgoldenrodyellow:4210742015,lightgray:3553874943,lightgreen:2431553791,lightgrey:3553874943,lightpink:4290167295,lightsalmon:4288707327,lightseagreen:548580095,lightskyblue:2278488831,lightslategray:2005441023,lightslategrey:2005441023,lightsteelblue:2965692159,lightyellow:4294959359,lime:16711935,limegreen:852308735,linen:4210091775,magenta:4278255615,maroon:2147483903,mediumaquamarine:1724754687,mediumblue:52735,mediumorchid:3126187007,mediumpurple:2473647103,mediumseagreen:1018393087,mediumslateblue:2070474495,mediumspringgreen:16423679,mediumturquoise:1221709055,mediumvioletred:3340076543,midnightblue:421097727,mintcream:4127193855,mistyrose:4293190143,moccasin:4293178879,navajowhite:4292783615,navy:33023,oldlace:4260751103,olive:2155872511,olivedrab:1804477439,orange:4289003775,orangered:4282712319,orchid:3664828159,palegoldenrod:4008225535,palegreen:2566625535,paleturquoise:2951671551,palevioletred:3681588223,papayawhip:4293907967,peachpuff:4292524543,peru:3448061951,pink:4290825215,plum:3718307327,powderblue:2967529215,purple:2147516671,rebeccapurple:1714657791,red:4278190335,rosybrown:3163525119,royalblue:1097458175,saddlebrown:2336560127,salmon:4202722047,sandybrown:4104413439,seagreen:780883967,seashell:4294307583,sienna:2689740287,silver:3233857791,skyblue:2278484991,slateblue:1784335871,slategray:1887473919,slategrey:1887473919,snow:4294638335,springgreen:16744447,steelblue:1182971135,tan:3535047935,teal:8421631,thistle:3636451583,tomato:4284696575,turquoise:1088475391,violet:4001558271,wheat:4125012991,white:4294967295,whitesmoke:4126537215,yellow:4294902015,yellowgreen:2597139199}});const Vo=((e,{applyAnimatedValues:t=(()=>!1),createAnimatedStyle:n=(e=>new qt(e)),getComponentProps:o=(e=>e)}={})=>{const r={applyAnimatedValues:t,createAnimatedStyle:n,getComponentProps:o},a=e=>{const t=on(e)||"Anonymous";return(e=He.str(e)?a[e]||(a[e]=en(e,r)):e[nn]||(e[nn]=en(e,r))).displayName=`Animated(${t})`,e};return Ee(e,((t,n)=>{He.arr(e)&&(n=on(t)),a[n]=a(t)})),{animated:a}})(["a","abbr","address","area","article","aside","audio","b","base","bdi","bdo","big","blockquote","body","br","button","canvas","caption","cite","code","col","colgroup","data","datalist","dd","del","details","dfn","dialog","div","dl","dt","em","embed","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","iframe","img","input","ins","kbd","keygen","label","legend","li","link","main","map","mark","menu","menuitem","meta","meter","nav","noscript","object","ol","optgroup","option","output","p","param","picture","pre","progress","q","rp","rt","ruby","s","samp","script","section","select","small","source","span","strong","style","sub","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","title","tr","track","u","ul","var","video","wbr","circle","clipPath","defs","ellipse","foreignObject","g","image","line","linearGradient","mask","path","pattern","polygon","polyline","radialGradient","rect","stop","svg","text","tspan"],{applyAnimatedValues:function(e,t){if(!e.nodeType||!e.setAttribute)return!1;const n="filter"===e.nodeName||e.parentNode&&"filter"===e.parentNode.nodeName,o=t,{style:r,children:a,scrollTop:i,scrollLeft:s}=o,c=Ho(o,_o),l=Object.values(c),m=Object.keys(c).map((t=>n||e.hasAttribute(t)?t:Lo[t]||(Lo[t]=t.replace(/([A-Z])/g,(e=>"-"+e.toLowerCase())))));void 0!==a&&(e.textContent=a);for(let t in r)if(r.hasOwnProperty(t)){const n=Eo(t,r[t]);vo.test(t)?e.style.setProperty(t,n):e.style[t]=n}m.forEach(((t,n)=>{e.setAttribute(t,l[n])})),void 0!==i&&(e.scrollTop=i),void 0!==s&&(e.scrollLeft=s)},createAnimatedStyle:e=>new Ao(e),getComponentProps:e=>Ho(e,Po)}),Ro=Vo.animated;var jo=i(25158);i(12560);const Do=(0,e.forwardRef)((function(t,n){let{className:o,children:r,spokenMessage:i=r,politeness:s="polite",actions:c=[],onRemove:l=h.noop,icon:m=null,explicitDismiss:u=!1,onDismiss:d=null,__unstableHTML:p=!1}=t;function C(e){e&&e.preventDefault&&e.preventDefault(),d(),l()}d=d||h.noop,function(t,n){const o="string"==typeof t?t:(0,e.renderToString)(t);(0,e.useEffect)((()=>{o&&(0,jo.speak)(o,n)}),[o,n])}(i,s),(0,e.useEffect)((()=>{const e=setTimeout((()=>{u||(d(),l())}),1e4);return()=>clearTimeout(e)}),[u,d,l]);const g=W()(o,"components-snackbar",{"components-snackbar-explicit-dismiss":!!u});c&&c.length>1&&("undefined"!=typeof process&&process.env,c=[c[0]]);const f=W()("components-snackbar__content",{"components-snackbar__content-with-icon":!!m});return!0===p&&(r=(0,e.createElement)(e.RawHTML,null,r)),(0,e.createElement)("div",{ref:n,className:g,onClick:u?h.noop:C,tabIndex:"0",role:u?"":"button",onKeyPress:u?h.noop:C,"aria-label":u?"":(0,v.__)("Dismiss this notice","woocommerce")},(0,e.createElement)("div",{className:f},m&&(0,e.createElement)("div",{className:"components-snackbar__icon"},m),r,c.map(((t,n)=>{let{label:o,onClick:r,url:i}=t;return(0,e.createElement)(a.Button,{key:n,href:i,isTertiary:!0,onClick:e=>function(e,t){e.stopPropagation(),l(),t&&t(e)}(e,r),className:"components-snackbar__action"},o)})),u&&(0,e.createElement)("span",{role:"button","aria-label":"Dismiss this notice",tabIndex:"0",className:"components-snackbar__dismiss-button",onClick:C,onKeyPress:C},"✕")))})),Zo=function(t){let{notices:n,className:o,children:r,onRemove:a=h.noop,onRemove2:i=h.noop}=t;const s=(0,c.useReducedMotion)(),[l]=(0,e.useState)((()=>new WeakMap)),m=function(e,t,n){const o=He.fun(t)&&t,{reset:r,sort:a,trail:i=0,expires:s=!0,exitBeforeEnter:c=!1,onDestroyed:l,ref:m,config:u}=o?o():t,p=(0,d.useMemo)((()=>o||3==arguments.length?mo():void 0),[]),C=Le(e),h=[],g=(0,d.useRef)(null),f=r?null:g.current;Zt((()=>{g.current=h})),Pt((()=>()=>{ve(g.current,(e=>{e.expired&&clearTimeout(e.expirationId),yn(e.ctrl,p),e.ctrl.stop(!0)}))}));const y=Co(C,o?o():t,f),w=r&&g.current||[];Zt((()=>ve(w,(({ctrl:e,item:t,key:n})=>{yn(e,p),an(l,t,n)}))));const H=[];if(f&&ve(f,((e,t)=>{e.expired?(clearTimeout(e.expirationId),w.push(e)):~(t=H[t]=y.indexOf(e.key))&&(h[t]=e)})),ve(C,((e,t)=>{h[t]||(h[t]={key:y[t],item:e,phase:uo.MOUNT,ctrl:new $n},h[t].ctrl.item=e)})),H.length){let e=-1;const{leave:n}=o?o():t;ve(H,((t,o)=>{const r=f[o];~t?(e=h.indexOf(r),h[e]=rn({},r,{item:C[t]})):n&&h.splice(++e,0,r)}))}He.fun(a)&&h.sort(((e,t)=>a(e.item,t.item)));let _=-i;const v=Rt(),E=un(t),L=new Map,b=(0,d.useRef)(new Map),k=(0,d.useRef)(!1);ve(h,((e,n)=>{const r=e.key,a=e.phase,l=o?o():t;let d,p,C=an(l.delay||0,r);if(a==uo.MOUNT)d=l.enter,p=uo.ENTER;else{const e=y.indexOf(r)<0;if(a!=uo.LEAVE)if(e)d=l.leave,p=uo.LEAVE;else{if(!(d=l.update))return;p=uo.UPDATE}else{if(e)return;d=l.enter,p=uo.ENTER}}if(d=an(d,e.item,n),d=He.obj(d)?Cn(d):{to:d},!d.config){const t=u||E.config;d.config=an(t,e.item,n,p)}_+=i;const h=rn({},E,{delay:C+_,ref:m,immediate:l.immediate,reset:!1},d);if(p==uo.ENTER&&He.und(h.from)){const r=o?o():t,a=He.und(r.initial)||f?r.from:r.initial;h.from=an(a,e.item,n)}const{onResolve:w}=h;h.onResolve=e=>{an(w,e);const t=g.current,n=t.find((e=>e.key===r));if(n&&(!e.cancelled||n.phase==uo.UPDATE)&&n.ctrl.idle){const e=t.every((e=>e.ctrl.idle));if(n.phase==uo.LEAVE){const t=an(s,n.item);if(!1!==t){const o=!0===t?0:t;if(n.expired=!0,!e&&o>0)return void(o<=2147483647&&(n.expirationId=setTimeout(v,o)))}}e&&t.some((e=>e.expired))&&(b.current.delete(n),c&&(k.current=!0),v())}};const H=to(e.ctrl,h);p===uo.LEAVE&&c?b.current.set(e,{phase:p,springs:H,payload:h}):L.set(e,{phase:p,springs:H,payload:h})}));const S=(0,d.useContext)(io),M=Dt(S),N=S!==M&&gn(S);Zt((()=>{N&&ve(h,(e=>{e.ctrl.start({default:S})}))}),[S]),ve(L,((e,t)=>{if(b.current.size){const e=h.findIndex((e=>e.key===t.key));h.splice(e,1)}})),Zt((()=>{ve(b.current.size?b.current:L,(({phase:e,payload:t},n)=>{const{ctrl:o}=n;n.phase=e,null==p||p.add(o),N&&e==uo.ENTER&&o.start({default:S}),t&&(wn(o,t.ref),o.ref&&!k.current?o.update(t):(o.start(t),k.current&&(k.current=!1)))}))}),r?void 0:n);const x=e=>d.createElement(d.Fragment,null,h.map(((t,n)=>{const{springs:o}=L.get(t)||t.ctrl,r=e(rn({},o),t.item,t,n);return r&&r.type?d.createElement(r.type,rn({},r.props,{key:He.str(t.key)||He.num(t.key)?t.key:t.ctrl.id,ref:r.ref})):r})));return p?[x,p]:x}(n,{keys:e=>e.id,from:{opacity:0,height:0},enter:e=>async t=>await t({opacity:1,height:l.get(e).offsetHeight}),leave:()=>async e=>{await e({opacity:0}),await e({height:0})},immediate:s});o=W()("components-snackbar-list",o);const u=e=>()=>{a(e.id),i(e.id)};return(0,e.createElement)("div",{className:o},r,m(((t,n)=>(0,e.createElement)(Ro.div,{style:t},(0,e.createElement)("div",{className:"components-snackbar-list__notice-container",ref:e=>e&&l.set(n,e)},(0,e.createElement)(Do,(0,ee.Z)({},(0,h.omit)(n,["content"]),{onRemove:u(n)}),n.content))))))},Bo="woocommerce_admin_transient_notices_queue";function Fo(t){const{removeNotice:n}=(0,l.useDispatch)("core/notices"),{createNotice:r,removeNotice:a}=(0,l.useDispatch)("core/notices2"),{updateOptions:i}=(0,l.useDispatch)(o.OPTIONS_STORE_NAME),{currentUser:s={},notices:c=[],notices2:m=[],noticesQueue:u={}}=(0,l.useSelect)((e=>({currentUser:e(o.USER_STORE_NAME).getCurrentUser(),notices:e("core/notices").getNotices(),notices2:e("core/notices2").getNotices(),noticesQueue:e(o.OPTIONS_STORE_NAME).getOption(Bo)})));(0,e.useEffect)((()=>{Object.values(u).filter((e=>e.user_id===s.id||!e.user_id)).forEach((e=>{const t=(0,_.applyFilters)("woocommerce_admin_queued_notice_filter",e);r(t.status,t.content,{onDismiss:()=>{(e=>{const t={...u};delete t[e],i({[Bo]:t})})(t.id)},...t.options||{}})}))}),[]);const{className:d}=t,p=W()("woocommerce-transient-notices","components-notices__snackbar",d),C=c.concat(m);return(0,e.createElement)(w.WooFooterItem,null,(0,e.createElement)(Zo,{notices:C,className:p,onRemove:n,onRemove2:a}))}Fo.propTypes={className:C().string,notices:C().array};var zo=i(23374),Wo=i(91250),Uo=i(12532);const Go=()=>(0,e.createElement)("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,e.createElement)("mask",{id:"mask0_2915:6733",maskUnits:"userSpaceOnUse",x:"4",y:"3",width:"16",height:"18"},(0,e.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M4.5 3.5H13.5L13.9 5.5H19.5V15.5H12.5L12.1 13.5H6.5V20.5H4.5V3.5ZM12.26 7.5L11.86 5.5H6.5V11.5H13.74L14.14 13.5H17.5V7.5H12.26Z",fill:"white"})),(0,e.createElement)("g",{mask:"url(#mask0_2915:6733)"},(0,e.createElement)("rect",{width:"24",height:"24",fill:"#50575E"})));var Yo=i(23322);const Qo={page:1,per_page:o.QUERY_DEFAULTS.pageSize,status:"unactioned",type:o.QUERY_DEFAULTS.noteTypes,orderby:"date",order:"desc"};function qo(e){const{getNotes:t,getNotesError:n,isResolving:r}=e(o.NOTES_STORE_NAME),{getCurrentUser:a}=e(o.USER_STORE_NAME),i=a(),s=parseInt(i&&i.woocommerce_meta&&i.woocommerce_meta.activity_panel_inbox_last_read,10);if(!s)return null;t(Qo);const c=Boolean(n("getNotes",[Qo])),l=r("getNotes",[Qo]);if(c||l)return null;const m=t(Qo);return(0,Yo.fs)(m,s)>0}const Jo=t=>{let{icon:n,title:o,name:r,unread:i,selected:s,isPanelOpen:c,onTabClick:l}=t;const m=W()("woocommerce-layout__activity-panel-tab",{"is-active":c&&s,"has-unread":i}),u=`activity-panel-tab-${r}`;return(0,e.createElement)(a.Button,{role:"tab",className:m,"aria-selected":s,"aria-controls":`activity-panel-${r}`,key:u,id:u,onClick:()=>{l(r)}},n,o," ",i&&(0,e.createElement)("span",{className:"screen-reader-text"},(0,v.__)("unread activity","woocommerce")))},Ko=t=>{let{tabs:n,onTabClick:o,selectedTab:r,tabOpen:i=!1}=t;const[{tabOpen:s,currentTab:c},l]=(0,e.useState)({tabOpen:i,currentTab:r});return(0,e.useEffect)((()=>{l({tabOpen:i,currentTab:r})}),[i,r]),(0,e.createElement)(a.NavigableMenu,{role:"tablist",orientation:"horizontal",className:"woocommerce-layout__activity-panel-tabs"},n&&n.map(((t,n)=>{if(t.component){const{component:o,options:r}=t;return(0,e.createElement)(o,(0,ee.Z)({key:n},r))}return(0,e.createElement)(Jo,(0,ee.Z)({key:n,index:n,isPanelOpen:s,selected:c===t.name},t,{onTabClick:()=>{const e=c!==t.name&&""!==c||!s;e&&c===t.name||(0,f.recordEvent)("activity_panel_open",{tab:t.name}),l({tabOpen:e,currentTab:t.name}),o(t,e)}}))})))},Xo=t=>{let{setupTasksComplete:n,setupCompletePercent:o}=t;return(0,e.createElement)("svg",{className:"woocommerce-layout__activity-panel-tab-icon setup-progress",viewBox:"0 0 25 25"},(0,e.createElement)("path",{className:"setup-progress-ring",d:"M 12.476 23.237 C 18.369 23.237 23.146 18.414 23.146 12.464 C 23.146 6.512 18.369 1.687 12.476 1.687 C 6.581 1.687 1.803 6.512 1.803 12.464 C 1.803 18.414 6.581 23.237 12.476 23.237 Z"}),(0,e.createElement)("path",{className:"setup-progress-slice",transform:"matrix(-0.034188, 0, 0, 0.034134, 38.373184, -8.278505)",d:"M 522 607 A 237 237 0 0 1 759 370 L 759 607 Z",fill:n>0?"currentColor":"white"}),(0,e.createElement)("path",{className:"setup-progress-slice",transform:"matrix(-0.034188, 0, 0, -0.034134, 38.368454, 33.13131)",d:"M 522 607 A 237 237 0 0 1 759 370 L 759 607 Z",fill:o>=50?"currentColor":"white"}),(0,e.createElement)("path",{className:"setup-progress-slice",transform:"matrix(0.034188, 0, 0, -0.034134, -13.500516, 33.133827)",d:"M 522 607 A 237 237 0 0 1 759 370 L 759 607 Z",fill:o>=75?"currentColor":"white"}),(0,e.createElement)("path",{className:"setup-progress-slice",transform:"matrix(0.034188, 0, 0, 0.034134, -13.495783, -8.281025)",d:"M 522 607 A 237 237 0 0 1 759 370 L 759 607 Z",fill:"white"}))};var $o=i(92342),er=i(70261);const tr="highlight-tooltip__show";function nr(t){let{title:n,closeButtonText:o,content:r,show:i=!0,id:s,onClose:c,delay:l,onShow:m=h.noop,useAnchor:u=!1}=t;const[d,p]=(0,e.useState)(l>0?null:i),[C,g]=(0,e.useState)(null),[f,y]=(0,e.useState)(null);function w(){if(u){const e=document.getElementById(s);y(e.getBoundingClientRect())}}(0,e.useEffect)((()=>{const e=document.getElementById(s);let t,n;e&&!C&&(u?(n=document.createElement("div"),document.body.appendChild(n)):n=e.parentElement,t=document.createElement("div"),t.classList.add("highlight-tooltip__container"),n.appendChild(t),g(t));const o=H(t);return()=>{if(t){const e=t.parentElement;e.removeChild(t),u&&e.remove()}o&&clearTimeout(o)}}),[]),(0,e.useEffect)((()=>{!d&&C&&C.classList.remove(tr)}),[d]),(0,e.useEffect)((()=>{i!==d&&null!==d&&C&&(p(i),i?C&&H(C):C.classList.remove(tr))}),[i]),(0,e.useLayoutEffect)((()=>(window.addEventListener("resize",w),()=>window.removeEventListener("resize",w))),[]);const H=e=>{let t=null;return l>0?t=setTimeout((()=>{t=null,_(e)}),l):d||_(e),t},_=e=>{const t=document.getElementById(s);t&&u&&y(t.getBoundingClientRect()),e&&e.classList.add(tr),p(!0),m()},E=()=>{p(!1),c&&c()};return C?(0,e.createPortal)((0,e.createElement)("div",{className:"highlight-tooltip__portal"},d?(0,e.createElement)(e.Fragment,null,(0,e.createElement)(a.IsolatedEventContainer,{className:"highlight-tooltip__overlay"}),(0,e.createElement)(a.Popover,{className:"highlight-tooltip__popover",noArrow:!1,anchorRect:f,focusOnMount:"container"},(0,e.createElement)(a.Card,{size:"medium"},(0,e.createElement)(a.CardHeader,null,n,(0,e.createElement)(a.Button,{isSmall:!0,onClick:E,icon:er.Z})),(0,e.createElement)(a.CardBody,null,r||null),(0,e.createElement)(a.CardFooter,{isBorderless:!0},(0,e.createElement)(a.Button,{size:"small",isPrimary:!0,onClick:E},o||(0,v.__)("Close","woocommerce")))))):null),C):null}nr.propTypes={id:C().string.isRequired,title:C().string.isRequired,closeButtonText:C().string.isRequired,content:C().oneOfType([C().string,C().node]),show:C().bool,onClose:C().func,delay:C().number,onShow:C().func,useAnchor:C().bool};var or=i(45904);const rr=["button","submit"];const ar=t=>{let{content:n,isPanelOpen:o,isPanelSwitching:r,currentTab:a,tab:i,closePanel:s,clearPanel:c}=t;const l="woocommerce-layout__activity-panel-wrapper",m=function(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"firstElement";const n=(0,e.useRef)(t);return(0,e.useEffect)((()=>{n.current=t}),[t]),(0,e.useCallback)((e=>{if(!e||!1===n.current)return;if(e.contains(e.ownerDocument.activeElement))return;let t=e;if("firstElement"===n.current){const n=or.focus.tabbable.find(e)[0];n&&(t=n)}t.focus()}),[])}(),u=function(t){const n=(0,e.useRef)(t);(0,e.useEffect)((()=>{n.current=t}),[t]);const o=(0,e.useRef)(!1),r=(0,e.useRef)(),a=(0,e.useCallback)((()=>{clearTimeout(r.current)}),[]);(0,e.useEffect)((()=>()=>a()),[]),(0,e.useEffect)((()=>{t||a()}),[t,a]);const i=(0,e.useCallback)((e=>{const{type:t,target:n}=e;(0,h.includes)(["mouseup","touchend"],t)?o.current=!1:function(e){if(!(e instanceof window.HTMLElement))return!1;switch(e.nodeName){case"A":case"BUTTON":return!0;case"INPUT":return(0,h.includes)(rr,e.type)}return!1}(n)&&(o.current=!0)}),[]),s=(0,e.useCallback)((e=>{e.persist(),o.current||(r.current=setTimeout((()=>{document.hasFocus()?"function"==typeof n.current&&n.current(e):e.preventDefault()}),0))}),[]);return{onFocus:a,onMouseDown:i,onMouseUp:i,onTouchStart:i,onTouchEnd:i,onBlur:s}}((e=>{const t=e.relatedTarget&&(e.relatedTarget.closest(".woocommerce-inbox-dismiss-confirmation_modal")||e.relatedTarget.closest(".components-snackbar__action"));o&&!t&&s()})),d=(0,e.useRef)(null),p=(0,e.useCallback)((e=>{d.current=e,m(e)}),[]);if(!i)return(0,e.createElement)("div",{className:l});if(!n)return null;const C=W()(l,{"is-open":o,"is-switching":r});return(0,e.createElement)("div",(0,ee.Z)({className:C,tabIndex:0,role:"tabpanel","aria-label":i.title,onTransitionEnd:e=>{e&&"transform"===e.propertyName&&(c(),d.current&&o&&i&&m(d.current))}},u,{ref:p}),(0,e.createElement)("div",{className:"woocommerce-layout__activity-panel-content",key:"activity-panel-"+a,id:"activity-panel-"+a},(0,e.createElement)(e.Suspense,{fallback:(0,e.createElement)(E.Spinner,null)},n)))};var ir=i(22983),sr=i(14138),cr=i(30554),lr=i(34374),mr=i(74693);const ur="woocommerce_ces_product_feedback_shown",dr=t=>{let{currentTab:n}=t;const r=(()=>{const{hasShownTour:e}=(0,l.useSelect)((e=>{const{getOption:t}=e(o.OPTIONS_STORE_NAME);return{hasShownTour:t(ur)}}));return e})(),[a,i]=(0,e.useState)(!1),s=(0,e.useRef)(null),{updateOptions:c}=(0,l.useDispatch)(o.OPTIONS_STORE_NAME),m=()=>{clearTimeout(s.current),s.current=null};return(0,e.useEffect)((()=>{if(!1===r)return s.current=setTimeout((()=>{i(!0)}),42e4),()=>m()}),[r]),(0,e.useEffect)((()=>{a&&c({[ur]:!0})}),[a]),"feedback"===n&&(a||s.current)&&(i(!1),m()),a?(0,e.createElement)(E.TourKit,{config:{steps:[{referenceElements:{desktop:"#activity-panel-tab-feedback"},meta:{name:"product-feedback-tour-1",heading:(0,v.__)("🫣 Feeling stuck?","woocommerce"),descriptions:{desktop:(0,v.__)("You have been working on this product for a few minutes now. Is there something you're struggling with? Share your feedback.","woocommerce")},primaryButton:{isHidden:!0}}}],placement:"bottom-start",options:{effects:{liveResize:{mutation:!0,resize:!0}}},closeHandler:()=>{i(!1)}}}):null},pr=(0,e.lazy)((()=>Promise.all([i.e(9529),i.e(6732)]).then(i.bind(i,37015)))),Cr=(0,e.lazy)((()=>Promise.all([i.e(7846),i.e(8851)]).then(i.bind(i,72122)))),hr=(0,e.lazy)((()=>i.e(7844).then(i.bind(i,28029)))),gr=t=>{let{isEmbedded:a,query:i}=t;const[s,c]=(0,e.useState)(""),[m,u]=(0,e.useState)(!1),[d,p]=(0,e.useState)(!1),[C,y]=(0,e.useState)(!1),{fills:H}=(0,Y.useSlot)(cr.$Q),_=Boolean(null==H?void 0:H.length),{updateUserPreferences:L,...b}=(0,o.useUserPreferences)(),k=(0,q.useActiveSetupTasklist)();(0,e.useEffect)((()=>(0,g.addHistoryListener)((()=>{F(),z()}))),[]);const S=(0,w.useExtendLayout)("activity-panel"),M=(e,t)=>{let n={};if("wc-admin"===i.page&&"appearance"===i.task){var r,a,s;const{getTaskLists:i}=e(o.ONBOARDING_STORE_NAME),c=i().reduce(((e,t)=>[...e,...t.tasks]),[]).find((e=>"appearance"===e.id));n={set_notice:t("woocommerce_demo_store_notice")?"Y":"N",create_homepage:!0===(null==c||null===(r=c.additionalData)||void 0===r?void 0:r.hasHomepage)?"Y":"N",upload_logo:null!=c&&null!==(a=c.additionalData)&&void 0!==a&&null!==(s=a.themeMods)&&void 0!==s&&s.custom_logo?"Y":"N"}}return n};function N(e,t,n){const o=(0,ir.sg)(e),r=!!t&&(0,ir.xX)(e,o)>0,a=!!t&&(0,sr.Vh)(e),i=!!t&&(0,ir.ae)(e);return n>0||r||a||i||_}const{hasUnreadNotes:x,hasAbbreviatedNotifications:T,isCompletedTask:I,thingsToDoNextCount:A,requestingTaskListOptions:O,setupTaskListComplete:P,setupTaskListHidden:V,setupTasksCompleteCount:R,setupTasksCount:j,previewSiteBtnTrackData:D}=(0,l.useSelect)((e=>{var t,n;const{getOption:r}=e(o.OPTIONS_STORE_NAME),{getTask:a,getTaskList:s,hasFinishedResolution:c}=e(o.ONBOARDING_STORE_NAME),l=s(k),m=null===(t=null==l?void 0:l.isHidden)||void 0===t||t,u=(0,o.getVisibleTasks)((null==l?void 0:l.tasks)||[]),d=(p=s("extended"))&&p.tasks.length&&!p.isHidden?p.tasks.filter((e=>e.canView&&!e.isComplete&&!e.isDismissed)).length:0;var p;return{hasUnreadNotes:qo(e),hasAbbreviatedNotifications:N(e,m,d),thingsToDoNextCount:d,requestingTaskListOptions:!c("getTaskLists"),setupTaskListComplete:null==l?void 0:l.isComplete,setupTaskListHidden:m,setupTasksCount:u.length,setupTasksCompleteCount:u.filter((e=>e.isComplete)).length,isCompletedTask:Boolean(i.task&&(null===(n=a(i.task))||void 0===n?void 0:n.isComplete)),previewSiteBtnTrackData:M(e,r)}})),{showCesModal:Z}=(0,l.useDispatch)(n.STORE_KEY),{currentUserCan:B}=(0,o.useUser)(),F=()=>{u(!0),p(!1)},z=()=>{d||(u(!1),y(!1),c(""))},W=()=>"wc-admin"===i.page&&!i.path,U=()=>{const[e]=function(e){var t,n;const o=null!=e&&e.startsWith("/")?1:0,r=null!=e&&e.endsWith("/")?-1:void 0;return null!==(t=null==e||null===(n=e.slice(o,r))||void 0===n?void 0:n.split("/"))&&void 0!==t?t:[]}(i.path);return"add-product"===e||"product"===e},G=()=>{const e=(0,lr.DP)(window.location.search);return a&&/post-new\.php$/.test(window.location.pathname)&&"product"===(null==e?void 0:e.post_type)},Q=()=>i.task&&!i.path&&(!0===O||!1===V&&!1===P),J=()=>{const t={name:"activity",title:(0,v.__)("Activity","woocommerce"),icon:(0,e.createElement)(Go,null),unread:x||T,visible:(a||!W())&&!Q()&&!U()},n={name:"feedback",title:(0,v.__)("Feedback","woocommerce"),icon:(0,e.createElement)(mr.A,null),onClick:()=>{c("feedback"),p(!0),Z({action:"product_feedback",title:(0,v.__)("How's your experience with the product editor?","woocommerce"),firstQuestion:(0,v.__)("The product editing screen is easy to use","woocommerce"),secondQuestion:(0,v.__)("The product editing screen's functionality meets my needs","woocommerce")},{onRecordScore:()=>{c(""),p(!1)},onCloseModal:()=>{c(""),p(!1)}},{type:"snackbar",icon:(0,e.createElement)("span",null,"🌟")})},visible:G()},o={name:"setup",title:(0,v.__)("Finish setup","woocommerce"),icon:(0,e.createElement)(Xo,{setupTasksComplete:R,setupCompletePercent:Math.ceil(R/j*100)}),visible:B("manage_woocommerce")&&!O&&!P&&!V&&!W()&&!U()},s={name:"help",title:(0,v.__)("Help","woocommerce"),icon:(0,e.createElement)(zo.Z,{icon:Wo.Z}),visible:B("manage_woocommerce")&&(W()&&!a||Q())},l={component:$o.T,visible:B("manage_woocommerce")&&!a&&W()&&!Q()};return[t,n,o,{name:"previewSite",title:(0,v.__)("Preview site","woocommerce"),icon:(0,e.createElement)(zo.Z,{icon:Uo.Z}),visible:W()&&"appearance"===i.task,onClick:()=>(window.open((0,r.O3)("siteUrl")),(0,f.recordEvent)("wcadmin_tasklist_previewsite",D),null)},{name:"previewStore",title:(0,v.__)("Preview store","woocommerce"),icon:(0,e.createElement)(zo.Z,{icon:Uo.Z}),visible:W()&&"appearance"!==i.task,onClick:()=>(window.open((0,r.O3)("shopUrl")),(0,f.recordEvent)("wcadmin_previewstore_click"),null)},l,s].filter((e=>e.visible))},K=J(),X=(0,h.uniqueId)("activity-panel-header_"),$=(()=>{const{task:e}=i,t=b&&b.task_list_tracked_started_tasks,n=b&&b.help_panel_highlight_shown;return!(!(e&&"yes"!==n&&(t||{})[e]>1)||I)})();return(0,e.createElement)(w.LayoutContextProvider,{value:S},(0,e.createElement)("div",null,(0,e.createElement)(E.H,{id:X,className:"screen-reader-text"},(0,v.__)("Store Activity","woocommerce")),(0,e.createElement)(E.Section,{component:"aside",id:"woocommerce-activity-panel",className:"woocommerce-layout__activity-panel","aria-labelledby":X},(0,e.createElement)(Ko,{tabs:K,tabOpen:d,selectedTab:s,onTabClick:(e,t)=>{e.onClick?e.onClick():((e,t)=>{let{name:n}=e;const o=n!==s&&""!==s&&t&&d;m||(c(n),p(t),y(o))})(e,t)}}),(0,e.createElement)(ar,{currentTab:!0,isPanelOpen:d,isPanelSwitching:C,tab:(0,h.find)(J(),{name:s}),content:(t=>{const{task:n}=i;switch(t){case"activity":return(0,e.createElement)(Cr,{hasAbbreviatedNotifications:T,thingsToDoNextCount:A});case"help":return(0,e.createElement)(pr,{taskName:n});case"setup":return(0,e.createElement)(hr,{query:i});default:return null}})(s),closePanel:()=>F(),clearPanel:()=>z()})),G()&&(0,e.createElement)(dr,{currentTab:s}),$?(0,e.createElement)(nr,{delay:1e3,useAnchor:!0,title:(0,v.__)("We're here for help","woocommerce"),content:(0,v.__)("If you have any questions, feel free to explore the WooCommerce docs listed here.","woocommerce"),closeButtonText:(0,v.__)("Got it","woocommerce"),id:"activity-panel-tab-help",onClose:()=>((0,f.recordEvent)("help_tooltip_click"),void(b&&L&&L({help_panel_highlight_shown:"yes"}))),onShow:()=>(0,f.recordEvent)("help_tooltip_view")}):null))};gr.defaultProps={getHistory:g.getHistory};const fr=gr;(0,y.registerPlugin)("activity-panel-header-item",{render:()=>(0,e.createElement)(w.WooHeaderItem,{order:20},(t=>{let{isEmbedded:n,query:o}=t;return window.wcAdminFeatures["activity-panels"]?(0,e.createElement)(fr,{isEmbedded:n,query:o}):null})),scope:"woocommerce-admin"});var yr=i(46290);const wr="android",Hr=()=>(0,e.createElement)("svg",{width:"37",height:"37",viewBox:"0 0 92 92",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,e.createElement)("rect",{width:"92",height:"92",rx:"21.3953",fill:"#7F54B3"}),(0,e.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M72.5937 28.043H19.8094C16.4781 28.0459 13.7783 30.7705 13.7754 34.1324V54.4501C13.7783 57.812 16.4781 60.5366 19.8094 60.5395H44.8229L56.2573 66.9607L53.6672 60.5395H72.599C74.2009 60.5402 75.7374 59.8983 76.8702 58.7552C78.0029 57.612 78.639 56.0614 78.6383 54.4447V34.1324C78.6376 32.5157 78.0002 30.9657 76.8664 29.8235C75.7327 28.6814 74.1956 28.0408 72.5937 28.043ZM19.1057 32.4208C18.4658 32.4324 17.8646 32.7359 17.467 33.2482C17.0888 33.7635 16.9404 34.4175 17.058 35.0502C18.5962 45.0986 20.0338 51.8757 21.371 55.3816C21.8779 56.658 22.4896 57.2703 23.2063 57.2185C24.3075 57.1489 25.6263 55.5968 27.1627 52.5621C27.9964 50.8412 29.2602 48.2662 30.9539 44.837C32.3785 49.88 34.309 53.6787 36.7456 56.2331C37.4291 56.9436 38.1204 57.2748 38.8195 57.2266C39.4185 57.1931 39.953 56.8315 40.217 56.2813C40.4753 55.7358 40.5806 55.1278 40.5211 54.5248C40.3516 52.0703 40.5919 48.667 41.2421 44.3149C41.9081 39.8057 42.7523 36.5818 43.7749 34.6432C43.9822 34.2526 44.0733 33.8087 44.037 33.366C44.0039 32.7587 43.7116 32.1969 43.2374 31.829C42.7745 31.4367 42.1799 31.2446 41.5803 31.2935C40.8334 31.3325 40.1682 31.7885 39.8499 32.4797C38.2331 35.5019 37.0812 40.4109 36.3943 47.2068C35.2823 44.2394 34.4509 41.1703 33.9114 38.0412C33.623 36.4613 32.9037 35.7125 31.7536 35.7946C30.9592 35.8589 30.3063 36.3944 29.7819 37.4012L24.0348 48.5643C23.0997 44.6692 22.2205 39.9289 21.3972 34.3433C21.1997 32.9652 20.4358 32.3244 19.1057 32.4208ZM69.9089 34.6877C71.6969 35.0381 73.2407 36.2 74.1186 37.8559C74.9693 39.3247 75.3946 41.1161 75.3946 43.23C75.4148 45.9567 74.7062 48.6357 73.3477 50.9687C71.7778 53.7023 69.7195 55.0691 67.1727 55.0691C66.6933 55.0668 66.2153 55.0128 65.7467 54.9078C63.9584 54.5581 62.4143 53.396 61.5371 51.7396C60.6864 50.2452 60.261 48.4411 60.261 46.3272C60.2357 43.6127 60.945 40.9454 62.3079 38.6295C63.9023 35.8959 65.9607 34.5291 68.4829 34.5291C68.9623 34.5304 69.4402 34.5836 69.9089 34.6877ZM68.7937 49.4848C69.7707 48.5773 70.4399 47.2269 70.8012 45.4337V45.4419C70.9315 44.7826 70.9959 44.1112 70.9933 43.4382C70.986 42.5849 70.8291 41.74 70.5302 40.9452C70.1443 39.901 69.6304 39.3124 68.9884 39.1793C68.0378 38.9643 67.1239 39.5256 66.2469 40.8632C65.5812 41.8393 65.109 42.9432 64.8577 44.1106C64.7276 44.7708 64.6632 45.4432 64.6657 46.1171C64.6739 46.9677 64.8308 47.8096 65.1287 48.6019C65.5146 49.6388 66.0294 50.2274 66.6731 50.3678C67.3169 50.5081 68.0237 50.2138 68.7937 49.4848ZM57.9079 37.8559C57.0291 36.2008 55.4854 35.0392 53.6976 34.6877C53.2279 34.5837 52.749 34.5306 52.2687 34.5291C49.7443 34.5291 47.6856 35.8959 46.0927 38.6295C44.7295 40.9454 44.0201 43.6127 44.0454 46.3272C44.0454 48.4411 44.4699 50.2452 45.319 51.7396C46.1976 53.3949 47.7414 54.5566 49.5294 54.9078C49.999 55.0126 50.4779 55.0667 50.9582 55.0691C53.5055 55.0691 55.5642 53.7023 57.1343 50.9687C58.4922 48.6355 59.2001 45.9565 59.1789 43.23C59.1789 41.1161 58.7544 39.3247 57.9053 37.8559H57.9079ZM54.5903 45.4337C54.2307 47.2269 53.5614 48.5773 52.5825 49.4848C51.8115 50.2065 51.101 50.5017 50.4589 50.3678C49.8169 50.2338 49.3011 49.6461 48.9169 48.6019C48.6181 47.8097 48.4603 46.9678 48.4511 46.1171C48.4495 45.4431 48.5148 44.7707 48.6459 44.1106C48.8971 42.9432 49.3694 41.8393 50.0353 40.8632C50.9124 39.5256 51.8264 38.9643 52.7773 39.1793C53.4193 39.3124 53.9333 39.901 54.3193 40.9452C54.617 41.7404 54.7739 42.585 54.7824 43.4382C54.785 44.1112 54.7207 44.7826 54.5903 45.4419V45.4337Z",fill:"white"})),_r="wcadmin_mobile_android_banner_click",vr="woocommerce-layout__show-app-banner",Er=t=>{let{onInstall:n,onDismiss:o}=t;const[r,i]=(0,e.useState)(!1),s=(/iPhone|iPad|iPod/i.test(window.navigator.userAgent)?"ios":/Android/i.test(window.navigator.userAgent)?wr:"unknown")===wr&&!r;return(0,e.useEffect)((()=>{const e=document.getElementsByClassName("woocommerce-layout")[0];return s&&e&&e.classList.add(vr),()=>{e&&e.classList.remove(vr)}}),[s]),s?(0,e.createElement)("div",{className:"woocommerce-mobile-app-banner"},(0,e.createElement)(zo.Z,{icon:(0,e.createElement)(yr.Z,{"data-testid":"dismiss-btn"}),onClick:()=>{o(),i(!0),(0,f.recordEvent)(_r,{action:"dismiss"})}}),(0,e.createElement)(Hr,null),(0,e.createElement)("div",{className:"woocommerce-mobile-app-banner__description"},(0,e.createElement)("p",{className:"woocommerce-mobile-app-banner__description__text"},(0,v.__)("Run your store from anywhere","woocommerce")),(0,e.createElement)("p",{className:"woocommerce-mobile-app-banner__description__text"},(0,v.__)("Download the WooCommerce app","woocommerce"))),(0,e.createElement)(a.Button,{href:"https://play.google.com/store/apps/details?id=com.woocommerce.android",isSecondary:!0,onClick:()=>{n(),i(!0),(0,f.recordEvent)(_r,{action:"install"})}},(0,v.__)("Install","woocommerce"))):null};(0,y.registerPlugin)("mobile-banner-header-item",{render:()=>{const{updateUserPreferences:t,...n}=(0,o.useUserPreferences)(),r=()=>{t({android_app_banner_dismissed:"yes"})};return"yes"===n.android_app_banner_dismissed?null:(0,e.createElement)(w.WooHeaderItem,null,(0,e.createElement)(Er,{onDismiss:r,onInstall:r}))},scope:"woocommerce-admin"});const Lr=e=>0===e.indexOf("http")?e:(0,G.getAdminLink)(e),br=e=>{const t=e.replace(/[-\/\\^$*+?.()|[\]{}]/gi,"\\$&"),[n,o,r]=t.split(/\\\?|#/),a=r?`(.*#${r}$)`:"";return"^"+n+(o?o.split("&").reduce(((e,t)=>`${e}(?=.*[?|&]${t}(&|$|#))`),""):"")+a},kr=e=>{let t=null,n=0;return e.forEach((e=>{const o=function(e,t){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;if(!t)return 0;const o=Lr(t),{href:r}=e;if(o===r)return Number.MAX_SAFE_INTEGER;const a=br(o),i=new RegExp(n||a,"i");return(decodeURIComponent(r).match(i)||[]).length}(window.location,e.url,e.matchExpression);o>0&&o>=n&&(n=o,t=e)})),t||null},Sr=["primary","favorites","plugins","secondary"],Mr={woocommerce:{id:"woocommerce",isCategory:!0,menuId:"primary",migrate:!0,order:10,parent:"",title:"WooCommerce"}};var Nr=i(70444);const xr=(0,e.createElement)(Nr.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"-2 -2 24 24"},(0,e.createElement)(Nr.Path,{d:"M20 10c0-5.51-4.49-10-10-10C4.48 0 0 4.49 0 10c0 5.52 4.48 10 10 10 5.51 0 10-4.48 10-10zM7.78 15.37L4.37 6.22c.55-.02 1.17-.08 1.17-.08.5-.06.44-1.13-.06-1.11 0 0-1.45.11-2.37.11-.18 0-.37 0-.58-.01C4.12 2.69 6.87 1.11 10 1.11c2.33 0 4.45.87 6.05 2.34-.68-.11-1.65.39-1.65 1.58 0 .74.45 1.36.9 2.1.35.61.55 1.36.55 2.46 0 1.49-1.4 5-1.4 5l-3.03-8.37c.54-.02.82-.17.82-.17.5-.05.44-1.25-.06-1.22 0 0-1.44.12-2.38.12-.87 0-2.33-.12-2.33-.12-.5-.03-.56 1.2-.06 1.22l.92.08 1.26 3.41zM17.41 10c.24-.64.74-1.87.43-4.25.7 1.29 1.05 2.71 1.05 4.25 0 3.29-1.73 6.24-4.4 7.78.97-2.59 1.94-5.2 2.92-7.78zM6.1 18.09C3.12 16.65 1.11 13.53 1.11 10c0-1.3.23-2.48.72-3.59C3.25 10.3 4.67 14.2 6.1 18.09zm4.03-6.63l2.58 6.98c-.86.29-1.76.45-2.71.45-.79 0-1.57-.11-2.29-.33.81-2.38 1.62-4.74 2.42-7.1z"})),Tr=()=>{const t=(0,G.getSetting)("siteTitle",""),n=(0,G.getSetting)("homeUrl",""),o=Q(),[r,i]=(0,e.useState)(document.body.classList.contains(!1)),s="is-wc-nav-folded",c="is-wc-nav-expanded",m=()=>{document.body.classList.add(s),document.body.classList.remove(c),i(!0)},u=()=>{document.body.classList.remove(s),document.body.classList.add(c),i(!1)},d=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:document.body.clientWidth;e<=960?m():u()};(0,e.useEffect)((()=>{d();const e=[{eventName:"orientationchange",handler:e=>d(e.target.screen.availWidth)},{eventName:"resize",handler:(0,h.debounce)((()=>d()),200)}];for(const{eventName:t,handler:n}of e)window.addEventListener(t,n,!1);(0,g.addHistoryListener)((()=>d()))}),[]);let p=(0,e.createElement)(zo.Z,{size:"36px",icon:xr});const{isRequestingSiteIcon:C,siteIconUrl:f}=(0,l.useSelect)((e=>{const{isResolving:t}=e("core/data"),{getEntityRecord:n}=e("core"),o=n("root","__unstableBase",void 0)||{};return{isRequestingSiteIcon:t("core","getEntityRecord",["root","__unstableBase",void 0]),siteIconUrl:o.siteIconUrl}}));f?p=(0,e.createElement)("img",{alt:(0,v.__)("Site Icon","woocommerce"),src:f}):C&&(p=null);const y=W()("woocommerce-navigation-header",{"is-scrolled":o});return(0,e.createElement)("div",{className:y},(0,e.createElement)(a.Button,{onClick:()=>{document.body.classList.contains(s)?u():m()},className:"woocommerce-navigation-header__site-icon","aria-label":"Fold navigation",role:"switch","aria-checked":r?"true":"false"},p),(0,e.createElement)(a.Button,{title:t,href:n,className:"woocommerce-navigation-header__site-title",as:"span"},(0,U.decodeEntities)(t)))},Ir=(0,e.createElement)(Nr.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,e.createElement)(Nr.Path,{d:"M11.776 4.454a.25.25 0 01.448 0l2.069 4.192a.25.25 0 00.188.137l4.626.672a.25.25 0 01.139.426l-3.348 3.263a.25.25 0 00-.072.222l.79 4.607a.25.25 0 01-.362.263l-4.138-2.175a.25.25 0 00-.232 0l-4.138 2.175a.25.25 0 01-.363-.263l.79-4.607a.25.25 0 00-.071-.222L4.754 9.881a.25.25 0 01.139-.426l4.626-.672a.25.25 0 00.188-.137l2.069-4.192z"})),Ar=(0,e.createElement)(Nr.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,e.createElement)(Nr.Path,{fillRule:"evenodd",d:"M9.706 8.646a.25.25 0 01-.188.137l-4.626.672a.25.25 0 00-.139.427l3.348 3.262a.25.25 0 01.072.222l-.79 4.607a.25.25 0 00.362.264l4.138-2.176a.25.25 0 01.233 0l4.137 2.175a.25.25 0 00.363-.263l-.79-4.607a.25.25 0 01.072-.222l3.347-3.262a.25.25 0 00-.139-.427l-4.626-.672a.25.25 0 01-.188-.137l-2.069-4.192a.25.25 0 00-.448 0L9.706 8.646zM12 7.39l-.948 1.921a1.75 1.75 0 01-1.317.957l-2.12.308 1.534 1.495c.412.402.6.982.503 1.55l-.362 2.11 1.896-.997a1.75 1.75 0 011.629 0l1.895.997-.362-2.11a1.75 1.75 0 01.504-1.55l1.533-1.495-2.12-.308a1.75 1.75 0 01-1.317-.957L12 7.39z",clipRule:"evenodd"})),Or=t=>{let{id:n}=t;const{favorites:r,isResolving:i}=(0,l.useSelect)((e=>({favorites:e(o.NAVIGATION_STORE_NAME).getFavorites(),isResolving:e(o.NAVIGATION_STORE_NAME).isResolving("getFavorites")}))),{addFavorite:s,removeFavorite:c}=(0,l.useDispatch)(o.NAVIGATION_STORE_NAME),m=r.includes(n);return i?null:(0,e.createElement)(a.Button,{id:"woocommerce-navigation-favorite-button",className:"woocommerce-navigation-favorite-button",isTertiary:!0,onClick:()=>{(m?c:s)(n),(0,f.recordEvent)("navigation_favorite",{id:n,action:m?"unfavorite":"favorite"})},"aria-label":m?(0,v.__)("Add this item to your favorites.","woocommerce"):(0,v.__)("Remove this item from your favorites.","woocommerce")},(0,e.createElement)(zo.Z,{icon:m?Ir:Ar,className:`star-${m?"filled":"empty"}-icon`}))},Pr="woocommerce_navigation_favorites_tooltip_hidden",Vr=()=>{const{isFavoritesResolving:t,isOptionResolving:n,isTooltipHidden:r}=(0,l.useSelect)((e=>{const{getOption:t,isResolving:n}=e(o.OPTIONS_STORE_NAME);return{isFavoritesResolving:e(o.NAVIGATION_STORE_NAME).isResolving("getFavorites"),isOptionResolving:n("getOption",[Pr]),isTooltipHidden:"yes"===t(Pr)}})),{updateOptions:a}=(0,l.useDispatch)(o.OPTIONS_STORE_NAME);return t||r||n||document.body.classList.contains("is-wc-nav-folded")?null:(0,e.createElement)(nr,{delay:1e3,title:(0,v.__)("Introducing favorites","woocommerce"),content:(0,v.__)("You can now favorite your extensions to pin them in the top level of the navigation.","woocommerce"),closeButtonText:(0,v.__)("Got it","woocommerce"),id:"woocommerce-navigation-favorite-button",onClose:()=>a({[Pr]:"yes"}),useAnchor:!0})},Rr=t=>{let{category:n}=t;const{id:o,menuId:r,title:a}=n,i="woocommerce-navigation-category-title";return["plugins","favorites"].includes(r)?(0,e.createElement)("span",{className:i},(0,e.createElement)("span",{className:`${i}__text`},a),(0,e.createElement)(Or,{id:o}),(0,e.createElement)(Vr,null)):(0,e.createElement)("span",{className:i},a)},jr=t=>{var n;let{item:o}=t;const r=(0,Y.useSlot)("woocommerce_navigation_"+o.id),a=Boolean(null==r||null===(n=r.fills)||void 0===n?void 0:n.length),i=e=>{(0,f.recordEvent)("navigation_click",{menu_item:e})};return a&&!o.isCategory?(0,e.createElement)(Y.NavigationItem,{key:o.id,item:o.id},(0,e.createElement)("div",{onClick:()=>i(o.id)},(0,e.createElement)(g.WooNavigationItem.Slot,{name:o.id}))):(0,e.createElement)(Y.NavigationItem,{key:o.id,item:o.id,title:o.title,badge:o.badge?o.badge:null,href:o.url,navigateToMenu:!o.url&&o.id,onClick:()=>i(o.id),hideIfTargetMenuEmpty:!0})},Dr=t=>{let{category:n,onBackClick:o,pluginItems:r,primaryItems:a}=t;if(!a.length&&!r.length)return null;const i=(0,_.applyFilters)("woocommerce_navigation_root_back_label",(0,v.__)("WordPress Dashboard","woocommerce")),s=(0,_.applyFilters)("woocommerce_navigation_root_back_url",window.wcNavigation.rootBackUrl),c="woocommerce"===n.id&&s;return(0,e.createElement)(Y.NavigationMenu,{title:(0,e.createElement)(Rr,{category:n}),menu:n.id,parentMenu:n.parent,backButtonLabel:c?i:n.backButtonLabel||null,onBackButtonClick:c?()=>{o("woocommerce"),window.location=s}:()=>o(n.id)},!!a.length&&(0,e.createElement)(Y.NavigationGroup,null,a.map((t=>(0,e.createElement)(jr,{key:t.id,item:t})))),!!r.length&&(0,e.createElement)(Y.NavigationGroup,{title:"woocommerce"===n.id?(0,v.__)("Extensions","woocommerce"):null},r.map((t=>(0,e.createElement)(jr,{key:t.id,item:t})))))},Zr=t=>{let{category:n,items:o,onBackClick:r}=t;if(!o.length)return null;const a="woocommerce"===n.id;return(0,e.createElement)(Y.NavigationMenu,{className:"components-navigation__menu-secondary",title:!a&&(0,e.createElement)(Rr,{category:n}),menu:n.id,parentMenu:n.parent,backButtonLabel:n.backButtonLabel||null,onBackButtonClick:a?null:()=>r(n.id)},(0,e.createElement)(Y.NavigationGroup,{onBackButtonClick:()=>r(n.id)},o.map((t=>(0,e.createElement)(jr,{key:t.id,item:t})))))},Br=(0,o.withNavigationHydration)(window.wcNavigation)((()=>{const{menuItems:t}=(0,l.useSelect)((e=>({menuItems:e(o.NAVIGATION_STORE_NAME).getMenuItems()})));(0,e.useEffect)((()=>{document.documentElement.classList.remove("wp-toolbar"),document.body.classList.add("has-woocommerce-navigation");const e=document.getElementById("adminmenumain");e&&e.classList.add("folded")}),[]);const[n,r]=(0,e.useState)("woocommerce-home"),[a,i]=(0,e.useState)("woocommerce");(0,e.useEffect)((()=>{const e=kr(t);return e&&n!==e&&(r(e),i(e.parent)),(0,g.addHistoryListener)((()=>{setTimeout((()=>{const e=kr(t);e&&(r(e),i(e.parent))}),0)}))}),[t]);const{currentUserCan:s}=(0,o.useUser)(),{categories:c,items:m}=(0,e.useMemo)((()=>((e,t)=>{const n={...Mr},o=(e=>e.sort(((e,t)=>e.order===t.order?e.title.localeCompare(t.title):e.order-t.order)))(e).reduce(((e,o)=>{if(e[o.parent]||(e[o.parent]={},Sr.forEach((t=>{e[o.parent][t]=[]}))),!e[o.parent][o.menuId])return e;if(t&&o.capability&&!t(o.capability))return e;o.isCategory&&(n[o.id]=o);const r=e[o.parent][o.menuId];return r&&r.push(o),e}),{});return{items:o,categories:n}})(t,s)),[t,s]),u=(0,e.useRef)(null),d=e=>{(0,f.recordEvent)("navigation_back_click",{category:e})},p="woocommerce"===a,C=W()("woocommerce-navigation",{"is-root":p});return(0,e.createElement)("div",{className:C},(0,e.createElement)(Tr,null),(0,e.createElement)("div",{className:"woocommerce-navigation__wrapper",ref:u},(0,e.createElement)(Y.Navigation,{activeItem:n?n.id:null,activeMenu:a,onActivateMenu:function(){u&&u.current&&(u.current.scrollTop=0),i(...arguments)}},Object.values(c).map((t=>{const n=m[t.id];return!!n&&[(0,e.createElement)(Dr,{key:t.id,category:t,onBackClick:d,primaryItems:[...n.primary,...n.favorites],pluginItems:n.plugins}),(0,e.createElement)(Zr,{key:`secondary/${t.id}`,category:t,onBackClick:d,items:n.secondary})]})))))}));(0,y.registerPlugin)("wc-admin-navigation",{render:()=>{const{persistedQuery:t}=(0,l.useSelect)((e=>({persistedQuery:e(o.NAVIGATION_STORE_NAME).getPersistedQuery()})));if(!window.wcAdminFeatures.navigation)return null;if(!(0,g.isWCAdmin)())return(0,e.createElement)(w.WooHeaderNavigationItem,{order:-100},(0,e.createElement)(Br,null));const n=(0,L.Z)().filter((e=>e.navArgs)),r=B().filter((e=>e.navArgs)).map((e=>"/analytics/settings"===e.path?{...e,breadcrumbs:[(0,v.__)("Analytics","woocommerce")]}:e));return(0,e.createElement)(e.Fragment,null,(0,e.createElement)(w.WooHeaderNavigationItem,{order:-100},(0,e.createElement)(Br,null)),r.map((n=>(0,e.createElement)(g.WooNavigationItem,{item:n.navArgs.id,key:n.navArgs.id},(0,e.createElement)(E.Link,{className:"components-button",href:(0,g.getNewPath)((0,g.pathIsExcluded)(n.path)?{}:t,n.path,{}),type:"wc-admin"},n.breadcrumbs[n.breadcrumbs.length-1])))),n.map((n=>(0,e.createElement)(g.WooNavigationItem,{item:n.navArgs.id,key:n.navArgs.id},(0,e.createElement)(E.Link,{className:"components-button",href:(0,g.getNewPath)((0,g.pathIsExcluded)(n.report)?{}:t,`/analytics/${n.report}`,{}),type:"wc-admin"},n.title)))))},scope:"woocommerce-navigation"});const Fr=(0,e.lazy)((()=>i.e(8597).then(i.bind(i,4578)))),zr=(0,e.lazy)((()=>Promise.resolve().then(i.bind(i,52260))));class Wr extends e.Component{render(){const{children:t}=this.props;return(0,e.createElement)("div",{className:"woocommerce-layout__primary",id:"woocommerce-layout__primary"},window.wcAdminFeatures["store-alerts"]&&(0,e.createElement)(e.Suspense,{fallback:null},(0,e.createElement)(Fr,null)),(0,e.createElement)($,null),t)}}const Ur=e=>{let{children:t}=e;const n=(0,u.TH)(),o=(0,u.bS)(n.pathname),r={params:(0,u.UO)(),url:o.pathname};return d.Children.toArray(t).map((e=>(0,d.cloneElement)(e,{...e.props,location:n,match:r})))};function Gr(t){var o,r;let{activePlugins:i,installedPlugins:s,isEmbedded:c,isJetpackConnected:l,location:m,match:u,page:d}=t;!function(t){(0,e.useEffect)((()=>{if(!t.path)return;const e=`woocommerce-admin-page_${"/"===(n=t.path)?"_home":n.replace(/:[a-zA-Z?]+/g,(function(e){return(t=e,t.replace(/[A-Z]/g,(e=>`-${e.toLowerCase()}`))).replace(":","");var t})).replace(/\//g,"_")}`;var n;return document.body.classList.add(e),()=>{document.body.classList.remove(e)}}),[t.path])}(d),(0,e.useEffect)((()=>{(0,n.triggerExitPageCesSurvey)()}),[]),(0,e.useEffect)((()=>{!function(){const e={has_navigation:!!window.wcNavigation};if(c){const t=document.location.pathname+document.location.search;return void(0,f.recordPageView)(t,{is_embedded:!0,...e})}const{pathname:t}=m;if(!t)return;let n=t.substring(1).replace(/\//g,"_");0===n.length&&(n="home_screen"),(0,f.recordPageView)(n,{jetpack_installed:s.includes("jetpack"),jetpack_active:i.includes("jetpack"),jetpack_connected:l,...e})}(),setTimeout((()=>{(0,n.triggerExitPageCesSurvey)()}),0)}),[null==m?void 0:m.pathname]);const{breadcrumbs:p,layout:C={header:!0,footer:!0}}=d,{header:H=!0,footer:_=!0}=C,v=(0,g.getQuery)();return(0,e.createElement)(w.LayoutContextProvider,{value:(0,w.getLayoutContextValue)([(null==d||null===(o=d.navArgs)||void 0===o||null===(r=o.id)||void 0===r?void 0:r.toLowerCase())||"page"])},(0,e.createElement)(a.SlotFillProvider,null,(0,e.createElement)("div",{className:"woocommerce-layout"},H&&(0,e.createElement)(J,{sections:(0,h.isFunction)(p)?p({match:u}):p,isEmbedded:c,query:v}),(0,e.createElement)(Fo,null),!c&&(0,e.createElement)(Wr,null,(0,e.createElement)("div",{className:"woocommerce-layout__main"},(0,e.createElement)(F,{page:d,match:u,query:v}))),c&&function(){const{page:e,section:t,tab:n}=(0,g.getQuery)();return"wc-settings"===e&&"checkout"===n&&"woocommerce_payments"===t}()&&(0,e.createElement)(e.Suspense,{fallback:null},(0,e.createElement)(zr,null)),_&&(0,e.createElement)(K,null),(0,e.createElement)(n.CustomerEffortScoreModalContainer,null)),(0,e.createElement)(y.PluginArea,{scope:"woocommerce-admin"}),window.wcAdminFeatures.navigation&&(0,e.createElement)(y.PluginArea,{scope:"woocommerce-navigation"}),(0,e.createElement)(y.PluginArea,{scope:"woocommerce-tasks"})))}Gr.propTypes={isEmbedded:C().bool,page:C().shape({container:C().oneOfType([C().func,C().object]),path:C().string,breadcrumbs:C().oneOfType([C().func,C().arrayOf(C().oneOfType([C().arrayOf(C().string),C().string]))]).isRequired,wpOpenMenu:C().string}).isRequired};const Yr=(0,r.O3)("dataEndpoints"),Qr=(0,c.compose)((0,o.withPluginsHydration)({...(0,r.O3)("plugins",{}),jetpackStatus:Yr&&Yr.jetpackStatus||!1}),(0,l.withSelect)(((e,t)=>{let{isEmbedded:n}=t;if(n)return;const{getActivePlugins:r,getInstalledPlugins:a,isJetpackConnected:i}=e(o.PLUGINS_STORE_NAME);return{activePlugins:r(),isJetpackConnected:i(),installedPlugins:a()}})))((t=>t.isEmbedded?(0,e.createElement)(Gr,t):(0,e.createElement)(Ur,null,(0,e.createElement)(Gr,t)))),qr=(0,c.compose)(window.wcSettings.admin?(0,o.withOptionsHydration)({...(0,r.O3)("preloadOptions",{})}):h.identity)((()=>{const{currentUserCan:t}=(0,o.useUser)(),n=document.location.pathname,r=n.substring(0,n.lastIndexOf("/"));return(0,e.createElement)(m.M,{history:(0,g.getHistory)()},(0,e.createElement)(u.Z5,{basename:r},B().filter((e=>!e.capability||t(e.capability))).map((t=>(0,e.createElement)(u.AW,{key:t.path,path:t.path,exact:!0,element:(0,e.createElement)(Qr,{page:t})})))))})),Jr=(0,c.compose)((0,r.O3)("preloadOptions")?(0,o.withOptionsHydration)({...(0,r.O3)("preloadOptions")}):h.identity)((()=>(0,e.createElement)(Qr,{page:{breadcrumbs:(0,r.O3)("embedBreadcrumbs",[])},isEmbedded:!0}))),Kr="woocommerce_show_marketplace_suggestions",Xr=t=>{let{children:n}=t;const{currentUserCan:r}=(0,o.useUser)(),a=(0,l.useSelect)((e=>{const{getOption:t,hasFinishedResolution:n}=e(o.OPTIONS_STORE_NAME),r=n("getOption",[Kr]),a="no"!==t(Kr);return r&&a}));return r("install_plugins")&&a?(0,e.createElement)(e.Fragment,null,n):null},$r=(0,e.lazy)((()=>i.e(5396).then(i.bind(i,22079)))),ea=(0,e.lazy)((()=>window.wcAdminFeatures["shipping-smart-defaults"]?i.e(6125).then(i.bind(i,57418)):i.e(6125).then(i.bind(i,15883)))),ta="woocommerce_store_address",na="woocommerce_store_city",oa="woocommerce_store_postcode",ra=e=>{const t=document.querySelector(e);return!!t&&t.value.length>0},aa=()=>{const{isLoading:t,show:n}=(()=>{const{hasFilledStoreAddress:e,isLoading:t}=(0,l.useSelect)((e=>{const{hasFinishedResolution:t,getOption:n}=e(o.OPTIONS_STORE_NAME);return{isLoading:!t("getOption",[ta])||!t("getOption",[na])||!t("getOption",[oa]),hasFilledStoreAddress:""!==n(ta)&&""!==n(na)&&""!==n(oa)}}));return{isLoading:t,show:!t&&!e}})(),[r,a]=(0,e.useState)(!1),i={steps:[{referenceElements:{desktop:"#store_address-description + table.form-table"},meta:{name:"store-location-tour-step-1",heading:"Add your store location",descriptions:{desktop:(0,v.__)('Add your store location details to help us configure shipping, taxes, currency and more in a fully automated way. Once done, click on the "Save" button at the end of the form.',"woocommerce")},primaryButton:{text:(0,v.__)("Got it","woocommerce")}}}],placement:"bottom-start",options:{effects:{liveResize:{mutation:!0,resize:!0},spotlight:{styles:{inset:"0px auto auto -8px",paddingInline:"8px"},interactivity:{enabled:!0}}}},closeHandler:(e,t,n)=>{const o={address_1:ra("input#woocommerce_store_address"),address_2:ra("input#woocommerce_store_address_2"),city:ra("input#woocommerce_store_city"),postcode:ra("input#woocommerce_store_postcode")};(0,f.recordEvent)("settings_store_address_tour_dismiss",{source:n,fields_filled:o}),a(!0)}};return r||t||!n?null:(0,e.createElement)(E.TourKit,{config:i})},ia=[t=>{let{page:n,tab:o,section:r}=t;return"wc-settings"!==n||"checkout"!==o||r?null:(0,e.createElement)(Xr,null,(0,e.createElement)(e.Suspense,{fallback:null},(0,e.createElement)($r,null)))},t=>{let{page:n,tab:o,section:r,zone_id:a}=t;return"wc-settings"!==n||"shipping"!==o||Boolean(r)||Boolean(a)?null:(0,e.createElement)(Xr,null,(0,e.createElement)(e.Suspense,{fallback:null},(0,e.createElement)(ea,null)))},t=>{let{page:n,tab:o,tutorial:r}=t;return"wc-settings"!==n||"general"!==o&&void 0!==o?null:r?(0,e.createElement)(aa,null):null}],sa=()=>{(0,e.useEffect)((()=>{(0,n.triggerExitPageCesSurvey)()}),[]);const t=(0,H.parse)(location.search.substring(1));let o={page:"",tab:""};void 0!==t.page&&(o=t);const r=(0,_.applyFilters)("woocommerce_admin_embedded_layout_components",ia,o);return(0,e.createElement)(w.LayoutContextProvider,{value:(0,w.getLayoutContextValue)(["page"])},(0,e.createElement)("div",{className:"woocommerce-embedded-layout__primary",id:"woocommerce-embedded-layout__primary"},r.map(((t,n)=>(0,e.createElement)(t,(0,ee.Z)({key:n},o))))))};var ca=i(5060),la=i(90523);const ma=()=>{(0,f.recordEvent)("settings_payments_banner_connect_click")},ua=()=>{const t=(0,G.getAdminLink)("admin.php?wcpay-connect=1&_wpnonce="+(0,r.O3)("wcpay_welcome_page_connect_nonce")),n=(0,r.O3)("isWooPayEligible");return(0,e.createElement)(ca.WCPayBanner,null,(0,e.createElement)(ca.WCPayBannerBody,{textPosition:"right",actionButton:(0,e.createElement)(a.Button,{href:t,isPrimary:!0,onClick:ma},(0,v.__)("Get started","woocommerce")),isWooPayEligible:n}),(0,e.createElement)(ca.WCPayBannerFooter,{isWooPayEligible:n}))},da=()=>(0,e.createElement)(e.Fragment,null,(0,e.createElement)("h2",null,(0,v.__)("Payment Methods","woocommerce")),(0,e.createElement)("div",{id:"payment_gateways_options-description"},(0,e.createElement)("p",null,(0,v.__)("Installed payment methods are listed below and can be sorted to control their display order on the frontend.","woocommerce")))),pa=()=>{const{hasFinishedResolution:t,shouldShowBanner:n}=(()=>{const{installedPaymentGateways:e,paymentGatewaySuggestions:t,hasFinishedResolution:n}=(0,l.useSelect)((e=>({installedPaymentGateways:e(o.PAYMENT_GATEWAYS_STORE_NAME).getPaymentGateways(),paymentGatewaySuggestions:e(o.ONBOARDING_STORE_NAME).getPaymentGatewaySuggestions(),hasFinishedResolution:e(o.ONBOARDING_STORE_NAME).hasFinishedResolution("getPaymentGatewaySuggestions")&&e(o.PAYMENT_GATEWAYS_STORE_NAME).hasFinishedResolution("getPaymentGateways")}))),r=e.some((e=>"woocommerce_payments"===e.id)),a=e.find((e=>"woocommerce_payments"===e.id&&!1===e.enabled));return{hasFinishedResolution:n,shouldShowBanner:(0,la.j)(t)&&r&&a}})();return t&&n?(0,e.createElement)(ua,null):(0,e.createElement)(da,null)},{Fill:Ca,Slot:ha}=(0,a.createSlotFill)("__EXPERIMENTAL__WcAdminPaymentsGatewaysSettingsBanner");(0,y.registerPlugin)("woocommerce-admin-paymentsgateways-settings-banner",{scope:"woocommerce-settings",render:()=>(0,e.createElement)(Ca,null,(0,e.createElement)(pa,null))});var ga=i(75283),fa=i(28601),ya=i(31863);const{Fill:wa,Slot:Ha}=(0,a.createSlotFill)("__EXPERIMENTAL__WcAdminConflictError"),_a=()=>(0,e.createElement)(a.Button,{href:"https://woocommerce.com/document/setting-up-taxes-in-woocommerce/",target:"_blank"},(0,v.__)("Learn more","woocommerce"));function va(e,t){let n=parseInt(e.slice(1,3),16),o=parseInt(e.slice(3,5),16),r=parseInt(e.slice(5,7),16);return n=Math.floor(255*(1-t)+t*n),o=Math.floor(255*(1-t)+t*o),r=Math.floor(255*(1-t)+t*r),"#"+n.toString(16).padStart(2,"0")+o.toString(16).padStart(2,"0")+r.toString(16).padStart(2,"0")}(0,y.registerPlugin)("woocommerce-admin-tax-settings-conflict-warning",{scope:"woocommerce-settings",render:()=>{const[n,o]=(0,e.useState)(!1),[r,i]=(0,e.useState)("yes"===document.forms.mainform.elements.woocommerce_prices_include_tax.value?"incl":"excl"),[s,c]=(0,e.useState)(window.jQuery("#woocommerce_tax_display_shop").val()),[m,u]=(0,e.useState)(window.jQuery("#woocommerce_tax_display_cart").val()),{createNotice:d}=(0,l.useDispatch)(t.store),p=()=>{window.jQuery("#woocommerce_tax_display_shop").val(r).trigger("change"),window.jQuery("#woocommerce_tax_display_cart").val(r).trigger("change"),d("success",(0,v.__)("Recommended settings applied.","woocommerce")),(0,f.recordEvent)("tax_settings_conflict_recommended_settings_clicked")};(0,e.useEffect)((()=>{document.querySelectorAll("input[name='woocommerce_prices_include_tax']").forEach((e=>{e.addEventListener("change",(()=>i("yes"===document.forms.mainform.elements.woocommerce_prices_include_tax.value?"incl":"excl")))}))}),[]),(0,e.useEffect)((()=>{window.jQuery("#woocommerce_tax_display_shop").on("click change",(()=>c(document.getElementById("woocommerce_tax_display_shop").value)))}),[]),(0,e.useEffect)((()=>{window.jQuery("#woocommerce_tax_display_cart").on("click change",(()=>u(document.getElementById("woocommerce_tax_display_cart").value)))}),[]);const[C,h]=(0,e.useState)(!1);return(0,e.useEffect)((()=>{s===r&&m===r?h(!1):(h(!0),(0,f.recordEvent)("tax_settings_conflict",{main:r,shop:s,cart:m}))}),[m,s,r]),!C||n?(0,e.createElement)(wa,null):(0,e.createElement)(wa,null,(0,e.createElement)("div",{className:"woocommerce_tax_settings_conflict_error"},(0,e.createElement)(a.Card,null,(0,e.createElement)(a.CardBody,{className:"woocommerce_tax_settings_conflict_error_card_body"},(0,e.createElement)("div",null,(0,e.createElement)("img",{className:"woocommerce_tax_settings_conflict_error_card_body__warning_icon",src:ya,alt:"Warning Icon"})),(0,e.createElement)("div",null,(0,e.createElement)("div",{className:"woocommerce_tax_settings_conflict_error_card_body__body_text"},(0,e.createElement)("p",{style:{fontSize:13}},(0,ga.Z)({mixedString:(0,v.__)("{{b}}Inconsistent tax settings:{{/b}} To avoid possible rounding errors, prices should be entered and displayed consistently in all locations either including, or excluding taxes.","woocommerce"),components:{b:(0,e.createElement)("b",null)}}))),(0,e.createElement)("div",{className:"woocommerce_tax_settings_conflict_error_card_body__buttons"},(0,e.createElement)((()=>(0,e.createElement)(a.Button,{variant:"primary",onClick:p},(0,v.__)("Use recommended settings","woocommerce"))),null)," ",(0,e.createElement)(_a,null))),(0,e.createElement)("div",null,(0,e.createElement)(a.Button,{className:"woocommerce_tax_settings_conflict_error_card_body__close_icon",onClick:()=>{o(!0),(0,f.recordEvent)("tax_settings_conflict_dismissed")}},(0,e.createElement)(zo.Z,{icon:fa.Z})))))))}}),i.p=i.g.wcAdminAssets.path;const Ea=i.u;i.u=e=>`${Ea(e)}?ver=${window.wcAdminAssets.version}`;const La=i.miniCssF;i.miniCssF=e=>`${La(e)}?ver=${window.wcAdminAssets.version}`;const ba=document.getElementById("root"),ka=document.getElementById("woocommerce-embedded-root"),Sa="wc_admin",Ma=(0,r.O3)("currentUserData");if((()=>{const e=window.getComputedStyle(document.body).getPropertyValue("--wp-admin-theme-color").trim();document.documentElement.style.setProperty("--wp-admin-theme-color-background-04",va(e,.04)),document.documentElement.style.setProperty("--wp-admin-theme-color-background-25",va(e,.25))})(),ba){let t=(0,o.withSettingsHydration)(Sa,window.wcSettings.admin)(qr);const n=!!window.wcSettings.admin&&window.wcSettings.admin.preloadSettings;n&&n.general&&(t=(0,o.withSettingsHydration)("general",{general:n.general})(t)),Ma&&(t=(0,o.withCurrentUserHydration)(Ma)(t)),(0,e.render)((0,e.createElement)(t,null),ba)}else if(ka){let t=(0,o.withSettingsHydration)(Sa,window.wcSettings.admin)(Jr);Ma&&(t=(0,o.withCurrentUserHydration)(Ma)(t)),(0,e.render)((0,e.createElement)(t,null),ka),ka.classList.remove("is-embed-loading");const n=document.getElementById("wpbody-content"),r=document.getElementById("wc_payment_gateways_banner_slotfill");r&&(0,e.render)((0,e.createElement)(e.Fragment,null,(0,e.createElement)(a.SlotFillProvider,null,(0,e.createElement)(ha,null),(0,e.createElement)(y.PluginArea,{scope:"woocommerce-settings"}))),r);const i=document.getElementById("wc_conflict_error_slotfill");i&&(0,e.render)((0,e.createElement)(e.Fragment,null,(0,e.createElement)(a.SlotFillProvider,null,(0,e.createElement)(Ha,null),(0,e.createElement)(y.PluginArea,{scope:"woocommerce-settings"}))),i);const s=n.querySelector(".wrap.woocommerce")||document.querySelector("#wpbody-content > .woocommerce")||n.querySelector(".wrap"),c=document.createElement("div");(0,e.render)((0,e.createElement)("div",{className:"woocommerce-layout"},(0,e.createElement)(Wr,null)),n.insertBefore(c,s));const l=document.createElement("div");(0,e.render)((0,e.createElement)(sa,null),n.insertBefore(l,s.nextSibling))}window.wcAdminFeatures&&!0===window.wcAdminFeatures["customer-effort-score-tracks"]&&function(){const t=ba||ka;(0,e.render)((0,e.createElement)(n.CustomerEffortScoreTracksContainer,null),t.insertBefore(document.createElement("div"),null))}()})(),(window.wc=window.wc||{}).app=s})();index.js.LICENSE.txt000064400000001030151547157720010120 0ustar00/*!
  Copyright (c) 2018 Jed Watson.
  Licensed under the MIT License (MIT), see
  http://jedwatson.github.io/classnames
*/

/*! @license DOMPurify 2.3.6 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.6/LICENSE */

/**
 * React Router v6.3.0
 *
 * Copyright (c) Remix Software Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE.md file in the root directory of this source tree.
 *
 * @license MIT
 */
style-rtl.css000064400000210307151547157720007236 0ustar00.woocommerce-admin-page .wrap{margin:0}.woocommerce-admin-page #wpcontent,.woocommerce-admin-page.woocommerce_page_wc-admin #wpbody-content{padding:0;overflow-x:hidden!important;min-height:calc(100vh - 32px)}@media(min-width:783px){.woocommerce-admin-page #wpbody-content{padding-right:0}}@media(max-width:782px){.woocommerce-admin-page .wp-responsive-open #woocommerce-embedded-root,.woocommerce-admin-page .wp-responsive-open #wpbody{position:relative;left:-14.5em}.woocommerce-admin-page #wpbody-content,.woocommerce-admin-page #wpcontent{min-height:calc(100vh - 46px)}}@media(min-width:961px){.woocommerce-admin-page #toplevel_page_wcadmin--analytics.menu-top>a:focus,.woocommerce-admin-page #toplevel_page_woocommerce.menu-top>a:focus{padding-bottom:1px}}#wpbody,.woocommerce-layout *{box-sizing:border-box}#wpbody{display:inline-block;width:100%;padding-top:0;margin-top:60px}#wpfooter{display:none}.woocommerce_page_wc-admin .woocommerce-filters-date__content:not(.is-mobile){z-index:2}@media(max-width:600px){#wpadminbar{position:fixed}html.wp-toolbar{padding-top:46px}}@media(max-width:782px){.jetpack-masterbar #wpadminbar #wp-admin-bar-menu-toggle{margin-top:-10px}.jetpack-masterbar #wpwrap .woocommerce-layout__header-heading{padding-right:60px}.jetpack-masterbar.wp-admin .wrap h1,.jetpack-masterbar.wp-admin .wrap h2{padding-right:0}}.woocommerce-admin-page .wp-has-current-submenu:after{left:0;content:" ";height:0;width:0;position:absolute;pointer-events:none;border:8px solid transparent;border-left-color:#f1f1f1;top:0;margin-top:10px}@media(max-width:960px){.woocommerce-admin-page .wp-has-current-submenu:after{border-width:4px;margin-top:14px}}:root{--large-gap:40px;--main-gap:24px}@media(max-width:960px){:root{--large-gap:24px}}@media(max-width:782px){:root{--large-gap:16px;--main-gap:16px}}@keyframes loading-fade{0%{opacity:.7}50%{opacity:1}to{opacity:.7}}.woocommerce-layout select:hover{color:#1a1a1a}.woocommerce-layout .components-base-control select.components-select-control__input{max-width:100%;line-height:normal}.woocommerce-layout .components-panel__body>.components-panel__body-title:hover,.woocommerce-layout .woocommerce-experimental-list__item:hover,.woocommerce-layout .woocommerce-inbox-message:hover{background:#f6f7f7}.woocommerce-layout__jitm .jitm-card{margin:3rem 1.25rem 1.25rem}.woocommerce-layout__jitm-hide{display:none}body.woocommerce-admin-page .components-button.is-primary:not(:disabled):not([aria-disabled=true]):hover{color:#fff}body.woocommerce-admin-page .components-snackbar .components-button.is-tertiary{color:#fff}body.woocommerce-admin-page .components-snackbar .components-button.is-tertiary:not(:disabled):not([aria-disabled=true]):hover{color:#fff}.woocommerce-embed-page #wpbody .woocommerce-layout,.woocommerce-embed-page .woocommerce-layout__notice-list-hide+.wrap{padding-top:10px}.woocommerce-embed-page #wpbody-content,.woocommerce-embed-page #wpcontent{overflow-x:initial!important}.woocommerce-embed-page #wpbody-content{padding-top:0}.woocommerce-embed-page #wpbody-content .notice{margin-top:15px}.woocommerce-embed-page .wrap{padding:0 20px}@media(max-width:782px){.woocommerce-embed-page .wrap p.search-box{width:calc(100% - 40px)}}.woocommerce-embed-page .wrap .wrap{padding:0}.woocommerce-embed-page #screen-meta{border-left:0;margin:0}.woocommerce-embed-page #screen-meta-links{position:relative}.woocommerce-embed-page .notice{padding:1px 12px}.woocommerce-embed-page .woocommerce-layout__header.is-scrolled{box-shadow:0 8px 16px 0 rgba(85,93,102,.3)}.woocommerce-embed-page .woocommerce-layout__header .woocommerce-layout__header-heading{margin-top:0;margin-bottom:0}.woocommerce-embed-page #screen-meta-links.is-hidden-by-notices,.woocommerce-embed-page #screen-meta.is-hidden-by-notices{display:none!important}.woocommerce-embed-page .woocommerce-layout__primary{margin:0}@media(max-width:782px){.woocommerce-embed-page .woocommerce-layout__primary{padding-top:10px}}@keyframes isLoaded{0%{opacity:0}to{opacity:1}}.woocommerce-embed-page .woocommerce-layout__activity-panel-tabs{animation:isLoaded;animation-duration:2s}.woocommerce-embed-page .woocommerce-layout__notice-list-show{margin-top:10px;margin-bottom:16px}@media(max-width:600px){.woocommerce-embed-page .woocommerce-layout__notice-list-show{margin-top:80px;margin-bottom:-16px}}@media(min-width:601px)and (max-width:782px){.woocommerce-embed-page .woocommerce-layout__notice-list-show{margin-top:32px}}.woocommerce-embed-page .woocommerce-activity-card__actions a.components-button:not(.is-primary){color:#2e4453}.woocommerce-layout{margin:0;padding:0}.woocommerce-layout__no-match{display:flex;align-items:center;justify-content:center}.woocommerce-layout__no-match .components-card{max-width:680px;width:100%}.woocommerce-layout__primary{margin:var(--large-gap) var(--large-gap) 128px 0}@media(max-width:782px){.woocommerce-layout__primary{margin-top:20px}}.woocommerce-layout .woocommerce-layout__main{padding-left:40px;padding-left:var(--large-gap);max-width:100%}.woocommerce-admin-is-loading #adminmenumain,.woocommerce-admin-is-loading #wpadminbar,.woocommerce-admin-is-loading #wpbody-content,.woocommerce-admin-is-loading #wpcontent,.woocommerce-admin-is-loading #wpfooter,.woocommerce-admin-is-loading .components-modal__screen-overlay,.woocommerce-admin-is-loading .error,.woocommerce-admin-is-loading .notice,.woocommerce-admin-is-loading .update-nag,.woocommerce-admin-is-loading .updated,.woocommerce-admin-is-loading .woocommerce-layout__header,.woocommerce-admin-is-loading .woocommerce-message,.woocommerce-admin-is-loading .woocommerce-store-alerts,.woocommerce-admin-page .update-nag{display:none}.woocommerce-admin-full-screen{background:#f6f7f7;color:#50575e;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif}.woocommerce-admin-full-screen #wpwrap{top:0}.woocommerce-admin-full-screen #wpbody-content{min-height:100vh!important}.woocommerce-admin-full-screen #adminmenumain,.woocommerce-admin-full-screen #wpcontent>*,.woocommerce-admin-full-screen .error,.woocommerce-admin-full-screen .notice,.woocommerce-admin-full-screen .update-nag,.woocommerce-admin-full-screen .updated,.woocommerce-admin-full-screen .woocommerce-layout__header,.woocommerce-admin-full-screen .woocommerce-message,.woocommerce-admin-full-screen .woocommerce-store-alerts{display:none}.woocommerce-admin-full-screen #wpcontent{margin-right:0!important}.woocommerce-admin-full-screen #wpcontent>#wpbody{display:block;margin-top:0!important}.woocommerce-admin-full-screen.has-woocommerce-navigation #wpbody{padding-right:0}.is-wp-toolbar-disabled #wpadminbar{display:none!important}.wp-toolbar .is-wp-toolbar-disabled{margin-top:-32px}@media(max-width:600px){.wp-toolbar .is-wp-toolbar-disabled{margin-top:-46px}}.woocommerce-admin-page .components-modal__frame .components-button.is-button,.woocommerce-profile-wizard__body .components-button.is-button,.woocommerce-task-dashboard__container .components-button.is-button{height:48px;padding-right:25px;padding-left:25px;text-align:center;font-size:14px;line-height:36px;font-weight:500;align-items:center}.woocommerce-admin-page .components-modal__frame .components-button.is-button:disabled,.woocommerce-profile-wizard__body .components-button.is-button:disabled,.woocommerce-task-dashboard__container .components-button.is-button:disabled{cursor:not-allowed}.components-modal__header .components-button svg+span{display:none}.components-modal__frame.woocommerce-usage-modal{width:600px;max-width:100%}.components-modal__frame.woocommerce-usage-modal .components-modal__header{margin-bottom:0}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__wrapper{flex-grow:1;display:flex;flex-direction:column}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__wrapper a{color:#50575e}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__wrapper button.is-primary{align-self:flex-end}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__actions{display:flex;justify-content:flex-end;margin-top:16px}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__actions button{margin-right:16px}.woocommerce-payments__usage-modal .components-modal__header{height:auto;padding:24px 24px 0}.woocommerce-payments__usage-modal .components-modal__header .components-modal__header-heading{font-size:24px;line-height:32px;margin:0 0 24px}.woocommerce-payments__usage-modal .woocommerce-payments__usage-modal-message{padding:16px 0;font-size:16px;line-height:24px}.woocommerce-payments__usage-modal .woocommerce-payments__usage-footer{display:flex;justify-content:flex-end;padding:16px 0}.woocommerce-payments__usage-modal .woocommerce-payments__usage-footer button{margin-right:16px}.components-modal__frame.woocommerce-cart-modal{width:600px;max-width:100%}.components-modal__frame.woocommerce-cart-modal .components-modal__header{border-bottom:0;margin-bottom:16px;margin-top:16px}.components-modal__frame.woocommerce-cart-modal .components-modal__header button{display:none}.components-modal__frame.woocommerce-cart-modal .components-modal__header-heading{font-style:normal;font-weight:400;font-size:24px;line-height:32px}.components-modal__frame.woocommerce-cart-modal .woocommerce-list{margin-top:24px}.components-modal__frame.woocommerce-cart-modal .woocommerce-list .woocommerce-list__item:first-child{border-top:1px solid #dcdcde}.components-modal__frame.woocommerce-cart-modal .woocommerce-list__item{border-bottom:1px solid #dcdcde}.components-modal__frame.woocommerce-cart-modal .woocommerce-cart-modal__help-text{font-size:16px;line-height:24px}.components-modal__frame.woocommerce-cart-modal .woocommerce-cart-modal__actions{text-align:left}.components-modal__frame.woocommerce-cart-modal .woocommerce-cart-modal__actions button.is-link{margin-left:16px;text-decoration:none;font-weight:600;font-size:14px}.components-modal__frame.woocommerce-cart-modal .woocommerce-cart-modal__actions button.is-primary{align-self:flex-end}.woocommerce-layout__header{background:#fff;box-sizing:border-box;padding:0;position:fixed;width:calc(100% - 160px);top:32px;z-index:1001}.woocommerce-layout__header.is-scrolled{box-shadow:0 8px 8px 0 rgba(85,93,102,.3)}.woocommerce-layout__header .woocommerce-layout__header-wrapper{display:flex;align-items:center;min-height:60px}@media(max-width:782px){.woocommerce-layout__header{flex-flow:row wrap;top:46px;width:100%}}@media(min-width:783px)and (max-width:960px){.woocommerce-layout__header{width:calc(100% - 36px)}}.woocommerce-layout__header .woocommerce-layout__header-breadcrumbs-wrapper{display:flex;justify-content:space-between;flex-direction:row}.woocommerce-layout__header .woocommerce-layout__header-heading{display:flex;align-items:center;padding:0 40px 0 0;padding:0 var(--large-gap) 0 0;flex:1 auto;height:60px;background:#fff;font-weight:600;font-size:14px}.folded .woocommerce-layout__header{width:calc(100% - 36px)}@media(max-width:782px){.folded .woocommerce-layout__header{width:100%}}.is-wp-toolbar-disabled .woocommerce-layout__header{top:0}.has-woocommerce-navigation .woocommerce-layout__header{right:0;width:100%}.woocommerce-admin-page #contextual-help-link-wrap,.woocommerce-admin-page #screen-options-link-wrap{margin-top:-1px}.wp-responsive-open .woocommerce-layout__header{margin-right:2px}.woocommerce-task-payment{display:flex;flex-direction:row;align-items:center;position:relative;overflow:hidden}.woocommerce-task-payment .components-card__media{width:85px;flex-shrink:0;align-self:flex-start}.woocommerce-task-payment .components-card__media .is-placeholder,.woocommerce-task-payment .components-card__media img,.woocommerce-task-payment .components-card__media svg{margin:auto;max-width:36px;display:block}.woocommerce-task-payment .components-card__media .is-placeholder{height:36px}.woocommerce-task-payment .woocommerce-task-payment__footer .is-placeholder{width:70px;height:36px}.woocommerce-task-payment>.components-form-toggle{min-width:52px}.woocommerce-task-payment .woocommerce-task-payment__title{display:flex;align-items:center;font-size:16px;font-weight:500;color:#2c3338;margin-top:0;margin-bottom:8px}.woocommerce-task-payment .woocommerce-task-payment__title .woocommerce-pill{margin-right:8px}.woocommerce-task-payment .woocommerce-task-payment__title .woocommerce-pill.pill-green{color:#008a20;border-color:#008a20}.woocommerce-task-payment .woocommerce-task-payment__content{font-size:14px;color:#50575e;margin:0 0 0 36px}.woocommerce-task-payment .woocommerce-task-payment__content .text-style-strong{font-weight:700}.woocommerce-task-payment .woocommerce-task-payment__content p{font-size:12px}.woocommerce-task-payment .woocommerce-task-payment__transaction-processors_images{padding-top:16px;display:flex;flex-direction:row;flex-wrap:wrap;gap:8px}.woocommerce-task-payment .woocommerce-task-payment__transaction-processors_images img{height:24px}.woocommerce-task-payment .woocommerce-task-payment__description{flex:1}@media(max-width:600px){.woocommerce-task-payment{flex-wrap:wrap}.woocommerce-task-payment .woocommerce-task-payment__content{margin:0}.woocommerce-task-payment .components-card__media{order:1;flex-basis:50%}.woocommerce-task-payment .components-card__media>img,.woocommerce-task-payment .components-card__media>svg{margin:0 24px 0 0}.woocommerce-task-payment .woocommerce-task-payment__description{order:3;padding:24px 24px 0 0}.woocommerce-task-payment .woocommerce-task-payment__footer{flex-basis:50%;align-self:flex-start;order:2;text-align:left}}.woocommerce-payment-gateway-suggestions-list-placeholder .is-placeholder{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent;display:inline-block;max-width:240px;width:80%}.woocommerce-payment-gateway-suggestions-list-placeholder .is-placeholder:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-payment-gateway-suggestions-list-placeholder .is-placeholder{animation:none}}.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-label{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent;display:inline-block;width:60%}.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-label:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-label{animation:none}}.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-icon{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent}.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-icon:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-icon{animation:none}}.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step:first-child .woocommerce-stepper__step-label{width:30%}.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-number{display:none}.woocommerce-wcpay-suggestion .woocommerce-recommended-payments-banner{min-width:100%;margin-bottom:24px}.woocommerce-wcpay-suggestion .woocommerce-recommended-payments-banner .woocommerce-recommended-payments-banner__body{padding:30px 24px 0 0;justify-content:space-between}.woocommerce-wcpay-suggestion .woocommerce-recommended-payments-banner .woocommerce-recommended-payments-banner__text_container{width:305px;margin-right:0}.woocommerce-wcpay-suggestion .woocommerce-recommended-payments-banner .woocommerce-recommended-payments-banner__footer{flex-direction:column;align-items:flex-start;padding:20px 24px 30px 38.2px}.woocommerce-wcpay-suggestion .woocommerce-recommended-payments-banner .woocommerce-recommended-payments-banner__footer_icon_container{margin-top:12px;margin-right:-5px}.woocommerce-wcpay-suggestion .woocommerce-recommended-payments-banner .woocommerce-recommended-payments-banner__footer_icon_container>svg{width:38px;height:24px;margin-left:8px}.woocommerce-wcpay-suggestion .woocommerce-wcpay-benefits{margin-bottom:24px}.woocommerce-task-payments .components-card+.components-card{margin-top:24px}.woocommerce-task-payments .woocommerce-task-payment__setup_required{display:flex;align-items:center;font-size:14px;margin-right:12px;font-weight:400;gap:3px}.woocommerce-task-payments .woocommerce-task-payment__setup_required>svg{fill:#efb854}.woocommerce-task-payments .components-card__header{font-size:20px;font-weight:400;line-height:28px;margin:0;display:flex;flex-direction:column;align-items:flex-start}.woocommerce-task-payments .woocommerce-task-payment-header__description{margin:0;color:#757575;font-weight:400;font-size:14px}.woocommerce-task-payments .components-card__footer a.components-button .gridicon{margin-right:4px}.woocommerce-task-payments .woocommerce-task-payment__recommended-pill{border:1px solid #dcdcde;border-radius:28px;display:inline-block;font-size:13px;margin-right:12px;padding:1px 10px}.woocommerce-task-payments .woocommerce-task-payment__recommended-pill span{max-width:70px}.woocommerce-task-payments .components-card__divider:last-child{display:none}.woocommerce-task-payments .woocommerce-task-payment-wcpay{margin-bottom:0}.woocommerce-task-payment-method>h3{margin:0;color:#1d2327}.woocommerce-task-payment-method p{font-size:14px;color:#646970;font-weight:400;margin-top:16px;margin-bottom:16px}.woocommerce-task-payment-method__fields{display:grid;grid-template-columns:1fr 1fr;grid-gap:0 16px;margin-bottom:8px}.woocommerce-task-payment-method__fields .components-base-control{margin-bottom:0}.woocommerce-shipping-rate{display:flex;padding-top:12px;padding-bottom:12px}.woocommerce-shipping-rate .woocommerce-shipping-rate__main{width:100%}.woocommerce-shipping-rate .woocommerce-shipping-rate__icon{padding-top:16px;margin-left:24px}.woocommerce-shipping-rate .woocommerce-shipping-rate__name{align-items:center;display:flex;padding-top:16px;font-size:16px;line-height:22px;color:#1d2327;margin-bottom:12px;border-top:1px solid #dcdcde}.woocommerce-shipping-rate .woocommerce-shipping-rate__name .components-form-toggle{margin-right:auto;height:18px}.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .components-base-control{margin-bottom:0}.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .components-base-control__label{display:block;position:relative;top:-8px;width:100%;font-size:12px}.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .text-control-with-affixes__prefix,.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .text-control-with-affixes__suffix{font-size:16px;line-height:24px;color:#646970;border:0;padding:0;align-items:center;display:flex;top:-11px}.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .components-text-control__input{position:relative;top:-11px}.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .text-control-with-affixes__prefix{margin-left:4px}.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .text-control-with-affixes__suffix{margin-right:4px}.woocommerce-task-shipping-recommendation__plugins-install{padding:24px 20px;border:1px solid #ddd;border-radius:3px;justify-content:space-around;margin-bottom:24px}.woocommerce-task-shipping-recommendation__plugins-install.dual{display:flex;flex-direction:column;justify-content:flex-start;width:285px}.woocommerce-task-shipping-recommendation__plugins-install.dual p{margin-top:0;margin-bottom:15px;color:#757575}.woocommerce-task-shipping-recommendation__plugins-install.dual .plugins-install__plugin-banner-image{display:flex;margin-bottom:24px}.woocommerce-task-shipping-recommendation__plugins-install.dual .plugins-install__plugin-banner-image img{width:120px;height:28px}.woocommerce-task-shipping-recommendation__plugins-install.single{display:flex}.woocommerce-task-shipping-recommendation__plugins-install.single .plugins-install__list{max-width:360px}.woocommerce-task-shipping-recommendation__plugins-install .plugins-install__plugin-banner-image{display:flex;width:150px}.woocommerce-task-shipping-recommendation__plugins-install .plugins-install__plugin-banner-image img{width:100%}.woocommerce-task-shipping-recommendation__plugins-install .plugins-install__list{display:flex;flex-direction:column;justify-content:space-around;gap:10px}.woocommerce-task-shipping-recommendation__plugins-install .woocommerce-task-shipping-recommendations_plugins-buttons{display:flex;justify-content:space-between;margin-top:24px;flex-grow:1;align-items:flex-end}.woocommerce-task-shipping-recommendation__plugins-install .woocommerce-task-shipping-recommendations_plugins-buttons button{min-width:40%;padding-inline:8px;margin-inline:4px}.woocommerce-task-shipping-recommendation__plugins-install .plugins-install__list-item{display:flex;align-items:center}.woocommerce-task-shipping-recommendation__plugins-install .plugins-install__list-icon{margin-left:12px}.woocommerce-task-shipping-recommendation_plugins-install-container{display:flex;flex-direction:row;flex-wrap:wrap;gap:10px}.woocommerce-task-shipping-recommendations_skip-button.dual{margin-inline:calc(50% - 34px)}.woocommerce-task-marketing .components-card__header{flex-direction:column;align-items:flex-start;display:flex!important}.woocommerce-task-marketing .components-card__header h2{align-self:start!important;color:#1e1e1e;font-size:20px;margin-bottom:0}.woocommerce-task-marketing .components-card__header span{margin-right:0;margin-top:8px;color:#757575}.woocommerce-plugin-list__plugin{display:flex;padding:24px;border-top:1px solid #e0e0e0}.woocommerce-plugin-list__plugin:first-child{border-top:0}.woocommerce-plugin-list__plugin h4{margin-bottom:12px;font-weight:600;color:#1e1e1e}.woocommerce-plugin-list__plugin p{color:#757575;font-weight:400}.woocommerce-plugin-list__plugin-logo{margin-left:45px;display:flex;align-items:center}.woocommerce-plugin-list__plugin-logo img{width:50px}.woocommerce-plugin-list__plugin-text{max-width:370px;margin-left:16px}.woocommerce-plugin-list__plugin-text .woocommerce-pill{color:#646970;margin-right:12px}.woocommerce-plugin-list__plugin-action{display:flex;align-items:center;margin-right:auto}.woocommerce-plugin-list__title{padding:24px 30px 0;position:relative}.woocommerce-plugin-list__title h3{font-weight:500;color:#000;text-transform:uppercase}.woocommerce-plugin-list__plugin{border-top:none;border-bottom:1px solid #e0e0e0}.woocommerce-tax-partner-card{border:1px solid #ddd;border-radius:3px;margin:0 8px 16px;padding:20px;display:flex;flex-direction:column}.woocommerce-tax-partner-card a{text-decoration:none}.woocommerce-tax-partner-card__logo{margin-bottom:8px}.woocommerce-tax-partner-card__logo img{max-height:35px}.woocommerce-tax-partner-card__description{color:#757575}.woocommerce-tax-partner-card__benefits{color:#1e1e1e;list-style:none}.woocommerce-tax-partner-card__benefits li{margin-bottom:8px;display:flex}.woocommerce-tax-partner-card__benefits li svg{margin-left:10px}.woocommerce-tax-partner-card__action{margin-top:auto}.woocommerce-tax-partner-card__terms{color:#949494;font-size:9px;margin-bottom:8px}.woocommerce-tax-partners__partners{display:grid;grid-template-columns:1fr 1fr}.woocommerce-tax-partners__partners.woocommerce-tax-partners__partners-count-1{grid-template-columns:1fr}@media(max-width:782px){.woocommerce-tax-partners__partners{grid-template-columns:1fr}}.woocommerce-tax-partners__partners-count-1 .woocommerce-tax-partners__partners{grid-template-columns:1fr;justify-items:center}.woocommerce-tax-partners .components-card__body.is-size-medium{padding:36px}.woocommerce-tax-partners .components-card__header{line-height:28px;font-size:20px}.woocommerce-tax-partners__other-actions{text-align:center;list-style:none;display:flex;align-items:center;justify-content:center;margin:0}@media(max-width:782px){.woocommerce-tax-partners__other-actions{flex-direction:column}}.woocommerce-tax-partners__other-actions li{margin-top:16px;margin-left:4px}.woocommerce-tax-partners__other-actions li button.is-tertiary{padding:0;height:auto}.woocommerce-tax-partners__other-actions li:after{content:"•";color:#bbb;margin-right:4px}@media(max-width:782px){.woocommerce-tax-partners__other-actions li:after{content:""}}.woocommerce-tax-partners__other-actions li:last-child{margin-left:0}.woocommerce-tax-partners__other-actions li:last-child:after{content:""}.woocommerce-task-tax__automated-tax-control{display:flex;align-items:center;margin-top:16px}.woocommerce-task-tax__automated-tax-control i{margin-right:16px;margin-left:24px}.woocommerce-task-tax__automated-tax-control .woocommerce-task-tax__automated-tax-control-inner{border-top:1px solid #dcdcde;display:flex;align-items:center;flex:1;font-size:16px;padding-top:16px;padding-bottom:16px}.woocommerce-task-tax__automated-tax-control .components-form-toggle{margin-right:auto}.woocommerce-task-tax__success{display:flex;flex-direction:column;align-items:center;padding:40px;text-align:center}.woocommerce-task-tax__success .woocommerce-task-tax__success-icon{font-size:48px;height:48px;align-items:center;display:flex}.woocommerce-task-tax__success #woocommerce-task-tax__success-message{font-size:32px;font-weight:400}.woocommerce-task-tax__success p{margin-top:0;font-size:16px}.woocommerce-task-dashboard__container .woocommerce-task-card{max-width:680px;margin-right:auto;margin-left:auto;margin-bottom:24px}.woocommerce-task-dashboard__container .woocommerce-task-card .components-card__header.is-size-large{padding-bottom:12px}.woocommerce-task-dashboard__container .woocommerce-task-card .components-card__header.is-size-large .woocommerce-card__menu{margin-top:8px}.woocommerce-task-dashboard__container .woocommerce-task-card .wooocommerce-task-card__header{display:flex}.woocommerce-task-dashboard__container .woocommerce-task-card .wooocommerce-task-card__header .components-text+.woocommerce-badge{margin-right:16px}.woocommerce-task-dashboard__container .woocommerce-task-card .woocommerce-list__item-text .woocommerce-pill{padding:1px 8px;margin-right:8px}.woocommerce-task-dashboard__container .woocommerce-task-card .components-popover__content{min-width:unset}.woocommerce-task-dashboard__container .woocommerce-task-payments{width:680px;margin:auto;max-width:100%}.woocommerce-task-list__item-expandable-content,.woocommerce-task__additional-info,.woocommerce-task__estimated-time{color:#757575;font-weight:400;font-size:12px}.woocommerce-task-list__item-expandable-content{font-size:13px}.components-modal__screen-overlay{background:rgba(43,45,47,.4)}.components-modal__frame .components-modal__header{margin-bottom:0}.components-modal__frame .woocommerce-task-payments__stripe-error-wrapper{align-items:flex-end;flex-grow:1;display:flex;flex-direction:column}.woocommerce-task-dashboard__container .woocommerce-stepper button.components-button.is-primary{margin:0 0 0 8px}.woocommerce-task-dashboard__container button.components-button.is-link{margin:0;height:auto;color:#50575e;font-weight:400}.woocommerce-task-card__prompt{width:100%;min-width:100%;margin-bottom:24px;margin-top:-4px;cursor:default}.woocommerce-task-card__prompt .components-snackbar__content{display:block;align-items:unset;justify-content:unset}.woocommerce-task-card__prompt .components-snackbar__content span{margin-right:-24px}.woocommerce-task-card__prompt .woocommerce-task-card__prompt-actions button.is-link,.woocommerce-task-card__prompt .woocommerce-task-card__prompt-actions button.is-link:active,.woocommerce-task-card__prompt .woocommerce-task-card__prompt-actions button.is-link:focus{color:#fff;margin-right:24px;background:transparent}.woocommerce-task-card__prompt .woocommerce-task-card__prompt-actions button.is-link:hover{color:#fff}.woocommerce-task-card__prompt .woocommerce-task-card__prompt-pointer{border-bottom:10px solid #1e1e1e;border-right:10px solid transparent;border-left:10px solid transparent;position:relative;width:0;height:0;display:inline-block;top:-30px}.woocommerce-task-card__prompt .woocommerce-task-card__prompt-content{display:flex;align-items:baseline;justify-content:space-between;max-height:10px;margin-right:24px;position:relative;top:-40px}.woocommerce-task-card__prompt .woocommerce-task-card__prompt-actions{margin-left:-16px}.woocommerce-task-card__prompt:hover .woocommerce-task-card__prompt-pointer{border-bottom-color:#1e1e1e}.woocommerce-task-dashboard__container .muriel-component{margin-top:16px;margin-bottom:16px}.woocommerce-task-dashboard__container .components-base-control.has-error{margin-bottom:32px!important;border-color:#d63638}@media(max-width:782px){.woocommerce-task-dashboard__container .components-base-control.has-error{margin-bottom:44px!important}}.woocommerce-task-dashboard__container .components-base-control.has-error .components-base-control__help{top:100%;right:12px;position:absolute;margin-top:4px;font-size:12px;font-style:normal;color:#d63638}.woocommerce-task-dashboard__container .components-form-toggle{display:inline-block}.woocommerce-task-dashboard__container .components-form-toggle label{font-size:14px}.woocommerce-task-dashboard__container .components-form-toggle .components-base-control{display:inline-block}.woocommerce-task-dashboard__container .components-form-toggle .components-base-control__field{margin-bottom:0}.woocommerce-task-dashboard__container .woocommerce-task__caption{color:#757575;margin-top:16px}.woocommerce-task-list__setup .woocommerce-experimental-list .woocommerce-experimental-list__item.complete{text-decoration:line-through;color:var(--wp-admin-theme-color)}.woocommerce-task-list__setup .woocommerce-experimental-list .woocommerce-experimental-list__item.complete .woocommerce-task-list__item-title{color:var(--wp-admin-theme-color)}.woocommerce-task-progress-header{position:relative;min-height:28px}.woocommerce-task-progress-header h1{font-size:24px}.woocommerce-task-progress-header p{color:#757575;font-size:16px;line-height:24px;margin-top:8px}.woocommerce-task-progress-header .woocommerce-task-progress-header__progress-bar{appearance:none;border:1px solid #ddd;border-radius:16px;height:12px;width:100%;margin-bottom:0;background-color:#fff}.woocommerce-task-progress-header .woocommerce-task-progress-header__progress-bar::-moz-progress-bar{background-color:var(--wp-admin-theme-color);border-radius:16px}.woocommerce-task-progress-header .woocommerce-task-progress-header__progress-bar::-webkit-progress-bar{background-color:#fff;border-radius:16px}.woocommerce-task-progress-header .woocommerce-task-progress-header__progress-bar::-webkit-progress-value{background-color:var(--wp-admin-theme-color);border-bottom-right-radius:16px;border-top-right-radius:16px}.woocommerce-task-progress-header .woocommerce-card__menu{position:absolute;left:0;top:-7px}.woocommerce-setup-panel .woocommerce-task-progress-header{padding:16px}.woocommerce-setup-panel .woocommerce-task-progress-header h1{font-size:16px;font-weight:600}.woocommerce-setup-panel .woocommerce-task-progress-header p{font-size:16px}.woocommerce-setup-panel .woocommerce-task-progress-header .woocommerce-card__menu{top:17px}@media(max-width:782px){.woocommerce-task-progress-header{padding-right:var(--large-gap);padding-left:var(--large-gap)}}h1.woocommerce-task-progress-header__title{padding-top:4px}@media(max-width:782px){h1.woocommerce-task-progress-header__title{padding-right:var(--large-gap);padding-left:var(--large-gap)}}.wooocommerce-task-card__header .wooocommerce-task-card__header-subtitle{color:#757575;margin-bottom:24px}.wooocommerce-task-card__finished-header-image{max-width:75%}.customer-feedback-simple__container{height:64px}.woocommerce-task-card__header-menu{position:absolute;left:0;top:0}.wooocommerce-task-card__header-ces-feedback{height:64px;display:flex;justify-content:center;align-items:center}.woocommerce-task-header__contents-container.woocommerce-task-header__customize-store img.svg-background{padding-left:16px;padding-top:9px}.woocommerce-homescreen .woocommerce-task-dashboard__container:empty{margin-bottom:0}.woocommerce-task-dashboard__container .woocommerce-homescreen-card{max-width:none;width:100%}.woocommerce-task-dashboard__container .wooocommerce-task-card__header-container{display:flex;position:relative;border-bottom:1px solid #dcdcde}.woocommerce-task-dashboard__container .wooocommerce-task-card__header{width:100%;flex:1}.woocommerce-task-dashboard__container .woocommerce-ellipsis-menu{position:absolute;top:16px;left:24px}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .components-card__header{background:#fff;height:130px;display:block}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .components-card__header .is-placeholder{margin:20px;width:100%;height:90px}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .wooocommerce-task-card__header{align-self:inherit}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-task-list__item-before .is-placeholder{height:36px;width:36px;border-radius:50%}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-task-list__item-text .is-placeholder{width:80%}.woocommerce-task-dashboard__container .woocommerce-task-card.completed,.woocommerce-task-dashboard__container .woocommerce-task-card.completed .components-card__header{display:block}.woocommerce-task-dashboard__container .woocommerce-task-card.completed h2{margin-top:24px;margin-bottom:12px}.woocommerce-task-dashboard__container .woocommerce-task-card.completed .wooocommerce-task-card__header{display:block;text-align:center}.woocommerce-task-dashboard__container .woocommerce-task-card.completed button.is-secondary{margin-left:12px}.woocommerce-task-dashboard__container.setup-task-list{max-width:1032px;display:flex;flex-direction:row;margin:0 auto;justify-content:space-between}.woocommerce-task-dashboard__container.setup-task-list ul li.complete .woocommerce-task-list__item-title{font-weight:600;color:#949494}.woocommerce-task-dashboard__container.setup-task-list ul li{display:block;width:100%;border-left:1px solid #e0e0e0;border-top:none;padding:16px 24px}.woocommerce-task-dashboard__container.setup-task-list ul li:last-child{border-left:none}.woocommerce-task-dashboard__container.setup-task-list ul li.is-active{box-shadow:inset 0 -4px 0 0 var(--wp-admin-theme-color)}.woocommerce-task-dashboard__container.setup-task-list ul li.is-active .woocommerce-task-list__item-badge{background-color:#fff;position:relative;z-index:1}.woocommerce-task-dashboard__container.setup-task-list ul li.is-active:after{background-color:var(--wp-admin-theme-color);opacity:.1;content:"";top:0;right:0;position:absolute;width:100%;height:100%;pointer-events:none}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete) .woocommerce-task__icon{border:1px solid var(--wp-admin-theme-color);background:transparent}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item.complete:not(.complete) .woocommerce-task__icon{border:none}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item-before{display:block;padding:0}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item-text{margin-top:10px}.woocommerce-task-dashboard__container.setup-task-list .numbered-circle,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-1 .woocommerce-task__icon:after,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-2 .woocommerce-task__icon:after,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-3 .woocommerce-task__icon:after,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-4 .woocommerce-task__icon:after,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-5 .woocommerce-task__icon:after,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-6 .woocommerce-task__icon:after,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-7 .woocommerce-task__icon:after,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-8 .woocommerce-task__icon:after,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-9 .woocommerce-task__icon:after,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-10 .woocommerce-task__icon:after{justify-content:center;display:flex;align-items:center;height:100%;width:100%}.woocommerce-task-dashboard__container.setup-task-list ul{display:block}.woocommerce-task-dashboard__container.setup-task-list ul li{display:grid;grid-template-columns:48px auto 48px;border-left:none;border-bottom:1px solid #e0e0e0}.woocommerce-task-dashboard__container.setup-task-list ul li.is-active{box-shadow:inset -5px 0 0 0 var(--wp-admin-theme-color);transition:box-shadow .1s linear}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item-text{display:block;padding:0;margin-top:0}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents{max-width:65%}@media(max-width:600px){.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents{max-width:100%}}.woocommerce-task-dashboard__container.setup-task-list .svg-background{left:6%}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-1 .woocommerce-task__icon:after{content:"1";color:var(--wp-admin-theme-color);font-weight:700}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-2 .woocommerce-task__icon:after{content:"2";color:var(--wp-admin-theme-color);font-weight:700}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-3 .woocommerce-task__icon:after{content:"3";color:var(--wp-admin-theme-color);font-weight:700}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-4 .woocommerce-task__icon:after{content:"4";color:var(--wp-admin-theme-color);font-weight:700}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-5 .woocommerce-task__icon:after{content:"5";color:var(--wp-admin-theme-color);font-weight:700}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-6 .woocommerce-task__icon:after{content:"6";color:var(--wp-admin-theme-color);font-weight:700}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-7 .woocommerce-task__icon:after{content:"7";color:var(--wp-admin-theme-color);font-weight:700}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-8 .woocommerce-task__icon:after{content:"8";color:var(--wp-admin-theme-color);font-weight:700}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-9 .woocommerce-task__icon:after{content:"9";color:var(--wp-admin-theme-color);font-weight:700}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-10 .woocommerce-task__icon:after{content:"10";color:var(--wp-admin-theme-color);font-weight:700}@media(max-width:782px){.woocommerce-task-dashboard__container.setup-task-list ul{display:block}.woocommerce-task-dashboard__container.setup-task-list ul li{display:grid;grid-template-columns:48px auto 48px;border-left:none;border-bottom:1px solid #e0e0e0}.woocommerce-task-dashboard__container.setup-task-list ul li.is-active{box-shadow:inset -5px 0 0 0 var(--wp-admin-theme-color);transition:box-shadow .1s linear}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item-text{display:block;padding:0;margin-top:0}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents{max-width:65%}}@media(max-width:782px)and (max-width:782px){.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents{max-width:380px}}@media(max-width:782px)and (max-width:600px){.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents{max-width:100%}}@media(max-width:782px){.woocommerce-task-dashboard__container.setup-task-list .svg-background{left:6%}}.woocommerce-setup-panel .woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents-container{padding:16px}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents-container{padding:20px 24px;position:relative;flex:1;overflow:hidden;width:100%}.woocommerce-task-dashboard__container.setup-task-list .svg-background{position:absolute;z-index:0;left:24px;max-width:25%;max-height:150px;width:auto;height:auto}@media(max-width:600px){.woocommerce-task-dashboard__container.setup-task-list .svg-background{display:none}}@media(max-width:782px){.woocommerce-task-dashboard__container.setup-task-list .svg-background{left:.5%;width:40%}}.woocommerce-task-dashboard__container.setup-task-list .svg-background .admin-theme-color{fill:var(--wp-admin-theme-color)}.woocommerce-task-dashboard__container.setup-task-list .svg-background .admin-theme-color-darker-10{fill:var(--wp-admin-theme-color-darker-10)}.woocommerce-task-dashboard__container.setup-task-list .svg-background .admin-theme-color-darker-20{fill:var(--wp-admin-theme-color-darker-20)}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents{max-width:70%;position:relative;z-index:1}@media(max-width:782px){.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents{max-width:380px}}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents p,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents span{color:#757575}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents p:first-of-type{margin-top:4px}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__timer{display:flex;align-items:center;line-height:22px;margin-bottom:0}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__timer img{margin-left:6px}.woocommerce-task-dismiss-modal{width:565px;max-width:100%}.woocommerce-task-dismiss-modal .components-modal__header{border-bottom:1px solid #ddd}.woocommerce-task-dismiss-modal .woocommerce-usage-modal__message{box-sizing:border-box;border-bottom:1px solid #ddd;padding:0 32px;display:flex;flex-direction:row;justify-content:space-between;background:#fff;align-items:center;height:60px;z-index:10;position:relative;position:sticky;top:0;margin:0 -32px 24px;font-size:1.2em}.woocommerce-task-dismiss-modal .woocommerce-usage-modal__actions{display:flex;justify-content:flex-end;margin-top:16px}.woocommerce-task-dismiss-modal .woocommerce-usage-modal__actions button{margin-right:16px}.woocommerce-layout__header-back-button{cursor:pointer;margin-right:40px;margin-right:var(--large-gap);margin-left:-16px;display:flex;z-index:2}.woocommerce-layout__header-back-button:focus{box-shadow:inset 1px -1px 0 #757575,inset -1px 1px 0 #757575}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-card__body{border-top:1px solid #dcdcde}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .is-placeholder{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent;display:inline-block;height:16px}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .is-placeholder:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .is-placeholder{animation:none}}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-card__title .is-placeholder{width:70%;height:28px}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-list__item-before .is-placeholder{height:36px;width:36px}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-list__item-text{width:100%}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-list__item-text .woocommerce-list__item-title .is-placeholder{height:22px;width:60%}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-list__item-after .is-placeholder{height:18px;width:60px}.woocommerce-task-list__item.woocommerce-list__item-enter{opacity:0;max-height:0}.woocommerce-task-list__item.woocommerce-list__item-enter-active{opacity:1;max-height:100px;transition:opacity .5s,max-height .5s}.woocommerce-task-list__item.woocommerce-list__item-exit{opacity:1;max-height:100px}.woocommerce-task-list__item.woocommerce-list__item-exit-active{opacity:0;max-height:0;transition:opacity .5s,max-height .5s}.woocommerce-layout__header-tasks-reminder-bar{height:40px;background-color:#007cba;display:flex;justify-content:space-between;align-items:center;color:#fff}.woocommerce-layout__header-tasks-reminder-bar:before{content:""}.woocommerce-layout__header-tasks-reminder-bar button{color:inherit}.woocommerce-layout__header-tasks-reminder-bar button:hover{opacity:.7;color:inherit}.woocommerce-layout__header-tasks-reminder-bar a{color:inherit}.woocommerce-layout__header-tasks-reminder-bar p{font-size:13px}.woocommerce-layout__footer{background:#fff;border-top:1px solid #e0e0e0;box-sizing:border-box;padding:0;position:fixed;width:calc(100% - 160px);bottom:-1px;z-index:1001}.woocommerce-profile-wizard__body .woocommerce-layout__footer{width:100%}@media(min-width:783px)and (max-width:960px){.woocommerce-layout__footer{width:calc(100% - 36px)}}@media(max-width:782px){.woocommerce-layout__footer{flex-flow:row wrap;width:100%}}.folded .woocommerce-layout__footer{width:calc(100% - 36px)}.woocommerce-transient-notices{position:absolute;right:16px;bottom:100%;margin-bottom:12px;z-index:100001;width:auto}.woocommerce-profile-wizard__body .woocommerce-transient-notices{right:unset;width:100%}.woocommerce-profile-wizard__body .woocommerce-transient-notices .components-snackbar{margin-right:auto;margin-left:auto}.components-snackbar.components-snackbar-explicit-dismiss{cursor:default}.components-snackbar__content{white-space:pre-line}.components-snackbar .components-snackbar__content-with-icon{margin-right:32px}.components-snackbar .components-snackbar__icon{position:absolute;top:24px;right:26px}.components-snackbar .components-snackbar__dismiss-button{margin-right:32px;cursor:pointer}.woocommerce-layout__activity-panel{display:flex;flex-direction:row;align-items:center;height:60px}.woocommerce-layout__activity-panel-tabs{width:100%;display:flex;height:60px;justify-content:flex-end}.woocommerce-layout__activity-panel-tabs .dashicon,.woocommerce-layout__activity-panel-tabs .gridicon{width:100%}.woocommerce-layout__activity-panel-tabs svg{width:24px;height:24px}.woocommerce-layout__activity-panel-tabs svg.woocommerce-layout__activity-panel-tab-icon{fill:none}.woocommerce-layout__activity-panel-tabs svg.woocommerce-layout__activity-panel-tab-icon path,.woocommerce-layout__activity-panel-tabs svg.woocommerce-layout__activity-panel-tab-icon rect{stroke:currentColor}.woocommerce-layout__activity-panel-tabs svg .setup-progress-slice{stroke:none}.woocommerce-layout__activity-panel-tabs svg .setup-progress-ring{stroke-width:2px}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__homescreen-display-options svg.woocommerce-layout__activity-panel-tab-icon{height:14px}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__homescreen-extension-tasklist-toggle{min-width:205px}.woocommerce-layout__activity-panel-tabs .components-icon-button{display:initial;text-indent:0;border-radius:0}.woocommerce-layout__activity-panel-tabs .components-icon-button.has-text svg{margin:0}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab{display:flex;flex-direction:column;justify-content:center;align-items:center;position:relative;border:none;outline:none;cursor:pointer;background-color:#fff;width:100%;height:60px;color:#757575;white-space:nowrap}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:before{background-color:var(--wp-admin-theme-color);bottom:0;content:"";height:0;opacity:0;transition-property:height,opacity;transition-duration:.3s;transition-timing-function:ease-in-out;right:0;position:absolute;left:0}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.is-active,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.is-opened{color:#1e1e1e;box-shadow:none}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.is-active:before,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.is-opened:before{height:3px;opacity:1}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.woocommerce-layout__activity-panel-tab-wordpress-notices:after{content:" ";position:absolute;padding:1px;background:#d94f4f;border:2px solid #fff;width:4px;height:4px;display:inline-block;border-radius:50%;top:8px;right:50%}@media(min-width:783px)and (max-width:960px){.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.woocommerce-layout__activity-panel-tab-wordpress-notices:after{left:18px;right:auto;margin-right:0}}@media(min-width:961px){.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.woocommerce-layout__activity-panel-tab-wordpress-notices:after{left:28px;right:auto;margin-right:0}}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.components-button:not(:disabled):not([aria-disabled=true]):hover,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:hover{background-color:#f0f0f0;box-shadow:none}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.components-button:not(:disabled):not([aria-disabled=true]):hover.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.components-button:not(:disabled):not([aria-disabled=true]):hover.woocommerce-layout__activity-panel-tab-wordpress-notices:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:hover.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:hover.woocommerce-layout__activity-panel-tab-wordpress-notices:after{border-color:#e0e0e0}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.components-button:not(:disabled):not([aria-disabled=true]):focus,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:focus{box-shadow:inset 1px -1px 0 #757575,inset -1px 1px 0 #757575}@media(max-width:782px){.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.display-options{display:none}}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-popover{margin-top:0;z-index:1001}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-popover .components-menu-group{padding:12px}.woocommerce-layout__activity-panel-toggle-bubble.has-unread:after{content:" ";position:absolute;padding:1px;background:#ca4a1f;border:2px solid #fff;width:4px;height:4px;display:inline-block;border-radius:50%;top:6px;left:4px}@keyframes tabSwitch{0%,to{transform:translateX(0)}50%{transform:translateX(-100px)}}.woocommerce-layout__activity-panel-wrapper{height:calc(100vh - 106px);background:#f0f0f0;width:430px;transform:translateX(-100%);transition-property:transform box-shadow;transition-duration:.3s;transition-timing-function:ease-in-out;position:absolute;left:0;top:100%;z-index:1000;overflow-x:hidden;overflow-y:auto}@media(max-width:782px){.woocommerce-layout__activity-panel-wrapper{width:100%}}@media screen and (prefers-reduced-motion:reduce){.woocommerce-layout__activity-panel-wrapper{transition-duration:1ms}}@media(min-width:783px){.woocommerce-layout__activity-panel-wrapper{height:calc(100vh - 92px)}}.has-woocommerce-navigation .woocommerce-layout__activity-panel-wrapper{height:calc(100vh - 60px);top:60px}.woocommerce-layout__activity-panel-wrapper.is-open{transform:none;box-shadow:0 12px 12px 0 rgba(85,93,102,.3)}.woocommerce-layout__activity-panel-wrapper.is-switching{animation:tabSwitch;animation-duration:.3s}@media screen and (prefers-reduced-motion:reduce){.woocommerce-layout__activity-panel-wrapper.is-switching{animation:none}}.woocommerce-layout__activity-panel-wrapper .woocommerce-empty-content{padding-right:24px;padding-left:24px}.woocommerce-layout__activity-panel-avatar-flag-overlay{position:relative;top:-12px}.woocommerce-layout__activity-panel-avatar-flag-overlay .woocommerce-flag{position:relative;top:16px;border:2px solid #fff}.woocommerce-layout__notice-list-hide{display:none}.highlight-tooltip__container{position:absolute;width:0;height:0}.highlight-tooltip__container.highlight-tooltip__show{top:0;right:0;width:100%;height:100%}.highlight-tooltip__portal{width:100%;height:100%;position:relative}.highlight-tooltip__portal .highlight-tooltip__overlay{position:fixed;top:0;left:0;bottom:0;right:0;background-color:rgba(0,0,0,.35);z-index:100000;animation:edit-post__fade-in-animation .2s ease-out 0s;animation-fill-mode:forwards}@media(prefers-reduced-motion:reduce){.highlight-tooltip__portal .highlight-tooltip__overlay{animation-duration:1ms}}.highlight-tooltip__popover .components-card{min-width:360px}.highlight-tooltip__popover .components-card__header{font-size:16px;font-size:1rem;font-weight:600;box-sizing:border-box}.highlight-tooltip__popover .components-card__footer{justify-content:flex-end;box-sizing:border-box}@media(max-width:782px){.woocommerce-layout__show-app-banner .woocommerce-layout__header-wrapper{padding-top:56px}}.woocommerce-mobile-app-banner{background-color:#3c2861;position:absolute;top:0;right:0;width:100%;display:flex;height:56px;align-items:center;padding:0 4px 0 6px}@media(min-width:401px){.woocommerce-mobile-app-banner{padding:0 10px 0 13px}}@media(min-width:783px){.woocommerce-mobile-app-banner{display:none}}.woocommerce-mobile-app-banner .gridicon{fill:#fff;margin-left:10px}.woocommerce-mobile-app-banner .components-button.is-secondary{margin-right:auto;color:#fff;box-shadow:inset 0 0 0 1px #fff}.woocommerce-mobile-app-banner .components-button.is-secondary:active,.woocommerce-mobile-app-banner .components-button.is-secondary:hover{color:#fff;box-shadow:inset 0 0 0 1px #fff;background:none}.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description{color:#fff;margin-right:8px}@media(min-width:401px){.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description{margin-right:13px}}.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description .woocommerce-mobile-app-banner__description__text{margin:0;font-size:10px}.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description .woocommerce-mobile-app-banner__description__text:first-child{font-weight:700}@media(min-width:401px){.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description .woocommerce-mobile-app-banner__description__text{margin-right:13px;font-size:13px}}.woocommerce-navigation{display:grid;grid-template-rows:min-content 1fr;height:100%}.woocommerce-navigation .woocommerce-navigation__wrapper h2>span{width:100%}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__menu{overflow-y:auto;margin-bottom:0;padding-bottom:24px}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__group+.components-navigation__group{margin-top:24px}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item{margin-bottom:0}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item .components-button{opacity:1}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item:not(:hover) .components-button{color:#ccc}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item:hover .components-button{color:#fff}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item.is-active .components-button{color:#fff}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item a.components-button{padding:6px 16px}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item:not(:hover) a.components-button{color:#ccc}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item.is-active a.components-button{color:#fff}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation{height:100%}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation>div{height:100%;display:grid;grid-template-rows:1fr min-content}.woocommerce-navigation .woocommerce-navigation__wrapper.is-root .components-navigation__menu-secondary{border-top:1px solid #2c3338;margin:0 -8px;padding:16px 8px 12px}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__group-title,.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__menu-title{color:#f0f0f0;opacity:1}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button{color:#ccc;opacity:1}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button,.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button span{font-size:13px;line-height:normal}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button:hover,.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button:hover:not(:disabled){color:#ddd}.woocommerce-navigation-header{display:flex;align-items:center;border:none;border-radius:0;height:auto}.woocommerce-navigation-header .woocommerce-navigation-header__site-icon.components-button{padding:12px;height:60px;color:#fff}.woocommerce-navigation-header .woocommerce-navigation-header__site-icon.components-button:focus,.woocommerce-navigation-header .woocommerce-navigation-header__site-icon.components-button:hover,.woocommerce-navigation-header .woocommerce-navigation-header__site-icon.components-button:not([aria-disabled=true]):active{color:#fff}.woocommerce-navigation-header .woocommerce-navigation-header__site-title.components-button{padding-right:0;color:#ccc;font-weight:600}.woocommerce-navigation-header .woocommerce-navigation-header__site-title.components-button:active,.woocommerce-navigation-header .woocommerce-navigation-header__site-title.components-button:focus,.woocommerce-navigation-header .woocommerce-navigation-header__site-title.components-button:hover{color:#e0e0e0}.woocommerce-navigation-header .woocommerce-navigation-header__site-title{padding-top:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.woocommerce-navigation{position:relative;width:240px;box-sizing:border-box;background-color:#1e1e1e;z-index:1100}@media(max-width:960px){.woocommerce-navigation{width:60px;height:60px}}.woocommerce-navigation .components-navigation{box-sizing:border-box}.woocommerce-navigation .components-navigation__menu-title{overflow:visible}.woocommerce-navigation .components-navigation__menu{scrollbar-color:#757575 #1e1e1e;scrollbar-width:thin}.woocommerce-navigation .components-navigation__menu::-webkit-scrollbar-thumb{border-radius:10px;background-color:#757575}.woocommerce-navigation .components-navigation__menu::-webkit-scrollbar-thumb:hover{background-color:#757575;width:8px;height:8px}.woocommerce-navigation .components-navigation__menu::-webkit-scrollbar{width:8px;height:8px}.woocommerce-navigation__wrapper{background-color:#1e1e1e;position:absolute;top:60px;width:100%;height:calc(100vh - 92px);overflow-y:auto}.is-wp-toolbar-disabled .woocommerce-navigation__wrapper{height:calc(100vh - 60px)}body.is-wc-nav-expanded .woocommerce-navigation{width:240px;height:100%}body.is-wc-nav-expanded font>.xdebug-error{margin-right:256px}body.is-wc-nav-folded .woocommerce-navigation{width:60px;height:60px;overflow:hidden}body.is-wc-nav-folded .woocommerce-navigation .woocommerce-navigation-header>*{display:none}body.is-wc-nav-folded .woocommerce-navigation .woocommerce-navigation-header__site-icon{display:block}body.is-wc-nav-folded .woocommerce-navigation .components-navigation{display:none}body.is-wc-nav-folded .woocommerce-transient-notices{right:16px}body.is-wc-nav-folded #wpbody{padding-right:0}.has-woocommerce-navigation #adminmenuback,.has-woocommerce-navigation #adminmenuwrap{display:none!important}.has-woocommerce-navigation.woocommerce_page_wc-reports .woo-nav-tab-wrapper,.has-woocommerce-navigation.woocommerce_page_wc-settings .woo-nav-tab-wrapper,.has-woocommerce-navigation.woocommerce_page_wc-status .woo-nav-tab-wrapper{display:none}.has-woocommerce-navigation.woocommerce_page_wc-reports .woocommerce .subsubsub,.has-woocommerce-navigation.woocommerce_page_wc-settings .woocommerce .subsubsub,.has-woocommerce-navigation.woocommerce_page_wc-status .woocommerce .subsubsub{font-size:14px;margin:5px 0}.has-woocommerce-navigation #wpcontent,.has-woocommerce-navigation #wpfooter{margin-right:0}@media(max-width:960px){.has-woocommerce-navigation #wpcontent,.has-woocommerce-navigation #wpfooter{margin-right:0}}.has-woocommerce-navigation #wpbody{padding-right:240px}@media(max-width:960px){.has-woocommerce-navigation #wpbody{padding-right:0}}.has-woocommerce-navigation .woocommerce-layout__header.is-embed-loading:before{content:"";position:fixed;width:240px;height:100%;background:#1e1e1e}@media(max-width:960px){.has-woocommerce-navigation .woocommerce-layout__header.is-embed-loading:before{width:60px;height:60px}}.has-woocommerce-navigation #woocommerce-embedded-root.is-embed-loading{margin-bottom:-32px}.has-woocommerce-navigation:not(.is-wp-toolbar-disabled) #wpbody-content{margin-top:32px}.has-woocommerce-navigation font>.xdebug-error{margin-top:60px}.woocommerce-navigation-category-title{display:flex;align-items:center;font-size:20px;line-height:28px}.woocommerce-navigation-category-title .woocommerce-navigation-favorite-button{margin-right:auto}.woocommerce-navigation-favorite-button.components-button .star-empty-icon{color:#949494}.woocommerce-navigation-favorite-button.components-button .star-filled-icon{color:#ffb900}.woocommerce-embedded-layout__primary{padding:0 20px}.woocommerce-embedded-layout__primary .components-card__footer,.woocommerce-embedded-layout__primary .components-card__header{box-sizing:border-box}@media(max-width:782px){.woocommerce-embedded-layout__primary{padding:0}}.woocommerce-recommended-payments-card{margin:0 0 10px 15px;animation:isLoaded;animation-duration:.25s}.woocommerce-recommended-payments-card .woocommerce-list__item:hover{background-color:#fff}.woocommerce-recommended-payments-card .woocommerce-list__item:hover .woocommerce-list__item-title{color:#1e1e1e}.woocommerce-recommended-payments-card .woocommerce-list__item-title{font-size:14px;color:#1e1e1e;font-weight:600}.woocommerce-recommended-payments-card .woocommerce-review-activity-card__section-controls{text-align:center}.woocommerce-recommended-payments-card .woocommerce-pill{margin-right:4px;padding:2px 8px}@media(max-width:480px){.woocommerce-recommended-payments-card .woocommerce-pill{margin-top:4px;margin-bottom:4px}}.woocommerce-recommended-payments-card .components-card__footer .gridicon{margin-right:4px}.woocommerce-recommended-payments-card .woocommerce-list__item-enter{opacity:0;max-height:100vh;transform:none}.woocommerce-recommended-payments-card .woocommerce-list__item-enter-active{opacity:1;transition:opacity .2s}.woocommerce-recommended-payments-card .woocommerce-list__item-after .components-button{margin-right:12px}.woocommerce-recommended-payments-card .woocommerce-list__item-before img{max-width:96px;height:36px;border-radius:8px}.woocommerce-recommended-payments-card .woocommerce-list__item-text,.woocommerce-recommended-payments-card .woocommerce-recommended-payments__header-heading{max-width:749px}@media(max-width:782px){.woocommerce-recommended-payments-card{margin:0 0 10px}}:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color--rgb:0,124,186;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-10--rgb:0,107,161;--wp-admin-theme-color-darker-20:#005a87;--wp-admin-theme-color-darker-20--rgb:0,90,135;--wp-admin-border-width-focus:2px}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){:root{--wp-admin-border-width-focus:1.5px}}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color--rgb:0,133,186;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-10--rgb:0,115,161;--wp-admin-theme-color-darker-20:#006187;--wp-admin-theme-color-darker-20--rgb:0,97,135;--wp-admin-border-width-focus:2px}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){body.admin-color-light{--wp-admin-border-width-focus:1.5px}}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color--rgb:56,88,233;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-10--rgb:33,69,230;--wp-admin-theme-color-darker-20:#183ad6;--wp-admin-theme-color-darker-20--rgb:24,58,214;--wp-admin-border-width-focus:2px}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){body.admin-color-modern{--wp-admin-border-width-focus:1.5px}}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color--rgb:9,100,132;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-10--rgb:7,82,108;--wp-admin-theme-color-darker-20:#064054;--wp-admin-theme-color-darker-20--rgb:6,64,84;--wp-admin-border-width-focus:2px}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){body.admin-color-blue{--wp-admin-border-width-focus:1.5px}}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color--rgb:70,64,60;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-10--rgb:56,51,48;--wp-admin-theme-color-darker-20:#2b2724;--wp-admin-theme-color-darker-20--rgb:43,39,36;--wp-admin-border-width-focus:2px}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){body.admin-color-coffee{--wp-admin-border-width-focus:1.5px}}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color--rgb:82,63,109;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-10--rgb:70,54,93;--wp-admin-theme-color-darker-20:#3a2c4d;--wp-admin-theme-color-darker-20--rgb:58,44,77;--wp-admin-border-width-focus:2px}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){body.admin-color-ectoplasm{--wp-admin-border-width-focus:1.5px}}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color--rgb:225,77,67;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-10--rgb:221,56,45;--wp-admin-theme-color-darker-20:#d02c21;--wp-admin-theme-color-darker-20--rgb:208,44,33;--wp-admin-border-width-focus:2px}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){body.admin-color-midnight{--wp-admin-border-width-focus:1.5px}}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color--rgb:98,124,131;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-10--rgb:87,110,116;--wp-admin-theme-color-darker-20:#4c6066;--wp-admin-theme-color-darker-20--rgb:76,96,102;--wp-admin-border-width-focus:2px}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){body.admin-color-ocean{--wp-admin-border-width-focus:1.5px}}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color--rgb:221,130,59;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-10--rgb:217,116,38;--wp-admin-theme-color-darker-20:#c36922;--wp-admin-theme-color-darker-20--rgb:195,105,34;--wp-admin-border-width-focus:2px}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){body.admin-color-sunrise{--wp-admin-border-width-focus:1.5px}}.woocommerce_tax_settings_conflict_error{padding:15px 10px;min-width:400px;width:400px}.woocommerce_tax_settings_conflict_error_card_body{display:flex;flex-direction:row;padding-left:16px!important}.woocommerce_tax_settings_conflict_error_card_body__warning_icon{width:20px;height:20px;margin-block:10px;margin-left:16px}.woocommerce_tax_settings_conflict_error_card_body__body_text{margin-bottom:16px}.woocommerce_tax_settings_conflict_error_card_body__close_icon{padding:0;height:24px;color:grey}.woocommerce_admin_tax_settings_slotfill_td,.woocommerce_admin_tax_settings_slotfill_th{padding:0!important}[aria-labelledby=select2-woocommerce_tax_display_shop-container].is-conflict{border:red!important;border-style:solid!important;border-width:1px!important}style.css000064400000210256151547157720006442 0ustar00.woocommerce-admin-page .wrap{margin:0}.woocommerce-admin-page #wpcontent,.woocommerce-admin-page.woocommerce_page_wc-admin #wpbody-content{padding:0;overflow-x:hidden!important;min-height:calc(100vh - 32px)}@media(min-width:783px){.woocommerce-admin-page #wpbody-content{padding-left:0}}@media(max-width:782px){.woocommerce-admin-page .wp-responsive-open #woocommerce-embedded-root,.woocommerce-admin-page .wp-responsive-open #wpbody{position:relative;right:-14.5em}.woocommerce-admin-page #wpbody-content,.woocommerce-admin-page #wpcontent{min-height:calc(100vh - 46px)}}@media(min-width:961px){.woocommerce-admin-page #toplevel_page_wcadmin--analytics.menu-top>a:focus,.woocommerce-admin-page #toplevel_page_woocommerce.menu-top>a:focus{padding-bottom:1px}}#wpbody,.woocommerce-layout *{box-sizing:border-box}#wpbody{display:inline-block;width:100%;padding-top:0;margin-top:60px}#wpfooter{display:none}.woocommerce_page_wc-admin .woocommerce-filters-date__content:not(.is-mobile){z-index:2}@media(max-width:600px){#wpadminbar{position:fixed}html.wp-toolbar{padding-top:46px}}@media(max-width:782px){.jetpack-masterbar #wpadminbar #wp-admin-bar-menu-toggle{margin-top:-10px}.jetpack-masterbar #wpwrap .woocommerce-layout__header-heading{padding-left:60px}.jetpack-masterbar.wp-admin .wrap h1,.jetpack-masterbar.wp-admin .wrap h2{padding-left:0}}.woocommerce-admin-page .wp-has-current-submenu:after{right:0;content:" ";height:0;width:0;position:absolute;pointer-events:none;border:8px solid transparent;border-right-color:#f1f1f1;top:0;margin-top:10px}@media(max-width:960px){.woocommerce-admin-page .wp-has-current-submenu:after{border-width:4px;margin-top:14px}}:root{--large-gap:40px;--main-gap:24px}@media(max-width:960px){:root{--large-gap:24px}}@media(max-width:782px){:root{--large-gap:16px;--main-gap:16px}}@keyframes loading-fade{0%{opacity:.7}50%{opacity:1}to{opacity:.7}}.woocommerce-layout select:hover{color:#1a1a1a}.woocommerce-layout .components-base-control select.components-select-control__input{max-width:100%;line-height:normal}.woocommerce-layout .components-panel__body>.components-panel__body-title:hover,.woocommerce-layout .woocommerce-experimental-list__item:hover,.woocommerce-layout .woocommerce-inbox-message:hover{background:#f6f7f7}.woocommerce-layout__jitm .jitm-card{margin:3rem 1.25rem 1.25rem}.woocommerce-layout__jitm-hide{display:none}body.woocommerce-admin-page .components-button.is-primary:not(:disabled):not([aria-disabled=true]):hover{color:#fff}body.woocommerce-admin-page .components-snackbar .components-button.is-tertiary{color:#fff}body.woocommerce-admin-page .components-snackbar .components-button.is-tertiary:not(:disabled):not([aria-disabled=true]):hover{color:#fff}.woocommerce-embed-page #wpbody .woocommerce-layout,.woocommerce-embed-page .woocommerce-layout__notice-list-hide+.wrap{padding-top:10px}.woocommerce-embed-page #wpbody-content,.woocommerce-embed-page #wpcontent{overflow-x:initial!important}.woocommerce-embed-page #wpbody-content{padding-top:0}.woocommerce-embed-page #wpbody-content .notice{margin-top:15px}.woocommerce-embed-page .wrap{padding:0 20px}@media(max-width:782px){.woocommerce-embed-page .wrap p.search-box{width:calc(100% - 40px)}}.woocommerce-embed-page .wrap .wrap{padding:0}.woocommerce-embed-page #screen-meta{border-right:0;margin:0}.woocommerce-embed-page #screen-meta-links{position:relative}.woocommerce-embed-page .notice{padding:1px 12px}.woocommerce-embed-page .woocommerce-layout__header.is-scrolled{box-shadow:0 8px 16px 0 rgba(85,93,102,.3)}.woocommerce-embed-page .woocommerce-layout__header .woocommerce-layout__header-heading{margin-top:0;margin-bottom:0}.woocommerce-embed-page #screen-meta-links.is-hidden-by-notices,.woocommerce-embed-page #screen-meta.is-hidden-by-notices{display:none!important}.woocommerce-embed-page .woocommerce-layout__primary{margin:0}@media(max-width:782px){.woocommerce-embed-page .woocommerce-layout__primary{padding-top:10px}}@keyframes isLoaded{0%{opacity:0}to{opacity:1}}.woocommerce-embed-page .woocommerce-layout__activity-panel-tabs{animation:isLoaded;animation-duration:2s}.woocommerce-embed-page .woocommerce-layout__notice-list-show{margin-top:10px;margin-bottom:16px}@media(max-width:600px){.woocommerce-embed-page .woocommerce-layout__notice-list-show{margin-top:80px;margin-bottom:-16px}}@media(min-width:601px)and (max-width:782px){.woocommerce-embed-page .woocommerce-layout__notice-list-show{margin-top:32px}}.woocommerce-embed-page .woocommerce-activity-card__actions a.components-button:not(.is-primary){color:#2e4453}.woocommerce-layout{margin:0;padding:0}.woocommerce-layout__no-match{display:flex;align-items:center;justify-content:center}.woocommerce-layout__no-match .components-card{max-width:680px;width:100%}.woocommerce-layout__primary{margin:var(--large-gap) 0 128px var(--large-gap)}@media(max-width:782px){.woocommerce-layout__primary{margin-top:20px}}.woocommerce-layout .woocommerce-layout__main{padding-right:40px;padding-right:var(--large-gap);max-width:100%}.woocommerce-admin-is-loading #adminmenumain,.woocommerce-admin-is-loading #wpadminbar,.woocommerce-admin-is-loading #wpbody-content,.woocommerce-admin-is-loading #wpcontent,.woocommerce-admin-is-loading #wpfooter,.woocommerce-admin-is-loading .components-modal__screen-overlay,.woocommerce-admin-is-loading .error,.woocommerce-admin-is-loading .notice,.woocommerce-admin-is-loading .update-nag,.woocommerce-admin-is-loading .updated,.woocommerce-admin-is-loading .woocommerce-layout__header,.woocommerce-admin-is-loading .woocommerce-message,.woocommerce-admin-is-loading .woocommerce-store-alerts,.woocommerce-admin-page .update-nag{display:none}.woocommerce-admin-full-screen{background:#f6f7f7;color:#50575e;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif}.woocommerce-admin-full-screen #wpwrap{top:0}.woocommerce-admin-full-screen #wpbody-content{min-height:100vh!important}.woocommerce-admin-full-screen #adminmenumain,.woocommerce-admin-full-screen #wpcontent>*,.woocommerce-admin-full-screen .error,.woocommerce-admin-full-screen .notice,.woocommerce-admin-full-screen .update-nag,.woocommerce-admin-full-screen .updated,.woocommerce-admin-full-screen .woocommerce-layout__header,.woocommerce-admin-full-screen .woocommerce-message,.woocommerce-admin-full-screen .woocommerce-store-alerts{display:none}.woocommerce-admin-full-screen #wpcontent{margin-left:0!important}.woocommerce-admin-full-screen #wpcontent>#wpbody{display:block;margin-top:0!important}.woocommerce-admin-full-screen.has-woocommerce-navigation #wpbody{padding-left:0}.is-wp-toolbar-disabled #wpadminbar{display:none!important}.wp-toolbar .is-wp-toolbar-disabled{margin-top:-32px}@media(max-width:600px){.wp-toolbar .is-wp-toolbar-disabled{margin-top:-46px}}.woocommerce-admin-page .components-modal__frame .components-button.is-button,.woocommerce-profile-wizard__body .components-button.is-button,.woocommerce-task-dashboard__container .components-button.is-button{height:48px;padding-left:25px;padding-right:25px;text-align:center;font-size:14px;line-height:36px;font-weight:500;align-items:center}.woocommerce-admin-page .components-modal__frame .components-button.is-button:disabled,.woocommerce-profile-wizard__body .components-button.is-button:disabled,.woocommerce-task-dashboard__container .components-button.is-button:disabled{cursor:not-allowed}.components-modal__header .components-button svg+span{display:none}.components-modal__frame.woocommerce-usage-modal{width:600px;max-width:100%}.components-modal__frame.woocommerce-usage-modal .components-modal__header{margin-bottom:0}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__wrapper{flex-grow:1;display:flex;flex-direction:column}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__wrapper a{color:#50575e}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__wrapper button.is-primary{align-self:flex-end}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__actions{display:flex;justify-content:flex-end;margin-top:16px}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__actions button{margin-left:16px}.woocommerce-payments__usage-modal .components-modal__header{height:auto;padding:24px 24px 0}.woocommerce-payments__usage-modal .components-modal__header .components-modal__header-heading{font-size:24px;line-height:32px;margin:0 0 24px}.woocommerce-payments__usage-modal .woocommerce-payments__usage-modal-message{padding:16px 0;font-size:16px;line-height:24px}.woocommerce-payments__usage-modal .woocommerce-payments__usage-footer{display:flex;justify-content:flex-end;padding:16px 0}.woocommerce-payments__usage-modal .woocommerce-payments__usage-footer button{margin-left:16px}.components-modal__frame.woocommerce-cart-modal{width:600px;max-width:100%}.components-modal__frame.woocommerce-cart-modal .components-modal__header{border-bottom:0;margin-bottom:16px;margin-top:16px}.components-modal__frame.woocommerce-cart-modal .components-modal__header button{display:none}.components-modal__frame.woocommerce-cart-modal .components-modal__header-heading{font-style:normal;font-weight:400;font-size:24px;line-height:32px}.components-modal__frame.woocommerce-cart-modal .woocommerce-list{margin-top:24px}.components-modal__frame.woocommerce-cart-modal .woocommerce-list .woocommerce-list__item:first-child{border-top:1px solid #dcdcde}.components-modal__frame.woocommerce-cart-modal .woocommerce-list__item{border-bottom:1px solid #dcdcde}.components-modal__frame.woocommerce-cart-modal .woocommerce-cart-modal__help-text{font-size:16px;line-height:24px}.components-modal__frame.woocommerce-cart-modal .woocommerce-cart-modal__actions{text-align:right}.components-modal__frame.woocommerce-cart-modal .woocommerce-cart-modal__actions button.is-link{margin-right:16px;text-decoration:none;font-weight:600;font-size:14px}.components-modal__frame.woocommerce-cart-modal .woocommerce-cart-modal__actions button.is-primary{align-self:flex-end}.woocommerce-layout__header{background:#fff;box-sizing:border-box;padding:0;position:fixed;width:calc(100% - 160px);top:32px;z-index:1001}.woocommerce-layout__header.is-scrolled{box-shadow:0 8px 8px 0 rgba(85,93,102,.3)}.woocommerce-layout__header .woocommerce-layout__header-wrapper{display:flex;align-items:center;min-height:60px}@media(max-width:782px){.woocommerce-layout__header{flex-flow:row wrap;top:46px;width:100%}}@media(min-width:783px)and (max-width:960px){.woocommerce-layout__header{width:calc(100% - 36px)}}.woocommerce-layout__header .woocommerce-layout__header-breadcrumbs-wrapper{display:flex;justify-content:space-between;flex-direction:row}.woocommerce-layout__header .woocommerce-layout__header-heading{display:flex;align-items:center;padding:0 0 0 40px;padding:0 0 0 var(--large-gap);flex:1 auto;height:60px;background:#fff;font-weight:600;font-size:14px}.folded .woocommerce-layout__header{width:calc(100% - 36px)}@media(max-width:782px){.folded .woocommerce-layout__header{width:100%}}.is-wp-toolbar-disabled .woocommerce-layout__header{top:0}.has-woocommerce-navigation .woocommerce-layout__header{left:0;width:100%}.woocommerce-admin-page #contextual-help-link-wrap,.woocommerce-admin-page #screen-options-link-wrap{margin-top:-1px}.wp-responsive-open .woocommerce-layout__header{margin-left:2px}.woocommerce-task-payment{display:flex;flex-direction:row;align-items:center;position:relative;overflow:hidden}.woocommerce-task-payment .components-card__media{width:85px;flex-shrink:0;align-self:flex-start}.woocommerce-task-payment .components-card__media .is-placeholder,.woocommerce-task-payment .components-card__media img,.woocommerce-task-payment .components-card__media svg{margin:auto;max-width:36px;display:block}.woocommerce-task-payment .components-card__media .is-placeholder{height:36px}.woocommerce-task-payment .woocommerce-task-payment__footer .is-placeholder{width:70px;height:36px}.woocommerce-task-payment>.components-form-toggle{min-width:52px}.woocommerce-task-payment .woocommerce-task-payment__title{display:flex;align-items:center;font-size:16px;font-weight:500;color:#2c3338;margin-top:0;margin-bottom:8px}.woocommerce-task-payment .woocommerce-task-payment__title .woocommerce-pill{margin-left:8px}.woocommerce-task-payment .woocommerce-task-payment__title .woocommerce-pill.pill-green{color:#008a20;border-color:#008a20}.woocommerce-task-payment .woocommerce-task-payment__content{font-size:14px;color:#50575e;margin:0 36px 0 0}.woocommerce-task-payment .woocommerce-task-payment__content .text-style-strong{font-weight:700}.woocommerce-task-payment .woocommerce-task-payment__content p{font-size:12px}.woocommerce-task-payment .woocommerce-task-payment__transaction-processors_images{padding-top:16px;display:flex;flex-direction:row;flex-wrap:wrap;gap:8px}.woocommerce-task-payment .woocommerce-task-payment__transaction-processors_images img{height:24px}.woocommerce-task-payment .woocommerce-task-payment__description{flex:1}@media(max-width:600px){.woocommerce-task-payment{flex-wrap:wrap}.woocommerce-task-payment .woocommerce-task-payment__content{margin:0}.woocommerce-task-payment .components-card__media{order:1;flex-basis:50%}.woocommerce-task-payment .components-card__media>img,.woocommerce-task-payment .components-card__media>svg{margin:0 0 0 24px}.woocommerce-task-payment .woocommerce-task-payment__description{order:3;padding:24px 0 0 24px}.woocommerce-task-payment .woocommerce-task-payment__footer{flex-basis:50%;align-self:flex-start;order:2;text-align:right}}.woocommerce-payment-gateway-suggestions-list-placeholder .is-placeholder{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent;display:inline-block;max-width:240px;width:80%}.woocommerce-payment-gateway-suggestions-list-placeholder .is-placeholder:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-payment-gateway-suggestions-list-placeholder .is-placeholder{animation:none}}.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-label{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent;display:inline-block;width:60%}.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-label:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-label{animation:none}}.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-icon{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent}.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-icon:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-icon{animation:none}}.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step:first-child .woocommerce-stepper__step-label{width:30%}.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-number{display:none}.woocommerce-wcpay-suggestion .woocommerce-recommended-payments-banner{min-width:100%;margin-bottom:24px}.woocommerce-wcpay-suggestion .woocommerce-recommended-payments-banner .woocommerce-recommended-payments-banner__body{padding:30px 0 0 24px;justify-content:space-between}.woocommerce-wcpay-suggestion .woocommerce-recommended-payments-banner .woocommerce-recommended-payments-banner__text_container{width:305px;margin-left:0}.woocommerce-wcpay-suggestion .woocommerce-recommended-payments-banner .woocommerce-recommended-payments-banner__footer{flex-direction:column;align-items:flex-start;padding:20px 38.2px 30px 24px}.woocommerce-wcpay-suggestion .woocommerce-recommended-payments-banner .woocommerce-recommended-payments-banner__footer_icon_container{margin-top:12px;margin-left:-5px}.woocommerce-wcpay-suggestion .woocommerce-recommended-payments-banner .woocommerce-recommended-payments-banner__footer_icon_container>svg{width:38px;height:24px;margin-right:8px}.woocommerce-wcpay-suggestion .woocommerce-wcpay-benefits{margin-bottom:24px}.woocommerce-task-payments .components-card+.components-card{margin-top:24px}.woocommerce-task-payments .woocommerce-task-payment__setup_required{display:flex;align-items:center;font-size:14px;margin-left:12px;font-weight:400;gap:3px}.woocommerce-task-payments .woocommerce-task-payment__setup_required>svg{fill:#efb854}.woocommerce-task-payments .components-card__header{font-size:20px;font-weight:400;line-height:28px;margin:0;display:flex;flex-direction:column;align-items:flex-start}.woocommerce-task-payments .woocommerce-task-payment-header__description{margin:0;color:#757575;font-weight:400;font-size:14px}.woocommerce-task-payments .components-card__footer a.components-button .gridicon{margin-left:4px}.woocommerce-task-payments .woocommerce-task-payment__recommended-pill{border:1px solid #dcdcde;border-radius:28px;display:inline-block;font-size:13px;margin-left:12px;padding:1px 10px}.woocommerce-task-payments .woocommerce-task-payment__recommended-pill span{max-width:70px}.woocommerce-task-payments .components-card__divider:last-child{display:none}.woocommerce-task-payments .woocommerce-task-payment-wcpay{margin-bottom:0}.woocommerce-task-payment-method>h3{margin:0;color:#1d2327}.woocommerce-task-payment-method p{font-size:14px;color:#646970;font-weight:400;margin-top:16px;margin-bottom:16px}.woocommerce-task-payment-method__fields{display:grid;grid-template-columns:1fr 1fr;grid-gap:0 16px;margin-bottom:8px}.woocommerce-task-payment-method__fields .components-base-control{margin-bottom:0}.woocommerce-shipping-rate{display:flex;padding-top:12px;padding-bottom:12px}.woocommerce-shipping-rate .woocommerce-shipping-rate__main{width:100%}.woocommerce-shipping-rate .woocommerce-shipping-rate__icon{padding-top:16px;margin-right:24px}.woocommerce-shipping-rate .woocommerce-shipping-rate__name{align-items:center;display:flex;padding-top:16px;font-size:16px;line-height:22px;color:#1d2327;margin-bottom:12px;border-top:1px solid #dcdcde}.woocommerce-shipping-rate .woocommerce-shipping-rate__name .components-form-toggle{margin-left:auto;height:18px}.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .components-base-control{margin-bottom:0}.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .components-base-control__label{display:block;position:relative;top:-8px;width:100%;font-size:12px}.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .text-control-with-affixes__prefix,.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .text-control-with-affixes__suffix{font-size:16px;line-height:24px;color:#646970;border:0;padding:0;align-items:center;display:flex;top:-11px}.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .components-text-control__input{position:relative;top:-11px}.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .text-control-with-affixes__prefix{margin-right:4px}.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .text-control-with-affixes__suffix{margin-left:4px}.woocommerce-task-shipping-recommendation__plugins-install{padding:24px 20px;border:1px solid #ddd;border-radius:3px;justify-content:space-around;margin-bottom:24px}.woocommerce-task-shipping-recommendation__plugins-install.dual{display:flex;flex-direction:column;justify-content:flex-start;width:285px}.woocommerce-task-shipping-recommendation__plugins-install.dual p{margin-top:0;margin-bottom:15px;color:#757575}.woocommerce-task-shipping-recommendation__plugins-install.dual .plugins-install__plugin-banner-image{display:flex;margin-bottom:24px}.woocommerce-task-shipping-recommendation__plugins-install.dual .plugins-install__plugin-banner-image img{width:120px;height:28px}.woocommerce-task-shipping-recommendation__plugins-install.single{display:flex}.woocommerce-task-shipping-recommendation__plugins-install.single .plugins-install__list{max-width:360px}.woocommerce-task-shipping-recommendation__plugins-install .plugins-install__plugin-banner-image{display:flex;width:150px}.woocommerce-task-shipping-recommendation__plugins-install .plugins-install__plugin-banner-image img{width:100%}.woocommerce-task-shipping-recommendation__plugins-install .plugins-install__list{display:flex;flex-direction:column;justify-content:space-around;gap:10px}.woocommerce-task-shipping-recommendation__plugins-install .woocommerce-task-shipping-recommendations_plugins-buttons{display:flex;justify-content:space-between;margin-top:24px;flex-grow:1;align-items:flex-end}.woocommerce-task-shipping-recommendation__plugins-install .woocommerce-task-shipping-recommendations_plugins-buttons button{min-width:40%;padding-inline:8px;margin-inline:4px}.woocommerce-task-shipping-recommendation__plugins-install .plugins-install__list-item{display:flex;align-items:center}.woocommerce-task-shipping-recommendation__plugins-install .plugins-install__list-icon{margin-right:12px}.woocommerce-task-shipping-recommendation_plugins-install-container{display:flex;flex-direction:row;flex-wrap:wrap;gap:10px}.woocommerce-task-shipping-recommendations_skip-button.dual{margin-inline:calc(50% - 34px)}.woocommerce-task-marketing .components-card__header{flex-direction:column;align-items:flex-start;display:flex!important}.woocommerce-task-marketing .components-card__header h2{align-self:start!important;color:#1e1e1e;font-size:20px;margin-bottom:0}.woocommerce-task-marketing .components-card__header span{margin-left:0;margin-top:8px;color:#757575}.woocommerce-plugin-list__plugin{display:flex;padding:24px;border-top:1px solid #e0e0e0}.woocommerce-plugin-list__plugin:first-child{border-top:0}.woocommerce-plugin-list__plugin h4{margin-bottom:12px;font-weight:600;color:#1e1e1e}.woocommerce-plugin-list__plugin p{color:#757575;font-weight:400}.woocommerce-plugin-list__plugin-logo{margin-right:45px;display:flex;align-items:center}.woocommerce-plugin-list__plugin-logo img{width:50px}.woocommerce-plugin-list__plugin-text{max-width:370px;margin-right:16px}.woocommerce-plugin-list__plugin-text .woocommerce-pill{color:#646970;margin-left:12px}.woocommerce-plugin-list__plugin-action{display:flex;align-items:center;margin-left:auto}.woocommerce-plugin-list__title{padding:24px 30px 0;position:relative}.woocommerce-plugin-list__title h3{font-weight:500;color:#000;text-transform:uppercase}.woocommerce-plugin-list__plugin{border-top:none;border-bottom:1px solid #e0e0e0}.woocommerce-tax-partner-card{border:1px solid #ddd;border-radius:3px;margin:0 8px 16px;padding:20px;display:flex;flex-direction:column}.woocommerce-tax-partner-card a{text-decoration:none}.woocommerce-tax-partner-card__logo{margin-bottom:8px}.woocommerce-tax-partner-card__logo img{max-height:35px}.woocommerce-tax-partner-card__description{color:#757575}.woocommerce-tax-partner-card__benefits{color:#1e1e1e;list-style:none}.woocommerce-tax-partner-card__benefits li{margin-bottom:8px;display:flex}.woocommerce-tax-partner-card__benefits li svg{margin-right:10px}.woocommerce-tax-partner-card__action{margin-top:auto}.woocommerce-tax-partner-card__terms{color:#949494;font-size:9px;margin-bottom:8px}.woocommerce-tax-partners__partners{display:grid;grid-template-columns:1fr 1fr}.woocommerce-tax-partners__partners.woocommerce-tax-partners__partners-count-1{grid-template-columns:1fr}@media(max-width:782px){.woocommerce-tax-partners__partners{grid-template-columns:1fr}}.woocommerce-tax-partners__partners-count-1 .woocommerce-tax-partners__partners{grid-template-columns:1fr;justify-items:center}.woocommerce-tax-partners .components-card__body.is-size-medium{padding:36px}.woocommerce-tax-partners .components-card__header{line-height:28px;font-size:20px}.woocommerce-tax-partners__other-actions{text-align:center;list-style:none;display:flex;align-items:center;justify-content:center;margin:0}@media(max-width:782px){.woocommerce-tax-partners__other-actions{flex-direction:column}}.woocommerce-tax-partners__other-actions li{margin-top:16px;margin-right:4px}.woocommerce-tax-partners__other-actions li button.is-tertiary{padding:0;height:auto}.woocommerce-tax-partners__other-actions li:after{content:"•";color:#bbb;margin-left:4px}@media(max-width:782px){.woocommerce-tax-partners__other-actions li:after{content:""}}.woocommerce-tax-partners__other-actions li:last-child{margin-right:0}.woocommerce-tax-partners__other-actions li:last-child:after{content:""}.woocommerce-task-tax__automated-tax-control{display:flex;align-items:center;margin-top:16px}.woocommerce-task-tax__automated-tax-control i{margin-left:16px;margin-right:24px}.woocommerce-task-tax__automated-tax-control .woocommerce-task-tax__automated-tax-control-inner{border-top:1px solid #dcdcde;display:flex;align-items:center;flex:1;font-size:16px;padding-top:16px;padding-bottom:16px}.woocommerce-task-tax__automated-tax-control .components-form-toggle{margin-left:auto}.woocommerce-task-tax__success{display:flex;flex-direction:column;align-items:center;padding:40px;text-align:center}.woocommerce-task-tax__success .woocommerce-task-tax__success-icon{font-size:48px;height:48px;align-items:center;display:flex}.woocommerce-task-tax__success #woocommerce-task-tax__success-message{font-size:32px;font-weight:400}.woocommerce-task-tax__success p{margin-top:0;font-size:16px}.woocommerce-task-dashboard__container .woocommerce-task-card{max-width:680px;margin-left:auto;margin-right:auto;margin-bottom:24px}.woocommerce-task-dashboard__container .woocommerce-task-card .components-card__header.is-size-large{padding-bottom:12px}.woocommerce-task-dashboard__container .woocommerce-task-card .components-card__header.is-size-large .woocommerce-card__menu{margin-top:8px}.woocommerce-task-dashboard__container .woocommerce-task-card .wooocommerce-task-card__header{display:flex}.woocommerce-task-dashboard__container .woocommerce-task-card .wooocommerce-task-card__header .components-text+.woocommerce-badge{margin-left:16px}.woocommerce-task-dashboard__container .woocommerce-task-card .woocommerce-list__item-text .woocommerce-pill{padding:1px 8px;margin-left:8px}.woocommerce-task-dashboard__container .woocommerce-task-card .components-popover__content{min-width:unset}.woocommerce-task-dashboard__container .woocommerce-task-payments{width:680px;margin:auto;max-width:100%}.woocommerce-task-list__item-expandable-content,.woocommerce-task__additional-info,.woocommerce-task__estimated-time{color:#757575;font-weight:400;font-size:12px}.woocommerce-task-list__item-expandable-content{font-size:13px}.components-modal__screen-overlay{background:rgba(43,45,47,.4)}.components-modal__frame .components-modal__header{margin-bottom:0}.components-modal__frame .woocommerce-task-payments__stripe-error-wrapper{align-items:flex-end;flex-grow:1;display:flex;flex-direction:column}.woocommerce-task-dashboard__container .woocommerce-stepper button.components-button.is-primary{margin:0 8px 0 0}.woocommerce-task-dashboard__container button.components-button.is-link{margin:0;height:auto;color:#50575e;font-weight:400}.woocommerce-task-card__prompt{width:100%;min-width:100%;margin-bottom:24px;margin-top:-4px;cursor:default}.woocommerce-task-card__prompt .components-snackbar__content{display:block;align-items:unset;justify-content:unset}.woocommerce-task-card__prompt .components-snackbar__content span{margin-left:-24px}.woocommerce-task-card__prompt .woocommerce-task-card__prompt-actions button.is-link,.woocommerce-task-card__prompt .woocommerce-task-card__prompt-actions button.is-link:active,.woocommerce-task-card__prompt .woocommerce-task-card__prompt-actions button.is-link:focus{color:#fff;margin-left:24px;background:transparent}.woocommerce-task-card__prompt .woocommerce-task-card__prompt-actions button.is-link:hover{color:#fff}.woocommerce-task-card__prompt .woocommerce-task-card__prompt-pointer{border-bottom:10px solid #1e1e1e;border-left:10px solid transparent;border-right:10px solid transparent;position:relative;width:0;height:0;display:inline-block;top:-30px}.woocommerce-task-card__prompt .woocommerce-task-card__prompt-content{display:flex;align-items:baseline;justify-content:space-between;max-height:10px;margin-left:24px;position:relative;top:-40px}.woocommerce-task-card__prompt .woocommerce-task-card__prompt-actions{margin-right:-16px}.woocommerce-task-card__prompt:hover .woocommerce-task-card__prompt-pointer{border-bottom-color:#1e1e1e}.woocommerce-task-dashboard__container .muriel-component{margin-top:16px;margin-bottom:16px}.woocommerce-task-dashboard__container .components-base-control.has-error{margin-bottom:32px!important;border-color:#d63638}@media(max-width:782px){.woocommerce-task-dashboard__container .components-base-control.has-error{margin-bottom:44px!important}}.woocommerce-task-dashboard__container .components-base-control.has-error .components-base-control__help{top:100%;left:12px;position:absolute;margin-top:4px;font-size:12px;font-style:normal;color:#d63638}.woocommerce-task-dashboard__container .components-form-toggle{display:inline-block}.woocommerce-task-dashboard__container .components-form-toggle label{font-size:14px}.woocommerce-task-dashboard__container .components-form-toggle .components-base-control{display:inline-block}.woocommerce-task-dashboard__container .components-form-toggle .components-base-control__field{margin-bottom:0}.woocommerce-task-dashboard__container .woocommerce-task__caption{color:#757575;margin-top:16px}.woocommerce-task-list__setup .woocommerce-experimental-list .woocommerce-experimental-list__item.complete{text-decoration:line-through;color:var(--wp-admin-theme-color)}.woocommerce-task-list__setup .woocommerce-experimental-list .woocommerce-experimental-list__item.complete .woocommerce-task-list__item-title{color:var(--wp-admin-theme-color)}.woocommerce-task-progress-header{position:relative;min-height:28px}.woocommerce-task-progress-header h1{font-size:24px}.woocommerce-task-progress-header p{color:#757575;font-size:16px;line-height:24px;margin-top:8px}.woocommerce-task-progress-header .woocommerce-task-progress-header__progress-bar{appearance:none;border:1px solid #ddd;border-radius:16px;height:12px;width:100%;margin-bottom:0;background-color:#fff}.woocommerce-task-progress-header .woocommerce-task-progress-header__progress-bar::-moz-progress-bar{background-color:var(--wp-admin-theme-color);border-radius:16px}.woocommerce-task-progress-header .woocommerce-task-progress-header__progress-bar::-webkit-progress-bar{background-color:#fff;border-radius:16px}.woocommerce-task-progress-header .woocommerce-task-progress-header__progress-bar::-webkit-progress-value{background-color:var(--wp-admin-theme-color);border-bottom-left-radius:16px;border-top-left-radius:16px}.woocommerce-task-progress-header .woocommerce-card__menu{position:absolute;right:0;top:-7px}.woocommerce-setup-panel .woocommerce-task-progress-header{padding:16px}.woocommerce-setup-panel .woocommerce-task-progress-header h1{font-size:16px;font-weight:600}.woocommerce-setup-panel .woocommerce-task-progress-header p{font-size:16px}.woocommerce-setup-panel .woocommerce-task-progress-header .woocommerce-card__menu{top:17px}@media(max-width:782px){.woocommerce-task-progress-header{padding-left:var(--large-gap);padding-right:var(--large-gap)}}h1.woocommerce-task-progress-header__title{padding-top:4px}@media(max-width:782px){h1.woocommerce-task-progress-header__title{padding-left:var(--large-gap);padding-right:var(--large-gap)}}.wooocommerce-task-card__header .wooocommerce-task-card__header-subtitle{color:#757575;margin-bottom:24px}.wooocommerce-task-card__finished-header-image{max-width:75%}.customer-feedback-simple__container{height:64px}.woocommerce-task-card__header-menu{position:absolute;right:0;top:0}.wooocommerce-task-card__header-ces-feedback{height:64px;display:flex;justify-content:center;align-items:center}.woocommerce-task-header__contents-container.woocommerce-task-header__customize-store img.svg-background{padding-right:16px;padding-top:9px}.woocommerce-homescreen .woocommerce-task-dashboard__container:empty{margin-bottom:0}.woocommerce-task-dashboard__container .woocommerce-homescreen-card{max-width:none;width:100%}.woocommerce-task-dashboard__container .wooocommerce-task-card__header-container{display:flex;position:relative;border-bottom:1px solid #dcdcde}.woocommerce-task-dashboard__container .wooocommerce-task-card__header{width:100%;flex:1}.woocommerce-task-dashboard__container .woocommerce-ellipsis-menu{position:absolute;top:16px;right:24px}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .components-card__header{background:#fff;height:130px;display:block}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .components-card__header .is-placeholder{margin:20px;width:100%;height:90px}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .wooocommerce-task-card__header{align-self:inherit}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-task-list__item-before .is-placeholder{height:36px;width:36px;border-radius:50%}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-task-list__item-text .is-placeholder{width:80%}.woocommerce-task-dashboard__container .woocommerce-task-card.completed,.woocommerce-task-dashboard__container .woocommerce-task-card.completed .components-card__header{display:block}.woocommerce-task-dashboard__container .woocommerce-task-card.completed h2{margin-top:24px;margin-bottom:12px}.woocommerce-task-dashboard__container .woocommerce-task-card.completed .wooocommerce-task-card__header{display:block;text-align:center}.woocommerce-task-dashboard__container .woocommerce-task-card.completed button.is-secondary{margin-right:12px}.woocommerce-task-dashboard__container.setup-task-list{max-width:1032px;display:flex;flex-direction:row;margin:0 auto;justify-content:space-between}.woocommerce-task-dashboard__container.setup-task-list ul li.complete .woocommerce-task-list__item-title{font-weight:600;color:#949494}.woocommerce-task-dashboard__container.setup-task-list ul li{display:block;width:100%;border-right:1px solid #e0e0e0;border-top:none;padding:16px 24px}.woocommerce-task-dashboard__container.setup-task-list ul li:last-child{border-right:none}.woocommerce-task-dashboard__container.setup-task-list ul li.is-active{box-shadow:inset 0 -4px 0 0 var(--wp-admin-theme-color)}.woocommerce-task-dashboard__container.setup-task-list ul li.is-active .woocommerce-task-list__item-badge{background-color:#fff;position:relative;z-index:1}.woocommerce-task-dashboard__container.setup-task-list ul li.is-active:after{background-color:var(--wp-admin-theme-color);opacity:.1;content:"";top:0;left:0;position:absolute;width:100%;height:100%;pointer-events:none}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete) .woocommerce-task__icon{border:1px solid var(--wp-admin-theme-color);background:transparent}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item.complete:not(.complete) .woocommerce-task__icon{border:none}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item-before{display:block;padding:0}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item-text{margin-top:10px}.woocommerce-task-dashboard__container.setup-task-list .numbered-circle,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-1 .woocommerce-task__icon:after,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-2 .woocommerce-task__icon:after,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-3 .woocommerce-task__icon:after,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-4 .woocommerce-task__icon:after,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-5 .woocommerce-task__icon:after,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-6 .woocommerce-task__icon:after,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-7 .woocommerce-task__icon:after,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-8 .woocommerce-task__icon:after,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-9 .woocommerce-task__icon:after,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-10 .woocommerce-task__icon:after{justify-content:center;display:flex;align-items:center;height:100%;width:100%}.woocommerce-task-dashboard__container.setup-task-list ul{display:block}.woocommerce-task-dashboard__container.setup-task-list ul li{display:grid;grid-template-columns:48px auto 48px;border-right:none;border-bottom:1px solid #e0e0e0}.woocommerce-task-dashboard__container.setup-task-list ul li.is-active{box-shadow:inset 5px 0 0 0 var(--wp-admin-theme-color);transition:box-shadow .1s linear}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item-text{display:block;padding:0;margin-top:0}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents{max-width:65%}@media(max-width:600px){.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents{max-width:100%}}.woocommerce-task-dashboard__container.setup-task-list .svg-background{right:6%}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-1 .woocommerce-task__icon:after{content:"1";color:var(--wp-admin-theme-color);font-weight:700}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-2 .woocommerce-task__icon:after{content:"2";color:var(--wp-admin-theme-color);font-weight:700}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-3 .woocommerce-task__icon:after{content:"3";color:var(--wp-admin-theme-color);font-weight:700}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-4 .woocommerce-task__icon:after{content:"4";color:var(--wp-admin-theme-color);font-weight:700}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-5 .woocommerce-task__icon:after{content:"5";color:var(--wp-admin-theme-color);font-weight:700}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-6 .woocommerce-task__icon:after{content:"6";color:var(--wp-admin-theme-color);font-weight:700}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-7 .woocommerce-task__icon:after{content:"7";color:var(--wp-admin-theme-color);font-weight:700}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-8 .woocommerce-task__icon:after{content:"8";color:var(--wp-admin-theme-color);font-weight:700}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-9 .woocommerce-task__icon:after{content:"9";color:var(--wp-admin-theme-color);font-weight:700}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item:not(.complete).index-10 .woocommerce-task__icon:after{content:"10";color:var(--wp-admin-theme-color);font-weight:700}@media(max-width:782px){.woocommerce-task-dashboard__container.setup-task-list ul{display:block}.woocommerce-task-dashboard__container.setup-task-list ul li{display:grid;grid-template-columns:48px auto 48px;border-right:none;border-bottom:1px solid #e0e0e0}.woocommerce-task-dashboard__container.setup-task-list ul li.is-active{box-shadow:inset 5px 0 0 0 var(--wp-admin-theme-color);transition:box-shadow .1s linear}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-list__item-text{display:block;padding:0;margin-top:0}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents{max-width:65%}}@media(max-width:782px)and (max-width:782px){.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents{max-width:380px}}@media(max-width:782px)and (max-width:600px){.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents{max-width:100%}}@media(max-width:782px){.woocommerce-task-dashboard__container.setup-task-list .svg-background{right:6%}}.woocommerce-setup-panel .woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents-container{padding:16px}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents-container{padding:20px 24px;position:relative;flex:1;overflow:hidden;width:100%}.woocommerce-task-dashboard__container.setup-task-list .svg-background{position:absolute;z-index:0;right:24px;max-width:25%;max-height:150px;width:auto;height:auto}@media(max-width:600px){.woocommerce-task-dashboard__container.setup-task-list .svg-background{display:none}}@media(max-width:782px){.woocommerce-task-dashboard__container.setup-task-list .svg-background{right:.5%;width:40%}}.woocommerce-task-dashboard__container.setup-task-list .svg-background .admin-theme-color{fill:var(--wp-admin-theme-color)}.woocommerce-task-dashboard__container.setup-task-list .svg-background .admin-theme-color-darker-10{fill:var(--wp-admin-theme-color-darker-10)}.woocommerce-task-dashboard__container.setup-task-list .svg-background .admin-theme-color-darker-20{fill:var(--wp-admin-theme-color-darker-20)}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents{max-width:70%;position:relative;z-index:1}@media(max-width:782px){.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents{max-width:380px}}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents p,.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents span{color:#757575}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__contents p:first-of-type{margin-top:4px}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__timer{display:flex;align-items:center;line-height:22px;margin-bottom:0}.woocommerce-task-dashboard__container.setup-task-list .woocommerce-task-header__timer img{margin-right:6px}.woocommerce-task-dismiss-modal{width:565px;max-width:100%}.woocommerce-task-dismiss-modal .components-modal__header{border-bottom:1px solid #ddd}.woocommerce-task-dismiss-modal .woocommerce-usage-modal__message{box-sizing:border-box;border-bottom:1px solid #ddd;padding:0 32px;display:flex;flex-direction:row;justify-content:space-between;background:#fff;align-items:center;height:60px;z-index:10;position:relative;position:sticky;top:0;margin:0 -32px 24px;font-size:1.2em}.woocommerce-task-dismiss-modal .woocommerce-usage-modal__actions{display:flex;justify-content:flex-end;margin-top:16px}.woocommerce-task-dismiss-modal .woocommerce-usage-modal__actions button{margin-left:16px}.woocommerce-layout__header-back-button{cursor:pointer;margin-left:40px;margin-left:var(--large-gap);margin-right:-16px;display:flex;z-index:2}.woocommerce-layout__header-back-button:focus{box-shadow:inset -1px -1px 0 #757575,inset 1px 1px 0 #757575}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-card__body{border-top:1px solid #dcdcde}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .is-placeholder{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent;display:inline-block;height:16px}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .is-placeholder:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .is-placeholder{animation:none}}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-card__title .is-placeholder{width:70%;height:28px}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-list__item-before .is-placeholder{height:36px;width:36px}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-list__item-text{width:100%}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-list__item-text .woocommerce-list__item-title .is-placeholder{height:22px;width:60%}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-list__item-after .is-placeholder{height:18px;width:60px}.woocommerce-task-list__item.woocommerce-list__item-enter{opacity:0;max-height:0}.woocommerce-task-list__item.woocommerce-list__item-enter-active{opacity:1;max-height:100px;transition:opacity .5s,max-height .5s}.woocommerce-task-list__item.woocommerce-list__item-exit{opacity:1;max-height:100px}.woocommerce-task-list__item.woocommerce-list__item-exit-active{opacity:0;max-height:0;transition:opacity .5s,max-height .5s}.woocommerce-layout__header-tasks-reminder-bar{height:40px;background-color:#007cba;display:flex;justify-content:space-between;align-items:center;color:#fff}.woocommerce-layout__header-tasks-reminder-bar:before{content:""}.woocommerce-layout__header-tasks-reminder-bar button{color:inherit}.woocommerce-layout__header-tasks-reminder-bar button:hover{opacity:.7;color:inherit}.woocommerce-layout__header-tasks-reminder-bar a{color:inherit}.woocommerce-layout__header-tasks-reminder-bar p{font-size:13px}.woocommerce-layout__footer{background:#fff;border-top:1px solid #e0e0e0;box-sizing:border-box;padding:0;position:fixed;width:calc(100% - 160px);bottom:-1px;z-index:1001}.woocommerce-profile-wizard__body .woocommerce-layout__footer{width:100%}@media(min-width:783px)and (max-width:960px){.woocommerce-layout__footer{width:calc(100% - 36px)}}@media(max-width:782px){.woocommerce-layout__footer{flex-flow:row wrap;width:100%}}.folded .woocommerce-layout__footer{width:calc(100% - 36px)}.woocommerce-transient-notices{position:absolute;left:16px;bottom:100%;margin-bottom:12px;z-index:100001;width:auto}.woocommerce-profile-wizard__body .woocommerce-transient-notices{left:unset;width:100%}.woocommerce-profile-wizard__body .woocommerce-transient-notices .components-snackbar{margin-left:auto;margin-right:auto}.components-snackbar.components-snackbar-explicit-dismiss{cursor:default}.components-snackbar__content{white-space:pre-line}.components-snackbar .components-snackbar__content-with-icon{margin-left:32px}.components-snackbar .components-snackbar__icon{position:absolute;top:24px;left:26px}.components-snackbar .components-snackbar__dismiss-button{margin-left:32px;cursor:pointer}.woocommerce-layout__activity-panel{display:flex;flex-direction:row;align-items:center;height:60px}.woocommerce-layout__activity-panel-tabs{width:100%;display:flex;height:60px;justify-content:flex-end}.woocommerce-layout__activity-panel-tabs .dashicon,.woocommerce-layout__activity-panel-tabs .gridicon{width:100%}.woocommerce-layout__activity-panel-tabs svg{width:24px;height:24px}.woocommerce-layout__activity-panel-tabs svg.woocommerce-layout__activity-panel-tab-icon{fill:none}.woocommerce-layout__activity-panel-tabs svg.woocommerce-layout__activity-panel-tab-icon path,.woocommerce-layout__activity-panel-tabs svg.woocommerce-layout__activity-panel-tab-icon rect{stroke:currentColor}.woocommerce-layout__activity-panel-tabs svg .setup-progress-slice{stroke:none}.woocommerce-layout__activity-panel-tabs svg .setup-progress-ring{stroke-width:2px}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__homescreen-display-options svg.woocommerce-layout__activity-panel-tab-icon{height:14px}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__homescreen-extension-tasklist-toggle{min-width:205px}.woocommerce-layout__activity-panel-tabs .components-icon-button{display:initial;text-indent:0;border-radius:0}.woocommerce-layout__activity-panel-tabs .components-icon-button.has-text svg{margin:0}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab{display:flex;flex-direction:column;justify-content:center;align-items:center;position:relative;border:none;outline:none;cursor:pointer;background-color:#fff;width:100%;height:60px;color:#757575;white-space:nowrap}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:before{background-color:var(--wp-admin-theme-color);bottom:0;content:"";height:0;opacity:0;transition-property:height,opacity;transition-duration:.3s;transition-timing-function:ease-in-out;left:0;position:absolute;right:0}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.is-active,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.is-opened{color:#1e1e1e;box-shadow:none}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.is-active:before,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.is-opened:before{height:3px;opacity:1}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.woocommerce-layout__activity-panel-tab-wordpress-notices:after{content:" ";position:absolute;padding:1px;background:#d94f4f;border:2px solid #fff;width:4px;height:4px;display:inline-block;border-radius:50%;top:8px;left:50%}@media(min-width:783px)and (max-width:960px){.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.woocommerce-layout__activity-panel-tab-wordpress-notices:after{right:18px;left:auto;margin-left:0}}@media(min-width:961px){.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.woocommerce-layout__activity-panel-tab-wordpress-notices:after{right:28px;left:auto;margin-left:0}}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.components-button:not(:disabled):not([aria-disabled=true]):hover,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:hover{background-color:#f0f0f0;box-shadow:none}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.components-button:not(:disabled):not([aria-disabled=true]):hover.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.components-button:not(:disabled):not([aria-disabled=true]):hover.woocommerce-layout__activity-panel-tab-wordpress-notices:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:hover.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:hover.woocommerce-layout__activity-panel-tab-wordpress-notices:after{border-color:#e0e0e0}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.components-button:not(:disabled):not([aria-disabled=true]):focus,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:focus{box-shadow:inset -1px -1px 0 #757575,inset 1px 1px 0 #757575}@media(max-width:782px){.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.display-options{display:none}}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-popover{margin-top:0;z-index:1001}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-popover .components-menu-group{padding:12px}.woocommerce-layout__activity-panel-toggle-bubble.has-unread:after{content:" ";position:absolute;padding:1px;background:#ca4a1f;border:2px solid #fff;width:4px;height:4px;display:inline-block;border-radius:50%;top:6px;right:4px}@keyframes tabSwitch{0%,to{transform:translateX(0)}50%{transform:translateX(100px)}}.woocommerce-layout__activity-panel-wrapper{height:calc(100vh - 106px);background:#f0f0f0;width:430px;transform:translateX(100%);transition-property:transform box-shadow;transition-duration:.3s;transition-timing-function:ease-in-out;position:absolute;right:0;top:100%;z-index:1000;overflow-x:hidden;overflow-y:auto}@media(max-width:782px){.woocommerce-layout__activity-panel-wrapper{width:100%}}@media screen and (prefers-reduced-motion:reduce){.woocommerce-layout__activity-panel-wrapper{transition-duration:1ms}}@media(min-width:783px){.woocommerce-layout__activity-panel-wrapper{height:calc(100vh - 92px)}}.has-woocommerce-navigation .woocommerce-layout__activity-panel-wrapper{height:calc(100vh - 60px);top:60px}.woocommerce-layout__activity-panel-wrapper.is-open{transform:none;box-shadow:0 12px 12px 0 rgba(85,93,102,.3)}.woocommerce-layout__activity-panel-wrapper.is-switching{animation:tabSwitch;animation-duration:.3s}@media screen and (prefers-reduced-motion:reduce){.woocommerce-layout__activity-panel-wrapper.is-switching{animation:none}}.woocommerce-layout__activity-panel-wrapper .woocommerce-empty-content{padding-left:24px;padding-right:24px}.woocommerce-layout__activity-panel-avatar-flag-overlay{position:relative;top:-12px}.woocommerce-layout__activity-panel-avatar-flag-overlay .woocommerce-flag{position:relative;top:16px;border:2px solid #fff}.woocommerce-layout__notice-list-hide{display:none}.highlight-tooltip__container{position:absolute;width:0;height:0}.highlight-tooltip__container.highlight-tooltip__show{top:0;left:0;width:100%;height:100%}.highlight-tooltip__portal{width:100%;height:100%;position:relative}.highlight-tooltip__portal .highlight-tooltip__overlay{position:fixed;top:0;right:0;bottom:0;left:0;background-color:rgba(0,0,0,.35);z-index:100000;animation:edit-post__fade-in-animation .2s ease-out 0s;animation-fill-mode:forwards}@media(prefers-reduced-motion:reduce){.highlight-tooltip__portal .highlight-tooltip__overlay{animation-duration:1ms}}.highlight-tooltip__popover .components-card{min-width:360px}.highlight-tooltip__popover .components-card__header{font-size:16px;font-size:1rem;font-weight:600;box-sizing:border-box}.highlight-tooltip__popover .components-card__footer{justify-content:flex-end;box-sizing:border-box}@media(max-width:782px){.woocommerce-layout__show-app-banner .woocommerce-layout__header-wrapper{padding-top:56px}}.woocommerce-mobile-app-banner{background-color:#3c2861;position:absolute;top:0;left:0;width:100%;display:flex;height:56px;align-items:center;padding:0 6px 0 4px}@media(min-width:401px){.woocommerce-mobile-app-banner{padding:0 13px 0 10px}}@media(min-width:783px){.woocommerce-mobile-app-banner{display:none}}.woocommerce-mobile-app-banner .gridicon{fill:#fff;margin-right:10px}.woocommerce-mobile-app-banner .components-button.is-secondary{margin-left:auto;color:#fff;box-shadow:inset 0 0 0 1px #fff}.woocommerce-mobile-app-banner .components-button.is-secondary:active,.woocommerce-mobile-app-banner .components-button.is-secondary:hover{color:#fff;box-shadow:inset 0 0 0 1px #fff;background:none}.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description{color:#fff;margin-left:8px}@media(min-width:401px){.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description{margin-left:13px}}.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description .woocommerce-mobile-app-banner__description__text{margin:0;font-size:10px}.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description .woocommerce-mobile-app-banner__description__text:first-child{font-weight:700}@media(min-width:401px){.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description .woocommerce-mobile-app-banner__description__text{margin-left:13px;font-size:13px}}.woocommerce-navigation{display:grid;grid-template-rows:min-content 1fr;height:100%}.woocommerce-navigation .woocommerce-navigation__wrapper h2>span{width:100%}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__menu{overflow-y:auto;margin-bottom:0;padding-bottom:24px}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__group+.components-navigation__group{margin-top:24px}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item{margin-bottom:0}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item .components-button{opacity:1}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item:not(:hover) .components-button{color:#ccc}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item:hover .components-button{color:#fff}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item.is-active .components-button{color:#fff}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item a.components-button{padding:6px 16px}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item:not(:hover) a.components-button{color:#ccc}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item.is-active a.components-button{color:#fff}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation{height:100%}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation>div{height:100%;display:grid;grid-template-rows:1fr min-content}.woocommerce-navigation .woocommerce-navigation__wrapper.is-root .components-navigation__menu-secondary{border-top:1px solid #2c3338;margin:0 -8px;padding:16px 8px 12px}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__group-title,.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__menu-title{color:#f0f0f0;opacity:1}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button{color:#ccc;opacity:1}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button,.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button span{font-size:13px;line-height:normal}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button:hover,.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button:hover:not(:disabled){color:#ddd}.woocommerce-navigation-header{display:flex;align-items:center;border:none;border-radius:0;height:auto}.woocommerce-navigation-header .woocommerce-navigation-header__site-icon.components-button{padding:12px;height:60px;color:#fff}.woocommerce-navigation-header .woocommerce-navigation-header__site-icon.components-button:focus,.woocommerce-navigation-header .woocommerce-navigation-header__site-icon.components-button:hover,.woocommerce-navigation-header .woocommerce-navigation-header__site-icon.components-button:not([aria-disabled=true]):active{color:#fff}.woocommerce-navigation-header .woocommerce-navigation-header__site-title.components-button{padding-left:0;color:#ccc;font-weight:600}.woocommerce-navigation-header .woocommerce-navigation-header__site-title.components-button:active,.woocommerce-navigation-header .woocommerce-navigation-header__site-title.components-button:focus,.woocommerce-navigation-header .woocommerce-navigation-header__site-title.components-button:hover{color:#e0e0e0}.woocommerce-navigation-header .woocommerce-navigation-header__site-title{padding-top:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.woocommerce-navigation{position:relative;width:240px;box-sizing:border-box;background-color:#1e1e1e;z-index:1100}@media(max-width:960px){.woocommerce-navigation{width:60px;height:60px}}.woocommerce-navigation .components-navigation{box-sizing:border-box}.woocommerce-navigation .components-navigation__menu-title{overflow:visible}.woocommerce-navigation .components-navigation__menu{scrollbar-color:#757575 #1e1e1e;scrollbar-width:thin}.woocommerce-navigation .components-navigation__menu::-webkit-scrollbar-thumb{border-radius:10px;background-color:#757575}.woocommerce-navigation .components-navigation__menu::-webkit-scrollbar-thumb:hover{background-color:#757575;width:8px;height:8px}.woocommerce-navigation .components-navigation__menu::-webkit-scrollbar{width:8px;height:8px}.woocommerce-navigation__wrapper{background-color:#1e1e1e;position:absolute;top:60px;width:100%;height:calc(100vh - 92px);overflow-y:auto}.is-wp-toolbar-disabled .woocommerce-navigation__wrapper{height:calc(100vh - 60px)}body.is-wc-nav-expanded .woocommerce-navigation{width:240px;height:100%}body.is-wc-nav-expanded font>.xdebug-error{margin-left:256px}body.is-wc-nav-folded .woocommerce-navigation{width:60px;height:60px;overflow:hidden}body.is-wc-nav-folded .woocommerce-navigation .woocommerce-navigation-header>*{display:none}body.is-wc-nav-folded .woocommerce-navigation .woocommerce-navigation-header__site-icon{display:block}body.is-wc-nav-folded .woocommerce-navigation .components-navigation{display:none}body.is-wc-nav-folded .woocommerce-transient-notices{left:16px}body.is-wc-nav-folded #wpbody{padding-left:0}.has-woocommerce-navigation #adminmenuback,.has-woocommerce-navigation #adminmenuwrap{display:none!important}.has-woocommerce-navigation.woocommerce_page_wc-reports .woo-nav-tab-wrapper,.has-woocommerce-navigation.woocommerce_page_wc-settings .woo-nav-tab-wrapper,.has-woocommerce-navigation.woocommerce_page_wc-status .woo-nav-tab-wrapper{display:none}.has-woocommerce-navigation.woocommerce_page_wc-reports .woocommerce .subsubsub,.has-woocommerce-navigation.woocommerce_page_wc-settings .woocommerce .subsubsub,.has-woocommerce-navigation.woocommerce_page_wc-status .woocommerce .subsubsub{font-size:14px;margin:5px 0}.has-woocommerce-navigation #wpcontent,.has-woocommerce-navigation #wpfooter{margin-left:0}@media(max-width:960px){.has-woocommerce-navigation #wpcontent,.has-woocommerce-navigation #wpfooter{margin-left:0}}.has-woocommerce-navigation #wpbody{padding-left:240px}@media(max-width:960px){.has-woocommerce-navigation #wpbody{padding-left:0}}.has-woocommerce-navigation .woocommerce-layout__header.is-embed-loading:before{content:"";position:fixed;width:240px;height:100%;background:#1e1e1e}@media(max-width:960px){.has-woocommerce-navigation .woocommerce-layout__header.is-embed-loading:before{width:60px;height:60px}}.has-woocommerce-navigation #woocommerce-embedded-root.is-embed-loading{margin-bottom:-32px}.has-woocommerce-navigation:not(.is-wp-toolbar-disabled) #wpbody-content{margin-top:32px}.has-woocommerce-navigation font>.xdebug-error{margin-top:60px}.woocommerce-navigation-category-title{display:flex;align-items:center;font-size:20px;line-height:28px}.woocommerce-navigation-category-title .woocommerce-navigation-favorite-button{margin-left:auto}.woocommerce-navigation-favorite-button.components-button .star-empty-icon{color:#949494}.woocommerce-navigation-favorite-button.components-button .star-filled-icon{color:#ffb900}.woocommerce-embedded-layout__primary{padding:0 20px}.woocommerce-embedded-layout__primary .components-card__footer,.woocommerce-embedded-layout__primary .components-card__header{box-sizing:border-box}@media(max-width:782px){.woocommerce-embedded-layout__primary{padding:0}}.woocommerce-recommended-payments-card{margin:0 15px 10px 0;animation:isLoaded;animation-duration:.25s}.woocommerce-recommended-payments-card .woocommerce-list__item:hover{background-color:#fff}.woocommerce-recommended-payments-card .woocommerce-list__item:hover .woocommerce-list__item-title{color:#1e1e1e}.woocommerce-recommended-payments-card .woocommerce-list__item-title{font-size:14px;color:#1e1e1e;font-weight:600}.woocommerce-recommended-payments-card .woocommerce-review-activity-card__section-controls{text-align:center}.woocommerce-recommended-payments-card .woocommerce-pill{margin-left:4px;padding:2px 8px}@media(max-width:480px){.woocommerce-recommended-payments-card .woocommerce-pill{margin-top:4px;margin-bottom:4px}}.woocommerce-recommended-payments-card .components-card__footer .gridicon{margin-left:4px}.woocommerce-recommended-payments-card .woocommerce-list__item-enter{opacity:0;max-height:100vh;transform:none}.woocommerce-recommended-payments-card .woocommerce-list__item-enter-active{opacity:1;transition:opacity .2s}.woocommerce-recommended-payments-card .woocommerce-list__item-after .components-button{margin-left:12px}.woocommerce-recommended-payments-card .woocommerce-list__item-before img{max-width:96px;height:36px;border-radius:8px}.woocommerce-recommended-payments-card .woocommerce-list__item-text,.woocommerce-recommended-payments-card .woocommerce-recommended-payments__header-heading{max-width:749px}@media(max-width:782px){.woocommerce-recommended-payments-card{margin:0 0 10px}}:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color--rgb:0,124,186;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-10--rgb:0,107,161;--wp-admin-theme-color-darker-20:#005a87;--wp-admin-theme-color-darker-20--rgb:0,90,135;--wp-admin-border-width-focus:2px}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){:root{--wp-admin-border-width-focus:1.5px}}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color--rgb:0,133,186;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-10--rgb:0,115,161;--wp-admin-theme-color-darker-20:#006187;--wp-admin-theme-color-darker-20--rgb:0,97,135;--wp-admin-border-width-focus:2px}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){body.admin-color-light{--wp-admin-border-width-focus:1.5px}}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color--rgb:56,88,233;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-10--rgb:33,69,230;--wp-admin-theme-color-darker-20:#183ad6;--wp-admin-theme-color-darker-20--rgb:24,58,214;--wp-admin-border-width-focus:2px}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){body.admin-color-modern{--wp-admin-border-width-focus:1.5px}}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color--rgb:9,100,132;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-10--rgb:7,82,108;--wp-admin-theme-color-darker-20:#064054;--wp-admin-theme-color-darker-20--rgb:6,64,84;--wp-admin-border-width-focus:2px}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){body.admin-color-blue{--wp-admin-border-width-focus:1.5px}}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color--rgb:70,64,60;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-10--rgb:56,51,48;--wp-admin-theme-color-darker-20:#2b2724;--wp-admin-theme-color-darker-20--rgb:43,39,36;--wp-admin-border-width-focus:2px}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){body.admin-color-coffee{--wp-admin-border-width-focus:1.5px}}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color--rgb:82,63,109;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-10--rgb:70,54,93;--wp-admin-theme-color-darker-20:#3a2c4d;--wp-admin-theme-color-darker-20--rgb:58,44,77;--wp-admin-border-width-focus:2px}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){body.admin-color-ectoplasm{--wp-admin-border-width-focus:1.5px}}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color--rgb:225,77,67;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-10--rgb:221,56,45;--wp-admin-theme-color-darker-20:#d02c21;--wp-admin-theme-color-darker-20--rgb:208,44,33;--wp-admin-border-width-focus:2px}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){body.admin-color-midnight{--wp-admin-border-width-focus:1.5px}}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color--rgb:98,124,131;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-10--rgb:87,110,116;--wp-admin-theme-color-darker-20:#4c6066;--wp-admin-theme-color-darker-20--rgb:76,96,102;--wp-admin-border-width-focus:2px}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){body.admin-color-ocean{--wp-admin-border-width-focus:1.5px}}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color--rgb:221,130,59;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-10--rgb:217,116,38;--wp-admin-theme-color-darker-20:#c36922;--wp-admin-theme-color-darker-20--rgb:195,105,34;--wp-admin-border-width-focus:2px}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){body.admin-color-sunrise{--wp-admin-border-width-focus:1.5px}}.woocommerce_tax_settings_conflict_error{padding:15px 10px;min-width:400px;width:400px}.woocommerce_tax_settings_conflict_error_card_body{display:flex;flex-direction:row;padding-right:16px!important}.woocommerce_tax_settings_conflict_error_card_body__warning_icon{width:20px;height:20px;margin-block:10px;margin-right:16px}.woocommerce_tax_settings_conflict_error_card_body__body_text{margin-bottom:16px}.woocommerce_tax_settings_conflict_error_card_body__close_icon{padding:0;height:24px;color:grey}.woocommerce_admin_tax_settings_slotfill_td,.woocommerce_admin_tax_settings_slotfill_th{padding:0!important}[aria-labelledby=select2-woocommerce_tax_display_shop-container].is-conflict{border:red!important;border-style:solid!important;border-width:1px!important}