Your IP : 216.73.216.162


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

Helpers.php000066600000024225151134036610006670 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;
			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 );
	}
}PluginUpgraderSilentAjax.php000066600000023674151134473650012221 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',
	];
}PluginUpgraderSkin.php000066600000003257151134473650011056 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
}NetworkCache.php000066600000005416151134473650007655 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();
	}
}Addons.php000066600000071455151134473650006516 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 '';
	}
}Features.php000066600000012002151134473650007043 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 );
	}
}ActionScheduler.php000066600000020774151134473650010360 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' ] )
			->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.
		}
	}
}Tags.php000066600000124307151134473650006177 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
		];
	}
}Templates.php000066600000005504151134473650007234 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 );
	}
}Database.php000066600000136636151134473650007015 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;
	}
}VueSettings.php000066600000023150151134473650007553 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,
			'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 );
	}
}CachePrune.php000066600000004074151134473650007314 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;
	}
}Filesystem.php000066600000014234151134473650007422 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;
	}
}Backup.php000066600000004413151134473650006501 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()
		];
	}
}Cache.php000066600000021126151134473650006277 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;
	}
}Blocks.php000066600000010366151134473650006515 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' );
	}
}Access.php000066600000017735151134473650006510 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_dashboard',
		'aioseo_general_settings',
		'aioseo_search_appearance_settings',
		'aioseo_social_networks_settings',
		'aioseo_sitemap_settings',
		'aioseo_link_assistant_settings',
		'aioseo_redirects_manage',
		'aioseo_page_redirects_manage',
		'aioseo_redirects_settings',
		'aioseo_seo_analysis_settings',
		'aioseo_search_statistics_settings',
		'aioseo_tools_settings',
		'aioseo_feature_manager_settings',
		'aioseo_page_analysis',
		'aioseo_page_general_settings',
		'aioseo_page_advanced_settings',
		'aioseo_page_schema_settings',
		'aioseo_page_social_settings',
		'aioseo_page_link_assistant_settings',
		'aioseo_page_redirects_settings',
		'aioseo_local_seo_settings',
		'aioseo_page_local_seo_settings',
		'aioseo_page_writing_assistant_settings',
		'aioseo_about_us_page',
		'aioseo_setup_wizard',
		'aioseo_page_seo_revisions_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_analysis',
					'aioseo_page_general_settings',
					'aioseo_page_advanced_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;
	}
}Assets.php000066600000004125151134473650006536 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
		}
	}
}