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/app.tar

class-orbit-fox-public.php000066600000004377151134027570011571 0ustar00<?php
/**
 * The public-specific functionality of the plugin.
 *
 * @link       https://themeisle.com
 * @since      1.0.0
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app
 */

/**
 * The public-specific functionality of the plugin.
 *
 * Defines the plugin name, version, and two examples hooks for how to
 * enqueue the public-specific stylesheet and JavaScript.
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app
 * @author     Themeisle <friends@themeisle.com>
 */
class Orbit_Fox_Public {

	/**
	 * The ID of this plugin.
	 *
	 * @since    1.0.0
	 * @access   private
	 * @var      string $plugin_name The ID of this plugin.
	 */
	private $plugin_name;

	/**
	 * The version of this plugin.
	 *
	 * @since    1.0.0
	 * @access   private
	 * @var      string $version The current version of this plugin.
	 */
	private $version;

	/**
	 * Initialize the class and set its properties.
	 *
	 * @since    1.0.0
	 * @param      string $plugin_name The name of this plugin.
	 * @param      string $version The version of this plugin.
	 */
	public function __construct( $plugin_name, $version ) {

		$this->plugin_name = $plugin_name;
		$this->version     = $version;

	}

	/**
	 * Register the stylesheets for the admin area.
	 *
	 * @since    1.0.0
	 */
	public function enqueue_styles() {

		/**
		 * This function is provided for demonstration purposes only.
		 *
		 * An instance of this class should be passed to the run() function
		 * defined in Orbit_Fox_Loader as all of the hooks are defined
		 * in that particular class.
		 *
		 * The Orbit_Fox_Loader will then create the relationship
		 * between the defined hooks and the functions defined in this
		 * class.
		 */
		do_action( 'obfx_public_enqueue_styles' );
	}

	/**
	 * Register the JavaScript for the public area.
	 *
	 * @since    1.0.0
	 */
	public function enqueue_scripts() {

		/**
		 * This function is provided for demonstration purposes only.
		 *
		 * An instance of this class should be passed to the run() function
		 * defined in Orbit_Fox_Loader as all of the hooks are defined
		 * in that particular class.
		 *
		 * The Orbit_Fox_Loader will then create the relationship
		 * between the defined hooks and the functions defined in this
		 * class.
		 */
		do_action( 'obfx_public_enqueue_scripts' );
	}
}
class-orbit-fox-module-factory.php000066600000001742151134027570013236 0ustar00<?php
/**
 * The factory logic for creating modules for plugin.
 *
 * @link       https://themeisle.com
 * @since      1.0.0
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app
 */

/**
 * The class responsible for instantiating new OBFX_Module classes.
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app
 * @author     Themeisle <friends@themeisle.com>
 */
class Orbit_Fox_Module_Factory {

	/**
	 * The build method for creating a new OBFX_Module class.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @param   string $module_name The name of the module to instantiate.
	 * @return mixed
	 * @throws Exception Thrown if no module class exists for provided $module_name.
	 */
	public static function build( $module_name ) {
		$module = str_replace( '-', '_', ucwords( $module_name ) ) . '_OBFX_Module';
		if ( class_exists( $module ) ) {
			return new $module;
		}
		// @codeCoverageIgnoreStart
		throw new Exception( 'Invalid module name given.' );
		// @codeCoverageIgnoreEnd
	}
}
class-orbit-fox-admin.php000066600000041471151134027570011377 0ustar00<?php
/**
 * The admin-specific functionality of the plugin.
 *
 * @link       https://themeisle.com
 * @since      1.0.0
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app
 */

/**
 * The admin-specific functionality of the plugin.
 *
 * Defines the plugin name, version, and two examples hooks for how to
 * enqueue the admin-specific stylesheet and JavaScript.
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app
 * @author     Themeisle <friends@themeisle.com>
 */
class Orbit_Fox_Admin {

	/**
	 * The ID of this plugin.
	 *
	 * @since    1.0.0
	 * @access   private
	 * @var      string $plugin_name The ID of this plugin.
	 */
	private $plugin_name;

	/**
	 * The version of this plugin.
	 *
	 * @since    1.0.0
	 * @access   private
	 * @var      string $version The current version of this plugin.
	 */
	private $version;

	/**
	 * Initialize the class and set its properties.
	 *
	 * @since    1.0.0
	 *
	 * @param      string $plugin_name The name of this plugin.
	 * @param      string $version The version of this plugin.
	 */
	public function __construct( $plugin_name, $version ) {

		$this->plugin_name = $plugin_name;
		$this->version     = $version;

	}

	/**
	 * Register the stylesheets for the admin area.
	 *
	 * @since    1.0.0
	 */
	public function enqueue_styles() {

		/**
		 * This function is provided for demonstration purposes only.
		 *
		 * An instance of this class should be passed to the run() function
		 * defined in Orbit_Fox_Loader as all of the hooks are defined
		 * in that particular class.
		 *
		 * The Orbit_Fox_Loader will then create the relationship
		 * between the defined hooks and the functions defined in this
		 * class.
		 */
		$screen = get_current_screen();
		if ( empty( $screen ) ) {
			return;
		}
		if ( in_array( $screen->id, array( 'toplevel_page_obfx_companion' ) ) ) {
			wp_enqueue_style( $this->plugin_name, plugin_dir_url( __FILE__ ) . '../assets/css/orbit-fox-admin.css', array(), $this->version, 'all' );
		}
		do_action( 'obfx_admin_enqueue_styles' );
	}

	/**
	 * Register the JavaScript for the admin area.
	 *
	 * @since    1.0.0
	 */
	public function enqueue_scripts() {

		/**
		 * This function is provided for demonstration purposes only.
		 *
		 * An instance of this class should be passed to the run() function
		 * defined in Orbit_Fox_Loader as all of the hooks are defined
		 * in that particular class.
		 *
		 * The Orbit_Fox_Loader will then create the relationship
		 * between the defined hooks and the functions defined in this
		 * class.
		 */

		$screen = get_current_screen();
		if ( empty( $screen ) ) {
			return;
		}
		if ( in_array( $screen->id, array( 'toplevel_page_obfx_companion' ) ) ) {
			wp_enqueue_script( $this->plugin_name, plugin_dir_url( __FILE__ ) . '../assets/js/orbit-fox-admin.js', array( 'jquery' ), $this->version, false );
		}
		do_action( 'obfx_admin_enqueue_scripts' );
	}

	/**
	 * Add admin menu items for orbit-fox.
	 *
	 * @since   1.0.0
	 * @access  public
	 */
	public function menu_pages() {
		add_menu_page(
			__( 'Orbit Fox', 'themeisle-companion' ),
			__( 'Orbit Fox', 'themeisle-companion' ),
			'manage_options',
			'obfx_companion',
			array(
				$this,
				'page_modules_render',
			),
			'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MzQuNjIiIGhlaWdodD0iMzkxLjMzIiB2aWV3Qm94PSIwIDAgNDM0LjYyIDM5MS4zMyI+PGRlZnM+PHN0eWxlPi5he2ZpbGw6I2ZmZjt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPmxvZ28tb3JiaXQtZm94LTI8L3RpdGxlPjxwYXRoIGNsYXNzPSJhIiBkPSJNNzA4LjE1LDM5NS4yM2gwYy0xLjQ5LTEzLjc2LTcuNjEtMjkuMjEtMTUuOTQtNDQuNzZsLTE0LjQ2LDguMzVhNzYsNzYsMCwxLDEtMTQ1LDQyLjd2MUg0OTEuMWE3Niw3NiwwLDEsMS0xNDQuODYtNDMuNjVsLTE0LjQyLTguMzNjLTguMTcsMTUuMjgtMTQuMjIsMzAuNDctMTUuODMsNDQtLjA2LjM3LS4xMS43NS0uMTQsMS4xMnMtLjA2LjQ2LS4wOC42OGguMDVBMTUuNTcsMTUuNTcsMCwwLDAsMzIwLjM1LDQwOEw1MDEsNTU1LjExYTE1LjU0LDE1LjU0LDAsMCwwLDExLDQuNTVoMGExNS41NCwxNS41NCwwLDAsMCwxMS00LjU1TDcwMy42OSw0MDhBMTUuNjMsMTUuNjMsMCwwLDAsNzA4LjE1LDM5NS4yM1pNNDc5LjU5LDQ0MC41MWwyMi4wNSw1LjkxLTIuMDcsNy43My0yMi4wNS01LjkxWm0zLDE4Ljc1LDIyLjA1LDUuOTEtMi4wNyw3LjczTDQ4MC41Miw0NjdabTEsMTguNzUsMjIuMDUsNS45MS0yLjA3LDcuNzMtMjIuMDUtNS45MVptMzEsNjMuMzhhMTIuMzgsMTIuMzgsMCwwLDAtMSwuOTEsMi4yMSwyLjIxLDAsMCwxLTEuNTguNjNoMGEyLjIxLDIuMjEsMCwwLDEtMS41OC0uNjMsMTIuMzgsMTIuMzgsMCwwLDAtMS0uOTFMNDg2Ljg5LDUyM2M4LjItLjUzLDE2LjYzLS44MSwyNS4xMS0uODFzMTYuOTMuMjgsMjUuMTUuODFabTUuODktNDkuNzQtMi4wNy03LjczTDU0MC40OSw0NzhsMi4wNyw3LjczWm0xLTE4Ljc1LTIuMDctNy43MywyMi4wNi01LjkxLDIuMDcsNy43M1ptMy0xOC43NS0yLjA3LTcuNzMsMjIuMDYtNS45MSwyLjA3LDcuNzNaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjk0LjY5IC0xNjguMzQpIi8+PHBhdGggY2xhc3M9ImEiIGQ9Ik03MjkuMjYsMjA5YTEyMC4xOCwxMjAuMTgsMCwwLDAtMS4xOC0xNC43OGMtLjEzLS44OC0uMjctMS43Mi0uNDItMi41Ni0uMjItMS4yLS40Ni0yLjM1LS43Mi0zLjQ3LS4xOC0uNzktLjM4LTEuNTctLjU4LTIuMzItLjM2LTEuMjgtLjc0LTIuNDgtMS4xNi0zLjYyLS4zNi0uOTUtLjc0LTEuODUtMS4xNS0yLjctLjItLjQzLS40MS0uODQtLjYzLTEuMjRzLS40My0uNzktLjY1LTEuMTVhMTkuNzYsMTkuNzYsMCwwLDAtMS4xNS0xLjY4LDE0LjE5LDE0LjE5LDAsMCwwLTEuMTUtMS4zNiwxMS44NywxMS44NywwLDAsMC0xLS45MWMtLjExLS4xLS4yNS0uMTgtLjM3LS4yN2ExNS4yMSwxNS4yMSwwLDAsMC0yLjU0LTEuNTlsLTEuMDYtLjQ5YTI1LjU3LDI1LjU3LDAsMCwwLTMuODUtMS4yNWMtLjc0LS4xOC0xLjUyLS4zNS0yLjMzLS40OS0xLjExLS4xOS0yLjI4LS4zNS0zLjUtLjQ3cy0yLjY5LS4yMS00LjExLS4yNWMtMi4xNC0uMDctNC4zOSwwLTYuNzMuMDktMS41Ny4wOC0zLjE4LjItNC44Mi4zNmwtMi44MS4zYTE3MSwxNzEsMCwwLDAtMTgsMy4xN2wtMy4xMi43NHEtNC44NywxLjItOS43OSwyLjY0Yy0zLjI3LDEtNi41NCwyLTkuNzcsMy4xMXEtNS4yNCwxLjc4LTEwLjMsNGMtLjg1LjM3LTEuNjkuNzUtMi41MywxLjE0cS0zLjc4LDEuNzYtNy40OCwzLjc4YTE0Mi4zNywxNDIuMzcsMCwwLDAtMTIuOCw3Ljg4Yy0xLjQsMS0yLjgxLDItNC4yLDNhMjAxLjUzLDIwMS41MywwLDAsMC0yMy43LDIwLjc3Yy0yMC4zNy0xNC00Mi4zLTIwLTczLjctMjAuNDZ2MS43N2gwdi0xLjc3Yy0zMS40MS41LTUzLjM1LDYuNDQtNzMuNzIsMjAuNDctMTkuODQtMjAuMS0zOS4yNi0zMy4xNi02MS00MC42LTI5LjU2LTEwLjExLTYyLTE0LjU5LTcyLjc2LTUuNnMtMTEuOTUsNDEuNzYtNy4xMyw3Mi42M2M0LjU1LDI5LjEsMTguODMsNTYsNDQuNzgsODdsMCwuMDYsMTQuNDgsOC4zNkE3Niw3NiwwLDAsMSw0OTIuMjIsMzgyaDM5LjU2QTc2LDc2LDAsMCwxLDY2Ny40LDM0MS4xOWwxNC41Mi04LjM5LDAtLjA3cTMuNTctNC4yNiw2Ljg0LTguNDNjMS41LTEuODksMi45NC0zLjc3LDQuMzQtNS42NHMyLjc2LTMuNzMsNC4wNy01LjU3Yy42Ni0uOTIsMS4zLTEuODQsMS45NC0yLjc2cTEuOS0yLjc2LDMuNjctNS40OCwyLjY3LTQuMSw1LTguMTN0NC40NS04LjA1Yy45Mi0xLjc4LDEuODEtMy41NiwyLjY1LTUuMzRxMS44OS00LDMuNTEtOGMuNzItMS43OCwxLjM5LTMuNTUsMi01LjMzLjMyLS44OC42My0xLjc3LjkzLTIuNjYuNi0xLjc4LDEuMTUtMy41NiwxLjY3LTUuMzRhMTMxLjU0LDEzMS41NCwwLDAsMCwzLjYxLTE2LjIxLDIyMS4yNCwyMjEuMjQsMCwwLDAsMi42OC0zMS40NkM3MjkuMzIsMjEyLjUyLDcyOS4zMSwyMTAuNzMsNzI5LjI2LDIwOVpNMzg5LjMxLDI2OC43OWMtOS4yOSwxMS41OC0yMi4zNywyNy43Ni0zNC45NCw0NS42Ni0xMS42NC0xNi45Mi0yNC43Ni0zOC42MS0yNy40OS01Ny42NS0zLjEzLTIxLjg2LTEuOTQtMzcuNTktLjA3LTQzLjQ4YTMyLjY1LDMyLjY1LDAsMCwxLDQuMjktLjI1YzkuODYsMCwyNC4yOCwyLjkyLDM4LjU5LDcuODEsMTMuNTMsNC42MywyNi4xNSwxMi41NiwzOS4yNiwyNC44NUM0MDIuNjgsMjUyLjU0LDM5Ni4yMSwyNjAuMTksMzg5LjMxLDI2OC43OVptMzA3LjgxLTEyYy0yLjczLDE5LTE1LjgzLDQwLjctMjcuNDYsNTcuNjEtMTIuNTctMTcuODgtMjUuNjQtMzQuMDUtMzQuOTMtNDUuNjItNi45MS04LjYxLTEzLjM4LTE2LjI2LTE5LjY2LTIzLjA4LDEzLjExLTEyLjI4LDI1LjcyLTIwLjIsMzkuMjQtMjQuODMsMTQuMzEtNC44OSwyOC43My03LjgxLDM4LjU5LTcuODFhMzIuNjUsMzIuNjUsMCwwLDEsNC4yOS4yNUM2OTkuMDYsMjE5LjIxLDcwMC4yNSwyMzQuOTQsNjk3LjEyLDI1Ni44WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTI5NC42OSAtMTY4LjM0KSIvPjxwYXRoIGNsYXNzPSJhIiBkPSJNNDE2LjQ0LDMzMS41N0E1Ni41MSw1Ni41MSwwLDEsMCw0NzMsMzg4LjA4LDU2LjU3LDU2LjU3LDAsMCwwLDQxNi40NCwzMzEuNTdabTMxLjYyLDg2LjM2YTIzLjQ0LDIzLjQ0LDAsMSwxLDUtNy4zOCw5LjI1LDkuMjUsMCwwLDEtMS43OSwzLjM5QTIyLjcxLDIyLjcxLDAsMCwxLDQ0OC4wNiw0MTcuOTNaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjk0LjY5IC0xNjguMzQpIi8+PHBhdGggY2xhc3M9ImEiIGQ9Ik02MDcuNTYsMzMxLjU3YTU2LjUxLDU2LjUxLDAsMSwwLDU2LjUxLDU2LjUxQTU2LjU3LDU2LjU3LDAsMCwwLDYwNy41NiwzMzEuNTdabTEuNTMsODYuMzZhMjMuNDIsMjMuNDIsMCwwLDEtMzMuMTMsMCwyMy4xOCwyMy4xOCwwLDAsMS0zLjE5LTQsOS4wOCw5LjA4LDAsMCwxLTEuNzgtMy4zOSwyMy40MiwyMy40MiwwLDEsMSwzOC4xLDcuMzhaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjk0LjY5IC0xNjguMzQpIi8+PC9zdmc+',
			'75'
		);
		add_submenu_page( 'obfx_companion', __( 'Orbit Fox General Options', 'themeisle-companion' ), __( 'General Settings', 'themeisle-companion' ), 'manage_options', 'obfx_companion' );
	}

	/**
	 * Add the initial dashboard notice to guide the user to the OrbitFox admin page.
	 *
	 * @since   2.3.4
	 * @access  public
	 */
	public function visit_dashboard_notice() {
		global $current_user;
		$user_id = $current_user->ID;
		if ( ! get_user_meta( $user_id, 'obfx_ignore_visit_dashboard_notice' ) ) { ?>
			<div class="notice notice-info" style="position:relative;">
				<p><?php echo sprintf( __( 'You have activated Orbit Fox plugin! Go to the %s to get started with the extra features.', 'themeisle-companion' ), sprintf( '<a href="%s">%s</a>', admin_url( 'admin.php?page=obfx_companion&obfx_ignore_visit_dashboard_notice=0' ), __( 'Dashboard Page', 'themeisle-companion' ) ) ); ?></p>
				<a href="?obfx_ignore_visit_dashboard_notice=0" class="notice-dismiss" style="text-decoration: none;">
					<span class="screen-reader-text">Dismiss this notice.</span>
				</a>
			</div>
			<?php
		}
	}

	/**
	 * Dismiss the initial dashboard notice.
	 *
	 * @since   2.3.4
	 * @access  public
	 */
	function visit_dashboard_notice_dismiss() {
		global $current_user;
		$user_id = $current_user->ID;
		if ( isset( $_GET['obfx_ignore_visit_dashboard_notice'] ) && '0' == $_GET['obfx_ignore_visit_dashboard_notice'] ) {
			add_user_meta( $user_id, 'obfx_ignore_visit_dashboard_notice', 'true', true );
		}
	}

	/**
	 * Calls the orbit_fox_modules hook.
	 *
	 * @since   1.0.0
	 * @access  public
	 */
	public function load_modules() {
		do_action( 'orbit_fox_modules' );
	}

	/**
	 * This method is called via AJAX and processes the
	 * request for updating module options.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 */
	public function obfx_update_module_options() {
		$json                = stripslashes( str_replace( '&quot;', '"', $_POST['data'] ) );
		$data                = json_decode( $json, true );
		$response['type']    = 'error';
		$response['message'] = __( 'Could not process the request!', 'themeisle-companion' );
		if ( isset( $data['noance'] ) && wp_verify_nonce( $data['noance'], 'obfx_update_module_options_' . $data['module-slug'] ) ) {
			$response = $this->try_module_save( $data );
		}
		echo json_encode( $response );
		wp_die();
	}

	/**
	 * A method used for saving module options data
	 * and returning a well formatted response as an array.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 *
	 * @param   array $data The options data to try and save via the module model.
	 *
	 * @return array
	 */
	public function try_module_save( $data ) {
		$response            = array();
		$global_settings     = new Orbit_Fox_Global_Settings();
		$modules             = $global_settings::$instance->module_objects;
		$response['type']    = 'error';
		$response['message'] = __( 'No module found! No data was updated.', 'themeisle-companion' );
		if ( isset( $modules[ $data['module-slug'] ] ) ) {
			$module = $modules[ $data['module-slug'] ];
			unset( $data['noance'] );
			unset( $data['module-slug'] );
			$response['type']    = 'warning';
			$response['message'] = __( 'Something went wrong, data might not be saved!', 'themeisle-companion' );
			$result              = $module->set_options( $data );
			if ( $result ) {
				$response['type']    = 'success';
				$response['message'] = __( 'Options updated, successfully!', 'themeisle-companion' );
			}
		}

		return $response;
	}

	/**
	 * This method is called via AJAX and processes the
	 * request for updating module options.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 */
	public function obfx_update_module_active_status() {
		$json                = stripslashes( str_replace( '&quot;', '"', $_POST['data'] ) );
		$data                = json_decode( $json, true );
		$response['type']    = 'error';
		$response['message'] = __( 'Could not process the request!', 'themeisle-companion' );
		if ( isset( $data['noance'] ) && wp_verify_nonce( $data['noance'], 'obfx_activate_mod_' . $data['name'] ) ) {
			$response = $this->try_module_activate( $data );
		}
		echo json_encode( $response );
		wp_die();
	}

	/**
	 * A method used for saving module status data
	 * and returning a well formatted response as an array.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 *
	 * @param   array $data The data to try and update status via the module model.
	 *
	 * @return array
	 */
	public function try_module_activate( $data ) {
		$response            = array();
		$global_settings     = new Orbit_Fox_Global_Settings();
		$modules             = $global_settings::$instance->module_objects;
		$response['type']    = 'error';
		$response['message'] = __( 'No module found!', 'themeisle-companion' );
		if ( isset( $modules[ $data['name'] ] ) ) {
			$module              = $modules[ $data['name'] ];
			$response['type']    = 'warning';
			$response['message'] = __( 'Something went wrong, can not change module status!', 'themeisle-companion' );
			$result              = $module->set_status( 'active', $data['checked'] );
			$this->trigger_activate_deactivate( $data['checked'], $module );
			if ( $result ) {
				$response['type']    = 'success';
				$response['message'] = __( 'Module status changed!', 'themeisle-companion' );
			}
		}

		return $response;
	}

	/**
	 * A method to trigger module activation or deavtivation hook
	 * based in active status.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   2.3.3
	 * @access  public
	 * @param   boolean                   $active_status The active status.
	 * @param   Orbit_Fox_Module_Abstract $module The module referenced.
	 */
	public function trigger_activate_deactivate( $active_status, Orbit_Fox_Module_Abstract $module ) {
		if ( $active_status == true ) {
			do_action( $module->get_slug() . '_activate' );
		} else {
			do_action( $module->get_slug() . '_deactivate' );
		}
	}

	/**
	 * Method to display modules page.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 */
	public function page_modules_render() {
		$global_settings = new Orbit_Fox_Global_Settings();

		$modules = $global_settings::$instance->module_objects;

		$rdh           = new Orbit_Fox_Render_Helper();
		$tiles         = '';
		$panels        = '';
		$toasts        = '';
		$count_modules = 0;
		foreach ( $modules as $slug => $module ) {
			if ( $module->enable_module() ) {
				$notices        = $module->get_notices();
				$showed_notices = $module->get_status( 'showed_notices' );
				if ( ! is_array( $showed_notices ) ) {
					$showed_notices = array();
				}
				if ( isset( $showed_notices ) && is_array( $showed_notices ) ) {
					foreach ( $notices as $notice ) {
						$hash = md5( serialize( $notice ) );
						$data = array(
							'notice' => $notice,
						);
						if ( $notice['display_always'] == false && ! in_array( $hash, $showed_notices ) ) {
							$toasts .= $rdh->get_partial( 'module-toast', $data );
						} elseif ( $notice['display_always'] == true ) {
							$toasts .= $rdh->get_partial( 'module-toast', $data );
						}
					}
				}

				$module->update_showed_notices();
				if ( $module->auto == false ) {
					$count_modules ++;
					$checked = '';
					if ( $module->get_is_active() ) {
						$checked = 'checked';
					}

					$data  = array(
						'slug'           => $slug,
						'name'           => $module->name,
						'description'    => $module->description,
						'checked'        => $checked,
						'beta'           => $module->beta,
						'confirm_intent' => $module->confirm_intent,
					);
					$tiles .= $rdh->get_partial( 'module-tile', $data );
					$tiles .= '<div class="divider"></div>';
				}

				$module_options = $module->get_options();
				$options_fields = '';
				if ( ! empty( $module_options ) ) {
					foreach ( $module_options as $option ) {
						$options_fields .= $rdh->render_option( $option, $module );
					}

					$panels .= $rdh->get_partial(
						'module-panel',
						array(
							'slug'           => $slug,
							'name'           => $module->name,
							'active'         => $module->get_is_active(),
							'description'    => $module->description,
							'show'           => $module->show,
							'no_save'        => $module->no_save,
							'options_fields' => $options_fields,
						)
					);
				}
			}// End if().
		}// End foreach().

		$no_modules = false;
		$empty_tpl  = '';
		if ( $count_modules == 0 ) {
			$no_modules = true;
			$empty_tpl  = $rdh->get_partial(
				'empty',
				array(
					'title'     => __( 'No modules found.', 'themeisle-companion' ),
					'sub_title' => __( 'Please contact support for more help.', 'themeisle-companion' ),
					'show_btn'  => true,
				)
			);
			$panels     = $rdh->get_partial(
				'empty',
				array(
					'title'     => __( 'No active modules.', 'themeisle-companion' ),
					'sub_title' => __( 'Activate a module using the toggles above.', 'themeisle-companion' ),
					'show_btn'  => false,
				)
			);
		}

		$data   = array(
			'no_modules'    => $no_modules,
			'empty_tpl'     => $empty_tpl,
			'count_modules' => $count_modules,
			'tiles'         => $tiles,
			'toasts'        => $toasts,
			'panels'        => $panels,
		);
		$output = $rdh->get_view( 'modules', $data );
		echo $output;
	}

}
abstract/class-orbit-fox-module-abstract.php000066600000041724151134027570015201 0ustar00<?php
/**
 * The abstract class for Orbit Fox Modules.
 *
 * @link       https://themeisle.com
 * @since      1.0.0
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app/abstract
 */

/**
 * The class that defines the required methods and variables needed by a OBFX_Module.
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app/abstract
 * @author     Themeisle <friends@themeisle.com>
 */
abstract class Orbit_Fox_Module_Abstract {

	/**
	 * Holds the name of the module
	 *
	 * @since   1.0.0
	 * @access  public
	 * @var     string $name The name of the module.
	 */
	public $name;
	/**
	 * Holds the description of the module
	 *
	 * @since   1.0.0
	 * @access  public
	 * @var     string $description The description of the module.
	 */
	public $description;
	/**
	 * Confirm intent array. It should contain a title and a subtitle for the confirm intent modal.
	 *
	 * @since   2.4.1
	 * @access  public
	 * @var     array $confirm_intent Stores an array of the modal with 'title' and 'subtitle' keys.
	 */
	public $confirm_intent = array();
	/**
	 * Flags if module should autoload.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @var     bool $auto The flag for automatic activation.
	 */
	public $auto = false;
	/**
	 * Flags module should have the section open.
	 *
	 * @since   2.5.0
	 * @access  public
	 * @var     bool $show The flag for section opened.
	 */
	public $show = false;
	/**
	 * Holds the module slug.
	 *
	 * @since   1.0.0
	 * @access  protected
	 * @var     string $slug The module slug.
	 */
	protected $slug;
	/**
	 * Holds the default setting activation state of the module.
	 *
	 * @since   2.1.0
	 * @access  protected
	 * @var     boolean $active_default The default active state of the module.
	 */
	protected $active_default = false;
	/**
	 * Stores an array of notices
	 *
	 * @since   1.0.0
	 * @access  public
	 * @var     array $notices Stores an array of notices to be displayed on the admin panel.
	 */
	protected $notices = array();
	/**
	 * Has an instance of the Orbit_Fox_Loader class used for adding actions and filters.
	 *
	 * @since   1.0.0
	 * @access  protected
	 * @var     Orbit_Fox_Loader $loader A instance of Orbit_Fox_Loader.
	 */
	protected $loader;

	/**
	 * Has an instance of the Orbit_Fox_Model class used for interacting with DB data.
	 *
	 * @since   1.0.0
	 * @access  protected
	 * @var     Orbit_Fox_Model $model A instance of Orbit_Fox_Model.
	 */
	protected $model;

	/**
	 * Stores the curent version of Orbit fox for use during the enqueue.
	 *
	 * @since   1.0.0
	 * @access  protected
	 * @var     string $version The current version of Orbit Fox.
	 */
	protected $version;

	/**
	 * Enable module in beta mode..
	 *
	 * @since   1.0.0
	 * @access  protected
	 * @var     boolean $beta Is module in beta.
	 */
	public $beta;

	/**
	 * Module needs save buttons.
	 *
	 * @since   1.0.0
	 * @access  protected
	 * @var     boolean $no_save Should we show the save buttons.
	 */
	public $no_save = false;

	/**
	 * Stores the localized arrays for both public and admin JS files that need to be loaded.
	 *
	 * @access  protected
	 * @var     array $localized The localized arrays for both public and admin JS files that need to be loaded.
	 */
	protected $localized = array();

	/**
	 * Orbit_Fox_Module_Abstract constructor.
	 *
	 * @since   1.0.0
	 * @access  public
	 */
	public function __construct() {
		$this->slug = str_replace( '_', '-', strtolower( str_replace( '_OBFX_Module', '', get_class( $this ) ) ) );
	}

	/**
	 * Registers the loader.
	 * And setup activate and deactivate hooks. Added in v2.3.3.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @updated 2.3.3
	 * @access  public
	 *
	 * @param Orbit_Fox_Loader $loader The loader class used to register action hooks and filters.
	 */
	public function register_loader( Orbit_Fox_Loader $loader ) {
		$this->loader = $loader;
		$this->loader->add_action( $this->get_slug() . '_activate', $this, 'activate' );
		$this->loader->add_action( $this->get_slug() . '_deactivate', $this, 'deactivate' );
	}

	/**
	 * Getter method for slug.
	 *
	 * @since   2.3.3
	 * @access  public
	 * @return mixed|string
	 */
	public function get_slug() {
		return $this->slug;
	}

	/**
	 * Registers the loader.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 *
	 * @param Orbit_Fox_Model $model The loader class used to register action hooks and filters.
	 */
	public function register_model( Orbit_Fox_Model $model ) {
		$this->model = $model;
	}

	/**
	 * Method to return the notices array
	 *
	 * @since   1.0.0
	 * @access  public
	 * @return array
	 */
	public function get_notices() {
		return $this->notices;
	}

	/**
	 * Utility method to updated showed notices array.
	 *
	 * @since   1.0.0
	 * @access  public
	 */
	public function update_showed_notices() {
		$showed_notices = $this->get_status( 'showed_notices' );
		if ( $showed_notices == false ) {
			$showed_notices = array();
		}
		foreach ( $this->notices as $notice ) {
			if ( $notice['display_always'] == false ) {
				$hash = md5( serialize( $notice ) );
				if ( ! in_array( $hash, $showed_notices ) ) {
					$showed_notices[] = $hash;
				}
			}
		}
		$this->set_status( 'showed_notices', $showed_notices );
	}

	/**
	 * Method to retrieve from model the module status for
	 * the provided key.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 *
	 * @param   string $key Key to look for.
	 *
	 * @return bool
	 */
	final public function get_status( $key ) {
		return $this->model->get_module_status( $this->slug, $key );
	}

	/**
	 * Method to update in model the module status for
	 * the provided key value pair.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 *
	 * @param   string $key Key to update.
	 * @param   string $value The new value.
	 *
	 * @return mixed
	 */
	final public function set_status( $key, $value ) {
		return $this->model->set_module_status( $this->slug, $key, $value );
	}

	/**
	 * Method to determine if the module is enabled or not.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @return bool
	 */
	public abstract function enable_module();

	/**
	 * The method for the module load logic.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @return mixed
	 */
	public abstract function load();

	/**
	 * Method to define actions and filters needed for the module.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 */
	public abstract function hooks();

	/**
	 * Method to check if module status is active.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 * @return bool
	 */
	final public function get_is_active() {
		if ( $this->auto == true ) {
			return true;
		}
		if ( ! isset( $this->model ) ) {
			return false;
		}
		return $this->model->get_is_module_active( $this->slug, $this->active_default );
	}

	/**
	 * Method to update an option key value pair.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 *
	 * @param   string $key The key name.
	 * @param   string $value The new value.
	 *
	 * @return mixed
	 */
	final public function set_option( $key, $value ) {
		if ( ! isset( $this->model ) ) {
			return false;
		}
		return $this->model->set_module_option( $this->slug, $key, $value );
	}

	/**
	 * Stub for activate hook.
	 *
	 * @since   2.3.3
	 * @access  public
	 */
	public function activate() {
	}

	/**
	 * Stub for deactivate hook.
	 *
	 * @since   2.3.3
	 * @access  public
	 */
	public function deactivate() {
	}

	/**
	 * Method to update a set of options.
	 * Added in v2.3.3 actions for before and after options save.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @updated 2.3.3
	 * @access  public
	 *
	 * @param   array $options An associative array of options to be
	 *                         updated. Eg. ( 'key' => 'new_value' ).
	 *
	 * @return mixed
	 */
	final public function set_options( $options ) {
		do_action( $this->get_slug() . '_before_options_save', $options );
		$result = $this->model->set_module_options( $this->slug, $options );
		do_action( $this->get_slug() . '_after_options_save' );

		return $result;
	}

	/**
	 * Method to retrieve the options for the module.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 * @return array
	 */
	final public function get_options() {
		$model_options = $this->options();
		$options       = array();
		$index         = 0;
		foreach ( $model_options as $opt ) {
			$options[ $index ]          = $opt;
			$options[ $index ]['value'] = $this->get_option( $opt['name'] );
			$index ++;
		}

		return $options;
	}

	/**
	 * Method to define the options fields for the module
	 *
	 * @since   1.0.0
	 * @access  public
	 * @return array
	 */
	public abstract function options();

	/**
	 * Method to retrieve an option value from model.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 *
	 * @param   string $key The option key to retrieve.
	 *
	 * @return bool
	 */
	final public function get_option( $key ) {
		$default_options = $this->get_options_defaults();
		$db_option       = $this->model->get_module_option( $this->slug, $key );
		$value           = $db_option;
		if ( $db_option === false ) {
			$value = isset( $default_options[ $key ] ) ? $default_options[ $key ] : '';
		}

		return $value;
	}

	/**
	 * Method to define the default model value for options, based on
	 * the options array if not set DB.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 * @return array
	 */
	final public function get_options_defaults() {
		$options  = $this->options();
		$defaults = array();
		foreach ( $options as $opt ) {
			if ( ! isset( $opt['default'] ) ) {
				$opt['default'] = '';
			}
			$defaults[ $opt['name'] ] = $opt['default'];
		}

		return $defaults;
	}

	/**
	 * Adds the hooks for amdin and public enqueue.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 *
	 * @param   string $version The version for the files.
	 */
	final public function set_enqueue( $version ) {
		$this->version = $version;
		$this->loader->add_action( 'obfx_admin_enqueue_styles', $this, 'set_admin_styles' );
		$this->loader->add_action( 'obfx_admin_enqueue_scripts', $this, 'set_admin_scripts' );

		$this->loader->add_action( 'obfx_public_enqueue_styles', $this, 'set_public_styles' );
		$this->loader->add_action( 'obfx_public_enqueue_scripts', $this, 'set_public_scripts' );
	}

	/**
	 * Sets the styles for admin from the module array.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 */
	public function set_admin_styles() {
		$this->set_styles( $this->admin_enqueue(), 'adm' );
	}

	/**
	 * Actually sets the styles.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  private
	 *
	 * @param   array  $enqueue The array of files to enqueue.
	 * @param   string $prefix The string to prefix in the enqueued name.
	 */
	private function set_styles( $enqueue, $prefix ) {
		$module_dir = $this->slug;
		if ( ! empty( $enqueue ) ) {
			if ( isset( $enqueue['css'] ) && ! empty( $enqueue['css'] ) ) {
				$order = 0;
				$map   = array();
				foreach ( $enqueue['css'] as $file_name => $dependencies ) {
					if ( $dependencies == false ) {
						$dependencies = array();
					} else {
						// check if any dependency has been loaded by us. If yes, then use that id as the dependency.
						foreach ( $dependencies as $index => $dep ) {
							if ( array_key_exists( $dep, $map ) ) {
								unset( $dependencies[ $index ] );
								$dependencies[ $index ] = $map[ $dep ];
							}
						}
					}
					$url      = filter_var( $file_name, FILTER_SANITIZE_URL );
					$resource = plugin_dir_url( $this->get_dir() ) . $module_dir . '/css/' . $file_name . '.css';
					if ( ! filter_var( $url, FILTER_VALIDATE_URL ) === false ) {
						$resource = $url;
					}
					$id                = 'obfx-module-' . $prefix . '-css-' . str_replace( ' ', '-', strtolower( $this->name ) ) . '-' . $order;
					$map[ $file_name ] = $id;
					wp_enqueue_style(
						$id,
						$resource,
						$dependencies,
						$this->version,
						'all'
					);
					$order ++;
				}
			}
		}
	}

	/**
	 * Method that returns an array of scripts and styles to be loaded
	 * for the admin part.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @return array
	 */
	public abstract function admin_enqueue();

	/**
	 * Sets the scripts for admin from the module array.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 */
	public function set_admin_scripts() {
		$this->set_scripts( $this->admin_enqueue(), 'adm' );
	}

	/**
	 * Actually sets the scripts.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  private
	 *
	 * @param   array  $enqueue The array of files to enqueue.
	 * @param   string $prefix The string to prefix in the enqueued name.
	 */
	private function set_scripts( $enqueue, $prefix ) {
		$sanitized = str_replace( ' ', '-', strtolower( $this->name ) );

		$module_dir = $this->slug;

		if ( ! empty( $enqueue ) ) {
			if ( isset( $enqueue['js'] ) && ! empty( $enqueue['js'] ) ) {
				$order = 0;
				$map   = array();
				foreach ( $enqueue['js'] as $file_name => $dependencies ) {
					if ( $dependencies == false ) {
						$dependencies = array();
					} else {
						// check if any dependency has been loaded by us. If yes, then use that id as the dependency.
						foreach ( $dependencies as $index => $dep ) {
							if ( array_key_exists( $dep, $map ) ) {
								unset( $dependencies[ $index ] );
								$dependencies[ $index ] = $map[ $dep ];
							}
						}
					}
					$url      = filter_var( $file_name, FILTER_SANITIZE_URL );
					$resource = plugin_dir_url( $this->get_dir() ) . $module_dir . '/js/' . $file_name . '.js';
					if ( ! filter_var( $url, FILTER_VALIDATE_URL ) === false ) {
						$resource = $url;
					}
					$id                = 'obfx-module-' . $prefix . '-js-' . $sanitized . '-' . $order;
					$map[ $file_name ] = $id;

					wp_enqueue_script(
						$id,
						$resource,
						$dependencies,
						$this->version,
						false
					);

					// check if we need to enqueue or localize.
					if ( array_key_exists( $file_name, $this->localized ) ) {
						wp_localize_script(
							$id,
							str_replace( '-', '_', $sanitized ),
							$this->localized[ $file_name ]
						);
					}
					$order ++;
				}// End foreach().
			}// End if().
		}// End if().
	}

	/**
	 * Sets the styles for public from the module array.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 */
	public function set_public_styles() {
		$this->set_styles( $this->public_enqueue(), 'pub' );
	}

	/**
	 * Method that returns an array of scripts and styles to be loaded
	 * for the front end part.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @return array
	 */
	public abstract function public_enqueue();

	/**
	 * Sets the scripts for public from the module array.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  public
	 */
	public function set_public_scripts() {
		$this->set_scripts( $this->public_enqueue(), 'pub' );
	}

	/**
	 * Method to return URL to child class in a Reflective Way.
	 *
	 * @codeCoverageIgnore
	 *
	 * @access  protected
	 * @return string
	 */
	protected function get_url() {
		return plugin_dir_url( $this->get_dir() ) . $this->slug;
	}

	/**
	 * Method to return path to child class in a Reflective Way.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  protected
	 * @return string
	 */
	protected function get_dir() {
		$reflector = new ReflectionClass( get_class( $this ) );

		return dirname( $reflector->getFileName() );
	}

	/**
	 * Utility method to return active theme dir name.
	 *
	 * @since   1.0.0
	 * @access  protected
	 *
	 * @param   boolean $is_child Flag for child themes.
	 *
	 * @return string
	 */
	protected function get_active_theme_dir( $is_child = false ) {
		if ( $is_child ) {
			return basename( get_stylesheet_directory() );
		}

		return basename( get_template_directory() );
	}

	/**
	 * Utility method to render a view from module.
	 *
	 * @codeCoverageIgnore
	 *
	 * @since   1.0.0
	 * @access  protected
	 *
	 * @param   string $view_name The view name w/o the `-tpl.php` part.
	 * @param   array  $args An array of arguments to be passed to the view.
	 *
	 * @return string
	 */
	protected function render_view( $view_name, $args = array() ) {
		ob_start();
		$file = $this->get_dir() . '/views/' . $view_name . '-tpl.php';
		if ( ! empty( $args ) ) {
			foreach ( $args as $obfx_rh_name => $obfx_rh_value ) {
				$$obfx_rh_name = $obfx_rh_value;
			}
		}
		if ( file_exists( $file ) ) {
			include $file;
		}

		return ob_get_clean();
	}
	/**
	 * Check if the users is choosen to show this in beta.
	 *
	 * @param int $percent Amount of users to show.
	 *
	 * @return bool Random result.
	 */
	protected function is_lucky_user( $percent = 10 ) {
		$force_beta = isset( $_GET['force_beta'] ) && $_GET['force_beta'] === 'yes';
		if ( $force_beta ) {
			update_option( 'obfx_beta_show_' . $this->get_slug(), 'yes' );

			return true;
		}
		$luck = get_option( 'obfx_beta_show_' . $this->get_slug() );
		if ( ! empty( $luck ) ) {
			return $luck === 'yes';
		}
		$luck = rand( 1, 100 );

		$luck = $luck <= $percent;
		update_option( 'obfx_beta_show_' . $this->get_slug(), $luck ? 'yes' : 'no' );

		return $luck;
	}
}
abstract/.htaccess000066600000000424151134027570010156 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>.htaccess000066600000000424151134027570006353 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>models/class-orbit-fox-model.php000066600000013352151134027570012667 0ustar00<?php
/**
 * The core model class for Orbit Fox.
 *
 * @link       https://themeisle.com
 * @since      1.0.0
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app/models
 */

/**
 * The class that defines a model for interacting with data.
 * Provides utility methods for saving and retrieving data.
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app/models
 * @author     Themeisle <friends@themeisle.com>
 */
class Orbit_Fox_Model {

	/**
	 * The model namespace.
	 *
	 * @since   1.0.0
	 * @access  private
	 * @var     string $namespace The model namespace.
	 */
	private $namespace = 'obfx_data';

	/**
	 * Holds the core settings.
	 *
	 * @since   1.0.0
	 * @access  private
	 * @var     array $core_settings Stores the core settings.
	 */
	private $core_settings;

	/**
	 * Holds all enabled modules statuses.
	 *
	 * @since   1.0.0
	 * @access  private
	 * @var     array $module_status Stores the modules statuses.
	 */
	private $module_status;

	/**
	 * Holds all enabled modules options.
	 *
	 * @since   1.0.0
	 * @access  private
	 * @var     array $module_settings Stores the modules options.
	 */
	private $module_settings;

	/**
	 * Orbit_Fox_Model constructor.
	 *
	 * @since   1.0.0
	 * @access  public
	 */
	public function __construct() {
		$this->core_settings = array();
	}

	/**
	 * Defines the modules data.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @param   array $modules The modules array passed by Orbit_Fox.
	 */
	public function register_modules_data( $modules = array() ) {
		$module_status   = array();
		$module_settings = array();
		if ( ! empty( $modules ) ) {
			foreach ( $modules as $slug => $module ) {
				$is_enabled     = $module->enable_module();
				$is_auto        = $module->auto;
				$active         = false;
				$showed_notices = array();

				$module_status[ $slug ] = array(
					'enabled'        => $is_enabled,
					'autoload'       => $is_auto,
					'showed_notices' => $showed_notices,
					'active'         => $active,
				);

				$module_settings[ $slug ] = $module->get_options_defaults();
			}
		}

		$this->module_status   = $module_status;
		$this->module_settings = $module_settings;

	}

	/**
	 * Defines a default data array.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @return array
	 */
	public function default_data() {
		$data = array(
			'core_settings'   => $this->core_settings,
			'module_status'   => $this->module_status,
			'module_settings' => $this->module_settings,
		);

		return $data;
	}

	/**
	 * Utility method to return the active status of a module.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @param   string  $slug The module slug.
	 * @param   boolean $default The default active state.
	 * @return bool
	 */
	public function get_is_module_active( $slug, $default ) {
		$data = $this->get();
		if ( isset( $data['module_status'][ $slug ]['active'] ) ) {
			return $data['module_status'][ $slug ]['active'];
		}
		return $default; // @codeCoverageIgnore
	}

	/**
	 * Utility method to retrieve a module option.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @param   string $slug The module slug.
	 * @param   string $key Key to lookup.
	 * @return bool
	 */
	public function get_module_option( $slug, $key ) {
		$data = $this->get();
		if ( isset( $data['module_settings'][ $slug ][ $key ] ) ) {
			return $data['module_settings'][ $slug ][ $key ];
		}
		return false; // @codeCoverageIgnore
	}

	/**
	 * Utility method to set a module option.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @param   string $slug The module slug.
	 * @param   string $key Key to lookup.
	 * @param   mixed  $value The new value.
	 * @return mixed
	 */
	public function set_module_option( $slug, $key, $value ) {
		$new                                     = array();
		$new['module_settings'][ $slug ][ $key ] = $value;
		return $this->save( $new );
	}

	/**
	 * Utility method to set batch module options.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @param   string $slug The module slug.
	 * @param   array  $options The associative options array.
	 * @return mixed
	 */
	public function set_module_options( $slug, $options = array() ) {
		$new['module_settings'][ $slug ] = $options;
		return $this->save( $new );
	}

	/**
	 * Utility method to get a module status value.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @param   string $slug The module slug.
	 * @param   string $key Key to lookup.
	 * @return bool
	 */
	public function get_module_status( $slug, $key ) {
		$data = $this->get();
		if ( isset( $data['module_status'][ $slug ][ $key ] ) ) {
			return $data['module_status'][ $slug ][ $key ];
		}
		return false; // @codeCoverageIgnore
	}

	/**
	 * Utility method to set a module status.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @param   string $slug The module slug.
	 * @param   string $key Key to lookup.
	 * @param   mixed  $value The new value.
	 * @return mixed
	 */
	public function set_module_status( $slug, $key, $value ) {
		$new                                   = array();
		$new['module_status'][ $slug ][ $key ] = $value;
		return $this->save( $new );
	}

	/**
	 * Base model method to save data to DB.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @param   array $new The new data array to be saved.
	 * @return mixed
	 */
	public function save( $new = array() ) {
		$old_data = $this->get();
		$new_data = array_replace_recursive( $old_data, $new );
		return update_option( $this->namespace, $new_data );
	}

	/**
	 * Base model method to retrieve data from DB.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @return mixed
	 */
	public function get() {
		return get_option( $this->namespace, $this->default_data() );
	}

	/**
	 * Method used for resetting model and clearing the DB.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @return mixed
	 */
	public function destroy_model() {
		return delete_option( $this->namespace );
	}
}
models/.htaccess000066600000000424151134027570007636 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>index.php000066600000000033151134027570006371 0ustar00<?php // Silence is golden
class-orbit-fox-global-settings.php000066600000005020151134027570013373 0ustar00<?php
/**
 * The global settings of the plugin.
 *
 * @link       https://themeisle.com
 * @since      1.0.0
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app
 */

/**
 * The global settings of the plugin.
 *
 * Defines the plugin global settings instance and modules.
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app
 * @author     Themeisle <friends@themeisle.com>
 */
class Orbit_Fox_Global_Settings {

	/**
	 * The main instance var.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @var     Orbit_Fox_Global_Settings $instance The instance of this class.
	 */
	public static $instance;

	/**
	 * Stores the default modules data.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @var     array $modules Modules List.
	 */
	public $modules = array();

	/**
	 * Stores an array of module objects.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @var     array $module_objects Stores references to modules Objects.
	 */
	public $module_objects = array();

	/**
	 * The instance method for the static class.
	 * Defines and returns the instance of the static class.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @return Orbit_Fox_Global_Settings
	 */
	public static function instance() {
		if ( ! isset( self::$instance ) && ! ( self::$instance instanceof Orbit_Fox_Global_Settings ) ) {
			self::$instance          = new Orbit_Fox_Global_Settings;
			self::$instance->modules = apply_filters(
				'obfx_modules',
				array(
					'social-sharing',
					'image-cdn',
					'uptime-monitor',
					'google-analytics',
					'gutenberg-blocks',
					'companion-legacy',
					'elementor-widgets',
					'template-directory',
					'menu-icons',
					'mystock-import',
					'policy-notice',
					'beaver-widgets',
					'safe-updates',
				)
			);
		}// End if().

		return self::$instance;
	}

	/**
	 * Registers a module object reference in the $module_objects array.
	 *
	 * @since   1.0.0
	 * @access  public
	 *
	 * @param   string                    $name The name of the module from $modules array.
	 * @param   Orbit_Fox_Module_Abstract $module The module object.
	 */
	public function register_module_reference( $name, Orbit_Fox_Module_Abstract $module ) {
		self::$instance->module_objects[ $name ] = $module;
	}

	/**
	 * Method to retrieve instance of modules.
	 *
	 * @since   1.0.0
	 * @access  public
	 * @return array
	 */
	public function get_modules() {
		return self::instance()->modules;
	}

	/**
	 * Method to destroy singleton.
	 *
	 * @since   1.0.0
	 * @access  public
	 */
	public static function destroy_instance() {
		static::$instance = null;
	}
}
views/partials/empty-tpl.php000066600000001653151134027570012202 0ustar00<?php
/**
 * Empty modules template.
 * Imported via the Orbit_Fox_Render_Helper.
 *
 * @link       https://themeisle.com
 * @since      1.0.0
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app/views/partials
 */

if ( ! isset( $title ) ) {
	$title = __( 'There are no modules for the Fox!', 'themeisle-companion' );
}

if ( ! isset( $btn_text ) ) {
	$btn_text = __( 'Contact support', 'themeisle-companion' );
}

if ( ! isset( $show_btn ) ) {
	$show_btn = true;
}

?>
<div class="empty">
	<div class="empty-icon">
		<i class="dashicons dashicons-warning" style="width: 48px; height: 48px; font-size: 48px; "></i>
	</div>
	<h4 class="empty-title"><?php echo $title; ?></h4>
	<?php echo ( isset( $sub_title ) ) ? '<p class="empty-subtitle">' . $sub_title . '</p>' : ''; ?>
	<?php
	if ( $show_btn ) {
		?>
		<div class="empty-action">
			<button class="btn btn-primary"><?php echo $btn_text; ?></button>
		</div>
		<?php
	}
	?>
</div>
views/partials/module-panel-tpl.php000066600000004476151134027570013434 0ustar00<?php
/**
 * Panel modules template.
 * Imported via the Orbit_Fox_Render_Helper.
 *
 * @link       https://themeisle.com
 * @since      1.0.0
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app/views/partials
 */

if ( ! isset( $slug ) ) {
	$slug = '';
}
$noance = wp_create_nonce( 'obfx_update_module_options_' . $slug );

if ( ! isset( $active ) ) {
	$active = false;
}

if ( ! isset( $name ) ) {
	$name = __( 'The Module Name', 'themeisle-companion' );
}

if ( ! isset( $description ) ) {
	$description = __( 'The Module Description ...', 'themeisle-companion' );
}

if ( ! isset( $options_fields ) ) {
	$options_fields = __( 'No options provided.', 'themeisle-companion' );
}
$styles          = array();
$disabled_fields = '';
if ( ! $active ) {
	$styles []       = 'display: none';
	$disabled_fields = 'disabled';
}
$btn_class = '';
if ( isset( $show ) && $show ) {
	$btn_class = 'active';

}
$styles = sprintf( 'style="%s"', implode( ':', $styles ) );

?>
<div id="obfx-mod-<?php echo $slug; ?>" class="panel options <?php echo esc_attr( $btn_class ); ?>" <?php echo $styles; ?>>
	<div class="panel-header">
		<button class="btn btn-action circle btn-expand <?php echo esc_attr( $btn_class ); ?>"
				style="float: right; margin-right: 10px;">
			<i class="dashicons dashicons-arrow-down-alt2"></i>
		</button>
		<div class="panel-title"><?php echo $name; ?></div>
		<div class="panel-subtitle"><?php echo $description; ?></div>
		<div class="obfx-mod-toast toast" style="display: none;">
			<button class="obfx-toast-dismiss btn btn-clear float-right"></button>
			<span>Mock text for Toast Element</span>
		</div>
	</div>
	<form id="obfx-module-form-<?php echo $slug; ?>" class="obfx-module-form <?php echo esc_attr( $btn_class ); ?> ">
		<fieldset <?php echo $disabled_fields; ?> >
			<input type="hidden" name="module-slug" value="<?php echo $slug; ?>">
			<input type="hidden" name="noance" value="<?php echo $noance; ?>">
			<div class="panel-body">
				<?php echo $options_fields; ?>
				<div class="divider"></div>
			</div>
			<?php if ( isset( $no_save ) && $no_save === false ) : ?>
			<div class="panel-footer text-right">
				<button class="btn obfx-mod-btn-cancel" disabled>Cancel</button>
				<button type="submit" class="btn btn-primary obfx-mod-btn-save" disabled>Save</button>
			</div>
			<?php endif; ?>
		</fieldset>
	</form>
</div>
views/partials/.htaccess000066600000000424151134027570011327 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>views/partials/module-toast-tpl.php000066600000000723151134027570013456 0ustar00<?php
/**
 * Toast modules template.
 * Imported via the Orbit_Fox_Render_Helper.
 *
 * @link       https://themeisle.com
 * @since      1.0.0
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app/views/partials
 */

?>
<div class="obfx-mod-toast toast toast-<?php echo $notice['type']; ?>">
	<button class="obfx-toast-dismiss btn btn-clear float-right"></button>
	<b><?php echo $notice['title']; ?></b><br/>
	<span><?php echo $notice['message']; ?></span>
</div>
views/partials/module-tile-tpl.php000066600000005120151134027570013255 0ustar00<?php
/**
 * Tile modules template.
 * Imported via the Orbit_Fox_Render_Helper.
 *
 * @link       https://themeisle.com
 * @since      1.0.0
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app/views/partials
 */

if ( ! isset( $name ) ) {
	$name = __( 'Module Name', 'themeisle-companion' );
}

if ( ! isset( $description ) ) {
	$description = __( 'Module Description ...', 'themeisle-companion' );
}

if ( ! isset( $checked ) ) {
	$checked = '';
}
if ( ! isset( $beta ) ) {
	$beta = false;
}

$toggle_class = 'obfx-mod-switch';

if ( ! empty( $confirm_intent ) ) {

	$toggle_class .= ' obfx-mod-confirm-intent';
	$modal        = '
        <div id="' . esc_attr( $slug ) . '" class="modal">
            <a href="#close" class="close-confirm-intent modal-overlay" aria-label="Close"></a>
            <div class="modal-container"> 
                <div class="modal-header">
                    <a href="#" class="btn btn-clear float-right close-confirm-intent" aria-label="Close"></a>
                </div>
                <div class="modal-body">' . wp_kses_post( $confirm_intent ) . '</div>
                <div class="modal-footer">
                        <button class="btn btn-primary accept-confirm-intent">' . __( 'Got it!', 'themeisle-companion' ) . '</button>
                </div>
            </div>
        </div>';
}

$noance = wp_create_nonce( 'obfx_activate_mod_' . $slug );

?>
<div class="tile <?php echo 'obfx-tile-' . esc_attr( $slug ); ?>" >
	<div class="tile-icon">
		<div class="example-tile-icon">
			<i class="dashicons dashicons-admin-plugins centered"></i>
		</div>
	</div>
	<div class="tile-content">
		<p class="tile-title"><?php echo $name; ?></p>
		<p class="tile-subtitle"><?php echo $description; ?></p>
	</div>
	<div class="tile-action">
		<div class="form-group">
			<label class="form-switch <?php echo empty( $checked ) ? '' : 'activated'; ?>">
				<input class="<?php echo esc_attr( $toggle_class ); ?>" type="checkbox" name="<?php echo $slug; ?>"
					   value="<?php echo $noance; ?>" <?php echo $checked; ?> >
				<i class="form-icon"></i>
				<span class="inactive"><?php echo __( 'Activate', 'themeisle-companion' ); ?></span>
				<i class="dashicons dashicons-yes"></i>

			</label>
			<?php if ( $beta ) { ?>
				<p class="obfx-beta-module"><?php echo __( 'Beta module', 'themeisle-companion' ); ?></p>
			<?php } ?>
			<?php
			if ( ! empty( $modal ) ) {
				echo wp_kses_post( $modal );
			}
			?>
			<?php do_action( 'obfx_activate_btn_before', $slug, $checked === 'checked' ); ?>
		</div>
	</div>
	<?php do_action( 'obfx_module_tile_after', $slug, $checked === 'checked' ); ?>
</div>

views/.htaccess000066600000000424151134027570007510 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>views/modules-page.php000066600000003371151134027570011011 0ustar00<?php
/**
 * The View Page for Orbit Fox Modules.
 *
 * @link       https://themeisle.com
 * @since      1.0.0
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app/views
 * @codeCoverageIgnore
 */

if ( ! isset( $no_modules ) ) {
	$no_modules = true;
}

if ( ! isset( $empty_tpl ) ) {
	$empty_tpl = '';
}

if ( ! isset( $count_modules ) ) {
	$count_modules = 0;
}

if ( ! isset( $tiles ) ) {
	$tiles = '';
}

if ( ! isset( $toasts ) ) {
	$toasts = '';
}

if ( ! isset( $panels ) ) {
	$panels = '';
}
?>
<div class="obfx-wrapper obfx-header">
	<div class="obfx-header-content">
		<img src="<?php echo OBFX_URL; ?>/images/orbit-fox.png" title="Orbit Fox" class="obfx-logo"/>
		<h1><?php echo __( 'Orbit Fox Companion', 'themeisle-companion' ); ?></h1><span class="powered"> by <a
					href="https://themeisle.com" target="_blank"><b>ThemeIsle</b></a></span>
	</div>
</div>
<div id="obfx-wrapper" style="padding: 0; margin-top: 10px; margin-bottom: 5px;">
	<?php
	echo $toasts;
	?>
</div>
<div class="obfx-wrapper" id="obfx-modules-wrapper">
	<?php
	if ( $no_modules ) {
		echo $empty_tpl;
	} else {
		?>
		<div class="panel">
			<div class="panel-header text-center">
				<div class="panel-title mt-10"><?php echo __( 'Available Modules', 'themeisle-companion' ); ?></div>
			</div>
			<div class="panel-body">
				<?php echo $tiles; ?>
			</div>
			<div class="panel-footer">
				<!-- buttons or inputs -->
			</div>
		</div>
		<div class="panel">
			<div class="panel-header text-center">
				<div class="panel-title mt-10"><?php echo __( 'Activated Modules Options', 'themeisle-companion' ); ?></div>
			</div>
			<?php echo ( $panels == '' ) ? '<p class="text-center">' . __( 'No modules activated.', 'themeisle-companion' ) . '</p>' : $panels; ?>
		</div>
		<?php
	}
	?>
</div>
helpers/class-orbit-fox-render-helper.php000066600000030122151134027570014474 0ustar00<?php
/**
 * The Helper Class for content rendering.
 *
 * @link       https://themeisle.com
 * @since      1.0.0
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app/helpers
 */

/**
 * The class that contains utility methods to render partials, views or elements.
 *
 * @package    Orbit_Fox
 * @subpackage Orbit_Fox/app/helpers
 * @author     Themeisle <friends@themeisle.com>
 */
class Orbit_Fox_Render_Helper {

	/**
	 * Get a partial template and return the output.
	 *
	 * @since   1.0.0
	 * @access  public
	 *
	 * @param   string $name The name of the partial w/o '-tpl.php'.
	 * @param   array  $args Optional. An associative array with name and value to be
	 *                                  passed to the partial.
	 *
	 * @return string
	 */
	public function get_partial( $name = '', $args = array() ) {
		ob_start();
		$file = OBX_PATH . '/core/app/views/partials/' . $name . '-tpl.php';
		if ( ! empty( $args ) ) {
			foreach ( $args as $obfx_rh_name => $obfx_rh_value ) {
				$$obfx_rh_name = $obfx_rh_value;
			}
		}
		if ( file_exists( $file ) ) {
			include $file;
		}

		return ob_get_clean();
	}

	/**
	 * Get a view template and return the output.
	 *
	 * @since   1.0.0
	 * @access  public
	 *
	 * @param   string $name The name of the partial w/o '-page.php'.
	 * @param   array  $args Optional. An associative array with name and value to be
	 *                                  passed to the view.
	 *
	 * @return string
	 */
	public function get_view( $name = '', $args = array() ) {
		ob_start();
		$file = OBX_PATH . '/core/app/views/' . $name . '-page.php';
		if ( ! empty( $args ) ) {
			foreach ( $args as $obfx_rh_name => $obfx_rh_value ) {
				$$obfx_rh_name = $obfx_rh_value;
			}
		}
		if ( file_exists( $file ) ) {
			include $file;
		}

		return ob_get_clean();
	}

	/**
	 * Method to render option to a field.
	 *
	 * @since   1.0.0
	 * @access  public
	 *
	 * @param   array $option The option from the module..
	 *
	 * @return mixed
	 */
	public function render_option( $option = array() ) {

		$option = $this->sanitize_option( $option );
		switch ( $option['type'] ) {
			case 'text':
				return $this->field_text( $option );
				break;
			case 'email':
				return $this->field_text( $option, true );
				break;
			case 'textarea':
				return $this->field_textarea( $option );
				break;
			case 'select':
				return $this->field_select( $option );
				break;
			case 'radio':
				return $this->field_radio( $option );
				break;
			case 'checkbox':
				return $this->field_checkbox( $option );
				break;
			case 'toggle':
				return $this->field_toggle( $option );
				break;
			case 'title':
				return $this->field_title( $option );
				break;
			case 'custom':
				return apply_filters( 'obfx_custom_control_' . $option['id'], '' );
				break;
			case 'link':
				return $this->field_link( $option );
				break;
			case 'password':
				return $this->field_password( $option );
				break;
			default:
				return __( 'No option found for provided type', 'themeisle-companion' );
				break;
		}

	}

	/**
	 * Merges specific defaults with general ones.
	 *
	 * @since   1.0.0
	 * @access  private
	 *
	 * @param   array $option The specific defaults array.
	 *
	 * @return array
	 */
	private function sanitize_option( $option ) {
		$general_defaults = array(
			'id'          => null,
			'class'       => null,
			'name'        => null,
			'label'       => 'Module Text Label',
			'title'       => false,
			'description' => false,
			'type'        => null,
			'value'       => '',
			'default'     => '',
			'placeholder' => 'Add some text',
			'disabled'    => false,
			'options'     => array(),
		);

		return wp_parse_args( $option, $general_defaults );
	}

	/**
	 * Render an input text field.
	 *
	 * @since   1.0.0
	 * @access  private
	 *
	 * @param   array $option The option from the module.
	 * @param   bool  $is_email Render an email input instead of text.
	 *
	 * @return mixed
	 */
	private function field_text( $option = array(), $is_email = false ) {
		$input_type = 'text';
		if ( $is_email === true ) {
			$input_type = 'email';
		}

		$field_value = $this->set_field_value( $option );
		$field       = '<input class="form-input ' . $option['class'] . '" type="' . esc_attr( $input_type ) . '" id="' . $option['id'] . '" name="' . $option['name'] . '" placeholder="' . $option['placeholder'] . '" value="' . $field_value . '">';
		$field       = $this->wrap_element( $option, $field );

		return $field;
	}

	/**
	 * Method to set field value.
	 *
	 * @since   1.0.0
	 * @access  private
	 *
	 * @param   array $option The option from the module.
	 *
	 * @return mixed
	 */
	private function set_field_value( $option = array() ) {
		$field_value = $option['default'];
		if ( isset( $option['value'] ) && $option['value'] != '' ) {
			$field_value = $option['value'];
		}

		return $field_value;
	}

	/**
	 * Utility method to wrap an element with proper blocks.
	 *
	 * @since   1.0.0
	 * @access  private
	 *
	 * @param   array  $option The option array.
	 * @param   string $element The element we want to wrap.
	 *
	 * @return string
	 */
	private function wrap_element( $option, $element ) {
		$title       = $this->get_title( $option['id'], $option['title'] );
		$description = $this->get_description( $option['description'] );

		$before_wrap = '';
		if ( isset( $option['before_wrap'] ) ) {
			$before_wrap = wp_kses_post( $option['before_wrap'] ); // @codeCoverageIgnore
		}

		$after_wrap = '';
		if ( isset( $option['after_wrap'] ) ) {
			$after_wrap = wp_kses_post( $option['after_wrap'] ); // @codeCoverageIgnore
		}

		return '
		' . $before_wrap . '
		<div class="form-group ' . $option['class'] . '">
			' . $title . '
			' . $element . '
			' . $description . '
		</div>
		' . $after_wrap . '
		';
	}

	/**
	 * Method to return a title for element if needed.
	 *
	 * @since   1.0.0
	 * @access  private
	 *
	 * @param   string $element_id The option id field.
	 * @param   string $title The option title field.
	 *
	 * @return string
	 */
	private function get_title( $element_id, $title ) {
		$display_title = '';
		if ( $title ) {
			$display_title = '<label class="form-label" for="' . $element_id . '">' . $title . '</label>';
		}

		return $display_title;
	}

	/**
	 * Method to return a description for element if needed.
	 *
	 * @since   1.0.0
	 * @access  private
	 *
	 * @param   string $description The option description field.
	 *
	 * @return string
	 */
	private function get_description( $description ) {
		$display_description = '';
		if ( $description ) {
			$display_description = '<p><small>' . $description . '</small></p>';
		}

		return $display_description;
	}

	/**
	 * Render a textarea field.
	 *
	 * @since   1.0.0
	 * @access  private
	 *
	 * @param   array $option The option from the module.
	 *
	 * @return mixed
	 */
	private function field_textarea( $option = array() ) {
		$field_value = $this->set_field_value( $option );
		$field       = '<textarea class="form-input ' . $option['class'] . '" id="' . $option['id'] . '" name="' . $option['name'] . '" placeholder="' . $option['placeholder'] . '" rows="3">' . $field_value . '</textarea>';
		$field       = $this->wrap_element( $option, $field );

		return $field;
	}

	/**
	 * Render a select field.
	 *
	 * @since   1.0.0
	 * @access  private
	 *
	 * @param   array $option The option from the module.
	 *
	 * @return mixed
	 */
	private function field_select( $option = array() ) {
		$field_value    = $this->set_field_value( $option );
		$select_options = '';
		foreach ( $option['options'] as $value => $label ) {
			$is_selected = '';
			if ( $field_value == $value ) {
				$is_selected = 'selected';
			}
			$select_options .= '<option value="' . $value . '" ' . $is_selected . '>' . $label . '</option>';
		}
		$field = '
			<select class="form-select ' . $option['class'] . '" id="' . $option['id'] . '" name="' . $option['name'] . '" placeholder="' . $option['placeholder'] . '">
				' . $select_options . '
			</select>';
		$field = $this->wrap_element( $option, $field );

		return $field;
	}

	/**
	 * Render a radio field.
	 *
	 * @since   1.0.0
	 * @access  private
	 *
	 * @param   array $option The option from the module.
	 *
	 * @return mixed
	 */
	private function field_radio( $option = array() ) {
		$field_value    = $this->set_field_value( $option );
		$select_options = '';
		foreach ( $option['options'] as $value => $label ) {
			$checked = '';
			if ( $value == $field_value ) {
				$checked = 'checked';
			}
			$select_options .= $this->generate_check_type( 'radio', $value, $checked, $label, $option );
		}
		$field = $this->wrap_element( $option, $select_options );

		return $field;
	}

	/**
	 * DRY method to generate checkbox or radio field types
	 *
	 * @since   1.0.0
	 * @access  private
	 *
	 * @param   string $type The field type ( checkbox | radio ).
	 * @param   string $field_value The field value.
	 * @param   string $checked The checked flag.
	 * @param   string $label The option label.
	 * @param   array  $option The option from the module.
	 *
	 * @return string
	 */
	private function generate_check_type( $type = 'radio', $field_value, $checked, $label, $option = array() ) {
		return '
		<label class="form-' . $type . ' ' . $option['class'] . '">
			<input type="' . $type . '" name="' . $option['name'] . '" value="' . $field_value . '" ' . $checked . ' />
			<i class="form-icon"></i> ' . $label . '
		</label>
		';
	}

	/**
	 * Render a checkbox field.
	 *
	 * @since   1.0.0
	 * @access  private
	 *
	 * @param   array $option The option from the module.
	 *
	 * @return mixed
	 */
	private function field_checkbox( $option = array() ) {
		$field_value = $this->set_field_value( $option );
		$checked     = '';
		if ( $field_value ) {
			$checked = 'checked';
		}
		$select_options = $this->generate_check_type( 'checkbox', 1, $checked, $option['label'], $option );
		$field          = $this->wrap_element( $option, $select_options );

		return $field;
	}

	/**
	 * Render a toggle field.
	 *
	 * @since   1.0.0
	 * @access  private
	 *
	 * @param   array $option The option from the module.
	 *
	 * @return mixed
	 */
	private function field_toggle( $option = array() ) {
		$field_value = $this->set_field_value( $option );
		$checked     = '';
		if ( $field_value ) {
			$checked = 'checked';
		}
		$field = '
			<label class="form-switch ' . $option['class'] . '">
				<input type="checkbox" name="' . $option['name'] . '" value="1" ' . $checked . ' />
				<i class="form-icon"></i> ' . $option['label'] . '
			</label>';
		$field = $this->wrap_element( $option, $field );

		return $field;
	}

	/**
	 * Render a title field.
	 *
	 * @since   2.5.0
	 * @access  private
	 *
	 * @param   array $option The option from the module.
	 *
	 * @return mixed
	 */
	private function field_title( $option = array() ) {

		$field = $this->wrap_element( $option, '' );

		return $field;
	}

	/**
	 * Render a toggle field.
	 *
	 * @since   1.0.0
	 * @access  private
	 *
	 * @param   array $option The option from the module.
	 *
	 * @return mixed
	 */
	private function field_link( $option = array() ) {
		if ( ! isset( $option['link-id'] ) ) {
			$option['link-id'] = $option['id'];
		}
		if ( ! isset( $option['target'] ) ) {
			$option['target'] = '';
		}
		$field = '
			<a id="' . esc_attr( $option['link-id'] ) . '" target="' . esc_attr( $option['target'] ) . '" class="' . esc_attr( isset( $option['link-class'] ) ? $option['link-class'] : '' ) . '" href="' . esc_url( $option['url'] ) . '">' .
				 wp_kses_post( $option['text'] )
				 . '</a>';

		$field = $this->wrap_element( $option, $field );

		return $field;
	}

	/**
	 * Render an input password field.
	 *
	 * @since   1.0.0
	 * @access  private
	 *
	 * @param   array $option The option from the module.
	 * @param   bool  $is_email Render an email input instead of text.
	 *
	 * @return mixed
	 */
	private function field_password( $option = array(), $is_email = false ) {
		$input_type = 'password';

		$field_value = $this->set_field_value( $option );
		$field       = '<input class="form-input ' . $option['class'] . '" type="' . esc_attr( $input_type ) . '" id="' . $option['id'] . '" name="' . $option['name'] . '" placeholder="' . $option['placeholder'] . '" value="' . $field_value . '">';
		$field       = $this->wrap_element( $option, $field );

		return $field;
	}


}
helpers/.htaccess000066600000000424151134027570010015 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>init/notices.php000066600000020417151135505570007703 0ustar00<?php
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable Generic.Arrays.DisallowLongArraySyntax.Found
if ( ! function_exists( 'aioseo_php_notice' ) ) {
	/**
	 * Display the notice after deactivation.
	 *
	 * @since 4.0.0
	 */
	function aioseo_php_notice() {
		$medium = false !== strpos( AIOSEO_PHP_VERSION_DIR, 'pro' ) ? 'proplugin' : 'liteplugin';
		?>
		<div class="notice notice-error">
			<p>
				<?php
				echo wp_kses(
					sprintf(
							// Translators: 1 - Opening HTML bold tag, 2 - Closing HTML bold tag, 3 - Opening HTML link tag, 4 - Closing HTML link tag.
						__( 'Your site is running an %1$sinsecure version%2$s of PHP that is no longer supported. Please contact your web hosting provider to update your PHP version or switch to a %3$srecommended WordPress hosting company%4$s.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'<strong>',
						'</strong>',
						'<a href="https://www.wpbeginner.com/wordpress-hosting/" target="_blank" rel="noopener noreferrer">',
						'</a>'
					),
					array(
						'a'      => array(
							'href'   => array(),
							'target' => array(),
							'rel'    => array(),
						),
						'strong' => array(),
					)
				);
				?>
				<br><br>
				<?php
				echo wp_kses(
					sprintf(
							// Translators: 1 - Opening HTML bold tag, 2 - Closing HTML bold tag, 3 - The short plugin name ("AIOSEO"), 4 - Opening HTML link tag, 5 - Closing HTML link tag.
						__( '%1$sNote:%2$s %3$s plugin is disabled on your site until you fix the issue. %4$sRead more for additional information.%5$s', 'all-in-one-seo-pack' ),
						'<strong>',
						'</strong>',
						'AIOSEO',
						'<a href="https://aioseo.com/docs/supported-php-version/?utm_source=WordPress&utm_medium=' . $medium . '&utm_campaign=outdated-php-notice" target="_blank" rel="noopener noreferrer">', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'</a>'
					),
					array(
						'a'      => array(
							'href'   => array(),
							'target' => array(),
							'rel'    => array(),
						),
						'strong' => array(),
					)
				);
				?>
			</p>
		</div>

		<?php
		// In case this is on plugin activation.
		if ( isset( $_GET['activate'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended	
			unset( $_GET['activate'] );
		}
	}
}

if ( ! function_exists( 'aioseo_php_notice_deprecated' ) ) {
	/**
	 * Display the notice after deactivation.
	 *
	 * @since 4.0.0
	 */
	function aioseo_php_notice_deprecated() {
		$medium = false !== strpos( AIOSEO_PHP_VERSION_DIR, 'pro' ) ? 'proplugin' : 'liteplugin';
		?>
		<div class="notice notice-error">
			<p>
				<?php
				echo wp_kses(
					sprintf(
						// Translators: 1 - Opening HTML bold tag, 2 - Closing HTML bold tag, 3 - Opening HTML link tag, 4 - Closing HTML link tag.
						__( 'Your site is running an %1$soutdated version%2$s of PHP that is no longer supported and may cause issues with %3$s. Please contact your web hosting provider to update your PHP version or switch to a %4$srecommended WordPress hosting company%5$s.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'<strong>',
						'</strong>',
						'<strong>AIOSEO</strong>',
						'<a href="https://www.wpbeginner.com/wordpress-hosting/" target="_blank" rel="noopener noreferrer">',
						'</a>'
					),
					array(
						'a'      => array(
							'href'   => array(),
							'target' => array(),
							'rel'    => array(),
						),
						'strong' => array(),
					)
				);
				?>
				<br><br>
				<?php
				echo wp_kses(
					sprintf(
						// phpcs:ignore Generic.Files.LineLength.MaxExceeded
						// Translators: 1 - Opening HTML bold tag, 2 - Closing HTML bold tag, 3 - The PHP version, 4 - The current year, 5 - The short plugin name ("AIOSEO"), 6 - Opening HTML link tag, 7 - Closing HTML link tag.
						__( '%1$sNote:%2$s Support for PHP %3$s will be discontinued in %4$s. After this, if no further action is taken, %5$s functionality will be disabled. %6$sRead more for additional information.%7$s', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'<strong>',
						'</strong>',
						PHP_VERSION,
						gmdate( 'Y' ),
						'AIOSEO',
						'<a href="https://aioseo.com/docs/supported-php-version/?utm_source=WordPress&utm_medium=' . $medium . '&utm_campaign=outdated-php-notice" target="_blank" rel="noopener noreferrer">', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'</a>'
					),
					array(
						'a'      => array(
							'href'   => array(),
							'target' => array(),
							'rel'    => array(),
						),
						'strong' => array(),
					)
				);
				?>
			</p>
		</div>

		<?php
		// In case this is on plugin activation.
		if ( isset( $_GET['activate'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended	
			unset( $_GET['activate'] );
		}
	}
}

if ( ! function_exists( 'aioseo_wordpress_notice' ) ) {
	/**
	 * Display the notice after deactivation.
	 *
	 * @since 4.1.2
	 */
	function aioseo_wordpress_notice() {
		$medium = false !== strpos( AIOSEO_PHP_VERSION_DIR, 'pro' ) ? 'proplugin' : 'liteplugin';
		?>
		<div class="notice notice-error">
			<p>
				<?php
				echo wp_kses(
					sprintf(
							// Translators: 1 - Opening HTML bold tag, 2 - Closing HTML bold tag, 3 - The plugin name ("All in One SEO").
						__( 'Your site is running an %1$sinsecure version%2$s of WordPress that is no longer supported. Please update your site to the latest version of WordPress in order to continue using %3$s.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'<strong>',
						'</strong>',
						'All in One SEO'
					),
					array(
						'strong' => array(),
					)
				);
				?>
				<br><br>
				<?php
				echo wp_kses(
					sprintf(
						// phpcs:ignore Generic.Files.LineLength.MaxExceeded
						// Translators: 1 - Opening HTML bold tag, 2 - Closing HTML bold tag, 3 - The short plugin name ("AIOSEO"), 4 - The current year, 5 - Opening HTML link tag, 6 - Closing HTML link tag.
						__( '%1$sNote:%2$s %3$s will be discontinuing support for WordPress versions older than version 5.7 by the end of %4$s. %5$sRead more for additional information.%6$s', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'<strong>',
						'</strong>',
						'AIOSEO',
						gmdate( 'Y' ),
						'<a href="https://aioseo.com/docs/update-wordpress/?utm_source=WordPress&utm_medium=' . $medium . '&utm_campaign=outdated-wordpress-notice" target="_blank" rel="noopener noreferrer">', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'</a>'
					),
					array(
						'a'      => array(
							'href'   => array(),
							'target' => array(),
							'rel'    => array(),
						),
						'strong' => array(),
					)
				);
				?>
			</p>
		</div>

		<?php
		// In case this is on plugin activation.
		if ( isset( $_GET['activate'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended	
			unset( $_GET['activate'] );
		}
	}
}

if ( ! function_exists( 'aioseo_lite_notice' ) ) {
	/**
	 * Display the notice after deactivation when Pro is still active
	 * and user wanted to activate the Lite version of the plugin.
	 *
	 * @since 4.0.0
	 */
	function aioseo_lite_notice() {

		global $aioseoLiteJustActivated, $aioseoLiteJustDeactivated;

		if (
			empty( $aioseoLiteJustActivated ) ||
			empty( $aioseoLiteJustDeactivated )
		) {
			return;
		}

		// Currently tried to activate Lite with Pro still active, so display the message.
		printf(
			'<div class="notice notice-warning">
				<p>%1$s</p>
				<p>%2$s</p>
			</div>',
			esc_html__( 'Heads up!', 'all-in-one-seo-pack' ),
			// Translators: 1 - "AIOSEO Pro", 2 - "AIOSEO Lite".
			sprintf( esc_html__( 'Your site already has %1$s activated. If you want to switch to %2$s, please first go to Plugins > Installed Plugins and deactivate %1$s. Then, you can activate %2$s.', 'all-in-one-seo-pack' ), 'AIOSEO Pro', 'AIOSEO Lite' ) // phpcs:ignore Generic.Files.LineLength.MaxExceeded
		);

		if ( isset( $_GET['activate'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended	
			unset( $_GET['activate'] );
		}

		unset( $aioseoLiteJustActivated, $aioseoLiteJustDeactivated );
	}
}init/activation.php000066600000003744151135505570010404 0ustar00<?php
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! function_exists( 'aioseo_lite_just_activated' ) ) {
	/**
	 * Store temporarily that the Lite version of the plugin was activated.
	 * This is needed because WP does a redirect after activation and
	 * we need to preserve this state to know whether user activated Lite or not.
	 *
	 * @since 4.0.0
	 */
	function aioseo_lite_just_activated() {
		aioseo()->core->cache->update( 'lite_just_activated', true );
	}
}

if ( ! function_exists( 'aioseo_lite_just_deactivated' ) ) {
	/**
	 * Store temporarily that Lite plugin was deactivated.
	 * Convert temporary "activated" value to a global variable,
	 * so it is available through the request. Remove from the storage.
	 *
	 * @since 4.0.0
	 */
	function aioseo_lite_just_deactivated() {
		global $aioseoLiteJustActivated, $aioseoLiteJustDeactivated;

		$aioseoLiteJustActivated   = (bool) aioseo()->core->cache->get( 'lite_just_activated' );
		$aioseoLiteJustDeactivated = true;

		aioseo()->core->cache->delete( 'lite_just_activated' );
	}
}

if ( ! function_exists( 'aioseo_pro_just_activated' ) ) {
	/**
	 * Store temporarily that the Pro version of the plugin was activated.
	 * This is needed because when we activate the Pro version on top
	 * of the Lite version, it does not trigger the activation hook in Pro.
	 *
	 * @since 4.0.0
	 */
	function aioseo_pro_just_activated() {
		$liteActivated = is_plugin_active( 'all-in-one-seo-pack/all_in_one_seo_pack.php' );
		if ( $liteActivated ) {
			// Add capabilities for the current user on upgrade so that the menu is visible on the first request.
			aioseo()->activate->addCapabilitiesOnUpgrade();

			aioseo()->core->cache->update( 'pro_just_deactivated_lite', true );
		}
	}
}

// If we detect that V3 is active, let's deactivate it now.
if ( defined( 'AIOSEOP_VERSION' ) && defined( 'AIOSEO_PLUGIN_FILE' ) ) {
	require_once ABSPATH . 'wp-admin/includes/plugin.php';
	deactivate_plugins( plugin_basename( AIOSEO_PLUGIN_FILE ) );
}init/blocks.js000066600000002403151135505570007334 0ustar00/**
 * Since we dynamically load our blocks, wordpress.org cannot pick them up properly.
 * This file solely exists to let WordPress know what blocks we are currently using.
 *
 * @since 4.2.4
 */

/* eslint-disable no-undef */

registerBlockType('aioseo/breadcrumbs', {
	title : 'AIOSEO - Breadcrumbs'
})
registerBlockType('aioseo/html-sitemap', {
	title : 'AIOSEO - HTML Sitemap'
})
registerBlockType('aioseo/faq', {
	title : 'AIOSEO - FAQ with JSON Schema'
})
registerBlockType('aioseo/table-of-contents', {
	title : 'AIOSEO - Table of Contents'
})
registerBlockType('aioseo/businessinfo', {
	title : 'AIOSEO - Local Business Info'
})
registerBlockType('aioseo/locationcategories', {
	title : 'AIOSEO - Local Business Location Categories'
})
registerBlockType('aioseo/locations', {
	title : 'AIOSEO - Local Business Locations'
})
registerBlockType('aioseo/locationmap', {
	title : 'AIOSEO - Local Business Google Map'
})
registerBlockType('aioseo/openinghours', {
	title : 'AIOSEO - Local Business Opening Hours'
})
registerBlockType('aioseo/openinghours', {
	title : 'AIOSEO - Author Bio (E-E-A-T)'
})
registerBlockType('aioseo/openinghours', {
	title : 'AIOSEO - Author Name (E-E-A-T)'
})
registerBlockType('aioseo/openinghours', {
	title : 'AIOSEO - Reviewer Name (E-E-A-T)'
})init/init.php000066600000002117151135505570007177 0ustar00<?php
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! function_exists( 'aioseoMaybePluginIsDisabled' ) ) {
	/**
	 * Disable the AIOSEO if triggered externally.
	 *
	 * @since   4.1.5
	 * @version 4.5.0 Added the $file parameter and Lite check.
	 *
	 * @param  string $file The plugin file.
	 * @return bool         True if the plugin should be disabled.
	 */
	function aioseoMaybePluginIsDisabled( $file ) {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';
		if (
			'all-in-one-seo-pack/all_in_one_seo_pack.php' === plugin_basename( $file ) &&
			is_plugin_active( 'all-in-one-seo-pack-pro/all_in_one_seo_pack.php' )
		) {
			return true;
		}

		if ( ! defined( 'AIOSEO_DEV_VERSION' ) && ! isset( $_REQUEST['aioseo-dev'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			return false;
		}

		if ( ! isset( $_REQUEST['aioseo-disable-plugin'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			return false;
		}

		return true;
	}
}Lite/Utils/Helpers.php000066600000001314151135505570010666 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Utils;

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

use AIOSEO\Plugin\Common\Utils as CommonUtils;

/**
 * Contains helper functions.
 *
 * @since 4.2.4
 */
class Helpers extends CommonUtils\Helpers {
	/**
	 * Get the headers for internal API requests.
	 *
	 * @since 4.2.4
	 *
	 * @return array An array of headers.
	 */
	public function getApiHeaders() {
		return [];
	}

	/**
	 * Get the User Agent for internal API requests.
	 *
	 * @since 4.2.4
	 *
	 * @return string The User Agent.
	 */
	public function getApiUserAgent() {
		return 'WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' ) . '; AIOSEO/Lite/' . AIOSEO_VERSION;
	}
}Lite/Admin/Usage.php000066600000001116151135505570010260 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Admin;

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

use AIOSEO\Plugin\Common\Admin as CommonAdmin;

/**
 * Usage tracking class.
 *
 * @since 4.0.0
 */
class Usage extends CommonAdmin\Usage {
	/**
	 * Class Constructor
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		parent::__construct();

		$this->enabled = aioseo()->options->advanced->usageTracking;
	}

	/**
	 * Get the type for the request.
	 *
	 * @since 4.0.0
	 *
	 * @return string The install type.
	 */
	public function getType() {
		return 'lite';
	}
}Lite/Admin/Admin.php000066600000005132151135505570010246 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Admin;

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

use AIOSEO\Plugin\Common\Admin as CommonAdmin;

/**
 * Abstract class that Pro and Lite both extend.
 *
 * @since 4.0.0
 */
class Admin extends CommonAdmin\Admin {
	/**
	 * Connect class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Connect
	 */
	public $connect = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		if ( ! wp_doing_cron() ) {
			parent::__construct();
		}

		$this->connect = new Connect();
	}

	/**
	 * Actually adds the menu items to the admin bar.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function addAdminBarMenuItems() {
		// Add an upsell to Pro.
		if ( current_user_can( $this->getPageRequiredCapability( '' ) ) ) {
			$this->adminBarMenuItems['aioseo-pro-upgrade'] = [
				'parent' => 'aioseo-main',
				'title'  => '<span class="aioseo-menu-highlight lite">' . __( 'Upgrade to Pro', 'all-in-one-seo-pack' ) . '</span>',
				'id'     => 'aioseo-pro-upgrade',
				'href'   => apply_filters(
					'aioseo_upgrade_link',
					esc_url( admin_url( 'admin.php?page=aioseo-tools&aioseo-redirect-upgrade=1' ) )
				),
				'meta'   => [ 'target' => '_blank' ],
			];
		}

		parent::addAdminBarMenuItems();
	}

	/**
	 * Add the menu inside of WordPress.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addMenu() {
		parent::addMenu();

		$capability = $this->getPageRequiredCapability( '' );

		// We use the global submenu, because we are adding an external link here.
		if ( current_user_can( $capability ) ) {
			global $submenu;
			$submenu[ $this->pageSlug ][] = [
				'<span class="aioseo-menu-highlight lite">' . esc_html__( 'Upgrade to Pro', 'all-in-one-seo-pack' ) . '</span>',
				$capability,
				apply_filters(
					'aioseo_upgrade_link',
					esc_url( admin_url( 'admin.php?page=aioseo-tools&aioseo-redirect-upgrade=1' ) )
				)
			];
		}
	}

	/**
	 * Check the query args to see if we need to redirect to an external URL.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	protected function checkForRedirects() {
		$mappedUrls = [
			// Added to resolve an issue with the open_basedir in the IIS.

			'aioseo-redirect-upgrade' => apply_filters(
				'aioseo_upgrade_link',
				aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'lite-upgrade/', 'admin-bar', null, false )
			)
		];

		foreach ( $mappedUrls as $queryArg => $redirectUrl ) {
			if ( isset( $_GET[ $queryArg ] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
				wp_redirect( $redirectUrl );
			}
		}
	}
}Lite/Admin/PostSettings.php000066600000003366151135505570011673 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Admin;

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

use AIOSEO\Plugin\Common\Admin as CommonAdmin;

/**
 * Abstract class that Pro and Lite both extend.
 *
 * @since 4.0.0
 */
class PostSettings extends CommonAdmin\PostSettings {
	/**
	 * Holds a list of page builder integration class instances.
	 * This prop exists for backwards compatibility with pre-4.2.0 versions (see backwardsCompatibilityLoad() in AIOSEO.php).
	 *
	 * @since 4.4.2
	 *
	 * @var object[]
	 */
	public $integrations = null;

	/**
	 * Initialize the admin.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function __construct() {
		parent::__construct();
	}

	/**
	 * Add upsell to terms.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function init() {
		if ( is_admin() ) {
			// We don't call getPublicTaxonomies() here because we want to show the CTA for Product Attributes as well.
			$taxonomies = get_taxonomies( [], 'objects' );
			foreach ( $taxonomies as $taxObject ) {
				if (
					empty( $taxObject->label ) ||
					! is_taxonomy_viewable( $taxObject )
				) {
					unset( $taxonomies[ $taxObject->name ] );
				}
			}

			foreach ( $taxonomies as $taxonomy ) {
				add_action( $taxonomy->name . '_edit_form', [ $this, 'addTaxonomyUpsell' ] );
				add_action( 'after-' . $taxonomy->name . '-table', [ $this, 'addTaxonomyUpsell' ] );
			}
		}
	}

	/**
	 * Add Taxonomy Upsell
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addTaxonomyUpsell() {
		$screen = aioseo()->helpers->getCurrentScreen();
		if (
			! isset( $screen->parent_base ) ||
			'edit' !== $screen->parent_base ||
			empty( $screen->taxonomy )
		) {
			return;
		}

		include_once AIOSEO_DIR . '/app/Lite/Views/taxonomy-upsell.php';
	}
}Lite/Admin/Notices/Notices.php000066600000004706151135505570012234 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Admin\Notices;

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

use AIOSEO\Plugin\Common\Admin\Notices as CommonNotices;
use AIOSEO\Plugin\Common\Models;

/**
 * Lite version of the notices class.
 *
 * @since 4.0.0
 */
class Notices extends CommonNotices\Notices {
	/**
	 * Initialize the internal notices.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function initInternalNotices() {
		parent::initInternalNotices();

		$this->wooUpsellNotice();
	}

	/**
	 * Validates the notification type.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $type The notification type we are targeting.
	 * @return boolean       True if yes, false if no.
	 */
	public function validateType( $type ) {
		$validated = parent::validateType( $type );

		// Any lite notification should pass here.
		if ( 'lite' === $type ) {
			$validated = true;
		}

		return $validated;
	}

	/**
	 * Add a notice if WooCommerce is detected and not licensed or running Lite.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function wooUpsellNotice() {
		$notification = Models\Notification::getNotificationByName( 'woo-upsell' );

		if (
			! class_exists( 'WooCommerce' )
		) {
			if ( $notification->exists() ) {
				Models\Notification::deleteNotificationByName( 'woo-upsell' );
			}

			return;
		}

		if ( $notification->exists() ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'woo-upsell',
			// Translators: 1 - "WooCommerce".
			'title'             => sprintf( __( 'Advanced %1$s Support', 'all-in-one-seo-pack' ), 'WooCommerce' ),
			// Translators: 1 - "WooCommerce", 2 - The plugin short name ("AIOSEO").
			'content'           => sprintf( __( 'We have detected you are running %1$s. Upgrade to %2$s to unlock our advanced eCommerce SEO features, including SEO for Product Categories and more.', 'all-in-one-seo-pack' ), 'WooCommerce', AIOSEO_PLUGIN_SHORT_NAME . ' Pro' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'type'              => 'info',
			'level'             => [ 'all' ],
			// Translators: 1 - "Pro".
			'button1_label'     => sprintf( __( 'Upgrade to %1$s', 'all-in-one-seo-pack' ), 'Pro' ),
			'button1_action'    => html_entity_decode( apply_filters( 'aioseo_upgrade_link', aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'lite-upgrade/', 'woo-notification-upsell', false ) ) ),
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}
}Lite/Admin/Connect.php000066600000027046151135505570010617 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Admin;

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

use AIOSEO\Plugin\Common\Utils;

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

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

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

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

		// Check for connect-specific parameter.
		// phpcs:disable HM.Security.ValidatedSanitizedInput.InputNotSanitized, HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended, Generic.Files.LineLength.MaxExceeded
		if ( ! isset( $_GET['page'] ) ) {
			return;
		}

		$page = sanitize_text_field( wp_unslash( $_GET['page'] ) );
		// phpcs:enable

		// Check if we're on the right page and if current user is allowed to save settings.
		if (
			( 'aioseo-connect-pro' !== $page && 'aioseo-connect' !== $page ) ||
			! current_user_can( 'aioseo_manage_seo' )
		) {
			return;
		}

		set_current_screen();

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

		if ( 'aioseo-connect-pro' === $page ) {
			$this->loadConnectPro();

			return;
		}

		$this->loadConnect();
		// phpcs:enable
	}

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

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

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

		aioseo()->core->assets->load( 'src/vue/standalone/connect/main.js', [], aioseo()->helpers->getVueData() );
	}

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

		aioseo()->core->assets->load( 'src/vue/standalone/connect-pro/main.js', [], aioseo()->helpers->getVueData() );
	}

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

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

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

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

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

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

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

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

		// Generate a hash that can be compared after the user is redirected back.
		$oth       = hash( 'sha512', wp_rand() );
		$hashedOth = hash_hmac( 'sha512', $oth, wp_salt() );

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

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

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

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

	/**
	 * Process AIOSEO Connect.
	 *
	 * @since 1.0.0
	 *
	 * @return array An array containing a valid response or an error message.
	 */
	public function process() {
		// phpcs:disable HM.Security.NonceVerification.Missing, WordPress.Security.NonceVerification
		$hashedOth   = ! empty( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '';
		$downloadUrl = ! empty( $_POST['file'] ) ? esc_url_raw( wp_unslash( $_POST['file'] ) ) : '';
		// phpcs:enable

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

		$success = esc_html__( 'Plugin installed & activated.', 'all-in-one-seo-pack' );

		// Check if all required params are present.
		if ( empty( $downloadUrl ) || empty( $hashedOth ) ) {
			wp_send_json_error( $error );
		}

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

		// Check if the stored hash matches the salted one that is sent back from the server.
		if ( hash_hmac( 'sha512', $oth, wp_salt() ) !== $hashedOth ) {
			wp_send_json_error( $error );
		}

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

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

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

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

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

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

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

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

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

			wp_send_json_success( $success );
		}

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

		$fs = aioseo()->core->fs->noConflict();
		$fs->init( $creds );
		if ( ! $fs->isWpfsValid() ) {
			wp_send_json_error( $error );
		}

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

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

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

		$installer->install( $downloadUrl );

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

		$pluginBasename = $installer->plugin_info();

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

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

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

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

		wp_send_json_success( $success );
	}
}Lite/Traits/Options.php000066600000005230151135505570011066 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Traits;

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

/**
 * Options trait.
 *
 * @since 4.0.0
 */
trait Options {
	/**
	 * Initialize the options.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function init() {
		parent::init();

		$dbOptions = $this->getDbOptions( $this->optionsName . '_lite' );

		// Refactor options.
		$this->defaultsMerged = array_replace_recursive( $this->defaults, $this->liteDefaults );

		$mergedDefaults = array_replace_recursive(
			$this->liteDefaults,
			$this->addValueToValuesArray( $this->liteDefaults, $dbOptions )
		);

		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$dbOptions     = array_replace_recursive(
			$cachedOptions,
			$mergedDefaults
		);

		aioseo()->core->optionsCache->setOptions( $this->optionsName, $dbOptions );
	}

	/**
	 * Merge defaults with liteDefaults.
	 *
	 * @since 4.1.4
	 *
	 * @return array An array of dafults.
	 */
	public function getDefaults() {
		return array_replace_recursive( parent::getDefaults(), $this->liteDefaults );
	}

	/**
	 * Updates the options in the database.
	 *
	 * @since 4.1.4
	 *
	 * @param  string     $optionsName An optional option name to update.
	 * @param  string     $defaults    The defaults to filter the options by.
	 * @param  array|null $options     An optional options array.
	 * @return void
	 */
	public function update( $optionsName = null, $defaults = null, $options = null ) {
		$optionsName = empty( $optionsName ) ? $this->optionsName . '_lite' : $optionsName;
		$defaults    = empty( $defaults ) ? $this->liteDefaults : $defaults;

		// We're creating a new array here because it was setting it by reference.
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$optionsBefore = json_decode( wp_json_encode( $cachedOptions ), true );

		parent::update( $this->optionsName, $options );
		parent::update( $optionsName, $defaults, $optionsBefore );
	}

	/**
	 * Updates the options in the database.
	 *
	 * @since 4.1.4
	 *
	 * @param  boolean $force       Whether or not to force an immediate save.
	 * @param  string  $optionsName An optional option name to update.
	 * @param  string  $defaults    The defaults to filter the options by.
	 * @return void
	 */
	public function save( $force = false, $optionsName = null, $defaults = null ) {
		if ( ! $this->shouldSave && ! $force ) {
			return;
		}

		$optionsName = empty( $optionsName ) ? $this->optionsName . '_lite' : $optionsName;
		$defaults    = empty( $defaults ) ? $this->liteDefaults : $defaults;

		parent::save( $force, $this->optionsName );
		parent::save( $force, $optionsName, $defaults );
	}
}Lite/Options/InternalOptions.php000066600000002071151135505570012750 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Options;

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

use AIOSEO\Plugin\Common\Options as CommonOptions;
use AIOSEO\Plugin\Lite\Traits;

/**
 * Class that holds all internal options for AIOSEO.
 *
 * @since 4.0.0
 */
class InternalOptions extends CommonOptions\InternalOptions {
	use Traits\Options;

	/**
	 * Defaults options for Lite.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $liteDefaults = [
		// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
		'internal' => [
			'activated'      => [ 'type' => 'number', 'default' => 0 ],
			'firstActivated' => [ 'type' => 'number', 'default' => 0 ],
			'installed'      => [ 'type' => 'number', 'default' => 0 ],
			'connect'        => [
				'key'     => [ 'type' => 'string' ],
				'time'    => [ 'type' => 'number', 'default' => 0 ],
				'network' => [ 'type' => 'boolean', 'default' => false ],
				'token'   => [ 'type' => 'string' ]
			]
		]
		// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	];
}Lite/Options/Options.php000066600000002371151135505570011256 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Options;

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

use AIOSEO\Plugin\Common\Options as CommonOptions;
use AIOSEO\Plugin\Lite\Traits;

/**
 * Class that holds all options for AIOSEO.
 *
 * @since 4.0.0
 */
class Options extends CommonOptions\Options {
	use Traits\Options;

	/**
	 * Defaults options for Lite.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $liteDefaults = [
		// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
		'advanced' => [
			'usageTracking' => [ 'type' => 'boolean', 'default' => false ]
		]
		// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	];

	/**
	 * Sanitizes, then saves the options to the database.
	 *
	 * @since 4.7.2
	 *
	 * @param  array $options An array of options to sanitize, then save.
	 * @return void
	 */
	public function sanitizeAndSave( $options ) {
		if ( isset( $options['advanced']['emailSummary']['recipients'] ) ) {
			$options['advanced']['emailSummary']['recipients']                 = [ array_shift( $options['advanced']['emailSummary']['recipients'] ) ];
			$options['advanced']['emailSummary']['recipients'][0]['frequency'] = 'monthly';
		}

		parent::sanitizeAndSave( $options );
	}
}Lite/Api/Api.php000066600000001351151135505570007407 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Api;

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

use AIOSEO\Plugin\Common\Api as CommonApi;

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

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

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

use AIOSEO\Plugin\Common\Api as CommonApi;

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

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

		return $response;
	}
}Lite/Main/Filters.php000066600000005774151135505570010476 0ustar00<?php
namespace AIOSEO\Plugin\Lite\Main;

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

use AIOSEO\Plugin\Common\Main as CommonMain;

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

		$actionLinks = [
			'suggest-feature' => [
				// Translators: This is an action link users can click to open a feature request.
				'label' => __( 'Suggest a Feature', 'all-in-one-seo-pack' ),
				'url'   => aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'suggest-a-feature/', 'plugin-row-meta', 'feature' ),
			],
			'review'          => [
				'label' => $reviewLabel,
				'url'   => aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'review-aioseo', 'plugin-row-meta', 'review' ),
				'title' => sprintf(
					// Translators: 1 - The plugin short name ("AIOSEO").
					__( 'Rate %1$s', 'all-in-one-seo-pack' ),
					'AIOSEO'
				)
			]
		];

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

	/**
	 * Registers our action links for the plugins page.
	 *
	 * @since 4.0.0
	 *
	 * @param  array  $actions    List of existing actions.
	 * @param  string $pluginFile The plugin file.
	 * @return array              List of action links.
	 */
	public function pluginActionLinks( $actions, $pluginFile = '' ) {
		$actionLinks = [
			'settings'   => [
				'label' => __( 'SEO Settings', 'all-in-one-seo-pack' ),
				'url'   => get_admin_url( null, 'admin.php?page=aioseo-settings' ),
			],
			'support'    => [
				// Translators: This is an action link users can click to open our premium support.
				'label' => __( 'Support', 'all-in-one-seo-pack' ),
				'url'   => aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'contact/', 'plugin-action-links', 'Support' ),
			],
			'docs'       => [
				// Translators: This is an action link users can click to open our general documentation page.
				'label' => __( 'Documentation', 'all-in-one-seo-pack' ),
				'url'   => aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'docs/', 'plugin-action-links', 'Documentation' ),
			],
			'proupgrade' => [
				// Translators: This is an action link users can click to purchase a license for All in One SEO Pro.
				'label' => __( 'Upgrade to Pro', 'all-in-one-seo-pack' ),
				'url'   => apply_filters( 'aioseo_upgrade_link', aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'lite-upgrade/', 'plugin-action-links', 'Upgrade', false ) ),
			]
		];

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

		return $this->parseActionLinks( $actions, $pluginFile, $actionLinks, 'before' );
	}
}Lite/Views/taxonomy-upsell.php000066600000133476151135505570012460 0ustar00<?php
// phpcs:disable Generic.Files.LineLength.MaxExceeded

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<style>
	#poststuff.aioseo-taxonomy-upsell {
		min-width: auto;
		overflow: hidden;
	}
</style>
<div id="poststuff" class="aioseo-taxonomy-upsell" style="margin-top:30px;max-width: 800px;">
	<div id="advanced-sortables" class="meta-box-sortables">
		<div id="aioseo-tabbed" class="postbox ">
			<h2 class="hndle">
				<span><?php esc_html_e( 'AIOSEO Settings', 'all-in-one-seo-pack' ); ?></span>
			</h2>
			<div>
				<div class="aioseo-app aioseo-post-settings">
					<div class="aioseo-blur">
						<div class="aioseo-tabs internal">
							<div class="tabs-scroller">
								<div class="var-tabs var--box var-tabs--item-horizontal var-tabs--layout-horizontal-padding">
									<div class="var-tabs__tab-wrap var-tabs--layout-horizontal-scrollable var-tabs--layout-horizontal">
										<div class="var-tab var--box var-tab--active var-tab--horizontal">
											<span class="label">General</span>
										</div>
										<div class="var-tab var--box var-tab--inactive var-tab--horizontal">
											<span class="label">Social</span>
										</div>
										<div class="var-tab var--box var-tab--inactive var-tab--horizontal">
											<span class="label">Redirects</span>
										</div>
										<div class="var-tab var--box var-tab--inactive var-tab--horizontal">
											<span class="label">SEO Revisions</span>
										</div>
										<div class="var-tab var--box var-tab--inactive var-tab--horizontal">
											<span class="label">Advanced</span>
										</div>
									<div class="var-tabs__indicator var-tabs--layout-horizontal-indicator" style="width: 102px; transform: translateX(0px);"><div class="var-tabs__indicator-inner var-tabs--layout-horizontal-indicator-inner"></div>
									</div>
								</div>
							</div>
						</div>
						<div class="tabs-extra"></div>
					</div>
						<div class="aioseo-tab-content aioseo-post-general">
							<div class="aioseo-settings-row mobile-radio-buttons aioseo-row ">
								<div class="aioseo-col col-xs-12 col-md-3 text-xs-left">
									<div class="settings-name">
										<div class="name"> </div>
									</div>
								</div>
								<div class="aioseo-col col-xs-12 col-md-9 text-xs-left">
									<div class="settings-content">
										<div class="aioseo-radio-toggle circle">
											<div><input id="id_previewGeneralIsMobile_0" name="previewGeneralIsMobile"
													type="radio"><label for="id_previewGeneralIsMobile_0" class="dark"><svg
														width="20" height="18" viewBox="0 0 20 18" fill="none"
														xmlns="http://www.w3.org/2000/svg" class="aioseo-desktop">
														<path fill-rule="evenodd" clip-rule="evenodd"
															d="M2.50004 0.666504H17.5C18.4167 0.666504 19.1667 1.4165 19.1667 2.33317V12.3332C19.1667 13.2498 18.4167 13.9998 17.5 13.9998H11.6667V15.6665H13.3334V17.3332H6.66671V15.6665H8.33337V13.9998H2.50004C1.58337 13.9998 0.833374 13.2498 0.833374 12.3332V2.33317C0.833374 1.4165 1.58337 0.666504 2.50004 0.666504ZM2.50004 12.3332H17.5V2.33317H2.50004V12.3332Z"
															fill="currentColor"></path>
													</svg></label></div>
											<div><input id="id_previewGeneralIsMobile_1" name="previewGeneralIsMobile"
													type="radio"><label for="id_previewGeneralIsMobile_1" class=""><svg
														width="12" height="20" viewBox="0 0 12 20" fill="none"
														xmlns="http://www.w3.org/2000/svg" class="aioseo-mobile">
														<path fill-rule="evenodd" clip-rule="evenodd"
															d="M1.72767 0.833496L10.061 0.841829C10.9777 0.841829 11.7277 1.5835 11.7277 2.50016V17.5002C11.7277 18.4168 10.9777 19.1668 10.061 19.1668H1.72767C0.811003 19.1668 0.0693359 18.4168 0.0693359 17.5002V2.50016C0.0693359 1.5835 0.811003 0.833496 1.72767 0.833496ZM1.72763 15.8335H10.061V4.16683H1.72763V15.8335Z"
															fill="currentColor"></path>
													</svg>
												</label>
											</div>
										</div>
									</div>
								</div>
							</div>
							<div class="aioseo-settings-row snippet-preview-row aioseo-row ">
								<div class="aioseo-col col-xs-12 col-md-3 text-xs-left">
									<div class="settings-name">
										<div class="name"> Snippet Preview </div>
									</div>
								</div>
								<div class="aioseo-col col-xs-12 col-md-9 text-xs-left">
									<div class="settings-content">
										<div class="aioseo-google-search-preview">
											<div class="domain"> https://aioseo.com/category/uncategorized/ </div>
											<div class="site-title">Taxonomy Title | aioseo.com</div>
											<div class="meta-description">Sample taxonomy description</div>
										</div>
									</div>
								</div>
							</div>
							<div class="aioseo-settings-row snippet-title-row aioseo-row ">
								<div class="aioseo-col col-xs-12 col-md-3 text-xs-left">
									<div class="settings-name">
										<div class="name"> Category Title </div>
									</div>
								</div>
								<div class="aioseo-col col-xs-12 col-md-9 text-xs-left">
									<div class="settings-content">
										<div class="aioseo-html-tags-editor">

											<div>
												<div class="aioseo-description tags-description"> Click on the tags below to
													insert variables into your title. </div>
												<div class="add-tags">
													<div class="aioseo-add-template-tag"><svg viewBox="0 0 10 11"
															fill="none" xmlns="http://www.w3.org/2000/svg"
															class="aioseo-plus">
															<path
																d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																fill="currentColor"></path>
														</svg> Category Title </div>
													<div class="aioseo-add-template-tag"><svg viewBox="0 0 10 11"
															fill="none" xmlns="http://www.w3.org/2000/svg"
															class="aioseo-plus">
															<path
																d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																fill="currentColor"></path>
														</svg> Separator </div>
													<div class="aioseo-add-template-tag"><svg viewBox="0 0 10 11"
															fill="none" xmlns="http://www.w3.org/2000/svg"
															class="aioseo-plus">
															<path
																d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																fill="currentColor"></path>
														</svg> Site Title </div><a href="#" class="aioseo-view-all-tags">
														View all tags&nbsp;→ </a>
												</div>
											</div>
											<div class="aioseo-editor">
												<div class="ql-toolbar ql-snow"><span class="ql-formats"></span></div>
												<div class="aioseo-editor-single ql-container ql-snow">
													<div class="ql-editor" data-gramm="false" contenteditable="true"></div>
													<div class="ql-clipboard" contenteditable="true" tabindex="-1"></div>
													<div class="ql-tooltip ql-hidden"><a class="ql-preview"
															rel="noopener noreferrer" target="_blank"
															href="about:blank"></a><input type="text" data-formula="e=mc^2"
															data-link="https://quilljs.com" data-video="Embed URL"><a
															class="ql-action"></a><a class="ql-remove"></a></div>
													<div class="ql-mention-list-container"
														style="display: none; position: absolute;">
														<div class="aioseo-tag-custom">
															<div data-v-3f0a80a7="" class="aioseo-input">

																<input data-v-3f0a80a7="" type="text"
																	placeholder="Enter a custom field name..."
																	spellcheck="true" class="small">
															</div>
														</div>
														<div class="aioseo-tag-search">
															<div data-v-3f0a80a7="" class="aioseo-input">
																<div data-v-3f0a80a7="" class="prepend-icon medium"><svg
																		data-v-3f0a80a7="" viewBox="0 0 15 16"
																		xmlns="http://www.w3.org/2000/svg"
																		class="aioseo-search">
																		<path
																			d="M14.8828 14.6152L11.3379 11.0703C11.25 11.0117 11.1621 10.9531 11.0742 10.9531H10.6934C11.6016 9.89844 12.1875 8.49219 12.1875 6.96875C12.1875 3.62891 9.43359 0.875 6.09375 0.875C2.72461 0.875 0 3.62891 0 6.96875C0 10.3379 2.72461 13.0625 6.09375 13.0625C7.61719 13.0625 8.99414 12.5059 10.0781 11.5977V11.9785C10.0781 12.0664 10.1074 12.1543 10.166 12.2422L13.7109 15.7871C13.8574 15.9336 14.0918 15.9336 14.209 15.7871L14.8828 15.1133C15.0293 14.9961 15.0293 14.7617 14.8828 14.6152ZM6.09375 11.6562C3.48633 11.6562 1.40625 9.57617 1.40625 6.96875C1.40625 4.39062 3.48633 2.28125 6.09375 2.28125C8.67188 2.28125 10.7812 4.39062 10.7812 6.96875C10.7812 9.57617 8.67188 11.6562 6.09375 11.6562Z"
																			fill="currentColor"></path>
																	</svg></div>
																<input data-v-3f0a80a7="" type="text"
																	placeholder="Search for an item..." spellcheck="true"
																	class="medium prepend">
															</div>
														</div>
														<ul class="ql-mention-list"></ul>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Category Description</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Category Description </div>
															<div class="aioseo-tag-description"> Current or first category
																description. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Category Title</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Category Title </div>
															<div class="aioseo-tag-description"> Current or first category
																title. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Current Date</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Current Date </div>
															<div class="aioseo-tag-description"> The current date,
																localized. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Current Day</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Current Day </div>
															<div class="aioseo-tag-description"> The current day of the
																month, localized. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Current Month</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Current Month </div>
															<div class="aioseo-tag-description"> The current month,
																localized. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Current Year</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Current Year </div>
															<div class="aioseo-tag-description"> The current year,
																localized. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Custom Field</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Custom Field </div>
															<div class="aioseo-tag-description"> A custom field from the
																current page/post. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Permalink</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Permalink </div>
															<div class="aioseo-tag-description"> The permalink for the
																current page/post. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Separator</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Separator </div>
															<div class="aioseo-tag-description"> The separator defined in
																the search appearance settings. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Site Title</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Site Title </div>
															<div class="aioseo-tag-description"> Your site title. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Tagline</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Tagline </div>
															<div class="aioseo-tag-description"> The tagline for your site,
																set in the general settings. </div>
														</div>
													</div>
												</div>
												<div style="display: none;">
													<div data-v-3f0a80a7="" class="aioseo-input">
														<div data-v-3f0a80a7="" class="prepend-icon medium"><svg
																data-v-3f0a80a7="" viewBox="0 0 15 16"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-search">
																<path
																	d="M14.8828 14.6152L11.3379 11.0703C11.25 11.0117 11.1621 10.9531 11.0742 10.9531H10.6934C11.6016 9.89844 12.1875 8.49219 12.1875 6.96875C12.1875 3.62891 9.43359 0.875 6.09375 0.875C2.72461 0.875 0 3.62891 0 6.96875C0 10.3379 2.72461 13.0625 6.09375 13.0625C7.61719 13.0625 8.99414 12.5059 10.0781 11.5977V11.9785C10.0781 12.0664 10.1074 12.1543 10.166 12.2422L13.7109 15.7871C13.8574 15.9336 14.0918 15.9336 14.209 15.7871L14.8828 15.1133C15.0293 14.9961 15.0293 14.7617 14.8828 14.6152ZM6.09375 11.6562C3.48633 11.6562 1.40625 9.57617 1.40625 6.96875C1.40625 4.39062 3.48633 2.28125 6.09375 2.28125C8.67188 2.28125 10.7812 4.39062 10.7812 6.96875C10.7812 9.57617 8.67188 11.6562 6.09375 11.6562Z"
																	fill="currentColor"></path>
															</svg></div>
														<input data-v-3f0a80a7="" type="text"
															placeholder="Search for an item..." spellcheck="true"
															class="medium prepend">


													</div>
												</div>
												<div style="display: none;">
													<div data-v-3f0a80a7="" class="aioseo-input">

														<input data-v-3f0a80a7="" type="text"
															placeholder="Enter a custom field name..." spellcheck="true"
															class="small">


													</div>
												</div>
											</div>
										</div>
										<div class="max-recommended-count"><strong>32</strong> out of <strong>60</strong>
											max recommended characters.</div>
									</div>
								</div>
							</div>
							<div class="aioseo-settings-row snippet-description-row aioseo-row ">
								<div class="aioseo-col col-xs-12 col-md-3 text-xs-left">
									<div class="settings-name">
										<div class="name"> Meta Description </div>
										<!---->
									</div>
								</div>
								<div class="aioseo-col col-xs-12 col-md-9 text-xs-left">
									<div class="settings-content">
										<div class="aioseo-html-tags-editor">
											<!---->
											<div>
												<div class="aioseo-description tags-description"> Click on the tags below to
													insert variables into your meta description. </div>
												<div class="add-tags">
													<div class="aioseo-add-template-tag"><svg viewBox="0 0 10 11"
															fill="none" xmlns="http://www.w3.org/2000/svg"
															class="aioseo-plus">
															<path
																d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																fill="currentColor"></path>
														</svg> Category Title </div>
													<div class="aioseo-add-template-tag"><svg viewBox="0 0 10 11"
															fill="none" xmlns="http://www.w3.org/2000/svg"
															class="aioseo-plus">
															<path
																d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																fill="currentColor"></path>
														</svg> Separator </div>
													<div class="aioseo-add-template-tag"><svg viewBox="0 0 10 11"
															fill="none" xmlns="http://www.w3.org/2000/svg"
															class="aioseo-plus">
															<path
																d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																fill="currentColor"></path>
														</svg> Category Description </div><a href="#"
														class="aioseo-view-all-tags"> View all tags&nbsp;→ </a>
												</div>
											</div>
											<div class="aioseo-editor">
												<div class="ql-toolbar ql-snow"><span class="ql-formats"></span></div>
												<div class="aioseo-editor-description ql-container ql-snow">
													<div class="ql-editor" data-gramm="false" contenteditable="true"></div>
													<div class="ql-clipboard" contenteditable="true" tabindex="-1"></div>
													<div class="ql-tooltip ql-hidden"><a class="ql-preview"
															rel="noopener noreferrer" target="_blank"
															href="about:blank"></a><input type="text" data-formula="e=mc^2"
															data-link="https://quilljs.com" data-video="Embed URL"><a
															class="ql-action"></a><a class="ql-remove"></a></div>
													<div class="ql-mention-list-container"
														style="display: none; position: absolute;">
														<div class="aioseo-tag-custom">
															<div data-v-3f0a80a7="" class="aioseo-input">

																<input data-v-3f0a80a7="" type="text"
																	placeholder="Enter a custom field name..."
																	spellcheck="true" class="small">
															</div>
														</div>
														<div class="aioseo-tag-search">
															<div data-v-3f0a80a7="" class="aioseo-input">
																<div data-v-3f0a80a7="" class="prepend-icon medium"><svg
																		data-v-3f0a80a7="" viewBox="0 0 15 16"
																		xmlns="http://www.w3.org/2000/svg"
																		class="aioseo-search">
																		<path
																			d="M14.8828 14.6152L11.3379 11.0703C11.25 11.0117 11.1621 10.9531 11.0742 10.9531H10.6934C11.6016 9.89844 12.1875 8.49219 12.1875 6.96875C12.1875 3.62891 9.43359 0.875 6.09375 0.875C2.72461 0.875 0 3.62891 0 6.96875C0 10.3379 2.72461 13.0625 6.09375 13.0625C7.61719 13.0625 8.99414 12.5059 10.0781 11.5977V11.9785C10.0781 12.0664 10.1074 12.1543 10.166 12.2422L13.7109 15.7871C13.8574 15.9336 14.0918 15.9336 14.209 15.7871L14.8828 15.1133C15.0293 14.9961 15.0293 14.7617 14.8828 14.6152ZM6.09375 11.6562C3.48633 11.6562 1.40625 9.57617 1.40625 6.96875C1.40625 4.39062 3.48633 2.28125 6.09375 2.28125C8.67188 2.28125 10.7812 4.39062 10.7812 6.96875C10.7812 9.57617 8.67188 11.6562 6.09375 11.6562Z"
																			fill="currentColor"></path>
																	</svg></div>
																<input data-v-3f0a80a7="" type="text"
																	placeholder="Search for an item..." spellcheck="true"
																	class="medium prepend">
															</div>
														</div>
														<ul class="ql-mention-list"></ul>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Category Description</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Category Description </div>
															<div class="aioseo-tag-description"> Current or first category
																description. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Category Title</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Category Title </div>
															<div class="aioseo-tag-description"> Current or first category
																title. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Current Date</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Current Date </div>
															<div class="aioseo-tag-description"> The current date,
																localized. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Current Day</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Current Day </div>
															<div class="aioseo-tag-description"> The current day of the
																month, localized. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Current Month</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Current Month </div>
															<div class="aioseo-tag-description"> The current month,
																localized. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Current Year</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Current Year </div>
															<div class="aioseo-tag-description"> The current year,
																localized. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Custom Field</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Custom Field </div>
															<div class="aioseo-tag-description"> A custom field from the
																current page/post. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Permalink</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Permalink </div>
															<div class="aioseo-tag-description"> The permalink for the
																current page/post. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Separator</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Separator </div>
															<div class="aioseo-tag-description"> The separator defined in
																the search appearance settings. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Site Title</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Site Title </div>
															<div class="aioseo-tag-description"> Your site title. </div>
														</div>
													</div>
												</div>
												<div style="display: none;"><span class="aioseo-tag"><span
															class="tag-name">Tagline</span>
														<span class="tag-toggle"><svg viewBox="0 0 24 24" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-caret">
																<path
																	d="M16.59 8.29492L12 12.8749L7.41 8.29492L6 9.70492L12 15.7049L18 9.70492L16.59 8.29492Z"
																	fill="currentColor"></path>
															</svg></span>
													</span></div>
												<div style="display: none;">
													<div class="aioseo-tag-item">
														<div><svg viewBox="0 0 10 11" fill="none"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-plus">
																<path
																	d="M6 0.00115967H4V4.00116H0V6.00116H4V10.0012H6V6.00116H10V4.00116H6V0.00115967Z"
																	fill="currentColor"></path>
															</svg></div>
														<div>
															<div class="aioseo-tag-title"> Tagline </div>
															<div class="aioseo-tag-description"> The tagline for your site,
																set in the general settings. </div>
														</div>
													</div>
												</div>
												<div style="display: none;">
													<div data-v-3f0a80a7="" class="aioseo-input">
														<div data-v-3f0a80a7="" class="prepend-icon medium"><svg
																data-v-3f0a80a7="" viewBox="0 0 15 16"
																xmlns="http://www.w3.org/2000/svg" class="aioseo-search">
																<path
																	d="M14.8828 14.6152L11.3379 11.0703C11.25 11.0117 11.1621 10.9531 11.0742 10.9531H10.6934C11.6016 9.89844 12.1875 8.49219 12.1875 6.96875C12.1875 3.62891 9.43359 0.875 6.09375 0.875C2.72461 0.875 0 3.62891 0 6.96875C0 10.3379 2.72461 13.0625 6.09375 13.0625C7.61719 13.0625 8.99414 12.5059 10.0781 11.5977V11.9785C10.0781 12.0664 10.1074 12.1543 10.166 12.2422L13.7109 15.7871C13.8574 15.9336 14.0918 15.9336 14.209 15.7871L14.8828 15.1133C15.0293 14.9961 15.0293 14.7617 14.8828 14.6152ZM6.09375 11.6562C3.48633 11.6562 1.40625 9.57617 1.40625 6.96875C1.40625 4.39062 3.48633 2.28125 6.09375 2.28125C8.67188 2.28125 10.7812 4.39062 10.7812 6.96875C10.7812 9.57617 8.67188 11.6562 6.09375 11.6562Z"
																	fill="currentColor"></path>
															</svg></div>
														<input data-v-3f0a80a7="" type="text"
															placeholder="Search for an item..." spellcheck="true"
															class="medium prepend">
													</div>
												</div>
												<div style="display: none;">
													<div data-v-3f0a80a7="" class="aioseo-input">

														<input data-v-3f0a80a7="" type="text"
															placeholder="Enter a custom field name..." spellcheck="true"
															class="small">
													</div>
												</div>
											</div>
										</div>
										<div class="max-recommended-count"><strong>27</strong> out of <strong>160</strong>
											max recommended characters.</div>
									</div>
								</div>
							</div>
						</div>
					</div>

					<div class="aioseo-cta floating" style="max-width: 630px;">
						<div class="aioseo-cta-background">
							<div class="type-1">
								<div class="header-text"><?php esc_html_e( 'Custom Taxonomies are a PRO Feature', 'all-in-one-seo-pack' ); ?></div>
								<div class="description"><?php esc_html_e( 'Set custom SEO meta, social meta and more for individual terms.', 'all-in-one-seo-pack' ); ?></div>
								<div class="feature-list aioseo-row ">
									<div class="aioseo-col col-xs-12 col-md-6 text-xs-left">
										<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="aioseo-circle-check">
											<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM10 14.17L16.59 7.58L18 9L10 17L6 13L7.41 11.59L10 14.17Z" fill="currentColor"></path>
										</svg> SEO Title/Description
									</div>
									<div class="aioseo-col col-xs-12 col-md-6 text-xs-left">
										<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="aioseo-circle-check">
											<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM10 14.17L16.59 7.58L18 9L10 17L6 13L7.41 11.59L10 14.17Z" fill="currentColor"></path>
										</svg> Social Meta
									</div>
									<div class="aioseo-col col-xs-12 col-md-6 text-xs-left">
										<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="aioseo-circle-check">
											<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM10 14.17L16.59 7.58L18 9L10 17L6 13L7.41 11.59L10 14.17Z" fill="currentColor"></path>
										</svg> SEO Revisions
									</div>
									<div class="aioseo-col col-xs-12 col-md-6 text-xs-left">
										<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="aioseo-circle-check">
											<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM10 14.17L16.59 7.58L18 9L10 17L6 13L7.41 11.59L10 14.17Z" fill="currentColor"></path>
										</svg> Import/Export
									</div>
								</div>
								<div class="actions">
									<a type="" to="" class="aioseo-button green" href="<?php echo esc_attr( aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'lite-upgrade/', 'taxonomies-upsell', 'features=[]=taxonomies', false ) ); ?>" target="_blank"><?php esc_html_e( 'Unlock Custom Taxonomies', 'all-in-one-seo-pack' ); ?></a>
									<a href="https://aioseo.com/?utm_source=WordPress&amp;utm_campaign=liteplugin&amp;utm_medium=taxonomies-upsell&amp;features[]=taxonomies" target="_blank" class="learn-more"><?php esc_html_e( 'Learn more about all features', 'all-in-one-seo-pack' ); ?></a>
								</div>


								<div class="aioseo-alert yellow medium bonus-alert"> 🎁 <span>
									<strong><?php esc_html_e( 'Bonus:', 'all-in-one-seo-pack' ); ?></strong>
									<?php esc_html_e( 'You can upgrade to the Pro plan today and ', 'all-in-one-seo-pack' ); ?>
									<strong><?php esc_html_e( 'save 60% off', 'all-in-one-seo-pack' ); ?></strong>
									<?php esc_html_e( '(discount auto-applied)', 'all-in-one-seo-pack' ); ?>.</span>
								</div>
							</div>
						</div>
					</div>
				</div>
			</div>
		</div>
	</div>
</div>Common/EmailReports/Summary/Summary.php000066600000022740151135505570014225 0ustar00<?php

namespace AIOSEO\Plugin\Common\EmailReports\Summary;

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

/**
 * Summary class.
 *
 * @since 4.7.2
 */
class Summary {
	/**
	 * The action hook to execute when the event is run.
	 *
	 * @since 4.7.2
	 *
	 * @var string
	 */
	public $actionHook = 'aioseo_report_summary';

	/**
	 * Recipient for the email. Multiple recipients can be separated by a comma.
	 *
	 * @since 4.7.2
	 *
	 * @var string
	 */
	private $recipient;

	/**
	 * Email chosen frequency. Can be either 'weekly' or 'monthly'.
	 *
	 * @since 4.7.2
	 *
	 * @var string
	 */
	private $frequency;

	/**
	 * Class constructor.
	 *
	 * @since 4.7.2
	 */
	public function __construct() {
		// No need to run any of this during a WP AJAX request.
		if ( wp_doing_ajax() ) {
			return;
		}

		// No need to keep trying scheduling unless on admin.
		add_action( 'admin_init', [ $this, 'maybeSchedule' ], 20 );

		add_action( $this->actionHook, [ $this, 'cronTrigger' ] );
	}

	/**
	 * The summary cron callback.
	 * Hooked into `{@see self::$actionHook}` action hook.
	 *
	 * @since 4.7.2
	 *
	 * @param  string $frequency The frequency of the email.
	 * @return void
	 */
	public function cronTrigger( $frequency ) {
		// Keep going only if the feature is enabled.
		if (
			! aioseo()->options->advanced->emailSummary->enable ||
			! apply_filters( 'aioseo_report_summary_enable', true, $frequency )
		) {
			return;
		}

		// Get all recipients for the given frequency.
		$recipients = wp_list_filter( aioseo()->options->advanced->emailSummary->recipients, [ 'frequency' => $frequency ] );
		if ( ! $recipients ) {
			return;
		}

		try {
			// Get only the email addresses.
			$recipients = array_column( $recipients, 'email' );

			$this->run( [
				'recipient' => implode( ',', $recipients ),
				'frequency' => $frequency,
			] );
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Trigger the sending of the summary.
	 *
	 * @since 4.7.2
	 *
	 * @param  array      $data All the initial data needed for the summary to be sent.
	 * @throws \Exception       If the email could not be sent.
	 * @return void
	 */
	public function run( $data ) {
		try {
			$this->recipient = $data['recipient'] ?? '';
			$this->frequency = $data['frequency'] ?? '';

			aioseo()->emailReports->mail->send( $this->getRecipient(), $this->getSubject(), $this->getContentHtml(), $this->getHeaders() );
		} catch ( \Exception $e ) {
			throw new \Exception( esc_html( $e->getMessage() ), esc_html( $e->getCode() ) );
		}
	}

	/**
	 * Maybe (re)schedule the summary.
	 *
	 * @since 4.7.2
	 *
	 * @return void
	 */
	public function maybeSchedule() {
		$allowedFrequencies = $this->getAllowedFrequencies();

		// Add at least 6 hours after the day starts.
		$addToStart = HOUR_IN_SECONDS * 6;
		// Add the timezone offset.
		$addToStart -= aioseo()->helpers->getTimeZoneOffset();
		// Add a random time offset to avoid all emails being sent at the same time. 1440 * 3 = 3 days range.
		$addToStart += aioseo()->helpers->generateRandomTimeOffset( aioseo()->helpers->getSiteDomain( true ), 1440 * 3 ) * MINUTE_IN_SECONDS;

		foreach ( $allowedFrequencies as $frequency => $data ) {
			aioseo()->actionScheduler->scheduleRecurrent( $this->actionHook, $data['start'] + $addToStart, $data['interval'], compact( 'frequency' ) );
		}
	}

	/**
	 * Get one or more valid recipients.
	 *
	 * @since 4.7.2
	 *
	 * @throws \Exception If no valid recipient was set for the email.
	 * @return string     The valid recipients.
	 */
	private function getRecipient() {
		$recipients = array_map( 'trim', explode( ',', $this->recipient ) );
		$recipients = array_filter( $recipients, 'is_email' );

		if ( empty( $recipients ) ) {
			throw new \Exception( 'No valid recipient was set for the email.' ); // Not shown to the user.
		}

		return implode( ',', $recipients );
	}

	/**
	 * Get email subject.
	 *
	 * @since 4.7.2
	 *
	 * @return string The email subject.
	 */
	private function getSubject() {
		// Translators: 1 - Date range.
		$out = esc_html__( 'Your SEO Performance Report for %1$s', 'all-in-one-seo-pack' );

		$dateRange = $this->getDateRange();
		$suffix    = date_i18n( 'F', $dateRange['endDateRaw'] );
		if ( 'weekly' === $this->frequency ) {
			$suffix = $dateRange['range'];
		}

		return sprintf( $out, $suffix );
	}

	/**
	 * Get content html.
	 *
	 * @since 4.7.2
	 *
	 * @return string The email content.
	 */
	private function getContentHtml() { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$dateRange        = $this->getDateRange();
		$content          = new Content( $dateRange );
		$upsell           = [
			'search-statistics' => []
		];
		$preHeader        = sprintf(
			// Translators: 1 - The plugin short name ("AIOSEO").
			esc_html__( 'Dive into your top-performing pages with %1$s and uncover growth opportunities.', 'all-in-one-seo-pack' ),
			AIOSEO_PLUGIN_SHORT_NAME
		);
		$iconCalendar     = 'weekly' === $this->frequency
			? 'icon-calendar-weekly'
			: 'icon-calendar-monthly';
		$heading          = 'weekly' === $this->frequency
			? esc_html__( 'Your Weekly SEO Email Summary', 'all-in-one-seo-pack' )
			: esc_html__( 'Your Monthly SEO Email Summary', 'all-in-one-seo-pack' );
		$subheading       = 'weekly' === $this->frequency
			? esc_html__( 'Let\'s take a look at your SEO updates and content progress this week.', 'all-in-one-seo-pack' )
			: esc_html__( 'Let\'s take a look at your SEO updates and content progress this month.', 'all-in-one-seo-pack' );
		$statisticsReport = [
			'posts'      => [],
			'keywords'   => [],
			'milestones' => [],
			'cta'        => [
				'text' => esc_html__( 'See All SEO Statistics', 'all-in-one-seo-pack' ),
				'url'  => $content->searchStatisticsUrl
			],
		];

		if ( ! $content->allowSearchStatistics() ) {
			$upsell['search-statistics'] = [
				'cta' => [
					'text' => esc_html__( 'Unlock Search Statistics', 'all-in-one-seo-pack' ),
					'url'  => $content->searchStatisticsUrl,
				],
			];
		}

		if ( ! $upsell['search-statistics'] ) {
			$subheading = 'weekly' === $this->frequency
				? esc_html__( 'Let\'s take a look at how your site has performed in search results this week.', 'all-in-one-seo-pack' )
				: esc_html__( 'Let\'s take a look at how your site has performed in search results this month.', 'all-in-one-seo-pack' );

			$statisticsReport['posts']      = $content->getPostsStatistics();
			$statisticsReport['keywords']   = $content->getKeywords();
			$statisticsReport['milestones'] = $content->getMilestones();
		}

		$mktUrl = trailingslashit( AIOSEO_MARKETING_URL );
		$medium = 'email-report-summary';

		$posts     = $content->getAioPosts();
		$resources = [
			'posts' => array_map( function ( $item ) use ( $medium, $content ) {
				return array_merge( $item, [
					'url'   => aioseo()->helpers->utmUrl( $item['url'], $medium ),
					'image' => [
						'url' => ! empty( $item['image']['sizes']['medium']['source_url'] )
							? $item['image']['sizes']['medium']['source_url']
							: $content->featuredImagePlaceholder
					]
				] );
			}, $content->getResources() ),
			'cta'   => [
				'text' => esc_html__( 'See All Resources', 'all-in-one-seo-pack' ),
				'url'  => aioseo()->helpers->utmUrl( 'https://aioseo.com/blog/', $medium ),
			],
		];
		$links     = [
			'disable'        => admin_url( 'admin.php?page=aioseo-settings&aioseo-scroll=aioseo-email-summary-row&aioseo-highlight=aioseo-email-summary-row&aioseo-tab=advanced' ),
			'update'         => admin_url( 'update-core.php' ),
			'marketing-site' => aioseo()->helpers->utmUrl( $mktUrl, $medium ),
			'facebook'       => aioseo()->helpers->utmUrl( $mktUrl . 'plugin/facebook', $medium ),
			'linkedin'       => aioseo()->helpers->utmUrl( $mktUrl . 'plugin/linkedin', $medium ),
			'youtube'        => aioseo()->helpers->utmUrl( $mktUrl . 'plugin/youtube', $medium ),
			'twitter'        => aioseo()->helpers->utmUrl( $mktUrl . 'plugin/twitter', $medium ),
		];

		ob_start();
		require AIOSEO_DIR . '/app/Common/Views/report/summary.php';

		return ob_get_clean();
	}

	/**
	 * Get email headers.
	 *
	 * @since 4.7.2
	 *
	 * @return array The email headers.
	 */
	private function getHeaders() {
		return [ 'Content-Type: text/html; charset=UTF-8' ];
	}

	/**
	 * Get all allowed frequencies.
	 *
	 * @since 4.7.2
	 *
	 * @return array The email allowed frequencies.
	 */
	private function getAllowedFrequencies() {
		$time           = time();
		$secondsTillNow = $time - strtotime( 'today' );

		return [
			'weekly'  => [
				'interval' => WEEK_IN_SECONDS,
				'start'    => strtotime( 'next Monday' ) - $time
			],
			'monthly' => [
				'interval' => MONTH_IN_SECONDS,
				'start'    => ( strtotime( 'first day of next month' ) + ( DAY_IN_SECONDS * 2 ) - $secondsTillNow ) - $time
			]
		];
	}

	/**
	 * Retrieves the date range data based on the frequency.
	 *
	 * @since 4.7.3
	 *
	 * @return array The date range data.
	 */
	private function getDateRange() {
		$dateFormat = get_option( 'date_format' );

		// If frequency is 'monthly'.
		$endDateRaw   = strtotime( 'last day of last month' );
		$startDateRaw = strtotime( 'first day of last month' );

		// If frequency is 'weekly'.
		if ( 'weekly' === $this->frequency ) {
			$endDateRaw   = strtotime( 'last Saturday' );
			$startDateRaw = strtotime( 'last Sunday', $endDateRaw );
		}

		$endDate   = date_i18n( $dateFormat, $endDateRaw );
		$startDate = date_i18n( $dateFormat, $startDateRaw );

		return [
			'endDate'      => $endDate,
			'endDateRaw'   => $endDateRaw,
			'startDate'    => $startDate,
			'startDateRaw' => $startDateRaw,
			'range'        => "$startDate - $endDate",
		];
	}
}Common/EmailReports/Summary/Content.php000066600000050371151135505570014203 0ustar00<?php

namespace AIOSEO\Plugin\Common\EmailReports\Summary;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Summary content class.
 *
 * @since 4.7.2
 */
class Content {
	/**
	 * The date range data.
	 *
	 * @since 4.7.2
	 *
	 * @var array
	 */
	public $dateRange;

	/**
	 * The SEO Statistics data.
	 *
	 * @since 4.7.2
	 *
	 * @var array
	 */
	private $seoStatistics = [];

	/**
	 * The Keywords data.
	 *
	 * @since 4.7.2
	 *
	 * @var array
	 */
	private $keywords = [];

	/**
	 * The Search Statistics page URL.
	 *
	 * @since 4.7.2
	 *
	 * @var string
	 */
	public $searchStatisticsUrl;

	/**
	 * The featured image placeholder URL.
	 *
	 * @since 4.7.3
	 *
	 * @var string
	 */
	public $featuredImagePlaceholder = 'https://static.aioseo.io/report/ste/featured-image-placeholder.png';

	/**
	 * Class constructor.
	 *
	 * @since 4.7.2
	 *
	 * @param  array $dateRange The date range data.
	 * @return void
	 */
	public function __construct( $dateRange ) {
		$this->dateRange           = $dateRange;
		$this->searchStatisticsUrl = admin_url( 'admin.php?page=aioseo-search-statistics' );

		$this->setSeoStatistics();
		$this->setKeywords();
	}

	/**
	 * Sets the SEO Statistics data.
	 *
	 * @since 4.7.2
	 *
	 * @return void
	 */
	private function setSeoStatistics() {
		try {
			$seoStatistics = aioseo()->searchStatistics->getSeoStatisticsData( [
				'startDate' => gmdate( 'Y-m-d', $this->dateRange['startDateRaw'] ),
				'endDate'   => gmdate( 'Y-m-d', $this->dateRange['endDateRaw'] ),
				'orderBy'   => 'clicks',
				'orderDir'  => 'desc',
				'limit'     => '5',
				'offset'    => '0',
				'filter'    => 'all',
			] );

			if ( empty( $seoStatistics['data'] ) ) {
				return;
			}

			$this->seoStatistics = $seoStatistics['data'];
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Sets the Keywords data.
	 *
	 * @since 4.7.2
	 *
	 * @return void
	 */
	private function setKeywords() {
		try {
			$keywords = aioseo()->searchStatistics->getKeywordsData( [
				'startDate' => gmdate( 'Y-m-d', $this->dateRange['startDateRaw'] ),
				'endDate'   => gmdate( 'Y-m-d', $this->dateRange['endDateRaw'] ),
				'orderBy'   => 'clicks',
				'orderDir'  => 'desc',
				'limit'     => '5',
				'offset'    => '0',
				'filter'    => 'all',
			] );

			if ( empty( $keywords['data'] ) ) {
				return;
			}

			$this->keywords = $keywords['data'];
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Retrieves the content performance data.
	 *
	 * @since 4.7.2
	 *
	 * @return array The content performance data or an empty array.
	 */
	public function getPostsStatistics() {
		if ( ! $this->seoStatistics ) {
			return [];
		}

		$result = [
			'winning' => [
				'url'   => add_query_arg( [
					'aioseo-scroll' => 'aioseo-search-statistics-post-table',
					'aioseo-tab'    => 'seo-statistics',
					'table-filter'  => 'TopWinningPages'
				], $this->searchStatisticsUrl ),
				'items' => []
			],
			'losing'  => [
				'url'   => add_query_arg( [
					'aioseo-scroll' => 'aioseo-search-statistics-post-table',
					'aioseo-tab'    => 'seo-statistics',
					'table-filter'  => 'TopLosingPages'
				], $this->searchStatisticsUrl ),
				'items' => []
			]
		];

		foreach ( array_slice( $this->seoStatistics['pages']['topWinning']['rows'], 0, 3 ) as $row ) {
			$postId                       = $row['objectId'] ?? 0;
			$result['winning']['items'][] = [
				'title'      => $row['objectTitle'],
				'url'        => get_permalink( $postId ),
				'tru_seo'    => aioseo()->helpers->isTruSeoEligible( $postId ) ? $this->parseSeoScore( $row['seoScore'] ?? 0 ) : [],
				'clicks'     => $this->parseClicks( $row['clicks'] ),
				'difference' => [
					'clicks' => $this->parseDifference( $row['difference']['clicks'] ?? '' ),
				]
			];
		}

		$result['winning']['show_tru_seo'] = ! empty( array_filter( array_column( $result['winning']['items'], 'tru_seo' ) ) );

		foreach ( array_slice( $this->seoStatistics['pages']['topLosing']['rows'], 0, 3 ) as $row ) {
			$postId                      = $row['objectId'] ?? 0;
			$result['losing']['items'][] = [
				'title'      => $row['objectTitle'],
				'url'        => get_permalink( $postId ),
				'tru_seo'    => aioseo()->helpers->isTruSeoEligible( $postId ) ? $this->parseSeoScore( $row['seoScore'] ?? 0 ) : [],
				'clicks'     => $this->parseClicks( $row['clicks'] ),
				'difference' => [
					'clicks' => $this->parseDifference( $row['difference']['clicks'] ?? '' ),
				]
			];
		}

		$result['losing']['show_tru_seo'] = ! empty( array_filter( array_column( $result['losing']['items'], 'tru_seo' ) ) );

		return $result;
	}

	/**
	 * Retrieves the milestones data.
	 *
	 * @since 4.7.2
	 *
	 * @return array The milestones data or an empty array.
	 */
	public function getMilestones() { // phpcs:ignore Generic.Files.LineLength.MaxExceeded
		$milestones = [];
		if ( ! $this->seoStatistics ) {
			return $milestones;
		}

		$currentData = [
			'impressions' => $this->seoStatistics['statistics']['impressions'] ?? null,
			'clicks'      => $this->seoStatistics['statistics']['clicks'] ?? null,
			'ctr'         => $this->seoStatistics['statistics']['ctr'] ?? null,
			'keywords'    => $this->seoStatistics['statistics']['keywords'] ?? null,
		];
		$difference  = [
			'impressions' => $this->seoStatistics['statistics']['difference']['impressions'] ?? null,
			'clicks'      => $this->seoStatistics['statistics']['difference']['clicks'] ?? null,
			'ctr'         => $this->seoStatistics['statistics']['difference']['ctr'] ?? null,
			'keywords'    => $this->seoStatistics['statistics']['difference']['keywords'] ?? null,
		];

		if ( is_numeric( $currentData['impressions'] ) && is_numeric( $difference['impressions'] ) ) {
			$intDifference = intval( $difference['impressions'] );
			$message       = esc_html__( 'Your site has received the same number of impressions compared to the previous period.', 'all-in-one-seo-pack' );

			if ( $intDifference > 0 ) {
				// Translators: 1 - The number of impressions, 2 - The percentage increase.
				$message = esc_html__( 'Your site has received %1$s more impressions compared to the previous period, which is a %2$s increase.', 'all-in-one-seo-pack' );
			}

			if ( $intDifference < 0 ) {
				// Translators: 1 - The number of impressions, 2 - The percentage increase.
				$message = esc_html__( 'Your site has received %1$s fewer impressions compared to the previous period, which is a %2$s decrease.', 'all-in-one-seo-pack' );
			}

			if ( false !== strpos( $message, '%1' ) ) {
				$percentageDiff = 0 === absint( $currentData['impressions'] )
					? 100
					: round( ( absint( $intDifference ) / absint( $currentData['impressions'] ) ) * 100, 2 );
				$percentageDiff = false !== strpos( $percentageDiff, '.' )
					? number_format_i18n( $percentageDiff, count( explode( '.', $percentageDiff ) ) )
					: $percentageDiff;
				$message        = sprintf(
					$message,
					'<strong>' . aioseo()->helpers->compactNumber( absint( $intDifference ) ) . '</strong>',
					'<strong>' . $percentageDiff . '%</strong>'
				);
			}

			$milestones[] = [
				'message'    => $message,
				'background' => '#f0f6ff',
				'color'      => '#004F9D',
				'icon'       => 'icon-milestone-impressions'
			];
		}

		if ( is_numeric( $currentData['clicks'] ) && is_numeric( $difference['clicks'] ) ) {
			$intDifference = intval( $difference['clicks'] );
			$message       = esc_html__( 'Your site has received the same number of clicks compared to the previous period.', 'all-in-one-seo-pack' );

			if ( $intDifference > 0 ) {
				// Translators: 1 - The number of clicks, 2 - The percentage increase.
				$message = esc_html__( 'Your site has received %1$s more clicks compared to the previous period, which is a %2$s increase.', 'all-in-one-seo-pack' );
			}

			if ( $intDifference < 0 ) {
				// Translators: 1 - The number of clicks, 2 - The percentage increase.
				$message = esc_html__( 'Your site has received %1$s fewer clicks compared to the previous period, which is a %2$s decrease.', 'all-in-one-seo-pack' );
			}

			if ( false !== strpos( $message, '%1' ) ) {
				$percentageDiff = 0 === absint( $currentData['clicks'] )
					? 100
					: round( ( absint( $intDifference ) / absint( $currentData['clicks'] ) ) * 100, 2 );
				$percentageDiff = false !== strpos( $percentageDiff, '.' )
					? number_format_i18n( $percentageDiff, count( explode( '.', $percentageDiff ) ) )
					: $percentageDiff;
				$message        = sprintf(
					$message,
					'<strong>' . aioseo()->helpers->compactNumber( absint( $intDifference ) ) . '</strong>',
					'<strong>' . $percentageDiff . '%</strong>'
				);
			}

			$milestones[] = [
				'message'    => $message,
				'background' => '#ecfdf5',
				'color'      => '#077647',
				'icon'       => 'icon-milestone-clicks'
			];
		}

		if ( is_numeric( $currentData['ctr'] ) && is_numeric( $difference['ctr'] ) ) {
			$intDifference = floatval( $difference['ctr'] );
			$message       = esc_html__( 'Your site has the same CTR compared to the previous period.', 'all-in-one-seo-pack' );

			if ( $intDifference > 0 ) {
				// Translators: 1 - The CTR.
				$message = esc_html__( 'Your site has a %1$s higher CTR compared to the previous period.', 'all-in-one-seo-pack' );
			}

			if ( $intDifference < 0 ) {
				// Translators: 1 - The CTR.
				$message = esc_html__( 'Your site has a %1$s lower CTR compared to the previous period.', 'all-in-one-seo-pack' );
			}

			if ( false !== strpos( $message, '%1' ) ) {
				$message = sprintf(
					$message,
					'<strong>' . number_format_i18n( abs( $intDifference ), count( explode( '.', $intDifference ) ) ) . '%</strong>'
				);
			}

			$milestones[] = [
				'message'    => $message,
				'background' => '#fffbeb',
				'color'      => '#be6903',
				'icon'       => 'icon-milestone-ctr'
			];
		}

		if ( is_numeric( $currentData['keywords'] ) && is_numeric( $difference['keywords'] ) ) {
			$intDifference = intval( $difference['keywords'] );
			$message       = esc_html__( 'Your site ranked for the same number of keywords compared to the previous period.', 'all-in-one-seo-pack' );

			if ( $intDifference > 0 ) {
				// Translators: 1 - The number of keywords, 2 - The percentage increase.
				$message = esc_html__( 'Your site ranked for %1$s more keywords compared to the previous period, which is a %2$s increase.', 'all-in-one-seo-pack' );
			}

			if ( $intDifference < 0 ) {
				// Translators: 1 - The number of keywords, 2 - The percentage increase.
				$message = esc_html__( 'Your site ranked for %1$s fewer keywords compared to the previous period, which is a %2$s decrease.', 'all-in-one-seo-pack' );
			}

			if ( false !== strpos( $message, '%1' ) ) {
				$percentageDiff = 0 === absint( $currentData['keywords'] )
					? 100
					: round( ( absint( $intDifference ) / absint( $currentData['keywords'] ) ) * 100, 2 );
				$percentageDiff = false !== strpos( $percentageDiff, '.' )
					? number_format_i18n( $percentageDiff, count( explode( '.', $percentageDiff ) ) )
					: $percentageDiff;
				$message        = sprintf(
					$message,
					'<strong>' . aioseo()->helpers->compactNumber( absint( $intDifference ) ) . '</strong>',
					'<strong>' . $percentageDiff . '%</strong>'
				);
			}

			$milestones[] = [
				'message'    => $message,
				'background' => '#fef2f2',
				'color'      => '#ab2039',
				'icon'       => 'icon-milestone-keywords'
			];
		}

		return $milestones;
	}

	/**
	 * Retrieves the keyword performance data.
	 *
	 * @since 4.7.2
	 *
	 * @return array The keyword performance data or an empty array.
	 */
	public function getKeywords() {
		if ( ! $this->keywords ) {
			return [];
		}

		$result = [
			'winning' => [
				'url'   => add_query_arg( [
					'aioseo-scroll' => 'aioseo-search-statistics-keywords-table',
					'aioseo-tab'    => 'keyword-rank-tracker',
					'tab'           => 'AllKeywords',
					'table-filter'  => 'TopWinningKeywords'
				], $this->searchStatisticsUrl ),
				'items' => []
			],
			'losing'  => [
				'url'   => add_query_arg( [
					'aioseo-scroll' => 'aioseo-search-statistics-keywords-table',
					'aioseo-tab'    => 'keyword-rank-tracker',
					'tab'           => 'AllKeywords',
					'table-filter'  => 'TopLosingKeywords'
				], $this->searchStatisticsUrl ),
				'items' => []
			]
		];

		foreach ( array_slice( $this->keywords['topWinning'], 0, 3 ) as $row ) {
			$result['winning']['items'][] = [
				'title'      => $row['keyword'],
				'clicks'     => $this->parseClicks( $row['clicks'] ),
				'difference' => [
					'clicks' => $this->parseDifference( $row['difference']['clicks'] ?? '' ),
				]
			];
		}

		foreach ( array_slice( $this->keywords['topLosing'], 0, 3 ) as $row ) {
			$result['losing']['items'][] = [
				'title'      => $row['keyword'],
				'clicks'     => $this->parseClicks( $row['clicks'] ),
				'difference' => [
					'clicks' => $this->parseDifference( $row['difference']['clicks'] ?? '' ),
				]
			];
		}

		return $result;
	}

	/**
	 * Retrieves the posts data.
	 *
	 * @since 4.7.2
	 *
	 * @return array The posts' data.
	 */
	public function getAioPosts() {
		$result = [
			'publish'  => [],
			'optimize' => [],
			'cta'      => [
				'text' => esc_html__( 'Create New Post', 'all-in-one-seo-pack' ),
				'url'  => admin_url( 'post-new.php' )
			],
		];

		// 1. Retrieve the published posts.
		$publishPosts = aioseo()->core->db
			->start( 'posts as wp' )
			->select( 'wp.ID, wp.post_title, aio.seo_score' )
			->join( 'aioseo_posts as aio', 'aio.post_id = wp.ID', 'INNER' )
			->whereIn( 'wp.post_type', [ 'post' ] )
			->whereIn( 'wp.post_status', [ 'publish' ] )
			->orderBy( 'wp.post_date DESC' )
			->limit( 5 )
			->run()
			->result();

		if ( $publishPosts ) {
			$items             = $this->parsePosts( $publishPosts );
			$result['publish'] = [
				'url'          => admin_url( 'edit.php?post_status=publish&post_type=post' ),
				'items'        => $items,
				'show_stats'   => ! empty( array_filter( array_column( $items, 'stats' ) ) ),
				'show_tru_seo' => ! empty( array_filter( array_column( $items, 'tru_seo' ) ) ),
			];
		}

		// 2. Retrieve the posts to optimize.
		$optimizePosts = aioseo()->searchStatistics->getContentRankingsData( [
			'endDate'  => gmdate( 'Y-m-d', $this->dateRange['endDateRaw'] ),
			'orderBy'  => 'decayPercent',
			'orderDir' => 'asc',
			'limit'    => '3',
			'offset'   => '0',
			'filter'   => 'all',
		] );

		if ( is_array( $optimizePosts['data']['paginated']['rows'] ?? '' ) ) {
			$items = [];
			foreach ( array_slice( $optimizePosts['data']['paginated']['rows'], 0, 3 ) as $i => $row ) {
				$postId      = $row['objectId'] ?? 0;
				$items[ $i ] = [
					'title'         => $row['objectTitle'],
					'url'           => get_permalink( $postId ),
					'image_url'     => $this->getThumbnailUrl( $postId ),
					'tru_seo'       => aioseo()->helpers->isTruSeoEligible( $postId ) ? $this->parseSeoScore( $row['seoScore'] ?? 0 ) : [],
					'decay_percent' => $this->parseDifference( $row['decayPercent'] ?? '', true ),
					'issues'        => [
						'url'   => add_query_arg( [
							'aioseo-tab' => 'post-detail',
							'post'       => $postId
						], $this->searchStatisticsUrl ),
						'items' => []
					]
				];

				$aioPost = Models\Post::getPost( $postId );
				if ( $aioPost ) {
					$items[ $i ]['issues']['items'] = aioseo()->searchStatistics->helpers->getSuggestedChanges( $aioPost );
				}
			}

			$result['optimize'] = [
				'url'          => add_query_arg( [
					'aioseo-tab' => 'content-rankings',
				], $this->searchStatisticsUrl ),
				'items'        => $items,
				'show_tru_seo' => ! empty( array_filter( array_column( $items, 'tru_seo' ) ) ),
			];
		}

		return $result;
	}

	/**
	 * Retrieves the resources data.
	 *
	 * @since 4.7.2
	 *
	 * @return array The resources' data.
	 */
	public function getResources() {
		$items = aioseo()->helpers->fetchAioseoArticles( true );

		return array_slice( array_filter( $items ), 0, 3 );
	}

	/**
	 * Returns if Search Statistics content is allowed.
	 *
	 * @since 4.7.3
	 *
	 * @return bool Whether Search Statistics content is allowed.
	 */
	public function allowSearchStatistics() {
		static $return = null;
		if ( isset( $return ) ) {
			return $return;
		}

		$return = aioseo()->searchStatistics->api->auth->isConnected() &&
					aioseo()->license &&
					aioseo()->license->hasCoreFeature( 'search-statistics', 'seo-statistics' ) &&
					aioseo()->license->hasCoreFeature( 'search-statistics', 'keyword-rankings' );

		return $return;
	}

	/**
	 * Parses the SEO score.
	 *
	 * @since 4.7.2
	 *
	 * @param  int|string $score The SEO score.
	 * @return array             The parsed SEO score.
	 */
	private function parseSeoScore( $score ) {
		$score  = intval( $score );
		$parsed = [
			'value' => $score,
			'color' => '#a1a1a1',
			'text'  => $score ? "$score/100" : esc_html__( 'N/A', 'all-in-one-seo-pack' ),
		];

		if ( $parsed['value'] > 79 ) {
			$parsed['color'] = '#00aa63';
		} elseif ( $parsed['value'] > 49 ) {
			$parsed['color'] = '#ff8c00';
		} elseif ( $parsed['value'] > 0 ) {
			$parsed['color'] = '#df2a4a';
		}

		return $parsed;
	}

	/**
	 * Parses a difference.
	 *
	 * @since 4.7.2
	 *
	 * @param  int|string $number     The number to parse.
	 * @param  bool       $percentage Whether to return the text result as a percentage.
	 * @return array                  The parsed result.
	 */
	private function parseDifference( $number, $percentage = false ) {
		$parsed = [
			'color' => '#a1a1a1',
			'text'  => esc_html__( 'N/A', 'all-in-one-seo-pack' ),
		];
		if ( ! is_numeric( $number ) ) {
			return $parsed;
		}

		$number         = intval( $number );
		$parsed['text'] = aioseo()->helpers->compactNumber( absint( $number ) );

		if ( $percentage ) {
			$parsed['text'] = $number . '%';
		}

		if ( $number > 0 ) {
			$parsed['color'] = '#00aa63';
		} elseif ( $number < 0 ) {
			$parsed['color'] = '#df2a4a';
		}

		return $parsed;
	}

	/**
	 * Parses the clicks number.
	 *
	 * @since 4.7.2
	 *
	 * @param  float|int|string $number The number of clicks.
	 * @return string                   The parsed number of clicks.
	 */
	private function parseClicks( $number ) {
		return aioseo()->helpers->compactNumber( $number );
	}

	/**
	 * Parses the posts data.
	 *
	 * @since 4.7.2
	 *
	 * @param  array $posts The posts.
	 * @return array        The parsed posts' data.
	 */
	private function parsePosts( $posts ) {
		$parsed = [];
		foreach ( $posts as $k => $item ) {
			$parsed[ $k ] = [
				'title'     => aioseo()->helpers->truncate( $item->post_title, 75 ),
				'url'       => get_permalink( $item->ID ),
				'image_url' => $this->getThumbnailUrl( $item->ID ),
				'tru_seo'   => aioseo()->helpers->isTruSeoEligible( $item->ID ) ? $this->parseSeoScore( $item->seo_score ?? 0 ) : [],
				'stats'     => []
			];

			try {
				$statistics = [];
				if (
					$this->allowSearchStatistics() &&
					method_exists( aioseo()->searchStatistics, 'getPostDetailSeoStatisticsData' )
				) {
					$statistics = aioseo()->searchStatistics->getPostDetailSeoStatisticsData( [
						'startDate' => gmdate( 'Y-m-d', $this->dateRange['startDateRaw'] ),
						'endDate'   => gmdate( 'Y-m-d', $this->dateRange['endDateRaw'] ),
						'postId'    => $item->ID,
					], false );
				}

				if ( isset( $statistics['data']['statistics']['position'] ) ) {
					$parsed[ $k ]['stats'][] = [
						'icon'  => 'position',
						'label' => esc_html__( 'Position', 'all-in-one-seo-pack' ),
						'value' => round( floatval( $statistics['data']['statistics']['position'] ) ),
					];
				}

				if ( isset( $statistics['data']['statistics']['ctr'] ) ) {
					$value                   = round( floatval( $statistics['data']['statistics']['ctr'] ), 2 );
					$parsed[ $k ]['stats'][] = [
						'icon'  => 'ctr',
						'label' => 'CTR',
						'value' => ( number_format_i18n( $value, count( explode( '.', $value ) ) ) ) . '%',
					];
				}

				if ( isset( $statistics['data']['statistics']['impressions'] ) ) {
					$parsed[ $k ]['stats'][] = [
						'icon'  => 'impressions',
						'label' => esc_html__( 'Impressions', 'all-in-one-seo-pack' ),
						'value' => aioseo()->helpers->compactNumber( $statistics['data']['statistics']['impressions'] ),
					];
				}
			} catch ( \Exception $e ) {
				// Do nothing.
			}
		}

		return $parsed;
	}

	/**
	 * Retrieves the thumbnail URL.
	 *
	 * @since 4.7.2
	 *
	 * @param  int    $postId The post ID.
	 * @return string         The post featured image URL (thumbnail size).
	 */
	private function getThumbnailUrl( $postId ) {
		$imageUrl = get_the_post_thumbnail_url( $postId );

		return $imageUrl ?: $this->featuredImagePlaceholder;
	}
}Common/EmailReports/EmailReports.php000066600000004222151135505570013534 0ustar00<?php

namespace AIOSEO\Plugin\Common\EmailReports;

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

use AIOSEO\Plugin\Common\Models;

/**
 * EmailReports class.
 *
 * @since 4.7.2
 */
class EmailReports {
	/**
	 * Mail object.
	 *
	 * @since 4.7.2
	 *
	 * @var Mail
	 */
	public $mail = null;

	/**
	 * Summary object.
	 *
	 * @since 4.7.2
	 *
	 * @var Summary\Summary
	 */
	public $summary;

	/**
	 * Class constructor.
	 *
	 * @since 4.7.2
	 */
	public function __construct() {
		$this->mail    = new Mail();
		$this->summary = new Summary\Summary();

		add_action( 'aioseo_email_reports_enable_reminder', [ $this, 'enableReminder' ] );
	}

	/**
	 * Enable reminder.
	 *
	 * @since 4.7.7
	 *
	 * @return void
	 */
	public function enableReminder() {
		// User already enabled email reports.
		if ( aioseo()->options->advanced->emailSummary->enable ) {
			return;
		}

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

		// Add notification.
		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'email-reports-enable-reminder',
			'title'             => __( 'Email Reports', 'all-in-one-seo-pack' ),
			'content'           => __( 'Stay ahead in SEO with our new email digest! Get the latest tips, trends, and tools delivered right to your inbox, helping you optimize smarter and faster. Enable it today and never miss an update that can take your rankings to the next level.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'type'              => 'info',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Enable Email Reports', 'all-in-one-seo-pack' ),
			'button1_action'    => 'https://route#aioseo-settings&aioseo-scroll=aioseo-email-summary-row&aioseo-highlight=aioseo-email-summary-row:advanced',
			'button2_label'     => __( 'All Good, I\'m already getting it', 'all-in-one-seo-pack' ),
			'button2_action'    => 'http://action#notification/email-reports-enable',
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}
}Common/EmailReports/Mail.php000066600000001163151135505570012011 0ustar00<?php

namespace AIOSEO\Plugin\Common\EmailReports;

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

/**
 * Mail class.
 *
 * @since 4.7.2
 */
class Mail {
	/**
	 * Send the email.
	 *
	 * @since 4.7.2
	 *
	 * @param  mixed $to      Receiver.
	 * @param  mixed $subject Email subject.
	 * @param  mixed $message Message.
	 * @param  array $headers Email headers.
	 * @return bool           Whether the email was sent successfully.
	 */
	public function send( $to, $subject, $message, $headers = [ 'Content-Type: text/html; charset=UTF-8' ] ) {
		return wp_mail( $to, $subject, $message, $headers );
	}
}Common/WritingAssistant/SeoBoost/Service.php000066600000014005151135505570015172 0ustar00<?php
namespace AIOSEO\Plugin\Common\WritingAssistant\SeoBoost;

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

/**
 * Service class for SeoBoost.
 *
 * @since 4.7.4
 */
class Service {
	/**
	 * The base URL for the SeoBoost microservice.
	 *
	 * @since 4.7.4
	 *
	 * @var string
	 */
	private $baseUrl = 'https://app.seoboost.com/api/';

	/**
	 * Sends the keyword to be processed.
	 *
	 * @since 4.7.4
	 *
	 * @param  string          $keyword  The keyword.
	 * @param  string          $country  The country code.
	 * @param  string          $language The language code.
	 * @return array|\WP_Error           The response.
	 */
	public function processKeyword( $keyword, $country = 'US', $language = 'en' ) {
		if ( empty( $keyword ) || empty( $country ) || empty( $language ) ) {
			return new \WP_Error( 'service-error', __( 'Missing parameters', 'all-in-one-seo-pack' ) );
		}

		$reportRequest = $this->doRequest( 'waAddNewReport', [
			'params' => [
				'keyword'  => $keyword,
				'country'  => $country,
				'language' => $language
			]
		] );

		if ( is_wp_error( $reportRequest ) ) {
			return $reportRequest;
		}

		if ( empty( $reportRequest ) || empty( $reportRequest['status'] ) ) {
			return new \WP_Error( 'service-error', __( 'Empty response from service', 'all-in-one-seo-pack' ) );
		}

		if ( 'success' !== $reportRequest['status'] ) {
			return new \WP_Error( 'service-error', $reportRequest['msg'] );
		}

		return $reportRequest;
	}

	/**
	 * Sends a post content to be analyzed.
	 *
	 * @since 4.7.4
	 *
	 * @param  string          $title       The title.
	 * @param  string          $description The description.
	 * @param  string          $content     The content.
	 * @param  string          $reportSlug  The report slug.
	 * @return array|\WP_Error              The response.
	 */
	public function getContentAnalysis( $title, $description, $content, $reportSlug ) {
		return $this->doRequest( 'waAnalyzeContent', [
			'title'       => $title,
			'description' => $description,
			'content'     => $content,
			'slug'        => $reportSlug
		] );
	}

	/**
	 * Gets the progress for a keyword.
	 *
	 * @since 4.7.4
	 *
	 * @param  string          $uuid The uuid.
	 * @return array|\WP_Error       The progress.
	 */
	public function getProgressAndResult( $uuid ) {
		$response = $this->doRequest( 'waGetReport', [ 'slug' => $uuid ] );

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		if ( empty( $response ) ) {
			return new \WP_Error( 'empty-progress-and-result', __( 'Empty progress and result.', 'all-in-one-seo-pack' ) );
		}

		return $response;
	}

	/**
	 * Gets the user options.
	 *
	 * @since 4.7.4
	 *
	 * @return array|\WP_Error The user options.
	 */
	public function getUserOptions() {
		return $this->doRequest( 'waGetUserOptions' );
	}

	/**
	 * Gets the user information.
	 *
	 * @since 4.7.4
	 *
	 * @return array|\WP_Error The user information.
	 */
	public function getUserInfo() {
		return $this->doRequest( 'waGetUserInfo' );
	}

	/**
	 * Gets the access token.
	 *
	 * @since 4.7.4
	 *
	 * @param  string          $authToken The auth token.
	 * @return array|\WP_Error            The response.
	 */
	public function getAccessToken( $authToken ) {
		return $this->doRequest( 'oauthaccess', [ 'token' => $authToken ] );
	}

	/**
	 * Refreshes the access token.
	 *
	 * @since 4.7.4
	 *
	 * @return bool Was the token refreshed?
	 */
	private function refreshAccessToken() {
		$newAccessToken = $this->doRequest( 'waRefreshAccessToken' );
		if (
			is_wp_error( $newAccessToken ) ||
			'success' !== $newAccessToken['status']
		) {
			aioseo()->writingAssistant->seoBoost->setAccessToken( '' );

			return false;
		}

		aioseo()->writingAssistant->seoBoost->setAccessToken( $newAccessToken['token'] );

		return true;
	}

	/**
	 * Sends a POST request to the microservice.
	 *
	 * @since 4.7.4
	 *
	 * @param  string          $path        The path.
	 * @param  array           $requestBody The request body.
	 * @return array|\WP_Error              Returns the response body or WP_Error if the request failed.
	 */
	private function doRequest( $path, $requestBody = [] ) {
		// Prevent API requests if no access token is present.
		if (
			'oauthaccess' !== $path && // Except if we're getting the access token.
			empty( aioseo()->writingAssistant->seoBoost->getAccessToken() )
		) {
			return new \WP_Error( 'service-error', __( 'Missing access token', 'all-in-one-seo-pack' ) );
		}

		$requestData = [
			'headers' => [
				'X-SeoBoost-Access-Token' => aioseo()->writingAssistant->seoBoost->getAccessToken(),
				'X-SeoBoost-Domain'       => aioseo()->helpers->getMultiSiteDomain(),
				'Content-Type'            => 'application/json'
			],
			'timeout' => 60,
			'method'  => 'GET'
		];

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

		$path         = trailingslashit( $this->getUrl() ) . trailingslashit( $path );
		$response     = wp_remote_request( $path, $requestData );
		$responseBody = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( ! $responseBody ) {
			$response = new \WP_Error( 'service-failed', __( 'Error in the SeoBoost service. Please contact support.', 'all-in-one-seo-pack' ) );
		}

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		// Refresh access token if expired and redo the request.
		if (
			isset( $responseBody['error'] ) &&
			'invalid-access-token' === $responseBody['error']
		) {
			if ( $this->refreshAccessToken() ) {
				return $this->doRequest( $path, $requestBody );
			}
		}

		return $responseBody;
	}

	/**
	 * Returns the URL for the Writing Assistant service.
	 *
	 * @since 4.7.4
	 *
	 * @return string The URL.
	 */
	public function getUrl() {
		$url = $this->baseUrl;
		if ( defined( 'AIOSEO_WRITING_ASSISTANT_SERVICE_URL' ) ) {
			$url = AIOSEO_WRITING_ASSISTANT_SERVICE_URL;
		}

		return $url;
	}

	/**
	 * Gets the report history.
	 *
	 * @since 4.7.4
	 *
	 * @return array|\WP_Error
	 */
	public function getReportHistory() {
		return $this->doRequest( 'waGetReportHistory' );
	}
}Common/WritingAssistant/SeoBoost/SeoBoost.php000066600000020236151135505570015332 0ustar00<?php
namespace AIOSEO\Plugin\Common\WritingAssistant\SeoBoost;

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

/**
 * Handles the connection with SEOBoost.
 *
 * @since 4.7.4
 */
class SeoBoost {
	/**
	 * URL of the login page.
	 *
	 * @since 4.7.4
	 */
	private $loginUrl = 'https://app.seoboost.com/login/';

	/**
	 * URL of the Create Account page.
	 *
	 * @since 4.7.4
	 */
	private $createAccountUrl = 'https://seoboost.com/checkout/';

	/**
	 * The service.
	 *
	 * @since 4.7.4
	 *
	 * @var Service
	 */
	public $service;

	/**
	 * Class constructor.
	 *
	 * @since 4.7.4
	 */
	public function __construct() {
		$this->service = new Service();

		$returnParam = isset( $_GET['aioseo-writing-assistant'] ) // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			? sanitize_text_field( wp_unslash( $_GET['aioseo-writing-assistant'] ) ) // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			: null;

		if ( 'auth_return' === $returnParam ) {
			add_action( 'init', [ $this, 'checkToken' ], 50 );
		}

		if ( 'ms_logged_in' === $returnParam ) {
			add_action( 'init', [ $this, 'marketingSiteCallback' ], 50 );
		}

		add_action( 'init', [ $this, 'migrateUserData' ], 10 );
		add_action( 'init', [ $this, 'refreshUserOptionsAfterError' ] );
	}

	/**
	 * Returns if the user has an access key.
	 *
	 * @since 4.7.4
	 *
	 * @return bool
	 */
	public function isLoggedIn() {
		return $this->getAccessToken() !== '';
	}

	/**
	 * Gets the login URL.
	 *
	 * @since 4.7.4
	 *
	 * @return string The login URL.
	 */
	public function getLoginUrl() {
		$url = $this->loginUrl;
		if ( defined( 'AIOSEO_WRITING_ASSISTANT_LOGIN_URL' ) ) {
			$url = AIOSEO_WRITING_ASSISTANT_LOGIN_URL;
		}

		$params = [
			'oauth'    => true,
			'redirect' => get_site_url() . '?' . build_query( [ 'aioseo-writing-assistant' => 'auth_return' ] ),
			'domain'   => aioseo()->helpers->getMultiSiteDomain()
		];

		return trailingslashit( $url ) . '?' . build_query( $params );
	}

	/**
	 * Gets the login URL.
	 *
	 * @since 4.7.4
	 *
	 * @return string The login URL.
	 */
	public function getCreateAccountUrl() {
		$url = $this->createAccountUrl;
		if ( defined( 'AIOSEO_WRITING_ASSISTANT_CREATE_ACCOUNT_URL' ) ) {
			$url = AIOSEO_WRITING_ASSISTANT_CREATE_ACCOUNT_URL;
		}

		$params = [
			'url'                        => base64_encode( get_site_url() . '?' . build_query( [ 'aioseo-writing-assistant' => 'ms_logged_in' ] ) ),
			'writing-assistant-checkout' => true
		];

		return trailingslashit( $url ) . '?' . build_query( $params );
	}

	/**
	 * Gets the user's access token.
	 *
	 * @since 4.7.4
	 *
	 * @return string The access token.
	 */
	public function getAccessToken() {
		$metaKey = 'seoboost_access_token_' . get_current_blog_id();

		return get_user_meta( get_current_user_id(), $metaKey, true );
	}

	/**
	 * Sets the user's access token.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	public function setAccessToken( $accessToken ) {
		$metaKey = 'seoboost_access_token_' . get_current_blog_id();
		update_user_meta( get_current_user_id(), $metaKey, $accessToken );

		$this->refreshUserOptions();
	}

	/**
	 * Refreshes user options from SEOBoost.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	public function refreshUserOptions() {
		$userOptions = $this->service->getUserOptions();
		if ( is_wp_error( $userOptions ) || ! empty( $userOptions['error'] ) ) {
			$userOptions = $this->getDefaultUserOptions();

			aioseo()->cache->update( 'seoboost_get_user_options_error', time() + DAY_IN_SECONDS, MONTH_IN_SECONDS );
		}

		$this->setUserOptions( $userOptions );
	}

	/**
	 * Gets the user options.
	 *
	 * @since 4.7.4
	 *
	 * @param  bool  $refresh Whether to refresh the user options.
	 * @return array          The user options.
	 */
	public function getUserOptions( $refresh = false ) {
		if ( ! $refresh ) {
			$metaKey     = 'seoboost_user_options_' . get_current_blog_id();
			$userOptions = get_user_meta( get_current_user_id(), $metaKey, true );

			if ( ! empty( $userOptions ) ) {
				return json_decode( (string) $userOptions, true ) ?? [];
			}
		}

		// If there are no options or we need to refresh them, get them from SEOBoost.
		$this->refreshUserOptions();

		$userOptions = $this->getUserOptions();
		if ( empty( $userOptions ) ) {
			return $this->getDefaultUserOptions();
		}

		return $userOptions;
	}

	/**
	 * Gets the user options.
	 *
	 * @since 4.7.4
	 *
	 * @param  array $options The user options.
	 * @return void
	 */
	public function setUserOptions( $options ) {
		if ( ! is_array( $options ) ) {
			return;
		}

		$metaKey     = 'seoboost_user_options_' . get_current_blog_id();
		$userOptions = array_intersect_key( $options, $this->getDefaultUserOptions() );

		update_user_meta( get_current_user_id(), $metaKey, wp_json_encode( $userOptions ) );
	}

	/**
	 * Gets the user info from SEOBoost.
	 *
	 * @since 4.7.4
	 *
	 * @return array|\WP_Error The user info or a WP_Error.
	 */
	public function getUserInfo() {
		return $this->service->getUserInfo();
	}

	/**
	 * Checks the token.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	public function checkToken() {
		$authToken = isset( $_GET['token'] ) // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			? sanitize_key( wp_unslash( $_GET['token'] ) ) // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			: null;

		if ( $authToken ) {
			$accessToken = $this->service->getAccessToken( $authToken );

			if ( ! is_wp_error( $accessToken ) && ! empty( $accessToken['token'] ) ) {
				$this->setAccessToken( $accessToken['token'] );
				?>
				<script>
					// Send message to parent window.
					window.opener.postMessage('seoboost-authenticated', '*');
				</script>
				<?php
			}
		}
		?>
		<script>
			// Close window.
			window.close();
		</script>
		<?php
		die;
	}

	/**
	 * Handles the callback from the marketing site after completing authentication.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	public function marketingSiteCallback() {
		?>
		<script>
			// Send message to parent window.
			window.opener.postMessage('seoboost-ms-logged-in', '*');
			window.close();
		</script>
		<?php
	}

	/**
	 * Resets the logins.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	public function resetLogins() {
		// Delete access token and user options from the database.
		aioseo()->core->db->delete( 'usermeta' )->whereRaw( 'meta_key LIKE \'seoboost_access_token%\'' )->run();
		aioseo()->core->db->delete( 'usermeta' )->where( 'meta_key', 'seoboost_user_options' )->run();
	}

	/**
	 * Gets the report history.
	 *
	 * @since 4.7.4
	 *
	 * @return array|\WP_Error The report history.
	 */
	public function getReportHistory() {
		return $this->service->getReportHistory();
	}

	/**
	 * Migrate Writing Assistant access tokens.
	 * This handles the fix for multisites where subsites all used the same workspace/account.
	 *
	 * @since 4.7.7
	 *
	 * @return void
	 */
	public function migrateUserData() {
		$userToken = get_user_meta( get_current_user_id(), 'seoboost_access_token', true );
		if ( ! empty( $userToken ) ) {
			$this->setAccessToken( $userToken );
			delete_user_meta( get_current_user_id(), 'seoboost_access_token' );
		}

		$userOptions = get_user_meta( get_current_user_id(), 'seoboost_user_options', true );
		if ( ! empty( $userOptions ) ) {
			$this->setUserOptions( $userOptions );
			delete_user_meta( get_current_user_id(), 'seoboost_user_options' );
		}
	}

	/**
	 * Refreshes user options after an error.
	 * This needs to run on init since service class is not available in the constructor.
	 *
	 * @since 4.7.7.2
	 *
	 * @return void
	 */
	public function refreshUserOptionsAfterError() {
		$userOptionsFetchError = aioseo()->cache->get( 'seoboost_get_user_options_error' );
		if ( $userOptionsFetchError && time() > $userOptionsFetchError ) {
			aioseo()->cache->delete( 'seoboost_get_user_options_error' );

			$this->refreshUserOptions();
		}
	}

	/**
	 * Returns the default user options.
	 *
	 * @since 4.7.7.1
	 *
	 * @return array The default user options.
	 */
	private function getDefaultUserOptions() {
		return [
			'language' => 'en',
			'country'  => 'US'
		];
	}
}Common/WritingAssistant/WritingAssistant.php000066600000001102151135505570015344 0ustar00<?php
namespace AIOSEO\Plugin\Common\WritingAssistant;

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

/**
 * Main class.
 *
 * @since 4.7.4
 */
class WritingAssistant {
	/**
	 * Helpers.
	 *
	 * @since 4.7.4
	 *
	 * @var Utils\Helpers
	 */
	public $helpers;

	/**
	 * SeoBoost.
	 *
	 * @since 4.7.4
	 *
	 * @var SeoBoost\SeoBoost
	 */
	public $seoBoost;

	/**
	 * Load our classes.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	public function __construct() {
		$this->helpers  = new Utils\Helpers();
		$this->seoBoost = new SeoBoost\SeoBoost();
	}
}Common/WritingAssistant/Utils/Helpers.php000066600000043000151135505570014534 0ustar00<?php
namespace AIOSEO\Plugin\Common\WritingAssistant\Utils;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Helper functions.
 *
 * @since 4.7.4
 */
class Helpers {
	/**
	 * Gets the data for vue.
	 *
	 * @since 4.7.4
	 *
	 * @return array An array of data.
	 */
	public function getStandaloneVueData() {
		$keyword = Models\WritingAssistantPost::getKeyword( get_the_ID() );

		return [
			'postId'          => get_the_ID(),
			'report'          => $keyword,
			'keywordText'     => ! empty( $keyword->keyword ) ? $keyword->keyword : '',
			'contentAnalysis' => Models\WritingAssistantPost::getContentAnalysis( get_the_ID() ),
			'seoBoost'        => [
				'isLoggedIn'       => aioseo()->writingAssistant->seoBoost->isLoggedIn(),
				'loginUrl'         => aioseo()->writingAssistant->seoBoost->getLoginUrl(),
				'createAccountUrl' => aioseo()->writingAssistant->seoBoost->getCreateAccountUrl(),
				'userOptions'      => aioseo()->writingAssistant->seoBoost->getUserOptions()
			]
		];
	}

	/**
	 * Gets the data for vue.
	 *
	 * @since 4.7.4
	 *
	 * @return array An array of data.
	 */
	public function getSettingsVueData() {
		return [
			'seoBoost' => [
				'isLoggedIn'       => aioseo()->writingAssistant->seoBoost->isLoggedIn(),
				'loginUrl'         => aioseo()->writingAssistant->seoBoost->getLoginUrl(),
				'createAccountUrl' => aioseo()->writingAssistant->seoBoost->getCreateAccountUrl(),
				'userOptions'      => aioseo()->writingAssistant->seoBoost->getUserOptions(),
				'countries'        => $this->getCountries(),
				'languages'        => $this->getLanguages(),
				'searchEngines'    => $this->getSearchEngines()
			]
		];
	}

	/**
	 * Returns the list of countries.
	 *
	 * @since 4.7.7.1
	 * @version 4.8.3 Moved from SeoBoost/SeoBoost.php
	 *
	 * @return array The list of countries.
	 */
	private function getCountries() {
		$countries = [
			'AF' => __( 'Afghanistan', 'all-in-one-seo-pack' ),
			'AL' => __( 'Albania', 'all-in-one-seo-pack' ),
			'DZ' => __( 'Algeria', 'all-in-one-seo-pack' ),
			'AS' => __( 'American Samoa', 'all-in-one-seo-pack' ),
			'AD' => __( 'Andorra', 'all-in-one-seo-pack' ),
			'AO' => __( 'Angola', 'all-in-one-seo-pack' ),
			'AI' => __( 'Anguilla', 'all-in-one-seo-pack' ),
			'AG' => __( 'Antigua & Barbuda', 'all-in-one-seo-pack' ),
			'AR' => __( 'Argentina', 'all-in-one-seo-pack' ),
			'AM' => __( 'Armenia', 'all-in-one-seo-pack' ),
			'AU' => __( 'Australia', 'all-in-one-seo-pack' ),
			'AT' => __( 'Austria', 'all-in-one-seo-pack' ),
			'AZ' => __( 'Azerbaijan', 'all-in-one-seo-pack' ),
			'BS' => __( 'Bahamas', 'all-in-one-seo-pack' ),
			'BH' => __( 'Bahrain', 'all-in-one-seo-pack' ),
			'BD' => __( 'Bangladesh', 'all-in-one-seo-pack' ),
			'BY' => __( 'Belarus', 'all-in-one-seo-pack' ),
			'BE' => __( 'Belgium', 'all-in-one-seo-pack' ),
			'BZ' => __( 'Belize', 'all-in-one-seo-pack' ),
			'BJ' => __( 'Benin', 'all-in-one-seo-pack' ),
			'BT' => __( 'Bhutan', 'all-in-one-seo-pack' ),
			'BO' => __( 'Bolivia', 'all-in-one-seo-pack' ),
			'BA' => __( 'Bosnia & Herzegovina', 'all-in-one-seo-pack' ),
			'BW' => __( 'Botswana', 'all-in-one-seo-pack' ),
			'BR' => __( 'Brazil', 'all-in-one-seo-pack' ),
			'VG' => __( 'British Virgin Islands', 'all-in-one-seo-pack' ),
			'BN' => __( 'Brunei', 'all-in-one-seo-pack' ),
			'BG' => __( 'Bulgaria', 'all-in-one-seo-pack' ),
			'BF' => __( 'Burkina Faso', 'all-in-one-seo-pack' ),
			'BI' => __( 'Burundi', 'all-in-one-seo-pack' ),
			'KH' => __( 'Cambodia', 'all-in-one-seo-pack' ),
			'CM' => __( 'Cameroon', 'all-in-one-seo-pack' ),
			'CA' => __( 'Canada', 'all-in-one-seo-pack' ),
			'CV' => __( 'Cape Verde', 'all-in-one-seo-pack' ),
			'CF' => __( 'Central African Republic', 'all-in-one-seo-pack' ),
			'TD' => __( 'Chad', 'all-in-one-seo-pack' ),
			'CL' => __( 'Chile', 'all-in-one-seo-pack' ),
			'CO' => __( 'Colombia', 'all-in-one-seo-pack' ),
			'CG' => __( 'Congo - Brazzaville', 'all-in-one-seo-pack' ),
			'CD' => __( 'Congo - Kinshasa', 'all-in-one-seo-pack' ),
			'CK' => __( 'Cook Islands', 'all-in-one-seo-pack' ),
			'CR' => __( 'Costa Rica', 'all-in-one-seo-pack' ),
			'CI' => __( 'Côte d’Ivoire', 'all-in-one-seo-pack' ),
			'HR' => __( 'Croatia', 'all-in-one-seo-pack' ),
			'CU' => __( 'Cuba', 'all-in-one-seo-pack' ),
			'CY' => __( 'Cyprus', 'all-in-one-seo-pack' ),
			'CZ' => __( 'Czechia', 'all-in-one-seo-pack' ),
			'DK' => __( 'Denmark', 'all-in-one-seo-pack' ),
			'DJ' => __( 'Djibouti', 'all-in-one-seo-pack' ),
			'DM' => __( 'Dominica', 'all-in-one-seo-pack' ),
			'DO' => __( 'Dominican Republic', 'all-in-one-seo-pack' ),
			'EC' => __( 'Ecuador', 'all-in-one-seo-pack' ),
			'EG' => __( 'Egypt', 'all-in-one-seo-pack' ),
			'SV' => __( 'El Salvador', 'all-in-one-seo-pack' ),
			'EE' => __( 'Estonia', 'all-in-one-seo-pack' ),
			'ET' => __( 'Ethiopia', 'all-in-one-seo-pack' ),
			'FJ' => __( 'Fiji', 'all-in-one-seo-pack' ),
			'FI' => __( 'Finland', 'all-in-one-seo-pack' ),
			'FR' => __( 'France', 'all-in-one-seo-pack' ),
			'GA' => __( 'Gabon', 'all-in-one-seo-pack' ),
			'GM' => __( 'Gambia', 'all-in-one-seo-pack' ),
			'GE' => __( 'Georgia', 'all-in-one-seo-pack' ),
			'DE' => __( 'Germany', 'all-in-one-seo-pack' ),
			'GH' => __( 'Ghana', 'all-in-one-seo-pack' ),
			'GI' => __( 'Gibraltar', 'all-in-one-seo-pack' ),
			'GR' => __( 'Greece', 'all-in-one-seo-pack' ),
			'GL' => __( 'Greenland', 'all-in-one-seo-pack' ),
			'GT' => __( 'Guatemala', 'all-in-one-seo-pack' ),
			'GG' => __( 'Guernsey', 'all-in-one-seo-pack' ),
			'GY' => __( 'Guyana', 'all-in-one-seo-pack' ),
			'HT' => __( 'Haiti', 'all-in-one-seo-pack' ),
			'HN' => __( 'Honduras', 'all-in-one-seo-pack' ),
			'HK' => __( 'Hong Kong', 'all-in-one-seo-pack' ),
			'HU' => __( 'Hungary', 'all-in-one-seo-pack' ),
			'IS' => __( 'Iceland', 'all-in-one-seo-pack' ),
			'IN' => __( 'India', 'all-in-one-seo-pack' ),
			'ID' => __( 'Indonesia', 'all-in-one-seo-pack' ),
			'IQ' => __( 'Iraq', 'all-in-one-seo-pack' ),
			'IE' => __( 'Ireland', 'all-in-one-seo-pack' ),
			'IM' => __( 'Isle of Man', 'all-in-one-seo-pack' ),
			'IL' => __( 'Israel', 'all-in-one-seo-pack' ),
			'IT' => __( 'Italy', 'all-in-one-seo-pack' ),
			'JM' => __( 'Jamaica', 'all-in-one-seo-pack' ),
			'JP' => __( 'Japan', 'all-in-one-seo-pack' ),
			'JE' => __( 'Jersey', 'all-in-one-seo-pack' ),
			'JO' => __( 'Jordan', 'all-in-one-seo-pack' ),
			'KZ' => __( 'Kazakhstan', 'all-in-one-seo-pack' ),
			'KE' => __( 'Kenya', 'all-in-one-seo-pack' ),
			'KI' => __( 'Kiribati', 'all-in-one-seo-pack' ),
			'KW' => __( 'Kuwait', 'all-in-one-seo-pack' ),
			'KG' => __( 'Kyrgyzstan', 'all-in-one-seo-pack' ),
			'LA' => __( 'Laos', 'all-in-one-seo-pack' ),
			'LV' => __( 'Latvia', 'all-in-one-seo-pack' ),
			'LB' => __( 'Lebanon', 'all-in-one-seo-pack' ),
			'LS' => __( 'Lesotho', 'all-in-one-seo-pack' ),
			'LY' => __( 'Libya', 'all-in-one-seo-pack' ),
			'LI' => __( 'Liechtenstein', 'all-in-one-seo-pack' ),
			'LT' => __( 'Lithuania', 'all-in-one-seo-pack' ),
			'LU' => __( 'Luxembourg', 'all-in-one-seo-pack' ),
			'MG' => __( 'Madagascar', 'all-in-one-seo-pack' ),
			'MW' => __( 'Malawi', 'all-in-one-seo-pack' ),
			'MY' => __( 'Malaysia', 'all-in-one-seo-pack' ),
			'MV' => __( 'Maldives', 'all-in-one-seo-pack' ),
			'ML' => __( 'Mali', 'all-in-one-seo-pack' ),
			'MT' => __( 'Malta', 'all-in-one-seo-pack' ),
			'MU' => __( 'Mauritius', 'all-in-one-seo-pack' ),
			'MX' => __( 'Mexico', 'all-in-one-seo-pack' ),
			'FM' => __( 'Micronesia', 'all-in-one-seo-pack' ),
			'MD' => __( 'Moldova', 'all-in-one-seo-pack' ),
			'MN' => __( 'Mongolia', 'all-in-one-seo-pack' ),
			'ME' => __( 'Montenegro', 'all-in-one-seo-pack' ),
			'MS' => __( 'Montserrat', 'all-in-one-seo-pack' ),
			'MA' => __( 'Morocco', 'all-in-one-seo-pack' ),
			'MZ' => __( 'Mozambique', 'all-in-one-seo-pack' ),
			'MM' => __( 'Myanmar (Burma)', 'all-in-one-seo-pack' ),
			'NA' => __( 'Namibia', 'all-in-one-seo-pack' ),
			'NR' => __( 'Nauru', 'all-in-one-seo-pack' ),
			'NP' => __( 'Nepal', 'all-in-one-seo-pack' ),
			'NL' => __( 'Netherlands', 'all-in-one-seo-pack' ),
			'NZ' => __( 'New Zealand', 'all-in-one-seo-pack' ),
			'NI' => __( 'Nicaragua', 'all-in-one-seo-pack' ),
			'NE' => __( 'Niger', 'all-in-one-seo-pack' ),
			'NG' => __( 'Nigeria', 'all-in-one-seo-pack' ),
			'NU' => __( 'Niue', 'all-in-one-seo-pack' ),
			'MK' => __( 'North Macedonia', 'all-in-one-seo-pack' ),
			'NO' => __( 'Norway', 'all-in-one-seo-pack' ),
			'OM' => __( 'Oman', 'all-in-one-seo-pack' ),
			'PK' => __( 'Pakistan', 'all-in-one-seo-pack' ),
			'PS' => __( 'Palestine', 'all-in-one-seo-pack' ),
			'PA' => __( 'Panama', 'all-in-one-seo-pack' ),
			'PG' => __( 'Papua New Guinea', 'all-in-one-seo-pack' ),
			'PY' => __( 'Paraguay', 'all-in-one-seo-pack' ),
			'PE' => __( 'Peru', 'all-in-one-seo-pack' ),
			'PH' => __( 'Philippines', 'all-in-one-seo-pack' ),
			'PN' => __( 'Pitcairn Islands', 'all-in-one-seo-pack' ),
			'PL' => __( 'Poland', 'all-in-one-seo-pack' ),
			'PT' => __( 'Portugal', 'all-in-one-seo-pack' ),
			'PR' => __( 'Puerto Rico', 'all-in-one-seo-pack' ),
			'QA' => __( 'Qatar', 'all-in-one-seo-pack' ),
			'RO' => __( 'Romania', 'all-in-one-seo-pack' ),
			'RU' => __( 'Russia', 'all-in-one-seo-pack' ),
			'RW' => __( 'Rwanda', 'all-in-one-seo-pack' ),
			'WS' => __( 'Samoa', 'all-in-one-seo-pack' ),
			'SM' => __( 'San Marino', 'all-in-one-seo-pack' ),
			'ST' => __( 'São Tomé & Príncipe', 'all-in-one-seo-pack' ),
			'SA' => __( 'Saudi Arabia', 'all-in-one-seo-pack' ),
			'SN' => __( 'Senegal', 'all-in-one-seo-pack' ),
			'RS' => __( 'Serbia', 'all-in-one-seo-pack' ),
			'SC' => __( 'Seychelles', 'all-in-one-seo-pack' ),
			'SL' => __( 'Sierra Leone', 'all-in-one-seo-pack' ),
			'SG' => __( 'Singapore', 'all-in-one-seo-pack' ),
			'SK' => __( 'Slovakia', 'all-in-one-seo-pack' ),
			'SI' => __( 'Slovenia', 'all-in-one-seo-pack' ),
			'SB' => __( 'Solomon Islands', 'all-in-one-seo-pack' ),
			'SO' => __( 'Somalia', 'all-in-one-seo-pack' ),
			'ZA' => __( 'South Africa', 'all-in-one-seo-pack' ),
			'KR' => __( 'South Korea', 'all-in-one-seo-pack' ),
			'ES' => __( 'Spain', 'all-in-one-seo-pack' ),
			'LK' => __( 'Sri Lanka', 'all-in-one-seo-pack' ),
			'SH' => __( 'St. Helena', 'all-in-one-seo-pack' ),
			'VC' => __( 'St. Vincent & Grenadines', 'all-in-one-seo-pack' ),
			'SR' => __( 'Suriname', 'all-in-one-seo-pack' ),
			'SE' => __( 'Sweden', 'all-in-one-seo-pack' ),
			'CH' => __( 'Switzerland', 'all-in-one-seo-pack' ),
			'TW' => __( 'Taiwan', 'all-in-one-seo-pack' ),
			'TJ' => __( 'Tajikistan', 'all-in-one-seo-pack' ),
			'TZ' => __( 'Tanzania', 'all-in-one-seo-pack' ),
			'TH' => __( 'Thailand', 'all-in-one-seo-pack' ),
			'TL' => __( 'Timor-Leste', 'all-in-one-seo-pack' ),
			'TG' => __( 'Togo', 'all-in-one-seo-pack' ),
			'TO' => __( 'Tonga', 'all-in-one-seo-pack' ),
			'TT' => __( 'Trinidad & Tobago', 'all-in-one-seo-pack' ),
			'TN' => __( 'Tunisia', 'all-in-one-seo-pack' ),
			'TR' => __( 'Turkey', 'all-in-one-seo-pack' ),
			'TM' => __( 'Turkmenistan', 'all-in-one-seo-pack' ),
			'VI' => __( 'U.S. Virgin Islands', 'all-in-one-seo-pack' ),
			'UG' => __( 'Uganda', 'all-in-one-seo-pack' ),
			'UA' => __( 'Ukraine', 'all-in-one-seo-pack' ),
			'AE' => __( 'United Arab Emirates', 'all-in-one-seo-pack' ),
			'GB' => __( 'United Kingdom', 'all-in-one-seo-pack' ),
			'US' => __( 'United States', 'all-in-one-seo-pack' ),
			'UY' => __( 'Uruguay', 'all-in-one-seo-pack' ),
			'UZ' => __( 'Uzbekistan', 'all-in-one-seo-pack' ),
			'VU' => __( 'Vanuatu', 'all-in-one-seo-pack' ),
			'VE' => __( 'Venezuela', 'all-in-one-seo-pack' ),
			'VN' => __( 'Vietnam', 'all-in-one-seo-pack' ),
			'ZM' => __( 'Zambia', 'all-in-one-seo-pack' ),
			'ZW' => __( 'Zimbabwe', 'all-in-one-seo-pack' )
		];

		return $countries;
	}

	/**
	 * Returns the list of languages.
	 *
	 * @since 4.7.7.1
	 * @version 4.8.3 Moved from SeoBoost/SeoBoost.php
	 *
	 * @return array The list of languages.
	 */
	private function getLanguages() {
		$languages = [
			'ca' => __( 'Catalan', 'all-in-one-seo-pack' ),
			'da' => __( 'Danish', 'all-in-one-seo-pack' ),
			'nl' => __( 'Dutch', 'all-in-one-seo-pack' ),
			'en' => __( 'English', 'all-in-one-seo-pack' ),
			'fr' => __( 'French', 'all-in-one-seo-pack' ),
			'de' => __( 'German', 'all-in-one-seo-pack' ),
			'id' => __( 'Indonesian', 'all-in-one-seo-pack' ),
			'it' => __( 'Italian', 'all-in-one-seo-pack' ),
			'no' => __( 'Norwegian', 'all-in-one-seo-pack' ),
			'pt' => __( 'Portuguese', 'all-in-one-seo-pack' ),
			'ro' => __( 'Romanian', 'all-in-one-seo-pack' ),
			'es' => __( 'Spanish', 'all-in-one-seo-pack' ),
			'sv' => __( 'Swedish', 'all-in-one-seo-pack' ),
			'tr' => __( 'Turkish', 'all-in-one-seo-pack' )
		];

		return $languages;
	}

	/**
	 * Returns the list of search engines.
	 *
	 * @since 4.7.7.1
	 * @version 4.8.3 Moved from SeoBoost/SeoBoost.php
	 *
	 * @return array The list of search engines.
	 */
	private function getSearchEngines() {
		$searchEngines = [
			'AF' => 'google.com.af',
			'AL' => 'google.al',
			'DZ' => 'google.dz',
			'AS' => 'google.as',
			'AD' => 'google.ad',
			'AO' => 'google.it.ao',
			'AI' => 'google.com.ai',
			'AG' => 'google.com.ag',
			'AR' => 'google.com.ar',
			'AM' => 'google.am',
			'AU' => 'google.com.au',
			'AT' => 'google.at',
			'AZ' => 'google.az',
			'BS' => 'google.bs',
			'BH' => 'google.com.bh',
			'BD' => 'google.com.bd',
			'BY' => 'google.com.by',
			'BE' => 'google.be',
			'BZ' => 'google.com.bz',
			'BJ' => 'google.bj',
			'BT' => 'google.bt',
			'BO' => 'google.com.bo',
			'BA' => 'google.ba',
			'BW' => 'google.co.bw',
			'BR' => 'google.com.br',
			'VG' => 'google.vg',
			'BN' => 'google.com.bn',
			'BG' => 'google.bg',
			'BF' => 'google.bf',
			'BI' => 'google.bi',
			'KH' => 'google.com.kh',
			'CM' => 'google.cm',
			'CA' => 'google.ca',
			'CV' => 'google.cv',
			'CF' => 'google.cf',
			'TD' => 'google.td',
			'CL' => 'google.cl',
			'CO' => 'google.com.co',
			'CG' => 'google.cg',
			'CD' => 'google.cd',
			'CK' => 'google.co.ck',
			'CR' => 'google.co.cr',
			'CI' => 'google.ci',
			'HR' => 'google.hr',
			'CU' => 'google.com.cu',
			'CY' => 'google.com.cy',
			'CZ' => 'google.cz',
			'DK' => 'google.dk',
			'DJ' => 'google.dj',
			'DM' => 'google.dm',
			'DO' => 'google.com.do',
			'EC' => 'google.com.ec',
			'EG' => 'google.com.eg',
			'SV' => 'google.com.sv',
			'EE' => 'google.ee',
			'ET' => 'google.com.et',
			'FJ' => 'google.com.fj',
			'FI' => 'google.fi',
			'FR' => 'google.fr',
			'GA' => 'google.ga',
			'GM' => 'google.gm',
			'GE' => 'google.ge',
			'DE' => 'google.de',
			'GH' => 'google.com.gh',
			'GI' => 'google.com.gi',
			'GR' => 'google.gr',
			'GL' => 'google.gl',
			'GT' => 'google.com.gt',
			'GG' => 'google.gg',
			'GY' => 'google.gy',
			'HT' => 'google.ht',
			'HN' => 'google.hn',
			'HK' => 'google.com.hk',
			'HU' => 'google.hu',
			'IS' => 'google.is',
			'IN' => 'google.co.in',
			'ID' => 'google.co.id',
			'IQ' => 'google.iq',
			'IE' => 'google.ie',
			'IM' => 'google.co.im',
			'IL' => 'google.co.il',
			'IT' => 'google.it',
			'JM' => 'google.com.jm',
			'JP' => 'google.co.jp',
			'JE' => 'google.co.je',
			'JO' => 'google.jo',
			'KZ' => 'google.kz',
			'KE' => 'google.co.ke',
			'KI' => 'google.ki',
			'KW' => 'google.com.kw',
			'KG' => 'google.com.kg',
			'LA' => 'google.la',
			'LV' => 'google.lv',
			'LB' => 'google.com.lb',
			'LS' => 'google.co.ls',
			'LY' => 'google.com.ly',
			'LI' => 'google.li',
			'LT' => 'google.lt',
			'LU' => 'google.lu',
			'MG' => 'google.mg',
			'MW' => 'google.mw',
			'MY' => 'google.com.my',
			'MV' => 'google.mv',
			'ML' => 'google.ml',
			'MT' => 'google.com.mt',
			'MU' => 'google.mu',
			'MX' => 'google.com.mx',
			'FM' => 'google.fm',
			'MD' => 'google.md',
			'MN' => 'google.mn',
			'ME' => 'google.me',
			'MS' => 'google.ms',
			'MA' => 'google.co.ma',
			'MZ' => 'google.co.mz',
			'MM' => 'google.com.mm',
			'NA' => 'google.com.na',
			'NR' => 'google.nr',
			'NP' => 'google.com.np',
			'NL' => 'google.nl',
			'NZ' => 'google.co.nz',
			'NI' => 'google.com.ni',
			'NE' => 'google.ne',
			'NG' => 'google.com.ng',
			'NU' => 'google.nu',
			'MK' => 'google.mk',
			'NO' => 'google.no',
			'OM' => 'google.com.om',
			'PK' => 'google.com.pk',
			'PS' => 'google.ps',
			'PA' => 'google.com.pa',
			'PG' => 'google.com.pg',
			'PY' => 'google.com.py',
			'PE' => 'google.com.pe',
			'PH' => 'google.com.ph',
			'PN' => 'google.pn',
			'PL' => 'google.pl',
			'PT' => 'google.pt',
			'PR' => 'google.com.pr',
			'QA' => 'google.com.qa',
			'RO' => 'google.ro',
			'RU' => 'google.ru',
			'RW' => 'google.rw',
			'WS' => 'google.as',
			'SM' => 'google.sm',
			'ST' => 'google.st',
			'SA' => 'google.com.sa',
			'SN' => 'google.sn',
			'RS' => 'google.rs',
			'SC' => 'google.sc',
			'SL' => 'google.com.sl',
			'SG' => 'google.com.sg',
			'SK' => 'google.sk',
			'SI' => 'google.si',
			'SB' => 'google.com.sb',
			'SO' => 'google.so',
			'ZA' => 'google.co.za',
			'KR' => 'google.co.kr',
			'ES' => 'google.es',
			'LK' => 'google.lk',
			'SH' => 'google.sh',
			'VC' => 'google.com.vc',
			'SR' => 'google.sr',
			'SE' => 'google.se',
			'CH' => 'google.ch',
			'TW' => 'google.com.tw',
			'TJ' => 'google.com.tj',
			'TZ' => 'google.co.tz',
			'TH' => 'google.co.th',
			'TL' => 'google.tl',
			'TG' => 'google.tg',
			'TO' => 'google.to',
			'TT' => 'google.tt',
			'TN' => 'google.tn',
			'TR' => 'google.com.tr',
			'TM' => 'google.tm',
			'VI' => 'google.co.vi',
			'UG' => 'google.co.ug',
			'UA' => 'google.com.ua',
			'AE' => 'google.ae',
			'GB' => 'google.co.uk',
			'US' => 'google.com',
			'UY' => 'google.com.uy',
			'UZ' => 'google.co.uz',
			'VU' => 'google.vu',
			'VE' => 'google.co.ve',
			'VN' => 'google.com.vn',
			'ZM' => 'google.co.zm',
			'ZW' => 'google.co.zw'
		];

		return $searchEngines;
	}
}Common/Models/WritingAssistantPost.php000066600000006734151135505570014140 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

/**
 * Class Posts.
 *
 * @since 4.7.4
 */
class WritingAssistantPost extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.7.4
	 *
	 * @var string
	 */
	protected $table = 'aioseo_writing_assistant_posts';

	/**
	 * Fields that should be integer values.
	 *
	 * @since 4.7.4
	 *
	 * @var array
	 */
	protected $integerFields = [ 'id', 'post_id', 'keyword_id' ];

	/**
	 * Fields that should be boolean values.
	 *
	 * @since 4.7.4
	 *
	 * @var array
	 */
	protected $booleanFields = [];

	/**
	 * Fields that should be encoded/decoded on save/get.
	 *
	 * @since 4.7.4
	 *
	 * @var array
	 */
	protected $jsonFields = [ 'content_analysis' ];

	/**
	 * Gets a post's content analysis.
	 *
	 * @since 4.7.4
	 *
	 * @param  int   $postId A post ID.
	 * @return array         The post content's analysis.
	 */
	public static function getContentAnalysis( $postId ) {
		$post = self::getPost( $postId );

		return ! empty( $post->content_analysis ) && is_object( $post->content_analysis ) ? (array) $post->content_analysis : [];
	}

	/**
	 * Gets a writing assistant post.
	 *
	 * @since 4.7.4
	 *
	 * @param  int                  $postId A post ID.
	 * @return WritingAssistantPost         The post object.
	 */
	public static function getPost( $postId ) {
		$post = aioseo()->core->db->start( 'aioseo_writing_assistant_posts' )
			->where( 'post_id', $postId )
			->run()
			->model( 'AIOSEO\Plugin\Common\Models\WritingAssistantPost' );

		if ( ! $post->exists() ) {
			$post->post_id = $postId;
		}

		return $post;
	}

	/**
	 * Gets a post's current keyword.
	 *
	 * @since 4.7.4
	 *
	 * @param  int                          $postId A post ID.
	 * @return WritingAssistantKeyword|bool         An attached keyword.
	 */
	public static function getKeyword( $postId ) {
		$post = self::getPost( $postId );
		if ( ! $post->exists() || empty( $post->keyword_id ) ) {
			return false;
		}

		$keyword = aioseo()->core->db->start( 'aioseo_writing_assistant_keywords' )
			->where( 'id', $post->keyword_id )
			->run()
			->model( 'AIOSEO\Plugin\Common\Models\WritingAssistantKeyword' );

		// This is here so this property is reactive in the frontend.
		if ( ! empty( $keyword->keywords ) ) {
			foreach ( $keyword->keywords as &$keyph ) {
				$keyph->contentCount = 0;
			}
		}

		// Help sorting in the frontend.
		if ( ! empty( $keyword->competitors->competitors ) ) {
			foreach ( $keyword->competitors->competitors as &$competitor ) {
				$competitor->wasAnalyzed = true;
				if ( 0 >= $competitor->wordCount ) {
					$competitor->wordCount        = 0;
					$competitor->readabilityScore = 999;
					$competitor->readabilityGrade = '';
					$competitor->gradeScore       = 0;
					$competitor->grade            = '';
					$competitor->wasAnalyzed      = false;
				}

				$competitor->readabilityScore = (float) $competitor->readabilityScore;
			}
		}

		return $keyword;
	}

	/**
	 * Return if a post has a keyword.
	 *
	 * @since 4.7.4
	 *
	 * @param  int     $postId A post ID.
	 * @return boolean         Has a keyword.
	 */
	public static function hasKeyword( $postId ) {
		$post = self::getPost( $postId );

		return (bool) $post->keyword_id;
	}

	/**
	 * Attaches a keyword to a post.
	 *
	 * @since 4.7.4
	 *
	 * @param  int  $keywordId The keyword ID.
	 * @return void
	 */
	public function attachKeyword( $keywordId ) {
		$this->keyword_id = $keywordId;
		$this->save();
	}
}Common/Models/Model.php000066600000025156151135505570011014 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

/**
 * Base Model class.
 *
 * @since 4.0.0
 */
#[\AllowDynamicProperties]
class Model implements \JsonSerializable {
	/**
	 * Fields that can be null when saving to the database.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $nullFields = [];

	/**
	 * Fields that should be encoded/decoded on save/get.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $jsonFields = [];

	/**
	 * Fields that should be boolean values.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $booleanFields = [];

	/**
	 * Fields that should be integer values.
	 *
	 * @since   4.1.0
	 * @version 4.7.3 Renamed from numericFields to integerFields.
	 *
	 * @var array
	 */
	protected $integerFields = [];

	/**
	 * Fields that should be float values.
	 *
	 * @since 4.7.3
	 *
	 * @var array
	 */
	protected $floatFields = [];

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $hidden = [];

	/**
	 * The table used in the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $table = '';

	/**
	 * The primary key retrieved from the table.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $pk = 'id';

	/**
	 * The ID of the model.
	 * This needs to be null in order for MySQL to auto-increment correctly if the NO_AUTO_VALUE_ON_ZERO SQL mode is enabled.
	 *
	 * @since 4.2.7
	 *
	 * @var int|null
	 */
	public $id = null;

	/**
	 * An array of columns from the DB that we can use.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private static $columns = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 *
	 * @param mixed $var This can be the primary key of the resource, or it could be an array of data to manufacture a resource without a database query.
	 */
	public function __construct( $var = null ) {
		$skip = [ 'id', 'created', 'updated' ];
		$fields = [];
		foreach ( $this->getColumns() as $column => $value ) {
			if ( ! in_array( $column, $skip, true ) ) {
				$fields[ $column ] = $value;
			}
		}

		$this->applyKeys( $fields );

		// Process straight through if we were given a valid array.
		if ( is_array( $var ) || is_object( $var ) ) {
			// Apply keys to object.
			$this->applyKeys( $var );

			if ( $this->exists() ) {
				return true;
			}

			return false;
		}

		return $this->loadData( $var );
	}

	/**
	 * Load the data from the database!
	 *
	 * @since 4.0.0
	 *
	 * @param  mixed      $var Generally the primary key to load up the model from the DB.
	 * @return Model|bool      Returns the current object.
	 */
	protected function loadData( $var = null ) {
		// Return false if var is invalid or not supplied.
		if ( null === $var ) {
			return false;
		}

		$query = aioseo()->core->db
			->start( $this->table )
			->where( $this->pk, $var )
			->limit( 1 )
			->output( 'ARRAY_A' );

		$result = $query->run();
		if ( ! $result || $result->nullSet() ) {
			return $this;
		}

		// Apply keys to object.
		$this->applyKeys( $result->result()[0] );

		return $this;
	}

	/**
	 * Take the keys from the result array and add them to the Model.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $array The array of keys and values to add to the Model.
	 * @return void
	 */
	protected function applyKeys( $array ) {
		if ( ! is_object( $array ) && ! is_array( $array ) ) {
			throw new \Exception( '$array must either be an object or an array.' );
		}

		foreach ( (array) $array as $key => $value ) {
			$key = trim( $key );
			$this->$key = $value;

			if ( null === $value && in_array( $key, $this->nullFields, true ) ) {
				continue;
			}

			if ( in_array( $key, $this->jsonFields, true ) ) {
				if ( $value ) {
					$this->$key = is_string( $value ) ? json_decode( $value ) : $value;
				}
				continue;
			}

			if ( in_array( $key, $this->booleanFields, true ) ) {
				$this->$key = (bool) $value;
				continue;
			}

			if ( in_array( $key, $this->integerFields, true ) ) {
				$this->$key = (int) $value;
				continue;
			}

			if ( in_array( $key, $this->floatFields, true ) ) {
				$this->$key = (float) $value;
				continue;
			}
		}
	}

	/**
	 * Let's filter out any properties we cannot save to the database.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $keys The array of keys to filter.
	 * @return array       The array of valid columns for the database query.
	 */
	protected function filter( $keys ) {
		$fields    = [];
		$skip      = [ 'created', 'updated' ];
		$dbColumns = aioseo()->db->getColumns( $this->table );

		foreach ( $dbColumns as $column ) {
			if ( ! in_array( $column, $skip, true ) && array_key_exists( $column, $keys ) ) {
				$fields[ $column ] = $keys[ $column ];
			}
		}

		return $fields;
	}

	/**
	 * Transforms the data to be null if it exists in the nullFields variables.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $data The data array to transform.
	 * @return array       The transformed data.
	 */
	protected function transform( $data, $set = false ) {
		foreach ( $this->nullFields as $field ) {
			if ( isset( $data[ $field ] ) && empty( $data[ $field ] ) ) {
				// Because sitemap prio can both be 0 and null, we need to make sure it's 0 if it's set.
				if ( 'priority' === $field && 0.0 === $data[ $field ] ) {
					continue;
				}

				$data[ $field ] = null;
			}
		}

		foreach ( $this->booleanFields as $field ) {
			if ( isset( $data[ $field ] ) && '' === $data[ $field ] ) {
				unset( $data[ $field ] );
			} elseif ( isset( $data[ $field ] ) ) {
				$data[ $field ] = (bool) $data[ $field ] ? 1 : 0;
			}
		}

		if ( $set ) {
			return $data;
		}

		foreach ( $this->integerFields as $field ) {
			if ( isset( $data[ $field ] ) ) {
				$data[ $field ] = (int) $data[ $field ];
			}
		}

		foreach ( $this->jsonFields as $field ) {
			if ( isset( $data[ $field ] ) && ! aioseo()->helpers->isJsonString( $data[ $field ] ) ) {
				if ( is_array( $data[ $field ] ) && aioseo()->helpers->isArrayNumeric( $data[ $field ] ) ) {
					$data[ $field ] = array_values( $data[ $field ] );
				}
				$data[ $field ] = wp_json_encode( $data[ $field ] );
			}
		}

		return $data;
	}

	/**
	 * Sets a piece of data to the requested resource.
	 *
	 * @since 4.0.0
	 */
	public function set() {
		$args  = func_get_args();
		$count = func_num_args();

		if ( ! is_array( $args[0] ) && $count < 2 ) {
			throw new \Exception( 'The set method must contain at least 2 arguments: key and value. Or an array of data. Only one argument was passed and it was not an array.' );
		}

		$key   = $args[0];
		$value = ! empty( $args[1] ) ? $args[1] : null;

		// Make sure we have a key.
		if ( false === $key ) {
			return false;
		}

		// If it's not an array, make it one.
		if ( ! is_array( $key ) ) {
			$key = [ $key => $value ];
		}

		// Preprocess data.
		$key = $this->transform( $key, true );

		// Save the items in this object.
		foreach ( $key as $k => $v ) {
			if ( ! empty( $k ) ) {
				$this->$k = $v;
			}
		}
	}

	/**
	 * Delete the Model Resource itself.
	 *
	 * @since 4.0.0
	 *
	 * @return null
	 */
	public function delete() {
		if ( ! $this->exists() ) {
			return;
		}

		aioseo()->core->db
			->delete( $this->table )
			->where( $this->pk, $this->id )
			->run();

		return null;
	}

	/**
	 * Saves data to the requested resource.
	 *
	 * @since 4.0.0
	 */
	public function save() {
		$fields = $this->transform( $this->filter( (array) get_object_vars( $this ) ) );

		$id = null;
		if ( count( $fields ) > 0 ) {
			$pk = $this->pk;

			if ( isset( $this->$pk ) && '' !== $this->$pk ) {
				// PK specified.
				$pkv   = $this->$pk;
				$query = aioseo()->core->db
					->start( $this->table )
					->where( [ $pk => $pkv ] )
					->resetCache()
					->run();

				if ( ! $query->nullSet() ) {
					// Row exists in database.
					$fields['updated'] = gmdate( 'Y-m-d H:i:s' );
					aioseo()->core->db
						->update( $this->table )
						->set( $fields )
						->where( [ $pk => $pkv ] )
						->run();
					$id = $this->$pk;
				} else {
					// Row does not exist.
					$fields[ $pk ]     = $pkv;
					$fields['created'] = gmdate( 'Y-m-d H:i:s' );
					$fields['updated'] = gmdate( 'Y-m-d H:i:s' );

					$id = aioseo()->core->db
						->insert( $this->table )
						->set( $fields )
						->run()
						->insertId();

					if ( $id ) {
						$this->$pk = $id;
					}
				}
			} else {
				$fields['created'] = gmdate( 'Y-m-d H:i:s' );
				$fields['updated'] = gmdate( 'Y-m-d H:i:s' );

				$id = aioseo()->core->db
					->insert( $this->table )
					->set( $fields )
					->run()
					->insertId();

				if ( $id ) {
					$this->$pk = $id;
				}
			}
		}

		// Refresh the resource.
		$this->reset( $id );
	}

	/**
	 * Check if the model exists in the database.
	 *
	 * @since 4.0.0
	 *
	 * @return bool If the model exists, true otherwise false.
	 */
	public function exists() {
		return ( ! empty( $this->{$this->pk} ) ) ? true : false;
	}

	/**
	 * Resets a resource by forcing internal updates to be applied.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $id The resource ID.
	 * @return void
	 */
	public function reset( $id = null ) {
		$id = ! empty( $id ) ? $id : $this->{$this->pk};
		$this->__construct( $id );
	}

	/**
	 * Helper function to remove data we don't want serialized.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of data that we are okay with serializing.
	 */
	#[\ReturnTypeWillChange]
	// The attribute above omits a deprecation notice from PHP 8.1 that is thrown because the return type of jsonSerialize() isn't "mixed".
	// Once PHP 7.x is our minimum supported version, this can be removed in favour of overriding the return type in the method signature like this -
	// public function jsonSerialize() : array
	public function jsonSerialize() {
		$array = [];

		foreach ( $this->getColumns() as $column => $value ) {
			if ( in_array( $column, $this->hidden, true ) ) {
				continue;
			}

			$array[ $column ] = isset( $this->$column ) ? $this->$column : null;
		}

		return $array;
	}

	/**
	 * Retrieves the columns for the model.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of columns.
	 */
	public function getColumns() {
		if ( empty( self::$columns[ get_called_class() ] ) ) {
			self::$columns[ get_called_class() ] = [];

			// Let's set the columns that are available by default.
			$table   = aioseo()->core->db->prefix . $this->table;
			$results = aioseo()->core->db->start( $table )
				->output( 'OBJECT' )
				->execute( 'SHOW COLUMNS FROM `' . $table . '`', true )
				->result();

			foreach ( $results as $col ) {
				self::$columns[ get_called_class() ][ $col->Field ] = $col->Default;
			}

			if ( ! empty( $this->appends ) ) {
				foreach ( $this->appends as $append ) {
					self::$columns[ get_called_class() ][ $append ] = null;
				}
			}
		}

		return self::$columns[ get_called_class() ];
	}
}Common/Models/CrawlCleanupLog.php000066600000003434151135505570012771 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

use AIOSEO\Plugin\Common\Models as CommonModels;

/**
 * The Crawl Cleanup Log DB Model.
 *
 * @since 4.5.8
 */
class CrawlCleanupLog extends CommonModels\Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.5.8
	 *
	 * @var string
	 */
	protected $table = 'aioseo_crawl_cleanup_logs';

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @since 4.5.8
	 *
	 * @var array
	 */
	protected $hidden = [ 'id' ];

	/**
	 * Fields that should be numeric values.
	 *
	 * @since 4.5.8
	 *
	 * @var array
	 */
	protected $integerFields = [ 'id', 'hits' ];

	/**
	 * Field to count hits.
	 *
	 * @since 4.5.8
	 *
	 * @var integer
	 */
	public $hits = 0;


	/**
	 * Create a Log in case it doesn't exist.
	 *
	 * @since 4.5.8
	 *
	 * @return void
	 */
	public function create() {
		if ( null !== $this->id ) {
			$this->hits++;
		}

		parent::save();
	}

	/**
	 * Get Crawl Cleanup passing Slug
	 *
	 * @since 4.5.8
	 *
	 * @param  string          $slug The Slug to search.
	 * @return CrawlCleanupLog       The CrawlCleanupLog object.
	 */
	public static function getBySlug( $slug ) {
		return aioseo()->core->db
			->start( 'aioseo_crawl_cleanup_logs' )
			->where( 'hash', sha1( $slug ) )
			->run()
			->model( 'AIOSEO\\Plugin\\Common\\Models\\CrawlCleanupLog' );
	}

	/**
	 * Transforms data as needed.
	 *
	 * @since 4.5.8
	 *
	 * @param  array $data The data array to transform.
	 * @return array       The transformed data.
	 */
	protected function transform( $data, $set = false ) {
		$data = parent::transform( $data, $set );

		// Create slug hash.
		if ( ! empty( $data['slug'] ) ) {
			$data['hash'] = sha1( $data['slug'] );
		}

		return $data;
	}
}Common/Models/Post.php000066600000076370151135505570010705 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

/**
 * The Post DB Model.
 *
 * @since 4.0.0
 */
class Post extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $table = 'aioseo_posts';

	/**
	 * Fields that should be json encoded on save and decoded on get.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $jsonFields = [
		'keywords',
		'keyphrases',
		'page_analysis',
		'schema',
		// 'schema_type_options',
		'images',
		'videos',
		'open_ai',
		'options',
		'local_seo',
		'primary_term',
		'breadcrumb_settings',
		'og_article_tags'
	];

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $hidden = [ 'id' ];

	/**
	 * Fields that should be boolean values.
	 *
	 * @since 4.0.13
	 *
	 * @var array
	 */
	protected $booleanFields = [
		'twitter_use_og',
		'pillar_content',
		'robots_default',
		'robots_noindex',
		'robots_noarchive',
		'robots_nosnippet',
		'robots_nofollow',
		'robots_noimageindex',
		'robots_noodp',
		'robots_notranslate',
		'limit_modified_date',
	];

	/**
	 * Fields that can be null when saved.
	 *
	 * @since 4.5.7
	 *
	 * @var array
	 */
	protected $nullFields = [
		'priority'
	];

	/**
	 * Fields that should be float values.
	 *
	 * @since 4.7.3
	 *
	 * @var array
	 */
	protected $floatFields = [
		'priority'
	];

	/**
	 * Returns a Post with a given ID.
	 *
	 * @since 4.0.0
	 *
	 * @param  int  $postId The post ID.
	 * @return Post         The Post object.
	 */
	public static function getPost( $postId ) {
		// This is needed to prevent an error when upgrading from 4.1.8 to 4.1.9.
		// WordPress deletes the attachment .zip file for the new plugin version after installing it, which triggers the "delete_post" hook.
		// In-between the 4.1.8 to 4.1.9 update, the new Core class does not exist yet, causing the PHP error.
		// TODO: Delete this in a future release.
		$post = new self();
		if ( ! property_exists( aioseo(), 'core' ) ) {
			return $post;
		}

		$post = aioseo()->core->db->start( 'aioseo_posts' )
			->where( 'post_id', $postId )
			->run()
			->model( 'AIOSEO\\Plugin\\Common\\Models\\Post' );

		if ( ! $post->exists() ) {
			$post->post_id = $postId;
			$post          = self::setDynamicDefaults( $post, $postId );
		} else {
			$post = self::runDynamicMigrations( $post );
		}

		// Set options object.
		$post = self::setOptionsDefaults( $post );

		return apply_filters( 'aioseo_get_post', $post );
	}

	/**
	 * Sets the dynamic defaults on the post object if it doesn't exist in the DB yet.
	 *
	 * @since 4.1.4
	 *
	 * @param  Post $post   The Post object.
	 * @param  int  $postId The post ID.
	 * @return Post         The modified Post object.
	 */
	private static function setDynamicDefaults( $post, $postId ) {
		if ( 'page' === get_post_type( $postId ) ) { // This check cannot be deleted and is required to prevent errors after WordPress cleans up the attachment it creates when a plugin is updated.
			$isWooCommerceCheckoutPage = aioseo()->helpers->isWooCommerceCheckoutPage( $postId );
			if (
				$isWooCommerceCheckoutPage ||
				aioseo()->helpers->isWooCommerceCartPage( $postId ) ||
				aioseo()->helpers->isWooCommerceAccountPage( $postId )
			) {
				$post->robots_default = false;
				$post->robots_noindex = true;
			}
		}

		if ( aioseo()->helpers->isStaticHomePage( $postId ) ) {
			$post->og_object_type = 'website';
		}

		$post->twitter_use_og = aioseo()->options->social->twitter->general->useOgData;

		if ( property_exists( $post, 'schema' ) && null === $post->schema ) {
			$post->schema = self::getDefaultSchemaOptions();
		}

		return $post;
	}

	/**
	 * Migrates removed QAPage schema on-the-fly when the post is loaded.
	 *
	 * @since 4.1.8
	 *
	 * @param  Post $aioseoPost The post object.
	 * @return Post             The modified post object.
	 */
	private static function migrateRemovedQaSchema( $aioseoPost ) {
		if ( ! $aioseoPost->schema_type || 'webpage' !== strtolower( $aioseoPost->schema_type ) ) {
			return $aioseoPost;
		}

		$schemaTypeOptions = json_decode( $aioseoPost->schema_type_options );
		if ( 'qapage' !== strtolower( $schemaTypeOptions->webPage->webPageType ) ) {
			return $aioseoPost;
		}

		$schemaTypeOptions->webPage->webPageType = 'WebPage';
		$aioseoPost->schema_type_options         = wp_json_encode( $schemaTypeOptions );
		$aioseoPost->save();

		return $aioseoPost;
	}

	/**
	 * Runs dynamic migrations whenever the post object is loaded.
	 *
	 * @since 4.1.7
	 *
	 * @param  Post $post The Post object.
	 * @return Post       The modified Post object.
	 */
	private static function runDynamicMigrations( $post ) {
		$post = self::migrateRemovedQaSchema( $post );
		$post = self::migrateImageTypes( $post );
		$post = self::runDynamicSchemaMigration( $post );
		$post = self::migrateKoreaCountryCodeSchemas( $post );

		return $post;
	}


	/**
	 * Migrates the post's schema data when it is loaded.
	 *
	 * @since 4.2.5
	 *
	 * @param  Post $post The Post object.
	 * @return Post       The modified Post object.
	 */
	private static function runDynamicSchemaMigration( $post ) {
		if ( ! property_exists( $post, 'schema' ) ) {
			return $post;
		}

		if ( null === $post->schema ) {
			$post = aioseo()->updates->migratePostSchemaHelper( $post );
		}

		// If the schema prop isn't set yet, we want to set it here.
		// We also want to run this regardless of whether it is already set to make sure the default schema graph
		// is correctly propagated on the frontend after changing it.
		$post->schema = self::getDefaultSchemaOptions( $post->schema );

		// Filter out null or empty graphs.
		$post->schema->graphs = array_filter( $post->schema->graphs, function( $graph ) {
			return ! empty( $graph );
		} );

		foreach ( $post->schema->graphs as $graph ) {
			// If the first character of the graph ID isn't a pound, add one.
			// We have to do this because the schema migration in 4.2.5 didn't add the pound for custom graphs.
			if ( property_exists( $graph, 'id' ) && '#' !== substr( $graph->id, 0, 1 ) ) {
				$graph->id = '#' . $graph->id;
			}

			// If the graph has an old rating value, we need to migrate it to the review.
			if (
				property_exists( $graph, 'id' ) &&
				preg_match( '/(movie|software-application)/', (string) $graph->id ) &&
				property_exists( $graph->properties, 'rating' ) &&
				property_exists( $graph->properties->rating, 'value' )
			) {
				$graph->properties->review->rating = $graph->properties->rating->value;
				unset( $graph->properties->rating->value );
			}

			// If the graph has audience data, we need to migrate it to the correct one.
			if (
				property_exists( $graph, 'id' ) &&
				preg_match( '/(product|product-review)/', $graph->id ) &&
				property_exists( $graph->properties, 'audience' )
			) {
				$graph->properties->audience = self::migratePostAudienceAgeSchema( $graph->properties->audience );
			}
		}

		return $post;
	}

	/**
	 * Migrates the post's image types when it is loaded.
	 *
	 * @since 4.2.5
	 *
	 * @param  Post $post The Post object.
	 * @return Post       The modified Post object.
	 */
	private static function migrateImageTypes( $post ) {
		$pageBuilder = aioseo()->helpers->getPostPageBuilderName( $post->post_id );
		if ( ! $pageBuilder ) {
			return $post;
		}

		$deprecatedImageSources = 'seedprod' === strtolower( $pageBuilder )
			? [ 'auto', 'custom', 'featured' ]
			: [ 'auto' ];

		if ( ! empty( $post->og_image_type ) && in_array( $post->og_image_type, $deprecatedImageSources, true ) ) {
			$post->og_image_type = 'default';
		}

		if ( ! empty( $post->twitter_image_type ) && in_array( $post->twitter_image_type, $deprecatedImageSources, true ) ) {
			$post->twitter_image_type = 'default';
		}

		return $post;
	}

	/**
	 * Saves the Post object.
	 *
	 * @since 4.0.3
	 *
	 * @param  int              $postId The Post ID.
	 * @param  array            $data   The post data to save.
	 * @return bool|void|string         Whether the post data was saved or a DB error message.
	 */
	public static function savePost( $postId, $data ) {
		if ( empty( $data ) ) {
			return false;
		}

		$thePost = self::getPost( $postId );
		$data    = apply_filters( 'aioseo_save_post', $data, $thePost );

		// Before setting the data, we check if the title/description are the same as the defaults and clear them if so.
		$data    = self::checkForDefaultFormat( $postId, $thePost, $data );
		$thePost = self::sanitizeAndSetDefaults( $postId, $thePost, $data );

		// Update traditional post meta so that it can be used by multilingual plugins.
		self::updatePostMeta( $postId, $data );

		$thePost->save();
		$thePost->reset();

		$lastError = aioseo()->core->db->lastError();
		if ( ! empty( $lastError ) ) {
			return $lastError;
		}

		// Fires once an AIOSEO post has been saved.
		do_action( 'aioseo_insert_post', $postId );
	}

	/**
	 * Checks if the title/description is the same as their default format in Search Appearance and nulls it if this is the case.
	 * Doing this ensures that updates to the default title/description format also propogate to the post.
	 *
	 * @since 4.1.5
	 *
	 * @param  int   $postId  The post ID.
	 * @param  Post  $thePost The Post object.
	 * @param  array $data    The data.
	 * @return array          The data.
	 */
	private static function checkForDefaultFormat( $postId, $thePost, $data ) {
		$data['title']       = trim( (string) $data['title'] );
		$data['description'] = trim( (string) $data['description'] );

		$post                     = aioseo()->helpers->getPost( $postId );
		$defaultTitleFormat       = trim( aioseo()->meta->title->getPostTypeTitle( $post->post_type ) );
		$defaultDescriptionFormat = trim( aioseo()->meta->description->getPostTypeDescription( $post->post_type ) );
		if ( ! empty( $data['title'] ) && $data['title'] === $defaultTitleFormat ) {
			$data['title'] = null;
		}

		if ( ! empty( $data['description'] ) && $data['description'] === $defaultDescriptionFormat ) {
			$data['description'] = null;
		}

		return $data;
	}

	/**
	 * Sanitize the keyphrases posted data.
	 *
	 * @since 4.2.8
	 *
	 * @param  array $data An array containing the keyphrases field data.
	 * @return array       The sanitized data.
	 */
	private static function sanitizeKeyphrases( $data ) {
		if (
			! empty( $data['focus']['analysis'] ) &&
			is_array( $data['focus']['analysis'] )
		) {
			foreach ( $data['focus']['analysis'] as &$analysis ) {
				// Remove unnecessary 'title' and 'description'.
				unset( $analysis['title'] );
				unset( $analysis['description'] );
			}
		}

		if (
			! empty( $data['additional'] ) &&
			is_array( $data['additional'] )
		) {
			foreach ( $data['additional'] as &$additional ) {
				if (
					! empty( $additional['analysis'] ) &&
					is_array( $additional['analysis'] )
				) {
					foreach ( $additional['analysis'] as &$additionalAnalysis ) {
						// Remove unnecessary 'title' and 'description'.
						unset( $additionalAnalysis['title'] );
						unset( $additionalAnalysis['description'] );
					}
				}
			}
		}

		return $data;
	}

	/**
	 * Sanitize the page_analysis posted data.
	 *
	 * @since 4.2.7
	 *
	 * @param  array $data An array containing the page_analysis field data.
	 * @return array       The sanitized data.
	 */
	private static function sanitizePageAnalysis( $data ) {
		if (
			empty( $data['analysis'] ) ||
			! is_array( $data['analysis'] )
		) {
			return $data;
		}

		foreach ( $data['analysis'] as &$analysis ) {
			foreach ( $analysis as $key => $result ) {
				// Remove unnecessary data.
				foreach ( [ 'title', 'description', 'highlightSentences' ] as $keyToRemove ) {
					if ( isset( $analysis[ $key ][ $keyToRemove ] ) ) {
						unset( $analysis[ $key ][ $keyToRemove ] );
					}
				}
			}
		}

		return $data;
	}

	/**
	 * Sanitizes the post data and sets it (or the default value) to the Post object.
	 *
	 * @since 4.1.5
	 *
	 * @param  int   $postId  The post ID.
	 * @param  Post  $thePost The Post object.
	 * @param  array $data    The data.
	 * @return Post           The Post object with data set.
	 */
	protected static function sanitizeAndSetDefaults( $postId, $thePost, $data ) {
		// General
		$thePost->post_id                     = $postId;
		$thePost->title                       = ! empty( $data['title'] ) ? sanitize_text_field( $data['title'] ) : null;
		$thePost->description                 = ! empty( $data['description'] ) ? sanitize_text_field( $data['description'] ) : null;
		$thePost->canonical_url               = ! empty( $data['canonicalUrl'] ) ? sanitize_text_field( $data['canonicalUrl'] ) : null;
		$thePost->keywords                    = ! empty( $data['keywords'] ) ? aioseo()->helpers->sanitize( $data['keywords'] ) : null;
		$thePost->pillar_content              = isset( $data['pillar_content'] ) ? rest_sanitize_boolean( $data['pillar_content'] ) : 0;
		// TruSEO
		$thePost->keyphrases                  = ! empty( $data['keyphrases'] ) ? self::sanitizeKeyphrases( $data['keyphrases'] ) : null;
		$thePost->page_analysis               = ! empty( $data['page_analysis'] ) ? self::sanitizePageAnalysis( $data['page_analysis'] ) : null;
		$thePost->seo_score                   = ! empty( $data['seo_score'] ) ? sanitize_text_field( $data['seo_score'] ) : 0;
		// Sitemap
		$thePost->priority                    = isset( $data['priority'] ) ? ( 'default' === sanitize_text_field( $data['priority'] ) ? null : (float) $data['priority'] ) : null;
		$thePost->frequency                   = ! empty( $data['frequency'] ) ? sanitize_text_field( $data['frequency'] ) : 'default';
		// Robots Meta
		$thePost->robots_default              = isset( $data['default'] ) ? rest_sanitize_boolean( $data['default'] ) : 1;
		$thePost->robots_noindex              = isset( $data['noindex'] ) ? rest_sanitize_boolean( $data['noindex'] ) : 0;
		$thePost->robots_nofollow             = isset( $data['nofollow'] ) ? rest_sanitize_boolean( $data['nofollow'] ) : 0;
		$thePost->robots_noarchive            = isset( $data['noarchive'] ) ? rest_sanitize_boolean( $data['noarchive'] ) : 0;
		$thePost->robots_notranslate          = isset( $data['notranslate'] ) ? rest_sanitize_boolean( $data['notranslate'] ) : 0;
		$thePost->robots_noimageindex         = isset( $data['noimageindex'] ) ? rest_sanitize_boolean( $data['noimageindex'] ) : 0;
		$thePost->robots_nosnippet            = isset( $data['nosnippet'] ) ? rest_sanitize_boolean( $data['nosnippet'] ) : 0;
		$thePost->robots_noodp                = isset( $data['noodp'] ) ? rest_sanitize_boolean( $data['noodp'] ) : 0;
		$thePost->robots_max_snippet          = isset( $data['maxSnippet'] ) && is_numeric( $data['maxSnippet'] ) ? (int) sanitize_text_field( $data['maxSnippet'] ) : -1;
		$thePost->robots_max_videopreview     = isset( $data['maxVideoPreview'] ) && is_numeric( $data['maxVideoPreview'] ) ? (int) sanitize_text_field( $data['maxVideoPreview'] ) : -1;
		$thePost->robots_max_imagepreview     = ! empty( $data['maxImagePreview'] ) ? sanitize_text_field( $data['maxImagePreview'] ) : 'large';
		// Open Graph Meta
		$thePost->og_title                    = ! empty( $data['og_title'] ) ? sanitize_text_field( $data['og_title'] ) : null;
		$thePost->og_description              = ! empty( $data['og_description'] ) ? sanitize_text_field( $data['og_description'] ) : null;
		$thePost->og_object_type              = ! empty( $data['og_object_type'] ) ? sanitize_text_field( $data['og_object_type'] ) : 'default';
		$thePost->og_image_type               = ! empty( $data['og_image_type'] ) ? sanitize_text_field( $data['og_image_type'] ) : 'default';
		$thePost->og_image_url                = null; // We'll reset this below.
		$thePost->og_image_width              = null; // We'll reset this below.
		$thePost->og_image_height             = null; // We'll reset this below.
		$thePost->og_image_custom_url         = ! empty( $data['og_image_custom_url'] ) ? esc_url_raw( $data['og_image_custom_url'] ) : null;
		$thePost->og_image_custom_fields      = ! empty( $data['og_image_custom_fields'] ) ? sanitize_text_field( $data['og_image_custom_fields'] ) : null;
		$thePost->og_video                    = ! empty( $data['og_video'] ) ? sanitize_text_field( $data['og_video'] ) : '';
		$thePost->og_article_section          = ! empty( $data['og_article_section'] ) ? sanitize_text_field( $data['og_article_section'] ) : null;
		$thePost->og_article_tags             = ! empty( $data['og_article_tags'] ) ? aioseo()->helpers->sanitize( $data['og_article_tags'] ) : null;
		// Twitter Meta
		$thePost->twitter_title               = ! empty( $data['twitter_title'] ) ? sanitize_text_field( $data['twitter_title'] ) : null;
		$thePost->twitter_description         = ! empty( $data['twitter_description'] ) ? sanitize_text_field( $data['twitter_description'] ) : null;
		$thePost->twitter_use_og              = isset( $data['twitter_use_og'] ) ? rest_sanitize_boolean( $data['twitter_use_og'] ) : 0;
		$thePost->twitter_card                = ! empty( $data['twitter_card'] ) ? sanitize_text_field( $data['twitter_card'] ) : 'default';
		$thePost->twitter_image_type          = ! empty( $data['twitter_image_type'] ) ? sanitize_text_field( $data['twitter_image_type'] ) : 'default';
		$thePost->twitter_image_url           = null; // We'll reset this below.
		$thePost->twitter_image_custom_url    = ! empty( $data['twitter_image_custom_url'] ) ? esc_url_raw( $data['twitter_image_custom_url'] ) : null;
		$thePost->twitter_image_custom_fields = ! empty( $data['twitter_image_custom_fields'] ) ? sanitize_text_field( $data['twitter_image_custom_fields'] ) : null;
		// Schema
		$thePost->schema                      = ! empty( $data['schema'] ) ? self::getDefaultSchemaOptions( $data['schema'] ) : null;
		$thePost->local_seo                   = ! empty( $data['local_seo'] ) ? $data['local_seo'] : null;
		$thePost->limit_modified_date         = isset( $data['limit_modified_date'] ) ? rest_sanitize_boolean( $data['limit_modified_date'] ) : 0;
		$thePost->open_ai                     = ! empty( $data['open_ai'] ) ? self::getDefaultOpenAiOptions( $data['open_ai'] ) : null;
		$thePost->updated                     = gmdate( 'Y-m-d H:i:s' );
		$thePost->primary_term                = ! empty( $data['primary_term'] ) ? $data['primary_term'] : null;
		$thePost->breadcrumb_settings         = isset( $data['breadcrumb_settings']['default'] ) && false === $data['breadcrumb_settings']['default'] ? $data['breadcrumb_settings'] : null;

		// Before we determine the OG/Twitter image, we need to set the meta data cache manually because the changes haven't been saved yet.
		aioseo()->meta->metaData->bustPostCache( $thePost->post_id, $thePost );

		// Set the OG/Twitter image data.
		$thePost = self::setOgTwitterImageData( $thePost );

		if ( ! $thePost->exists() ) {
			$thePost->created = gmdate( 'Y-m-d H:i:s' );
		}

		// Update defaults from addons.
		foreach ( aioseo()->addons->getLoadedAddons() as $addon ) {
			if ( isset( $addon->postModel ) && method_exists( $addon->postModel, 'sanitizeAndSetDefaults' ) ) {
				$thePost = $addon->postModel->sanitizeAndSetDefaults( $postId, $thePost, $data );
			}
		}

		return $thePost;
	}

	/**
	 * Set the OG/Twitter image data on the post object.
	 *
	 * @since 4.1.6
	 *
	 * @param  Post $thePost The Post object to modify.
	 * @return Post          The modified Post object.
	 */
	public static function setOgTwitterImageData( $thePost ) {
		// Set the OG image.
		if (
			in_array( $thePost->og_image_type, [
				'featured',
				'content',
				'attach',
				'custom',
				'custom_image'
			], true )
		) {
			// Disable the cache.
			aioseo()->social->image->useCache = false;

			// Set the image details.
			$ogImage                  = aioseo()->social->facebook->getImage( $thePost->post_id );
			$thePost->og_image_url    = is_array( $ogImage ) ? $ogImage[0] : $ogImage;
			$thePost->og_image_width  = aioseo()->social->facebook->getImageWidth();
			$thePost->og_image_height = aioseo()->social->facebook->getImageHeight();

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

		// Set the Twitter image.
		if (
			! $thePost->twitter_use_og &&
			in_array( $thePost->twitter_image_type, [
				'featured',
				'content',
				'attach',
				'custom',
				'custom_image'
			], true )
		) {
			// Disable the cache.
			aioseo()->social->image->useCache = false;

			// Set the image details.
			$ogImage                    = aioseo()->social->twitter->getImage( $thePost->post_id );
			$thePost->twitter_image_url = is_array( $ogImage ) ? $ogImage[0] : $ogImage;

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

		return $thePost;
	}

	/**
	 * Saves some of the data as post meta so that it can be used for localization.
	 *
	 * @since 4.1.5
	 *
	 * @param  int   $postId The post ID.
	 * @param  array $data   The data.
	 * @return void
	 */
	public static function updatePostMeta( $postId, $data ) {
		// Update the post meta as well for localization.
		$keywords      = ! empty( $data['keywords'] ) ? aioseo()->helpers->jsonTagsToCommaSeparatedList( $data['keywords'] ) : [];
		$ogArticleTags = ! empty( $data['og_article_tags'] ) ? aioseo()->helpers->jsonTagsToCommaSeparatedList( $data['og_article_tags'] ) : [];

		update_post_meta( $postId, '_aioseo_title', $data['title'] );
		update_post_meta( $postId, '_aioseo_description', $data['description'] );
		update_post_meta( $postId, '_aioseo_keywords', $keywords );
		update_post_meta( $postId, '_aioseo_og_title', $data['og_title'] );
		update_post_meta( $postId, '_aioseo_og_description', $data['og_description'] );
		update_post_meta( $postId, '_aioseo_og_article_section', $data['og_article_section'] );
		update_post_meta( $postId, '_aioseo_og_article_tags', $ogArticleTags );
		update_post_meta( $postId, '_aioseo_twitter_title', $data['twitter_title'] );
		update_post_meta( $postId, '_aioseo_twitter_description', $data['twitter_description'] );
	}

	/**
	 * Returns the default values for the TruSEO page analysis.
	 *
	 * @since 4.0.0
	 *
	 * @param  object|null $pageAnalysis The page analysis object.
	 * @return object                    The default values.
	 */
	public static function getPageAnalysisDefaults( $pageAnalysis = null ) {
		$defaults = [
			'analysis' => [
				'basic'       => [
					'lengthContent' => [
						'error'    => 1,
						'maxScore' => 9,
						'score'    => 6,
					],
				],
				'title'       => [
					'titleLength' => [
						'error'    => 1,
						'maxScore' => 9,
						'score'    => 1,
					],
				],
				'readability' => [
					'contentHasAssets' => [
						'error'    => 1,
						'maxScore' => 5,
						'score'    => 0,
					],
				]
			]
		];

		if ( empty( $pageAnalysis ) ) {
			return json_decode( wp_json_encode( $defaults ) );
		}

		return $pageAnalysis;
	}

	/**
	 * Returns a JSON object with default schema options.
	 *
	 * @since 4.2.5
	 *
	 * @param  string        $existingOptions The existing options in JSON.
	 * @param  null|\WP_Post $post            The post object.
	 * @return object                         The existing options with defaults added in JSON.
	 */
	public static function getDefaultSchemaOptions( $existingOptions = '', $post = null ) {
		$defaultGraphName = aioseo()->schema->getDefaultPostTypeGraph( $post );

		$defaults = [
			'blockGraphs'  => [],
			'customGraphs' => [],
			'default'      => [
				'data'      => [
					'Article'             => [],
					'Course'              => [],
					'Dataset'             => [],
					'FAQPage'             => [],
					'Movie'               => [],
					'Person'              => [],
					'Product'             => [],
					'ProductReview'       => [],
					'Car'                 => [],
					'Recipe'              => [],
					'Service'             => [],
					'SoftwareApplication' => [],
					'WebPage'             => []
				],
				'graphName' => $defaultGraphName,
				'isEnabled' => true,
			],
			'graphs'       => []
		];

		if ( empty( $existingOptions ) ) {
			return json_decode( wp_json_encode( $defaults ) );
		}

		$existingOptions = json_decode( wp_json_encode( $existingOptions ), true );
		$existingOptions = array_replace_recursive( $defaults, $existingOptions );

		if ( isset( $existingOptions['defaultGraph'] ) && ! empty( $existingOptions['defaultPostTypeGraph'] ) ) {
			$existingOptions['default']['isEnabled'] = ! empty( $existingOptions['defaultGraph'] );

			unset( $existingOptions['defaultGraph'] );
			unset( $existingOptions['defaultPostTypeGraph'] );
		}

		// Reset the default graph type to make sure it's accurate.
		if ( $defaultGraphName ) {
			$existingOptions['default']['graphName'] = $defaultGraphName;
		}

		return json_decode( wp_json_encode( $existingOptions ) );
	}

	/**
	 * Returns the defaults for the keyphrases column.
	 *
	 * @since 4.1.7
	 *
	 * @param  null|object $keyphrases The database keyphrases.
	 * @return object                  The defaults.
	 */
	public static function getKeyphrasesDefaults( $keyphrases = null ) {
		$defaults = [
			'focus'      => [
				'keyphrase' => '',
				'score'     => 0,
				'analysis'  => [
					'keyphraseInTitle' => [
						'score'    => 0,
						'maxScore' => 9,
						'error'    => 1
					]
				]
			],
			'additional' => []
		];

		if ( empty( $keyphrases ) ) {
			return json_decode( wp_json_encode( $defaults ) );
		}

		if ( empty( $keyphrases->focus ) ) {
			$keyphrases->focus = $defaults['focus'];
		}

		if ( empty( $keyphrases->additional ) ) {
			$keyphrases->additional = $defaults['additional'];
		}

		return $keyphrases;
	}

	/**
	 * Returns the defaults for the options column.
	 *
	 * @since   4.2.2
	 * @version 4.2.9
	 *
	 * @param  Post $post   The Post object.
	 * @return Post         The modified Post object.
	 */
	public static function setOptionsDefaults( $post ) {
		$defaults = [
			'linkFormat'  => [
				'internalLinkCount'      => 0,
				'linkAssistantDismissed' => false
			],
			'primaryTerm' => [
				'productEducationDismissed' => false
			]
		];

		if ( empty( $post->options ) ) {
			$post->options = json_decode( wp_json_encode( $defaults ) );

			return $post;
		}

		$post->options = json_decode( wp_json_encode( $post->options ), true );
		$post->options = array_replace_recursive( $defaults, $post->options );
		$post->options = json_decode( wp_json_encode( $post->options ) );

		return $post;
	}

	/**
	 * Returns the default Open AI options.
	 *
	 * @since 4.3.2
	 *
	 * @param  array $existingOptions The existing options.
	 * @return object                 The default options.
	 */
	public static function getDefaultOpenAiOptions( $existingOptions = [] ) {
		$defaults = [
			'title'       => [
				'suggestions' => [],
				'usage'       => 0
			],
			'description' => [
				'suggestions' => [],
				'usage'       => 0
			]
		];

		if ( empty( $existingOptions ) ) {
			return json_decode( wp_json_encode( $defaults ) );
		}

		$existingOptions = json_decode( wp_json_encode( $existingOptions ), true );
		$existingOptions = array_replace_recursive( $defaults, $existingOptions );

		return json_decode( wp_json_encode( $existingOptions ) );
	}

	/**
	 * Returns the default breadcrumb settings options.
	 *
	 * @since 4.8.3
	 *
	 * @param  array  $postType        The post type.
	 * @param  array  $existingOptions The existing options.
	 * @return object                  The default options.
	 */
	public static function getDefaultBreadcrumbSettingsOptions( $postType, $existingOptions = [] ) {
		$default       = aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->useDefaultTemplate ?? true;
		$showHomeCrumb = $default ? aioseo()->options->breadcrumbs->homepageLink : aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->showHomeCrumb ?? true;

		$defaults = [
			'default'            => true,
			'separator'          => aioseo()->options->breadcrumbs->separator,
			'showHomeCrumb'      => $showHomeCrumb ?? true,
			'showTaxonomyCrumbs' => aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->showTaxonomyCrumbs ?? true,
			'showParentCrumbs'   => aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->showParentCrumbs ?? true,
			'template'           => aioseo()->helpers->encodeOutputHtml( aioseo()->breadcrumbs->frontend->getDefaultTemplate( 'single' ) ),
			'parentTemplate'     => aioseo()->helpers->encodeOutputHtml( aioseo()->breadcrumbs->frontend->getDefaultTemplate( 'single' ) ),
			'taxonomy'           => aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->taxonomy ?? '',
			'primaryTerm'        => null
		];

		if ( empty( $existingOptions ) ) {
			return json_decode( wp_json_encode( $defaults ) );
		}

		$existingOptions = json_decode( wp_json_encode( $existingOptions ), true );
		$existingOptions = array_replace_recursive( $defaults, $existingOptions );

		return json_decode( wp_json_encode( $existingOptions ) );
	}

	/**
	 * Migrates the post's audience age schema data when it is loaded.
	 * Min age: [0 => newborns, 0.25 => infants, 1 => toddlers, 5 => kids, 13 => adults]
	 * Max age: [0.25 => newborns, 1 => infants, 5 => toddlers, 13 => kids]
	 *
	 * @since 4.7.9
	 *
	 * @param  object $audience The audience data.
	 * @return object
	 */
	public static function migratePostAudienceAgeSchema( $audience ) {
		$ages = [ 0, 0.25, 1, 5, 13 ];

		// converts variable to integer if it's a number otherwise is null.
		$parsedMinAge = filter_var( $audience->minimumAge, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE );
		$parsedMaxAge = filter_var( $audience->maximumAge, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE );

		if ( null === $parsedMinAge && null === $parsedMaxAge ) {
			return $audience;
		}

		$minAge = is_numeric( $parsedMinAge ) ? $parsedMinAge : 0;
		$maxAge = is_numeric( $parsedMaxAge ) ? $parsedMaxAge : null;

		// get the minimumAge if available or the nearest bigger one.
		foreach ( $ages as $age ) {
			if ( $age >= $minAge ) {
				$audience->minimumAge = $age;
				break;
			}
		}

		// get the maximumAge if available or the nearest bigger one.
		foreach ( $ages as $age ) {
			if ( $age >= $maxAge ) {
				$maxAge = $age;
				break;
			}
		}

		// makes sure the maximumAge is 13 below
		if ( null !== $maxAge ) {
			$audience->maximumAge = 13 < $maxAge ? 13 : $maxAge;
		}

		// Minimum age 13 is for adults.
		// If minimumAge is still higher or equal 13 then it's for adults and maximumAge should be empty.
		if ( 13 <= $audience->minimumAge ) {
			$audience->minimumAge = 13;
			$audience->maximumAge = null;
		}

		return $audience;
	}

	/**
	 * Migrates update Korea country code for Person, Product, Event, and JobsPosting schemas.
	 *
	 * @since 4.7.1
	 *
	 * @param  Post $aioseoPost The post object.
	 * @return Post             The modified post object.
	 */
	private static function migrateKoreaCountryCodeSchemas( $aioseoPost ) {
		if ( empty( $aioseoPost->schema ) || empty( $aioseoPost->schema->graphs ) ) {
			return $aioseoPost;
		}

		foreach ( $aioseoPost->schema->graphs as $key => $graph ) {
			if ( isset( $aioseoPost->schema->graphs[ $key ]->properties->location->country ) ) {
				$aioseoPost->schema->graphs[ $key ]->properties->location->country = self::invertKoreaCode( $graph->properties->location->country );
			}

			if ( isset( $aioseoPost->schema->graphs[ $key ]->properties->shippingDestinations ) ) {
				$aioseoPost->schema->graphs[ $key ]->properties->shippingDestinations = array_map( function( $item ) {
					$item->country = self::invertKoreaCode( $item->country );

					return $item;
				}, $graph->properties->shippingDestinations );
			}
		}

		$aioseoPost->save();

		return $aioseoPost;
	}

	/**
	 * Utility function to invert the country code for Korea.
	 *
	 * @since 4.7.1
	 *
	 * @param  string $code country code.
	 * @return string       country code.
	 */
	public static function invertKoreaCode( $code ) {
		return 'KP' === $code ? 'KR' : $code;
	}
}Common/Models/SeoAnalyzerResult.php000066600000011075151135505570013402 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

/**
 * The SeoAnalyzerResult Model.
 *
 * @since 4.8.3
 */
class SeoAnalyzerResult extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.8.3
	 *
	 * @var string
	 */
	protected $table = 'aioseo_seo_analyzer_results';

	/**
	 * Fields that should be json encoded on save and decoded on get.
	 *
	 * @since 4.8.3
	 *
	 * @var array
	 */
	protected $jsonFields = [
		'data'
	];

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @since 4.8.3
	 *
	 * @var array
	 */
	protected $hidden = [ 'id' ];

	/**
	 * Fields that can be null when saved.
	 *
	 * @since 4.8.3
	 *
	 * @var array
	 */
	protected $nullFields = [
		'competitor_url',
	];

	/**
	 * An array of columns from the DB that we can use.
	 *
	 * @since 4.8.3
	 *
	 * @var array
	 */
	protected $columns = [
		'id',
		'score',
		'data',
		'competitor_url',
		'created',
		'updated',
	];

	/**
	 * Returns all not competitors results.
	 *
	 * @since 4.8.3
	 *
	 * @return array List of results.
	 */
	public static function getResults() {
		$results = aioseo()->core->db->start( 'aioseo_seo_analyzer_results' )
			->select( '*' )
			->where( 'competitor_url', null )
			->run()
			->result();

		if ( empty( $results ) ) {
			return [];
		}

		return self::parseObjects( $results );
	}

	/**
	 * Returns all competitors results.
	 *
	 * @since 4.8.3
	 *
	 * @return array List of results.
	 */
	public static function getCompetitorsResults() {
		$results = aioseo()->core->db->start( 'aioseo_seo_analyzer_results' )
			->select( '*' )
			->whereRaw( 'competitor_url IS NOT NULL' )
			->orderBy( 'updated DESC' )
			->run()
			->result();

		if ( empty( $results ) ) {
			return [];
		}

		return self::parseObjects( $results, true );
	}

	/**
	 * Parse results to the front end format.
	 *
	 * @since 4.8.3
	 *
	 * @param  array $objects      List of objects.
	 * @param  bool  $isCompetitor Flag that indicates if is parsing a competitor or a homepage result.
	 * @return array               List of results.
	 */
	private static function parseObjects( $objects, $isCompetitor = false ) {
		$results = [];

		foreach ( $objects as $obj ) {
			$data = json_decode( $obj->data ?? '[]', true );

			if ( ! $isCompetitor ) {
				$results['score'] = $obj->score ?? 0;
			}

			foreach ( $data as $result ) {
				$metadata = $result['metadata'] ?? [];
				$item     = empty( $result['status'] ) && ! empty( $metadata['value'] ) ? $metadata['value'] : array_merge( $metadata, [ 'status' => $result['status'] ] );

				if ( $isCompetitor ) {
					if ( empty( $obj->competitor_url ) || empty( $result['group'] ) || empty( $result['name'] ) ) {
						continue;
					}

					$results[ $obj->competitor_url ]['results'][ $result['group'] ][ $result['name'] ] = $item;
					$results[ $obj->competitor_url ]['score'] = ! empty( $obj->score ) ? $obj->score : 0;
				} else {
					$results['results'][ $result['group'] ][ $result['name'] ] = $item;
				}
			}
		}

		return $results;
	}

	/**
	 * Delete results by competitor url, if null we are deleting the homepage results.
	 *
	 * @since 4.8.3
	 *
	 * @param  string $url The competitor url.
	 * @return void
	 */
	public static function deleteByUrl( $url ) {
		aioseo()->core->db
			->delete( 'aioseo_seo_analyzer_results' )
			->where( 'competitor_url', $url )
			->run();
	}

	/**
	 * Add multiple results at once.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	public static function addResults( $results, $competitorUrl = null ) {
		if ( empty( $results['results'] ) ) {
			return;
		}

		// Delete the results for the competitor url if it exists.
		self::deleteByUrl( $competitorUrl );

		$data = [
			'competitor_url' => $competitorUrl,
			'score'          => $results['score'],
			'data'           => []
		];

		foreach ( $results['results'] as $group => $items ) {
			foreach ( $items as $name => $result ) {
				$fields = [
					'name'     => $name,
					'group'    => $group,
					'status'   => empty( $result['status'] ) ? null : $result['status'],
					'metadata' => null,
				];

				if ( ! is_array( $result ) ) {
					$fields['metadata'] = [ 'value' => $result ];
				} else {
					$metadata = [];
					foreach ( $result as $key => $value ) {
						if ( 'status' !== $key ) {
							$metadata[ $key ] = $value;
						}
					}

					if ( ! empty( $metadata ) ) {
						$fields['metadata'] = $metadata;
					}
				}

				$data['data'][] = $fields;
			}
		}

		$data['data'] = wp_json_encode( $data['data'] );
		$newResult = new SeoAnalyzerResult( $data );
		$newResult->save();
	}
}Common/Models/Notification.php000066600000023010151135505570012365 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

/**
 * The Notification DB Model.
 *
 * @since 4.0.0
 */
class Notification extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $table = 'aioseo_notifications';

	/**
	 * An array of fields to set to null if already empty when saving to the database.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $nullFields = [
		'start',
		'end',
		'notification_id',
		'notification_name',
		'button1_label',
		'button1_action',
		'button2_label',
		'button2_action'
	];

	/**
	 * Fields that should be json encoded on save and decoded on get.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $jsonFields = [ 'level' ];

	/**
	 * Fields that should be boolean values.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $booleanFields = [ 'dismissed' ];

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @var array
	 */
	protected $hidden = [ 'id' ];

	/**
	 * An array of fields attached to this resource.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $columns = [
		'id',
		'slug',
		'addon',
		'title',
		'content',
		'type',
		'level',
		'notification_id',
		'notification_name',
		'start',
		'end',
		'button1_label',
		'button1_action',
		'button2_label',
		'button2_action',
		'dismissed',
		'new',
		'created',
		'updated'
	];

	/**
	 * Get the list of notifications.
	 *
	 * @since 4.1.3
	 *
	 * @param  bool  $reset Whether or not to reset the notifications.
	 * @return array        An array of notifications.
	 */
	public static function getNotifications( $reset = true ) {
		static $notifications = null;
		if ( null !== $notifications ) {
			return $notifications;
		}

		$notifications = [
			'active'    => self::getAllActiveNotifications(),
			'new'       => self::getNewNotifications( $reset ),
			'dismissed' => self::getAllDismissedNotifications()
		];

		return $notifications;
	}

	/**
	 * Get an array of active notifications.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of active notifications.
	 */
	public static function getAllActiveNotifications() {
		static $activeNotifications = null;
		if ( null !== $activeNotifications ) {
			return $activeNotifications;
		}

		$staticNotifications = self::getStaticNotifications();
		$notifications       = array_values( json_decode( wp_json_encode( self::getActiveNotifications() ), true ) );

		$activeNotifications = ! empty( $staticNotifications )
			? array_merge( $staticNotifications, $notifications )
			: $notifications;

		return $activeNotifications;
	}

	/**
	 * Get all new notifications. After retrieving them, this will reset them.
	 * This means that calling this method twice will result in no results
	 * the second time. The only exception is to pass false as a reset variable to prevent it.
	 *
	 * @since 4.1.3
	 *
	 * @param  bool  $reset Whether or not to reset the new notifications.
	 * @return array        An array of new notifications if any exist.
	 */
	public static function getNewNotifications( $reset = true ) {
		static $newNotifications = null;
		if ( null !== $newNotifications ) {
			return $newNotifications;
		}

		$newNotifications = self::filterNotifications(
			aioseo()->core->db
				->start( 'aioseo_notifications' )
				->where( 'dismissed', 0 )
				->where( 'new', 1 )
				->whereRaw( "(start <= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR start IS NULL)" )
				->whereRaw( "(end >= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR end IS NULL)" )
				->orderBy( 'start DESC' )
				->orderBy( 'created DESC' )
				->run()
				->models( 'AIOSEO\\Plugin\\Common\\Models\\Notification' )
		);

		if ( $reset ) {
			self::resetNewNotifications();
		}

		return $newNotifications;
	}

	/**
	 * Resets all new notifications.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	public static function resetNewNotifications() {
		aioseo()->core->db
			->update( 'aioseo_notifications' )
			->where( 'new', 1 )
			->set( 'new', 0 )
			->run();
	}

	/**
	 * Returns all static notifications.
	 *
	 * @since 4.1.2
	 *
	 * @return array An array of static notifications.
	 */
	public static function getStaticNotifications() {
		$staticNotifications = [];
		$notifications       = [
			'unlicensed-addons',
			'review'
		];

		foreach ( $notifications as $notification ) {
			switch ( $notification ) {
				case 'review':
					// If they intentionally dismissed the main notification, we don't show the repeat one.
					$originalDismissed = get_user_meta( get_current_user_id(), '_aioseo_plugin_review_dismissed', true );
					if ( '4' !== $originalDismissed ) {
						break;
					}

					$dismissed = get_user_meta( get_current_user_id(), '_aioseo_notification_plugin_review_dismissed', true );
					if ( '3' === $dismissed ) {
						break;
					}

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

					$activated = aioseo()->internalOptions->internal->firstActivated( time() );
					if ( $activated > strtotime( '-20 days' ) ) {
						break;
					}

					$isV3                  = get_option( 'aioseop_options' ) || get_option( 'aioseo_options_v3' );
					$staticNotifications[] = [
						'slug'      => 'notification-' . $notification,
						'component' => 'notifications-' . $notification . ( $isV3 ? '' : '2' )
					];
					break;
				case 'unlicensed-addons':
					$unlicensedAddons = aioseo()->addons->unlicensedAddons();
					if ( empty( $unlicensedAddons['addons'] ) ) {
						break;
					}

					$staticNotifications[] = [
						'slug'      => 'notification-' . $notification,
						'component' => 'notifications-' . $notification,
						'addons'    => $unlicensedAddons['addons'],
						'message'   => $unlicensedAddons['message']
					];
					break;
			}
		}

		return $staticNotifications;
	}

	/**
	 * Retrieve active notifications.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of active notifications or empty.
	 */
	protected static function getActiveNotifications() {
		return self::filterNotifications(
			aioseo()->core->db
				->start( 'aioseo_notifications' )
				->where( 'dismissed', 0 )
				->whereRaw( "(start <= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR start IS NULL)" )
				->whereRaw( "(end >= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR end IS NULL)" )
				->orderBy( 'start DESC' )
				->orderBy( 'created DESC' )
				->run()
				->models( 'AIOSEO\\Plugin\\Common\\Models\\Notification' )
		);
	}

	/**
	 * Get an array of dismissed notifications.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of dismissed notifications.
	 */
	protected static function getAllDismissedNotifications() {
		return array_values( json_decode( wp_json_encode( self::getDismissedNotifications() ), true ) );
	}

	/**
	 * Retrieve dismissed notifications.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of dismissed notifications or empty.
	 */
	protected static function getDismissedNotifications() {
		static $dismissedNotifications = null;
		if ( null !== $dismissedNotifications ) {
			return $dismissedNotifications;
		}

		$dismissedNotifications = self::filterNotifications(
			aioseo()->core->db
				->start( 'aioseo_notifications' )
				->where( 'dismissed', 1 )
				->orderBy( 'updated DESC' )
				->run()
				->models( 'AIOSEO\\Plugin\\Common\\Models\\Notification' )
		);

		return $dismissedNotifications;
	}

	/**
	 * Returns a notification by its name.
	 *
	 * @since 4.0.0
	 *
	 * @param  string       $name The notification name.
	 * @return Notification       The notification.
	 */
	public static function getNotificationByName( $name ) {
		return aioseo()->core->db
			->start( 'aioseo_notifications' )
			->where( 'notification_name', $name )
			->run()
			->model( 'AIOSEO\\Plugin\\Common\\Models\\Notification' );
	}

	/**
	 * Stores a new notification in the DB.
	 *
	 * @since 4.0.0
	 *
	 * @param  array        $fields       The fields.
	 * @return Notification $notification The notification.
	 */
	public static function addNotification( $fields ) {
		// Set the dismissed status to false.
		$fields['dismissed'] = 0;

		$notification = new self();
		$notification->set( $fields );
		$notification->save();

		return $notification;
	}

	/**
	 * Deletes a notification by its name.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name The notification name.
	 * @return void
	 */
	public static function deleteNotificationByName( $name ) {
		aioseo()->core->db
			->delete( 'aioseo_notifications' )
			->where( 'notification_name', $name )
			->run();
	}

	/**
	 * Filters the notifications based on the targeted plan levels.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $notifications          The notifications
	 * @return array $remainingNotifications The remaining notifications.
	 */
	protected static function filterNotifications( $notifications ) {
		$remainingNotifications = [];
		foreach ( $notifications as $notification ) {
			// If announcements are disabled and this is an announcement, skip adding it and move on.
			if (
				! aioseo()->options->advanced->announcements &&
				'success' === $notification->type
			) {
				continue;
			}

			// If this is an addon notification and the addon is disabled, skip adding it and move on.
			if ( ! empty( $notification->addon ) && ! aioseo()->addons->getLoadedAddon( $notification->addon ) ) {
				continue;
			}

			$levels = $notification->level;
			if ( ! is_array( $levels ) ) {
				$levels = empty( $notification->level ) ? [ 'all' ] : [ $notification->level ];
			}

			foreach ( $levels as $level ) {
				if ( ! aioseo()->notices->validateType( $level ) ) {
					continue 2;
				}
			}

			$remainingNotifications[] = $notification;
		}

		return $remainingNotifications;
	}
}Common/Models/WritingAssistantKeyword.php000066600000003047151135505570014631 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

/**
 * Class Keyword.
 *
 * @since 4.7.4
 */
class WritingAssistantKeyword extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.7.4
	 *
	 * @var string
	 */
	protected $table = 'aioseo_writing_assistant_keywords';

	/**
	 * Fields that should be numeric values.
	 *
	 * @since 4.7.4
	 *
	 * @var array
	 */
	protected $integerFields = [ 'id', 'progress' ];

	/**
	 * Fields that should be boolean values.
	 *
	 * @since 4.7.4
	 *
	 * @var array
	 */
	protected $booleanFields = [];

	/**
	 * Fields that should be encoded/decoded on save/get.
	 *
	 * @since 4.7.4
	 *
	 * @var array
	 */
	protected $jsonFields = [ 'keywords', 'competitors', 'content_analysis' ];

	/**
	 * Gets a keyword.
	 *
	 * @since 4.7.4
	 *
	 * @param  string $keyword  A keyword.
	 * @param  string $country  The country code.
	 * @param  string $language The language code.
	 * @return object           A keyword found.
	 */
	public static function getKeyword( $keyword, $country, $language ) {
		$dbKeyword = aioseo()->core->db->start( 'aioseo_writing_assistant_keywords' )
			->where( 'keyword', $keyword )
			->where( 'country', $country )
			->where( 'language', $language )
			->run()
			->model( 'AIOSEO\Plugin\Common\Models\WritingAssistantKeyword' );

		if ( ! $dbKeyword->exists() ) {
			$dbKeyword->keyword  = $keyword;
			$dbKeyword->country  = $country;
			$dbKeyword->language = $language;
		}

		return $dbKeyword;
	}
}Common/Models/CrawlCleanupBlockedArg.php000066600000012252151135505570014243 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

use AIOSEO\Plugin\Common\Models as CommonModels;
/**
 * The Crawl Cleanup Blocked Arg DB Model.
 *
 * @since 4.5.8
 */
class CrawlCleanupBlockedArg extends CommonModels\Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.5.8
	 *
	 * @var string
	 */
	protected $table = 'aioseo_crawl_cleanup_blocked_args';

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @since 4.5.8
	 *
	 * @var array
	 */
	protected $hidden = [ 'id' ];

	/**
	 * Fields that should be numeric values.
	 *
	 * @since 4.5.8
	 *
	 * @var array
	 */
	protected $integerFields = [ 'id', 'hits' ];

	/**
	 * Field to count hits.
	 *
	 * @since 4.5.8
	 *
	 * @var integer
	 */
	protected $hits = 0;

	/**
	 * Field for Regex.
	 *
	 * @since 4.5.8
	 *
	 * @var string
	 */
	public $regex = null;

	/**
	 * Field that contains the hash for key+value
	 *
	 * @since 4.5.8
	 *
	 * @var string
	 */
	public $key_value_hash = null;

	/**
	 * Separator used to merge key and value string.
	 *
	 * @since 4.5.8
	 *
	 * @var string
	 */
	private static $keyValueSeparator = '=';

	/**
	 * Separator used to merge key and value string.
	 *
	 * @since 4.5.8
	 *
	 * @var CrawlCleanupBlockedArg|null
	 */
	private static $regexBlockedArgs = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.5.8
	 *
	 * @param mixed $var This can be the primary key of the resource, or it could be an array of data to manufacture a resource without a database query.
	 */
	public function __construct( $var = null ) {
		parent::__construct( $var );
	}

	/**
	 * Get Blocked row using Key and Value.
	 *
	 * @since 4.5.8
	 *
	 * @param  string                 $key   The key to search.
	 * @param  string                 $value The value to search.
	 * @return CrawlCleanupBlockedArg        The CrawlCleanupBlockedArg object.
	 */
	public static function getByKeyValue( $key, $value ) {
		$keyValue = self::getKeyValueString( $key, $value );

		return aioseo()->core->db
			->start( 'aioseo_crawl_cleanup_blocked_args' )
			->where( 'key_value_hash', sha1( $keyValue ) )
			->run()
			->model( 'AIOSEO\\Plugin\\Common\\Models\\CrawlCleanupBlockedArg' );
	}

	/**
	 * Get Blocked row using Regex Value.
	 *
	 * @since 4.5.8
	 *
	 * @param  string                 $regex The regex value to search.
	 * @return CrawlCleanupBlockedArg        The CrawlCleanupBlockedArg object.
	 */
	public static function getByRegex( $regex ) {
		return aioseo()->core->db
			->start( 'aioseo_crawl_cleanup_blocked_args' )
			->where( 'regex', $regex )
			->run()
			->model( 'AIOSEO\\Plugin\\Common\\Models\\CrawlCleanupBlockedArg' );
	}

	/**
	 * Look for regex match by key and value.
	 *
	 * @since 4.5.8
	 *
	 * @param  string                 $key   The key to search.
	 * @param  string                 $value The value to search.
	 * @return CrawlCleanupBlockedArg        The CrawlCleanupBlockedArg object.
	 */
	public static function matchRegex( $key, $value ) {
		$keyValue = self::getKeyValueString( $key, $value );
		$regexBlockedArgs = self::getRegexBlockedArgs();

		foreach ( $regexBlockedArgs as $regexQueryArg ) {
			$escapedRegex = str_replace( '@', '\@', $regexQueryArg->regex );
			if ( preg_match( "@{$escapedRegex}@", (string) $keyValue ) ) {
				return new CrawlCleanupBlockedArg( $regexQueryArg->id );
			}
		}

		return new CrawlCleanupBlockedArg();
	}

	/**
	 * Get Regex rows.
	 *
	 * @since 4.5.8
	 *
	 * @return CrawlCleanupBlockedArg The CrawlCleanupBlockedArg object.
	 */
	public static function getRegexBlockedArgs() {
		if ( null === self::$regexBlockedArgs ) {
			self::$regexBlockedArgs = aioseo()->core->db
				->start( 'aioseo_crawl_cleanup_blocked_args' )
				->select( 'id, regex' )
				->whereRaw( 'regex IS NOT NULL' )
				->run()
				->result();
		}

		return self::$regexBlockedArgs;
	}

	/**
	 * Transforms data as needed.
	 *
	 * @since 4.5.8
	 *
	 * @param  array $data The data array to transform.
	 * @return array       The transformed data.
	 */
	protected function transform( $data, $set = false ) {
		$data = parent::transform( $data, $set );

		// Create key+value hash.
		if ( ! empty( $data['key'] ) ) {
			$keyValue = self::getKeyValueString( $data['key'], $data['value'] );
			$data['key_value_hash'] = sha1( $keyValue );
		}

		// Case hits number are empty start with 0.
		if ( empty( $data['hits'] ) ) {
			$data['hits'] = 0;
		}

		return $data;
	}

	/**
	 * Increase hits and save.
	 *
	 * @since 4.5.8
	 *
	 */
	public function addHit() {
		if ( $this->id ) {
			$this->hits++;
			parent::save();
		}
	}

	/**
	 * Return string with key and value with pattern model defined.
	 *
	 * @since 4.5.8
	 *
	 * @param  string $key   The key to merge.
	 * @param  string $value The value to merge.
	 * @return string        The result string merging key and value (case not empty).
	 */
	public static function getKeyValueString( $key, $value ) {
		return $key . ( $value ? self::getKeyValueSeparator() . $value : '' );
	}

	/**
	 * Return string to separate key and value.
	 *
	 * @since 4.5.8
	 *
	 * @return string The separator for key and value.
	 */
	public static function getKeyValueSeparator() {
		return self::$keyValueSeparator;
	}
}Common/QueryArgs/CrawlCleanup.php000066600000024060151135505570013024 0ustar00<?php
namespace AIOSEO\Plugin\Common\QueryArgs;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Class to control Crawl Cleanup.
 *
 * @since 4.5.8
 */
class CrawlCleanup {

	/**
	 * Construct method.
	 *
	 * @since 4.5.8
	 */
	public function __construct() {
		// Add action to clear crawl cleanup logs.
		add_action( 'aioseo_crawl_cleanup_clear_logs', [ $this, 'clearLogs' ] );

		if ( aioseo()->options->searchAppearance->advanced->blockArgs->optimizeUtmParameters ) {
			add_action( 'template_redirect', [ $this, 'maybeRedirectUtmParameters' ], 50 );
		}
	}

	/**
	 * Redirects the UTM parameters to with (#) equivalent.
	 *
	 * @since 4.8.0
	 *
	 * @return void
	 */
	public function maybeRedirectUtmParameters() {
		$requestUri = aioseo()->helpers->getRequestUrl();
		if ( empty( $requestUri ) ) {
			return;
		}

		$parsed = wp_parse_url( $requestUri );
		if ( empty( $parsed['query'] ) ) {
			return;
		}

		$args = [];
		wp_parse_str( $parsed['query'], $args );

		// Reset query to reconstruct without utm_ parameters.
		$parsed['query'] = '';

		// Initialize the fragment key if it's not set.
		if ( ! isset( $parsed['fragment'] ) ) {
			$parsed['fragment'] = '';
		}

		// Check if there are any utm_ parameters and redirect accordingly.
		$utmFound = false;
		foreach ( $args as $key => $value ) {
			$keyValue = $key . '=' . $value;
			if ( 0 === stripos( $key, 'utm_' ) ) {
				$utmFound = true;
				// Rebuild the URL with # instead of ?.
				$parsed['fragment'] .= ! empty( $parsed['fragment'] ) ? '&' . $keyValue : $keyValue;
			} else {
				$parsed['query'] .= ! empty( $parsed['query'] ) ? '&' . $keyValue : $keyValue;
			}
		}

		if ( $utmFound ) {
			aioseo()->helpers->redirect( aioseo()->helpers->buildUrl( $parsed ), 301, 'Optimize UTM parameters' );
		}
	}

	/**
	 * Schedule clearing of the logs.
	 *
	 * @since 4.5.8
	 *
	 * @return void
	 */
	public function scheduleClearingLogs() {
		aioseo()->actionScheduler->unschedule( 'aioseo_crawl_cleanup_clear_logs' );
		$optionLength = json_decode( aioseo()->options->searchAppearance->advanced->blockArgs->logsRetention )->value;
		if (
			aioseo()->options->searchAppearance->advanced->blockArgs->enable &&
			'forever' !== $optionLength
		) {
			aioseo()->actionScheduler->scheduleRecurrent( 'aioseo_crawl_cleanup_clear_logs', 0, HOUR_IN_SECONDS );
		}
	}

	/**
	 * Clears the logs.
	 *
	 * @since 4.5.8
	 *
	 * @return void
	 */
	public function clearLogs() {
		$optionLength = json_decode( aioseo()->options->searchAppearance->advanced->blockArgs->logsRetention )->value;
		if ( 'forever' === $optionLength ) {
			return;
		}

		$date = gmdate( 'Y-m-d H:i:s', strtotime( '-1 ' . $optionLength ) );
		aioseo()->core->db
			->delete( 'aioseo_crawl_cleanup_logs' )
			->where( 'updated <', $date )
			->run();
	}

	/**
	 * Fetch Crawl Cleanup Logs.
	 *
	 * @since 4.5.8
	 *
	 * @param  \WP_REST_Request  $request The REST Request.
	 * @return \WP_REST_Response          The response.
	 */
	public static function fetchLogs( $request ) {
		$filter            = $request->get_param( 'filter' );
		$body              = $request->get_json_params();
		$orderByUnblocked  = ! empty( $body['orderBy'] ) ? sanitize_text_field( $body['orderBy'] ) : 'logs.updated';
		$orderByBlocked    = ! empty( $body['orderBy'] ) ? sanitize_text_field( $body['orderBy'] ) : 'b.id';
		$orderDir          = ! empty( $body['orderDir'] ) && ! empty( $body['orderBy'] ) ? strtoupper( sanitize_text_field( $body['orderDir'] ) ) : 'DESC';
		$limit             = ! empty( $body['limit'] ) ? intval( $body['limit'] ) : aioseo()->settings->tablePagination['queryArgs'];
		$offset            = ! empty( $body['offset'] ) ? intval( $body['offset'] ) : 0;
		$searchTerm        = ! empty( $body['searchTerm'] ) ? sanitize_text_field( $body['searchTerm'] ) : null;
		$keyValueSeparator = Models\CrawlCleanupBlockedArg::getKeyValueSeparator();
		$dateFormat        = get_option( 'date_format' );
		$timeFormat        = get_option( 'time_format' );
		$dateTimeFormat    = $dateFormat . ' ' . $timeFormat;

		// Query to get Arg Logs (unblocked) and the total.
		$queryUnblocked = aioseo()->core->db
			->start( 'aioseo_crawl_cleanup_logs as logs' )
			->select( ' logs.id,
						logs.slug,
						logs.key,
						logs.value,
						logs.hits,
						logs.updated' )
			->leftJoin( 'aioseo_crawl_cleanup_blocked_args as blocked',
				'blocked.key_value_hash = sha1(logs.key) OR
					blocked.key_value_hash = sha1(concat(logs.key, "' . $keyValueSeparator . '", logs.value))' )
			->limit( $limit, $offset );

		if ( ! empty( $searchTerm ) ) {
			// Apply escape to the search term.
			$searchTerm = esc_sql( aioseo()->core->db->db->esc_like( $searchTerm ) );
			$where = '
				(
					logs.slug LIKE \'%' . $searchTerm . '%\' OR
					logs.slug LIKE \'%' . str_replace( '%20', '-', $searchTerm ) . '%\' OR
					logs.slug LIKE \'%' . str_replace( '%20', '+', $searchTerm ) . '%\'
				)
			';

			$queryUnblocked->whereRaw( $where );
		}

		$queryUnblocked->where( 'blocked.id', null );
		$queryUnblocked->orderBy( "$orderByUnblocked $orderDir" );

		$rowsUnblocked = $queryUnblocked->run( false )->result();
		$totalUnblocked = $queryUnblocked->reset( [ 'limit' ] )->count();

		// Test logs (unblocked) to see if have some regex block.
		$regexMatches = [];
		foreach ( $rowsUnblocked as $unblocked ) {
			$blockedRegex = Models\CrawlCleanupBlockedArg::matchRegex( $unblocked->key, $unblocked->value );
			if ( $blockedRegex->exists() ) {
				$regexMatches[ $unblocked->id ] = $blockedRegex->regex;
			}
		}

		// Query to get Blocked Args and the total.
		$queryBlocked = aioseo()->core->db
			->select( ' b.id,
						b.key,
						b.value,
						b.regex,
						b.hits,
						b.updated' )
			->start( 'aioseo_crawl_cleanup_blocked_args as b' )
			->limit( $limit, $offset );

		if ( ! empty( $searchTerm ) ) {
			// Escape (esc_like) has already been applied.
			$searchTerms = [
				$searchTerm,
				str_replace( '%20', '-', $searchTerm ),
				str_replace( '%20', '+', $searchTerm )
			];

			$comparisons = [
				'b.key',
				'b.value',
				'b.regex',
				'CONCAT(b.key, \'' . $keyValueSeparator . '\', IF(b.value, b.value, \'*\'))'
			];

			$where = '';
			foreach ( $comparisons as $comparison ) {
				foreach ( $searchTerms as $s ) {
					if ( ! empty( $where ) ) {
						$where .= ' OR ';
					}

					$where .= aioseo()->db->db->prepare( " $comparison LIKE %s ", '%' . $s . '%' );
				}
			}

			$where = "( $where )";
			$queryBlocked->whereRaw( $where );
		}

		$queryBlocked->orderBy( "$orderByBlocked $orderDir" );

		$rowsBlocked = $queryBlocked->run( false )->result();
		$totalBlocked = $queryBlocked->reset( [ 'limit' ] )->count();

		switch ( $filter ) {
			case 'blocked':
				$total = $totalBlocked;
				$rows = $rowsBlocked;
				break;
			case 'unblocked':
				$total = $totalUnblocked;
				$rows = $rowsUnblocked;
				break;
			default:
				return new \WP_REST_Response( [
					'success' => false
				], 404 );
		}

		foreach ( $rows as $row ) {
			$row->updated = get_date_from_gmt( $row->updated, $dateTimeFormat );
		}

		return new \WP_REST_Response( [
			'success' => true,
			'rows'    => $rows,
			'regex'   => $regexMatches,
			'totals'  => [
				'total' => $total,
				'pages' => 0 === $total ? 1 : ceil( $total / $limit ),
				'page'  => 0 === $offset ? 1 : ( $offset / $limit ) + 1
			],
			'filters' => [
				[
					'slug'   => 'unblocked',
					'name'   => __( 'Unblocked', 'all-in-one-seo-pack' ),
					'count'  => $totalUnblocked,
					'active' => 'unblocked' === $filter
				],
				[
					'slug'   => 'blocked',
					'name'   => __( 'Blocked', 'all-in-one-seo-pack' ),
					'count'  => $totalBlocked,
					'active' => 'blocked' === $filter
				]
			]
		], 200 );
	}

	/**
	 * Set block Arg Query.
	 *
	 * @since 4.5.8
	 *
	 * @param  \WP_REST_Request  $request The REST Request.
	 * @return \WP_REST_Response          The response.
	 */
	public static function blockArg( $request ) {
		$body      = $request->get_json_params();
		$return    = true;
		$listSaved = [];
		$exists    = [];
		$error     = 0;

		try {
			foreach ( $body as $block ) {
				if ( $block ) {
					$blocked = Models\CrawlCleanupBlockedArg::getByKeyValue( $block['key'], $block['value'] );
					if ( ! $blocked->exists() && ! empty( $block['regex'] ) ) {
						$blocked = Models\CrawlCleanupBlockedArg::getByRegex( $block['regex'] );
					}

					if ( $blocked->exists() ) {
						$exists[] = [
							'key'   => $block['key'],
							'value' => $block['value']
						];

						$keyValue = sha1( Models\CrawlCleanupBlockedArg::getKeyValueString( $block['key'], $block['value'] ) );
						if ( ! in_array( $keyValue, $listSaved, true ) ) {
							$return = false;
							$error  = 1;
						}

						continue;
					}

					$blocked = new Models\CrawlCleanupBlockedArg();
					$blocked->set( $block );
					$blocked->save();

					$listSaved[] = $blocked->key_value_hash;
				}
			}
		} catch ( \Throwable $th ) {
			$return = false;
		}

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

	/**
	 * Delete Blocked Arg.
	 *
	 * @since 4.5.8
	 *
	 * @param  \WP_REST_Request  $request The REST Request.
	 * @return \WP_REST_Response          The response.
	 */
	public static function deleteBlocked( $request ) {
		$body = $request->get_json_params();
		$return = true;

		try {
			foreach ( $body as $block ) {
				$blocked = new Models\CrawlCleanupBlockedArg( $block );
				if ( $blocked->exists() ) {
					$blocked->delete();
				}
			}
		} catch ( \Throwable $th ) {
			$return = false;
		}

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

	/**
	 * Delete Log.
	 *
	 * @since 4.5.8
	 *
	 * @param  \WP_REST_Request  $request The REST Request.
	 * @return \WP_REST_Response          The response.
	 */
	public static function deleteLog( $request ) {
		$body = $request->get_json_params();
		$return = true;

		try {
			foreach ( $body as $block ) {
				$log = new Models\CrawlCleanupLog( $block );
				if ( $log->exists() ) {
					$log->delete();
				}
			}
		} catch ( \Throwable $th ) {
			$return = false;
		}

		return new \WP_REST_Response( [
			'success' => $return
		], 200 );
	}
}Common/SearchStatistics/Site.php000066600000007077151135505570012717 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchStatistics;

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

/**
 * Handles the site for the search statistics.
 *
 * @since 4.6.2
 */
class Site {
	/**
	 * The action name.
	 *
	 * @since 4.6.2
	 *
	 * @var string
	 */
	public $action = 'aioseo_search_statistics_site_check';

	/**
	 * Class constructor.
	 *
	 * @since 4.6.2
	 */
	public function __construct() {
		add_action( 'admin_init', [ $this, 'init' ] );
		add_action( $this->action, [ $this, 'worker' ] );
	}

	/**
	 * Initialize the class.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	public function init() {
		if (
			! aioseo()->searchStatistics->api->auth->isConnected() ||
			aioseo()->actionScheduler->isScheduled( $this->action )
		) {
			return;
		}

		aioseo()->actionScheduler->scheduleAsync( $this->action );
	}

	/**
	 * Check whether the site is verified on Google Search Console and verifies it if needed.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	public function worker() {
		if ( ! aioseo()->searchStatistics->api->auth->isConnected() ) {
			return;
		}

		$siteStatus = $this->checkStatus();
		if ( empty( $siteStatus ) ) {
			// If it failed to communicate with the server, try again in a few hours.
			aioseo()->actionScheduler->scheduleSingle( $this->action, wp_rand( HOUR_IN_SECONDS, 2 * HOUR_IN_SECONDS ), [], true );

			return;
		}

		$this->processStatus( $siteStatus );

		// Schedule a new check for the next week.
		aioseo()->actionScheduler->scheduleSingle( $this->action, WEEK_IN_SECONDS + wp_rand( 0, 3 * DAY_IN_SECONDS ), [], true );
	}

	/**
	 * Maybe verifies the site on Google Search Console.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	public function maybeVerify() {
		if ( ! aioseo()->searchStatistics->api->auth->isConnected() ) {
			return;
		}

		$siteStatus = $this->checkStatus();
		if ( empty( $siteStatus ) ) {
			return;
		}

		$this->processStatus( $siteStatus );
	}

	/**
	 * Checks the site status on Google Search Console.
	 *
	 * @since 4.6.2
	 *
	 * @return array The site status.
	 */
	private function checkStatus() {
		$api      = new Api\Request( 'google-search-console/site/check/' );
		$response = $api->request();

		if ( is_wp_error( $response ) ) {
			return [];
		}

		return $response;
	}

	/**
	 * Processes the site status.
	 *
	 * @since 4.6.3
	 *
	 * @param  array $siteStatus The site status.
	 * @return void
	 */
	private function processStatus( $siteStatus ) {
		switch ( $siteStatus['code'] ) {
			case 'site_verified':
				aioseo()->internalOptions->searchStatistics->site->verified  = true;
				aioseo()->internalOptions->searchStatistics->site->lastFetch = time();
				break;
			case 'verification_needed':
				$this->verify( $siteStatus['data'] );
				break;
			case 'site_not_found':
			case 'couldnt_get_token':
			default:
				aioseo()->internalOptions->searchStatistics->site->verified  = false;
				aioseo()->internalOptions->searchStatistics->site->lastFetch = time();
		}
	}

	/**
	 * Verifies the site on Google Search Console.
	 *
	 * @since 4.6.2
	 *
	 * @param  string $token The verification token.
	 * @return void
	 */
	private function verify( $token = '' ) {
		if ( empty( $token ) ) {
			return;
		}

		aioseo()->options->webmasterTools->google = esc_attr( $token );

		$api      = new Api\Request( 'google-search-console/site/verify/' );
		$response = $api->request();

		if ( is_wp_error( $response ) || 'site_verified' !== $response['code'] ) {
			return;
		}

		aioseo()->internalOptions->searchStatistics->site->verified  = true;
		aioseo()->internalOptions->searchStatistics->site->lastFetch = time();
	}
}Common/SearchStatistics/IndexStatus.php000066600000023210151135505570014251 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchStatistics;

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

/**
 * Index Status class.
 *
 * @since 4.8.2
 */
class IndexStatus {
	/**
	 * Retrieves the overview data.
	 *
	 * @since 4.8.2
	 *
	 * @return array The overview data.
	 */
	public function getOverview() {
		$data = [
			'post' => [
				'results' => [
					[
						'count'         => 164,
						'coverageState' => 'Submitted and Indexed', // No need to translate this. It's translated on the front-end.
					],
					[
						'count'         => 112,
						'coverageState' => 'Discovered - Currently Not Indexed',
					],
					[
						'count'         => 44,
						'coverageState' => 'Crawled - Currently Not Indexed',
					],
					[
						'count'         => 8,
						'coverageState' => 'URL is unknown to Google',
					]
				]
			]
		];

		$data['post']['total'] = array_sum( array_column( $data['post']['results'], 'count' ) );

		return $data;
	}

	/**
	 * Retrieves all the objects, formatted.
	 *
	 * @since 4.8.2
	 *
	 * @return array The formatted objects.
	 */
	public function getFormattedObjects() {
		$siteUrl = aioseo()->helpers->getSiteUrl();

		$rows = [
			[
				'objectId'             => 4,
				'objectTitle'          => 'Homepage',
				'verdict'              => 'PASS',
				'coverageState'        => 'Submitted and Indexed',
				'robotsTxtState'       => 'ALLOWED',
				'indexingState'        => 'INDEXING_ALLOWED',
				'pageFetchState'       => 'SUCCESSFUL',
				'crawledAs'            => 'MOBILE',
				'lastCrawlTime'        => aioseo()->helpers->dateToWpFormat( '2025-01-05 13:54:00' ),
				'userCanonical'        => $siteUrl,
				'googleCanonical'      => $siteUrl,
				'sitemap'              => [
					aioseo()->sitemap->helpers->getUrl( 'general' )
				],
				'referringUrls'        => [],
				'richResultsResult'    => [
					'detectedItems' => [
						[
							'richResultType' => 'Breadcrumbs',
							'items'          => [
								[
									'name' => 'Unnamed item'
								]
							]
						],
						[
							'richResultType' => 'FAQ',
							'items'          => [
								[
									'name' => 'Unnamed item'
								]
							]
						]
					]
				],
				'inspectionResultLink' => '#',
				'richResultsTestLink'  => '#'
			],
			[
				'objectId'             => 6,
				'objectTitle'          => 'About',
				'verdict'              => 'PASS',
				'coverageState'        => 'Submitted and Indexed',
				'robotsTxtState'       => 'ALLOWED',
				'indexingState'        => 'INDEXING_ALLOWED',
				'pageFetchState'       => 'SUCCESSFUL',
				'crawledAs'            => 'MOBILE',
				'lastCrawlTime'        => aioseo()->helpers->dateToWpFormat( '2025-01-06 09:22:00' ),
				'userCanonical'        => $siteUrl . '/about',
				'googleCanonical'      => $siteUrl . '/about',
				'sitemap'              => [
					aioseo()->sitemap->helpers->getUrl( 'general' )
				],
				'referringUrls'        => [
					$siteUrl
				],
				'richResultsResult'    => [
					'detectedItems' => [
						[
							'richResultType' => 'Breadcrumbs',
							'items'          => [
								[
									'name' => 'Unnamed item'
								]
							]
						]
					]
				],
				'inspectionResultLink' => '#',
				'richResultsTestLink'  => '#'
			],
			[
				'objectId'             => 1,
				'objectTitle'          => 'Contact Us',
				'verdict'              => 'PASS',
				'coverageState'        => 'Submitted and Indexed',
				'robotsTxtState'       => 'ALLOWED',
				'indexingState'        => 'INDEXING_ALLOWED',
				'pageFetchState'       => 'SUCCESSFUL',
				'crawledAs'            => 'DESKTOP',
				'lastCrawlTime'        => aioseo()->helpers->dateToWpFormat( '2025-01-02 16:47:00' ),
				'userCanonical'        => $siteUrl . '/contact-us',
				'googleCanonical'      => $siteUrl . '/contact-us',
				'sitemap'              => [
					aioseo()->sitemap->helpers->getUrl( 'general' )
				],
				'referringUrls'        => [
					$siteUrl
				],
				'richResultsResult'    => [
					'detectedItems' => [
						[
							'richResultType' => 'Breadcrumbs',
							'items'          => [
								[
									'name' => 'Unnamed item'
								]
							]
						],
						[
							'richResultType' => 'FAQ',
							'items'          => [
								[
									'name' => 'Unnamed item'
								]
							]
						]
					]
				],
				'inspectionResultLink' => '#',
				'richResultsTestLink'  => '#'
			],
			[
				'objectId'             => 2,
				'objectTitle'          => 'Pricing',
				'verdict'              => 'NEUTRAL',
				'coverageState'        => 'Crawled - Currently Not Indexed',
				'robotsTxtState'       => 'DISALLOWED',
				'indexingState'        => 'BLOCKED_BY_META_TAG',
				'pageFetchState'       => 'SUCCESSFUL',
				'crawledAs'            => 'DESKTOP',
				'lastCrawlTime'        => aioseo()->helpers->dateToWpFormat( '2024-01-15 11:00:00' ),
				'userCanonical'        => $siteUrl . '/pricing',
				'googleCanonical'      => $siteUrl . '/pricing',
				'sitemap'              => [
					aioseo()->sitemap->helpers->getUrl( 'general' )
				],
				'referringUrls'        => [
					$siteUrl
				],
				'richResultsResult'    => [
					'detectedItems' => [
						[
							'richResultType' => 'Breadcrumbs',
							'items'          => [
								[
									'name' => 'Unnamed item'
								]
							]
						],
						[
							'richResultType' => 'Product snippet',
							'items'          => [
								[
									'name'   => 'All in One SEO (AIOSEO)',
									'issues' => [
										[
											'issueMessage' => 'Missing field "priceValidUntil"',
											'severity'     => 'WARNING'
										]
									]
								]
							]
						]
					]
				],
				'inspectionResultLink' => '#',
				'richResultsTestLink'  => '#'
			],
			[
				'objectId'             => 3,
				'objectTitle'          => 'Blog',
				'verdict'              => 'PASS',
				'coverageState'        => 'Submitted and Indexed',
				'robotsTxtState'       => 'ALLOWED',
				'indexingState'        => 'INDEXED',
				'pageFetchState'       => 'SUCCESSFUL',
				'crawledAs'            => 'MOBILE',
				'lastCrawlTime'        => aioseo()->helpers->dateToWpFormat( '2024-03-01 08:00:00' ),
				'userCanonical'        => $siteUrl . '/blog',
				'googleCanonical'      => $siteUrl . '/blog',
				'sitemap'              => [
					aioseo()->sitemap->helpers->getUrl( 'general' )
				],
				'referringUrls'        => [
					$siteUrl
				],
				'inspectionResultLink' => '#',
				'richResultsTestLink'  => '#'
			],
		];

		return [
			'paginated' => [
				'rows'   => $rows,
				'totals' => [
					'total' => count( $rows ),
					'pages' => 1,
					'page'  => 1
				]
			]
		];
	}

	/**
	 * Returns the data for Vue.
	 *
	 * @since 4.8.2
	 *
	 * @return array The data for Vue.
	 */
	public function getVueData() {
		return [
			'objects'  => $this->getFormattedObjects(),
			'overview' => $this->getOverview(),
			'options'  => $this->getUiOptions()
		];
	}

	/**
	 * Retrieves options ideally only for Vue usage on the front-end.
	 *
	 * @since 4.8.2
	 *
	 * @return array The options.
	 */
	protected function getUiOptions() {
		$postTypeOptions = [
			[
				'label' => __( 'All Post Types', 'all-in-one-seo-pack' ),
				'value' => ''
			],
			[
				'label' => __( 'Post', 'all-in-one-seo-pack' ),
				'value' => 'post'
			],
			[
				'label' => __( 'Page', 'all-in-one-seo-pack' ),
				'value' => 'page'
			]
		];

		$statusOptions = [
			[
				'label' => __( 'Status (All)', 'all-in-one-seo-pack' ),
				'value' => ''
			],
			[
				'label' => __( 'Indexed', 'all-in-one-seo-pack' ),
				'value' => 'submitted',
				'color' => '#00AA63',
			],
			[
				'label' => __( 'Crawled, Not Indexed', 'all-in-one-seo-pack' ),
				'value' => 'crawled',
				'color' => '#F18200',
			],
			[
				'label' => __( 'Discovered, Not Indexed', 'all-in-one-seo-pack' ),
				'value' => 'discovered',
				'color' => '#005AE0',
			],
			[
				'label' => __( 'Other, Not Indexed', 'all-in-one-seo-pack' ),
				'value' => 'unknown|excluded|invalid|error',
				'color' => '#DF2A4A',
			],
			[
				'label' => __( 'No Results Yet', 'all-in-one-seo-pack' ),
				'value' => 'empty',
				'color' => '#999999',
			]
		];

		$robotsTxtStateOptions = [
			[
				'label' => __( 'Robots.txt (All)', 'all-in-one-seo-pack' ),
				'value' => ''
			],
			[
				'label' => __( 'Allowed', 'all-in-one-seo-pack' ),
				'value' => 'ALLOWED'
			],
			[
				'label' => __( 'Blocked', 'all-in-one-seo-pack' ),
				'value' => 'DISALLOWED'
			]
		];

		$crawledAsOptions = [
			[
				'label' => __( 'Crawled As (All)', 'all-in-one-seo-pack' ),
				'value' => ''
			],
			[
				'label' => __( 'Desktop', 'all-in-one-seo-pack' ),
				'value' => 'DESKTOP'
			],
			[
				'label' => __( 'Mobile', 'all-in-one-seo-pack' ),
				'value' => 'MOBILE'
			]
		];

		$pageFetchStateOptions = [
			[
				'label' => __( 'Page Fetch (All)', 'all-in-one-seo-pack' ),
				'value' => ''
			],
			[
				'label' => __( 'Successful', 'all-in-one-seo-pack' ),
				'value' => 'SUCCESSFUL'
			],
			[
				'label' => __( 'Error', 'all-in-one-seo-pack' ),
				'value' => 'SOFT_404,BLOCKED_ROBOTS_TXT,NOT_FOUND,ACCESS_DENIED,SERVER_ERROR,REDIRECT_ERROR,ACCESS_FORBIDDEN,BLOCKED_4XX,INTERNAL_CRAWL_ERROR,INVALID_URL'
			]
		];

		$additionalFilters = [
			'postTypeOptions'       => [
				'name'    => 'postType',
				'options' => $postTypeOptions
			],
			'statusOptions'         => [
				'name'    => 'status',
				'options' => $statusOptions
			],
			'robotsTxtStateOptions' => [
				'name'    => 'robotsTxtState',
				'options' => $robotsTxtStateOptions
			],
			'pageFetchStateOptions' => [
				'name'    => 'pageFetchState',
				'options' => $pageFetchStateOptions
			],
			'crawledAsOptions'      => [
				'name'    => 'crawledAs',
				'options' => $crawledAsOptions
			],
		];

		return [
			'table' => [
				'additionalFilters' => $additionalFilters
			]
		];
	}
}Common/SearchStatistics/Notices.php000066600000013237151135505570013412 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchStatistics;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Handles the notices for the Search Statistics.
 *
 * @since 4.6.2
 */
class Notices {
	/**
	 * Class constructor.
	 *
	 * @since 4.6.2
	 */
	public function __construct() {
		if ( ! is_admin() ) {
			return;
		}

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

	/**
	 * Initialize the class.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	public function init() {
		$this->siteConnected();
		$this->siteVerified();
		$this->sitemapHasErrors();
	}

	/**
	 * Add a notice if the site is not connected.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	private function siteConnected() {
		$notification = Models\Notification::getNotificationByName( 'search-console-site-not-connected' );
		if ( aioseo()->searchStatistics->api->auth->isConnected() ) {
			if ( $notification->exists() ) {
				Models\Notification::deleteNotificationByName( 'search-console-site-not-connected' );
			}

			return;
		}

		if ( $notification->exists() ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'search-console-site-not-connected',
			'title'             => __( 'Have you connected your site to Google Search Console?', 'all-in-one-seo-pack' ),
			'content'           => sprintf(
				// Translators: 1 - The plugin short name ("AIOSEO").
				__( '%1$s can now verify whether your site is correctly verified with Google Search Console and that your sitemaps have been submitted correctly. Connect with Google Search Console now to ensure your content is being added to Google as soon as possible for increased rankings.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				AIOSEO_PLUGIN_SHORT_NAME
			),
			'type'              => 'warning',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Connect to Google Search Console', 'all-in-one-seo-pack' ),
			'button1_action'    => 'https://route#aioseo-settings&aioseo-scroll=google-search-console-settings&aioseo-highlight=google-search-console-settings:webmaster-tools?activetool=googleSearchConsole', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Add a notice if the site is not verified or was deleted.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	private function siteVerified() {
		$notification = Models\Notification::getNotificationByName( 'search-console-site-not-verified' );
		if (
			! aioseo()->searchStatistics->api->auth->isConnected() ||
			aioseo()->internalOptions->searchStatistics->site->verified ||
			0 === aioseo()->internalOptions->searchStatistics->site->lastFetch // Not fetched yet.
		) {
			if ( $notification->exists() ) {
				Models\Notification::deleteNotificationByName( 'search-console-site-not-verified' );
			}

			return;
		}

		if ( $notification->exists() ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'search-console-site-not-verified',
			'title'             => __( 'Your site was removed from Google Search Console.', 'all-in-one-seo-pack' ),
			'content'           => __( 'We detected that your site has been removed from Google Search Console. If this was done in error, click below to re-sync and resolve this issue.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'type'              => 'warning',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Reconnect Google Search Console', 'all-in-one-seo-pack' ),
			'button1_action'    => 'https://route#aioseo-settings&aioseo-scroll=google-search-console-settings&aioseo-highlight=google-search-console-settings:webmaster-tools?activetool=googleSearchConsole', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Add a notice if the sitemap has errors.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	private function sitemapHasErrors() {
		$notification = Models\Notification::getNotificationByName( 'search-console-sitemap-has-errors' );
		if (
			! aioseo()->searchStatistics->api->auth->isConnected() ||
			! aioseo()->internalOptions->searchStatistics->site->verified ||
			0 === aioseo()->internalOptions->searchStatistics->sitemap->lastFetch || // Not fetched yet.
			! aioseo()->searchStatistics->sitemap->getSitemapsWithErrors()
		) {
			if ( $notification->exists() ) {
				Models\Notification::deleteNotificationByName( 'search-console-sitemap-has-errors' );
			}

			return;
		}

		if ( $notification->exists() ) {
			return;
		}

		$lastFetch = aioseo()->internalOptions->searchStatistics->sitemap->lastFetch;
		$lastFetch = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $lastFetch );

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'search-console-sitemap-has-errors',
			'title'             => __( 'Your sitemap has errors.', 'all-in-one-seo-pack' ),
			'content'           => sprintf(
				// Translators: 1 - Last fetch date.
				__( 'We detected that your sitemap has errors. The last fetch was on %1$s. Click below to resolve this issue.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				$lastFetch
			),
			'type'              => 'warning',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Fix Sitemap Errors', 'all-in-one-seo-pack' ),
			'button1_action'    => 'https://route#aioseo-sitemaps&open-modal=true:general-sitemap', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}
}Common/SearchStatistics/KeywordRankTracker.php000066600000020037151135505570015556 0ustar00<?php

namespace AIOSEO\Plugin\Common\SearchStatistics;

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

/**
 * Keyword Rank Tracker class.
 *
 * @since 4.7.0
 */
class KeywordRankTracker {
	/**
	 * Retrieves all the keywords' statistics.
	 *
	 * @since 4.7.0
	 *
	 * @param  array $formattedKeywords The formatted keywords.
	 * @param  array $args              The arguments.
	 * @return array                    The statistics for the keywords.
	 */
	public function fetchKeywordsStatistics( &$formattedKeywords = [], $args = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return [
			'distribution'          => [
				'top3'       => '6.86',
				'top10'      => '11.03',
				'top50'      => '52.10',
				'top100'     => '30.01',
				'difference' => [
					'top3'   => '24.31',
					'top10'  => '33.70',
					'top50'  => '-30.50',
					'top100' => '-27.51'
				]
			],
			'distributionIntervals' => [
				[
					'date'   => '2022-10-23',
					'top3'   => '30.70',
					'top10'  => '38.60',
					'top50'  => '24.50',
					'top100' => '6.20'
				],
				[
					'date'   => '2022-10-30',
					'top3'   => '31.60',
					'top10'  => '42.10',
					'top50'  => '21.00',
					'top100' => '5.30'
				],
				[
					'date'   => '2022-11-06',
					'top3'   => '31.30',
					'top10'  => '44.40',
					'top50'  => '20.30',
					'top100' => '4.00'
				],
				[
					'date'   => '2022-11-13',
					'top3'   => '31.70',
					'top10'  => '44.20',
					'top50'  => '20.20',
					'top100' => '3.90'
				],
				[
					'date'   => '2022-11-20',
					'top3'   => '31.70',
					'top10'  => '45.70',
					'top50'  => '18.00',
					'top100' => '4.60'
				],
				[
					'date'   => '2022-11-27',
					'top3'   => '32.50',
					'top10'  => '47.80',
					'top50'  => '16.80',
					'top100' => '2.90'
				],
				[
					'date'   => '2022-12-04',
					'top3'   => '32.50',
					'top10'  => '47.20',
					'top50'  => '17.90',
					'top100' => '2.40'
				],
				[
					'date'   => '2022-12-11',
					'top3'   => '31.80',
					'top10'  => '43.70',
					'top50'  => '21.00',
					'top100' => '3.50'
				],
				[
					'date'   => '2022-12-18',
					'top3'   => '30.40',
					'top10'  => '43.60',
					'top50'  => '22.40',
					'top100' => '3.60'
				],
				[
					'date'   => '2022-12-25',
					'top3'   => '26.90',
					'top10'  => '37.20',
					'top50'  => '29.70',
					'top100' => '6.20'
				],
				[
					'date'   => '2023-01-01',
					'top3'   => '27.00',
					'top10'  => '33.80',
					'top50'  => '31.60',
					'top100' => '7.60'
				],
				[
					'date'   => '2023-01-08',
					'top3'   => '26.60',
					'top10'  => '38.60',
					'top50'  => '30.00',
					'top100' => '4.80'
				],
				[
					'date'   => '2023-01-16',
					'top3'   => '31.10',
					'top10'  => '43.90',
					'top50'  => '22.50',
					'top100' => '2.50'
				]
			]
		];
	}

	/**
	 * Retrieves all the keywords, formatted.
	 *
	 * @since 4.7.0
	 *
	 * @param  array $args The arguments.
	 * @return array       The formatted keywords.
	 */
	public function getFormattedKeywords( $args = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$statistics = [];
		for ( $i = 1; $i < 9; $i++ ) {
			$statistics[ $i ] = [
				'clicks'      => wp_rand( 1, 1000 ),
				'impressions' => wp_rand( 10, 10000 ),
				'ctr'         => wp_rand( 1, 99 ),
				'position'    => wp_rand( 1, 100 ),
				'history'     => [
					[
						'date'     => gmdate( 'Y-m-d', strtotime( '-30 days' ) ),
						'position' => wp_rand( 1, 15 ),
						'clicks'   => wp_rand( 10, 100 ),
					],
					[
						'date'     => gmdate( 'Y-m-d', strtotime( '-23 days' ) ),
						'position' => wp_rand( 1, 15 ),
						'clicks'   => wp_rand( 10, 100 ),
					],
					[
						'date'     => gmdate( 'Y-m-d', strtotime( '-16 days' ) ),
						'position' => wp_rand( 1, 15 ),
						'clicks'   => wp_rand( 10, 100 ),
					],
					[
						'date'     => gmdate( 'Y-m-d', strtotime( '-9 days' ) ),
						'position' => wp_rand( 1, 15 ),
						'clicks'   => wp_rand( 10, 100 ),
					],
					[
						'date'     => gmdate( 'Y-m-d', strtotime( '-2 days' ) ),
						'position' => wp_rand( 1, 15 ),
						'clicks'   => wp_rand( 10, 100 ),
					]
				]
			];
		}

		return [
			'rows'   => [
				[
					'id'         => 1,
					'name'       => 'best seo plugin',
					'favorited'  => false,
					'groups'     => [
						[
							'id'   => 1,
							'name' => 'Blog Pages Group'
						]
					],
					'statistics' => $statistics[1]
				],
				[
					'id'         => 2,
					'name'       => 'aioseo is the best',
					'favorited'  => true,
					'groups'     => [
						[
							'id'   => 2,
							'name' => 'Low Performance Group'
						]
					],
					'statistics' => $statistics[2]
				],
				[
					'id'         => 3,
					'name'       => 'analyze my seo',
					'favorited'  => false,
					'groups'     => [
						[
							'id'   => 3,
							'name' => 'High Performance Group'
						]
					],
					'statistics' => $statistics[3]
				],
				[
					'id'         => 4,
					'name'       => 'wordpress seo',
					'favorited'  => false,
					'groups'     => [],
					'statistics' => $statistics[4]
				],
				[
					'id'         => 5,
					'name'       => 'best seo plugin pro',
					'favorited'  => false,
					'groups'     => [],
					'statistics' => $statistics[5]
				],
				[
					'id'         => 6,
					'name'       => 'aioseo wordpress',
					'favorited'  => false,
					'groups'     => [],
					'statistics' => $statistics[6]
				],
				[
					'id'         => 7,
					'name'       => 'headline analyzer aioseo',
					'favorited'  => false,
					'groups'     => [],
					'statistics' => $statistics[7]
				],
				[
					'id'         => 8,
					'name'       => 'best seo plugin plugin',
					'favorited'  => false,
					'groups'     => [],
					'statistics' => $statistics[8]
				]
			],
			'totals' => [
				'total' => 8,
				'pages' => 1,
				'page'  => 1
			],
		];
	}

	/**
	 * Retrieves all the keyword groups, formatted.
	 *
	 * @since 4.7.0
	 *
	 * @param  array $args The arguments.
	 * @return array       The formatted keyword groups.
	 */
	public function getFormattedGroups( $args = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$statistics = [];
		for ( $i = 1; $i < 4; $i++ ) {
			$statistics[ $i ] = [
				'clicks'      => wp_rand( 1, 1000 ),
				'impressions' => wp_rand( 10, 10000 ),
				'ctr'         => wp_rand( 1, 99 ),
				'position'    => wp_rand( 1, 100 )
			];
		}

		return [
			'rows'   => [
				[
					'id'          => 1,
					'name'        => 'Blog Pages Group',
					'keywordsQty' => 1,
					'keywords'    => [],
					'statistics'  => $statistics[1]
				],
				[
					'id'          => 2,
					'name'        => 'Low Performance Group',
					'keywordsQty' => 1,
					'keywords'    => [],
					'statistics'  => $statistics[2]
				],
				[
					'id'          => 3,
					'name'        => 'High Performance Group',
					'keywordsQty' => 1,
					'keywords'    => [],
					'statistics'  => $statistics[3]
				]
			],
			'totals' => [
				'total' => 8,
				'pages' => 1,
				'page'  => 1
			],
		];
	}

	/**
	 * Returns the data for Vue.
	 *
	 * @since 4.7.0
	 *
	 * @return array The data for Vue.
	 */
	public function getVueData() {
		$formattedKeywords = $this->getFormattedKeywords();
		$formattedGroups   = $this->getFormattedGroups();

		return [
			// Dummy data to show on the UI.
			'keywords' => [
				'all'        => $formattedKeywords,
				'paginated'  => $formattedKeywords,
				'count'      => count( $formattedKeywords['rows'] ),
				'statistics' => $this->fetchKeywordsStatistics( $formattedKeywords ),
			],
			'groups'   => [
				'all'       => $formattedGroups,
				'paginated' => $formattedGroups,
				'count'     => count( $formattedGroups['rows'] ),
			],
		];
	}

	/**
	 * Returns the data for Vue.
	 *
	 * @since 4.7.0
	 *
	 * @return array The data.
	 */
	public function getVueDataEdit() {
		$formattedKeywords = $this->getFormattedKeywords();

		return [
			// Dummy data to show on the UI.
			'keywords' => [
				'all'       => $formattedKeywords,
				'paginated' => $formattedKeywords,
				'count'     => count( $formattedKeywords['rows'] ),
			],
		];
	}
}Common/SearchStatistics/SearchStatistics.php000066600000075513151135505570015273 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchStatistics;

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

/**
 * Class that holds our Search Statistics feature.
 *
 * @since 4.3.0
 */
class SearchStatistics {
	/**
	 * Holds the instance of the API class.
	 *
	 * @since 4.3.0
	 *
	 * @var Api\Api
	 */
	public $api;

	/**
	 * Holds the instance of the Site class.
	 *
	 * @since 4.6.2
	 *
	 * @var Site
	 */
	public $site;

	/**
	 * Holds the instance of the Sitemap class.
	 *
	 * @since 4.6.2
	 *
	 * @var Sitemap
	 */
	public $sitemap;

	/**
	 * Holds the instance of the Notices class.
	 *
	 * @since 4.6.2
	 *
	 * @var Notices
	 */
	public $notices;

	/**
	 * Holds the instance of the Keyword Rank Tracker class.
	 *
	 * @since 4.7.0
	 *
	 * @var KeywordRankTracker
	 */
	public $keywordRankTracker;

	/**
	 * Holds the instance of the Index Status class.
	 *
	 * @since 4.8.2
	 *
	 * @var IndexStatus
	 */
	public $indexStatus;

	/**
	 * Class constructor.
	 *
	 * @since 4.3.0
	 */
	public function __construct() {
		$this->api                = new Api\Api();
		$this->site               = new Site();
		$this->sitemap            = new Sitemap();
		$this->notices            = new Notices();
		$this->keywordRankTracker = new KeywordRankTracker();
		$this->indexStatus        = new IndexStatus();
	}

	/**
	 * Returns the data for Vue.
	 *
	 * @since 4.3.0
	 *
	 * @return array The data for Vue.
	 */
	public function getVueData() {
		$data = [
			'isConnected'         => aioseo()->searchStatistics->api->auth->isConnected(),
			'latestAvailableDate' => null,
			'range'               => [],
			'rolling'             => aioseo()->internalOptions->internal->searchStatistics->rolling,
			'authedSite'          => aioseo()->searchStatistics->api->auth->getAuthedSite(),
			'sitemapsWithErrors'  => aioseo()->searchStatistics->sitemap->getSitemapsWithErrors(),
			'data'                => [
				'seoStatistics'   => $this->getSeoOverviewData(),
				'keywords'        => $this->getKeywordsData(),
				'contentRankings' => $this->getContentRankingsData()
			]
		];

		return $data;
	}

	/**
	 * Resets the Search Statistics.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	public function reset() {
		aioseo()->internalOptions->searchStatistics->sitemap->reset();
		aioseo()->internalOptions->searchStatistics->site->reset();

		// Clear the cache for the Search Statistics.
		aioseo()->searchStatistics->clearCache();
	}

	/**
	 * Returns the data for the SEO Overview.
	 *
	 * @since 4.3.0
	 *
	 * @param  array $dateRange The date range.
	 * @return array            The data for the SEO Overview.
	 */
	protected function getSeoOverviewData( $dateRange = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$pageRows = [
			'/'                       => [
				'ctr'              => '1.25',
				'page'             => '/',
				'clicks'           => 15460,
				'position'         => '74.01',
				'difference'       => [
					'ctr'         => '-0.23',
					'decay'       => 192211,
					'clicks'      => -26,
					'current'     => true,
					'position'    => '19.66',
					'comparison'  => true,
					'impressions' => 192237
				],
				'impressions'      => 1235435,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 65
			],
			'/test-page/'             => [
				'ctr'              => '0.30',
				'page'             => '/test-page/',
				'clicks'           => 5688,
				'position'         => '35.28',
				'difference'       => [
					'ctr'         => '0.05',
					'decay'       => 378973,
					'clicks'      => 1941,
					'current'     => true,
					'position'    => '-2.33',
					'comparison'  => true,
					'impressions' => 377032
				],
				'impressions'      => 1881338,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/test-page/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 95
			],
			'/high-ranking-page/'     => [
				'ctr'              => '6.03',
				'page'             => '/high-ranking-page/',
				'clicks'           => 3452,
				'position'         => '22.85',
				'difference'       => [
					'ctr'         => '-0.95',
					'decay'       => -5986,
					'clicks'      => -898,
					'current'     => true,
					'position'    => '-0.22',
					'comparison'  => true,
					'impressions' => -5088
				],
				'impressions'      => 57248,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/high-ranking-page/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 100
			],
			'/pricing/'               => [
				'ctr'              => '1.35',
				'page'             => '/pricing/',
				'clicks'           => 2749,
				'position'         => '40.40',
				'difference'       => [
					'ctr'         => '-0.16',
					'decay'       => 15991,
					'clicks'      => -94,
					'current'     => true,
					'position'    => '9.77',
					'comparison'  => true,
					'impressions' => 16085
				],
				'impressions'      => 203794,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/pricing/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 100
			],
			'/features-and-benefits/' => [
				'ctr'              => '2.48',
				'page'             => '/features-and-benefits/',
				'clicks'           => 2600,
				'position'         => '15.53',
				'difference'       => [
					'ctr'         => '0.99',
					'decay'       => 23466,
					'clicks'      => 1367,
					'current'     => true,
					'position'    => '1.51',
					'comparison'  => true,
					'impressions' => 22099
				],
				'impressions'      => 104769,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/features-and-benefits/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 90
			],
			'/documentation/'         => [
				'ctr'              => '2.64',
				'page'             => '/documentation/',
				'clicks'           => 1716,
				'position'         => '27.85',
				'difference'       => [
					'ctr'         => '-0.04',
					'decay'       => 7274,
					'clicks'      => 167,
					'current'     => true,
					'position'    => '-9.51',
					'comparison'  => true,
					'impressions' => 7107
				],
				'impressions'      => 64883,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/documentation/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 93
			],
			'/blog/'                  => [
				'ctr'              => '3.75',
				'page'             => '/blog/',
				'clicks'           => 1661,
				'position'         => '36.60',
				'difference'       => [
					'ctr'         => '0.42',
					'decay'       => -3145,
					'clicks'      => 77,
					'current'     => true,
					'position'    => '-9.40',
					'comparison'  => true,
					'impressions' => -3222
				],
				'impressions'      => 44296,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/blog/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 97
			],
			'/blog/my-best-content/'  => [
				'ctr'              => '7.08',
				'page'             => '/blog/my-best-content/',
				'clicks'           => 1573,
				'position'         => '9.61',
				'difference'       => [
					'ctr'         => '0.16',
					'decay'       => -201,
					'clicks'      => 22,
					'current'     => true,
					'position'    => '-2.03',
					'comparison'  => true,
					'impressions' => -223
				],
				'impressions'      => 22203,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/blog/my-best-content/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 56
			],
			'/contact-us/'            => [
				'ctr'              => '1.45',
				'page'             => '/contact-us/',
				'clicks'           => 1550,
				'position'         => '32.05',
				'difference'       => [
					'ctr'         => '0.12',
					'decay'       => 1079,
					'clicks'      => 140,
					'current'     => true,
					'position'    => '-3.47',
					'comparison'  => true,
					'impressions' => 939
				],
				'impressions'      => 106921,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/contact-us/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 78
			],
			'/support/'               => [
				'ctr'              => '5.94',
				'page'             => '/support/',
				'clicks'           => 1549,
				'position'         => '25.83',
				'difference'       => [
					'ctr'         => '-0.74',
					'decay'       => 3885,
					'clicks'      => 62,
					'current'     => true,
					'position'    => '-1.48',
					'comparison'  => true,
					'impressions' => 3823
				],
				'impressions'      => 26099,
				'context'          => [],
				'objectId'         => 0,
				'objectTitle'      => '/support/',
				'objectType'       => 'post',
				'inspectionResult' => $this->getInspectionResult(),
				'seoScore'         => 86
			]
		];

		// Get the 10 most recent posts.
		$recentPosts = aioseo()->db->db->get_results(
			sprintf(
				'SELECT ID, post_title FROM %1$s WHERE post_status = "publish" AND post_type = "post" ORDER BY post_date DESC LIMIT 10',
				aioseo()->db->db->posts
			)
		);

		// Loop through the default page rows and update the key with the permalink from the most recent posts.
		$i = 0;
		foreach ( $pageRows as $key => $pageRow ) {
			// Get the permalink of the recent post that matches the $i index.
			$permalink = isset( $recentPosts[ $i ] ) ? get_permalink( $recentPosts[ $i ]->ID ) : '';

			// If we don't have a permalink, continue to the next row.
			if ( empty( $permalink ) ) {
				continue;
			}

			// Remove the domain from the permalink by parsing the URL and getting the path.
			$permalink = wp_parse_url( $permalink, PHP_URL_PATH );

			// If the permalink already exists, continue to the next row.
			if ( isset( $pageRows[ $permalink ] ) ) {
				// Update the objectId and objectTitle with the recent post ID and title.
				$pageRows[ $permalink ]['objectId']    = $recentPosts[ $i ]->ID;
				$pageRows[ $permalink ]['objectTitle'] = $recentPosts[ $i ]->post_title;

				continue;
			}

			$pageRows[ $permalink ] = $pageRows[ $key ];

			// Remove the old key.
			unset( $pageRows[ $key ] );

			// Update the objectId and objectTitle with the recent post ID and title.
			$pageRows[ $permalink ]['objectId']    = $recentPosts[ $i ]->ID;
			$pageRows[ $permalink ]['objectTitle'] = $recentPosts[ $i ]->post_title;

			$i++;
		}

		return [
			'statistics' => [
				'ctr'         => '0.74',
				'clicks'      => 111521,
				'keywords'    => 19335,
				'position'    => '49.28',
				'difference'  => [
					'ctr'         => '0.03',
					'clicks'      => 1736,
					'keywords'    => 2853,
					'position'    => '1.01',
					'impressions' => -475679
				],
				'impressions' => 14978074
			],
			'intervals'  => [
				[
					'ctr'         => '0.72',
					'date'        => '2022-10-23',
					'clicks'      => 7091,
					'position'    => '48.88',
					'impressions' => 985061
				],
				[
					'ctr'         => '0.77',
					'date'        => '2022-10-30',
					'clicks'      => 8544,
					'position'    => '46.48',
					'impressions' => 1111602
				],
				[
					'ctr'         => '0.73',
					'date'        => '2022-11-06',
					'clicks'      => 9087,
					'position'    => '48.44',
					'impressions' => 1247506
				],
				[
					'ctr'         => '0.75',
					'date'        => '2022-11-13',
					'clicks'      => 9952,
					'position'    => '50.03',
					'impressions' => 1326910
				],
				[
					'ctr'         => '0.73',
					'date'        => '2022-11-20',
					'clicks'      => 9696,
					'position'    => '50.28',
					'impressions' => 1324633
				],
				[
					'ctr'         => '0.69',
					'date'        => '2022-11-27',
					'clicks'      => 9590,
					'position'    => '51.03',
					'impressions' => 1382602
				],
				[
					'ctr'         => '0.71',
					'date'        => '2022-12-04',
					'clicks'      => 9691,
					'position'    => '51.07',
					'impressions' => 1365509
				],
				[
					'ctr'         => '0.71',
					'date'        => '2022-12-11',
					'clicks'      => 9291,
					'position'    => '51.22',
					'impressions' => 1316184
				],
				[
					'ctr'         => '0.80',
					'date'        => '2022-12-18',
					'clicks'      => 8659,
					'position'    => '48.20',
					'impressions' => 1081944
				],
				[
					'ctr'         => '0.75',
					'date'        => '2022-12-25',
					'clicks'      => 6449,
					'position'    => '49.31',
					'impressions' => 857591
				],
				[
					'ctr'         => '0.66',
					'date'        => '2023-01-01',
					'clicks'      => 5822,
					'position'    => '48.16',
					'impressions' => 876828
				],
				[
					'ctr'         => '0.77',
					'date'        => '2023-01-08',
					'clicks'      => 7501,
					'position'    => '47.34',
					'impressions' => 975764
				],
				[
					'ctr'         => '0.90',
					'date'        => '2023-01-16',
					'clicks'      => 10148,
					'position'    => '48.29',
					'impressions' => 1125940
				]
			],
			'pages'      => [
				'topPages'   => [
					'rows' => $pageRows
				],
				'paginated'  => [
					'rows'              => $pageRows,
					'totals'            => [
						'page'  => 1,
						'pages' => 1,
						'total' => 10
					],
					'filters'           => [
						[
							'slug'   => 'all',
							'name'   => 'All',
							'active' => true
						],
						[
							'slug'   => 'topLosing',
							'name'   => 'Top Losing',
							'active' => false
						],
						[
							'slug'   => 'topWinning',
							'name'   => 'Top Winning',
							'active' => false
						]
					],
					'additionalFilters' => [
						[
							'name'    => 'postType',
							'options' => [
								[
									'label' => __( 'All Content Types', 'all-in-one-seo-pack' ),
									'value' => ''
								]
							]
						]
					]
				],
				'topLosing'  => [
					'rows' => $pageRows
				],
				'topWinning' => [
					'rows' => $pageRows
				]
			]
		];
	}

	/**
	 * Returns the data for the Keywords.
	 *
	 * @since 4.3.0
	 *
	 * @param  array $args The arguments.
	 * @return array       The data for the Keywords.
	 */
	public function getKeywordsData( $args = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$keywordsRows = [
			[
				'ctr'         => '4.89',
				'clicks'      => 5000,
				'keyword'     => 'best seo plugin',
				'position'    => '1.93',
				'difference'  => [
					'ctr'         => '-1.06',
					'decay'       => 6590,
					'clicks'      => -652,
					'position'    => '0.07',
					'impressions' => 7242
				],
				'impressions' => 102233
			],
			[
				'ctr'         => '7.06',
				'clicks'      => 4404,
				'keyword'     => 'aioseo is the best',
				'position'    => '1.32',
				'difference'  => [
					'ctr'         => '0.13',
					'decay'       => 8586,
					'clicks'      => 633,
					'position'    => '0.12',
					'impressions' => 7953
				],
				'impressions' => 62357
			],
			[
				'ctr'         => '2.81',
				'clicks'      => 1715,
				'keyword'     => 'analyze my seo',
				'position'    => '6.29',
				'difference'  => [
					'ctr'         => '-0.03',
					'decay'       => 13217,
					'clicks'      => 347,
					'position'    => '-0.34',
					'impressions' => 12870
				],
				'impressions' => 61102
			],
			[
				'ctr'         => '7.46',
				'clicks'      => 717,
				'keyword'     => 'wordpress seo',
				'position'    => '1.18',
				'difference'  => [
					'ctr'         => '-0.69',
					'decay'       => 2729,
					'clicks'      => 144,
					'position'    => '-0.08',
					'impressions' => 2585
				],
				'impressions' => 9614
			],
			[
				'ctr'         => '6.66',
				'clicks'      => 446,
				'keyword'     => 'best seo plugin pro',
				'position'    => '1.65',
				'difference'  => [
					'ctr'         => '0.36',
					'decay'       => -121,
					'clicks'      => 16,
					'position'    => '0.33',
					'impressions' => -137
				],
				'impressions' => 6693
			],
			[
				'ctr'         => '7.39',
				'clicks'      => 409,
				'keyword'     => 'aioseo wordpress',
				'position'    => '1.77',
				'difference'  => [
					'ctr'         => '-0.39',
					'decay'       => 534,
					'clicks'      => 19,
					'position'    => '-0.13',
					'impressions' => 515
				],
				'impressions' => 5531
			],
			[
				'ctr'         => '1.11',
				'clicks'      => 379,
				'keyword'     => 'headline analyzer aioseo',
				'position'    => '8.41',
				'difference'  => [
					'ctr'         => '0.43',
					'decay'       => 134,
					'clicks'      => 147,
					'position'    => '-1.36',
					'impressions' => -13
				],
				'impressions' => 34043
			],
			[
				'ctr'         => '2.63',
				'clicks'      => 364,
				'keyword'     => 'best seo plugin plugin',
				'position'    => '2.38',
				'difference'  => [
					'ctr'         => '0.06',
					'decay'       => 836,
					'clicks'      => 29,
					'position'    => '0.20',
					'impressions' => 807
				],
				'impressions' => 13837
			],
			[
				'ctr'         => '1.52',
				'clicks'      => 326,
				'keyword'     => 'best seo plugin pack',
				'position'    => '4.14',
				'difference'  => [
					'ctr'         => '-0.19',
					'decay'       => -1590,
					'clicks'      => -66,
					'position'    => '0.64',
					'impressions' => -1524
				],
				'impressions' => 21450
			],
			[
				'ctr'         => '6.70',
				'clicks'      => 264,
				'keyword'     => 'youtube title analyzer aioseo',
				'position'    => '7.19',
				'difference'  => [
					'ctr'         => '4.73',
					'decay'       => 3842,
					'clicks'      => 257,
					'position'    => '-4.18',
					'impressions' => 3585
				],
				'impressions' => 3940
			]
		];

		return [
			'paginated'             => [
				'rows'    => $keywordsRows,
				'totals'  => [
					'page'  => 1,
					'pages' => 1,
					'total' => 10
				],
				'filters' => [
					[
						'slug'   => 'all',
						'name'   => 'All',
						'active' => true
					],
					[
						'slug'   => 'topLosing',
						'name'   => 'Top Losing',
						'active' => false
					],
					[
						'slug'   => 'topWinning',
						'name'   => 'Top Winning',
						'active' => false
					]
				]
			],
			'topLosing'             => $keywordsRows,
			'topWinning'            => $keywordsRows,
			'topKeywords'           => $keywordsRows,
			'distribution'          => [
				'top3'       => '6.86',
				'top10'      => '11.03',
				'top50'      => '52.10',
				'top100'     => '30.01',
				'difference' => [
					'top3'   => '24.31',
					'top10'  => '33.70',
					'top50'  => '-30.50',
					'top100' => '-27.51'
				]
			],
			'distributionIntervals' => [
				[
					'date'   => '2022-10-23',
					'top3'   => '30.70',
					'top10'  => '38.60',
					'top50'  => '24.50',
					'top100' => '6.20'
				],
				[
					'date'   => '2022-10-30',
					'top3'   => '31.60',
					'top10'  => '42.10',
					'top50'  => '21.00',
					'top100' => '5.30'
				],
				[
					'date'   => '2022-11-06',
					'top3'   => '31.30',
					'top10'  => '44.40',
					'top50'  => '20.30',
					'top100' => '4.00'
				],
				[
					'date'   => '2022-11-13',
					'top3'   => '31.70',
					'top10'  => '44.20',
					'top50'  => '20.20',
					'top100' => '3.90'
				],
				[
					'date'   => '2022-11-20',
					'top3'   => '31.70',
					'top10'  => '45.70',
					'top50'  => '18.00',
					'top100' => '4.60'
				],
				[
					'date'   => '2022-11-27',
					'top3'   => '32.50',
					'top10'  => '47.80',
					'top50'  => '16.80',
					'top100' => '2.90'
				],
				[
					'date'   => '2022-12-04',
					'top3'   => '32.50',
					'top10'  => '47.20',
					'top50'  => '17.90',
					'top100' => '2.40'
				],
				[
					'date'   => '2022-12-11',
					'top3'   => '31.80',
					'top10'  => '43.70',
					'top50'  => '21.00',
					'top100' => '3.50'
				],
				[
					'date'   => '2022-12-18',
					'top3'   => '30.40',
					'top10'  => '43.60',
					'top50'  => '22.40',
					'top100' => '3.60'
				],
				[
					'date'   => '2022-12-25',
					'top3'   => '26.90',
					'top10'  => '37.20',
					'top50'  => '29.70',
					'top100' => '6.20'
				],
				[
					'date'   => '2023-01-01',
					'top3'   => '27.00',
					'top10'  => '33.80',
					'top50'  => '31.60',
					'top100' => '7.60'
				],
				[
					'date'   => '2023-01-08',
					'top3'   => '26.60',
					'top10'  => '38.60',
					'top50'  => '30.00',
					'top100' => '4.80'
				],
				[
					'date'   => '2023-01-16',
					'top3'   => '31.10',
					'top10'  => '43.90',
					'top50'  => '22.50',
					'top100' => '2.50'
				]
			]
		];
	}

	/**
	 * Returns the content performance data.
	 *
	 * @since 4.7.2
	 *
	 * @return array The content performance data.
	 */
	public function getSeoStatisticsData( $args = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return [];
	}

	/**
	 * Returns the Content Rankings data.
	 *
	 * @since 4.3.6
	 *
	 * @param  array $args The arguments.
	 * @return array       The Content Rankings data.
	 */
	public function getContentRankingsData( $args = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return [
			'paginated' => [
				'rows'              => [
					'/'                       => [
						'points'           => [
							'2022-04' => 13655,
							'2022-05' => 12683,
							'2022-06' => 13923,
							'2022-07' => 13031,
							'2022-08' => 10978,
							'2022-09' => 9726,
							'2022-10' => 13943,
							'2022-11' => 21813,
							'2022-12' => 11163,
							'2023-01' => 4442,
							'2023-02' => 4798,
							'2023-03' => 5405
						],
						'page'             => '/',
						'peak'             => 21813,
						'decayPercent'     => 75,
						'decay'            => 16407,
						'recovering'       => false,
						'context'          => [
							'lastUpdated' => 'December 6, 2021'
						],
						'objectTitle'      => 'Homepage',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
					'/high-ranking-page/'     => [
						'points'           => [
							'2022-04' => 18426,
							'2022-05' => 18435,
							'2022-06' => 19764,
							'2022-07' => 14769,
							'2022-08' => 6486,
							'2022-09' => 11984,
							'2022-10' => 11539,
							'2022-11' => 9939,
							'2022-12' => 5340,
							'2023-01' => 3965,
							'2023-02' => 3799,
							'2023-03' => 5440
						],
						'page'             => '/high-ranking-page/',
						'peak'             => 19764,
						'decayPercent'     => 72,
						'decay'            => 14323,
						'recovering'       => false,
						'context'          => [
							'lastUpdated' => 'November 17, 2022'
						],
						'objectTitle'      => 'High Ranking Page',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
					'/pricing/'               => [
						'points'           => [
							'2022-04' => 5356,
							'2022-05' => 5425,
							'2022-06' => 5165,
							'2022-07' => 5479,
							'2022-08' => 4995,
							'2022-09' => 4466,
							'2022-10' => 4545,
							'2022-11' => 5361,
							'2022-12' => 3092,
							'2023-01' => 1948,
							'2023-02' => 1630,
							'2023-03' => 2341
						],
						'page'             => '/pricing/',
						'peak'             => 5479,
						'decayPercent'     => 57,
						'decay'            => 3137,
						'recovering'       => false,
						'context'          => [
							'lastUpdated' => 'December 8, 2021'
						],
						'objectTitle'      => 'Pricing',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
					'/features-and-benefits/' => [
						'points'           => [
							'2022-04' => 1272,
							'2022-05' => 4151,
							'2022-06' => 6953,
							'2022-07' => 7785,
							'2022-08' => 4177,
							'2022-09' => 3378,
							'2022-10' => 2553,
							'2022-11' => 3971,
							'2022-12' => 2143,
							'2023-01' => 2335,
							'2023-02' => 1666,
							'2023-03' => 4892
						],
						'page'             => '/features-and-benefits/',
						'peak'             => 7785,
						'decayPercent'     => 37,
						'decay'            => 2893,
						'recovering'       => false,
						'context'          => [
							'lastUpdated' => 'February 7, 2022'
						],
						'objectTitle'      => 'Features and Benefits',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
					'/documentation/'         => [
						'points'           => [
							'2022-04' => 594,
							'2022-05' => 385,
							'2022-06' => 337,
							'2022-07' => 378,
							'2022-08' => 714,
							'2022-09' => 2637,
							'2022-10' => 2831,
							'2022-11' => 2907,
							'2022-12' => 1851,
							'2023-01' => 277,
							'2023-02' => 226,
							'2023-03' => 175
						],
						'page'             => '/documentation/',
						'peak'             => 2907,
						'decayPercent'     => 93,
						'decay'            => 2731,
						'recovering'       => false,
						'context'          => [
							'lastUpdated' => 'January 7, 2022'
						],
						'objectTitle'      => 'Documentation',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
					'/blog/'                  => [
						'points'           => [
							'2022-04' => 2956,
							'2022-05' => 2363,
							'2022-06' => 2347,
							'2022-07' => 2154,
							'2022-08' => 2604,
							'2022-09' => 1995,
							'2022-10' => 1528,
							'2022-11' => 1578,
							'2022-12' => 1458,
							'2023-01' => 927,
							'2023-02' => 629,
							'2023-03' => 592
						],
						'page'             => '/blog/',
						'peak'             => 2956,
						'decayPercent'     => 79,
						'decay'            => 2363,
						'recovering'       => false,
						'context'          => [
							'lastUpdated' => 'April 21, 2022'
						],
						'objectTitle'      => 'Blog',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
					'/blog/my-best-content/'  => [
						'points'           => [
							'2022-04' => 1889,
							'2022-05' => 1714,
							'2022-06' => 2849,
							'2022-07' => 4175,
							'2022-08' => 5343,
							'2022-09' => 6360,
							'2022-10' => 6492,
							'2022-11' => 6955,
							'2022-12' => 6930,
							'2023-01' => 5880,
							'2023-02' => 5211,
							'2023-03' => 4683
						],
						'page'             => '/blog/my-best-content/',
						'peak'             => 6955,
						'decayPercent'     => 32,
						'decay'            => 2272,
						'recovering'       => false,
						'context'          => [
							'lastUpdated' => 'April 22, 2022'
						],
						'objectTitle'      => 'My Best Content',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
					'/contact-us/'            => [
						'points'           => [
							'2022-04' => 3668,
							'2022-05' => 3699,
							'2022-06' => 4934,
							'2022-07' => 5488,
							'2022-08' => 5092,
							'2022-09' => 5526,
							'2022-10' => 4694,
							'2022-11' => 4791,
							'2022-12' => 3989,
							'2023-01' => 4089,
							'2023-02' => 4189,
							'2023-03' => 4289
						],
						'page'             => '/contact-us/',
						'peak'             => 5526,
						'decayPercent'     => 34,
						'decay'            => 1907,
						'recovering'       => true,
						'context'          => [
							'lastUpdated' => 'January 28, 2022'
						],
						'objectTitle'      => 'Contact Us',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
					'/support/'               => [
						'points'           => [
							'2022-04' => 2715,
							'2022-05' => 2909,
							'2022-06' => 2981,
							'2022-07' => 2988,
							'2022-08' => 2586,
							'2022-09' => 2592,
							'2022-10' => 2391,
							'2022-11' => 2446,
							'2022-12' => 2045,
							'2023-01' => 1239,
							'2023-02' => 1077,
							'2023-03' => 1198
						],
						'page'             => '/support/',
						'peak'             => 2988,
						'decayPercent'     => 59,
						'decay'            => 1789,
						'recovering'       => false,
						'context'          => [
							'lastUpdated' => 'February 21, 2021'
						],
						'objectTitle'      => 'Support',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
					'/blog/top-10-contents/'  => [
						'points'           => [
							'2022-04' => 1889,
							'2022-05' => 1714,
							'2022-06' => 2849,
							'2022-07' => 4175,
							'2022-08' => 5343,
							'2022-09' => 6360,
							'2022-10' => 6492,
							'2022-11' => 6955,
							'2022-12' => 6930,
							'2023-01' => 5880,
							'2023-02' => 5211,
							'2023-03' => 4683
						],
						'page'             => '/blog/top-10-contents/',
						'peak'             => 6955,
						'decayPercent'     => 32,
						'decay'            => 2272,
						'recovering'       => false,
						'context'          => [
							'lastUpdated' => 'October 14, 2022'
						],
						'objectTitle'      => 'Top 10 Contents',
						'objectType'       => 'post',
						'inspectionResult' => $this->getInspectionResult(),
						'objectId'         => 0
					],
				],
				'totals'            => [
					'page'  => 1,
					'pages' => 1,
					'total' => 10
				],
				'additionalFilters' => [
					[
						'name'    => 'postType',
						'options' => [
							[
								'label' => __( 'All Content Types', 'all-in-one-seo-pack' ),
								'value' => ''
							]
						]
					]
				]
			]
		];
	}

	/**
	 * Get minimum required values for the inspection result.
	 *
	 * @since 4.5.0
	 *
	 * @return array The inspection result.
	 */
	private function getInspectionResult() {
		$verdicts = [
			'PASS',
			'FAIL',
			'NEUTRAL'
		];

		return [
			'indexStatusResult' => [
				'verdict' => $verdicts[ array_rand( $verdicts ) ],
			]
		];
	}

	/**
	 * Clears the Search Statistics cache.
	 *
	 * @since   4.5.0
	 * @version 4.6.2 Moved from Pro to Common.
	 *
	 * @return void
	 */
	public function clearCache() {
		aioseo()->core->cache->clearPrefix( 'aioseo_search_statistics_' );
		aioseo()->core->cache->clearPrefix( 'search_statistics_' );
	}

	/**
	 * Returns all scheduled Search Statistics related actions.
	 *
	 * @since 4.6.2
	 *
	 * @return array The Search Statistics actions.
	 */
	protected function getActionSchedulerActions() {
		return [
			$this->site->action,
			$this->sitemap->action
		];
	}

	/**
	 * Cancels all scheduled Search Statistics related actions.
	 *
	 * @since   4.3.3
	 * @version 4.6.2 Moved from Pro to Common.
	 *
	 * @return void
	 */
	public function cancelActions() {
		foreach ( $this->getActionSchedulerActions() as $actionName ) {
			as_unschedule_all_actions( $actionName );
		}
	}
}Common/SearchStatistics/Sitemap.php000066600000007546151135505570013416 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchStatistics;

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

/**
 * Handles the sitemaps for the search statistics.
 *
 * @since 4.6.2
 */
class Sitemap {
	/**
	 * The action name.
	 *
	 * @since 4.6.2
	 *
	 * @var string
	 */
	public $action = 'aioseo_search_statistics_sitemap_sync';

	/**
	 * Class constructor.
	 *
	 * @since 4.6.2
	 */
	public function __construct() {
		add_action( 'admin_init', [ $this, 'init' ] );
		add_action( $this->action, [ $this, 'worker' ] );
	}

	/**
	 * Initialize the class.
	 *
	 * @since 4.6.2
	 *
	 * @return void
	 */
	public function init() {
		if (
			! aioseo()->searchStatistics->api->auth->isConnected() ||
			! aioseo()->internalOptions->searchStatistics->site->verified ||
			aioseo()->actionScheduler->isScheduled( $this->action )
		) {
			return;
		}

		aioseo()->actionScheduler->scheduleAsync( $this->action );
	}

	/**
	 * Sync the sitemap.
	 *
	 * @since 4.6.3
	 *
	 * @return void
	 */
	public function worker() {
		if ( ! $this->canSync() ) {
			return;
		}

		$api      = new Api\Request( 'google-search-console/sitemap/sync/', [ 'sitemaps' => aioseo()->sitemap->helpers->getSitemapUrls() ] );
		$response = $api->request();

		if ( is_wp_error( $response ) || empty( $response['data'] ) ) {
			// If it failed to communicate with the server, try again in a few hours.
			aioseo()->actionScheduler->scheduleSingle( $this->action, wp_rand( HOUR_IN_SECONDS, 2 * HOUR_IN_SECONDS ), [], true );

			return;
		}

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

		// Schedule a new sync for the next week.
		aioseo()->actionScheduler->scheduleSingle( $this->action, WEEK_IN_SECONDS + wp_rand( 0, 3 * DAY_IN_SECONDS ), [], true );
	}

	/**
	 * Maybe sync the sitemap after updating the options.
	 * It will check whether the sitemap options have changed and sync the sitemap if needed.
	 *
	 * @since 4.6.2
	 *
	 * @param array $oldSitemapOptions The old sitemap options.
	 * @param array $newSitemapOptions The new sitemap options.
	 *
	 * @return void
	 */
	public function maybeSync( $oldSitemapOptions, $newSitemapOptions ) {
		if (
			! $this->canSync() ||
			empty( $oldSitemapOptions ) ||
			empty( $newSitemapOptions )
		) {
			return;
		}

		// Ignore the HTML sitemap, since it's not actually a sitemap to be synced with Google.
		unset( $newSitemapOptions['html'] );

		$shouldResync = false;
		foreach ( $newSitemapOptions as $type => $options ) {
			if ( empty( $oldSitemapOptions[ $type ] ) ) {
				continue;
			}

			if ( $oldSitemapOptions[ $type ]['enable'] !== $options['enable'] ) {
				$shouldResync = true;
				break;
			}
		}

		if ( ! $shouldResync ) {
			return;
		}

		aioseo()->actionScheduler->unschedule( $this->action );
		aioseo()->actionScheduler->scheduleAsync( $this->action );
	}

	/**
	 * Get the sitemaps with errors.
	 *
	 * @since 4.6.2
	 *
	 * @return array
	 */
	public function getSitemapsWithErrors() {
		$sitemaps = aioseo()->internalOptions->searchStatistics->sitemap->list;
		$ignored  = aioseo()->internalOptions->searchStatistics->sitemap->ignored;
		if ( empty( $sitemaps ) ) {
			return [];
		}

		$errors         = [];
		$pluginSitemaps = aioseo()->sitemap->helpers->getSitemapUrls();
		foreach ( $sitemaps as $sitemap ) {
			if (
				empty( $sitemap['errors'] ) ||
				in_array( $sitemap['path'], $ignored, true ) || // Skip user-ignored sitemaps.
				in_array( $sitemap['path'], $pluginSitemaps, true ) // Skip plugin sitemaps.
			) {
				continue;
			}

			$errors[] = $sitemap;
		}

		return $errors;
	}

	/**
	 * Check if the sitemap can be synced.
	 *
	 * @since 4.6.2
	 *
	 * @return bool
	 */
	private function canSync() {
		return aioseo()->searchStatistics->api->auth->isConnected() && aioseo()->internalOptions->searchStatistics->site->verified;
	}
}Common/SearchStatistics/Api/Listener.php000066600000016145151135505570014305 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchStatistics\Api;

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

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

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

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

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

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

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

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

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

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

		$this->saveAndRedirect( $profile );
	}

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

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

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

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

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

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

		$this->saveAndRedirect( $profile );
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		return $authedSite;
	}

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

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

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

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

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

		return $trustToken;
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

			unset( $data['body'] );

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

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

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

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

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

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

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

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

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

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

		return $responseBody;
	}

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

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

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

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

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

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

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

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

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

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

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

		return $this->profile;
	}

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

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

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

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

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

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

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

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

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

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

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

		return ! is_wp_error( $response );
	}

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

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

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

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

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

		return true;
	}
}Common/SearchStatistics/Api/Api.php000066600000004550151135505570013226 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchStatistics\Api;

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

/**
 * API class.
 *
 * @since   4.3.0
 * @version 4.6.2 Moved from Pro to Common.
 */
class Api {
	/**
	 * Holds the instance of the Auth class.
	 *
	 * @since 4.3.0
	 *
	 * @var Auth
	 */
	public $auth;

	/**
	 * Holds the instance of the TrustToken class.
	 *
	 * @since 4.3.0
	 *
	 * @var TrustToken
	 */
	public $trustToken;

	/**
	 * Holds the instance of the Listener class.
	 *
	 * @since 4.3.0
	 *
	 * @var Listener
	 */
	public $listener;

	/**
	 * The base URL for the Search Statistics microservice.
	 *
	 * @since 4.3.0
	 *
	 * @var string
	 */
	private $url = 'google.aioseo.com';

	/**
	 * The API version for the Search Statistics microservice.
	 *
	 * @since 4.3.0
	 *
	 * @var string
	 */
	private $version = 'v1';

	/**
	 * Class constructor.
	 *
	 * @since 4.3.0
	 */
	public function __construct() {
		$this->auth       = new Auth();
		$this->trustToken = new TrustToken();
		$this->listener   = new Listener();
	}

	/**
	 * Returns the site identifier key according to the WordPress keys.
	 *
	 * @since 4.3.0
	 *
	 * @return string The site identifier key.
	 */
	public function getSiteIdentifier() {
		$authKey       = defined( 'AUTH_KEY' ) ? AUTH_KEY : '';
		$secureAuthKey = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '';
		$loggedInKey   = defined( 'LOGGED_IN_KEY' ) ? LOGGED_IN_KEY : '';

		$siteIdentifier = $authKey . $secureAuthKey . $loggedInKey;
		$siteIdentifier = preg_replace( '/[^a-zA-Z0-9]/', '', (string) $siteIdentifier );
		$siteIdentifier = sanitize_text_field( $siteIdentifier );
		$siteIdentifier = trim( $siteIdentifier );
		$siteIdentifier = ( strlen( $siteIdentifier ) > 30 ) ? substr( $siteIdentifier, 0, 30 ) : $siteIdentifier;

		return $siteIdentifier;
	}

	/**
	 * Returns the URL of the remote endpoint.
	 *
	 * @since 4.3.0
	 *
	 * @return string The URL.
	 */
	public function getApiUrl() {
		if ( defined( 'AIOSEO_SEARCH_STATISTICS_API_URL' ) ) {
			return AIOSEO_SEARCH_STATISTICS_API_URL;
		}

		return $this->url;
	}

	/**
	 * Returns the version of the remote endpoint.
	 *
	 * @since 4.3.0
	 *
	 * @return string The version.
	 */
	public function getApiVersion() {
		if ( defined( 'AIOSEO_SEARCH_STATISTICS_API_VERSION' ) ) {
			return AIOSEO_SEARCH_STATISTICS_API_VERSION;
		}

		return $this->version;
	}
}Common/Sitemap/Root.php000066600000043317151135505570011055 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

/**
 * Determines which indexes should appear in the sitemap root index.
 *
 * @since 4.0.0
 */
class Root {
	/**
	 * Returns the indexes for the sitemap root index.
	 *
	 * @since 4.0.0
	 *
	 * @return array The indexes.
	 */
	public function indexes() {
		$indexes = [];
		if ( 'general' !== aioseo()->sitemap->type ) {
			$addonIndexes = aioseo()->addons->doAddonFunction( 'root', 'indexes' );

			foreach ( $addonIndexes as $addonIndex ) {
				if ( $addonIndex ) {
					return $addonIndex;
				}
			}

			return $indexes;
		}

		$filename   = aioseo()->sitemap->filename;
		$postTypes  = aioseo()->sitemap->helpers->includedPostTypes();
		$taxonomies = aioseo()->sitemap->helpers->includedTaxonomies();

		$indexes = array_merge( $indexes, $this->getAdditionalIndexes() );

		if ( $postTypes ) {
			$postArchives = [];

			foreach ( $postTypes as $postType ) {
				$postIndexes = $this->buildIndexesPostType( $postType );
				$indexes     = array_merge( $indexes, $postIndexes );

				if (
					get_post_type_archive_link( $postType ) &&
					aioseo()->dynamicOptions->noConflict()->searchAppearance->archives->has( $postType ) &&
					(
						aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->default ||
						! aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->noindex
					)
				) {
					$lastModifiedPostTime = aioseo()->sitemap->helpers->lastModifiedPostTime( $postType );
					if ( $lastModifiedPostTime ) {
						$postArchives[ $postType ] = $lastModifiedPostTime;
					}
				}
			}

			if ( ! empty( $postArchives ) ) {
				usort( $postArchives, function( $date1, $date2 ) {
					return $date1 < $date2 ? 1 : 0;
				} );

				$indexes[] = [
					'loc'     => aioseo()->helpers->localizedUrl( "/post-archive-$filename.xml" ),
					'lastmod' => $postArchives[0],
					'count'   => count( $postArchives )
				];
			}
		}

		if ( $taxonomies ) {
			foreach ( $taxonomies as $taxonomy ) {
				$indexes = array_merge( $indexes, $this->buildIndexesTaxonomy( $taxonomy ) );
			}
		}

		$postsTable = aioseo()->core->db->db->posts;
		if (
			aioseo()->sitemap->helpers->lastModifiedPost() &&
			aioseo()->options->sitemap->general->author &&
			aioseo()->options->searchAppearance->archives->author->show &&
			(
				aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->default ||
				! aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->noindex
			) &&
			(
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default ||
				! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindex
			)
		) {
			$usersTable        = aioseo()->core->db->db->users; // We get the table name from WPDB since multisites share the same table.
			$authorPostTypes   = aioseo()->sitemap->helpers->getAuthorPostTypes();
			$implodedPostTypes = aioseo()->helpers->implodeWhereIn( $authorPostTypes, true );
			$result            = aioseo()->core->db->execute(
				"SELECT count(*) as amountOfAuthors FROM
				(
					SELECT u.ID FROM {$usersTable} as u
					INNER JOIN {$postsTable} as p ON u.ID = p.post_author
					WHERE p.post_status = 'publish' AND p.post_type IN ( {$implodedPostTypes} )
					GROUP BY u.ID
				) as x",
				true
			)->result();

			if ( ! empty( $result[0]->amountOfAuthors ) ) {
				$indexes = array_merge( $indexes, $this->buildAuthorIndexes( (int) $result[0]->amountOfAuthors ) );
			}
		}

		if (
			aioseo()->sitemap->helpers->lastModifiedPost() &&
			aioseo()->options->sitemap->general->date &&
			aioseo()->options->searchAppearance->archives->date->show &&
			(
				aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->default ||
				! aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->noindex
			) &&
			(
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default ||
				! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindex
			)
		) {
			$result = aioseo()->core->db->execute(
				"SELECT count(*) as amountOfUrls FROM (
					SELECT post_date
					FROM {$postsTable}
					WHERE post_type = 'post' AND post_status = 'publish'
					GROUP BY
						YEAR(post_date),
						MONTH(post_date)
					LIMIT 50000
				) as dates",
				true
			)->result();

			$indexes[] = $this->buildIndex( 'date', $result[0]->amountOfUrls );
		}

		if (
			aioseo()->helpers->isWooCommerceActive() &&
			in_array( 'product_attributes', aioseo()->sitemap->helpers->includedTaxonomies(), true )
		) {
			$productAttributes = aioseo()->sitemap->content->productAttributes( true );

			if ( ! empty( $productAttributes ) ) {
				$indexes[] = $this->buildIndex( 'product_attributes', $productAttributes );
			}
		}

		if ( isset( aioseo()->standalone->buddyPress->sitemap ) ) {
			$indexes = array_merge( $indexes, aioseo()->standalone->buddyPress->sitemap->indexes() );
		}

		return apply_filters( 'aioseo_sitemap_indexes', array_filter( $indexes ) );
	}

	/**
	 * Returns the additional page indexes.
	 *
	 * @since 4.2.1
	 *
	 * @return array
	 */
	private function getAdditionalIndexes() {
		$additionalPages = [];
		if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
			$additionalPages = array_map( 'json_decode', aioseo()->options->sitemap->general->additionalPages->pages );
			$additionalPages = array_filter( $additionalPages, function( $additionalPage ) {
				return ! empty( $additionalPage->url );
			} );
		}

		$entries = [];
		foreach ( $additionalPages as $additionalPage ) {
			$entries[] = [
				'loc'        => $additionalPage->url,
				'lastmod'    => aioseo()->sitemap->helpers->lastModifiedAdditionalPage( $additionalPage ),
				'changefreq' => $additionalPage->frequency->value,
				'priority'   => $additionalPage->priority->value,
				'isTimezone' => true
			];
		}

		if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
			$entries = apply_filters( 'aioseo_sitemap_additional_pages', $entries );
		}

		$postTypes             = aioseo()->sitemap->helpers->includedPostTypes();
		$shouldIncludeHomepage = 'posts' === get_option( 'show_on_front' ) || ! in_array( 'page', $postTypes, true );
		if ( ! $shouldIncludeHomepage && ! count( $entries ) ) {
			return [];
		}

		$indexes = $this->buildAdditionalIndexes( $entries, $shouldIncludeHomepage );

		return $indexes;
	}

	/**
	 * Builds a given index.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $indexName    The index name.
	 * @param  integer $amountOfUrls The amount of URLs in the index.
	 * @return array                 The index.
	 */
	private function buildIndex( $indexName, $amountOfUrls ) {
		$filename = aioseo()->sitemap->filename;

		return [
			'loc'     => aioseo()->helpers->localizedUrl( "/$indexName-$filename.xml" ),
			'lastmod' => aioseo()->sitemap->helpers->lastModifiedPostTime(),
			'count'   => $amountOfUrls
		];
	}

	/**
	 * Builds the additional pages index.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $entries               The additional pages.
	 * @param  bool  $shouldIncludeHomepage Whether or not the homepage should be included.
	 * @return array                        The indexes.
	 */
	private function buildAdditionalIndexes( $entries, $shouldIncludeHomepage ) {
		if ( $shouldIncludeHomepage ) {
			$entries[] = [
				'loc'     => home_url(),
				'lastmod' => aioseo()->sitemap->helpers->lastModifiedPostTime()
			];
		}

		if ( empty( $entries ) ) {
			return [];
		}

		$filename  = aioseo()->sitemap->filename;
		$chunks    = aioseo()->sitemap->helpers->chunkEntries( $entries );

		$indexes = [];
		for ( $i = 0; $i < count( $chunks ); $i++ ) {
			$chunk       = array_values( $chunks[ $i ] );
			$indexNumber = 1 < count( $chunks ) ? $i + 1 : '';

			$index = [
				'loc'     => aioseo()->helpers->localizedUrl( "/addl-$filename$indexNumber.xml" ),
				'lastmod' => ! empty( $chunk[0]['lastmod'] ) ? aioseo()->helpers->dateTimeToIso8601( $chunk[0]['lastmod'] ) : '',
				'count'   => count( $chunks[ $i ] )
			];

			$indexes[] = $index;
		}

		return $indexes;
	}

	/**
	 * Builds the author archive indexes.
	 *
	 * @since 4.3.1
	 *
	 * @param  integer $amountOfAuthors The amount of author archives.
	 * @return array                    The indexes.
	 */
	private function buildAuthorIndexes( $amountOfAuthors ) {
		if ( ! $amountOfAuthors ) {
			return [];
		}

		$postTypes = aioseo()->sitemap->helpers->includedPostTypes();
		$filename  = aioseo()->sitemap->filename;
		$chunks    = $amountOfAuthors / aioseo()->sitemap->linksPerIndex;
		if ( $chunks < 1 ) {
			$chunks = 1;
		}

		$indexes = [];
		for ( $i = 0; $i < $chunks; $i++ ) {
			$indexNumber = 1 < $chunks ? $i + 1 : '';

			$usersTableName = aioseo()->core->db->db->users; // We get the table name from WPDB since multisites share the same table.
			$lastModified   = aioseo()->core->db->start( "$usersTableName as u", true )
				->select( 'MAX(p.post_modified_gmt) as lastModified' )
				->join( 'posts as p', 'u.ID = p.post_author' )
				->where( 'p.post_status', 'publish' )
				->whereIn( 'p.post_type', $postTypes )
				->groupBy( 'u.ID' )
				->orderBy( 'lastModified DESC' )
				->limit( aioseo()->sitemap->linksPerIndex, $i * aioseo()->sitemap->linksPerIndex )
				->run()
				->result();

			$lastModified = ! empty( $lastModified[0]->lastModified ) ? aioseo()->helpers->dateTimeToIso8601( $lastModified[0]->lastModified ) : '';

			$index = [
				'loc'     => aioseo()->helpers->localizedUrl( "/author-$filename$indexNumber.xml" ),
				'lastmod' => $lastModified,
				'count'   => $i + 1 === $chunks ? $amountOfAuthors % aioseo()->sitemap->linksPerIndex : aioseo()->sitemap->linksPerIndex
			];

			$indexes[] = $index;
		}

		return $indexes;
	}

	/**
	 * Builds indexes for all eligible posts of a given post type.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $postType The post type.
	 * @return array            The indexes.
	 */
	private function buildIndexesPostType( $postType ) {
		$prefix                 = aioseo()->core->db->prefix;
		$postsTable             = $prefix . 'posts';
		$aioseoPostsTable       = $prefix . 'aioseo_posts';
		$termRelationshipsTable = $prefix . 'term_relationships';
		$termTaxonomyTable      = $prefix . 'term_taxonomy';
		$termsTable             = $prefix . 'terms';
		$linksPerIndex          = aioseo()->sitemap->linksPerIndex;

		if ( 'attachment' === $postType && 'disabled' !== aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls ) {
			return [];
		}

		$excludedPostIds = [];
		$excludedTermIds = aioseo()->sitemap->helpers->excludedTerms();
		if ( ! empty( $excludedTermIds ) ) {
			$excludedTermIds = explode( ', ', $excludedTermIds );
			$excludedPostIds = aioseo()->core->db->start( 'term_relationships' )
				->select( 'object_id' )
				->whereIn( 'term_taxonomy_id', $excludedTermIds )
				->run()
				->result();

			$excludedPostIds = array_map( function( $post ) {
				return $post->object_id;
			}, $excludedPostIds );
		}

		if ( 'page' === $postType ) {
			$isStaticHomepage = 'page' === get_option( 'show_on_front' );
			if ( $isStaticHomepage ) {
				$blogPageId = (int) get_option( 'page_for_posts' );
				$excludedPostIds[] = $blogPageId;
			}
		}

		$whereClause         = '';
		$excludedPostsString = aioseo()->sitemap->helpers->excludedPosts();
		if ( ! empty( $excludedPostsString ) ) {
			$excludedPostIds = array_merge( $excludedPostIds, explode( ', ', $excludedPostsString ) );
		}

		if ( ! empty( $excludedPostIds ) ) {
			$implodedPostIds = aioseo()->helpers->implodeWhereIn( $excludedPostIds, true );
			$whereClause     = "AND p.ID NOT IN ( $implodedPostIds )";
		}

		if (
			apply_filters( 'aioseo_sitemap_woocommerce_exclude_hidden_products', true ) &&
			aioseo()->helpers->isWooCommerceActive() &&
			'product' === $postType
		) {
			$whereClause .= " AND p.ID NOT IN (
				SELECT CONVERT(tr.object_id, unsigned) AS object_id
				FROM {$termRelationshipsTable} AS tr
				JOIN {$termTaxonomyTable} AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
				JOIN {$termsTable} AS t ON tt.term_id = t.term_id
				WHERE t.name = 'exclude-from-catalog'
			)";
		}

		// Include the blog page in the posts post type unless manually excluded.
		$blogPageId = (int) get_option( 'page_for_posts' );
		if (
			$blogPageId &&
			! in_array( $blogPageId, $excludedPostIds, true ) &&
			'post' === $postType
		) {
			$whereClause .= " OR `p`.`ID` = $blogPageId ";
		}

		$posts = aioseo()->core->db->execute(
			aioseo()->core->db->db->prepare(
				"SELECT ID, post_modified_gmt
				FROM (
					SELECT @row := @row + 1 AS rownum, ID, post_modified_gmt
					FROM (
						SELECT p.ID, ap.priority, p.post_modified_gmt
						FROM {$postsTable} AS p
						LEFT JOIN {$aioseoPostsTable} AS ap ON p.ID = ap.post_id
						WHERE p.post_status = %s
							AND p.post_type = %s
							AND p.post_password = ''
							AND (ap.robots_noindex IS NULL OR ap.robots_default = 1 OR ap.robots_noindex = 0)
							{$whereClause}
						ORDER BY ap.priority DESC, p.post_modified_gmt DESC
					) AS x
					CROSS JOIN (SELECT @row := 0) AS vars
					ORDER BY post_modified_gmt DESC
				) AS y
				WHERE rownum = 1 OR rownum % %d = 1;",
				[
					'attachment' === $postType ? 'inherit' : 'publish',
					$postType,
					$linksPerIndex
				]
			),
			true
		)->result();

		$totalPosts = aioseo()->core->db->execute(
			aioseo()->core->db->db->prepare(
				"SELECT COUNT(*) as count
				FROM {$postsTable} as p
				LEFT JOIN {$aioseoPostsTable} as ap ON p.ID = ap.post_id
				WHERE p.post_status = %s
					AND p.post_type = %s
					AND p.post_password = ''
					AND (ap.robots_noindex IS NULL OR ap.robots_default = 1 OR ap.robots_noindex = 0)
					{$whereClause}
				",
				[
					'attachment' === $postType ? 'inherit' : 'publish',
					$postType
				]
			),
			true
		)->result();

		if ( $posts ) {
			$indexes   = [];
			$filename  = aioseo()->sitemap->filename;
			$postCount = count( $posts );
			for ( $i = 0; $i < $postCount; $i++ ) {
				$indexNumber = 0 !== $i && 1 < $postCount ? $i + 1 : '';

				$indexes[] = [
					'loc'     => aioseo()->helpers->localizedUrl( "/$postType-$filename$indexNumber.xml" ),
					'lastmod' => aioseo()->helpers->dateTimeToIso8601( $posts[ $i ]->post_modified_gmt ),
					'count'   => $linksPerIndex
				];
			}

			// We need to update the count of the last index since it won't necessarily be the same as the links per index.
			$indexes[ count( $indexes ) - 1 ]['count'] = $totalPosts[0]->count - ( $linksPerIndex * ( $postCount - 1 ) );

			return $indexes;
		}

		if ( ! $posts ) {
			$addonsPosts = aioseo()->addons->doAddonFunction( 'root', 'buildIndexesPostType', [ $postType ] );

			foreach ( $addonsPosts as $addonPosts ) {
				if ( $addonPosts ) {
					$posts = $addonPosts;
					break;
				}
			}
		}

		if ( ! $posts ) {
			return [];
		}

		return $this->buildIndexes( $postType, $posts );
	}

	/**
	 * Builds indexes for all eligible terms of a given taxonomy.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $taxonomy The taxonomy.
	 * @return array            The indexes.
	 */
	private function buildIndexesTaxonomy( $taxonomy ) {
		$terms = aioseo()->sitemap->content->terms( $taxonomy, [ 'root' => true ] );

		if ( ! $terms ) {
			$addonsTerms = aioseo()->addons->doAddonFunction( 'root', 'buildIndexesTaxonomy', [ $taxonomy ] );

			foreach ( $addonsTerms as $addonTerms ) {
				if ( $addonTerms ) {
					$terms = $addonTerms;
					break;
				}
			}
		}

		if ( ! $terms ) {
			return [];
		}

		return $this->buildIndexes( $taxonomy, $terms );
	}

	/**
	 * Builds indexes for a given type.
	 *
	 * Acts as a helper function for buildIndexesPostTypes() and buildIndexesTaxonomies().
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name    The name of the object parent.
	 * @param  array  $entries The sitemap entries.
	 * @return array           The indexes.
	 */
	public function buildIndexes( $name, $entries ) {
		$filename = aioseo()->sitemap->filename;
		$chunks   = aioseo()->sitemap->helpers->chunkEntries( $entries );
		$indexes  = [];
		for ( $i = 0; $i < count( $chunks ); $i++ ) {
			$chunk       = array_values( $chunks[ $i ] );
			$indexNumber = 0 !== $i && 1 < count( $chunks ) ? $i + 1 : '';

			$index = [
				'loc'   => aioseo()->helpers->localizedUrl( "/$name-$filename$indexNumber.xml" ),
				'count' => count( $chunks[ $i ] )
			];

			if ( isset( $entries[0]->ID ) ) {
				$ids = array_map( function( $post ) {
					return $post->ID;
				}, $chunk );
				$ids = implode( "', '", $ids );

				$lastModified = null;
				if ( ! apply_filters( 'aioseo_sitemap_lastmod_disable', false ) ) {
					$lastModified = aioseo()->core->db
						->start( aioseo()->core->db->db->posts . ' as p', true )
						->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
						->whereRaw( "( `p`.`ID` IN ( '$ids' ) )" )
						->run()
						->result();
				}

				if ( ! empty( $lastModified[0]->last_modified ) ) {
					$index['lastmod'] = aioseo()->helpers->dateTimeToIso8601( $lastModified[0]->last_modified );
				}
				$indexes[] = $index;
				continue;
			}

			$termIds = [];
			foreach ( $chunk as $term ) {
				$termIds[] = $term->term_id;
			}
			$termIds = implode( "', '", $termIds );

			$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';

			$lastModified = null;
			if ( ! apply_filters( 'aioseo_sitemap_lastmod_disable', false ) ) {
				$lastModified = aioseo()->core->db
					->start( aioseo()->core->db->db->posts . ' as p', true )
					->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
					->whereRaw( "
					( `p`.`ID` IN
						(
							SELECT CONVERT(`tr`.`object_id`, unsigned)
							FROM `$termRelationshipsTable` as tr
							WHERE `tr`.`term_taxonomy_id` IN ( '$termIds' )
						)
					)" )
					->run()
					->result();
			}

			if ( ! empty( $lastModified[0]->last_modified ) ) {
				$index['lastmod'] = aioseo()->helpers->dateTimeToIso8601( $lastModified[0]->last_modified );
			}
			$indexes[] = $index;
		}

		return $indexes;
	}
}Common/Sitemap/Output.php000066600000011644151135505570011430 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

/**
 * Handles outputting the sitemap.
 *
 * @since 4.0.0
 */
class Output {
	/**
	 * Outputs the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $entries The sitemap entries.
	 * @return void
	 */
	public function output( $entries ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		if ( ! in_array( aioseo()->sitemap->type, [ 'general', 'rss' ], true ) ) {
			return;
		}

		// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$entries       = aioseo()->sitemap->helpers->decodeSitemapEntries( $entries );
		$charset       = aioseo()->helpers->getCharset();
		$excludeImages = aioseo()->sitemap->helpers->excludeImages();
		$generation    = ! isset( aioseo()->sitemap->isStatic ) || aioseo()->sitemap->isStatic ? __( 'statically', 'all-in-one-seo-pack' ) : __( 'dynamically', 'all-in-one-seo-pack' );
		$version       = aioseo()->helpers->getAioseoVersion();

		if ( ! empty( $version ) ) {
			$version = 'v' . $version;
		}

		// Clear all output buffers to avoid conflicts.
		aioseo()->helpers->clearBuffers();

		echo '<?xml version="1.0" encoding="' . esc_attr( $charset ) . "\"?>\r\n";
		echo '<!-- ' . sprintf(
			// Translators: 1 - "statically" or "dynamically", 2 - The date, 3 - The time, 4 - The plugin name ("All in One SEO"), 5 - Currently installed version.
			esc_html__( 'This sitemap was %1$s generated on %2$s at %3$s by %4$s %5$s - the original SEO plugin for WordPress.', 'all-in-one-seo-pack' ),
			esc_html( $generation ),
			esc_html( date_i18n( get_option( 'date_format' ) ) ),
			esc_html( date_i18n( get_option( 'time_format' ) ) ),
			esc_html( AIOSEO_PLUGIN_NAME ),
			esc_html( $version )
		) . ' -->';

		if ( 'rss' === aioseo()->sitemap->type ) {
			$xslUrl = home_url() . '/default-sitemap.xsl';

			if ( ! is_multisite() ) {
				$title       = get_bloginfo( 'name' );
				$description = get_bloginfo( 'blogdescription' );
				$link        = home_url();
			} else {
				$title       = get_blog_option( get_current_blog_id(), 'blogname' );
				$description = get_blog_option( get_current_blog_id(), 'blogdescription' );
				$link        = get_blog_option( get_current_blog_id(), 'siteurl' );
			}

			$ttl = apply_filters( 'aioseo_sitemap_rss_ttl', 60 );

			echo "\r\n\r\n<?xml-stylesheet type=\"text/xsl\" href=\"" . esc_url( $xslUrl ) . "\"?>\r\n";
			include_once AIOSEO_DIR . '/app/Common/Views/sitemap/xml/rss.php';

			return;
		}

		if ( 'root' === aioseo()->sitemap->indexName && aioseo()->sitemap->indexes ) {
			$xslUrl = add_query_arg( 'sitemap', aioseo()->sitemap->indexName, home_url() . '/default-sitemap.xsl' );

			echo "\r\n\r\n<?xml-stylesheet type=\"text/xsl\" href=\"" . esc_url( $xslUrl ) . "\"?>\r\n";
			include AIOSEO_DIR . '/app/Common/Views/sitemap/xml/root.php';

			return;
		}

		$xslUrl = add_query_arg( 'sitemap', aioseo()->sitemap->indexName, home_url() . '/default-sitemap.xsl' );

		echo "\r\n\r\n<?xml-stylesheet type=\"text/xsl\" href=\"" . esc_url( $xslUrl ) . "\"?>\r\n";
		include AIOSEO_DIR . '/app/Common/Views/sitemap/xml/default.php';
	}

	/**
	 * Escapes and echoes the given XML tag value.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $value The tag value.
	 * @param  bool   $wrap  Whether the value should we wrapped in a CDATA section.
	 * @return void
	 */
	public function escapeAndEcho( $value, $wrap = true ) {
		$safeText = is_string( $value ) ? wp_check_invalid_utf8( $value, true ) : $value;
		$isZero   = is_numeric( $value ) ? 0 === (int) $value : false;
		if ( ! $safeText && ! $isZero ) {
			return;
		}

		$cdataRegex = '\<\!\[CDATA\[.*?\]\]\>';
		$regex      = "/(?=.*?{$cdataRegex})(?<non_cdata_followed_by_cdata>(.*?))(?<cdata>({$cdataRegex}))|(?<non_cdata>(.*))/sx";

		$safeText = (string) preg_replace_callback(
			$regex,
			static function( $matches ) {
				if ( ! $matches[0] ) {
					return '';
				}

				if ( ! empty( $matches['non_cdata'] ) ) {
					// Escape HTML entities in the non-CDATA section.
					return _wp_specialchars( $matches['non_cdata'], ENT_XML1 );
				}

				// Return the CDATA Section unchanged, escape HTML entities in the rest.
				return _wp_specialchars( $matches['non_cdata_followed_by_cdata'], ENT_XML1 ) . $matches['cdata'];
			},
			$safeText
		);

		$safeText = $safeText ? $safeText : ( $isZero ? $value : '' );

		if ( ! $wrap ) {
			return print( $safeText ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}

		printf( '<![CDATA[%1$s]]>', $safeText ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Returns the URL for the sitemap stylesheet.
	 *
	 * This is needed for compatibility with multilingual plugins such as WPML.
	 *
	 * @since 4.0.0
	 *
	 * @return string The URL to the sitemap stylesheet.
	 */
	private function xslUrl() {
		return esc_url( apply_filters( 'aioseo_sitemap_xsl_url', aioseo()->helpers->localizedUrl( '/sitemap.xsl' ) ) );
	}
}Common/Sitemap/Content.php000066600000074527151135505570011553 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Determines which content should be included in the sitemap.
 *
 * @since 4.0.0
 */
class Content {
	/**
	 * Returns the entries for the requested sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @return array The sitemap entries.
	 */
	public function get() {
		if ( ! in_array( aioseo()->sitemap->type, [ 'general', 'rss' ], true ) || ! $this->isEnabled() ) {
			return [];
		}

		if ( 'rss' === aioseo()->sitemap->type ) {
			return $this->rss();
		}

		if ( 'general' !== aioseo()->sitemap->type ) {
			return [];
		}

		$indexesEnabled = aioseo()->options->sitemap->general->indexes;
		if ( ! $indexesEnabled ) {
			if ( 'root' === aioseo()->sitemap->indexName ) {
				// If indexes are disabled, throw all entries together into one big file.
				return $this->nonIndexed();
			}

			return [];
		}

		if ( 'root' === aioseo()->sitemap->indexName ) {
			return aioseo()->sitemap->root->indexes();
		}

		// Check if requested index has a dedicated method.
		$methodName = aioseo()->helpers->dashesToCamelCase( aioseo()->sitemap->indexName );
		if ( method_exists( $this, $methodName ) ) {
			return $this->$methodName();
		}

		// Check if requested index is a registered post type.
		if ( in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
			return $this->posts( aioseo()->sitemap->indexName );
		}

		// Check if requested index is a registered taxonomy.
		if (
			in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedTaxonomies(), true ) &&
			'product_attributes' !== aioseo()->sitemap->indexName
		) {
			return $this->terms( aioseo()->sitemap->indexName );
		}

		if (
			aioseo()->helpers->isWooCommerceActive() &&
			in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedTaxonomies(), true ) &&
			'product_attributes' === aioseo()->sitemap->indexName
		) {
			return $this->productAttributes();
		}

		return [];
	}

	/**
	 * Returns the total entries number for the requested sitemap.
	 *
	 * @since 4.1.5
	 *
	 * @return int The total entries number.
	 */
	public function getTotal() {
		if ( ! in_array( aioseo()->sitemap->type, [ 'general', 'rss' ], true ) || ! $this->isEnabled() ) {
			return 0;
		}

		if ( 'rss' === aioseo()->sitemap->type ) {
			return count( $this->rss() );
		}

		if ( 'general' !== aioseo()->sitemap->type ) {
			return 0;
		}

		$indexesEnabled = aioseo()->options->sitemap->general->indexes;
		if ( ! $indexesEnabled ) {
			if ( 'root' === aioseo()->sitemap->indexName ) {
				// If indexes are disabled, throw all entries together into one big file.
				return count( $this->nonIndexed() );
			}

			return 0;
		}

		if ( 'root' === aioseo()->sitemap->indexName ) {
			return count( aioseo()->sitemap->root->indexes() );
		}

		// Check if requested index has a dedicated method.
		$methodName = aioseo()->helpers->dashesToCamelCase( aioseo()->sitemap->indexName );
		if ( method_exists( $this, $methodName ) ) {
			$res = $this->$methodName();

			return ! empty( $res ) ? count( $res ) : 0;
		}

		// Check if requested index is a registered post type.
		if ( in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
			return aioseo()->sitemap->query->posts( aioseo()->sitemap->indexName, [ 'count' => true ] );
		}

		// Check if requested index is a registered taxonomy.
		if ( in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedTaxonomies(), true ) ) {
			return aioseo()->sitemap->query->terms( aioseo()->sitemap->indexName, [ 'count' => true ] );
		}

		return 0;
	}

	/**
	 * Checks if the requested sitemap is enabled.
	 *
	 * @since 4.0.0
	 *
	 * @return boolean Whether the sitemap is enabled.
	 */
	public function isEnabled() {
		$options = aioseo()->options->noConflict();
		if ( ! $options->sitemap->{aioseo()->sitemap->type}->enable ) {
			return false;
		}

		if ( $options->sitemap->{aioseo()->sitemap->type}->postTypes->all ) {
			return true;
		}

		$included = aioseo()->sitemap->helpers->includedPostTypes();

		return ! empty( $included );
	}

	/**
	 * Returns all sitemap entries if indexing is disabled.
	 *
	 * @since 4.0.0
	 *
	 * @return array $entries The sitemap entries.
	 */
	private function nonIndexed() {
		$additional       = $this->addl();
		$postTypes        = aioseo()->sitemap->helpers->includedPostTypes();
		$isStaticHomepage = 'page' === get_option( 'show_on_front' );
		$blogPageEntry    = [];
		$homePageEntry    = ! $isStaticHomepage ? [ array_shift( $additional ) ] : [];
		$entries          = array_merge( $additional, $this->author(), $this->date(), $this->postArchive() );

		if ( $postTypes ) {
			foreach ( $postTypes as $postType ) {
				$postTypeEntries = $this->posts( $postType );

				// If we don't have a static homepage, it's business as usual.
				if ( ! $isStaticHomepage ) {
					$entries = array_merge( $entries, $postTypeEntries );
					continue;
				}

				$homePageId = (int) get_option( 'page_on_front' );
				$blogPageId = (int) get_option( 'page_for_posts' );

				if ( 'post' === $postType && $blogPageId ) {
					$blogPageEntry[] = array_shift( $postTypeEntries );
				}

				if ( 'page' === $postType && $homePageId ) {
					$homePageEntry[] = array_shift( $postTypeEntries );
				}

				$entries = array_merge( $entries, $postTypeEntries );
			}
		}

		$taxonomies = aioseo()->sitemap->helpers->includedTaxonomies();
		if ( $taxonomies ) {
			foreach ( $taxonomies as $taxonomy ) {
				$entries = array_merge( $entries, $this->terms( $taxonomy ) );
			}
		}

		// Sort first by priority, then by last modified date.
		usort( $entries, function ( $a, $b ) {
			// If the priorities are equal, sort by last modified date.
			if ( $a['priority'] === $b['priority'] ) {
				return $a['lastmod'] > $b['lastmod'] ? -1 : 1;
			}

			return $a['priority'] > $b['priority'] ? -1 : 1;
		} );

		// Merge the arrays with the home page always first.
		return array_merge( $homePageEntry, $blogPageEntry, $entries );
	}

	/**
	 * Returns all post entries for a given post type.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $postType       The name of the post type.
	 * @param  array  $additionalArgs Any additional arguments for the post query.
	 * @return array                  The sitemap entries.
	 */
	public function posts( $postType, $additionalArgs = [] ) {
		$posts = aioseo()->sitemap->query->posts( $postType, $additionalArgs );
		if ( ! $posts ) {
			return [];
		}

		// Return if we're determining the root indexes.
		if ( ! empty( $additionalArgs['root'] ) && $additionalArgs['root'] ) {
			return $posts;
		}

		$entries          = [];
		$isStaticHomepage = 'page' === get_option( 'show_on_front' );
		$homePageId       = (int) get_option( 'page_on_front' );
		$excludeImages    = aioseo()->sitemap->helpers->excludeImages();
		foreach ( $posts as $post ) {
			$entry = [
				'loc'        => get_permalink( $post->ID ),
				'lastmod'    => aioseo()->helpers->dateTimeToIso8601( $this->getLastModified( $post ) ),
				'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', $post, $postType ),
				'priority'   => aioseo()->sitemap->priority->priority( 'postTypes', $post, $postType ),
			];

			if ( ! $excludeImages ) {
				$entry['images'] = ! empty( $post->images ) ? json_decode( $post->images ) : [];
			}

			// Override priority/frequency for static homepage.
			if ( $isStaticHomepage && ( $homePageId === $post->ID || aioseo()->helpers->wpmlIsHomePage( $post->ID ) ) ) {
				$entry['loc']        = aioseo()->helpers->maybeRemoveTrailingSlash( aioseo()->helpers->wpmlHomeUrl( $post->ID ) ?: $entry['loc'] );
				$entry['changefreq'] = aioseo()->sitemap->priority->frequency( 'homePage' );
				$entry['priority']   = aioseo()->sitemap->priority->priority( 'homePage' );
			}

			$entries[] = apply_filters( 'aioseo_sitemap_post', $entry, $post->ID, $postType, 'post' );
		}

		// We can't remove the post type here because other plugins rely on it.
		return apply_filters( 'aioseo_sitemap_posts', $entries, $postType );
	}

	/**
	 * Returns all post archive entries.
	 *
	 * @since 4.0.0
	 *
	 * @return array $entries The sitemap entries.
	 */
	private function postArchive() {
		$entries = [];
		foreach ( aioseo()->sitemap->helpers->includedPostTypes( true ) as $postType ) {
			if (
				aioseo()->dynamicOptions->noConflict()->searchAppearance->archives->has( $postType ) &&
				! aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->default &&
				aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->noindex
			) {
				continue;
			}

			$post = aioseo()->core->db
				->start( aioseo()->core->db->db->posts . ' as p', true )
				->select( 'p.ID' )
				->where( 'p.post_status', 'publish' )
				->where( 'p.post_type', $postType )
				->limit( 1 )
				->run()
				->result();

			if ( ! $post ) {
				continue;
			}

			$url = get_post_type_archive_link( $postType );
			if ( $url ) {
				$entry = [
					'loc'        => $url,
					'lastmod'    => aioseo()->sitemap->helpers->lastModifiedPostTime( $postType ),
					'changefreq' => aioseo()->sitemap->priority->frequency( 'archive' ),
					'priority'   => aioseo()->sitemap->priority->priority( 'archive' ),
				];

				// To be consistent with our other entry filters, we need to pass the entry ID as well, but as null in this case.
				$entries[] = apply_filters( 'aioseo_sitemap_archive_entry', $entry, null, $postType, 'archive' );
			}
		}

		return apply_filters( 'aioseo_sitemap_post_archives', $entries );
	}

	/**
	 * Returns all term entries for a given taxonomy.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $taxonomy       The name of the taxonomy.
	 * @param  array  $additionalArgs Any additional arguments for the term query.
	 * @return array                  The sitemap entries.
	 */
	public function terms( $taxonomy, $additionalArgs = [] ) {
		$terms = aioseo()->sitemap->query->terms( $taxonomy, $additionalArgs );
		if ( ! $terms ) {
			return [];
		}

		// Get all registered post types for the taxonomy.
		$postTypes = [];
		foreach ( get_post_types() as $postType ) {
			$taxonomies = get_object_taxonomies( $postType );
			foreach ( $taxonomies as $name ) {
				if ( $taxonomy === $name ) {
					$postTypes[] = $postType;
				}
			}
		}

		// Return if we're determining the root indexes.
		if ( ! empty( $additionalArgs['root'] ) && $additionalArgs['root'] ) {
			return $terms;
		}

		$entries = [];
		foreach ( $terms as $term ) {
			$entry = [
				'loc'        => get_term_link( $term->term_id ),
				'lastmod'    => $this->getTermLastModified( $term ),
				'changefreq' => aioseo()->sitemap->priority->frequency( 'taxonomies', $term, $taxonomy ),
				'priority'   => aioseo()->sitemap->priority->priority( 'taxonomies', $term, $taxonomy ),
				'images'     => aioseo()->sitemap->image->term( $term )
			];

			$entries[] = apply_filters( 'aioseo_sitemap_term', $entry, $term->term_id, $term->taxonomy, 'term' );
		}

		return apply_filters( 'aioseo_sitemap_terms', $entries );
	}

	/**
	 * Returns the last modified date for a given term.
	 *
	 * @since 4.0.0
	 *
	 * @param  int|object $term The term data object.
	 * @return string           The lastmod timestamp.
	 */
	public function getTermLastModified( $term ) {
		$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
		$termTaxonomyTable      = aioseo()->core->db->db->prefix . 'term_taxonomy';

		// If the term is an ID, get the term object.
		if ( is_numeric( $term ) ) {
			$term = aioseo()->helpers->getTerm( $term );
		}

		// First, check the count of the term. If it's 0, then we're dealing with a parent term that does not have
		// posts assigned to it. In this case, we need to get the last modified date of all its children.
		if ( empty( $term->count ) ) {
			$lastModified = aioseo()->core->db
				->start( aioseo()->core->db->db->posts . ' as p', true )
				->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
				->where( 'p.post_status', 'publish' )
				->whereRaw( "
				( `p`.`ID` IN
					(
						SELECT CONVERT(`tr`.`object_id`, unsigned)
						FROM `$termRelationshipsTable` as tr
						JOIN `$termTaxonomyTable` as tt ON `tr`.`term_taxonomy_id` = `tt`.`term_taxonomy_id`
						WHERE `tt`.`term_id` IN
							(
								SELECT `tt`.`term_id`
								FROM `$termTaxonomyTable` as tt
								WHERE `tt`.`parent` = '{$term->term_id}'
							)
					)
				)" )
				->run()
				->result();
		} else {
			$lastModified = aioseo()->core->db
				->start( aioseo()->core->db->db->posts . ' as p', true )
				->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
				->where( 'p.post_status', 'publish' )
				->whereRaw( "
				( `p`.`ID` IN
					(
						SELECT CONVERT(`tr`.`object_id`, unsigned)
						FROM `$termRelationshipsTable` as tr
						JOIN `$termTaxonomyTable` as tt ON `tr`.`term_taxonomy_id` = `tt`.`term_taxonomy_id`
						WHERE `tt`.`term_id` = '{$term->term_id}'
					)
				)" )
				->run()
				->result();
		}

		$lastModified = $lastModified[0]->last_modified ?? '';

		return aioseo()->helpers->dateTimeToIso8601( $lastModified );
	}

	/**
	 * Returns all additional pages.
	 *
	 * @since 4.0.0
	 *
	 * @param  bool  $shouldChunk Whether the entries should be chuncked. Is set to false when the static sitemap is generated.
	 * @return array              The sitemap entries.
	 */
	public function addl( $shouldChunk = true ) {
		$additionalPages = [];
		if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
			$additionalPages = array_map( 'json_decode', aioseo()->options->sitemap->general->additionalPages->pages );
			$additionalPages = array_filter( $additionalPages, function( $additionalPage ) {
				return ! empty( $additionalPage->url );
			} );
		}

		$entries = [];
		foreach ( $additionalPages as $additionalPage ) {
			$entries[] = [
				'loc'        => $additionalPage->url,
				'lastmod'    => aioseo()->sitemap->helpers->lastModifiedAdditionalPage( $additionalPage ),
				'changefreq' => $additionalPage->frequency->value,
				'priority'   => $additionalPage->priority->value,
				'isTimezone' => true
			];
		}

		$postTypes             = aioseo()->sitemap->helpers->includedPostTypes();
		$shouldIncludeHomepage = 'posts' === get_option( 'show_on_front' ) || ! in_array( 'page', $postTypes, true );
		if ( $shouldIncludeHomepage ) {
			$frontPageId  = (int) get_option( 'page_on_front' );
			$frontPageUrl = aioseo()->helpers->localizedUrl( '/' );
			$post         = aioseo()->helpers->getPost( $frontPageId );

			$homepageEntry = [
				'loc'        => aioseo()->helpers->maybeRemoveTrailingSlash( $frontPageUrl ),
				'lastmod'    => $post ? aioseo()->helpers->dateTimeToIso8601( $this->getLastModified( $post ) ) : aioseo()->sitemap->helpers->lastModifiedPostTime(),
				'changefreq' => aioseo()->sitemap->priority->frequency( 'homePage' ),
				'priority'   => aioseo()->sitemap->priority->priority( 'homePage' )
			];

			$translatedHomepages = aioseo()->helpers->wpmlHomePages();
			foreach ( $translatedHomepages as $languageCode => $translatedHomepage ) {
				if ( untrailingslashit( $translatedHomepage['url'] ) !== untrailingslashit( $homepageEntry['loc'] ) ) {
					$homepageEntry['languages'][] = [
						'language' => $languageCode,
						'location' => $translatedHomepage['url']
					];
				}
			}

			// Add homepage to the first position.
			array_unshift( $entries, $homepageEntry );
		}

		if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
			$entries = apply_filters( 'aioseo_sitemap_additional_pages', $entries );
		}

		if ( empty( $entries ) ) {
			return [];
		}

		if ( aioseo()->options->sitemap->general->indexes && $shouldChunk ) {
			$entries = aioseo()->sitemap->helpers->chunkEntries( $entries );
			$entries = $entries[ aioseo()->sitemap->pageNumber ] ?? [];
		}

		return $entries;
	}

	/**
	 * Returns all author archive entries.
	 *
	 * @since 4.0.0
	 *
	 * @return array The sitemap entries.
	 */
	public function author() {
		if (
			! aioseo()->sitemap->helpers->lastModifiedPost() ||
			! aioseo()->options->sitemap->general->author ||
			! aioseo()->options->searchAppearance->archives->author->show ||
			(
				! aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->default &&
				aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->noindex
			) ||
			(
				aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->default &&
				(
					! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default &&
					aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindex
				)
			)
		) {
			return [];
		}

		// Allow users to filter the authors in case their sites use a membership plugin or have custom code that affect the authors on their site.
		// e.g. there might be additional roles/conditions that need to be checked here.
		$authors = apply_filters( 'aioseo_sitemap_authors', [] );
		if ( empty( $authors ) ) {
			$usersTableName = aioseo()->core->db->db->users; // We get the table name from WPDB since multisites share the same table.
			$authors        = aioseo()->core->db->start( "$usersTableName as u", true )
				->select( 'u.ID as ID, u.user_nicename as nicename, MAX(p.post_modified_gmt) as lastModified' )
				->join( 'posts as p', 'u.ID = p.post_author' )
				->where( 'p.post_status', 'publish' )
				->whereIn( 'p.post_type', aioseo()->sitemap->helpers->getAuthorPostTypes() )
				->groupBy( 'u.ID' )
				->orderBy( 'lastModified DESC' )
				->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->pageNumber * aioseo()->sitemap->linksPerIndex )
				->run()
				->result();
		}

		if ( empty( $authors ) ) {
			return [];
		}

		$entries = [];
		foreach ( $authors as $authorData ) {
			$entry = [
				'loc'        => ! empty( $authorData->authorUrl )
					? $authorData->authorUrl
					: get_author_posts_url( $authorData->ID, $authorData->nicename ?: '' ),
				'lastmod'    => aioseo()->helpers->dateTimeToIso8601( $authorData->lastModified ),
				'changefreq' => aioseo()->sitemap->priority->frequency( 'author' ),
				'priority'   => aioseo()->sitemap->priority->priority( 'author' )
			];

			$entries[] = apply_filters( 'aioseo_sitemap_author_entry', $entry, $authorData->ID, $authorData->nicename, 'author' );
		}

		return apply_filters( 'aioseo_sitemap_author_archives', $entries );
	}

	/**
	 * Returns all data archive entries.
	 *
	 * @since 4.0.0
	 *
	 * @return array The sitemap entries.
	 */
	public function date() {
		if (
			! aioseo()->sitemap->helpers->lastModifiedPost() ||
			! aioseo()->options->sitemap->general->date ||
			! aioseo()->options->searchAppearance->archives->date->show ||
			(
				! aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->default &&
				aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->noindex
			) ||
			(
				aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->default &&
				(
					! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default &&
					aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindex
				)
			)
		) {
			return [];
		}

		$postsTable = aioseo()->core->db->db->posts;
		$dates      = aioseo()->core->db->execute(
			"SELECT
				YEAR(post_date) AS `year`,
				MONTH(post_date) AS `month`,
				post_date_gmt,
				post_modified_gmt
			FROM {$postsTable}
			WHERE post_type = 'post' AND post_status = 'publish'
			GROUP BY
				YEAR(post_date),
				MONTH(post_date)
			ORDER BY post_date ASC 
			LIMIT 50000",
			true
		)->result();

		if ( empty( $dates ) ) {
			return [];
		}

		$entries = [];
		$year    = '';
		foreach ( $dates as $date ) {
			$entry = [
				'lastmod'    => aioseo()->helpers->dateTimeToIso8601( $this->getLastModified( $date ) ),
				'changefreq' => aioseo()->sitemap->priority->frequency( 'date' ),
				'priority'   => aioseo()->sitemap->priority->priority( 'date' ),
			];

			// Include each year only once.
			if ( $year !== $date->year ) {
				$year         = $date->year;
				$entry['loc'] = get_year_link( $date->year );
				$entries[]    = apply_filters( 'aioseo_sitemap_date_entry', $entry, $date, 'year', 'date' );
			}

			$entry['loc'] = get_month_link( $date->year, $date->month );
			$entries[]    = apply_filters( 'aioseo_sitemap_date_entry', $entry, $date, 'month', 'date' );
		}

		return apply_filters( 'aioseo_sitemap_date_archives', $entries );
	}

	/**
	 * Returns all entries for the RSS Sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @return array The sitemap entries.
	 */
	public function rss() {
		$posts = aioseo()->sitemap->query->posts(
			aioseo()->sitemap->helpers->includedPostTypes(),
			[ 'orderBy' => '`p`.`post_modified_gmt` DESC' ]
		);

		if ( ! count( $posts ) ) {
			return [];
		}

		$entries = [];
		foreach ( $posts as $post ) {
			$entry = [
				'guid'        => get_permalink( $post->ID ),
				'title'       => get_the_title( $post ),
				'description' => get_post_field( 'post_excerpt', $post->ID ),
				'pubDate'     => aioseo()->helpers->dateTimeToRfc822( $this->getLastModified( $post ) )
			];

			// If the entry is the homepage, we need to check if the permalink structure
			// does not have a trailing slash. If so, we need to strip it because WordPress adds it
			// regardless for the home_url() in get_page_link() which is used in the get_permalink() function.
			static $homeId = null;
			if ( null === $homeId ) {
				$homeId = get_option( 'page_for_posts' );
			}

			if ( aioseo()->helpers->getHomePageId() === $post->ID ) {
				$entry['guid'] = aioseo()->helpers->maybeRemoveTrailingSlash( $entry['guid'] );
			}

			$entries[] = apply_filters( 'aioseo_sitemap_post_rss', $entry, $post->ID, $post->post_type, 'post' );
		}

		usort( $entries, function( $a, $b ) {
			return $a['pubDate'] < $b['pubDate'] ? 1 : 0;
		});

		return apply_filters( 'aioseo_sitemap_rss', $entries );
	}

	/**
	 * Returns the last modified date for a given post.
	 *
	 * @since 4.6.3
	 *
	 * @param  object $post The post object.
	 *
	 * @return string The last modified date.
	 */
	public function getLastModified( $post ) {
		$publishDate      = $post->post_date_gmt;
		$lastModifiedDate = $post->post_modified_gmt;

		// Get the date which is the latest.
		return $lastModifiedDate > $publishDate ? $lastModifiedDate : $publishDate;
	}

	/**
	 * Returns all entries for the BuddyPress Activity Sitemap.
	 * This method is automagically called from {@see get()} if the current index name equals to 'bp-activity'
	 *
	 * @since 4.7.6
	 *
	 * @return array The sitemap entries.
	 */
	public function bpActivity() {
		$entries = [];
		if ( ! in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
			return $entries;
		}

		$postType = 'bp-activity';
		$query    = aioseo()->core->db
			->start( 'bp_activity as a' )
			->select( '`a`.`id`, `a`.`date_recorded`' )
			->whereRaw( "a.is_spam = 0 AND a.hide_sitewide = 0 AND a.type NOT IN ('activity_comment', 'last_activity')" )
			->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->offset )
			->orderBy( 'a.date_recorded DESC' );

		$items = $query->run()
						->result();

		foreach ( $items as $item ) {
			$entry = [
				'loc'        => BuddyPressIntegration::getComponentSingleUrl( 'activity', $item->id ),
				'lastmod'    => aioseo()->helpers->dateTimeToIso8601( $item->date_recorded ),
				'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
				'priority'   => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
			];

			$entries[] = apply_filters( 'aioseo_sitemap_post', $entry, $item->id, $postType );
		}

		$archiveUrl = BuddyPressIntegration::getComponentArchiveUrl( 'activity' );
		if (
			aioseo()->helpers->isUrl( $archiveUrl ) &&
			! in_array( $postType, aioseo()->helpers->getNoindexedObjects( 'archives' ), true )
		) {
			$lastMod = ! empty( $items[0] ) ? $items[0]->date_recorded : current_time( 'mysql' );
			$entry   = [
				'loc'        => $archiveUrl,
				'lastmod'    => $lastMod,
				'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
				'priority'   => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
			];

			array_unshift( $entries, $entry );
		}

		return apply_filters( 'aioseo_sitemap_posts', $entries, $postType );
	}

	/**
	 * Returns all entries for the BuddyPress Group Sitemap.
	 * This method is automagically called from {@see get()} if the current index name equals to 'bp-group'
	 *
	 * @since 4.7.6
	 *
	 * @return array The sitemap entries.
	 */
	public function bpGroup() {
		$entries = [];
		if ( ! in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
			return $entries;
		}

		$postType = 'bp-group';
		$query    = aioseo()->core->db
			->start( 'bp_groups as g' )
			->select( '`g`.`id`, `g`.`date_created`, `gm`.`meta_value` as date_modified' )
			->leftJoin( 'bp_groups_groupmeta as gm', 'g.id = gm.group_id' )
			->whereRaw( "g.status = 'public' AND gm.meta_key = 'last_activity'" )
			->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->offset )
			->orderBy( 'gm.meta_value DESC' )
			->orderBy( 'g.date_created DESC' );

		$items = $query->run()
						->result();

		foreach ( $items as $item ) {
			$lastMod = $item->date_modified ?: $item->date_created;
			$entry   = [
				'loc'        => BuddyPressIntegration::getComponentSingleUrl( 'group', BuddyPressIntegration::callFunc( 'bp_get_group_by', 'id', $item->id ) ),
				'lastmod'    => aioseo()->helpers->dateTimeToIso8601( $lastMod ),
				'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
				'priority'   => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
			];

			$entries[] = apply_filters( 'aioseo_sitemap_post', $entry, $item->id, $postType );
		}

		$archiveUrl = BuddyPressIntegration::getComponentArchiveUrl( 'group' );
		if (
			aioseo()->helpers->isUrl( $archiveUrl ) &&
			! in_array( $postType, aioseo()->helpers->getNoindexedObjects( 'archives' ), true )
		) {
			$lastMod = ! empty( $items[0] ) ? $items[0]->date_modified : current_time( 'mysql' );
			$entry   = [
				'loc'        => $archiveUrl,
				'lastmod'    => $lastMod,
				'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
				'priority'   => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
			];

			array_unshift( $entries, $entry );
		}

		return apply_filters( 'aioseo_sitemap_posts', $entries, $postType );
	}

	/**
	 * Returns all entries for the BuddyPress Member Sitemap.
	 * This method is automagically called from {@see get()} if the current index name equals to 'bp-member'
	 *
	 * @since 4.7.6
	 *
	 * @return array The sitemap entries.
	 */
	public function bpMember() {
		$entries = [];
		if ( ! in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
			return $entries;
		}

		$postType = 'bp-member';
		$query    = aioseo()->core->db
			->start( 'bp_activity as a' )
			->select( '`a`.`user_id` as id, `a`.`date_recorded`' )
			->whereRaw( "a.component = 'members' AND a.type = 'last_activity'" )
			->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->offset )
			->orderBy( 'a.date_recorded DESC' );

		$items = $query->run()
			->result();

		foreach ( $items as $item ) {
			$entry = [
				'loc'        => BuddyPressIntegration::getComponentSingleUrl( 'member', $item->id ),
				'lastmod'    => aioseo()->helpers->dateTimeToIso8601( $item->date_recorded ),
				'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
				'priority'   => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
			];

			$entries[] = apply_filters( 'aioseo_sitemap_post', $entry, $item->id, $postType );
		}

		$archiveUrl = BuddyPressIntegration::getComponentArchiveUrl( 'member' );
		if (
			aioseo()->helpers->isUrl( $archiveUrl ) &&
			! in_array( $postType, aioseo()->helpers->getNoindexedObjects( 'archives' ), true )
		) {
			$lastMod = ! empty( $items[0] ) ? $items[0]->date_recorded : current_time( 'mysql' );
			$entry   = [
				'loc'        => $archiveUrl,
				'lastmod'    => $lastMod,
				'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
				'priority'   => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
			];

			array_unshift( $entries, $entry );
		}

		return apply_filters( 'aioseo_sitemap_posts', $entries, $postType );
	}

	/**
	 * Returns all entries for the WooCommerce Product Attributes sitemap.
	 * Note: This sitemap does not support pagination.
	 *
	 * @since 4.7.8
	 *
	 * @param  bool  $count Whether to return the count of the entries. This is used to determine the indexes.
	 * @return array        The sitemap entries.
	 */
	public function productAttributes( $count = false ) {
		$aioseoTermsTable           = aioseo()->core->db->prefix . 'aioseo_terms';
		$wcAttributeTaxonomiesTable = aioseo()->core->db->prefix . 'woocommerce_attribute_taxonomies';
		$termTaxonomyTable          = aioseo()->core->db->prefix . 'term_taxonomy';

		$selectClause = 'COUNT(*) as childProductAttributes';
		if ( ! $count ) {
			$selectClause = aioseo()->pro ? 'tt.term_id, tt.taxonomy, at.frequency, at.priority' : 'tt.term_id, tt.taxonomy';
		}

		$joinClause   = aioseo()->pro ? "LEFT JOIN {$aioseoTermsTable} AS at ON tt.term_id = at.term_id" : '';
		$whereClause  = aioseo()->pro ? 'AND (at.robots_noindex IS NULL OR at.robots_noindex = 0)' : '';
		$limitClause  = $count ? '' : 'LIMIT 50000';

		$result = aioseo()->core->db->execute(
			"SELECT {$selectClause}
			FROM {$termTaxonomyTable} AS tt
			JOIN {$wcAttributeTaxonomiesTable} AS wat ON tt.taxonomy = CONCAT('pa_', wat.attribute_name)
			{$joinClause}
			WHERE wat.attribute_public = 1
				{$whereClause}
				AND tt.count > 0
			{$limitClause};",
			true
		)->result();

		if ( $count ) {
			return ! empty( $result[0]->childProductAttributes ) ? (int) $result[0]->childProductAttributes : 0;
		}

		if ( empty( $result ) ) {
			return [];
		}

		$entries = [];
		foreach ( $result as $term ) {
			$term   = (object) $term;
			$termId = (int) $term->term_id;

			$entry = [
				'loc'        => get_term_link( $termId ),
				'lastmod'    => $this->getTermLastModified( $termId ),
				'changefreq' => aioseo()->sitemap->priority->frequency( 'taxonomies', $term, 'product_attributes' ),
				'priority'   => aioseo()->sitemap->priority->priority( 'taxonomies', $term, 'product_attributes' ),
				'images'     => aioseo()->sitemap->image->term( $term )
			];

			$entries[] = apply_filters( 'aioseo_sitemap_product_attributes', $entry, $termId, $term->taxonomy, 'term' );
		}

		return $entries;
	}
}Common/Sitemap/File.php000066600000020235151135505570011003 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

/**
 * Handles the static sitemap.
 *
 * @since 4.0.0
 */
class File {
	/**
	 * Whether the static files have already been updated during the current request.
	 *
	 * We keep track of this so that setting changes to do not trigger the regeneration multiple times.
	 *
	 * @since 4.0.0
	 *
	 * @var boolean
	 */
	private static $isUpdated = false;

	/**
	 * Generates the static sitemap files.
	 *
	 * @since 4.0.0
	 *
	 * @param  boolean $force Whether or not to force it through.
	 * @return void
	 */
	public function generate( $force = false ) {
		aioseo()->addons->doAddonFunction( 'file', 'generate', [ $force ] );

		// Exit if static sitemap generation isn't enabled.
		if (
			! $force &&
			(
				self::$isUpdated ||
				! aioseo()->options->sitemap->general->enable ||
				! aioseo()->options->sitemap->general->advancedSettings->enable ||
				! in_array( 'staticSitemap', aioseo()->internalOptions->internal->deprecatedOptions, true ) ||
				aioseo()->options->deprecated->sitemap->general->advancedSettings->dynamic
			)
		) {
			return;
		}

		$files           = [];
		self::$isUpdated = true;
		// We need to set these values here as setContext() doesn't run.
		// Subsequently, we need to manually reset the index name below for each query we run.
		// Also, since we need to chunck the entries manually, we cannot limit any queries and need to reset the amount of allowed URLs per index.
		aioseo()->sitemap->offset        = 0;
		aioseo()->sitemap->type          = 'general';
		$sitemapName                     = aioseo()->sitemap->helpers->filename();
		aioseo()->sitemap->indexes       = aioseo()->options->sitemap->general->indexes;
		aioseo()->sitemap->linksPerIndex = PHP_INT_MAX;
		aioseo()->sitemap->isStatic      = true;

		$additionalPages = [];
		if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
			foreach ( aioseo()->options->sitemap->general->additionalPages->pages as $additionalPage ) {
				$additionalPage = json_decode( $additionalPage );
				if ( empty( $additionalPage->url ) ) {
					continue;
				}

				// Decode Additional Page Url to properly show Unicode Characters.
				$additionalPages[] = $additionalPage;
			}
		}

		$postTypes       = aioseo()->sitemap->helpers->includedPostTypes();
		$additionalPages = apply_filters( 'aioseo_sitemap_additional_pages', $additionalPages );

		if (
			'posts' === get_option( 'show_on_front' ) ||
			count( $additionalPages ) ||
			! in_array( 'page', $postTypes, true )
		) {
			$entries            = aioseo()->sitemap->content->addl( false );
			$filename           = "addl-$sitemapName.xml";
			$files[ $filename ] = [
				'total'   => count( $entries ),
				'entries' => $entries
			];
		}

		if (
			aioseo()->sitemap->helpers->lastModifiedPost() &&
			aioseo()->options->sitemap->general->author
		) {
			$entries            = aioseo()->sitemap->content->author();
			$filename           = "author-$sitemapName.xml";
			$files[ $filename ] = [
				'total'   => count( $entries ),
				'entries' => $entries
			];
		}

		if (
			aioseo()->sitemap->helpers->lastModifiedPost() &&
			aioseo()->options->sitemap->general->date
		) {
			$entries            = aioseo()->sitemap->content->date();
			$filename           = "date-$sitemapName.xml";
			$files[ $filename ] = [
				'total'   => count( $entries ),
				'entries' => $entries
			];
		}

		$postTypes = aioseo()->sitemap->helpers->includedPostTypes();
		if ( $postTypes ) {
			foreach ( $postTypes as $postType ) {
				aioseo()->sitemap->indexName = $postType;

				$posts = aioseo()->sitemap->content->posts( $postType );
				if ( ! $posts ) {
					continue;
				}

				$total = aioseo()->sitemap->query->posts( $postType, [ 'count' => true ] );

				// We need to temporarily reset the linksPerIndex count here so that we can properly chunk.
				aioseo()->sitemap->linksPerIndex = aioseo()->options->sitemap->general->linksPerIndex;
				$chunks = aioseo()->sitemap->helpers->chunkEntries( $posts );
				aioseo()->sitemap->linksPerIndex = PHP_INT_MAX;

				if ( 1 === count( $chunks ) ) {
					$filename           = "$postType-$sitemapName.xml";
					$files[ $filename ] = [
						'total'   => $total,
						'entries' => $chunks[0]
					];
				} else {
					for ( $i = 1; $i <= count( $chunks ); $i++ ) {
						$filename           = "$postType-$sitemapName$i.xml";
						$files[ $filename ] = [
							'total'   => $total,
							'entries' => $chunks[ $i - 1 ]
						];
					}
				}
			}
		}

		$taxonomies = aioseo()->sitemap->helpers->includedTaxonomies();
		if ( $taxonomies ) {
			foreach ( $taxonomies as $taxonomy ) {
				aioseo()->sitemap->indexName = $taxonomy;

				$terms = aioseo()->sitemap->content->terms( $taxonomy );
				if ( ! $terms ) {
					continue;
				}

				$total = aioseo()->sitemap->query->terms( $taxonomy, [ 'count' => true ] );

				// We need to temporarily reset the linksPerIndex count here so that we can properly chunk.
				aioseo()->sitemap->linksPerIndex = aioseo()->options->sitemap->general->linksPerIndex;
				$chunks = aioseo()->sitemap->helpers->chunkEntries( $terms );
				aioseo()->sitemap->linksPerIndex = PHP_INT_MAX;

				if ( 1 === count( $chunks ) ) {
					$filename           = "$taxonomy-$sitemapName.xml";
					$files[ $filename ] = [
						'total'   => $total,
						'entries' => $chunks[0]
					];
				} else {
					for ( $i = 1; $i <= count( $chunks ); $i++ ) {
						$filename           = "$taxonomy-$sitemapName$i.xml";
						$files[ $filename ] = [
							'total'   => $total,
							'entries' => $chunks[ $i - 1 ]
						];
					}
				}
			}
		}
		$this->writeSitemaps( $files );
	}

	/**
	 * Writes all sitemap files.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $files The sitemap files.
	 * @return void
	 */
	public function writeSitemaps( $files ) {
		$sitemapName = aioseo()->sitemap->helpers->filename();
		if ( aioseo()->sitemap->indexes ) {
			$indexes = [];
			foreach ( $files as $filename => $data ) {
				if ( empty( $data['entries'] ) ) {
					continue;
				}
				$indexes[] = [
					'loc'     => trailingslashit( home_url() ) . $filename,
					'lastmod' => array_values( $data['entries'] )[0]['lastmod'],
					'count'   => count( $data['entries'] )
				];
			}
			$files[ "$sitemapName.xml" ] = [
				'total'   => 0,
				'entries' => $indexes,
			];
			foreach ( $files as $filename => $data ) {
				$this->writeSitemap( $filename, $data['entries'], $data['total'] );
			}

			return;
		}

		$content = [];
		foreach ( $files as $filename => $data ) {
			foreach ( $data['entries'] as $entry ) {
				$content[] = $entry;
			}
		}
		$this->writeSitemap( "$sitemapName.xml", $content, count( $content ) );
	}

	/**
	 * Writes a given sitemap file to the root dir.
	 *
	 * Helper function for writeSitemaps().
	 *
	 * @since 4.0.0
	 *
	 * @param  string $filename The name of the file.
	 * @param  array  $entries  The sitemap entries for the file.
	 * @return void
	 */
	protected function writeSitemap( $filename, $entries, $total = 0 ) {
		$sitemapName                 = aioseo()->sitemap->helpers->filename();
		aioseo()->sitemap->indexName = $filename;
		if ( "$sitemapName.xml" === $filename && aioseo()->sitemap->indexes ) {
			// Set index name to root so that we use the right output template.
			aioseo()->sitemap->indexName = 'root';
		}

		aioseo()->sitemap->xsl->saveXslData( $filename, $entries, $total );

		ob_start();
		aioseo()->sitemap->output->output( $entries );
		aioseo()->addons->doAddonFunction( 'output', 'output', [ $entries, $total ] );
		$content = ob_get_clean();

		$fs         = aioseo()->core->fs;
		$file       = ABSPATH . sanitize_file_name( $filename );
		$fileExists = $fs->exists( $file );
		if ( ! $fileExists || $fs->isWritable( $file ) ) {
			$fs->putContents( $file, $content );
		}
	}

	/**
	 * Return an array of sitemap files.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of files.
	 */
	public function files() {
		require_once ABSPATH . 'wp-admin/includes/file.php';
		$files = list_files( get_home_path(), 1 );
		if ( ! count( $files ) ) {
			return [];
		}

		$sitemapFiles = [];
		foreach ( $files as $filename ) {
			if ( preg_match( '#.*sitemap.*#', (string) $filename ) ) {
				$sitemapFiles[] = $filename;
			}
		}

		return $sitemapFiles;
	}
}Common/Sitemap/Helpers.php000066600000043214151135505570011530 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

/**
 * Contains general helper methods specific to the sitemap.
 *
 * @since 4.0.0
 */
class Helpers {
	/**
	 * Used to track the performance of the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 *            $memory The peak memory that is required to generate the sitemap.
	 *            $time   The time that is required to generate the sitemap.
	 */
	private $performance;

	/**
	 * Returns the sitemap filename.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $type The sitemap type. We pass it in when we need to get the filename for a specific sitemap outside of the context of the sitemap.
	 * @return string        The sitemap filename.
	 */
	public function filename( $type = '' ) {
		if ( ! $type ) {
			$type = isset( aioseo()->sitemap->type ) ? aioseo()->sitemap->type : 'general';
		}

		return apply_filters( 'aioseo_sitemap_filename', aioseo()->options->sitemap->$type->filename );
	}

	/**
	 * Returns the last modified post.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $additionalArgs Any additional arguments for the post query.
	 * @return mixed                 WP_Post object or false.
	 */
	public function lastModifiedPost( $additionalArgs = [] ) {
		$args = [
			'post_status'    => 'publish',
			'posts_per_page' => 1,
			'orderby '       => 'modified',
			'order'          => 'ASC'
		];

		if ( $additionalArgs ) {
			foreach ( $additionalArgs as $k => $v ) {
				$args[ $k ] = $v;
			}
		}

		$query = ( new \WP_Query( $args ) );
		if ( ! $query->post_count ) {
			return false;
		}

		return $query->posts[0];
	}

	/**
	 * Returns the timestamp of the last modified post.
	 *
	 * @since 4.0.0
	 *
	 * @param  array  $postTypes      The relevant post types.
	 * @param  array  $additionalArgs Any additional arguments for the post query.
	 * @return string                 Formatted date string (ISO 8601).
	 */
	public function lastModifiedPostTime( $postTypes = [ 'post', 'page' ], $additionalArgs = [] ) {
		if ( is_array( $postTypes ) ) {
			$postTypes = implode( "', '", $postTypes );
		}

		$query = aioseo()->core->db
			->start( aioseo()->core->db->db->posts . ' as p', true )
			->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
			->where( 'p.post_status', 'publish' )
			->whereRaw( "( `p`.`post_type` IN ( '$postTypes' ) )" );

		if ( isset( $additionalArgs['author'] ) ) {
			$query->where( 'p.post_author', $additionalArgs['author'] );
		}

		$lastModified = $query->run()
			->result();

		return ! empty( $lastModified[0]->last_modified )
			? aioseo()->helpers->dateTimeToIso8601( $lastModified[0]->last_modified )
			: '';
	}

	/**
	 * Returns the timestamp of the last modified additional page.
	 *
	 * @since 4.0.0
	 *
	 * @return string Formatted date string (ISO 8601).
	 */
	public function lastModifiedAdditionalPagesTime() {
		$pages = [];
		if ( 'posts' === get_option( 'show_on_front' ) || ! in_array( 'page', $this->includedPostTypes(), true ) ) {
			$frontPageId = (int) get_option( 'page_on_front' );
			$post        = aioseo()->helpers->getPost( $frontPageId );
			$pages[]     = $post ? strtotime( $post->post_modified_gmt ) : strtotime( aioseo()->sitemap->helpers->lastModifiedPostTime() );
		}

		foreach ( aioseo()->options->sitemap->general->additionalPages->pages as $page ) {
			$additionalPage = json_decode( $page );
			if ( empty( $additionalPage->url ) ) {
				continue;
			}

			$pages[] = strtotime( $additionalPage->lastModified );
		}

		if ( empty( $pages ) ) {
			$additionalPages = apply_filters( 'aioseo_sitemap_additional_pages', [] );
			if ( empty( $additionalPages ) ) {
				return false;
			}

			$lastModified = 0;
			$timestamp    = time();
			foreach ( $additionalPages as $page ) {
				if ( empty( $page['lastmod'] ) ) {
					continue;
				}
				$timestamp = strtotime( $page['lastmod'] );
				if ( ! $timestamp ) {
					continue;
				}
				if ( $lastModified < $timestamp ) {
					$lastModified = $timestamp;
				}
			}

			return 0 !== $lastModified ? aioseo()->helpers->dateTimeToIso8601( gmdate( 'Y-m-d H:i:s', $timestamp ) ) : false;
		}

		return aioseo()->helpers->dateTimeToIso8601( gmdate( 'Y-m-d H:i:s', max( $pages ) ) );
	}

	/**
	 * Formats a given image URL for usage in the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $url The URL.
	 * @return string      The formatted URL.
	 */
	public function formatUrl( $url ) {
		// Remove URL parameters.
		$url = strtok( $url, '?' );
		$url = htmlspecialchars( $url, ENT_COMPAT, 'UTF-8', false );

		return aioseo()->helpers->makeUrlAbsolute( $url );
	}

	/**
	 * Logs the performance of the sitemap for debugging purposes.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function logPerformance() {
		// Start logging the performance.
		if ( ! $this->performance ) {
			$this->performance['time']   = microtime( true );
			$this->performance['memory'] = ( memory_get_peak_usage( true ) / 1024 ) / 1024;

			return;
		}

		// Stop logging the performance.
		$time      = microtime( true ) - $this->performance['time'];
		$memory    = $this->performance['memory'];
		$type      = aioseo()->sitemap->type;
		$indexName = aioseo()->sitemap->indexName;

		// phpcs:disable WordPress.PHP.DevelopmentFunctions
		error_log( wp_json_encode( "$indexName index of $type sitemap generated in $time seconds using a maximum of $memory mb of memory." ) );
		// phpcs:enable WordPress.PHP.DevelopmentFunctions
	}

	/**
	 * Returns the post types that should be included in the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @param  boolean $hasArchivesOnly Whether or not to only include post types which have archives.
	 * @return array   $postTypes       The included post types.
	 */
	public function includedPostTypes( $hasArchivesOnly = false ) {
		if ( aioseo()->options->sitemap->{aioseo()->sitemap->type}->postTypes->all ) {
			$postTypes = aioseo()->helpers->getPublicPostTypes( true, $hasArchivesOnly );
		} else {
			$postTypes = aioseo()->options->sitemap->{aioseo()->sitemap->type}->postTypes->included;
		}

		if ( ! $postTypes ) {
			return $postTypes;
		}

		$options         = aioseo()->options->noConflict();
		$dynamicOptions  = aioseo()->dynamicOptions->noConflict();
		$publicPostTypes = aioseo()->helpers->getPublicPostTypes( true, $hasArchivesOnly );
		foreach ( $postTypes as $postType ) {
			// Check if post type is no longer registered.
			if ( ! in_array( $postType, $publicPostTypes, true ) || ! $dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
				$postTypes = aioseo()->helpers->unsetValue( $postTypes, $postType );
				continue;
			}

			// Check if post type isn't noindexed.
			if ( aioseo()->helpers->isPostTypeNoindexed( $postType ) ) {
				if ( ! $this->checkForIndexedPost( $postType ) ) {
					$postTypes = aioseo()->helpers->unsetValue( $postTypes, $postType );
					continue;
				}
			}

			if (
				$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default &&
				! $options->searchAppearance->advanced->globalRobotsMeta->default &&
				$options->searchAppearance->advanced->globalRobotsMeta->noindex
			) {
				if ( ! $this->checkForIndexedPost( $postType ) ) {
					$postTypes = aioseo()->helpers->unsetValue( $postTypes, $postType );
				}
			}
		}

		return $postTypes;
	}

	/**
	 * Checks if any post is explicitly indexed when the post type is noindexed.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $postType The post type to check for.
	 * @return bool             Whether or not there is an indexed post.
	 */
	private function checkForIndexedPost( $postType ) {
		$db    = aioseo()->core->db->noConflict();
		$posts = $db->start( aioseo()->core->db->db->posts . ' as p', true )
			->select( 'p.ID' )
			->join( 'aioseo_posts as ap', '`ap`.`post_id` = `p`.`ID`' )
			->where( 'p.post_status', 'attachment' === $postType ? 'inherit' : 'publish' )
			->where( 'p.post_type', $postType )
			->whereRaw( '( `ap`.`robots_default` = 0 AND `ap`.`robots_noindex` = 0 )' )
			->limit( 1 )
			->run()
			->result();

		if ( $posts && count( $posts ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Returns the taxonomies that should be included in the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @return array The included taxonomies.
	 */
	public function includedTaxonomies() {
		$taxonomies = [];
		if ( aioseo()->options->sitemap->{aioseo()->sitemap->type}->taxonomies->all ) {
			$taxonomies = get_taxonomies();
		} else {
			$taxonomies = aioseo()->options->sitemap->{aioseo()->sitemap->type}->taxonomies->included;
		}

		if ( ! $taxonomies ) {
			return [];
		}

		$options          = aioseo()->options->noConflict();
		$dynamicOptions   = aioseo()->dynamicOptions->noConflict();
		$publicTaxonomies = aioseo()->helpers->getPublicTaxonomies( true );
		foreach ( $taxonomies as $taxonomy ) {
			if (
				aioseo()->helpers->isWooCommerceActive() &&
				aioseo()->helpers->isWooCommerceProductAttribute( $taxonomy )
			) {
				$taxonomies = aioseo()->helpers->unsetValue( $taxonomies, $taxonomy );
				if ( ! in_array( 'product_attributes', $taxonomies, true ) ) {
					$taxonomies[] = 'product_attributes';
				}
				continue;
			}

			// Check if taxonomy is no longer registered.
			if ( ! in_array( $taxonomy, $publicTaxonomies, true ) || ! $dynamicOptions->searchAppearance->taxonomies->has( $taxonomy ) ) {
				$taxonomies = aioseo()->helpers->unsetValue( $taxonomies, $taxonomy );
				continue;
			}

			// Check if taxonomy isn't noindexed.
			if ( aioseo()->helpers->isTaxonomyNoindexed( $taxonomy ) ) {
				$taxonomies = aioseo()->helpers->unsetValue( $taxonomies, $taxonomy );
				continue;
			}

			if (
				$dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->default &&
				! $options->searchAppearance->advanced->globalRobotsMeta->default &&
				$options->searchAppearance->advanced->globalRobotsMeta->noindex
			) {
				$taxonomies = aioseo()->helpers->unsetValue( $taxonomies, $taxonomy );
				continue;
			}
		}

		return $taxonomies;
	}

	/**
	 * Splits sitemap entries into chuncks based on the max. amount of URLs per index.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $entries The sitemap entries.
	 * @return array          The chunked sitemap entries.
	 */
	public function chunkEntries( $entries ) {
		return array_chunk( $entries, aioseo()->sitemap->linksPerIndex, true );
	}

	/**
	 * Formats the last Modified date of a user-submitted additional page as an ISO 8601 date.
	 *
	 * @since 4.0.0
	 *
	 * @param  object $page The additional page object.
	 * @return string       The formatted datetime.
	 */
	public function lastModifiedAdditionalPage( $page ) {
		return aioseo()->helpers->isValidDate( $page->lastModified ) ? gmdate( 'c', strtotime( $page->lastModified ) ) : '';
	}

	/**
	 * Returns a list of excluded post IDs.
	 *
	 * @since 4.0.0
	 *
	 * @return string The excluded IDs.
	 */
	public function excludedPosts() {
		static $excludedPosts = null;
		if ( null === $excludedPosts ) {
			$excludedPosts = $this->excludedObjectIds( 'excludePosts' );
		}

		return $excludedPosts;
	}

	/**
	 * Returns a list of excluded term IDs.
	 *
	 * @since 4.0.0
	 *
	 * @return string The excluded IDs.
	 */
	public function excludedTerms() {
		static $excludedTerms = null;
		if ( null === $excludedTerms ) {
			$excludedTerms = $this->excludedObjectIds( 'excludeTerms' );
		}

		return $excludedTerms;
	}

	/**
	 * Returns a list of excluded IDs for a given option as a comma separated string.
	 *
	 * Helper method for excludedPosts() and excludedTerms().
	 *
	 * @since   4.0.0
	 * @version 4.4.7 Improved method name.
	 *
	 * @param  string $option The option name.
	 * @return string         The excluded IDs.
	 */
	private function excludedObjectIds( $option ) {
		$type = aioseo()->sitemap->type;
		// The RSS Sitemap needs to exclude whatever is excluded in the general sitemap.
		if ( 'rss' === $type ) {
			$type = 'general';
		}

		// Allow WPML to filter out hidden language posts/terms.
		$hiddenObjectIds = [];
		if ( aioseo()->helpers->isWpmlActive() ) {
			$hiddenLanguages = apply_filters( 'wpml_setting', [], 'hidden_languages' );
			foreach ( $hiddenLanguages as $language ) {
				$objectTypes = [];
				if ( 'excludePosts' === $option ) {
					$objectTypes = aioseo()->sitemap->helpers->includedPostTypes();
					$objectTypes = array_map( function( $postType ) {
						return "post_{$postType}";
					}, $objectTypes );
				}

				if ( 'excludeTerms' === $option ) {
					$objectTypes = aioseo()->sitemap->helpers->includedTaxonomies();
					$objectTypes = array_map( function( $taxonomy ) {
						return "tax_{$taxonomy}";
					}, $objectTypes );
				}

				$dbNoConflict = aioseo()->core->db->noConflict();
				$rows         = $dbNoConflict->start( 'icl_translations' )
					->select( 'element_id' )
					->whereIn( 'element_type', $objectTypes )
					->where( 'language_code', $language )
					->run()
					->result();

				$ids = array_map( function( $row ) {
					return (int) $row->element_id;
				}, $rows );

				$hiddenObjectIds = array_merge( $hiddenObjectIds, $ids );
			}
		}

		$hasFilter = has_filter( 'aioseo_sitemap_' . aioseo()->helpers->toSnakeCase( $option ) );
		$advanced  = aioseo()->options->sitemap->$type->advancedSettings->enable;
		$excluded  = array_merge( $hiddenObjectIds, aioseo()->options->sitemap->{$type}->advancedSettings->{$option} );

		if (
			! $advanced &&
			empty( $excluded ) &&
			! $hasFilter
		) {
			return '';
		}

		$ids = [];
		foreach ( $excluded as $object ) {
			if ( is_numeric( $object ) ) {
				$ids[] = (int) $object;
				continue;
			}

			$object = json_decode( $object );
			if ( is_int( $object->value ) ) {
				$ids[] = $object->value;
			}
		}

		if ( 'excludePosts' === $option ) {
			$ids = apply_filters( 'aioseo_sitemap_exclude_posts', $ids, $type );
		}

		if ( 'excludeTerms' === $option ) {
			$ids = apply_filters( 'aioseo_sitemap_exclude_terms', $ids, $type );
		}

		return count( $ids ) ? esc_sql( implode( ', ', $ids ) ) : '';
	}

	/**
	 * Returns the URLs of all active sitemaps.
	 *
	 * @since   4.0.0
	 * @version 4.6.2 Removed the prefix from the list of URLs.
	 *
	 * @return array $urls The sitemap URLs.
	 */
	public function getSitemapUrls() {
		static $urls = [];
		if ( $urls ) {
			return $urls;
		}

		$addonsUrls = array_filter( aioseo()->addons->doAddonFunction( 'helpers', 'getSitemapUrls' ) );

		foreach ( $addonsUrls as $addonUrls ) {
			$urls = array_merge( $urls, $addonUrls );
		}

		if ( aioseo()->options->sitemap->general->enable ) {
			$urls[] = $this->getUrl( 'general' );
		}
		if ( aioseo()->options->sitemap->rss->enable ) {
			$urls[] = $this->getUrl( 'rss' );
		}

		return $urls;
	}

	/**
	 * Returns the URLs of all active sitemaps with the 'Sitemap: ' prefix.
	 *
	 * @since 4.6.2
	 *
	 * @return array $urls The sitemap URLs.
	 */
	public function getSitemapUrlsPrefixed() {
		$urls = $this->getSitemapUrls();

		foreach ( $urls as &$url ) {
			$url = 'Sitemap: ' . $url;
		}

		return $urls;
	}

	/**
	 * Extracts existing sitemap URLs from the robots.txt file.
	 * We need this in case users have existing sitemap directives added to their robots.txt file.
	 *
	 * @since   4.0.10
	 * @version 4.4.9
	 *
	 * @return array The sitemap URLs.
	 */
	public function extractSitemapUrlsFromRobotsTxt() {
		// First, we need to remove our filter, so that it doesn't run unintentionally.
		remove_filter( 'robots_txt', [ aioseo()->robotsTxt, 'buildRules' ], 10000 );
		$robotsTxt = apply_filters( 'robots_txt', '', true );
		add_filter( 'robots_txt', [ aioseo()->robotsTxt, 'buildRules' ], 10000 );

		if ( ! $robotsTxt ) {
			return [];
		}

		$lines = explode( "\n", $robotsTxt );
		if ( ! is_array( $lines ) || ! count( $lines ) ) {
			return [];
		}

		return aioseo()->robotsTxt->extractSitemapUrls( $robotsTxt );
	}

	/**
	 * Returns the URL of the given sitemap type.
	 *
	 * @since 4.1.5
	 *
	 * @param  string $type The sitemap type.
	 * @return string       The sitemap URL.
	 */
	public function getUrl( $type ) {
		$url = home_url( 'sitemap.xml' );

		if ( 'rss' === $type ) {
			$url = home_url( 'sitemap.rss' );
		}

		if ( 'general' === $type ) {
			// Check if user has a custom filename from the V3 migration.
			$filename = $this->filename( 'general' ) ?: 'sitemap';
			$url      = home_url( $filename . '.xml' );
		}

		$addon = aioseo()->addons->getLoadedAddon( $type );
		if ( ! empty( $addon->helpers ) && method_exists( $addon->helpers, 'getUrl' ) ) {
			$url = $addon->helpers->getUrl();
		}

		return $url;
	}

	/**
	 * Returns if images should be excluded from the sitemap.
	 *
	 * @since 4.2.2
	 *
	 * @return bool
	 */
	public function excludeImages() {
		$shouldExclude = aioseo()->options->sitemap->general->advancedSettings->enable && aioseo()->options->sitemap->general->advancedSettings->excludeImages;

		return apply_filters( 'aioseo_sitemap_exclude_images', $shouldExclude );
	}

	/**
	 * Returns the post types to check against for the author sitemap.
	 *
	 * @since 4.4.4
	 *
	 * @return array The post types.
	 */
	public function getAuthorPostTypes() {
		// By default, WP only considers posts for author archives, but users can include additional post types.
		$postTypes = [ 'post' ];

		return apply_filters( 'aioseo_sitemap_author_post_types', $postTypes );
	}

	/**
	 * Decode the Urls from Posts and Terms so they properly show in the Sitemap.
	 *
	 * @since 4.6.9
	 *
	 * @param  mixed $data   The data to decode.
	 * @return array $result The converted data with decoded URLs.
	 */
	public function decodeSitemapEntries( $data ) {
		$result = [];

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

		// Decode Url to properly show Unicode Characters.
		foreach ( $data as $item ) {
			if ( isset( $item['loc'] ) ) {
				$item['loc'] = aioseo()->helpers->decodeUrl( $item['loc'] );
			}
			// This is for the RSS Sitemap.
			if ( isset( $item['guid'] ) ) {
				$item['guid'] = aioseo()->helpers->decodeUrl( $item['guid'] );
			}

			$result[] = $item;
		}

		return $result;
	}
}Common/Sitemap/SitemapAbstract.php000066600000004054151135505570013213 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

/**
 * Abstract class holding the class properties of our main AIOSEO class.
 *
 * @since 4.4.3
 */
abstract class SitemapAbstract {
	/**
	 * Content class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Content
	 */
	public $content = null;

	/**
	 * Root class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Root
	 */
	public $root = null;

	/**
	 * Query class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Query
	 */
	public $query = null;

	/**
	 * File class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var File
	 */
	public $file = null;

	/**
	 * Image class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Image\Image
	 */
	public $image = null;

	/**
	 * Priority class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Priority
	 */
	public $priority = null;

	/**
	 * Output class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Output
	 */
	public $output = null;

	/**
	 * Helpers class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Helpers
	 */
	public $helpers = null;

	/**
	 * RequestParser class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var RequestParser
	 */
	public $requestParser = null;

	/**
	 * Xsl class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Xsl
	 */
	public $xsl = null;

	/**
	 * The sitemap type (e.g. "general", "news", "video", "rss", etc.).
	 *
	 * @since 4.2.7
	 *
	 * @var string
	 */
	public $type = '';

	/**
	 * Index name.
	 *
	 * @since 4.4.3
	 *
	 * @var string
	 */
	public $indexName = '';

	/**
	 * Page number.
	 *
	 * @since 4.4.3
	 *
	 * @var int
	 */
	public $pageNumber = 0;

	/**
	 * Page number.
	 *
	 * @since 4.4.3
	 *
	 * @var int
	 */
	public $offset = 0;

	/**
	 * Indexes active.
	 *
	 * @since 4.4.3
	 *
	 * @var bool
	 */
	public $indexes = false;

	/**
	 * Links per index.
	 *
	 * @since 4.4.3
	 *
	 * @var int
	 */
	public $linksPerIndex = PHP_INT_MAX;

	/**
	 * Is static.
	 *
	 * @since 4.4.3
	 *
	 * @var bool
	 */
	public $isStatic = false;

	/**
	 * Filename.
	 *
	 * @since 4.4.3
	 *
	 * @var string
	 */
	public $filename = '';
}Common/Sitemap/Image/Image.php000066600000022536151135505570012176 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap\Image;

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

/**
 * Determines which images are included in a post/term.
 *
 * @since 4.0.0
 */
class Image {
	/**
	 * The image scan action name.
	 *
	 * @since 4.0.13
	 *
	 * @var string
	 */
	private $imageScanAction = 'aioseo_image_sitemap_scan';

	/**
	 * The supported image extensions.
	 *
	 * @since 4.2.2
	 *
	 * @var array[string]
	 */
	public $supportedExtensions = [
		'gif',
		'heic',
		'jpeg',
		'jpg',
		'png',
		'svg',
		'webp',
		'ico'
	];

	/**
	 * The post object.
	 *
	 * @since 4.2.7
	 *
	 * @var \WP_Post
	 */
	private $post = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.0.5
	 */
	public function __construct() {
		// Column may not have been created yet.
		if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'image_scan_date' ) ) {
			return;
		}

		// NOTE: This needs to go above the is_admin check in order for it to run at all.
		add_action( $this->imageScanAction, [ $this, 'scanPosts' ] );

		// Don't schedule a scan if we are not in the admin.
		if ( ! is_admin() ) {
			return;
		}

		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		// Don't schedule a scan if an importer or the V3 migration is running.
		// We'll do our scans there.
		if (
			aioseo()->importExport->isImportRunning() ||
			aioseo()->migration->isMigrationRunning()
		) {
			return;
		}
		// Action Scheduler hooks.
		add_action( 'init', [ $this, 'scheduleScan' ], 3001 );
	}

	/**
	 * Schedules the image sitemap scan.
	 *
	 * @since 4.0.5
	 *
	 * @return void
	 */
	public function scheduleScan() {
		if (
			! aioseo()->options->sitemap->general->enable ||
			aioseo()->sitemap->helpers->excludeImages()
		) {
			return;
		}

		aioseo()->actionScheduler->scheduleSingle( $this->imageScanAction, 10 );
	}

	/**
	 * Scans posts for images.
	 *
	 * @since 4.0.5
	 *
	 * @return void
	 */
	public function scanPosts() {
		if (
			! aioseo()->options->sitemap->general->enable ||
			aioseo()->sitemap->helpers->excludeImages()
		) {
			return;
		}

		$postsPerScan = apply_filters( 'aioseo_image_sitemap_posts_per_scan', 10 );
		$postTypes    = implode( "', '", aioseo()->helpers->getPublicPostTypes( true ) );

		$posts = aioseo()->core->db
			->start( aioseo()->core->db->db->posts . ' as p', true )
			->select( '`p`.`ID`, `p`.`post_type`, `p`.`post_content`, `p`.`post_excerpt`, `p`.`post_modified_gmt`' )
			->leftJoin( 'aioseo_posts as ap', '`ap`.`post_id` = `p`.`ID`' )
			->whereRaw( '( `ap`.`id` IS NULL OR `p`.`post_modified_gmt` > `ap`.`image_scan_date` OR `ap`.`image_scan_date` IS NULL )' )
			->whereRaw( "`p`.`post_status` IN ( 'publish', 'inherit' )" )
			->whereRaw( "`p`.`post_type` IN ( '$postTypes' )" )
			->limit( $postsPerScan )
			->run()
			->result();

		if ( ! $posts ) {
			aioseo()->actionScheduler->scheduleSingle( $this->imageScanAction, 15 * MINUTE_IN_SECONDS, [], true );

			return;
		}

		foreach ( $posts as $post ) {
			$this->scanPost( $post );
		}

		aioseo()->actionScheduler->scheduleSingle( $this->imageScanAction, 30, [], true );
	}

	/**
	 * Returns the image entries for a given post.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|int $post The post object or ID.
	 * @return void
	 */
	public function scanPost( $post ) {
		if ( is_numeric( $post ) ) {
			$post = get_post( $post );
		}

		$this->post = $post;

		if ( ! empty( $post->post_password ) ) {
			$this->updatePost( $post->ID );

			return;
		}

		if ( 'attachment' === $post->post_type ) {
			if ( ! wp_attachment_is( 'image', $post->ID ) ) {
				$this->updatePost( $post->ID );

				return;
			}

			$image = $this->buildEntries( [ $post->ID ] );
			$this->updatePost( $post->ID, $image );

			return;
		}

		$images = $this->extract();
		$images = $this->removeImageDimensions( $images );

		$images = apply_filters( 'aioseo_sitemap_images', $images, $post );

		// Limit to a 1,000 URLs, in accordance to Google's specifications.
		$images = array_slice( $images, 0, 1000 );
		$this->updatePost( $post->ID, $this->buildEntries( $images ) );
	}

	/**
	 * Returns the image entries for a given term.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Term $term The term object.
	 * @return array          The image entries.
	 */
	public function term( $term ) {
		if ( aioseo()->sitemap->helpers->excludeImages() ) {
			return [];
		}

		$id = get_term_meta( $term->term_id, 'thumbnail_id', true );
		if ( ! $id ) {
			return [];
		}

		return $this->buildEntries( [ $id ] );
	}

	/**
	 * Builds the image entries.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $images The images, consisting of attachment IDs or external URLs.
	 * @return array         The image entries.
	 */
	private function buildEntries( $images ) {
		$entries = [];
		foreach ( $images as $image ) {
			$idOrUrl  = $this->getImageIdOrUrl( $image );
			$imageUrl = is_numeric( $idOrUrl ) ? wp_get_attachment_url( $idOrUrl ) : $idOrUrl;
			$imageUrl = aioseo()->sitemap->helpers->formatUrl( $imageUrl );
			if ( ! $imageUrl || ! preg_match( $this->getImageExtensionRegexPattern(), (string) $imageUrl ) ) {
				continue;
			}

			// If the image URL is not external, make it relative.
			// This is important for users who scan their sites in a local/staging environment and then
			// push the data to production.
			if ( ! aioseo()->helpers->isExternalUrl( $imageUrl ) ) {
				$imageUrl = aioseo()->helpers->makeUrlRelative( $imageUrl );
			}

			$entries[ $idOrUrl ] = [ 'image:loc' => $imageUrl ];
		}

		return array_values( $entries );
	}

	/**
	 * Returns the ID of the image if it's hosted on the site. Otherwise it returns the external URL.
	 *
	 * @since 4.1.3
	 *
	 * @param  int|string $image The attachment ID or URL.
	 * @return int|string        The attachment ID or URL.
	 */
	private function getImageIdOrUrl( $image ) {
		if ( is_numeric( $image ) ) {
			return $image;
		}

		$attachmentId = false;
		if ( aioseo()->helpers->isValidAttachment( $image ) ) {
			$attachmentId = aioseo()->helpers->attachmentUrlToPostId( $image );
		}

		return $attachmentId ? $attachmentId : $image;
	}

	/**
	 * Extracts all image URls and IDs from the post.
	 *
	 * @since 4.0.0
	 *
	 * @return array The image URLs and IDs.
	 */
	private function extract() {
		$images = [];

		if ( has_post_thumbnail( $this->post ) ) {
			$images[] = get_the_post_thumbnail_url( $this->post );
		}

		// Get the galleries here before doShortcodes() runs below to prevent buggy behaviour.
		// WordPress is supposed to only return the attached images but returns a different result if the shortcode has no valid attributes, so we need to grab them manually.
		$images = array_merge( $images, $this->getPostGalleryImages() );

		// Now, get the remaining images from image tags in the post content.
		$parsedPostContent = do_blocks( $this->post->post_content );
		$parsedPostContent = aioseo()->helpers->doShortcodes( $parsedPostContent, true, $this->post->ID );
		$parsedPostContent = preg_replace( '/\s\s+/u', ' ', (string) trim( $parsedPostContent ) ); // Trim both internal and external whitespace.

		// Get the images from any third-party plugins/themes that are active.
		$thirdParty = new ThirdParty( $this->post, $parsedPostContent );
		$images     = array_merge( $images, $thirdParty->extract() );

		preg_match_all( '#<(amp-)?img[^>]+src="([^">]+)"#', (string) $parsedPostContent, $matches );
		foreach ( $matches[2] as $url ) {
			$images[] = aioseo()->helpers->makeUrlAbsolute( $url );
		}

		return array_unique( $images );
	}

	/**
	 * Returns all images from WP Core post galleries.
	 *
	 * @since 4.2.2
	 *
	 * @return array[string] The image URLs.
	 */
	private function getPostGalleryImages() {
		$images    = [];
		$galleries = get_post_galleries( $this->post, false );
		foreach ( $galleries as $gallery ) {
			foreach ( $gallery['src'] as $imageUrl ) {
				$images[] = $imageUrl;
			}
		}

		// Now, get rid of them so that we don't process the shortcodes again.
		$regex                    = get_shortcode_regex( [ 'gallery' ] );
		$this->post->post_content = preg_replace( "/$regex/i", '', (string) $this->post->post_content );

		return $images;
	}

	/**
	 * Removes image dimensions from the slug.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $urls         The image URLs.
	 * @return array $preparedUrls The formatted image URLs.
	 */
	private function removeImageDimensions( $urls ) {
		$preparedUrls = [];
		foreach ( $urls as $url ) {
			$preparedUrls[] = aioseo()->helpers->removeImageDimensions( $url );
		}

		return array_unique( array_filter( $preparedUrls ) );
	}

	/**
	 * Stores the image data for a given post in our DB table.
	 *
	 * @since 4.0.5
	 *
	 * @param  int   $postId The post ID.
	 * @param  array $images The images.
	 * @return void
	 */
	private function updatePost( $postId, $images = [] ) {
		$post                    = \AIOSEO\Plugin\Common\Models\Post::getPost( $postId );
		$meta                    = $post->exists() ? [] : aioseo()->migration->meta->getMigratedPostMeta( $postId );
		$meta['post_id']         = $postId;
		$meta['images']          = ! empty( $images ) ? $images : null;
		$meta['image_scan_date'] = gmdate( 'Y-m-d H:i:s' );

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

	/**
	 * Returns the image extension regex pattern.
	 *
	 * @since 4.2.2
	 *
	 * @return string
	 */
	public function getImageExtensionRegexPattern() {
		static $pattern;
		if ( null !== $pattern ) {
			return $pattern;
		}

		$pattern = '/http.*\.(' . implode( '|', $this->supportedExtensions ) . ')$/i';

		return $pattern;
	}
}Common/Sitemap/Image/ThirdParty.php000066600000017641151135505570013247 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap\Image;

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

/**
 * Holds all code to extract images from third-party content.
 *
 * @since 4.2.2
 */
class ThirdParty {
	/**
	 * The post object.
	 *
	 * @since 4.2.2
	 *
	 * @var \WP_Post
	 */
	private $post;

	/**
	 * The parsed post content.
	 * The post object holds the unparsed content as we need that for Divi.
	 *
	 * @since 4.2.5
	 *
	 * @var string
	 */
	private $parsedPostContent;

	/**
	 * The image URLs and IDs.
	 *
	 * @since 4.2.2
	 *
	 * @var array[mixed]
	 */
	private $images = [];

	/**
	 * Divi shortcodes.
	 *
	 * @since 4.2.3
	 *
	 * @var string[]
	 */
	private $shortcodes = [
		'et_pb_section',
		'et_pb_column',
		'et_pb_row',
		'et_pb_image',
		'et_pb_gallery',
		'et_pb_accordion',
		'et_pb_accordion_item',
		'et_pb_counters',
		'et_pb_blurb',
		'et_pb_cta',
		'et_pb_code',
		'et_pb_contact_form',
		'et_pb_divider',
		'et_pb_filterable_portfolio',
		'et_pb_map',
		'et_pb_number_counter',
		'et_pb_post_slider',
		'et_pb_pricing_tables',
		'et_pb_pricing_table',
		'et_pb_shop',
		'et_pb_slider',
		'et_pb_slide',
		'et_pb_tabs',
		'et_pb_tab',
		'et_pb_text',
		'et_pb_video',
		'et_pb_audio',
		'et_pb_blog',
		'et_pb_circle_counter',
		'et_pb_comments',
		'et_pb_countdown_timer',
		'et_pb_signup',
		'et_pb_login',
		'et_pb_menu',
		'et_pb_team_member',
		'et_pb_post_nav',
		'et_pb_post_title',
		'et_pb_search',
		'et_pb_sidebar',
		'et_pb_social_media_follow',
		'et_pb_social_media_follow_network',
		'et_pb_testimonial',
		'et_pb_toggle',
		'et_pb_video_slider',
		'et_pb_video_slider_item',
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.2.2
	 *
	 * @param \WP_Post $post              The post object.
	 * @param string   $parsedPostContent The parsed post content.
	 */
	public function __construct( $post, $parsedPostContent ) {
		$this->post              = $post;
		$this->parsedPostContent = $parsedPostContent;
	}

	/**
	 * Extracts the images from third-party content.
	 *
	 * @since 4.2.2
	 *
	 * @return array[mixed] The image URLs and IDs.
	 */
	public function extract() {
		$integrations = [
			'acf',
			'divi',
			'nextGen',
			'wooCommerce',
			'kadenceBlocks'
		];

		foreach ( $integrations as $integration ) {
			$this->{$integration}();
		}

		return $this->images;
	}

	/**
	 * Extracts image URLs from ACF fields.
	 *
	 * @since 4.2.2
	 *
	 * @return void
	 */
	private function acf() {
		if ( ! class_exists( 'ACF' ) || ! function_exists( 'get_fields' ) ) {
			return;
		}

		$fields = get_fields( $this->post->ID );
		if ( ! $fields ) {
			return;
		}

		$images       = $this->acfHelper( $fields );
		$this->images = array_merge( $this->images, $images );
	}

	/**
	 * Helper function for acf().
	 *
	 * @since 4.2.2
	 *
	 * @param  array         $fields The ACF fields.
	 * @return array[string]         The image URLs or IDs.
	 */
	private function acfHelper( $fields ) {
		$images = [];
		foreach ( $fields as $value ) {
			if ( is_array( $value ) ) {
				// Recursively loop over grouped fields.
				// We continue on since arrays aren't necessarily groups and might also simply aLready contain the value we're looking for.
				$images = array_merge( $images, $this->acfHelper( $value ) );

				if ( isset( $value['type'] ) && 'image' !== strtolower( $value['type'] ) ) {
					$images[] = $value['url'];
				}

				continue;
			}

			// Capture the value if it's an image URL, but not the default thumbnail from ACF.
			if ( is_string( $value ) && preg_match( aioseo()->sitemap->image->getImageExtensionRegexPattern(), (string) $value ) && ! preg_match( '/media\/default\.png$/i', (string) $value ) ) {
				$images[] = $value;
				continue;
			}

			// Capture the value if it's a numeric image ID, but make sure it's not an array of random field object properties.
			if (
				is_numeric( $value ) &&
				! isset( $fields['ID'] ) &&
				! isset( $fields['thumbnail'] )
			) {
				$images[] = $value;
			}
		}

		return $images;
	}

	/**
	 * Extracts images from Divi shortcodes.
	 *
	 * @since 4.1.8
	 *
	 * @return void
	 */
	private function divi() {
		if ( ! defined( 'ET_BUILDER_VERSION' ) ) {
			return;
		}

		$urls  = [];
		$regex = implode( '|', array_map( 'preg_quote', $this->shortcodes ) );

		preg_match_all(
			"/\[($regex)(?![\w-])([^\]\/]*(?:\/(?!\])[^\]\/]*)*?)(?:(\/)\]|\](?:([^\[]*+(?:\[(?!\/\2\])[^\[]*+)*+)\[\/\2\])?)(\]?)/i",
			(string) $this->post->post_content,
			$matches,
			PREG_SET_ORDER
		);

		foreach ( $matches as $shortcode ) {
			$attributes = shortcode_parse_atts( $shortcode[0] );
			if ( ! empty( $attributes['src'] ) ) {
				$urls[] = $attributes['src'];
			}

			if ( ! empty( $attributes['image_src'] ) ) {
				$urls[] = $attributes['image_src'];
			}

			if ( ! empty( $attributes['image_url'] ) ) {
				$urls[] = $attributes['image_url'];
			}

			if ( ! empty( $attributes['portrait_url'] ) ) {
				$urls[] = $attributes['portrait_url'];
			}

			if ( ! empty( $attributes['image'] ) ) {
				$urls[] = $attributes['image'];
			}

			if ( ! empty( $attributes['background_image'] ) ) {
				$urls[] = $attributes['background_image'];
			}

			if ( ! empty( $attributes['logo'] ) ) {
				$urls[] = $attributes['logo'];
			}

			if ( ! empty( $attributes['gallery_ids'] ) ) {
				$attachmentIds = explode( ',', $attributes['gallery_ids'] );
				foreach ( $attachmentIds as $attachmentId ) {
					$urls[] = wp_get_attachment_url( $attachmentId );
				}
			}
		}

		$this->images = array_merge( $this->images, $urls );
	}

	/**
	 * Extracts the image IDs of more advanced NextGen Pro gallerlies like the Mosaic and Thumbnail Grid.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	private function nextGen() {
		if ( ! defined( 'NGG_PLUGIN_BASENAME' ) && ! defined( 'NGG_PRO_PLUGIN_BASENAME' ) ) {
			return;
		}

		preg_match_all( '/data-image-id=\"([0-9]*)\"/i', (string) $this->parsedPostContent, $imageIds );
		if ( ! empty( $imageIds[1] ) ) {
			$this->images = array_merge( $this->images, $imageIds[1] );
		}

		// For this specific check, we only want to parse blocks and do not want to run shortcodes because some NextGen blocks (e.g. Mosaic) are parsed into shortcodes.
		// And after parsing the shortcodes, the attributes we're looking for are gone.
		$contentWithBlocksParsed = do_blocks( $this->post->post_content );

		$imageIds = [];
		preg_match_all( '/\[ngg.*src="galleries" ids="(.*?)".*\]/i', (string) $contentWithBlocksParsed, $shortcodes );
		if ( empty( $shortcodes[1] ) ) {
			return;
		}

		foreach ( $shortcodes[1] as $shortcode ) {
			$galleryIds = explode( ',', $shortcode[0] );
			foreach ( $galleryIds as $galleryId ) {
				global $nggdb;
				$galleryImageIds = $nggdb->get_ids_from_gallery( $galleryId );
				if ( empty( $galleryImageIds ) ) {
					continue;
				}

				foreach ( $galleryImageIds as $galleryImageId ) {
					$image = $nggdb->find_image( $galleryImageId );
					if ( ! empty( $image ) ) {
						$imageIds[] = $image->get_permalink();
					}
				}
			}
		}

		$this->images = array_merge( $this->images, $imageIds );
	}

	/**
	 * Extracts the image IDs of WooCommerce product galleries.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	private function wooCommerce() {
		if ( ! aioseo()->helpers->isWooCommerceActive() || 'product' !== $this->post->post_type ) {
			return;
		}

		$productImageIds = get_post_meta( $this->post->ID, '_product_image_gallery', true );
		if ( ! $productImageIds ) {
			return;
		}

		$productImageIds = explode( ',', $productImageIds );
		$this->images    = array_merge( $this->images, $productImageIds );
	}

	/**
	 * Extracts the image IDs of Kadence Block galleries.
	 *
	 * @since 4.4.5
	 *
	 * @return void
	 */
	private function kadenceBlocks() {
		if ( ! defined( 'KADENCE_BLOCKS_VERSION' ) ) {
			return [];
		}

		$blocks = aioseo()->helpers->parseBlocks( $this->post );

		foreach ( $blocks as $block ) {
			if ( 'kadence/advancedgallery' === $block['blockName'] && ! empty( $block['attrs']['ids'] ) ) {
				$this->images = array_merge( $this->images, $block['attrs']['ids'] );
			}
		}
	}
}Common/Sitemap/Query.php000066600000027332151135505570011236 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

use AIOSEO\Plugin\Common\Utils as CommonUtils;

/**
 * Handles all complex queries for the sitemap.
 *
 * @since 4.0.0
 */
class Query {
	/**
	 * Returns all eligble sitemap entries for a given post type.
	 *
	 * @since 4.0.0
	 *
	 * @param  mixed $postTypes      The post type(s). Either a singular string or an array of strings.
	 * @param  array $additionalArgs Any additional arguments for the post query.
	 * @return array|int             The post objects or the post count.
	 */
	public function posts( $postTypes, $additionalArgs = [] ) {
		$includedPostTypes = $postTypes;
		$postTypesArray    = ! is_array( $postTypes ) ? [ $postTypes ] : $postTypes;
		if ( is_array( $postTypes ) ) {
			$includedPostTypes = implode( "', '", $postTypes );
		}

		if (
			empty( $includedPostTypes ) ||
			( 'attachment' === $includedPostTypes && 'disabled' !== aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls )
		) {
			return [];
		}

		// Set defaults.
		$maxAge = '';
		$fields = implode( ', ', [
			'p.ID',
			'p.post_excerpt',
			'p.post_type',
			'p.post_password',
			'p.post_parent',
			'p.post_date_gmt',
			'p.post_modified_gmt',
			'ap.priority',
			'ap.frequency'
		] );

		if ( in_array( aioseo()->sitemap->type, [ 'html', 'rss' ], true ) ) {
			$fields .= ', p.post_title';
		}

		if ( 'general' !== aioseo()->sitemap->type || ! aioseo()->sitemap->helpers->excludeImages() ) {
			$fields .= ', ap.images';
		}

		// Order by highest priority first (highest priority at the top),
		// then by post modified date (most recently updated at the top).
		$orderBy = 'ap.priority DESC, p.post_modified_gmt DESC';

		// Override defaults if passed as additional arg.
		foreach ( $additionalArgs as $name => $value ) {
			// Attachments need to be fetched with all their fields because we need to get their post parent further down the line.
			$$name = esc_sql( $value );
			if ( 'root' === $name && $value && 'attachment' !== $includedPostTypes ) {
				$fields = 'p.ID, p.post_type';
			}
			if ( 'count' === $name && $value ) {
				$fields = 'count(p.ID) as total';
			}
		}

		$query = aioseo()->core->db
			->start( aioseo()->core->db->db->posts . ' as p', true )
			->select( $fields )
			->leftJoin( 'aioseo_posts as ap', 'ap.post_id = p.ID' )
			->where( 'p.post_status', 'attachment' === $includedPostTypes ? 'inherit' : 'publish' )
			->whereRaw( "p.post_type IN ( '$includedPostTypes' )" );

		$homePageId = (int) get_option( 'page_on_front' );

		if ( ! is_array( $postTypes ) ) {
			if ( ! aioseo()->helpers->isPostTypeNoindexed( $includedPostTypes ) ) {
				$query->whereRaw( "( `ap`.`robots_noindex` IS NULL OR `ap`.`robots_default` = 1 OR `ap`.`robots_noindex` = 0 OR post_id = $homePageId )" );
			} else {
				$query->whereRaw( "( `ap`.`robots_default` = 0 AND `ap`.`robots_noindex` = 0 OR post_id = $homePageId )" );
			}
		} else {
			$robotsMetaSql = [];
			foreach ( $postTypes as $postType ) {
				if ( ! aioseo()->helpers->isPostTypeNoindexed( $postType ) ) {
					$robotsMetaSql[] = "( `p`.`post_type` = '$postType' AND ( `ap`.`robots_noindex` IS NULL OR `ap`.`robots_default` = 1 OR `ap`.`robots_noindex` = 0 OR post_id = $homePageId ) )";
				} else {
					$robotsMetaSql[] = "( `p`.`post_type` = '$postType' AND ( `ap`.`robots_default` = 0 AND `ap`.`robots_noindex` = 0 OR post_id = $homePageId ) )";
				}
			}
			$query->whereRaw( '( ' . implode( ' OR ', $robotsMetaSql ) . ' )' );
		}

		$excludedPosts = aioseo()->sitemap->helpers->excludedPosts();

		if ( $excludedPosts ) {
			$query->whereRaw( "( `p`.`ID` NOT IN ( $excludedPosts ) OR post_id = $homePageId )" );
		}

		// Exclude posts assigned to excluded terms.
		$excludedTerms = aioseo()->sitemap->helpers->excludedTerms();
		if ( $excludedTerms ) {
			$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
			$query->whereRaw("
				( `p`.`ID` NOT IN
					(
						SELECT `tr`.`object_id`
						FROM `$termRelationshipsTable` as tr
						WHERE `tr`.`term_taxonomy_id` IN ( $excludedTerms )
					)
				)" );
		}

		if ( $maxAge ) {
			$query->whereRaw( "( `p`.`post_date_gmt` >= '$maxAge' )" );
		}

		if (
			'rss' === aioseo()->sitemap->type ||
			(
				aioseo()->sitemap->indexes &&
				empty( $additionalArgs['root'] ) &&
				empty( $additionalArgs['count'] )
			)
		) {
			$query->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->offset );
		}

		$isStaticHomepage = 'page' === get_option( 'show_on_front' );
		if ( $isStaticHomepage ) {
			$excludedPostIds = array_map( 'intval', explode( ',', $excludedPosts ) );
			$blogPageId      = (int) get_option( 'page_for_posts' );

			if ( in_array( 'page', $postTypesArray, true ) ) {
				// Exclude the blog page from the pages post type.
				if ( $blogPageId ) {
					$query->whereRaw( "`p`.`ID` != $blogPageId" );
				}

				// Custom order by statement to always move the home page to the top.
				if ( $homePageId ) {
					$orderBy = "case when `p`.`ID` = $homePageId then 0 else 1 end, $orderBy";
				}
			}

			// Include the blog page in the posts post type unless manually excluded.
			if (
				$blogPageId &&
				! in_array( $blogPageId, $excludedPostIds, true ) &&
				in_array( 'post', $postTypesArray, true )
			) {
				// We are using a database class hack to get in an OR clause to
				// bypass all the other WHERE statements and just include the
				// blog page ID manually.
				$query->whereRaw( "1=1 OR `p`.`ID` = $blogPageId" );

				// Custom order by statement to always move the blog posts page to the top.
				$orderBy = "case when `p`.`ID` = $blogPageId then 0 else 1 end, $orderBy";
			}
		}

		$query->orderByRaw( $orderBy );
		$query = $this->filterPostQuery( $query, $postTypes );

		// Return the total if we are just counting the posts.
		if ( ! empty( $additionalArgs['count'] ) ) {
			return (int) $query->run( true, 'var' )
				->result();
		}

		$posts = $query->run()
			->result();

		// Convert ID from string to int.
		foreach ( $posts as $post ) {
			$post->ID = (int) $post->ID;
		}

		return $this->filterPosts( $posts );
	}

	/**
	 * Filters the post query.
	 *
	 * @since 4.1.4
	 *
	 * @param  \AIOSEO\Plugin\Common\Utils\Database $query    The query.
	 * @param  string                               $postType The post type.
	 * @return \AIOSEO\Plugin\Common\Utils\Database           The filtered query.
	 */
	private function filterPostQuery( $query, $postType ) {
		switch ( $postType ) {
			case 'product':
				return $this->excludeHiddenProducts( $query );
			default:
				break;
		}

		return $query;
	}

	/**
	 * Adds a condition to the query to exclude hidden WooCommerce products.
	 *
	 * @since 4.1.4
	 *
	 * @param  \AIOSEO\Plugin\Common\Utils\Database $query The query.
	 * @return \AIOSEO\Plugin\Common\Utils\Database        The filtered query.
	 */
	private function excludeHiddenProducts( $query ) {
		if (
			! aioseo()->helpers->isWooCommerceActive() ||
			! apply_filters( 'aioseo_sitemap_woocommerce_exclude_hidden_products', true )
		) {
			return $query;
		}

		static $hiddenProductIds = null;
		if ( null === $hiddenProductIds ) {
			$tempDb         = new CommonUtils\Database();
			$hiddenProducts = $tempDb->start( 'term_relationships as tr' )
				->select( 'tr.object_id' )
				->join( 'term_taxonomy as tt', 'tr.term_taxonomy_id = tt.term_taxonomy_id' )
				->join( 'terms as t', 'tt.term_id = t.term_id' )
				->where( 't.name', 'exclude-from-catalog' )
				->run()
				->result();

			if ( empty( $hiddenProducts ) ) {
				return $query;
			}

			$hiddenProductIds = [];
			foreach ( $hiddenProducts as $hiddenProduct ) {
				$hiddenProductIds[] = (int) $hiddenProduct->object_id;
			}
			$hiddenProductIds = esc_sql( implode( ', ', $hiddenProductIds ) );
		}

		$query->whereRaw( "p.ID NOT IN ( $hiddenProductIds )" );

		return $query;
	}

	/**
	 * Filters the queried posts.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $posts          The posts.
	 * @return array $remainingPosts The remaining posts.
	 */
	public function filterPosts( $posts ) {
		$remainingPosts = [];
		foreach ( $posts as $post ) {
			switch ( $post->post_type ) {
				case 'attachment':
					if ( ! $this->isInvalidAttachment( $post ) ) {
						$remainingPosts[] = $post;
					}
					break;
				default:
					$remainingPosts[] = $post;
					break;
			}
		}

		return $remainingPosts;
	}

	/**
	 * Excludes attachments if their post parent isn't published or parent post type isn't registered anymore.
	 *
	 * @since 4.0.0
	 *
	 * @param  Object  $post The post.
	 * @return boolean       Whether the attachment is invalid.
	 */
	private function isInvalidAttachment( $post ) {
		if ( empty( $post->post_parent ) ) {
			return false;
		}

		$parent = get_post( $post->post_parent );
		if ( ! is_object( $parent ) ) {
			return false;
		}

		if (
			'publish' !== $parent->post_status ||
			! in_array( $parent->post_type, get_post_types(), true ) ||
			$parent->post_password
		) {
			return true;
		}

		return false;
	}

	/**
	 * Returns all eligible sitemap entries for a given taxonomy.
	 *
	 * @since 4.0.0
	 *
	 * @param  string    $taxonomy       The taxonomy.
	 * @param  array     $additionalArgs Any additional arguments for the term query.
	 * @return array|int                 The term objects or the term count.
	 */
	public function terms( $taxonomy, $additionalArgs = [] ) {
		// Set defaults.
		$fields  = 't.term_id';
		$offset  = aioseo()->sitemap->offset;

		// Override defaults if passed as additional arg.
		foreach ( $additionalArgs as $name => $value ) {
			$$name = esc_sql( $value );
			if ( 'root' === $name && $value ) {
				$fields = 't.term_id, tt.count';
			}
			if ( 'count' === $name && $value ) {
				$fields = 'count(t.term_id) as total';
			}
		}

		$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
		$termTaxonomyTable      = aioseo()->core->db->db->prefix . 'term_taxonomy';

		// Include all terms that have assigned posts or whose children have assigned posts.
		$query = aioseo()->core->db
			->start( aioseo()->core->db->db->terms . ' as t', true )
			->select( $fields )
			->leftJoin( 'term_taxonomy as tt', '`tt`.`term_id` = `t`.`term_id`' )
			->whereRaw( "
			( `t`.`term_id` IN
				(
					SELECT `tt`.`term_id`
					FROM `$termTaxonomyTable` as tt
					WHERE `tt`.`taxonomy` = '$taxonomy'
					AND 
						(
							`tt`.`count` > 0 OR
							EXISTS (
								SELECT 1
								FROM `$termTaxonomyTable` as tt2
								WHERE `tt2`.`parent` = `tt`.`term_id` 
								AND `tt2`.`count` > 0
							)
						)
				)
			)" );

		$excludedTerms = aioseo()->sitemap->helpers->excludedTerms();
		if ( $excludedTerms ) {
			$query->whereRaw("
				( `t`.`term_id` NOT IN
					(
						SELECT `tr`.`term_taxonomy_id`
						FROM `$termRelationshipsTable` as tr
						WHERE `tr`.`term_taxonomy_id` IN ( $excludedTerms )
					)
				)" );
		}

		if (
			aioseo()->sitemap->indexes &&
			empty( $additionalArgs['root'] ) &&
			empty( $additionalArgs['count'] )
		) {
			$query->limit( aioseo()->sitemap->linksPerIndex, $offset );
		}

		// Return the total if we are just counting the terms.
		if ( ! empty( $additionalArgs['count'] ) ) {
			return (int) $query->run( true, 'var' )
				->result();
		}

		$terms = $query->orderBy( 't.term_id ASC' )
			->run()
			->result();

		foreach ( $terms as $term ) {
			// Convert ID from string to int.
			$term->term_id = (int) $term->term_id;
			// Add taxonomy name to object manually instead of querying it to prevent redundant join.
			$term->taxonomy = $taxonomy;
		}

		return $terms;
	}

	/**
	 * Wipes all data and forces the plugin to rescan the site for images.
	 *
	 * @since 4.0.13
	 *
	 * @return void
	 */
	public function resetImages() {
		aioseo()->core->db
			->update( 'aioseo_posts' )
			->set(
				[
					'images'          => null,
					'image_scan_date' => null
				]
			)
			->run();
	}
}Common/Sitemap/Html/Query.php000066600000014575151135505570012147 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap\Html;

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

/**
 * Handles all queries for the HTML sitemap.
 *
 * @since 4.1.3
 */
class Query {
	/**
	 * Returns all eligible sitemap entries for a given post type.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $postType   The post type.
	 * @param  array  $attributes The attributes.
	 * @return array              The post objects.
	 */
	public function posts( $postType, $attributes ) {
		$fields  = '`ID`, `post_title`,';
		$fields .= '`post_parent`, `post_date_gmt`, `post_modified_gmt`';

		$orderBy = '';
		switch ( $attributes['order_by'] ) {
			case 'last_updated':
				$orderBy = 'post_modified_gmt';
				break;
			case 'alphabetical':
				$orderBy = 'post_title';
				break;
			case 'id':
				$orderBy = 'ID';
				break;
			case 'publish_date':
			default:
				$orderBy = 'post_date_gmt';
				break;
		}

		switch ( strtolower( $attributes['order'] ) ) {
			case 'desc':
				$orderBy .= ' DESC';
				break;
			default:
				$orderBy .= ' ASC';
		}

		$query = aioseo()->core->db
			->start( 'posts' )
			->select( $fields )
			->where( 'post_status', 'publish' )
			->where( 'post_type', $postType );

		$excludedPosts = $this->getExcludedObjects( $attributes );
		if ( $excludedPosts ) {
			$query->whereRaw( "( `ID` NOT IN ( $excludedPosts ) )" );
		}

		$posts = $query->orderBy( $orderBy )
			->run()
			->result();

		foreach ( $posts as $post ) {
			$post->ID = (int) $post->ID;
		}

		return $posts;
	}

	/**
	 * Returns all eligble sitemap entries for a given taxonomy.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $taxonomy   The taxonomy name.
	 * @param  array  $attributes The attributes.
	 * @return array              The term objects.
	 */
	public function terms( $taxonomy, $attributes = [] ) {
		$fields                 = 't.term_id, t.name, tt.parent';
		$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
		$termTaxonomyTable      = aioseo()->core->db->db->prefix . 'term_taxonomy';

		$orderBy = '';
		switch ( $attributes['order_by'] ) {
			case 'alphabetical':
				$orderBy = 't.name';
				break;
			// We can only sort by date after getting the terms.
			case 'id':
			case 'publish_date':
			case 'last_updated':
			default:
				$orderBy = 't.term_id';
				break;
		}

		switch ( strtolower( $attributes['order'] ) ) {
			case 'desc':
				$orderBy .= ' DESC';
				break;
			default:
				$orderBy .= ' ASC';
		}

		$query = aioseo()->core->db
			->start( 'terms as t' )
			->select( $fields )
			->join( 'term_taxonomy as tt', 't.term_id = tt.term_id' )
			->whereRaw( "
			( `t`.`term_id` IN
				(
					SELECT `tt`.`term_id`
					FROM `$termTaxonomyTable` as tt
					WHERE `tt`.`taxonomy` = '$taxonomy'
					AND `tt`.`count` > 0
				)
			)" );

		$excludedTerms = $this->getExcludedObjects( $attributes, false );
		if ( $excludedTerms ) {
			$query->whereRaw("
				( `t`.`term_id` NOT IN
					(
						SELECT `tr`.`term_taxonomy_id`
						FROM `$termRelationshipsTable` as tr
						WHERE `tr`.`term_taxonomy_id` IN ( $excludedTerms )
					)
				)" );
		}

		$terms = $query->orderBy( $orderBy )
			->run()
			->result();

		foreach ( $terms as $term ) {
			$term->term_id  = (int) $term->term_id;
			$term->taxonomy = $taxonomy;
		}

		$shouldSort = false;
		if ( 'last_updated' === $attributes['order_by'] ) {
			$shouldSort = true;
			foreach ( $terms as $term ) {
				$term->timestamp = strtotime( aioseo()->sitemap->content->getTermLastModified( $term->term_id ) );
			}
		}

		if ( 'publish_date' === $attributes['order_by'] ) {
			$shouldSort = true;
			foreach ( $terms as $term ) {
				$term->timestamp = strtotime( $this->getTermPublishDate( $term->term_id ) );
			}
		}

		if ( $shouldSort ) {
			if ( 'asc' === strtolower( $attributes['order'] ) ) {
				usort( $terms, function( $term1, $term2 ) {
					return $term1->timestamp > $term2->timestamp ? 1 : 0;
				} );
			} else {
				usort( $terms, function( $term1, $term2 ) {
					return $term1->timestamp < $term2->timestamp ? 1 : 0;
				} );
			}
		}

		return $terms;
	}

	/**
	 * Returns a list of date archives that can be included.
	 *
	 * @since 4.1.3
	 *
	 * @return array The date archives.
	 */
	public function archives() {
		$result = aioseo()->core->db
			->start( 'posts', false, 'SELECT DISTINCT' )
			->select( 'YEAR(post_date) AS year, MONTH(post_date) AS month' )
			->where( 'post_type', 'post' )
			->where( 'post_status', 'publish' )
			->whereRaw( "post_password=''" )
			->orderBy( 'year DESC' )
			->orderBy( 'month DESC' )
			->run()
			->result();

		$dates = [];
		foreach ( $result as $date ) {
			$dates[ $date->year ][ $date->month ] = 1;
		}

		return $dates;
	}

	/**
	 * Returns the publish date for a given term.
	 * This is the publish date of the oldest post that is assigned to the term.
	 *
	 * @since 4.1.3
	 *
	 * @param  int $termId The term ID.
	 * @return int         The publish date timestamp.
	 */
	public function getTermPublishDate( $termId ) {
		$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';

		$post = aioseo()->core->db
			->start( 'posts as p' )
			->select( 'MIN(`p`.`post_date_gmt`) as publish_date' )
			->whereRaw( "
			( `p`.`ID` IN
				(
					SELECT `tr`.`object_id`
					FROM `$termRelationshipsTable` as tr
					WHERE `tr`.`term_taxonomy_id` = '$termId'
				)
			)" )
			->run()
			->result();

		return ! empty( $post[0]->publish_date ) ? strtotime( $post[0]->publish_date ) : 0;
	}

	/**
	 * Returns a comma-separated string of excluded object IDs.
	 *
	 * @since 4.1.3
	 *
	 * @param  array   $attributes The attributes.
	 * @param  boolean $posts      Whether the objects are posts.
	 * @return string              The excluded object IDs.
	 */
	private function getExcludedObjects( $attributes, $posts = true ) {
		$excludedObjects = $posts
			? aioseo()->sitemap->helpers->excludedPosts()
			: aioseo()->sitemap->helpers->excludedTerms();
		$key             = $posts ? 'excluded_posts' : 'excluded_terms';

		if ( ! empty( $attributes[ $key ] ) ) {
			$ids = explode( ',', $excludedObjects );

			$extraIds = [];
			if ( is_array( $attributes[ $key ] ) ) {
				$extraIds = $attributes[ $key ];
			}
			if ( is_string( $attributes[ $key ] ) ) {
				$extraIds = array_map( 'trim', explode( ',', $attributes[ $key ] ) );
			}

			$ids = array_filter( array_merge( $ids, $extraIds ), 'is_numeric' );

			$excludedObjects = esc_sql( implode( ', ', $ids ) );
		}

		return $excludedObjects;
	}
}Common/Sitemap/Html/Shortcode.php000066600000001336151135505570012763 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap\Html;

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

/**
 * Handles the HTML sitemap shortcode.
 *
 * @since 4.1.3
 */
class Shortcode {
	/**
	 * Class constructor.
	 *
	 * @since 4.1.3
	 */
	public function __construct() {
		add_shortcode( 'aioseo_html_sitemap', [ $this, 'render' ] );
	}

	/**
	 * Shortcode callback.
	 *
	 * @since 4.1.3
	 *
	 * @param  array       $attributes The shortcode attributes.
	 * @return string|void             The HTML sitemap.
	 */
	public function render( $attributes ) {
		$attributes = aioseo()->htmlSitemap->frontend->getAttributes( $attributes );

		return aioseo()->htmlSitemap->frontend->output( false, $attributes );
	}
}Common/Sitemap/Html/Frontend.php000066600000031777151135505570012624 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap\Html;

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

/**
 * Handles the output of the HTML sitemap.
 *
 * @since 4.1.3
 */
class Frontend {
	/**
	 * Instance of Query class.
	 *
	 * @since 4.1.3
	 *
	 * @var Query
	 */
	public $query;

	/**
	 * The attributes for the block/widget/shortcode.
	 *
	 * @since 4.1.3
	 *
	 * @var array
	 */
	private $attributes = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.3
	 */
	public function __construct() {
		$this->query = new Query();
	}

	/**
	 * Returns the attributes.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $attributes The user-defined attributes
	 * @return array             The defaults with user-defined attributes merged.
	 */
	public function getAttributes( $attributes = [] ) {
		aioseo()->sitemap->type = 'html';

		$defaults = [
			'label_tag'        => 'h4',
			'show_label'       => true,
			'order'            => aioseo()->options->sitemap->html->sortDirection,
			'order_by'         => aioseo()->options->sitemap->html->sortOrder,
			'nofollow_links'   => false,
			'publication_date' => aioseo()->options->sitemap->html->publicationDate,
			'archives'         => aioseo()->options->sitemap->html->compactArchives,
			'post_types'       => aioseo()->sitemap->helpers->includedPostTypes(),
			'taxonomies'       => aioseo()->sitemap->helpers->includedTaxonomies(),
			'excluded_posts'   => [],
			'excluded_terms'   => [],
			'is_admin'         => false
		];

		$attributes                   = shortcode_atts( $defaults, $attributes );
		$attributes['show_label']     = filter_var( $attributes['show_label'], FILTER_VALIDATE_BOOLEAN );
		$attributes['nofollow_links'] = filter_var( $attributes['nofollow_links'], FILTER_VALIDATE_BOOLEAN );
		$attributes['is_admin']       = filter_var( $attributes['is_admin'], FILTER_VALIDATE_BOOLEAN );

		return $attributes;
	}

	/**
	 * Formats the publish date according to what's set under Settings > General.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $date The date that should be formatted.
	 * @return string       The formatted date.
	 */
	private function formatDate( $date ) {
		$dateFormat = apply_filters( 'aioseo_html_sitemap_date_format', get_option( 'date_format' ) );

		return date_i18n( $dateFormat, strtotime( $date ) );
	}

	/**
	 * Returns the posts of a given post type that should be included.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $postType       The post type.
	 * @param  array  $additionalArgs Additional arguments for the post query (optional).
	 * @return array                  The post entries.
	 */
	private function posts( $postType, $additionalArgs = [] ) {
		$posts = $this->query->posts( $postType, $additionalArgs );
		if ( ! $posts ) {
			return [];
		}

		$entries = [];
		foreach ( $posts as $post ) {
			$entry = [
				'id'     => $post->ID,
				'title'  => get_the_title( $post ),
				'loc'    => get_permalink( $post->ID ),
				'date'   => $this->formatDate( $post->post_date_gmt ),
				'parent' => ! empty( $post->post_parent ) ? $post->post_parent : null
			];

			$entries[] = $entry;
		}

		return apply_filters( 'aioseo_html_sitemap_posts', $entries, $postType );
	}

	/**
	 * Returns the terms of a given taxonomy that should be included.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $taxonomy        The taxonomy name.
	 * @param  array  $additionalArgs  Additional arguments for the query (optional).
	 * @return array                   The term entries.
	 */
	private function terms( $taxonomy, $additionalArgs = [] ) {
		$terms = $this->query->terms( $taxonomy, $additionalArgs );
		if ( ! $terms ) {
			return [];
		}

		$entries = [];
		foreach ( $terms as $term ) {
			$entries[] = [
				'id'     => $term->term_id,
				'title'  => $term->name,
				'loc'    => get_term_link( $term->term_id ),
				'parent' => ! empty( $term->parent ) ? $term->parent : null
			];
		}

		return apply_filters( 'aioseo_html_sitemap_terms', $entries, $taxonomy );
	}

	/**
	 * Outputs the sitemap to the frontend.
	 *
	 * @since 4.1.3
	 *
	 * @param  bool  $echo       Whether the sitemap should be printed to the screen.
	 * @param  array $attributes The shortcode attributes.
	 * @return string|void       The HTML sitemap.
	 */
	public function output( $echo = true, $attributes = [] ) {
		$this->attributes = $attributes;

		if ( ! aioseo()->options->sitemap->html->enable ) {
			return;
		}

		aioseo()->sitemap->type = 'html';
		if ( filter_var( $attributes['archives'], FILTER_VALIDATE_BOOLEAN ) ) {
			return ( new CompactArchive() )->output( $attributes, $echo );
		}

		if ( ! empty( $attributes['default'] ) ) {
			$attributes = $this->getAttributes();
		}

		$noResultsMessage = esc_html__( 'No posts/terms could be found.', 'all-in-one-seo-pack' );
		if ( empty( $this->attributes['post_types'] ) && empty( $this->attributes['taxonomies'] ) ) {
			if ( $echo ) {
				echo $noResultsMessage; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			}

			return $noResultsMessage;
		}

		// TODO: Consider moving all remaining HTML code below to a dedicated view instead of printing it in PHP.
		$sitemap = sprintf(
			'<div class="aioseo-html-sitemap%s">',
			! $this->attributes['show_label'] ? ' labels-hidden' : ''
		);

		$sitemap .= '<style>.aioseo-html-sitemap.labels-hidden ul { margin: 0; }</style>';

		$hasPosts  = false;
		$postTypes = $this->getIncludedObjects( $this->attributes['post_types'] );
		foreach ( $postTypes as $postType ) {
			if ( 'attachment' === $postType ) {
				continue;
			}

			// Check if post type is still registered.
			if ( ! in_array( $postType, aioseo()->helpers->getPublicPostTypes( true ), true ) ) {
				continue;
			}

			$posts = $this->posts( $postType, $attributes );
			if ( empty( $posts ) ) {
				continue;
			}

			$hasPosts = true;

			$postTypeObject = get_post_type_object( $postType );
			$label          = ! empty( $postTypeObject->label ) ? $postTypeObject->label : ucfirst( $postType );

			$sitemap .= '<div class="aioseo-html-' . esc_attr( $postType ) . '-sitemap">';
			$sitemap .= $this->generateLabel( $label );

			if ( is_post_type_hierarchical( $postType ) ) {
				$sitemap .= $this->generateHierarchicalList( $posts ) . '</div>';
				if ( $this->attributes['show_label'] ) {
					$sitemap .= '<br />';
				}
				continue;
			}

			$sitemap .= $this->generateList( $posts );
			if ( $this->attributes['show_label'] ) {
				$sitemap .= '<br />';
			}
		}

		$hasTerms   = false;
		$taxonomies = $this->getIncludedObjects( $this->attributes['taxonomies'], false );
		foreach ( $taxonomies as $taxonomy ) {
			// Check if post type is still registered.
			if ( ! in_array( $taxonomy, aioseo()->helpers->getPublicTaxonomies( true ), true ) ) {
				continue;
			}

			$terms = $this->terms( $taxonomy, $attributes );
			if ( empty( $terms ) ) {
				continue;
			}

			$hasTerms = true;

			$taxonomyObject = get_taxonomy( $taxonomy );
			$label          = ! empty( $taxonomyObject->label ) ? $taxonomyObject->label : ucfirst( $taxonomy );

			$sitemap .= '<div class="aioseo-html-' . esc_attr( $taxonomy ) . '-sitemap">';
			$sitemap .= $this->generateLabel( $label );

			if ( is_taxonomy_hierarchical( $taxonomy ) ) {
				$sitemap .= $this->generateHierarchicalList( $terms ) . '</div>';
				if ( $this->attributes['show_label'] ) {
					$sitemap .= '<br />';
				}
				continue;
			}

			$sitemap .= $this->generateList( $terms );
			if ( $this->attributes['show_label'] ) {
				$sitemap .= '<br />';
			}
		}

		$sitemap .= '</div>';

		// Check if we actually were able to fetch any results.
		if ( ! $hasPosts && ! $hasTerms ) {
			$sitemap = $noResultsMessage;
		}

		if ( $echo ) {
			echo $sitemap; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}

		return $sitemap;
	}

	/**
	 * Generates the label for a section of the sitemap.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $label The label.
	 * @return string        The HTML code for the label.
	 */
	private function generateLabel( $label ) {
		$labelTag = ! empty( $this->attributes['label_tag'] ) ? $this->attributes['label_tag'] : 'h4';

		return $this->attributes['show_label']
			? wp_kses_post( sprintf( '<%2$s>%1$s</%2$s>', $label, $labelTag ) )
			: '';
	}

	/**
	 * Generates the HTML for a non-hierarchical list of objects.
	 *
	 * @since 4.1.3
	 *
	 * @param  array  $objects The object.
	 * @return string          The HTML code.
	 */
	private function generateList( $objects ) {
		$list = '<ul>';
		foreach ( $objects as $object ) {
			$list .= $this->generateListItem( $object ) . '</li>';
		}

		return $list . '</ul></div>';
	}

	/**
	 * Generates a list item for an object (without the closing tag).
	 * We cannot close it as the caller might need to generate a hierarchical structure inside the list item.
	 *
	 * @since 4.1.3
	 *
	 * @param  array  $object The object.
	 * @return string         The HTML code.
	 */
	private function generateListItem( $object ) {
		$li = '';
		if ( ! empty( $object['title'] ) ) {
			$li .= '<li>';

			// add nofollow to the link.
			if ( filter_var( $this->attributes['nofollow_links'], FILTER_VALIDATE_BOOLEAN ) ) {
				$li .= sprintf(
					'<a href="%1$s" %2$s %3$s>',
					esc_url( $object['loc'] ),
					'rel="nofollow"',
					$this->attributes['is_admin'] ? 'target="_blank"' : ''
				);
			} else {
				$li .= sprintf(
					'<a href="%1$s" %2$s>',
					esc_url( $object['loc'] ),
					$this->attributes['is_admin'] ? 'target="_blank"' : ''
				);
			}

			$li .= sprintf( '%s', esc_attr( $object['title'] ) );

			// add publication date on the list item.
			if ( ! empty( $object['date'] ) && filter_var( $this->attributes['publication_date'], FILTER_VALIDATE_BOOLEAN ) ) {
				$li .= sprintf( ' (%s)', esc_attr( $object['date'] ) );
			}

			$li .= '</a>';
		}

		return $li;
	}

	/**
	 * Generates the HTML for a hierarchical list of objects.
	 *
	 * @since 4.1.3
	 *
	 * @param  array  $objects The objects.
	 * @return string          The HTML of the hierarchical objects section.
	 */
	private function generateHierarchicalList( $objects ) {
		if ( empty( $objects ) ) {
			return '';
		}

		$objects = $this->buildHierarchicalTree( $objects );

		$list = '<ul>';
		foreach ( $objects as $object ) {
			$list .= $this->generateListItem( $object );

			if ( ! empty( $object['children'] ) ) {
				$list .= $this->generateHierarchicalTree( $object );
			}
			$list .= '</li>';
		}
		$list .= '</ul>';

		return $list;
	}

	/**
	 * Recursive helper function for generateHierarchicalList().
	 * Generates hierarchical structure for objects with child objects.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $object The object.
	 * @return string        The HTML code of the hierarchical tree.
	 */
	private function generateHierarchicalTree( $object ) {
		static $nestedLevel = 0;

		$tree = '<ul>';
		foreach ( $object['children'] as $child ) {
			$nestedLevel++;
			$tree .= $this->generateListItem( $child );
			if ( ! empty( $child['children'] ) ) {
				$tree .= $this->generateHierarchicalTree( $child );
			}
			$tree .= '</li>';
		}
		$tree .= '</ul>';

		return $tree;
	}

	/**
	 * Builds the structure for hierarchical objects that have a parent.
	 *
	 * @since 4.1.3
	 * @version 4.2.8
	 *
	 * @param  array $objects The list of hierarchical objects.
	 * @return array          Multidimensional array with the hierarchical structure.
	 */
	private function buildHierarchicalTree( $objects ) {
		$topLevelIds = [];
		$objects     = json_decode( wp_json_encode( $objects ) );

		foreach ( $objects as $listItem ) {

			// Create an array of top level IDs for later reference.
			if ( empty( $listItem->parent ) ) {
				array_push( $topLevelIds, $listItem->id );
			}

			// Create an array of children that belong to the current item.
			$children = array_filter( $objects, function( $child ) use ( $listItem ) {
				if ( ! empty( $child->parent ) ) {
					return absint( $child->parent ) === absint( $listItem->id );
				}
			} );

			if ( ! empty( $children ) ) {
				$listItem->children = $children;
			}
		}

		// Remove child objects from the root level since they've all been nested.
		$objects = array_filter( $objects, function ( $item ) use ( $topLevelIds ) {
			return in_array( $item->id, $topLevelIds, true );
		} );

		return array_values( json_decode( wp_json_encode( $objects ), true ) );
	}

	/**
	 * Returns the names of the included post types or taxonomies.
	 *
	 * @since 4.1.3
	 *
	 * @param  array|string $objects      The included post types/taxonomies.
	 * @param  boolean      $arePostTypes Whether the objects are post types.
	 * @return array                      The names of the included post types/taxonomies.
	 */
	private function getIncludedObjects( $objects, $arePostTypes = true ) {
		if ( is_array( $objects ) ) {
			return $objects;
		}

		if ( empty( $objects ) ) {
			return [];
		}

		$exploded = explode( ',', $objects );
		$objects  = array_map( function( $object ) {
			return trim( $object );
		}, $exploded );

		$publicObjects = $arePostTypes
			? aioseo()->helpers->getPublicPostTypes( true )
			: aioseo()->helpers->getPublicTaxonomies( true );

		$objects = array_filter( $objects, function( $object ) use ( $publicObjects ) {
			return in_array( $object, $publicObjects, true );
		});

		return $objects;
	}
}Common/Sitemap/Html/Block.php000066600000006636151135505570012073 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap\Html;

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

/**
 * Handles the HTML sitemap block.
 *
 * @since 4.1.3
 */
class Block {
	/**
	 * Class constructor.
	 *
	 * @since 4.1.1
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'register' ] );
	}

	/**
	 * Registers the block.
	 *
	 * @since  4.1.3
	 *
	 * @return void
	 */
	public function register() {
		aioseo()->blocks->registerBlock(
			'aioseo/html-sitemap', [
				'attributes'      => [
					'default'          => [
						'type'    => 'boolean',
						'default' => true
					],
					'post_types'       => [
						'type'    => 'string',
						'default' => wp_json_encode( [ 'post', 'page' ] )
					],
					'post_types_all'   => [
						'type'    => 'boolean',
						'default' => true
					],
					'taxonomies'       => [
						'type'    => 'string',
						'default' => wp_json_encode( [ 'category', 'post_tag' ] )
					],
					'taxonomies_all'   => [
						'type'    => 'boolean',
						'default' => true
					],
					'show_label'       => [
						'type'    => 'boolean',
						'default' => true
					],
					'archives'         => [
						'type'    => 'boolean',
						'default' => false
					],
					'publication_date' => [
						'type'    => 'boolean',
						'default' => true
					],
					'nofollow_links'   => [
						'type'    => 'boolean',
						'default' => false
					],
					'order_by'         => [
						'type'    => 'string',
						'default' => 'publish_date'
					],
					'order'            => [
						'type'    => 'string',
						'default' => 'asc'
					],
					'excluded_posts'   => [
						'type'    => 'string',
						'default' => wp_json_encode( [] )
					],
					'excluded_terms'   => [
						'type'    => 'string',
						'default' => wp_json_encode( [] )
					],
					'is_admin'         => [
						'type'    => 'boolean',
						'default' => false
					]
				],
				'render_callback' => [ $this, 'render' ],
				'editor_style'    => 'aioseo-html-sitemap'
			]
		);
	}

	/**
	 * Renders the block.
	 *
	 * @since 4.1.3
	 *
	 * @param  array  $attributes The attributes.
	 * @return string             The HTML sitemap code.
	 */
	public function render( $attributes ) {
		if ( ! $attributes['default'] ) {
			$jsonFields = [ 'post_types', 'taxonomies', 'excluded_posts', 'excluded_terms' ];
			foreach ( $attributes as $k => $v ) {
				if ( in_array( $k, $jsonFields, true ) ) {
					$attributes[ $k ] = json_decode( $v );
				}
			}

			$attributes['excluded_posts'] = $this->extractIds( $attributes['excluded_posts'] );
			$attributes['excluded_terms'] = $this->extractIds( $attributes['excluded_terms'] );

			if ( ! empty( $attributes['post_types_all'] ) ) {
				$attributes['post_types'] = aioseo()->helpers->getPublicPostTypes( true );
			}
			if ( ! empty( $attributes['taxonomies_all'] ) ) {
				$attributes['taxonomies'] = aioseo()->helpers->getPublicTaxonomies( true );
			}
		} else {
			$attributes = [];
		}

		$attributes = aioseo()->htmlSitemap->frontend->getAttributes( $attributes );

		return aioseo()->htmlSitemap->frontend->output( false, $attributes );
	}

	/**
	 * Extracts the IDs from the excluded objects.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $objects The objects.
	 * @return array          The object IDs.
	 */
	private function extractIds( $objects ) {
		return array_map( function ( $object ) {
			$object = json_decode( $object );

			return (int) $object->value;
		}, $objects );
	}
}Common/Sitemap/Html/Sitemap.php000066600000014025151135505570012432 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap\Html {
	// Exit if accessed directly.
	if ( ! defined( 'ABSPATH' ) ) {
		exit;
	}

	/**
	* Main class for the HTML sitemap.
	*
	* @since 4.1.3
	*/
	class Sitemap {
		/** Instance of the frontend class.
		 *
		 * @since 4.1.3
		 *
		 * @var Frontend
		 */
		public $frontend;

		/**
		 * Instance of the shortcode class.
		 *
		 * @since 4.1.3
		 *
		 * @var Shortcode
		 */
		public $shortcode;

		/**
		 * Instance of the block class.
		 *
		 * @since 4.1.3
		 *
		 * @var Block
		 */
		public $block;

		/**
		 * Whether the current queried page is the dedicated sitemap page.
		 *
		 * @since 4.1.3
		 *
		 * @var bool
		 */
		public $isDedicatedPage = false;

		/**
		 * Class constructor.
		 *
		 * @since 4.1.3
		 */
		public function __construct() {
			$this->frontend  = new Frontend();
			$this->shortcode = new Shortcode();
			$this->block     = new Block();

			add_action( 'widgets_init', [ $this, 'registerWidget' ] );
			add_filter( 'aioseo_canonical_url', [ $this, 'getCanonicalUrl' ] );

			if ( ! is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
				add_action( 'template_redirect', [ $this, 'checkForDedicatedPage' ] );
			}
		}

		/**
		 * Register our HTML sitemap widget.
		 *
		 * @since 4.1.3
		 *
		 * @return void
		 */
		public function registerWidget() {
			if ( aioseo()->helpers->canRegisterLegacyWidget( 'aioseo-html-sitemap-widget' ) ) {
				register_widget( 'AIOSEO\Plugin\Common\Sitemap\Html\Widget' );
			}
		}

		/**
		 * Checks whether the current request is for our dedicated HTML sitemap page.
		 *
		 * @since 4.1.3
		 *
		 * @return void
		 */
		public function checkForDedicatedPage() {
			if ( ! aioseo()->options->sitemap->html->enable ) {
				return;
			}

			global $wp;
			$sitemapUrl = aioseo()->options->sitemap->html->pageUrl;
			if ( ! $sitemapUrl || empty( $wp->request ) ) {
				return;
			}

			$sitemapUrl = wp_parse_url( $sitemapUrl );
			if ( empty( $sitemapUrl['path'] ) ) {
				return;
			}

			$sitemapUrl = trim( $sitemapUrl['path'], '/' );
			if ( trim( $wp->request, '/' ) === $sitemapUrl ) {
				$this->isDedicatedPage = true;
				$this->generatePage();
			}
		}

		/**
		 * Checks whether the current request is for our dedicated HTML sitemap page.
		 *
		 * @since 4.1.3
		 *
		 * @return void
		 */
		private function generatePage() {
			global $wp_query, $wp, $post; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

			$postId     = -1337; // Set a negative ID to prevent conflicts with existing posts.
			$sitemapUrl = aioseo()->options->sitemap->html->pageUrl;
			$path       = trim( wp_parse_url( $sitemapUrl )['path'], '/' );

			$fakePost                 = new \stdClass();
			$fakePost->ID             = $postId;
			$fakePost->post_author    = 1;
			$fakePost->post_date      = current_time( 'mysql' );
			$fakePost->post_date_gmt  = current_time( 'mysql', 1 );
			$fakePost->post_title     = apply_filters( 'aioseo_html_sitemap_page_title', __( 'Sitemap', 'all-in-one-seo-pack' ) );
			$fakePost->post_content   = '[aioseo_html_sitemap archives=false]';
			// We're using post instead of page to prevent calls to get_ancestors(), which will trigger errors.
			// To loead the page template, we set is_page to true on the WP_Query object.
			$fakePost->post_type      = 'post';
			$fakePost->post_status    = 'publish';
			$fakePost->comment_status = 'closed';
			$fakePost->ping_status    = 'closed';
			$fakePost->post_name      = $path;
			$fakePost->filter         = 'raw'; // Needed to prevent calls to the database when creating the WP_Post object.
			$postObject               = new \WP_Post( $fakePost );

			$post = $postObject;

			// We'll set as much properties on the WP_Query object as we can to prevent conflicts with other plugins/themes.
			// phpcs:disable Squiz.NamingConventions.ValidVariableName
			$wp_query->is_404            = false;
			$wp_query->is_page           = true;
			$wp_query->is_singular       = true;
			$wp_query->post              = $postObject;
			$wp_query->posts             = [ $postObject ];
			$wp_query->queried_object    = $postObject;
			$wp_query->queried_object_id = $postId;
			$wp_query->found_posts       = 1;
			$wp_query->post_count        = 1;
			$wp_query->max_num_pages     = 1;

			unset( $wp_query->query['error'] );
			$wp_query->query_vars['error'] = '';
			// phpcs:enable Squiz.NamingConventions.ValidVariableName

			// We need to add the post object to the cache so that get_post() calls don't trigger database calls.
			wp_cache_add( $postId, $postObject, 'posts' );

			$GLOBALS['wp_query'] = $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			$wp->register_globals();

			// Setting is_404 is not sufficient, so we still need to change the status code.
			status_header( 200 );
		}

		/**
		 * Get the canonical URL for the dedicated HTML sitemap page.
		 *
		 * @since 4.5.7
		 *
		 * @param  string $originalUrl The canonical URL.
		 * @return string              The canonical URL.
		 */
		public function getCanonicalUrl( $originalUrl ) {
			$sitemapOptions = aioseo()->options->sitemap->html;

			if ( ! $sitemapOptions->enable || ! $this->isDedicatedPage ) {
				return $originalUrl;
			}

			// If the user has set a custom URL for the sitemap page, use that.
			if ( $sitemapOptions->pageUrl ) {
				return $sitemapOptions->pageUrl;
			}

			// Return the current URL of WP.
			global $wp;

			return home_url( $wp->request );
		}
	}
}

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

	if ( ! function_exists( 'aioseo_html_sitemap' ) ) {
		/**
		 * Global function that can be used to print the HTML sitemap.
		 *
		 * @since 4.1.3
		 *
		 * @param  array   $attributes User-defined attributes that override the default settings.
		 * @param  boolean $echo       Whether to echo the output or return it.
		 * @return string              The HTML sitemap code.
		 */
		function aioseo_html_sitemap( $attributes = [], $echo = true ) {
			$attributes = aioseo()->htmlSitemap->frontend->getAttributes( $attributes );

			return aioseo()->htmlSitemap->frontend->output( $echo, $attributes );
		}
	}
}Common/Sitemap/Html/CompactArchive.php000066600000005677151135505570013735 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap\Html;

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

/**
 * Handles the Compact Archive's output.
 *
 * @since 4.1.3
 */
class CompactArchive {
	/**
	 * The shortcode attributes.
	 *
	 * @since 4.1.3
	 *
	 * @var array
	 */
	private $attributes;

	/**
	 * Outputs the compact archives sitemap.
	 *
	 * @since 4.1.3
	 *
	 * @param  array   $attributes The shortcode attributes.
	 * @param  boolean $echo       Whether the HTML code should be printed or returned.
	 * @return string              The HTML for the compact archive.
	 */
	public function output( $attributes, $echo = true ) {
		$dateArchives     = ( new Query() )->archives();
		$this->attributes = $attributes;

		if ( 'asc' === strtolower( $this->attributes['order'] ) ) {
			$dateArchives = array_reverse( $dateArchives, true );
		}

		$data = [
			'dateArchives' => $dateArchives,
			'lines'        => ''
		];
		foreach ( $dateArchives as $year => $months ) {
			$data['lines'] .= $this->generateYearLine( $year, $months ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}

		ob_start();
		aioseo()->templates->getTemplate( 'sitemap/html/compact-archive.php', $data );
		$output = ob_get_clean();

		if ( $echo ) {
			echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}

		return $output;
	}
	// phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable

	/**
	* Generates the HTML for a year line.
	*
	* @since 4.1.3
	*
	* @param  int    $year   The year archive.
	* @param  array  $months The month archives for the current year.
	* @return string         The HTML code for the year.
	*/
	protected function generateYearLine( $year, $months ) {
		$html = '<li><strong><a href="' . get_year_link( $year ) . '">' . esc_html( $year ) . '</a>: </strong> ';

		for ( $month = 1; $month <= 12; $month++ ) {
			$html .= $this->generateMonth( $year, $months, $month );
		}

		$html .= '</li>' . "\n";

		return wp_kses_post( $html );
	}

	/**
	 * Generates the HTML for a month.
	 *
	 * @since 4.1.3
	 *
	 * @param  int    $year   The year archive.
	 * @param  array  $months All month archives for the current year.
	 * @param  int    $month  The month archive.
	 * @return string         The HTML code for the month.
	 */
	public function generateMonth( $year, $months, $month ) {
		$hasPosts         = isset( $months[ $month ] );
		$dummyDate        = strtotime( "2009/{$month}/25" );
		$monthAbbrevation = date_i18n( 'M', $dummyDate );

		$html = '<span class="aioseo-empty-month">' . esc_html( $monthAbbrevation ) . '</span> ';
		if ( $hasPosts ) {
			$noFollow = filter_var( $this->attributes['nofollow_links'], FILTER_VALIDATE_BOOLEAN );
			$html     = sprintf(
				'<a href="%1$s" title="%2$s"%3$s>%4$s</a> ',
				get_month_link( $year, $month ),
				esc_attr( date_i18n( 'F Y', $dummyDate ) ),
				$noFollow ? ' rel="nofollow"' : '',
				esc_html( $monthAbbrevation )
			);
		}

		return $html;
	}
}Common/Sitemap/Html/Widget.php000066600000013607151135505570012260 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap\Html;

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

/**
 * Class Widget.
 *
 * @since 4.1.3
 */
class Widget extends \WP_Widget {
	/**
	 * The default attributes.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $defaults = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.3
	 */
	public function __construct() {
		// The default widget settings.
		$this->defaults = [
			'title'            => '',
			'show_label'       => 'on',
			'archives'         => '',
			'nofollow_links'   => '',
			'order'            => 'asc',
			'order_by'         => 'publish_date',
			'publication_date' => 'on',
			'post_types'       => [ 'post', 'page' ],
			'taxonomies'       => [ 'category', 'post_tag' ],
			'excluded_posts'   => '',
			'excluded_terms'   => ''
		];

		$widgetSlug     = 'aioseo-html-sitemap-widget';
		$widgetOptions  = [
			'classname'   => $widgetSlug,
			// Translators: The short plugin name ("AIOSEO").
			'description' => sprintf( esc_html__( '%1$s HTML sitemap widget.', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME )
		];
		$controlOptions = [
			'id_base' => $widgetSlug
		];

		// Translators: 1 - The plugin short name ("AIOSEO").
		$name = sprintf( esc_html__( '%1$s - HTML Sitemap', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME );
		$name .= ' ' . esc_html__( '(legacy)', 'all-in-one-seo-pack' );
		parent::__construct( $widgetSlug, $name, $widgetOptions, $controlOptions );
	}

	/**
	 * Callback for the widget.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $args     The widget arguments.
	 * @param  array $instance The widget instance options.
	 * @return void
	 */
	public function widget( $args, $instance ) {
		if ( ! aioseo()->options->sitemap->html->enable ) {
			return;
		}

		// Merge with defaults.
		$instance = wp_parse_args( (array) $instance, $this->defaults );

		echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped

		if ( ! empty( $instance['title'] ) ) {
			echo $args['before_title'] . apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped,Generic.Files.LineLength.MaxExceeded
		}

		$instance = aioseo()->htmlSitemap->frontend->getAttributes( $instance );
		aioseo()->htmlSitemap->frontend->output( true, $instance );
		echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Callback to update the widget options.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $newOptions The new options.
	 * @param  array $oldOptions The old options.
	 * @return array             The new options.
	 */
	public function update( $newOptions, $oldOptions ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$settings = [
			'title',
			'order',
			'order_by',
			'show_label',
			'publication_date',
			'archives',
			'excluded_posts',
			'excluded_terms'
		];

		foreach ( $settings as $setting ) {
			$newOptions[ $setting ] = ! empty( $newOptions[ $setting ] ) ? wp_strip_all_tags( $newOptions[ $setting ] ) : '';
		}

		$includedPostTypes = [];
		if ( ! empty( $newOptions['post_types'] ) ) {
			$postTypes = $this->getPublicPostTypes( true );
			foreach ( $newOptions['post_types'] as $v ) {
				if ( is_numeric( $v ) ) {
					$includedPostTypes[] = $postTypes[ $v ];
				} else {
					$includedPostTypes[] = $v;
				}
			}
		}
		$newOptions['post_types'] = $includedPostTypes;

		$includedTaxonomies = [];
		if ( ! empty( $newOptions['taxonomies'] ) ) {
			$taxonomies = aioseo()->helpers->getPublicTaxonomies( true );
			foreach ( $newOptions['taxonomies'] as $v ) {
				if ( is_numeric( $v ) ) {
					$includedTaxonomies[] = $taxonomies[ $v ];
				} else {
					$includedTaxonomies[] = $v;
				}
			}
		}
		$newOptions['taxonomies'] = $includedTaxonomies;

		if ( ! empty( $newOptions['excluded_posts'] ) ) {
			$newOptions['excluded_posts'] = $this->sanitizeExcludedIds( $newOptions['excluded_posts'] );
		}

		if ( ! empty( $newOptions['excluded_terms'] ) ) {
			$newOptions['excluded_terms'] = $this->sanitizeExcludedIds( $newOptions['excluded_terms'] );
		}

		return $newOptions;
	}

	/**
	 * Callback for the widgets options form.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $instance The widget options.
	 * @return void
	 */
	public function form( $instance ) {
		// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$instance        = wp_parse_args( (array) $instance, $this->defaults );
		$postTypeObjects = $this->getPublicPostTypes();
		$postTypes       = $this->getPublicPostTypes( true );
		$taxonomyObjects = aioseo()->helpers->getPublicTaxonomies();
		// phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable

		include AIOSEO_DIR . '/app/Common/Views/sitemap/html/widget-options.php';
	}

	/**
	 * Returns the public post types (without attachments).
	 *
	 * @since 4.1.3
	 *
	 * @param  boolean $namesOnly Whether only the names should be returned.
	 * @return array              The public post types.
	 */
	private function getPublicPostTypes( $namesOnly = false ) {
		$postTypes = aioseo()->helpers->getPublicPostTypes( $namesOnly );
		foreach ( $postTypes as $k => $postType ) {
			if ( is_array( $postType ) && 'attachment' === $postType['name'] ) {
				unset( $postTypes[ $k ] );
				break;
			}
			if ( ! is_array( $postType ) && 'attachment' === $postType ) {
				unset( $postTypes[ $k ] );
				break;
			}
		}

		return array_values( $postTypes );
	}

	/**
	 * Sanitizes the excluded IDs by removing any non-integer values.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $ids The IDs as a string, comma-separated.
	 * @return string      The sanitized IDs as a string, comma-separated.
	 */
	private function sanitizeExcludedIds( $ids ) {
		$ids = array_map( 'trim', explode( ',', $ids ) );
		$ids = array_filter( $ids, 'is_numeric' );
		$ids = esc_sql( implode( ', ', $ids ) );

		return $ids;
	}
}Common/Sitemap/RequestParser.php000066600000016473151135505570012742 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

/**
 * Parses the current request and checks whether we need to serve a sitemap or a stylesheet.
 *
 * @since 4.2.1
 */
class RequestParser {
	/**
	 * The cleaned slug of the current request.
	 *
	 * @since 4.2.1
	 *
	 * @var string
	 */
	public $slug;

	/**
	 * Whether we've checked if the page needs to be redirected.
	 *
	 * @since 4.2.3
	 *
	 * @var bool
	 */
	protected $checkedForRedirects = false;

	/**
	 * CLass constructor.
	 *
	 * @since 4.2.1
	 */
	public function __construct() {
		if ( is_admin() ) {
			return;
		}

		add_action( 'parse_request', [ $this, 'checkRequest' ] );
	}

	/**
	 * Checks whether we need to serve a sitemap or related stylesheet.
	 *
	 * @since 4.2.1
	 *
	 * @param  \WP  $wp The main WordPress environment instance.
	 * @return void
	 */
	public function checkRequest( $wp ) {
		$this->slug = $wp->request ?? $this->cleanSlug( $wp->request );
		if ( ! $this->slug && isset( $_SERVER['REQUEST_URI'] ) ) {
			// We must fallback to the REQUEST URI in case the site uses plain permalinks.
			$this->slug = $this->cleanSlug( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) );
		}

		if ( ! $this->slug ) {
			return;
		}

		// Check if we need to remove the trailing slash or redirect another sitemap URL like "wp-sitemap.xml".
		$this->maybeRedirect();

		$this->checkForXsl();

		if ( aioseo()->options->sitemap->general->enable ) {
			$this->checkForGeneralSitemap();
		}

		if ( aioseo()->options->sitemap->rss->enable ) {
			$this->checkForRssSitemap();
		}
	}

	/**
	 * Cleans the slug of the current request before we use it.
	 *
	 * @since 4.2.3
	 *
	 * @param  string $slug The slug.
	 * @return string       The cleaned slug.
	 */
	public function cleanSlug( $slug ) {
		$slug = strtolower( $slug );
		$slug = aioseo()->helpers->unleadingSlashIt( $slug );
		$slug = untrailingslashit( $slug );

		return $slug;
	}

	/**
	 * Checks whether the general XML sitemap needs to be served.
	 *
	 * @since 4.2.1
	 *
	 * @return void
	 */
	private function checkForGeneralSitemap() {
		$fileName       = aioseo()->sitemap->helpers->filename( 'general' );
		$indexesEnabled = aioseo()->options->sitemap->general->indexes;

		if ( ! $indexesEnabled ) {
			// If indexes are disabled, check for the root index.
			if ( preg_match( "/^{$fileName}\.xml(\.gz)?$/i", (string) $this->slug, $match ) ) {
				$this->setContext( 'general', $fileName );
				aioseo()->sitemap->generate();
			}

			return;
		}

		// First, check for the root index.
		if ( preg_match( "/^{$fileName}\.xml(\.gz)?$/i", (string) $this->slug, $match ) ) {
			$this->setContext( 'general', $fileName );
			aioseo()->sitemap->generate();

			return;
		}

		if (
			// Now, check for the other indexes.
			preg_match( "/^(?P<objectName>.+)-{$fileName}\.xml(\.gz)?$/i", (string) $this->slug, $match ) ||
			preg_match( "/^(?P<objectName>.+)-{$fileName}(?P<pageNumber>\d+)\.xml(\.gz)?$/i", (string) $this->slug, $match )
		) {
			$pageNumber = ! empty( $match['pageNumber'] ) ? $match['pageNumber'] : 0;
			$this->setContext( 'general', $fileName, $match['objectName'], $pageNumber );
			aioseo()->sitemap->generate();
		}
	}

	/**
	 * Checks whether the RSS sitemap needs to be served.
	 *
	 * @since 4.2.1
	 *
	 * @return void
	 */
	private function checkForRssSitemap() {
		if ( ! preg_match( '/^sitemap(\.latest)?\.rss$/i', (string) $this->slug, $match ) ) {
			return;
		}

		$this->setContext( 'rss' );
		aioseo()->sitemap->generate();
	}

	/**
	 * Checks if we need to serve a stylesheet.
	 *
	 * @since 4.2.1
	 *
	 * @return void
	 */
	protected function checkForXsl() {
		// Trim off the URL params.
		$newSlug = preg_replace( '/\?.*$/', '', (string) $this->slug );
		if ( preg_match( '/^default-sitemap\.xsl$/i', (string) $newSlug ) ) {
			aioseo()->sitemap->xsl->generate();
		}
	}

	/**
	 * Sets the context for the requested sitemap.
	 *
	 * @since 4.2.1
	 *
	 * @param  string     $type       The sitemap type (e.g. "general" or "rss").
	 * @param  string     $fileName   The sitemap filename.
	 * @param  string     $indexName  The index name ("root" or an object name like "post", "page", "post_tag", etc.).
	 * @param  int        $pageNumber The index number.
	 * @return void|never
	 */
	public function setContext( $type, $fileName = 'sitemap', $indexName = 'root', $pageNumber = 0 ) {
		$indexesEnabled = aioseo()->options->sitemap->{$type}->indexes;

		aioseo()->sitemap->type          = $type;
		aioseo()->sitemap->filename      = $fileName;
		aioseo()->sitemap->indexes       = $indexesEnabled;
		aioseo()->sitemap->indexName     = $indexName;
		aioseo()->sitemap->linksPerIndex = aioseo()->options->sitemap->{$type}->linksPerIndex <= 50000 ? aioseo()->options->sitemap->{$type}->linksPerIndex : 50000;
		aioseo()->sitemap->pageNumber    = $pageNumber >= 1 ? $pageNumber - 1 : 0;
		aioseo()->sitemap->offset        = aioseo()->sitemap->linksPerIndex * aioseo()->sitemap->pageNumber;
		aioseo()->sitemap->isStatic      = false;
	}

	/**
	 * Redirects or alters the current request if:
	 * 1. The request includes our deprecated "aiosp_sitemap_path" URL param.
	 * 2. The request is for one of our sitemaps, but has a trailing slash.
	 * 3. The request is for the first index of a type, but has a page number.
	 * 4. The request is for a sitemap from WordPress Core/other plugin.
	 *
	 * @since 4.2.1
	 */
	protected function maybeRedirect() {
		if ( $this->checkedForRedirects ) {
			return;
		}

		$requestUri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
		if ( ! $requestUri ) {
			return;
		}

		$this->checkedForRedirects = true;

		// The request includes our deprecated "aiosp_sitemap_path" URL param.
		if ( preg_match( '/^\/\?aiosp_sitemap_path=root/i', (string) $requestUri ) ) {
			wp_safe_redirect( home_url( 'sitemap.xml' ) );
			exit;
		}

		// The request is for one of our sitemaps, but has a trailing slash.
		if ( preg_match( '/\/(.*sitemap[0-9]*?\.xml(\.gz)?|.*sitemap(\.latest)?\.rss)\/$/i', (string) $requestUri ) ) {
			wp_safe_redirect( home_url() . untrailingslashit( $requestUri ) );
			exit;
		}

		// The request is for the first index of a type, but has a page number.
		if ( preg_match( '/.*sitemap(0|1){1}?\.xml(\.gz)?$/i', (string) $requestUri ) ) {
			$pathWithoutNumber = preg_replace( '/(.*sitemap)(0|1){1}?(\.xml(\.gz)?)$/i', '$1$3', $requestUri );
			wp_safe_redirect( home_url() . $pathWithoutNumber );
			exit;
		}

		// The request is for a sitemap from WordPress Core/other plugin, but the general sitemap is enabled.
		if ( ! aioseo()->options->sitemap->general->enable ) {
			return;
		}

		$sitemapPatterns = [
			'general' => [
				'sitemap\.txt',
				'sitemaps\.xml',
				'sitemap-xml\.xml',
				'sitemap[0-9]+\.xml',
				'sitemap(|[-_\/])?index[0-9]*\.xml',
				'wp-sitemap\.xml',
			],
			'rss'     => [
				'rss[0-9]*\.xml',
			]
		];

		$addonSitemapPatterns = aioseo()->addons->doAddonFunction( 'helpers', 'getOtherSitemapPatterns' );
		if ( ! empty( $addonSitemapPatterns ) ) {
			$sitemapPatterns = array_merge( $sitemapPatterns, $addonSitemapPatterns );
		}

		foreach ( $sitemapPatterns as $type => $patterns ) {
			foreach ( $patterns as $pattern ) {
				if ( preg_match( "/^$pattern$/i", (string) $this->slug ) ) {
					wp_safe_redirect( aioseo()->sitemap->helpers->getUrl( $type ) );
					exit;
				}
			}
		}
	}
}Common/Sitemap/Localization.php000066600000024757151135505570012571 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

/**
 * Handles sitemap localization logic.
 *
 * @since 4.2.1
 */
class Localization {
	/**
	 * This is cached so we don't do the lookup each query.
	 *
	 * @since 4.0.0
	 *
	 * @var boolean
	 */
	private static $wpml = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.2.1
	 */
	public function __construct() {
		if ( apply_filters( 'aioseo_sitemap_localization_disable', false ) ) {
			return;
		}

		if ( aioseo()->helpers->isWpmlActive() ) {
			self::$wpml = [
				'defaultLanguage' => apply_filters( 'wpml_default_language', null ),
				'activeLanguages' => apply_filters( 'wpml_active_languages', null )
			];

			add_filter( 'aioseo_sitemap_term', [ $this, 'localizeWpml' ], 10, 4 );
			add_filter( 'aioseo_sitemap_post', [ $this, 'localizeWpml' ], 10, 4 );
		}

		if ( aioseo()->helpers->isPluginActive( 'weglot' ) ) {
			add_filter( 'aioseo_sitemap_term', [ $this, 'localizeWeglot' ], 10, 4 );
			add_filter( 'aioseo_sitemap_post', [ $this, 'localizeWeglot' ], 10, 4 );
			add_filter( 'aioseo_sitemap_author_entry', [ $this, 'localizeWeglot' ], 10, 4 );
			add_filter( 'aioseo_sitemap_archive_entry', [ $this, 'localizeWeglot' ], 10, 4 );
			add_filter( 'aioseo_sitemap_date_entry', [ $this, 'localizeWeglot' ], 10, 4 );
			add_filter( 'aioseo_sitemap_product_attributes', [ $this, 'localizeWeglot' ], 10, 4 );
		}
	}

	/**
	 * Localize the entries for Weglot.
	 *
	 * @since 4.8.3
	 *
	 * @param  array       $entry      The entry.
	 * @param  mixed       $entryId    The object ID, null or a date object.
	 * @param  string      $objectName The post type, taxonomy name or date type ('year' or 'month').
	 * @param  string|null $entryType  Whether the entry represents a post, term, author, archive or date.
	 * @return array                   The entry.
	 */
	public function localizeWeglot( $entry, $entryId, $objectName, $entryType = null ) {
		try {
			$originalLang = function_exists( 'weglot_get_original_language' ) ? weglot_get_original_language() : '';
			$translations = function_exists( 'weglot_get_destination_languages' ) ? weglot_get_destination_languages() : [];
			if ( empty( $originalLang ) || empty( $translations ) ) {
				return $entry;
			}

			switch ( $entryType ) {
				case 'post':
					$permalink = get_permalink( $entryId );
					break;
				case 'term':
					$permalink = get_term_link( $entryId, $objectName );
					break;
				case 'author':
					$permalink = get_author_posts_url( $entryId, $objectName );
					break;
				case 'archive':
					$permalink = get_post_type_archive_link( $objectName );
					break;
				case 'date':
					$permalink = 'year' === $objectName ? get_year_link( $entryId->year ) : get_month_link( $entryId->year, $entryId->month );
					break;
				default:
					$permalink = '';
			}

			$entry['languages'] = [];
			foreach ( $translations as $translation ) {
				// If the translation is not public we skip it.
				if ( empty( $translation['public'] ) ) {
					continue;
				}

				$l10nPermalink = $this->weglotGetLocalizedUrl( $permalink, $translation['language_to'] );
				if ( ! empty( $l10nPermalink ) ) {
					$entry['languages'][] = [
						'language' => $translation['language_to'],
						'location' => $l10nPermalink
					];
				}
			}

			// Also include the main page as a translated variant, per Google's specifications, but only if we found at least one other language.
			if ( ! empty( $entry['languages'] ) ) {
				$entry['languages'][] = [
					'language' => $originalLang,
					'location' => aioseo()->helpers->decodeUrl( $entry['loc'] )
				];
			} else {
				unset( $entry['languages'] );
			}

			return $this->validateSubentries( $entry );
		} catch ( \Exception $e ) {
			// Do nothing. It only exists because some "weglot" functions above throw exceptions.
		}

		return $entry;
	}

	/**
	 * Localize the entries for WPML.
	 *
	 * @since   4.0.0
	 * @version 4.8.3 Rename from localizeEntry to localizeWpml.
	 *
	 * @param  array  $entry      The entry.
	 * @param  int    $entryId    The post/term ID.
	 * @param  string $objectName The post type or taxonomy name.
	 * @param  string $objectType Whether the entry is a post or term.
	 * @return array              The entry.
	 */
	public function localizeWpml( $entry, $entryId, $objectName, $objectType ) {
		$elementId   = $entryId;
		$elementType = 'post_' . $objectName;
		if ( 'term' === $objectType ) {
			$term        = aioseo()->helpers->getTerm( $entryId, $objectName );
			$elementId   = $term->term_taxonomy_id;
			$elementType = 'tax_' . $objectName;
		}

		$translationGroupId = apply_filters( 'wpml_element_trid', null, $elementId, $elementType );
		$translations       = apply_filters( 'wpml_get_element_translations', null, $translationGroupId, $elementType );
		if ( empty( $translations ) ) {
			return $entry;
		}

		$entry['languages'] = [];
		$hiddenLanguages    = apply_filters( 'wpml_setting', [], 'hidden_languages' );
		foreach ( $translations as $translation ) {
			if (
				empty( $translation->element_id ) ||
				! isset( self::$wpml['activeLanguages'][ $translation->language_code ] ) ||
				in_array( $translation->language_code, $hiddenLanguages, true )
			) {
				continue;
			}

			$currentLanguage = ! empty( self::$wpml['activeLanguages'][ $translation->language_code ] ) ? self::$wpml['activeLanguages'][ $translation->language_code ] : null;
			$languageCode    = ! empty( $currentLanguage['tag'] ) ? $currentLanguage['tag'] : $translation->language_code;

			if ( (int) $elementId === (int) $translation->element_id ) {
				$entry['language'] = $languageCode;
				continue;
			}

			$translatedObjectId = apply_filters( 'wpml_object_id', $entryId, $objectName, false, $translation->language_code );
			if (
				( 'post' === $objectType && $this->isExcludedPost( $translatedObjectId ) ) ||
				( 'term' === $objectType && $this->isExcludedTerm( $translatedObjectId ) )
			) {
				continue;
			}

			if ( 'post' === $objectType ) {
				$permalink = get_permalink( $translatedObjectId );

				// Special treatment for the home page translations.
				if ( 'page' === get_option( 'show_on_front' ) && aioseo()->helpers->wpmlIsHomePage( $entryId ) ) {
					$permalink = aioseo()->helpers->wpmlHomeUrl( $translation->language_code );
				}
			} else {
				$permalink = get_term_link( $translatedObjectId, $objectName );
			}

			if ( ! empty( $languageCode ) && ! empty( $permalink ) ) {
				$entry['languages'][] = [
					'language' => $languageCode,
					'location' => aioseo()->helpers->decodeUrl( $permalink )
				];
			}
		}

		// Also include the main page as a translated variant, per Google's specifications, but only if we found at least one other language.
		if ( ! empty( $entry['language'] ) && ! empty( $entry['languages'] ) ) {
			$entry['languages'][] = [
				'language' => $entry['language'],
				'location' => aioseo()->helpers->decodeUrl( $entry['loc'] )
			];
		} else {
			unset( $entry['languages'] );
		}

		return $this->validateSubentries( $entry );
	}

	/**
	 * Validates the subentries with translated variants to ensure all required values are set.
	 *
	 * @since 4.2.3
	 *
	 * @param  array $entry The entry.
	 * @return array        The validated entry.
	 */
	private function validateSubentries( $entry ) {
		if ( ! isset( $entry['languages'] ) ) {
			return $entry;
		}

		foreach ( $entry['languages'] as $index => $subentry ) {
			if ( empty( $subentry['language'] ) || empty( $subentry['location'] ) ) {
				unset( $entry['languages'][ $index ] );
			}
		}

		return $entry;
	}

	/**
	 * Checks whether the given post should be excluded.
	 *
	 * @since 4.2.4
	 *
	 * @param  int  $postId The post ID.
	 * @return bool         Whether the post should be excluded.
	 */
	private function isExcludedPost( $postId ) {
		static $excludedPostIds = null;
		if ( null === $excludedPostIds ) {
			$excludedPostIds = explode( ', ', aioseo()->sitemap->helpers->excludedPosts() );
			$excludedPostIds = array_map( function ( $postId ) {
				return (int) $postId;
			}, $excludedPostIds );
		}

		if ( in_array( $postId, $excludedPostIds, true ) ) {
			return true;
		}

		// Let's also check if the post is published and not password-protected.
		$post = get_post( $postId );
		if ( ! is_a( $post, 'WP_Post' ) ) {
			return true;
		}

		if ( ! empty( $post->post_password ) || 'publish' !== $post->post_status ) {
			return true;
		}

		// Now, we must also check for noindex.
		$metaData = aioseo()->meta->metaData->getMetaData( $post );
		if ( ! empty( $metaData->robots_noindex ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Checks whether the given term should be excluded.
	 *
	 * @since 4.2.4
	 *
	 * @param  int  $termId The term ID.
	 * @return bool         Whether the term should be excluded.
	 */
	private function isExcludedTerm( $termId ) {
		static $excludedTermIds = null;
		if ( null === $excludedTermIds ) {
			$excludedTermIds = explode( ', ', aioseo()->sitemap->helpers->excludedTerms() );
			$excludedTermIds = array_map( function ( $termId ) {
				return (int) $termId;
			}, $excludedTermIds );
		}

		if ( in_array( $termId, $excludedTermIds, true ) ) {
			return true;
		}

		// Now, we must also check for noindex.
		$term = aioseo()->helpers->getTerm( $termId );
		if ( ! is_a( $term, 'WP_Term' ) ) {
			return true;
		}

		// At least one post must be assigned to the term.
		$posts = aioseo()->core->db->start( 'term_relationships' )
			->select( 'object_id' )
			->where( 'term_taxonomy_id =', $term->term_taxonomy_id )
			->limit( 1 )
			->run()
			->result();

		if ( empty( $posts ) ) {
			return true;
		}

		$metaData = aioseo()->meta->metaData->getMetaData( $term );
		if ( ! empty( $metaData->robots_noindex ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Retrieves the localized URL.
	 *
	 * @since 4.8.3
	 *
	 * @param  string       $url  The page URL to localize.
	 * @param  string       $code The language code (e.g. 'br', 'en').
	 * @return string|false       The localized URL or false if it fails.
	 */
	private function weglotGetLocalizedUrl( $url, $code ) {
		try {
			if (
				! $url ||
				! function_exists( 'weglot_get_service' )
			) {
				return false;
			}

			$languageService   = weglot_get_service( 'Language_Service_Weglot' );
			$requestUrlService = weglot_get_service( 'Request_Url_Service_Weglot' );
			$wgUrl             = $requestUrlService->create_url_object( $url );
			$language          = $languageService->get_language_from_internal( $code );

			return $wgUrl->getForLanguage( $language );
		} catch ( \Exception $e ) {
			// Do nothing. It only exists because some "weglot" functions above throw exceptions.
		}

		return false;
	}
}Common/Sitemap/Xsl.php000066600000012230151135505570010666 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

/**
 * Serves stylesheets for sitemaps.
 *
 * @since 4.2.1
 */
class Xsl {
	/**
	 * Generates the XSL stylesheet for the current sitemap.
	 *
	 * @since 4.2.1
	 *
	 * @return void
	 */
	public function generate() {
		aioseo()->sitemap->headers();

		$charset     = aioseo()->helpers->getCharset();
		$sitemapUrl  = wp_get_referer();
		$sitemapPath = aioseo()->helpers->getPermalinkPath( $sitemapUrl );

		// Figure out which sitemap we're serving.
		preg_match( '/\/(.*?)-?sitemap([0-9]*)\.xml/', (string) $sitemapPath, $sitemapInfo );
		$sitemapName = ! empty( $sitemapInfo[1] ) ? strtoupper( $sitemapInfo[1] ) : '';

		// Remove everything after ? from sitemapPath to avoid caching issues.
		$sitemapPath = wp_parse_url( $sitemapPath, PHP_URL_PATH ) ?: '';

		// These variables are used in the XSL file.
		// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$linksPerIndex = aioseo()->options->sitemap->general->linksPerIndex;
		$advanced      = aioseo()->options->sitemap->general->advancedSettings->enable;
		$excludeImages = aioseo()->sitemap->helpers->excludeImages();
		$sitemapParams = aioseo()->helpers->getParametersFromUrl( $sitemapUrl );
		$xslParams     = aioseo()->core->cache->get( 'aioseo_sitemap_' . aioseo()->sitemap->requestParser->cleanSlug( $sitemapPath ) );
		// phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable

		if ( ! empty( $sitemapInfo[1] ) ) {
			switch ( $sitemapInfo[1] ) {
				case 'addl':
					$sitemapName = __( 'Additional Pages', 'all-in-one-seo-pack' );
					// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
					$excludeImages = true;
					break;
				case 'post-archive':
					$sitemapName = __( 'Post Archive', 'all-in-one-seo-pack' );
					break;
				case 'bp-activity':
				case 'bp-group':
				case 'bp-member':
					$bpFakePostTypes = aioseo()->standalone->buddyPress->getFakePostTypes();
					$labels          = array_column( wp_list_filter( $bpFakePostTypes, [ 'name' => $sitemapInfo[1] ] ), 'label' );
					$sitemapName     = ! empty( $labels[0] ) ? $labels[0] : $sitemapName;
					break;
				case 'product_attributes':
					$sitemapName = __( 'Product Attributes', 'all-in-one-seo-pack' );
					break;
				default:
					if ( post_type_exists( $sitemapInfo[1] ) ) {
						$postTypeObject = get_post_type_object( $sitemapInfo[1] );
						$sitemapName    = $postTypeObject->labels->singular_name;
					}
					if ( taxonomy_exists( $sitemapInfo[1] ) ) {
						$taxonomyObject = get_taxonomy( $sitemapInfo[1] );
						$sitemapName    = $taxonomyObject->labels->singular_name;
					}
					break;
			}
		}

		$currentPage = ! empty( $sitemapInfo[2] ) ? (int) $sitemapInfo[2] : 1;

		// Translators: 1 - The sitemap name, 2 - The current page.
		$title = sprintf( __( '%1$s Sitemap %2$s', 'all-in-one-seo-pack' ), $sitemapName, $currentPage > 1 ? $currentPage : '' );
		$title = trim( $title );

		echo '<?xml version="1.0" encoding="' . esc_attr( $charset ) . '"?>';
		include_once AIOSEO_DIR . '/app/Common/Views/sitemap/xsl/default.php';
		exit;
	}

	/**
	 * Save the data to use in the XSL.
	 *
	 * @since 4.1.5
	 *
	 * @param  string $fileName The sitemap file name.
	 * @param  array  $entries  The sitemap entries.
	 * @param  int    $total    The total sitemap entries count.
	 * @return void
	 */
	public function saveXslData( $fileName, $entries, $total ) {
		$counts     = [];
		$datetime   = [];
		$dateFormat = get_option( 'date_format' );
		$timeFormat = get_option( 'time_format' );

		$entries = aioseo()->sitemap->helpers->decodeSitemapEntries( $entries );

		foreach ( $entries as $index ) {
			$url = ! empty( $index['guid'] ) ? $index['guid'] : $index['loc'];

			if ( ! empty( $index['count'] ) && aioseo()->options->sitemap->general->linksPerIndex !== (int) $index['count'] ) {
				$counts[ $url ] = $index['count'];
			}

			if ( ! empty( $index['lastmod'] ) || ! empty( $index['publicationDate'] ) || ! empty( $index['pubDate'] ) ) {
				$date             = ! empty( $index['lastmod'] ) ? $index['lastmod'] : ( ! empty( $index['publicationDate'] ) ? $index['publicationDate'] : $index['pubDate'] );
				$isTimezone       = ! empty( $index['isTimezone'] ) && $index['isTimezone'];
				$datetime[ $url ] = [
					'date' => $isTimezone ? date_i18n( $dateFormat, strtotime( $date ) ) : get_date_from_gmt( $date, $dateFormat ),
					'time' => $isTimezone ? date_i18n( $timeFormat, strtotime( $date ) ) : get_date_from_gmt( $date, $timeFormat )
				];
			}
		}

		$data = [
			'counts'     => $counts,
			'datetime'   => $datetime,
			'pagination' => [
				'showing' => count( $entries ),
				'total'   => $total
			]
		];

		// Set a high expiration date so we still have the cache for static sitemaps.
		aioseo()->core->cache->update( 'aioseo_sitemap_' . $fileName, $data, MONTH_IN_SECONDS );
	}

	/**
	 * Retrieve the data to use on the XSL.
	 *
	 * @since 4.2.1
	 *
	 * @param  string $fileName The sitemap file name.
	 * @return array            The XSL data for the given file name.
	 */
	public function getXslData( $fileName ) {
		return aioseo()->core->cache->get( 'aioseo_sitemap_' . $fileName );
	}
}Common/Sitemap/Sitemap.php000066600000025377151135505570011542 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Handles our sitemaps.
 *
 * @since 4.0.0
 */
class Sitemap extends SitemapAbstract {
	/**
	 * The sitemap filename.
	 *
	 * @since 4.4.2
	 *
	 * @var string
	 */
	public $filename = '';

	/**
	 * Whether the sitemap indexes are enabled.
	 *
	 * @since 4.4.2
	 *
	 * @var bool
	 */
	public $indexes = false;

	/**
	 * The sitemap index name.
	 *
	 * @since 4.4.2
	 *
	 * @var string
	 */
	public $indexName = '';

	/**
	 * The number of links per index.
	 *
	 * @since 4.4.2
	 *
	 * @var int
	 */
	public $linksPerIndex = 1000;

	/**
	 * The current page number.
	 *
	 * @since 4.4.2
	 *
	 * @var int
	 */
	public $pageNumber = 0;

	/**
	 * The entries' offset.
	 *
	 * @since 4.4.2
	 *
	 * @var int
	 */
	public $offset = 0;

	/**
	 * Whether the sitemap is static.
	 *
	 * @since 4.4.2
	 *
	 * @var bool
	 */
	public $isStatic = false;

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->content       = new Content();
		$this->root          = new Root();
		$this->query         = new Query();
		$this->file          = new File();
		$this->image         = new Image\Image();
		$this->priority      = new Priority();
		$this->output        = new Output();
		$this->helpers       = new Helpers();
		$this->requestParser = new RequestParser();
		$this->xsl           = new Xsl();

		new Localization();

		$this->disableWpSitemap();
	}

	/**
	 * Adds our hooks.
	 * Note: This runs init and is triggered in the main AIOSEO class.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function init() {
		add_action( 'aioseo_static_sitemap_regeneration', [ $this, 'regenerateStaticSitemap' ] );

		// Check if static files need to be updated.
		add_action( 'wp_insert_post', [ $this, 'regenerateOnUpdate' ] );
		add_action( 'edited_term', [ $this, 'regenerateStaticSitemap' ] );

		add_action( 'admin_init', [ $this, 'detectStatic' ] );

		$this->maybeAddHtaccessRewriteRules();
	}

	/**
	 * Disables the WP Core sitemap if our general sitemap is enabled.
	 *
	 * @since 4.2.1
	 *
	 * @return void
	 */
	protected function disableWpSitemap() {
		if ( ! aioseo()->options->sitemap->general->enable ) {
			return;
		}

		remove_action( 'init', 'wp_sitemaps_get_server' );
		add_filter( 'wp_sitemaps_enabled', '__return_false' );
	}

	/**
	 * Check if the .htaccess rewrite rules are present if the user is using Apache. If not, add them.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	private function maybeAddHtaccessRewriteRules() {
		if ( ! aioseo()->helpers->isApache() || wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		ob_start();
		aioseo()->templates->getTemplate( 'sitemap/htaccess-rewrite-rules.php' );
		$rewriteRules = ob_get_clean();

		$escapedRewriteRules = aioseo()->helpers->escapeRegex( $rewriteRules );

		$contents = aioseo()->helpers->decodeHtmlEntities( aioseo()->htaccess->getContents() );
		if ( get_option( 'permalink_structure' ) ) {
			if ( preg_match( '/All in One SEO Sitemap Rewrite Rules/i', (string) $contents ) && ! aioseo()->core->cache->get( 'aioseo_sitemap_htaccess_rewrite_rules_remove' ) ) {
				aioseo()->core->cache->update( 'aioseo_sitemap_htaccess_rewrite_rules_remove', time(), HOUR_IN_SECONDS );

				$contents = preg_replace( "/$escapedRewriteRules/i", '', (string) $contents );
				aioseo()->htaccess->saveContents( $contents );
			}

			return;
		}

		if ( preg_match( '/All in One SEO Sitemap Rewrite Rules/i', (string) $contents ) || aioseo()->core->cache->get( 'aioseo_sitemap_htaccess_rewrite_rules_add' ) ) {
			return;
		}

		aioseo()->core->cache->update( 'aioseo_sitemap_htaccess_rewrite_rules_add', time(), HOUR_IN_SECONDS );

		$contents .= $rewriteRules;

		aioseo()->htaccess->saveContents( $contents );
	}

	/**
	 * Checks if static sitemap files prevent dynamic sitemap generation.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function detectStatic() {
		$isGeneralSitemapStatic = aioseo()->options->sitemap->general->advancedSettings->enable &&
			in_array( 'staticSitemap', aioseo()->internalOptions->internal->deprecatedOptions, true ) &&
			! aioseo()->options->deprecated->sitemap->general->advancedSettings->dynamic;

		if ( $isGeneralSitemapStatic ) {
			Models\Notification::deleteNotificationByName( 'sitemap-static-files' );

			return;
		}

		require_once ABSPATH . 'wp-admin/includes/file.php';
		$files = list_files( get_home_path(), 1 );
		if ( ! count( $files ) ) {
			return;
		}

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

		$this->maybeShowStaticSitemapNotification( $detectedFiles );
	}

	/**
	 * If there are files, show a notice, otherwise delete it.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $detectedFiles An array of detected files.
	 * @return void
	 */
	protected function maybeShowStaticSitemapNotification( $detectedFiles ) {
		if ( ! count( $detectedFiles ) ) {
			Models\Notification::deleteNotificationByName( 'sitemap-static-files' );

			return;
		}

		$notification = Models\Notification::getNotificationByName( 'sitemap-static-files' );
		if ( $notification->notification_name ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'sitemap-static-files',
			'title'             => __( 'Static sitemap files detected', 'all-in-one-seo-pack' ),
			'content'           => sprintf(
				// Translators: 1 - The plugin short name ("AIOSEO"), 2 - Same as previous.
				__( '%1$s has detected static sitemap files in the root folder of your WordPress installation.
				As long as these files are present, %2$s is not able to dynamically generate your sitemap.', 'all-in-one-seo-pack' ),
				AIOSEO_PLUGIN_SHORT_NAME,
				AIOSEO_PLUGIN_SHORT_NAME
			),
			'type'              => 'error',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Delete Static Files', 'all-in-one-seo-pack' ),
			'button1_action'    => 'http://action#sitemap/delete-static-files',
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Regenerates the static sitemap files when a post is updated.
	 *
	 * @since 4.0.0
	 *
	 * @param  integer $postId The post ID.
	 * @return void
	 */
	public function regenerateOnUpdate( $postId ) {
		if ( aioseo()->helpers->isValidPost( $postId ) ) {
			$this->scheduleRegeneration();
		}
	}

	/**
	 * Schedules an action to regenerate the static sitemap files.
	 *
	 * @since 4.0.5
	 *
	 * @return void
	 */
	public function scheduleRegeneration() {
		try {
			if (
				! aioseo()->options->deprecated->sitemap->general->advancedSettings->dynamic &&
				! as_next_scheduled_action( 'aioseo_static_sitemap_regeneration' )
			) {
				as_schedule_single_action( time() + 60, 'aioseo_static_sitemap_regeneration', [], 'aioseo' );
			}
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Regenerates the static sitemap files.
	 *
	 * @since 4.0.5
	 *
	 * @return void
	 */
	public function regenerateStaticSitemap() {
		aioseo()->sitemap->file->generate();
	}

	/**
	 * Generates the requested sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function generate() {
		if ( empty( $this->type ) ) {
			return;
		}

		// This is a hack to prevent WordPress from running it's default stuff during our processing.
		global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$wp_query->is_home = false; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		// This prevents the sitemap from including terms twice when WPML is active.
		if ( class_exists( 'SitePress' ) ) {
			global $sitepress_settings; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			// Before building the sitemap make sure links aren't translated.
			// The setting should not be updated in the DB.
			$sitepress_settings['auto_adjust_ids'] = 0; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		}

		// If requested sitemap should be static and doesn't exist, then generate it.
		// We'll then serve it dynamically for the current request so that we don't serve a blank page.
		$this->doesFileExist();

		$options = aioseo()->options->noConflict();
		if ( ! $options->sitemap->{aioseo()->sitemap->type}->enable ) {
			aioseo()->helpers->notFoundPage();

			return;
		}

		$entries = aioseo()->sitemap->content->get();
		$total   = aioseo()->sitemap->content->getTotal();
		if ( ! $entries ) {
			$addonsEntries = aioseo()->addons->doAddonFunction( 'content', 'get' );
			$addonTotals   = aioseo()->addons->doAddonFunction( 'content', 'getTotal' );
			foreach ( $addonsEntries as $addonSlug => $addonEntries ) {
				if ( ! empty( $addonEntries ) ) {
					$entries = $addonEntries;
					$total   = ! empty( $addonTotals[ $addonSlug ] ) ? $addonTotals[ $addonSlug ] : count( $entries );
					break;
				}
			}
		}

		if ( 0 === $total && empty( $entries ) ) {
			status_header( 404 );
		}

		$this->xsl->saveXslData(
			aioseo()->sitemap->requestParser->slug,
			$entries,
			$total
		);

		$this->headers();
		aioseo()->sitemap->output->output( $entries );
		aioseo()->addons->doAddonFunction( 'output', 'output', [ $entries ] );

		exit;
	}

	/**
	 * Checks if static file should be served and generates it if it doesn't exist.
	 *
	 * This essentially acts as a safety net in case a file doesn't exist yet or has been deleted.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function doesFileExist() {
		aioseo()->addons->doAddonFunction( 'sitemap', 'doesFileExist' );

		if (
			'general' !== $this->type ||
			! aioseo()->options->sitemap->general->advancedSettings->enable ||
			! in_array( 'staticSitemap', aioseo()->internalOptions->internal->deprecatedOptions, true ) ||
			aioseo()->options->sitemap->general->advancedSettings->dynamic
		) {
			return;
		}

		require_once ABSPATH . 'wp-admin/includes/file.php';
		if ( isset( $_SERVER['REQUEST_URI'] ) && ! aioseo()->core->fs->exists( get_home_path() . sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ) {
			$this->scheduleRegeneration();
		}
	}

	/**
	 * Sets the HTTP headers for the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function headers() {
		$charset = aioseo()->helpers->getCharset();
		header( "Content-Type: text/xml; charset=$charset", true );
		header( 'X-Robots-Tag: noindex, follow', true );
	}

	/**
	 * Registers an active sitemap addon and its classes.
	 * NOTE: This is deprecated and only there for users who already were using the previous sitemap addons version.
	 *
	 * @final 4.2.7
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addAddon() {}
}Common/Sitemap/Priority.php000066600000016577151135505570011763 0ustar00<?php
namespace AIOSEO\Plugin\Common\Sitemap;

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

/**
 * Determines the priority/frequency.
 *
 * @since 4.0.0
 */
class Priority {
	/**
	 * Whether the advanced settings are enabled for the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @var boolean
	 */
	private static $advanced;

	/**
	 * The global priority for the page type.
	 *
	 * @since 4.0.0
	 *
	 * @var boolean
	 */
	private static $globalPriority = [];

	/**
	 * The global frequency for the page type.
	 *
	 * @since 4.0.0
	 *
	 * @var boolean
	 */
	private static $globalFrequency = [];

	/**
	 * Whether or not we have grouped our settings.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private static $grouped = [];

	/**
	 * The current object type priority.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private static $objectTypePriority = [];

	/**
	 * The current object type frequency.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private static $objectTypeFrequency = [];

	/**
	 * Returns the sitemap priority for a given page.
	 *
	 * @since 4.0.0
	 *
	 * @param  string         $pageType   The type of page (e.g. homepage, blog, post, taxonomies, etc.).
	 * @param  \stdClass|bool $object     The post/term object (optional).
	 * @param  string         $objectType The post/term object type (optional).
	 * @return float                      The priority.
	 */
	public function priority( $pageType, $object = false, $objectType = '' ) {
		// Store setting values in static properties so that we can cache them.
		// Otherwise this has a significant impact on the load time of the sitemap.
		if ( ! self::$advanced ) {
			self::$advanced = aioseo()->options->sitemap->general->advancedSettings->enable;
		}

		if ( ! isset( self::$globalPriority[ $pageType . $objectType ] ) ) {
			$options = aioseo()->options->noConflict();

			$pageTypeConditional = 'date' === $pageType ? 'archive' : $pageType;
			self::$globalPriority[ $pageType . $objectType ] = self::$advanced && $options->sitemap->general->advancedSettings->priority->has( $pageTypeConditional )
				? json_decode( $options->sitemap->general->advancedSettings->priority->$pageTypeConditional->priority )
				: false;
		}

		if ( ! isset( self::$grouped[ $pageType . $objectType ] ) ) {
			$options = aioseo()->options->noConflict();
			self::$grouped[ $pageType . $objectType ] = self::$advanced &&
				$options->sitemap->general->advancedSettings->priority->has( $pageType ) &&
				$options->sitemap->general->advancedSettings->priority->$pageType->has( 'grouped' )
					? $options->sitemap->general->advancedSettings->priority->$pageType->grouped
					: true;
		}

		if ( empty( self::$grouped[ $pageType . $objectType ] ) && self::$advanced ) {
			if ( ! isset( self::$objectTypePriority[ $pageType . $objectType ] ) ) {
				$dynamicOptions = aioseo()->dynamicOptions->noConflict();
				self::$objectTypePriority[ $pageType . $objectType ] = $dynamicOptions->sitemap->priority->has( $pageType ) && $dynamicOptions->sitemap->priority->$pageType->has( $objectType )
					? json_decode( $dynamicOptions->sitemap->priority->$pageType->$objectType->priority )
					: false;
			}
		}

		$priority = $this->defaultPriority( $pageType );
		if ( self::$globalPriority[ $pageType . $objectType ] ) {
			$defaultValue = ! self::$grouped[ $pageType . $objectType ] &&
				self::$advanced &&
				! empty( self::$objectTypePriority[ $pageType . $objectType ] )
					? self::$objectTypePriority[ $pageType . $objectType ]
					: self::$globalPriority[ $pageType . $objectType ];
			$priority     = 'default' === $defaultValue->value ? $priority : $defaultValue->value;
		}

		return $priority;
	}

	/**
	 * Returns the sitemap frequency for a given page.
	 *
	 * @since 4.0.0
	 *
	 * @param  string         $pageType   The type of page (e.g. homepage, blog, post, taxonomies, etc.).
	 * @param  \stdClass|bool $object     The post/term object (optional).
	 * @param  string         $objectType The post/term object type (optional).
	 * @return float                      The frequency.
	 */
	public function frequency( $pageType, $object = false, $objectType = '' ) {
		// Store setting values in static properties so that we can cache them.
		// Otherwise this has a significant impact on the load time of the sitemap.
		if ( ! self::$advanced ) {
			self::$advanced = aioseo()->options->sitemap->general->advancedSettings->enable;
		}
		if ( ! isset( self::$globalFrequency[ $pageType . $objectType ] ) ) {
			$options = aioseo()->options->noConflict();
			$pageTypeConditional = 'date' === $pageType ? 'archive' : $pageType;
			self::$globalFrequency[ $pageType . $objectType ] = self::$advanced && $options->sitemap->general->advancedSettings->priority->has( $pageTypeConditional )
				? json_decode( $options->sitemap->general->advancedSettings->priority->$pageTypeConditional->frequency )
				: false;
		}

		if ( ! isset( self::$grouped[ $pageType . $objectType ] ) ) {
			$options = aioseo()->options->noConflict();
			self::$grouped[ $pageType . $objectType ] = self::$advanced &&
				$options->sitemap->general->advancedSettings->priority->has( $pageType ) &&
				$options->sitemap->general->advancedSettings->priority->$pageType->has( 'grouped' )
					? $options->sitemap->general->advancedSettings->priority->$pageType->grouped
					: true;
		}

		if ( empty( self::$grouped[ $pageType . $objectType ] ) && self::$advanced ) {
			if ( ! isset( self::$objectTypeFrequency[ $pageType . $objectType ] ) ) {
				$dynamicOptions = aioseo()->dynamicOptions->noConflict();

				self::$objectTypeFrequency[ $pageType . $objectType ] = $dynamicOptions->sitemap->priority->has( $pageType ) && $dynamicOptions->sitemap->priority->$pageType->has( $objectType )
					? json_decode( $dynamicOptions->sitemap->priority->$pageType->$objectType->frequency )
					: false;
			}
		}

		$frequency = $this->defaultFrequency( $pageType );
		if ( self::$globalFrequency[ $pageType . $objectType ] ) {
			$defaultValue = ! self::$grouped[ $pageType . $objectType ] &&
				self::$advanced &&
				! empty( self::$objectTypeFrequency[ $pageType . $objectType ] )
					? self::$objectTypeFrequency[ $pageType . $objectType ]
					: self::$globalFrequency[ $pageType . $objectType ];
			$frequency    = 'default' === $defaultValue->value ? $frequency : $defaultValue->value;
		}

		return $frequency;
	}

	/**
	 * Returns the default priority for the page.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $pageType The type of page.
	 * @return float            The default priority.
	 */
	private function defaultPriority( $pageType ) {
		$defaults = [
			'homePage'   => 1.0,
			'blog'       => 0.9,
			'sitemap'    => 0.8,
			'postTypes'  => 0.7,
			'archive'    => 0.5,
			'author'     => 0.3,
			'taxonomies' => 0.3,
			'other'      => 0.5
		];

		if ( array_key_exists( $pageType, $defaults ) ) {
			return $defaults[ $pageType ];
		}

		return $defaults['other'];
	}

	/**
	 * Returns the default frequency for the page.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $pageType The type of page.
	 * @return float            The default frequency.
	 */
	private function defaultFrequency( $pageType ) {
		$defaults = [
			'homePage'   => 'always',
			'sitemap'    => 'hourly',
			'blog'       => 'daily',
			'postTypes'  => 'weekly',
			'author'     => 'weekly',
			'archive'    => 'monthly',
			'taxonomies' => 'monthly',
			'other'      => 'weekly'
		];

		if ( array_key_exists( $pageType, $defaults ) ) {
			return $defaults[ $pageType ];
		}

		return $defaults['other'];
	}
}Common/Options/Cache.php000066600000003103151135505570011153 0ustar00<?php
namespace AIOSEO\Plugin\Common\Options;

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

/**
 * Class that holds all the cache for the AIOSEO options.
 *
 * @since 4.1.4
 */
class Cache {
	/**
	 * The DB options cache.
	 *
	 * @since 4.1.4
	 *
	 * @var array
	 */
	private static $db = [];

	/**
	 * The options cache.
	 *
	 * @since 4.1.4
	 *
	 * @var array
	 */
	private static $options = [];

	/**
	 * Sets the cache for the DB option.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $name  The cache name.
	 * @param  array  $value The value.
	 * @return void
	 */
	public function setDb( $name, $value ) {
		self::$db[ $name ] = $value;
	}

	/**
	 * Gets the cache for the DB option.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $name The cache name.
	 * @return array        The data from the cache.
	 */
	public function getDb( $name ) {
		return ! empty( self::$db[ $name ] ) ? self::$db[ $name ] : [];
	}

	/**
	 * Sets the cache for the options.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $name  The cache name.
	 * @param  array  $value The value.
	 * @return void
	 */
	public function setOptions( $name, $value ) {
		self::$options[ $name ] = $value;
	}

	/**
	 * Gets the cache for the options.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $name The cache name.
	 * @return array        The data from the cache.
	 */
	public function getOptions( $name ) {
		return ! empty( self::$options[ $name ] ) ? self::$options[ $name ] : [];
	}

	/**
	 * Resets the DB cache.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function resetDb() {
		self::$db = [];
	}
}Common/Options/NetworkOptions.php000066600000003622151135505570013163 0ustar00<?php
namespace AIOSEO\Plugin\Common\Options;

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

use AIOSEO\Plugin\Common\Traits;
use AIOSEO\Plugin\Common\Utils;

/**
 * Class that holds all network options for AIOSEO.
 *
 * @since 4.2.5
 */
class NetworkOptions {
	use Traits\Options;
	use Traits\NetworkOptions;

	/**
	 * Holds the helpers class.
	 *
	 * @since 4.2.5
	 *
	 * @var Utils\Helpers
	 */
	protected $helpers;

	/**
	 * All the default options.
	 *
	 * @since 4.2.5
	 *
	 * @var array
	 */
	protected $defaults = [
		// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
		'searchAppearance' => [
			'advanced' => [
				'unwantedBots'  => [
					'all'      => [ 'type' => 'boolean', 'default' => false ],
					'settings' => [
						'googleAdsBot'             => [ 'type' => 'boolean', 'default' => false ],
						'openAiGptBot'             => [ 'type' => 'boolean', 'default' => false ],
						'commonCrawlCcBot'         => [ 'type' => 'boolean', 'default' => false ],
						'googleGeminiVertexAiBots' => [ 'type' => 'boolean', 'default' => false ]
					]
				],
				'searchCleanup' => [
					'settings' => [
						'preventCrawling' => [ 'type' => 'boolean', 'default' => false ]
					]
				]
			]
		],
		'tools'            => [
			'robots' => [
				'enable'         => [ 'type' => 'boolean', 'default' => false ],
				'rules'          => [ 'type' => 'array', 'default' => [] ],
				'robotsDetected' => [ 'type' => 'boolean', 'default' => true ],
			]
		]
		// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	];

	/**
	 * The Construct method.
	 *
	 * @since 4.2.5
	 *
	 * @param string $optionsName The options name.
	 */
	public function __construct( $optionsName = 'aioseo_options_network' ) {
		$this->helpers     = new Utils\Helpers();
		$this->optionsName = $optionsName;

		$this->init();

		add_action( 'shutdown', [ $this, 'save' ] );
	}
}Common/Options/Options.php000066600000106145151135505570011615 0ustar00<?php
namespace AIOSEO\Plugin\Common\Options;

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

use AIOSEO\Plugin\Common\Models;
use AIOSEO\Plugin\Common\Traits;

/**
 * Class that holds all options for AIOSEO.
 *
 * @since 4.0.0
 */
class Options {
	use Traits\Options;

	/**
	 * All the default options.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $defaults = [
		// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
		'internal'         => [],
		'webmasterTools'   => [
			'google'                    => [ 'type' => 'string' ],
			'bing'                      => [ 'type' => 'string' ],
			'yandex'                    => [ 'type' => 'string' ],
			'baidu'                     => [ 'type' => 'string' ],
			'pinterest'                 => [ 'type' => 'string' ],
			'microsoftClarityProjectId' => [ 'type' => 'string' ],
			'norton'                    => [ 'type' => 'string' ],
			'miscellaneousVerification' => [ 'type' => 'html' ]
		],
		'breadcrumbs'      => [
			'separator'             => [ 'type' => 'string', 'default' => '&raquo;' ],
			'homepageLink'          => [ 'type' => 'boolean', 'default' => true ],
			'homepageLabel'         => [ 'type' => 'string', 'default' => 'Home' ],
			'breadcrumbPrefix'      => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
			'archiveFormat'         => [ 'type' => 'string', 'default' => 'Archives for #breadcrumb_archive_post_type_name', 'localized' => true ],
			'searchResultFormat'    => [ 'type' => 'string', 'default' => 'Search Results for \'#breadcrumb_search_string\'', 'localized' => true ],
			'errorFormat404'        => [ 'type' => 'string', 'default' => '404 - Page Not Found', 'localized' => true ],
			'showCurrentItem'       => [ 'type' => 'boolean', 'default' => true ],
			'linkCurrentItem'       => [ 'type' => 'boolean', 'default' => false ],
			'categoryFullHierarchy' => [ 'type' => 'boolean', 'default' => false ],
			'showBlogHome'          => [ 'type' => 'boolean', 'default' => false ]
		],
		'rssContent'       => [
			'before' => [ 'type' => 'html' ],
			'after'  => [
				'type'    => 'html',
				'default' => <<<TEMPLATE
&lt;p&gt;The post #post_link first appeared on #site_link.&lt;/p&gt;
TEMPLATE
			]
		],
		'advanced'         => [
			'truSeo'           => [ 'type' => 'boolean', 'default' => true ],
			'headlineAnalyzer' => [ 'type' => 'boolean', 'default' => true ],
			'seoAnalysis'      => [ 'type' => 'boolean', 'default' => true ],
			'dashboardWidgets' => [ 'type' => 'array', 'default' => [ 'seoSetup', 'seoOverview', 'seoNews' ] ],
			'announcements'    => [ 'type' => 'boolean', 'default' => true ],
			'postTypes'        => [
				'all'      => [ 'type' => 'boolean', 'default' => true ],
				'included' => [ 'type' => 'array', 'default' => [ 'post', 'page', 'product' ] ],
			],
			'taxonomies'       => [
				'all'      => [ 'type' => 'boolean', 'default' => true ],
				'included' => [ 'type' => 'array', 'default' => [ 'category', 'post_tag', 'product_cat', 'product_tag' ] ],
			],
			'uninstall'        => [ 'type' => 'boolean', 'default' => false ],
			'emailSummary'     => [
				'enable'     => [ 'type' => 'boolean', 'default' => false ],
				'recipients' => [ 'type' => 'array', 'default' => [] ]
			]
		],
		'sitemap'          => [
			'general' => [
				'enable'           => [ 'type' => 'boolean', 'default' => true ],
				'filename'         => [ 'type' => 'string', 'default' => 'sitemap' ],
				'indexes'          => [ 'type' => 'boolean', 'default' => true ],
				'linksPerIndex'    => [ 'type' => 'number', 'default' => 1000 ],
				// @TODO: [V4+] Convert this to the dynamic options like in search appearance so we can have backups when plugins are deactivated.
				'postTypes'        => [
					'all'      => [ 'type' => 'boolean', 'default' => true ],
					'included' => [ 'type' => 'array', 'default' => [ 'post', 'page', 'attachment', 'product' ] ],
				],
				// @TODO: [V4+] Convert this to the dynamic options like in search appearance so we can have backups when plugins are deactivated.
				'taxonomies'       => [
					'all'      => [ 'type' => 'boolean', 'default' => true ],
					'included' => [ 'type' => 'array', 'default' => [ 'category', 'post_tag', 'product_cat', 'product_tag' ] ],
				],
				'author'           => [ 'type' => 'boolean', 'default' => false ],
				'date'             => [ 'type' => 'boolean', 'default' => false ],
				'additionalPages'  => [
					'enable' => [ 'type' => 'boolean', 'default' => false ],
					'pages'  => [ 'type' => 'array', 'default' => [] ]
				],
				'advancedSettings' => [
					'enable'        => [ 'type' => 'boolean', 'default' => false ],
					'excludeImages' => [ 'type' => 'boolean', 'default' => false ],
					'excludePosts'  => [ 'type' => 'array', 'default' => [] ],
					'excludeTerms'  => [ 'type' => 'array', 'default' => [] ],
					'priority'      => [
						'homePage'   => [
							'priority'  => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ],
							'frequency' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ]
						],
						'postTypes'  => [
							'grouped'   => [ 'type' => 'boolean', 'default' => true ],
							'priority'  => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ],
							'frequency' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ]
						],
						'taxonomies' => [
							'grouped'   => [ 'type' => 'boolean', 'default' => true ],
							'priority'  => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ],
							'frequency' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ]
						],
						'archive'    => [
							'priority'  => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ],
							'frequency' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ]
						],
						'author'     => [
							'priority'  => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ],
							'frequency' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ]
						]
					]
				]
			],
			'rss'     => [
				'enable'        => [ 'type' => 'boolean', 'default' => true ],
				'linksPerIndex' => [ 'type' => 'number', 'default' => 50 ],
				// @TODO: [V4+] Convert this to the dynamic options like in search appearance so we can have backups when plugins are deactivated.
				'postTypes'     => [
					'all'      => [ 'type' => 'boolean', 'default' => true ],
					'included' => [ 'type' => 'array', 'default' => [ 'post', 'page', 'product' ] ],
				]
			],
			'html'    => [
				'enable'           => [ 'type' => 'boolean', 'default' => true ],
				'pageUrl'          => [ 'type' => 'string', 'default' => '' ],
				'postTypes'        => [
					'all'      => [ 'type' => 'boolean', 'default' => true ],
					'included' => [ 'type' => 'array', 'default' => [ 'post', 'page', 'product' ] ],
				],
				'taxonomies'       => [
					'all'      => [ 'type' => 'boolean', 'default' => true ],
					'included' => [ 'type' => 'array', 'default' => [ 'category', 'post_tag', 'product_cat', 'product_tag' ] ],
				],
				'sortOrder'        => [ 'type' => 'string', 'default' => 'publish_date' ],
				'sortDirection'    => [ 'type' => 'string', 'default' => 'asc' ],
				'publicationDate'  => [ 'type' => 'boolean', 'default' => true ],
				'compactArchives'  => [ 'type' => 'boolean', 'default' => false ],
				'advancedSettings' => [
					'enable'        => [ 'type' => 'boolean', 'default' => false ],
					'nofollowLinks' => [ 'type' => 'boolean', 'default' => false ],
					'excludePosts'  => [ 'type' => 'array', 'default' => [] ],
					'excludeTerms'  => [ 'type' => 'array', 'default' => [] ]
				]
			],
		],
		'social'           => [
			'profiles' => [
				'sameUsername'   => [
					'enable'   => [ 'type' => 'boolean', 'default' => false ],
					'username' => [ 'type' => 'string' ],
					'included' => [ 'type' => 'array', 'default' => [ 'facebookPageUrl', 'twitterUrl', 'tiktokUrl', 'pinterestUrl', 'instagramUrl', 'youtubeUrl', 'linkedinUrl' ] ]
				],
				'urls'           => [
					'facebookPageUrl' => [ 'type' => 'string' ],
					'twitterUrl'      => [ 'type' => 'string' ],
					'instagramUrl'    => [ 'type' => 'string' ],
					'tiktokUrl'       => [ 'type' => 'string' ],
					'pinterestUrl'    => [ 'type' => 'string' ],
					'youtubeUrl'      => [ 'type' => 'string' ],
					'linkedinUrl'     => [ 'type' => 'string' ],
					'tumblrUrl'       => [ 'type' => 'string' ],
					'yelpPageUrl'     => [ 'type' => 'string' ],
					'soundCloudUrl'   => [ 'type' => 'string' ],
					'wikipediaUrl'    => [ 'type' => 'string' ],
					'myspaceUrl'      => [ 'type' => 'string' ],
					'googlePlacesUrl' => [ 'type' => 'string' ],
					'wordPressUrl'    => [ 'type' => 'string' ],
					'blueskyUrl'      => [ 'type' => 'string' ],
					'threadsUrl'      => [ 'type' => 'string' ]
				],
				'additionalUrls' => [ 'type' => 'string' ]
			],
			'facebook' => [
				'general'  => [
					'enable'                  => [ 'type' => 'boolean', 'default' => true ],
					'defaultImageSourcePosts' => [ 'type' => 'string', 'default' => 'default' ],
					'customFieldImagePosts'   => [ 'type' => 'string' ],
					'defaultImagePosts'       => [ 'type' => 'string', 'default' => '' ],
					'defaultImagePostsWidth'  => [ 'type' => 'number', 'default' => '' ],
					'defaultImagePostsHeight' => [ 'type' => 'number', 'default' => '' ],
					'showAuthor'              => [ 'type' => 'boolean', 'default' => true ],
					'siteName'                => [ 'type' => 'string', 'localized' => true, 'default' => '#site_title #separator_sa #tagline' ]
				],
				'homePage' => [
					'image'       => [ 'type' => 'string', 'default' => '' ],
					'title'       => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
					'description' => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
					'imageWidth'  => [ 'type' => 'number', 'default' => '' ],
					'imageHeight' => [ 'type' => 'number', 'default' => '' ],
					'objectType'  => [ 'type' => 'string', 'default' => 'website' ]
				],
				'advanced' => [
					'enable'              => [ 'type' => 'boolean', 'default' => false ],
					'adminId'             => [ 'type' => 'string', 'default' => '' ],
					'appId'               => [ 'type' => 'string', 'default' => '' ],
					'authorUrl'           => [ 'type' => 'string', 'default' => '' ],
					'generateArticleTags' => [ 'type' => 'boolean', 'default' => false ],
					'useKeywordsInTags'   => [ 'type' => 'boolean', 'default' => true ],
					'useCategoriesInTags' => [ 'type' => 'boolean', 'default' => true ],
					'usePostTagsInTags'   => [ 'type' => 'boolean', 'default' => true ]
				]
			],
			'twitter'  => [
				'general'  => [
					'enable'                  => [ 'type' => 'boolean', 'default' => true ],
					'useOgData'               => [ 'type' => 'boolean', 'default' => true ],
					'defaultCardType'         => [ 'type' => 'string', 'default' => 'summary_large_image' ],
					'defaultImageSourcePosts' => [ 'type' => 'string', 'default' => 'default' ],
					'customFieldImagePosts'   => [ 'type' => 'string' ],
					'defaultImagePosts'       => [ 'type' => 'string', 'default' => '' ],
					'showAuthor'              => [ 'type' => 'boolean', 'default' => true ],
					'additionalData'          => [ 'type' => 'boolean', 'default' => false ]
				],
				'homePage' => [
					'image'       => [ 'type' => 'string', 'default' => '' ],
					'title'       => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
					'description' => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
					'cardType'    => [ 'type' => 'string', 'default' => 'summary' ]
				],
			]
		],
		'searchAppearance' => [
			'global'   => [
				'separator'       => [ 'type' => 'string', 'default' => '&#45;' ],
				'siteTitle'       => [ 'type' => 'string', 'localized' => true, 'default' => '#site_title #separator_sa #tagline' ],
				'metaDescription' => [ 'type' => 'string', 'localized' => true, 'default' => '#tagline' ],
				'keywords'        => [ 'type' => 'string', 'localized' => true ],
				'schema'          => [
					'websiteName'             => [ 'type' => 'string', 'default' => '#site_title' ],
					'websiteAlternateName'    => [ 'type' => 'string' ],
					'siteRepresents'          => [ 'type' => 'string', 'default' => 'organization' ],
					'person'                  => [ 'type' => 'string' ],
					'organizationName'        => [ 'type' => 'string', 'default' => '#site_title' ],
					'organizationDescription' => [ 'type' => 'string', 'default' => '#tagline' ],
					'organizationLogo'        => [ 'type' => 'string' ],
					'personName'              => [ 'type' => 'string' ],
					'personLogo'              => [ 'type' => 'string' ],
					'phone'                   => [ 'type' => 'string' ],
					'email'                   => [ 'type' => 'string' ],
					'foundingDate'            => [ 'type' => 'string' ],
					'numberOfEmployees'       => [
						'isRange' => [ 'type' => 'boolean' ],
						'from'    => [ 'type' => 'number' ],
						'to'      => [ 'type' => 'number' ],
						'number'  => [ 'type' => 'number' ]
					]
				]
			],
			'advanced' => [
				'globalRobotsMeta'             => [
					'default'           => [ 'type' => 'boolean', 'default' => true ],
					'noindex'           => [ 'type' => 'boolean', 'default' => false ],
					'nofollow'          => [ 'type' => 'boolean', 'default' => false ],
					'noindexPaginated'  => [ 'type' => 'boolean', 'default' => true ],
					'nofollowPaginated' => [ 'type' => 'boolean', 'default' => true ],
					'noindexFeed'       => [ 'type' => 'boolean', 'default' => true ],
					'noarchive'         => [ 'type' => 'boolean', 'default' => false ],
					'noimageindex'      => [ 'type' => 'boolean', 'default' => false ],
					'notranslate'       => [ 'type' => 'boolean', 'default' => false ],
					'nosnippet'         => [ 'type' => 'boolean', 'default' => false ],
					'noodp'             => [ 'type' => 'boolean', 'default' => false ],
					'maxSnippet'        => [ 'type' => 'number', 'default' => -1 ],
					'maxVideoPreview'   => [ 'type' => 'number', 'default' => -1 ],
					'maxImagePreview'   => [ 'type' => 'string', 'default' => 'large' ]
				],
				'noIndexEmptyCat'              => [ 'type' => 'boolean', 'default' => true ],
				'removeStopWords'              => [ 'type' => 'boolean', 'default' => false ],
				'useKeywords'                  => [ 'type' => 'boolean', 'default' => false ],
				'keywordsLooking'              => [ 'type' => 'boolean', 'default' => true ],
				'useCategoriesForMetaKeywords' => [ 'type' => 'boolean', 'default' => false ],
				'useTagsForMetaKeywords'       => [ 'type' => 'boolean', 'default' => false ],
				'dynamicallyGenerateKeywords'  => [ 'type' => 'boolean', 'default' => false ],
				'pagedFormat'                  => [ 'type' => 'string', 'default' => '#separator_sa Page #page_number', 'localized' => true ],
				'runShortcodes'                => [ 'type' => 'boolean', 'default' => false ],
				'crawlCleanup'                 => [
					'enable' => [ 'type' => 'boolean', 'default' => false ],
					'feeds'  => [
						'global'         => [ 'type' => 'boolean', 'default' => true ],
						'globalComments' => [ 'type' => 'boolean', 'default' => false ],
						'staticBlogPage' => [ 'type' => 'boolean', 'default' => true ],
						'authors'        => [ 'type' => 'boolean', 'default' => true ],
						'postComments'   => [ 'type' => 'boolean', 'default' => false ],
						'search'         => [ 'type' => 'boolean', 'default' => false ],
						'attachments'    => [ 'type' => 'boolean', 'default' => false ],
						'archives'       => [
							'all'      => [ 'type' => 'boolean', 'default' => false ],
							'included' => [ 'type' => 'array', 'default' => [] ],
						],
						'taxonomies'     => [
							'all'      => [ 'type' => 'boolean', 'default' => false ],
							'included' => [ 'type' => 'array', 'default' => [ 'category' ] ],
						],
						'atom'           => [ 'type' => 'boolean', 'default' => false ],
						'rdf'            => [ 'type' => 'boolean', 'default' => false ],
						'paginated'      => [ 'type' => 'boolean', 'default' => false ]
					]
				],
				'unwantedBots'                 => [
					'all'      => [ 'type' => 'boolean', 'default' => false ],
					'settings' => [
						'googleAdsBot'             => [ 'type' => 'boolean', 'default' => false ],
						'openAiGptBot'             => [ 'type' => 'boolean', 'default' => false ],
						'commonCrawlCcBot'         => [ 'type' => 'boolean', 'default' => false ],
						'googleGeminiVertexAiBots' => [ 'type' => 'boolean', 'default' => false ]
					]
				],
				'searchCleanup'                => [
					'enable'   => [ 'type' => 'boolean', 'default' => false ],
					'settings' => [
						'maxAllowedNumberOfChars' => [ 'type' => 'number', 'default' => 50 ],
						'emojisAndSymbols'        => [ 'type' => 'boolean', 'default' => false ],
						'commonPatterns'          => [ 'type' => 'boolean', 'default' => false ],
						'redirectPrettyUrls'      => [ 'type' => 'boolean', 'default' => false ],
						'preventCrawling'         => [ 'type' => 'boolean', 'default' => false ]
					]
				],
				'blockArgs'                    => [
					'enable'                => [ 'type' => 'boolean', 'default' => false ],
					'optimizeUtmParameters' => [ 'type' => 'boolean', 'default' => false ],
					'logsRetention'         => [ 'type' => 'string', 'default' => '{"label":"1 week","value":"week"}' ]
				],
				'removeCategoryBase'           => [ 'type' => 'boolean', 'default' => false ]
			],
			'archives' => [
				'author' => [
					'show'            => [ 'type' => 'boolean', 'default' => true ],
					'title'           => [ 'type' => 'string', 'localized' => true, 'default' => '#author_name #separator_sa #site_title' ],
					'metaDescription' => [ 'type' => 'string', 'localized' => true, 'default' => '#author_bio' ],
					'advanced'        => [
						'robotsMeta'                => [
							'default'         => [ 'type' => 'boolean', 'default' => true ],
							'noindex'         => [ 'type' => 'boolean', 'default' => false ],
							'nofollow'        => [ 'type' => 'boolean', 'default' => false ],
							'noarchive'       => [ 'type' => 'boolean', 'default' => false ],
							'noimageindex'    => [ 'type' => 'boolean', 'default' => false ],
							'notranslate'     => [ 'type' => 'boolean', 'default' => false ],
							'nosnippet'       => [ 'type' => 'boolean', 'default' => false ],
							'noodp'           => [ 'type' => 'boolean', 'default' => false ],
							'maxSnippet'      => [ 'type' => 'number', 'default' => -1 ],
							'maxVideoPreview' => [ 'type' => 'number', 'default' => -1 ],
							'maxImagePreview' => [ 'type' => 'string', 'default' => 'large' ]
						],
						'showDateInGooglePreview'   => [ 'type' => 'boolean', 'default' => true ],
						'showPostThumbnailInSearch' => [ 'type' => 'boolean', 'default' => true ],
						'showMetaBox'               => [ 'type' => 'boolean', 'default' => true ],
						'keywords'                  => [ 'type' => 'string', 'localized' => true ]
					]
				],
				'date'   => [
					'show'            => [ 'type' => 'boolean', 'default' => true ],
					'title'           => [ 'type' => 'string', 'localized' => true, 'default' => '#archive_date #separator_sa #site_title' ],
					'metaDescription' => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
					'advanced'        => [
						'robotsMeta'                => [
							'default'         => [ 'type' => 'boolean', 'default' => true ],
							'noindex'         => [ 'type' => 'boolean', 'default' => false ],
							'nofollow'        => [ 'type' => 'boolean', 'default' => false ],
							'noarchive'       => [ 'type' => 'boolean', 'default' => false ],
							'noimageindex'    => [ 'type' => 'boolean', 'default' => false ],
							'notranslate'     => [ 'type' => 'boolean', 'default' => false ],
							'nosnippet'       => [ 'type' => 'boolean', 'default' => false ],
							'noodp'           => [ 'type' => 'boolean', 'default' => false ],
							'maxSnippet'      => [ 'type' => 'number', 'default' => -1 ],
							'maxVideoPreview' => [ 'type' => 'number', 'default' => -1 ],
							'maxImagePreview' => [ 'type' => 'string', 'default' => 'large' ]
						],
						'showDateInGooglePreview'   => [ 'type' => 'boolean', 'default' => true ],
						'showPostThumbnailInSearch' => [ 'type' => 'boolean', 'default' => true ],
						'showMetaBox'               => [ 'type' => 'boolean', 'default' => true ],
						'keywords'                  => [ 'type' => 'string', 'localized' => true ]
					]
				],
				'search' => [
					'show'            => [ 'type' => 'boolean', 'default' => false ],
					'title'           => [ 'type' => 'string', 'localized' => true, 'default' => '#search_term #separator_sa #site_title' ],
					'metaDescription' => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
					'advanced'        => [
						'robotsMeta'                => [
							'default'         => [ 'type' => 'boolean', 'default' => false ],
							'noindex'         => [ 'type' => 'boolean', 'default' => true ],
							'nofollow'        => [ 'type' => 'boolean', 'default' => false ],
							'noarchive'       => [ 'type' => 'boolean', 'default' => false ],
							'noimageindex'    => [ 'type' => 'boolean', 'default' => false ],
							'notranslate'     => [ 'type' => 'boolean', 'default' => false ],
							'nosnippet'       => [ 'type' => 'boolean', 'default' => false ],
							'noodp'           => [ 'type' => 'boolean', 'default' => false ],
							'maxSnippet'      => [ 'type' => 'number', 'default' => -1 ],
							'maxVideoPreview' => [ 'type' => 'number', 'default' => -1 ],
							'maxImagePreview' => [ 'type' => 'string', 'default' => 'large' ]
						],
						'showDateInGooglePreview'   => [ 'type' => 'boolean', 'default' => true ],
						'showPostThumbnailInSearch' => [ 'type' => 'boolean', 'default' => true ],
						'showMetaBox'               => [ 'type' => 'boolean', 'default' => true ],
						'keywords'                  => [ 'type' => 'string', 'localized' => true ]
					]
				]
			]
		],
		'searchStatistics' => [
			'postTypes' => [
				'all'      => [ 'type' => 'boolean', 'default' => true ],
				'included' => [ 'type' => 'array', 'default' => [ 'post', 'page' ] ],
			]
		],
		'tools'            => [
			'robots'       => [
				'enable'         => [ 'type' => 'boolean', 'default' => false ],
				'rules'          => [ 'type' => 'array', 'default' => [] ],
				'robotsDetected' => [ 'type' => 'boolean', 'default' => true ],
			],
			'importExport' => [
				'backup' => [
					'lastTime' => [ 'type' => 'string' ],
					'data'     => [ 'type' => 'string' ],
				]
			]
		],
		'deprecated'       => [
			'breadcrumbs'      => [
				'enable' => [ 'type' => 'boolean', 'default' => true ]
			],
			'searchAppearance' => [
				'global'   => [
					'descriptionFormat' => [ 'type' => 'string' ],
					'schema'            => [
						'enableSchemaMarkup' => [ 'type' => 'boolean', 'default' => true ]
					]
				],
				'advanced' => [
					'autogenerateDescriptions'               => [ 'type' => 'boolean', 'default' => true ],
					'runShortcodesInDescription'             => [ 'type' => 'boolean', 'default' => true ], // TODO: Remove this in a future update.
					'useContentForAutogeneratedDescriptions' => [ 'type' => 'boolean', 'default' => false ],
					'excludePosts'                           => [ 'type' => 'array', 'default' => [] ],
					'excludeTerms'                           => [ 'type' => 'array', 'default' => [] ],
					'noPaginationForCanonical'               => [ 'type' => 'boolean', 'default' => true ]
				]
			],
			'sitemap'          => [
				'general' => [
					'advancedSettings' => [
						'dynamic' => [ 'type' => 'boolean', 'default' => true ]
					]
				]
			]
		],
		'writingAssistant' => [
			'postTypes' => [
				'all'      => [ 'type' => 'boolean', 'default' => true ],
				'included' => [ 'type' => 'array', 'default' => [ 'post', 'page' ] ],
			]
		]
		// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	];

	/**
	 * The Construct method.
	 *
	 * @since 4.0.0
	 *
	 * @param string $optionsName An array of options.
	 */
	public function __construct( $optionsName = 'aioseo_options' ) {
		$this->optionsName = $optionsName;

		$this->init();

		add_action( 'shutdown', [ $this, 'save' ] );
	}

	/**
	 * Initializes the options.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function init() {
		$this->setInitialDefaults();
		add_action( 'init', [ $this, 'translateDefaults' ] );

		$this->setDbOptions();

		add_action( 'wp_loaded', [ $this, 'maybeFlushRewriteRules' ] );
	}

	/**
	 * Sets the DB options to the class after merging in new defaults and dropping unknown values.
	 *
	 * @since 4.0.14
	 *
	 * @return void
	 */
	public function setDbOptions() {
		// Refactor options.
		$this->defaultsMerged = array_replace_recursive( $this->defaults, $this->defaultsMerged );

		$dbOptions = $this->getDbOptions( $this->optionsName );

		$options = array_replace_recursive(
			$this->defaultsMerged,
			$this->addValueToValuesArray( $this->defaultsMerged, $dbOptions )
		);

		aioseo()->core->optionsCache->setOptions( $this->optionsName, apply_filters( 'aioseo_get_options', $options ) );

		// Get the localized options.
		$dbOptionsLocalized = get_option( $this->optionsName . '_localized' );
		if ( empty( $dbOptionsLocalized ) ) {
			$dbOptionsLocalized = [];
		}
		$this->localized = $dbOptionsLocalized;
	}

	/**
	 * Sets the initial defaults that can't be defined in the property because of PHP 5.4.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	protected function setInitialDefaults() {
		static $hasInitialized = false;
		if ( $hasInitialized ) {
			return;
		}

		$hasInitialized = true;

		$this->defaults['searchAppearance']['global']['schema']['organizationLogo']['default'] = aioseo()->helpers->getSiteLogoUrl() ? aioseo()->helpers->getSiteLogoUrl() : '';

		$this->defaults['advanced']['emailSummary']['recipients']['default'] = [
			[
				'email'     => get_bloginfo( 'admin_email' ),
				'frequency' => 'monthly',
			]
		];
	}

	/**
	 * For our defaults array, some options need to be translated, so we do that here.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function translateDefaults() {
		static $hasInitialized = false;
		if ( $hasInitialized ) {
			return;
		}

		$hasInitialized = true;

		$default = sprintf( '{"label":"%1$s","value":"default"}', __( 'default', 'all-in-one-seo-pack' ) );
		$this->defaults['sitemap']['general']['advancedSettings']['priority']['homePage']['priority']['default']    = $default;
		$this->defaults['sitemap']['general']['advancedSettings']['priority']['homePage']['frequency']['default']   = $default;
		$this->defaults['sitemap']['general']['advancedSettings']['priority']['postTypes']['priority']['default']   = $default;
		$this->defaults['sitemap']['general']['advancedSettings']['priority']['postTypes']['frequency']['default']  = $default;
		$this->defaults['sitemap']['general']['advancedSettings']['priority']['taxonomies']['priority']['default']  = $default;
		$this->defaults['sitemap']['general']['advancedSettings']['priority']['taxonomies']['frequency']['default'] = $default;

		$this->defaults['breadcrumbs']['homepageLabel']['default']      = __( 'Home', 'all-in-one-seo-pack' );
		$this->defaults['breadcrumbs']['archiveFormat']['default']      = sprintf( '%1$s #breadcrumb_archive_post_type_name', __( 'Archives for', 'all-in-one-seo-pack' ) );
		$this->defaults['breadcrumbs']['searchResultFormat']['default'] = sprintf( '%1$s \'#breadcrumb_search_string\'', __( 'Search Results for', 'all-in-one-seo-pack' ) );
		$this->defaults['breadcrumbs']['errorFormat404']['default']     = __( '404 - Page Not Found', 'all-in-one-seo-pack' );
	}

	/**
	 * Sanitizes, then saves the options to the database.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $options An array of options to sanitize, then save.
	 * @return void
	 */
	public function sanitizeAndSave( $options ) {
		$sitemapOptions                  = ! empty( $options['sitemap'] ) ? $options['sitemap'] : null;
		$oldSitemapOptions               = aioseo()->options->sitemap->all();
		$generalSitemapOptions           = ! empty( $options['sitemap']['general'] ) ? $options['sitemap']['general'] : null;
		$oldGeneralSitemapOptions        = aioseo()->options->sitemap->general->all();
		$deprecatedGeneralSitemapOptions = ! empty( $options['deprecated']['sitemap']['general'] )
				? $options['deprecated']['sitemap']['general']
				: null;
		$oldDeprecatedGeneralSitemapOptions = aioseo()->options->deprecated->sitemap->general->all();
		$oldPhoneOption                     = aioseo()->options->searchAppearance->global->schema->phone;
		$phoneNumberOptions                 = isset( $options['searchAppearance']['global']['schema']['phone'] )
				? $options['searchAppearance']['global']['schema']['phone']
				: null;
		$oldHtmlSitemapUrl = aioseo()->options->sitemap->html->pageUrl;
		$logsRetention     = isset( $options['searchAppearance']['advanced']['blockArgs']['logsRetention'] ) ? $options['searchAppearance']['advanced']['blockArgs']['logsRetention'] : null;
		$oldLogsRetention  = aioseo()->options->searchAppearance->advanced->blockArgs->logsRetention;

		// Remove category base.
		$removeCategoryBase    = isset( $options['searchAppearance']['advanced']['removeCategoryBase'] ) ? $options['searchAppearance']['advanced']['removeCategoryBase'] : null;
		$removeCategoryBaseOld = aioseo()->options->searchAppearance->advanced->removeCategoryBase;

		$options = $this->maybeRemoveUnfilteredHtmlFields( $options );

		$this->init();

		if ( ! is_array( $options ) ) {
			return;
		}

		$this->sanitizeEmailSummary( $options );

		// First, recursively replace the new options into the cached state.
		// It's important we use the helper method since we want to replace populated arrays with empty ones if needed (when a setting was cleared out).
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$dbOptions     = aioseo()->helpers->arrayReplaceRecursive(
			$cachedOptions,
			$this->addValueToValuesArray( $cachedOptions, $options, [], true )
		);

		// Now, we must also intersect both arrays to delete any individual keys that were unset.
		// We must do this because, while arrayReplaceRecursive will update the values for keys or empty them out,
		// it will keys that aren't present in the replacement array unaffected in the target array.
		$dbOptions = aioseo()->helpers->arrayIntersectRecursive(
			$dbOptions,
			$this->addValueToValuesArray( $cachedOptions, $options, [], true ),
			'value'
		);

		if ( isset( $options['social']['profiles']['additionalUrls'] ) ) {
			$dbOptions['social']['profiles']['additionalUrls'] = preg_replace( '/\h/', "\n", (string) $options['social']['profiles']['additionalUrls'] );
		}

		$newOptions = ! empty( $options['sitemap']['html'] ) ? $options['sitemap']['html'] : null;
		if ( ! empty( $newOptions ) && aioseo()->options->sitemap->html->enable ) {
			$newOptions = ! empty( $options['sitemap']['html'] ) ? $options['sitemap']['html'] : null;

			$pageUrl = wp_parse_url( $newOptions['pageUrl'] );
			$path    = ! empty( $pageUrl['path'] ) ? untrailingslashit( $pageUrl['path'] ) : '';
			if ( $path ) {
				$existingPage = get_page_by_path( $path, OBJECT );
				if ( is_object( $existingPage ) ) {
					// If the page exists, don't override the previous URL.
					$options['sitemap']['html']['pageUrl'] = $oldHtmlSitemapUrl;
				}
			}
		}

		// Update the cache state.
		aioseo()->core->optionsCache->setOptions( $this->optionsName, $dbOptions );

		// Update localized options.
		update_option( $this->optionsName . '_localized', $this->localized );

		// Finally, save the new values to the DB.
		$this->save( true );

		// If phone settings have changed, let's see if we need to dump the phone number notice.
		if (
			$phoneNumberOptions &&
			$phoneNumberOptions !== $oldPhoneOption
		) {
			$notification = Models\Notification::getNotificationByName( 'v3-migration-schema-number' );
			if ( $notification->exists() ) {
				Models\Notification::deleteNotificationByName( 'v3-migration-schema-number' );
			}
		}

		// If sitemap settings were changed, static files need to be regenerated.
		if (
			! empty( $deprecatedGeneralSitemapOptions ) &&
			! empty( $generalSitemapOptions )
		) {
			if (
				(
					aioseo()->helpers->arraysDifferent( $oldGeneralSitemapOptions, $generalSitemapOptions ) ||
					aioseo()->helpers->arraysDifferent( $oldDeprecatedGeneralSitemapOptions, $deprecatedGeneralSitemapOptions )
				) &&
				$generalSitemapOptions['advancedSettings']['enable'] &&
				! $deprecatedGeneralSitemapOptions['advancedSettings']['dynamic']
			) {
				aioseo()->sitemap->scheduleRegeneration();
			}
		}

		// Add or remove schedule for clearing crawl cleanup logs.
		if ( ! empty( $logsRetention ) && $oldLogsRetention !== $logsRetention ) {
			aioseo()->crawlCleanup->scheduleClearingLogs();
		}

		if ( ! empty( $sitemapOptions ) ) {
			aioseo()->searchStatistics->sitemap->maybeSync( $oldSitemapOptions, $sitemapOptions );
		}

		if (
			null !== $removeCategoryBase &&
			$removeCategoryBase !== $removeCategoryBaseOld
		) {
			aioseo()->options->flushRewriteRules();
		}

		// This is required in order for the Pro options to be refreshed before they save data again.
		$this->refresh();
	}

	/**
	 * Sanitizes the `emailSummary` option.
	 *
	 * @since 4.7.2
	 *
	 * @param  array $options All options, passed by reference.
	 * @return void
	 */
	private function sanitizeEmailSummary( &$options ) {
		foreach ( ( $options['advanced']['emailSummary']['recipients'] ?? [] ) as $k => &$recipient ) {
			$recipient['email'] = is_email( $recipient['email'] );

			// Remove empty emails.
			if ( empty( $recipient['email'] ) ) {
				unset( $options['advanced']['emailSummary']['recipients'][ $k ] );

				continue;
			}

			// Remove duplicate emails with the same frequency.
			foreach ( $options['advanced']['emailSummary']['recipients'] as $k2 => $recipient2 ) {
				if (
					$k !== $k2 &&
					$recipient['email'] === $recipient2['email'] &&
					$recipient['frequency'] === $recipient2['frequency']
				) {
					unset( $options['advanced']['emailSummary']['recipients'][ $k ] );

					break;
				}
			}
		}
	}

	/**
	 * If the user does not have access to unfiltered HTML, we need to remove them from saving.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $options An array of options.
	 * @return array          An array of options.
	 */
	private function maybeRemoveUnfilteredHtmlFields( $options ) {
		if ( current_user_can( 'unfiltered_html' ) ) {
			return $options;
		}

		if (
			! empty( $options['webmasterTools'] ) &&
			isset( $options['webmasterTools']['miscellaneousVerification'] )
		) {
			unset( $options['webmasterTools']['miscellaneousVerification'] );
		}

		if (
			! empty( $options['rssContent'] ) &&
			isset( $options['rssContent']['before'] )
		) {
			unset( $options['rssContent']['before'] );
		}

		if (
			! empty( $options['rssContent'] ) &&
			isset( $options['rssContent']['after'] )
		) {
			unset( $options['rssContent']['after'] );
		}

		return $options;
	}

	/**
	 * Indicate we need to flush rewrite rules on next load.
	 *
	 * @since 4.0.17
	 *
	 * @return void
	 */
	public function flushRewriteRules() {
		update_option( 'aioseo_flush_rewrite_rules_flag', true );
	}

	/**
	 * Flush rewrite rules if needed.
	 *
	 * @since 4.0.17
	 *
	 * @return void
	 */
	public function maybeFlushRewriteRules() {
		if ( get_option( 'aioseo_flush_rewrite_rules_flag' ) ) {
			flush_rewrite_rules();
			delete_option( 'aioseo_flush_rewrite_rules_flag' );
		}
	}
}Common/Options/DynamicOptions.php000066600000026113151135505570013116 0ustar00<?php
namespace AIOSEO\Plugin\Common\Options;

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

use AIOSEO\Plugin\Common\Traits;

/**
 * Handles the dynamic options.
 *
 * @since 4.1.4
 */
class DynamicOptions {
	use Traits\Options;

	/**
	 * The default options.
	 *
	 * @since 4.1.4
	 *
	 * @var array
	 */
	protected $defaults = [
		// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
		'sitemap'          => [
			'priority' => [
				'postTypes'  => [],
				'taxonomies' => []
			]
		],
		'social'           => [
			'facebook' => [
				'general' => [
					'postTypes' => []
				]
			]
		],
		'searchAppearance' => [
			'postTypes'  => [],
			'taxonomies' => [],
			'archives'   => []
		]
		// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 *
	 * @param string $optionsName The options name.
	 */
	public function __construct( $optionsName = 'aioseo_options_dynamic' ) {
		$this->optionsName = $optionsName;

		// Load defaults in case this is a complete fresh install.
		$this->init();

		add_action( 'shutdown', [ $this, 'save' ] );
	}

	/**
	 * Initializes the options.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	protected function init() {
		$this->addDynamicDefaults();
		$this->setDbOptions();
	}

	/**
	 * Sets the DB options.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	protected function setDbOptions() {
		$dbOptions = $this->getDbOptions( $this->optionsName );

		// Refactor options.
		$this->defaultsMerged = array_replace_recursive( $this->defaults, $this->defaultsMerged );

		$dbOptions = array_replace_recursive(
			$this->defaultsMerged,
			$this->addValueToValuesArray( $this->defaultsMerged, $dbOptions )
		);

		aioseo()->core->optionsCache->setOptions( $this->optionsName, $dbOptions );

		// Get the localized options.
		$dbOptionsLocalized = get_option( $this->optionsName . '_localized' );
		if ( empty( $dbOptionsLocalized ) ) {
			$dbOptionsLocalized = [];
		}
		$this->localized = $dbOptionsLocalized;
	}

	/**
	 * Sanitizes, then saves the options to the database.
	 *
	 * @since 4.1.4
	 *
	 * @param  array $options An array of options to sanitize, then save.
	 * @return void
	 */
	public function sanitizeAndSave( $options ) {
		if ( ! is_array( $options ) ) {
			return;
		}

		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );

		aioseo()->dynamicBackup->maybeBackup( $cachedOptions );

		// First, recursively replace the new options into the cached state.
		// It's important we use the helper method since we want to replace populated arrays with empty ones if needed (when a setting was cleared out).
		$dbOptions = aioseo()->helpers->arrayReplaceRecursive(
			$cachedOptions,
			$this->addValueToValuesArray( $cachedOptions, $options, [], true )
		);

		// Now, we must also intersect both arrays to delete any individual keys that were unset.
		// We must do this because, while arrayReplaceRecursive will update the values for keys or empty them out,
		// it will keys that aren't present in the replacement array unaffected in the target array.
		$dbOptions = aioseo()->helpers->arrayIntersectRecursive(
			$dbOptions,
			$this->addValueToValuesArray( $cachedOptions, $options, [], true ),
			'value'
		);

		// Update the cache state.
		aioseo()->core->optionsCache->setOptions( $this->optionsName, $dbOptions );

		// Update localized options.
		update_option( $this->optionsName . '_localized', $this->localized );

		// Finally, save the new values to the DB.
		$this->save( true );
	}

	/**
	 * Adds some defaults that are dynamically generated.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function addDynamicDefaults() {
		$this->addDynamicPostTypeDefaults();
		$this->addDynamicTaxonomyDefaults();
		$this->addDynamicArchiveDefaults();
	}

	/**
	 * Adds the dynamic defaults for the public post types.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	protected function addDynamicPostTypeDefaults() {
		$postTypes = aioseo()->helpers->getPublicPostTypes( false, false, false, [ 'include' => [ 'buddypress' ] ] );
		foreach ( $postTypes as $postType ) {
			if ( 'type' === $postType['name'] ) {
				$postType['name'] = '_aioseo_type';
			}

			$defaultTitle = '#post_title #separator_sa #site_title';
			if ( ! empty( $postType['defaultTitle'] ) ) {
				$defaultTitle = $postType['defaultTitle'];
			}
			$defaultDescription = ! empty( $postType['supports']['excerpt'] ) ? '#post_excerpt' : '#post_content';
			if ( ! empty( $postType['defaultDescription'] ) ) {
				$defaultDescription = $postType['defaultDescription'];
			}
			$defaultSchemaType  = 'WebPage';
			$defaultWebPageType = 'WebPage';
			$defaultArticleType = 'BlogPosting';

			switch ( $postType['name'] ) {
				case 'post':
					$defaultSchemaType = 'Article';
					break;
				case 'attachment':
					$defaultDescription = '#attachment_caption';
					$defaultSchemaType  = 'ItemPage';
					$defaultWebPageType = 'ItemPage';
					break;
				case 'product':
					$defaultSchemaType  = 'WebPage';
					$defaultWebPageType = 'ItemPage';
					break;
				case 'news':
					$defaultArticleType = 'NewsArticle';
					break;
				case 'web-story':
					$defaultWebPageType = 'WebPage';
					$defaultSchemaType  = 'WebPage';
					break;
				default:
					break;
			}

			$defaultOptions = array_replace_recursive(
				$this->getDefaultSearchAppearanceOptions(),
				[
					'title'           => [
						'type'      => 'string',
						'localized' => true,
						'default'   => $defaultTitle
					],
					'metaDescription' => [
						'type'      => 'string',
						'localized' => true,
						'default'   => $defaultDescription
					],
					'schemaType'      => [
						'type'    => 'string',
						'default' => $defaultSchemaType
					],
					'webPageType'     => [
						'type'    => 'string',
						'default' => $defaultWebPageType
					],
					'articleType'     => [
						'type'    => 'string',
						'default' => $defaultArticleType
					],
					'customFields'    => [ 'type' => 'html' ],
					'advanced'        => [
						'bulkEditing' => [
							'type'    => 'string',
							'default' => 'enabled'
						]
					]
				]
			);

			if ( 'attachment' === $postType['name'] ) {
				$defaultOptions['redirectAttachmentUrls'] = [
					'type'    => 'string',
					'default' => 'attachment'
				];
			}

			$this->defaults['searchAppearance']['postTypes'][ $postType['name'] ] = $defaultOptions;
			$this->setDynamicSocialOptions( 'postTypes', $postType['name'] );
			$this->setDynamicSitemapOptions( 'postTypes', $postType['name'] );
		}
	}

	/**
	 * Adds the dynamic defaults for the public taxonomies.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	protected function addDynamicTaxonomyDefaults() {
		$taxonomies = aioseo()->helpers->getPublicTaxonomies();
		foreach ( $taxonomies as $taxonomy ) {
			if ( 'type' === $taxonomy['name'] ) {
				$taxonomy['name'] = '_aioseo_type';
			}

			$defaultOptions = array_replace_recursive(
				$this->getDefaultSearchAppearanceOptions(),
				[
					'title'           => [
						'type'      => 'string',
						'localized' => true,
						'default'   => '#taxonomy_title #separator_sa #site_title'
					],
					'metaDescription' => [
						'type'      => 'string',
						'localized' => true,
						'default'   => '#taxonomy_description'
					],
				]
			);

			$this->setDynamicSitemapOptions( 'taxonomies', $taxonomy['name'] );

			$this->defaults['searchAppearance']['taxonomies'][ $taxonomy['name'] ] = $defaultOptions;
		}
	}

	/**
	 * Adds the dynamic defaults for the archive pages.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	protected function addDynamicArchiveDefaults() {
		$postTypes = aioseo()->helpers->getPublicPostTypes( false, true, false, [ 'include' => [ 'buddypress' ] ] );
		foreach ( $postTypes as $postType ) {
			if ( 'type' === $postType['name'] ) {
				$postType['name'] = '_aioseo_type';
			}

			$defaultOptions = array_replace_recursive(
				$this->getDefaultSearchAppearanceOptions(),
				[
					'title'           => [
						'type'      => 'string',
						'localized' => true,
						'default'   => '#archive_title #separator_sa #site_title'
					],
					'metaDescription' => [
						'type'      => 'string',
						'localized' => true,
						'default'   => ''
					],
					'customFields'    => [ 'type' => 'html' ],
					'advanced'        => [
						'keywords' => [
							'type'      => 'string',
							'localized' => true
						]
					]
				]
			);

			$this->defaults['searchAppearance']['archives'][ $postType['name'] ] = $defaultOptions;
		}
	}

	/**
	 * Returns the search appearance options for dynamic objects.
	 *
	 * @since 4.1.4
	 *
	 * @return array The default options.
	 */
	protected function getDefaultSearchAppearanceOptions() {
		return [ // phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
			'show'     => [ 'type' => 'boolean', 'default' => true ],
			'advanced' => [
				'robotsMeta'                => [
					'default'         => [ 'type' => 'boolean', 'default' => true ],
					'noindex'         => [ 'type' => 'boolean', 'default' => false ],
					'nofollow'        => [ 'type' => 'boolean', 'default' => false ],
					'noarchive'       => [ 'type' => 'boolean', 'default' => false ],
					'noimageindex'    => [ 'type' => 'boolean', 'default' => false ],
					'notranslate'     => [ 'type' => 'boolean', 'default' => false ],
					'nosnippet'       => [ 'type' => 'boolean', 'default' => false ],
					'noodp'           => [ 'type' => 'boolean', 'default' => false ],
					'maxSnippet'      => [ 'type' => 'number', 'default' => -1 ],
					'maxVideoPreview' => [ 'type' => 'number', 'default' => -1 ],
					'maxImagePreview' => [ 'type' => 'string', 'default' => 'large' ]
				],
				'showDateInGooglePreview'   => [ 'type' => 'boolean', 'default' => true ],
				'showPostThumbnailInSearch' => [ 'type' => 'boolean', 'default' => true ],
				'showMetaBox'               => [ 'type' => 'boolean', 'default' => true ]
			]
		]; // phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	}

	/**
	 * Sets the dynamic social settings for a given post type or taxonomy.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $objectType Whether the object belongs to the dynamic "postTypes" or "taxonomies".
	 * @param  string $objectName The object name.
	 * @return void
	 */
	protected function setDynamicSocialOptions( $objectType, $objectName ) {
		$defaultOptions = [
			'objectType' => [
				'type'    => 'string',
				'default' => 'article'
			]
		];

		$this->defaults['social']['facebook']['general'][ $objectType ][ $objectName ] = $defaultOptions;
	}

	/**
	 * Sets the dynamic sitemap settings for a given post type or taxonomy.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $objectType Whether the object belongs to the dynamic "postTypes" or "taxonomies".
	 * @param  string $objectName The object name.
	 * @return void
	 */
	protected function setDynamicSitemapOptions( $objectType, $objectName ) {
		$this->defaults['sitemap']['priority'][ $objectType ][ $objectName ] = [
			'priority'  => [
				'type'    => 'string',
				'default' => '{"label":"default","value":"default"}'
			],
			'frequency' => [
				'type'    => 'string',
				'default' => '{"label":"default","value":"default"}'
			]
		];
	}
}Common/Options/DynamicBackup.php000066600000021020151135505570012660 0ustar00<?php
namespace AIOSEO\Plugin\Common\Options;

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

/**
 * Handles the dynamic backup.
 *
 * @since 4.1.3
 */
class DynamicBackup {
	/**
	 * A the name of the option to save dynamic backups to.
	 *
	 * @since 4.1.3
	 *
	 * @var string
	 */
	protected $optionsName = 'aioseo_dynamic_settings_backup';

	/**
	 * The dynamic backup.
	 *
	 * @since 4.1.3
	 *
	 * @var array
	 */
	protected $backup = [];

	/**
	 * Whether the backup should be updated.
	 *
	 * @since 4.1.3
	 *
	 * @var boolean
	 */
	protected $shouldBackup = false;

	/**
	 * The option defaults.
	 *
	 * @since 4.1.3
	 *
	 * @var array
	 */
	protected $defaultOptions = [];

	/**
	 * The public post types.
	 *
	 * @since 4.1.5
	 *
	 * @var array
	 */
	protected $postTypes = [];

	/**
	 * The public taxonomies.
	 *
	 * @since 4.1.5
	 *
	 * @var array
	 */
	protected $taxonomies = [];

	/**
	 * The public archives.
	 *
	 * @since 4.1.5
	 *
	 * @var array
	 */
	protected $archives = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.3
	 */
	public function __construct() {
		add_action( 'wp_loaded', [ $this, 'init' ], 5000 );
		add_action( 'shutdown', [ $this, 'updateBackup' ] );
	}

	/**
	 * Updates the backup after restoring options.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	public function updateBackup() {
		if ( $this->shouldBackup ) {
			$this->shouldBackup = false;
			$backup = aioseo()->dynamicOptions->convertOptionsToValues( $this->backup, 'value' );
			update_option( $this->optionsName, wp_json_encode( $backup ), 'no' );
		}
	}

	/**
	 * Checks whether data from the backup has to be restored.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	public function init() {
		$this->postTypes  = wp_list_pluck( aioseo()->helpers->getPublicPostTypes( false, false, true ), 'name' );
		$this->taxonomies = wp_list_pluck( aioseo()->helpers->getPublicTaxonomies( false, true ), 'name' );
		$this->archives   = wp_list_pluck( aioseo()->helpers->getPublicPostTypes( false, true, true ), 'name' );

		$backup = json_decode( get_option( $this->optionsName ), true );
		if ( empty( $backup ) ) {
			update_option( $this->optionsName, '{}', 'no' );

			return;
		}

		$this->backup         = $backup;
		$this->defaultOptions = aioseo()->dynamicOptions->getDefaults();

		$this->restorePostTypes();
		$this->restoreTaxonomies();
		$this->restoreArchives();
	}

	/**
	 * Restores the dynamic Post Types options.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	protected function restorePostTypes() {
		foreach ( $this->postTypes as $postType ) {
			// Restore the post types for Search Appearance.
			if ( ! empty( $this->backup['postTypes'][ $postType ]['searchAppearance'] ) ) {
				$this->restoreOptions( $this->backup['postTypes'][ $postType ]['searchAppearance'], [ 'searchAppearance', 'postTypes', $postType ] );
				unset( $this->backup['postTypes'][ $postType ]['searchAppearance'] );
				$this->shouldBackup = true;
			}

			// Restore the post types for Social Networks.
			if ( ! empty( $this->backup['postTypes'][ $postType ]['social']['facebook'] ) ) {
				$this->restoreOptions( $this->backup['postTypes'][ $postType ]['social']['facebook'], [ 'social', 'facebook', 'general', 'postTypes', $postType ] );
				unset( $this->backup['postTypes'][ $postType ]['social']['facebook'] );
				$this->shouldBackup = true;
			}
		}
	}

	/**
	 * Restores the dynamic Taxonomies options.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	protected function restoreTaxonomies() {
		foreach ( $this->taxonomies as $taxonomy ) {
			// Restore the taxonomies for Search Appearance.
			if ( ! empty( $this->backup['taxonomies'][ $taxonomy ]['searchAppearance'] ) ) {
				$this->restoreOptions( $this->backup['taxonomies'][ $taxonomy ]['searchAppearance'], [ 'searchAppearance', 'taxonomies', $taxonomy ] );
				unset( $this->backup['taxonomies'][ $taxonomy ]['searchAppearance'] );
				$this->shouldBackup = true;
			}

			// Restore the taxonomies for Social Networks.
			if ( ! empty( $this->backup['taxonomies'][ $taxonomy ]['social']['facebook'] ) ) {
				$this->restoreOptions( $this->backup['taxonomies'][ $taxonomy ]['social']['facebook'], [ 'social', 'facebook', 'general', 'taxonomies', $taxonomy ] );
				unset( $this->backup['taxonomies'][ $taxonomy ]['social']['facebook'] );
				$this->shouldBackup = true;
			}
		}
	}

	/**
	 * Restores the dynamic Archives options.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	protected function restoreArchives() {
		foreach ( $this->archives as $postType ) {
			// Restore the archives for Search Appearance.
			if ( ! empty( $this->backup['archives'][ $postType ]['searchAppearance'] ) ) {
				$this->restoreOptions( $this->backup['archives'][ $postType ]['searchAppearance'], [ 'searchAppearance', 'archives', $postType ] );
				unset( $this->backup['archives'][ $postType ]['searchAppearance'] );
				$this->shouldBackup = true;
			}
		}
	}

	/**
	 * Restores the backuped options.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $backupOptions The options to be restored.
	 * @param  array $groups        The group that the option should be restored to.
	 * @return void
	 */
	protected function restoreOptions( $backupOptions, $groups ) {
		$defaultOptions = $this->defaultOptions;
		foreach ( $groups as $group ) {
			if ( ! isset( $defaultOptions[ $group ] ) ) {
				return;
			}

			$defaultOptions = $defaultOptions[ $group ];
		}

		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		foreach ( $backupOptions as $setting => $value ) {
			// Check if the option exists before proceeding. If not, it might be a group.
			$type = $defaultOptions[ $setting ]['type'] ?? '';
			if (
				! $type &&
				is_array( $value ) &&
				aioseo()->helpers->isArrayAssociative( $value )
			) {
				$nextGroups = array_merge( $groups, [ $setting ] );

				$this->restoreOptions( $backupOptions[ $setting ], $nextGroups );

				continue;
			}

			// If we still can't find the option, it might be a group.
			if ( ! $type ) {
				continue;
			}

			foreach ( $groups as $group ) {
				$dynamicOptions = $dynamicOptions->$group;
			}

			$dynamicOptions->$setting = $value;
		}
	}

	/**
	 * Maybe backup the options if it has disappeared.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $newOptions An array of options to check.
	 * @return void
	 */
	public function maybeBackup( $newOptions ) {
		$this->maybeBackupPostType( $newOptions );
		$this->maybeBackupTaxonomy( $newOptions );
		$this->maybeBackupArchives( $newOptions );
	}

	/**
	 * Maybe backup the Post Types.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $newOptions An array of options to check.
	 * @return void
	 */
	protected function maybeBackupPostType( $newOptions ) {
		// Maybe backup the post types for Search Appearance.
		foreach ( $newOptions['searchAppearance']['postTypes'] as $dynamicPostTypeName => $dynamicPostTypeSettings ) {
			$found = in_array( $dynamicPostTypeName, $this->postTypes, true );
			if ( ! $found ) {
				$this->backup['postTypes'][ $dynamicPostTypeName ]['searchAppearance'] = $dynamicPostTypeSettings;
				$this->shouldBackup = true;
			}
		}

		// Maybe backup the post types for Social Networks.
		foreach ( $newOptions['social']['facebook']['general']['postTypes'] as $dynamicPostTypeName => $dynamicPostTypeSettings ) {
			$found = in_array( $dynamicPostTypeName, $this->postTypes, true );
			if ( ! $found ) {
				$this->backup['postTypes'][ $dynamicPostTypeName ]['social']['facebook'] = $dynamicPostTypeSettings;
				$this->shouldBackup = true;
			}
		}
	}

	/**
	 * Maybe backup the Taxonomies.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $newOptions An array of options to check.
	 * @return void
	 */
	protected function maybeBackupTaxonomy( $newOptions ) {
		// Maybe backup the taxonomies for Search Appearance.
		foreach ( $newOptions['searchAppearance']['taxonomies'] as $dynamicTaxonomyName => $dynamicTaxonomySettings ) {
			$found = in_array( $dynamicTaxonomyName, $this->taxonomies, true );
			if ( ! $found ) {
				$this->backup['taxonomies'][ $dynamicTaxonomyName ]['searchAppearance'] = $dynamicTaxonomySettings;
				$this->shouldBackup = true;
			}
		}
	}

	/**
	 * Maybe backup the Archives.
	 *
	 * @since 4.1.3
	 *
	 * @param  array $newOptions An array of options to check.
	 * @return void
	 */
	protected function maybeBackupArchives( $newOptions ) {
		// Maybe backup the archives for Search Appearance.
		foreach ( $newOptions['searchAppearance']['archives'] as $archiveName => $archiveSettings ) {
			$found = in_array( $archiveName, $this->archives, true );
			if ( ! $found ) {
				$this->backup['archives'][ $archiveName ]['searchAppearance'] = $archiveSettings;
				$this->shouldBackup = true;
			}
		}
	}
}Common/Options/InternalNetworkOptions.php000066600000001622151135505570014656 0ustar00<?php
namespace AIOSEO\Plugin\Common\Options;

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

use AIOSEO\Plugin\Common\Traits;
use AIOSEO\Plugin\Common\Utils;

/**
 * Class that holds all internal network options for AIOSEO.
 *
 * @since 4.2.5
 */
class InternalNetworkOptions {
	use Traits\Options;
	use Traits\NetworkOptions;

	/**
	 * Holds the helpers class.
	 *
	 * @since 4.2.5
	 *
	 * @var Utils\Helpers
	 */
	protected $helpers;

	/**
	 * All the default options.
	 *
	 * @since 4.2.5
	 *
	 * @var array
	 */
	protected $defaults = [];

	/**
	 * The Construct method.
	 *
	 * @since 4.2.5
	 *
	 * @param string $optionsName The options name.
	 */
	public function __construct( $optionsName = 'aioseo_options_network_internal' ) {
		$this->helpers     = new Utils\Helpers();
		$this->optionsName = $optionsName;

		$this->init();

		add_action( 'shutdown', [ $this, 'save' ] );
	}
}Common/Options/InternalOptions.php000066600000013205151135505570013304 0ustar00<?php
namespace AIOSEO\Plugin\Common\Options;

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

use AIOSEO\Plugin\Common\Traits;

/**
 * Class that holds all internal options for AIOSEO.
 *
 * @since 4.0.0
 */
class InternalOptions {
	use Traits\Options;

	/**
	 * Holds a list of all the possible deprecated options.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $allDeprecatedOptions = [
		'autogenerateDescriptions',
		'breadcrumbsEnable',
		'descriptionFormat',
		'enableSchemaMarkup',
		'excludePosts',
		'excludeTerms',
		'googleAnalytics',
		'noPaginationForCanonical',
		'staticSitemap',
		'staticVideoSitemap',
		'useContentForAutogeneratedDescriptions'
	];

	/**
	 * All the default options.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $defaults = [
		// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
		'internal'     => [
			'validLicenseKey'   => [ 'type' => 'string' ],
			'lastActiveVersion' => [ 'type' => 'string', 'default' => '0.0' ],
			'migratedVersion'   => [ 'type' => 'string' ],
			'siteAnalysis'      => [
				'connectToken' => [ 'type' => 'string' ],
			],
			'headlineAnalysis'  => [
				'headlines' => [ 'type' => 'array', 'default' => [] ]
			],
			'wizard'            => [ 'type' => 'string' ],
			'category'          => [ 'type' => 'string' ],
			'categoryOther'     => [ 'type' => 'string' ],
			'deprecatedOptions' => [ 'type' => 'array', 'default' => [] ],
			'searchStatistics'  => [
				'profile'    => [ 'type' => 'array', 'default' => [] ],
				'trustToken' => [ 'type' => 'string' ],
				'rolling'    => [ 'type' => 'string', 'default' => 'last28Days' ],
				'site'       => [
					'verified'  => [ 'type' => 'boolean', 'default' => false ],
					'lastFetch' => [ 'type' => 'number', 'default' => 0 ]
				],
				'sitemap'    => [
					'list'      => [ 'type' => 'array', 'default' => [] ],
					'ignored'   => [ 'type' => 'array', 'default' => [] ],
					'lastFetch' => [ 'type' => 'number', 'default' => 0 ]
				]
			]
		],
		'integrations' => [
			'semrush' => [
				'accessToken'  => [ 'type' => 'string' ],
				'tokenType'    => [ 'type' => 'string' ],
				'expires'      => [ 'type' => 'string' ],
				'refreshToken' => [ 'type' => 'string' ]
			]
		],
		'database'     => [
			'installedTables' => [ 'type' => 'string' ]
		]
		// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	];

	/**
	 * The Construct method.
	 *
	 * @since 4.0.0
	 *
	 * @param string $optionsName The options name.
	 */
	public function __construct( $optionsName = 'aioseo_options_internal' ) {
		$this->optionsName = $optionsName;

		$this->init();

		add_action( 'shutdown', [ $this, 'save' ] );
	}

	/**
	 * Initializes the options.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function init() {
		// Options from the DB.
		$dbOptions = $this->getDbOptions( $this->optionsName );

		// Refactor options.
		$this->defaultsMerged = array_replace_recursive( $this->defaults, $this->defaultsMerged );

		$options = array_replace_recursive(
			$this->defaultsMerged,
			$this->addValueToValuesArray( $this->defaultsMerged, $dbOptions )
		);

		aioseo()->core->optionsCache->setOptions( $this->optionsName, apply_filters( 'aioseo_get_options_internal', $options ) );

		// Get the localized options.
		$dbOptionsLocalized = get_option( $this->optionsName . '_localized' );
		if ( empty( $dbOptionsLocalized ) ) {
			$dbOptionsLocalized = [];
		}
		$this->localized = $dbOptionsLocalized;
	}

	/**
	 * Get all the deprecated options.
	 *
	 * @since 4.0.0
	 *
	 * @param  bool  $includeNamesAndValues Whether or not to include option names.
	 * @return array                        An array of deprecated options.
	 */
	public function getAllDeprecatedOptions( $includeNamesAndValues = false ) {
		if ( ! $includeNamesAndValues ) {
			return $this->allDeprecatedOptions;
		}

		$options = [];
		foreach ( $this->allDeprecatedOptions as $deprecatedOption ) {
			$options[] = [
				'label'   => ucwords( str_replace( '_', ' ', aioseo()->helpers->toSnakeCase( $deprecatedOption ) ) ),
				'value'   => $deprecatedOption,
				'enabled' => in_array( $deprecatedOption, aioseo()->internalOptions->internal->deprecatedOptions, true )
			];
		}

		return $options;
	}

	/**
	 * Sanitizes, then saves the options to the database.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $options An array of options to sanitize, then save.
	 * @return void
	 */
	public function sanitizeAndSave( $options ) {
		if ( ! is_array( $options ) ) {
			return;
		}

		// First, recursively replace the new options into the cached state.
		// It's important we use the helper method since we want to replace populated arrays with empty ones if needed (when a setting was cleared out).
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$dbOptions     = aioseo()->helpers->arrayReplaceRecursive(
			$cachedOptions,
			$this->addValueToValuesArray( $cachedOptions, $options, [], true )
		);

		// Now, we must also intersect both arrays to delete any individual keys that were unset.
		// We must do this because, while arrayReplaceRecursive will update the values for keys or empty them out,
		// it will keys that aren't present in the replacement array unaffected in the target array.
		$dbOptions = aioseo()->helpers->arrayIntersectRecursive(
			$dbOptions,
			$this->addValueToValuesArray( $cachedOptions, $options, [], true ),
			'value'
		);

		// Update the cache state.
		aioseo()->core->optionsCache->setOptions( $this->optionsName, $dbOptions );

		// Update localized options.
		update_option( $this->optionsName . '_localized', $this->localized );

		// Finally, save the new values to the DB.
		$this->save( true );
	}
}Common/Meta/Traits/Helpers/BuddyPress.php000066600000001370151135505570014343 0ustar00<?php

namespace AIOSEO\Plugin\Common\Meta\Traits\Helpers;

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

/**
 * Contains BuddyPress specific helper methods.
 *
 * @since 4.7.6
 */
trait BuddyPress {
	/**
	 * Sanitizes the title/description.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $value       The value.
	 * @param  int    $objectId    The object ID.
	 * @param  bool   $replaceTags Whether the smart tags should be replaced.
	 * @return string              The sanitized value.
	 */
	public function bpSanitize( $value, $objectId = 0, $replaceTags = false ) {
		$value = $replaceTags ? $value : aioseo()->standalone->buddyPress->tags->replaceTags( $value, $objectId );

		return $this->sanitize( $value, $objectId, true );
	}
}Common/Meta/Links.php000066600000011442151135505570010470 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

/**
 * Instantiates the meta links "next" and "prev".
 *
 * @since 4.0.0
 */
class Links {
	/**
	 * Get the prev/next links for the current page.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of link data.
	 */
	public function getLinks() {
		$links = [
			'prev' => '',
			'next' => '',
		];

		if ( is_home() || is_archive() || is_paged() ) {
			$links = $this->getHomeLinks();
		}

		if ( is_page() || is_single() ) {
			global $post;
			$links = $this->getPostLinks( $post );
		}

		$links['prev'] = apply_filters( 'aioseo_prev_link', $links['prev'] );
		$links['next'] = apply_filters( 'aioseo_next_link', $links['next'] );

		return $links;
	}

	/**
	 * Get the prev/next links for the current page (home/archive, etc.).
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of link data.
	 */
	private function getHomeLinks() {
		$prev = '';
		$next = '';
		$page = aioseo()->helpers->getPageNumber();

		global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$maxPage = $wp_query->max_num_pages; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if ( $page > 1 ) {
			$prev = get_previous_posts_page_link();
		}
		if ( $page < $maxPage ) {
			$next  = get_next_posts_page_link();
			$paged = is_paged();
			if ( ! is_single() ) {
				if ( ! $paged ) {
					$page = 1;
				}
				$nextpage = intval( $page ) + 1;
				if ( ! $maxPage || $maxPage >= $nextpage ) {
					$next = get_pagenum_link( $nextpage );
				}
			}
		}

		// Remove trailing slashes if not set in the permalink structure.
		$prev = aioseo()->helpers->maybeRemoveTrailingSlash( $prev );
		$next = aioseo()->helpers->maybeRemoveTrailingSlash( $next );

		// Remove any query args that may be set on the URL, except if the site is using plain permalinks.
		$permalinkStructure = get_option( 'permalink_structure' );
		if ( ! empty( $permalinkStructure ) ) {
			$prev = explode( '?', $prev )[0];
			$next = explode( '?', $next )[0];
		}

		return [
			'prev' => $prev,
			'next' => $next,
		];
	}

	/**
	 * Get the prev/next links for the current post.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post The post.
	 * @return array          An array of link data.
	 */
	private function getPostLinks( $post ) {
		$prev     = '';
		$next     = '';
		$numpages = 1;
		$page     = aioseo()->helpers->getPageNumber();
		$content  = is_a( $post, 'WP_Post' ) ? $post->post_content : '';
		if ( false !== strpos( $content, '<!--nextpage-->', 0 ) ) {
			$content = str_replace( "\n<!--nextpage-->\n", '<!--nextpage-->', $content );
			$content = str_replace( "\n<!--nextpage-->", '<!--nextpage-->', $content );
			$content = str_replace( "<!--nextpage-->\n", '<!--nextpage-->', $content );
			// Ignore nextpage at the beginning of the content.
			if ( 0 === strpos( $content, '<!--nextpage-->', 0 ) ) {
				$content = substr( $content, 15 );
			}
			$pages    = explode( '<!--nextpage-->', $content );
			$numpages = count( $pages );
		} else {
			$page = null;
		}
		if ( ! empty( $page ) ) {
			if ( $page > 1 ) {
				$prev = $this->getLinkPage( $page - 1 );
			}
			if ( $page + 1 <= $numpages ) {
				$next = $this->getLinkPage( $page + 1 );
			}
		}

		return [
			'prev' => $prev,
			'next' => $next,
		];
	}

	/**
	 * This is a clone of _wp_link_page, except that we don't output HTML.
	 *
	 * @since 4.0.0
	 *
	 * @param  integer $number The page number.
	 * @return string          The URL.
	 */
	private function getLinkPage( $number ) {
		global $wp_rewrite; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$post      = get_post();
		$queryArgs = [];

		if ( 1 === (int) $number ) {
			$url = get_permalink();
		} else {
			if ( ! get_option( 'permalink_structure' ) || in_array( $post->post_status, [ 'draft', 'pending' ], true ) ) {
				$url = add_query_arg( 'page', $number, get_permalink() );
			} elseif ( 'page' === get_option( 'show_on_front' ) && get_option( 'page_on_front' ) === $post->ID ) {
				$url = trailingslashit( get_permalink() ) . user_trailingslashit( "$wp_rewrite->pagination_base/" . $number, 'single_paged' ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			} else {
				$url = trailingslashit( get_permalink() ) . user_trailingslashit( $number, 'single_paged' );
			}
		}

		if ( is_preview() ) {
			// phpcs:disable HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended	
			if ( ( 'draft' !== $post->post_status ) && isset( $_GET['preview_id'], $_GET['preview_nonce'] ) ) {
				$queryArgs['preview_id']    = sanitize_text_field( wp_unslash( $_GET['preview_id'] ) );
				$queryArgs['preview_nonce'] = sanitize_text_field( wp_unslash( $_GET['preview_nonce'] ) );
			}
			// phpcs:enable

			$url = get_preview_post_link( $post, $queryArgs, $url );
		}

		return esc_url( $url );
	}
}Common/Meta/Title.php000066600000015461151135505570010476 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Handles the title.
 *
 * @since 4.0.0
 */
class Title {
	/**
	 * Helpers class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Helpers
	 */
	public $helpers = null;

	/**
	 * Class constructor.
	 *
	* @since 4.1.2
	 */
	public function __construct() {
		$this->helpers = new Helpers( 'title' );
	}

	/**
	 * Returns the filtered page title.
	 *
	 * Acts as a helper for getTitle() because we need to encode the title before sending it back to the filter.
	 *
	 * @since 4.0.0
	 *
	 * @return string The page title.
	 */
	public function filterPageTitle( $wpTitle = '' ) {
		$title = $this->getTitle();

		return ! empty( $title ) ? aioseo()->helpers->encodeOutputHtml( $title ) : $wpTitle;
	}

	/**
	 * Returns the homepage title.
	 *
	 * @since 4.0.0
	 *
	 * @return string The homepage title.
	 */
	public function getHomePageTitle() {
		if ( 'page' === get_option( 'show_on_front' ) ) {
			$title = $this->getPostTitle( (int) get_option( 'page_on_front' ) );

			return $title ? $title : aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) );
		}

		$title = aioseo()->options->searchAppearance->global->siteTitle;
		if ( aioseo()->helpers->isWpmlActive() ) {
			// Allow WPML to translate the title if the homepage is not static.
			$title = apply_filters( 'wpml_translate_single_string', $title, 'admin_texts_aioseo_options_localized', '[aioseo_options_localized]searchAppearance_global_siteTitle' );
		}

		$title = $this->helpers->prepare( $title );

		return $title ? $title : aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) );
	}

	/**
	 * Returns the title for the current page.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post    The post object (optional).
	 * @param  boolean  $default Whether we want the default value, not the post one.
	 * @return string            The page title.
	 */
	public function getTitle( $post = null, $default = false ) {
		if ( BuddyPressIntegration::isComponentPage() ) {
			return aioseo()->standalone->buddyPress->component->getMeta( 'title' );
		}

		if ( is_home() ) {
			return $this->getHomePageTitle();
		}

		if ( $post || is_singular() || aioseo()->helpers->isStaticPage() ) {
			return $this->getPostTitle( $post, $default );
		}

		if ( is_category() || is_tag() || is_tax() ) {
			$term = $post ? $post : aioseo()->helpers->getTerm();

			return $this->getTermTitle( $term, $default );
		}

		if ( is_author() ) {
			return $this->helpers->prepare( aioseo()->options->searchAppearance->archives->author->title );
		}

		if ( is_date() ) {
			return $this->helpers->prepare( aioseo()->options->searchAppearance->archives->date->title );
		}

		if ( is_search() ) {
			return $this->helpers->prepare( aioseo()->options->searchAppearance->archives->search->title );
		}

		if ( is_post_type_archive() ) {
			$postType = get_queried_object();
			if ( is_a( $postType, 'WP_Post_Type' ) ) {
				return $this->helpers->prepare( $this->getArchiveTitle( $postType->name ) );
			}
		}

		return '';
	}

	/**
	 * Returns the post title.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|int $post    The post object or ID.
	 * @param  boolean      $default Whether we want the default value, not the post one.
	 * @return string                The post title.
	 */
	public function getPostTitle( $post, $default = false ) {
		$post = $post && is_object( $post ) ? $post : aioseo()->helpers->getPost( $post );
		if ( ! is_a( $post, 'WP_Post' ) ) {
			return '';
		}

		static $posts = [];
		if ( isset( $posts[ $post->ID ] ) ) {
			return $posts[ $post->ID ];
		}

		$title    = '';
		$metaData = aioseo()->meta->metaData->getMetaData( $post );

		if ( ! empty( $metaData->title ) && ! $default ) {
			$title = $this->helpers->prepare( $metaData->title, $post->ID );
		}

		if ( ! $title ) {
			$title = $this->helpers->prepare( $this->getPostTypeTitle( $post->post_type ), $post->ID, $default );
		}

		// If this post is the static home page and we have no title, let's reset to the site name.
		if ( empty( $title ) && 'page' === get_option( 'show_on_front' ) && (int) get_option( 'page_on_front' ) === $post->ID ) {
			$title = aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) );
		}

		if ( empty( $title ) ) {
			// Just return the WP default.
			$title = get_the_title( $post->ID ) . ' - ' . get_bloginfo( 'name' );
			$title = aioseo()->helpers->decodeHtmlEntities( $title );
		}

		$posts[ $post->ID ] = $title;

		return $posts[ $post->ID ];
	}

	/**
	 * Retrieve the default title for the archive template.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $postType The custom post type.
	 * @return string           The title.
	 */
	public function getArchiveTitle( $postType ) {
		static $archiveTitle = [];
		if ( isset( $archiveTitle[ $postType ] ) ) {
			return $archiveTitle[ $postType ];
		}

		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		if ( $dynamicOptions->searchAppearance->archives->has( $postType ) ) {
			$title = aioseo()->dynamicOptions->searchAppearance->archives->{ $postType }->title;
		}

		$archiveTitle[ $postType ] = empty( $title ) ? '' : $title;

		return $archiveTitle[ $postType ];
	}

	/**
	 * Retrieve the default title for the post type.
	 *
	 * @since 4.0.6
	 *
	 * @param  string $postType The post type.
	 * @return string           The title.
	 */
	public function getPostTypeTitle( $postType ) {
		static $postTypeTitle = [];
		if ( isset( $postTypeTitle[ $postType ] ) ) {
			return $postTypeTitle[ $postType ];
		}

		if ( aioseo()->dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
			$title = aioseo()->dynamicOptions->searchAppearance->postTypes->{$postType}->title;
		}

		$postTypeTitle[ $postType ] = empty( $title ) ? '' : $title;

		return $postTypeTitle[ $postType ];
	}

	/**
	 * Returns the term title.
	 *
	 * @since 4.0.6
	 *
	 * @param  \WP_Term $term    The term object.
	 * @param  boolean  $default Whether we want the default value, not the post one.
	 * @return string            The term title.
	 */
	public function getTermTitle( $term, $default = false ) {
		if ( ! is_a( $term, 'WP_Term' ) ) {
			return '';
		}

		static $terms = [];
		if ( isset( $terms[ $term->term_id ] ) ) {
			return $terms[ $term->term_id ];
		}

		$title          = '';
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		if ( ! $title && $dynamicOptions->searchAppearance->taxonomies->has( $term->taxonomy ) ) {
			$newTitle = aioseo()->dynamicOptions->searchAppearance->taxonomies->{$term->taxonomy}->title;
			$newTitle = preg_replace( '/#taxonomy_title/', aioseo()->helpers->escapeRegexReplacement( $term->name ), (string) $newTitle );
			$title    = $this->helpers->prepare( $newTitle, $term->term_id, $default );
		}

		$terms[ $term->term_id ] = $title;

		return $terms[ $term->term_id ];
	}
}Common/Meta/Amp.php000066600000002202151135505570010117 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

/**
 * Adds support for Google AMP.
 *
 * @since 4.0.0
 */
class Amp {
	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'runAmp' ] );
	}

	/**
	 * Run the AMP hooks.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function runAmp() {
		if ( is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		// Add social meta to AMP plugin.
		$enableAmp = apply_filters( 'aioseo_enable_amp_social_meta', true );

		if ( $enableAmp ) {
			$useSchema = apply_filters( 'aioseo_amp_schema', true );

			if ( $useSchema ) {
				add_action( 'amp_post_template_head', [ $this, 'removeHooksAmpSchema' ], 9 );
			}

			add_action( 'amp_post_template_head', [ aioseo()->head, 'output' ], 11 );
		}
	}

	/**
	 * Remove Hooks with AMP's Schema.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function removeHooksAmpSchema() {
		// Remove AMP Schema hook used for outputting data.
		remove_action( 'amp_post_template_head', 'amp_print_schemaorg_metadata' );
	}
}Common/Meta/Meta.php000066600000002700151135505570010273 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Instantiates the Meta classes.
 *
 * @since 4.0.0
 */
class Meta {
	/**
	 * MetaData class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var MetaData
	 */
	public $metaData = null;

	/**
	 * Title class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Title
	 */
	public $title = null;

	/**
	 * Description class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Description
	 */
	public $description = null;

	/**
	 * Keywords class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Keywords
	 */
	public $keywords = null;

	/**
	 * Robots class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Robots
	 */
	public $robots = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->metaData     = new MetaData();
		$this->title        = new Title();
		$this->description  = new Description();
		$this->keywords     = new Keywords();
		$this->robots       = new Robots();

		new Amp();
		new Links();

		add_action( 'delete_post', [ $this, 'deletePostMeta' ], 1000 );
	}

	/**
	 * When we delete the meta, we want to delete our post model.
	 *
	 * @since 4.0.1
	 *
	 * @param  integer $postId The ID of the post.
	 * @return void
	 */
	public function deletePostMeta( $postId ) {
		$aioseoPost = Models\Post::getPost( $postId );
		if ( $aioseoPost->exists() ) {
			$aioseoPost->delete();
		}
	}
}Common/Meta/MetaData.php000066600000007556151135505570011103 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Handles fetching metadata for the current object.
 *
 * @since 4.0.0
 */
class MetaData {
	/**
	 * The cached meta data for posts.
	 *
	 * @since 4.1.7
	 *
	 * @var array
	 */
	private $posts = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'wpml_pro_translation_completed', [ $this, 'updateWpmlLocalization' ], 1000, 3 );
	}

	/**
	 * Update the localized data in our posts table.
	 *
	 * @since 4.0.0
	 *
	 * @param  integer $postId The post ID.
	 * @param  array   $fields An array of fields to update.
	 * @return void
	 */
	public function updateWpmlLocalization( $postId, $fields = [], $job = null ) {
		$aioseoFields = [
			'_aioseo_title',
			'_aioseo_description',
			'_aioseo_keywords',
			'_aioseo_og_title',
			'_aioseo_og_description',
			'_aioseo_twitter_title',
			'_aioseo_twitter_description'
		];

		$parentId    = $job->original_doc_id;
		$parentPost  = Models\Post::getPost( $parentId );
		$currentPost = Models\Post::getPost( $postId );
		$columns     = $parentPost->getColumns();
		foreach ( $columns as $column => $value ) {
			// Skip the ID columns.
			if ( 'id' === $column || 'post_id' === $column ) {
				continue;
			}

			$currentPost->$column = $parentPost->$column;
		}

		$currentPost->post_id = $postId;

		foreach ( $aioseoFields as $aioseoField ) {
			if ( ! empty( $fields[ 'field-' . $aioseoField . '-0' ] ) ) {
				$value = $fields[ 'field-' . $aioseoField . '-0' ]['data'];
				if ( '_aioseo_keywords' === $aioseoField ) {
					$value = explode( ',', $value );
					foreach ( $value as $k => $keyword ) {
						$value[ $k ] = [
							'label' => $keyword,
							'value' => $keyword
						];
					}

					$value = wp_json_encode( $value );
				}
				$currentPost->{ str_replace( '_aioseo_', '', $aioseoField ) } = $value;
			}
		}

		$currentPost->save();
	}

	/**
	 * Returns the metadata for the current object.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post         $post The post object (optional).
	 * @return Models\Post|bool       The meta data or false.
	 */
	public function getMetaData( $post = null ) {
		if ( ! $post ) {
			$post = aioseo()->helpers->getPost();
		}

		if ( $post ) {
			$post = is_object( $post ) ? $post : aioseo()->helpers->getPost( $post );
			// If we still have no post, let's return false.
			if ( ! is_a( $post, 'WP_Post' ) ) {
				return false;
			}

			if ( isset( $this->posts[ $post->ID ] ) ) {
				return $this->posts[ $post->ID ];
			}

			$this->posts[ $post->ID ] = Models\Post::getPost( $post->ID );

			if ( ! $this->posts[ $post->ID ]->exists() ) {
				$migratedMeta = aioseo()->migration->meta->getMigratedPostMeta( $post->ID );
				if ( ! empty( $migratedMeta ) ) {
					foreach ( $migratedMeta as $k => $v ) {
						$this->posts[ $post->ID ]->{$k} = $v;
					}

					$this->posts[ $post->ID ]->save();
				}
			}

			return $this->posts[ $post->ID ];
		}

		return false;
	}

	/**
	 * Returns the cached OG image from the meta data.
	 *
	 * @since 4.1.6
	 *
	 * @param  Object $metaData The meta data object.
	 * @return array            An array of image data.
	 */
	public function getCachedOgImage( $metaData ) {
		return [
			$metaData->og_image_url,
			isset( $metaData->og_image_width ) ? $metaData->og_image_width : null,
			isset( $metaData->og_image_height ) ? $metaData->og_image_height : null
		];
	}

	/**
	 * Busts the meta data cache for a given post.
	 *
	 * @since 4.1.7
	 *
	 * @param  int         $postId   The post ID.
	 * @param  Models\Post $metaData The meta data.
	 * @return void
	 */
	public function bustPostCache( $postId, $metaData = null ) {
		if ( null === $metaData || ! is_a( $metaData, 'AIOSEO\Plugin\Common\Models\Post' ) ) {
			unset( $this->posts[ $postId ] );
		}

		$this->posts[ $postId ] = $metaData;
	}
}Common/Meta/Included.php000066600000006250151135505570011140 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

/**
 * To check whether SEO is enabled for the queried object.
 *
 * @since 4.0.0
 */
class Included {
	/**
	 * Checks whether the queried object is included.
	 *
	 * @since 4.0.0
	 *
	 * @return bool
	 */
	public function isIncluded() {
		if ( is_admin() || is_feed() ) {
			return false;
		}

		if ( apply_filters( 'aioseo_disable', false ) || $this->isExcludedGlobal() ) {
			return false;
		}

		if ( ! $this->isQueriedObjectPublic() ) {
			return false;
		}

		return true;
	}

	/**
	 * Checks whether the queried object is public.
	 *
	 * @since 4.2.2
	 *
	 * @return bool Whether the queried object is public.
	 */
	protected function isQueriedObjectPublic() {
		$queriedObject = get_queried_object(); // Don't use the getTerm helper here.

		if ( is_a( $queriedObject, 'WP_Post' ) ) {
			return aioseo()->helpers->isPostTypePublic( $queriedObject->post_type );
		}

		// Check if the current page is a post type archive page.
		if ( is_a( $queriedObject, 'WP_Post_Type' ) ) {
			return aioseo()->helpers->isPostTypePublic( $queriedObject->name );
		}

		if ( is_a( $queriedObject, 'WP_Term' ) ) {
			if ( aioseo()->helpers->isWooCommerceProductAttribute( $queriedObject->taxonomy ) ) {
				// Check if the attribute has archives enabled.
				$taxonomy = get_taxonomy( $queriedObject->taxonomy );

				return $taxonomy->public;
			}

			return aioseo()->helpers->isTaxonomyPublic( $queriedObject->taxonomy );
		}

		// Return true in all other cases (e.g. search page, date archive, etc.).
		return true;
	}

	/**
	 * Checks whether the queried object has been excluded globally.
	 *
	 * @since 4.0.0
	 *
	 * @return bool
	 */
	protected function isExcludedGlobal() {
		if ( is_category() || is_tag() || is_tax() ) {
			return $this->isTaxExcludedGlobal();
		}

		if ( ! in_array( 'excludePosts', aioseo()->internalOptions->deprecatedOptions, true ) ) {
			return false;
		}

		$excludedPosts = aioseo()->options->deprecated->searchAppearance->advanced->excludePosts;

		if ( empty( $excludedPosts ) ) {
			return false;
		}

		$ids = [];
		foreach ( $excludedPosts as $object ) {
			$object = json_decode( $object );
			if ( is_int( $object->value ) ) {
				$ids[] = (int) $object->value;
			}
		}

		$post = aioseo()->helpers->getPost();
		if ( empty( $post ) ) {
			return false;
		}

		if ( in_array( (int) $post->ID, $ids, true ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Checks whether the queried object has been excluded globally.
	 *
	 * @since 4.0.0
	 *
	 * @return bool
	 */
	protected function isTaxExcludedGlobal() {
		if ( ! in_array( 'excludeTerms', aioseo()->internalOptions->deprecatedOptions, true ) ) {
			return false;
		}

		$excludedTerms = aioseo()->options->deprecated->searchAppearance->advanced->excludeTerms;

		if ( empty( $excludedTerms ) ) {
			return false;
		}

		$ids = [];
		foreach ( $excludedTerms as $object ) {
			$object = json_decode( $object );
			if ( is_int( $object->value ) ) {
				$ids[] = (int) $object->value;
			}
		}

		$term = aioseo()->helpers->getTerm();
		if ( in_array( (int) $term->term_id, $ids, true ) ) {
			return true;
		}

		return false;
	}
}Common/Meta/Helpers.php000066600000006324151135505570011015 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

/**
 * Contains helper methods for the title/description classes.
 *
 * @since 4.1.2
 */
class Helpers {
	use Traits\Helpers\BuddyPress;

	/**
	 * The name of the class where this instance is constructed.
	 *
	 * @since 4.1.2
	 *
	 * @param string $name The name of the class. Either "title" or "description".
	 */
	private $name;

	/**
	 * Supported filters we can run after preparing the value.
	 *
	 * @since 4.1.2
	 *
	 * @var array
	 */
	private $supportedFilters = [
		'title'       => 'aioseo_title',
		'description' => 'aioseo_description'
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.2
	 *
	 * @param string $name The name of the class where this instance is constructed.
	 */
	public function __construct( $name ) {
		$this->name = $name;
	}

	/**
	 * Sanitizes the title/description.
	 *
	 * @since 4.1.2
	 *
	 * @param  string   $value       The value.
	 * @param  int|bool $objectId    The post/term ID.
	 * @param  bool     $replaceTags Whether the smart tags should be replaced.
	 * @return string                The sanitized value.
	 */
	public function sanitize( $value, $objectId = false, $replaceTags = false ) {
		$value = $replaceTags ? $value : aioseo()->tags->replaceTags( $value, $objectId );
		$value = aioseo()->helpers->doShortcodes( $value );

		$value = aioseo()->helpers->decodeHtmlEntities( $value );
		$value = $this->encodeExceptions( $value );
		$value = wp_strip_all_tags( strip_shortcodes( $value ) );
		// Because we encoded the exceptions, we need to decode them again first to prevent double encoding later down the line.
		$value = aioseo()->helpers->decodeHtmlEntities( $value );

		// Trim internal and external whitespace.
		$value = preg_replace( '/[\s]+/u', ' ', (string) trim( $value ) );

		return aioseo()->helpers->internationalize( $value );
	}

	/**
	 * Prepares the title/description before returning it.
	 *
	 * @since 4.1.2
	 *
	 * @param  string   $value       The value.
	 * @param  int|bool $objectId    The post/term ID.
	 * @param  bool     $replaceTags Whether the smart tags should be replaced.
	 * @return string                The sanitized value.
	 */
	public function prepare( $value, $objectId = false, $replaceTags = false ) {
		if (
			! empty( $value ) &&
			! is_admin() &&
			1 < aioseo()->helpers->getPageNumber()
		) {
			$value .= '&nbsp;' . trim( aioseo()->options->searchAppearance->advanced->pagedFormat );
		}

		$value = $replaceTags ? $value : aioseo()->tags->replaceTags( $value, $objectId );
		$value = apply_filters( $this->supportedFilters[ $this->name ], $value );

		return $this->sanitize( $value, $objectId, $replaceTags );
	}

	/**
	 * Encodes a number of exceptions before we strip tags.
	 * We need this function to allow certain character (combinations) in the title/description.
	 *
	 * @since 4.1.1
	 *
	 * @param  string $string The string.
	 * @return string $string The string with exceptions encoded.
	 */
	public function encodeExceptions( $string ) {
		$exceptions = [ '<3' ];
		foreach ( $exceptions as $exception ) {
			$string = preg_replace( "/$exception/", aioseo()->helpers->encodeOutputHtml( $exception ), (string) $string );
		}

		return $string;
	}
}Common/Meta/Description.php000066600000021263151135505570011675 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Handles the (Open Graph) description.
 *
 * @since 4.0.0
 */
class Description {
	/**
	 * Helpers class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Helpers
	 */
	public $helpers = null;

	/**
	 * Class constructor.
	 *
	* @since 4.1.2
	 */
	public function __construct() {
		$this->helpers = new Helpers( 'description' );
	}

	/**
	 * Returns the homepage description.
	 *
	 * @since 4.0.0
	 *
	 * @return string The homepage description.
	 */
	public function getHomePageDescription() {
		if ( 'page' === get_option( 'show_on_front' ) ) {
			$description = $this->getPostDescription( (int) get_option( 'page_on_front' ) );

			return $description ? $description : aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'description' ) );
		}

		$description = aioseo()->options->searchAppearance->global->metaDescription;
		if ( aioseo()->helpers->isWpmlActive() ) {
			// Allow WPML to translate the title if the homepage is not static.
			$description = apply_filters( 'wpml_translate_single_string', $description, 'admin_texts_aioseo_options_localized', '[aioseo_options_localized]searchAppearance_global_metaDescription' );
		}

		$description = $this->helpers->prepare( $description );

		return $description ? $description : aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'description' ) );
	}

	/**
	 * Returns the description for the current page.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post    The post object (optional).
	 * @param  boolean  $default Whether we want the default value, not the post one.
	 * @return string            The page description.
	 */
	public function getDescription( $post = null, $default = false ) {
		if ( BuddyPressIntegration::isComponentPage() ) {
			return aioseo()->standalone->buddyPress->component->getMeta( 'description' );
		}

		if ( is_home() ) {
			return $this->getHomePageDescription();
		}

		if ( $post || is_singular() || aioseo()->helpers->isStaticPage() ) {
			$description = $this->getPostDescription( $post, $default );
			if ( $description ) {
				return $description;
			}

			if ( is_attachment() ) {
				$post    = empty( $post ) ? aioseo()->helpers->getPost() : $post;
				$caption = wp_get_attachment_caption( $post->ID );

				return $caption ? $this->helpers->prepare( $caption ) : $this->helpers->prepare( $post->post_content );
			}
		}

		if ( is_category() || is_tag() || is_tax() ) {
			$term = $post ? $post : aioseo()->helpers->getTerm();

			return $this->getTermDescription( $term, $default );
		}

		if ( is_author() ) {
			$description = $this->helpers->prepare( aioseo()->options->searchAppearance->archives->author->metaDescription );
			if ( $description ) {
				return $description;
			}

			$author = get_queried_object();

			return $author ? $this->helpers->prepare( get_the_author_meta( 'description', $author->ID ) ) : '';
		}

		if ( is_date() ) {
			return $this->helpers->prepare( aioseo()->options->searchAppearance->archives->date->metaDescription );
		}

		if ( is_search() ) {
			return $this->helpers->prepare( aioseo()->options->searchAppearance->archives->search->metaDescription );
		}

		if ( is_post_type_archive() ) {
			$postType = get_queried_object();
			if ( is_a( $postType, 'WP_Post_Type' ) ) {
				return $this->helpers->prepare( $this->getArchiveDescription( $postType->name ) );
			}
		}

		return '';
	}

	/**
	 * Returns the description for a given post.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|int $post    The post object or ID.
	 * @param  boolean      $default Whether we want the default value, not the post one.
	 * @return string                The post description.
	 */
	public function getPostDescription( $post, $default = false ) {
		$post = $post && is_object( $post ) ? $post : aioseo()->helpers->getPost( $post );
		if ( ! is_a( $post, 'WP_Post' ) ) {
			return '';
		}

		static $posts = [];
		if ( isset( $posts[ $post->ID ] ) ) {
			return $posts[ $post->ID ];
		}

		$description = '';
		$metaData    = aioseo()->meta->metaData->getMetaData( $post );
		if ( ! empty( $metaData->description ) && ! $default ) {
			$description = $this->helpers->prepare( $metaData->description, $post->ID, false );
		}

		if (
			$description ||
			(
				in_array( 'autogenerateDescriptions', aioseo()->internalOptions->deprecatedOptions, true ) &&
				! aioseo()->options->deprecated->searchAppearance->advanced->autogenerateDescriptions
			)
		) {
			$posts[ $post->ID ] = $description;

			return $description;
		}

		$description = $this->helpers->sanitize( $this->getPostTypeDescription( $post->post_type ), $post->ID, $default );

		$generateDescriptions = apply_filters( 'aioseo_generate_descriptions_from_content', true, [ $post ] );
		if ( ! $description && ! post_password_required( $post ) ) {
			$description = $post->post_excerpt;
			if (
				$generateDescriptions &&
				in_array( 'useContentForAutogeneratedDescriptions', aioseo()->internalOptions->deprecatedOptions, true ) &&
				aioseo()->options->deprecated->searchAppearance->advanced->useContentForAutogeneratedDescriptions
			) {
				$description = aioseo()->helpers->getDescriptionFromContent( $post );
			}

			$description = $this->helpers->sanitize( $description, $post->ID, $default );
			if ( ! $description && $generateDescriptions && $post->post_content ) {
				$description = $this->helpers->sanitize( aioseo()->helpers->getDescriptionFromContent( $post ), $post->ID, $default );
			}
		}

		if ( ! is_paged() ) {
			if ( in_array( 'descriptionFormat', aioseo()->internalOptions->deprecatedOptions, true ) ) {
				$descriptionFormat = aioseo()->options->deprecated->searchAppearance->global->descriptionFormat;
				if ( $descriptionFormat ) {
					$description = preg_replace( '/#description/', $description, (string) $descriptionFormat );
				}
			}
		}

		$posts[ $post->ID ] = $description ? $this->helpers->prepare( $description, $post->ID, $default ) : $this->helpers->prepare( term_description( '' ), $post->ID, $default );

		return $posts[ $post->ID ];
	}

	/**
	 * Retrieve the default description for the archive template.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $postType The custom post type.
	 * @return string           The description.
	 */
	public function getArchiveDescription( $postType ) {
		static $archiveDescription = [];
		if ( isset( $archiveDescription[ $postType ] ) ) {
			return $archiveDescription[ $postType ];
		}

		$archiveDescription[ $postType ] = '';

		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		if ( $dynamicOptions->searchAppearance->archives->has( $postType ) ) {
			$archiveDescription[ $postType ] = aioseo()->dynamicOptions->searchAppearance->archives->{$postType}->metaDescription;
		}

		return $archiveDescription[ $postType ];
	}

	/**
	 * Retrieve the default description for the post type.
	 *
	 * @since 4.0.6
	 *
	 * @param  string $postType The post type.
	 * @return string           The description.
	 */
	public function getPostTypeDescription( $postType ) {
		static $postTypeDescription = [];
		if ( isset( $postTypeDescription[ $postType ] ) ) {
			return $postTypeDescription[ $postType ];
		}

		if ( aioseo()->dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
			$description = aioseo()->dynamicOptions->searchAppearance->postTypes->{$postType}->metaDescription;
		}

		$postTypeDescription[ $postType ] = empty( $description ) ? '' : $description;

		return $postTypeDescription[ $postType ];
	}

	/**
	 * Returns the term description.
	 *
	 * @since 4.0.6
	 *
	 * @param  \WP_Term $term    The term object.
	 * @param  boolean  $default Whether we want the default value, not the post one.
	 * @return string            The term description.
	 */
	public function getTermDescription( $term, $default = false ) {
		if ( ! is_a( $term, 'WP_Term' ) ) {
			return '';
		}

		static $terms = [];
		if ( isset( $terms[ $term->term_id ] ) ) {
			return $terms[ $term->term_id ];
		}

		$description = '';
		if (
			in_array( 'autogenerateDescriptions', aioseo()->internalOptions->deprecatedOptions, true ) &&
			! aioseo()->options->deprecated->searchAppearance->advanced->autogenerateDescriptions
		) {
			$terms[ $term->term_id ] = $description;

			return $description;
		}

		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		if ( ! $description && $dynamicOptions->searchAppearance->taxonomies->has( $term->taxonomy ) ) {
			$description = $this->helpers->prepare( aioseo()->dynamicOptions->searchAppearance->taxonomies->{$term->taxonomy}->metaDescription, false, $default );
		}

		$terms[ $term->term_id ] = $description ? $description : $this->helpers->prepare( term_description( $term->term_id ), false, $default );

		return $terms[ $term->term_id ];
	}
}Common/Meta/Keywords.php000066600000017722151135505570011226 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Handles the keywords.
 *
 * @since 4.0.0
 */
class Keywords {
	/**
	 * Get the keywords for the meta output.
	 *
	 * @since 4.0.0
	 *
	 * @return string The keywords as a string.
	 */
	public function getKeywords() {
		if ( ! aioseo()->options->searchAppearance->advanced->useKeywords ) {
			return '';
		}

		if ( BuddyPressIntegration::isComponentPage() ) {
			return aioseo()->standalone->buddyPress->component->getMeta( 'keywords' );
		}

		$isStaticArchive = aioseo()->helpers->isWooCommerceShopPage() || aioseo()->helpers->isStaticPostsPage();
		$dynamicContent  = is_archive() || is_post_type_archive() || is_home() || aioseo()->helpers->isWooCommerceShopPage() || is_category() || is_tag() || is_tax();
		$generate        = aioseo()->options->searchAppearance->advanced->dynamicallyGenerateKeywords;
		if ( $dynamicContent && $generate ) {
			return $this->prepareKeywords( $this->getGeneratedKeywords() );
		}

		if ( is_front_page() && ! aioseo()->helpers->isStaticHomePage() ) {
			$keywords = $this->extractMetaKeywords( aioseo()->options->searchAppearance->global->keywords );

			return $this->prepareKeywords( $keywords );
		}

		if ( $dynamicContent && ! $isStaticArchive ) {
			if ( is_date() ) {
				$keywords = $this->extractMetaKeywords( aioseo()->options->searchAppearance->archives->date->advanced->keywords );

				return $this->prepareKeywords( $keywords );
			}

			if ( is_author() ) {
				$keywords = $this->extractMetaKeywords( aioseo()->options->searchAppearance->archives->author->advanced->keywords );

				return $this->prepareKeywords( $keywords );
			}

			if ( is_search() ) {
				$keywords = $this->extractMetaKeywords( aioseo()->options->searchAppearance->archives->search->advanced->keywords );

				return $this->prepareKeywords( $keywords );
			}

			$postType = get_queried_object();

			return is_a( $postType, 'WP_Post_Type' )
				? $this->prepareKeywords( $this->getArchiveKeywords( $postType->name ) )
				: '';
		}

		return $this->prepareKeywords( $this->getAllKeywords() );
	}

	/**
	 * Retrieves the default keywords for the archive template.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $postType The post type.
	 * @return array            The keywords.
	 */
	public function getArchiveKeywords( $postType ) {
		static $archiveKeywords = [];
		if ( isset( $archiveKeywords[ $postType ] ) ) {
			return $archiveKeywords[ $postType ];
		}

		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		if ( $dynamicOptions->searchAppearance->archives->has( $postType ) ) {
			$keywords = $this->extractMetaKeywords( aioseo()->dynamicOptions->searchAppearance->archives->{ $postType }->advanced->keywords );
		}

		$archiveKeywords[ $postType ] = empty( $keywords ) ? [] : $keywords;

		return $archiveKeywords[ $postType ];
	}

	/**
	 * Get generated keywords for an archive page.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of generated keywords.
	 */
	private function getGeneratedKeywords() {
		global $posts, $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		$keywords        = [];
		$isStaticArchive = aioseo()->helpers->isWooCommerceShopPage() || aioseo()->helpers->isStaticPostsPage();
		if ( $isStaticArchive ) {
			$keywords = $this->getAllKeywords();
		} elseif ( is_front_page() && ! aioseo()->helpers->isStaticHomePage() ) {
			$keywords = $this->extractMetaKeywords( aioseo()->options->searchAppearance->global->keywords );
		} elseif ( is_category() || is_tag() || is_tax() ) {
			$metaData = aioseo()->meta->metaData->getMetaData();
			if ( ! empty( $metaData->keywords ) ) {
				$keywords = $this->extractMetaKeywords( $metaData->keywords );
			}
		}

		$wpPosts = $posts;
		if ( empty( $posts ) ) {
			$wpPosts = array_filter( [ aioseo()->helpers->getPost() ] );
		}

		// Turn off current query so we can get specific post data.
		// phpcs:disable Squiz.NamingConventions.ValidVariableName
		$originalTag      = $wp_query->is_tag;
		$originalTax      = $wp_query->is_tax;
		$originalCategory = $wp_query->is_category;

		$wp_query->is_tag      = false;
		$wp_query->is_tax      = false;
		$wp_query->is_category = false;

		foreach ( $wpPosts as $post ) {
			$metaData    = aioseo()->meta->metaData->getMetaData( $post );
			$tmpKeywords = $this->extractMetaKeywords( $metaData->keywords );
			if ( count( $tmpKeywords ) ) {
				foreach ( $tmpKeywords as $keyword ) {
					$keywords[] = $keyword;
				}
			}
		}

		$wp_query->is_tag      = $originalTag;
		$wp_query->is_tax      = $originalTax;
		$wp_query->is_category = $originalCategory;
		// phpcs:enable Squiz.NamingConventions.ValidVariableName

		return $keywords;
	}

	/**
	 * Returns the keywords.
	 *
	 * @since 4.0.0
	 *
	 * @return array A list of unique keywords.
	 */
	public function getAllKeywords() {
		$keywords = [];
		$post     = aioseo()->helpers->getPost();
		$metaData = aioseo()->meta->metaData->getMetaData();
		if ( ! empty( $metaData->keywords ) ) {
			$keywords = $this->extractMetaKeywords( $metaData->keywords );
		}

		if ( $post ) {
			if ( aioseo()->options->searchAppearance->advanced->useTagsForMetaKeywords ) {
				$keywords = array_merge( $keywords, aioseo()->helpers->getAllTags( $post->ID ) );
			}

			if ( aioseo()->options->searchAppearance->advanced->useCategoriesForMetaKeywords && ! is_page() ) {
				$keywords = array_merge( $keywords, aioseo()->helpers->getAllCategories( $post->ID ) );
			}
		}

		return $keywords;
	}

	/**
	 * Prepares the keywords for display.
	 *
	 * @since 4.0.0
	 *
	 * @param  array  $keywords Raw keywords.
	 * @return string           A list of prepared keywords, comma-separated.
	 */
	public function prepareKeywords( $keywords ) {
		$keywords = $this->getUniqueKeywords( $keywords );
		$keywords = trim( $keywords );
		$keywords = aioseo()->helpers->internationalize( $keywords );
		$keywords = stripslashes( $keywords );
		$keywords = str_replace( '"', '', $keywords );
		$keywords = wp_filter_nohtml_kses( $keywords );

		return apply_filters( 'aioseo_keywords', $keywords );
	}

	/**
	 * Returns an array of keywords, based on a stringified list separated by commas.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $keywords The keywords string.
	 * @return array            The keywords.
	 */
	public function keywordStringToList( $keywords ) {
		$keywords = str_replace( '"', '', $keywords );

		return ! empty( $keywords ) ? explode( ',', $keywords ) : [];
	}

	/**
	 * Returns a stringified list of unique keywords, separated by commas.
	 *
	 * @since 4.0.0
	 *
	 * @param  array        $keywords The keywords.
	 * @param  boolean      $toString Whether or not to turn it into a comma separated string.
	 * @return string|array           The keywords.
	 */
	public function getUniqueKeywords( $keywords, $toString = true ) {
		$keywords = $this->keywordsToLowerCase( $keywords );

		return $toString ? implode( ',', $keywords ) : $keywords;
	}

	/**
	 * Returns the keywords in lowercase.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $keywords The keywords.
	 * @return array           The formatted keywords.
	 */
	private function keywordsToLowerCase( $keywords ) {
		$smallKeywords = [];
		if ( ! is_array( $keywords ) ) {
			$keywords = $this->keywordStringToList( $keywords );
		}
		if ( ! empty( $keywords ) ) {
			foreach ( $keywords as $keyword ) {
				$smallKeywords[] = trim( aioseo()->helpers->toLowercase( $keyword ) );
			}
		}

		return array_unique( $smallKeywords );
	}

	/**
	 * Extract keywords and then return as a string.
	 *
	 * @since 4.0.0
	 *
	 * @param  array|string $keywords An array of keywords or a json string.
	 * @return array                  An array of keywords that were extracted.
	 */
	public function extractMetaKeywords( $keywords ) {
		$extracted = [];

		$keywords = is_string( $keywords ) ? json_decode( $keywords ) : $keywords;

		if ( ! empty( $keywords ) ) {
			foreach ( $keywords as $keyword ) {
				$extracted[] = trim( $keyword->value );
			}
		}

		return $extracted;
	}
}Common/Meta/SiteVerification.php000066600000001650151135505570012657 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

/**
 * Handles the site verification meta tags.
 *
 * @since 4.0.0
 */
class SiteVerification {
	/**
	 * An array of webmaster tools and their meta names.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $webmasterTools = [
		'google'    => 'google-site-verification',
		'bing'      => 'msvalidate.01',
		'pinterest' => 'p:domain_verify',
		'yandex'    => 'yandex-verification',
		'baidu'     => 'baidu-site-verification'
	];

	/**
	 * Returns the robots meta tag value.
	 *
	 * @since 4.0.0
	 *
	 * @return mixed The robots meta tag value or false.
	 */
	public function meta() {
		$metaArray = [];
		foreach ( $this->webmasterTools as $key => $metaName ) {
			$value = aioseo()->options->webmasterTools->$key;
			if ( ! empty( $value ) ) {
				$metaArray[ $metaName ] = $value;
			}
		}

		return $metaArray;
	}
}Common/Meta/Robots.php000066600000026152151135505570010664 0ustar00<?php
namespace AIOSEO\Plugin\Common\Meta;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Handles the robots meta tag.
 *
 * @since 4.0.0
 */
class Robots {
	/**
	 * The robots meta tag attributes.
	 *
	 * We'll already set the keys on construction so that we always output the attributes in the same order.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $attributes = [
		'noindex'           => '',
		'nofollow'          => '',
		'noarchive'         => '',
		'nosnippet'         => '',
		'noimageindex'      => '',
		'noodp'             => '',
		'notranslate'       => '',
		'max-snippet'       => '',
		'max-image-preview' => '',
		'max-video-preview' => ''
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.16
	 */
	public function __construct() {
		add_action( 'wp_loaded', [ $this, 'unregisterWooCommerceNoindex' ] );
		add_action( 'template_redirect', [ $this, 'noindexFeed' ] );
		add_action( 'wp_head', [ $this, 'disableWpRobotsCore' ], -1 );
	}

	/**
	 * Prevents WooCommerce from noindexing the Cart/Checkout pages.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	public function unregisterWooCommerceNoindex() {
		if ( has_action( 'wp_head', 'wc_page_noindex' ) ) {
			remove_action( 'wp_head', 'wc_page_noindex' );
		}
	}

	/**
	 * Prevents WP Core from outputting its own robots meta tag.
	 *
	 * @since 4.0.16
	 *
	 * @return void
	 */
	public function disableWpRobotsCore() {
		remove_all_filters( 'wp_robots' );
	}

	/**
	 * Noindexes RSS feed pages.
	 *
	 * @since 4.0.17
	 *
	 * @return void
	 */
	public function noindexFeed() {
		if (
			! is_feed() ||
			( ! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default && ! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindexFeed )
		) {
			return;
		}

		header( 'X-Robots-Tag: noindex, follow', true );
	}

	/**
	 * Returns the robots meta tag value.
	 *
	 * @since 4.0.0
	 *
	 * @return mixed The robots meta tag value or false.
	 */
	public function meta() {
		// We need this check to happen first as spammers can attempt to make the page appear like a post or term by using URL params e.g. "cat=".
		if ( is_search() ) {
			$this->globalValues( [ 'archives', 'search' ] );

			return $this->metaHelper();
		}

		if ( BuddyPressIntegration::isComponentPage() ) {
			return aioseo()->standalone->buddyPress->component->getMeta( 'robots' );
		}

		if ( is_category() || is_tag() || is_tax() ) {
			$this->term();

			return $this->metaHelper();
		}

		if ( is_home() && 'page' !== get_option( 'show_on_front' ) ) {
			$this->globalValues();

			return $this->metaHelper();
		}

		$post = aioseo()->helpers->getPost();
		if ( $post ) {
			$this->post();

			return $this->metaHelper();
		}

		if ( is_author() ) {
			$this->globalValues( [ 'archives', 'author' ] );

			return $this->metaHelper();
		}

		if ( is_date() ) {
			$this->globalValues( [ 'archives', 'date' ] );

			return $this->metaHelper();
		}

		if ( is_404() ) {
			return apply_filters( 'aioseo_404_robots', 'noindex' );
		}

		if ( is_archive() ) {
			$this->archives();

			return $this->metaHelper();
		}
	}

	/**
	 * Stringifies and filters the robots meta tag value.
	 *
	 * Acts as a helper for meta().
	 *
	 * @since 4.0.0
	 *
	 * @param  bool         $array Whether or not to return the value as an array.
	 * @return array|string        The robots meta tag value.
	 */
	public function metaHelper( $array = false ) {
		$pageNumber = aioseo()->helpers->getPageNumber();
		if ( 1 < $pageNumber || aioseo()->helpers->getCommentPageNumber() ) {
			if (
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default ||
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindexPaginated
			) {
				$this->attributes['noindex'] = 'noindex';
			}

			if (
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default ||
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->nofollowPaginated
			) {
				$this->attributes['nofollow'] = 'nofollow';
			}
		}

		// Never allow users to noindex the first page of the homepage.
		if ( is_front_page() && 1 === $pageNumber ) {
			$this->attributes['noindex'] = '';
		}

		// Because we prevent WordPress Core from outputting a robots tag in disableWpRobotsCore(), we need to noindex/nofollow non-public sites ourselves.
		if ( ! get_option( 'blog_public' ) ) {
			$this->attributes['noindex']  = 'noindex';
			$this->attributes['nofollow'] = 'nofollow';
		}

		$this->attributes = array_filter( (array) apply_filters( 'aioseo_robots_meta', $this->attributes ) );

		return $array ? $this->attributes : implode( ', ', $this->attributes );
	}

	/**
	 * Sets the attributes for the current post.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|null $post The post object.
	 * @return void
	 */
	public function post( $post = null ) {
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		$post           = aioseo()->helpers->getPost( $post );
		$metaData       = aioseo()->meta->metaData->getMetaData( $post );

		if ( ! empty( $metaData ) && ! $metaData->robots_default ) {
			$this->metaValues( $metaData );

			return;
		}

		if ( $dynamicOptions->searchAppearance->postTypes->has( $post->post_type ) ) {
			$this->globalValues( [ 'postTypes', $post->post_type ], true );
		}
	}

	/**
	 * Returns the robots meta tag value for the current term.
	 *
	 * @since 4.0.6
	 *
	 * @param  \WP_Term|null $term The term object if any.
	 * @return void
	 */
	public function term( $term = null ) {
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		$term           = is_a( $term, 'WP_Term' ) ? $term : aioseo()->helpers->getTerm();

		// Misbehaving themes/plugins can manipulate the loop and make archives return a post as the queried object.
		if ( ! is_a( $term, 'WP_Term' ) ) {
			return;
		}

		if ( $dynamicOptions->searchAppearance->taxonomies->has( $term->taxonomy ) ) {
			$this->globalValues( [ 'taxonomies', $term->taxonomy ], true );

			return;
		}

		$this->globalValues();
	}

	/**
	 * Sets the attributes for the current archive.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function archives() {
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		$postType       = aioseo()->helpers->getTerm();
		if ( ! empty( $postType->name ) && $dynamicOptions->searchAppearance->archives->has( $postType->name ) ) {
			$this->globalValues( [ 'archives', $postType->name ], true );
		}
	}

	/**
	 * Sets the attributes based on the global values.
	 *
	 * @since 4.0.0
	 *
	 * @param  array   $optionOrder     The order in which the options need to be called to get the relevant robots meta settings.
	 * @param  boolean $isDynamicOption Whether this is for a dynamic option.
	 * @return void
	 */
	public function globalValues( $optionOrder = [], $isDynamicOption = false ) {
		$robotsMeta = [];
		if ( count( $optionOrder ) ) {
			$options = $isDynamicOption
				? aioseo()->dynamicOptions->noConflict( true )->searchAppearance
				: aioseo()->options->noConflict()->searchAppearance;

			foreach ( $optionOrder as $option ) {
				if ( ! $options->has( $option, false ) ) {
					return;
				}
				$options = $options->$option;
			}

			$clonedOptions = clone $options;
			if ( ! $clonedOptions->show ) {
				$this->attributes['noindex'] = 'noindex';
			}

			$robotsMeta = $options->advanced->robotsMeta->all();
			if ( $robotsMeta['default'] ) {
				$robotsMeta = aioseo()->options->searchAppearance->advanced->globalRobotsMeta->all();
			}
		} else {
			$robotsMeta = aioseo()->options->searchAppearance->advanced->globalRobotsMeta->all();
		}

		$this->attributes['max-image-preview'] = 'max-image-preview:large';

		if ( $robotsMeta['default'] ) {
			return;
		}

		if ( $robotsMeta['noindex'] ) {
			$this->attributes['noindex'] = 'noindex';
		}
		if ( $robotsMeta['nofollow'] ) {
			$this->attributes['nofollow'] = 'nofollow';
		}
		if ( $robotsMeta['noarchive'] ) {
			$this->attributes['noarchive'] = 'noarchive';
		}
		$noSnippet = $robotsMeta['nosnippet'];
		if ( $noSnippet ) {
			$this->attributes['nosnippet'] = 'nosnippet';
		}
		if ( $robotsMeta['noodp'] ) {
			$this->attributes['noodp'] = 'noodp';
		}
		if ( $robotsMeta['notranslate'] ) {
			$this->attributes['notranslate'] = 'notranslate';
		}
		$maxSnippet = $robotsMeta['maxSnippet'];
		if ( ! $noSnippet && is_numeric( $maxSnippet ) ) {
			$this->attributes['max-snippet'] = "max-snippet:$maxSnippet";
		}
		$maxImagePreview = $robotsMeta['maxImagePreview'];
		$noImageIndex    = $robotsMeta['noimageindex'];
		if ( ! $noImageIndex && $maxImagePreview && in_array( $maxImagePreview, [ 'none', 'standard', 'large' ], true ) ) {
			$this->attributes['max-image-preview'] = "max-image-preview:$maxImagePreview";
		}
		$maxVideoPreview = $robotsMeta['maxVideoPreview'];
		if ( isset( $maxVideoPreview ) && is_numeric( $maxVideoPreview ) ) {
			$this->attributes['max-video-preview'] = "max-video-preview:$maxVideoPreview";
		}

		// Check this last so that we can prevent max-image-preview from being output if noimageindex is enabled.
		if ( $noImageIndex ) {
			$this->attributes['max-image-preview'] = '';
			$this->attributes['noimageindex']      = 'noimageindex';
		}
	}

	/**
	 * Sets the attributes from the meta data.
	 *
	 * @since 4.0.0
	 *
	 * @param  \AIOSEO\Plugin\Common\Models\Post|\AIOSEO\Plugin\Pro\Models\Term $metaData The post/term meta data.
	 * @return void
	 */
	protected function metaValues( $metaData ) {
		if ( $metaData->robots_noindex || $this->isPasswordProtected() ) {
			$this->attributes['noindex'] = 'noindex';
		}
		if ( $metaData->robots_nofollow ) {
			$this->attributes['nofollow'] = 'nofollow';
		}
		if ( $metaData->robots_noarchive ) {
			$this->attributes['noarchive'] = 'noarchive';
		}
		if ( $metaData->robots_nosnippet ) {
			$this->attributes['nosnippet'] = 'nosnippet';
		}
		if ( $metaData->robots_noodp ) {
			$this->attributes['noodp'] = 'noodp';
		}
		if ( $metaData->robots_notranslate ) {
			$this->attributes['notranslate'] = 'notranslate';
		}
		if ( ! $metaData->robots_nosnippet && isset( $metaData->robots_max_snippet ) && is_numeric( $metaData->robots_max_snippet ) ) {
			$this->attributes['max-snippet'] = "max-snippet:$metaData->robots_max_snippet";
		}
		if ( ! $metaData->robots_noimageindex && $metaData->robots_max_imagepreview && in_array( $metaData->robots_max_imagepreview, [ 'none', 'standard', 'large' ], true ) ) {
			$this->attributes['max-image-preview'] = "max-image-preview:$metaData->robots_max_imagepreview";
		}
		if ( isset( $metaData->robots_max_videopreview ) && is_numeric( $metaData->robots_max_videopreview ) ) {
			$this->attributes['max-video-preview'] = "max-video-preview:$metaData->robots_max_videopreview";
		}

		// Check this last so that we can prevent max-image-preview from being output if noimageindex is enabled.
		if ( $metaData->robots_noimageindex ) {
			$this->attributes['max-image-preview'] = '';
			$this->attributes['noimageindex']      = 'noimageindex';
		}
	}

	/**
	 * Checks whether the current post is password protected.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether the post is password protected.
	 */
	private function isPasswordProtected() {
		$post = aioseo()->helpers->getPost();

		return is_object( $post ) && $post->post_password;
	}
}Common/Standalone/Standalone.php000066600000005446151135505570012711 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

use AIOSEO\Plugin\Pro\Standalone as ProStandalone;

/**
 * Registers the standalone components.
 *
 * @since 4.2.0
 */
class Standalone {
	/**
	 * HeadlineAnalyzer class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var HeadlineAnalyzer
	 */
	public $headlineAnalyzer = null;

	/**
	 * FlyoutMenu class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var FlyoutMenu
	 */
	public $flyoutMenu = null;

	/**
	 * SeoPreview class instance.
	 *
	 * @since 4.2.8
	 *
	 * @var SeoPreview
	 */
	public $seoPreview = null;

	/**
	 * SetupWizard class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var SetupWizard
	 */
	public $setupWizard = null;

	/**
	 * PrimaryTerm class instance.
	 *
	 * @since 4.3.6
	 *
	 * @var PrimaryTerm
	 */
	public $primaryTerm = null;

	/**
	 * UserProfileTab class instance.
	 *
	 * @since 4.5.4
	 *
	 * @var UserProfileTab
	 */
	public $userProfileTab = null;

	/**
	 * BuddyPress class instance.
	 *
	 * @since 4.7.6
	 *
	 * @var BuddyPress\BuddyPress
	 */
	public $buddyPress = null;

	/**
	 * BbPress class instance.
	 *
	 * @since 4.8.1
	 *
	 * @var BbPress\BbPress
	 */
	public $bbPress = null;

	/**
	 * List of page builder integration class instances.
	 *
	 * @since 4.2.7
	 *
	 * @var array[Object]
	 */
	public $pageBuilderIntegrations = [];

	/**
	 * List of block class instances.
	 *
	 * @since 4.2.7
	 *
	 * @var array[Object]
	 */
	public $standaloneBlocks = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.2.0
	 */
	public function __construct() {
		$this->headlineAnalyzer = new HeadlineAnalyzer();
		$this->flyoutMenu       = new FlyoutMenu();
		$this->seoPreview       = new SeoPreview();
		$this->setupWizard      = new SetupWizard();
		$this->primaryTerm      = aioseo()->pro ? new ProStandalone\PrimaryTerm() : new PrimaryTerm();
		$this->userProfileTab   = new UserProfileTab();
		$this->buddyPress       = aioseo()->pro ? new ProStandalone\BuddyPress\BuddyPress() : new BuddyPress\BuddyPress();
		$this->bbPress          = aioseo()->pro ? new ProStandalone\BbPress\BbPress() : new BbPress\BbPress();

		aioseo()->pro ? new ProStandalone\DetailsColumn() : new DetailsColumn();

		new AdminBarNoindexWarning();
		new LimitModifiedDate();
		new Notifications();
		new PublishPanel();
		new WpCode();

		$this->pageBuilderIntegrations = [
			'elementor'  => new PageBuilders\Elementor(),
			'divi'       => new PageBuilders\Divi(),
			'seedprod'   => new PageBuilders\SeedProd(),
			'wpbakery'   => new PageBuilders\WPBakery(),
			'avada'      => new PageBuilders\Avada(),
			'siteorigin' => new PageBuilders\SiteOrigin(),
			'thrive'     => new PageBuilders\ThriveArchitect()
		];

		$this->standaloneBlocks = [
			'tocBlock' => new Blocks\TableOfContents(),
			'faqBlock' => new Blocks\FaqPage()
		];
	}
}Common/Standalone/HeadlineAnalyzer.php000066600000040326151135505570014034 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

/**
 * Handles the headline analysis.
 *
 * @since 4.1.2
 */
class HeadlineAnalyzer {
	/**
	 * Class constructor.
	 *
	 * @since 4.1.2
	 */
	public function __construct() {
		if ( ! is_admin() || wp_doing_cron() ) {
			return;
		}

		add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue' ] );

		if ( ! aioseo()->options->advanced->headlineAnalyzer ) {
			return;
		}

		add_filter( 'monsterinsights_headline_analyzer_enabled', '__return_false' );
		add_filter( 'exactmetrics_headline_analyzer_enabled', '__return_false' );
	}

	/**
	 * Enqueues the headline analyzer.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	public function enqueue() {
		if (
			! aioseo()->helpers->isScreenBase( 'post' ) ||
			! aioseo()->access->hasCapability( 'aioseo_page_analysis' )
		) {
			return;
		}

		if ( ! aioseo()->options->advanced->headlineAnalyzer ) {
			return;
		}

		$path = '/vendor/jwhennessey/phpinsight/autoload.php';
		if ( ! aioseo()->core->fs->exists( AIOSEO_DIR . $path ) ) {
			return;
		}
		require AIOSEO_DIR . $path;

		aioseo()->core->assets->load( 'src/vue/standalone/headline-analyzer/main.js' );
	}

	/**
	 * Returns the result of the analsyis.
	 *
	 * @since 4.1.2
	 *
	 * @param  string $title The title.
	 * @return array         The result.
	 */
	public function getResult( $title ) {
		$result = $this->getHeadlineScore( html_entity_decode( $title ) );

		return [
			'result'   => $result,
			'analysed' => ! $result->err,
			'sentence' => ucwords( wp_unslash( sanitize_text_field( $title ) ) ),
			'score'    => ! empty( $result->score ) ? $result->score : 0
		];
	}

	/**
	 * Returns the score.
	 *
	 * @since 4.1.2
	 *
	 * @param  string    $title The title.
	 * @return \stdClass        The result.
	 */
	public function getHeadlineScore( $title ) {
		$result                           = new \stdClass();
		$result->originalExplodedHeadline = explode( ' ', wp_unslash( $title ) );

		// Strip useless characters and whitespace.
		$title = preg_replace( '/[^A-Za-z0-9 ]/', '', (string) $title );
		$title = preg_replace( '!\s+!', ' ', (string) $title );
		$title = strtolower( $title );

		$result->input = $title;

		// If the headline is invalid, return an error.
		if ( ! $title || ' ' === $title || trim( $title ) === '' ) {
			$result->err = true;
			$result->msg = 'The headline is invalid.';

			return $result;
		}

		$totalScore               = 0;
		$explodedHeadline         = explode( ' ', $title );
		$result->explodedHeadline = $explodedHeadline;
		$result->err              = false;

		// The optimal length is 55 characters.
		$result->length = strlen( str_replace( ' ', '', $title ) );
		$totalScore     = $totalScore + 3;

		//phpcs:disable Squiz.ControlStructures.ControlSignature
		if ( $result->length <= 19 ) { $totalScore += 5; }
		elseif ( $result->length >= 20 && $result->length <= 34 ) { $totalScore += 8; }
		elseif ( $result->length >= 35 && $result->length <= 66 ) { $totalScore += 11; }
		elseif ( $result->length >= 67 && $result->length <= 79 ) { $totalScore += 8; }
		elseif ( $result->length >= 80 ) { $totalScore += 5; }

		// The average headline is 6-7 words long.
		$result->wordCount = count( $explodedHeadline );
		$totalScore        = $totalScore + 3;

		if ( 0 === $result->wordCount ) { $totalScore = 0; }
		elseif ( $result->wordCount >= 2 && $result->wordCount <= 4 ) { $totalScore += 5; }
		elseif ( $result->wordCount >= 5 && $result->wordCount <= 9 ) { $totalScore += 11; }
		elseif ( $result->wordCount >= 10 && $result->wordCount <= 11 ) { $totalScore += 8; }
		elseif ( $result->wordCount >= 12 ) { $totalScore += 5; }

		// Check for power words, emotional words, etc.
		$result->powerWords               = $this->matchWords( $result->input, $result->explodedHeadline, $this->powerWords() );
		$result->powerWordsPercentage     = count( $result->powerWords ) / $result->wordCount;
		$result->emotionWords             = $this->matchWords( $result->input, $result->explodedHeadline, $this->emotionPowerWords() );
		$result->emotionalWordsPercentage = count( $result->emotionWords ) / $result->wordCount;
		$result->commonWords              = $this->matchWords( $result->input, $result->explodedHeadline, $this->commonWords() );
		$result->commonWordsPercentage    = count( $result->commonWords ) / $result->wordCount;
		$result->uncommonWords            = $this->matchWords( $result->input, $result->explodedHeadline, $this->uncommonWords() );
		$result->uncommonWordsPercentage  = count( $result->uncommonWords ) / $result->wordCount;
		$result->detectedWordTypes        = [];

		if ( $result->emotionalWordsPercentage < 0.1 ) {
			$result->detectedWordTypes[] = 'emotion';
		} else {
			$totalScore = $totalScore + 15;
		}

		if ( $result->commonWordsPercentage < 0.2 ) {
			$result->detectedWordTypes[] = 'common';
		} else {
			$totalScore = $totalScore + 11;
		}

		if ( $result->uncommonWordsPercentage < 0.1 ) {
			$result->detectedWordTypes[] = 'uncommon';
		} else {
			$totalScore = $totalScore + 15;
		}

		if ( count( $result->powerWords ) < 1 ) {
			$result->detectedWordTypes[] = 'power';
		} else {
			$totalScore = $totalScore + 19;
		}

		if (
			$result->emotionalWordsPercentage >= 0.1 &&
			$result->commonWordsPercentage >= 0.2 &&
			$result->uncommonWordsPercentage >= 0.1 &&
			count( $result->powerWords ) >= 1
		) {
			$totalScore = $totalScore + 3;
		}

		$sentiment         = new \PHPInsight\Sentiment();
		$sentimentClass    = $sentiment->categorise( $title );
		$result->sentiment = $sentimentClass;

		$totalScore = $totalScore + ( 'pos' === $result->sentiment ? 10 : ( 'neg' === $result->sentiment ? 10 : 7 ) );

		$headlineTypes = [];
		if ( strpos( $title, 'how to' ) !== false || strpos( $title, 'howto' ) !== false ) {
			$headlineTypes[] = __( 'How-To', 'all-in-one-seo-pack' );
			$totalScore      = $totalScore + 7;
		}

		$listWords = array_intersect( $explodedHeadline, $this->numericalIndicators() );
		if ( preg_match( '~[0-9]+~', (string) $title ) || ! empty( $listWords ) ) {
			$headlineTypes[] = __( 'List', 'all-in-one-seo-pack' );
			$totalScore      = $totalScore + 7;
		}

		if ( in_array( $explodedHeadline[0], $this->primaryQuestionIndicators(), true ) ) {
			if ( in_array( $explodedHeadline[1], $this->secondaryQuestionIndicators(), true ) ) {
				$headlineTypes[] = __( 'Question', 'all-in-one-seo-pack' );
				$totalScore      = $totalScore + 7;
			}
		}

		if ( empty( $headlineTypes ) ) {
			$headlineTypes[] = __( 'General', 'all-in-one-seo-pack' );
			$totalScore      = $totalScore + 5;
		}

		$result->headlineTypes = $headlineTypes;
		$result->score         = $totalScore >= 93 ? 93 : $totalScore;

		return $result;
	}

	/**
	* Tries to find matches for power words, emotional words, etc. in the headline.
	*
	* @since 4.1.2
	*
	* @param  string $headline         The headline.
	* @param  array  $explodedHeadline The exploded headline.
	* @param  array  $words            The words to match.
	* @return array                    The matches that were found.
	*/
	public function matchWords( $headline, $explodedHeadline, $words ) {
		$foundMatches = [];
		foreach ( $words as $word ) {
			$strippedWord = preg_replace( '/[^A-Za-z0-9 ]/', '', (string) $word );

			// Check if word is a phrase.
			if ( strpos( $word, ' ' ) !== false ) {
				if ( strpos( $headline, $strippedWord ) !== false ) {
					$foundMatches[] = $word;
				}
				continue;
			}
			// Check if it is a single word.
			if ( in_array( $strippedWord, $explodedHeadline, true ) ) {
				$foundMatches[] = $word;
			}
		}

		return $foundMatches;
	}

	/**
	 * Returns a list of numerical indicators.
	 *
	 * @since 4.1.2
	 *
	 * @return array The list of numerical indicators.
	 */
	private function numericalIndicators() {
		return [
			'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'eleven', 'twelve', 'thirt', 'fift', 'hundred', 'thousand' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
		];
	}

	/**
	 * Returns a list of primary question indicators.
	 *
	 * @since 4.1.2
	 *
	 * @return array The list of primary question indicators.
	 */
	private function primaryQuestionIndicators() {
		return [
			'where', 'when', 'how', 'what', 'have', 'has', 'does', 'do', 'can', 'are', 'will' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
		];
	}

	/**
	 * Returns a list of secondary question indicators.
	 *
	 * @since 4.1.2
	 *
	 * @return array The list of secondary question indicators.
	 */
	private function secondaryQuestionIndicators() {
		return [
			'you', 'they', 'he', 'she', 'your', 'it', 'they', 'my', 'have', 'has', 'does', 'do', 'can', 'are', 'will' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
		];
	}

	/**
	 * Returns a list of power words.
	 *
	 * @since 4.1.2
	 *
	 * @return array The list of power words.
	 */
	private function powerWords() {
		return [
			'great', 'free', 'focus', 'remarkable', 'confidential', 'sale', 'wanted', 'obsession', 'sizable', 'new', 'absolutely lowest', 'surging', 'wonderful', 'professional', 'interesting', 'revisited', 'delivered', 'guaranteed', 'challenge', 'unique', 'secrets', 'special', 'lifetime', 'bargain', 'scarce', 'tested', 'highest', 'hurry', 'alert famous', 'improved', 'expert', 'daring', 'strong', 'immediately', 'advice', 'pioneering', 'unusual', 'limited', 'the truth about', 'destiny', 'outstanding', 'simplistic', 'compare', 'unsurpassed', 'energy', 'powerful', 'colorful', 'genuine', 'instructive', 'big', 'affordable', 'informative', 'liberal', 'popular', 'ultimate', 'mainstream', 'rare', 'exclusive', 'willpower', 'complete', 'edge', 'valuable', 'attractive', 'last chance', 'superior', 'how to', 'easily', 'exploit', 'unparalleled', 'endorsed', 'approved', 'quality', 'fascinating', 'unlimited', 'competitive', 'gigantic', 'compromise', 'discount', 'full', 'love', 'odd', 'fundamentals', 'mammoth', 'lavishly', 'bottom line', 'under priced', 'innovative', 'reliable', 'zinger', 'suddenly', 'it\'s here', 'terrific', 'simplified', 'perspective', 'just arrived', 'breakthrough', 'tremendous', 'launching', 'sure fire', 'emerging', 'helpful', 'skill', 'soar', 'profitable', 'special offer', 'reduced', 'beautiful', 'sampler', 'technology', 'better', 'crammed', 'noted', 'selected', 'shrewd', 'growth', 'luxury', 'sturdy', 'enormous', 'promising', 'unconditional', 'wealth', 'spotlight', 'astonishing', 'timely', 'successful', 'useful', 'imagination', 'bonanza', 'opportunities', 'survival', 'greatest', 'security', 'last minute', 'largest', 'high tech', 'refundable', 'monumental', 'colossal', 'latest', 'quickly', 'startling', 'now', 'important', 'revolutionary', 'quick', 'unlock', 'urgent', 'miracle', 'easy', 'fortune', 'amazing', 'magic', 'direct', 'authentic', 'exciting', 'proven', 'simple', 'announcing', 'portfolio', 'reward', 'strange', 'huge gift', 'revealing', 'weird', 'value', 'introducing', 'sensational', 'surprise', 'insider', 'practical', 'excellent', 'delighted', 'download' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
		];
	}

	/**
	 * Returns a list of common words.
	 *
	 * @since 4.1.2
	 *
	 * @return array The list of common words.
	 */
	private function commonWords() {
		return [
			'a', 'for', 'about', 'from', 'after', 'get', 'all', 'has', 'an', 'have', 'and', 'he', 'are', 'her', 'as', 'his', 'at', 'how', 'be', 'I', 'but', 'if', 'by', 'in', 'can', 'is', 'did', 'it', 'do', 'just', 'ever', 'like', 'll', 'these', 'me', 'they', 'most', 'things', 'my', 'this', 'no', 'to', 'not', 'up', 'of', 'was', 'on', 'what', 're', 'when', 'she', 'who', 'sould', 'why', 'so', 'will', 'that', 'with', 'the', 'you', 'their', 'your', 'there' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
		];
	}

	/**
	 * Returns a list of uncommon words.
	 *
	 * @since 4.1.2
	 *
	 * @return array The list of uncommon words.
	 */
	private function uncommonWords() {
		return [
			'actually', 'happened', 'need', 'thing', 'awesome', 'heart', 'never', 'think', 'baby', 'here', 'new', 'time', 'beautiful', 'its', 'now', 'valentines', 'being', 'know', 'old', 'video', 'best', 'life', 'one', 'want', 'better', 'little', 'out', 'watch', 'boy', 'look', 'people', 'way', 'dog', 'love', 'photos', 'ways', 'down', 'made', 'really', 'world', 'facebook', 'make', 'reasons', 'year', 'first', 'makes', 'right', 'years', 'found', 'man', 'see', 'you’ll', 'girl', 'media', 'seen', 'good', 'mind', 'social', 'guy', 'more', 'something' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
		];
	}

	/**
	 * Returns a list of emotional power words.
	 *
	 * @since 4.1.2
	 *
	 * @return array The list of emotional power words.
	 */
	private function emotionPowerWords() {
		return [
			'destroy', 'extra', 'in a', 'devastating', 'eye-opening', 'gift', 'in the world', 'devoted', 'fail', 'in the', 'faith', 'grateful', 'inexpensive', 'dirty', 'famous', 'disastrous', 'fantastic', 'greed', 'grit', 'insanely', 'disgusting', 'fearless', 'disinformation', 'feast', 'insidious', 'dollar', 'feeble', 'gullible', 'double', 'fire', 'hack', 'fleece', 'had enough', 'invasion', 'drowning', 'floundering', 'happy', 'ironclad', 'dumb', 'flush', 'hate', 'irresistibly', 'hazardous', 'is the', 'fool', 'is what happens when', 'fooled', 'helpless', 'it looks like a', 'embarrass', 'for the first time', 'help are the', 'jackpot', 'forbidden', 'hidden', 'jail', 'empower', 'force-fed', 'high', 'jaw-dropping', 'forgotten', 'jeopardy', 'energize', 'hoax', 'jubilant', 'foul', 'hope', 'killer', 'frantic', 'horrific', 'know it all', 'epic', 'how to make', 'evil', 'freebie', 'frenzy', 'hurricane', 'excited', 'fresh on the mind', 'frightening', 'hypnotic', 'lawsuit', 'frugal', 'illegal', 'fulfill', 'lick', 'explode', 'lies', 'exposed', 'gambling', 'like a normal', 'nightmare', 'results', 'line', 'no good', 'pound', 'loathsome', 'no questions asked', 'revenge', 'lonely', 'looks like a', 'obnoxious', 'preposterous', 'revolting', 'looming', 'priced', 'lost', 'prison', 'lowest', 'of the', 'privacy', 'rich', 'lunatic', 'off-limits', 'private', 'risky', 'lurking', 'offer', 'prize', 'ruthless', 'lust', 'official', 'luxurious', 'on the', 'profit', 'scary', 'lying', 'outlawed', 'protected', 'scream', 'searing', 'overcome', 'provocative', 'make you', 'painful', 'pummel', 'secure', 'pale', 'punish', 'marked down', 'panic', 'quadruple', 'seductively', 'massive', 'pay zero', 'seize', 'meltdown', 'payback', 'might look like a', 'peril', 'mind-blowing', 'shameless', 'minute', 'rave', 'shatter', 'piranha', 'reckoning', 'shellacking', 'mired', 'pitfall', 'reclaim', 'mistakes', 'plague', 'sick and tired', 'money', 'played', 'refugee', 'silly', 'money-grubbing', 'pluck', 'refund', 'moneyback', 'plummet', 'plunge', 'murder', 'pointless', 'sinful', 'myths', 'poor', 'remarkably', 'six-figure', 'never again', 'research', 'surrender', 'to the', 'varify', 'skyrocket', 'toxic', 'vibrant', 'slaughter', 'swindle', 'trap', 'victim', 'sleazy', 'taboo', 'treasure', 'victory', 'smash', 'tailspin', 'vindication', 'smug', 'tank', 'triple', 'viral', 'smuggled', 'tantalizing', 'triumph', 'volatile', 'sniveling', 'targeted', 'truth', 'vulnerable', 'snob', 'tawdry', 'try before you buy', 'tech', 'turn the tables', 'wanton', 'soaring', 'warning', 'teetering', 'unauthorized', 'spectacular', 'temporary fix', 'unbelievably', 'spine', 'tempting', 'uncommonly', 'what happened', 'spirit', 'what happens when', 'terror', 'under', 'what happens', 'staggering', 'underhanded', 'what this', 'that will make you', 'undo","when you see', 'that will make', 'unexpected', 'when you', 'strangle', 'that will', 'whip', 'the best', 'whopping', 'stuck up', 'the ranking of', 'wicked', 'stunning', 'the most', 'will make you', 'stupid', 'the reason why is', 'unscrupulous', 'thing ive ever seen', 'withheld', 'this is the', 'this is what happens', 'unusually', 'wondrous', 'this is what', 'uplifting', 'worry', 'sure', 'this is', 'wounded', 'surge', 'thrilled', 'you need to know', 'thrilling', 'valor', 'you need to', 'you see what', 'surprising', 'tired', 'you see', 'surprisingly', 'to be', 'vaporize' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
		];
	}
}Common/Standalone/Notifications.php000066600000001357151135505570013427 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Handles the notifications standalone.
 *
 * @since 4.2.0
 */
class Notifications {
	/**
	 * Class constructor.
	 *
	 * @since 4.2.0
	 */
	public function __construct() {
		if ( ! is_admin() ) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScript' ] );
	}

	/**
	 * Enqueues the script.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	public function enqueueScript() {
		aioseo()->core->assets->load( 'src/vue/standalone/notifications/main.js', [], [
			'newNotifications' => count( Models\Notification::getNewNotifications() )
		], 'aioseoNotifications' );
	}
}Common/Standalone/PrimaryTerm.php000066600000002402151135505570013061 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

/**
 * Handles the Primary Term feature.
 *
 * @since 4.3.6
 */
class PrimaryTerm {
	/**
	 * Class constructor.
	 *
	 * @since 4.3.6
	 */
	public function __construct() {
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}

		if ( wp_doing_ajax() || wp_doing_cron() || ! is_admin() ) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAssets' ] );
	}

	/**
	 * Enqueues the JS/CSS for the on page/posts settings.
	 *
	 * @since 4.3.6
	 *
	 * @return void
	 */
	public function enqueueAssets() {
		if ( ! aioseo()->helpers->isScreenBase( 'post' ) ) {
			return;
		}

		aioseo()->core->assets->load( 'src/vue/standalone/primary-term/main.js', [], aioseo()->helpers->getVueData( 'post' ) );
	}

	/**
	 * Returns the primary term for the given taxonomy name.
	 *
	 * @since 4.3.6
	 *
	 * @param  int            $postId       The post ID.
	 * @param  string         $taxonomyName The taxonomy name.
	 * @return \WP_Term|false               The term or false.
	 */
	public function getPrimaryTerm( $postId, $taxonomyName ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return false;
	}
}Common/Standalone/AdminBarNoindexWarning.php000066600000003166151135505570015146 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Handles the admin bar noindex warning.
 *
 * @since 4.6.7
 */
class AdminBarNoindexWarning {
	/**
	 * Class constructor.
	 *
	 * @since 4.6.7
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'init' ] );
	}

	/**
	 * Initializes the standalone.
	 *
	 * @since 4.6.7
	 *
	 * @return void
	 */
	public function init() {
		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		$isSitePublic = get_option( 'blog_public' );
		if ( $isSitePublic ) {
			return;
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScript' ] );
		add_action( 'wp_enqueue_scripts', [ $this, 'enqueueScript' ] );

		add_action( 'admin_bar_menu', [ $this, 'addAdminBarElement' ], 99999 );
	}

	/**
	 * Enqueues the script.
	 *
	 * @since 4.6.7
	 *
	 * @return void
	 */
	public function enqueueScript() {
		aioseo()->core->assets->load( 'src/vue/standalone/admin-bar-noindex-warning/main.js', [], [
			'optionsReadingUrl' => admin_url( 'options-reading.php' ),
		], 'aioseoAdminBarNoindexWarning' );
	}

	/**
	 * Adds the admin bar element.
	 *
	 * @since 4.6.7
	 *
	 * @param  \WP_Admin_Bar $wpAdminBar The admin bar object.
	 * @return void
	 */
	public function addAdminBarElement( $wpAdminBar ) {
		$wpAdminBar->add_node(
			[
				'id'    => 'aioseo-admin-bar-noindex-warning',
				'title' => __( 'Search Engines Blocked!', 'all-in-one-seo-pack' ),
				'href'  => admin_url( 'options-reading.php' )
			]
		);
	}
}Common/Standalone/SeoPreview.php000066600000021725151135505570012707 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

use AIOSEO\Plugin\Common\Models;
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Handles the SEO Preview feature on the front-end.
 *
 * @since 4.2.8
 */
class SeoPreview {
	/**
	 * Whether this feature is allowed on the current page or not.
	 *
	 * @since 4.2.8
	 *
	 * @var bool
	 */
	private $enable = false;

	/**
	 * The relative JS filename for this standalone.
	 *
	 * @since 4.3.1
	 *
	 * @var string
	 */
	private $mainAssetRelativeFilename = 'src/vue/standalone/seo-preview/main.js';

	/**
	 * Class constructor.
	 *
	 * @since 4.2.8
	 */
	public function __construct() {
		// Allow users to disable SEO Preview.
		if ( apply_filters( 'aioseo_seo_preview_disable', false ) ) {
			return;
		}

		// Hook into `wp` in order to have access to the WP queried object.
		add_action( 'wp', [ $this, 'init' ], 20 );
	}

	/**
	 * Initialize the feature.
	 * Hooked into `wp` action hook.
	 *
	 * @since 4.2.8
	 *
	 * @return void
	 */
	public function init() {
		if (
			is_admin() ||
			! aioseo()->helpers->isAdminBarEnabled() ||
			// If we're seeing the Divi theme Visual Builder.
			( function_exists( 'et_core_is_fb_enabled' ) && et_core_is_fb_enabled() ) ||
			aioseo()->helpers->isAmpPage()
		) {
			return;
		}

		$allow = [
			'archive',
			'attachment',
			'author',
			'date',
			'dynamic_home',
			'page',
			'search',
			'single',
			'taxonomy',
		];

		if ( ! in_array( aioseo()->helpers->getTemplateType(), $allow, true ) ) {
			return;
		}

		$this->enable = true;

		// Prevent Autoptimize from optimizing the translations for the SEO Preview. If we don't do this, Autoptimize can break the frontend for certain languages - #5235.
		if ( is_user_logged_in() && 'en_US' !== get_user_locale() ) {
			add_filter( 'autoptimize_filter_noptimize', '__return_true' );
		}

		// As WordPress uses priority 10 to print footer scripts we use 9 to make sure our script still gets output.
		add_action( 'wp_print_footer_scripts', [ $this, 'enqueueScript' ], 9 );
	}

	/**
	 * Hooked into `wp_print_footer_scripts` action hook.
	 * Enqueue the standalone JS the latest possible and prevent 3rd-party performance plugins from merging it.
	 *
	 * @since 4.3.1
	 *
	 * @return void
	 */
	public function enqueueScript() {
		aioseo()->core->assets->load( $this->mainAssetRelativeFilename, [], $this->getVueData(), 'aioseoSeoPreview' );
		aioseo()->main->enqueueTranslations();
	}

	/**
	 * Returns the data for Vue.
	 *
	 * @since 4.2.8
	 *
	 * @return array The data.
	 */
	private function getVueData() {
		$data = [
			'editGoogleSnippetUrl'   => '',
			'editFacebookSnippetUrl' => '',
			'editTwitterSnippetUrl'  => '',
			'editObjectBtnText'      => '',
			'editObjectUrl'          => '',
			'keyphrases'             => '',
			'page_analysis'          => '',
			'urls'                   => [
				'home'        => home_url(),
				'domain'      => aioseo()->helpers->getSiteDomain(),
				'mainSiteUrl' => aioseo()->helpers->getSiteUrl(),
			],
			'mainAssetCssQueue'      => aioseo()->core->assets->getJsAssetCssQueue( $this->mainAssetRelativeFilename ),
			'data'                   => [
				'isDev'           => aioseo()->helpers->isDev(),
				'siteName'        => aioseo()->helpers->getWebsiteName(),
				'usingPermalinks' => aioseo()->helpers->usingPermalinks()
			]
		];

		if ( BuddyPressIntegration::isComponentPage() ) {
			return array_merge( $data, aioseo()->standalone->buddyPress->getVueDataSeoPreview() );
		}

		$queriedObject = get_queried_object(); // Don't use the getTerm helper here.
		$templateType  = aioseo()->helpers->getTemplateType();
		if (
			'taxonomy' === $templateType ||
			'single' === $templateType ||
			'page' === $templateType ||
			'attachment' === $templateType
		) {
			$labels = null;

			if ( is_a( $queriedObject, 'WP_Term' ) ) {
				$wpObject              = $queriedObject;
				$labels                = get_taxonomy_labels( get_taxonomy( $queriedObject->taxonomy ) );
				$data['editObjectUrl'] = get_edit_term_link( $queriedObject, $queriedObject->taxonomy );
			} else {
				$wpObject = aioseo()->helpers->getPost();

				if ( is_a( $wpObject, 'WP_Post' ) ) {
					$labels                = get_post_type_labels( get_post_type_object( $wpObject->post_type ) );
					$data['editObjectUrl'] = get_edit_post_link( $wpObject, 'url' );

					if (
						! aioseo()->helpers->isSpecialPage( $wpObject->ID ) &&
						'attachment' !== $templateType
					) {
						$aioseoPost            = Models\Post::getPost( $wpObject->ID );
						$data['page_analysis'] = Models\Post::getPageAnalysisDefaults( $aioseoPost->page_analysis );
						$data['keyphrases']    = Models\Post::getKeyphrasesDefaults( $aioseoPost->keyphrases );
					}
				}
			}

			// At this point if `$wpObject` is not an instance of WP_Term nor WP_Post, then we can't have the URLs.
			if (
				is_object( $wpObject ) &&
				is_object( $labels )
			) {
				$data['editObjectBtnText'] = sprintf(
					// Translators: 1 - A noun for something that's being edited ("Post", "Page", "Article", "Product", etc.).
					esc_html__( 'Edit %1$s', 'all-in-one-seo-pack' ),
					$labels->singular_name
				);
				$data['editGoogleSnippetUrl']   = $this->getEditSnippetUrl( $templateType, 'google', $wpObject );
				$data['editFacebookSnippetUrl'] = $this->getEditSnippetUrl( $templateType, 'facebook', $wpObject );
				$data['editTwitterSnippetUrl']  = $this->getEditSnippetUrl( $templateType, 'twitter', $wpObject );
			}
		}

		if (
			'archive' === $templateType ||
			'author' === $templateType ||
			'date' === $templateType ||
			'search' === $templateType
		) {
			if ( is_a( $queriedObject, 'WP_User' ) ) {
				$data['editObjectUrl']     = get_edit_user_link( $queriedObject->ID );
				$data['editObjectBtnText'] = esc_html__( 'Edit User', 'all-in-one-seo-pack' );
			}

			$data['editGoogleSnippetUrl'] = $this->getEditSnippetUrl( $templateType, 'google' );
		}

		if ( 'dynamic_home' === $templateType ) {
			$data['editGoogleSnippetUrl']   = $this->getEditSnippetUrl( $templateType, 'google' );
			$data['editFacebookSnippetUrl'] = $this->getEditSnippetUrl( $templateType, 'facebook' );
			$data['editTwitterSnippetUrl']  = $this->getEditSnippetUrl( $templateType, 'twitter' );
		}

		return $data;
	}

	/**
	 * Get the URL to the place where the snippet details can be edited.
	 *
	 * @since 4.2.8
	 *
	 * @param  string                 $templateType The WP template type {@see WpContext::getTemplateType}.
	 * @param  string                 $snippet      'google', 'facebook' or 'twitter'.
	 * @param  \WP_Post|\WP_Term|null $object       Post or term object.
	 * @return string                               The URL. Returns an empty string if nothing matches.
	 */
	private function getEditSnippetUrl( $templateType, $snippet, $object = null ) {
		$url = '';

		// Bail if `$snippet` doesn't fit requirements.
		if ( ! in_array( $snippet, [ 'google', 'facebook', 'twitter' ], true ) ) {
			return $url;
		}

		// If we're in a post/page/term (not an attachment) we'll have a URL directly to the meta box.
		if ( in_array( $templateType, [ 'single', 'page', 'attachment', 'taxonomy' ], true ) ) {
			$url = 'taxonomy' === $templateType
				? get_edit_term_link( $object, $object->taxonomy ) . '#aioseo-term-settings-field'
				: get_edit_post_link( $object, 'url' ) . '#aioseo-settings';

			$queryArgs = [ 'aioseo-tab' => 'general' ];
			if ( in_array( $snippet, [ 'facebook', 'twitter' ], true ) ) {
				$queryArgs = [
					'aioseo-tab' => 'social',
					'social-tab' => $snippet
				];
			}

			return add_query_arg( $queryArgs, $url );
		}

		// If we're in any sort of archive let's point to the global archive editing.
		if ( in_array( $templateType, [ 'archive', 'author', 'date', 'search' ], true ) ) {
			return admin_url( 'admin.php?page=aioseo-search-appearance' ) . '#/archives';
		}

		// If homepage is set to show the latest posts let's point to the global home page editing.
		if ( 'dynamic_home' === $templateType ) {
			// Default `$url` for 'google' snippet.
			$url = add_query_arg(
				[ 'aioseo-scroll' => 'home-page-settings' ],
				admin_url( 'admin.php?page=aioseo-search-appearance' ) . '#/global-settings'
			);

			if ( in_array( $snippet, [ 'facebook', 'twitter' ], true ) ) {
				$url = admin_url( 'admin.php?page=aioseo-social-networks' ) . '#/' . $snippet;
			}

			return $url;
		}

		return $url;
	}

	/**
	 * Returns the "SEO Preview" submenu item data ("node" as WP calls it).
	 *
	 * @since 4.2.8
	 *
	 * @return array The admin bar menu item data or an empty array if this feature is disabled.
	 */
	public function getAdminBarMenuItemNode() {
		if ( ! $this->enable ) {
			return [];
		}

		$title = esc_html__( 'SEO Preview', 'all-in-one-seo-pack' );

		// @TODO Remove 'NEW' after a couple months.
		$title .= '<span class="aioseo-menu-new-indicator">';
		$title .= esc_html__( 'NEW', 'all-in-one-seo-pack' ) . '!';
		$title .= '</span>';

		return [
			'id'     => 'aioseo-seo-preview',
			'parent' => 'aioseo-main',
			'title'  => $title,
			'href'   => '#',
		];
	}
}Common/Standalone/BuddyPress/Sitemap.php000066600000015654151135505570014311 0ustar00<?php

namespace AIOSEO\Plugin\Common\Standalone\BuddyPress;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * BuddyPress Sitemap class.
 *
 * @since 4.7.6
 */
class Sitemap {
	/**
	 * Returns the indexes for the sitemap root index.
	 *
	 * @since 4.7.6
	 *
	 * @return array The indexes.
	 */
	public function indexes() {
		$indexes           = [];
		$includedPostTypes = array_flip( aioseo()->sitemap->helpers->includedPostTypes() );
		$filterPostTypes   = array_filter( [
			BuddyPressIntegration::isComponentActive( 'activity' ) && isset( $includedPostTypes['bp-activity'] ) ? 'bp-activity' : '',
			BuddyPressIntegration::isComponentActive( 'group' ) && isset( $includedPostTypes['bp-group'] ) ? 'bp-group' : '',
			BuddyPressIntegration::isComponentActive( 'member' ) && isset( $includedPostTypes['bp-member'] ) ? 'bp-member' : '',
		] );

		foreach ( $filterPostTypes as $postType ) {
			$indexes = array_merge( $indexes, $this->buildIndexesPostType( $postType ) );
		}

		return $indexes;
	}

	/**
	 * Builds BuddyPress related root indexes.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $postType The BuddyPress fake post type.
	 * @return array            The BuddyPress related root indexes.
	 */
	private function buildIndexesPostType( $postType ) {
		switch ( $postType ) {
			case 'bp-activity':
				return $this->buildIndexesActivity();
			case 'bp-group':
				return $this->buildIndexesGroup();
			case 'bp-member':
				return $this->buildIndexesMember();
			default:
				return [];
		}
	}

	/**
	 * Builds activity root indexes.
	 *
	 * @since 4.7.6
	 *
	 * @return array The activity root indexes.
	 */
	private function buildIndexesActivity() {
		$activityTable = aioseo()->core->db->prefix . 'bp_activity';
		$linksPerIndex = aioseo()->sitemap->linksPerIndex;
		$items         = aioseo()->core->db->execute(
			aioseo()->core->db->db->prepare(
				"SELECT id, date_recorded
				FROM (
					SELECT @row := @row + 1 AS rownum, id, date_recorded
					FROM (
						SELECT a.id, a.date_recorded FROM $activityTable as a
						WHERE a.is_spam = 0
							AND a.hide_sitewide = 0
							AND a.type NOT IN ('activity_comment', 'last_activity')
						ORDER BY a.date_recorded DESC
					) AS x
					CROSS JOIN (SELECT @row := 0) AS vars
					ORDER BY date_recorded DESC
				) AS y
				WHERE rownum = 1 OR rownum % %d = 1;",
				[
					$linksPerIndex
				]
			),
			true
		)->result();

		$totalItems = aioseo()->core->db->execute(
			"SELECT COUNT(*) as count
			FROM $activityTable as a
			WHERE a.is_spam = 0
				AND a.hide_sitewide = 0
				AND a.type NOT IN ('activity_comment', 'last_activity')
			",
			true
		)->result();

		$indexes = [];
		if ( $items ) {
			$filename = aioseo()->sitemap->filename;
			$count    = count( $items );
			for ( $i = 0; $i < $count; $i++ ) {
				$indexNumber = 0 !== $i && 1 < $count ? $i + 1 : '';

				$indexes[] = [
					'loc'     => aioseo()->helpers->localizedUrl( "/bp-activity-$filename$indexNumber.xml" ),
					'lastmod' => aioseo()->helpers->dateTimeToIso8601( $items[ $i ]->date_recorded ),
					'count'   => $linksPerIndex
				];
			}

			// We need to update the count of the last index since it won't necessarily be the same as the links per index.
			$indexes[ count( $indexes ) - 1 ]['count'] = $totalItems[0]->count - ( $linksPerIndex * ( $count - 1 ) );
		}

		return $indexes;
	}

	/**
	 * Builds group root indexes.
	 *
	 * @since 4.7.6
	 *
	 * @return array The group root indexes.
	 */
	private function buildIndexesGroup() {
		$groupsTable     = aioseo()->core->db->prefix . 'bp_groups';
		$groupsMetaTable = aioseo()->core->db->prefix . 'bp_groups_groupmeta';
		$linksPerIndex   = aioseo()->sitemap->linksPerIndex;
		$items           = aioseo()->core->db->execute(
			aioseo()->core->db->db->prepare(
				"SELECT id, date_modified
				FROM (
					SELECT @row := @row + 1 AS rownum, id, date_modified
					FROM (
						SELECT g.id, gm.group_id, MAX(gm.meta_value) as date_modified FROM $groupsTable as g
						INNER JOIN $groupsMetaTable AS gm ON g.id = gm.group_id
						WHERE g.status = 'public'
							AND gm.meta_key = 'last_activity'
						GROUP BY g.id
						ORDER BY date_modified DESC
					) AS x
					CROSS JOIN (SELECT @row := 0) AS vars
					ORDER BY date_modified DESC
				) AS y
				WHERE rownum = 1 OR rownum % %d = 1;",
				[
					$linksPerIndex
				]
			),
			true
		)->result();

		$totalItems = aioseo()->core->db->execute(
			"SELECT COUNT(*) as count
			FROM $groupsTable as g
			WHERE g.status = 'public'
			",
			true
		)->result();

		$indexes = [];
		if ( $items ) {
			$filename = aioseo()->sitemap->filename;
			$count    = count( $items );
			for ( $i = 0; $i < $count; $i++ ) {
				$indexNumber = 0 !== $i && 1 < $count ? $i + 1 : '';

				$indexes[] = [
					'loc'     => aioseo()->helpers->localizedUrl( "/bp-group-$filename$indexNumber.xml" ),
					'lastmod' => aioseo()->helpers->dateTimeToIso8601( $items[ $i ]->date_modified ),
					'count'   => $linksPerIndex
				];
			}

			// We need to update the count of the last index since it won't necessarily be the same as the links per index.
			$indexes[ count( $indexes ) - 1 ]['count'] = $totalItems[0]->count - ( $linksPerIndex * ( $count - 1 ) );
		}

		return $indexes;
	}

	/**
	 * Builds member root indexes.
	 *
	 * @since 4.7.6
	 *
	 * @return array The member root indexes.
	 */
	private function buildIndexesMember() {
		$activityTable = aioseo()->core->db->prefix . 'bp_activity';
		$linksPerIndex = aioseo()->sitemap->linksPerIndex;
		$items         = aioseo()->core->db->execute(
			aioseo()->core->db->db->prepare(
				"SELECT user_id, date_recorded
				FROM (
					SELECT @row := @row + 1 AS rownum, user_id, date_recorded
					FROM (
						SELECT a.user_id, a.date_recorded FROM $activityTable as a
						WHERE a.component = 'members'
							AND a.type = 'last_activity'
						ORDER BY a.date_recorded DESC
					) AS x
					CROSS JOIN (SELECT @row := 0) AS vars
					ORDER BY date_recorded DESC
				) AS y
				WHERE rownum = 1 OR rownum % %d = 1;",
				[
					$linksPerIndex
				]
			),
			true
		)->result();

		$totalItems = aioseo()->core->db->execute(
			"SELECT COUNT(*) as count
			FROM $activityTable as a
			WHERE a.component = 'members'
				AND a.type = 'last_activity'
			",
			true
		)->result();

		$indexes = [];
		if ( $items ) {
			$filename = aioseo()->sitemap->filename;
			$count    = count( $items );
			for ( $i = 0; $i < $count; $i++ ) {
				$indexNumber = 0 !== $i && 1 < $count ? $i + 1 : '';

				$indexes[] = [
					'loc'     => aioseo()->helpers->localizedUrl( "/bp-member-$filename$indexNumber.xml" ),
					'lastmod' => aioseo()->helpers->dateTimeToIso8601( $items[ $i ]->date_recorded ),
					'count'   => $linksPerIndex
				];
			}

			// We need to update the count of the last index since it won't necessarily be the same as the links per index.
			$indexes[ count( $indexes ) - 1 ]['count'] = $totalItems[0]->count - ( $linksPerIndex * ( $count - 1 ) );
		}

		return $indexes;
	}
}Common/Standalone/BuddyPress/Component.php000066600000030273151135505570014643 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\BuddyPress;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
use AIOSEO\Plugin\Common\Schema\Graphs as CommonGraphs;

/**
 * BuddyPress Component class.
 *
 * @since 4.7.6
 */
class Component {
	/**
	 * The current component template type.
	 *
	 * @since 4.7.6
	 *
	 * @var string|null
	 */
	public $templateType = null;

	/**
	 * The component ID.
	 *
	 * @since 4.7.6
	 *
	 * @var int
	 */
	public $id = 0;

	/**
	 * The component author.
	 *
	 * @since 4.7.6
	 *
	 * @var \WP_User|false
	 */
	public $author = false;

	/**
	 * The component date.
	 *
	 * @since 4.7.6
	 *
	 * @var int|false
	 */
	public $date = false;

	/**
	 * The activity single page data.
	 *
	 * @since 4.7.6
	 *
	 * @var array
	 */
	public $activity = [];

	/**
	 * The group single page data.
	 *
	 * @since 4.7.6
	 *
	 * @var array
	 */
	public $group = [];

	/**
	 * The type of the group archive page.
	 *
	 * @since 4.7.6
	 *
	 * @var array
	 */
	public $groupType = [];

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

		$this->setTemplateType();
		$this->setId();
		$this->setAuthor();
		$this->setDate();
		$this->setActivity();
		$this->setGroup();
		$this->setGroupType();
	}

	/**
	 * Sets the template type.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	private function setTemplateType() {
		if ( BuddyPressIntegration::callFunc( 'bp_is_single_activity' ) ) {
			$this->templateType = 'bp-activity_single';
		} elseif ( BuddyPressIntegration::callFunc( 'bp_is_group' ) ) {
			$this->templateType = 'bp-group_single';
		} elseif (
			BuddyPressIntegration::callFunc( 'bp_is_user' ) &&
			false === BuddyPressIntegration::callFunc( 'bp_is_single_activity' )
		) {
			$this->templateType = 'bp-member_single';
		} elseif ( BuddyPressIntegration::callFunc( 'bp_is_activity_directory' ) ) {
			$this->templateType = 'bp-activity_archive';
		} elseif ( BuddyPressIntegration::callFunc( 'bp_is_members_directory' ) ) {
			$this->templateType = 'bp-member_archive';
		} elseif ( BuddyPressIntegration::callFunc( 'bp_is_groups_directory' ) ) {
			$this->templateType = 'bp-group_archive';
		} elseif (
			BuddyPressIntegration::callFunc( 'bp_is_current_action', 'feed' ) &&
			BuddyPressIntegration::callFunc( 'bp_is_activity_component' )
		) {
			$this->templateType = 'bp-activity_feed';
		}
	}

	/**
	 * Sets the component ID.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	private function setId() {
		switch ( $this->templateType ) {
			case 'bp-activity_single':
				$id = get_query_var( 'bp_member_action' );
				break;
			case 'bp-group_single':
				$id = get_query_var( 'bp_group' );
				break;
			case 'bp-member_single':
				$id = get_query_var( 'bp_member' );
				break;
			default:
				$id = $this->id;
		}

		$this->id = $id;
	}

	/**
	 * Sets the component author.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	private function setAuthor() {
		switch ( $this->templateType ) {
			case 'bp-activity_single':
				if ( ! $this->activity ) {
					$this->setActivity();
				}

				if ( $this->activity ) {
					$this->author = get_user_by( 'id', $this->activity['user_id'] );

					return;
				}

				break;
			case 'bp-group_single':
				if ( ! $this->group ) {
					$this->setGroup();
				}

				if ( $this->group ) {
					$this->author = get_user_by( 'id', $this->group['creator_id'] );

					return;
				}

				break;
			case 'bp-member_single':
				$this->author = get_user_by( 'slug', $this->id );

				return;
		}
	}

	/**
	 * Sets the component date.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	private function setDate() {
		switch ( $this->templateType ) {
			case 'bp-activity_single':
				if ( ! $this->activity ) {
					$this->setActivity();
				}
				$date = strtotime( $this->activity['date_recorded'] );
				break;
			case 'bp-group_single':
				if ( ! $this->group ) {
					$this->setGroup();
				}
				$date = strtotime( $this->group['date_created'] );
				break;
			default:
				$date = $this->date;
		}

		$this->date = $date;
	}

	/**
	 * Sets the activity data.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	private function setActivity() {
		if ( 'bp-activity_single' !== $this->templateType ) {
			return;
		}

		$activities = BuddyPressIntegration::callFunc( 'bp_activity_get_specific', [
			'activity_ids'     => [ $this->id ],
			'display_comments' => true
		] );
		if ( ! empty( $activities['activities'] ) ) {
			list( $activity ) = current( $activities );

			$this->activity = (array) $activity;

			// The `content_rendered` is AIOSEO specific.
			$this->activity['content_rendered'] = $this->activity['content'] ?? '';
			if ( ! empty( $this->activity['content'] ) ) {
				$this->activity['content_rendered'] = apply_filters( 'bp_get_activity_content', $this->activity['content'] );
			}

			return;
		}

		$this->resetComponent();
	}

	/**
	 * Sets the group data.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	private function setGroup() {
		if ( 'bp-group_single' !== $this->templateType ) {
			return;
		}

		$group = BuddyPressIntegration::callFunc( 'bp_get_group_by', 'slug', $this->id );
		if ( ! empty( $group ) ) {
			$this->group = (array) $group;

			return;
		}

		$this->resetComponent();
	}

	/**
	 * Sets the group type.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	private function setGroupType() {
		if ( 'bp-group_archive' !== $this->templateType ) {
			return;
		}

		$type = BuddyPressIntegration::callFunc( 'bp_get_current_group_directory_type' );
		if ( ! $type ) {
			return;
		}

		$term = get_term_by( 'slug', $type, 'bp_group_type' );
		if ( ! $term ) {
			return;
		}

		$meta = get_metadata( 'term', $term->term_id );
		if ( ! $meta ) {
			return;
		}

		$this->groupType = [
			'singular' => $meta['bp_type_singular_name'][0] ?? '',
			'plural'   => $meta['bp_type_name'][0] ?? '',
		];
	}

	/**
	 * Resets some of the component properties.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	private function resetComponent() {
		$this->templateType = null;
		$this->id           = 0;
	}

	/**
	 * Retrieves the SEO metadata value.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $which The SEO metadata to get.
	 * @return string        The SEO metadata value.
	 */
	public function getMeta( $which ) {
		list( $postType, $suffix ) = explode( '_', $this->templateType );

		switch ( $which ) {
			case 'title':
				$meta = 'single' === $suffix
					? aioseo()->meta->title->getPostTypeTitle( $postType )
					: aioseo()->meta->title->getArchiveTitle( $postType );
				$meta = aioseo()->meta->description->helpers->bpSanitize( $meta, $this->id );
				break;
			case 'description':
				$meta = 'single' === $suffix
					? aioseo()->meta->description->getPostTypeDescription( $postType )
					: aioseo()->meta->description->getArchiveDescription( $postType );
				$meta = aioseo()->meta->description->helpers->bpSanitize( $meta, $this->id );
				break;
			case 'keywords':
				$meta = 'single' === $suffix
					? ''
					: aioseo()->meta->keywords->getArchiveKeywords( $postType );
				$meta = aioseo()->meta->keywords->prepareKeywords( $meta );
				break;
			case 'robots':
				$dynamicOptions = aioseo()->dynamicOptions->noConflict();
				if ( 'single' === $suffix && $dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
					aioseo()->meta->robots->globalValues( [ 'postTypes', $postType ], true );
				} elseif ( $dynamicOptions->searchAppearance->archives->has( $postType ) ) {
					aioseo()->meta->robots->globalValues( [ 'archives', $postType ], true );
				}

				$meta = aioseo()->meta->robots->metaHelper();
				break;
			case 'canonical':
				$meta = '';
				if ( 'single' === $suffix ) {
					if ( 'bp-member' === $postType ) {
						$meta = BuddyPressIntegration::getComponentSingleUrl( 'member', $this->author->ID );
					} elseif ( 'bp-group' === $postType ) {
						$meta = BuddyPressIntegration::getComponentSingleUrl( 'group', $this->group['id'] );
					}
				}
				break;
			default:
				$meta = '';
		}

		return $meta;
	}

	/**
	 * Determines the schema type for the current component.
	 *
	 * @since 4.7.6
	 *
	 * @param  \AIOSEO\Plugin\Common\Schema\Context $contextInstance The Context class instance.
	 * @return void
	 */
	public function determineSchemaGraphsAndContext( $contextInstance ) {
		list( $postType ) = explode( '_', $this->templateType );

		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		if ( $dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
			$defaultType = $dynamicOptions->searchAppearance->postTypes->{$postType}->schemaType;
			switch ( $defaultType ) {
				case 'Article':
					aioseo()->schema->graphs[] = $dynamicOptions->searchAppearance->postTypes->{$postType}->articleType;
					break;
				case 'WebPage':
					aioseo()->schema->graphs[] = $dynamicOptions->searchAppearance->postTypes->{$postType}->webPageType;
					break;
				default:
					aioseo()->schema->graphs[] = $defaultType;
			}
		}

		switch ( $this->templateType ) {
			case 'bp-activity_single':
				$datePublished = $this->activity['date_recorded'];
				$contextUrl    = BuddyPressIntegration::getComponentSingleUrl( 'activity', $this->activity['id'] );

				break;
			case 'bp-group_single':
				$datePublished = $this->group['date_created'];
				$contextUrl    = BuddyPressIntegration::getComponentSingleUrl( 'group', $this->group['id'] );

				break;
			case 'bp-member_single':
				aioseo()->schema->graphs[] = 'ProfilePage';

				$contextUrl = BuddyPressIntegration::getComponentSingleUrl( 'member', $this->author->ID );

				break;
			case 'bp-activity_archive':
			case 'bp-group_archive':
			case 'bp-member_archive':
				list( , $component ) = explode( '-', $postType );

				$contextUrl     = BuddyPressIntegration::getComponentArchiveUrl( $component );
				$breadcrumbType = 'CollectionPage';

				break;
			default:
				break;
		}

		if ( ! empty( $datePublished ) ) {
			CommonGraphs\Article\NewsArticle::setOverwriteGraphData( [
				'properties' => compact( 'datePublished' )
			] );
		}

		if ( ! empty( $contextUrl ) ) {
			$name                = aioseo()->meta->title->getTitle();
			$description         = aioseo()->meta->description->getDescription();
			$breadcrumbPositions = [
				'name'        => $name,
				'description' => $description,
				'url'         => $contextUrl,
			];

			if ( ! empty( $breadcrumbType ) ) {
				$breadcrumbPositions['type'] = $breadcrumbType;
			}

			aioseo()->schema->context = [
				'name'        => $name,
				'description' => $description,
				'url'         => $contextUrl,
				'breadcrumb'  => $contextInstance->breadcrumb->setPositions( $breadcrumbPositions ),
			];
		}
	}

	/**
	 * Gets the breadcrumbs for the current component.
	 *
	 * @since 4.7.6
	 *
	 * @return array
	 */
	public function getCrumbs() {
		$crumbs = [];
		switch ( $this->templateType ) {
			case 'bp-activity_single':
				$crumbs[] = aioseo()->breadcrumbs->makeCrumb(
					BuddyPressIntegration::callFunc( 'bp_get_directory_title', 'activity' ),
					BuddyPressIntegration::getComponentArchiveUrl( 'activity' )
				);
				$crumbs[] = aioseo()->breadcrumbs->makeCrumb( sanitize_text_field( $this->activity['action'] ) );
				break;
			case 'bp-group_single':
				$crumbs[] = aioseo()->breadcrumbs->makeCrumb(
					BuddyPressIntegration::callFunc( 'bp_get_directory_title', 'groups' ),
					BuddyPressIntegration::getComponentArchiveUrl( 'group' )
				);
				$crumbs[] = aioseo()->breadcrumbs->makeCrumb( $this->group['name'] );
				break;
			case 'bp-member_single':
				$crumbs[] = aioseo()->breadcrumbs->makeCrumb(
					BuddyPressIntegration::callFunc( 'bp_get_directory_title', 'members' ),
					BuddyPressIntegration::getComponentArchiveUrl( 'member' )
				);
				$crumbs[] = aioseo()->breadcrumbs->makeCrumb( $this->author->display_name );
				break;
			case 'bp-activity_archive':
				$crumbs[] = aioseo()->breadcrumbs->makeCrumb( BuddyPressIntegration::callFunc( 'bp_get_directory_title', 'activity' ) );
				break;
			case 'bp-group_archive':
				$crumbs[] = aioseo()->breadcrumbs->makeCrumb( BuddyPressIntegration::callFunc( 'bp_get_directory_title', 'groups' ) );
				break;
			case 'bp-member_archive':
				$crumbs[] = aioseo()->breadcrumbs->makeCrumb( BuddyPressIntegration::callFunc( 'bp_get_directory_title', 'members' ) );
				break;
			default:
				break;
		}

		return $crumbs;
	}
}Common/Standalone/BuddyPress/BuddyPress.php000066600000023313151135505570014762 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\BuddyPress;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Handles the BuddyPress integration with AIOSEO.
 *
 * @since 4.7.6
 */
class BuddyPress {
	/**
	 * Instance of the Tags class.
	 *
	 * @since 4.7.6
	 *
	 * @var Tags
	 */
	public $tags;

	/**
	 * Instance of the Component class.
	 *
	 * @since 4.7.6
	 *
	 * @var Component
	 */
	public $component;

	/**
	 * Instance of the Sitemap class.
	 *
	 * @since 4.7.6
	 *
	 * @var Sitemap
	 */
	public $sitemap = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.7.6
	 */
	public function __construct() {
		if (
			aioseo()->helpers->isAjaxCronRestRequest() ||
			! aioseo()->helpers->isPluginActive( 'buddypress' )
		) {
			return;
		}

		// Hook into `plugins_loaded` to ensure BuddyPress has loaded some necessary functions.
		add_action( 'plugins_loaded', [ $this, 'maybeLoad' ], 20 );
	}

	/**
	 * Hooked into `plugins_loaded` action hook.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	public function maybeLoad() {
		// If the BuddyPress version is below 12 we bail.
		if ( ! function_exists( 'bp_get_version' ) || version_compare( bp_get_version(), '12', '<' ) ) {
			return;
		}

		// If none of the necessary BuddyPress components are active we bail.
		if (
			! BuddyPressIntegration::isComponentActive( 'activity' ) &&
			! BuddyPressIntegration::isComponentActive( 'group' ) &&
			! BuddyPressIntegration::isComponentActive( 'member' )
		) {
			return;
		}

		$this->sitemap = new Sitemap();

		add_action( 'init', [ $this, 'setTags' ], 20 );
		add_action( 'bp_parse_query', [ $this, 'setComponent' ], 20 );
	}

	/**
	 * Hooked into `init` action hook.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	public function setTags() {
		$this->tags = new Tags();
	}

	/**
	 * Hooked into `bp_parse_query` action hook.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	public function setComponent() {
		$this->component = new Component();
	}

	/**
	 * Adds the BuddyPress fake post types to the list of post types, so they appear under e.g. Search Appearance.
	 *
	 * @since 4.7.6
	 *
	 * @param  array $postTypes       Public post types from {@see \AIOSEO\Plugin\Common\Traits\Helpers\Wp::getPublicPostTypes}.
	 * @param  bool  $namesOnly       Whether only the names should be included.
	 * @param  bool  $hasArchivesOnly Whether to only include post types which have archives.
	 * @param  array $args            Additional arguments.
	 * @return void
	 */
	public function maybeAddPostTypes( &$postTypes, $namesOnly, $hasArchivesOnly, $args ) {
		// If one of these CPTs is already registered we bail, so we don't overwrite them and possibly break something.
		if (
			post_type_exists( 'bp-activity' ) ||
			post_type_exists( 'bp-group' ) ||
			post_type_exists( 'bp-member' )
		) {
			return;
		}

		/**
		 * The BP components are registered with the `buddypress` CPT which is not viewable, so we add it here to include our metadata inside <head>.
		 * {@see \AIOSEO\Plugin\Common\Main\Head::wpHead}.
		 */
		if (
			$namesOnly &&
			doing_action( 'wp_head' )
		) {
			$postTypes = array_merge( $postTypes, [ 'buddypress' ] );

			return;
		}

		$fakePostTypes = $this->getFakePostTypes();

		if ( ! BuddyPressIntegration::isComponentActive( 'activity' ) ) {
			unset( $fakePostTypes['bp-activity'] );
		}

		if ( ! BuddyPressIntegration::isComponentActive( 'group' ) ) {
			unset( $fakePostTypes['bp-group'] );
		}

		if ( ! BuddyPressIntegration::isComponentActive( 'member' ) ) {
			unset( $fakePostTypes['bp-member'] );
		}

		if ( $hasArchivesOnly ) {
			$fakePostTypes = array_filter( $fakePostTypes, function ( $postType ) {
				return $postType['hasArchive'];
			} );
		}

		if ( $namesOnly ) {
			$fakePostTypes = array_keys( $fakePostTypes );
		}

		// 0. Below we'll add/merge the BuddyPress post types only under certain conditions.
		$fakePostTypes = array_values( $fakePostTypes );
		$currentScreen = aioseo()->helpers->getCurrentScreen();

		if (
			// 1. If the `buddypress` CPT is set in the list of post types to be included.
			( ! empty( $args['include'] ) && in_array( 'buddypress', $args['include'], true ) ) ||
			// 2. If the current request is for the sitemap.
			( ! empty( aioseo()->sitemap->filename ) && 'general' === ( aioseo()->sitemap->type ?? '' ) ) ||
			// 3. If we're on the Search Appearance screen.
			( $currentScreen && strpos( $currentScreen->id, 'aioseo-search-appearance' ) !== false ) ||
			// 4. If we're on the BuddyPress component front-end screen.
			BuddyPressIntegration::isComponentPage()
		) {
			$postTypes = array_merge( $postTypes, $fakePostTypes );
		}
	}

	/**
	 * Get edit links for the SEO Preview data.
	 *
	 * @since 4.7.6
	 *
	 * @return array
	 */
	public function getVueDataSeoPreview() {
		$data = [
			'editGoogleSnippetUrl' => '',
			'editObjectBtnText'    => '',
			'editObjectUrl'        => '',
		];

		list( $postType, $suffix ) = explode( '_', aioseo()->standalone->buddyPress->component->templateType );

		$bpFakePostTypes  = $this->getFakePostTypes();
		$fakePostTypeData = array_values( wp_list_filter( $bpFakePostTypes, [ 'name' => $postType ] ) );
		$fakePostTypeData = $fakePostTypeData[0] ?? [];
		if ( ! $fakePostTypeData ) {
			return $data;
		}

		if ( 'single' === $suffix ) {
			switch ( $postType ) {
				case 'bp-activity':
					$componentId = aioseo()->standalone->buddyPress->component->activity['id'];
					break;
				case 'bp-group':
					$componentId = aioseo()->standalone->buddyPress->component->group['id'];
					break;
				case 'bp-member':
					$componentId = aioseo()->standalone->buddyPress->component->author->ID;
					break;
				default:
					$componentId = 0;
			}
		}

		$scrollToId                   = 'aioseo-card-' . $postType . ( 'single' === $suffix ? 'SA' : 'ArchiveArchives' );
		$data['editGoogleSnippetUrl'] = 'single' === $suffix
			? admin_url( 'admin.php?page=aioseo-search-appearance' ) . '#/content-types'
			: admin_url( 'admin.php?page=aioseo-search-appearance' ) . '#/archives';
		$data['editGoogleSnippetUrl'] = add_query_arg( [
			'aioseo-scroll'    => $scrollToId,
			'aioseo-highlight' => $scrollToId
		], $data['editGoogleSnippetUrl'] );

		$data['editObjectBtnText'] = sprintf(
			// Translators: 1 - A noun for something that's being edited ("Post", "Page", "Article", "Product", etc.).
			esc_html__( 'Edit %1$s', 'all-in-one-seo-pack' ),
			'single' === $suffix ? $fakePostTypeData['singular'] : $fakePostTypeData['label']
		);

		list( , $component ) = explode( '-', $postType );

		$data['editObjectUrl'] = 'single' === $suffix
			? BuddyPressIntegration::getComponentEditUrl( $component, $componentId ?? 0 )
			: BuddyPressIntegration::callFunc( 'bp_get_admin_url', add_query_arg( 'page', 'bp-rewrites', 'admin.php' ) );

		return $data;
	}

	/**
	 * Retrieves the BuddyPress fake post types.
	 *
	 * @since 4.7.6
	 *
	 * @return array The BuddyPress fake post types.
	 */
	public function getFakePostTypes() {
		return [
			'bp-activity' => [
				'name'               => 'bp-activity',
				'label'              => sprintf(
					// Translators: 1 - The hard coded string 'BuddyPress'.
					_x( 'Activities (%1$s)', 'BuddyPress', 'all-in-one-seo-pack' ),
					'BuddyPress'
				),
				'singular'           => 'Activity',
				'icon'               => 'dashicons-buddicons-buddypress-logo',
				'hasExcerpt'         => false,
				'hasArchive'         => true,
				'hierarchical'       => false,
				'taxonomies'         => [],
				'slug'               => 'bp-activity',
				'buddyPress'         => true,
				'defaultTags'        => [
					'postTypes' => [
						'title'       => [
							'bp_activity_action',
							'separator_sa',
							'site_title',
						],
						'description' => [
							'bp_activity_content',
							'separator_sa'
						]
					]
				],
				'defaultTitle'       => '#bp_activity_action #separator_sa #site_title',
				'defaultDescription' => '#bp_activity_content',
			],
			'bp-group'    => [
				'name'               => 'bp-group',
				'label'              => sprintf(
					// Translators: 1 - The hard coded string 'BuddyPress'.
					_x( 'Groups (%1$s)', 'BuddyPress', 'all-in-one-seo-pack' ),
					'BuddyPress'
				),
				'singular'           => 'Group',
				'icon'               => 'dashicons-buddicons-buddypress-logo',
				'hasExcerpt'         => false,
				'hasArchive'         => true,
				'hierarchical'       => false,
				'taxonomies'         => [],
				'slug'               => 'bp-group',
				'buddyPress'         => true,
				'defaultTags'        => [
					'postTypes' => [
						'title'       => [
							'bp_group_name',
							'separator_sa',
							'site_title',
						],
						'description' => [
							'bp_group_description',
							'separator_sa'
						]
					]
				],
				'defaultTitle'       => '#bp_group_name #separator_sa #site_title',
				'defaultDescription' => '#bp_group_description',
			],
			'bp-member'   => [
				'name'               => 'bp-member',
				'label'              => sprintf(
					// Translators: 1 - The hard coded string 'BuddyPress'.
					_x( 'Members (%1$s)', 'BuddyPress', 'all-in-one-seo-pack' ),
					'BuddyPress'
				),
				'singular'           => 'Member',
				'icon'               => 'dashicons-buddicons-buddypress-logo',
				'hasExcerpt'         => false,
				'hasArchive'         => true,
				'hierarchical'       => false,
				'taxonomies'         => [],
				'slug'               => 'bp-member',
				'buddyPress'         => true,
				'defaultTags'        => [
					'postTypes' => [
						'title'       => [
							'author_name',
							'separator_sa',
							'site_title',
						],
						'description' => [
							'author_bio',
							'separator_sa'
						]
					]
				],
				'defaultTitle'       => '#author_name #separator_sa #site_title',
				'defaultDescription' => '#author_bio',
			],
		];
	}
}Common/Standalone/BuddyPress/Tags.php000066600000023531151135505570013576 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\BuddyPress;

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

/**
 * BuddyPress Tags class.
 *
 * @since 4.7.6
 */
class Tags {
	/**
	 * Class constructor.
	 *
	 * @since 4.7.6
	 */
	public function __construct() {
		aioseo()->tags->addContext( $this->getContexts() );
		aioseo()->tags->addTags( $this->getTags() );
	}

	/**
	 * Retrieves the contexts for BuddyPress.
	 *
	 * @since 4.7.6
	 *
	 * @return array An array of contextual data.
	 */
	public function getContexts() {
		return [
			'bp-activityTitle'              => [
				'author_first_name',
				'author_last_name',
				'author_name',
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'post_date',
				'post_day',
				'post_month',
				'post_year',
				'separator_sa',
				'site_title',
				'tagline',
				'bp_activity_action',
				'bp_activity_content',
			],
			'bp-activityArchiveTitle'       => [
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'separator_sa',
				'site_title',
				'tagline',
				'archive_title',
			],
			'bp-activityDescription'        => [
				'author_first_name',
				'author_last_name',
				'author_name',
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'post_date',
				'post_day',
				'post_month',
				'post_year',
				'separator_sa',
				'site_title',
				'tagline',
				'bp_activity_action',
				'bp_activity_content',
			],
			'bp-activityArchiveDescription' => [
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'separator_sa',
				'site_title',
				'tagline',
				'archive_title',
			],
			'bp-groupTitle'                 => [
				'author_first_name',
				'author_last_name',
				'author_name',
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'post_date',
				'post_day',
				'post_month',
				'post_year',
				'separator_sa',
				'site_title',
				'tagline',
				'bp_group_name',
				'bp_group_description',
			],
			'bp-groupArchiveTitle'          => [
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'separator_sa',
				'site_title',
				'tagline',
				'archive_title',
				'bp_group_type_singular_name',
				'bp_group_type_plural_name',
			],
			'bp-groupDescription'           => [
				'author_first_name',
				'author_last_name',
				'author_name',
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'post_date',
				'post_day',
				'post_month',
				'post_year',
				'separator_sa',
				'site_title',
				'tagline',
				'bp_group_name',
				'bp_group_description',
			],
			'bp-groupArchiveDescription'    => [
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'separator_sa',
				'site_title',
				'tagline',
				'archive_title',
				'bp_group_type_singular_name',
				'bp_group_type_plural_name',
			],
			'bp-memberTitle'                => [
				'author_first_name',
				'author_last_name',
				'author_name',
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'separator_sa',
				'site_title',
				'tagline',
			],
			'bp-memberArchiveTitle'         => [
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'separator_sa',
				'site_title',
				'tagline',
				'archive_title',
			],
			'bp-memberDescription'          => [
				'author_first_name',
				'author_last_name',
				'author_name',
				'author_bio',
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'separator_sa',
				'site_title',
				'tagline',
			],
			'bp-memberArchiveDescription'   => [
				'current_date',
				'current_day',
				'current_month',
				'current_year',
				'separator_sa',
				'site_title',
				'tagline',
				'archive_title',
			],
		];
	}

	/**
	 * Retrieves the custom tags for BuddyPress.
	 *
	 * @since 4.7.6
	 *
	 * @return array An array of tags.
	 */
	public function getTags() {
		return [
			[
				'id'          => 'bp_activity_action',
				'name'        => _x( 'Activity Action', 'BuddyPress', 'all-in-one-seo-pack' ),
				'description' => _x( 'The activity action.', 'BuddyPress', 'all-in-one-seo-pack' ),
				'instance'    => $this,
			],
			[
				'id'          => 'bp_activity_content',
				'name'        => _x( 'Activity Content', 'BuddyPress', 'all-in-one-seo-pack' ),
				'description' => _x( 'The activity content.', 'BuddyPress', 'all-in-one-seo-pack' ),
				'instance'    => $this,
			],
			[
				'id'          => 'bp_group_name',
				'name'        => _x( 'Group Name', 'BuddyPress', 'all-in-one-seo-pack' ),
				'description' => _x( 'The group name.', 'BuddyPress', 'all-in-one-seo-pack' ),
				'instance'    => $this,
			],
			[
				'id'          => 'bp_group_description',
				'name'        => _x( 'Group Description', 'BuddyPress', 'all-in-one-seo-pack' ),
				'description' => _x( 'The group description.', 'BuddyPress', 'all-in-one-seo-pack' ),
				'instance'    => $this,
			],
			[
				'id'          => 'bp_group_type_singular_name',
				'name'        => _x( 'Group Type Singular Name', 'BuddyPress', 'all-in-one-seo-pack' ),
				'description' => _x( 'The group type singular name.', 'BuddyPress', 'all-in-one-seo-pack' ),
				'instance'    => $this,
			],
			[
				'id'          => 'bp_group_type_plural_name',
				'name'        => _x( 'Group Type Plural Name', 'BuddyPress', 'all-in-one-seo-pack' ),
				'description' => _x( 'The group type plural name.', 'BuddyPress', 'all-in-one-seo-pack' ),
				'instance'    => $this,
			],
		];
	}

	/**
	 * Replace the tags in the string provided.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $string The string to look for tags in.
	 * @param  int    $id     The object ID.
	 * @return string         The string with tags replaced.
	 */
	public function replaceTags( $string, $id ) {
		if ( ! $string || ! preg_match( '/' . aioseo()->tags->denotationChar . '/', $string ) ) {
			return $string;
		}

		foreach ( array_unique( aioseo()->helpers->flatten( $this->getContexts() ) ) as $tag ) {
			$tagId   = aioseo()->tags->denotationChar . $tag;
			$pattern = "/$tagId(?![a-zA-Z0-9_])/im";
			if ( preg_match( $pattern, $string ) ) {
				$tagValue = $this->getTagValue( [ 'id' => $tag ], $id );
				$string   = preg_replace( $pattern, '%|%' . aioseo()->helpers->escapeRegexReplacement( $tagValue ), $string );
			}
		}

		return str_replace( '%|%', '', $string );
	}

	/**
	 * Get the value of the tag to replace.
	 *
	 * @since 4.7.6
	 *
	 * @param  array    $tag        The tag to look for.
	 * @param  int|null $id         The object ID.
	 * @param  bool     $sampleData Whether to fill empty values with sample data.
	 * @return string               The value of the tag.
	 */
	public function getTagValue( $tag, $id = null, $sampleData = false ) {
		$sampleData = $sampleData || empty( aioseo()->standalone->buddyPress->component->templateType );

		switch ( $tag['id'] ) {
			case 'author_bio':
				$out = $sampleData
					? __( 'Sample author biography', 'all-in-one-seo-pack' )
					: aioseo()->standalone->buddyPress->component->author->description;
				break;
			case 'author_first_name':
				$out = $sampleData
					? wp_get_current_user()->first_name
					: aioseo()->standalone->buddyPress->component->author->first_name;
				break;
			case 'author_last_name':
				$out = $sampleData
					? wp_get_current_user()->last_name
					: aioseo()->standalone->buddyPress->component->author->last_name;
				break;
			case 'author_name':
				$out = $sampleData
					? wp_get_current_user()->display_name
					: aioseo()->standalone->buddyPress->component->author->display_name;
				break;
			case 'post_date':
				$out = $sampleData
					? aioseo()->tags->formatDateAsI18n( date_i18n( 'U' ) )
					: aioseo()->tags->formatDateAsI18n( aioseo()->standalone->buddyPress->component->date );
				break;
			case 'post_day':
				$out = $sampleData
					? date_i18n( 'd' )
					: date( 'd', aioseo()->standalone->buddyPress->component->date );
				break;
			case 'post_month':
				$out = $sampleData
					? date_i18n( 'F' )
					: date( 'F', aioseo()->standalone->buddyPress->component->date );
				break;
			case 'post_year':
				$out = $sampleData
					? date_i18n( 'Y' )
					: date( 'Y', aioseo()->standalone->buddyPress->component->date );
				break;
			case 'archive_title':
				$out = $sampleData
					? __( 'Sample Archive Title', 'all-in-one-seo-pack' )
					: esc_html( get_the_title() );
				break;
			case 'bp_activity_action':
				$out = $sampleData
					? _x( 'Sample Activity Action', 'BuddyPress', 'all-in-one-seo-pack' )
					: aioseo()->standalone->buddyPress->component->activity['action'];
				break;
			case 'bp_activity_content':
				$out = $sampleData
					? _x( 'Sample activity content', 'BuddyPress', 'all-in-one-seo-pack' )
					: aioseo()->standalone->buddyPress->component->activity['content_rendered'];
				break;
			case 'bp_group_name':
				$out = $sampleData
					? _x( 'Sample Group Name', 'BuddyPress', 'all-in-one-seo-pack' )
					: aioseo()->standalone->buddyPress->component->group['name'];
				break;
			case 'bp_group_description':
				$out = $sampleData
					? _x( 'Sample group description', 'BuddyPress', 'all-in-one-seo-pack' )
					: aioseo()->standalone->buddyPress->component->group['description'];
				break;
			case 'bp_group_type_singular_name':
				$out = $sampleData ? _x( 'Sample Type Singular', 'BuddyPress', 'all-in-one-seo-pack' ) : '';
				if ( ! empty( aioseo()->standalone->buddyPress->component->groupType ) ) {
					$out = aioseo()->standalone->buddyPress->component->groupType['singular'];
				}
				break;
			case 'bp_group_type_plural_name':
				$out = $sampleData ? _x( 'Sample Type Plural', 'BuddyPress', 'all-in-one-seo-pack' ) : '';
				if ( ! empty( aioseo()->standalone->buddyPress->component->groupType ) ) {
					$out = aioseo()->standalone->buddyPress->component->groupType['plural'];
				}
				break;
			default:
				$out = aioseo()->tags->getTagValue( $tag, $id );
		}

		return $out ?? '';
	}
}Common/Standalone/DetailsColumn.php000066600000020310151135505570013347 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Handles the AIOSEO Details post column.
 *
 * @since 4.2.0
 */
class DetailsColumn {
	/**
	 * The slug for the script.
	 *
	 * @since 4.2.0
	 *
	 * @var string
	 */
	protected $scriptSlug = 'src/vue/standalone/posts-table/main.js';

	/**
	 * Class constructor.
	 *
	 * @since 4.2.0
	 */
	public function __construct() {
		if ( wp_doing_ajax() ) {
			add_action( 'init', [ $this, 'addPostColumnsAjax' ], 1 );
		}

		if ( ! is_admin() || wp_doing_cron() ) {
			return;
		}

		add_action( 'current_screen', [ $this, 'registerColumnHooks' ], 1 );
	}

	/**
	 * Adds the columns to the page/post types.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function registerColumnHooks() {
		$screen = aioseo()->helpers->getCurrentScreen();
		if ( empty( $screen->base ) || empty( $screen->post_type ) ) {
			return;
		}

		if ( ! $this->shouldRegisterColumn( $screen->base, $screen->post_type ) ) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScripts' ] );

		if ( 'product' === $screen->post_type ) {
			add_filter( 'manage_edit-product_columns', [ $this, 'addColumn' ] );
			add_action( 'manage_posts_custom_column', [ $this, 'renderColumn' ], 10, 2 );

			return;
		}

		if ( 'attachment' === $screen->post_type ) {
			$enabled = apply_filters( 'aioseo_image_seo_media_columns', true );
			if ( ! $enabled ) {
				return;
			}

			add_filter( 'manage_media_columns', [ $this, 'addColumn' ] );
			add_action( 'manage_media_custom_column', [ $this, 'renderColumn' ], 10, 2 );

			return;
		}

		add_filter( "manage_edit-{$screen->post_type}_columns", [ $this, 'addColumn' ] );
		add_action( "manage_{$screen->post_type}_posts_custom_column", [ $this, 'renderColumn' ], 10, 2 );
	}

	/**
	 * Registers our post columns after a post has been quick-edited.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	public function addPostColumnsAjax() {
		if (
			! isset( $_POST['_inline_edit'], $_POST['post_ID'], $_POST['aioseo-has-details-column'] ) ||
			! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_inline_edit'] ) ), 'inlineeditnonce' )
		) {
			return;
		}

		$postId = (int) $_POST['post_ID'];
		if ( ! $postId ) {
			return;
		}

		$post     = get_post( $postId );
		$postType = $post->post_type;

		add_filter( "manage_edit-{$postType}_columns", [ $this, 'addColumn' ] );
		add_action( "manage_{$postType}_posts_custom_column", [ $this, 'renderColumn' ], 10, 2 );
	}

	/**
	 * Enqueues the JS/CSS for the page/posts table page.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function enqueueScripts() {
		$data          = aioseo()->helpers->getVueData();
		$data['posts'] = [];
		$data['terms'] = [];

		aioseo()->core->assets->load( $this->scriptSlug, [], $data );
	}

	/**
	 * Adds the AIOSEO Details column to the page/post tables in the admin.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $columns The columns we are adding ours onto.
	 * @return array          The modified columns.
	 */
	public function addColumn( $columns ) {
		$canManageSeo = apply_filters( 'aioseo_manage_seo', 'aioseo_manage_seo' );
		if (
			! current_user_can( $canManageSeo ) &&
			(
				! current_user_can( 'aioseo_page_general_settings' ) &&
				! current_user_can( 'aioseo_page_analysis' )
			)
		) {
			return $columns;
		}

		// Translators: 1 - The short plugin name ("AIOSEO").
		$columns['aioseo-details'] = sprintf( esc_html__( '%1$s Details', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME );

		return $columns;
	}

	/**
	 * Renders the column in the page/post table.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $columnName The column name.
	 * @param  int    $postId     The current rows, post id.
	 * @return void
	 */
	public function renderColumn( $columnName, $postId = 0 ) {
		if ( ! current_user_can( 'edit_post', $postId ) && ! current_user_can( 'aioseo_manage_seo' ) ) {
			return;
		}

		if ( 'aioseo-details' !== $columnName ) {
			return;
		}

		// Add this column/post to the localized array.
		global $wp_scripts; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if (
			! is_object( $wp_scripts ) || // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			! method_exists( $wp_scripts, 'get_data' ) || // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			! method_exists( $wp_scripts, 'add_data' ) // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		) {
			return;
		}

		$data = null;
		if ( is_object( $wp_scripts ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			$data = $wp_scripts->get_data( 'aioseo/js/' . $this->scriptSlug, 'data' ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		}

		if ( ! is_array( $data ) ) {
			$data = json_decode( str_replace( 'var aioseo = ', '', substr( $data, 0, -1 ) ), true );
		}

		// We have to temporarily modify the query here since the query incorrectly identifies
		// the current page as a category page when posts are filtered by a specific category.
		// phpcs:disable Squiz.NamingConventions.ValidVariableName
		global $wp_query;
		$originalQuery         = clone $wp_query;
		$wp_query->is_category = false;
		$wp_query->is_tag      = false;
		$wp_query->is_tax      = false;
		// phpcs:enable Squiz.NamingConventions.ValidVariableName

		$posts    = ! empty( $data['posts'] ) ? $data['posts'] : [];
		$postData = $this->getPostData( $postId, $columnName );

		$addonsColumnData = array_filter( aioseo()->addons->doAddonFunction( 'admin', 'renderColumnData', [
			$columnName,
			$postId,
			$postData
		] ) );

		$wp_query = $originalQuery; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		foreach ( $addonsColumnData as $addonColumnData ) {
			$postData = array_merge( $postData, $addonColumnData );
		}

		$posts[]       = $postData;
		$data['posts'] = $posts;

		$wp_scripts->add_data( 'aioseo/js/' . $this->scriptSlug, 'data', '' ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		wp_localize_script( 'aioseo/js/' . $this->scriptSlug, 'aioseo', $data );

		require AIOSEO_DIR . '/app/Common/Views/admin/posts/columns.php';
	}

	/**
	 * Gets the post data for the column.
	 *
	 * @since 4.5.0
	 *
	 * @param  int    $postId     The Post ID.
	 * @param  string $columnName The column name.
	 * @return array              The post data.
	 */
	protected function getPostData( $postId, $columnName ) {
		$nonce    = wp_create_nonce( "aioseo_meta_{$columnName}_{$postId}" );
		$thePost  = Models\Post::getPost( $postId );
		$postType = get_post_type( $postId );
		$postData = [
			'id'                 => $postId,
			'columnName'         => $columnName,
			'nonce'              => $nonce,
			'title'              => $thePost->title,
			'defaultTitle'       => aioseo()->meta->title->getPostTypeTitle( $postType ),
			'showTitle'          => apply_filters( 'aioseo_details_column_post_show_title', true, $postId ),
			'description'        => $thePost->description,
			'defaultDescription' => aioseo()->meta->description->getPostTypeDescription( $postType ),
			'showDescription'    => apply_filters( 'aioseo_details_column_post_show_description', true, $postId ),
			'value'              => ! empty( $thePost->seo_score ) ? (int) $thePost->seo_score : 0,
			'showMedia'          => false,
			'isSpecialPage'      => aioseo()->helpers->isSpecialPage( $postId ),
			'postType'           => $postType,
			'isPostVisible'      => aioseo()->helpers->isPostPubliclyViewable( $postId )
		];

		return $postData;
	}

	/**
	 * Checks whether the AIOSEO Details column should be registered.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether the column should be registered.
	 */
	public function shouldRegisterColumn( $screen, $postType ) {
		// Only allow users with the correct permissions to see the column.
		if ( ! current_user_can( 'aioseo_page_general_settings' ) ) {
			return false;
		}

		if ( 'type' === $postType ) {
			$postType = '_aioseo_type';
		}

		if ( 'edit' === $screen || 'upload' === $screen ) {
			if (
				aioseo()->options->advanced->postTypes->all &&
				in_array( $postType, aioseo()->helpers->getPublicPostTypes( true ), true )
			) {
				return true;
			}

			$postTypes = aioseo()->options->advanced->postTypes->included;
			if ( in_array( $postType, $postTypes, true ) ) {
				return true;
			}
		}

		return false;
	}
}Common/Standalone/WpCode.php000066600000001354151135505570011774 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

/**
 * Handles registering the AIOSEO username in the WPCode snippets library.
 *
 * @since 4.3.8
 */
class WpCode {
	/**
	 * Class constructor.
	 *
	 * @since 4.3.8
	 */
	public function __construct() {
		if ( ! is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		add_action( 'wpcode_loaded', [ $this, 'registerUsername' ] );
	}

	/**
	 * Enqueues the script.
	 *
	 * @since 4.3.8
	 *
	 * @return void
	 */
	public function registerUsername() {
		if ( ! function_exists( 'wpcode_register_library_username' ) ) {
			return;
		}

		wpcode_register_library_username( 'aioseo', AIOSEO_PLUGIN_SHORT_NAME );
	}
}Common/Standalone/LimitModifiedDate.php000066600000016355151135505570014137 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Limit Modified Date class.
 *
 * @since 4.1.8
 */
class LimitModifiedDate {
	/**
	 * Class constructor.
	 *
	 * @since 4.1.8
	 *
	 * @return void
	 */
	public function __construct() {
		if ( apply_filters( 'aioseo_last_modified_date_disable', false ) ) {
			return;
		}

		// Reset modified date when the post is updated.
		add_filter( 'wp_insert_post_data', [ $this, 'resetModifiedDate' ], 99999, 2 );
		add_filter( 'wp_insert_attachment_data', [ $this, 'resetModifiedDate' ], 99999, 2 );
		add_action( 'woocommerce_before_product_object_save', [ $this, 'limitWooCommerceModifiedDate' ] );

		add_action( 'rest_api_init', [ $this, 'registerRestHooks' ] );

		if ( ! is_admin() ) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScripts' ], 20 );
		add_action( 'post_submitbox_misc_actions', [ $this, 'classicEditorField' ] );
	}

	/**
	 * Register the REST API hooks.
	 *
	 * @since 4.1.8
	 *
	 * @return void
	 */
	public function registerRestHooks() {
		// Prevent REST API from dropping limit modified date value before updating the post.
		foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
			add_filter( "rest_pre_insert_$postType", [ $this, 'addLimitModifiedDateValue' ], 10, 2 );
		}
	}

	/**
	 * Enqueues the scripts for the Limited Modified Date functionality.
	 *
	 * @since 4.1.8
	 *
	 * @return void
	 */
	public function enqueueScripts() {
		if ( ! $this->isAllowed() || ! aioseo()->helpers->isScreenBase( 'post' ) ) {
			return;
		}

		// Only enqueue this script if the post-settings-metabox is already enqueued.
		if ( wp_script_is( 'aioseo/js/src/vue/standalone/post-settings/main.js', 'enqueued' ) ) {
			aioseo()->core->assets->load( 'src/vue/standalone/limit-modified-date/main.js' );
		}
	}

	/**
	 * Adds the Limit Modified Date field to the post object to prevent it from being dropped.
	 *
	 * @since 4.1.8
	 *
	 * @param  object           $preparedPost The post data.
	 * @param  \WP_REST_Request $restRequest  The request.
	 * @return object                         The modified post data.
	 */
	public function addLimitModifiedDateValue( $preparedPost, $restRequest = null ) {
		if ( 'PUT' !== $restRequest->get_method() ) {
			return $preparedPost;
		}

		$params = $restRequest->get_json_params();
		if ( empty( $params ) || ! isset( $params['aioseo_limit_modified_date'] ) ) {
			return $preparedPost;
		}

		$preparedPost->aioseo_limit_modified_date = $params['aioseo_limit_modified_date'];

		return $preparedPost;
	}

	/**
	 * Resets the modified date when a post is updated if the Limit Modified Date option is enabled.
	 *
	 * @since 4.1.8
	 *
	 * @param  array $data      An array of slashed, sanitized, and processed post data.
	 * @param  array $postArray An array of sanitized (and slashed) but otherwise unmodified post data.
	 * @return array            The modified sanitized post data.
	 */
	public function resetModifiedDate( $data, $postArray = [] ) {
		// If the ID isn't set, a new post is being inserted.
		if ( ! isset( $postArray['ID'] ) ) {
			return $data;
		}

		static $shouldReset = false;

		// Handle the REST API request from the Block Editor.
		if ( aioseo()->helpers->isRestApiRequest() ) {
			// If the value isn't set, then the value wasn't changed in the editor, and we can grab it from the post.
			if ( ! isset( $postArray['aioseo_limit_modified_date'] ) ) {
				$aioseoPost = Models\Post::getPost( $postArray['ID'] );
				if ( $aioseoPost->exists() && $aioseoPost->limit_modified_date ) {
					$shouldReset = true;
				}
			} else {
				if ( $postArray['aioseo_limit_modified_date'] ) {
					$shouldReset = true;
				}
			}
		}

		// Handle the POST request.
		if ( isset( $postArray['aioseo-post-settings'] ) ) {
			$aioseoData = json_decode( stripslashes( $postArray['aioseo-post-settings'] ) );
			if ( ! empty( $aioseoData->limit_modified_date ) ) {
				$shouldReset = true;
			}
		}

		// Handle post revision.
		if ( ! empty( $GLOBALS['action'] ) && in_array( $GLOBALS['action'], [ 'restore',  'inline-save' ], true ) ) {
			$aioseoPost = Models\Post::getPost( $postArray['ID'] );
			if ( $aioseoPost->exists() && $aioseoPost->limit_modified_date ) {
				$shouldReset = true;
			}
		}

		foreach ( aioseo()->standalone->pageBuilderIntegrations as $pageBuilder ) {
			if ( $pageBuilder->isBuiltWith( $postArray['ID'] ) && $pageBuilder->limitModifiedDate( $postArray['ID'] ) ) {
				$shouldReset = true;
				break;
			}
		}

		if ( $shouldReset && isset( $postArray['post_modified'], $postArray['post_modified_gmt'] ) ) {
			$originalPost = get_post( $postArray['ID'] );

			$data['post_modified']     = $originalPost->post_modified;
			$data['post_modified_gmt'] = $originalPost->post_modified_gmt;
		}

		return $data;
	}

	/**
	 * Limits the modified date for WooCommerce products.
	 *
	 * @since 4.8.1
	 *
	 * @param  \WC_Product $product The WooCommerce product.
	 * @return void
	 */
	public function limitWooCommerceModifiedDate( $product ) {
		if ( ! isset( $_POST['PostSettingsNonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['PostSettingsNonce'] ) ), 'aioseoPostSettingsNonce' ) ) {
			return;
		}

		if ( ! isset( $_POST['aioseo-post-settings'] ) ) {
			return;
		}

		$aioseoData = json_decode( sanitize_text_field( wp_unslash( ( $_POST['aioseo-post-settings'] ) ) ) );
		if ( empty( $aioseoData ) || empty( $aioseoData->limit_modified_date ) ) {
			return;
		}

		$product->set_date_modified( get_post_field( 'post_modified', $product->get_id() ) );
	}

	/**
	 * Add the checkbox in the Classic Editor.
	 *
	 * @since 4.1.8
	 *
	 * @param  \WP_Post $post The post object.
	 * @return void
	 */
	public function classicEditorField( $post ) {
		if ( ! $this->isAllowed( $post->post_type ) ) {
			return;
		}

		?>
		<div class="misc-pub-section">
			<div id="aioseo-limit-modified-date"></div>
		</div>
		<?php
	}

	/**
	 * Check if the Limit Modified Date functionality is allowed to run.
	 *
	 * @since 4.1.8
	 *
	 * @param  string $postType The current post type.
	 * @return bool             Whether the functionality is allowed.
	 */
	private function isAllowed( $postType = '' ) {
		if ( empty( $postType ) ) {
			$postType = get_post_type();
		}

		if ( class_exists( 'Limit_Modified_Date', false ) ) {
			return false;
		}

		if ( ! $this->isAllowedPostType( $postType ) ) {
			return false;
		}

		if ( ! aioseo()->access->hasCapability( 'aioseo_page_general_settings' ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Check if the given post type is allowed to limit the modified date.
	 *
	 * @since 4.1.8
	 *
	 * @param  string $postType The post type name.
	 * @return bool             Whether the post type is allowed.
	 */
	private function isAllowedPostType( $postType ) {
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		$postTypes      = aioseo()->helpers->getPublicPostTypes( true );
		$postTypes      = apply_filters( 'aioseo_limit_modified_date_post_types', $postTypes );

		if ( ! in_array( $postType, $postTypes, true ) ) {
			return false;
		}

		if ( ! $dynamicOptions->searchAppearance->postTypes->has( $postType ) || ! $dynamicOptions->searchAppearance->postTypes->$postType->advanced->showMetaBox ) {
			return false;
		}

		return true;
	}
}Common/Standalone/SetupWizard.php000066600000015073151135505570013077 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

/**
 * Class that holds our setup wizard.
 *
 * @since 4.0.0
 */
class SetupWizard {
	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		if ( ! is_admin() || wp_doing_cron() || wp_doing_ajax() ) {
			return;
		}

		add_action( 'admin_menu', [ $this, 'addDashboardPage' ] );
		add_action( 'admin_head', [ $this, 'hideDashboardPageFromMenu' ] );
		add_action( 'admin_init', [ $this, 'maybeLoadOnboardingWizard' ] );
		add_action( 'admin_init', [ $this, 'redirect' ], 9999 );
	}

	/**
	 * Onboarding Wizard redirect.
	 *
	 * This function checks if a new install or update has just occurred. If so,
	 * then we redirect the user to the appropriate page.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function redirect() {
		// Check if we should consider redirection.
		if ( ! aioseo()->core->cache->get( 'activation_redirect' ) ) {
			return;
		}

		// If we are redirecting, clear the transient so it only happens once.
		aioseo()->core->cache->delete( 'activation_redirect' );

		// Check option to disable welcome redirect.
		if ( get_option( 'aioseo_activation_redirect', false ) ) {
			return;
		}

		// Only do this for single site installs.
		if ( isset( $_GET['activate-multi'] ) || is_network_admin() ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			return;
		}

		wp_safe_redirect( admin_url( 'index.php?page=aioseo-setup-wizard' ) );
		exit;
	}

	/**
	 * Adds a dashboard page for our setup wizard.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addDashboardPage() {
		add_dashboard_page( '', '', aioseo()->admin->getPageRequiredCapability( 'aioseo-setup-wizard' ), 'aioseo-setup-wizard', '' );
	}

	/**
	 * Hide the dashboard page from the menu.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	public function hideDashboardPageFromMenu() {
		remove_submenu_page( 'index.php', 'aioseo-setup-wizard' );
	}

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

		// Check for wizard-specific parameter
		// Allow plugins to disable the setup wizard
		// Check if current user is allowed to save settings.
		if (
			// phpcs:disable HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			! isset( $_GET['page'] ) ||
			'aioseo-setup-wizard' !== sanitize_text_field( wp_unslash( $_GET['page'] ) ) ||
			// phpcs:enable
			! current_user_can( aioseo()->admin->getPageRequiredCapability( 'aioseo-setup-wizard' ) )
		) {
			return;
		}

		set_current_screen();

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

		// If we are redirecting, clear the transient so it only happens once.
		aioseo()->core->cache->delete( 'activation_redirect' );

		$this->loadOnboardingWizard();
	}

	/**
	 * Load the Onboarding Wizard template.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function loadOnboardingWizard() {
		$this->enqueueScripts();
		$this->setupWizardHeader();
		$this->setupWizardContent();
		$this->setupWizardFooter();
		exit;
	}

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

		aioseo()->core->assets->load( 'src/vue/standalone/setup-wizard/main.js', [], aioseo()->helpers->getVueData( 'setup-wizard' ) );

		aioseo()->main->enqueueTranslations();

		wp_enqueue_style( 'common' );
		wp_enqueue_media();
	}

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

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

	/**
	 * Outputs the simplified footer used for the Onboarding Wizard.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function setupWizardFooter() {
		?>
		<?php
		wp_print_scripts( 'aioseo-vendors' );
		wp_print_scripts( 'aioseo-common' );
		wp_print_scripts( 'aioseo-setup-wizard-script' );
		do_action( 'admin_footer', '' );
		do_action( 'admin_print_footer_scripts' );
		// do_action( 'customize_controls_print_footer_scripts' );
		?>
		</body>
		</html>
		<?php
	}

	/**
	 * Check whether or not the Setup Wizard is completed.
	 *
	 * @since 4.2.0
	 *
	 * @return boolean Whether or not the Setup Wizard is completed.
	 */
	public function isCompleted() {
		$wizard = (string) aioseo()->internalOptions->internal->wizard;
		$wizard = json_decode( $wizard );
		if ( ! $wizard ) {
			return false;
		}

		$totalStageCount   = count( $wizard->stages );
		$currentStageCount = array_search( $wizard->currentStage, $wizard->stages, true );

		// If not found, let's assume it's completed.
		if ( false === $currentStageCount ) {
			return true;
		}

		return $currentStageCount + 1 === $totalStageCount;
	}

	/**
	 * Get the next stage of the wizard.
	 *
	 * @since 4.6.2
	 *
	 * @return string The next stage or empty.
	 */
	public function getNextStage() {
		$wizard    = (string) aioseo()->internalOptions->internal->wizard;
		$wizard    = json_decode( $wizard );
		if ( ! $wizard ) {
			return '';
		}

		// Default to success.
		$nextStage = 'success';

		// Get the next stage of the wizard.
		$currentStageIndex = array_search( $wizard->currentStage, $wizard->stages, true );
		if ( ! empty( $wizard->stages[ $currentStageIndex + 1 ] ) ) {
			$nextStage = $wizard->stages[ $currentStageIndex + 1 ];
		}

		return $nextStage;
	}
}Common/Standalone/UserProfileTab.php000066600000012537151135505570013506 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

use AIOSEO\Plugin\Pro\Standalone as ProStandalone;

/**
 * Registers the standalone components.
 *
 * @since 4.2.2
 */
class UserProfileTab {
	/**
	 * Class constructor.
	 *
	 * @since 4.2.2
	 */
	public function __construct() {
		if ( ! is_admin() ) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScript' ] );
		add_action( 'profile_update', [ $this, 'updateUserSocialProfiles' ] );
	}

	/**
	 * Enqueues the script.
	 *
	 * @since 4.2.2
	 *
	 * @return void
	 */
	public function enqueueScript() {
		if ( apply_filters( 'aioseo_user_profile_tab_disable', false ) ) {
			return;
		}

		$screen = aioseo()->helpers->getCurrentScreen();
		if ( empty( $screen->id ) ) {
			return;
		}

		if ( ! in_array( $screen->id, [ 'user-edit', 'profile' ], true ) ) {
			if ( 'follow-up_page_followup-emails-reports' === $screen->id ) {
				aioseo()->core->assets->load( 'src/vue/standalone/user-profile-tab/follow-up-emails-nav-bar.js' );
			}

			return;
		}

		global $user_id; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if ( ! intval( $user_id ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			return;
		}

		aioseo()->core->assets->load( 'src/vue/standalone/user-profile-tab/main.js', [], $this->getVueData() );
		// Load script again so we can add extra data to localize the strings.
		aioseo()->core->assets->load( 'src/vue/standalone/user-profile-tab/main.js', [], [
			'translations' => aioseo()->helpers->getJedLocaleData( 'aioseo-eeat' )
		], 'eeat' );
	}

	/**
	 * Returns the data Vue requires.
	 *
	 * @since 4.2.2
	 *
	 * @return array
	 */
	public function getVueData() {
		global $user_id; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		$socialProfiles = $this->getSocialProfiles();
		foreach ( $socialProfiles as $platformKey => $v ) {
			$metaName                        = 'aioseo_' . aioseo()->helpers->toSnakeCase( $platformKey );
			$socialProfiles[ $platformKey ] = get_user_meta( $user_id, $metaName, true ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		}

		$sameUsername = get_user_meta( $user_id, 'aioseo_profiles_same_username', true ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if ( empty( $sameUsername ) ) {
			$sameUsername = [
				'enable'   => false,
				'username' => '',
				'included' => [ 'facebookPageUrl', 'twitterUrl', 'tiktokUrl', 'pinterestUrl', 'instagramUrl', 'youtubeUrl', 'linkedinUrl' ] // Same as in Options.php.
			];
		}

		$additionalurls = get_user_meta( $user_id, 'aioseo_profiles_additional_urls', true ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		$extraVueData = [
			'userProfile' => [
				'userData'                          => get_userdata( $user_id )->data, // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				'profiles'                          => [
					'sameUsername'   => $sameUsername,
					'urls'           => $socialProfiles,
					'additionalUrls' => $additionalurls
				],
				'isWooCommerceFollowupEmailsActive' => aioseo()->helpers->isWooCommerceFollowupEmailsActive()
			]
		];

		$vueData = aioseo()->helpers->getVueData();
		$vueData = array_merge( $vueData, $extraVueData );

		return $vueData;
	}

	/**
	 * Updates the user social profile URLs when a user's profile is updated.
	 *
	 * @since 4.2.2
	 *
	 * @param  int  $userId The user ID.
	 * @return void
	 */
	public function updateUserSocialProfiles( $userId ) {
		if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'update-user_' . $userId ) ) {
			return;
		}

		if ( empty( $_POST['aioseo-user-social-profiles'] ) ) {
			return;
		}

		$data = json_decode( sanitize_text_field( wp_unslash( $_POST['aioseo-user-social-profiles'] ) ), true );
		if ( empty( $data ) ) {
			return;
		}

		$sanitizedIncluded = [];
		foreach ( $data['sameUsername']['included'] as $platformKey ) {
			$sanitizedIncluded[] = sanitize_text_field( $platformKey );
		}

		$sanitizedSameUsernameData = [
			'enable'   => (bool) $data['sameUsername']['enable'],
			'username' => sanitize_text_field( $data['sameUsername']['username'] ),
			'included' => array_filter( $sanitizedIncluded )
		];

		update_user_meta( $userId, 'aioseo_profiles_same_username', $sanitizedSameUsernameData );

		foreach ( $data['urls'] as $platformKey => $value ) {
			$value    = sanitize_text_field( $value );
			$metaName = 'aioseo_' . aioseo()->helpers->toSnakeCase( $platformKey );
			update_user_meta( $userId, $metaName, $value );
		}

		$additionalUrls          = sanitize_text_field( $data['additionalUrls'] );
		$sanitizedAdditionalUrls = preg_replace( '/\h/', "\n", (string) $additionalUrls );
		update_user_meta( $userId, 'aioseo_profiles_additional_urls', $sanitizedAdditionalUrls );
	}

	/**
	 * Returns a list of supported social profiles.
	 *
	 * @since 4.2.2
	 *
	 * @return array
	 */
	public function getSocialProfiles() {
		return [
			'facebookPageUrl' => '',
			'twitterUrl'      => '',
			'instagramUrl'    => '',
			'tiktokUrl'       => '',
			'pinterestUrl'    => '',
			'youtubeUrl'      => '',
			'linkedinUrl'     => '',
			'tumblrUrl'       => '',
			'yelpPageUrl'     => '',
			'soundCloudUrl'   => '',
			'wikipediaUrl'    => '',
			'myspaceUrl'      => '',
			'wordPressUrl'    => '',
			'blueskyUrl'      => '',
			'threadsUrl'      => ''
		];
	}
}Common/Standalone/BbPress/Component.php000066600000005100151135505570014106 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\BbPress;

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

/**
 * bbPress Component class.
 *
 * @since 4.8.1
 */
class Component {
	/**
	 * The current component template type.
	 *
	 * @since 4.8.1
	 *
	 * @var string|null
	 */
	public $templateType = null;

	/**
	 * The topic single page data.
	 *
	 * @since 4.8.1
	 *
	 * @var array
	 */
	public $topic = [];

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

		$this->setTemplateType();
		$this->setTopic();
	}

	/**
	 * Sets the template type.
	 *
	 * @since 4.8.1
	 *
	 * @return void
	 */
	private function setTemplateType() {
		if ( function_exists( 'bbp_is_single_topic' ) && bbp_is_single_topic() ) {
			$this->templateType = 'bbp-topic_single';
		}
	}

	/**
	 * Sets the topic data.
	 *
	 * @since 4.8.1
	 *
	 * @return void
	 */
	private function setTopic() {
		if ( 'bbp-topic_single' !== $this->templateType ) {
			return;
		}

		if (
			! function_exists( 'bbpress' ) ||
			! function_exists( 'bbp_has_replies' ) ||
			! bbp_has_replies()
		) {
			return;
		}

		$replyQuery = bbpress()->reply_query ?? null;
		$replies    = $replyQuery->posts ?? [];
		$mainTopic  = is_array( $replies ) && ! empty( $replies ) ? array_shift( $replies ) : null;

		if ( $mainTopic instanceof \WP_Post ) {
			$this->topic = [
				'title'   => $mainTopic->post_title,
				'content' => $mainTopic->post_content,
				'date'    => $mainTopic->post_date,
				'author'  => get_the_author_meta( 'display_name', $mainTopic->post_author ),
			];

			$comments = [];
			if ( ! empty( $replies ) ) {
				foreach ( $replies as $reply ) {
					if ( ! $reply instanceof \WP_Post ) {
						continue;
					}

					$comments[ $reply->ID ] = [
						'content'       => $reply->post_content,
						'date_recorded' => $reply->post_date,
						'user_fullname' => get_the_author_meta( 'display_name', $reply->post_author ),
					];

					if ( ! empty( $reply->reply_to ) ) {
						$comments[ $reply->reply_to ]['children'][] = $comments[ $reply->ID ];

						unset( $comments[ $reply->ID ] );
					}
				}

				$this->topic['comment'] = array_values( $comments );
			}

			return;
		}

		$this->resetComponent();
	}

	/**
	 * Resets some of the component properties.
	 *
	 * @since 4.8.1
	 *
	 * @return void
	 */
	private function resetComponent() {
		$this->templateType = null;
	}

	/**
	 * Determines the schema type for the current component.
	 *
	 * @since 4.8.1
	 *
	 * @return void
	 */
	public function determineSchemaGraphsAndContext() {
	}
}Common/Standalone/BbPress/BbPress.php000066600000002273151135505570013514 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\BbPress;

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

/**
 * Handles the bbPress integration with AIOSEO.
 *
 * @since 4.8.1
 */
class BbPress {
	/**
	 * Instance of the Component class.
	 *
	 * @since 4.8.1
	 *
	 * @var Component
	 */
	public $component;

	/**
	 * Class constructor.
	 *
	 * @since 4.8.1
	 */
	public function __construct() {
		if (
			aioseo()->helpers->isAjaxCronRestRequest() ||
			! aioseo()->helpers->isPluginActive( 'bbpress' )
		) {
			return;
		}

		// Hook into `plugins_loaded` to ensure bbPress has loaded some necessary functions.
		add_action( 'plugins_loaded', [ $this, 'maybeLoad' ], 20 );
	}

	/**
	 * Hooked into `plugins_loaded` action hook.
	 *
	 * @since 4.8.1
	 *
	 * @return void
	 */
	public function maybeLoad() {
		// If the bbPress version is below 2 we bail.
		if ( ! function_exists( 'bbp_get_version' ) || version_compare( bbp_get_version(), '2', '<' ) ) {
			return;
		}

		add_action( 'wp', [ $this, 'setComponent' ] );
	}

	/**
	 * Hooked into `wp` action hook.
	 *
	 * @since 4.8.1
	 *
	 * @return void
	 */
	public function setComponent() {
		$this->component = new Component();
	}
}Common/Standalone/FlyoutMenu.php000066600000003405151135505570012721 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

/**
 * Handles the flyout menu.
 *
 * @since 4.2.0
 */
class FlyoutMenu {
	/**
	 * Class constructor.
	 *
	 * @since 4.2.0
	 */
	public function __construct() {
		if (
			! is_admin() ||
			wp_doing_ajax() ||
			wp_doing_cron() ||
			! $this->isEnabled()
		) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAssets' ], 11 );
		add_filter( 'admin_body_class', [ $this, 'addBodyClass' ] );
	}

	/**
	 * Enqueues the required assets.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	public function enqueueAssets() {
		if ( ! $this->shouldEnqueue() ) {
			return;
		}

		aioseo()->core->assets->load( 'src/vue/standalone/flyout-menu/main.js' );
	}

	/**
	 * Filters the CSS classes for the body tag in the admin.
	 *
	 * @since 4.2.0
	 *
	 * @param  string $classes Space-separated list of CSS classes.
	 * @return string          Space-separated list of CSS classes.
	 */
	public function addBodyClass( $classes ) {
		if ( $this->shouldEnqueue() ) {
			// This adds a bottom margin to our menu so that we push the footer down and prevent the flyout menu from overlapping the "Save Changes" button.
			$classes .= ' aioseo-flyout-menu-enabled ';
		}

		return $classes;
	}

	/**
	 * Checks whether the flyout menu script should be enqueued.
	 *
	 * @since 4.2.0
	 *
	 * @return bool Whether the flyout menu script should be enqueued.
	 */
	private function shouldEnqueue() {
		return aioseo()->admin->isAioseoScreen();
	}

	/**
	 * Checks whether the flyout menu is enabled.
	 *
	 * @since 4.2.0
	 *
	 * @return bool Whether the flyout menu is enabled.
	 */
	public function isEnabled() {
		return apply_filters( 'aioseo_flyout_menu_enable', true );
	}
}Common/Standalone/Blocks/FaqPage.php000066600000001075151135505570013334 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\Blocks;

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

/**
 * FaqPage Block.
 *
 * @since 4.2.3
 */
class FaqPage extends Blocks {
	/**
	 * Register the block.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	public function register() {
		aioseo()->blocks->registerBlock( 'aioseo/faq',
			[
				'render_callback' => function( $attributes, $content ) {
					if ( isset( $attributes['hidden'] ) && true === $attributes['hidden'] ) {
						return '';
					}

					return $content;
				},
			]
		);
	}
}Common/Standalone/Blocks/Blocks.php000066600000001166151135505570013246 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\Blocks;

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

/**
 * Loads core classes.
 *
 * @since 4.2.3
 */
abstract class Blocks {
	/**
	 * Class constructor.
	 *
	 * @since 4.2.3
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'init' ] );
	}

	/**
	 * Initializes our blocks.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	public function init() {
		$this->register();
	}

	/**
	 * Registers the block. This is a wrapper to be extended in the child class.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	public function register() {}
}Common/Standalone/Blocks/TableOfContents.php000066600000000616151135505570015062 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\Blocks;

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

/**
 * Table of Contents Block.
 *
 * @since 4.2.3
 */
class TableOfContents extends Blocks {
	/**
	 * Register the block.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	public function register() {
		aioseo()->blocks->registerBlock( 'aioseo/table-of-contents' );
	}
}Common/Standalone/PublishPanel.php000066600000001365151135505570013203 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Handles the Publish Panel in the Block Editor.
 *
 * @since 4.2.0
 */
class PublishPanel {
	/**
	 * Class constructor.
	 *
	 * @since 4.2.0
	 */
	public function __construct() {
		if ( ! is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScript' ] );
	}

	/**
	 * Enqueues the script.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	public function enqueueScript() {
		if ( ! aioseo()->helpers->isScreenBase( 'post' ) ) {
			return;
		}

		aioseo()->core->assets->load( 'src/vue/standalone/publish-panel/main.js' );
	}
}Common/Standalone/PageBuilders/Avada.php000066600000005617151135505570014203 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;

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

/**
 * Integrate our SEO Panel with Avada Page Builder.
 *
 * @since 4.5.2
 */
class Avada extends Base {
	/**
	 * The plugin files.
	 *
	 * @since 4.5.2
	 *
	 * @var array
	 */
	public $plugins = [
		'fusion-builder/fusion-builder.php'
	];

	/**
	 * The integration slug.
	 *
	 * @since 4.5.2
	 *
	 * @var string
	 */
	public $integrationSlug = 'avada';

	/**
	 * Init the integration.
	 *
	 * @since 4.5.2
	 *
	 * @return void
	 */
	public function init() {
		add_action( 'fusion_enqueue_live_scripts', [ $this, 'enqueue' ] );
		add_action( 'fusion_builder_admin_scripts_hook', [ $this, 'enqueue' ] );
		add_action( 'wp_footer', [ $this, 'addSidebarWrapper' ] );
	}

	/**
	 * Check if we are in the front-end builder.
	 *
	 * @since 4.5.2
	 *
	 * @return boolean Whether or not we are in the front-end builder.
	 */
	public function isBuilder() {
		return function_exists( 'fusion_is_builder_frame' ) && fusion_is_builder_frame();
	}

	/**
	 * Check if we are in the front-end preview.
	 *
	 * @since 4.5.2
	 *
	 * @return boolean Whether or not we are in the front-end preview.
	 */
	public function isPreview() {
		return function_exists( 'fusion_is_preview_frame' ) && fusion_is_preview_frame();
	}

	/**
	 * Adds the sidebar wrapper in footer when is in page builder.
	 *
	 * @since 4.5.2
	 *
	 * @return void
	 */
	public function addSidebarWrapper() {
		if ( ! $this->isBuilder() ) {
			return;
		}

		echo '<div id="fusion-builder-aioseo-sidebar"></div>';
	}

	/**
	 * Enqueue the scripts and styles.
	 *
	 * @since 4.5.2
	 *
	 * @return void
	 */
	public function enqueue() {
		if ( ! aioseo()->postSettings->canAddPostSettingsMetabox( get_post_type( $this->getPostId() ) ) ) {
			return;
		}

		parent::enqueue();
	}

	/**
	 * Returns whether or not the given Post ID was built with WPBakery.
	 *
	 * @since 4.5.2
	 *
	 * @param  int $postId The Post ID.
	 * @return boolean     Whether or not the Post was built with WPBakery.
	 */
	public function isBuiltWith( $postId ) {
		return 'active' === get_post_meta( $postId, 'fusion_builder_status', true );
	}

	/**
	 * Returns whether should or not limit the modified date.
	 *
	 * @since 4.5.2
	 *
	 * @param  int     $postId The Post ID.
	 * @return boolean         Whether or not sholud limit the modified date.
	 */
	public function limitModifiedDate( $postId ) {
		// This method is supposed to be used in the `wp_ajax_fusion_app_save_post_content` action.
		if ( ! isset( $_POST['fusion_load_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['fusion_load_nonce'] ) ), 'fusion_load_nonce' ) ) {
			return false;
		}

		$editorPostId = ! empty( $_REQUEST['post_id'] ) ? intval( $_REQUEST['post_id'] ) : 0;
		if ( $editorPostId !== $postId ) {
			return false;
		}

		return ! empty( $_REQUEST['query']['aioseo_limit_modified_date'] );
	}
}Common/Standalone/PageBuilders/SeedProd.php000066600000007154151135505570014672 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;

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

/**
 * Integrate our SEO Panel with SeedProd Page Builder.
 *
 * @since 4.1.7
 */
class SeedProd extends Base {
	/**
	 * The plugin files.
	 *
	 * @since 4.1.7
	 *
	 * @var array
	 */
	public $plugins = [
		'coming-soon/coming-soon.php',
		'seedprod-coming-soon-pro-5/seedprod-coming-soon-pro-5.php',
	];

	/**
	 * The integration slug.
	 *
	 * @since 4.1.7
	 *
	 * @var string
	 */
	public $integrationSlug = 'seedprod';

	/**
	 * Init the integration.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function init() {
		$postType = get_post_type( $this->getPostId() );

		if ( ! aioseo()->postSettings->canAddPostSettingsMetabox( $postType ) ) {
			return;
		}

		// SeedProd de-enqueues and de-register scripts/styles on priority PHP_INT_MAX.
		// Thus, we need to enqueue our scripts at the same priority for more compatibility.
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ], PHP_INT_MAX );
		add_filter( 'style_loader_tag', [ $this, 'replaceStyleTag' ], 10, 2 );
	}

	/**
	 * Enqueue the scripts and styles.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function enqueue() {
		if ( ! $this->isBuilderScreen() ) {
			return;
		}

		parent::enqueue();
	}

	/**
	 * Check whether or not is builder screen.
	 *
	 * @since 4.1.7
	 *
	 * @return boolean Whether or not is builder screen.
	 */
	public function isBuilderScreen() {
		$currentScreen = aioseo()->helpers->getCurrentScreen();

		return $currentScreen && preg_match( '/seedprod.*?_builder$/i', (string) $currentScreen->base );
	}

	/**
	 * Replace original tag to prevent being removed by SeedProd.
	 *
	 * @param  string $tag    The <link> tag for the enqueued style.
	 * @param  string $handle The style's registered handle.
	 * @return string         The tag.
	 */
	public function replaceStyleTag( $tag, $handle = '' ) {
		if ( ! $this->isBuilderScreen() ) {
			return $tag;
		}

		$aioseoCommonHandle = 'aioseo-' . $this->integrationSlug . '-common';

		if ( $aioseoCommonHandle === $handle ) {
			// All the *common.css links are removed from SeedProd.
			// https://github.com/awesomemotive/seedprod-plugins/blob/32854442979bfa068aadf9b8a8a929e5f9f353e5/seedprod-pro/resources/views/builder.php#L406
			$tag = str_ireplace( 'href=', 'data-href=', $tag );
		}

		return $tag;
	}

	/**
	 * Returns whether or not the given Post ID was built with SeedProd.
	 *
	 * @since 4.1.7
	 *
	 * @param  int $postId The Post ID.
	 * @return boolean     Whether or not the Post was built with SeedProd.
	 */
	public function isBuiltWith( $postId ) {
		$isSeedProd = get_post_meta( $postId, '_seedprod_page', true );
		if ( ! empty( $isSeedProd ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Checks whether or not we should prevent the date from being modified.
	 *
	 * @since 4.5.2
	 *
	 * @param  int  $postId The Post ID.
	 * @return bool         Whether or not we should prevent the date from being modified.
	 */
	public function limitModifiedDate( $postId ) {
		// This method is supposed to be used in the `wp_ajax_seedprod_pro_save_lpage` action.
		if ( wp_doing_ajax() && ! check_ajax_referer( 'seedprod_nonce', false, false ) ) {
			return false;
		}

		$landingPageId = ! empty( $_REQUEST['lpage_id'] ) ? (int) $_REQUEST['lpage_id'] : false;
		if ( $landingPageId !== $postId ) {
			return false;
		}

		$settings = ! empty( $_REQUEST['settings'] ) ? json_decode( sanitize_text_field( wp_unslash( $_REQUEST['settings'] ) ) ) : false;
		if ( empty( $settings ) || empty( $settings->aioseo_limit_modified_date ) ) {
			return false;
		}

		return true;
	}
}Common/Standalone/PageBuilders/ThriveArchitect.php000066600000031364151135505570016255 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;

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

/**
 * Integrate our SEO Panel with Thrive Architect Page Builder.
 *
 * @since 4.6.6
 */
class ThriveArchitect extends Base {
	/**
	 * The plugin files.
	 *
	 * @since 4.6.6
	 *
	 * @var array
	 */
	public $plugins = [
		'thrive-visual-editor/thrive-visual-editor.php'
	];

	/**
	 * The integration slug.
	 *
	 * @since 4.6.6
	 *
	 * @var string
	 */
	public $integrationSlug = 'thrive-architect';

	/**
	 * Init the integration.
	 *
	 * @since 4.6.6
	 *
	 * @return void
	 */
	public function init() {
		add_filter( 'tcb_allowed_ajax_options', [ $this, 'makeSettingsAllowed' ] );

		if ( ! aioseo()->postSettings->canAddPostSettingsMetabox( get_post_type( $this->getPostId() ) ) ) {
			return;
		}

		add_action( 'tcb_main_frame_enqueue', [ $this, 'enqueue' ] );
		add_filter( 'tve_main_js_dependencies', [ $this, 'mainJsDependencies' ] );
		add_action( 'tcb_right_sidebar_content_settings', [ $this, 'addSettingsTab' ] );
		add_action( 'tcb_sidebar_extra_links', [ $this, 'addSidebarButton' ] );
		add_filter( 'tcb_main_frame_localize', [ $this, 'localizeData' ] );
	}

	/**
	 * Overrides the parent enqueue to add WordPress styles that we need.
	 *
	 * @since 4.6.6
	 *
	 * @return void
	 */
	public function enqueue() {
		wp_enqueue_style( 'common' );
		wp_enqueue_style( 'buttons' );
		wp_enqueue_style( 'forms' );
		wp_enqueue_style( 'list-tables' );
		wp_enqueue_style( 'wp-components' );

		print_admin_styles();

		parent::enqueue();
	}

	/**
	 * Add our javascript to the plugin dependencies.
	 *
	 * @since 4.6.6
	 *
	 * @param  array $dependencies The dependencies.
	 * @return array               The dependencies.
	 */
	public function mainJsDependencies( $dependencies ) {
		$dependencies[] = aioseo()->core->assets->jsHandle( "src/vue/standalone/page-builders/{$this->integrationSlug}/main.js" );

		return $dependencies;
	}

	/**
	 * Add the extra link to the sidebar.
	 *
	 * @since 4.6.6
	 *
	 * @return void
	 */
	public function addSidebarButton() {
		$tooltip = sprintf(
			// Translators: 1 - The plugin short name ("AIOSEO").
			esc_html__( '%1$s Settings', 'all-in-one-seo-pack' ),
			AIOSEO_PLUGIN_SHORT_NAME
		);

		// phpcs:disable Generic.Files.LineLength.MaxExceeded
		?>
		<a href="javascript:void(0)" class="mouseenter mouseleave sidebar-item tcb-sidebar-icon-aioseo" data-position="left" data-toggle="settings" data-tooltip="<?php echo esc_attr( $tooltip ); ?>">
			<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
				<path d="M10.0011 19C14.9722 19 19.0021 14.9706 19.0021 10C19.0021 5.02944 14.9722 1 10.0011 1C5.02991 1 1 5.02944 1 10C1 14.9706 5.02991 19 10.0011 19Z" stroke="currentColor"/>
				<path d="M9.99664 13.3228C9.99896 13.2104 9.47813 13.1752 9.37307 13.141C8.56228 12.8777 8.04027 12.3293 7.78204 11.5205C7.6493 11.1043 7.68851 10.6765 7.68163 10.2515C7.68162 10.2511 7.68134 10.2507 7.68093 10.2506V10.2506C7.68051 10.2504 7.68023 10.25 7.68023 10.2496C7.68069 9.99579 7.67884 9.74246 7.68115 9.48867C7.683 9.31099 7.74131 9.25453 7.92133 9.25222C8.07128 9.25037 8.22168 9.24482 8.37116 9.25407C8.48454 9.26101 8.86945 9.25407 8.86945 9.25407M9.99664 13.3228C9.98877 13.7037 10.0003 14.1192 10.0008 14.5M9.99664 13.3228C10.0036 13.7152 9.99664 14.1076 10.0008 14.5M9.99664 13.3228C9.99863 13.2265 10.5453 13.1946 10.6332 13.1715C11.6884 12.8929 12.42 11.955 12.4311 10.8639C12.4358 10.4137 12.433 9.96389 12.432 9.51366C12.432 9.30312 12.3788 9.25268 12.1641 9.25176C12.0197 9.25083 11.8749 9.24435 11.7314 9.25407C11.6213 9.26148 11.2793 9.25176 11.2793 9.25176M10.0008 14.5C10.0008 14.7878 9.72149 14.8117 9.50075 15C9.29018 15.1795 8.77054 15.5587 8.52758 15.6966C8.32442 15.8118 8.12033 15.8428 7.90097 15.7401C7.72373 15.6573 7.53862 15.5916 7.36276 15.5059C7.13877 15.3963 7.04344 15.1936 7.08879 14.947C7.1323 14.7091 7.17765 14.4718 7.22578 14.2348C7.27529 13.9933 7.21745 13.7897 7.02632 13.6291C6.78706 13.4283 6.5677 13.2071 6.37195 12.9637C6.21321 12.7671 6.01098 12.7102 5.76941 12.762C5.5445 12.8101 5.31866 12.8559 5.09282 12.9008C4.84339 12.9503 4.64902 12.8587 4.53425 12.6329C4.42457 12.4173 4.33433 12.1924 4.2589 11.9624C4.1793 11.719 4.24085 11.5191 4.4491 11.3683C4.64532 11.2262 4.84616 11.0911 5.04794 10.9574C5.26961 10.8107 5.34828 10.6071 5.31866 10.348C5.2858 10.0606 5.28673 9.7714 5.31635 9.48405C5.34411 9.21613 5.25711 9.01253 5.02757 8.86585C4.8383 8.74461 4.65318 8.6169 4.46853 8.4878C4.23576 8.32492 4.16541 8.12086 4.26028 7.85525C4.33803 7.6387 4.42318 7.42446 4.51852 7.21531C4.62218 6.98811 4.80822 6.89186 5.05349 6.93258C5.28025 6.97006 5.50609 7.0168 5.731 7.06585C5.99154 7.12322 6.20812 7.06631 6.3775 6.84929C6.56261 6.61238 6.77781 6.40322 7.00503 6.2061C7.18829 6.04693 7.27205 5.8549 7.21143 5.60549C7.16006 5.3931 7.12906 5.17608 7.08509 4.96184C7.01429 4.61803 7.1036 4.42924 7.4257 4.28024C7.6085 4.19603 7.79453 4.11783 7.98335 4.04842C8.22399 3.9605 8.42762 4.02574 8.57385 4.23536C8.71546 4.43849 8.84967 4.64718 8.98341 4.85587C9.12317 5.07382 9.32032 5.15896 9.57392 5.12703C9.86131 5.09094 10.1492 5.09325 10.437 5.12564C10.6763 5.15248 10.8669 5.0715 11.0002 4.86698C11.1261 4.67402 11.251 4.48014 11.3778 4.28765C11.5611 4.01001 11.7467 3.94384 12.054 4.05952C12.2479 4.13217 12.4395 4.21176 12.6264 4.3006C12.8731 4.41813 12.9684 4.6134 12.9198 4.87993C12.8763 5.11777 12.8291 5.35469 12.7828 5.59207C12.737 5.82621 12.7944 6.0261 12.9804 6.18297C13.2234 6.38842 13.4437 6.61561 13.6473 6.86086C13.8005 7.04502 13.993 7.11443 14.2337 7.05474C14.4567 6.99921 14.6839 6.95896 14.9098 6.91407C15.1648 6.86317 15.3661 6.95988 15.4822 7.19217C15.5882 7.40364 15.6761 7.6225 15.7506 7.84693C15.8335 8.09726 15.771 8.29901 15.5558 8.45495C15.3596 8.59654 15.1583 8.73166 14.9565 8.86585C14.7473 9.00466 14.6645 9.19855 14.6913 9.44425C14.7237 9.74363 14.7242 10.0435 14.6946 10.3429C14.6691 10.6038 14.7552 10.8033 14.9783 10.9467C15.1624 11.0652 15.3429 11.1897 15.523 11.3146C15.7816 11.4946 15.8506 11.6973 15.7451 11.9879C15.6766 12.1771 15.5993 12.3636 15.5174 12.5473C15.3818 12.8504 15.1907 12.9401 14.8621 12.8721C14.6423 12.8263 14.4211 12.7888 14.2017 12.7398C13.9939 12.6935 13.8213 12.7574 13.689 12.911C13.4627 13.1738 13.2234 13.4218 12.9638 13.6527C12.8088 13.7906 12.7467 13.9706 12.7958 14.1849C12.8485 14.4148 12.8874 14.6476 12.9337 14.879C12.9957 15.189 12.8999 15.3856 12.6088 15.5235C12.4478 15.5999 12.2789 15.66 12.1178 15.7364C11.911 15.8345 11.7175 15.8058 11.5236 15.7003C11.2265 15.5388 10.741 15.2332 10.5009 15C10.3403 14.8441 10.0031 14.7207 10.0008 14.5ZM11.2793 9.25176C11.2848 8.85382 11.2873 8.01509 11.2804 7.61719C11.2795 7.56905 11.2791 7.5401 11.279 7.52286C11.2788 7.50546 11.2793 7.50858 11.2794 7.52598C11.2798 7.63906 11.2816 8.09163 11.2833 8.28906C11.2796 8.687 11.2714 8.85382 11.2793 9.25176ZM11.2793 9.25176C11.2793 9.25176 10.9086 9.25685 10.7873 9.255C10.2968 9.24806 9.80624 9.24852 9.31569 9.255C9.19999 9.25638 8.86945 9.25407 8.86945 9.25407M8.86945 9.25407C8.87593 8.8677 8.87547 8.34389 8.87408 7.95752C8.87346 7.78806 8.87262 7.62829 8.87143 7.54953C8.8709 7.51441 8.86954 7.51752 8.86963 7.55263C8.86985 7.62907 8.8701 7.7811 8.86945 7.95752C8.86853 8.34435 8.86251 8.8677 8.86945 9.25407Z" stroke="currentColor"/>
			</svg>
		</a>
		<?php
		//phpcs:enable Generic.Files.LineLength.MaxExceeded
	}

	/**
	 * Adds the settings tab for AIOSEO in the Thrive Architect page builder.
	 *
	 * @since 4.6.6
	 *
	 * @return void
	 */
	public function addSettingsTab() {
		//phpcs:disable Generic.Files.LineLength.MaxExceeded
		?>
		<div class="tve-component s-item tcb-aioseo">
			<div class="dropdown-header">
				<div class="group-description s-name">
					<?php echo esc_html( AIOSEO_PLUGIN_SHORT_NAME ); ?>
				</div>
			</div>
			<div class="dropdown-content">
				<div class="tcb-aioseo-settings">
					<button class="click tcb-settings-modal-open-button s-item inside-button">
						<span class="s-name">
							<?php
								printf(
									// Translators: 1 - The plugin short name ("AIOSEO").
									esc_html__( '%1$s Settings', 'all-in-one-seo-pack' ),
									esc_html( AIOSEO_PLUGIN_SHORT_NAME )
								);
							?>
						</span>
					</button>
					<div class="mt-10 button-group">
						<div id="aioseo-score-btn-settings"></div>
						<button type="button" class="p-3 action-btn click" id="settings-action-btn">
							<svg class="when-active" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
								<path d="M9.61433 9.94582C10.145 11.0636 10.2253 12.3535 9.45767 13.2683C9.24696 13.5194 8.89896 13.533 8.66555 13.3372L6.22379 11.2883L4.64347 13.1717C4.62842 13.1896 4.59542 13.1925 4.58036 13.2104L3.42703 13.7098C3.28287 13.7722 3.12128 13.6366 3.15772 13.4838L3.44925 12.2613C3.4643 12.2434 3.47935 12.2254 3.4944 12.2075L5.07472 10.3241L2.63295 8.27524C2.3816 8.06433 2.35256 7.73431 2.56327 7.4832C3.3158 6.58636 4.59718 6.40838 5.7901 6.73691L7.81453 4.7983L7.06045 4.16555C6.80909 3.95464 6.78006 3.62462 6.99077 3.37351L7.7132 2.51255C7.90885 2.27937 8.25395 2.23272 8.50531 2.44363L13.3888 6.54141C13.6222 6.73725 13.6542 7.10028 13.4585 7.33345L12.7361 8.19441C12.5254 8.44552 12.1774 8.45917 11.944 8.26332L11.172 7.61551L9.61433 9.94582Z" fill="#FFFFFF"/>
							</svg>

							<svg class="when-inactive" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
								<path fill-rule="evenodd" clip-rule="evenodd" d="M5.87874 6.50704C5.88995 6.50899 5.90115 6.51097 5.91236 6.513L7.23686 5.29914L6.55461 4.72665C6.3212 4.5308 6.27436 4.18554 6.48527 3.93418L7.6905 2.49785C7.88635 2.26445 8.24956 2.23267 8.48297 2.42852L13.3665 6.52629C13.6179 6.73721 13.6317 7.08535 13.4358 7.31876L12.2306 8.75509C12.0197 9.00645 11.6895 9.03534 11.4381 8.82442L10.7738 8.26701L9.80841 9.78218C9.81235 9.79286 9.81625 9.80354 9.82011 9.81424L9.23164 9.32046L10.6275 7.16519L11.7766 8.12937L12.7408 6.9803L8.14451 3.12358L7.18033 4.27264L8.3294 5.23682L6.4644 6.99846L5.87874 6.50704ZM4.72914 6.45619C3.84314 6.53709 3.0184 6.91015 2.43354 7.63253C2.31301 7.77616 2.31817 8.02525 2.47976 8.16084L5.35242 10.5713L3.54458 12.7258L3.51445 12.7617L3.176 13.4568C3.09138 13.6305 3.28888 13.7962 3.44531 13.6827L4.07103 13.2287L4.10116 13.1928L5.909 11.0383L8.78167 13.4488C8.94326 13.5844 9.18946 13.5462 9.30998 13.4025C9.90734 12.6906 10.1364 11.8178 10.0663 10.9346L9.15211 10.1675C9.42355 10.9846 9.399 11.8779 8.95854 12.6181L3.26707 7.84242C3.90443 7.26739 4.79027 7.09684 5.64573 7.2253L4.72914 6.45619Z" fill="#50565F"/>
								<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4242 11.9993L3.23163 4.28583L3.68158 3.7496L12.8741 11.463L12.4242 11.9993Z" fill="#50565F"/>
							</svg>
						</button>
					</div>
				</div>
			</div>
		</div>
		<?php
		//phpcs:enable Generic.Files.LineLength.MaxExceeded
	}

	/**
	 * Localizes the data by adding the 'is_aioseo_settings_enabled' option to the provided data array.
	 *
	 * @since 4.6.6
	 *
	 * @param  array $data The data array to be localized.
	 * @return array       The localized data array with the 'is_aioseo_settings_enabled' option added.
	 */
	public function localizeData( $data ) {
		// We use get_option here since it is how Thrive Architect saves the settings.
		$data['is_aioseo_settings_enabled'] = get_option( 'is_aioseo_settings_enabled', true );

		return $data;
	}

	/**
	 * Adds 'is_aioseo_settings_enabled' to the list of allowed settings.
	 *
	 * @since 4.6.6
	 *
	 * @param  array $options The array of allowed settings.
	 * @return array          The updated array of allowed settings.
	 */
	public function makeSettingsAllowed( $options ) {
		$options[] = 'is_aioseo_settings_enabled';

		return $options;
	}

	/**
	 * Returns whether or not the given Post ID was built with Thrive Architect.
	 *
	 * @since 4.6.6
	 *
	 * @param  int     $postId The Post ID.
	 * @return boolean         Whether or not the Post was built with Thrive Architect.
	 */
	public function isBuiltWith( $postId ) {
		if ( ! function_exists( 'tcb_post' ) ) {
			return false;
		}

		return tcb_post( $postId )->editor_enabled();
	}

	/**
	 * Returns whether should or not limit the modified date.
	 *
	 * @since 4.6.6
	 *
	 * @param  int     $postId The Post ID.
	 * @return boolean         Whether or not sholud limit the modified date.
	 */
	public function limitModifiedDate( $postId ) {
		if ( ! class_exists( 'TCB_Editor_Ajax' ) ) {
			return false;
		}

		// This method is supposed to be used in the `wp_ajax_tcb_editor_ajax` action.
		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), \TCB_Editor_Ajax::NONCE_KEY ) ) {
			return false;
		}

		$editorPostId = ! empty( $_REQUEST['post_id'] ) ? intval( $_REQUEST['post_id'] ) : 0;
		if ( $editorPostId !== $postId ) {
			return false;
		}

		return ! empty( $_REQUEST['aioseo_limit_modified_date'] ) && 'true' === $_REQUEST['aioseo_limit_modified_date'];
	}
}Common/Standalone/PageBuilders/Base.php000066600000012307151135505570014033 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;

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

/**
 * Base class for each of our page builder integrations.
 *
 * @since 4.1.7
 */
abstract class Base {
	/**
	 * The plugin files we can integrate with.
	 *
	 * @since 4.1.7
	 *
	 * @var array
	 */
	public $plugins = [];

	/**
	 * The themes names we can integrate with.
	 *
	 * @since 4.1.7
	 *
	 * @var array
	 */
	public $themes = [];

	/**
	 * The integration slug.
	 *
	 * @since 4.1.7
	 *
	 * @var string
	 */
	public $integrationSlug = '';

	/**
	 * Class constructor.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function __construct() {
		// We need to delay it to give other plugins a chance to register custom post types.
		add_action( 'init', [ $this, '_init' ], PHP_INT_MAX );
	}

	/**
	 * The internal init function.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function _init() {
		// Check if we do have an integration slug.
		if ( empty( $this->integrationSlug ) ) {
			return;
		}

		// Check if the plugin or theme to integrate with is active.
		if ( ! $this->isPluginActive() && ! $this->isThemeActive() ) {
			return;
		}

		// Check if we can proceed with the integration.
		if ( apply_filters( 'aioseo_page_builder_integration_disable', false, $this->integrationSlug ) ) {
			return;
		}

		$this->init();
	}

	/**
	 * The init function.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function init() {}

	/**
	 * Check if the integration is active.
	 *
	 * @since 4.4.8
	 *
	 * @return bool Whether or not the integration is active.
	 */
	public function isActive() {
		return $this->isPluginActive() || $this->isThemeActive();
	}

	/**
	 * Check whether or not the plugin is active.
	 *
	 * @since 4.1.7
	 *
	 * @return bool Whether or not the plugin is active.
	 */
	public function isPluginActive() {
		include_once ABSPATH . 'wp-admin/includes/plugin.php';

		$plugins = apply_filters( 'aioseo_page_builder_integration_plugins', $this->plugins, $this->integrationSlug );

		foreach ( $plugins as $basename ) {
			if ( is_plugin_active( $basename ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Check whether or not the theme is active.
	 *
	 * @since 4.1.7
	 *
	 * @return bool Whether or not the theme is active.
	 */
	public function isThemeActive() {
		$themes = apply_filters( 'aioseo_page_builder_integration_themes', $this->themes, $this->integrationSlug );

		$theme = wp_get_theme();
		foreach ( $themes as $name ) {
			if ( $name === $theme->stylesheet || $name === $theme->template ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Enqueue the scripts and styles.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function enqueue() {
		$integrationSlug = $this->integrationSlug;
		aioseo()->core->assets->load( "src/vue/standalone/page-builders/$integrationSlug/main.js", [], aioseo()->helpers->getVueData( 'post', $this->getPostId(), $integrationSlug ) );

		aioseo()->core->assets->enqueueCss( 'src/vue/assets/scss/integrations/main.scss' );

		aioseo()->admin->addAioseoModalPortal();
		aioseo()->main->enqueueTranslations();
	}

	/**
	 * Get the post ID.
	 *
	 * @since 4.1.7
	 *
	 * @return int|null The post ID or null.
	 */
	public function getPostId() {
		// phpcs:disable HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		foreach ( [ 'id', 'post', 'post_id' ] as $key ) {
			if ( ! empty( $_GET[ $key ] ) ) {
				return (int) sanitize_text_field( wp_unslash( $_GET[ $key ] ) );
			}
		}
		// phpcs:enable

		if ( ! empty( $GLOBALS['post'] ) ) {
			return (int) $GLOBALS['post']->ID;
		}

		return null;
	}

	/**
	 * Returns the page builder edit url for the given Post ID.
	 *
	 * @since 4.3.1
	 *
	 * @param  int    $postId The Post ID.
	 * @return string         The Edit URL.
	 */
	public function getEditUrl( $postId ) { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return '';
	}

	/**
	 * Returns whether or not the given Post ID was built with the Page Builder.
	 *
	 * @since 4.1.7
	 *
	 * @param  int $postId The Post ID.
	 * @return boolean     Whether or not the Post was built with the Page Builder.
	 */
	public function isBuiltWith( $postId ) { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return false;
	}

	/**
	 * Checks whether or not we should prevent the date from being modified.
	 *
	 * @since 4.5.2
	 *
	 * @param  int  $postId The Post ID.
	 * @return bool         Whether or not we should prevent the date from being modified.
	 */
	public function limitModifiedDate( $postId ) { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return false;
	}

	/**
	 * Returns the processed page builder content.
	 *
	 * @since 4.5.2
	 *
	 * @param  int    $postId  The post id.
	 * @param  string $content The raw content.
	 * @return string          The processed content.
	 */
	public function processContent( $postId, $content = '' ) {
		if ( empty( $content ) ) {
			$post = get_post( $postId );
			if ( is_a( $post, 'WP_Post' ) ) {
				$content = $post->post_content;
			}
		}

		if ( aioseo()->helpers->isAjaxCronRestRequest() ) {
			return apply_filters( 'the_content', $content );
		}

		return $content;
	}
}Common/Standalone/PageBuilders/WPBakery.php000066600000006772151135505570014656 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;

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

/**
 * Integrate our SEO Panel with WPBakery Page Builder.
 *
 * @since 4.5.2
 */
class WPBakery extends Base {
	/**
	 * The plugin files.
	 *
	 * @since 4.5.2
	 *
	 * @var array
	 */
	public $plugins = [
		'js_composer/js_composer.php',
		'js_composer_salient/js_composer.php'
	];

	/**
	 * The integration slug.
	 *
	 * @since 4.5.2
	 *
	 * @var string
	 */
	public $integrationSlug = 'wpbakery';

	/**
	 * Init the integration.
	 *
	 * @since 4.5.2
	 *
	 * @return void
	 */
	public function init() {
		// Disable SEO meta tags from WP Bakery.
		if ( defined( 'WPB_VC_VERSION' ) && version_compare( WPB_VC_VERSION, '7.4', '>=' ) ) {
			add_filter( 'get_post_metadata', [ $this, 'maybeDisableWpBakeryMetaTags' ], 10, 3 );
		}

		if ( ! aioseo()->postSettings->canAddPostSettingsMetabox( get_post_type( $this->getPostId() ) ) ) {
			return;
		}

		add_action( 'vc_frontend_editor_enqueue_js_css', [ $this, 'enqueue' ] );
		add_action( 'vc_backend_editor_enqueue_js_css', [ $this, 'enqueue' ] );

		add_filter( 'vc_nav_front_controls', [ $this, 'addNavbarCotnrols' ] );
		add_filter( 'vc_nav_controls', [ $this, 'addNavbarCotnrols' ] );
	}

	/**
	 * Maybe disable WP Bakery meta tags.
	 *
	 * @since 4.7.1
	 *
	 * @param  mixed  $value    The value of the meta.
	 * @param  int    $objectId The object ID.
	 * @param  string $metaKey  The meta key.
	 * @return mixed            The value of the meta.
	 */
	public function maybeDisableWpBakeryMetaTags( $value, $objectId, $metaKey ) {
		if ( is_singular() && '_wpb_post_custom_seo_settings' === $metaKey ) {
			return null;
		}

		return $value;
	}

	public function addNavbarCotnrols( $controlList ) {
		$controlList[] = [
			'aioseo',
			'<li class="vc_show-mobile"><div id="aioseo-wpbakery" style="height: 100%;"></div></li>'
		];

		return $controlList;
	}

	/**
	 * Returns whether or not the given Post ID was built with WPBakery.
	 *
	 * @since 4.5.2
	 *
	 * @param  int $postId The Post ID.
	 * @return boolean     Whether or not the Post was built with WPBakery.
	 */
	public function isBuiltWith( $postId ) {
		$postObj = get_post( $postId );
		if ( ! empty( $postObj ) && preg_match( '/vc_row/', (string) $postObj->post_content ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Returns whether should or not limit the modified date.
	 *
	 * @since 4.5.2
	 *
	 * @param  int     $postId The Post ID.
	 * @return boolean         Whether or not sholud limit the modified date.
	 */
	public function limitModifiedDate( $postId ) {
		// This method is supposed to be used in the `saveAjaxFe` action.
		if ( empty( $_REQUEST['_vcnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_vcnonce'] ) ), 'vc-nonce-vc-admin-nonce' ) ) {
			return false;
		}

		$editorPostId = ! empty( $_REQUEST['post_id'] ) ? intval( $_REQUEST['post_id'] ) : 0;
		if ( $editorPostId !== $postId ) {
			return false;
		}

		return ! empty( $_REQUEST['aioseo_limit_modified_date'] ) && (bool) $_REQUEST['aioseo_limit_modified_date'];
	}

	/**
	 * Returns the processed page builder content.
	 *
	 * @since 4.5.2
	 *
	 * @param  int    $postId  The post id.
	 * @param  string $content The raw content.
	 * @return string          The processed content.
	 */
	public function processContent( $postId, $content = '' ) {
		if ( method_exists( '\WPBMap', 'addAllMappedShortcodes' ) ) {
			\WPBMap::addAllMappedShortcodes();
		}

		return parent::processContent( $postId, $content );
	}
}Common/Standalone/PageBuilders/Divi.php000066600000012577151135505570014065 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;

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

/**
 * Integrate our SEO Panel with Divi Page Builder.
 *
 * @since 4.1.7
 */
class Divi extends Base {
	/**
	 * The theme name.
	 *
	 * @since 4.1.7
	 *
	 * @var array
	 */
	public $themes = [ 'Divi', 'Extra' ];

	/**
	 * The plugin files.
	 *
	 * @since 4.2.0
	 *
	 * @var array
	 */
	public $plugins = [
		'divi-builder/divi-builder.php'
	];

	/**
	 * The integration slug.
	 *
	 * @since 4.1.7
	 *
	 * @var string
	 */
	public $integrationSlug = 'divi';

	/**
	 * Init the integration.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function init() {
		add_action( 'wp', [ $this, 'maybeRun' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAdmin' ] );
	}

	/**
	 * Check if we are in the Page Builder and run the integrations.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function maybeRun() {
		$postType = get_post_type( $this->getPostId() );

		if (
			! defined( 'ET_BUILDER_PRODUCT_VERSION' ) ||
			! version_compare( '4.9.2', ET_BUILDER_PRODUCT_VERSION, '<=' ) ||
			! ( function_exists( 'et_core_is_fb_enabled' ) && et_core_is_fb_enabled() ) ||
			! aioseo()->postSettings->canAddPostSettingsMetabox( $postType )
		) {
			return;
		}

		add_action( 'wp_footer', [ $this, 'addContainers' ] );
		add_action( 'wp_footer', [ $this, 'addIframeWatcher' ] );
		add_action( 'wp_enqueue_scripts', [ $this, 'enqueue' ] );
		add_filter( 'script_loader_tag', [ $this, 'addEtTag' ], 10, 2 );
	}

	/**
	 * Enqueue the required scripts for the admin screen.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function enqueueAdmin() {
		if ( ! aioseo()->helpers->isScreenBase( 'toplevel_page_et_divi_options' ) ) {
			return;
		}

		aioseo()->core->assets->load( 'src/vue/standalone/page-builders/divi-admin/main.js', [], aioseo()->helpers->getVueData() );

		aioseo()->main->enqueueTranslations();
	}

	/**
	 * Add et attributes to script tags.
	 *
	 * @since 4.1.7
	 *
	 * @param  string $tag    The <script> tag for the enqueued script.
	 * @param  string $handle The script's registered handle.
	 * @return string         The tag.
	 */
	public function addEtTag( $tag, $handle = '' ) {
		$scriptHandles = [
			'aioseo/js/src/vue/standalone/page-builders/divi/main.js',
			'aioseo/js/src/vue/standalone/app/main.js'
		];

		if ( in_array( $handle, $scriptHandles, true ) ) {
			// These tags load in parent window only, not in Divi iframe.
			return preg_replace( '/<script/', '<script class="et_fb_ignore_iframe"', (string) $tag );
		}

		return $tag;
	}

	/**
	 * Add the Divi watcher.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function addIframeWatcher() {
		?>
		<script type="text/javascript">
			if (typeof jQuery === 'function') {
				jQuery(window).on('et_builder_api_ready et_fb_section_content_change', function(event) {
					window.parent.postMessage({ eventType : event.type })
				})
			}
		</script>
		<?php
	}

	/**
	 * Add the containers to mount our panel.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function addContainers() {
		echo '<div id="aioseo-app-modal" class="et_fb_ignore_iframe"><div class="et_fb_ignore_iframe"></div></div>';
		echo '<div id="aioseo-settings" class="et_fb_ignore_iframe"></div>';
		echo '<div id="aioseo-admin" class="et_fb_ignore_iframe"></div>';
		echo '<div id="aioseo-modal-portal" class="et_fb_ignore_iframe"></div>';
	}

	/**
	 * Returns whether or not the given Post ID was built with Divi.
	 *
	 * @since 4.1.7
	 *
	 * @param  int $postId The Post ID.
	 * @return boolean     Whether or not the Post was built with Divi.
	 */
	public function isBuiltWith( $postId ) {
		if ( ! function_exists( 'et_pb_is_pagebuilder_used' ) ) {
			return false;
		}

		return et_pb_is_pagebuilder_used( $postId );
	}

	/**
	 * Returns the Divi edit url for the given Post ID.
	 *
	 * @since 4.3.1
	 *
	 * @param  int    $postId The Post ID.
	 * @return string         The Edit URL.
	 */
	public function getEditUrl( $postId ) {
		if ( ! function_exists( 'et_fb_get_vb_url' ) ) {
			return '';
		}

		$isDiviLibrary = 'et_pb_layout' === get_post_type( $postId );
		$editUrl       = $isDiviLibrary ? get_edit_post_link( $postId, 'raw' ) : get_permalink( $postId );

		if ( et_pb_is_pagebuilder_used( $postId ) ) {
			$editUrl = et_fb_get_vb_url( $editUrl );
		} else {
			if ( ! et_pb_is_allowed( 'divi_builder_control' ) ) {
				// Prevent link when user lacks `Toggle Divi Builder` capability.
				return '';
			}

			$editUrl = add_query_arg(
				[ 'et_fb_activation_nonce' => wp_create_nonce( 'et_fb_activation_nonce_' . $postId ) ],
				$editUrl
			);
		}

		return $editUrl;
	}

	/**
	 * Checks whether or not we should prevent the date from being modified.
	 *
	 * @since 4.5.2
	 *
	 * @param  int  $postId The Post ID.
	 * @return bool         Whether or not we should prevent the date from being modified.
	 */
	public function limitModifiedDate( $postId ) {
		// This method is supposed to be used in the `wp_ajax_et_fb_ajax_save` action.
		if ( empty( $_REQUEST['et_fb_save_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['et_fb_save_nonce'] ) ), 'et_fb_save_nonce' ) ) {
			return false;
		}

		$editorPostId = ! empty( $_REQUEST['post_id'] ) ? intval( $_REQUEST['post_id'] ) : 0;
		if ( $editorPostId !== $postId ) {
			return false;
		}

		return ! empty( $_REQUEST['options']['conditional_tags']['aioseo_limit_modified_date'] );
	}
}Common/Standalone/PageBuilders/SiteOrigin.php000066600000005157151135505570015242 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;

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

/**
 * Integrate our SEO Panel with SiteOrigin Page Builder.
 *
 * @since 4.6.6
 */
class SiteOrigin extends Base {
	/**
	 * The plugin files.
	 *
	 * @since 4.6.6
	 *
	 * @var array
	 */
	public $plugins = [
		'siteorigin-panels/siteorigin-panels.php'
	];

	/**
	 * The integration slug.
	 *
	 * @since 4.6.6
	 *
	 * @var string
	 */
	public $integrationSlug = 'siteorigin';

	/**
	 * Init the integration.
	 *
	 * @since 4.6.6
	 *
	 * @return void
	 */
	public function init() {
		$postType = get_post_type( $this->getPostId() );
		if ( empty( $postType ) ) {
			$postType = ! empty( $_GET['post_type'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type'] ) ) : 'post'; // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended, Generic.Files.LineLength.MaxExceeded
		}

		if ( ! aioseo()->postSettings->canAddPostSettingsMetabox( $postType ) ) {
			return;
		}

		add_action( 'siteorigin_panel_enqueue_admin_scripts', [ $this, 'enqueue' ] );
	}

	/**
	 * Returns whether or not the given Post ID was built with SiteOrigin.
	 *
	 * @since 4.6.6
	 *
	 * @param  int $postId The Post ID.
	 * @return bool        Whether or not the Post was built with SiteOrigin.
	 */
	public function isBuiltWith( $postId ) {
		$postObj = get_post( $postId );
		if (
			! empty( $postObj ) &&
			(
				preg_match( '/siteorigin_widget/', (string) $postObj->post_content ) ||
				preg_match( '/so-panel widget/', (string) $postObj->post_content )
			)
		) {
			return true;
		}

		return false;
	}

	/**
	 * Returns the processed page builder content.
	 *
	 * @since 4.6.6
	 *
	 * @param  int    $postId  The post id.
	 * @param  string $content The raw content.
	 * @return string          The processed content.
	 */
	public function processContent( $postId, $content = '' ) {
		// When performing a save_post action, we must execute the siteorigin_widget shortcodes if there are image widgets.
		// This ensures that the getFirstImageInContent method can locate the images, as SiteOrigin uses shortcodes for images.
		// We cache the first image in the content during post saving.
		if (
			doing_action( 'save_post' ) &&
			aioseo()->options->searchAppearance->advanced->runShortcodes &&
			(
				stripos( $content, 'SiteOrigin_Widget_Image_Widget' ) !== false ||
				stripos( $content, 'WP_Widget_Media_Image' ) !== false
			)
		) {
			$content = aioseo()->helpers->doAllowedShortcodes( $content, $postId, [ 'siteorigin_widget' ] );
		}

		return parent::processContent( $postId, $content );
	}
}Common/Standalone/PageBuilders/Elementor.php000066600000013656151135505570015123 0ustar00<?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;

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

use Elementor\Controls_Manager as ControlsManager;
use Elementor\Core\DocumentTypes\PageBase;

/**
 * Integrate our SEO Panel with Elementor Page Builder.
 *
 * @since 4.1.7
 */
class Elementor extends Base {
	/**
	 * The plugin files.
	 *
	 * @since 4.1.7
	 *
	 * @var array
	 */
	public $plugins = [
		'elementor/elementor.php',
		'elementor-pro/elementor-pro.php',
	];

	/**
	 * The integration slug.
	 *
	 * @since 4.1.7
	 *
	 * @var string
	 */
	public $integrationSlug = 'elementor';

	/**
	 * Init the integration.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function init() {
		if ( ! aioseo()->postSettings->canAddPostSettingsMetabox( get_post_type( $this->getPostId() ) ) ) {
			return;
		}

		if ( ! did_action( 'elementor/init' ) ) {
			add_action( 'elementor/init', [ $this, 'addPanelTab' ] );
		} else {
			$this->addPanelTab();
		}

		add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'enqueue' ] );
		add_action( 'elementor/documents/register_controls', [ $this, 'registerDocumentControls' ] );
		add_action( 'elementor/editor/footer', [ $this, 'addContainers' ] );

		// Add the SEO tab to the main Elementor panel.
		add_action( 'elementor/editor/footer', [ $this, 'startCapturing' ], 0 );
		add_action( 'elementor/editor/footer', [ $this, 'endCapturing' ], 999 );
	}

	/**
	 * Start capturing buffer.
	 *
	 * @since 4.3.5
	 *
	 * @return void
	 */
	public function startCapturing() {
		ob_start();
	}

	/**
	 * End capturing buffer and add button.
	 * This is a hack to add the SEO tab to the main Elementor panel.
	 * We need to do this because Elementor doesn't provide a filter to add tabs to the main panel.
	 *
	 * @since 4.3.5
	 *
	 * @return void
	 */
	public function endCapturing() {
		$output  = ob_get_clean();
		$search  = '/(<div class="elementor-component-tab elementor-panel-navigation-tab" data-tab="global">.*<\/div>)/m';
		$replace = '${1}<div class="elementor-component-tab elementor-panel-navigation-tab" data-tab="aioseo">SEO</div>';
		echo preg_replace( $search, $replace, $output ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Add the AIOSEO Panel Tab on Elementor.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function addPanelTab() {
		ControlsManager::add_tab( 'aioseo', AIOSEO_PLUGIN_SHORT_NAME );
	}

	/**
	 * Register the Elementor Document Controls.
	 *
	 * @since 4.1.7
	 *
	 * @return void
	 */
	public function registerDocumentControls( $document ) {
		// PageBase is the base class for documents like `post` `page` and etc.
		if ( ! $document instanceof PageBase || ! $document::get_property( 'has_elements' ) ) {
			return;
		}

		// This is needed to get the tab to appear, but will be overwritten in the JavaScript.
		$document->start_controls_section(
			'aioseo_section',
			[
				'label' => AIOSEO_PLUGIN_SHORT_NAME,
				'tab'   => 'aioseo',
			]
		);

		$document->end_controls_section();
	}

	/**
	 * Returns whether or not the given Post ID was built with Elementor.
	 *
	 * @since 4.1.7
	 *
	 * @param  int     $postId The Post ID.
	 * @return boolean         Whether or not the Post was built with Elementor.
	 */
	public function isBuiltWith( $postId ) {
		$document = $this->getElementorDocument( $postId );
		if ( ! $document ) {
			return false;
		}

		return $document->is_built_with_elementor();
	}

	/**
	 * Returns the Elementor edit url for the given Post ID.
	 *
	 * @since 4.3.1
	 *
	 * @param  int    $postId The Post ID.
	 * @return string         The Edit URL.
	 */
	public function getEditUrl( $postId ) {
		$document = $this->getElementorDocument( $postId );
		if ( ! $document || ! $document->is_editable_by_current_user() ) {
			return '';
		}

		return esc_url( $document->get_edit_url() );
	}

	/**
	 * Add the containers to mount our panel.
	 *
	 * @since 4.1.9
	 *
	 * @return void
	 */
	public function addContainers() {
		echo '<div id="aioseo-admin"></div>';
	}

	/**
	 * Returns the Elementor Document instance for the given Post ID.
	 *
	 * @since 4.3.5
	 *
	 * @param  int    $postId The Post ID.
	 * @return object         The Elementor Document instance.
	 */
	private function getElementorDocument( $postId ) {
		if (
			! class_exists( '\Elementor\Plugin' ) ||
			! is_object( \Elementor\Plugin::instance()->documents ) ||
			! method_exists( \Elementor\Plugin::instance()->documents, 'get' )
		) {
			return false;
		}

		$elementorDocument = \Elementor\Plugin::instance()->documents->get( $postId );
		if ( empty( $elementorDocument ) ) {
			return false;
		}

		return $elementorDocument;
	}

	/**
	 * Checks whether or not we should prevent the date from being modified.
	 * This method is supposed to be used in the `wp_ajax_seedprod_pro_save_lpage` action.
	 *
	 * @since 4.5.2
	 *
	 * @param  int  $postId The Post ID.
	 * @return bool         Whether or not we should prevent the date from being modified.
	 */
	public function limitModifiedDate( $postId ) {
		// This method is supposed to be used in the `wp_ajax_elementor_ajax` action.
		if ( empty( $_REQUEST['_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_nonce'] ) ), 'elementor_ajax' ) ) {
			return false;
		}

		$editorPostId = ! empty( $_REQUEST['editor_post_id'] ) ? (int) $_REQUEST['editor_post_id'] : false;
		if ( $editorPostId !== $postId ) {
			return false;
		}

		return ! empty( $_REQUEST['aioseo_limit_modified_date'] );
	}

	/**
	 * Get the post ID.
	 *
	 * @since 4.6.9
	 *
	 * @return int|null The post ID or null.
	 */
	public function getPostId() {
		// phpcs:disable HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		if ( aioseo()->helpers->isAjaxCronRestRequest() ) {
			foreach ( [ 'editor_post_id', 'initial_document_id' ] as $key ) {
				if ( ! empty( $_REQUEST[ $key ] ) ) {
					return intval( wp_unslash( $_REQUEST[ $key ] ) );
				}
			}
		}
		// phpcs:enable

		return parent::getPostId();
	}
}Common/Integrations/BuddyPress.php000066600000010740151135505570013254 0ustar00<?php
namespace AIOSEO\Plugin\Common\Integrations;

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

/**
 * Class to integrate with the BuddyPress plugin.
 *
 * @since 4.7.6
 */
class BuddyPress {
	/**
	 * Call the callback given by the first parameter.
	 *
	 * @since 4.7.6
	 *
	 * @param  callable   $callback The function to be called.
	 * @param  mixed      ...$args  Zero or more parameters to be passed to the function
	 * @return mixed|null           The function result or null if the function is not callable.
	 */
	public static function callFunc( $callback, ...$args ) {
		if ( is_callable( $callback ) ) {
			return call_user_func( $callback, ...$args );
		}

		return null;
	}

	/**
	 * Returns the BuddyPress email custom post type slug.
	 *
	 * @since 4.7.6
	 *
	 * @return string The BuddyPress email custom post type slug if found or an empty string.
	 */
	public static function getEmailCptSlug() {
		$slug = '';
		if ( aioseo()->helpers->isPluginActive( 'buddypress' ) ) {
			$slug = self::callFunc( 'bp_get_email_post_type' );
		}

		return is_scalar( $slug ) ? strval( $slug ) : '';
	}

	/**
	 * Retrieves the BuddyPress component archive page permalink.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $component The BuddyPress component.
	 * @return string            The component archive page permalink.
	 */
	public static function getComponentArchiveUrl( $component ) {
		switch ( $component ) {
			case 'activity':
				$output = self::callFunc( 'bp_get_activity_directory_permalink' );
				break;
			case 'member':
				$output = self::callFunc( 'bp_get_members_directory_permalink' );
				break;
			case 'group':
				$output = self::callFunc( 'bp_get_groups_directory_url' );
				break;
			default:
				$output = '';
		}

		return is_scalar( $output ) ? strval( $output ) : '';
	}

	/**
	 * Returns the BuddyPress component single page permalink.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $component The BuddyPress component.
	 * @param  mixed  $id        The component ID.
	 * @return string            The component single page permalink.
	 */
	public static function getComponentSingleUrl( $component, $id ) {
		switch ( $component ) {
			case 'activity':
				$output = self::callFunc( 'bp_activity_get_permalink', $id );
				break;
			case 'group':
				$output = self::callFunc( 'bp_get_group_url', $id );
				break;
			case 'member':
				$output = self::callFunc( 'bp_core_get_userlink', $id, false, true );
				break;
			default:
				$output = '';
		}

		return is_scalar( $output ) ? strval( $output ) : '';
	}

	/**
	 * Returns the BuddyPress component edit link.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $component The BuddyPress component.
	 * @param  mixed  $id        The component ID.
	 * @return string            The component edit link.
	 */
	public static function getComponentEditUrl( $component, $id ) {
		switch ( $component ) {
			case 'activity':
				$output = add_query_arg( [
					'page'   => 'bp-activity',
					'aid'    => $id,
					'action' => 'edit'
				], self::callFunc( 'bp_get_admin_url', 'admin.php' ) );
				break;
			case 'group':
				$output = add_query_arg( [
					'page'   => 'bp-groups',
					'gid'    => $id,
					'action' => 'edit'
				], self::callFunc( 'bp_get_admin_url', 'admin.php' ) );
				break;
			case 'member':
				$output = get_edit_user_link( $id );
				break;
			default:
				$output = '';
		}

		return is_scalar( $output ) ? strval( $output ) : '';
	}

	/**
	 * Returns whether the BuddyPress component is active or not.
	 *
	 * @since 4.7.6
	 *
	 * @param  string $component The BuddyPress component.
	 * @return bool              Whether the BuddyPress component is active.
	 */
	public static function isComponentActive( $component ) {
		static $active = [];
		if ( isset( $active[ $component ] ) ) {
			return $active[ $component ];
		}

		switch ( $component ) {
			case 'activity':
				$active[ $component ] = self::callFunc( 'bp_is_active', 'activity' );
				break;
			case 'group':
				$active[ $component ] = self::callFunc( 'bp_is_active', 'groups' );
				break;
			case 'member':
				$active[ $component ] = self::callFunc( 'bp_is_active', 'members' );
				break;
			default:
				$active[ $component ] = false;
		}

		return $active[ $component ];
	}

	/**
	 * Returns whether the current page is a BuddyPress component page.
	 *
	 * @since 4.7.6
	 *
	 * @return bool Whether the current page is a BuddyPress component page.
	 */
	public static function isComponentPage() {
		return ! empty( aioseo()->standalone->buddyPress->component->templateType );
	}
}Common/Integrations/Semrush.php000066600000013171151135505570012617 0ustar00<?php
namespace AIOSEO\Plugin\Common\Integrations;

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

/**
 * Class to integrate with the Semrush API.
 *
 * @since 4.0.16
 */
class Semrush {
	/**
	 * The Oauth2 URL.
	 *
	 * @since 4.0.16
	 *
	 * @var string
	 */
	public static $url = 'https://oauth.semrush.com/oauth2/access_token';

	/**
	 * The client ID for the Oauth2 integration.
	 *
	 * @since 4.0.16
	 *
	 * @var string
	 */
	public static $clientId = 'aioseo';

	/**
	 * The client secret for the Oauth2 integration.
	 *
	 * @since 4.0.16
	 *
	 * @var string
	 */
	public static $clientSecret = 'sdDUjYt6umO7sKM7mp4OrN8yeePTOQBy';

	/**
	 * Static method to authenticate the user.
	 *
	 * @since 4.0.16
	 *
	 * @param  string $authorizationCode The authorization code for the Oauth2 authentication.
	 * @return bool                      Whether the user is succesfully authenticated.
	 */
	public static function authenticate( $authorizationCode ) {
		$time     = time();
		$response = wp_remote_post( self::$url, [
			'headers' => [ 'Content-Type' => 'application/json' ],
			'body'    => wp_json_encode( [
				'client_id'     => self::$clientId,
				'client_secret' => self::$clientSecret,
				'grant_type'    => 'authorization_code',
				'code'          => $authorizationCode,
				'redirect_uri'  => 'https://oauth.semrush.com/oauth2/aioseo/success'
			] )
		] );

		$responseCode = wp_remote_retrieve_response_code( $response );
		if ( 200 === $responseCode ) {
			$tokens = json_decode( wp_remote_retrieve_body( $response ) );

			return self::saveTokens( $tokens, $time );
		}

		return false;
	}

	/**
	 * Static method to refresh the tokens once expired.
	 *
	 * @since 4.0.16
	 *
	 * @return bool Whether the tokens were successfully renewed.
	 */
	public static function refreshTokens() {
		$refreshToken = aioseo()->internalOptions->integrations->semrush->refreshToken;
		if ( empty( $refreshToken ) ) {
			self::reset();

			return false;
		}

		$time     = time();
		$response = wp_remote_post( self::$url, [
			'headers' => [ 'Content-Type' => 'application/json' ],
			'body'    => wp_json_encode( [
				'client_id'     => self::$clientId,
				'client_secret' => self::$clientSecret,
				'grant_type'    => 'refresh_token',
				'refresh_token' => $refreshToken
			] )
		] );

		$responseCode = wp_remote_retrieve_response_code( $response );
		if ( 200 === $responseCode ) {
			$tokens = json_decode( wp_remote_retrieve_body( $response ) );

			return self::saveTokens( $tokens, $time );
		}

		return false;
	}

	/**
	 * Clears out the internal options to reset the tokens.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	private static function reset() {
		aioseo()->internalOptions->integrations->semrush->accessToken  = '';
		aioseo()->internalOptions->integrations->semrush->tokenType    = '';
		aioseo()->internalOptions->integrations->semrush->expires      = '';
		aioseo()->internalOptions->integrations->semrush->refreshToken = '';
	}

	/**
	 * Checks if the token has expired
	 *
	 * @since 4.0.16
	 *
	 * @return boolean Whether or not the token has expired.
	 */
	public static function hasExpired() {
		$tokens = self::getTokens();

		return time() >= $tokens['expires'];
	}

	/**
	 * Returns the tokens.
	 *
	 * @since 4.0.16
	 *
	 * @return array An array of token data.
	 */
	public static function getTokens() {
		return aioseo()->internalOptions->integrations->semrush->all();
	}

	/**
	 * Saves the token options.
	 *
	 * @since 4.0.16
	 *
	 * @param  Object $tokens The tokens object.
	 * @param  string $time   The time set before the request was made.
	 * @return bool           Whether the response was valid and successfully saved.
	 */
	public static function saveTokens( $tokens, $time ) {
		$expectedProps = [
			'access_token',
			'token_type',
			'expires_in',
			'refresh_token'
		];

		// If the oAuth response does not include all expected properties, drop it.
		foreach ( $expectedProps as $prop ) {
			if ( empty( $tokens->$prop ) ) {
				return false;
			}
		}

		// Save the options.
		aioseo()->internalOptions->integrations->semrush->accessToken  = $tokens->access_token;
		aioseo()->internalOptions->integrations->semrush->tokenType    = $tokens->token_type;
		aioseo()->internalOptions->integrations->semrush->expires      = $time + $tokens->expires_in;
		aioseo()->internalOptions->integrations->semrush->refreshToken = $tokens->refresh_token;

		return true;
	}

	/**
	 * API call to get keyphrases from semrush.
	 *
	 * @since 4.0.16
	 *
	 * @param  string      $keyphrase A primary keyphrase.
	 * @param  string      $database  A country database.
	 * @return object|bool            The response object or false if the tokens could not be refreshed.
	 */
	public static function getKeyphrases( $keyphrase, $database ) {
		if ( self::hasExpired() ) {
			$success = self::refreshTokens();
			if ( ! $success ) {
				return false;
			}
		}

		$transientKey = 'semrush_keyphrases_' . $keyphrase . '_' . $database;
		$results      = aioseo()->core->cache->get( $transientKey );

		if ( null !== $results ) {
			return $results;
		}

		$params = [
			'phrase'         => $keyphrase,
			'export_columns' => 'Ph,Nq,Td',
			'database'       => strtolower( $database ),
			'display_limit'  => 10,
			'display_offset' => 0,
			'display_sort'   => 'nq_desc',
			'display_filter' => '%2B|Nq|Lt|1000',
			'access_token'   => aioseo()->internalOptions->integrations->semrush->accessToken
		];

		$url = 'https://oauth.semrush.com/api/v1/keywords/phrase_fullsearch?' . http_build_query( $params );

		$response = wp_remote_get( $url );
		$body     = json_decode( wp_remote_retrieve_body( $response ) );

		aioseo()->core->cache->update( $transientKey, $body );

		return $body;
	}
}Common/Integrations/WpCode.php000066600000006041151135505570012350 0ustar00<?php
namespace AIOSEO\Plugin\Common\Integrations;

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

/**
 * Route class for the API.
 *
 * @since 4.3.8
 */
class WpCode {
	/**
	 * Load the WPCode snippets for our desired username or return an empty array if not available.
	 *
	 * @since 4.3.8
	 *
	 * @return array The snippets.
	 */
	public static function loadWpCodeSnippets() {
		$snippets = self::getPlaceholderSnippets();
		if ( function_exists( 'wpcode_get_library_snippets_by_username' ) ) {
			$snippets = wpcode_get_library_snippets_by_username( 'aioseo' );
		}

		return $snippets;
	}

	/**
	 * Checks if the plugin is installed, either the lite or premium version.
	 *
	 * @since 4.3.8
	 *
	 * @return bool True if the plugin is installed.
	 */
	public static function isPluginInstalled() {
		return self::isProInstalled() || self::isLiteInstalled();
	}

	/**
	 * Is the pro plugin installed.
	 *
	 * @since 4.3.8
	 *
	 * @return bool True if the pro plugin is installed.
	 */
	public static function isProInstalled() {
		$installedPlugins = array_keys( get_plugins() );

		return in_array( 'wpcode-premium/wpcode.php', $installedPlugins, true );
	}

	/**
	 * Is the lite plugin installed.
	 *
	 * @since 4.3.8
	 *
	 * @return bool True if the lite plugin is installed.
	 */
	public static function isLiteInstalled() {
		$installedPlugins = array_keys( get_plugins() );

		return in_array( 'insert-headers-and-footers/ihaf.php', $installedPlugins, true );
	}

	/**
	 * Basic check if the plugin is active by looking for the main function.
	 *
	 * @since 4.3.8
	 *
	 * @return bool True if the plugin is active.
	 */
	public static function isPluginActive() {
		return function_exists( 'wpcode' );
	}

	/**
	 * Checks if the plugin is active but needs to be updated by checking if the function to load the
	 * library snippets by username exists.
	 *
	 * @since 4.3.8
	 *
	 * @return bool True if the plugin is active but needs to be updated.
	 */
	public static function pluginNeedsUpdate() {
		return self::isPluginActive() && ! function_exists( 'wpcode_get_library_snippets_by_username' );
	}

	/**
	 * Get placeholder snippets if the WPCode snippets are not available.
	 *
	 * @since 4.3.8
	 *
	 * @return array The placeholder snippets.
	 */
	private static function getPlaceholderSnippets() {
		$snippetTitles = [
			'Disable autogenerated shipping details schema for WooCommerce',
			'Disable SEO Preview feature',
			'Disable Shortcode Parsing in All in One SEO',
			'Enable WooCommerce Product Attributes in Search Appearance',
			'Fix LearnPress conflict that hides AIOSEO tabs on settings pages',
			'Limit Meta Description to 160 characters',
			'Limit SEO Title to 60 characters',
			'Noindex Product Search Pages',
			'Noindex Products under a Product Category',
		];

		$placeholderSnippets = [];
		foreach ( $snippetTitles as $snippetTitle ) {
			// Add placeholder install link so we show a button.
			$placeholderSnippets[] = [
				'title'   => $snippetTitle,
				'install' => 'https://library.wpcode.com/'
			];
		}

		return $placeholderSnippets;
	}
}Common/Integrations/BbPress.php000066600000000776151135505570012540 0ustar00<?php
namespace AIOSEO\Plugin\Common\Integrations;

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

/**
 * Class to integrate with the bbPress plugin.
 *
 * @since 4.8.1
 */
class BbPress {
	/**
	 * Returns whether the current page is a bbPress component page.
	 *
	 * @since 4.8.1
	 *
	 * @return bool Whether the current page is a bbPress component page.
	 */
	public static function isComponentPage() {
		return ! empty( aioseo()->standalone->bbPress->component->templateType );
	}
}Common/Schema/Schema.php000066600000021213151135505570011117 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
use AIOSEO\Plugin\Common\Integrations\BbPress as BbPressIntegration;

/**
 * Builds our schema.
 *
 * @since 4.0.0
 */
class Schema {
	/**
	 * The graphs that need to be generated.
	 *
	 * @since 4.2.5
	 *
	 * @var array
	 */
	public $graphs = [];

	/**
	 * The context data.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $context = [];

	/**
	 * Helpers class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Helpers
	 */
	public $helpers = null;

	/**
	 * The subdirectories that contain graph classes.
	 *
	 * @since 4.2.5
	 *
	 * @var array
	 */
	protected $graphSubDirectories = [
		'Article',
		'KnowledgeGraph',
		'WebPage'
	];

	/**
	 * All existing WebPage graphs.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $webPageGraphs = [
		'WebPage',
		'AboutPage',
		'CheckoutPage',
		'CollectionPage',
		'ContactPage',
		'FAQPage',
		'ItemPage',
		'MedicalWebPage',
		'ProfilePage',
		'RealEstateListing',
		'SearchResultsPage'
	];

	/**
	 * Fields that can be 0 or null, which shouldn't be stripped when cleaning the data.
	 *
	 * @since 4.1.2
	 *
	 * @var array
	 */
	public $nullableFields = [
		'price',          // Needs to be 0 if free for Software Application.
		'ratingValue',    // Needs to be 0 for 0 star ratings.
		'value',          // Needs to be 0 if free for product shipping details.
		'minValue',       // Needs to be 0 for product delivery time.
		'maxValue',       // Needs to be 0 for product delivery time.
		'suggestedMinAge' // Needs to be 0 for PeopleAudience minimum age.
	];

	/**
	 * List of mapped parents with properties that are allowed to contain a restricted set of HTML tags.
	 *
	 * @since 4.2.3
	 *
	 * @var array
	 */
	public $htmlAllowedFields = [
		// FAQPage
		'acceptedAnswer' => [
			'text'
		]
	];

	/**
	 * Whether we are generating the validator output.
	 *
	 * @since 4.6.3
	 *
	 * @var bool
	 */
	public $generatingValidatorOutput = false;

	/**
	 * Class constructor.
	 */
	public function __construct() {
		// No AJAX check since we need to be able to grab the schema output via the REST API.
		if ( wp_doing_cron() ) {
			return;
		}

		$this->helpers = new Helpers();
	}

	/**
	 * Returns the JSON schema output.
	 *
	 * @since 4.0.0
	 *
	 * @return string The JSON schema output.
	 */
	public function get() {
		// First, check if the schema is disabled.
		if ( ! $this->helpers->isEnabled() ) {
			return '';
		}

		$this->determineSmartGraphsAndContext();

		return $this->generateSchema();
	}

	/**
	 * Generates the JSON schema after the graphs/context have been determined.
	 *
	 * @since 4.2.5
	 *
	 * @return string The JSON schema output.
	 */
	protected function generateSchema() {
		// Now, filter the graphs.
		$this->graphs = apply_filters(
			'aioseo_schema_graphs',
			array_unique( array_filter( array_values( $this->graphs ) ) )
		);

		if ( ! $this->graphs ) {
			return '';
		}

		// Check if a WebPage graph is included. Otherwise add the default one.
		$webPageGraphFound = false;
		foreach ( $this->graphs as $graphName ) {
			if ( in_array( $graphName, $this->webPageGraphs, true ) ) {
				$webPageGraphFound = true;
				break;
			}
		}

		if ( ! $webPageGraphFound ) {
			$this->graphs[] = 'WebPage';
		}

		// Now that we've determined the graphs, start generating their data.
		$schema = [
			'@context' => 'https://schema.org',
			'@graph'   => []
		];

		// By determining the length of the array after every iteration, we are able to add additional graphs during runtime.
		// e.g. The Article graph may require a Person graph to be output for the author.
		$this->graphs = array_values( $this->graphs );
		for ( $i = 0; $i < count( $this->graphs ); $i++ ) {
			$namespace = $this->getGraphNamespace( $this->graphs[ $i ] );
			if ( $namespace ) {
				$schema['@graph'][] = ( new $namespace() )->get();
			}
		}

		return aioseo()->schema->helpers->getOutput( $schema );
	}

	/**
	 * Gets the relevant namespace for the given graph.
	 *
	 * @since 4.2.5
	 *
	 * @param  string $graphName The graph name.
	 * @return string            The namespace.
	 */
	protected function getGraphNamespace( $graphName ) {
		$namespace = "\AIOSEO\Plugin\Common\Schema\Graphs\\{$graphName}";
		if ( class_exists( $namespace ) ) {
			return $namespace;
		}

		// If we can't find it in the root dir, check if we can find it in a sub dir.
		foreach ( $this->graphSubDirectories as $dirName ) {
			$namespace = "\AIOSEO\Plugin\Common\Schema\Graphs\\{$dirName}\\{$graphName}";
			if ( class_exists( $namespace ) ) {
				return $namespace;
			}
		}

		return '';
	}

	/**
	 * Determines the smart graphs that need to be output by default, as well as the current context for the breadcrumbs.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	protected function determineSmartGraphsAndContext() {
		$this->graphs = array_merge( $this->graphs, $this->getDefaultGraphs() );

		$contextInstance = new Context();
		$this->context   = $contextInstance->defaults();

		if ( BuddyPressIntegration::isComponentPage() ) {
			aioseo()->standalone->buddyPress->component->determineSchemaGraphsAndContext( $contextInstance );

			return;
		}

		if ( BbPressIntegration::isComponentPage() ) {
			aioseo()->standalone->bbPress->component->determineSchemaGraphsAndContext();

			return;
		}

		if ( aioseo()->helpers->isDynamicHomePage() ) {
			$this->graphs[] = 'CollectionPage';
			$this->context  = $contextInstance->home();

			return;
		}

		if ( is_home() || aioseo()->helpers->isWooCommerceShopPage() ) {
			$this->graphs[] = 'CollectionPage';
			$this->context  = $contextInstance->post();

			return;
		}

		if ( is_singular() ) {
			$this->determineContextSingular( $contextInstance );

			if ( is_singular( 'web-story' ) ) {
				$this->graphs[] = 'AmpStory';
			}
		}

		if ( is_category() || is_tag() || is_tax() ) {
			$this->graphs[] = 'CollectionPage';
			$this->context  = $contextInstance->term();

			return;
		}

		if ( is_author() ) {
			$this->graphs[] = 'ProfilePage';
			$this->graphs[] = 'PersonAuthor';
			$this->context  = $contextInstance->author();
		}

		if ( is_post_type_archive() ) {
			$this->graphs[] = 'CollectionPage';
			$this->context  = $contextInstance->postArchive();

			return;
		}

		if ( is_date() ) {
			$this->graphs[] = 'CollectionPage';
			$this->context  = $contextInstance->date();

			return;
		}

		if ( is_search() ) {
			$this->graphs[] = 'SearchResultsPage';
			$this->context  = $contextInstance->search();

			return;
		}

		if ( is_404() ) {
			$this->context = $contextInstance->notFound();
		}
	}

	/**
	 * Determines the smart graphs and context for singular pages.
	 *
	 * @since 4.2.6
	 *
	 * @param  Context $contextInstance The Context class instance.
	 * @return void
	 */
	protected function determineContextSingular( $contextInstance ) {
		// If the current request is for the validator, we can't include the default graph here.
		// We need to include the default graph that the validator sent.
		// Don't do this if we're in Pro since we then need to get it from the post meta.
		if ( ! $this->generatingValidatorOutput ) {
			$this->graphs[] = $this->getDefaultPostGraph();
		}

		$this->context = $contextInstance->post();
	}

	/**
	 * Returns the default graph for the post type.
	 *
	 * @since 4.2.6
	 *
	 * @return string The default graph.
	 */
	public function getDefaultPostGraph() {
		return $this->getDefaultPostTypeGraph();
	}

	/**
	 * Returns the default graph for the current post type.
	 *
	 * @since 4.2.5
	 *
	 * @param  \WP_Post $post The post object.
	 * @return string         The default graph.
	 */
	public function getDefaultPostTypeGraph( $post = null ) {
		$post = $post ? $post : aioseo()->helpers->getPost();
		if ( ! is_a( $post, 'WP_Post' ) ) {
			return '';
		}

		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		if ( ! $dynamicOptions->searchAppearance->postTypes->has( $post->post_type ) ) {
			return '';
		}

		$defaultType = $dynamicOptions->searchAppearance->postTypes->{$post->post_type}->schemaType;
		switch ( $defaultType ) {
			case 'Article':
				return $dynamicOptions->searchAppearance->postTypes->{$post->post_type}->articleType;
			case 'WebPage':
				return $dynamicOptions->searchAppearance->postTypes->{$post->post_type}->webPageType;
			default:
				return $defaultType;
		}
	}

	/**
	 * Returns the default graphs that should be output on every page, regardless of its type.
	 *
	 * @since 4.2.5
	 *
	 * @return array The default graphs.
	 */
	protected function getDefaultGraphs() {
		$siteRepresents = ucfirst( aioseo()->options->searchAppearance->global->schema->siteRepresents );

		return [
			'BreadcrumbList',
			'Kg' . $siteRepresents,
			'WebSite'
		];
	}
}Common/Schema/Context.php000066600000014327151135505570011353 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema;

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

/**
 * Determines the context.
 *
 * @since 4.0.0
 */
class Context {
	/**
	 * Breadcrumb class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Breadcrumb
	 */
	public $breadcrumb = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->breadcrumb = new Breadcrumb();
	}

	/**
	 * Returns the default context data.
	 *
	 * @since 4.3.0
	 *
	 * @return array The context data.
	 */
	public function defaults() {
		return [
			'name'        => aioseo()->meta->title->getTitle(),
			'description' => aioseo()->meta->description->getDescription(),
			'url'         => aioseo()->helpers->getUrl(),
			'breadcrumb'  => []
		];
	}

	/**
	 * Returns the context data for the homepage.
	 *
	 * @since 4.0.0
	 *
	 * @return array $context The context data.
	 */
	public function home() {
		$context = [
			'url'         => aioseo()->helpers->getUrl(),
			'breadcrumb'  => $this->breadcrumb->home(),
			'name'        => aioseo()->meta->title->getTitle(),
			'description' => aioseo()->meta->description->getDescription()
		];

		// Homepage set to show latest posts.
		if ( 'posts' === get_option( 'show_on_front' ) && is_home() ) {
			return $context;
		}

		// Homepage set to static page.
		$post = aioseo()->helpers->getPost();
		if ( ! $post ) {
			return [
				'name'        => '',
				'description' => '',
				'url'         => aioseo()->helpers->getUrl(),
				'breadcrumb'  => [],
			];
		}

		$context['object'] = $post;

		return $context;
	}

	/**
	 * Returns the context data for the requested post.
	 *
	 * @since 4.0.0
	 *
	 * @return array The context data.
	 */
	public function post() {
		$post = aioseo()->helpers->getPost();
		if ( ! $post ) {
			return [
				'name'        => '',
				'description' => '',
				'url'         => aioseo()->helpers->getUrl(),
				'breadcrumb'  => [],
			];
		}

		return [
			'name'        => aioseo()->meta->title->getTitle( $post ),
			'description' => aioseo()->meta->description->getDescription( $post ),
			'url'         => aioseo()->helpers->getUrl(),
			'breadcrumb'  => $this->breadcrumb->post( $post ),
			'object'      => $post,
		];
	}

	/**
	 * Returns the context data for the requested term archive.
	 *
	 * @since 4.0.0
	 *
	 * @return array The context data.
	 */
	public function term() {
		$term = aioseo()->helpers->getTerm();
		if ( ! $term ) {
			return [
				'name'        => '',
				'description' => '',
				'url'         => aioseo()->helpers->getUrl(),
				'breadcrumb'  => [],
			];
		}

		return [
			'name'        => aioseo()->meta->title->getTitle(),
			'description' => aioseo()->meta->description->getDescription(),
			'url'         => aioseo()->helpers->getUrl(),
			'breadcrumb'  => $this->breadcrumb->term( $term )
		];
	}

	/**
	 * Returns the context data for the requested author archive.
	 *
	 * @since 4.0.0
	 *
	 * @return array The context data.
	 */
	public function author() {
		$author = get_queried_object();
		if ( ! $author ) {
			return [
				'name'        => '',
				'description' => '',
				'url'         => aioseo()->helpers->getUrl(),
				'breadcrumb'  => [],
			];
		}

		$title       = aioseo()->meta->title->getTitle();
		$description = aioseo()->meta->description->getDescription();
		$url         = aioseo()->helpers->getUrl();

		if ( ! $description ) {
			$description = get_the_author_meta( 'description', $author->ID );
		}

		return [
			'name'        => $title,
			'description' => $description,
			'url'         => $url,
			'breadcrumb'  => $this->breadcrumb->setPositions( [
				'name'        => get_the_author_meta( 'display_name', $author->ID ),
				'description' => $description,
				'url'         => $url,
				'type'        => 'CollectionPage'
			] )
		];
	}

	/**
	 * Returns the context data for the requested post archive.
	 *
	 * @since 4.0.0
	 *
	 * @return array The context data.
	 */
	public function postArchive() {
		$postType = get_queried_object();
		if ( ! $postType ) {
			return [
				'name'        => '',
				'description' => '',
				'url'         => aioseo()->helpers->getUrl(),
				'breadcrumb'  => [],
			];
		}

		$title       = aioseo()->meta->title->getTitle();
		$description = aioseo()->meta->description->getDescription();
		$url         = aioseo()->helpers->getUrl();

		return [
			'name'        => $title,
			'description' => $description,
			'url'         => $url,
			'breadcrumb'  => $this->breadcrumb->setPositions( [
				'name'        => $postType->label,
				'description' => $description,
				'url'         => $url,
				'type'        => 'CollectionPage'
			] )
		];
	}

	/**
	 * Returns the context data for the requested data archive.
	 *
	 * @since 4.0.0
	 *
	 * @return array $context The context data.
	 */
	public function date() {
		$context = [
			'name'        => aioseo()->meta->title->getTitle(),
			'description' => aioseo()->meta->description->getDescription(),
			'url'         => aioseo()->helpers->getUrl()
		];

		$context['breadcrumb'] = $this->breadcrumb->date();

		return $context;
	}

	/**
	 * Returns the context data for the search page.
	 *
	 * @since 4.0.0
	 *
	 * @return array The context data.
	 */
	public function search() {
		global $s;
		$title       = aioseo()->meta->title->getTitle();
		$description = aioseo()->meta->description->getDescription();
		$url         = aioseo()->helpers->getUrl();

		return [
			'name'        => $title,
			'description' => $description,
			'url'         => $url,
			'breadcrumb'  => $this->breadcrumb->setPositions( [
				'name'        => $s ? $s : $title,
				'description' => $description,
				'url'         => $url,
				'type'        => 'SearchResultsPage'
			] )
		];
	}

	/**
	 * Returns the context data for the 404 Not Found page.
	 *
	 * @since 4.0.0
	 *
	 * @return array The context data.
	 */
	public function notFound() {
		$title       = aioseo()->meta->title->getTitle();
		$description = aioseo()->meta->description->getDescription();
		$url         = aioseo()->helpers->getUrl();

		return [
			'name'        => $title,
			'description' => $description,
			'url'         => $url,
			'breadcrumb'  => $this->breadcrumb->setPositions( [
				'name'        => __( 'Not Found', 'all-in-one-seo-pack' ),
				'description' => $description,
				'url'         => $url
			] )
		];
	}
}Common/Schema/Breadcrumb.php000066600000023613151135505570011773 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema;

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

/**
 * Determines the breadcrumb trail.
 *
 * @since 4.0.0
 */
class Breadcrumb {
	/**
	 * Returns the breadcrumb trail for the homepage.
	 *
	 * @since 4.0.0
	 *
	 * @return array The breadcrumb trail.
	 */
	public function home() {
		// Since we just need the root breadcrumb (homepage), we can call this immediately without passing any breadcrumbs.
		return $this->setPositions();
	}

	/**
	 * Returns the breadcrumb trail for the requested post.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post The post object.
	 * @return array          The breadcrumb trail.
	 */
	public function post( $post ) {
		// Check if page is the static homepage.
		if ( aioseo()->helpers->isStaticHomePage() ) {
			return $this->home();
		}

		if ( is_post_type_hierarchical( $post->post_type ) ) {
			return $this->setPositions( $this->postHierarchical( $post ) );
		}

		return $this->setPositions( $this->postNonHierarchical( $post ) );
	}

	/**
	 * Returns the breadcrumb trail for a hierarchical post.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post The post object.
	 * @return array          The breadcrumb trail.
	 */
	private function postHierarchical( $post ) {
		$breadcrumbs = [];
		do {
			array_unshift(
				$breadcrumbs,
				[
					'name'        => $post->post_title,
					'description' => aioseo()->meta->description->getDescription( $post ),
					'url'         => get_permalink( $post ),
					'type'        => aioseo()->helpers->isWooCommerceShopPage( $post->ID ) || is_home() ? 'CollectionPage' : $this->getPostWebPageGraph()
				]
			);

			if ( $post->post_parent ) {
				$post = get_post( $post->post_parent );
			} else {
				$post = false;
			}
		} while ( $post );

		return $breadcrumbs;
	}

	/**
	 * Returns the breadcrumb trail for a non-hierarchical post.
	 *
	 * In this case we need to compare the permalink structure with the permalink of the requested post and loop through all objects we're able to find.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post The post object.
	 * @return array          The breadcrumb trail.
	 */
	private function postNonHierarchical( $post ) {
		global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$homeUrl   = aioseo()->helpers->escapeRegex( home_url() );
		$permalink = get_permalink();
		$slug      = preg_replace( "/$homeUrl/", '', (string) $permalink );
		$tags      = array_filter( explode( '/', get_option( 'permalink_structure' ) ) ); // Permalink structure exploded into separate tag strings.
		$objects   = array_filter( explode( '/', $slug ) ); // Permalink slug exploded into separate object slugs.
		$postGraph = $this->getPostWebPageGraph();

		if ( count( $tags ) !== count( $objects ) ) {
			return [
				'name'        => $post->post_title,
				'description' => aioseo()->meta->description->getDescription( $post ),
				'url'         => $permalink,
				'type'        => $postGraph
			];
		}

		$pairs = array_reverse( array_combine( $tags, $objects ) );

		$breadcrumbs = [];
		$dateName    = null;
		$timestamp   = strtotime( $post->post_date );
		foreach ( $pairs as $tag => $object ) {
			// Escape the delimiter.
			$escObject = aioseo()->helpers->escapeRegex( $object );
			// Determine the slug for the object.
			preg_match( "/.*{$escObject}[\/]/", (string) $permalink, $url );
			if ( empty( $url[0] ) ) {
				continue;
			}

			$breadcrumb = [];
			switch ( $tag ) {
				case '%category%':
					$term = aioseo()->standalone->primaryTerm->getPrimaryTerm( $post->ID, 'category' );
					if ( ! $term ) {
						$term = get_category_by_slug( $object );
					}

					if ( ! $term ) {
						break;
					}
					// phpcs:disable Squiz.NamingConventions.ValidVariableName
					$oldQueriedObject         = $wp_query->queried_object;
					$wp_query->queried_object = $term;
					$wp_query->is_category    = true;

					$breadcrumb = [
						'name'        => $term->name,
						'description' => aioseo()->meta->description->getDescription(),
						'url'         => get_term_link( $term ),
						'type'        => 'CollectionPage'
					];

					$wp_query->queried_object = $oldQueriedObject;
					$wp_query->is_category    = false;
					// phpcs:enable Squiz.NamingConventions.ValidVariableName
					break;
				case '%author%':
					$breadcrumb = [
						'name'        => get_the_author_meta( 'display_name', $post->post_author ),
						'description' => aioseo()->meta->description->helpers->prepare( aioseo()->options->searchAppearance->archives->author->metaDescription ),
						'url'         => $url[0],
						'type'        => 'ProfilePage'
					];
					break;
				case '%postid%':
				case '%postname%':
					$breadcrumb = [
						'name'        => $post->post_title,
						'description' => aioseo()->meta->description->getDescription( $post ),
						'url'         => $url[0],
						'type'        => $postGraph
					];
					break;
				case '%year%':
					$dateName = gmdate( 'Y', $timestamp );
				case '%monthnum%':
					if ( ! $dateName ) {
						$dateName = gmdate( 'F', $timestamp );
					}
				case '%day%':
					if ( ! $dateName ) {
						$dateName = gmdate( 'j', $timestamp );
					}
					$breadcrumb = [
						'name'        => $dateName,
						'description' => aioseo()->meta->description->helpers->prepare( aioseo()->options->searchAppearance->archives->date->metaDescription ),
						'url'         => $url[0],
						'type'        => 'CollectionPage'
					];
					$dateName = null;
					break;
				default:
					break;
			}

			if ( $breadcrumb ) {
				array_unshift( $breadcrumbs, $breadcrumb );
			}
		}

		return $breadcrumbs;
	}

	/**
	 * Returns the breadcrumb trail for the requested term.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Term $term The term object.
	 * @return array          The breadcrumb trail.
	 */
	public function term( $term ) {
		if ( 'product_attributes' === $term->taxonomy ) {
			$term = get_term( $term->term_id );
		}

		$breadcrumbs = [];
		do {
			array_unshift(
				$breadcrumbs,
				[
					'name'        => $term->name,
					'description' => aioseo()->meta->description->getDescription(),
					'url'         => get_term_link( $term, $term->taxonomy ),
					'type'        => 'CollectionPage'
				]
			);

			if ( $term->parent ) {
				$term = aioseo()->helpers->getTerm( $term->parent, $term->taxonomy );
			} else {
				$term = false;
			}
		} while ( $term );

		return $this->setPositions( $breadcrumbs );
	}

	/**
	 * Returns the breadcrumb trail for the requested date archive.
	 *
	 * @since 4.0.0
	 *
	 * @return array The breadcrumb trail.
	 */
	public function date() {
		// phpcs:disable Squiz.NamingConventions.ValidVariableName
		global $wp_query;

		$oldYear            = $wp_query->is_year;
		$oldMonth           = $wp_query->is_month;
		$oldDay             = $wp_query->is_day;
		$wp_query->is_year  = true;
		$wp_query->is_month = false;
		$wp_query->is_day   = false;

		$breadcrumbs = [
			[
				'name'        => get_the_date( 'Y' ),
				'description' => aioseo()->meta->description->getDescription(),
				'url'         => trailingslashit( get_year_link( $wp_query->query_vars['year'] ) ),
				'type'        => 'CollectionPage'
			]
		];

		$wp_query->is_year = $oldYear;

		// Fall through if data archive is more specific than the year.
		if ( is_year() ) {
			return $this->setPositions( $breadcrumbs );
		}

		$wp_query->is_month = true;

		$breadcrumbs[] = [
			'name'        => get_the_date( 'F, Y' ),
			'description' => aioseo()->meta->description->getDescription(),
			'url'         => trailingslashit( get_month_link(
				$wp_query->query_vars['year'],
				$wp_query->query_vars['monthnum']
			) ),
			'type'        => 'CollectionPage'
		];

		$wp_query->is_month = $oldMonth;

		// Fall through if data archive is more specific than the year & month.
		if ( is_month() ) {
			return $this->setPositions( $breadcrumbs );
		}

		$wp_query->is_day = $oldDay;

		$breadcrumbs[] = [
			'name'        => get_the_date(),
			'description' => aioseo()->meta->description->getDescription(),
			'url'         => trailingslashit( get_day_link(
				$wp_query->query_vars['year'],
				$wp_query->query_vars['monthnum'],
				$wp_query->query_vars['day']
			) ),
			'type'        => 'CollectionPage'
		];
		// phpcs:enable Squiz.NamingConventions.ValidVariableName

		return $this->setPositions( $breadcrumbs );
	}

	/**
	 * Sets the position for each breadcrumb after adding the root breadcrumb first.
	 *
	 * If no breadcrumbs are passed, then we assume we're on the homepage and just need the root breadcrumb.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $breadcrumbs The breadcrumb trail.
	 * @return array              The modified breadcrumb trail.
	 */
	public function setPositions( $breadcrumbs = [] ) {
		// If the array isn't two-dimensional, then we need to wrap it in another array before continuing.
		if (
			count( $breadcrumbs ) &&
			count( $breadcrumbs ) === count( $breadcrumbs, COUNT_RECURSIVE )
		) {
			$breadcrumbs = [ $breadcrumbs ];
		}

		// The homepage needs to be root item of all trails.
		$homepage = [
			// Translators: This refers to the homepage of the site.
			'name'        => apply_filters( 'aioseo_schema_breadcrumbs_home', __( 'Home', 'all-in-one-seo-pack' ) ),
			'description' => aioseo()->meta->description->getHomePageDescription(),
			'url'         => trailingslashit( home_url() ),
			'type'        => 'posts' === get_option( 'show_on_front' ) ? 'CollectionPage' : 'WebPage'
		];
		array_unshift( $breadcrumbs, $homepage );

		$breadcrumbs = array_filter( $breadcrumbs );
		foreach ( $breadcrumbs as $index => &$breadcrumb ) {
			$breadcrumb['position'] = $index + 1;
		}

		return $breadcrumbs;
	}

	/**
	 * Returns the most relevant WebPage graph for the post.
	 *
	 * @since 4.2.5
	 *
	 * @return string The graph name.
	 */
	private function getPostWebPageGraph() {
		foreach ( aioseo()->schema->graphs as $graphName ) {
			if ( in_array( $graphName, aioseo()->schema->webPageGraphs, true ) ) {
				return $graphName;
			}
		}

		// Return the default if no WebPage graph was found.
		return 'WebPage';
	}
}Common/Schema/Graphs/WebPage/RealEstateListing.php000066600000001244151135505570016042 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

/**
 * RealEstateListing graph class.
 *
 * @since 4.0.0
 */
class RealEstateListing extends WebPage {
	/**
	 * The graph type.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $type = 'RealEstateListing';

	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 *
	 * @return array $data The graph data.
	 */
	public function get() {
		$data = parent::get();
		$post = aioseo()->helpers->getPost();
		if ( ! $post ) {
			return $data;
		}

		$data['datePosted'] = mysql2date( DATE_W3C, $post->post_date, false );

		return $data;
	}
}Common/Schema/Graphs/WebPage/FAQPage.php000066600000000476151135505570013671 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

/**
 * FAQPage graph class.
 *
 * @since 4.0.0
 */
class FAQPage extends WebPage {
	/**
	 * The graph type.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $type = 'FAQPage';
}Common/Schema/Graphs/WebPage/MedicalWebPage.php000066600000000650151135505570015250 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

/**
 * MedicalWebPage graph class.
 *
 * @since 4.6.4
 */
class MedicalWebPage extends WebPage {
	/**
	 * The graph type.
	 *
	 * This value can be overridden by WebPage child graphs that are more specific.
	 *
	 * @since 4.6.4
	 *
	 * @var string
	 */
	protected $type = 'MedicalWebPage';
}Common/Schema/Graphs/WebPage/AboutPage.php000066600000000504151135505570014324 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

/**
 * AboutPage graph class.
 *
 * @since 4.0.0
 */
class AboutPage extends WebPage {
	/**
	 * The graph type.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $type = 'AboutPage';
}Common/Schema/Graphs/WebPage/CollectionPage.php000066600000000523151135505570015346 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

/**
 * CollectionPage graph class.
 *
 * @since 4.0.0
 */
class CollectionPage extends WebPage {
	/**
	 * The graph type.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $type = 'CollectionPage';
}Common/Schema/Graphs/WebPage/WebPage.php000066600000005577151135505570014006 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

use AIOSEO\Plugin\Common\Schema\Graphs;

/**
 * WebPage graph class.
 *
 * @since 4.0.0
 */
class WebPage extends Graphs\Graph {
	/**
	 * The graph type.
	 *
	 * This value can be overridden by WebPage child graphs that are more specific.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $type = 'WebPage';

	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 *
	 * @return array $data The graph data.
	 */
	public function get() {
		$homeUrl = trailingslashit( home_url() );
		$data    = [
			'@type'       => $this->type,
			'@id'         => aioseo()->schema->context['url'] . '#' . strtolower( $this->type ),
			'url'         => aioseo()->schema->context['url'],
			'name'        => aioseo()->meta->title->getTitle(),
			'description' => aioseo()->schema->context['description'],
			'inLanguage'  => aioseo()->helpers->currentLanguageCodeBCP47(),
			'isPartOf'    => [ '@id' => $homeUrl . '#website' ]
		];

		$breadcrumbs = aioseo()->breadcrumbs->frontend->getBreadcrumbs() ?? '';
		if ( ! empty( $breadcrumbs ) ) {
			$data['breadcrumb'] = [ '@id' => aioseo()->schema->context['url'] . '#breadcrumblist' ];
		}

		if ( is_singular() && 'page' !== get_post_type() ) {
			$post = aioseo()->helpers->getPost();
			if ( is_a( $post, 'WP_Post' ) && post_type_supports( $post->post_type, 'author' ) ) {
				$author = get_author_posts_url( $post->post_author );
				if ( ! empty( $author ) ) {
					if ( ! in_array( 'PersonAuthor', aioseo()->schema->graphs, true ) ) {
						aioseo()->schema->graphs[] = 'PersonAuthor';
					}

					$data['author']  = [ '@id' => $author . '#author' ];
					$data['creator'] = [ '@id' => $author . '#author' ];
				}
			}
		}

		if ( isset( aioseo()->schema->context['description'] ) && aioseo()->schema->context['description'] ) {
			$data['description'] = aioseo()->schema->context['description'];
		}

		if ( is_singular() ) {
			if ( ! isset( aioseo()->schema->context['object'] ) || ! aioseo()->schema->context['object'] ) {
				return $this->getAddonData( $data, 'webPage' );
			}

			$post = aioseo()->schema->context['object'];
			if ( has_post_thumbnail( $post ) ) {
				$image = $this->image( get_post_thumbnail_id(), 'mainImage' );
				if ( $image ) {
					$data['image']              = $image;
					$data['primaryImageOfPage'] = [
						'@id' => aioseo()->schema->context['url'] . '#mainImage'
					];
				}
			}

			$data['datePublished'] = mysql2date( DATE_W3C, $post->post_date, false );
			$data['dateModified']  = mysql2date( DATE_W3C, $post->post_modified, false );

			return $this->getAddonData( $data, 'webPage' );
		}

		if ( is_front_page() ) {
			$data['about'] = [ '@id' => trailingslashit( home_url() ) . '#' . aioseo()->options->searchAppearance->global->schema->siteRepresents ];
		}

		return $this->getAddonData( $data, 'webPage' );
	}
}Common/Schema/Graphs/WebPage/CheckoutPage.php000066600000000642151135505570015022 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

/**
 * CheckoutPage graph class.
 *
 * @since 4.6.4
 */
class CheckoutPage extends WebPage {
	/**
	 * The graph type.
	 *
	 * This value can be overridden by WebPage child graphs that are more specific.
	 *
	 * @since 4.6.4
	 *
	 * @var string
	 */
	protected $type = 'CheckoutPage';
}Common/Schema/Graphs/WebPage/SearchResultsPage.php000066600000000534151135505570016044 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

/**
 * SearchResultsPage graph class.
 *
 * @since 4.0.0
 */
class SearchResultsPage extends WebPage {
	/**
	 * The graph type.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $type = 'SearchResultsPage';
}Common/Schema/Graphs/WebPage/ContactPage.php000066600000000512151135505570014644 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

/**
 * ContactPage graph class.
 *
 * @since 4.0.0
 */
class ContactPage extends WebPage {
	/**
	 * The graph type.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $type = 'ContactPage';
}Common/Schema/Graphs/WebPage/ProfilePage.php000066600000004274151135505570014662 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * ProfilePage graph class.
 *
 * @since 4.0.0
 */
class ProfilePage extends WebPage {
	/**
	 * The graph type.
	 *
	 * @since 4.5.6
	 *
	 * @var string
	 */
	protected $type = 'ProfilePage';

	/**
	 * Returns the graph data.
	 *
	 * @since 4.5.4
	 *
	 * @return array The graph data.
	 */
	public function get() {
		$data = parent::get();

		$post   = aioseo()->helpers->getPost();
		$author = get_queried_object();
		if (
			! is_a( $author, 'WP_User' ) &&
			( is_singular() && ! is_a( $post, 'WP_Post' ) )
		) {
			return [];
		}

		global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$articles = [];
		$authorId = $author->ID ?? $post->post_author ?? 0;
		foreach ( $wp_query->posts as $post ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			if ( $post->post_author !== $authorId ) {
				continue;
			}

			$articles[] = [
				'@type'         => 'Article',
				'url'           => get_permalink( $post->ID ),
				'headline'      => $post->post_title,
				'datePublished' => mysql2date( DATE_W3C, $post->post_date, false ),
				'dateModified'  => mysql2date( DATE_W3C, $post->post_modified, false ),
				'author'        => [
					'@id' => get_author_posts_url( $authorId ) . '#author'
				]
			];
		}

		$data = array_merge( $data, [
			'dateCreated' => mysql2date( DATE_W3C, $author->user_registered, false ),
			'mainEntity'  => [
				'@id' => get_author_posts_url( $authorId ) . '#author'
			],
			'hasPart'     => $articles

		] );

		if (
			BuddyPressIntegration::isComponentPage() &&
			'bp-member_single' === aioseo()->standalone->buddyPress->component->templateType
		) {
			if ( ! isset( $data['mainEntity'] ) ) {
				$data['mainEntity'] = [];
			}

			$data['mainEntity']['@type'] = 'Person';
			$data['mainEntity']['name']  = aioseo()->standalone->buddyPress->component->author->display_name;
			$data['mainEntity']['url']   = BuddyPressIntegration::getComponentSingleUrl( 'member', aioseo()->standalone->buddyPress->component->author->ID );
		}

		return $data;
	}
}Common/Schema/Graphs/WebPage/ItemPage.php000066600000000626151135505570014155 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

/**
 * ItemPage graph class.
 *
 * @since 4.0.0
 */
class ItemPage extends WebPage {
	/**
	 * The graph type.
	 *
	 * This value can be overridden by WebPage child graphs that are more specific.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $type = 'ItemPage';
}Common/Schema/Graphs/WebPage/PersonAuthor.php000066600000004016151135505570015110 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;

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

use AIOSEO\Plugin\Common\Schema\Graphs;

/**
 * Person Author graph class.
 * This a secondary Person graph for post authors and BuddyPress profile pages.
 *
 * @since 4.0.0
 */
class PersonAuthor extends Graphs\Graph {
	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 *
	 * @param  int   $userId The user ID.
	 * @return array $data   The graph data.
	 */
	public function get( $userId = null ) {
		$post         = aioseo()->helpers->getPost();
		$user         = get_queried_object();
		$isAuthorPage = is_author() && is_a( $user, 'WP_User' );
		if (
			(
				( ! is_singular() && ! $isAuthorPage ) ||
				( is_singular() && ! is_a( $post, 'WP_Post' ) )
			) &&
			! $userId
		) {
			return [];
		}

		// Dynamically determine the User ID.
		if ( ! $userId ) {
			$userId = $isAuthorPage ? $user->ID : $post->post_author;
			if ( function_exists( 'bp_is_user' ) && bp_is_user() ) {
				$userId = intval( wp_get_current_user()->ID );
			}
		}

		if ( ! $userId ) {
			return [];
		}

		$authorUrl = get_author_posts_url( $userId );

		$data = [
			'@type' => 'Person',
			'@id'   => $authorUrl . '#author',
			'url'   => $authorUrl,
			'name'  => get_the_author_meta( 'display_name', $userId )
		];

		$avatar = $this->avatar( $userId, 'authorImage' );
		if ( $avatar ) {
			$data['image'] = $avatar;
		}

		$socialUrls = array_values( $this->getUserProfiles( $userId ) );
		if ( $socialUrls ) {
			$data['sameAs'] = $socialUrls;
		}

		if ( is_author() ) {
			$data['mainEntityOfPage'] = [
				'@id' => aioseo()->schema->context['url'] . '#profilepage'
			];
		}

		// Check if our addons need to modify this graph.
		$addonsPersonAuthorData = array_filter( aioseo()->addons->doAddonFunction( 'personAuthor', 'get', [
			'userId' => $userId,
			'data'   => $data
		] ) );

		foreach ( $addonsPersonAuthorData as $addonPersonAuthorData ) {
			$data = array_merge( $data, $addonPersonAuthorData );
		}

		return $data;
	}
}Common/Schema/Graphs/Traits/Image.php000066600000005462151135505570013443 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\Traits;

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

/**
 * Trait that handles images for the graphs.
 *
 * @since 4.2.5
 */
trait Image {
	/**
	 * Builds the graph data for a given image with a given schema ID.
	 *
	 * @since 4.0.0
	 *
	 * @param int    $imageId The image ID.
	 * @param string $graphId The graph ID (optional).
	 * @return array $data    The image graph data.
	 */
	protected function image( $imageId, $graphId = '' ) {
		$attachmentId = is_string( $imageId ) && ! is_numeric( $imageId ) ? aioseo()->helpers->attachmentUrlToPostId( $imageId ) : $imageId;
		$imageUrl     = wp_get_attachment_image_url( $attachmentId, 'full' );

		$data = [
			'@type' => 'ImageObject',
			'url'   => $imageUrl ? $imageUrl : $imageId,
		];

		if ( $graphId ) {
			$baseUrl     = aioseo()->schema->context['url'] ?? aioseo()->helpers->getUrl();
			$data['@id'] = trailingslashit( $baseUrl ) . '#' . $graphId;
		}

		if ( ! $attachmentId ) {
			return $data;
		}

		$metaData = wp_get_attachment_metadata( $attachmentId );
		if ( $metaData && ! empty( $metaData['width'] ) && ! empty( $metaData['height'] ) ) {
			$data['width']  = (int) $metaData['width'];
			$data['height'] = (int) $metaData['height'];
		}

		$caption = $this->getImageCaption( $attachmentId );
		if ( ! empty( $caption ) ) {
			$data['caption'] = $caption;
		}

		return $data;
	}

	/**
	 * Get the image caption.
	 *
	 * @since 4.1.4
	 *
	 * @param  int    $attachmentId The attachment ID.
	 * @return string               The caption.
	 */
	private function getImageCaption( $attachmentId ) {
		$caption = wp_get_attachment_caption( $attachmentId );
		if ( ! empty( $caption ) ) {
			return $caption;
		}

		return get_post_meta( $attachmentId, '_wp_attachment_image_alt', true );
	}

	/**
	 * Returns the graph data for the avatar of a given user.
	 *
	 * @since 4.0.0
	 *
	 * @param  int    $userId  The user ID.
	 * @param  string $graphId The graph ID.
	 * @return array           The graph data.
	 */
	protected function avatar( $userId, $graphId ) {
		if ( ! get_option( 'show_avatars' ) ) {
			return [];
		}

		$avatar = get_avatar_data( $userId );
		if ( ! $avatar['found_avatar'] ) {
			return [];
		}

		return array_filter( [
			'@type'   => 'ImageObject',
			'@id'     => aioseo()->schema->context['url'] . "#$graphId",
			'url'     => $avatar['url'],
			'width'   => $avatar['width'],
			'height'  => $avatar['height'],
			'caption' => get_the_author_meta( 'display_name', $userId )
		] );
	}

	/**
	 * Returns the graph data for the post's featured image.
	 *
	 * @since 4.2.5
	 *
	 * @return string The featured image URL.
	 */
	protected function getFeaturedImage() {
		$post = aioseo()->helpers->getPost();

		return has_post_thumbnail( $post ) ? $this->image( get_post_thumbnail_id() ) : '';
	}
}Common/Schema/Graphs/KnowledgeGraph/KgOrganization.php000066600000005336151135505570017002 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\KnowledgeGraph;

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

use \AIOSEO\Plugin\Common\Schema\Graphs;

/**
 * Knowledge Graph Organization graph class.
 *
 * @since 4.0.0
 */
class KgOrganization extends Graphs\Graph {
	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 *
	 * @return array $data The graph data.
	 */
	public function get() {
		$homeUrl                 = trailingslashit( home_url() );
		$organizationName        = aioseo()->tags->replaceTags( aioseo()->options->searchAppearance->global->schema->organizationName );
		$organizationDescription = aioseo()->tags->replaceTags( aioseo()->options->searchAppearance->global->schema->organizationDescription );

		$data = [
			'@type'        => 'Organization',
			'@id'          => $homeUrl . '#organization',
			'name'         => $organizationName ? $organizationName : aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) ),
			'description'  => $organizationDescription,
			'url'          => $homeUrl,
			'email'        => aioseo()->options->searchAppearance->global->schema->email,
			'telephone'    => aioseo()->options->searchAppearance->global->schema->phone,
			'foundingDate' => aioseo()->options->searchAppearance->global->schema->foundingDate
		];

		$numberOfEmployeesData = aioseo()->options->searchAppearance->global->schema->numberOfEmployees->all();

		if (
			$numberOfEmployeesData['isRange'] &&
			isset( $numberOfEmployeesData['from'] ) &&
			isset( $numberOfEmployeesData['to'] ) &&
			0 < $numberOfEmployeesData['to']
		) {
			$data['numberOfEmployees'] = [
				'@type'    => 'QuantitativeValue',
				'minValue' => $numberOfEmployeesData['from'],
				'maxValue' => $numberOfEmployeesData['to']
			];
		}

		if (
			! $numberOfEmployeesData['isRange'] &&
			! empty( $numberOfEmployeesData['number'] )
		) {
			$data['numberOfEmployees'] = [
				'@type' => 'QuantitativeValue',
				'value' => $numberOfEmployeesData['number']
			];
		}

		$logo = $this->logo();
		if ( ! empty( $logo ) ) {
			$data['logo']  = $logo;
			$data['image'] = [ '@id' => $data['logo']['@id'] ];
		}

		$socialUrls = array_values( $this->getOrganizationProfiles() );
		if ( $socialUrls ) {
			$data['sameAs'] = $socialUrls;
		}

		$data = $this->getAddonData( $data, 'kgOrganization' );

		return $data;
	}

	/**
	 * Returns the logo data.
	 *
	 * @since 4.0.0
	 *
	 * @return array The logo data.
	 */
	public function logo() {
		$logo = aioseo()->options->searchAppearance->global->schema->organizationLogo;
		if ( $logo ) {
			return $this->image( $logo, 'organizationLogo' );
		}

		$imageId = aioseo()->helpers->getSiteLogoId();
		if ( $imageId ) {
			return $this->image( $imageId, 'organizationLogo' );
		}

		return [];
	}
}Common/Schema/Graphs/KnowledgeGraph/KgPerson.php000066600000003457151135505570015606 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\KnowledgeGraph;

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

use \AIOSEO\Plugin\Common\Schema\Graphs;

/**
 * Knowledge Graph Person graph class.
 * This is the main Person graph that can be set to represent the site.
 *
 * @since 4.0.0
 */
class KgPerson extends Graphs\Graph {
	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 *
	 * @return array $data The graph data.
	 */
	public function get() {
		if ( 'person' !== aioseo()->options->searchAppearance->global->schema->siteRepresents ) {
			return [];
		}

		$person = aioseo()->options->searchAppearance->global->schema->person;
		if ( 'manual' === $person ) {
			return $this->manual();
		}

		$person = intval( $person );
		if ( empty( $person ) ) {
			return [];
		}

		$data = [
			'@type' => 'Person',
			'@id'   => trailingslashit( home_url() ) . '#person',
			'name'  => get_the_author_meta( 'display_name', $person )
		];

		$avatar = $this->avatar( $person, 'personImage' );
		if ( $avatar ) {
			$data['image'] = $avatar;
		}

		$socialUrls = array_values( $this->getUserProfiles( $person ) );
		if ( $socialUrls ) {
			$data['sameAs'] = $socialUrls;
		}

		return $data;
	}

	/**
	 * Returns the data for the person if it is set manually.
	 *
	 * @since 4.0.0
	 *
	 * @return array $data The graph data.
	 */
	private function manual() {
		$data = [
			'@type' => 'Person',
			'@id'   => trailingslashit( home_url() ) . '#person',
			'name'  => aioseo()->options->searchAppearance->global->schema->personName
		];

		$logo = aioseo()->options->searchAppearance->global->schema->personLogo;
		if ( $logo ) {
			$data['image'] = $logo;
		}

		$socialUrls = array_values( $this->getOrganizationProfiles() );
		if ( $socialUrls ) {
			$data['sameAs'] = $socialUrls;
		}

		return $data;
	}
}Common/Schema/Graphs/WebSite.php000066600000001747151135505570012517 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs;

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

/**
 * WebSite graph class.
 *
 * @since 4.0.0
 */
class WebSite extends Graph {
	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 *
	 * @return array $data The graph data.
	 */
	public function get() {
		$homeUrl = trailingslashit( home_url() );
		$data    = [
			'@type'         => 'WebSite',
			'@id'           => $homeUrl . '#website',
			'url'           => $homeUrl,
			'name'          => aioseo()->helpers->getWebsiteName(),
			'alternateName' => aioseo()->tags->replaceTags( aioseo()->options->searchAppearance->global->schema->websiteAlternateName ),
			'description'   => aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'description' ) ),
			'inLanguage'    => aioseo()->helpers->currentLanguageCodeBCP47(),
			'publisher'     => [ '@id' => $homeUrl . '#' . aioseo()->options->searchAppearance->global->schema->siteRepresents ]
		];

		return $data;
	}
}Common/Schema/Graphs/BreadcrumbList.php000066600000004205151135505570014047 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs;

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

/**
 * BreadcrumbList graph class.
 *
 * @since 4.0.0
 */
class BreadcrumbList extends Graph {
	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 *
	 * @return array The graph data.
	 */
	public function get() {
		$breadcrumbs = aioseo()->breadcrumbs->frontend->getBreadcrumbs() ?? '';
		if ( ! $breadcrumbs ) {
			return [];
		}

		// Set the position for each breadcrumb.
		foreach ( $breadcrumbs as $k => $breadcrumb ) {
			if ( ! isset( $breadcrumb['position'] ) ) {
				$breadcrumbs[ $k ]['position'] = $k + 1;
			}
		}

		$trailLength = count( $breadcrumbs );
		if ( ! $trailLength ) {
			return [];
		}

		$listItems = [];
		foreach ( $breadcrumbs as $breadcrumb ) {
			if ( empty( $breadcrumb['link'] ) ) {
				continue;
			}

			$listItem = [
				'@type'    => 'ListItem',
				'@id'      => $breadcrumb['link'] . '#listItem',
				'position' => $breadcrumb['position'],
				'name'     => $breadcrumb['label'] ?? ''
			];

			// Don't add "item" prop for last crumb.
			if ( $trailLength !== $breadcrumb['position'] ) {
				$listItem['item'] = $breadcrumb['link'];
			}

			if ( 1 === $trailLength ) {
				$listItems[] = $listItem;
				continue;
			}

			if ( $trailLength > $breadcrumb['position'] && ! empty( $breadcrumbs[ $breadcrumb['position'] ]['label'] ) ) {
				$listItem['nextItem'] = [
					'@type' => 'ListItem',
					'@id'   => $breadcrumbs[ $breadcrumb['position'] ]['link'] . '#listItem',
					'name'  => $breadcrumbs[ $breadcrumb['position'] ]['label'],
				];
			}

			if ( 1 < $breadcrumb['position'] && ! empty( $breadcrumbs[ $breadcrumb['position'] - 2 ]['label'] ) ) {
				$listItem['previousItem'] = [
					'@type' => 'ListItem',
					'@id'   => $breadcrumbs[ $breadcrumb['position'] - 2 ]['link'] . '#listItem',
					'name'  => $breadcrumbs[ $breadcrumb['position'] - 2 ]['label'],
				];
			}

			$listItems[] = $listItem;
		}

		$data = [
			'@type'           => 'BreadcrumbList',
			'@id'             => aioseo()->schema->context['url'] . '#breadcrumblist',
			'itemListElement' => $listItems
		];

		return $data;
	}
}Common/Schema/Graphs/Graph.php000066600000004542151135505570012212 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs;

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

use AIOSEO\Plugin\Common\Traits as CommonTraits;

/**
 * The base graph class.
 *
 * @since 4.0.0
 */
abstract class Graph {
	use Traits\Image;
	use CommonTraits\SocialProfiles;

	/**
	 * The graph data to overwrite.
	 *
	 * @since 4.7.6
	 *
	 * @var array
	 */
	protected static $overwriteGraphData = [];

	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 */
	abstract public function get();

	/**
	 * Iterates over a list of functions and sets the results as graph data.
	 *
	 * @since 4.0.13
	 *
	 * @param  array $data          The graph data to add to.
	 * @param  array $dataFunctions List of functions to loop over, associated with a graph property.
	 * @return array $data          The graph data with the results added.
	 */
	protected function getData( $data, $dataFunctions ) {
		foreach ( $dataFunctions as $k => $f ) {
			if ( ! method_exists( $this, $f ) ) {
				continue;
			}

			$value = $this->$f();
			if ( $value || in_array( $k, aioseo()->schema->nullableFields, true ) ) {
				$data[ $k ] = $value;
			}
		}

		return $data;
	}

	/**
	 * Decodes a multiselect field and returns the values.
	 *
	 * @since 4.6.4
	 *
	 * @param  string $json The JSON encoded multiselect field.
	 * @return array        The decoded values.
	 */
	protected function extractMultiselectTags( $json ) {
		$tags = is_string( $json ) ? json_decode( $json ) : [];
		if ( ! $tags ) {
			return [];
		}

		return wp_list_pluck( $tags, 'value' );
	}

	/**
	 * Merges in data from our addon plugins.
	 *
	 * @since   4.5.6
	 * @version 4.6.4 Moved to main graph class.
	 *
	 * @param  array $data The graph data.
	 * @return array       The graph data.
	 */
	protected function getAddonData( $data, $className, $methodName = 'getAdditionalGraphData' ) {
		$addonData = array_filter( aioseo()->addons->doAddonFunction( $className, $methodName, [
			'postId' => get_the_ID(),
			'data'   => $data
		] ) );

		foreach ( $addonData as $addonGraphData ) {
			$data = array_merge( $data, $addonGraphData );
		}

		return $data;
	}

	/**
	 * A way to overwrite the graph data.
	 *
	 * @since 4.7.6
	 *
	 * @param  array $data The data to overwrite.
	 * @return void
	 */
	public static function setOverwriteGraphData( $data ) {
		self::$overwriteGraphData[ static::class ] = $data;
	}
}Common/Schema/Graphs/Article/NewsArticle.php000066600000002315151135505570014750 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\Article;

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

/**
 * News Article graph class.
 *
 * @since 4.0.0
 */
class NewsArticle extends Article {
	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 *
	 * @param  object $graphData The graph data.
	 * @return array             The parsed graph data.
	 */
	public function get( $graphData = null ) {
		if ( ! empty( self::$overwriteGraphData[ __CLASS__ ] ) ) {
			$graphData = json_decode( wp_json_encode( wp_parse_args( self::$overwriteGraphData[ __CLASS__ ], $graphData ) ) );
		}

		$data = parent::get( $graphData );
		if ( ! $data ) {
			return [];
		}

		$data['@type'] = 'NewsArticle';
		$data['@id']   = ! empty( $graphData->id ) ? aioseo()->schema->context['url'] . $graphData->id : aioseo()->schema->context['url'] . '#newsarticle';

		$date = ! empty( $graphData->properties->datePublished )
			? mysql2date( 'F j, Y', $graphData->properties->datePublished, false )
			: get_the_date( 'F j, Y' );
		if ( $date ) {
			// Translators: 1 - A date (e.g. September 2, 2022).
			$data['dateline'] = sprintf( __( 'Published on %1$s.', 'all-in-one-seo-pack' ), $date );
		}

		return $data;
	}
}Common/Schema/Graphs/Article/BlogPosting.php000066600000001307151135505570014757 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\Article;

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

/**
 * Blog Posting graph class.
 *
 * @since 4.0.0
 */
class BlogPosting extends Article {
	/**
	 * Returns the graph data.
	 *
	 * @since 4.0.0
	 *
	 * @return object $graphData The graph data.
	 * @return array             The parsed graph data.
	 */
	public function get( $graphData = null ) {
		$data = parent::get( $graphData );
		if ( ! $data ) {
			return [];
		}

		$data['@type'] = 'BlogPosting';
		$data['@id']   = ! empty( $graphData->id ) ? aioseo()->schema->context['url'] . $graphData->id : aioseo()->schema->context['url'] . '#blogposting';

		return $data;
	}
}Common/Schema/Graphs/Article/Article.php000066600000011770151135505570014120 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\Article;

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

use AIOSEO\Plugin\Common\Schema\Graphs;

/**
 * Article graph class.
 *
 * @since 4.0.0
 */
class Article extends Graphs\Graph {
	/**
	 * Returns the graph data.
	 *
	 * @since 4.2.5
	 *
	 * @param  Object $graphData The graph data.
	 * @return array             The parsed graph data.
	 */
	public function get( $graphData = null ) {
		$post = aioseo()->helpers->getPost();
		if ( ! is_a( $post, 'WP_Post' ) ) {
			return [];
		}

		$data = [
			'@type'            => 'Article',
			'@id'              => ! empty( $graphData->id ) ? aioseo()->schema->context['url'] . $graphData->id : aioseo()->schema->context['url'] . '#article',
			'name'             => ! empty( $graphData->properties->name ) ? $graphData->properties->name : aioseo()->schema->context['name'],
			'headline'         => ! empty( $graphData->properties->headline ) ? $graphData->properties->headline : get_the_title(),
			'description'      => ! empty( $graphData->properties->description ) ? $graphData->properties->description : '',
			'author'           => [
				'@type' => 'Person',
				'name'  => ! empty( $graphData->properties->author->name ) ? $graphData->properties->author->name : get_the_author_meta( 'display_name' ),
				'url'   => ! empty( $graphData->properties->author->url ) ? $graphData->properties->author->url : '',
			],
			'publisher'        => [ '@id' => trailingslashit( home_url() ) . '#' . aioseo()->options->searchAppearance->global->schema->siteRepresents ],
			'image'            => ! empty( $graphData->properties->image ) ? $this->image( $graphData->properties->image ) : $this->postImage( $post ),
			'datePublished'    => ! empty( $graphData->properties->dates->datePublished )
				? mysql2date( DATE_W3C, $graphData->properties->dates->datePublished, false )
				: mysql2date( DATE_W3C, $post->post_date, false ),
			'dateModified'     => ! empty( $graphData->properties->dates->dateModified )
				? mysql2date( DATE_W3C, $graphData->properties->dates->dateModified, false )
				: mysql2date( DATE_W3C, $post->post_modified, false ),
			'inLanguage'       => aioseo()->helpers->currentLanguageCodeBCP47(),
			'commentCount'     => get_comment_count( $post->ID )['approved'],
			'mainEntityOfPage' => empty( $graphData ) ? [ '@id' => aioseo()->schema->context['url'] . '#webpage' ] : '',
			'isPartOf'         => empty( $graphData ) ? [ '@id' => aioseo()->schema->context['url'] . '#webpage' ] : ''
		];

		if ( empty( $graphData->properties->author->name ) ) {
			if ( ! in_array( 'PersonAuthor', aioseo()->schema->graphs, true ) ) {
				aioseo()->schema->graphs[] = 'PersonAuthor';
			}

			$data['author'] = [
				'@id' => get_author_posts_url( $post->post_author ) . '#author'
			];
		}

		if ( ! empty( $graphData->properties->keywords ) ) {
			$keywords = json_decode( $graphData->properties->keywords, true );
			$keywords = array_map( function ( $keywordObject ) {
				return $keywordObject['value'];
			}, $keywords );
			$data['keywords'] = implode( ', ', $keywords );
		}

		if ( isset( $graphData->properties->dates->include ) && ! $graphData->properties->dates->include ) {
			unset( $data['datePublished'] );
			unset( $data['dateModified'] );
		}

		$postTaxonomies = get_post_taxonomies( $post );
		$postTerms      = [];
		foreach ( $postTaxonomies as $taxonomy ) {
			$terms = get_the_terms( $post, $taxonomy );
			if ( $terms ) {
				$postTerms = array_merge( $postTerms, wp_list_pluck( $terms, 'name' ) );
			}
		}

		if ( ! empty( $postTerms ) ) {
			$data['articleSection'] = implode( ', ', $postTerms );
		}

		$pageNumber = aioseo()->helpers->getPageNumber();
		if ( 1 < $pageNumber ) {
			$data['pagination'] = $pageNumber;
		}

		return $data;
	}

	/**
	 * Returns the graph data for the post image.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post The post object.
	 * @return array          The image graph data.
	 */
	private function postImage( $post ) {
		$featuredImage = $this->getFeaturedImage();
		if ( $featuredImage ) {
			return $featuredImage;
		}

		preg_match_all( '#<img[^>]+src="([^">]+)"#', (string) $post->post_content, $matches );
		if ( isset( $matches[1] ) && isset( $matches[1][0] ) ) {
			$url     = aioseo()->helpers->removeImageDimensions( $matches[1][0] );
			$imageId = aioseo()->helpers->attachmentUrlToPostId( $url );
			if ( $imageId ) {
				return $this->image( $imageId, 'articleImage' );
			} else {
				return $this->image( $url, 'articleImage' );
			}
		}

		if ( 'organization' === aioseo()->options->searchAppearance->global->schema->siteRepresents ) {
			$logo = ( new Graphs\KnowledgeGraph\KgOrganization() )->logo();
			if ( ! empty( $logo ) ) {
				$logo['@id'] = trailingslashit( home_url() ) . '#articleImage';

				return $logo;
			}
		} else {
			$avatar = $this->avatar( $post->post_author, 'articleImage' );
			if ( $avatar ) {
				return $avatar;
			}
		}

		$imageId = aioseo()->helpers->getSiteLogoId();
		if ( $imageId ) {
			return $this->image( $imageId, 'articleImage' );
		}

		return [];
	}
}Common/Schema/Graphs/AmpStory.php000066600000002471151135505570012726 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema\Graphs;

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

/**
 * AmpStory graph class.
 *
 * @since 4.7.6
 */
class AmpStory extends Graph {
	/**
	 * Returns the graph data.
	 *
	 * @since 4.7.6
	 *
	 * @return array The parsed graph data.
	 */
	public function get() {
		$post = aioseo()->helpers->getPost();
		if ( ! is_a( $post, 'WP_Post' ) || 'web-story' !== $post->post_type ) {
			return [];
		}

		$data = [
			'@type'         => 'AmpStory',
			'@id'           => aioseo()->schema->context['url'] . '#amp-story',
			'name'          => aioseo()->schema->context['name'],
			'headline'      => get_the_title(),
			'author'        => [
				'@id' => get_author_posts_url( $post->post_author ) . '#author'
			],
			'publisher'     => [ '@id' => trailingslashit( home_url() ) . '#' . aioseo()->options->searchAppearance->global->schema->siteRepresents ],
			'image'         => $this->getFeaturedImage(),
			'datePublished' => mysql2date( DATE_W3C, $post->post_date, false ),
			'dateModified'  => mysql2date( DATE_W3C, $post->post_modified, false ),
			'inLanguage'    => aioseo()->helpers->currentLanguageCodeBCP47()
		];

		if ( ! in_array( 'PersonAuthor', aioseo()->schema->graphs, true ) ) {
			aioseo()->schema->graphs[] = 'PersonAuthor';
		}

		return $data;
	}
}Common/Schema/Helpers.php000066600000006776151135505570011342 0ustar00<?php
namespace AIOSEO\Plugin\Common\Schema;

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

/**
 * Contains helper methods for our schema classes.
 *
 * @since 4.2.5
 */
class Helpers {
	/**
	 * Checks whether the schema markup feature is enabled.
	 *
	 * @since 4.2.5
	 *
	 * @return bool Whether the schema markup feature is enabled or not.
	 */
	public function isEnabled() {
		$isEnabled = ! in_array( 'enableSchemaMarkup', aioseo()->internalOptions->deprecatedOptions, true ) || aioseo()->options->deprecated->searchAppearance->global->schema->enableSchemaMarkup;

		return ! apply_filters( 'aioseo_schema_disable', ! $isEnabled );
	}

	/**
	 * Strips HTML and removes all blank properties in each of our graphs.
	 * Also parses properties that might contain smart tags.
	 *
	 * @since   4.0.13
	 * @version 4.2.5
	 *
	 * @param  array  $data        The graph data.
	 * @param  string $parentKey   The key of the group parent (optional).
	 * @param  bool   $replaceTags Whether the smart tags should be replaced.
	 * @return array               The cleaned graph data.
	 */
	public function cleanAndParseData( $data, $parentKey = '', $replaceTags = true ) {
		foreach ( $data as $k => &$v ) {
			if ( is_numeric( $v ) || is_bool( $v ) || is_null( $v ) ) {
				// Do nothing.
			} elseif ( is_array( $v ) ) {
				$v = $this->cleanAndParseData( $v, $k, $replaceTags );
			} else {
				// Check if the prop can contain some HTML tags.
				if (
					isset( aioseo()->schema->htmlAllowedFields[ $parentKey ] ) &&
					in_array( $k, aioseo()->schema->htmlAllowedFields[ $parentKey ], true )
				) {
					$v = trim( wp_kses_post( $v ) );
				} else {
					$v = trim( wp_strip_all_tags( $v ) );
				}

				$v = $replaceTags ? aioseo()->tags->replaceTags( $v, get_the_ID() ) : $v;
			}

			if ( empty( $v ) && ! in_array( $k, aioseo()->schema->nullableFields, true ) ) {
				unset( $data[ $k ] );
			} else {
				$data[ $k ] = $v;
			}
		}

		return $data;
	}

	/**
	 * Sorts the schema data and then returns it as JSON.
	 * We temporarily change the floating point precision in order to prevent rounding errors.
	 * Otherwise e.g. 4.9 could be output as 4.90000004.
	 *
	 * @since 4.2.7
	 *
	 * @param  array  $schema      The schema data.
	 * @param  bool   $replaceTags Whether the smart tags should be replaced.
	 * @return string              The schema as JSON.
	 */
	public function getOutput( $schema, $replaceTags = true ) {
		$schema['@graph'] = apply_filters( 'aioseo_schema_output', $schema['@graph'] );
		$schema['@graph'] = $this->cleanAndParseData( $schema['@graph'], '', $replaceTags );

		// Sort the graphs alphabetically.
		usort( $schema['@graph'], function ( $a, $b ) {
			$typeA = $a['@type'] ?? null;
			$typeB = $b['@type'] ?? null;

			if ( is_null( $typeA ) || is_array( $typeA ) ) {
				return 1;
			}

			if ( is_null( $typeB ) || is_array( $typeB ) ) {
				return -1;
			}

			return strcmp( $typeA, $typeB );
		} );

		// Allow users to control the default json_encode flags.
		// Some users report better SEO performance when non-Latin unicode characters are not escaped.
		$jsonFlags = apply_filters( 'aioseo_schema_json_flags', 0 );

		$json = isset( $_GET['aioseo-dev'] ) || aioseo()->schema->generatingValidatorOutput // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			? aioseo()->helpers->wpJsonEncode( $schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE )
			: aioseo()->helpers->wpJsonEncode( $schema, $jsonFlags );

		return $json;
	}
}Common/Social/Image.php000066600000017435151135505570010766 0ustar00<?php
namespace AIOSEO\Plugin\Common\Social;

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

use AIOSEO\Plugin\Common\Models;

/**
 * Handles the Open Graph and Twitter Image.
 *
 * @since 4.0.0
 */
class Image {
	/**
	 * The type of image ("facebook" or "twitter").
	 *
	 * @since 4.1.6.2
	 *
	 * @var string
	 */
	protected $type;

	/**
	 * The post object.
	 *
	 * @since 4.1.6.2
	 *
	 * @var \WP_Post
	 */
	private $post;

	/**
	 * The default thumbnail size.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $thumbnailSize;

	/**
	 * Whether or not to use the cached images.
	 *
	 * @since 4.1.6
	 *
	 * @var boolean
	 */
	public $useCache = true;

	/**
	 * Returns the Facebook or Twitter image.
	 *
	 * @since 4.0.0
	 *
	 * @param  string        $type        The type ("Facebook" or "Twitter").
	 * @param  string        $imageSource The image source.
	 * @param  \WP_Post|null $post        The post object.
	 * @return string|array               The image data.
	 */
	public function getImage( $type, $imageSource, $post = null ) {
		$this->type          = $type;
		$this->post          = $post;
		$this->thumbnailSize = apply_filters( 'aioseo_thumbnail_size', 'fullsize' );
		$hash                = md5( wp_json_encode( [ $type, $imageSource, $post ] ) );

		static $images = [];
		if ( isset( $images[ $hash ] ) ) {
			return $images[ $hash ];
		}

		if ( 'auto' === $imageSource && aioseo()->helpers->getPostPageBuilderName( $post->ID ) ) {
			$imageSource = 'default';
		}

		if ( is_a( $this->post, 'WP_Post' ) ) {
			switch ( $imageSource ) {
				case 'featured':
					$image = $this->getFeaturedImage();
					break;
				case 'attach':
					$image = $this->getFirstAttachedImage();
					break;
				case 'content':
					$image = $this->getFirstImageInContent();
					break;
				case 'author':
					$image = $this->getAuthorAvatar();
					break;
				case 'auto':
					$image = $this->getFirstAvailableImage();
					break;
				case 'custom':
					$image = $this->getCustomFieldImage();
					break;
				case 'custom_image':
					$metaData = aioseo()->meta->metaData->getMetaData( $post );
					if ( empty( $metaData ) ) {
						break;
					}
					$image = 'facebook' === strtolower( $this->type )
						? $metaData->og_image_custom_url
						: $metaData->twitter_image_custom_url;
					break;
				case 'default':
				default:
					$image = aioseo()->options->social->{$this->type}->general->defaultImagePosts;
			}
		}

		if ( empty( $image ) ) {
			$image = aioseo()->options->social->{$this->type}->general->defaultImagePosts;
		}

		if ( is_array( $image ) ) {
			$images[ $hash ] = $image;

			return $images[ $hash ];
		}

		$imageWithoutDimensions = aioseo()->helpers->removeImageDimensions( $image );
		$attachmentId           = aioseo()->helpers->attachmentUrlToPostId( $imageWithoutDimensions );
		$images[ $hash ]        = $attachmentId ? wp_get_attachment_image_src( $attachmentId, $this->thumbnailSize ) : $image;

		return $images[ $hash ];
	}

	/**
	 * Returns the Featured Image for the post.
	 *
	 * @since 4.0.0
	 *
	 * @return array The image data.
	 */
	private function getFeaturedImage() {
		$cachedImage = $this->getCachedImage();
		if ( $cachedImage ) {
			return $cachedImage;
		}

		$imageId = get_post_thumbnail_id( $this->post->ID );

		return $imageId ? wp_get_attachment_image_src( $imageId, $this->thumbnailSize ) : '';
	}

	/**
	 * Returns the first attached image.
	 *
	 * @since 4.0.0
	 *
	 * @return string The image data.
	 */
	private function getFirstAttachedImage() {
		$cachedImage = $this->getCachedImage();
		if ( $cachedImage ) {
			return $cachedImage;
		}

		if ( 'attachment' === get_post_type( $this->post->ID ) ) {
			return wp_get_attachment_image_src( $this->post->ID, $this->thumbnailSize );
		}

		$attachments = get_children(
			[
				'post_parent'    => $this->post->ID,
				'post_status'    => 'inherit',
				'post_type'      => 'attachment',
				'post_mime_type' => 'image',
			]
		);

		return $attachments && count( $attachments ) ? wp_get_attachment_image_src( array_values( $attachments )[0]->ID, $this->thumbnailSize ) : '';
	}

	/**
	 * Returns the first image found in the post content.
	 *
	 * @since 4.0.0
	 *
	 * @return string The image URL.
	 */
	private function getFirstImageInContent() {
		$cachedImage = $this->getCachedImage();
		if ( $cachedImage ) {
			return $cachedImage;
		}

		$postContent = aioseo()->helpers->getPostContent( $this->post );
		preg_match_all( '|<img.*?src=[\'"](.*?)[\'"].*?>|i', (string) $postContent, $matches ); // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage

		// Ignore cover block background image - WP >= 5.7.
		if ( ! empty( $matches[0] ) && apply_filters( 'aioseo_social_image_ignore_cover_block', true, $this->post, $matches ) ) {
			foreach ( $matches[0] as $key => $match ) {
				if ( false !== stripos( $match, 'wp-block-cover__image-background' ) ) {
					unset( $matches[1][ $key ] );
				}
			}
		}

		return ! empty( $matches[1] ) ? current( $matches[1] ) : '';
	}

	/**
	 * Returns the author avatar.
	 *
	 * @since 4.0.0
	 *
	 * @return string The image URL.
	 */
	private function getAuthorAvatar() {
		$avatar = get_avatar( $this->post->post_author, 300 );
		preg_match( "/src='(.*?)'/i", (string) $avatar, $matches );

		return ! empty( $matches[1] ) ? $matches[1] : '';
	}

	/**
	 * Returns the first available image.
	 *
	 * @since 4.0.0
	 *
	 * @return string The image URL.
	 */
	private function getFirstAvailableImage() {
		// Disable the cache.
		$this->useCache = false;

		$image = $this->getCustomFieldImage();

		if ( ! $image ) {
			$image = $this->getFeaturedImage();
		}

		if ( ! $image ) {
			$image = $this->getFirstAttachedImage();
		}

		if ( ! $image ) {
			$image = $this->getFirstImageInContent();
		}

		if ( ! $image && 'twitter' === strtolower( $this->type ) ) {
			$image = aioseo()->options->social->twitter->homePage->image;
		}

		// Enable the cache.
		$this->useCache = true;

		return $image ? $image : aioseo()->options->social->facebook->homePage->image;
	}

	/**
	 * Returns the image from a custom field.
	 *
	 * @since 4.0.0
	 *
	 * @return string The image URL.
	 */
	private function getCustomFieldImage() {
		$cachedImage = $this->getCachedImage();
		if ( $cachedImage ) {
			return $cachedImage;
		}

		$prefix = 'facebook' === strtolower( $this->type ) ? 'og_' : 'twitter_';

		$aioseoPost   = Models\Post::getPost( $this->post->ID );
		$customFields = ! empty( $aioseoPost->{ $prefix . 'image_custom_fields' } )
			? $aioseoPost->{ $prefix . 'image_custom_fields' }
			: aioseo()->options->social->{$this->type}->general->customFieldImagePosts;

		if ( ! $customFields ) {
			return '';
		}

		$customFields = explode( ',', $customFields );
		foreach ( $customFields as $customField ) {
			$image = get_post_meta( $this->post->ID, $customField, true );

			if ( ! empty( $image ) ) {
				$image = is_array( $image ) ? $image[0] : $image;

				return is_numeric( $image )
					? wp_get_attachment_image_src( $image, $this->thumbnailSize )
					: $image;
			}
		}

		return '';
	}

	/**
	 * Returns the cached image if there is one.
	 *
	 * @since 4.1.6.2
	 *
	 * @param  \WP_Term     $object The object for which we need to get the cached image.
	 * @return string|array         The image URL or data.
	 */
	protected function getCachedImage( $object = null ) {
		if ( null === $object ) {
			// This isn't null if we call it from the Pro class.
			$object = $this->post;
		}

		$metaData = aioseo()->meta->metaData->getMetaData( $object );

		switch ( $this->type ) {
			case 'facebook':
				if ( ! empty( $metaData->og_image_url ) && $this->useCache ) {
					return aioseo()->meta->metaData->getCachedOgImage( $metaData );
				}
				break;
			case 'twitter':
				if ( ! empty( $metaData->twitter_image_url ) && $this->useCache ) {
					return $metaData->twitter_image_url;
				}
				break;
			default:
				break;
		}

		return '';
	}
}Common/Social/Social.php000066600000010213151135505570011141 0ustar00<?php
namespace AIOSEO\Plugin\Common\Social;

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

/**
 * Handles the Social Meta.
 *
 * @package AIOSEO\Plugin\Common\Social
 *
 * @since 4.0.0
 */
class Social {
	/**
	 * The name of the action to bust the OG cache.
	 *
	 * @since 4.2.0
	 *
	 * @var string
	 */
	private $bustOgCacheActionName = 'aioseo_og_cache_bust_post';

	/**
	 * Image class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Image
	 */
	public $image = null;

	/**
	 * Facebook class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Facebook
	 */
	public $facebook = null;

	/**
	 * Twitter class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Twitter
	 */
	public $twitter = null;

	/**
	 * Output class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Output
	 */
	public $output = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->image = new Image();

		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		$this->facebook = new Facebook();
		$this->twitter  = new Twitter();
		$this->output   = new Output();

		$this->hooks();
	}

	/**
	 * Registers our hooks.
	 *
	 * @since 4.0.0
	 */
	protected function hooks() {
		add_action( $this->bustOgCacheActionName, [ $this, 'bustOgCachePost' ] );

		// To avoid duplicate sets of meta tags.
		add_filter( 'jetpack_enable_open_graph', '__return_false' );

		if ( ! is_admin() ) {
			add_filter( 'language_attributes', [ $this, 'addAttributes' ] );

			return;
		}

		// Forces a refresh of the Facebook cache.
		add_action( 'post_updated', [ $this, 'scheduleBustOgCachePost' ], 10, 2 );
	}

	/**
	 * Adds our attributes to the registered language attributes.
	 *
	 * @since   4.0.0
	 * @version 4.4.5 Adds trim function the html tag removing empty spaces.
	 *
	 * @param  string $htmlTag The 'html' tag as a string.
	 * @return string          The filtered 'html' tag as a string.
	 */
	public function addAttributes( $htmlTag ) {
		if ( ! aioseo()->options->social->facebook->general->enable ) {
			return $htmlTag;
		}

		$attributes = apply_filters( 'aioseo_opengraph_attributes', [ 'prefix="og: https://ogp.me/ns#"' ] );
		foreach ( $attributes as $attr ) {
			if ( strpos( $htmlTag, $attr ) === false ) {
				$htmlTag .= " $attr ";
			}
		}

		return trim( $htmlTag );
	}

	/**
	 * Schedule a ping to bust the OG cache.
	 *
	 * @since 4.2.0
	 *
	 * @param  int      $postId The post ID.
	 * @param  \WP_Post $post   The post object.
	 * @return void
	 */
	public function scheduleBustOgCachePost( $postId, $post = null ) {
		if ( ! aioseo()->helpers->isSbCustomFacebookFeedActive() || ! aioseo()->helpers->isValidPost( $post ) ) {
			return;
		}

		if ( aioseo()->actionScheduler->isScheduled( $this->bustOgCacheActionName, [ 'postId' => $postId ] ) ) {
			return;
		}

		// Schedule the new ping.
		aioseo()->actionScheduler->scheduleAsync( $this->bustOgCacheActionName, [ 'postId' => $postId ] );
	}

	/**
	 * Pings Facebook and asks them to bust the OG cache for a particular post.
	 *
	 * @since 4.2.0
	 *
	 * @see https://developers.facebook.com/docs/sharing/opengraph/using-objects#update
	 *
	 * @param  int  $postId The post ID.
	 * @return void
	 */
	public function bustOgCachePost( $postId ) {
		$post              = get_post( $postId );
		$customAccessToken = apply_filters( 'aioseo_facebook_access_token', '' );

		if (
			! aioseo()->helpers->isValidPost( $post ) ||
			( ! aioseo()->helpers->isSbCustomFacebookFeedActive() && ! $customAccessToken )
		) {
			return;
		}

		$permalink = get_permalink( $postId );
		$this->bustOgCacheHelper( $permalink );
	}

	/**
	 * Helper function for bustOgCache().
	 *
	 * @since 4.2.0
	 *
	 * @param  string $permalink The permalink.
	 * @return void
	 */
	protected function bustOgCacheHelper( $permalink ) {
		$accessToken = aioseo()->helpers->getSbAccessToken();
		$accessToken = apply_filters( 'aioseo_facebook_access_token', $accessToken );
		if ( ! $accessToken ) {
			return;
		}

		$url = sprintf(
			'https://graph.facebook.com/?%s',
			http_build_query(
				[
					'id'           => $permalink,
					'scrape'       => true,
					'access_token' => $accessToken
				]
			)
		);

		wp_remote_post( $url, [ 'blocking' => false ] );
	}
}Common/Social/Facebook.php000066600000040211151135505570011441 0ustar00<?php
namespace AIOSEO\Plugin\Common\Social;

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

use AIOSEO\Plugin\Common\Traits;

/**
 * Handles the Open Graph meta.
 *
 * @since 4.0.0
 */
class Facebook {
	use Traits\SocialProfiles;

	/**
	 * Returns the Open Graph image URL.
	 *
	 * @since 4.0.0
	 *
	 * @param  int    $postId The post ID (optional).
	 * @return string         The image URL.
	 */
	public function getImage( $postId = null ) {
		$post = aioseo()->helpers->getPost( $postId );
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			$image = aioseo()->options->social->facebook->homePage->image;
			if ( empty( $image ) ) {
				$image = aioseo()->social->image->getImage( 'facebook', aioseo()->options->social->facebook->general->defaultImageSourcePosts, $post );
			}

			return $image;
		}

		$metaData = aioseo()->meta->metaData->getMetaData( $post );

		$image = '';
		if ( ! empty( $metaData ) ) {
			$imageSource = ! empty( $metaData->og_image_type ) && 'default' !== $metaData->og_image_type
				? $metaData->og_image_type
				: aioseo()->options->social->facebook->general->defaultImageSourcePosts;

			$image = aioseo()->social->image->getImage( 'facebook', $imageSource, $post );
		}

		// Since we could be on an archive page, let's check again for that default image.
		if ( ! $image ) {
			$image = aioseo()->social->image->getImage( 'facebook', 'default' );
		}

		if ( ! $image ) {
			$image = aioseo()->helpers->getSiteLogoUrl();
		}

		// Allow users to control the default image per post type.
		return apply_filters(
			'aioseo_opengraph_default_image',
			$image,
			[
				$post,
				$this->getObjectType()
			]
		);
	}

	/**
	 * Returns the width of the Open Graph image.
	 *
	 * @since 4.0.0
	 *
	 * @return string The image width.
	 */
	public function getImageWidth() {
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			$width = aioseo()->options->social->facebook->homePage->imageWidth;

			return $width ? $width : aioseo()->options->social->facebook->general->defaultImagePostsWidth;
		}

		$metaData = aioseo()->meta->metaData->getMetaData();
		if ( ! empty( $metaData->og_custom_image_width ) ) {
			return $metaData->og_custom_image_width;
		}

		$image = $this->getImage();
		if ( is_array( $image ) ) {
			return $image[1];
		}

		return aioseo()->options->social->facebook->general->defaultImagePostsWidth;
	}

	/**
	 * Returns the height of the Open Graph image.
	 *
	 * @since 4.0.0
	 *
	 * @return string The image height.
	 */
	public function getImageHeight() {
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			$height = aioseo()->options->social->facebook->homePage->imageHeight;

			return $height ? $height : aioseo()->options->social->facebook->general->defaultImagePostsHeight;
		}

		$metaData = aioseo()->meta->metaData->getMetaData();
		if ( ! empty( $metaData->og_custom_image_height ) ) {
			return $metaData->og_custom_image_height;
		}

		$image = $this->getImage();
		if ( is_array( $image ) ) {
			return $image[2];
		}

		return aioseo()->options->social->facebook->general->defaultImagePostsHeight;
	}

	/**
	 * Returns the Open Graph video URL.
	 *
	 * @since 4.0.0
	 *
	 * @return string The video URL.
	 */
	public function getVideo() {
		$metaData = aioseo()->meta->metaData->getMetaData();

		return ! empty( $metaData->og_video ) ? $metaData->og_video : '';
	}

	/**
	 * Returns the width of the video.
	 *
	 * @since 4.0.0
	 *
	 * @return string The video width.
	 */
	public function getVideoWidth() {
		$metaData = aioseo()->meta->metaData->getMetaData();

		return ! empty( $metaData->og_video_width ) ? $metaData->og_video_width : '';
	}

	/**
	 * Returns the height of the video.
	 *
	 * @since 4.0.0
	 *
	 * @return string The video height.
	 */
	public function getVideoHeight() {
		$metaData = aioseo()->meta->metaData->getMetaData();

		return ! empty( $metaData->og_video_height ) ? $metaData->og_video_height : '';
	}

	/**
	 * Returns the site name.
	 *
	 * @since 4.0.0
	 *
	 * @return string The site name.
	 */
	public function getSiteName() {
		$title = aioseo()->helpers->decodeHtmlEntities( aioseo()->tags->replaceTags( aioseo()->options->social->facebook->general->siteName, get_the_ID() ) );
		if ( ! $title ) {
			$title = aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) );
		}

		return wp_strip_all_tags( $title );
	}

	/**
	 * Returns the Open Graph object type.
	 *
	 * @since 4.0.0
	 *
	 * @return string The object type.
	 */
	public function getObjectType() {
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			$type = aioseo()->options->social->facebook->homePage->objectType;

			return $type ? $type : 'website';
		}

		if ( is_post_type_archive() ) {
			return 'website';
		}

		$post     = aioseo()->helpers->getPost();
		$metaData = aioseo()->meta->metaData->getMetaData( $post );
		if ( ! empty( $metaData->og_object_type ) && 'default' !== $metaData->og_object_type ) {
			return $metaData->og_object_type;
		}

		$postType          = get_post_type();
		$dynamicOptions    = aioseo()->dynamicOptions->noConflict();
		$defaultObjectType = $dynamicOptions->social->facebook->general->postTypes->has( $postType )
			? $dynamicOptions->social->facebook->general->postTypes->$postType->objectType
			: '';

		return ! empty( $defaultObjectType ) ? $defaultObjectType : 'article';
	}

	/**
	 * Returns the Open Graph title for the current page.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|integer $post The post object or ID (optional).
	 * @return string                 The Open Graph title.
	 */
	public function getTitle( $post = null ) {
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			$title = aioseo()->meta->title->helpers->prepare( aioseo()->options->social->facebook->homePage->title );

			return $title ? $title : aioseo()->meta->title->getTitle();
		}

		$post     = aioseo()->helpers->getPost( $post );
		$metaData = aioseo()->meta->metaData->getMetaData( $post );

		$title = '';
		if ( ! empty( $metaData->og_title ) ) {
			$title = aioseo()->meta->title->helpers->prepare( $metaData->og_title );
		}

		if ( is_post_type_archive() ) {
			$postType = get_queried_object();
			if ( is_a( $postType, 'WP_Post_Type' ) ) {
				$dynamicOptions = aioseo()->dynamicOptions->noConflict();
				if ( $dynamicOptions->searchAppearance->archives->has( $postType->name ) ) {
					$title = aioseo()->meta->title->helpers->prepare( aioseo()->dynamicOptions->searchAppearance->archives->{ $postType->name }->title );
				}
			}
		}

		return $title
			? $title
			: (
				$post
					? aioseo()->meta->title->getPostTitle( $post )
					: $title
			);
	}

	/**
	 * Returns the Open Graph description.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|integer $post The post object or ID (optional).
	 * @return string                 The Open Graph description.
	 */
	public function getDescription( $post = null ) {
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			$description = aioseo()->meta->description->helpers->prepare( aioseo()->options->social->facebook->homePage->description );

			return $description ? $description : aioseo()->meta->description->getDescription();
		}

		$post     = aioseo()->helpers->getPost( $post );
		$metaData = aioseo()->meta->metaData->getMetaData( $post );

		$description = '';
		if ( ! empty( $metaData->og_description ) ) {
			$description = aioseo()->meta->description->helpers->prepare( $metaData->og_description );
		}

		if ( is_post_type_archive() ) {
			$postType = get_queried_object();
			if ( is_a( $postType, 'WP_Post_Type' ) ) {
				$dynamicOptions = aioseo()->dynamicOptions->noConflict();
				if ( $dynamicOptions->searchAppearance->archives->has( $postType->name ) ) {
					$description = aioseo()->meta->description->helpers->prepare( aioseo()->dynamicOptions->searchAppearance->archives->{ $postType->name }->metaDescription );
				}
			}
		}

		return $description
			? $description
			: (
				$post
					? aioseo()->meta->description->getPostDescription( $post )
					: $description
			);
	}

	/**
	 * Returns the Open Graph article section name.
	 *
	 * @since 4.0.0
	 *
	 * @return string The article section name.
	 */
	public function getSection() {
		$metaData = aioseo()->meta->metaData->getMetaData();

		return ! empty( $metaData->og_article_section ) ? $metaData->og_article_section : '';
	}

	/**
	 * Returns the Open Graph publisher URL.
	 *
	 * @since 4.0.0
	 *
	 * @return string The Open Graph publisher URL.
	 */
	public function getPublisher() {
		if ( ! aioseo()->options->social->profiles->sameUsername->enable ) {
			return aioseo()->options->social->profiles->urls->facebookPageUrl;
		}

		$username = aioseo()->options->social->profiles->sameUsername->username;

		return ( $username && in_array( 'facebookPageUrl', aioseo()->options->social->profiles->sameUsername->included, true ) )
			? 'https://facebook.com/' . $username
			: '';
	}

	/**
	 * Returns the published time.
	 *
	 * @since 4.0.0
	 *
	 * @return string The published time.
	 */
	public function getPublishedTime() {
		$post = aioseo()->helpers->getPost();

		return $post ? aioseo()->helpers->dateTimeToIso8601( $post->post_date_gmt ) : '';
	}

	/**
	 * Returns the last modified time.
	 *
	 * @since 4.0.0
	 *
	 * @return string The last modified time.
	 */
	public function getModifiedTime() {
		$post = aioseo()->helpers->getPost();

		return $post ? aioseo()->helpers->dateTimeToIso8601( $post->post_modified_gmt ) : '';
	}


	/**
	 * Returns the Open Graph author.
	 *
	 * @since 4.0.0
	 *
	 * @return string The Open Graph author.
	 */
	public function getAuthor() {
		$post = aioseo()->helpers->getPost();
		if ( ! is_a( $post, 'WP_Post' ) || ! aioseo()->options->social->facebook->general->showAuthor ) {
			return '';
		}

		$author       = '';
		$userProfiles = $this->getUserProfiles( $post->post_author );
		if ( ! empty( $userProfiles['facebookPageUrl'] ) ) {
			$author = $userProfiles['facebookPageUrl'];
		}

		if ( empty( $author ) ) {
			$author = aioseo()->options->social->facebook->advanced->authorUrl;
		}

		return $author;
	}

	/**
	 * Returns the Open Graph article tags.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of unique keywords.
	 */
	public function getArticleTags() {
		$post     = aioseo()->helpers->getPost();
		$metaData = aioseo()->meta->metaData->getMetaData( $post );
		$tags     = ! empty( $metaData->og_article_tags ) ? aioseo()->meta->keywords->extractMetaKeywords( $metaData->og_article_tags ) : [];

		if (
			$post &&
			aioseo()->options->social->facebook->advanced->enable &&
			aioseo()->options->social->facebook->advanced->generateArticleTags
		) {
			if ( aioseo()->options->social->facebook->advanced->useKeywordsInTags ) {
				$keywords = aioseo()->meta->keywords->getKeywords();
				$keywords = aioseo()->tags->parseCustomFields( $keywords );
				$keywords = aioseo()->meta->keywords->keywordStringToList( $keywords );
				$tags     = array_merge( $tags, $keywords );
			}

			if ( aioseo()->options->social->facebook->advanced->useCategoriesInTags ) {
				$tags = array_merge( $tags, aioseo()->helpers->getAllCategories( $post->ID ) );
			}

			if ( aioseo()->options->social->facebook->advanced->usePostTagsInTags ) {
				$tags = array_merge( $tags, aioseo()->helpers->getAllTags( $post->ID ) );
			}
		}

		return aioseo()->meta->keywords->getUniqueKeywords( $tags, false );
	}

	/**
	 * Retreive the locale.
	 *
	 * @since 4.1.4
	 *
	 * @return string The locale.
	 */
	public function getLocale() {
		$locale = get_locale();

		// These are the locales FB supports.
		$validLocales = [
			'af_ZA', // Afrikaans.
			'ak_GH', // Akan.
			'am_ET', // Amharic.
			'ar_AR', // Arabic.
			'as_IN', // Assamese.
			'ay_BO', // Aymara.
			'az_AZ', // Azerbaijani.
			'be_BY', // Belarusian.
			'bg_BG', // Bulgarian.
			'bp_IN', // Bhojpuri.
			'bn_IN', // Bengali.
			'br_FR', // Breton.
			'bs_BA', // Bosnian.
			'ca_ES', // Catalan.
			'cb_IQ', // Sorani Kurdish.
			'ck_US', // Cherokee.
			'co_FR', // Corsican.
			'cs_CZ', // Czech.
			'cx_PH', // Cebuano.
			'cy_GB', // Welsh.
			'da_DK', // Danish.
			'de_DE', // German.
			'el_GR', // Greek.
			'en_GB', // English (UK).
			'en_PI', // English (Pirate).
			'en_UD', // English (Upside Down).
			'en_US', // English (US).
			'em_ZM',
			'eo_EO', // Esperanto.
			'es_ES', // Spanish (Spain).
			'es_LA', // Spanish.
			'es_MX', // Spanish (Mexico).
			'et_EE', // Estonian.
			'eu_ES', // Basque.
			'fa_IR', // Persian.
			'fb_LT', // Leet Speak.
			'ff_NG', // Fulah.
			'fi_FI', // Finnish.
			'fo_FO', // Faroese.
			'fr_CA', // French (Canada).
			'fr_FR', // French (France).
			'fy_NL', // Frisian.
			'ga_IE', // Irish.
			'gl_ES', // Galician.
			'gn_PY', // Guarani.
			'gu_IN', // Gujarati.
			'gx_GR', // Classical Greek.
			'ha_NG', // Hausa.
			'he_IL', // Hebrew.
			'hi_IN', // Hindi.
			'hr_HR', // Croatian.
			'hu_HU', // Hungarian.
			'ht_HT', // Haitian Creole.
			'hy_AM', // Armenian.
			'id_ID', // Indonesian.
			'ig_NG', // Igbo.
			'is_IS', // Icelandic.
			'it_IT', // Italian.
			'ik_US',
			'iu_CA',
			'ja_JP', // Japanese.
			'ja_KS', // Japanese (Kansai).
			'jv_ID', // Javanese.
			'ka_GE', // Georgian.
			'kk_KZ', // Kazakh.
			'km_KH', // Khmer.
			'kn_IN', // Kannada.
			'ko_KR', // Korean.
			'ks_IN', // Kashmiri.
			'ku_TR', // Kurdish (Kurmanji).
			'ky_KG', // Kyrgyz.
			'la_VA', // Latin.
			'lg_UG', // Ganda.
			'li_NL', // Limburgish.
			'ln_CD', // Lingala.
			'lo_LA', // Lao.
			'lt_LT', // Lithuanian.
			'lv_LV', // Latvian.
			'mg_MG', // Malagasy.
			'mi_NZ', // Maori.
			'mk_MK', // Macedonian.
			'ml_IN', // Malayalam.
			'mn_MN', // Mongolian.
			'mr_IN', // Marathi.
			'ms_MY', // Malay.
			'mt_MT', // Maltese.
			'my_MM', // Burmese.
			'nb_NO', // Norwegian (bokmal).
			'nd_ZW', // Ndebele.
			'ne_NP', // Nepali.
			'nl_BE', // Dutch (Belgie).
			'nl_NL', // Dutch.
			'nn_NO', // Norwegian (nynorsk).
			'nr_ZA', // Southern Ndebele.
			'ns_ZA', // Northern Sotho.
			'ny_MW', // Chewa.
			'om_ET', // Oromo.
			'or_IN', // Oriya.
			'pa_IN', // Punjabi.
			'pl_PL', // Polish.
			'ps_AF', // Pashto.
			'pt_BR', // Portuguese (Brazil).
			'pt_PT', // Portuguese (Portugal).
			'qc_GT', // Quiché.
			'qu_PE', // Quechua.
			'qr_GR',
			'qz_MM', // Burmese (Zawgyi).
			'rm_CH', // Romansh.
			'ro_RO', // Romanian.
			'ru_RU', // Russian.
			'rw_RW', // Kinyarwanda.
			'sa_IN', // Sanskrit.
			'sc_IT', // Sardinian.
			'se_NO', // Northern Sami.
			'si_LK', // Sinhala.
			'su_ID', // Sundanese.
			'sk_SK', // Slovak.
			'sl_SI', // Slovenian.
			'sn_ZW', // Shona.
			'so_SO', // Somali.
			'sq_AL', // Albanian.
			'sr_RS', // Serbian.
			'ss_SZ', // Swazi.
			'st_ZA', // Southern Sotho.
			'sv_SE', // Swedish.
			'sw_KE', // Swahili.
			'sy_SY', // Syriac.
			'sz_PL', // Silesian.
			'ta_IN', // Tamil.
			'te_IN', // Telugu.
			'tg_TJ', // Tajik.
			'th_TH', // Thai.
			'tk_TM', // Turkmen.
			'tl_PH', // Filipino.
			'tl_ST', // Klingon.
			'tn_BW', // Tswana.
			'tr_TR', // Turkish.
			'ts_ZA', // Tsonga.
			'tt_RU', // Tatar.
			'tz_MA', // Tamazight.
			'uk_UA', // Ukrainian.
			'ur_PK', // Urdu.
			'uz_UZ', // Uzbek.
			've_ZA', // Venda.
			'vi_VN', // Vietnamese.
			'wo_SN', // Wolof.
			'xh_ZA', // Xhosa.
			'yi_DE', // Yiddish.
			'yo_NG', // Yoruba.
			'zh_CN', // Simplified Chinese (China).
			'zh_HK', // Traditional Chinese (Hong Kong).
			'zh_TW', // Traditional Chinese (Taiwan).
			'zu_ZA', // Zulu.
			'zz_TR', // Zazaki.
		];

		// Catch some weird locales served out by WP that are not easily doubled up.
		$fixLocales = [
			'ca' => 'ca_ES',
			'en' => 'en_US',
			'el' => 'el_GR',
			'et' => 'et_EE',
			'ja' => 'ja_JP',
			'sq' => 'sq_AL',
			'uk' => 'uk_UA',
			'vi' => 'vi_VN',
			'zh' => 'zh_CN',
		];

		if ( isset( $fixLocales[ $locale ] ) ) {
			$locale = $fixLocales[ $locale ];
		}

		// Convert locales like "es" to "es_ES", in case that works for the given locale (sometimes it does).
		if ( 2 === strlen( $locale ) ) {
			$locale = strtolower( $locale ) . '_' . strtoupper( $locale );
		}

		// Check to see if the locale is a valid FB one, if not, use en_US as a fallback.
		if ( ! in_array( $locale, $validLocales, true ) ) {
			$locale = strtolower( substr( $locale, 0, 2 ) ) . '_' . strtoupper( substr( $locale, 0, 2 ) );

			if ( ! in_array( $locale, $validLocales, true ) ) {
				$locale = 'en_US';
			}
		}

		return apply_filters( 'aioseo_og_locale', $locale );
	}
}Common/Social/Twitter.php000066600000017724151135505570011407 0ustar00<?php
namespace AIOSEO\Plugin\Common\Social;

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

use AIOSEO\Plugin\Common\Traits;

/**
 * Handles the Twitter meta.
 *
 * @since 4.0.0
 */
class Twitter {
	use Traits\SocialProfiles;

	/**
	 * Returns the Twitter URL for the site.
	 *
	 * @since 4.0.0
	 *
	 * @return string The Twitter URL.
	 */
	public function getTwitterUrl() {
		if ( ! aioseo()->options->social->profiles->sameUsername->enable ) {
			return aioseo()->options->social->profiles->urls->twitterUrl;
		}

		$userName = aioseo()->options->social->profiles->sameUsername->username;

		return ( $userName && in_array( 'twitterUrl', aioseo()->options->social->profiles->sameUsername->included, true ) )
			? 'https://x.com/' . $userName
			: '';
	}

	/**
	 * Returns the Twitter card type.
	 *
	 * @since 4.0.0
	 *
	 * @return string $card The card type.
	 */
	public function getCardType() {
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			return aioseo()->options->social->twitter->homePage->cardType;
		}

		$metaData = aioseo()->meta->metaData->getMetaData();

		return ! empty( $metaData->twitter_card ) && 'default' !== $metaData->twitter_card ? $metaData->twitter_card : aioseo()->options->social->twitter->general->defaultCardType;
	}

	/**
	 * Returns the Twitter creator.
	 *
	 * @since 4.0.0
	 *
	 * @return string The creator.
	 */
	public function getCreator() {
		$post = aioseo()->helpers->getPost();
		if (
			! is_a( $post, 'WP_Post' ) ||
			! post_type_supports( $post->post_type, 'author' ) ||
			! aioseo()->options->social->twitter->general->showAuthor
		) {
			return '';
		}

		$author       = '';
		$userProfiles = $this->getUserProfiles( $post->post_author );
		if ( ! empty( $userProfiles['twitterUrl'] ) ) {
			$author = $userProfiles['twitterUrl'];
		}

		if ( empty( $author ) ) {
			$author = aioseo()->social->twitter->getTwitterUrl();
		}

		$author = aioseo()->social->twitter->prepareUsername( $author );

		return $author;
	}

	/**
	 * Returns the Twitter image URL.
	 *
	 * @since 4.0.0
	 *
	 * @param  int    $postId The post ID (optional).
	 * @return string         The image URL.
	 */
	public function getImage( $postId = null ) {
		$post = aioseo()->helpers->getPost( $postId );
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			$image = aioseo()->options->social->twitter->homePage->image;
			if ( empty( $image ) ) {
				$image = aioseo()->options->social->facebook->homePage->image;
			}
			if ( empty( $image ) ) {
				$image = aioseo()->social->image->getImage( 'twitter', aioseo()->options->social->twitter->general->defaultImageSourcePosts, $post );
			}

			return $image ? $image : aioseo()->social->facebook->getImage();
		}

		$metaData = aioseo()->meta->metaData->getMetaData( $post );

		if ( ! empty( $metaData->twitter_use_og ) ) {
			return aioseo()->social->facebook->getImage();
		}

		$image = '';
		if ( ! empty( $metaData ) ) {
			$imageSource = ! empty( $metaData->twitter_image_type ) && 'default' !== $metaData->twitter_image_type
				? $metaData->twitter_image_type
				: aioseo()->options->social->twitter->general->defaultImageSourcePosts;

			$image = aioseo()->social->image->getImage( 'twitter', $imageSource, $post );
		}

		return $image ? $image : aioseo()->social->facebook->getImage();
	}

	/**
	 * Returns the Twitter title for the current page.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|integer $post The post object or ID (optional).
	 * @return string                 The Twitter title.
	 */
	public function getTitle( $post = null ) {
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			$title = aioseo()->meta->title->helpers->prepare( aioseo()->options->social->twitter->homePage->title );

			return $title ? $title : aioseo()->social->facebook->getTitle( $post );
		}

		$post     = aioseo()->helpers->getPost( $post );
		$metaData = aioseo()->meta->metaData->getMetaData( $post );

		if ( ! empty( $metaData->twitter_use_og ) ) {
			return aioseo()->social->facebook->getTitle( $post );
		}

		$title = '';
		if ( ! empty( $metaData->twitter_title ) ) {
			$title = aioseo()->meta->title->helpers->prepare( $metaData->twitter_title );
		}

		return $title ? $title : aioseo()->social->facebook->getTitle( $post );
	}

	/**
	 * Returns the Twitter description for the current page.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|integer $post The post object or ID (optional).
	 * @return string                 The Twitter description.
	 */
	public function getDescription( $post = null ) {
		if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
			$description = aioseo()->meta->description->helpers->prepare( aioseo()->options->social->twitter->homePage->description );

			return $description ? $description : aioseo()->social->facebook->getDescription( $post );
		}

		$post     = aioseo()->helpers->getPost( $post );
		$metaData = aioseo()->meta->metaData->getMetaData( $post );

		if ( ! empty( $metaData->twitter_use_og ) ) {
			return aioseo()->social->facebook->getDescription( $post );
		}

		$description = '';
		if ( ! empty( $metaData->twitter_description ) ) {
			$description = aioseo()->meta->description->helpers->prepare( $metaData->twitter_description );
		}

		return $description ? $description : aioseo()->social->facebook->getDescription( $post );
	}

	/**
	 * Prepare twitter username for public display.
	 *
	 * We do things like strip out the URL, etc and return just (at)username.
	 * At the moment, we'll check for 1 of 3 things... (at)username, username, and https://x.com/username.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $profile   Twitter username.
	 * @param  boolean $includeAt Whether or not ot include the @ sign.
	 * @return string             Full Twitter username.
	 */
	public function prepareUsername( $profile, $includeAt = true ) {
		if ( ! $profile ) {
			return $profile;
		}

		$profile = (string) $profile;
		if ( preg_match( '/^(\@)?[A-Za-z0-9_]+$/', (string) $profile ) ) {
			if ( '@' !== $profile[0] && $includeAt ) {
				$profile = '@' . $profile;
			} elseif ( '@' === $profile[0] && ! $includeAt ) {
				$profile = ltrim( $profile, '@' );
			}
		}

		if ( strpos( $profile, 'twitter.com' ) || strpos( $profile, 'x.com' ) ) {
			$profile = esc_url( $profile );

			// Extract the twitter username from the URL.
			$parsedTwitterProfile = wp_parse_url( $profile );

			$path      = $parsedTwitterProfile['path'];
			$pathParts = explode( '/', $path );
			$profile   = $pathParts[1];

			if ( $profile ) {
				if ( '@' !== $profile[0] && $includeAt ) {
					$profile = '@' . $profile;
				}

				if ( '@' === $profile[0] && ! $includeAt ) {
					$profile = ltrim( $profile, '@' );
				}
			}
		}

		return $profile;
	}

	/**
	 * Get additional twitter data.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of additional twitter data.
	 */
	public function getAdditionalData() {
		if ( ! aioseo()->options->social->twitter->general->additionalData ) {
			return [];
		}

		$data = [];
		$post = aioseo()->helpers->getPost();
		if ( ! is_a( $post, 'WP_Post' ) ) {
			return $data;
		}

		if ( $post->post_author && post_type_supports( $post->post_type, 'author' ) ) {
			$data[] = [
				'label' => __( 'Written by', 'all-in-one-seo-pack' ),
				'value' => get_the_author_meta( 'display_name', $post->post_author )
			];
		}

		if ( ! empty( $post->post_content ) ) {
			$minutes = $this->getReadingTime( $post->post_content );
			if ( ! empty( $minutes ) ) {
				$data[] = [
					'label' => __( 'Est. reading time', 'all-in-one-seo-pack' ),
					// Translators: 1 - The estimated reading time.
					'value' => sprintf( _n( '%1$s minute', '%1$s minutes', $minutes, 'all-in-one-seo-pack' ), $minutes )
				];
			}
		}

		return $data;
	}

	/**
	 * Returns the estimated reading time for a string.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $string The string to count.
	 * @return integer         The estimated reading time as an integer.
	 */
	private function getReadingTime( $string ) {
		$wpm  = 200;
		$word = str_word_count( wp_strip_all_tags( $string ) );

		return round( $word / $wpm );
	}
}Common/Social/Output.php000066600000011065151135505570011235 0ustar00<?php
namespace AIOSEO\Plugin\Common\Social;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Outputs our social meta.
 *
 * @since 4.0.0
 */
class Output {

	/**
	 * Checks if the current page should have social meta.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether or not the page should have social meta.
	 */
	public function isAllowed() {
		if ( BuddyPressIntegration::isComponentPage() ) {
			return false;
		}

		if (
			! is_front_page() &&
			! is_home() &&
			! is_singular() &&
			! is_post_type_archive() &&
			! aioseo()->helpers->isWooCommerceShopPage()
		) {
			return false;
		}

		return true;
	}

	/**
	 * Returns the Open Graph meta.
	 *
	 * @since 4.0.0
	 *
	 * @return array The Open Graph meta.
	 */
	public function getFacebookMeta() {
		if ( ! $this->isAllowed() || ! aioseo()->options->social->facebook->general->enable ) {
			return [];
		}

		$meta = [
			'og:locale'      => aioseo()->social->facebook->getLocale(),
			'og:site_name'   => aioseo()->helpers->encodeOutputHtml( aioseo()->social->facebook->getSiteName() ),
			'og:type'        => aioseo()->social->facebook->getObjectType(),
			'og:title'       => aioseo()->helpers->encodeOutputHtml( aioseo()->social->facebook->getTitle() ),
			'og:description' => aioseo()->helpers->encodeOutputHtml( aioseo()->social->facebook->getDescription() ),
			'og:url'         => esc_url( aioseo()->helpers->canonicalUrl() ),
			'fb:app_id'      => aioseo()->options->social->facebook->advanced->appId,
			'fb:admins'      => implode( ',', array_map( 'trim', explode( ',', aioseo()->options->social->facebook->advanced->adminId ) ) ),
		];

		$image = aioseo()->social->facebook->getImage();
		if ( $image ) {
			$image = is_array( $image ) ? $image[0] : $image;
			$image = aioseo()->helpers->makeUrlAbsolute( $image );
			$image = set_url_scheme( esc_url( $image ) );

			$meta += [
				'og:image'            => $image,
				'og:image:secure_url' => is_ssl() ? $image : '',
				'og:image:width'      => aioseo()->social->facebook->getImageWidth(),
				'og:image:height'     => aioseo()->social->facebook->getImageHeight(),
			];
		}

		$video = aioseo()->social->facebook->getVideo();
		if ( $video ) {
			$video = set_url_scheme( esc_url( $video ) );

			$meta += [
				'og:video'            => $video,
				'og:video:secure_url' => is_ssl() ? $video : '',
				'og:video:width'      => aioseo()->social->facebook->getVideoWidth(),
				'og:video:height'     => aioseo()->social->facebook->getVideoHeight(),
			];
		}

		if ( ! empty( $meta['og:type'] ) && 'article' === $meta['og:type'] ) {
			$meta += [
				'article:section'        => aioseo()->social->facebook->getSection(),
				'article:tag'            => aioseo()->social->facebook->getArticleTags(),
				'article:published_time' => aioseo()->social->facebook->getPublishedTime(),
				'article:modified_time'  => aioseo()->social->facebook->getModifiedTime(),
				'article:publisher'      => aioseo()->social->facebook->getPublisher(),
				'article:author'         => aioseo()->social->facebook->getAuthor()
			];
		}

		return array_filter( apply_filters( 'aioseo_facebook_tags', $meta ) );
	}

	/**
	 * Returns the Twitter meta.
	 *
	 * @since 4.0.0
	 *
	 * @return array The Twitter meta.
	 */
	public function getTwitterMeta() {
		if ( ! $this->isAllowed() || ! aioseo()->options->social->twitter->general->enable ) {
			return [];
		}

		$meta = [
			'twitter:card'        => aioseo()->social->twitter->getCardType(),
			'twitter:site'        => aioseo()->social->twitter->prepareUsername( aioseo()->social->twitter->getTwitterUrl() ),
			'twitter:title'       => aioseo()->helpers->encodeOutputHtml( aioseo()->social->twitter->getTitle() ),
			'twitter:description' => aioseo()->helpers->encodeOutputHtml( aioseo()->social->twitter->getDescription() ),
			'twitter:creator'     => aioseo()->social->twitter->getCreator()
		];

		$image = aioseo()->social->twitter->getImage();
		if ( $image ) {
			$image = is_array( $image ) ? $image[0] : $image;
			$image = aioseo()->helpers->makeUrlAbsolute( $image );

			// Set the twitter image meta.
			$meta['twitter:image'] = $image;
		}

		if ( is_singular() ) {
			$additionalData = apply_filters( 'aioseo_social_twitter_additional_data', aioseo()->social->twitter->getAdditionalData() );
			if ( $additionalData ) {
				$i = 1;
				foreach ( $additionalData as $data ) {
					$meta[ "twitter:label$i" ] = $data['label'];
					$meta[ "twitter:data$i" ]  = $data['value'];
					$i++;
				}
			}
		}

		return array_filter( apply_filters( 'aioseo_twitter_tags', $meta ) );
	}
}Common/SeoRevisions/SeoRevisions.php000066600000002271151135505570013602 0ustar00<?php
namespace AIOSEO\Plugin\Common\SeoRevisions;

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

/**
 * SEO Revisions container class.
 *
 * @since 4.4.0
 */
class SeoRevisions {
	/**
	 * Returns the data for Vue.
	 *
	 * @since 4.4.0
	 *
	 * @return array The data.
	 */
	public function getVueDataCompare() {
		return [
			'currentUser' => $this->getVueDataCurrentUserMeta()
		];
	}

	/**
	 * Returns the data for Vue.
	 *
	 * @since 4.4.0
	 *
	 * @return array The data.
	 */
	public function getVueDataEdit() {
		return $this->getVueDataCompare();
	}

	/**
	 * Retrieve the current user info for usage on Vue UI.
	 *
	 * @since 4.4.0
	 *
	 * @return array Current logged-in user info.
	 */
	protected function getVueDataCurrentUserMeta() {
		$currentUserId = get_current_user_id();
		$avatarData    = get_avatar_data( $currentUserId, [
			'size'    => 32,
			'default' => 'mystery'
		] );

		return [
			'avatar'       => [
				'size' => absint( $avatarData['size'] ),
				'url'  => $avatarData['found_avatar'] ? esc_url( $avatarData['url'] ) : strval( get_avatar_url( 0, $avatarData ) )
			],
			'display_name' => get_the_author_meta( 'display_name', $currentUserId )
		];
	}
}Common/Tools/SystemStatus.php000066600000032103151135505570012307 0ustar00<?php
namespace AIOSEO\Plugin\Common\Tools;

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

class SystemStatus {
	/**
	 * Get an aggregated list of all system info.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of system information.
	 */
	public static function getSystemStatusInfo() {
		return [
			'wordPress'       => self::getWordPressInfo(),
			'constants'       => self::getConstants(),
			'serverInfo'      => self::getServerInfo(),
			'muPlugins'       => self::mustUsePlugins(),
			'activeTheme'     => self::activeTheme(),
			'activePlugins'   => self::activePlugins(),
			'inactivePlugins' => self::inactivePlugins(),
			'database'        => self::getDatabaseInfo()
		];
	}

	/**
	 * Get an array of system info from WordPress.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of system info.
	 */
	public static function getWordPressInfo() {
		$uploadsDir    = wp_upload_dir();
		$version       = get_bloginfo( 'version' );
		$updates       = get_site_transient( 'update_core' );
		$updateVersion = ! empty( $updates->updates[0]->version ) ? $updates->updates[0]->version : '';
		if ( version_compare( $version, $updateVersion, '<' ) ) {
			$version .= ' (' . __( 'Latest version:', 'all-in-one-seo-pack' ) . ' ' . $updateVersion . ')';
		}

		return [
			'label'   => 'WordPress',
			'results' => [
				[
					'header' => __( 'Version', 'all-in-one-seo-pack' ),
					'value'  => $version
				],
				[
					'header' => __( 'Site Title', 'all-in-one-seo-pack' ),
					'value'  => get_bloginfo( 'name' )
				],
				[
					'header' => __( 'Site Language', 'all-in-one-seo-pack' ),
					'value'  => get_locale() ?: 'en_US'
				],
				[
					'header' => __( 'User Language', 'all-in-one-seo-pack' ),
					'value'  => get_user_locale( get_current_user_id() )
				],
				[
					'header' => __( 'Timezone', 'all-in-one-seo-pack' ),
					'value'  => wp_timezone_string()
				],
				[
					'header' => __( 'Home URL', 'all-in-one-seo-pack' ),
					'value'  => home_url()
				],
				[
					'header' => __( 'Site URL', 'all-in-one-seo-pack' ),
					'value'  => site_url()
				],
				[
					'header' => __( 'Permalink Structure', 'all-in-one-seo-pack' ),
					'value'  => get_option( 'permalink_structure' ) ? get_option( 'permalink_structure' ) : __( 'Default', 'all-in-one-seo-pack' )
				],
				[
					'header' => __( 'Multisite', 'all-in-one-seo-pack' ),
					'value'  => is_multisite() ? __( 'Yes', 'all-in-one-seo-pack' ) : __( 'No', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'HTTPS',
					'value'  => is_ssl() ? __( 'Yes', 'all-in-one-seo-pack' ) : __( 'No', 'all-in-one-seo-pack' )
				],
				[
					'header' => __( 'User Count', 'all-in-one-seo-pack' ),
					'value'  => count_users()['total_users']
				],
				[
					'header' => __( 'Front Page Info', 'all-in-one-seo-pack' ),
					'value'  => 'page' === get_option( 'show_on_front' ) ? get_option( 'show_on_front' ) . ' [ID: ' . get_option( 'page_on_front' ) . ']' : get_option( 'show_on_front' )
				],
				[
					'header' => __( 'Search Engine Visibility', 'all-in-one-seo-pack' ),
					'value'  => get_option( 'blog_public' ) ? __( 'Visible', 'all-in-one-seo-pack' ) : __( 'Hidden', 'all-in-one-seo-pack' )
				],
				[
					'header' => __( 'Upload Directory Info', 'all-in-one-seo-pack' ),
					'value'  =>
						__( 'Path:', 'all-in-one-seo-pack' ) . ' ' . $uploadsDir['path'] . ', ' .
						__( 'Url:', 'all-in-one-seo-pack' ) . ' ' . $uploadsDir['url'] . ', ' .
						__( 'Base Directory:', 'all-in-one-seo-pack' ) . ' ' . $uploadsDir['basedir'] . ', ' .
						__( 'Base URL:', 'all-in-one-seo-pack' ) . ' ' . $uploadsDir['baseurl']
				]
			]
		];
	}

	/**
	 * Get an array of database info from WordPress.
	 *
	 * @since 4.4.5
	 *
	 * @return array An array of database info.
	 */
	public static function getDatabaseInfo() {
		$dbInfo = aioseo()->core->db->getDatabaseInfo();
		if ( empty( $dbInfo['tables'] ) ) {
			return [];
		}

		if ( ! aioseo()->helpers->isDev() ) {
			return [];
		}

		$results = [];
		$tables  = array_merge( $dbInfo['tables']['aioseo'], $dbInfo['tables']['other'] );
		foreach ( $tables as $tableName => $tableData ) {
			$results[] = [
				'header' => $tableName,
				'value'  => sprintf(
					// Translators: %1$s is the data size, %2$s is the index size, %3$s is the engine type.
					__( 'Data: %1$.2f MB / Index: %2$.2f MB / Engine: %3$s / Collation: %4$s', 'all-in-one-seo-pack' ),
					$tableData['data'],
					$tableData['index'],
					$tableData['engine'],
					$tableData['collation']
				)
			];
		}

		return [
			'label'   => __( 'Database', 'all-in-one-seo-pack' ),
			'results' => array_merge( [
				[
					'header' => __( 'Database Size', 'all-in-one-seo-pack' ),
					'value'  => sprintf( '%.2f MB', $dbInfo['size']['data'] + $dbInfo['size']['index'] )
				]
			], $results )
		];
	}

	/**
	 * Get an array of system info from WordPress constants.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of system info.
	 */
	public static function getConstants() {
		return [
			'label'   => __( 'Constants', 'all-in-one-seo-pack' ),
			'results' => [
				[
					'header' => 'ABSPATH',
					'value'  => ABSPATH
				],
				[
					'header' => 'WP_CONTENT_DIR',
					'value'  => defined( 'WP_CONTENT_DIR' ) ? ( WP_CONTENT_DIR ? WP_CONTENT_DIR : __( 'Disabled', 'all-in-one-seo-pack' ) ) : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'WP_CONTENT_URL',
					'value'  => defined( 'WP_CONTENT_URL' ) ? ( WP_CONTENT_URL ? WP_CONTENT_URL : __( 'Disabled', 'all-in-one-seo-pack' ) ) : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'UPLOADS',
					'value'  => defined( 'UPLOADS' ) ? ( UPLOADS ? UPLOADS : __( 'Disabled', 'all-in-one-seo-pack' ) ) : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'WP_DEBUG',
					'value'  => defined( 'WP_DEBUG' ) ? ( WP_DEBUG ? WP_DEBUG : __( 'Disabled', 'all-in-one-seo-pack' ) ) : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'WP_DEBUG_LOG',
					'value'  => defined( 'WP_DEBUG_LOG' ) ? ( WP_DEBUG_LOG ? WP_DEBUG_LOG : __( 'Disabled', 'all-in-one-seo-pack' ) ) : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'WP_DEBUG_DISPLAY',
					'value'  => defined( 'WP_DEBUG_DISPLAY' ) ? ( WP_DEBUG_DISPLAY ? WP_DEBUG_DISPLAY : __( 'Disabled', 'all-in-one-seo-pack' ) ) : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'WP_POST_REVISIONS',
					'value'  => defined( 'WP_POST_REVISIONS' ) ? WP_POST_REVISIONS : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'DISABLE_WP_CRON',
					'value'  => defined( 'DISABLE_WP_CRON' ) ? DISABLE_WP_CRON : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'EMPTY_TRASH_DAYS',
					'value'  => defined( 'EMPTY_TRASH_DAYS' ) ? EMPTY_TRASH_DAYS : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'AUTOSAVE_INTERVAL',
					'value'  => defined( 'AUTOSAVE_INTERVAL' ) ? AUTOSAVE_INTERVAL : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'SCRIPT_DEBUG',
					'value'  => defined( 'SCRIPT_DEBUG' ) ? SCRIPT_DEBUG : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'DB_CHARSET',
					'value'  => defined( 'DB_CHARSET' ) ? ( DB_CHARSET ? DB_CHARSET : __( 'Disabled', 'all-in-one-seo-pack' ) ) : __( 'Not set', 'all-in-one-seo-pack' )
				],
				[
					'header' => 'DB_COLLATE',
					'value'  => defined( 'DB_COLLATE' ) ? ( DB_COLLATE ? DB_COLLATE : __( 'Disabled', 'all-in-one-seo-pack' ) ) : __( 'Not set', 'all-in-one-seo-pack' )
				]
			]
		];
	}

	/**
	 * Get an array of system info from the server.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of system info.
	 */
	public static function getServerInfo() {
		$sqlMode   = null;
		$mysqlInfo = aioseo()->core->db->db->get_results( "SHOW VARIABLES LIKE 'sql_mode'" );
		if ( ! empty( $mysqlInfo ) && is_array( $mysqlInfo ) ) {
			$sqlMode = $mysqlInfo[0]->Value;
		}

		$dbServerInfo = method_exists( aioseo()->core->db->db, 'db_server_info' )
			? aioseo()->core->db->db->db_server_info()
			: ( function_exists( 'mysqli_get_server_info' )
				? mysqli_get_server_info( aioseo()->core->db->db->dbh ) // phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysqli_get_server_info
				: ''
		);

		return [
			'label'   => __( 'Server Info', 'all-in-one-seo-pack' ),
			'results' => [
				[
					'header' => __( 'Operating System', 'all-in-one-seo-pack' ),
					'value'  => PHP_OS
				],
				[
					'header' => __( 'Web Server', 'all-in-one-seo-pack' ),
					'value'  => ! empty( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : __( 'unknown', 'all-in-one-seo-pack' )
				],
				[
					'header' => __( 'Memory Usage', 'all-in-one-seo-pack' ),
					'value'  => function_exists( 'memory_get_usage' ) ? round( memory_get_usage() / 1024 / 1024, 2 ) . 'M' : __( 'N/A', 'all-in-one-seo-pack' )
				],
				[
					'header' => __( 'Database Powered By', 'all-in-one-seo-pack' ),
					'value'  => stripos( $dbServerInfo, 'mariadb' ) !== false ? 'MariaDB' : 'MySQL'
				],
				[
					'header' => __( 'Database Version', 'all-in-one-seo-pack' ),
					'value'  => aioseo()->core->db->db->db_version()
				],
				[
					'header' => __( 'SQL Mode', 'all-in-one-seo-pack' ),
					'value'  => $sqlMode ?? __( 'Not Set', 'all-in-one-seo-pack' ),
				],
				[
					'header' => __( 'PHP Version', 'all-in-one-seo-pack' ),
					'value'  => PHP_VERSION
				],
				[
					'header' => __( 'PHP Memory Limit', 'all-in-one-seo-pack' ),
					'value'  => ini_get( 'memory_limit' )
				],
				[
					'header' => __( 'PHP Max Upload Size', 'all-in-one-seo-pack' ),
					'value'  => ini_get( 'upload_max_filesize' )
				],
				[
					'header' => __( 'PHP Max Post Size', 'all-in-one-seo-pack' ),
					'value'  => ini_get( 'post_max_size' )
				],
				[
					'header' => __( 'PHP Max Script Execution Time', 'all-in-one-seo-pack' ),
					'value'  => ini_get( 'max_execution_time' )
				],
				[
					'header' => __( 'PHP Exif Support', 'all-in-one-seo-pack' ),
					'value'  => is_callable( 'exif_read_data' ) ? __( 'Yes', 'all-in-one-seo-pack' ) : __( 'No', 'all-in-one-seo-pack' )
				],
				[
					'header' => __( 'PHP IPTC Support', 'all-in-one-seo-pack' ),
					'value'  => is_callable( 'iptcparse' ) ? __( 'Yes', 'all-in-one-seo-pack' ) : __( 'No', 'all-in-one-seo-pack' )
				],
				[
					'header' => __( 'PHP XML Support', 'all-in-one-seo-pack' ),
					'value'  => is_callable( 'xml_parser_create' ) ? __( 'Yes', 'all-in-one-seo-pack' ) : __( 'No', 'all-in-one-seo-pack' )
				]
			]
		];
	}

	/**
	 * Get an array of system info from the active theme.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of system info.
	 */
	public static function activeTheme() {
		$themeData = wp_get_theme();

		return [
			'label'   => __( 'Active Theme', 'all-in-one-seo-pack' ),
			'results' => [
				[
					'header' => $themeData->name,
					'value'  => $themeData->version
				]
			]
		];
	}

	/**
	 * Get an array of system info from must-use plugins.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of system info.
	 */
	public static function mustUsePlugins() {
		$plugins   = [];
		$muPlugins = get_mu_plugins();
		if ( ! empty( $muPlugins ) ) {
			foreach ( $muPlugins as $pluginData ) {
				$plugins[] = [
					'header' => $pluginData['Name'],
					'value'  => $pluginData['Version']
				];
			}
		}

		return [
			'label'   => __( 'Must-Use Plugins', 'all-in-one-seo-pack' ),
			'results' => $plugins
		];
	}

	/**
	 * Get an array of system info from active plugins.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of system info.
	 */
	public static function activePlugins() {
		$plugins       = [];
		$allPlugins    = get_plugins();
		$activePlugins = get_option( 'active_plugins', [] );
		$updates       = get_plugin_updates();
		if ( ! empty( $allPlugins ) ) {
			foreach ( $allPlugins as $pluginPath => $pluginData ) {
				if ( ! in_array( $pluginPath, $activePlugins, true ) ) {
					continue;
				}

				$update    = ( array_key_exists( $pluginPath, $updates ) ) ? ' (' . __( 'needs update', 'all-in-one-seo-pack' ) . ' - ' . $updates[ $pluginPath ]->update->new_version . ')' : '';
				$plugins[] = [
					'header' => $pluginData['Name'],
					'value'  => $pluginData['Version'] . $update
				];
			}
		}

		return [
			'label'   => __( 'Active Plugins', 'all-in-one-seo-pack' ),
			'results' => $plugins
		];
	}

	/**
	 * Get an array of system info from inactive plugins.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of system info.
	 */
	public static function inactivePlugins() {
		$plugins       = [];
		$allPlugins    = get_plugins();
		$activePlugins = get_option( 'active_plugins', [] );
		$updates       = get_plugin_updates();
		if ( ! empty( $allPlugins ) ) {
			foreach ( $allPlugins as $pluginPath => $pluginData ) {
				if ( in_array( $pluginPath, $activePlugins, true ) ) {
					continue;
				}

				$update    = ( array_key_exists( $pluginPath, $updates ) ) ? ' (' . __( 'needs update', 'all-in-one-seo-pack' ) . ' - ' . $updates[ $pluginPath ]->update->new_version . ')' : '';
				$plugins[] = [
					'header' => $pluginData['Name'],
					'value'  => $pluginData['Version'] . $update
				];
			}
		}

		return [
			'label'   => __( 'Inactive Plugins', 'all-in-one-seo-pack' ),
			'results' => $plugins
		];
	}
}Common/Tools/RobotsTxt.php000066600000045145151135505570011601 0ustar00<?php
namespace AIOSEO\Plugin\Common\Tools;

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

use AIOSEO\Plugin\Common\Models;

class RobotsTxt {
	/**
	 * Which directives are allowed to be extracted.
	 *
	 * @since 4.4.2
	 *
	 * @var array
	 */
	private $allowedDirectives = [ 'user-agent', 'allow', 'disallow', 'clean-param', 'crawl-delay' ];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_filter( 'robots_txt', [ $this, 'buildRules' ], 10000 );

		if ( ! is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

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

	/**
	 * Build out the robots.txt rules.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $original The original rules to parse.
	 * @return string           The parsed/appended/modified rules.
	 */
	public function buildRules( $original ) {
		// Other plugins might call this too early.
		if ( ! property_exists( aioseo(), 'sitemap' ) ) {
			return $original;
		}

		$searchAppearanceRules = $this->extractSearchAppearanceRules();
		$networkRules          = [];
		if ( is_multisite() ) {
			$searchAppearanceRules = array_merge(
				$searchAppearanceRules,
				$this->extractSearchAppearanceRules( aioseo()->networkOptions->tools->robots->rules )
			);
			$networkRules          = aioseo()->networkOptions->tools->robots->enable ? aioseo()->networkOptions->tools->robots->rules : [];
		}

		$originalRules = $this->extractRules( $original );
		$ruleset       = $this->mergeRules( $originalRules, $this->groupRulesByUserAgent( $searchAppearanceRules ) );
		if ( ! aioseo()->options->tools->robots->enable ) {
			$ruleset = $this->mergeRules( $ruleset, $this->groupRulesByUserAgent( $networkRules ) );
		} else {
			$ruleset = $this->mergeRules(
				$ruleset,
				$this->mergeRules( $this->groupRulesByUserAgent( $networkRules ), $this->groupRulesByUserAgent( aioseo()->options->tools->robots->rules ) ),
				true
			);
		}

		/**
		 * Any plugin can wrongly modify the robots.txt output by hoking into the `do_robots` action hook,
		 * instead of hooking into the `robots_txt` filter hook.
		 * For the first scenario, to make sure our output doesn't conflict with theirs, a new line is necessary.
		 */
		return $this->stringifyRuleset( $ruleset ) . "\n";
	}

	/**
	 * Merges two rulesets.
	 *
	 * @since   4.0.0
	 * @version 4.4.2
	 *
	 * @param  array   $rules1          An array of rules to merge with.
	 * @param  array   $rules2          An array of rules to merge.
	 * @param  boolean $allowOverride   Whether to allow overriding.
	 * @param  boolean $allowDuplicates Whether to allow duplicates.
	 * @return array                    The validated rules.
	 */
	private function mergeRules( $rules1, $rules2, $allowOverride = false, $allowDuplicates = false ) {
		foreach ( $rules2 as $userAgent => $rules ) {
			if ( empty( $userAgent ) ) {
				continue;
			}

			if ( empty( $rules1[ $userAgent ] ) ) {
				$rules1[ $userAgent ] = array_unique( $rules2[ $userAgent ] );

				continue;
			}

			list( $rules1, $rules2 ) = $this->mergeRulesHelper( 'allow', $userAgent, $rules, $rules1, $rules2, $allowDuplicates, $allowOverride );
			list( $rules1, $rules2 ) = $this->mergeRulesHelper( 'disallow', $userAgent, $rules, $rules1, $rules2, $allowDuplicates, $allowOverride );

			$rules1[ $userAgent ] = array_unique( array_merge(
				$rules1[ $userAgent ],
				$rules2[ $userAgent ]
			) );
		}

		return $rules1;
	}

	/**
	 * Helper function for {@see mergeRules()}.
	 *
	 * @since   4.1.2
	 * @version 4.4.2
	 *
	 * @param  string $directive       The directive (allow/disallow).
	 * @param  string $userAgent       The user agent.
	 * @param  array  $rules           The rules.
	 * @param  array  $rules1          The original rules.
	 * @param  array  $rules2          The extra rules.
	 * @param  bool   $allowDuplicates Whether duplicates should be allowed
	 * @param  bool   $allowOverride   Whether the extra rules can override the original ones.
	 * @return array                   The original and extra rules.
	 */
	private function mergeRulesHelper( $directive, $userAgent, $rules, $rules1, $rules2, $allowDuplicates, $allowOverride ) {
		$otherDirective = ( 'allow' === $directive ) ? 'disallow' : 'allow';
		foreach ( $rules as $index1 => $rule ) {
			list( , $ruleValue ) = $this->parseRule( $rule );
			$index2 = array_search( "$otherDirective: $ruleValue", $rules1[ $userAgent ], true );
			if ( false !== $index2 && ! $allowDuplicates ) {
				if ( $allowOverride ) {
					unset( $rules1[ $userAgent ][ $index2 ] );
				} else {
					unset( $rules2[ $userAgent ][ $index1 ] );
				}
			}

			$pattern = str_replace( [ '.', '*', '?', '$' ], [ '\.', '(.*)', '\?', '\$' ], $ruleValue );

			foreach ( $rules1[ $userAgent ] as $rule1 ) {
				$matches = [];
				preg_match( "#^$otherDirective: $pattern$#", (string) $rule1, $matches );
			}

			if ( ! empty( $matches ) && ! $allowDuplicates ) {
				unset( $rules2[ $userAgent ][ $index1 ] );
			}
		}

		return [ $rules1, $rules2 ];
	}

	/**
	 * Parses a rule and extracts the directive and value.
	 *
	 * @since 4.4.2
	 *
	 * @param  string $rule The rule to parse.
	 * @return array        An array containing the parsed directive and value.
	 */
	private function parseRule( $rule ) {
		list( $directive, $value ) = array_map( 'trim', array_pad( explode( ':', $rule, 2 ), 2, '' ) );

		return [ $directive, $value ];
	}

	/**
	 * Stringifies the parsed rules.
	 *
	 * @since   4.0.0
	 * @version 4.4.2
	 *
	 * @param  array  $allRules The rules array.
	 * @return string           The stringified rules.
	 */
	private function stringifyRuleset( $allRules ) {
		$robots = [];
		foreach ( $allRules as $userAgent => $rules ) {
			if ( empty( $userAgent ) ) {
				continue;
			}

			$robots[] = "\r\nUser-agent: $userAgent";
			foreach ( $rules as $rule ) {
				list( $directive, $value ) = $this->parseRule( $rule );
				if ( empty( $directive ) || empty( $value ) ) {
					continue;
				}

				$robots[] = sprintf( '%s: %s', ucfirst( $directive ), $value );
			}
		}

		$robots      = implode( "\r\n", $robots );
		$sitemapUrls = $this->getSitemapRules();
		if ( ! empty( $sitemapUrls ) ) {
			$sitemapUrls = implode( "\r\n", $sitemapUrls );
			$robots      .= "\r\n\r\n$sitemapUrls";
		}

		return trim( $robots );
	}

	/**
	 * Get Sitemap URLs excluding the default ones.
	 *
	 * @since 4.1.7
	 *
	 * @return array An array of the Sitemap URLs.
	 */
	private function getSitemapRules() {
		$defaultSitemaps = $this->extractSitemapUrls( aioseo()->robotsTxt->getDefaultRobotsTxtContent() );
		$sitemapRules    = aioseo()->sitemap->helpers->getSitemapUrlsPrefixed();

		return array_diff( $sitemapRules, $defaultSitemaps );
	}

	/**
	 * Extracts the Search Appearance related rules.
	 *
	 * @since 4.8.1
	 *
	 * @param  array $rules The rules to extract from.
	 * @return array        The Search Appearance related rules.
	 */
	public function extractSearchAppearanceRules( $rules = [] ) {
		$currentRules = $rules ?: aioseo()->options->tools->robots->rules;

		return array_filter( $currentRules, function ( $rule ) {
			$parseRule = json_decode( $rule, true );

			return ! empty( $parseRule['bot'] ) || ! empty( $parseRule['preventCrawling'] );
		} );
	}

	/**
	 * Parses the rules.
	 *
	 * @since   4.0.0
	 * @version 4.4.2
	 *
	 * @param  array $rules An array of rules.
	 * @return array        The rules grouped by user agent.
	 */
	private function groupRulesByUserAgent( $rules ) {
		$groups = [];
		foreach ( $rules as $rule ) {
			$r = is_string( $rule ) ? json_decode( $rule, true ) : $rule;
			if ( empty( $r['userAgent'] ) || empty( $r['fieldValue'] ) ) {
				continue;
			}

			if ( empty( $groups[ $r['userAgent'] ] ) ) {
				$groups[ $r['userAgent'] ] = [];
			}

			$groups[ $r['userAgent'] ][] = "{$r['directive']}: {$r['fieldValue']}";
		}

		return $groups;
	}

	/**
	 * Extract rules from a string.
	 *
	 * @since   4.0.0
	 * @version 4.4.2
	 *
	 * @param  string $lines The lines to extract from.
	 * @return array         An array of extracted rules.
	 */
	public function extractRules( $lines ) {
		$lines              = array_filter( array_map( 'trim', explode( "\n", (string) $lines ) ) );
		$rules              = [];
		$userAgent          = null;
		$prevDirective      = null;
		$prevValue          = null;
		$siblingsUserAgents = [];
		foreach ( $lines as $line ) {
			list( $directive, $value ) = $this->parseRule( $line );
			if ( empty( $directive ) || empty( $value ) ) {
				continue;
			}

			$directive = strtolower( $directive );
			if ( ! in_array( $directive, $this->allowedDirectives, true ) ) {
				continue;
			}

			$value = $this->sanitizeDirectiveValue( $directive, $value );
			if ( ! $value ) {
				continue;
			}

			if ( 'user-agent' === $directive ) {
				if (
					! empty( $prevDirective ) &&
					! empty( $prevValue ) &&
					'user-agent' === $prevDirective
				) {
					$siblingsUserAgents[] = $prevValue;
				}

				$userAgent           = $value;
				$rules[ $userAgent ] = ! empty( $rules[ $userAgent ] ) ? $rules[ $userAgent ] : [];
			} else {
				$rules[ $userAgent ][] = "$directive: $value";
				if ( $siblingsUserAgents ) {
					foreach ( $siblingsUserAgents as $siblingUserAgent ) {
						$rules[ $siblingUserAgent ] = $rules[ $userAgent ];
					}

					$siblingsUserAgents = [];
				}
			}

			$prevDirective = $directive;
			$prevValue     = $value;
		}

		return $rules;
	}

	/**
	 * Extract sitemap URLs from a string.
	 *
	 * @since 4.0.10
	 *
	 * @param  string $lines The lines to extract from.
	 * @return array         An array of sitemap URLs.
	 */
	public function extractSitemapUrls( $lines ) {
		$lines       = array_filter( array_map( 'trim', explode( "\n", (string) $lines ) ) );
		$sitemapUrls = [];
		foreach ( $lines as $line ) {
			$array = array_map( 'trim', explode( 'sitemap:', strtolower( $line ) ) );
			if ( ! empty( $array[1] ) ) {
				$sitemapUrls[] = trim( $line );
			}
		}

		return $sitemapUrls;
	}

	/**
	 * Sanitize the robots.txt rule directive value.
	 *
	 * @since   4.0.0
	 * @version 4.4.2
	 *
	 * @param  string $directive The directive.
	 * @param  string $value     The value.
	 * @return string            The directive value.
	 */
	private function sanitizeDirectiveValue( $directive, $value ) {
		// Percent-encoded characters are stripped from our option values, so we decode.
		$value = rawurldecode( trim( $value ) );
		if ( ! $value ) {
			return $value;
		}

		$value = preg_replace( '/[><]/', '', (string) $value );

		if ( 'user-agent' === $directive ) {
			$value = preg_replace( '/[^a-z0-9\-_*,.\s]/i', '', (string) $value );
		}

		if ( 'allow' === $directive || 'disallow' === $directive ) {
			$value = preg_replace( '/^\/+/', '/', (string) $value );
		}

		return $value;
	}

	/**
	 * Check if a physical robots.txt file exists, and if it does add a notice.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function checkForPhysicalFiles() {
		if ( ! $this->hasPhysicalRobotsTxt() ) {
			return;
		}

		$notification = Models\Notification::getNotificationByName( 'robots-physical-file' );
		if ( $notification->exists() ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'robots-physical-file',
			'title'             => __( 'Physical Robots.txt File Detected', 'all-in-one-seo-pack' ),
			'content'           => sprintf(
				// Translators: 1 - The plugin short name ("AIOSEO"), 2 - The plugin short name ("AIOSEO").
				__( '%1$s has detected a physical robots.txt file in the root folder of your WordPress installation. We recommend removing this file as it could cause conflicts with WordPress\' dynamically generated one. %2$s can import this file and delete it, or you can simply delete it.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				AIOSEO_PLUGIN_SHORT_NAME,
				AIOSEO_PLUGIN_SHORT_NAME
			),
			'type'              => 'error',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Import and Delete', 'all-in-one-seo-pack' ),
			'button1_action'    => 'http://action#tools/import-robots-txt?redirect=aioseo-tools:robots-editor',
			'button2_label'     => __( 'Delete', 'all-in-one-seo-pack' ),
			'button2_action'    => 'http://action#tools/delete-robots-txt?redirect=aioseo-tools:robots-editor',
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Import physical robots.txt file.
	 *
	 * @since   4.0.0
	 * @version 4.4.2
	 *
	 * @param  int|string $blogId The blog ID or 'network'.
	 * @throws \Exception         If request fails or file is not readable.
	 * @return boolean            Whether the file imported correctly.
	 */
	public function importPhysicalRobotsTxt( $blogId ) {
		try {
			$fs = aioseo()->core->fs;
			if ( ! $fs->isWpfsValid() ) {
				$invalid = true;
			}

			$file = trailingslashit( $fs->fs->abspath() ) . 'robots.txt';
			if (
				isset( $invalid ) ||
				! $fs->isReadable( $file )
			) {
				throw new \Exception( esc_html__( 'There was an error importing the static robots.txt file.', 'all-in-one-seo-pack' ) );
			}

			$lines = trim( (string) $fs->getContents( $file ) );
			if ( $lines ) {
				$this->importRobotsTxtFromText( $lines, $blogId );
			}

			return true;
		} catch ( \Exception $e ) {
			throw new \Exception( esc_html( $e->getMessage() ) );
		}
	}

	/**
	 * Import robots.txt from a URL.
	 *
	 * @since 4.4.2
	 *
	 * @param  string     $text   The text to import from.
	 * @param  int|string $blogId The blog ID or 'network'.
	 * @throws \Exception         If no User-agent is found.
	 * @return boolean            Whether the file imported correctly or not.
	 */
	public function importRobotsTxtFromText( $text, $blogId ) {
		$newRules = $this->extractRules( $text );
		if ( ! key( $newRules ) ) {
			throw new \Exception( esc_html__( 'No User-agent found in the content beginning.', 'all-in-one-seo-pack' ) );
		}

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

		$options->tools->robots->rules = array_unique( array_merge( $options->tools->robots->rules, $this->prepareRobotsTxt( $newRules ) ) );

		return true;
	}

	/**
	 * Import robots.txt from a URL.
	 *
	 * @since 4.4.2
	 *
	 * @param  string     $url    The URL to import from.
	 * @param  int|string $blogId The blog ID or 'network'.
	 * @throws \Exception         If request fails.
	 * @return bool               Whether the import was successful or not.
	 */
	public function importRobotsTxtFromUrl( $url, $blogId ) {
		$request          = wp_remote_get( $url, [
			'timeout'   => 10,
			'sslverify' => false
		] );
		$robotsTxtContent = wp_remote_retrieve_body( $request );
		if ( ! $robotsTxtContent ) {
			throw new \Exception( esc_html__( 'There was an error importing the robots.txt content from the URL.', 'all-in-one-seo-pack' ) );
		}

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

		$newRules = $this->extractRules( $robotsTxtContent );

		$options->tools->robots->rules = array_unique( array_merge( $options->tools->robots->rules, $this->prepareRobotsTxt( $newRules ) ) );

		return true;
	}

	/**
	 * Deletes the physical robots.txt file.
	 *
	 * @since 4.4.5
	 *
	 * @throws \Exception If the file is not readable, or it can't be deleted.
	 * @return true       True if the file was successfully deleted.
	 */
	public function deletePhysicalRobotsTxt() {
		try {
			$fs = aioseo()->core->fs;
			if (
				! $fs->isWpfsValid() ||
				! $fs->fs->delete( trailingslashit( $fs->fs->abspath() ) . 'robots.txt' )
			) {
				throw new \Exception( __( 'There was an error deleting the physical robots.txt file.', 'all-in-one-seo-pack' ) );
			}

			Models\Notification::deleteNotificationByName( 'robots-physical-file' );

			return true;
		} catch ( \Exception $e ) {
			throw new \Exception( esc_html( $e->getMessage() ) );
		}
	}

	/**
	 * Prepare robots.txt rules to save.
	 *
	 * @since 4.1.4
	 *
	 * @param  array $allRules Array with the rules.
	 * @return array           The prepared rules array.
	 */
	public function prepareRobotsTxt( $allRules = [] ) {
		$robots = [];
		foreach ( $allRules as $userAgent => $rules ) {
			if ( empty( $userAgent ) ) {
				continue;
			}

			foreach ( $rules as $rule ) {
				list( $directive, $value ) = $this->parseRule( $rule );
				if ( empty( $directive ) || empty( $value ) ) {
					continue;
				}

				if (
					'*' === $userAgent &&
					(
						'allow' === $directive && '/wp-admin/admin-ajax.php' === $value ||
						'disallow' === $directive && '/wp-admin/' === $value
					)
				) {
					continue;
				}

				$robots[] = wp_json_encode( [
					'userAgent'  => $userAgent,
					'directive'  => $directive,
					'fieldValue' => $value
				] );
			}
		}

		return $robots;
	}

	/**
	 * Checks if a physical robots.txt file exists.
	 *
	 * @since 4.0.0
	 *
	 * @return boolean True if it does, false if not.
	 */
	public function hasPhysicalRobotsTxt() {
		$fs = aioseo()->core->fs;
		if ( ! $fs->isWpfsValid() ) {
			return false;
		}

		$accessType = get_filesystem_method();
		if ( 'direct' === $accessType ) {
			$file = trailingslashit( $fs->fs->abspath() ) . 'robots.txt';

			return $fs->exists( $file );
		}

		return false;
	}

	/**
	 * Get the default Robots.txt lines (excluding our own).
	 *
	 * @since   4.1.7
	 * @version 4.4.2
	 *
	 * @return string The robots.txt content rules (excluding our own).
	 */
	public function getDefaultRobotsTxtContent() {
		// First, we need to remove our filter, so that it doesn't run unintentionally.
		remove_filter( 'robots_txt', [ $this, 'buildRules' ], 10000 );

		ob_start();
		do_robots();
		if ( is_admin() ) {
			header( 'Content-Type: text/html; charset=utf-8' );
		}
		$rules = strval( ob_get_clean() );

		// Add the filter back.
		add_filter( 'robots_txt', [ $this, 'buildRules' ], 10000 );

		return $rules;
	}

	/**
	 * A check to see if the rewrite rules are set.
	 * This isn't perfect, but it will help us know in most cases.
	 *
	 * @since 4.0.0
	 *
	 * @return boolean Whether the rewrite rules are set or not.
	 */
	public function rewriteRulesExist() {
		// If we have a physical file, it's almost impossible to tell if the rewrite rules are set.
		// The only scenario is if we still get a 404.
		$response = wp_remote_get( aioseo()->helpers->getSiteUrl() . '/robots.txt' );
		if ( 299 < wp_remote_retrieve_response_code( $response ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Reset the Search Appearance related rules.
	 *
	 * @since 4.8.1
	 *
	 * @return void
	 */
	public function resetSearchAppearanceRules() {
		$currentRules = aioseo()->options->tools->robots->rules;
		$newRules     = [];
		foreach ( ( $currentRules ?? [] ) as $rule ) {
			$parseRule = json_decode( $rule, true );
			if ( empty( $parseRule['bot'] ) && empty( $parseRule['preventCrawling'] ) ) {
				$newRules[] = $rule;
			}
		}

		aioseo()->options->tools->robots->rules = $newRules;
	}
}Common/Tools/Htaccess.php000066600000004320151135505570011354 0ustar00<?php
namespace AIOSEO\Plugin\Common\Tools;

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

class Htaccess {
	/**
	 * The path to the .htaccess file.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $path = '';

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->path = ABSPATH . '.htaccess';
	}

	/**
	 * Get the contents of the .htaccess file.
	 *
	 * @since 4.0.0
	 *
	 * @return string The contents of the file.
	 */
	public function getContents() {
		$fs = aioseo()->core->fs;
		if ( ! $fs->exists( $this->path ) ) {
			return false;
		}

		$contents = $fs->getContents( $this->path );

		return aioseo()->helpers->encodeOutputHtml( $contents );
	}

	/**
	 * Saves the contents of the .htaccess file.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $contents The contents to write.
	 * @return boolean           True if the file was updated.
	 */
	public function saveContents( $contents ) {
		$fs = aioseo()->core->fs;
		if ( ! $fs->isWritable( $this->path ) ) {
			return [
				'success' => false,
				'reason'  => 'file-not-writable',
				'message' => __( 'We were unable to save the .htaccess file because the file was not writable. Please check the file permissions and try again.', 'all-in-one-seo-pack' )
			];
		}

		$fileExists       = $fs->exists( $this->path );
		$originalContents = $fileExists ? $fs->getContents( $this->path ) : null;
		$fileSaved        = $fs->putContents( $this->path, $contents );
		if ( false === $fileSaved ) {
			return [
				'success' => false,
				'reason'  => 'file-not-saved'
			];
		}

		$response       = wp_remote_get( home_url( '?' . time() ) );
		$isValidRequest = wp_remote_retrieve_response_code( $response );

		if (
			// Add an exception for Windows devs since the request fails in Local.
			! defined( 'AIOSEO_DEV_WINDOWS' ) &&
			( is_wp_error( $response ) || 200 !== $isValidRequest )
		) {
			$fs->putContents( $this->path, $originalContents );

			return [
				'success' => false,
				'reason'  => 'syntax-errors',
				'message' => __( 'We were unable to save the .htaccess file due to syntax errors. Please check the code below and try again.', 'all-in-one-seo-pack' )
			];
		}

		return [
			'success' => true
		];
	}
}Common/Api/Sitemaps.php000066600000011452151135505570011021 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

use AIOSEO\Plugin\Common\Models;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

			return is_object( $post );
		}

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

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

/**
 * Api class for the admin.
 *
 * @since 4.0.0
 */
class Api {
	/**
	 * The REST API Namespace
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	public $namespace = 'aioseo/v1';

	/**
	 * The routes we use in the rest API.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $routes = [
		// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
		'GET'    => [
			'options'                                     => [ 'callback' => [ 'Settings', 'getOptions' ], 'access' => 'everyone' ],
			'ping'                                        => [ 'callback' => [ 'Ping', 'ping' ], 'access' => 'everyone' ],
			'post'                                        => [ 'callback' => [ 'PostsTerms', 'getPostData' ], 'access' => 'everyone' ],
			'post/(?P<postId>[\d]+)/first-attached-image' => [ 'callback' => [ 'PostsTerms', 'getFirstAttachedImage' ], 'access' => 'aioseo_page_social_settings' ],
			'user/(?P<userId>[\d]+)/image'                => [ 'callback' => [ 'User', 'getUserImage' ], 'access' => 'aioseo_page_social_settings' ],
			'tags'                                        => [ 'callback' => [ 'Tags', 'getTags' ], 'access' => 'everyone' ],
			'search-statistics/url/auth'                  => [ 'callback' => [ 'SearchStatistics', 'getAuthUrl' ], 'access' => [ 'aioseo_search_statistics_settings', 'aioseo_general_settings', 'aioseo_setup_wizard' ] ], // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'search-statistics/url/reauth'                => [ 'callback' => [ 'SearchStatistics', 'getReauthUrl' ], 'access' => [ 'aioseo_search_statistics_settings', 'aioseo_general_settings' ] ],
			'writing-assistant/keyword/(?P<postId>[\d]+)' => [ 'callback' => [ 'WritingAssistant', 'getPostKeyword' ], 'access' => 'aioseo_page_writing_assistant_settings' ],
			'writing-assistant/user-info'                 => [ 'callback' => [ 'WritingAssistant', 'getUserInfo' ], 'access' => 'aioseo_page_writing_assistant_settings' ],
			'writing-assistant/user-options'              => [ 'callback' => [ 'WritingAssistant', 'getUserOptions' ], 'access' => 'aioseo_page_writing_assistant_settings' ],
			'writing-assistant/report-history'            => [ 'callback' => [ 'WritingAssistant', 'getReportHistory' ], 'access' => 'aioseo_page_writing_assistant_settings' ],
			'seo-analysis/homeresults'                    => [ 'callback' => [ 'Analyze', 'getHomeResults' ], 'access' => 'aioseo_seo_analysis_settings' ],
			'seo-analysis/competitors'                    => [ 'callback' => [ 'Analyze', 'getCompetitorsResults' ], 'access' => 'aioseo_seo_analysis_settings' ]
		],
		'POST'   => [
			'htaccess'                                              => [ 'callback' => [ 'Tools', 'saveHtaccess' ], 'access' => 'aioseo_tools_settings' ],
			'post'                                                  => [
				'callback' => [ 'PostsTerms', 'updatePosts' ],
				'access'   => [
					'aioseo_page_analysis',
					'aioseo_page_general_settings',
					'aioseo_page_advanced_settings',
					'aioseo_page_schema_settings',
					'aioseo_page_social_settings'
				]
			],
			'post/(?P<postId>[\d]+)/disable-primary-term-education' => [ 'callback' => [ 'PostsTerms', 'disablePrimaryTermEducation' ], 'access' => 'aioseo_page_general_settings' ],
			'post/(?P<postId>[\d]+)/disable-link-format-education'  => [ 'callback' => [ 'PostsTerms', 'disableLinkFormatEducation' ], 'access' => 'aioseo_page_general_settings' ],
			'post/(?P<postId>[\d]+)/update-internal-link-count'     => [ 'callback' => [ 'PostsTerms', 'updateInternalLinkCount' ], 'access' => 'aioseo_page_general_settings' ],
			'post/(?P<postId>[\d]+)/process-content'                => [ 'callback' => [ 'PostsTerms', 'processContent' ], 'access' => 'aioseo_page_general_settings' ],
			'posts-list/load-details-column'                        => [ 'callback' => [ 'PostsTerms', 'loadPostDetailsColumn' ], 'access' => 'aioseo_page_general_settings' ],
			'posts-list/update-details-column'                      => [ 'callback' => [ 'PostsTerms', 'updatePostDetailsColumn' ], 'access' => 'aioseo_page_general_settings' ],
			'terms-list/load-details-column'                        => [ 'callback' => [ 'PostsTerms', 'loadTermDetailsColumn' ], 'access' => 'aioseo_page_general_settings' ],
			'terms-list/update-details-column'                      => [ 'callback' => [ 'PostsTerms', 'updateTermDetailsColumn' ], 'access' => 'aioseo_page_general_settings' ],
			'keyphrases'                                            => [ 'callback' => [ 'PostsTerms', 'updatePostKeyphrases' ], 'access' => 'aioseo_page_analysis' ],
			'analyze'                                               => [ 'callback' => [ 'Analyze', 'analyzeSite' ], 'access' => 'aioseo_seo_analysis_settings' ],
			'analyze-headline'                                      => [ 'callback' => [ 'Analyze', 'analyzeHeadline' ], 'access' => 'everyone' ],
			'analyze-headline/delete'                               => [ 'callback' => [ 'Analyze', 'deleteHeadline' ], 'access' => 'aioseo_seo_analysis_settings' ],
			'analyze/delete-site'                                   => [ 'callback' => [ 'Analyze', 'deleteSite' ], 'access' => 'aioseo_seo_analysis_settings' ],
			'clear-log'                                             => [ 'callback' => [ 'Tools', 'clearLog' ], 'access' => 'aioseo_tools_settings' ],
			'connect'                                               => [ 'callback' => [ 'Connect', 'saveConnectToken' ], 'access' => [ 'aioseo_general_settings', 'aioseo_setup_wizard' ] ],
			'connect-pro'                                           => [ 'callback' => [ 'Connect', 'processConnect' ], 'access' => [ 'aioseo_general_settings', 'aioseo_setup_wizard' ] ],
			'connect-url'                                           => [ 'callback' => [ 'Connect', 'getConnectUrl' ], 'access' => [ 'aioseo_general_settings', 'aioseo_setup_wizard' ] ],
			'backup'                                                => [ 'callback' => [ 'Tools', 'createBackup' ], 'access' => 'aioseo_tools_settings' ],
			'backup/restore'                                        => [ 'callback' => [ 'Tools', 'restoreBackup' ], 'access' => 'aioseo_tools_settings' ],
			'email-debug-info'                                      => [ 'callback' => [ 'Tools', 'emailDebugInfo' ], 'access' => 'aioseo_tools_settings' ],
			'migration/fix-blank-formats'                           => [ 'callback' => [ 'Migration', 'fixBlankFormats' ], 'access' => 'any' ],
			'notification/blog-visibility-reminder'                 => [ 'callback' => [ 'Notifications', 'blogVisibilityReminder' ], 'access' => 'any' ],
			'notification/conflicting-plugins-reminder'             => [ 'callback' => [ 'Notifications', 'conflictingPluginsReminder' ], 'access' => 'any' ],
			'notification/description-format-reminder'              => [ 'callback' => [ 'Notifications', 'descriptionFormatReminder' ], 'access' => 'any' ],
			'notification/email-reports-enable'                     => [ 'callback' => [ 'EmailSummary', 'enableEmailReports' ], 'access' => 'any' ],
			'notification/install-addons-reminder'                  => [ 'callback' => [ 'Notifications', 'installAddonsReminder' ], 'access' => 'any' ],
			'notification/install-aioseo-image-seo-reminder'        => [ 'callback' => [ 'Notifications', 'installImageSeoReminder' ], 'access' => 'any' ],
			'notification/install-aioseo-local-business-reminder'   => [ 'callback' => [ 'Notifications', 'installLocalBusinessReminder' ], 'access' => 'any' ],
			'notification/install-aioseo-news-sitemap-reminder'     => [ 'callback' => [ 'Notifications', 'installNewsSitemapReminder' ], 'access' => 'any' ],
			'notification/install-aioseo-video-sitemap-reminder'    => [ 'callback' => [ 'Notifications', 'installVideoSitemapReminder' ], 'access' => 'any' ],
			'notification/install-mi-reminder'                      => [ 'callback' => [ 'Notifications', 'installMiReminder' ], 'access' => 'any' ],
			'notification/install-om-reminder'                      => [ 'callback' => [ 'Notifications', 'installOmReminder' ], 'access' => 'any' ],
			'notification/v3-migration-custom-field-reminder'       => [ 'callback' => [ 'Notifications', 'migrationCustomFieldReminder' ], 'access' => 'any' ],
			'notification/v3-migration-schema-number-reminder'      => [ 'callback' => [ 'Notifications', 'migrationSchemaNumberReminder' ], 'access' => 'any' ],
			'notifications/dismiss'                                 => [ 'callback' => [ 'Notifications', 'dismissNotifications' ], 'access' => 'any' ],
			'objects'                                               => [ 'callback' => [ 'PostsTerms', 'searchForObjects' ], 'access' => [ 'aioseo_search_appearance_settings', 'aioseo_sitemap_settings' ] ], // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'options'                                               => [ 'callback' => [ 'Settings', 'saveChanges' ], 'access' => 'any' ],
			'plugins/deactivate'                                    => [ 'callback' => [ 'Plugins', 'deactivatePlugins' ], 'access' => 'aioseo_feature_manager_settings' ],
			'plugins/install'                                       => [ 'callback' => [ 'Plugins', 'installPlugins' ], 'access' => [ 'install_plugins', 'aioseo_feature_manager_settings' ] ],
			'plugins/upgrade'                                       => [ 'callback' => [ 'Plugins', 'upgradePlugins' ], 'access' => [ 'update_plugins', 'aioseo_feature_manager_settings' ] ],
			'reset-settings'                                        => [ 'callback' => [ 'Settings', 'resetSettings' ], 'access' => 'aioseo_tools_settings' ],
			'search-statistics/sitemap/delete'                      => [ 'callback' => [ 'SearchStatistics', 'deleteSitemap' ], 'access' => [ 'aioseo_search_statistics_settings', 'aioseo_general_settings' ] ], // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'search-statistics/sitemap/ignore'                      => [ 'callback' => [ 'SearchStatistics', 'ignoreSitemap' ], 'access' => [ 'aioseo_search_statistics_settings', 'aioseo_general_settings' ] ], // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'settings/export'                                       => [ 'callback' => [ 'Settings', 'exportSettings' ], 'access' => 'aioseo_tools_settings' ],
			'settings/export-content'                               => [ 'callback' => [ 'Settings', 'exportContent' ], 'access' => 'aioseo_tools_settings' ],
			'settings/hide-setup-wizard'                            => [ 'callback' => [ 'Settings', 'hideSetupWizard' ], 'access' => 'any' ],
			'settings/hide-upgrade-bar'                             => [ 'callback' => [ 'Settings', 'hideUpgradeBar' ], 'access' => 'any' ],
			'settings/import'                                       => [ 'callback' => [ 'Settings', 'importSettings' ], 'access' => 'aioseo_tools_settings' ],
			'settings/import/(?P<siteId>[\d]+)'                     => [ 'callback' => [ 'Settings', 'importSettings' ], 'access' => 'aioseo_tools_settings' ],
			'settings/import-plugins'                               => [ 'callback' => [ 'Settings', 'importPlugins' ], 'access' => 'aioseo_tools_settings' ],
			'settings/toggle-card'                                  => [ 'callback' => [ 'Settings', 'toggleCard' ], 'access' => 'any' ],
			'settings/toggle-radio'                                 => [ 'callback' => [ 'Settings', 'toggleRadio' ], 'access' => 'any' ],
			'settings/dismiss-alert'                                => [ 'callback' => [ 'Settings', 'dismissAlert' ], 'access' => 'any' ],
			'settings/items-per-page'                               => [ 'callback' => [ 'Settings', 'changeItemsPerPage' ], 'access' => 'any' ],
			'settings/semrush-country'                              => [ 'callback' => [ 'Settings', 'changeSemrushCountry' ], 'access' => 'any' ],
			'settings/do-task'                                      => [ 'callback' => [ 'Settings', 'doTask' ], 'access' => 'aioseo_tools_settings' ],
			'sitemap/deactivate-conflicting-plugins'                => [ 'callback' => [ 'Sitemaps', 'deactivateConflictingPlugins' ], 'access' => 'any' ],
			'sitemap/delete-static-files'                           => [ 'callback' => [ 'Sitemaps', 'deleteStaticFiles' ], 'access' => 'aioseo_sitemap_settings' ],
			'sitemap/validate-html-sitemap-slug'                    => [ 'callback' => [ 'Sitemaps', 'validateHtmlSitemapSlug' ], 'access' => 'aioseo_sitemap_settings' ],
			'tools/delete-robots-txt'                               => [ 'callback' => [ 'Tools', 'deleteRobotsTxt' ], 'access' => 'aioseo_tools_settings' ],
			'tools/import-robots-txt'                               => [ 'callback' => [ 'Tools', 'importRobotsTxt' ], 'access' => 'aioseo_tools_settings' ],
			'wizard'                                                => [ 'callback' => [ 'Wizard', 'saveWizard' ], 'access' => 'aioseo_setup_wizard' ],
			'integration/semrush/authenticate'                      => [
				'callback' => [ 'Semrush', 'semrushAuthenticate', 'AIOSEO\\Plugin\\Common\\Api\\Integrations' ],
				'access'   => 'aioseo_page_analysis'
			],
			'integration/semrush/refresh'                           => [
				'callback' => [ 'Semrush', 'semrushRefresh', 'AIOSEO\\Plugin\\Common\\Api\\Integrations' ],
				'access'   => 'aioseo_page_analysis'
			],
			'integration/semrush/keyphrases'                        => [
				'callback' => [ 'Semrush', 'semrushGetKeyphrases', 'AIOSEO\\Plugin\\Common\\Api\\Integrations' ],
				'access'   => 'aioseo_page_analysis'
			],
			'integration/wpcode/snippets'                           => [
				'callback' => [ 'WpCode', 'getSnippets', 'AIOSEO\\Plugin\\Common\\Api\\Integrations' ],
				'access'   => 'aioseo_tools_settings'
			],
			'crawl-cleanup'                                         => [
				'callback' => [ 'CrawlCleanup', 'fetchLogs', 'AIOSEO\\Plugin\\Common\\QueryArgs' ],
				'access'   => 'aioseo_search_appearance_settings'
			],
			'crawl-cleanup/block'                                   => [
				'callback' => [ 'CrawlCleanup', 'blockArg', 'AIOSEO\\Plugin\\Common\\QueryArgs' ],
				'access'   => 'aioseo_search_appearance_settings'
			],
			'crawl-cleanup/delete-blocked'                          => [
				'callback' => [ 'CrawlCleanup', 'deleteBlocked', 'AIOSEO\\Plugin\\Common\\QueryArgs' ],
				'access'   => 'aioseo_search_appearance_settings'
			],
			'crawl-cleanup/delete-unblocked'                        => [
				'callback' => [ 'CrawlCleanup', 'deleteLog', 'AIOSEO\\Plugin\\Common\\QueryArgs' ],
				'access'   => 'aioseo_search_appearance_settings'
			],
			'email-summary/send'                                    => [
				'callback' => [ 'EmailSummary', 'send' ],
				'access'   => 'aioseo_page_advanced_settings'
			],
			'writing-assistant/process'                             => [
				'callback' => [ 'WritingAssistant', 'processKeyword' ],
				'access'   => 'aioseo_page_writing_assistant_settings'
			],
			'writing-assistant/content-analysis'                    => [
				'callback' => [ 'WritingAssistant', 'getContentAnalysis' ],
				'access'   => 'aioseo_page_writing_assistant_settings'
			],
			'writing-assistant/disconnect'                          => [
				'callback' => [ 'WritingAssistant', 'disconnect' ],
				'access'   => 'aioseo_page_writing_assistant_settings'
			],
			'writing-assistant/user-options'                        => [
				'callback' => [ 'WritingAssistant', 'saveUserOptions' ],
				'access'   => 'aioseo_page_writing_assistant_settings'
			],
			'writing-assistant/set-report-progress'                 => [
				'callback' => [ 'WritingAssistant', 'setReportProgress' ],
				'access'   => 'aioseo_page_writing_assistant_settings'
			]
		],
		'DELETE' => [
			'backup'                 => [ 'callback' => [ 'Tools', 'deleteBackup' ], 'access' => 'aioseo_tools_settings' ],
			'search-statistics/auth' => [ 'callback' => [ 'SearchStatistics', 'deleteAuth' ], 'access' => [ 'aioseo_search_statistics_settings', 'aioseo_general_settings' ] ]
		]
		// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
	];

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

	/**
	 * Get all the routes to register.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of routes.
	 */
	protected function getRoutes() {
		return $this->routes;
	}

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

	/**
	 * Sets headers that are allowed for our API routes.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function allowHeaders() {
		// TODO: Remove this entire function after a while. It's only here to ensure compatibility with people that are still using Image SEO 1.0.3 or lower.
		header( 'Access-Control-Allow-Headers: X-WP-Nonce' );
	}

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

		return $allowHeaders;
	}

	/**
	 * Determine if logged in or has the proper permissions.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request $request The REST Request.
	 * @return bool                      True if validated, false if not.
	 */
	public function validRequest( $request ) {
		return is_user_logged_in() && $this->validateAccess( $request );
	}

	/**
	 * Validates access from the routes array.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_REST_Request $request The REST Request.
	 * @return bool                      True if validated, false if not.
	 */
	public function validateAccess( $request ) {
		$routeData = $this->getRouteData( $request );
		if ( empty( $routeData ) || empty( $routeData['access'] ) ) {
			return false;
		}

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

		switch ( $routeData['access'] ) {
			case 'everyone':
				// Any user is able to access the route.
				return true;
			default:
				return aioseo()->access->hasCapability( $routeData['access'] );
		}
	}

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

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

		return $routeData;
	}
}Common/Api/Migration.php000066600000003575151135505570011174 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

					break;
			}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		$url = $urlData['url'];

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

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

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

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

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

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

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

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

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

use AIOSEO\Plugin\Common\Models;

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

		aioseo()->internalOptions->internal->wizard = wp_json_encode( $wizard );

		// Process the importers.
		if ( 'importers' === $section && ! empty( $wizard['importers'] ) ) {
			$importers = $wizard['importers'];

			try {
				foreach ( $importers as $plugin ) {
					aioseo()->importExport->startImport( $plugin, [
						'settings',
						'postMeta',
						'termMeta'
					] );
				}
			} catch ( \Exception $e ) {
				// Import failed. Let's create a notification but move on.
				$notification = Models\Notification::getNotificationByName( 'import-failed' );
				if ( ! $notification->exists() ) {
					Models\Notification::addNotification( [
						'slug'              => uniqid(),
						'notification_name' => 'import-failed',
						'title'             => __( 'SEO Plugin Import Failed', 'all-in-one-seo-pack' ),
						'content'           => __( 'Unfortunately, there was an error importing your SEO plugin settings. This could be due to an incompatibility in the version installed. Make sure you are on the latest version of the plugin and try again.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'type'              => 'error',
						'level'             => [ 'all' ],
						'button1_label'     => __( 'Try Again', 'all-in-one-seo-pack' ),
						'button1_action'    => 'http://route#aioseo-tools&aioseo-scroll=aioseo-import-others&aioseo-highlight=aioseo-import-others:import-export',
						'start'             => gmdate( 'Y-m-d H:i:s' )
					] );
				}
			}
		}

		// Save the category section.
		if (
			( 'category' === $section || 'searchAppearance' === $section ) && // We allow the user to update the site title/description in search appearance.
			! empty( $wizard['category'] )
		) {
			$category = $wizard['category'];
			if ( ! empty( $category['category'] ) ) {
				aioseo()->internalOptions->internal->category = $category['category'];
			}

			if ( ! empty( $category['categoryOther'] ) ) {
				aioseo()->internalOptions->internal->categoryOther = $category['categoryOther'];
			}

			// If the home page is a static page, let's find and set that,
			// otherwise set our home page settings.
			$staticHomePage = 'page' === get_option( 'show_on_front' ) ? get_post( get_option( 'page_on_front' ) ) : null;
			if ( ! empty( $staticHomePage ) ) {
				$update = false;
				$page   = Models\Post::getPost( $staticHomePage->ID );
				if ( ! empty( $category['siteTitle'] ) ) {
					$update      = true;
					$page->title = $category['siteTitle'];
				}

				if ( ! empty( $category['metaDescription'] ) ) {
					$update            = true;
					$page->description = $category['metaDescription'];
				}

				if ( $update ) {
					$page->save();
				}
			}

			if ( empty( $staticHomePage ) ) {
				if ( ! empty( $category['siteTitle'] ) ) {
					$options->searchAppearance->global->siteTitle = $category['siteTitle'];
				}

				if ( ! empty( $category['metaDescription'] ) ) {
					$options->searchAppearance->global->metaDescription = $category['metaDescription'];
				}
			}
		}

		// Save the additional information section.
		if ( 'additionalInformation' === $section && ! empty( $wizard['additionalInformation'] ) ) {
			$additionalInformation = $wizard['additionalInformation'];
			if ( ! empty( $additionalInformation['siteRepresents'] ) ) {
				$options->searchAppearance->global->schema->siteRepresents = $additionalInformation['siteRepresents'];
			}

			if ( ! empty( $additionalInformation['person'] ) ) {
				$options->searchAppearance->global->schema->person = $additionalInformation['person'];
			}

			if ( ! empty( $additionalInformation['organizationName'] ) ) {
				$options->searchAppearance->global->schema->organizationName = $additionalInformation['organizationName'];
			}

			if ( ! empty( $additionalInformation['organizationDescription'] ) ) {
				$options->searchAppearance->global->schema->organizationDescription = $additionalInformation['organizationDescription'];
			}

			if ( ! empty( $additionalInformation['phone'] ) ) {
				$options->searchAppearance->global->schema->phone = $additionalInformation['phone'];
			}

			if ( ! empty( $additionalInformation['organizationLogo'] ) ) {
				$options->searchAppearance->global->schema->organizationLogo = $additionalInformation['organizationLogo'];
			}

			if ( ! empty( $additionalInformation['personName'] ) ) {
				$options->searchAppearance->global->schema->personName = $additionalInformation['personName'];
			}

			if ( ! empty( $additionalInformation['personLogo'] ) ) {
				$options->searchAppearance->global->schema->personLogo = $additionalInformation['personLogo'];
			}

			if ( ! empty( $additionalInformation['socialShareImage'] ) ) {
				$options->social->facebook->general->defaultImagePosts = $additionalInformation['socialShareImage'];
				$options->social->twitter->general->defaultImagePosts  = $additionalInformation['socialShareImage'];
			}

			if ( ! empty( $additionalInformation['social'] ) && ! empty( $additionalInformation['social']['profiles'] ) ) {
				$profiles = $additionalInformation['social']['profiles'];
				if ( ! empty( $profiles['sameUsername'] ) ) {
					$sameUsername = $profiles['sameUsername'];
					if ( isset( $sameUsername['enable'] ) ) {
						$options->social->profiles->sameUsername->enable = $sameUsername['enable'];
					}

					if ( ! empty( $sameUsername['username'] ) ) {
						$options->social->profiles->sameUsername->username = $sameUsername['username'];
					}

					if ( ! empty( $sameUsername['included'] ) ) {
						$options->social->profiles->sameUsername->included = $sameUsername['included'];
					}
				}

				if ( ! empty( $profiles['urls'] ) ) {
					$urls = $profiles['urls'];
					if ( ! empty( $urls['facebookPageUrl'] ) ) {
						$options->social->profiles->urls->facebookPageUrl = $urls['facebookPageUrl'];
					}

					if ( ! empty( $urls['twitterUrl'] ) ) {
						$options->social->profiles->urls->twitterUrl = $urls['twitterUrl'];
					}

					if ( ! empty( $urls['instagramUrl'] ) ) {
						$options->social->profiles->urls->instagramUrl = $urls['instagramUrl'];
					}

					if ( ! empty( $urls['tiktokUrl'] ) ) {
						$options->social->profiles->urls->tiktokUrl = $urls['tiktokUrl'];
					}

					if ( ! empty( $urls['pinterestUrl'] ) ) {
						$options->social->profiles->urls->pinterestUrl = $urls['pinterestUrl'];
					}

					if ( ! empty( $urls['youtubeUrl'] ) ) {
						$options->social->profiles->urls->youtubeUrl = $urls['youtubeUrl'];
					}

					if ( ! empty( $urls['linkedinUrl'] ) ) {
						$options->social->profiles->urls->linkedinUrl = $urls['linkedinUrl'];
					}

					if ( ! empty( $urls['tumblrUrl'] ) ) {
						$options->social->profiles->urls->tumblrUrl = $urls['tumblrUrl'];
					}

					if ( ! empty( $urls['yelpPageUrl'] ) ) {
						$options->social->profiles->urls->yelpPageUrl = $urls['yelpPageUrl'];
					}

					if ( ! empty( $urls['soundCloudUrl'] ) ) {
						$options->social->profiles->urls->soundCloudUrl = $urls['soundCloudUrl'];
					}

					if ( ! empty( $urls['wikipediaUrl'] ) ) {
						$options->social->profiles->urls->wikipediaUrl = $urls['wikipediaUrl'];
					}

					if ( ! empty( $urls['myspaceUrl'] ) ) {
						$options->social->profiles->urls->myspaceUrl = $urls['myspaceUrl'];
					}

					if ( ! empty( $urls['googlePlacesUrl'] ) ) {
						$options->social->profiles->urls->googlePlacesUrl = $urls['googlePlacesUrl'];
					}

					if ( ! empty( $urls['wordPressUrl'] ) ) {
						$options->social->profiles->urls->wordPressUrl = $urls['wordPressUrl'];
					}

					if ( ! empty( $urls['blueskyUrl'] ) ) {
						$options->social->profiles->urls->blueskyUrl = $urls['blueskyUrl'];
					}

					if ( ! empty( $urls['threadsUrl'] ) ) {
						$options->social->profiles->urls->threadsUrl = $urls['threadsUrl'];
					}
				}
			}

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

		// Save the features section.
		if ( 'features' === $section && ! empty( $wizard['features'] ) ) {
			self::installPlugins( $wizard['features'], $network );

			if ( in_array( 'email-reports', $wizard['features'], true ) ) {
				$options->advanced->emailSummary->enable = true;
			}
		}

		// Save the search appearance section.
		if ( 'searchAppearance' === $section && ! empty( $wizard['searchAppearance'] ) ) {
			$searchAppearance = $wizard['searchAppearance'];

			if ( isset( $searchAppearance['underConstruction'] ) ) {
				update_option( 'blog_public', ! $searchAppearance['underConstruction'] );
			}

			if (
				! empty( $searchAppearance['postTypes'] ) &&
				! empty( $searchAppearance['postTypes']['postTypes'] )
			) {
				// Robots.
				if ( ! empty( $searchAppearance['postTypes']['postTypes']['all'] ) ) {
					foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
						if ( $dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
							$dynamicOptions->searchAppearance->postTypes->$postType->show                          = true;
							$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = true;
							$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->noindex = false;
						}
					}
				} else {
					foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
						if ( $dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
							if ( in_array( $postType, (array) $searchAppearance['postTypes']['postTypes']['included'], true ) ) {
								$dynamicOptions->searchAppearance->postTypes->$postType->show                          = true;
								$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = true;
								$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->noindex = false;
							} else {
								$dynamicOptions->searchAppearance->postTypes->$postType->show                          = false;
								$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = false;
								$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->noindex = true;
							}
						}
					}
				}

				// Sitemaps.
				if ( isset( $searchAppearance['postTypes']['postTypes']['all'] ) ) {
					$options->sitemap->general->postTypes->all = $searchAppearance['postTypes']['postTypes']['all'];
				}

				if ( isset( $searchAppearance['postTypes']['postTypes']['included'] ) ) {
					$options->sitemap->general->postTypes->included = $searchAppearance['postTypes']['postTypes']['included'];
				}
			}

			if ( isset( $searchAppearance['multipleAuthors'] ) ) {
				$options->searchAppearance->archives->author->show                          = $searchAppearance['multipleAuthors'];
				$options->searchAppearance->archives->author->advanced->robotsMeta->default = $searchAppearance['multipleAuthors'];
				$options->searchAppearance->archives->author->advanced->robotsMeta->noindex = ! $searchAppearance['multipleAuthors'];
			}

			if ( isset( $searchAppearance['redirectAttachmentPages'] ) && $dynamicOptions->searchAppearance->postTypes->has( 'attachment' ) ) {
				$dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = $searchAppearance['redirectAttachmentPages'] ? 'attachment' : 'disabled';
			}

			if ( isset( $searchAppearance['emailReports'] ) ) {
				$options->advanced->emailSummary->enable = $searchAppearance['emailReports'];
			}
		}

		// Save the smart recommendations section.
		if ( 'smartRecommendations' === $section && ! empty( $wizard['smartRecommendations'] ) ) {
			$smartRecommendations = $wizard['smartRecommendations'];
			if ( ! empty( $smartRecommendations['accountInfo'] ) && ! aioseo()->internalOptions->internal->siteAnalysis->connectToken ) {
				$url      = defined( 'AIOSEO_CONNECT_DIRECT_URL' ) ? AIOSEO_CONNECT_DIRECT_URL : 'https://aioseo.com/wp-json/aioseo-lite-connect/v1/connect/';
				$response = wp_remote_post( $url, [
					'timeout'    => 10,
					'headers'    => array_merge( [
						'Content-Type' => 'application/json'
					], aioseo()->helpers->getApiHeaders() ),
					'user-agent' => aioseo()->helpers->getApiUserAgent(),
					'body'       => wp_json_encode( [
						'accountInfo' => $smartRecommendations['accountInfo'],
						'homeurl'     => home_url()
					] )
				] );

				$token = json_decode( wp_remote_retrieve_body( $response ) );
				if ( ! empty( $token->token ) ) {
					aioseo()->internalOptions->internal->siteAnalysis->connectToken = $token->token;
				}
			}
		}

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

	/**
	 * Install all plugins that were selected in the features page of the Setup Wizard.
	 *
	 * @since 4.5.5
	 *
	 * @param  array $features The features that were selected.
	 * @param  bool  $network  Whether to install the plugins on the network.
	 * @return void
	 */
	private static function installPlugins( $features, $network ) {
		$pluginData = aioseo()->helpers->getPluginData();

		if ( in_array( 'analytics', $features, true ) ) {
			self::installMonsterInsights( $network );
		}

		if ( in_array( 'conversion-tools', $features, true ) && ! $pluginData['optinMonster']['activated'] ) {
			self::installOptinMonster( $network );
		}

		if ( in_array( 'broken-link-checker', $features, true ) && ! $pluginData['brokenLinkChecker']['activated'] ) {
			self::installBlc( $network );
		}
	}

	/**
	 * Installs the MonsterInsights plugin.
	 *
	 * @since 4.5.5
	 *
	 * @param  bool $network Whether to install the plugin on the network.
	 * @return void
	 */
	private static function installMonsterInsights( $network ) {
		$pluginData = aioseo()->helpers->getPluginData();

		$args = [
			'id'                => 'miLite',
			'pluginName'        => 'MonsterInsights',
			'pluginLongName'    => 'MonsterInsights Analytics',
			'notification-name' => 'install-mi'
		];

		// If MI Pro is active, bail.
		if ( $pluginData['miPro']['activated'] ) {
			return;
		}

		// If MI Pro is installed but not active, activate MI Pro.
		if ( $pluginData['miPro']['installed'] ) {
			$args['id'] = 'miPro';
		}

		if ( self::installPlugin( $args, $network ) ) {
			delete_transient( '_monsterinsights_activation_redirect' );
		}
	}

	/**
	 * Installs the OptinMonster plugin.
	 *
	 * @since 4.5.5
	 *
	 * @param  bool $network Whether to install the plugin on the network.
	 * @return void
	 */
	private static function installOptinMonster( $network ) {
		$args = [
			'id'                => 'optinMonster',
			'pluginName'        => 'OptinMonster',
			'pluginLongName'    => 'OptinMonster Conversion Tools',
			'notification-name' => 'install-om'
		];

		if ( self::installPlugin( $args, $network ) ) {
			delete_transient( 'optin_monster_api_activation_redirect' );
		}
	}

	/**
	 * Installs the Broken Link Checker plugin.
	 *
	 * @since 4.5.5
	 *
	 * @param  bool $network Whether to install the plugin on the network.
	 * @return void
	 */
	private static function installBlc( $network ) {
		$args = [
			'id'                => 'brokenLinkChecker',
			'pluginName'        => 'Broken Link Checker',
			'notification-name' => 'install-blc'
		];

		if ( self::installPlugin( $args, $network ) && function_exists( 'aioseoBrokenLinkChecker' ) ) {
			aioseoBrokenLinkChecker()->core->cache->delete( 'activation_redirect' );
		}
	}

	/**
	 * Helper method to install plugins through the Setup Wizard.
	 * Creates a notification if the plugin can't be installed.
	 *
	 * @since 4.5.5
	 *
	 * @param  array $args    The plugin arguments.
	 * @param  bool  $network Whether to install the plugin on the network.
	 * @return bool           Whether the plugin was installed.
	 */
	private static function installPlugin( $args, $network = false ) {
		if ( aioseo()->addons->canInstall() ) {
			return aioseo()->addons->installAddon( $args['id'], $network );
		}

		$pluginData = aioseo()->helpers->getPluginData();

		$notification = Models\Notification::getNotificationByName( $args['notification-name'] );
		if ( ! $notification->exists() ) {
			Models\Notification::addNotification( [
				'slug'              => uniqid(),
				'notification_name' => $args['notification-name'],
				'title'             => sprintf(
					// Translators: 1 - A plugin name (e.g. "MonsterInsights", "Broken Link Checker", etc.).
					__( 'Install %1$s', 'all-in-one-seo-pack' ),
					$args['pluginName']
				),
				'content'           => sprintf(
					// Translators: 1 - A plugin name (e.g. "MonsterInsights", "Broken Link Checker", etc.), 2 - The plugin short name ("AIOSEO").
					__( 'You selected to install the free %1$s plugin during the setup of %2$s, but there was an issue during installation. Click below to manually install.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
					AIOSEO_PLUGIN_SHORT_NAME,
					! empty( $args['pluginLongName'] ) ? $args['pluginLongName'] : $args['pluginName']
				),
				'type'              => 'info',
				'level'             => [ 'all' ],
				'button1_label'     => sprintf(
					// Translators: 1 - A plugin name (e.g. "MonsterInsights", "Broken Link Checker", etc.).
					__( 'Install %1$s', 'all-in-one-seo-pack' ),
					$args['pluginName']
				),
				'button1_action'    => $pluginData[ $args['id'] ]['wpLink'],
				'button2_label'     => __( 'Remind Me Later', 'all-in-one-seo-pack' ),
				'button2_action'    => "http://action#notification/{$args['notification-name']}-reminder",
				'start'             => gmdate( 'Y-m-d H:i:s' )
			] );
		}

		return false;
	}
}Common/Api/Network.php000066600000002547151135505570010672 0ustar00<?php
namespace AIOSEO\Plugin\Common\Api;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

namespace AIOSEO\Plugin\Common\Api;

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

use AIOSEO\Plugin\Common\Models;

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

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

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

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

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

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

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

use AIOSEO\Plugin\Common\SearchStatistics\Api;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

use AIOSEO\Plugin\Common\Models;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

			$keyword->save();
		}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		if ( ! $type ) {
			return false;
		}

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

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

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

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

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

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

		return $newContent;
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		return $posts;
	}

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

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

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

		return $csvString;
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

use AIOSEO\Plugin\Common\Models;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

use AIOSEO\Plugin\Common\Models\SeoAnalyzerResult;

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

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

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

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

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

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

			$result = SeoAnalyzerResult::getCompetitorsResults();

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

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

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

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

		SeoAnalyzerResult::deleteByUrl( $analyzeUrl );

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

		unset( $competitors[ $analyzeUrl ] );

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

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

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

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

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

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

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

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

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

		$headlines = array_reverse( $headlines, true );

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

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

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

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

		unset( $headlines[ $headline ] );

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

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

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

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

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

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

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

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

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

use AIOSEO\Plugin\Common\Models;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		return new \WP_REST_Response( [
			'success' => true,
			'content' => apply_filters( 'the_content', $content ),
		], 200 );
	}
}Common/SearchCleanup/SearchCleanup.php000066600000010313151135505570013750 0ustar00<?php
namespace AIOSEO\Plugin\Common\SearchCleanup;

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

/**
 * Class for Search Cleanup that handles prevention of search spams.
 *
 * @since 4.8.0
 */
class SearchCleanup {
	/**
	 * Patterns to match against to find spam.
	 *
	 * @since 4.8.0
	 *
	 * @var array
	 */
	private $patterns = [
		'/[:()【】[]]+/u',
		'/(TALK|QQ)\:/iu',
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.8.0
	 */
	public function __construct() {
		// If Crawl Cleanup is disabled, return early.
		if ( ! aioseo()->options->searchAppearance->advanced->crawlCleanup->enable ) {
			return;
		}

		if ( aioseo()->options->searchAppearance->advanced->searchCleanup->enable ) {
			add_filter( 'pre_get_posts', [ $this, 'validateSearch' ] );
		}

		if ( aioseo()->options->searchAppearance->advanced->searchCleanup->settings->redirectPrettyUrls ) {
			add_action( 'template_redirect', [ $this, 'maybeRedirectSearches' ], 0 );
		}
	}

	/**
	 * Check against unwanted patterns.
	 *
	 * @since 4.8.0
	 *
	 * @param  \WP_Query $query The main query.
	 * @return \WP_Query        The main query.
	 */
	public function validateSearch( $query ) {
		if ( ! $query->is_search() ) {
			return $query;
		}

		$searchString = rawurldecode( $query->get( 's' ) );

		$this->checkEmojis( $searchString );
		$this->checkCommonSpamPatterns( $searchString );
		$this->limitCharacters();

		return $query;
	}

	/**
	 * Limits the number of characters in the search term.
	 *
	 * @since 4.8.0
	 *
	 * @return void
	 */
	private function limitCharacters() {
		// We retrieve the search term unescaped as we want to count the characters. We make sure to escape it afterwards before we continue tom process it.
		$unescapedTerm = get_search_query( false );

		$maxAllowedNumberOfChars = aioseo()->options->searchAppearance->advanced->searchCleanup->settings->maxAllowedNumberOfChars;

		$rawSearchTerm = wp_unslash( $unescapedTerm );
		if ( mb_strlen( $rawSearchTerm, 'UTF-8' ) > $maxAllowedNumberOfChars ) {
			$newS = mb_substr( $rawSearchTerm, 0, $maxAllowedNumberOfChars, 'UTF-8' );
			set_query_var( 's', wp_slash( esc_attr( $newS ) ) );
		}
	}

	/**
	 * Check if query contains emojis and special characters.
	 *
	 * @since 4.8.0
	 *
	 * @param  string $searchString The search string.
	 * @return void
	 */
	private function checkEmojis( $searchString ) {
		if ( ! aioseo()->options->searchAppearance->advanced->searchCleanup->settings->emojisAndSymbols ) {
			return;
		}

		if ( aioseo()->helpers->hasEmojis( $searchString ) ) {
			aioseo()->helpers->notFoundPage();
		}
	}

	/**
	 * Checks against common search spam patterns.
	 *
	 * @since 4.8.0
	 *
	 * @param  string $searchString Search string.
	 * @return void
	 */
	private function checkCommonSpamPatterns( $searchString ) {
		if ( ! aioseo()->options->searchAppearance->advanced->searchCleanup->settings->commonPatterns ) {
			return;
		}

		$patterns = apply_filters( 'aioseo_search_cleanup_patterns', $this->patterns );
		foreach ( $patterns as $pattern ) {
			if ( preg_match( $pattern, $searchString ) ) {
				aioseo()->helpers->notFoundPage();
			}
		}
	}

	/**
	 * Redirect pretty search URLs to the "raw" equivalent
	 *
	 * @since 4.8.0
	 *
	 * @return void
	 */
	public function maybeRedirectSearches() {
		if ( ! is_search() ) {
			return;
		}

		$requestUri = aioseo()->helpers->getRequestUrl();
		if ( stripos( $requestUri, '/search/' ) === 0 ) {
			$args = [];

			$parsed = wp_parse_url( $requestUri );
			if ( ! empty( $parsed['query'] ) ) {
				wp_parse_str( $parsed['query'], $args );
			}

			// Extract the search query directly from the REQUEST_URI.
			$searchPath = trim( str_replace( '/search/', '', $parsed['path'] ), '/' );
			$args['s']  = aioseo()->helpers->decodeUrl( $searchPath );
			$properUrl  = home_url( '/' );

			if ( intval( get_query_var( 'paged' ) ) > 1 ) {
				$properUrl .= sprintf( 'page/%s/', \get_query_var( 'paged' ) );
				unset( $args['paged'] );
			}

			$properUrl = add_query_arg( array_map( 'rawurlencode_deep', $args ), $properUrl );

			if ( ! empty( $parsed['fragment'] ) ) {
				$properUrl .= '#' . rawurlencode( $parsed['fragment'] );
			}

			aioseo()->helpers->redirect( $properUrl, 301, 'We redirect pretty URLs to the raw format.' );
		}
	}
}Common/Rss.php000066600000034711151135505570007275 0ustar00<?php
namespace AIOSEO\Plugin\Common;

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

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Adds content before or after posts in the RSS feed.
 *
 * @since 4.0.0
 */
class Rss {
	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		if ( is_admin() ) {
			return;
		}

		add_filter( 'the_content_feed', [ $this, 'addRssContent' ] );
		add_filter( 'the_excerpt_rss', [ $this, 'addRssContentExcerpt' ] );

		// If Crawl Cleanup is disabled, return early.
		if ( ! aioseo()->options->searchAppearance->advanced->crawlCleanup->enable ) {
			return;
		}

		// Control which feed links are visible.
		remove_action( 'wp_head', 'feed_links_extra', 3 );
		add_action( 'wp_head', [ $this, 'rssFeedLinks' ], 3 );

		if ( ! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->global ) {
			add_filter( 'feed_links_show_posts_feed', '__return_false' );
		}

		if ( ! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->globalComments ) {
			add_filter( 'feed_links_show_comments_feed', '__return_false' );
		}

		// Disable feeds that we no longer want on this site.
		add_action( 'wp', [ $this, 'disableFeeds' ], -1000 );
	}

	/**
	 * Adds content before or after the RSS excerpt.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $content The post excerpt.
	 * @return string          The post excerpt with prepended/appended content.
	 */
	public function addRssContentExcerpt( $content ) {
		return $this->addRssContent( $content, 'excerpt' );
	}

	/**
	 * Adds content before or after the RSS post.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $content The post content.
	 * @param  string $type    Type of feed.
	 * @return string          The post content with prepended/appended content.
	 */
	public function addRssContent( $content, $type = 'complete' ) {
		$content = trim( $content );
		if ( empty( $content ) ) {
			return '';
		}

		if ( is_feed() ) {
			global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			$isHome = is_home();
			if ( $isHome ) {
				// If this feed is for the static blog page, we must temporarily set "is_home" to false.
				// Otherwise any getPost() calls will return the blog page object for every post in the feed.
				$wp_query->is_home = false; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			}

			$before = aioseo()->tags->replaceTags( aioseo()->options->rssContent->before, get_the_ID() );
			$after  = aioseo()->tags->replaceTags( aioseo()->options->rssContent->after, get_the_ID() );

			if ( $before || $after ) {
				if ( 'excerpt' === $type ) {
					$content = wpautop( $content );
				}
				$content = aioseo()->helpers->decodeHtmlEntities( $before ) . $content . aioseo()->helpers->decodeHtmlEntities( $after );
			}

			// Set back to the original value.
			$wp_query->is_home = $isHome; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		}

		return $content;
	}

	/**
	 * Disable feeds we don't want to have on this site.
	 *
	 * @since 4.2.1
	 *
	 * @return void
	 */
	public function disableFeeds() {
		$archives = aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->archives->included;

		if ( BuddyPressIntegration::isComponentPage() ) {
			list( $postType, $suffix ) = explode( '_', aioseo()->standalone->buddyPress->component->templateType );

			if (
				'feed' === $suffix &&
				! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->archives->all &&
				! in_array( $postType, $archives, true )
			) {
				$this->redirectRssFeed( BuddyPressIntegration::getComponentArchiveUrl( 'activity' ) );
			}
		}

		if ( ! is_feed() ) {
			return;
		}

		$rssFeed = get_query_var( 'feed' );
		$homeUrl = get_home_url();

		// Atom feed.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->atom &&
			'atom' === $rssFeed
		) {
			$this->redirectRssFeed( $homeUrl );
		}

		// RDF/RSS 1.0 feed.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->rdf &&
			'rdf' === $rssFeed
		) {
			$this->redirectRssFeed( $homeUrl );
		}

		// Global feed.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->global &&
			[ 'feed' => 'feed' ] === $GLOBALS['wp_query']->query
		) {
			$this->redirectRssFeed( $homeUrl );
		}

		// Global comments feed.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->globalComments &&
			is_comment_feed() &&
			! ( is_singular() || is_attachment() )
		) {
			$this->redirectRssFeed( $homeUrl );
		}

		// Static blog page feed.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->staticBlogPage &&
			aioseo()->helpers->getBlogPageId() === get_queried_object_id()
		) {
			$this->redirectRssFeed( $homeUrl );
		}

		// Post comment feeds.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->postComments &&
			is_comment_feed() &&
			is_singular()
		) {
			$this->redirectRssFeed( $homeUrl );
		}

		// Attachment feeds.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->attachments &&
			'feed' === $rssFeed &&
			get_query_var( 'attachment', false )
		) {
			$this->redirectRssFeed( $homeUrl );
		}

		// Author feeds.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->authors &&
			is_author()
		) {
			$this->redirectRssFeed( get_author_posts_url( (int) get_query_var( 'author' ) ) );
		}

		// Search results feed.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->search &&
			is_search()
		) {
			$this->redirectRssFeed( esc_url( trailingslashit( $homeUrl ) . '?s=' . get_search_query() ) );
		}

		// All post types.
		$postType = $this->getTheQueriedPostType();
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->archives->all &&
			! in_array( $postType, $archives, true ) &&
			is_post_type_archive()
		) {
			$this->redirectRssFeed( get_post_type_archive_link( $postType ) );
		}

		// All taxonomies.
		$taxonomies = aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->taxonomies->included;
		$term       = get_queried_object();
		if (
			is_a( $term, 'WP_Term' ) &&
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->taxonomies->all &&
			! in_array( $term->taxonomy, $taxonomies, true ) &&
			(
				is_category() ||
				is_tag() ||
				is_tax()
			)
		) {
			$termUrl = get_term_link( $term, $term->taxonomy );
			if ( is_wp_error( $termUrl ) ) {
				$termUrl = $homeUrl;
			}

			$this->redirectRssFeed( $termUrl );
		}

		if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
			return;
		}

		// Paginated feed pages. This one is last since we are using a regular expression to validate.
		if (
			! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->paginated &&
			preg_match( '/(\d+\/|(?<=\/)page\/\d+\/)$/', (string) sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) )
		) {
			$this->redirectRssFeed( $homeUrl );
		}
	}

	/**
	 * Get the currently queried post type.
	 *
	 * @since 4.2.1
	 *
	 * @return string The queried post type.
	 */
	private function getTheQueriedPostType() {
		$postType = get_query_var( 'post_type' );
		if ( is_array( $postType ) ) {
			$postType = reset( $postType );
		}

		return $postType;
	}

	/**
	 * Redirect the feed to the appropriate URL.
	 *
	 * @since 4.2.1
	 *
	 * @return void
	 */
	private function redirectRssFeed( $url ) {
		if ( empty( $url ) ) {
			return;
		}

		// Set or remove headers.
		header_remove( 'Content-Type' );
		header_remove( 'Last-Modified' );
		header_remove( 'Expires' );

		$cache = 'public, max-age=604800, s-maxage=604800, stale-while-revalidate=120, stale-if-error=14400';
		if ( is_user_logged_in() ) {
			$cache = 'private, max-age=0';
		}

		header( 'Cache-Control: ' . $cache, true );

		wp_safe_redirect( $url, 301, AIOSEO_PLUGIN_SHORT_NAME );
	}

	/**
	 * Rewrite the RSS feed links.
	 *
	 * @since 4.2.1
	 *
	 * @param  array $args The arguments to filter.
	 * @return void
	 */
	public function rssFeedLinks( $args ) {
		$defaults = [
			// Translators: Separator between blog name and feed type in feed links.
			'separator'     => _x( '-', 'feed link', 'all-in-one-seo-pack' ),
			// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Post title.
			'singletitle'   => __( '%1$s %2$s %3$s Comments Feed', 'all-in-one-seo-pack' ),
			// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Category name.
			'cattitle'      => __( '%1$s %2$s %3$s Category Feed', 'all-in-one-seo-pack' ),
			// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Tag name.
			'tagtitle'      => __( '%1$s %2$s %3$s Tag Feed', 'all-in-one-seo-pack' ),
			// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Term name, 4: Taxonomy singular name.
			'taxtitle'      => __( '%1$s %2$s %3$s %4$s Feed', 'all-in-one-seo-pack' ),
			// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Author name.
			'authortitle'   => __( '%1$s %2$s Posts by %3$s Feed', 'all-in-one-seo-pack' ),
			// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Search query.
			'searchtitle'   => __( '%1$s %2$s Search Results for &#8220;%3$s&#8221; Feed', 'all-in-one-seo-pack' ),
			// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Post type name.
			'posttypetitle' => __( '%1$s %2$s %3$s Feed', 'all-in-one-seo-pack' ),
		];

		$args       = wp_parse_args( $args, $defaults );
		$attributes = [
			'title' => null,
			'href'  => null
		];

		if (
			aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->postComments &&
			is_singular()
		) {
			$attributes = $this->getPostCommentsAttributes( $args );
		}

		$archives = aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->archives->included;
		$postType = $this->getTheQueriedPostType();
		if (
			(
				aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->archives->all ||
				in_array( $postType, $archives, true )
			) &&
			is_post_type_archive()
		) {
			$attributes = $this->getPostTypeArchivesAttributes( $args );
		}

		// All taxonomies.
		$taxonomies = aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->taxonomies->included;
		$term       = get_queried_object();
		if (
			$term &&
			isset( $term->taxonomy ) &&
			(
				aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->taxonomies->all ||
				in_array( $term->taxonomy, $taxonomies, true )
			) &&
			(
				is_category() ||
				is_tag() ||
				is_tax()
			)
		) {
			$attributes = $this->getTaxonomiesAttributes( $args, $term );
		}

		if (
			aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->authors &&
			is_author()
		) {
			$attributes = $this->getAuthorAttributes( $args );
		}

		if (
			aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->search &&
			is_search()
		) {
			$attributes = $this->getSearchAttributes( $args );
		}

		if ( ! empty( $attributes['title'] ) && ! empty( $attributes['href'] ) ) {
			echo '<link rel="alternate" type="application/rss+xml" title="' . esc_attr( $attributes['title'] ) . '" href="' . esc_url( $attributes['href'] ) . '" />' . "\n";
		}
	}

	/**
	 * Retrieve the attributes for post comments feed.
	 *
	 * @since 4.2.1
	 *
	 * @param  array $args An array of arguments.
	 * @return array       An array of attributes.
	 */
	private function getPostCommentsAttributes( $args ) {
		$id    = 0;
		$post  = get_post( $id );
		$title = null;
		$href  = null;

		if (
			comments_open() ||
			pings_open() ||
			0 < $post->comment_count
		) {
			$title = sprintf(
				$args['singletitle'],
				get_bloginfo( 'name' ),
				$args['separator'],
				the_title_attribute( [ 'echo' => false ] )
			);

			$href = get_post_comments_feed_link( $post->ID );
		}

		return [
			'title' => $title,
			'href'  => $href
		];
	}

	/**
	 * Retrieve the attributes for post type archives feed.
	 *
	 * @since 4.2.1
	 *
	 * @param  array $args An array of arguments.
	 * @return array       An array of attributes.
	 */
	private function getPostTypeArchivesAttributes( $args ) {
		$postTypeObject = get_post_type_object( $this->getQueriedPostType() );
		$title          = sprintf( $args['posttypetitle'], get_bloginfo( 'name' ), $args['separator'], $postTypeObject->labels->name );
		$href           = get_post_type_archive_feed_link( $postTypeObject->name );

		return [
			'title' => $title,
			'href'  => $href
		];
	}

	/**
	 * Retrieve the attributes for taxonomies feed.
	 *
	 * @since 4.2.1
	 *
	 * @param  array $args    An array of arguments.
	 * @param  \WP_Term $term The term.
	 * @return array          An array of attributes.
	 */
	private function getTaxonomiesAttributes( $args, $term ) {
		$title = null;
		$href  = null;

		if ( is_category() ) {
			$title = sprintf( $args['cattitle'], get_bloginfo( 'name' ), $args['separator'], $term->name );
			$href  = get_category_feed_link( $term->term_id );
		}

		if ( is_tag() ) {
			$title = sprintf( $args['tagtitle'], get_bloginfo( 'name' ), $args['separator'], $term->name );
			$href  = get_tag_feed_link( $term->term_id );
		}

		if ( is_tax() ) {
			$tax   = get_taxonomy( $term->taxonomy );
			$title = sprintf( $args['taxtitle'], get_bloginfo( 'name' ), $args['separator'], $term->name, $tax->labels->singular_name );
			$href  = get_term_feed_link( $term->term_id, $term->taxonomy );
		}

		return [
			'title' => $title,
			'href'  => $href
		];
	}

	/**
	 * Retrieve the attributes for the author feed.
	 *
	 * @since 4.2.1
	 *
	 * @param  array $args An array of arguments.
	 * @return array       An array of attributes.
	 */
	private function getAuthorAttributes( $args ) {
		$authorId = (int) get_query_var( 'author' );
		$title    = sprintf( $args['authortitle'], get_bloginfo( 'name' ), $args['separator'], get_the_author_meta( 'display_name', $authorId ) );
		$href     = get_author_feed_link( $authorId );

		return [
			'title' => $title,
			'href'  => $href
		];
	}

	/**
	 * Retrieve the attributes for the search feed.
	 *
	 * @since 4.2.1
	 *
	 * @param  array $args An array of arguments.
	 * @return array       An array of attributes.
	 */
	private function getSearchAttributes( $args ) {
		$title = sprintf( $args['searchtitle'], get_bloginfo( 'name' ), $args['separator'], get_search_query( false ) );
		$href  = get_search_feed_link();

		return [
			'title' => $title,
			'href'  => $href
		];
	}

	/**
	 * Get the currently queried post type.
	 *
	 * @since 4.2.1
	 *
	 * @return string The currently queried post type.
	 */
	private function getQueriedPostType() {
		$postType = get_query_var( 'post_type' );
		if ( is_array( $postType ) ) {
			$postType = reset( $postType );
		}

		return $postType;
	}
}Common/Views/admin/settings-page.php000066600000030033151135505570013456 0ustar00<?php
/**
 * This is the error page HTML.
 *
 * @since 4.1.9
 */

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

// phpcs:disable Generic.Files.LineLength.MaxExceeded
$logoImage = 'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTMyIDI2IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSJhaW9zZW8tbG9nbyI+Cgk8cGF0aAoJCWZpbGwtcnVsZT0iZXZlbm9kZCIKCQljbGlwLXJ1bGU9ImV2ZW5vZGQiCgkJZD0iTTExOS4wMzggMjUuOTI0MUMxMjYuMTk3IDI1LjkyNDEgMTMyIDIwLjEyMDggMTMyIDEyLjk2MkMxMzIgNS44MDMzIDEyNi4xOTcgMCAxMTkuMDM4IDBDMTExLjg3OSAwIDEwNi4wNzYgNS44MDMzIDEwNi4wNzYgMTIuOTYyQzEwNi4wNzYgMjAuMTIwOCAxMTEuODc5IDI1LjkyNDEgMTE5LjAzOCAyNS45MjQxWk0xMTYuOTc0IDQuNzQ0MDhDMTE2Ljc5OCA0LjQ3NjQ4IDExNi40NzMgNC4zNTEzNiAxMTYuMTc1IDQuNDU2NzJDMTE1LjgzNSA0LjU3NjczIDExNS41MDMgNC43MTc4NyAxMTUuMTggNC44NzkyOUMxMTQuODk3IDUuMDIwOTggMTE0Ljc1NSA1LjM0NDY2IDExNC44MTcgNS42NjAzM0wxMTUuMDM5IDYuNzg1MDdDMTE1LjA5NiA3LjA3NDU3IDExNC45NzggNy4zNjgzOSAxMTQuNzU0IDcuNTU1MDRDMTE0LjQgNy44NTAwMyAxMTQuMDcyIDguMTgzNTQgMTEzLjc3OSA4LjU1MjY4QzExMy41OTcgOC43ODIxMiAxMTMuMzA5IDguOTAzNzMgMTEzLjAyNSA4Ljg0NjM4TDExMS45MjMgOC42MjM2NEMxMTEuNjEzIDguNTYxMDcgMTExLjI5NiA4LjcwNzQ3IDExMS4xNTkgOC45OTczOEMxMTEuMDgxIDkuMTYxMTYgMTExLjAwNyA5LjMyODM3IDExMC45MzggOS40OTg5MkMxMTAuODY5IDkuNjY5NDcgMTEwLjgwNiA5Ljg0MDkzIDExMC43NDggMTAuMDEzMUMxMTAuNjQ2IDEwLjMxNzkgMTEwLjc3IDEwLjY0OTYgMTExLjAzMyAxMC44Mjc5TDExMS45NjkgMTEuNDYyNUMxMTIuMjEgMTEuNjI2IDExMi4zMyAxMS45MTkgMTEyLjMwMSAxMi4yMTI4QzExMi4yNTQgMTIuNjg1NiAxMTIuMjU2IDEzLjE1NzUgMTEyLjMwNCAxMy42MjE3QzExMi4zMzQgMTMuOTE1NCAxMTIuMjE1IDE0LjIwODggMTExLjk3NCAxNC4zNzMxTDExMS4wNCAxNS4wMTE0QzExMC43NzggMTUuMTkwNiAxMTAuNjU1IDE1LjUyMjQgMTEwLjc1OCAxNS44MjY4QzExMC44NzYgMTYuMTczNCAxMTEuMDE0IDE2LjUxMjUgMTExLjE3MiAxNi44NDE5QzExMS4zMTEgMTcuMTMxMSAxMTEuNjI5IDE3LjI3NjEgMTExLjkzOCAxNy4yMTI1TDExMy4wNCAxNi45ODU3QzExMy4zMjQgMTYuOTI3MyAxMTMuNjEyIDE3LjA0NzggMTEzLjc5NSAxNy4yNzY3QzExNC4wODQgMTcuNjM4NCAxMTQuNDExIDE3Ljk3MjMgMTE0Ljc3MiAxOC4yNzE1QzExNC45OTcgMTguNDU3NCAxMTUuMTE2IDE4Ljc1MDggMTE1LjA2IDE5LjA0MDVMMTE0Ljg0MiAyMC4xNjU2QzExNC43ODEgMjAuNDgxNyAxMTQuOTI0IDIwLjgwNDkgMTE1LjIwOCAyMC45NDU1QzExNS4zNjkgMjEuMDI0OSAxMTUuNTMzIDIxLjA5OTkgMTE1LjcgMjEuMTcwMkMxMTUuODY3IDIxLjI0MDUgMTE2LjAzNSAyMS4zMDUxIDExNi4yMDQgMjEuMzY0MkMxMTYuNjk3IDIxLjUzNjkgMTE3LjM4OCAyMC45MTg1IDExNy44OTkgMjAuNDYxM0MxMTguMTUxIDIwLjIzNTggMTE4LjMwNiAxOS45MTY3IDExOC4zMDggMTkuNTc1MUMxMTguMzA4IDE5LjU3MzIgMTE4LjMwOCAxOS41NzE0IDExOC4zMDggMTkuNTY5NkwxMTguMzA4IDE3LjY4ODJDMTE4LjMwOCAxNy42NjgyIDExOC4zMDkgMTcuNjQ4NSAxMTguMzEgMTcuNjI4OUMxMTYuODAxIDE3LjI2MDkgMTE1LjY4IDE1Ljg3NTkgMTE1LjY4IDE0LjIyMzZWMTIuMjI1OEMxMTUuNjggMTIuMDczOSAxMTUuOCAxMS45NTA4IDExNS45NDkgMTEuOTUwOEgxMTYuODg0VjkuOTg1MjFDMTE2Ljg4NCA5LjcxMzgxIDExNy4wOTkgOS40OTM4MSAxMTcuMzY1IDkuNDkzODFDMTE3LjYzMSA5LjQ5MzgxIDExNy44NDcgOS43MTM4MSAxMTcuODQ3IDkuOTg1MjFWMTEuOTUwOEgxMjAuMzc1VjkuOTg1MjFDMTIwLjM3NSA5LjcxMzgxIDEyMC41OTEgOS40OTM4MSAxMjAuODU3IDkuNDkzODFDMTIxLjEyMyA5LjQ5MzgxIDEyMS4zMzggOS43MTM4MSAxMjEuMzM4IDkuOTg1MjFWMTEuOTUwOEgxMjIuMjczQzEyMi40MjIgMTEuOTUwOCAxMjIuNTQyIDEyLjA3MzkgMTIyLjU0MiAxMi4yMjU4VjE0LjIyMzZDMTIyLjU0MiAxNS45MjgxIDEyMS4zNDggMTcuMzQ4MiAxMTkuNzY4IDE3LjY2MDhDMTE5Ljc2OCAxNy42Njk5IDExOS43NjggMTcuNjc5IDExOS43NjggMTcuNjg4MkwxMTkuNzY4IDE5LjU2MTVDMTE5Ljc2OCAxOS45MDk3IDExOS45MjggMjAuMjM0NiAxMjAuMTg3IDIwLjQ2MDlDMTIwLjcwNyAyMC45MTQzIDEyMS40MSAyMS41MjczIDEyMS45MDEgMjEuMzUzOUMxMjIuMjQxIDIxLjIzMzkgMTIyLjU3MyAyMS4wOTI3IDEyMi44OTYgMjAuOTMxM0MxMjMuMTc5IDIwLjc4OTYgMTIzLjMyMSAyMC40NjU5IDEyMy4yNTkgMjAuMTUwM0wxMjMuMDM3IDE5LjAyNTVDMTIyLjk4IDE4LjczNiAxMjMuMDk4IDE4LjQ0MjIgMTIzLjMyMiAxOC4yNTU1QzEyMy42NzYgMTcuOTYwNiAxMjQuMDA0IDE3LjYyNzEgMTI0LjI5NyAxNy4yNTc5QzEyNC40NzkgMTcuMDI4NSAxMjQuNzY3IDE2LjkwNjkgMTI1LjA1IDE2Ljk2NDJMMTI2LjE1MyAxNy4xODdDMTI2LjQ2MyAxNy4yNDk1IDEyNi43OCAxNy4xMDMxIDEyNi45MTcgMTYuODEzMkMxMjYuOTk1IDE2LjY0OTQgMTI3LjA2OSAxNi40ODIyIDEyNy4xMzggMTYuMzExN0MxMjcuMjA2IDE2LjE0MTIgMTI3LjI3IDE1Ljk2OTcgMTI3LjMyOCAxNS43OTc1QzEyNy40MyAxNS40OTI3IDEyNy4zMDYgMTUuMTYxMSAxMjcuMDQzIDE0Ljk4MjhMMTI2LjEwNyAxNC4zNDgxQzEyNS44NjYgMTQuMTg0NiAxMjUuNzQ2IDEzLjg5MTYgMTI1Ljc3NSAxMy41OTc4QzEyNS44MjIgMTMuMTI1IDEyNS44MiAxMi42NTMxIDEyNS43NzIgMTIuMTg4OUMxMjUuNzQyIDExLjg5NTIgMTI1Ljg2MSAxMS42MDE4IDEyNi4xMDIgMTEuNDM3NUwxMjcuMDM2IDEwLjc5OTJDMTI3LjI5OCAxMC42MjAxIDEyNy40MjEgMTAuMjg4MiAxMjcuMzE4IDkuOTgzODVDMTI3LjIgOS42MzcyMSAxMjcuMDYyIDkuMjk4MTUgMTI2LjkwMyA4Ljk2ODc0QzEyNi43NjUgOC42Nzk1NyAxMjYuNDQ3IDguNTM0NSAxMjYuMTM4IDguNTk4MTRMMTI1LjAzNiA4LjgyNDk0QzEyNC43NTIgOC44ODMzMSAxMjQuNDY0IDguNzYyNzcgMTI0LjI4MSA4LjUzMzkxQzEyMy45OTIgOC4xNzIyMiAxMjMuNjY1IDcuODM4MzIgMTIzLjMwNCA3LjUzOTE0QzEyMy4wNzkgNy4zNTMxOSAxMjIuOTU5IDcuMDU5NzkgMTIzLjAxNiA2Ljc3MDA5TDEyMy4yMzQgNS42NDUwMUMxMjMuMjk1IDUuMzI4OTYgMTIzLjE1MiA1LjAwNTY3IDEyMi44NjggNC44NjUxQzEyMi43MDcgNC43ODU2OCAxMjIuNTQzIDQuNzEwNzIgMTIyLjM3NiA0LjY0MDQzQzEyMi4yMDkgNC41NzAxNCAxMjIuMDQxIDQuNTA1NTEgMTIxLjg3MiA0LjQ0NjQ2QzEyMS41NzQgNC4zNDE5NCAxMjEuMjQ5IDQuNDY4MTkgMTIxLjA3NCA0LjczNjU0TDEyMC40NTIgNS42OTE4M0MxMjAuMjkyIDUuOTM3ODEgMTIwLjAwNSA2LjA2MDMyIDExOS43MTcgNi4wMzA2QzExOS4yNTMgNS45ODI3OSAxMTguNzkxIDUuOTg0NzMgMTE4LjMzNiA2LjAzMzUzQzExOC4wNDggNi4wNjQ0MSAxMTcuNzYxIDUuOTQyOTIgMTE3LjYgNS42OTc1MUwxMTYuOTc0IDQuNzQ0MDhaIgoJCWZpbGw9IiMwMDVBRTAiCgkvPgoJPHBhdGgKCQlmaWxsLXJ1bGU9ImV2ZW5vZGQiCgkJY2xpcC1ydWxlPSJldmVub2RkIgoJCWQ9Ik0xMDUuNTEzIDEuMDUzMzdIODguMjk0MVYyNS4xMDY4SDEwNS42MTVDMTA0LjgyMSAyMy40NDcyIDEwNC4xODUgMjEuNjk3OCAxMDMuNzI2IDE5Ljg3NzhIOTQuNDk2OFYxNS41NTAzSDEwMi45ODlDMTAyLjkxMiAxNC43MDEyIDEwMi44NzIgMTMuODQxMiAxMDIuODcyIDEyLjk3MkMxMDIuODcyIDEyLjA2NTggMTAyLjkxNSAxMS4xNjk2IDEwMi45OTkgMTAuMjg1M0g5NC40OTY4VjYuMjgyMzdIMTAzLjY3MkMxMDQuMTE1IDQuNDYzNzUgMTA0LjczNSAyLjcxNDM1IDEwNS41MTMgMS4wNTMzN1pNNzUuMzY3OSAyNS41Mzk1QzcwLjQ5OTUgMjUuNTM5NSA2Ny4xMDk2IDI0LjAyNDkgNjQuNjkzNSAyMS43MTY5TDY3Ljk3NTEgMTcuMDY0OUM2OS43MDYxIDE4Ljc5NTkgNzIuMzc0NyAyMC4yMzg0IDc1LjY1NjQgMjAuMjM4NEM3Ny43ODQgMjAuMjM4NCA3OS4wODIzIDE5LjMzNjggNzkuMDgyMyAxOC4xODI5Qzc5LjA4MjMgMTYuODEyNSA3Ny41MzE2IDE2LjI3MTYgNzQuOTcxMiAxNS43MzA2TDc0Ljc2NzQgMTUuNjg5OUM3MC44MTcgMTQuOTAxNiA2NS40NTA4IDEzLjgzMDYgNjUuNDUwOCA4LjIyOTczQzY1LjQ1MDggNC4xOTA3NyA2OC44NzY3IDAuNjkyNzQ5IDc1LjA0MzMgMC42OTI3NDlDNzguOTAxOSAwLjY5Mjc0OSA4Mi4yNTU3IDEuODQ2NzQgODQuODE2MSA0LjA0NjUyTDgxLjQyNjMgOC40ODIxNkM3OS40MDY4IDYuODIzMyA3Ni43NzQzIDUuOTkzODggNzQuNjQ2NiA1Ljk5Mzg4QzcyLjU5MTEgNS45OTM4OCA3MS43OTc3IDYuODIzMyA3MS43OTc3IDcuODY5MTFDNzEuNzk3NyA5LjEzMTI4IDczLjI3NjMgOS41NjQwMiA3NS45NDQ5IDEwLjA2ODlDNzkuOTExNyAxMC44OTgzIDg1LjM5MzEgMTIuMDUyMyA4NS4zOTMxIDE3LjQ5NzdDODUuMzkzMSAyMi4zMyA4MS44MjMgMjUuNTM5NSA3NS4zNjc5IDI1LjUzOTVaIgoJCWZpbGw9IiMwMDVBRTAiCgkvPgoJPHBhdGgKCQlkPSJNMTguNjY0NiAyNS4xMTg2SDI1LjIyNTNMMTYuMzg0MiAxLjcxNzU5SDguODA2MDZMMCAyNS4xMTg2SDYuNTYwNjlMNy43NTM1NSAyMS41NzUxSDE3LjQ3MThMMTguNjY0NiAyNS4xMTg2Wk0xMi41OTUxIDYuOTgwMThMMTUuODkzIDE2LjQ4NzlIOS4zMzIzMkwxMi41OTUxIDYuOTgwMThaIgoJCWZpbGw9IiMxNDFCMzgiCgkvPgoJPHBhdGgKCQlkPSJNMjcuOTk5IDI1LjExODZIMzQuMDMzNVYxLjcxNzU5SDI3Ljk5OVYyNS4xMTg2WiIKCQlmaWxsPSIjMTQxQjM4IgoJLz4KCTxwYXRoCgkJZD0iTTM3LjA1MDQgMTMuNDM1NkMzNy4wNTA0IDIwLjU1NzcgNDIuNDE4MyAyNS41Mzk2IDQ5LjU3NTQgMjUuNTM5NkM1Ni43MzI1IDI1LjUzOTYgNjIuMDY1MyAyMC41NTc3IDYyLjA2NTMgMTMuNDM1NkM2Mi4wNjUzIDYuMzEzNTggNTYuNzMyNSAxLjMzMTY3IDQ5LjU3NTQgMS4zMzE2N0M0Mi40MTgzIDEuMzMxNjcgMzcuMDUwNCA2LjMxMzU4IDM3LjA1MDQgMTMuNDM1NlpNNTUuOTI1NiAxMy40MzU2QzU1LjkyNTYgMTcuMjI0NyA1My40MzQ2IDIwLjIwNjggNDkuNTc1NCAyMC4yMDY4QzQ1LjY4MTEgMjAuMjA2OCA0My4xOTAxIDE3LjIyNDcgNDMuMTkwMSAxMy40MzU2QzQzLjE5MDEgOS42MTE0NyA0NS42ODExIDYuNjY0NDIgNDkuNTc1NCA2LjY2NDQyQzUzLjQzNDYgNi42NjQ0MiA1NS45MjU2IDkuNjExNDcgNTUuOTI1NiAxMy40MzU2WiIKCQlmaWxsPSIjMTQxQjM4IgoJLz4KPC9zdmc+';
$medium    = false !== strpos( AIOSEO_PHP_VERSION_DIR, 'pro' ) ? 'proplugin' : 'liteplugin';
?>
<style type="text/css">
	#aioseo-settings-area {
		visibility: hidden;
		margin: auto;
		width: 750px;
		max-width: 100%;
		animation: loadAioseoSettingsNoJSView 0s 5s forwards;
	}

	#aioseo-settings-error-loading-area {
		text-align: center;
		background-color: #fff;
		border: 1px solid #D6E2EC;
		padding: 15px 50px 30px;
		color: #141B38;
		margin: 82px 0;
	}

	#aioseo-settings-logo {
		max-width: 100%;
		width: 240px;
		padding: 30px 0 15px;
	}

	.aioseo-settings-button,
	.aioseo-settings-button:focus {
		margin-left: auto;
		background-color: #005ae0;
		border-color: #3380BC;
		border-bottom-width: 2px;
		color: #fff;
		border-radius: 3px;
		font-weight: 600;
		transition: all 0.1s ease-in-out;
		transition-duration: 0.2s;
		padding: 14px 35px;
		font-size: 16px;
		margin-top: 10px;
		margin-bottom: 20px;
		text-decoration: none;
		display: inline-block;
	}

	.aioseo-settings-button:hover {
		color: #fff;
		background-color: #1a82ea;
	}

	#aioseo-alert-message {
		position: relative;
		border-radius: 3px;
		padding: 12px 20px;
		font-size: 14px;
		color: #141B38;
		line-height: 1.4;
		border: 1px solid #DF2A4A;
		background-color: #FBE9EC;
	}

	#aioseo-settings-area h3 {
		font-size: 20px;
		color: #434343;
		font-weight: 500;
		line-height:1.4;
	}

	#aioseo-settings-area p {
		line-height: 1.5;
		margin: 1em 0;
		font-size: 16px;
		color: #434343;
		padding: 5px 20px 20px;
	}

	@keyframes loadAioseoSettingsNoJSView{
		to { visibility: visible; }
	}
</style>
<!--[if IE]>
	<style>
		#aioseo-settings-area{
			visibility: visible !important;
		}
	</style>
<![endif]-->

<script type="text/javascript">
	var ua   = window.navigator.userAgent;
	var msie = ua.indexOf( 'MSIE ' );
	if (0 < msie) {
		document.addEventListener('DOMContentLoaded', () => {
			var browserError = document.getElementById( 'aioseo-error-browser' ),
				jsError      = document.getElementById( 'aioseo-error-js' );

			jsError.style.display      = 'none';
			browserError.style.display = 'block';
		})
	} else {
		window.onerror = function myErrorHandler( errorMsg, url, lineNumber ) {
			/* Don't try to put error in container that no longer exists post-vue loading */
			var messageContainer = document.getElementById( 'aioseo-nojs-error-message' );
			if ( ! messageContainer ) {
				return false;
			}
			var message                    = document.getElementById( 'aioseo-alert-message' );
			message.innerHTML              = errorMsg;
			messageContainer.style.display = 'block';
			return false;
		}
	}
</script>

<div id="aioseo-settings-area">
	<div id="aioseo-settings-error-loading-area">
		<img
			id="aioseo-settings-logo"
			src="<?php echo esc_attr( $logoImage ); // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage ?>"
			alt="<?php echo esc_attr( AIOSEO_PLUGIN_NAME ); ?>"
		>

		<div id="aioseo-error-js">
			<h3><?php esc_html_e( 'Ooops! It Appears JavaScript Didn’t Load', 'all-in-one-seo-pack' ); ?></h3>

			<p>
				<?php
				printf(
					// Translators: 1 - Line break HTML tag, 2 - "AIOSEO".
					esc_html__( 'There seems to be an issue running JavaScript on your website. %1$s%2$s is built with JavaScript to give you the best experience possible.', 'all-in-one-seo-pack' ),
					'<br>',
					esc_attr( AIOSEO_PLUGIN_SHORT_NAME )
				);
				?>
			</p>

			<div style="display: none;" id="aioseo-nojs-error-message">
				<div id="aioseo-alert-message"></div>

				<p style="margin-top: 5px; font-size: 14px; color: #141B38;">
					<?php
					printf(
						// Translators: 1 - "AIOSEO".
						esc_html__( 'Copy the error message above and paste it in a message to the %1$s support team.', 'all-in-one-seo-pack' ),
						esc_attr( AIOSEO_PLUGIN_SHORT_NAME )
					);
					?>
				</p>
			</div>

			<a href="https://aioseo.com/docs/how-to-fix-javascript-errors/?utm_source=WordPress&utm_medium=<?php echo esc_attr( $medium ); ?>&utm_campaign=javascript-errors" class="aioseo-settings-button" target="_blank">
				<?php esc_html_e( 'Resolve This Issue', 'all-in-one-seo-pack' ); ?>
			</a>
		</div>

		<div id="aioseo-error-browser" style="display: none">
			<h3><?php esc_html_e( 'Your browser version is not supported', 'all-in-one-seo-pack' ); ?></h3>

			<p>
				<?php
				printf(
					// Translators: 1 - "AIOSEO".
					esc_html__( 'You are using a browser which is no longer supported by %1$s. Please update or use another browser in order to access the plugin settings.', 'all-in-one-seo-pack' ),
					esc_attr( AIOSEO_PLUGIN_SHORT_NAME )
				);
				?>
			</p>

			<a href="https://www.aioseo.com/docs/browser-support-policy/?utm_source=WordPress&utm_medium=<?php echo esc_attr( $medium ); ?>&utm_campaign=javascript-errors" class="aioseo-settings-button" target="_blank">
				<?php esc_html_e( 'View supported browsers', 'all-in-one-seo-pack' ); ?>
			</a>
		</div>
	</div>
</div>Common/Views/admin/posts/columns.php000066600000000637151135505570013543 0ustar00<?php
/**
 * This is the output for the columns on the page/post editor.
 *
 * @since 4.0.0
 */

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

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
?>

<div id="<?php echo esc_attr( $columnName ); ?>-<?php echo esc_attr( $postId ); ?>">
	<?php require AIOSEO_DIR . '/app/Common/Views/parts/loader.php'; ?>
</div>Common/Views/admin/terms/columns.php000066600000000636151135505570013524 0ustar00<?php
/**
 * This is the output for the columns on the taxonomy screen.
 *
 * @since 4.0.0
 */

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

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
?>

<div id="<?php echo esc_attr( $columnName ); ?>-<?php echo esc_attr( $termId ); ?>">
	<?php require AIOSEO_DIR . '/app/Common/Views/parts/loader.php'; ?>
</div>Common/Views/sitemap/xsl/default.php000066600000031502151135505570013512 0ustar00<?php
/**
 * XSL stylesheet for the sitemap.
 *
 * @since 4.0.0
 */

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

// phpcs:disable
$utmMedium = 'xml-sitemap';
if ( '/sitemap.rss' === $sitemapPath ) {
	$utmMedium = 'rss-sitemap';
}
?>
<xsl:stylesheet
	version="2.0"
	xmlns:html="http://www.w3.org/TR/html40"
	xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9"
	xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
	<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>

	<xsl:template match="/">
		<xsl:variable name="fileType">
			<xsl:choose>
				<xsl:when test="//channel">RSS</xsl:when>
				<xsl:when test="//sitemap:url">Sitemap</xsl:when>
				<xsl:otherwise>SitemapIndex</xsl:otherwise>
			</xsl:choose>
		</xsl:variable>
		<html xmlns="http://www.w3.org/1999/xhtml">
			<head>
				<title>
					<xsl:choose>
						<xsl:when test="$fileType='Sitemap' or $fileType='RSS'"><?php echo $title; ?></xsl:when>
						<xsl:otherwise><?php _e( 'Sitemap Index', 'all-in-one-seo-pack' ); ?></xsl:otherwise>
					</xsl:choose>
				</title>
				<meta name="viewport" content="width=device-width, initial-scale=1" />
				<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
				<?php aioseo()->templates->getTemplate( 'sitemap/xsl/styles.php' ); ?>
			</head>
			<body>
				<xsl:variable name="amountOfURLs">
					<xsl:choose>
						<xsl:when test="$fileType='RSS'">
							<xsl:value-of select="count(//channel/item)"></xsl:value-of>
						</xsl:when>
						<xsl:when test="$fileType='Sitemap'">
							<xsl:value-of select="count(sitemap:urlset/sitemap:url)"></xsl:value-of>
						</xsl:when>
						<xsl:otherwise>
							<xsl:value-of select="count(sitemap:sitemapindex/sitemap:sitemap)"></xsl:value-of>
						</xsl:otherwise>
					</xsl:choose>
				</xsl:variable>

				<xsl:call-template name="Header">
					<xsl:with-param name="title"><?php echo $title; ?></xsl:with-param>
					<xsl:with-param name="amountOfURLs" select="$amountOfURLs"/>
					<xsl:with-param name="fileType" select="$fileType"/>
				</xsl:call-template>

				<div class="content">
					<div class="container">
						<xsl:choose>
							<xsl:when test="$amountOfURLs = 0"><xsl:call-template name="emptySitemap"/></xsl:when>
							<xsl:when test="$fileType='Sitemap'"><xsl:call-template name="sitemapTable"/></xsl:when>
							<xsl:when test="$fileType='RSS'"><xsl:call-template name="sitemapRSS"/></xsl:when>
							<xsl:otherwise><xsl:call-template name="siteindexTable"/></xsl:otherwise>
						</xsl:choose>
					</div>
				</div>
			</body>
		</html>
	</xsl:template>

	<xsl:template name="siteindexTable">
		<?php
		$sitemapIndex = aioseo()->sitemap->helpers->filename( 'general' );
		$sitemapIndex = $sitemapIndex ? $sitemapIndex : 'sitemap';
		aioseo()->templates->getTemplate(
			'sitemap/xsl/partials/breadcrumb.php',
			[
				'items' => [
					[ 'title' => __( 'Sitemap Index', 'all-in-one-seo-pack' ), 'url' => $sitemapUrl ],
				]
			]
		);
		?>
		<div class="table-wrapper">
			<table cellpadding="3">
				<thead>
				<tr>
					<th class="left">
						<?php _e( 'URL', 'all-in-one-seo-pack' ); ?>
					</th>
					<th><?php _e( 'URL Count', 'all-in-one-seo-pack' ); ?></th>
					<th>
						<?php
						aioseo()->templates->getTemplate(
							'sitemap/xsl/partials/sortable-column.php',
							[
								'parameters' => $sitemapParams,
								'sitemapUrl' => $sitemapUrl,
								'column'     => 'date',
								'title'      => __( 'Last Updated', 'all-in-one-seo-pack' )
							]
						);
						?>
					</th>
				</tr>
				</thead>
				<tbody>
				<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'"/>
				<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
				<xsl:for-each select="sitemap:sitemapindex/sitemap:sitemap">
					<?php
					aioseo()->templates->getTemplate(
						'sitemap/xsl/partials/xsl-sort.php',
						[
							'parameters' => $sitemapParams,
							'node'       => 'sitemap:lastmod',
						]
					);
					?>
					<tr>
						<xsl:if test="position() mod 2 != 0">
							<xsl:attribute name="class">stripe</xsl:attribute>
						</xsl:if>
						<td class="left">
							<a>
								<xsl:attribute name="href">
									<xsl:value-of select="sitemap:loc" />
								</xsl:attribute>
								<xsl:value-of select="sitemap:loc"/>
							</a>
						</td>
						<td>
							<?php if ( ! empty( $xslParams['counts'] ) ) : ?>
							<div class="item-count">
							<xsl:choose>
								<?php foreach ( $xslParams['counts'] as $slug => $count ) : ?>
									<xsl:when test="contains(sitemap:loc, '<?php echo $slug; ?>')"><?php echo $count; ?></xsl:when>
								<?php endforeach; ?>
								<xsl:otherwise><?php echo $linksPerIndex; ?></xsl:otherwise>
							</xsl:choose>
							</div>
							<?php endif; ?>
						</td>
						<td class="datetime">
							<?php
							if ( ! empty( $xslParams['datetime'] ) ) {
								aioseo()->templates->getTemplate(
									'sitemap/xsl/partials/date-time.php',
									[
										'datetime' => $xslParams['datetime'],
										'node'     => 'sitemap:loc'
									]
								);
							}
							?>
						</td>
					</tr>
				</xsl:for-each>
				</tbody>
			</table>
		</div>
	</xsl:template>

	<xsl:template name="sitemapRSS">
		<?php
		aioseo()->templates->getTemplate(
			'sitemap/xsl/partials/breadcrumb.php',
			[
				'items' => [
					[ 'title' => $title, 'url' => $sitemapUrl ],
				]
			]
		);
		?>
		<div class="table-wrapper">
			<table cellpadding="3">
				<thead>
					<tr>
						<th class="left"><?php _e( 'URL', 'all-in-one-seo-pack' ); ?></th>
						<th>
							<?php
							aioseo()->templates->getTemplate(
								'sitemap/xsl/partials/sortable-column.php',
								[
									'parameters' => $sitemapParams,
									'sitemapUrl' => $sitemapUrl,
									'column'     => 'date',
									'title'      => __( 'Publication Date', 'all-in-one-seo-pack' )
								]
							);
							?>
						</th>
					</tr>
				</thead>
				<tbody>
				<xsl:for-each select="//channel/item">
					<?php
					if ( ! empty( $sitemapParams['sitemap-order'] ) ) {
						aioseo()->templates->getTemplate(
							'sitemap/xsl/partials/xsl-sort.php',
							[
								'parameters' => $sitemapParams,
								'node'       => 'pubDate',
							]
						);
					}
					?>
					<tr>
						<xsl:if test="position() mod 2 != 0">
							<xsl:attribute name="class">stripe</xsl:attribute>
						</xsl:if>
						<td class="left">
							<a>
								<xsl:attribute name="href">
									<xsl:value-of select="link" />
								</xsl:attribute>
								<xsl:value-of select="link"/>
							</a>
						</td>
						<td class="datetime">
							<?php
							if ( ! empty( $xslParams['datetime'] ) ) {
								aioseo()->templates->getTemplate(
									'sitemap/xsl/partials/date-time.php',
									[
										'datetime' => $xslParams['datetime'],
										'node'     => 'link'
									]
								);
							}
							?>
						</td>
					</tr>
				</xsl:for-each>
				</tbody>
			</table>
		</div>
	</xsl:template>

	<xsl:template name="sitemapTable">
		<?php
		$sitemapIndex  = aioseo()->sitemap->helpers->filename( 'general' );
		$sitemapIndex  = $sitemapIndex ?: 'sitemap';
		$excludeImages = isset( $excludeImages ) ? $excludeImages : aioseo()->sitemap->helpers->excludeImages();
		aioseo()->templates->getTemplate(
			'sitemap/xsl/partials/breadcrumb.php',
			[
				'items' => [
					[ 'title' => __( 'Sitemap Index', 'all-in-one-seo-pack' ), 'url' => home_url( "/$sitemapIndex.xml" ) ],
					[ 'title' => $title, 'url' => $sitemapUrl ],
				]
			]
		);
		?>
		<div class="table-wrapper">
			<table cellpadding="3">
				<thead>
					<tr>
						<th class="left">
							<?php _e( 'URL', 'all-in-one-seo-pack' ); ?>
						</th>
						<?php if ( ! $excludeImages ) : ?>
							<th>
								<?php
								aioseo()->templates->getTemplate(
									'sitemap/xsl/partials/sortable-column.php',
									[
										'parameters' => $sitemapParams,
										'sitemapUrl' => $sitemapUrl,
										'column'     => 'image',
										'title'      => __( 'Images', 'all-in-one-seo-pack' )
									]
								);
								?>
							</th>
						<?php endif; ?>
						<th>
							<?php
							aioseo()->templates->getTemplate(
								'sitemap/xsl/partials/sortable-column.php',
								[
									'parameters' => $sitemapParams,
									'sitemapUrl' => $sitemapUrl,
									'column'     => 'changefreq',
									'title'      => __( 'Change Frequency', 'all-in-one-seo-pack' )
								]
							);
							?>
						</th>
						<th>
							<?php
							aioseo()->templates->getTemplate(
								'sitemap/xsl/partials/sortable-column.php',
								[
									'parameters' => $sitemapParams,
									'sitemapUrl' => $sitemapUrl,
									'column'     => 'priority',
									'title'      => __( 'Priority', 'all-in-one-seo-pack' )
								]
							);
							?>
						</th>
						<th>
							<?php
							aioseo()->templates->getTemplate(
								'sitemap/xsl/partials/sortable-column.php',
								[
									'parameters' => $sitemapParams,
									'sitemapUrl' => $sitemapUrl,
									'column'     => 'date',
									'title'      => __( 'Last Updated', 'all-in-one-seo-pack' )
								]
							);
							?>
						</th>
					</tr>
				</thead>
				<tbody>
				<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'"/>
				<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
				<xsl:for-each select="sitemap:urlset/sitemap:url">
					<?php
					if ( ! empty( $sitemapParams['sitemap-order'] ) ) {
						switch ( $sitemapParams['sitemap-order'] ) {
							case 'image':
								$node = 'count(image:image)';
								break;
							case 'date':
								$node = 'sitemap:lastmod';
								break;
							default:
								$node = 'sitemap:' . $sitemapParams['sitemap-order'];
								break;
						}
						aioseo()->templates->getTemplate(
							'sitemap/xsl/partials/xsl-sort.php',
							[
								'parameters' => $sitemapParams,
								'node'       => $node,
							]
						);
					}
					?>
					<tr>
						<xsl:if test="position() mod 2 != 0">
							<xsl:attribute name="class">stripe</xsl:attribute>
						</xsl:if>

						<td class="left">
							<xsl:variable name="itemURL">
								<xsl:value-of select="sitemap:loc"/>
							</xsl:variable>

							<xsl:choose>
								<xsl:when test="count(./*[@rel='alternate']) > 0">
									<xsl:for-each select="./*[@rel='alternate']">
										<xsl:if test="position() = last()">
											<a href="{current()/@href}" class="localized">
												<xsl:value-of select="@href"/>
											</a> &#160;&#8594; <xsl:value-of select="@hreflang"/>
										</xsl:if>
									</xsl:for-each>
								</xsl:when>
								<xsl:otherwise>
									<a href="{$itemURL}">
										<xsl:value-of select="sitemap:loc"/>
									</a>
								</xsl:otherwise>
							</xsl:choose>

							<xsl:for-each select="./*[@rel='alternate']">
								<br />
								<xsl:if test="position() != last()">
									<a href="{current()/@href}" class="localized">
										<xsl:value-of select="@href"/>
									</a> &#160;&#8594; <xsl:value-of select="@hreflang"/>
								</xsl:if>
							</xsl:for-each>
						</td>
						<?php if ( ! $excludeImages ) : ?>
						<td>
							<div class="item-count">
								<xsl:value-of select="count(image:image)"/>
							</div>
						</td>
						<?php endif; ?>
						<td>
							<xsl:value-of select="concat(translate(substring(sitemap:changefreq, 1, 1),concat($lower, $upper),concat($upper, $lower)),substring(sitemap:changefreq, 2))"/>
						</td>
						<td>
							<xsl:if test="string(number(sitemap:priority))!='NaN'">
								<xsl:call-template name="formatPriority">
									<xsl:with-param name="priority" select="sitemap:priority"/>
								</xsl:call-template>
							</xsl:if>
						</td>
						<td class="datetime">
							<?php
							if ( ! empty( $xslParams['datetime'] ) ) {
								aioseo()->templates->getTemplate(
									'sitemap/xsl/partials/date-time.php',
									[
										'datetime' => $xslParams['datetime'],
										'node'     => 'sitemap:loc'
									]
								);
							}
							?>
						</td>
					</tr>
				</xsl:for-each>
				</tbody>
			</table>
		</div>
		<?php
		if ( ! empty( $xslParams['pagination'] ) ) {
			aioseo()->templates->getTemplate(
				'sitemap/xsl/partials/pagination.php',
				[
					'sitemapUrl'    => $sitemapUrl,
					'currentPage'   => $currentPage,
					'linksPerIndex' => $linksPerIndex,
					'total'         => $xslParams['pagination']['total'],
					'showing'       => $xslParams['pagination']['showing']
				]
			);
		}
		?>
	</xsl:template>

	<?php aioseo()->templates->getTemplate( 'sitemap/xsl/templates/header.php', [ 'utmMedium' => $utmMedium ] ); ?>
	<?php aioseo()->templates->getTemplate( 'sitemap/xsl/templates/format-priority.php' ); ?>
	<?php
	aioseo()->templates->getTemplate( 'sitemap/xsl/templates/empty-sitemap.php', [
		'utmMedium' => $utmMedium,
		'items'     => [
			[ 'title' => __( 'Sitemap Index', 'all-in-one-seo-pack' ), 'url' => $sitemapUrl ]
		]
	] );
	?>
</xsl:stylesheet>
Common/Views/sitemap/xsl/styles.php000066600000003512151135505570013411 0ustar00<?php
/**
 * Styles for the sitemap.
 *
 * @since 4.1.5
 */

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

// phpcs:disable
?>
<style type="text/css">
	body {
		margin: 0;
		font-family: Helvetica, Arial, sans-serif;
		font-size: 68.5%;
	}
	#content-head {
		background-color: #141B38;
		padding: 20px 40px;
	}
	#content-head h1,
	#content-head p,
	#content-head a {
		color: #fff;
		font-size: 1.2em;
	}
	#content-head h1 {
		font-size: 2em;
	}
	table {
		margin: 20px 40px;
		border: none;
		border-collapse: collapse;
		font-size: 1em;
		width: 75%;
	}
	th {
		border-bottom: 1px solid #ccc;
		text-align: left;
		padding: 15px 5px;
		font-size: 14px;
	}
	td {
		padding: 10px 5px;
		border-left: 3px solid #fff;
	}
	tr.stripe {
		background-color: #f7f7f7;
	}
	table td a:not(.localized) {
		display: block;
	}
	table td a img {
		max-height: 30px;
		margin: 6px 3px;
	}
	.empty-sitemap {
		margin: 20px 40px;
		width: 75%;
	}
	.empty-sitemap__title {
		font-size: 18px;
		line-height: 125%;
		margin: 12px 0;
	}
	.empty-sitemap svg {
		width: 140px;
		height: 140px;
	}
	.empty-sitemap__buttons {
		margin-bottom: 30px;
	}
	.empty-sitemap__buttons .button {
		margin-right: 5px;
	}
	.breadcrumb {
		margin: 20px 40px;
		width: 75%;

		display: flex;
		align-items: center;
		font-size: 12px;
		font-weight: 600;
	}
	.breadcrumb a {
		color: #141B38;
		text-decoration: none;
	}
	.breadcrumb svg {
		margin: 0 10px;
	}
	@media (max-width: 1023px) {
		.breadcrumb svg:not(.back),
		.breadcrumb a:not(:last-of-type),
		.breadcrumb span {
			display: none;
		}
		.breadcrumb a:last-of-type::before {
			content: '<?php _e( 'Back', 'all-in-one-seo-pack' ); ?>'
		}
	}
	@media (min-width: 1024px) {
		.breadcrumb {
			font-size: 14px;
		}
		.breadcrumb a {
			font-weight: 400;
		}
		.breadcrumb svg.back {
			display: none;
		}
	}
</style>
Common/Views/sitemap/xsl/partials/pagination.php000066600000003645151135505570016045 0ustar00<?php
/**
 * XSL Pagination partial for the sitemap.
 *
 * @since 4.1.5
 */

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

// Don't allow pagination for now.
return;

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable

// Check if requires pagination.
if ( $data['showing'] === $data['total'] ) {
	return;
}

$currentPage   = (int) $data['currentPage'];
$totalLinks    = (int) $data['total'];
$showing       = (int) $data['showing'];
$linksPerIndex = (int) $data['linksPerIndex'];
$totalPages    = ceil( $totalLinks / $linksPerIndex );
$start         = ( ( $currentPage - 1 ) * $linksPerIndex ) + 1;
$end           = ( ( $currentPage - 1 ) * $linksPerIndex ) + $showing;

$hasNextPage = $totalPages > $currentPage;
$hasPrevPage = $currentPage > 1;
$nextPageUri = $hasNextPage ? preg_replace( '/sitemap([0-9]*)\.xml/', 'sitemap' . ( $currentPage + 1 ) . '.xml', (string) $data['sitemapUrl'] ) : '#';
$prevPageUri = $hasPrevPage ? preg_replace( '/sitemap([0-9]*)\.xml/', 'sitemap' . ( $currentPage - 1 ) . '.xml', (string) $data['sitemapUrl'] ) : '#';
?>
<div class="pagination">
	<div class="label">
		<?php
		echo esc_html(
			sprintf(
				// Translators: 1 - The "start-end" pagination results, 2 - Total items.
				__( 'Showing %1$s of %2$s', 'all-in-one-seo-pack' ),
				"$start-$end",
				$totalLinks
			)
		);
		?>
	</div>

	<a href="<?php echo esc_attr( $prevPageUri ); ?>" class="<?php echo $hasPrevPage ? '' : 'disabled'; ?>">
		<svg width="7" height="10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.842 8.825L3.025 5l3.817-3.825L5.667 0l-5 5 5 5 1.175-1.175z" fill="#141B38"/></svg>
	</a>

	<a href="<?php echo esc_attr( $nextPageUri ); ?>" class="<?php echo $hasNextPage ? '' : 'disabled'; ?>">
		<svg width="7" height="10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M.158 8.825L3.975 5 .158 1.175 1.333 0l5 5-5 5L.158 8.825z" fill="#141B38"/></svg>
	</a>
</div>Common/Views/sitemap/xsl/partials/breadcrumb.php000066600000002372151135505570016016 0ustar00<?php
/**
 * XSL Breadcrumb partial for the sitemap.
 *
 * @since 4.1.5
 */

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

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
if ( empty( $data['items'] ) ) {
	return;
}

$sitemapIndex = aioseo()->sitemap->helpers->filename( 'general' );
$sitemapIndex = $sitemapIndex ? $sitemapIndex : 'sitemap';
?>
<div class="breadcrumb">
	<svg class="back" width="6" height="9" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5.274 7.56L2.22 4.5l3.054-3.06-.94-.94-4 4 4 4 .94-.94z" fill="#141B38"/></svg>

	<a href="<?php echo esc_attr( home_url() ); ?>"><span><?php esc_attr_e( 'Home', 'all-in-one-seo-pack' ); ?></span></a>

	<?php
	foreach ( $data['items'] as $key => $item ) {
		if ( empty( $item ) ) {
			continue;
		}
		?>
		<svg width="6" height="8" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M.727 7.06L3.78 4 .727.94l.94-.94 4 4-4 4-.94-.94z" fill="#141B38"/></svg>

		<?php if ( count( $data['items'] ) === $key + 1 ) : ?>
			<span><?php echo esc_html( $item['title'] ); ?></span>
		<?php else : ?>
			<a href="<?php echo esc_attr( $item['url'] ) ?>"><span><?php echo esc_html( $item['title'] ); ?></span></a>
		<?php endif; ?>
		<?php
	}
	?>
</div>Common/Views/sitemap/xsl/partials/xsl-sort.php000066600000001176151135505570015504 0ustar00<?php
/**
 * XSL XSLSort partial for the sitemap.
 *
 * @since 4.1.5
 */

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

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
if ( empty( $data['node'] ) ) {
	return;
}

$orderBy = '';
if ( ! empty( $data['parameters']['sitemap-orderby'] ) && in_array( $data['parameters']['sitemap-orderby'], [ 'ascending', 'descending' ], true ) ) {
	$orderBy = $data['parameters']['sitemap-orderby'];
}

if ( empty( $orderBy ) ) {
	return;
}
?>

<xsl:sort select="<?php echo esc_attr( $data['node'] ); ?>" order="<?php echo esc_attr( $orderBy ) ?>"/>Common/Views/sitemap/xsl/partials/sortable-column.php000066600000001673151135505570017021 0ustar00<?php
/**
 * XSL sortableColumn partial for the sitemap.
 *
 * @since 4.1.5
 */

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

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable

// Just print out the title for now.
echo esc_html( $data['title'] );

/*$orderBy = 'ascending';
if ( ! empty( $data['parameters']['sitemap-orderby'] ) ) {
	$orderBy = $data['parameters']['sitemap-orderby'];
}

$isOrdering = false;
if ( ! empty( $data['parameters']['sitemap-order'] ) ) {
	$isOrdering = $data['column'] === $data['parameters']['sitemap-order'];
}

$link = add_query_arg( [
	'sitemap-order'   => $data['column'],
	'sitemap-orderby' => 'ascending' === $orderBy ? 'descending' : 'ascending'
], $data['sitemapUrl'] );
?>
<a href="<?php echo esc_url( $link ); ?>" class="sortable <?php echo esc_attr( ( $isOrdering ? 'active' : '' ) . ' ' . $orderBy ); ?>">
	<?php echo esc_html( $data['title'] ); ?>
</a>*/Common/Views/sitemap/xsl/partials/date-time.php000066600000001605151135505570015557 0ustar00<?php
/**
 * XSL Breadcrumb partial for the sitemap.
 *
 * @since 4.1.5
 */

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

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
if ( empty( $data['datetime'] ) || empty( $data['node'] ) ) {
	return;
}

?>
<div class="date">
	<xsl:choose>
		<?php foreach ( $data['datetime'] as $slug => $datetime ) : ?>
			<xsl:when test="<?php echo esc_attr( $data['node'] ); ?> = '<?php echo esc_attr( $slug ); ?>'"><?php echo esc_html( $datetime['date'] ); ?></xsl:when>
		<?php endforeach; ?>
	</xsl:choose>
</div>
<div class="time">
	<xsl:choose>
		<?php foreach ( $data['datetime'] as $slug => $datetime ) : ?>
			<xsl:when test="<?php echo esc_attr( $data['node'] ); ?> = '<?php echo esc_attr( $slug ); ?>'"><?php echo esc_html( $datetime['time'] ); ?></xsl:when>
		<?php endforeach; ?>
	</xsl:choose>
</div>Common/Views/sitemap/xsl/templates/format-priority.php000066600000002127151135505570017234 0ustar00<?php
/**
 * XSL formatPriority template for the sitemap.
 *
 * @since 4.1.5
 */

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

// phpcs:disable
?>
<xsl:template name="formatPriority">
	<xsl:param name="priority"/>

	<xsl:variable name="priorityLevel">
		<xsl:choose>
			<xsl:when test="$priority &lt;= 0.5">low</xsl:when>
			<xsl:when test="$priority &gt;= 0.6 and $priority &lt;= 0.8">medium</xsl:when>
			<xsl:when test="$priority &gt;= 0.9">high</xsl:when>
		</xsl:choose>
	</xsl:variable>

	<xsl:variable name="priorityLabel">
		<xsl:choose>
			<xsl:when test="$priorityLevel = 'low'"><?php _e( 'Low', 'all-in-one-seo-pack' ); ?></xsl:when>
			<xsl:when test="$priorityLevel = 'medium'"><?php _e( 'Medium', 'all-in-one-seo-pack' ); ?></xsl:when>
			<xsl:when test="$priorityLevel = 'high'"><?php _e( 'High', 'all-in-one-seo-pack' ); ?></xsl:when>
		</xsl:choose>
	</xsl:variable>

	<div>
		<xsl:attribute name="class">
			<xsl:value-of select="concat('priority priority--', $priorityLevel)" />
		</xsl:attribute>
		<xsl:value-of select="$priorityLabel" />
	</div>
</xsl:template>
Common/Views/sitemap/xsl/templates/header.php000066600000006135151135505570015320 0ustar00<?php
/**
 * XSL Header template for the sitemap.
 *
 * @since 4.1.5
 */

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

// phpcs:disable
?>
<xsl:template name="Header">
	<xsl:param name="title"/>
	<xsl:param name="amountOfURLs"/>
	<xsl:param name="fileType"/>

	<div id="content-head">
		<h1><xsl:value-of select="$title"/></h1>
		<xsl:choose>
			<xsl:when test="$fileType='RSS'">
				<p><?php echo __( 'Generated by', 'all-in-one-seo-pack' ); ?> <a href="<?php echo aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL, $data['utmMedium'] ); ?>" target="_blank" rel="noreferrer noopener"><?php echo AIOSEO_PLUGIN_NAME; ?></a>, <?php echo __( 'this is an RSS Sitemap, meant to be consumed by search engines like Google or Bing.', 'all-in-one-seo-pack' ) ?></p>
				<p>
					<?php
						// Translators: 1 - Opening HTML link tag, 2 - Closing HTML link tag.
						printf( __( 'You can find more information about RSS Sitemaps at %1$ssitemaps.org%2$s.', 'all-in-one-seo-pack' ), '<a href="https://www.sitemaps.org/" target="_blank" rel="noreferrer noopener">', '</a>');
					?>
				</p>
			</xsl:when>
			<xsl:otherwise>
				<p><?php echo __( 'Generated by', 'all-in-one-seo-pack' ); ?> <a href="<?php echo aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL, $data['utmMedium'] ); ?>" target="_blank" rel="noreferrer noopener"><?php echo AIOSEO_PLUGIN_NAME; ?></a>, <?php echo __( 'this is an XML Sitemap, meant to be consumed by search engines like Google or Bing.', 'all-in-one-seo-pack' ) ?></p>
				<p>
					<?php
						// Translators: 1 - Opening HTML link tag, 2 - Closing HTML link tag.
						printf( __( 'You can find more information about XML Sitemaps at %1$ssitemaps.org%2$s.', 'all-in-one-seo-pack' ), '<a href="https://www.sitemaps.org/" target="_blank" rel="noreferrer noopener">', '</a>');
					?>
				</p>
			</xsl:otherwise>
		</xsl:choose>
		<xsl:if test="$amountOfURLs &gt; 0">
			<p>
				<xsl:choose>
					<xsl:when test="$fileType='Sitemap' or $fileType='RSS'">
						<?php echo __( 'This sitemap contains', 'all-in-one-seo-pack' ); ?>
						<xsl:value-of select="$amountOfURLs"/>
						<xsl:choose>
							<xsl:when test="$amountOfURLs = 1">
								<?php _e( 'URL', 'all-in-one-seo-pack' ); ?>
							</xsl:when>
							<xsl:otherwise>
								<?php _e( 'URLs', 'all-in-one-seo-pack' ); ?>
							</xsl:otherwise>
						</xsl:choose>
					</xsl:when>
					<xsl:otherwise>
						<?php echo __( 'This sitemap index contains', 'all-in-one-seo-pack' ); ?>
						<xsl:value-of select="$amountOfURLs"/>
						<xsl:choose>
							<xsl:when test="$amountOfURLs = 1">
								<?php _e( 'sitemap', 'all-in-one-seo-pack' ); ?>
							</xsl:when>
							<xsl:otherwise>
								<?php _e( 'sitemaps', 'all-in-one-seo-pack' ); ?>
							</xsl:otherwise>
						</xsl:choose>
					</xsl:otherwise>
				</xsl:choose>
				<?php 
					echo sprintf(
						// Translators: 1 - The generated date, 2 - The generated time.
						__( 'and was generated on %1$s at %2$s', 'all-in-one-seo-pack' ),
						date_i18n( get_option( 'date_format' ) ),
						date_i18n( get_option( 'time_format' ) )
					); 
				?>
			</p>
		</xsl:if>
	</div>
</xsl:template>
Common/Views/sitemap/xsl/templates/empty-sitemap.php000066600000004177151135505570016672 0ustar00<?php
/**
 * XSL emptySitemap template for the sitemap.
 *
 * @since 4.1.5
 */

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

$canManageSitemap = is_user_logged_in() && aioseo()->access->hasCapability( 'aioseo_sitemap_settings' );
$adminUrl         = admin_url( 'admin.php?page=aioseo-sitemaps' );

// phpcs:disable
if ( 'xml-sitemap' !== $data['utmMedium'] ) {
	$adminUrl .= '#/' . str_replace( 'aioseo-', '', $data['utmMedium'] );
}
?>
<xsl:template name="emptySitemap">
	<?php
	if ( ! empty( $data['items'] ) ) {
		aioseo()->templates->getTemplate(
			'sitemap/xsl/partials/breadcrumb.php',
			[ 'items' => $data['items'] ]
		);
	}
	?>
	<div class="empty-sitemap">
		<h2 class="empty-sitemap__title">
			<?php _e( 'Whoops!', 'all-in-one-seo-pack' ); ?>
			<br />
			<?php _e( 'There are no posts here', 'all-in-one-seo-pack' ); ?>
		</h2>
		<div class="empty-sitemap__buttons">
			<a href="<?php echo esc_attr( home_url() ); ?>" class="button"><?php _e( 'Back to Homepage', 'all-in-one-seo-pack' ); ?></a>
			<?php if ( $canManageSitemap ) : ?>
				<a href="<?php echo esc_attr( esc_url( $adminUrl ) ); ?>" class="button"><?php _e( 'Configure Sitemap', 'all-in-one-seo-pack' ); ?></a>
			<?php endif; ?>
		</div>

		<?php if ( $canManageSitemap ) : ?>
			<div class="aioseo-alert yellow">
				<?php
					echo sprintf(
						// Translators: 1 - Opening HTML link tag, 2 - Closing HTML link tag.
						__( 'Didn\'t expect to see this? Make sure your sitemap is enabled and your content is set to be indexed. %1$sLearn More →%2$s', 'all-in-one-seo-pack' ),
						'<a target="_blank" href="' . aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'docs/how-to-fix-a-404-error-when-viewing-your-sitemap/', $data['utmMedium'], 'learn-more' ) . '">',
						'</a>'
					);
				?>
			</div>
		<?php endif; ?>
	</div>
	<style>
		.hand-magnifier{
			animation: hand-magnifier .8s infinite ease-in-out;
			transform-origin: center 90%;
			transform-box: fill-box;
		}
		@keyframes hand-magnifier {
			0% {
				transform: rotate(0deg);
			}
			50% {
				transform: rotate(-12deg);
			}
			100% {
				transform: rotate(0deg);
			}
		}
	</style>
</xsl:template>
Common/Views/sitemap/htaccess-rewrite-rules.php000066600000000735151135505570015670 0ustar00<?php
/**
 * Htaccess rewrite rules for sites using plain permalinks.
 *
 * @since 4.2.5
 */

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

// phpcs:disable
?>


# START: All in One SEO Sitemap Rewrite Rules
# Do not make edits to these rules!
<IfModule mod_rewrite.c>
	RewriteEngine On

	RewriteRule sitemap(|[0-9]+)\.xml$ /index.php [L]
	RewriteRule (default|video)-sitemap\.xsl /index.php [L]
</IfModule>
# END: All in One SEO Sitemap Rewrite RulesCommon/Views/sitemap/html/compact-archive.php000066600000001202151135505570015263 0ustar00<?php
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
// phpcs:disable Generic.ControlStructures.InlineControlStructure.NotAllowed

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<div class="aioseo-html-sitemap">
	<div class="aioseo-html-sitemap-compact-archive">
		<?php if ( empty( $data['dateArchives'] ) ) esc_html_e( 'No date archives could be found.', 'all-in-one-seo-pack' ); ?>

		<?php if ( ! empty( $data['lines'] ) ) : ?>
			<ul>
				<?php echo $data['lines']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
			</ul>
		<?php endif; ?>

	</div>
</div>Common/Views/sitemap/html/widget-options.php000066600000015531151135505570015204 0ustar00<?php
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable

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

<div class="aioseo-html-sitemap">
	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" class="aioseo-title">
			<?php esc_html_e( 'Title', 'all-in-one-seo-pack' ); ?>
		</label>
		<input
			type="text"
			id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
			name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
			value="<?php echo esc_attr( $instance['title'] ); ?>"
			class="widefat"
		/>
	</p>
	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'archives' ) ); ?>">
			<input
				type="checkbox"
				id="<?php echo esc_attr( $this->get_field_id( 'archives' ) ); ?>"
				name="<?php echo esc_attr( $this->get_field_name( 'archives' ) ); ?>"
				<?php
				if ( 'on' === $instance['archives'] ) {
					echo 'checked="checked"';
				}
				?>
				class="widefat"
			/>
			<?php esc_html_e( 'Compact Archives', 'all-in-one-seo-pack' ); ?>
		</label>
	</p>
	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'show_label' ) ); ?>">
			<input
				type="checkbox"
				id="<?php echo esc_attr( $this->get_field_id( 'show_label' ) ); ?>"
				name="<?php echo esc_attr( $this->get_field_name( 'show_label' ) ); ?>"
				<?php
				if ( 'on' === $instance['show_label'] ) {
					echo 'checked="checked"';
				}
				?>
				class="widefat"
			/>
			<?php esc_html_e( 'Show Labels', 'all-in-one-seo-pack' ); ?>
		</label>
	</p>
	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'publication_date' ) ); ?>">
			<input
				type="checkbox"
				id="<?php echo esc_attr( $this->get_field_id( 'publication_date' ) ); ?>"
				name="<?php echo esc_attr( $this->get_field_name( 'publication_date' ) ); ?>"
				<?php
				if ( 'on' === $instance['publication_date'] ) {
					echo 'checked="checked"';
				}
				?>
				class="widefat"
			/>
			<?php esc_html_e( 'Show Publication Date', 'all-in-one-seo-pack' ); ?>
		</label>
	</p>

	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'post_types' ) ); ?>" class="aioseo-title">
		<?php esc_html_e( 'Post Types', 'all-in-one-seo-pack' ); ?>
		</label>

		<div class="aioseo-columns">
			<?php foreach ( $postTypeObjects as $i => $postTypeObject ) : ?>
			<div>
				<label>
					<input
						type="checkbox"
						name="<?php echo esc_attr( $this->get_field_name( 'post_types' ) ); ?>[]"
						id="<?php echo esc_attr( $this->get_field_id( 'post_types' . $i ) ); ?>"
						<?php checked( in_array( $postTypeObject['name'], $instance['post_types'], true ) ); ?>
						value="<?php echo esc_html( $i ); ?>"
					/>
					<?php echo esc_html( $postTypeObject['label'] ); ?>
				</label>
			</div>
			<?php endforeach ?>
		</div>
	</p>

	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'taxonomies' ) ); ?>" class="aioseo-title">
		<?php esc_html_e( 'Taxonomies', 'all-in-one-seo-pack' ); ?>
		</label>

		<div class="aioseo-columns">
			<?php foreach ( $taxonomyObjects as $i => $taxonomyObject ) : ?>
			<div>
				<label>
					<input
						type="checkbox"
						name="<?php echo esc_attr( $this->get_field_name( 'taxonomies' ) ); ?>[]"
						id="<?php echo esc_attr( $this->get_field_id( 'taxonomies' . $i ) ); ?>"
						<?php checked( in_array( $taxonomyObject['name'], $instance['taxonomies'], true ) ); ?>
						value="<?php echo esc_html( $i ); ?>"
					/>
					<?php echo esc_html( $taxonomyObject['label'] ); ?>
				</label>
			</div>
			<?php endforeach ?>
		</div>
	</p>

	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'order_by' ) ); ?>" class="aioseo-title">
			<?php esc_html_e( 'Sort Order', 'all-in-one-seo-pack' ); ?>
		</label>
		<select name="<?php echo esc_attr( $this->get_field_name( 'order_by' ) ); ?>" id="<?php echo esc_attr( $this->get_field_id( 'order_by' ) ); ?>" class="widefat">
			<option value="publish_date"<?php selected( 'publish_date', $instance['order_by'], true ); ?>>
				<?php esc_html_e( 'Publish Date', 'all-in-one-seo-pack' ); ?>
			</option>
			<option value="last_updated"<?php selected( 'last_updated', $instance['order_by'], true ); ?>>
				<?php esc_html_e( 'Last Updated', 'all-in-one-seo-pack' ); ?>
			</option>
			<option value="alphabetical"<?php selected( 'alphabetical', $instance['order_by'], true ); ?>>
				<?php esc_html_e( 'Alphabetical', 'all-in-one-seo-pack' ); ?>
			</option>
			<option value="id"<?php selected( 'id', $instance['order_by'], true ); ?>>
				<?php esc_html_e( 'ID', 'all-in-one-seo-pack' ); ?>
			</option>
		</select>
	</p>
	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'order' ) ); ?>" class="aioseo-title">
			<?php esc_html_e( 'Sort Direction', 'all-in-one-seo-pack' ); ?>
		</label>
		<select name="<?php echo esc_attr( $this->get_field_name( 'order' ) ); ?>" id="<?php echo esc_attr( $this->get_field_id( 'order' ) ); ?>" class="widefat">
			<option value="asc"<?php echo ( 'asc' === $instance['order'] ) ? ' selected="selected"' : '' ?>><?php esc_html_e( 'Ascending', 'all-in-one-seo-pack' ); ?></option>
			<option value="desc"<?php echo ( 'desc' === $instance['order'] ) ? ' selected="selected"' : '' ?>"><?php esc_html_e( 'Descending', 'all-in-one-seo-pack' ); ?></option>
		</select>
	</p>

	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'excluded_posts' ) ); ?>" class="aioseo-title">
			<?php esc_html_e( 'Exclude Posts / Pages', 'all-in-one-seo-pack' ); ?>
		</label>
		<input
			type="text"
			value="<?php echo esc_attr( $instance['excluded_posts'] ); ?>"
			name="<?php echo esc_attr( $this->get_field_name( 'excluded_posts' ) ); ?>"
			id="<?php echo esc_attr( $this->get_field_id( 'excluded_posts' ) ); ?>"
			class="widefat"
		/>
		<p class="aioseo-description"><?php esc_html_e( 'Enter a comma-separated list of post IDs.', 'all-in-one-seo-pack' ); ?></p>
	</p>

	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'excluded_terms' ) ); ?>" class="aioseo-title">
			<?php esc_html_e( 'Exclude Terms', 'all-in-one-seo-pack' ); ?>
		</label>
		<input
			type="text"
			value="<?php echo esc_attr( $instance['excluded_terms'] ); ?>"
			name="<?php echo esc_attr( $this->get_field_name( 'excluded_terms' ) ); ?>"
			id="<?php echo esc_attr( $this->get_field_id( 'excluded_terms' ) ); ?>"
			class="widefat"
		/>
		<p class="aioseo-description"><?php esc_html_e( 'Enter a comma-separated list of term IDs.', 'all-in-one-seo-pack' ); ?></p>
	</p>
</div>

<style>
	.aioseo-html-sitemap label.aioseo-title,
	.aioseo-html-sitemap label.aioseo-title select {
		color: #141B38 !important;
		font-weight: bold !important;
	}
	.aioseo-html-sitemap .aioseo-description {
		margin-top: -5px;
		font-style: italic;
		font-size: 13px;
	}
	.aioseo-html-sitemap select, .aioseo-html-sitemap input[type=text] {
		margin-top: 8px;
	}
	.aioseo-html-sitemap .aioseo-columns {
		display: flex;
		flex-wrap: wrap;
	}
	.aioseo-html-sitemap .aioseo-columns div {
		flex: 0 0 50%;
	}
</style>Common/Views/sitemap/xml/default.php000066600000003620151135505570013504 0ustar00<?php
/**
 * XML template for our sitemap index pages.
 *
 * @since 4.0.0
 */

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

// phpcs:disable
?>
<urlset
	xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
	xmlns:xhtml="http://www.w3.org/1999/xhtml"
<?php if ( ! aioseo()->sitemap->helpers->excludeImages() ): ?>
	xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
<?php endif; ?>
>
<?php foreach ( $entries as $entry ) {
	if ( empty( $entry['loc'] ) ) {
		continue;
	}
	?>
	<url>
		<loc><?php aioseo()->sitemap->output->escapeAndEcho( $entry['loc'] ); ?></loc><?php
	if ( ! empty( $entry['lastmod'] ) ) {
			?>

		<lastmod><?php aioseo()->sitemap->output->escapeAndEcho( $entry['lastmod'] ); ?></lastmod><?php
	}
	if ( ! empty( $entry['changefreq'] ) ) {
			?>

		<changefreq><?php aioseo()->sitemap->output->escapeAndEcho( $entry['changefreq'] ); ?></changefreq><?php
	}
	if ( isset( $entry['priority'] ) ) {
			?>

		<priority><?php aioseo()->sitemap->output->escapeAndEcho( $entry['priority'] ); ?></priority><?php
	}
	if ( ! empty( $entry['languages'] ) ) {
		foreach ( $entry['languages'] as $subentry ) {
			if ( empty( $subentry['language'] ) || empty( $subentry['location'] ) ) {
				continue;
			}
		?>

		<xhtml:link rel="alternate" hreflang="<?php echo esc_attr( $subentry['language'] ); ?>" href="<?php echo esc_url( $subentry['location'] ); ?>" /><?php
		}
	}
	if ( ! aioseo()->sitemap->helpers->excludeImages() && ! empty( $entry['images'] ) ) {
			foreach ( $entry['images'] as $image ) {
				$image = (array) $image;
			?>

		<image:image>
			<image:loc>
				<?php
				if ( aioseo()->helpers->isRelativeUrl( $image['image:loc'] ) ) {
					$image['image:loc'] = aioseo()->helpers->makeUrlAbsolute( $image['image:loc'] );
				}

				aioseo()->sitemap->output->escapeAndEcho( $image['image:loc'] );
				?>
			</image:loc>
		</image:image><?php
		}
	}
	?>

	</url>
<?php } ?>
</urlset>
Common/Views/sitemap/xml/rss.php000066600000003306151135505570012670 0ustar00<?php
/**
 * XML template for the RSS Sitemap.
 *
 * @since 4.0.0
 */

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

// phpcs:disable
?>

<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel><?php
	// Yandex doesn't support some tags so we need to check the user agent.
	if ( ! aioseo()->helpers->isYandexUserAgent() ) {
		?>

		<title><?php aioseo()->sitemap->output->escapeAndEcho( $title, false ); ?></title>
		<link><?php aioseo()->sitemap->output->escapeAndEcho( $link ); ?></link>
		<?php if ( $description ) {
		?><description><?php aioseo()->sitemap->output->escapeAndEcho( $description ); ?></description>
		<?php }
		?><?php if ( ! empty( $entries[0]['pubDate'] ) ) {
		?><lastBuildDate><?php aioseo()->sitemap->output->escapeAndEcho( $entries[0]['pubDate'] ); ?></lastBuildDate>
		<?php }
		?><docs>https://validator.w3.org/feed/docs/rss2.html</docs>
		<atom:link href="<?php echo aioseo()->sitemap->helpers->getUrl( 'rss' ); ?>" rel="self" type="application/rss+xml" />
		<ttl><?php aioseo()->sitemap->output->escapeAndEcho( $ttl ); ?></ttl>

<?php }
foreach ( $entries as $entry ) {
		if ( empty( $entry['guid'] ) ) {
			continue;
			}?>
		<item>
			<guid><?php aioseo()->sitemap->output->escapeAndEcho( $entry['guid'] ); ?></guid>
			<link><?php aioseo()->sitemap->output->escapeAndEcho( $entry['guid'] ); ?></link><?php
			if ( ! empty( $entry['title'] ) ) {
				?>

			<title><?php aioseo()->sitemap->output->escapeAndEcho( $entry['title'], false ); ?></title><?php
			}
			if ( ! empty( $entry['pubDate'] ) ) {
				?>

			<pubDate><?php aioseo()->sitemap->output->escapeAndEcho( $entry['pubDate'] ); ?></pubDate><?php
			}
			?>

		</item>
			<?php } ?>
	</channel>
</rss>
Common/Views/sitemap/xml/root.php000066600000001147151135505570013045 0ustar00<?php
/**
 * XML template for our root index page.
 *
 * @since 4.0.0
 */

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

 // phpcs:disable
?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<?php foreach ( $entries as $entry ) {
	if ( empty( $entry['loc'] ) ) {
		continue;
	}
	?>
	<sitemap>
		<loc><?php aioseo()->sitemap->output->escapeAndEcho( $entry['loc'] ); ?></loc><?php
	if ( ! empty( $entry['lastmod'] ) ) {
			?>

		<lastmod><?php aioseo()->sitemap->output->escapeAndEcho( $entry['lastmod'] ); ?></lastmod><?php
		}
	?>

	</sitemap>
<?php } ?>
</sitemapindex>
Common/Views/parts/loader.php000066600000002454151135505570012221 0ustar00<?php
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<style>
.aioseo-loading-spinner {
	width: 35px;
	height: 35px;
	position: absolute;
}

.aioseo-loading-spinner .double-bounce1,
.aioseo-loading-spinner .double-bounce2 {
	width: 100%;
	height: 100%;
	border-radius: 50%;
	background-color: #fff;
	opacity: 0.6;
	position: absolute;
	top: 0;
	left: 0;

	-webkit-animation: aioseo-sk-bounce 1.3s infinite ease-in-out;
	animation: aioseo-sk-bounce 1.3s infinite ease-in-out;
}

.aioseo-loading-spinner.dark .double-bounce1,
.aioseo-loading-spinner.dark .double-bounce2 {
	background-color: #8C8F9A;
}

.aioseo-loading-spinner .double-bounce2 {
	-webkit-animation-delay: -0.65s;
	animation-delay: -0.65s;
}

.aioseo-loading-spinner {}
.aioseo-loading-spinner {}

@-webkit-keyframes aioseo-sk-bounce {
	0%, 100% { -webkit-transform: scale(0.0) }
	50% { -webkit-transform: scale(1.0) }
}

@keyframes aioseo-sk-bounce {
	0%, 100% {
		transform: scale(0.0);
		-webkit-transform: scale(0.0);
	} 50% {
		transform: scale(1.0);
		-webkit-transform: scale(1.0);
	}
}
</style>
<div style="height:50px; position:relative;">
	<div class="aioseo-loading-spinner dark" style="top:calc( 50% - 17px);left:calc( 50% - 17px);">
		<div class="double-bounce1"></div>
		<div class="double-bounce2"></div>
	</div>
</div>Common/Views/report/summary.php000066600000164475151135505570012646 0ustar00<?php
/**
 * Summary report view.
 *
 * @since 4.7.2
 */

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

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
// phpcs:disable Generic.Files.LineLength.MaxExceeded
?>
<div style="background-color: #f3f4f5; color: #141b38; font-family: Helvetica, Roboto, Arial, sans-serif; font-size: 14px; line-height: 22px; margin: 0; padding: 0;">
	<span style="display: none !important; visibility: hidden; opacity: 0; height: 0; width: 0;"><?php echo $preHeader ?? '' ?></span>

	<div style="margin: 0 auto; padding: 70px 0; width: 100%; max-width: 680px;">
		<div style="background-color: #ffffff; border: 1px solid #e8e8eb;">
			<div style="padding-left: 20px; padding-right: 20px; padding-bottom: 20px;">
				<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
					<thead>
					<tr>
						<th style="padding: 0; width: 60%; line-height: 1;"></th>
						<th style="padding: 0; width: 40%; line-height: 1;"></th>
					</tr>
					</thead>

					<tbody>
					<tr>
						<td style="padding: 0;">
							<div style="padding-top: 20px;">
								<img
										style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none;"
										width="100"
										height="20"
										src="https://static.aioseo.io/report/ste/text-logo.jpg"
										alt="<?php echo esc_attr( AIOSEO_PLUGIN_SHORT_NAME ) ?>"
								/>
							</div>
						</td>

						<td style="padding: 0; word-break: break-word;">
							<div style="padding-top: 20px; font-size: 12px; text-align: right; line-height: 15px;"><?php echo $dateRange['range'] ?? ''; ?></div>
						</td>
					</tr>

					<tr>
						<td style="padding: 0; word-break: break-word;">
							<div style="padding-top: 10px;">
								<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;"><?php echo $heading ?? ''; ?></p>
							</div>
						</td>

						<td style="padding: 0; word-break: break-word;">
							<div style="padding-top: 10px; font-size: 12px; text-align: right; line-height: 15px;">
								<a
										href="<?php echo site_url(); ?>"
										style="color: #005ae0; font-weight: normal; text-decoration: none;"
								><?php echo site_url(); ?></a>
							</div>
						</td>
					</tr>
					</tbody>
				</table>
			</div>

			<div style="background-color: #004f9d; padding-bottom: 20px;">
				<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
					<thead>
					<tr>
						<th style="padding: 0; width: 70%; line-height: 1;"></th>
						<th style="padding: 0; width: 30%; line-height: 1;"></th>
					</tr>
					</thead>

					<tbody>
					<tr>
						<td style="padding: 0; word-break: break-word;">
							<div style="padding-right: 20px; padding-left: 20px; padding-top: 20px; line-height: 1;">
								<span style="color: #ffffff; margin-right: 3px; font-weight: 700; font-size: 28px; vertical-align: middle;"><?php esc_html_e( 'Hi there!', 'all-in-one-seo-pack' ); ?></span>

								<img
										style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
										width="28"
										height="28"
										src="https://static.aioseo.io/report/ste/emoji-1f44b.png"
										alt="Waving Hand Sign"
								/>
							</div>

							<div style="color: #ffffff; padding-right: 20px; padding-left: 20px; padding-top: 20px; font-size: 20px; line-height: 26px;  font-weight: 400;">
								<?php echo $subheading ?? ''; ?>
							</div>
						</td>

						<td style="padding: 0; text-align: right; word-break: break-word;">
							<img
									style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; padding-top: 20px;"
									width="142"
									height="140"
									src="<?php echo esc_attr( 'https://static.aioseo.io/report/ste/' . ( $iconCalendar ?? '' ) . '.png' ) ?>"
									alt=""
							/>
						</td>
					</tr>
					</tbody>
				</table>
			</div>
		</div>

		<?php if ( aioseo()->siteHealth->shouldUpdate() ) { ?>
			<div style="margin-top: 20px;">
				<div style="text-align: center; font-size: 14px; border-radius: 4px; margin: 0; padding: 8px 12px; background-color: #fffbeb; border: 1px solid #f18200;">
					<?php
					printf(
						// Translators: 1 - The plugin short name ("AIOSEO"), 2 - Opening link tag, 3 - HTML arrow, 4 - Closing link tag.
						__( 'An update is available for %1$s. %2$sUpgrade to the latest version%3$s%4$s', 'all-in-one-seo-pack' ),
						AIOSEO_PLUGIN_SHORT_NAME,
						'<a href="' . ( $links['update'] ?? '#' ) . '" style="color: #005ae0; font-weight: normal; text-decoration: underline;">',
						'&nbsp;&rarr;',
						'</a>'
					)
					?>
				</div>
			</div>
		<?php } ?>

		<div style="background-color: #ffffff; border: 1px solid #e8e8eb; margin-top: 20px;">
			<div style="border-bottom: 1px solid #e5e5e5; padding: 15px 20px;">
				<img
						style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-right: 6px;"
						width="35"
						height="35"
						src="https://static.aioseo.io/report/ste/icon-report.png"
						alt=""
				/>

				<h2 style="font-size: 20px; font-weight: 700; line-height: 24px; margin-bottom: 0; margin-top: 0; vertical-align: middle; display: inline-block;"><?php esc_html_e( 'SEO Report', 'all-in-one-seo-pack' ); ?></h2>
			</div>

			<div style="padding: 20px;">
				<?php if ( ! empty( $statisticsReport['posts']['winning'] ) ) { ?>
					<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
						<tbody>
						<tr>
							<td style="padding: 0; word-break: break-word;">
								<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;"><?php esc_html_e( 'Top Winning Posts', 'all-in-one-seo-pack' ); ?></p>
							</td>

							<td style="text-align: right; word-break: break-word;">
								<?php if ( ! empty( $statisticsReport['posts']['winning']['url'] ) ) { ?>
									<a
											href="<?php echo esc_attr( $statisticsReport['posts']['winning']['url'] ) ?>"
											style="color: #005ae0; font-weight: 700; text-decoration: underline;"
									>
										<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
									</a>
								<?php } ?>
							</td>
						</tr>
						</tbody>
					</table>

					<div style="margin-top: 16px; overflow-x: auto;">
						<table style="min-width: 460px; table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
							<thead>
							<tr style="border-color: #ffffff; border-bottom-width: 6px; border-bottom-style: solid;">
								<th style="width: 59%; background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; border-top-left-radius: 2px; border-bottom-left-radius: 2px; line-height: 1;">
									<?php esc_html_e( 'Post', 'all-in-one-seo-pack' ); ?>
								</th>

								<th style="width: <?php echo $statisticsReport['posts']['winning']['show_tru_seo'] ? '17%' : '0' ?>; text-align: center; background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
									<?php if ( $statisticsReport['posts']['winning']['show_tru_seo'] ) { ?>
										TruSEO
									<?php } ?>
								</th>

								<th style="width: 12%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960;  line-height: 1;">
									<?php esc_html_e( 'Clicks', 'all-in-one-seo-pack' ); ?>
								</th>

								<th style="width: 12%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; border-top-right-radius: 2px; border-bottom-right-radius: 2px; line-height: 1;">
									<?php esc_html_e( 'Diff', 'all-in-one-seo-pack' ); ?>
								</th>
							</tr>
							</thead>

							<tbody>
							<?php foreach ( ( $statisticsReport['posts']['winning']['items'] ?? [] ) as $i => $item ) { ?>
								<tr style="<?php echo 0 === $i % 2 ? 'background-color: #f3f4f5;' : '' ?>">
									<td style="padding: 0; word-break: break-word;">
										<div style="padding: 6px; font-size: 14px;">
											<a style="color: #141b38; font-weight: normal; text-decoration: none;"<?php echo ! empty( $item['url'] ) ? ' href="' . esc_attr( $item['url'] ) . '"' : '' ?>>
												<?php echo $item['title']; ?>
											</a>
										</div>
									</td>

									<td style="padding: 0; word-break: break-word;">
										<?php if ( ! empty( $item['tru_seo'] ) ) { ?>
											<div style="padding: 6px;">
												<div style="width: 45px; padding: 6px; margin-left: auto; margin-right: auto; font-size: 12px; text-align: center; line-height: 1; border-radius: 4px; border-width: 1px; border-style: solid; <?php echo "color: {$item['tru_seo']['color']}"; ?>">
													<?php echo $item['tru_seo']['text']; ?>
												</div>
											</div>
										<?php } ?>
									</td>

									<td style="padding: 0; word-break: break-word;">
										<div style="padding: 6px; font-size: 14px;">
											<?php echo $item['clicks']; ?>
										</div>
									</td>

									<td style="padding: 0; word-break: break-word;">
										<div style="padding: 6px; font-size: 0; <?php echo "color: {$item['difference']['clicks']['color']}"; ?>">
											<?php if ( '#00aa63' === $item['difference']['clicks']['color'] ) { ?>
												<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-bottom-color: #00aa63; border-bottom-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-top-width: 0;"></div>
											<?php } ?>

											<?php if ( '#df2a4a' === $item['difference']['clicks']['color'] ) { ?>
												<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-top-color: #df2a4a; border-top-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-bottom-width: 0;"></div>
											<?php } ?>

											<span style="display: inline-block; vertical-align: middle; font-size: 14px;"><?php echo $item['difference']['clicks']['text']; ?></span>
										</div>
									</td>
								</tr>
							<?php } ?>
							</tbody>
						</table>
					</div>
				<?php } ?>

				<?php if ( ! empty( $statisticsReport['posts']['losing'] ) ) { ?>
					<?php if ( ! empty( $statisticsReport['posts']['winning'] ) ) { ?>
						<div style="margin-top: 20px; margin-bottom: 20px; border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
					<?php } ?>

					<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
						<tbody>
						<tr>
							<td style="padding: 0; word-break: break-word;">
								<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;"><?php esc_html_e( 'Top Losing Posts', 'all-in-one-seo-pack' ); ?></p>
							</td>

							<td style="text-align: right; word-break: break-word;">
								<?php if ( ! empty( $statisticsReport['posts']['losing']['url'] ) ) { ?>
									<a
											href="<?php echo esc_attr( $statisticsReport['posts']['losing']['url'] ) ?>"
											style="color: #005ae0; font-weight: 700; text-decoration: underline;"
									>
										<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
									</a>
								<?php } ?>
							</td>
						</tr>
						</tbody>
					</table>

					<div style="margin-top: 16px; overflow-x: auto;">
						<table style="table-layout: fixed; min-width: 460px; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
							<thead>
							<tr style="border-color: #ffffff; border-bottom-width: 6px; border-bottom-style: solid;">
								<th style="width: 59%; background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; border-top-left-radius: 2px; border-bottom-left-radius: 2px; line-height: 1;">
									<?php esc_html_e( 'Post', 'all-in-one-seo-pack' ); ?>
								</th>

								<th style="width: <?php echo $statisticsReport['posts']['losing']['show_tru_seo'] ? '17%' : '0' ?>; text-align: center; background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
									<?php if ( $statisticsReport['posts']['losing']['show_tru_seo'] ) { ?>
										TruSEO
									<?php } ?>
								</th>

								<th style="width: 12%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
									<?php esc_html_e( 'Clicks', 'all-in-one-seo-pack' ); ?>
								</th>

								<th style="width: 12%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; border-top-right-radius: 2px; border-bottom-right-radius: 2px; line-height: 1;">
									<?php esc_html_e( 'Diff', 'all-in-one-seo-pack' ); ?>
								</th>
							</tr>
							</thead>

							<tbody>
							<?php foreach ( ( $statisticsReport['posts']['losing']['items'] ?? [] ) as $i => $item ) { ?>
								<tr style="<?php echo 0 === $i % 2 ? 'background-color: #f3f4f5;' : '' ?>">
									<td style="padding: 0; word-break: break-word;">
										<div style="padding: 6px; font-size: 14px;">
											<a style="color: #141b38; font-weight: normal; text-decoration: none;"<?php echo ! empty( $item['url'] ) ? ' href="' . esc_attr( $item['url'] ) . '"' : '' ?>>
												<?php echo $item['title']; ?>
											</a>
										</div>
									</td>

									<td style="padding: 0; word-break: break-word;">
										<?php if ( ! empty( $item['tru_seo'] ) ) { ?>
											<div style="padding: 6px;">
												<div style="width: 45px; padding: 6px; margin-left: auto; margin-right: auto; font-size: 12px; text-align: center; line-height: 1; border-radius: 4px; border-width: 1px; border-style: solid; <?php echo "color: {$item['tru_seo']['color']}"; ?>">
													<?php echo $item['tru_seo']['text']; ?>
												</div>
											</div>
										<?php } ?>
									</td>

									<td style="padding: 0; word-break: break-word;">
										<div style="padding: 6px; font-size: 14px;">
											<?php echo $item['clicks']; ?>
										</div>
									</td>

									<td style="padding: 0; word-break: break-word;">
										<div style="padding: 6px; font-size: 0; <?php echo "color: {$item['difference']['clicks']['color']}"; ?>">
											<?php if ( '#00aa63' === $item['difference']['clicks']['color'] ) { ?>
												<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-bottom-color: #00aa63; border-bottom-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-top-width: 0;"></div>
											<?php } ?>

											<?php if ( '#df2a4a' === $item['difference']['clicks']['color'] ) { ?>
												<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-top-color: #df2a4a; border-top-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-bottom-width: 0;"></div>
											<?php } ?>

											<span style="display: inline-block; vertical-align: middle; font-size: 14px;"><?php echo $item['difference']['clicks']['text']; ?></span>
										</div>
									</td>
								</tr>
							<?php } ?>
							</tbody>
						</table>
					</div>
				<?php } ?>

				<?php if ( ! empty( $statisticsReport['keywords']['winning'] ) || ! empty( $statisticsReport['keywords']['losing'] ) ) { ?>
					<?php if ( ! empty( $statisticsReport['posts']['winning'] ) || ! empty( $statisticsReport['posts']['losing'] ) ) { ?>
						<div style="margin-top: 20px; margin-bottom: 20px; border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
					<?php } ?>
					<div style="overflow-x: auto;">
						<table style="min-width: 600px; table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
							<thead>
							<tr>
								<th style="width: 47%; line-height: 1;"></th>
								<th style="width: 6%; line-height: 1;"></th>
								<th style="width: 47%; line-height: 1;"></th>
							</tr>
							</thead>

							<tbody>
							<tr style="height: 1px;">
								<td style="vertical-align: top; word-break: break-word;">
									<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
										<tbody>
										<tr>
											<td style="padding: 0; word-break: break-word;">
												<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;">
													<?php esc_html_e( 'Top Winning Keywords', 'all-in-one-seo-pack' ); ?>
												</p>
											</td>

											<td style="text-align: right; word-break: break-word;">
												<?php if ( ! empty( $statisticsReport['keywords']['winning']['url'] ) ) { ?>
													<a
															href="<?php echo esc_attr( $statisticsReport['keywords']['winning']['url'] ) ?>"
															style="color: #005ae0; font-weight: 700; text-decoration: underline;"
													>
														<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
													</a>
												<?php } ?>
											</td>
										</tr>
										</tbody>
									</table>

									<table style="margin-top: 16px; table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
										<thead>
										<tr style="border-color: #ffffff; border-bottom-width: 6px; border-bottom-style: solid;">
											<th style="width: 64%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; border-top-left-radius: 2px; border-bottom-left-radius: 2px; line-height: 1;">
												<?php esc_html_e( 'Keyword', 'all-in-one-seo-pack' ); ?>
											</th>
											<th style="width: 16%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
												<?php esc_html_e( 'Clicks', 'all-in-one-seo-pack' ); ?>
											</th>
											<th style="width: 20%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; border-top-right-radius: 2px; border-bottom-right-radius: 2px; line-height: 1;">
												<?php esc_html_e( 'Diff', 'all-in-one-seo-pack' ); ?>
											</th>
										</tr>
										</thead>

										<tbody>
										<?php foreach ( ( $statisticsReport['keywords']['winning']['items'] ?? [] ) as $i => $item ) { ?>
											<tr style="<?php echo 0 === $i % 2 ? 'background-color: #f3f4f5;' : '' ?>">
												<td style="padding: 0; word-break: break-word;">
													<div style="padding: 6px; font-size: 14px;">
														<?php echo $item['title']; ?>
													</div>
												</td>

												<td style="padding: 0; word-break: break-word;">
													<div style="padding: 6px; font-size: 14px;">
														<?php echo $item['clicks']; ?>
													</div>
												</td>

												<td style="padding: 0; word-break: break-word;">
													<div style="padding: 6px; font-size: 0; <?php echo "color: {$item['difference']['clicks']['color']}"; ?>">
														<?php if ( '#00aa63' === $item['difference']['clicks']['color'] ) { ?>
															<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-bottom-color: #00aa63; border-bottom-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-top-width: 0;"></div>
														<?php } ?>

														<?php if ( '#df2a4a' === $item['difference']['clicks']['color'] ) { ?>
															<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-top-color: #df2a4a; border-top-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-bottom-width: 0;"></div>
														<?php } ?>

														<span style="display: inline-block; vertical-align: middle; font-size: 14px;"><?php echo $item['difference']['clicks']['text']; ?></span>
													</div>
												</td>
											</tr>
										<?php } ?>
										</tbody>
									</table>
								</td>

								<td style="height: inherit; padding: 0; text-align: center; vertical-align: baseline; overflow: hidden; word-break: break-word;">
									<div style="width: 1px; margin-left: auto; margin-right: auto; background-color: #e5e5e5; height: 100%;"></div>
								</td>

								<td style="vertical-align: top; word-break: break-word;">
									<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
										<tbody>
										<tr>
											<td style="padding: 0; word-break: break-word;">
												<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;">
													<?php esc_html_e( 'Top Losing Keywords', 'all-in-one-seo-pack' ); ?>
												</p>
											</td>

											<td style="text-align: right; word-break: break-word;">
												<?php if ( ! empty( $statisticsReport['keywords']['losing']['url'] ) ) { ?>
													<a
															href="<?php echo esc_attr( $statisticsReport['keywords']['losing']['url'] ) ?>"
															style="color: #005ae0; font-weight: 700; text-decoration: underline;"
													>
														<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
													</a>
												<?php } ?>
											</td>
										</tr>
										</tbody>
									</table>

									<table style="margin-top: 16px; table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
										<thead>
										<tr style="border-color: #ffffff; border-bottom-width: 6px; border-bottom-style: solid;">
											<th style="width: 64%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; border-top-left-radius: 2px; border-bottom-left-radius: 2px; line-height: 1;">
												<?php esc_html_e( 'Keyword', 'all-in-one-seo-pack' ); ?>
											</th>
											<th style="width: 16%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
												<?php esc_html_e( 'Clicks', 'all-in-one-seo-pack' ); ?>
											</th>
											<th style="width: 20%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; border-top-right-radius: 2px; border-bottom-right-radius: 2px; line-height: 1;">
												<?php esc_html_e( 'Diff', 'all-in-one-seo-pack' ); ?>
											</th>
										</tr>
										</thead>

										<tbody>
										<?php foreach ( ( $statisticsReport['keywords']['losing']['items'] ?? [] ) as $i => $item ) { ?>
											<tr style="<?php echo 0 === $i % 2 ? 'background-color: #f3f4f5;' : '' ?>">
												<td style="padding: 0; word-break: break-word;">
													<div style="padding: 6px; font-size: 14px;">
														<?php echo $item['title']; ?>
													</div>
												</td>

												<td style="padding: 0; word-break: break-word;">
													<div style="padding: 6px; font-size: 14px;">
														<?php echo $item['clicks']; ?>
													</div>
												</td>

												<td style="padding: 0; word-break: break-word;">
													<div style="padding: 6px; font-size: 0; <?php echo "color: {$item['difference']['clicks']['color']}"; ?>">
														<?php if ( '#00aa63' === $item['difference']['clicks']['color'] ) { ?>
															<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-bottom-color: #00aa63; border-bottom-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-top-width: 0;"></div>
														<?php } ?>

														<?php if ( '#df2a4a' === $item['difference']['clicks']['color'] ) { ?>
															<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-top-color: #df2a4a; border-top-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-bottom-width: 0;"></div>
														<?php } ?>

														<span style="display: inline-block; vertical-align: middle; font-size: 14px;"><?php echo $item['difference']['clicks']['text']; ?></span>
													</div>
												</td>
											</tr>
										<?php } ?>
										</tbody>
									</table>
								</td>
							</tr>
							</tbody>
						</table>
					</div>
				<?php } ?>

				<?php if ( ! empty( $upsell['search-statistics'] ) ) { ?>
					<div>
						<img
								style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
								width="638"
								height="146"
								src="https://static.aioseo.io/report/ste/banner-search-statistics-cta-upsell.jpg"
								alt=""
						/>

						<p style="font-size: 16px; margin-bottom: 0; margin-top: 20px; text-align: center;">
							<?php esc_html_e( 'Connect your site to Google Search Console to receive insights on how content is being discovered. Identify areas for improvement and drive traffic to your website.', 'all-in-one-seo-pack' ); ?>
						</p>

						<div style="width: 475px; max-width: 96%; margin-top: 20px; margin-left: auto; margin-right: auto;">
							<div style="width: 210px; padding: 6px; display: inline-block; vertical-align: middle;">
								<img
										style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-right: 3px;"
										width="17"
										height="17"
										src="https://static.aioseo.io/report/ste/icon-check-circle-out.png"
										alt="&#10003;"
								/>

								<span style="display: inline-block; vertical-align: middle; line-height: 20px; max-width: 185px;"><?php esc_html_e( 'Search traffic insights', 'all-in-one-seo-pack' ); ?></span>
							</div>

							<div style="width: 210px; padding: 6px; display: inline-block; vertical-align: middle;">
								<img
										style="margin-right: 3px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
										width="17"
										height="17"
										src="https://static.aioseo.io/report/ste/icon-check-circle-out.png"
										alt="&#10003;"
								/>

								<span style="display: inline-block; vertical-align: middle; line-height: 20px; max-width: 185px;"><?php esc_html_e( 'Track page rankings', 'all-in-one-seo-pack' ); ?></span>
							</div>

							<div style="width: 210px; padding: 6px; display: inline-block; vertical-align: middle;">
								<img
										style="margin-right: 3px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
										width="17"
										height="17"
										src="https://static.aioseo.io/report/ste/icon-check-circle-out.png"
										alt="&#10003;"
								/>

								<span style="display: inline-block; vertical-align: middle; line-height: 20px; max-width: 185px;"><?php esc_html_e( 'Track keyword rankings', 'all-in-one-seo-pack' ); ?></span>
							</div>

							<div style="width: 210px; padding: 6px; display: inline-block; vertical-align: middle;">
								<img
										style="margin-right: 3px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
										width="17"
										height="17"
										src="https://static.aioseo.io/report/ste/icon-check-circle-out.png"
										alt="&#10003;"
								/>

								<span style="display: inline-block; vertical-align: middle; line-height: 20px; max-width: 185px;"><?php esc_html_e( 'Speed tests for individual pages/posts', 'all-in-one-seo-pack' ); ?></span>
							</div>
						</div>

						<div style="margin-top: 20px; text-align: center;">
							<a
									href="<?php echo esc_attr( $upsell['search-statistics']['cta']['url'] ) ?>"
									style="border-radius: 4px; border: none; display: inline-block; font-size: 14px; font-style: normal; font-weight: 700; text-align: center; text-decoration: none; user-select: none; vertical-align: middle; background-color: #00aa63; color: #ffffff; padding: 8px 20px;"
							>
								<?php echo $upsell['search-statistics']['cta']['text']; ?>
							</a>
						</div>
					</div>
				<?php } ?>

				<?php if ( empty( $upsell['search-statistics'] ) && ! empty( $statisticsReport['cta'] ) ) { ?>
					<div style="margin-top: 20px; margin-bottom: 20px; border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>

					<div style="text-align: center;">
						<a
								href="<?php echo esc_attr( $statisticsReport['cta']['url'] ) ?>"
								style="border-radius: 4px; border: none; display: inline-block; font-size: 14px; font-style: normal; font-weight: 700; text-align: center; text-decoration: none; user-select: none; vertical-align: middle; background-color: #005ae0; color: #ffffff; padding: 8px 20px;"
						>
							<?php echo $statisticsReport['cta']['text']; ?>
						</a>
					</div>
				<?php } ?>
			</div>
		</div>

		<?php if ( ! empty( $posts ) ) { ?>
			<div style="background-color: #ffffff; border: 1px solid #e8e8eb; margin-top: 20px;">
				<div style="border-bottom: 1px solid #e5e5e5; padding: 15px 20px;">
					<img
							style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-right: 6px;"
							width="35"
							height="35"
							src="https://static.aioseo.io/report/ste/icon-summary.png"
							alt=""
					/>

					<h2 style="font-size: 20px; font-weight: 700; line-height: 24px; margin-bottom: 0; margin-top: 0; vertical-align: middle; display: inline-block;"><?php esc_html_e( 'Content Summary', 'all-in-one-seo-pack' ); ?></h2>
				</div>

				<div style="padding: 20px;">
					<?php if ( ! empty( $posts['publish']['items'] ) ) { ?>
						<div>
							<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
								<tbody>
								<tr>
									<td style="padding: 0; word-break: break-word;">
										<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;">
											<?php esc_html_e( 'Most Recent Published', 'all-in-one-seo-pack' ); ?>
										</p>
									</td>

									<td style="text-align: right; word-break: break-word;">
										<a
												href="<?php echo esc_attr( $posts['publish']['url'] ) ?>"
												style="color: #005ae0; font-weight: 700; text-decoration: underline;"
										>
											<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
										</a>
									</td>
								</tr>
								</tbody>
							</table>

							<div style="margin-top: 16px; overflow-x: auto;">
								<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
									<thead>
									<tr>
										<th style="background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; border-top-left-radius: 2px; border-bottom-left-radius: 2px; width: 210px; line-height: 1;">
											<?php esc_html_e( 'Post', 'all-in-one-seo-pack' ); ?>
										</th>

										<th style="text-align: center; background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1; width: <?php echo $posts['publish']['show_tru_seo'] ? '115px' : '0' ?>">
											<?php if ( $posts['publish']['show_tru_seo'] ) { ?>
												TruSEO
											<?php } ?>
										</th>

										<th style="background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; border-top-right-radius: 2px; border-bottom-right-radius: 2px; line-height: 1; width: <?php echo $posts['publish']['show_stats'] ? '135px' : '0' ?>">
											<?php if ( $posts['publish']['show_stats'] ) { ?>
												<?php esc_html_e( 'Stats', 'all-in-one-seo-pack' ); ?>
											<?php } ?>
										</th>
									</tr>
									</thead>

									<tbody>
									<?php foreach ( $posts['publish']['items'] as $i => $item ) { ?>
										<?php if ( $i > 0 ) { ?>
											<tr>
												<td
														colspan="3"
														style="padding: 0; word-break: break-word;"
												>
													<div style="border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
												</td>
											</tr>
										<?php } ?>
										<tr>
											<td style="padding: 12px; word-break: break-word;">
												<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
													<thead>
													<tr>
														<th style="width: 35%; padding: 0; line-height: 1;"></th>
														<th style="width: 65%; padding: 0; line-height: 1;"></th>
													</tr>
													</thead>

													<tbody>
													<tr>
														<td style="padding: 0; word-break: break-word;">
															<div style="width: 100%; height: 65px; position: relative; overflow: hidden;">
																<a
																		style="color: #005ae0; font-weight: normal; text-decoration: underline;"
																		href="<?php echo esc_attr( $item['url'] ?: '#' ) ?>"
																>
																	<img
																			src="<?php echo esc_attr( $item['image_url'] ); ?>"
																			alt="<?php echo esc_attr( aioseo()->helpers->stripEmoji( $item['title'] ) ); ?>"
																			style="box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; position: absolute; width: 100%; height: 100%; top: 0; right: 0; bottom: 0; left: 0; object-position: center; object-fit: cover; border-width: 1px; border-style: solid; border-color: #e5e5e5;"
																	/>
																</a>
															</div>
														</td>

														<td style="padding: 0; word-break: break-word;">
															<div style="padding: 6px; display: inline-block; vertical-align: middle; font-size: 14px;">
																<a
																		style="color: #141b38; font-weight: normal; text-decoration: none;"
																		href="<?php echo esc_attr( $item['url'] ?: '#' ) ?>"
																>
																	<?php echo $item['title']; ?>
																</a>
															</div>
														</td>
													</tr>
													</tbody>
												</table>
											</td>

											<td style="padding: 0; word-break: break-word;">
												<?php if ( ! empty( $item['tru_seo'] ) ) { ?>
													<div style="padding: 12px;">
														<div style="width: 45px; padding: 6px; margin-left: auto; margin-right: auto; font-size: 12px; text-align: center; line-height: 1; border-radius: 4px; border-width: 1px; border-style: solid; <?php echo "color: {$item['tru_seo']['color']}"; ?>">
															<?php echo $item['tru_seo']['text']; ?>
														</div>
													</div>
												<?php } ?>
											</td>

											<td style="padding: 0; word-break: break-word;">
												<?php if ( ! empty( $item['stats'] ) ) { ?>
													<div style="padding: 12px; font-size: 14px;">
														<?php foreach ( $item['stats'] as $k => $stat ) { ?>
															<div style="line-height: 1;<?php echo $k > 0 ? ' margin-top: 6px;' : '' ?>">
																<img
																		style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
																		width="19"
																		height="19"
																		src="<?php echo esc_attr( 'https://static.aioseo.io/report/ste/icon-' . $stat['icon'] . '.png' ) ?>"
																		alt=""
																/>

																<span style="vertical-align: middle;"><?php echo $stat['label']; ?>:</span>

																<span style="vertical-align: middle; font-weight: 700;"><?php echo $stat['value']; ?></span>
															</div>
														<?php } ?>
													</div>
												<?php } ?>
											</td>
										</tr>
									<?php } ?>
									</tbody>
								</table>
							</div>
						</div>
					<?php } ?>

					<?php if ( ! empty( $posts['optimize']['items'] ) ) { ?>
						<?php if ( ! empty( $posts['publish']['items'] ) ) { ?>
							<div style="margin-top: 30px; margin-bottom: 30px; border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
						<?php } ?>
						<div>
							<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
								<tbody>
								<tr>
									<td style="padding: 0; word-break: break-word;">
										<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;">
											<?php esc_html_e( 'Posts to Optimize', 'all-in-one-seo-pack' ); ?>
										</p>
									</td>

									<td style="text-align: right; word-break: break-word;">
										<a
												href="<?php echo esc_attr( $posts['optimize']['url'] ) ?>"
												style="color: #005ae0; font-weight: 700; text-decoration: underline;"
										>
											<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
										</a>
									</td>
								</tr>
								</tbody>
							</table>

							<div style="margin-top: 16px; overflow-x: auto;">
								<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
									<thead>
									<tr>
										<th style="width: 319px; background-color: #f0f6ff; padding: 0; font-size: 12px; font-weight: 400; color: #434960; border-top-left-radius: 2px; border-bottom-left-radius: 2px; line-height: 1;">
											<div style="padding: 12px;"><?php esc_html_e( 'Post', 'all-in-one-seo-pack' ); ?></div>
										</th>

										<th style="width: <?php echo $posts['optimize']['show_tru_seo'] ? '159px' : '0' ?>; text-align: center; background-color: #f0f6ff; padding: 0; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
											<?php if ( $posts['optimize']['show_tru_seo'] ) { ?>
												<div style="padding: 12px;">TruSEO</div>
											<?php } ?>
										</th>

										<th style="width: 159px; text-align: center; background-color: #f0f6ff; padding: 0; font-size: 12px; font-weight: 400; color: #434960; border-top-right-radius: 2px; border-bottom-right-radius: 2px; line-height: 1;">
											<div style="padding: 12px;"><?php esc_html_e( 'Content Drop', 'all-in-one-seo-pack' ); ?></div>
										</th>
									</tr>
									</thead>

									<tbody>
									<?php foreach ( $posts['optimize']['items'] as $item ) { ?>
										<tr>
											<td
													colspan="3"
													style="padding-bottom: 8px; padding-top: 8px; word-break: break-word;"
											></td>
										</tr>

										<tr style="border-width: 1px; border-style: solid; border-color: #e5e5e5;">
											<td
													colspan="3"
													style="padding: 12px; word-break: break-word;"
											>
												<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
													<thead>
													<tr>
														<th style="width: 311px; padding: 0; line-height: 1;"></th>
														<th style="width: <?php echo $posts['optimize']['show_tru_seo'] ? '151px' : '0' ?>; padding: 0; line-height: 1;"></th>
														<th style="width: 151px; padding: 0; line-height: 1;"></th>
													</tr>
													</thead>

													<tbody>
													<tr>
														<td style="padding: 0; background-color: #f3f4f5; word-break: break-word;">
															<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
																<thead>
																<tr>
																	<th style="width: 35%; padding: 0; line-height: 1;"></th>
																	<th style="width: 65%; padding: 0; line-height: 1;"></th>
																</tr>
																</thead>

																<tbody>
																<tr>
																	<td style="padding: 0; word-break: break-word;">
																		<div style="width: 100%; height: 65px; position: relative; overflow: hidden;">
																			<a style="color: #005ae0; font-weight: normal; text-decoration: underline;"<?php echo ! empty( $item['url'] ) ? ' href="' . esc_attr( $item['url'] ) . '"' : '' ?>>
																				<img
																						src="<?php echo esc_attr( $item['image_url'] ); ?>"
																						alt="<?php echo esc_attr( aioseo()->helpers->stripEmoji( $item['title'] ) ); ?>"
																						style="box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; position: absolute; width: 100%; height: 100%; top: 0; right: 0; bottom: 0; left: 0; object-position: center; object-fit: cover; border-width: 1px; border-style: solid; border-color: #e5e5e5;"
																				/>
																			</a>
																		</div>
																	</td>

																	<td style="padding: 0; word-break: break-word;">
																		<div style="padding: 6px; display: inline-block; vertical-align: middle; font-size: 14px;">
																			<a style="color: #141b38; font-weight: normal; text-decoration: none;"<?php echo ! empty( $item['url'] ) ? ' href="' . esc_attr( $item['url'] ) . '"' : '' ?>>
																				<?php echo $item['title']; ?>
																			</a>
																		</div>
																	</td>
																</tr>
																</tbody>
															</table>
														</td>

														<td style="padding: 0; background-color: #f3f4f5; word-break: break-word;">
															<?php if ( ! empty( $item['tru_seo'] ) ) { ?>
																<div style="width: 45px; padding: 6px; margin-left: auto; margin-right: auto; font-size: 12px; text-align: center; line-height: 1; border-radius: 4px; border-width: 1px; border-style: solid; <?php echo "color: {$item['tru_seo']['color']}"; ?>">
																	<?php echo $item['tru_seo']['text']; ?>
																</div>
															<?php } ?>
														</td>

														<td style="padding: 0; background-color: #f3f4f5; word-break: break-word;">
															<div style="width: 50px; padding: 6px; margin-left: auto; margin-right: auto; font-size: 0; text-align: center; line-height: 1; border-radius: 4px; border-width: 1px; border-style: solid; <?php echo "color: {$item['decay_percent']['color']}"; ?>">
																<?php if ( '#00aa63' === $item['decay_percent']['color'] ) { ?>
																	<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-bottom-color: #00aa63; border-bottom-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-top-width: 0;"></div>
																<?php } ?>

																<?php if ( '#df2a4a' === $item['decay_percent']['color'] ) { ?>
																	<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-top-color: #df2a4a; border-top-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-bottom-width: 0;"></div>
																<?php } ?>

																<span style="display: inline-block; vertical-align: middle; font-size: 12px;"><?php echo $item['decay_percent']['text']; ?></span>
															</div>
														</td>
													</tr>

													<?php if ( ! empty( $item['issues']['items'] ) ) { ?>
														<tr>
															<td
																	colspan="2"
																	style="padding: 10px 0 0; word-break: break-word;"
															>
																<div style="font-size: 14px; font-weight: 700;"><?php esc_html_e( 'Issues', 'all-in-one-seo-pack' ); ?></div>
															</td>

															<td style="padding: 10px 0 0; text-align: right; word-break: break-word;">
																<?php if ( ! empty( $item['issues']['url'] ) ) { ?>
																	<a
																			href="<?php echo esc_attr( $item['issues']['url'] ) ?>"
																			style="color: #005ae0; font-weight: normal; text-decoration: underline; font-size: 12px;"
																	>
																		<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
																	</a>
																<?php } ?>
															</td>
														</tr>

														<tr>
															<td
																	colspan="3"
																	style="padding: 0; word-break: break-word;"
															>
																<?php foreach ( $item['issues']['items'] as $issue ) { ?>
																	<div style="padding-top: 0; padding-left: 0; padding-right: 0; pt-6 font-size: 14px;">
																		<img
																				style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-right: 3px;"
																				width="15"
																				height="15"
																				src="https://static.aioseo.io/report/ste/icon-remove.png"
																				alt="x"
																		/>

																		<span style="width: 94%; vertical-align: middle; display: inline-block;"><?php echo $issue['text'] ?></span>
																	</div>
																<?php } ?>
															</td>
														</tr>
													<?php } ?>
													</tbody>
												</table>
											</td>
										</tr>
									<?php } ?>
									</tbody>
								</table>
							</div>
						</div>
					<?php } ?>

					<?php if ( empty( $posts['publish']['items'] ) && empty( $posts['optimize']['items'] ) ) { ?>
						<div style="font-size: 16px; font-weight: 400; text-align: center;">
							<?php echo esc_html__( 'It seems there is no content yet to be displayed.', 'all-in-one-seo-pack' ) ?>
						</div>
					<?php } ?>

					<div style="margin-top: 20px; text-align: center;">
						<a
								href="<?php echo esc_attr( $posts['cta']['url'] ); ?>"
								style="border-radius: 4px; border: none; display: inline-block; font-size: 14px; font-style: normal; font-weight: 700; text-align: center; text-decoration: none; user-select: none; vertical-align: middle; background-color: #005ae0; color: #ffffff; padding: 8px 20px;"
						>
							<?php echo $posts['cta']['text']; ?>
						</a>
					</div>
				</div>
			</div>
		<?php } ?>

		<?php if ( ! empty( $statisticsReport['milestones'] ) ) { ?>
			<div style="background-color: #ffffff; border: 1px solid #e8e8eb; margin-top: 20px;">
				<div style="border-bottom: 1px solid #e5e5e5; padding: 15px 20px;">
					<img
							style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-right: 6px;"
							width="35"
							height="35"
							src="https://static.aioseo.io/report/ste/icon-flag.png"
							alt=""
					/>

					<h2 style="font-size: 20px; font-weight: 700; line-height: 24px; margin-bottom: 0; margin-top: 0; vertical-align: middle; display: inline-block;"><?php esc_html_e( 'SEO Milestones', 'all-in-one-seo-pack' ); ?></h2>
				</div>

				<div style="padding: 10px; overflow-x: auto;">
					<table style="min-width: 400px; width: 100%;">
						<tbody>
						<?php for ( $i = 0; $i < count( $statisticsReport['milestones'] ) / 2; $i++ ) { ?>
							<tr>
								<?php for ( $j = $i; $j < ( 2 + $i ); $j++ ) { ?>
									<?php $milestone = $statisticsReport['milestones'][ $i + $j ] ?? null ?>
									<?php if ( ! $milestone ) { ?>
										<?php continue; ?>
									<?php } ?>
									<td style="padding: 16px; word-break: break-word; vertical-align: top; border: 10px solid #ffff; text-align: center; border-radius: 4px; background-color: <?php echo $milestone['background'] ?>; color: <?php echo $milestone['color'] ?>; width: <?php echo max( 50, 100 / count( $statisticsReport['milestones'] ) ), '%' ?>">
										<img
												style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-left: auto; margin-right: auto;"
												width="35"
												height="35"
												src="<?php echo 'https://static.aioseo.io/report/ste/', $milestone['icon'], '.png' ?>"
												alt=""
										/>

										<p style="font-size: 16px; margin-bottom: 0; margin-top: 12px;">
											<?php echo $milestone['message']; ?>
										</p>
									</td>
								<?php } ?>
							</tr>
						<?php } ?>
						</tbody>
					</table>

					<?php if ( ! empty( $statisticsReport['cta'] ) ) { ?>
						<div style="margin: 10px 10px 20px; border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>

						<div style="padding-bottom: 10px; text-align: center;">
							<a
									href="<?php echo esc_attr( $statisticsReport['cta']['url'] ) ?>"
									style="border-radius: 4px; border: none; display: inline-block; font-size: 14px; font-style: normal; font-weight: 700; text-align: center; text-decoration: none; user-select: none; vertical-align: middle; background-color: #005ae0; color: #ffffff; padding: 8px 20px;"
							>
								<?php echo $statisticsReport['cta']['text']; ?>
							</a>
						</div>
					<?php } ?>
				</div>
			</div>
		<?php } ?>

		<?php if ( ! empty( $resources['posts'] ) ) { ?>
			<div style="background-color: #ffffff; border: 1px solid #e8e8eb; margin-top: 20px;">
				<div style="border-bottom: 1px solid #e5e5e5; padding: 15px 20px;">
					<img
							style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-right: 6px;"
							width="35"
							height="35"
							src="https://static.aioseo.io/report/ste/icon-star.png"
							alt=""
					/>

					<h2 style="font-size: 20px; font-weight: 700; line-height: 24px; margin-bottom: 0; margin-top: 0; vertical-align: middle; display: inline-block;"><?php esc_html_e( "What's New", 'all-in-one-seo-pack' ); ?></h2>
				</div>

				<div style="padding: 20px;">
					<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
						<tbody>
						<?php foreach ( $resources['posts'] as $k => $post ) { ?>
							<?php if ( $k > 0 ) { ?>
								<tr>
									<td
											colspan="3"
											style="padding-bottom: 8px; padding-top: 8px; word-break: break-word;"
									>
										<div style="border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
									</td>
								</tr>
							<?php } ?>

							<tr>
								<td style="padding: 0; word-break: break-word;">
									<div style="width: 147px; height: 82px; margin-top: 6px; margin-bottom: 6px; margin-right: 6px; display: inline-block; vertical-align: top; position: relative; overflow: hidden;">
										<a
												style="color: #005ae0; font-weight: normal; text-decoration: underline;"
												href="<?php echo esc_attr( $post['url'] ?: '#' ); ?>"
										>
											<img
													src="<?php echo esc_attr( $post['image']['url'] ) ?>"
													alt="<?php echo esc_attr( aioseo()->helpers->stripEmoji( $post['title'] ) ) ?>"
													style="box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; position: absolute; width: 100%; height: 100%; top: 0; right: 0; bottom: 0; left: 0; object-position: center; object-fit: cover; border-width: 1px; border-style: solid; border-color: #e5e5e5;"
											/>
										</a>
									</div>

									<div style="max-width: 448px; padding-bottom: 3px; padding-top: 3px; display: inline-block; vertical-align: top;">
										<a
												style="color: #141b38; font-weight: 700; text-decoration: none; font-size: 16px;"
												href="<?php echo esc_attr( $post['url'] ?: '#' ); ?>"
										>
											<?php echo $post['title']; ?>
										</a>

										<div style="margin-top: 6px; font-size: 14px;">
											<span style="display: block;"><?php echo $post['content'] ?? ''; ?></span>

											<a
													style="color: #005ae0; font-weight: normal; text-decoration: underline;"
													href="<?php echo esc_attr( $post['url'] ?: '#' ); ?>"
											>
												<?php esc_html_e( 'Continue Reading', 'all-in-one-seo-pack' ) ?>
											</a>
										</div>
									</div>
								</td>
							</tr>
						<?php } ?>
						</tbody>
					</table>

					<div style="padding-top: 8px;">
						<div style="border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
					</div>

					<div style="padding-top: 20px; text-align: center;">
						<a
								href="<?php echo esc_attr( $resources['cta']['url'] ); ?>"
								style="border-radius: 4px; border: none; display: inline-block; font-size: 14px; font-style: normal; font-weight: 700; text-align: center; text-decoration: none; user-select: none; vertical-align: middle; background-color: #005ae0; color: #ffffff; padding: 8px 20px;"
						>
							<?php echo $resources['cta']['text']; ?>
						</a>
					</div>
				</div>
			</div>
		<?php } ?>

		<div style="width: 600px; max-width: 90%; margin-top: 20px; margin-left: auto; margin-right: auto;">
			<div style="text-align: center;">
				<img
						style="border-radius: 9999px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-left: auto; margin-right: auto;"
						src="https://static.aioseo.io/report/ste/danny-circle.jpg"
						width="50"
						height="50"
						alt="<?php echo esc_attr( AIOSEO_PLUGIN_SHORT_NAME ) ?>"
				/>

				<p style="font-size: 14px; margin-bottom: 0; margin-top: 20px; text-align: center; color: #434960;">
					<?php
					// Translators: 1 - The plugin short name ("AIOSEO").
					printf( esc_html__( 'This email was auto-generated and sent from %1$s.', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME )
					?>
				</p>

				<p style="font-size: 14px; margin-bottom: 0; margin-top: 0; text-align: center; color: #434960;">
					<?php
					printf(
						// Translators: 1 - Opening link tag, 2 - Closing link tag.
						esc_html__( 'Learn how to %1$sdisable%2$s it.', 'all-in-one-seo-pack' ),
						'<a href="' . ( $links['disable'] ?? '#' ) . '" style="color: #141b38; font-weight: normal; text-decoration: underline;">', '</a>'
					)
					?>
				</p>
			</div>

			<div style="margin-top: 20px;">
				<div style="border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
			</div>

			<div style="margin-top: 20px;">
				<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
					<tbody>
					<tr>
						<td style="line-height: 1; word-break: break-word;">
							<a
									style="color: #005ae0; font-weight: normal; text-decoration: none; display: inline-block;"
									href="<?php echo esc_attr( $links['marketing-site'] ?? '#' ) ?>"
							>
								<img
										style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
										width="82"
										height="17"
										src="https://static.aioseo.io/report/ste/text-logo.png"
										alt="<?php echo esc_attr( AIOSEO_PLUGIN_SHORT_NAME ) ?>"
								/>
							</a>
						</td>

						<td style="line-height: 1; text-align: right; word-break: break-word;">
							<a
									style="margin-right: 6px; color: #005ae0; font-weight: normal; text-decoration: none; display: inline-block;"
									href="<?php echo esc_attr( $links['facebook'] ?? '#' ) ?>"
							>
								<img
										style="border-radius: 2px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; height: 20px; width: 20px;"
										src="https://static.aioseo.io/report/ste/facebook.jpg"
										alt="Fb"
										width="20"
										height="20"
								/>
							</a>

							<a
									style="margin-right: 6px; color: #005ae0; font-weight: normal; text-decoration: none; display: inline-block;"
									href="<?php echo esc_attr( $links['linkedin'] ?? '#' ) ?>"
							>
								<img
										style="border-radius: 2px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; height: 20px; width: 20px;"
										src="https://static.aioseo.io/report/ste/linkedin.jpg"
										alt="In"
										width="20"
										height="20"
								/>
							</a>

							<a
									style="margin-right: 6px; color: #005ae0; font-weight: normal; text-decoration: none; display: inline-block;"
									href="<?php echo esc_attr( $links['youtube'] ?? '#' ) ?>"
							>
								<img
										style="border-radius: 2px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; height: 20px; width: 20px;"
										src="https://static.aioseo.io/report/ste/youtube.jpg"
										alt="Yt"
										width="20"
										height="20"
								/>
							</a>

							<a
									style="color: #005ae0; font-weight: normal; text-decoration: none; display: inline-block;"
									href="<?php echo esc_attr( $links['twitter'] ?? '#' ) ?>"
							>
								<img
										style="border-radius: 2px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; height: 20px; width: 20px;"
										src="https://static.aioseo.io/report/ste/x.jpg"
										alt="Tw"
										width="20"
										height="20"
								/>
							</a>
						</td>
					</tr>
					</tbody>
				</table>
			</div>
		</div>
	</div>
</div>Common/Views/main/clarity.php000066600000001402151135505570012205 0ustar00<?php
/**
 * This is the output for Microsoft Clarity on the page.
 *
 * @since 4.1.9
 */

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

$projectId = aioseo()->options->webmasterTools->microsoftClarityProjectId;

if ( empty( $projectId ) || aioseo()->helpers->isAmpPage() ) {
	return;
}
?>
		<script type="text/javascript">
			(function(c,l,a,r,i,t,y){
			c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};t=l.createElement(r);t.async=1;
			t.src="https://www.clarity.ms/tag/"+i+"?ref=aioseo";y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
		})(window, document, "clarity", "script", "<?php echo esc_js( $projectId ); ?>");
		</script>
<?php
// Leave this comment to allow for a line break after the closing script tag.Common/Views/main/meta.php000066600000005727151135505570011502 0ustar00<?php
/**
 * This is the output for meta on the page.
 *
 * @since 4.0.0
 */

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

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
// phpcs:disable Generic.WhiteSpace.ScopeIndent.Incorrect
// phpcs:disable Generic.WhiteSpace.ScopeIndent.IncorrectExact
$description = aioseo()->helpers->encodeOutputHtml( aioseo()->meta->description->getDescription() );
$robots      = aioseo()->meta->robots->meta();
$keywords    = $this->keywords->getKeywords();
$canonical   = aioseo()->helpers->canonicalUrl();
$links       = $this->links->getLinks();
$postType    = get_post_type();
$post        = aioseo()->helpers->getPost();
?>
<?php if ( $description ) : ?>
	<meta name="description" content="<?php echo esc_attr( $description ); ?>" />
<?php endif; ?>
<?php if ( $robots ) : ?>
	<meta name="robots" content="<?php echo esc_html( $robots ); ?>" />
<?php
endif;
if (
	apply_filters( 'aioseo_author_meta', true ) &&
	! is_page() &&
	post_type_supports( $postType, 'author' ) &&
	! empty( $post->post_author ) &&
	! empty( get_the_author_meta( 'display_name', $post->post_author ) )
) :
	?>
	<meta name="author" content="<?php echo esc_attr( get_the_author_meta( 'display_name', $post->post_author ) ); ?>"/>
<?php
endif;
?>
<?php // Adds the site verification meta for webmaster tools. ?>
<?php foreach ( $this->verification->meta() as $metaName => $value ) : ?>
	<meta name="<?php echo esc_attr( $metaName ); ?>" content="<?php echo esc_attr( trim( wp_strip_all_tags( $value ) ) ); ?>" />
<?php endforeach; ?>
<?php if ( ! empty( $keywords ) ) : ?>
	<meta name="keywords" content="<?php echo esc_attr( $keywords ); ?>" />
<?php endif; ?>
<?php if ( ! empty( $canonical ) && ! aioseo()->helpers->isAmpPage( 'amp' ) ) : ?>
	<link rel="canonical" href="<?php echo esc_url( $canonical ); ?>" />
<?php endif; ?>
<?php if ( ! empty( $links['prev'] ) ) : ?>
	<link rel="prev" href="<?php echo esc_url( $links['prev'] ); ?>" />
<?php endif; ?>
<?php if ( ! empty( $links['next'] ) ) : ?>
	<link rel="next" href="<?php echo esc_url( $links['next'] ); ?>" />
<?php endif; ?>
<?php // Add our generator output. ?>
	<meta name="generator" content="<?php echo trim( sprintf( '%1$s (%2$s) %3$s', esc_html( AIOSEO_PLUGIN_NAME ), esc_html( AIOSEO_PLUGIN_SHORT_NAME ), aioseo()->helpers->getAioseoVersion() ) ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped, Generic.Files.LineLength.MaxExceeded ?>" />
<?php

// This adds the miscellaneous verification to the head tag inside our comments.
// @TODO: [V4+] Maybe move this out of meta? Better idea would be to have a global wp_head where meta gets
// attached as well as other things like this:
$miscellaneous = aioseo()->helpers->decodeHtmlEntities( aioseo()->options->webmasterTools->miscellaneousVerification );
$miscellaneous = trim( $miscellaneous );
if ( ! empty( $miscellaneous ) ) {
	echo "\n\t\t$miscellaneous\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}Common/Views/main/social.php000066600000001563151135505570012020 0ustar00<?php
/**
 * This is the output for social meta on the page.
 *
 * @since 4.0.0
 */

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

// phpcs:disable Generic.WhiteSpace.ScopeIndent

// Set context for meta class to social meta.
$facebookMeta = aioseo()->social->output->getFacebookMeta();
foreach ( $facebookMeta as $key => $meta ) :
	// Each article tag needs to be output in a separate meta tag so we cast and loop over each key.
	if ( ! is_array( $meta ) ) {
		$meta = [ $meta ];
	}
	foreach ( $meta as $m ) :
	?>
		<meta property="<?php echo esc_attr( $key ); ?>" content="<?php echo esc_attr( $m ); ?>" />
<?php
	endforeach;
endforeach;

$twitterMeta = aioseo()->social->output->getTwitterMeta();
foreach ( $twitterMeta as $key => $meta ) :
?>
		<meta name="<?php echo esc_attr( $key ); ?>" content="<?php echo esc_attr( $meta ); ?>" />
<?php
endforeach;Common/Views/main/schema.php000066600000001254151135505570012003 0ustar00<?php
/**
 * This is the output for structured data/schema on the page.
 *
 * @since 4.0.0
 */

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

// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
// phpcs:disable Generic.WhiteSpace.ScopeIndent.Incorrect
// phpcs:disable Generic.WhiteSpace.ScopeIndent.IncorrectExact
// phpcs:disable Generic.Files.EndFileNoNewline.Found

$schema = aioseo()->schema->get();
?>
<?php if ( ! empty( $schema ) ) : ?>
		<script type="application/ld+json" class="aioseo-schema">
			<?php echo $schema . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
		</script>
<?php
endif;
Common/Traits/NetworkOptions.php000066600000003700151135505570012773 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits;

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

/**
 * Options trait.
 *
 * @since 4.2.5
 */
trait NetworkOptions {
	/**
	 * Initializes the options.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	protected function init() {
		if ( ! is_multisite() ) {
			return;
		}

		aioseo()->helpers->switchToBlog( $this->helpers->getNetworkId() );

		$dbOptions = json_decode( get_option( $this->optionsName ), true );
		if ( empty( $dbOptions ) ) {
			$dbOptions = [];
		}

		$this->defaultsMerged = aioseo()->helpers->arrayReplaceRecursive( $this->defaults, $this->defaultsMerged );

		$options = aioseo()->helpers->arrayReplaceRecursive(
			$this->defaultsMerged,
			$this->addValueToValuesArray( $this->defaultsMerged, $dbOptions )
		);

		aioseo()->core->optionsCache->setOptions( $this->optionsName, $options );

		aioseo()->helpers->restoreCurrentBlog();
	}

	/**
	 * Sanitizes, then saves the options to the database.
	 *
	 * @since 4.2.5
	 *
	 * @param  array $newOptions The new options to sanitize, then save.
	 * @return void
	 */
	public function sanitizeAndSave( $newOptions ) {
		if ( ! is_multisite() ) {
			return;
		}

		if ( ! is_array( $newOptions ) ) {
			return;
		}

		$this->init();

		aioseo()->helpers->switchToBlog( $this->helpers->getNetworkId() );

		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$dbOptions     = aioseo()->helpers->arrayReplaceRecursive(
			$cachedOptions,
			$this->addValueToValuesArray( $cachedOptions, $newOptions, [], true )
		);

		// Tools.
		if ( ! empty( $newOptions['tools'] ) ) {
			if ( isset( $newOptions['tools']['robots']['rules'] ) ) {
				$dbOptions['tools']['robots']['rules']['value'] = $this->sanitizeField( $newOptions['tools']['robots']['rules'], 'array' );
			}
		}

		aioseo()->core->optionsCache->setOptions( $this->optionsName, $dbOptions );
		$this->save( true );

		aioseo()->helpers->restoreCurrentBlog();
	}
}Common/Traits/SocialProfiles.php000066600000011740151135505570012707 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits;

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

/**
 * Trait that handles the social profiles.
 *
 * @since 4.2.2
 */
trait SocialProfiles {
	/**
	 * List of base URLs.
	 *
	 * @since 4.2.2
	 *
	 * @var array
	 */
	private $baseUrls = [
		'facebookPageUrl' => 'https://facebook.com/',
		'twitterUrl'      => 'https://x.com/',
		'instagramUrl'    => 'https://instagram.com/',
		'tiktokUrl'       => 'https://tiktok.com/@',
		'pinterestUrl'    => 'https://pinterest.com/',
		'youtubeUrl'      => 'https://youtube.com/',
		'linkedinUrl'     => 'https://linkedin.com/in/',
		'tumblrUrl'       => 'https://tumblr.com/',
		'yelpPageUrl'     => 'https://yelp.com/biz/',
		'soundCloudUrl'   => 'https://soundcloud.com/',
		'wikipediaUrl'    => 'https://en.wikipedia.org/wiki/',
		'myspaceUrl'      => 'https://myspace.com/',
		'wordPressUrl'    => 'https://profiles.wordpress.org/',
		'blueskyUrl'      => 'https://bsky.app/profile/',
		'threadsUrl'      => 'https://threads.com/@',
	];

	/**
	 * Returns the profiles of the organization, set under Social Networks.
	 *
	 * @since 4.2.2
	 *
	 * @return array List of social profiles.
	 */
	protected function getOrganizationProfiles() {
		$socialProfiles = [
			'facebookPageUrl' => aioseo()->options->social->profiles->urls->facebookPageUrl,
			'twitterUrl'      => aioseo()->options->social->profiles->urls->twitterUrl,
			'instagramUrl'    => aioseo()->options->social->profiles->urls->instagramUrl,
			'tiktokUrl'       => aioseo()->options->social->profiles->urls->tiktokUrl,
			'pinterestUrl'    => aioseo()->options->social->profiles->urls->pinterestUrl,
			'youtubeUrl'      => aioseo()->options->social->profiles->urls->youtubeUrl,
			'linkedinUrl'     => aioseo()->options->social->profiles->urls->linkedinUrl,
			'tumblrUrl'       => aioseo()->options->social->profiles->urls->tumblrUrl,
			'yelpPageUrl'     => aioseo()->options->social->profiles->urls->yelpPageUrl,
			'soundCloudUrl'   => aioseo()->options->social->profiles->urls->soundCloudUrl,
			'wikipediaUrl'    => aioseo()->options->social->profiles->urls->wikipediaUrl,
			'myspaceUrl'      => aioseo()->options->social->profiles->urls->myspaceUrl,
			'wordPressUrl'    => aioseo()->options->social->profiles->urls->wordPressUrl,
			'blueskyUrl'      => aioseo()->options->social->profiles->urls->blueskyUrl,
			'threadsUrl'      => aioseo()->options->social->profiles->urls->threadsUrl,
		];

		if ( aioseo()->options->social->profiles->sameUsername->enable ) {
			$username          = aioseo()->options->social->profiles->sameUsername->username;
			$includedPlatforms = aioseo()->options->social->profiles->sameUsername->included;

			foreach ( $this->baseUrls as $platformKey => $baseUrl ) {
				if ( ! in_array( $platformKey, $includedPlatforms, true ) ) {
					continue;
				}

				$socialProfiles[ $platformKey ] = $baseUrl . $username;
			}
		}

		if ( aioseo()->options->social->profiles->additionalUrls ) {
			$additionalUrls = preg_split( '/\n|\r|\r\n/', (string) aioseo()->options->social->profiles->additionalUrls );
			$socialProfiles = array_merge( $socialProfiles, $additionalUrls );
		}

		if ( ! aioseo()->options->social->facebook->general->showAuthor ) {
			unset( $socialProfiles['facebookPageUrl'] );
		}

		if ( ! aioseo()->options->social->twitter->general->showAuthor ) {
			unset( $socialProfiles['twitterUrl'] );
		}

		return array_filter( $socialProfiles );
	}

	/**
	 * Returns the profiles of the given user, set under the User Profile.
	 *
	 * @since 4.2.2
	 *
	 * @param  int   $userId The user ID.
	 * @return array         List of social profiles.
	 */
	protected function getUserProfiles( $userId ) {
		$socialProfiles = $this->baseUrls;
		foreach ( $socialProfiles as $platformKey => $v ) {
			$metaName                       = 'aioseo_' . aioseo()->helpers->toSnakeCase( $platformKey );
			$socialProfiles[ $platformKey ] = get_user_meta( $userId, $metaName, true );
		}

		$sameUsernameData = get_user_meta( $userId, 'aioseo_profiles_same_username', true );
		if ( is_array( $sameUsernameData ) && (bool) $sameUsernameData['enable'] ) {
			foreach ( $this->baseUrls as $platform => $baseUrl ) {
				if ( ! in_array( $platform, $sameUsernameData['included'], true ) ) {
					continue;
				}

				$socialProfiles[ $platform ] = $baseUrl . $sameUsernameData['username'];
			}
		}

		$additionalUrls = get_user_meta( $userId, 'aioseo_profiles_additional_urls', true );
		if ( $additionalUrls ) {
			$additionalUrls = preg_split( '/\n|\r|\r\n/', (string) $additionalUrls );
			foreach ( $additionalUrls as $additionalUrl ) {
				// We need to set a random key because otherwise we'll override the ones from the organization.
				$socialProfiles[ uniqid() ] = $additionalUrl;
			}
		}

		if ( ! aioseo()->options->social->facebook->general->showAuthor ) {
			unset( $socialProfiles['facebookPageUrl'] );
		}

		if ( ! aioseo()->options->social->twitter->general->showAuthor ) {
			unset( $socialProfiles['twitterUrl'] );
		}

		return array_filter( $socialProfiles );
	}
}Common/Traits/Assets.php000066600000035647151135505570011247 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits;

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

/**
 * Assets trait.
 *
 * @since 4.1.9
 */
trait Assets {
	/**
	 * Whether we should load dev scripts.
	 *
	 * @since 4.1.9
	 *
	 * @var boolean|null
	 */
	private $shouldLoadDevScripts = null;

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

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

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

	/**
	 * Core class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Core\Core
	 */
	protected $core = null;

	/**
	 * The LocalBusiness addon version.
	 *
	 * @since 4.2.7
	 *
	 * @var string
	 */
	protected $version = '';

	/**
	 * The development site domain.
	 *
	 * @since 4.2.7
	 *
	 * @var string
	 */
	protected $domain = '';

	/**
	 * The development server port.
	 *
	 * @since 4.2.7
	 *
	 * @var int
	 */
	protected $port = 0;

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

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

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

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

		return $tag;
	}

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

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

			$urls[ $url ] = true;

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

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

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

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

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

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

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

		if ( ! $url ) {
			return;
		}

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

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

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

		wp_enqueue_style( $handle );
	}

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

			return;
		}

		$url = $this->shouldLoadDev()
			? $this->getDevUrl() . ltrim( $asset, '/' )
			: $this->jsUrl( $asset );

		if ( ! $url ) {
			return;
		}

		wp_register_script( $handle, $url, $dependencies, $this->version, true );

		if ( empty( $data ) ) {
			return;
		}

		wp_localize_script(
			$handle,
			$objectName,
			$data
		);
	}

	/**
	 * Register the JS to enqueue.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset        The script to load.
	 * @param  array  $dependencies An array of dependencies.
	 * @param  mixed  $data         Any data to be localized.
	 * @param  string $objectName   The object name to use when localizing.
	 * @return void
	 */
	public function enqueueJs( $asset, $dependencies = [], $data = null, $objectName = 'aioseo' ) {
		$this->registerJs( $asset, $dependencies, $data, $objectName );

		$handle = $this->jsHandle( $asset );
		if ( wp_script_is( $handle, 'enqueued' ) ) {
			return;
		}

		wp_enqueue_script( $handle );
	}

	/**
	 * Return the dev URL.
	 *
	 * @since 4.1.9
	 *
	 * @return string The dev URL.
	 */
	private function getDevUrl() {
		$protocol = is_ssl() ? 'https://' : 'http://';

		return $protocol . $this->domain . ':' . $this->port . '/';
	}

	/**
	 * Get the asset URL.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset The asset to find the URL for.
	 * @return string        The URL for the asset.
	 */
	private function assetUrl( $asset ) {
		$assetManifest = $this->getAssetManifestItem( $asset );

		return ! empty( $assetManifest['file'] )
			? $this->basePath() . $assetManifest['file']
			: $this->basePath() . ltrim( $asset, '/' );
	}

	/**
	 * Get the JS URL.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset The asset to find the URL for.
	 * @return string        The URL for the asset.
	 */
	public function jsUrl( $asset ) {
		$manifestAsset = $this->getManifestItem( $asset );

		return ! empty( $manifestAsset['file'] )
			? $this->basePath() . $manifestAsset['file']
			: $this->basePath() . ltrim( $asset, '/' );
	}

	/**
	 * Get an item from the manifest.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset The asset to find.
	 * @return string        Manifest object.
	 */
	private function getManifestItem( $asset ) {
		$manifest = $this->getManifest();

		$asset = ltrim( $asset, '/' );

		return isset( $manifest[ $asset ] ) ? $manifest[ $asset ] : null;
	}

	/**
	 * Get the CSS asset handle.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset The asset to find the handle for.
	 * @return string        The asset handle.
	 */
	public function cssHandle( $asset ) {
		return "{$this->scriptHandle}/css/$asset";
	}

	/**
	 * Get the JS asset handle.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset The asset to find the handle for.
	 * @return string        The asset handle.
	 */
	public function jsHandle( $asset = '' ) {
		return "{$this->scriptHandle}/js/$asset";
	}

	/**
	 * Get the manifest to load assets from.
	 *
	 * @since 4.1.9
	 *
	 * @return array An array of files.
	 */
	private function getManifest() {
		static $file = null;
		if ( $file ) {
			return $file;
		}

		$manifestJson = ''; // This is set in the view.

		if ( file_exists( $this->manifestFile ) ) {
			require_once $this->manifestFile;
		}

		$file = json_decode( $manifestJson, true );

		return $file;
	}

	/**
	 * Get an item from the asset manifest.
	 *
	 * @since 4.1.9
	 *
	 * @param  string      $item An item to retrieve.
	 * @return string|null       The asset item.
	 */
	private function getAssetManifestItem( $item ) {
		$assetManifest = $this->getManifest();

		return ! empty( $assetManifest[ $item ] ) ? $assetManifest[ $item ] : null;
	}

	/**
	 * Get an asset's array of URLs to import.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset The asset to find imports for.
	 * @return array         An array of imports.
	 */
	private function importsUrls( $asset ) {
		$urls          = [];
		$manifestAsset = $this->getManifestItem( $asset );
		if ( ! empty( $manifestAsset['imports'] ) ) {
			foreach ( $manifestAsset['imports'] as $import ) {
				$importAsset = $this->getManifestItem( $import );
				if ( ! empty( $importAsset['file'] ) ) {
					$urls[] = $this->getPublicUrlBase() . $importAsset['file'];

					// Load the import's CSS if any.
					$this->loadCss( $import );
				}
			}
		}

		return $urls;
	}

	/**
	 * Returns an asset's CSS urls.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $asset The asset to find CSS URLs for.
	 * @return array         An array of CSS URLs to load.
	 */
	private function getCssUrls( $asset ) {
		$urls          = [];
		$manifestAsset = $this->getManifestItem( $asset );

		if ( ! empty( $manifestAsset['css'] ) ) {
			foreach ( $manifestAsset['css'] as $file ) {
				$urls[ $file ] = $this->getPublicUrlBase() . $file;
			}
		}

		return $urls;
	}

	/**
	 * Check if we should load the dev watcher scripts.
	 *
	 * @since 4.1.9
	 *
	 * @return boolean True if we should load the dev watcher scripts.
	 */
	private function shouldLoadDev() {
		if ( null !== $this->shouldLoadDevScripts ) {
			return $this->shouldLoadDevScripts;
		}

		if (
			! $this->isDev ||
			(
				defined( 'AIOSEO_LOAD_DEV_SCRIPTS' ) &&
				false === AIOSEO_LOAD_DEV_SCRIPTS
			)
		) {
			$this->shouldLoadDevScripts = false;

			return $this->shouldLoadDevScripts;
		}

		if ( ! $this->domain && ! $this->port ) {
			$this->shouldLoadDevScripts = false;

			return $this->shouldLoadDevScripts;
		}

		set_error_handler( function() {} ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
		$connection = fsockopen( $this->domain, $this->port ); // phpcs:ignore WordPress.WP.AlternativeFunctions
		restore_error_handler();

		if ( ! $connection ) {
			$this->shouldLoadDevScripts = false;

			return $this->shouldLoadDevScripts;
		}

		$this->shouldLoadDevScripts = true;

		return $this->shouldLoadDevScripts;
	}

	/**
	 * Get the path for the assets.
	 *
	 * @since 4.1.9
	 *
	 * @param  bool   $maybeDev Whether to try and load dev scripts.
	 * @return string           The path for the assets.
	 */
	public function getAssetsPath( $maybeDev = true ) {
		return $maybeDev && $this->shouldLoadDev()
			? $this->getDevUrl()
			: $this->basePath();
	}

	/**
	 * Finds out if a handle should be loaded as regular JS and not as modern JS module.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $handle The script handle.
	 * @return bool           Should the module tag be skipped.
	 */
	public function skipModuleTag( $handle ) {
		if ( ! aioseo()->helpers->stringContains( $handle, $this->jsHandle( '' ) ) ) {
			return true;
		}

		foreach ( $this->noModuleTag as $tag ) {
			if ( aioseo()->helpers->stringContains( $handle, $tag ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Normalize the assets host. Some sites manually set the WP_PLUGINS_URL
	 * and if that domain has www. and the site_url does not, then it will fail to load
	 * our assets. This doesn't fix the issue 100% because it will still fail on
	 * sub-domains that don't have the proper CORS headers. Those sites will need
	 * manual fixes.
	 *
	 * @since 4.1.10
	 *
	 * @param  string $path The path to normalize.
	 * @return string       The normalized path.
	 */
	public function normalizeAssetsHost( $path ) {
		static $paths = [];
		if ( isset( $paths[ $path ] ) ) {
			return apply_filters( 'aioseo_normalize_assets_host', $paths[ $path ] );
		}

		// We need to verify the domain on the $path attribute matches
		// what's in site_url() for our assets or they won't load.
		$siteUrl        = site_url();
		$siteUrlEscaped = aioseo()->helpers->escapeRegex( $siteUrl );
		if ( preg_match( "/^$siteUrlEscaped/i", (string) $path ) ) {
			$paths[ $path ] = $path;

			return apply_filters( 'aioseo_normalize_assets_host', $paths[ $path ] );
		}

		// We now know that the path doesn't contain the site_url().
		$newPath        = $path;
		$siteUrlParsed  = wp_parse_url( $siteUrl );
		$host           = aioseo()->helpers->escapeRegex( str_replace( 'www.', '', $siteUrlParsed['host'] ) );
		$scheme         = aioseo()->helpers->escapeRegex( $siteUrlParsed['scheme'] );

		$siteUrlHasWww = preg_match( "/^{$scheme}:\/\/www\.$host/", (string) $siteUrl );
		$pathHasWww    = preg_match( "/^{$scheme}:\/\/www\.$host/", (string) $path );

		// Check if the path contains www.
		if ( $pathHasWww && ! $siteUrlHasWww ) {
			// If the path contains www., we want to strip it out.
			$newPath = preg_replace( "/^({$scheme}:\/\/)(www\.)($host)/", '$1$3', (string) $path );
		}

		// Check if the site_url contains www.
		if ( $siteUrlHasWww && ! $pathHasWww ) {
			// If the site_url contains www., we want to add it in to the path.
			$newPath = preg_replace( "/^({$scheme}:\/\/)($host)/", '$1www.$2', (string) $path );
		}

		$paths[ $path ] = $newPath;

		return apply_filters( 'aioseo_normalize_assets_host', $paths[ $path ] );
	}

	/**
	 * Get all the CSS files which a JS asset depends on.
	 * This won't work properly unless you've run `npm run build` first.
	 *
	 * @since 4.3.1
	 *
	 * @param  string $asset The asset to find the CSS dependencies for.
	 * @return array         All the asset's CSS dependencies if any.
	 */
	public function getJsAssetCssQueue( $asset ) {
		$queue = [];

		foreach ( $this->getCssUrls( $asset ) as $file => $url ) {
			$queue[] = [
				'handle' => $this->cssHandle( $file ),
				'url'    => $url
			];
		}

		$manifestAsset = $this->getManifestItem( $asset );
		if ( ! empty( $manifestAsset['imports'] ) ) {
			foreach ( $manifestAsset['imports'] as $subAsset ) {
				foreach ( $this->getCssUrls( $subAsset ) as $file => $url ) {
					$queue[] = [
						'handle' => $this->cssHandle( $file ),
						'url'    => $url
					];
				}
			}
		}

		return $queue;
	}
}Common/Traits/Helpers/ThirdParty.php000066600000055042151135505570013470 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Contains all third-party related helper methods.
 *
 * @since 4.1.4
 */
trait ThirdParty {
	/**
	 * Checks whether WooCommerce is active.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether WooCommerce is active.
	 */
	public function isWooCommerceActive() {
		return class_exists( 'WooCommerce' );
	}

	/**
	 * Checks if the current page is a special WooCommerce page (Cart, Checkout, ...).
	 *
	 * @since 4.0.0
	 *
	 * @param  int    $postId The post ID.
	 * @return string         The type of page or an empty string if it isn't a WooCommerce page.
	 */
	public function isWooCommercePage( $postId = 0 ) {
		$postId                  = $postId ? (int) $postId : get_the_ID();
		$specialWooCommercePages = $this->getWooCommercePages();

		if ( in_array( $postId, $specialWooCommercePages, true ) ) {
			return array_search( $postId, $specialWooCommercePages, true );
		}

		return '';
	}

	/**
	 * Returns the WooCommerce pages.
	 *
	 * @since 4.7.3
	 *
	 * @return array An associative list of special WooCommerce pages.
	 */
	public function getWooCommercePages() {
		if ( ! $this->isWooCommerceActive() ) {
			$wooCommercePages = [];

			return $wooCommercePages;
		}

		$wooCommercePages = [
			'cart'      => (int) get_option( 'woocommerce_cart_page_id' ),
			'checkout'  => (int) get_option( 'woocommerce_checkout_page_id' ),
			'myAccount' => (int) get_option( 'woocommerce_myaccount_page_id' ),
			'terms'     => (int) get_option( 'woocommerce_terms_page_id' ),
		];

		return $wooCommercePages;
	}

	/**
	 * Checks whether the current page is a special WooCommerce page we shouldn't show our schema settings for.
	 *
	 * @since 4.1.6
	 *
	 * @param  int  $postId The post ID.
	 * @return bool         Whether the current page is a disallowed WooCommerce page.
	 */
	public function isWooCommercePageWithoutSchema( $postId = 0 ) {
		$page = $this->isWooCommercePage( $postId );
		if ( ! $page ) {
			return false;
		}

		$disallowedPages = [ 'cart', 'checkout', 'myAccount' ];

		return in_array( $page, $disallowedPages, true );
	}

	/**
	 * Checks whether the queried object is the WooCommerce shop page.
	 *
	 * @since 4.0.0
	 *
	 * @param  int  $id The post ID to check against (optional).
	 * @return bool     Whether the current page is the WooCommerce shop page.
	 */
	public function isWooCommerceShopPage( $id = 0 ) {
		if ( ! $this->isWooCommerceActive() ) {
			return false;
		}

		if ( ! is_admin() && ! aioseo()->helpers->isAjaxCronRestRequest() && function_exists( 'is_shop' ) ) {
			return is_shop();
		}

		// Prevent non-numeric id.
		$id = is_numeric( $id ) ? (int) $id : 0;

		// phpcs:disable HM.Security.ValidatedSanitizedInput, HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		$id = ! $id && ! empty( $_GET['post'] )
			? (int) sanitize_text_field( wp_unslash( $_GET['post'] ) )
			: $id;
		// phpcs:enable

		return $id && wc_get_page_id( 'shop' ) === $id;
	}

	/**
	 * Checks whether the queried object is the WooCommerce cart page.
	 *
	 * @since 4.1.3
	 *
	 * @param  int  $id The post ID to check against (optional).
	 * @return bool     Whether the current page is the WooCommerce cart page.
	 */
	public function isWooCommerceCartPage( $id = 0 ) {
		if ( ! $this->isWooCommerceActive() ) {
			return false;
		}

		if ( ! is_admin() && ! aioseo()->helpers->isAjaxCronRestRequest() && function_exists( 'is_cart' ) ) {
			return is_cart();
		}

		// phpcs:disable HM.Security.ValidatedSanitizedInput, HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		$id = ! $id && ! empty( $_GET['post'] )
			? (int) sanitize_text_field( wp_unslash( $_GET['post'] ) )
			: (int) $id;
		// phpcs:enable

		return $id && wc_get_page_id( 'cart' ) === $id;
	}

	/**
	 * Checks whether the queried object is the WooCommerce checkout page.
	 *
	 * @since 4.1.3
	 *
	 * @param  int  $id The post ID to check against (optional).
	 * @return bool     Whether the current page is the WooCommerce checkout page.
	 */
	public function isWooCommerceCheckoutPage( $id = 0 ) {
		if ( ! $this->isWooCommerceActive() ) {
			return false;
		}

		if ( ! is_admin() && ! aioseo()->helpers->isAjaxCronRestRequest() && function_exists( 'is_checkout' ) ) {
			return is_checkout();
		}

		// phpcs:disable HM.Security.ValidatedSanitizedInput, HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		$id = ! $id && ! empty( $_GET['post'] )
			? (int) sanitize_text_field( wp_unslash( $_GET['post'] ) )
			: (int) $id;
		// phpcs:enable

		return $id && wc_get_page_id( 'checkout' ) === $id;
	}

	/**
	 * Checks whether the queried object is the WooCommerce account page.
	 *
	 * @since 4.1.3
	 *
	 * @param  int  $id The post ID to check against (optional).
	 * @return bool     Whether the current page is the WooCommerce account page.
	 */
	public function isWooCommerceAccountPage( $id = 0 ) {
		if ( ! $this->isWooCommerceActive() ) {
			return false;
		}

		if ( ! is_admin() && ! aioseo()->helpers->isAjaxCronRestRequest() && function_exists( 'is_account_page' ) ) {
			return is_account_page();
		}

		// phpcs:disable HM.Security.ValidatedSanitizedInput, HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		$id = ! $id && ! empty( $_GET['post'] )
			? (int) sanitize_text_field( wp_unslash( $_GET['post'] ) )
			: (int) $id;
		// phpcs:enable

		return $id && wc_get_page_id( 'myaccount' ) === $id;
	}

	/**
	 * Checks whether the queried object is a WooCommerce product page.
	 *
	 * @since 4.5.5
	 *
	 * @return bool Whether the current page is a WooCommerce product page.
	 */
	public function isWooCommerceProductPage() {
		if (
			! $this->isWooCommerceActive() ||
			! function_exists( 'is_product' )
		) {
			return false;
		}

		return is_product();
	}

	/**
	 * Checks whether the queried object is a WooCommerce taxonomy page.
	 *
	 * @since 4.5.5
	 *
	 * @return bool Whether the current page is a WooCommerce taxonomy page.
	 */
	public function isWooCommerceTaxonomyPage() {
		if (
			! $this->isWooCommerceActive() ||
			! function_exists( 'is_product_taxonomy' )
		) {
			return false;
		}

		return is_product_taxonomy();
	}

	/**
	 * Internationalize.
	 *
	 * @since 4.0.0
	 *
	 * @param $in
	 * @return mixed|void
	 */
	public function internationalize( $in ) {
		if ( function_exists( 'langswitch_filter_langs_with_message' ) ) {
			$in = langswitch_filter_langs_with_message( $in );
		}

		if ( function_exists( 'polyglot_filter' ) ) {
			$in = polyglot_filter( $in );
		}

		if ( function_exists( 'qtrans_useCurrentLanguageIfNotFoundUseDefaultLanguage' ) ) {
			$in = qtrans_useCurrentLanguageIfNotFoundUseDefaultLanguage( $in );
		} elseif ( function_exists( 'ppqtrans_useCurrentLanguageIfNotFoundUseDefaultLanguage' ) ) {
			$in = ppqtrans_useCurrentLanguageIfNotFoundUseDefaultLanguage( $in );
		} elseif ( function_exists( 'qtranxf_useCurrentLanguageIfNotFoundUseDefaultLanguage' ) ) {
			$in = qtranxf_useCurrentLanguageIfNotFoundUseDefaultLanguage( $in );
		}

		return apply_filters( 'localization', $in );
	}

	/**
	 * Checks if WPML is active.
	 *
	 * @since 4.0.0
	 *
	 * @return bool True if it is, false if not.
	 */
	public function isWpmlActive() {
		return class_exists( 'SitePress' );
	}

	/**
	 * Checks if TranslatePress is active.
	 *
	 * @since 4.7.3
	 *
	 * @return bool True if it is, false if not.
	 */
	public function isTranslatePressActive() {
		return class_exists( 'TRP_Translate_Press' );
	}

	/**
	 * Localizes a given URL.
	 *
	 * This is required for compatibility with WPML.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $path The relative path of the URL.
	 * @return string $url  The filtered URL.
	 */
	public function localizedUrl( $path ) {
		$url = apply_filters( 'wpml_home_url', home_url( '/' ) );

		// Remove URL parameters.
		preg_match_all( '/\?[\s\S]+/', (string) $url, $matches );

		// Get the base URL.
		$url  = preg_replace( '/\?[\s\S]+/', '', (string) $url );
		$url  = trailingslashit( $url );
		$url .= preg_replace( '/\//', '', (string) $path, 1 );

		// Readd URL parameters.
		if ( $matches && $matches[0] ) {
			$url .= $matches[0][0];
		}

		return $url;
	}

	/**
	 * Checks whether BuddyPress is active.
	 *
	 * @since 4.0.0
	 *
	 * @return boolean
	 */
	public function isBuddyPressActive() {
		return class_exists( 'BuddyPress' );
	}

	/**
	 * Checks whether the queried object is a buddy press user page.
	 *
	 * @since 4.0.0
	 *
	 * @return boolean
	 */
	public function isBuddyPressUser() {
		return $this->isBuddyPressActive() && function_exists( 'bp_is_user' ) && bp_is_user();
	}

	/**
	 * Returns if the page is a BuddyPress page (Activity, Members, Groups).
	 *
	 * @since 4.0.0
	 *
	 * @param  int  $postId The post ID.
	 * @return bool         If the page is a BuddyPress page or not.
	 */
	public function isBuddyPressPage( $postId = 0 ) {
		$bpPageIds = $this->getBuddyPressPageIds();

		return in_array( $postId, $bpPageIds, true );
	}

	/**
	 * Returns the BuddyPress pages.
	 *
	 * @since 4.7.3
	 *
	 * @return array A list of BuddyPress page IDs.
	 */
	public function getBuddyPressPageIds() {
		if ( ! $this->isBuddyPressActive() ) {
			return [];
		}

		static $bpPageIds = null;
		if ( null === $bpPageIds ) {
			$bpPageIds = (array) get_option( 'bp-pages' );
			$bpPageIds = array_map( 'intval', $bpPageIds );
		}

		return $bpPageIds;
	}

	/**
	 * Returns ACF fields as an array of meta keys and values.
	 *
	 * @since 4.0.6
	 *
	 * @param  \WP_Post|int $post  The post.
	 * @param  array        $types A whitelist of ACF field types.
	 * @return array               An array of meta keys and values.
	 */
	public function getAcfContent( $post = null, $types = [] ) {
		$post = ( $post && is_object( $post ) ) ? $post : $this->getPost( $post );

		if ( ! class_exists( 'ACF' ) || ! function_exists( 'get_field_objects' ) ) {
			return [];
		}

		if ( defined( 'ACF_VERSION' ) && version_compare( ACF_VERSION, '5.7.0', '<' ) ) {
			return [];
		}

		// Set defaults.
		$allowedTypes = [
			'text',
			'textarea',
			'email',
			'url',
			'wysiwyg',
			'image',
			'gallery',
			'link',
		];

		$types        = wp_parse_args( $types, $allowedTypes );
		$fieldObjects = get_field_objects( $post->ID );

		if ( empty( $fieldObjects ) ) {
			return [];
		}

		// Filter out any fields that are not in our allowed types.
		$fields = array_filter( $fieldObjects, function( $object ) use ( $types ) {
			return ! empty( $object['value'] ) && in_array( $object['type'], $types, true );
		});

		// Create an array with the field names and values with added HTML markup.
		$acfFields = [];
		foreach ( $fields as $field ) {
			switch ( $field['type'] ) {
				case 'url':
					$value = make_clickable( $field['value'] ?? '' );
					break;
				case 'image':
					// Image format options are array, URL (string), id (int).
					$imageUrl = is_array( $field['value'] ) ? $field['value']['url'] : $field['value'];
					$imageUrl = is_numeric( $imageUrl ) ? wp_get_attachment_image_url( $imageUrl ) : $imageUrl;

					$value = "<img src='$imageUrl' />"; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
					break;
				case 'gallery':
					$imageUrl = $field['value'];
					// The value of a gallery field should always be an array.
					if ( is_array( $imageUrl ) ) {
						$imageUrl = current( $imageUrl );
					}

					// Image array format.
					if ( is_array( $imageUrl ) && ! empty( $imageUrl['url'] ) ) {
						$imageUrl = $imageUrl['url'];
					}

					// Image ID format.
					$imageUrl = is_numeric( $imageUrl ) ? wp_get_attachment_image_url( $imageUrl ) : $imageUrl;

					$value = ! empty( $imageUrl ) ? "<img src='{$imageUrl}' />" : ''; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
					break;
				case 'link':
					$value = make_clickable( $field['value']['url'] ?? $field['value'] ?? '' );
					break;
				default:
					$value = $field['value'];
					break;
			}

			if ( $value ) {
				$acfFields[ $field['name'] ] = $value;
			}
		}

		return $acfFields;
	}

	/**
	 * Retrieves the ACF Flexible Content field value for a given post.
	 *
	 * @since 4.7.9
	 *
	 * @param  string     $name The name of the field.
	 * @param  int|object $post The post ID or object.
	 * @return string           The field value.
	 */
	public function getAcfFlexibleContentField( $name, $post ) {
		$output = '';
		if ( ! function_exists( 'acf_get_raw_field' ) || ! function_exists( 'acf_get_field' ) ) {
			return $output;
		}

		$parentTrace = [];
		$field       = acf_get_raw_field( $name ) ?? [];
		while ( ! empty( $field['parent'] ) && ! empty( $field['parent_layout'] ) ) {
			$parentField   = acf_get_field( $field['parent'] );
			$parentTrace[] = $parentField['name'] ?? '';
			$field         = $parentField;
		}

		$parentTrace = array_filter( $parentTrace );
		if ( empty( $parentTrace ) ) {
			return $output;
		}

		$parentTrace        = array_reverse( $parentTrace );
		$parentName         = array_shift( $parentTrace );
		$highestParentField = get_field( $parentName, $post );

		for ( $i = 0; $i <= count( $parentTrace ); $i++ ) {
			$values = array_filter( array_column( $highestParentField, $name ), 'is_scalar' );
			if ( $values ) {
				return implode( ' ', $values );
			}

			$highestParentField = $highestParentField[0] ?? '';
			if (
				! is_array( $highestParentField ) ||
				! isset( $parentTrace[ $i ] )
			) {
				break;
			}

			$highestParentField = $highestParentField[ $parentTrace[ $i ] ];
		}

		return $output;
	}

	/**
	 * Checks whether the Smash Balloon Custom Facebook Feed plugin is active.
	 *
	 * @since 4.2.0
	 *
	 * @return bool Whether the SB CFF plugin is active.
	 */
	public function isSbCustomFacebookFeedActive() {
		static $isActive = null;
		if ( null !== $isActive ) {
			return $isActive;
		}

		$isActive = defined( 'CFFVER' ) || is_plugin_active( 'custom-facebook-feed/custom-facebook-feed.php' );

		return $isActive;
	}

	/**
	 * Returns the access token for Facebook from Smash Balloon if there is one.
	 *
	 * @since 4.2.0
	 *
	 * @return string|false The access token or false if there is none.
	 */
	public function getSbAccessToken() {
		static $accessToken = null;
		if ( null !== $accessToken ) {
			return $accessToken;
		}

		if ( ! $this->isSbCustomFacebookFeedActive() ) {
			$accessToken = false;

			return $accessToken;
		}

		$oembedTokenData = get_option( 'cff_oembed_token', [] );
		if ( ! $oembedTokenData || empty( $oembedTokenData['access_token'] ) ) {
			$accessToken = false;

			return $accessToken;
		}

		$sbFacebookDataEncryptionInstance = new \CustomFacebookFeed\SB_Facebook_Data_Encryption();
		$accessToken                      = $sbFacebookDataEncryptionInstance->maybe_decrypt( $oembedTokenData['access_token'] );

		return $accessToken;
	}

	/**
	* Returns the homepage URL for a language code.
	*
	* @since 4.2.1
	*
	* @param  string|int $identifier The language code or the post id to return the url.
	* @return string                 The home URL.
	*/
	public function wpmlHomeUrl( $identifier ) {
		foreach ( $this->wpmlHomePages() as $langCode => $wpmlHomePage ) {
			if (
				( is_string( $identifier ) && $langCode === $identifier ) ||
				( is_numeric( $identifier ) && $wpmlHomePage['id'] === $identifier )
			) {
				return $wpmlHomePage['url'];
			}
		}

		return '';
	}

	/**
	 * Returns the homepage IDs.
	 *
	 * @since 4.2.1
	 *
	 * @return array An array of home page ids.
	 */
	public function wpmlHomePages() {
		global $sitepress;
		static $homePages = [];

		if ( ! $this->isWpmlActive() || empty( $sitepress ) || ! method_exists( $sitepress, 'language_url' ) ) {
			return $homePages;
		}

		if ( empty( $homePages ) ) {
			$languages  = apply_filters( 'wpml_active_languages', [] );
			$homePageId = (int) get_option( 'page_on_front' );
			foreach ( $languages as $language ) {
				$homePages[ $language['code'] ] = [
					'id'  => apply_filters( 'wpml_object_id', $homePageId, 'page', false, $language['code'] ),
					'url' => $sitepress->language_url( $language['code'] )
				];
			}
		}

		return $homePages;
	}

	/**
	 * Returns if the post id os a WPML home page.
	 *
	 * @since 4.2.1
	 *
	 * @param  int  $postId The post ID.
	 * @return bool         Is the post id a home page.
	 */
	public function wpmlIsHomePage( $postId ) {
		foreach ( $this->wpmlHomePages() as $wpmlHomePage ) {
			if ( $wpmlHomePage['id'] === $postId ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Returns the WPML url format.
	 *
	 * @since 4.2.8
	 *
	 * @return string The format.
	 */
	public function getWpmlUrlFormat() {
		global $sitepress;

		if (
			! $this->isWpmlActive() ||
			empty( $sitepress ) ||
			! method_exists( $sitepress, 'get_setting' )
		) {
			return '';
		}

		switch ( $sitepress->get_setting( 'language_negotiation_type' ) ) {
			case WPML_LANGUAGE_NEGOTIATION_TYPE_DIRECTORY:
			case 1:
				return 'directory';
			case WPML_LANGUAGE_NEGOTIATION_TYPE_DOMAIN:
			case 2:
				return 'domain';
			case WPML_LANGUAGE_NEGOTIATION_TYPE_PARAMETER:
			case 3:
				return 'parameter';
			default:
				return '';
		}
	}

	/**
	 * Returns the TranslatePress slugs code and slug.
	 *
	 * @since 4.7.3
	 *
	 * @return array The slugs.
	 */
	public function getTranslatePressUrlSlugs() {
		if ( ! $this->isTranslatePressActive() ) {
			return [];
		}

		$settings = maybe_unserialize( get_option( 'trp_settings', [] ) );

		return isset( $settings['url-slugs'] ) ? $settings['url-slugs'] : [];
	}

	/**
	 * Checks whether the WooCommerce Follow Up Emails plugin is active.
	 *
	 * @since 4.2.2
	 *
	 * @return bool Whether the plugin is active.
	 */
	public function isWooCommerceFollowupEmailsActive() {
		$isActive = defined( 'FUE_VERSION' ) || is_plugin_active( 'woocommerce-follow-up-emails/woocommerce-follow-up-emails.php' );

		return $isActive;
	}

	/**
	 * Checks if the current page is an AMP page.
	 * This function is only effective if called after the `wp` action.
	 *
	 * @since 4.2.3
	 *
	 * @param  string $pluginName The name of the AMP plugin to check for (optional).
	 * @return bool               Whether the current page is an AMP page.
	 */
	public function isAmpPage( $pluginName = '' ) {
		// Official AMP plugin.
		if ( 'amp' === $pluginName ) {
			// If we're checking for the AMP page plugin specifically, return early if it's not active.
			// Otherwise, we'll return true if AMP for WP is enabled because the helper method doesn't distinguish between the two.
			if ( ! defined( 'AMP__VERSION' ) ) {
				return false;
			}

			$options = get_option( 'amp-options' );
			if ( ! empty( $options['theme_support'] ) && 'standard' === strtolower( $options['theme_support'] ) ) {
				return true;
			}
		}

		return $this->isAmpPageHelper();
	}

	/**
	 * Helper function for {@see isAmpPage()}.
	 * Checks if the current page is an AMP page.
	 *
	 * @since 4.2.4
	 *
	 * @return bool Whether the current page is an AMP page.
	 */
	private function isAmpPageHelper() {
		// First check for the existence of any AMP plugin functions. Bail early if none are found, and prevent false positives.
		if (
			! function_exists( 'amp_is_request' ) &&
			! function_exists( 'is_amp_endpoint' ) &&
			! function_exists( 'ampforwp_is_amp_endpoint' ) &&
			! function_exists( 'is_amp_wp' )
		) {
			// If none of the AMP plugin functions are found, return false and allow compatibility with custom implementations.
			return apply_filters( 'aioseo_is_amp_page', false );
		}

		// AMP plugin requires the `wp` action to be called to function properly, otherwise, it will throw warnings.

		if ( did_action( 'wp' ) ) {
			// Check for the "AMP" plugin.
			if ( function_exists( 'amp_is_request' ) ) {
				return (bool) amp_is_request();
			}

			// Check for the "AMP" plugin (`is_amp_endpoint()` is deprecated).
			if ( function_exists( 'is_amp_endpoint' ) ) {
				return (bool) is_amp_endpoint();
			}

			// Check for the "AMP for WP – Accelerated Mobile Pages" plugin.
			if ( function_exists( 'ampforwp_is_amp_endpoint' ) ) {
				return (bool) ampforwp_is_amp_endpoint();
			}

			// Check for the "AMP WP" plugin.
			if ( function_exists( 'is_amp_wp' ) ) {
				return (bool) is_amp_wp();
			}
		}

		return false;
	}

	/**
	 * If we're in a LearnPress lesson page, return the lesson ID.
	 *
	 * @since 4.3.1
	 *
	 * @return int|false
	 */
	public function getLearnPressLesson() {
		// phpcs:disable Squiz.NamingConventions.ValidVariableName
		global $lp_course_item;
		if ( $lp_course_item && method_exists( $lp_course_item, 'get_id' ) ) {
			return $lp_course_item->get_id();
		}
		// phpcs:enable Squiz.NamingConventions.ValidVariableName

		return false;
	}

	/**
	 * Set a flag to indicate Divi whether it is processing internal content or not.
	 *
	 * @since 4.4.3
	 *
	 * @param  null|bool $flag The flag value.
	 * @return null|bool       The previous flag value to reset it later.
	 */
	public function setDiviInternalRendering( $flag ) {
		if ( ! defined( 'ET_BUILDER_VERSION' ) ) {
			return null;
		}
		// phpcs:disable Squiz.NamingConventions.ValidVariableName
		global $et_pb_rendering_column_content;

		$originalValue                  = $et_pb_rendering_column_content;
		$et_pb_rendering_column_content = $flag;
		// phpcs:enable Squiz.NamingConventions.ValidVariableName

		return $originalValue;
	}

	/**
	 * Checks whether the current request is being done by a crawler from Yandex.
	 *
	 * @since 4.4.0
	 *
	 * @return bool Whether the current request is being done by a crawler from Yandex.
	 */
	public function isYandexUserAgent() {
		if ( ! isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
			return false;
		}

		return preg_match( '#.*Yandex.*#', (string) sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) );
	}

	/**
	 * Checks whether the taxonomy is a WooCommerce product attribute.
	 *
	 * @since 4.7.8
	 *
	 * @param  mixed $taxonomy The taxonomy.
	 * @return bool            Whether the taxonomy is a WooCommerce product attribute.
	 */
	public function isWooCommerceProductAttribute( $taxonomy ) {
		$name = is_object( $taxonomy )
			? $taxonomy->name
			: (
				is_array( $taxonomy )
					? $taxonomy['name']
					: $taxonomy
			);

		return ! empty( $name ) && 'pa_' === substr( $name, 0, 3 );
	}

	/**
	 * Returns whether a plugin is active or not using abstraction.
	 *
	 * @since 4.8.1
	 *
	 * @param  string $slug The plugin slug.
	 * @return bool         Whether the plugin is active.
	 */
	public function isPluginActive( $slug ) {
		$mapped = [
			'buddypress' => 'buddypress/bp-loader.php',
			'bbpress'    => 'bbpress/bbpress.php',
			'weglot'     => 'weglot/weglot.php'
		];

		static $output = [];
		if ( isset( $output[ $slug ] ) ) {
			return $output[ $slug ];
		}

		$mapped[ $slug ] = $mapped[ $slug ] ?? $slug;
		$output[ $slug ] = function_exists( 'is_plugin_active' ) && is_plugin_active( $mapped[ $slug ] );

		return $output[ $slug ];
	}
}Common/Traits/Helpers/WpContext.php000066600000070077151135505570013336 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Contains all context related helper methods.
 * This includes methods to check the context of the current request, but also get WP objects.
 *
 * @since 4.1.4
 */
trait WpContext {
	/**
	 * The original main query.
	 *
	 * @since 4.3.0
	 *
	 * @var \WP_Query
	 */
	public $originalQuery;

	/**
	 * The original main post variable.
	 *
	 * @since 4.3.0
	 *
	 * @var \WP_Post
	 */
	public $originalPost;

	/**
	 * Get the home page object.
	 *
	 * @since 4.1.1
	 *
	 * @return \WP_Post|null The home page.
	 */
	public function getHomePage() {
		$homePageId = $this->getHomePageId();

		return $homePageId ? get_post( $homePageId ) : null;
	}

	/**
	 * Get the ID of the home page.
	 *
	 * @since 4.0.0
	 *
	 * @return int|false The home page ID.
	 */
	public function getHomePageId() {
		static $homeId = null;
		if ( null !== $homeId ) {
			return $homeId;
		}

		$pageShowOnFront = ( 'page' === get_option( 'show_on_front' ) );
		$pageOnFrontId   = get_option( 'page_on_front' );

		$homeId = $pageShowOnFront && $pageOnFrontId ? (int) $pageOnFrontId : false;

		return $homeId;
	}

	/**
	 * Returns the blog page.
	 *
	 * @since 4.0.0
	 *
	 * @return \WP_Post|null The blog page.
	 */
	public function getBlogPage() {
		$blogPageId = $this->getBlogPageId();

		return $blogPageId ? get_post( $blogPageId ) : null;
	}

	/**
	 * Gets the current blog page id if it's configured.
	 *
	 * @since 4.1.1
	 *
	 * @return int|null
	 */
	public function getBlogPageId() {
		$pageShowOnFront = ( 'page' === get_option( 'show_on_front' ) );
		$blogPageId      = (int) get_option( 'page_for_posts' );

		return $pageShowOnFront && $blogPageId ? $blogPageId : null;
	}

	/**
	 * Checks whether the current page is a taxonomy term archive.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether the current page is a taxonomy term archive.
	 */
	public function isTaxTerm() {
		$object = get_queried_object();

		return $object instanceof \WP_Term;
	}

	/**
	 * Checks whether the current page is a static one.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether the current page is a static one.
	 */
	public function isStaticPage() {
		return $this->isStaticHomePage() || $this->isStaticPostsPage() || $this->isWooCommerceShopPage();
	}

	/**
	 * Checks whether the current page is the static homepage.
	 *
	 * @since 4.0.0
	 *
	 * @param  mixed $post Pass in an optional post to check if its the static home page.
	 * @return bool        Whether the current page is the static homepage.
	 */
	public function isStaticHomePage( $post = null ) {
		static $isHomePage = null;
		if ( null !== $isHomePage ) {
			return $isHomePage;
		}

		$post = aioseo()->helpers->getPost( $post );

		$isHomePage = ( 'page' === get_option( 'show_on_front' ) && ! empty( $post->ID ) && (int) get_option( 'page_on_front' ) === $post->ID );

		return $isHomePage;
	}

	/**
	 * Checks whether the current page is the dynamic homepage.
	 *
	 * @since 4.2.3
	 *
	 * @return bool Whether the current page is the dynamic homepage.
	 */
	public function isDynamicHomePage() {
		return is_front_page() && is_home();
	}

	/**
	 * Checks whether the current page is the static posts page.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether the current page is the static posts page.
	 */
	public function isStaticPostsPage( $post = null ) {
		static $isStaticPostsPage = null;
		if ( null !== $isStaticPostsPage ) {
			return $isStaticPostsPage;
		}

		$post = aioseo()->helpers->getPost( $post );

		$isStaticPostsPage = (
			( is_home() && ( 0 !== (int) get_option( 'page_for_posts' ) ) ) ||
			( ! empty( $post->ID ) && (int) get_option( 'page_for_posts' ) === $post->ID )
		);

		return $isStaticPostsPage;
	}

	/**
	 * Checks whether current page supports meta.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether the current page supports meta.
	 */
	public function supportsMeta() {
		return ! is_date() && ! is_author() && ! is_search() && ! is_404();
	}

	/**
	 * Returns the current post object.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|int|bool $postId The post ID.
	 * @return \WP_Post|null             The post object.
	 */
	public function getPost( $postId = false ) {
		$postId = is_a( $postId, 'WP_Post' ) ? $postId->ID : $postId;

		if ( aioseo()->helpers->isWooCommerceShopPage( $postId ) ) {
			return get_post( wc_get_page_id( 'shop' ) );
		}

		if ( is_front_page() || is_home() ) {
			$showOnFront = 'page' === get_option( 'show_on_front' );
			if ( $showOnFront ) {
				if ( is_front_page() ) {
					$pageOnFront = (int) get_option( 'page_on_front' );

					return get_post( $pageOnFront );
				} elseif ( is_home() ) {
					$pageForPosts = (int) get_option( 'page_for_posts' );

					return get_post( $pageForPosts );
				}
			}
		}

		// Learnpress lessons load the course. So here we need to switch to the lesson.
		$learnPressLesson = aioseo()->helpers->getLearnPressLesson();
		if ( ! $postId && $learnPressLesson ) {
			$postId = $learnPressLesson;
		}

		// Allow other plugins to filter the post ID e.g. for a special archive page.
		$postId = apply_filters( 'aioseo_get_post_id', $postId );

		// We need to check these conditions and cannot always return get_post() because we'll return the first post on archive pages (dynamic homepage, term pages, etc.).

		if (
			$this->isScreenBase( 'post' ) ||
			$postId ||
			is_singular()
		) {
			return get_post( $postId );
		}

		return null;
	}

	/**
	 * Returns the term object for the given ID or the one from the main query.
	 *
	 * @since 4.7.8
	 *
	 * @param  int    $termId   The term ID.
	 * @param  string $taxonomy The taxonomy.
	 * @return \WP_Term         The term object.
	 */
	public function getTerm( $termId = 0, $taxonomy = '' ) {
		$term = null;
		if ( $termId ) {
			$term = get_term( $termId, $taxonomy );
		} else {
			$term = get_queried_object();
		}

		// If the term is a Product Attribute, set its parent taxonomy to our fake
		// "product_attributes" taxonomy so we can use the default settings.
		if ( is_a( $term, 'WP_Term' ) && $this->isWooCommerceProductAttribute( $term->taxonomy ) ) {
			$term           = clone $term;
			$term->taxonomy = 'product_attributes';
		}

		return $term;
	}

	/**
	 * Returns the current post ID.
	 *
	 * @since 4.3.1
	 *
	 * @return int|null The post ID.
	 */
	public function getPostId() {
		$post = $this->getPost();

		return is_object( $post ) && property_exists( $post, 'ID' ) ? $post->ID : null;
	}

	/**
	 * Returns the post content after parsing it.
	 *
	 * @since 4.1.5
	 *
	 * @param  \WP_Post|int $post The post (optional).
	 * @return string             The post content.
	 */
	public function getPostContent( $post = null ) {
		$post = is_a( $post, 'WP_Post' ) ? $post : $this->getPost( $post );

		static $content = [];
		if ( isset( $content[ $post->ID ] ) ) {
			return $content[ $post->ID ];
		}

		// We need to process the content for page builders.
		$postContent = $post->post_content;
		$pageBuilder = aioseo()->helpers->getPostPageBuilderName( $post->ID );
		if ( ! empty( $pageBuilder ) ) {
			$postContent = aioseo()->standalone->pageBuilderIntegrations[ $pageBuilder ]->processContent( $post->ID, $postContent );
		}

		$postContent = is_string( $postContent ) ? $postContent : '';

		$content[ $post->ID ] = $this->theContent( $postContent );

		if ( apply_filters( 'aioseo_description_include_custom_fields', true, $post ) ) {
			$content[ $post->ID ] .= $this->theContent( $this->getPostCustomFieldsContent( $post ) );
		}

		return $content[ $post->ID ];
	}

	/**
	 * Gets the content from configured custom fields.
	 *
	 * @since 4.2.7
	 *
	 * @param  \WP_Post|int $post A post object or ID.
	 * @return string             The content.
	 */
	public function getPostCustomFieldsContent( $post = null ) {
		$post = is_a( $post, 'WP_Post' ) ? $post : $this->getPost( $post );

		if ( ! aioseo()->dynamicOptions->searchAppearance->postTypes->has( $post->post_type ) ) {
			return '';
		}

		$customFieldKeys = aioseo()->dynamicOptions->searchAppearance->postTypes->{$post->post_type}->customFields;
		if ( empty( $customFieldKeys ) ) {
			return '';
		}

		$customFieldKeys = explode( ' ', sanitize_text_field( $customFieldKeys ) );

		return aioseo()->helpers->getCustomFieldsContent( $post, $customFieldKeys );
	}

	/**
	 * Returns the post content after parsing shortcodes and blocks.
	 * We avoid using the "the_content" hook because it breaks stuff if we call it outside the loop or main query.
	 * See https://developer.wordpress.org/reference/hooks/the_content/
	 *
	 * @since 4.1.5.2
	 *
	 * @param  string $postContent The post content.
	 * @return string              The parsed post content.
	 */
	public function theContent( $postContent ) {
		if ( ! aioseo()->options->searchAppearance->advanced->runShortcodes ) {
			return $postContent;
		}

		// Because do_blocks() and do_shortcodes() can trigger conflicts, we need to clone these objects and restore them afterwards.
		// We need to clone deep to sever pointers/references because these have nested object properties.
		global $wp_query, $post; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$this->originalQuery = $this->deepClone( $wp_query ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$this->originalPost  = is_a( $post, 'WP_Post' ) ? $this->deepClone( $post ) : null;

		// The order of the function calls below is intentional and should NOT change.
		$postContent = do_blocks( $postContent );
		$postContent = wpautop( $postContent );
		$postContent = $this->doShortcodes( $postContent );

		$this->restoreWpQuery();

		return $postContent;
	}

	/**
	 * Returns the description based on the post content.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post|int $post The post (optional).
	 * @return string             The description.
	 */
	public function getDescriptionFromContent( $post = null ) {
		$post = is_a( $post, 'WP_Post' ) ? $post : $this->getPost( $post );

		static $content = [];
		if ( isset( $content[ $post->ID ] ) ) {
			return $content[ $post->ID ];
		}

		$content[ $post->ID ] = '';
		if ( ! empty( $post->post_password ) ) {
			return $content[ $post->ID ];
		}

		$postContent = $this->getPostContent( $post );

		// Strip images, captions and WP oembed wrappers (e.g. YouTube URLs) from the post content.
		$postContent          = preg_replace( '/(<figure.*?\/figure>|<img.*?\/>|<div.*?class="wp-block-embed__wrapper".*?>.*?<\/div>)/s', '', (string) $postContent );
		$postContent          = str_replace( ']]>', ']]&gt;', (string) $postContent );
		$postContent          = trim( wp_strip_all_tags( strip_shortcodes( (string) $postContent ) ) );
		$content[ $post->ID ] = wp_trim_words( (string) $postContent, 55, '' );

		return $content[ $post->ID ];
	}

	/**
	 * Returns custom fields as a string.
	 *
	 * @since 4.0.6
	 *
	 * @param  \WP_Post|int $post The post.
	 * @param  array        $keys The post meta_keys to check for values.
	 * @return string             The custom field content.
	 */
	public function getCustomFieldsContent( $post = null, $keys = [] ) {
		$post = is_a( $post, 'WP_Post' ) ? $post : $this->getPost( $post );

		$customFieldContent = '';

		$acfFields = $this->getAcfContent( $post );
		foreach ( $keys as $key ) {
			// Try ACF.
			if ( isset( $acfFields[ $key ] ) && is_scalar( $acfFields[ $key ] ) ) {
				$customFieldContent .= "$acfFields[$key] ";
				continue;
			}

			// Fallback to post meta.
			$value = get_post_meta( $post->ID, $key, true );
			if ( $value && is_scalar( $value ) ) {
				$customFieldContent .= $value . ' ';
			}
		}

		return $customFieldContent;
	}

	/**
	 * Returns if the page is a special type (WooCommerce pages, Privacy page).
	 *
	 * @since 4.0.0
	 *
	 * @param  int  $postId The post ID.
	 * @return bool         If the page is special or not.
	 */
	public function isSpecialPage( $postId = 0 ) {
		$specialPages = $this->getSpecialPageIds();

		return in_array( (int) $postId, $specialPages, true );
	}

	/**
	 * Returns the ID of all special pages (e.g. homepage, blog page, WooCommerce, BuddyPress, etc.).
	 * This cannot be cached because the plugins need to be loaded first.
	 *
	 * @since 4.7.3
	 *
	 * @return array The IDs of all special pages.
	 */
	public function getSpecialPageIds() {
		$pageForPostsId         = (int) get_option( 'page_for_posts' );
		$pageForPrivacyPolicyId = (int) get_option( 'wp_page_for_privacy_policy' );
		$buddyPressPageIds      = $this->getBuddyPressPageIds();
		$wooCommercePageIds     = array_values( $this->getWooCommercePages() );

		$specialPageIds = array_merge(
			[
				$pageForPostsId,
				$pageForPrivacyPolicyId,
			],
			$buddyPressPageIds,
			$wooCommercePageIds
		);

		// Ensure all values are integers.
		$specialPageIds = array_map( 'intval', $specialPageIds );

		return $specialPageIds;
	}

	/**
	 * Returns whether a post is eligible for being analyzed by TruSEO.
	 *
	 * @since   4.6.1
	 * @version 4.7.3 Renamed from "isPageAnalysisEligible" to "isTruSeoEligible" to make it more clear.
	 *
	 * @param  int  $postId Post ID.
	 * @return bool         Whether a post is eligible for being analyzed by TruSEO.
	 */
	public function isTruSeoEligible( $postId ) {
		static $isTruSeoEnabled = null;
		if ( null === $isTruSeoEnabled ) {
			$isTruSeoEnabled = aioseo()->options->advanced->truSeo;
		}

		if ( ! $isTruSeoEnabled ) {
			return false;
		}

		static $isPostEligible = [];
		if ( isset( $isPostEligible[ $postId ] ) ) {
			return $isPostEligible[ $postId ];
		}

		// Set the default to true.
		$isPostEligible[ $postId ] = true;

		$wpPost = $this->getPost( $postId );
		if ( ! is_a( $wpPost, 'WP_Post' ) ) {
			$isPostEligible[ $postId ] = false;

			return false;
		}

		$eligiblePostTypes = $this->getTruSeoEligiblePostTypes();
		if (
			! in_array( $wpPost->post_type, $eligiblePostTypes, true ) ||
			$this->isSpecialPage( $wpPost->ID )
		) {
			$isPostEligible[ $postId ] = false;
		}

		return $isPostEligible[ $postId ];
	}

	/**
	 * Returns the post types that are eligible for TruSEO analysis.
	 *
	 * @since 4.7.3
	 *
	 * @return array The post types that are eligible for TruSEO analysis.
	 */
	public function getTruSeoEligiblePostTypes() {
		$allowedPostTypes  = aioseo()->helpers->getPublicPostTypes( true );
		$excludedPostTypes = [ 'attachment', 'aioseo-location', 'web-story' ];
		if ( class_exists( 'bbPress' ) ) {
			$excludedPostTypes = array_merge( $excludedPostTypes, [ 'forum', 'topic', 'reply' ] );
		}

		// Remove the excluded post types from the allowed ones.
		$allowedPostTypes = array_diff( $allowedPostTypes, $excludedPostTypes );

		// Now, check if the metabox is enabled and that the post type is public for each of these.
		foreach ( $allowedPostTypes as $postType ) {
			$postObjectType = get_post_type_object( $postType );
			if ( is_a( $postObjectType, 'WP_Post_Type' ) && ! $postObjectType->public ) {
				unset( $allowedPostTypes[ $postType ] );
			}

			$dynamicOptions = aioseo()->dynamicOptions->noConflict();
			if ( ! $dynamicOptions->searchAppearance->postTypes->has( $postType, false ) || ! $dynamicOptions->{$postType}->advanced->showMetaBox ) {
				// If not, unset it.
				unset( $allowedPostTypes[ $postType ] );
			}
		}

		// Considering post types get registered during various stages of the WP load process, we should not cache this.
		return $allowedPostTypes;
	}

	/**
	 * Returns the page number of the current page.
	 *
	 * @since 4.0.0
	 *
	 * @return int The page number.
	 */
	public function getPageNumber() {
		$page = get_query_var( 'page' );
		if ( ! empty( $page ) ) {
			return (int) $page;
		}

		$paged = get_query_var( 'paged' );
		if ( ! empty( $paged ) ) {
			return (int) $paged;
		}

		return 1;
	}


	/**
	 * Returns the page number for the comment page.
	 *
	 * @since 4.2.1
	 *
	 * @return int|false The page number or false if we're not on a comment page.
	 */
	public function getCommentPageNumber() {
		$cpage = get_query_var( 'cpage', null );
		if ( $this->isBlockTheme() ) {
			global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

			// For block themes we can't rely on `get_query_var()` because of {@see build_comment_query_vars_from_block()},
			// so we need to check the query directly.
			$cpage = $wp_query->query['cpage'] ?? null; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		}

		return isset( $cpage ) ? (int) $cpage : false;
	}

	/**
	 * Check if the post passed in is a valid post, not a revision or autosave.
	 *
	 * @since 4.0.5
	 *
	 * @param  \WP_Post $post                The Post object to check.
	 * @param  array    $allowedPostStatuses Allowed post statuses.
	 * @return bool                          True if valid, false if not.
	 */
	public function isValidPost( $post, $allowedPostStatuses = [ 'publish' ] ) {
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return false;
		}

		if ( ! is_object( $post ) ) {
			$post = get_post( $post );
		}

		// No post, no go.
		if ( empty( $post ) ) {
			return false;
		}

		// In order to prevent recursion, we are skipping scheduled-action posts and revisions.
		if (
			'scheduled-action' === $post->post_type ||
			'revision' === $post->post_type
		) {
			return false;
		}

		// Ensure this post has the proper post status.
		if (
			! in_array( $post->post_status, $allowedPostStatuses, true ) &&
			! in_array( 'all', $allowedPostStatuses, true )
		) {
			return false;
		}

		return true;
	}

	/**
	 * Checks whether the given URL is a valid attachment.
	 *
	 * @since 4.0.13
	 *
	 * @param  string $url The URL.
	 * @return bool        Whether the URL is a valid attachment.
	 */
	public function isValidAttachment( $url ) {
		$uploadDirUrl = aioseo()->helpers->escapeRegex( $this->getWpContentUrl() );

		return preg_match( "/$uploadDirUrl.*/", (string) $url );
	}

	/**
	 * Tries to convert an attachment URL into a post ID.
	 *
	 * This our own optimized version of attachment_url_to_postid().
	 *
	 * @since 4.0.13
	 *
	 * @param  string   $url The attachment URL.
	 * @return int|bool      The attachment ID or false if no attachment could be found.
	 */
	public function attachmentUrlToPostId( $url ) {
		$cacheName = 'attachment_url_to_post_id_' . sha1( "aioseo_attachment_url_to_post_id_$url" );

		$cachedId = aioseo()->core->cache->get( $cacheName );
		if ( $cachedId ) {
			return 'none' !== $cachedId && is_numeric( $cachedId ) ? (int) $cachedId : false;
		}

		$path          = $url;
		$uploadDirInfo = wp_get_upload_dir();

		$siteUrl   = wp_parse_url( $uploadDirInfo['url'] );
		$imagePath = wp_parse_url( $path );

		// Force the protocols to match if needed.
		if ( isset( $imagePath['scheme'] ) && ( $imagePath['scheme'] !== $siteUrl['scheme'] ) ) {
			$path = str_replace( $imagePath['scheme'], $siteUrl['scheme'], $path );
		}

		if ( ! $this->isValidAttachment( $path ) ) {
			aioseo()->core->cache->update( $cacheName, 'none' );

			return false;
		}

		if ( 0 === strpos( $path, $uploadDirInfo['baseurl'] . '/' ) ) {
			$path = substr( $path, strlen( $uploadDirInfo['baseurl'] . '/' ) );
		}

		$results = aioseo()->core->db->start( 'postmeta' )
			->select( 'post_id' )
			->where( 'meta_key', '_wp_attached_file' )
			->where( 'meta_value', $path )
			->limit( 1 )
			->run()
			->result();

		if ( empty( $results[0]->post_id ) ) {
			aioseo()->core->cache->update( $cacheName, 'none' );

			return false;
		}

		aioseo()->core->cache->update( $cacheName, $results[0]->post_id );

		return $results[0]->post_id;
	}

	/**
	 * Returns true if the request is a non-legacy REST API request.
	 * This function was copied from WooCommerce and improved.
	 *
	 * @since 4.1.2
	 *
	 * @return bool True if this is a REST API request.
	 */
	public function isRestApiRequest() {
		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return true;
		}

		global $wp_rewrite; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		if ( empty( $wp_rewrite ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			return false;
		}

		if ( empty( $_SERVER['REQUEST_URI'] ) ) {
			return false;
		}

		$restUrl = wp_parse_url( get_rest_url() );
		$restUrl = $restUrl['path'] . ( ! empty( $restUrl['query'] ) ? '?' . $restUrl['query'] : '' );

		$isRestApiRequest = ( 0 === strpos( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ), $restUrl ) );

		return apply_filters( 'aioseo_is_rest_api_request', $isRestApiRequest );
	}

	/**
	 * Checks whether the current request is an AJAX, CRON or REST request.
	 *
	 * @since 4.1.3
	 *
	 * @return bool Whether the request is an AJAX, CRON or REST request.
	 */
	public function isAjaxCronRestRequest() {
		return wp_doing_ajax() || wp_doing_cron() || $this->isRestApiRequest();
	}

	/**
	 * Check if we are in the middle of a WP-CLI call.
	 *
	 * @since 4.2.8
	 *
	 * @return bool True if we are in the WP_CLI context.
	 */
	public function isDoingWpCli() {
		return defined( 'WP_CLI' ) && WP_CLI;
	}

	/**
	 * Checks whether we're on the given screen.
	 *
	 * @since   4.0.7
	 * @version 4.3.1
	 *
	 * @param  string $screenName The screen name.
	 * @param  string $comparison Check as a prefix.
	 * @return bool               Whether we're on the given screen.
	 */
	public function isScreenBase( $screenName, $comparison = '' ) {
		$screen = $this->getCurrentScreen();
		if ( ! $screen || ! isset( $screen->base ) ) {
			return false;
		}

		if ( 'prefix' === $comparison ) {
			return 0 === stripos( $screen->base, $screenName );
		}

		return $screen->base === $screenName;
	}

	/**
	 * Returns if current screen is of a post type
	 *
	 * @since 4.0.17
	 *
	 * @param  string $postType Post type slug
	 * @return bool             True if the current screen is a post type screen.
	 */
	public function isScreenPostType( $postType ) {
		$screen = $this->getCurrentScreen();
		if ( ! $screen || ! isset( $screen->post_type ) ) {
			return false;
		}

		return $screen->post_type === $postType;
	}

	/**
	 * Returns if current screen is a post list, optionaly of a post type.
	 *
	 * @since 4.2.4
	 *
	 * @param  string $postType Post type slug.
	 * @return bool             Is a post list.
	 */
	public function isScreenPostList( $postType = '' ) {
		$screen = $this->getCurrentScreen();
		if (
			! $this->isScreenBase( 'edit' ) ||
			empty( $screen->post_type )
		) {
			return false;
		}

		if ( ! empty( $postType ) && $screen->post_type !== $postType ) {
			return false;
		}

		return true;
	}

	/**
	 * Returns if current screen is a post edit screen, optionaly of a post type.
	 *
	 * @since 4.2.4
	 *
	 * @param  string $postType Post type slug.
	 * @return bool             Is a post editing screen.
	 */
	public function isScreenPostEdit( $postType = '' ) {
		$screen = $this->getCurrentScreen();
		if (
			! $this->isScreenBase( 'post' ) ||
			empty( $screen->post_type )
		) {
			return false;
		}

		if ( ! empty( $postType ) && $screen->post_type !== $postType ) {
			return false;
		}

		return true;
	}

	/**
	 * Gets current admin screen.
	 *
	 * @since 4.0.17
	 *
	 * @return false|\WP_Screen|null
	 */
	public function getCurrentScreen() {
		if ( ! is_admin() || ! function_exists( 'get_current_screen' ) ) {
			return false;
		}

		return get_current_screen();
	}

	/**
	 * Checks whether the current site is a multisite subdomain.
	 *
	 * @since 4.1.9
	 *
	 * @return bool Whether the current site is a subdomain.
	 */
	public function isSubdomain() {
		if ( ! is_multisite() ) {
			return false;
		}

		return apply_filters( 'aioseo_multisite_subdomain', is_subdomain_install() );
	}

	/**
	 * Returns if the current page is the login or register page.
	 *
	 * @since 4.2.1
	 *
	 * @return bool Login or register page.
	 */
	public function isWpLoginPage() {
		// We can't sanitize the filename using sanitize_file_name() here because it will cause issues with custom login pages and certain plugins/themes where this function is not defined.
		$self = ! empty( $_SERVER['PHP_SELF'] ) ? sanitize_text_field( wp_unslash( $_SERVER['PHP_SELF'] ) ) : ''; // phpcs:ignore HM.Security.ValidatedSanitizedInput.InputNotSanitized
		if ( preg_match( '/wp-login\.php$|wp-register\.php$/', (string) $self ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Returns which type of WordPress page we're seeing.
	 * It will only work if {@see \WP_Query::$queried_object} has been set.
	 *
	 * @link https://developer.wordpress.org/themes/basics/template-hierarchy/#filter-hierarchy
	 *
	 * @since 4.2.8
	 *
	 * @return string|null The template type or `null` if no match.
	 */
	public function getTemplateType() {
		static $type = null;

		if ( ! empty( $type ) ) {
			return $type;
		}

		if ( is_attachment() ) {
			$type = 'attachment';
		} elseif ( is_single() ) {
			$type = 'single';
		} elseif (
			is_page() ||
			$this->isStaticPostsPage() ||
			$this->isWooCommerceShopPage()
		) {
			$type = 'page';
		} elseif ( is_author() ) { // An author page is an archive page, so it needs to be checked before `is_archive()`.
			$type = 'author';
		} elseif (
			is_tax() ||
			is_category() ||
			is_tag()
		) { // A taxonomy term page is an archive page, so it needs to be checked before `is_archive()`.
			$type = 'taxonomy';
		} elseif ( is_date() ) { // A date page is an archive page, so it needs to be checked before `is_archive()`.
			$type = 'date';
		} elseif ( is_archive() ) {
			$type = 'archive';
		} elseif ( is_home() && is_front_page() ) {
			$type = 'dynamic_home';
		} elseif ( is_search() ) {
			$type = 'search';
		}

		return $type;
	}

	/**
	 * Sets the given post as the queried object of the main query.
	 *
	 * @since 4.3.0
	 *
	 * @param  \WP_Post|int $wpPost The post object or ID.
	 * @return void
	 */
	public function setWpQueryPost( $wpPost ) {
		$wpPost = is_a( $wpPost, 'WP_Post' ) ? $wpPost : get_post( $wpPost );
		// phpcs:disable Squiz.NamingConventions.ValidVariableName
		global $wp_query, $post;
		$this->originalQuery = $this->deepClone( $wp_query );
		$this->originalPost  = is_a( $post, 'WP_Post' ) ? $this->deepClone( $post ) : null;

		$wp_query->posts                 = [ $wpPost ];
		$wp_query->post                  = $wpPost;
		$wp_query->post_count            = 1;
		$wp_query->get_queried_object_id = (int) $wpPost->ID;
		$wp_query->queried_object        = $wpPost;
		$wp_query->is_single             = true;
		$wp_query->is_singular           = true;

		if ( 'page' === $wpPost->post_type ) {
			$wp_query->is_page = true;
		}
		// phpcs:enable Squiz.NamingConventions.ValidVariableName

		$post = $wpPost;
	}

	/**
	 * Restores the main query back to the original query.
	 *
	 * @since 4.3.0
	 *
	 * @return void
	 */
	public function restoreWpQuery() {
		global $wp_query, $post; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if ( is_a( $this->originalQuery, 'WP_Query' ) ) {
			// Loop over all properties and replace the ones that have changed.
			// We want to avoid replacing the entire object because it can cause issues with other plugins.
			foreach ( $this->originalQuery as $key => $value ) {
				if ( $value !== $wp_query->{$key} ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
					$wp_query->{$key} = $value; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				}
			}
		}

		if ( is_a( $this->originalPost, 'WP_Post' ) ) {
			foreach ( $this->originalPost as $key => $value ) {
				if ( $value !== $post->{$key} ) {
					$post->{$key} = $value;
				}
			}
		}

		$this->originalQuery = null;
		$this->originalPost  = null;
	}

	/**
	 * Gets the list of theme features.
	 *
	 * @since 4.4.9
	 *
	 * @return array List of theme features.
	 */
	public function getThemeFeatures() {
		global $_wp_theme_features; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		return isset( $_wp_theme_features ) && is_array( $_wp_theme_features ) ? $_wp_theme_features : []; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
	}

	/**
	 * Returns whether the active theme is a block-based theme or not.
	 *
	 * @since 4.5.3
	 *
	 * @return bool Whether the active theme is a block-based theme or not.
	 */
	public function isBlockTheme() {
		if ( function_exists( 'wp_is_block_theme' ) ) {
			return wp_is_block_theme(); // phpcs:ignore AIOSEO.WpFunctionUse.NewFunctions.wp_is_block_themeFound
		}

		return false;
	}

	/**
	 * Retrieves the website name.
	 *
	 * @since 4.6.1
	 *
	 * @return string The website name.
	 */
	public function getWebsiteName() {
		return aioseo()->options->searchAppearance->global->schema->websiteName
			? aioseo()->tags->replaceTags( aioseo()->options->searchAppearance->global->schema->websiteName )
			: aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) );
	}
}Common/Traits/Helpers/Deprecated.php000066600000006757151135505570013447 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Contains deprecated methods to be removed at a later date..
 *
 * @since 4.1.9
 */
trait Deprecated {
	/**
	 * Helper method to enqueue scripts.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $script The script to enqueue.
	 * @param  string $url    The URL of the script.
	 * @param  bool   $vue    Whether or not this is a vue script.
	 * @return void
	 */
	public function enqueueScript( $script, $url, $vue = true ) {
		if ( ! wp_script_is( $script, 'enqueued' ) ) {
			wp_enqueue_script(
				$script,
				$this->getScriptUrl( $url, $vue ),
				[],
				aioseo()->version,
				true
			);
		}
	}

	/**
	 * Helper method to enqueue stylesheets.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $style The stylesheet to enqueue.
	 * @param  string $url   The URL of the stylesheet.
	 * @param  bool   $vue    Whether or not this is a vue stylesheet.
	 * @return void
	 */
	public function enqueueStyle( $style, $url, $vue = true ) {
		if ( ! wp_style_is( $style, 'enqueued' ) && $this->shouldEnqueue( $url ) ) {
			wp_enqueue_style(
				$style,
				$this->getScriptUrl( $url, $vue ),
				[],
				aioseo()->version
			);
		}
	}

	/**
	 * Whether or not we should enqueue a file.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $url The url to check against.
	 * @return bool        Whether or not we should enqueue.
	 */
	private function shouldEnqueue( $url ) {
		$version  = strtoupper( aioseo()->versionPath );
		$host     = defined( 'AIOSEO_DEV_' . $version ) ? constant( 'AIOSEO_DEV_' . $version ) : false;

		if ( ! $host ) {
			return true;
		}

		if ( false !== strpos( $url, 'chunk-common.css' ) ) {
			// return false;
		}

		return true;
	}

	/**
	 * Retrieve the proper URL for this script or style.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $url The url.
	 * @param  bool   $vue Whether or not this is a vue script.
	 * @return string      The modified url.
	 */
	public function getScriptUrl( $url, $vue = true ) {
		$version  = strtoupper( aioseo()->versionPath );
		$host     = $vue && defined( 'AIOSEO_DEV_' . $version ) ? constant( 'AIOSEO_DEV_' . $version ) : false;
		$localUrl = $url;
		$url      = plugins_url( 'dist/' . aioseo()->versionPath . '/assets/' . $url, AIOSEO_FILE );

		if ( ! $host ) {
			return $url;
		}

		if ( $host && ! self::$connection ) {
			$splitHost        = explode( ':', str_replace( '/', '', str_replace( 'http://', '', str_replace( 'https://', '', $host ) ) ) );
			self::$connection = @fsockopen( $splitHost[0], $splitHost[1] ); // phpcs:ignore WordPress
		}

		if ( ! self::$connection ) {
			return $url;
		}

		return $host . $localUrl;
	}

	/**
	 * Returns the filesystem object if we have access to it.
	 *
	 * @since 4.0.0
	 *
	 * @param  array                    $args The connection args.
	 * @return \WP_Filesystem_Base|bool       The filesystem object.
	 */
	public function wpfs( $args = [] ) {
		require_once ABSPATH . 'wp-admin/includes/file.php';
		WP_Filesystem( $args );

		// phpcs:disable Squiz.NamingConventions.ValidVariableName
		global $wp_filesystem;
		if ( is_object( $wp_filesystem ) ) {
			return $wp_filesystem;
		}
		// phpcs:enable Squiz.NamingConventions.ValidVariableName

		return false;
	}

	/**
	 * Checks whether the current request is an AJAX, CRON or REST request.
	 *
	 * @since 4.1.9.1
	 *
	 * @return bool Whether the current request is an AJAX, CRON or REST request.
	 */
	public function isAjaxCronRest() {
		return $this->isAjaxCronRestRequest();
	}
}Common/Traits/Helpers/Language.php000066600000000744151135505570013120 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Contains i18n and language (code) helper methods.
 *
 * @since 4.1.4
 */
trait Language {
	/**
	 * Returns the language of the current response in BCP 47 format.
	 *
	 * @since 4.1.4
	 *
	 * @return string The language code in BCP 47 format.
	 */
	public function currentLanguageCodeBCP47() {
		return str_replace( '_', '-', determine_locale() );
	}
}Common/Traits/Helpers/PostType.php000066600000001410151135505570013153 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Contains WordPress Post Type helpers.
 *
 * @since 4.2.4
 */
trait PostType {
	/**
	 * Returns a post type feature.
	 *
	 * @since 4.2.4
	 *
	 * @param  string|\WP_Post_Type $postType The post type.
	 * @param  string               $feature  The feature to find.
	 * @return mixed|false                    The post type feature or false if not found.
	 */
	public function getPostTypeFeature( $postType, $feature ) {
		if ( is_string( $postType ) ) {
			$postType = get_post_type_object( $postType );
		}

		if ( ! is_a( $postType, 'WP_Post_Type' ) || ! isset( $postType->$feature ) ) {
			return false;
		}

		return $postType->$feature;
	}
}Common/Traits/Helpers/Vue.php000066600000065374151135505570012146 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Integrations\WpCode as WpCodeIntegration;
use AIOSEO\Plugin\Common\Models;
use AIOSEO\Plugin\Common\Tools;

/**
 * Contains all Vue related helper methods.
 *
 * @since 4.1.4
 */
trait Vue {
	/**
	 * Holds the data for Vue.
	 *
	 * @since 4.4.9
	 *
	 * @var array
	 */
	private $data = [];

	/**
	 * Optional arguments for setting the data.
	 *
	 * @since 4.4.9
	 *
	 * @var array
	 */
	private $args = [];

	/**
	 * Holds the cached data.
	 *
	 * @since 4.5.1
	 *
	 * @var array
	 */
	private $cache = [];

	/**
	 * Returns the data for Vue.
	 *
	 * @since   4.0.0
	 * @version 4.4.9
	 *
	 * @param  string $page         The current page.
	 * @param  int    $staticPostId Data for a specific post.
	 * @param  string $integration  Data for integration (builder).
	 * @return array                The data.
	 */
	public function getVueData( $page = null, $staticPostId = null, $integration = null ) {
		$this->args = compact( 'page', 'staticPostId', 'integration' );
		$hash       = md5( implode( '', array_map( 'strval', $this->args ) ) );
		if ( isset( $this->cache[ $hash ] ) ) {
			return $this->cache[ $hash ];
		}

		// Clear the data so we start fresh.
		$this->data = [];

		$this->setInitialData();
		$this->setMultisiteData();
		$this->setPostData();
		$this->setDashboardData();
		$this->setSearchStatisticsData();
		$this->setSitemapsData();
		$this->setSetupWizardData();
		$this->setSearchAppearanceData();
		$this->setSocialNetworksData();
		$this->setSeoRevisionsData();
		$this->setToolsOrSettingsData();
		$this->setPageBuilderData();
		$this->setWritingAssistantData();
		$this->setBreadcrumbsData();
		$this->setSeoAnalyzerData();

		$this->cache[ $hash ] = $this->data;

		return $this->cache[ $hash ];
	}

	/**
	 * Set Vue initial data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setInitialData() {
		$screen           = aioseo()->helpers->getCurrentScreen();
		$isStaticHomePage = 'page' === get_option( 'show_on_front' );
		$staticHomePage   = intval( get_option( 'page_on_front' ) );

		$this->data = [
			'page'               => $this->args['page'],
			'screen'             => [
				'base'        => isset( $screen->base ) ? $screen->base : '',
				'postType'    => isset( $screen->post_type ) ? $screen->post_type : '',
				'blockEditor' => isset( $screen->is_block_editor ) ? $screen->is_block_editor : false,
				'new'         => isset( $screen->action ) && 'add' === $screen->action
			],
			'internalOptions'    => aioseo()->internalOptions->all(),
			'options'            => aioseo()->options->all(),
			'dynamicOptions'     => aioseo()->dynamicOptions->all(),
			'deprecatedOptions'  => aioseo()->internalOptions->getAllDeprecatedOptions( true ),
			'settings'           => aioseo()->settings->all(),
			'additional_scripts' => apply_filters( 'aioseo_vue_additional_scripts_enabled', true ),
			'tags'               => aioseo()->tags->all( true ),
			'nonce'              => wp_create_nonce( 'wp_rest' ),
			'urls'               => [
				'domain'            => $this->getSiteDomain(),
				'mainSiteUrl'       => $this->getSiteUrl(),
				'siteLogo'          => aioseo()->helpers->getSiteLogoUrl(),
				'home'              => home_url(),
				'restUrl'           => aioseo()->helpers->getRestUrl(),
				'editScreen'        => admin_url( 'edit.php' ),
				'publicPath'        => aioseo()->core->assets->normalizeAssetsHost( plugin_dir_url( AIOSEO_FILE ) ),
				'assetsPath'        => aioseo()->core->assets->getAssetsPath(),
				'generalSitemapUrl' => aioseo()->sitemap->helpers->getUrl( 'general' ),
				'rssSitemapUrl'     => aioseo()->sitemap->helpers->getUrl( 'rss' ),
				'robotsTxtUrl'      => $this->getSiteUrl() . '/robots.txt',
				'upgradeUrl'        => apply_filters( 'aioseo_upgrade_link', AIOSEO_MARKETING_URL ),
				'staticHomePage'    => 'page' === get_option( 'show_on_front' ) ? get_edit_post_link( get_option( 'page_on_front' ), 'url' ) : null,
				'feeds'             => [
					'rdf'            => get_bloginfo( 'rdf_url' ),
					'rss'            => get_bloginfo( 'rss_url' ),
					'atom'           => get_bloginfo( 'atom_url' ),
					'global'         => get_bloginfo( 'rss2_url' ),
					'globalComments' => get_bloginfo( 'comments_rss2_url' ),
					'staticBlogPage' => $this->getBlogPageId() ? trailingslashit( get_permalink( $this->getBlogPageId() ) ) . 'feed' : ''
				],
				'connect'           => add_query_arg( [
					'siteurl'  => site_url(),
					'homeurl'  => home_url(),
					'redirect' => rawurldecode( base64_encode( admin_url( 'index.php?page=aioseo-connect' ) ) )
				], defined( 'AIOSEO_CONNECT_URL' ) ? AIOSEO_CONNECT_URL : 'https://connect.aioseo.com' ),
				'aio'               => [
					'about'            => is_network_admin() ? network_admin_url( 'admin.php?page=aioseo-about' ) : admin_url( 'admin.php?page=aioseo-about' ),
					'dashboard'        => admin_url( 'admin.php?page=aioseo' ),
					'featureManager'   => admin_url( 'admin.php?page=aioseo-feature-manager' ),
					'linkAssistant'    => admin_url( 'admin.php?page=aioseo-link-assistant' ),
					'localSeo'         => admin_url( 'admin.php?page=aioseo-local-seo' ),
					'monsterinsights'  => admin_url( 'admin.php?page=aioseo-monsterinsights' ),
					'redirects'        => admin_url( 'admin.php?page=aioseo-redirects' ),
					'searchAppearance' => admin_url( 'admin.php?page=aioseo-search-appearance' ),
					'searchStatistics' => admin_url( 'admin.php?page=aioseo-search-statistics' ),
					'seoAnalysis'      => admin_url( 'admin.php?page=aioseo-seo-analysis' ),
					'settings'         => admin_url( 'admin.php?page=aioseo-settings' ),
					'sitemaps'         => admin_url( 'admin.php?page=aioseo-sitemaps' ),
					'socialNetworks'   => admin_url( 'admin.php?page=aioseo-social-networks' ),
					'tools'            => admin_url( 'admin.php?page=aioseo-tools' ),
					'wizard'           => admin_url( 'index.php?page=aioseo-setup-wizard' ),
					'networkSettings'  => is_network_admin() ? network_admin_url( 'admin.php?page=aioseo-settings' ) : '',
					'seoRevisions'     => admin_url( 'admin.php?page=aioseo-seo-revisions' ),
				],
				'admin'             => [
					'widgets'          => admin_url( 'widgets.php' ),
					'optionsReading'   => admin_url( 'options-reading.php' ),
					'scheduledActions' => admin_url( '/tools.php?page=action-scheduler&status=pending&s=aioseo' ),
					'generalSettings'  => admin_url( 'options-general.php' )
				],
				'truSeoWorker'      => aioseo()->core->assets->jsUrl( 'src/app/tru-seo/analyzer/main.js' )
			],
			'backups'            => [],
			'importers'          => [],
			'data'               => [
				'server'                => aioseo()->helpers->getServerName(),
				'robots'                => [
					'defaultRules'      => [],
					'hasPhysicalRobots' => null,
					'rewriteExists'     => null,
					'sitemapUrls'       => []
				],
				'status'                => [],
				'htaccess'              => '',
				'isMultisite'           => is_multisite(),
				'isNetworkAdmin'        => is_network_admin(),
				'currentBlogId'         => get_current_blog_id(),
				'mainSite'              => is_main_site(),
				'subdomain'             => $this->isSubdomain(),
				'isBBPressActive'       => class_exists( 'bbPress' ),
				'isClassicEditorActive' => $this->isClassicEditorActive(),
				'isWooCommerceActive'   => $this->isWooCommerceActive(),
				'staticHomePage'        => $isStaticHomePage ? $staticHomePage : false,
				'staticBlogPage'        => $this->getBlogPageId(),
				'staticBlogPageTitle'   => get_the_title( $this->getBlogPageId() ),
				'isDev'                 => $this->isDev(),
				'isLocal'               => $this->isLocalUrl( site_url() ),
				'isSsl'                 => is_ssl(),
				'hasUrlTrailingSlash'   => '/' === user_trailingslashit( '' ),
				'permalinkStructure'    => get_option( 'permalink_structure' ),
				'usingPermalinks'       => aioseo()->helpers->usingPermalinks(),
				'dateFormat'            => get_option( 'date_format' ),
				'timeFormat'            => get_option( 'time_format' ),
				'siteName'              => aioseo()->helpers->getWebsiteName(),
				'adminEmail'            => get_bloginfo( 'admin_email' ),
				'blocks'                => [
					'toc' => [
						'hashPrefix' => apply_filters( 'aioseo_toc_hash_prefix', 'aioseo-' )
					]
				]
			],
			'user'               => [
				'canManage'      => aioseo()->access->canManage(),
				'capabilities'   => aioseo()->access->getAllCapabilities(),
				'customRoles'    => $this->getCustomRoles(),
				'data'           => wp_get_current_user(),
				'locale'         => function_exists( 'get_user_locale' ) ? get_user_locale() : get_locale(),
				'roles'          => $this->getUserRoles(),
				'unfilteredHtml' => current_user_can( 'unfiltered_html' )
			],
			'plugins'            => $this->getPluginData(),
			'postData'           => [
				'postTypes'    => array_values( $this->getPublicPostTypes( false, false, true ) ),
				'taxonomies'   => array_values( $this->getPublicTaxonomies( false, true ) ),
				'archives'     => array_values( $this->getPublicPostTypes( false, true, true ) ),
				'postStatuses' => array_values( $this->getPublicPostStatuses() )
			],
			'notifications'      => array_merge( Models\Notification::getNotifications( true ), [
				'force' => $this->showNotificationsDrawer()
			] ),
			'addons'             => aioseo()->addons->getAddons(),
			'features'           => aioseo()->features->getFeatures(),
			'version'            => AIOSEO_VERSION,
			'wpVersion'          => get_bloginfo( 'version' ),
			'phpVersion'         => PHP_VERSION,
			'helpPanel'          => aioseo()->help->getDocs(),
			'scheduledActions'   => [
				'sitemaps' => []
			],
			'integration'        => $this->args['integration'],
			'theme'              => [
				'features' => aioseo()->helpers->getThemeFeatures()
			]
		];
	}

	/**
	 * Set Vue multisite data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setMultisiteData() {
		if ( ! is_multisite() ) {
			return;
		}

		$this->data['internalNetworkOptions'] = aioseo()->internalNetworkOptions->all();
		$this->data['networkOptions']         = aioseo()->networkOptions->all();
	}

	/**
	 * Set Vue post data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setPostData() {
		if ( 'post' !== $this->args['page'] ) {
			return;
		}

		$postId         = $this->args['staticPostId'] ?: get_the_ID();
		$postTypeObj    = get_post_type_object( get_post_type( $postId ) );
		$post           = Models\Post::getPost( $postId );
		$wpPost         = get_post( $postId );
		$staticHomePage = intval( get_option( 'page_on_front' ) );

		$this->data['currentPost'] = [
			'context'                        => 'post',
			'tags'                           => aioseo()->tags->getDefaultPostTags( $postId ),
			'id'                             => $postId,
			'priority'                       => isset( $post->priority ) && null !== $post->priority ? (float) $post->priority : 'default',
			'frequency'                      => ! empty( $post->frequency ) ? $post->frequency : 'default',
			'permalink'                      => get_permalink( $postId ),
			'editlink'                       => aioseo()->helpers->getPostEditLink( $postId ),
			'title'                          => ! empty( $post->title ) ? $post->title : aioseo()->meta->title->getPostTypeTitle( $postTypeObj->name ),
			'description'                    => ! empty( $post->description ) ? $post->description : aioseo()->meta->description->getPostTypeDescription( $postTypeObj->name ),
			'descriptionIncludeCustomFields' => apply_filters( 'aioseo_description_include_custom_fields', true, $post ),
			'keywords'                       => ! empty( $post->keywords ) ? $post->keywords : [],
			'keyphrases'                     => Models\Post::getKeyphrasesDefaults( $post->keyphrases ),
			'page_analysis'                  => Models\Post::getPageAnalysisDefaults( $post->page_analysis ),
			'loading'                        => [
				'focus'      => false,
				'additional' => [],
			],
			'type'                           => $postTypeObj->labels->singular_name,
			'postType'                       => 'type' === $postTypeObj->name ? '_aioseo_type' : $postTypeObj->name,
			'postStatus'                     => get_post_status( $postId ),
			'postAuthor'                     => (int) $wpPost->post_author,
			'isSpecialPage'                  => $this->isSpecialPage( $postId ),
			'isTruSeoEligible'               => $this->isTruSeoEligible( $postId ),
			'isStaticPostsPage'              => aioseo()->helpers->isStaticPostsPage(),
			'isHomePage'                     => $postId === $staticHomePage,
			'isWooCommercePageWithoutSchema' => $this->isWooCommercePageWithoutSchema( $postId ),
			'seo_score'                      => (int) $post->seo_score,
			'pillar_content'                 => ( (int) $post->pillar_content ) === 0 ? false : true,
			'canonicalUrl'                   => $post->canonical_url,
			'default'                        => ( (int) $post->robots_default ) === 0 ? false : true,
			'noindex'                        => ( (int) $post->robots_noindex ) === 0 ? false : true,
			'noarchive'                      => ( (int) $post->robots_noarchive ) === 0 ? false : true,
			'nosnippet'                      => ( (int) $post->robots_nosnippet ) === 0 ? false : true,
			'nofollow'                       => ( (int) $post->robots_nofollow ) === 0 ? false : true,
			'noimageindex'                   => ( (int) $post->robots_noimageindex ) === 0 ? false : true,
			'noodp'                          => ( (int) $post->robots_noodp ) === 0 ? false : true,
			'notranslate'                    => ( (int) $post->robots_notranslate ) === 0 ? false : true,
			'maxSnippet'                     => null === $post->robots_max_snippet ? -1 : (int) $post->robots_max_snippet,
			'maxVideoPreview'                => null === $post->robots_max_videopreview ? -1 : (int) $post->robots_max_videopreview,
			'maxImagePreview'                => $post->robots_max_imagepreview,
			'modalOpen'                      => false,
			'generalMobilePrev'              => false,
			'og_object_type'                 => ! empty( $post->og_object_type ) ? $post->og_object_type : 'default',
			'og_title'                       => $post->og_title,
			'og_description'                 => $post->og_description,
			'og_image_custom_url'            => $post->og_image_custom_url,
			'og_image_custom_fields'         => $post->og_image_custom_fields,
			'og_image_type'                  => ! empty( $post->og_image_type ) ? $post->og_image_type : 'default',
			'og_video'                       => ! empty( $post->og_video ) ? $post->og_video : '',
			'og_article_section'             => ! empty( $post->og_article_section ) ? $post->og_article_section : '',
			'og_article_tags'                => ! empty( $post->og_article_tags ) ? $post->og_article_tags : [],
			'twitter_use_og'                 => ( (int) $post->twitter_use_og ) === 0 ? false : true,
			'twitter_card'                   => $post->twitter_card,
			'twitter_image_custom_url'       => $post->twitter_image_custom_url,
			'twitter_image_custom_fields'    => $post->twitter_image_custom_fields,
			'twitter_image_type'             => $post->twitter_image_type,
			'twitter_title'                  => $post->twitter_title,
			'twitter_description'            => $post->twitter_description,
			'schema'                         => Models\Post::getDefaultSchemaOptions( $post->schema, aioseo()->helpers->getPost( $postId ) ),
			'metaDefaults'                   => [
				'title'       => aioseo()->meta->title->getPostTypeTitle( $postTypeObj->name ),
				'description' => aioseo()->meta->description->getPostTypeDescription( $postTypeObj->name )
			],
			'linkAssistant'                  => [
				'modalOpen' => false
			],
			'limit_modified_date'            => ( (int) $post->limit_modified_date ) === 0 ? false : true,
			'redirects'                      => [
				'modalOpen' => false
			],
			'options'                        => $post->options,
			'maxAdditionalKeyphrases'        => 0,
		];

		if ( empty( $this->args['integration'] ) ) {
			$this->data['integration'] = aioseo()->helpers->getPostPageBuilderName( $postId );
		}

		if ( ! $post->exists() ) {
			$oldPostMeta = aioseo()->migration->meta->getMigratedPostMeta( $postId );
			foreach ( $oldPostMeta as $k => $v ) {
				if ( preg_match( '#robots_.*#', (string) $k ) ) {
					$oldPostMeta[ preg_replace( '#robots_#', '', (string) $k ) ] = $v;
					continue;
				}
				if ( 'canonical_url' === $k ) {
					$oldPostMeta['canonicalUrl'] = $v;
				}
			}
			$this->data['currentPost'] = array_merge( $this->data['currentPost'], $oldPostMeta );
		}
	}

	/**
	 * Set Vue dashboard data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setDashboardData() {
		if ( 'dashboard' !== $this->args['page'] ) {
			return;
		}

		$this->data['setupWizard']['isCompleted'] = aioseo()->standalone->setupWizard->isCompleted();
		$this->data['seoOverview']                = aioseo()->postSettings->getPostTypesOverview();
		$this->data['importers']                  = aioseo()->importExport->plugins();
	}

	/**
	 * Set Vue search statistics data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setSearchStatisticsData() {
		$this->data['searchStatistics'] = [
			'isConnected'        => aioseo()->searchStatistics->api->auth->isConnected(),
			'sitemapsWithErrors' => aioseo()->searchStatistics->sitemap->getSitemapsWithErrors(),
		];

		if ( 'post' === $this->args['page'] ) {
			$this->data['keywordRankTracker'] = aioseo()->searchStatistics->keywordRankTracker->getVueDataEdit();
		}

		if ( 'search-statistics' === $this->args['page'] ) {
			$this->data['seoOverview']        = aioseo()->postSettings->getPostTypesOverview();
			$this->data['searchStatistics']   = array_merge( $this->data['searchStatistics'], aioseo()->searchStatistics->getVueData() );
			$this->data['keywordRankTracker'] = aioseo()->searchStatistics->keywordRankTracker->getVueData();
			$this->data['indexStatus']        = aioseo()->searchStatistics->indexStatus->getVueData();
		}
	}

	/**
	 * Set Vue sitemaps data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setSitemapsData() {
		if ( 'sitemaps' !== $this->args['page'] ) {
			return;
		}

		$this->data['data']['sitemapUrls'] = aioseo()->sitemap->helpers->getSitemapUrls();

		try {
			if ( as_next_scheduled_action( 'aioseo_static_sitemap_regeneration' ) ) {
				$this->data['scheduledActions']['sitemap'][] = 'staticSitemapRegeneration';
			}
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Set Vue setup wizard data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setSetupWizardData() {
		if ( 'setup-wizard' !== $this->args['page'] ) {
			return;
		}

		$isStaticHomePage = 'page' === get_option( 'show_on_front' );
		$staticHomePage   = intval( get_option( 'page_on_front' ) );

		$this->data['users']     = $this->getSiteUsers( [ 'administrator', 'editor', 'author' ] );
		$this->data['importers'] = aioseo()->importExport->plugins();
		$this->data['data']      += [
			'staticHomePageTitle'       => $isStaticHomePage ? aioseo()->meta->title->getTitle( $staticHomePage ) : '',
			'staticHomePageDescription' => $isStaticHomePage ? aioseo()->meta->description->getDescription( $staticHomePage ) : '',
		];
	}

	/**
	 * Set Vue search appearance data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setSearchAppearanceData() {
		if ( 'search-appearance' !== $this->args['page'] ) {
			return;
		}

		$isStaticHomePage = 'page' === get_option( 'show_on_front' );
		$staticHomePage   = intval( get_option( 'page_on_front' ) );

		$this->data['users'] = $this->getSiteUsers( [ 'administrator', 'editor', 'author' ] );
		$this->data['data']  += [
			'staticHomePageTitle'       => $isStaticHomePage ? aioseo()->meta->title->getTitle( $staticHomePage ) : '',
			'staticHomePageDescription' => $isStaticHomePage ? aioseo()->meta->description->getDescription( $staticHomePage ) : '',
		];
	}

	/**
	 * Set Vue social networks data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setSocialNetworksData() {
		if ( 'social-networks' !== $this->args['page'] ) {
			return;
		}

		$isStaticHomePage = 'page' === get_option( 'show_on_front' );
		$staticHomePage   = intval( get_option( 'page_on_front' ) );

		$this->data['data'] += [
			'staticHomePageOgTitle'            => $isStaticHomePage ? aioseo()->social->facebook->getTitle( $staticHomePage ) : '',
			'staticHomePageOgDescription'      => $isStaticHomePage ? aioseo()->social->facebook->getDescription( $staticHomePage ) : '',
			'staticHomePageTwitterTitle'       => $isStaticHomePage ? aioseo()->social->twitter->getTitle( $staticHomePage ) : '',
			'staticHomePageTwitterDescription' => $isStaticHomePage ? aioseo()->social->twitter->getDescription( $staticHomePage ) : '',
		];
	}

	/**
	 * Set Vue seo revisions data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setSeoRevisionsData() {
		if ( 'post' === $this->args['page'] ) {
			$this->data['seoRevisions'] = aioseo()->seoRevisions->getVueDataEdit( $this->args['staticPostId'] ?? null );
		}

		if ( 'seo-revisions' === $this->args['page'] ) {
			$this->data['seoRevisions'] = aioseo()->seoRevisions->getVueDataCompare();
		}
	}

	/**
	 * Set Vue tools or settings data.
	 *
	 * @since 4.4.9
	 *
	 * @return void
	 */
	private function setToolsOrSettingsData() {
		if (
			'tools' !== $this->args['page'] &&
			'settings' !== $this->args['page']
		) {
			return;
		}

		if ( 'tools' === $this->args['page'] ) {
			$this->data['backups']                = array_reverse( aioseo()->backup->all() );
			$this->data['importers']              = aioseo()->importExport->plugins();
			$this->data['data']['robots']         = [
				'defaultRules'      => $this->args['page'] ? aioseo()->robotsTxt->extractRules( aioseo()->robotsTxt->getDefaultRobotsTxtContent() ) : [],
				'hasPhysicalRobots' => aioseo()->robotsTxt->hasPhysicalRobotsTxt(),
				'rewriteExists'     => aioseo()->robotsTxt->rewriteRulesExist(),
				'sitemapUrls'       => array_merge( aioseo()->sitemap->helpers->getSitemapUrlsPrefixed(), aioseo()->sitemap->helpers->extractSitemapUrlsFromRobotsTxt() )
			];
			$this->data['data']['status']         = Tools\SystemStatus::getSystemStatusInfo();
			$this->data['data']['htaccess']       = aioseo()->htaccess->getContents();
			$this->data['data']['v3Options']      = ! empty( get_option( 'aioseop_options' ) );
			$this->data['integrations']['wpcode'] = [
				'snippets'          => WpCodeIntegration::loadWpCodeSnippets(),
				'pluginInstalled'   => WpCodeIntegration::isPluginInstalled(),
				'pluginActive'      => WpCodeIntegration::isPluginActive(),
				'pluginNeedsUpdate' => WpCodeIntegration::pluginNeedsUpdate()
			];
		}

		if ( 'settings' === $this->args['page'] ) {
			$this->data['breadcrumbs']['defaultTemplate'] = aioseo()->helpers->encodeOutputHtml( aioseo()->breadcrumbs->frontend->getDefaultTemplate() );
		}

		if (
			is_multisite() &&
			is_network_admin()
		) {
			$this->data['data']['network'] = [
				'sites'   => aioseo()->helpers->getSites( aioseo()->settings->tablePagination['networkDomains'] ),
				'backups' => []
			];
		}
	}

	/**
	 * Set Vue Page Builder data.
	 *
	 * @since   4.4.9
	 * @version 4.5.2 Renamed.
	 *
	 * @return void
	 */
	private function setPageBuilderData() {
		if ( empty( $this->args['integration'] ) ) {
			return;
		}

		if ( 'divi' === $this->args['integration'] ) {
			// This needs to be dropped in order to prevent JavaScript errors in Divi's visual builder.
			// Some of the data from the site analysis can contain HTML tags, e.g. the search preview, and somehow that causes JSON.parse to fail on our localized Vue data.
			unset( $this->data['internalOptions']['internal']['siteAnalysis'] );
		}
	}

	/**
	 * Returns Jed-formatted localization data. Added for backwards-compatibility.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $domain Translation domain.
	 * @return array          The information of the locale.
	 */
	public function getJedLocaleData( $domain ) {
		$translations = get_translations_for_domain( $domain );

		$locale = [
			'' => [
				'domain' => $domain,
				'lang'   => is_admin() && function_exists( 'get_user_locale' ) ? get_user_locale() : get_locale()
			],
		];

		if ( ! empty( $translations->headers['Plural-Forms'] ) ) {
			$locale['']['plural_forms'] = $translations->headers['Plural-Forms'];
		}

		foreach ( $translations->entries as $entry ) {
			if ( empty( $entry->translations ) || ! is_array( $entry->translations ) ) {
				continue;
			}

			foreach ( $entry->translations as $translation ) {
				// If any of the translated strings contains an HTML line break, we need to ignore it. Otherwise, logging into the admin breaks.

				if ( preg_match( '/<br[\s\/\\\\]*>/', (string) $translation ) ) {
					continue 2;
				}
			}

			// Set the translation data using the singular string as the index. This is how Jed expects it, even for plural strings.
			$locale[ $entry->singular ] = $entry->translations;
		}

		return $locale;
	}

	/**
	 * Set Vue writing assistant data.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	private function setWritingAssistantData() {
		// Settings page or not a post screen.
		if (
			'settings' !== $this->args['page'] &&
			! aioseo()->helpers->isScreenBase( 'post' )
		) {
			return;
		}

		$this->data['writingAssistantSettings'] = aioseo()->writingAssistant->helpers->getSettingsVueData();
	}

	/**
	 * Whether the notifications drawer should be shown or not.
	 *
	 * @since 4.4.9
	 *
	 * @return bool True if it should be shown, false otherwise.
	 */
	private function showNotificationsDrawer() {
		static $showNotificationsDrawer = null;
		if ( null === $showNotificationsDrawer ) {
			$showNotificationsDrawer = (bool) aioseo()->core->cache->get( 'show_notifications_drawer' );

			// If this is set to true, let's disable it now, so it doesn't pop up again.
			if ( $showNotificationsDrawer ) {
				aioseo()->core->cache->delete( 'show_notifications_drawer' );
			}
		}

		return $showNotificationsDrawer;
	}

	/**
	 * Set Vue breadcrumbs data.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	private function setBreadcrumbsData() {
		$isPostOrTermPage              = aioseo()->helpers->isScreenBase( 'post' ) || aioseo()->helpers->isScreenBase( 'term' );
		$isCurrentPageUsingPageBuilder = 'post' === $this->args['page'] && ! empty( $this->args['integration'] );
		$isSettingsPage                = ! empty( $this->args['page'] ) && 'settings' === $this->args['page'];
		if ( ! $isSettingsPage && ! $isCurrentPageUsingPageBuilder && ! $isPostOrTermPage ) {
			return;
		}

		$this->data['breadcrumbs']['defaultTemplate'] = aioseo()->helpers->encodeOutputHtml( aioseo()->breadcrumbs->frontend->getDefaultTemplate() );
	}

	/**
	 * Set Vue SEO Analyzer data.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	private function setSeoAnalyzerData() {
		if ( 'seo-analysis' !== $this->args['page'] ) {
			return;
		}

		$this->data['analyzer']['homeResults'] = Models\SeoAnalyzerResult::getResults();
		$this->data['analyzer']['competitors'] = Models\SeoAnalyzerResult::getCompetitorsResults();
	}
}Common/Traits/Helpers/Numbers.php000066600000001570151135505570013006 0ustar00<?php

namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Numbers trait.
 *
 * @since 4.7.2
 */
trait Numbers {
	/**
	 * Formats a number to a compact format.
	 *
	 * @since 4.7.2
	 *
	 * @param  float|int|string $number   The number to format.
	 * @param  int              $decimals The number of decimal places to include.
	 * @return string                     Formatted number in string format.
	 */
	public function compactNumber( $number, $decimals = 1 ) {
		$suffixes    = [ '', 'K', 'M', 'B', 'T', 'q', 'Q' ];
		$suffixIndex = 0;

		while ( abs( $number ) >= 1000 && $suffixIndex < count( $suffixes ) - 1 ) {
			$suffixIndex++;
			$number /= 1000;
		}

		// Remove trailing zeros.
		return preg_replace( '/\D0+$/', '', (string) number_format_i18n( $number, $decimals ) ) . $suffixes[ $suffixIndex ];
	}
}Common/Traits/Helpers/Wp.php000066600000066265151135505570011775 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Utils;

/**
 * Contains all WP related helper methods.
 *
 * @since 4.1.4
 */
trait Wp {
	/**
	 * Whether or not we have a local connection.
	 *
	 * @since 4.0.0
	 *
	 * @var bool
	 */
	private static $connection = false;

	/**
	 * Returns user roles in the current WP install.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of user roles.
	 */
	public function getUserRoles() {
		global $wp_roles; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		$wpRoles = $wp_roles; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if ( ! is_object( $wpRoles ) ) {
			// Don't assign this to the global because otherwise WordPress won't override it.
			$wpRoles = new \WP_Roles();
		}

		$roleNames = $wpRoles->get_names();
		asort( $roleNames );

		return $roleNames;
	}

	/**
	 * Returns the custom roles in the current WP install.
	 *
	 * @since 4.1.3
	 *
	 * @return array An array of custom roles.
	 */
	public function getCustomRoles() {
		$allRoles = $this->getUserRoles();

		$toSkip = array_merge(
			// Default WordPress roles.
			[ 'superadmin', 'administrator', 'editor', 'author', 'contributor' ],
			// Default AIOSEO roles.
			[ 'aioseo_manager', 'aioseo_editor' ],
			// Filterable roles.
			apply_filters( 'aioseo_access_control_excluded_roles', array_merge( [
				'subscriber'
			], aioseo()->helpers->isWooCommerceActive() ? [ 'customer' ] : [] ) )
		);

		// Remove empty entries.
		$toSkip = array_filter( $toSkip );

		$customRoles = [];
		foreach ( $allRoles as $roleName => $role ) {
			// Skip specific roles.
			if ( in_array( $roleName, $toSkip, true ) ) {
				continue;
			}

			$customRoles[ $roleName ] = $role;
		}

		return $customRoles;
	}

	/**
	 * Returns an array of plugins with the active status.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of plugins with active status.
	 */
	public function getPluginData() {
		$pluginUpgrader   = new Utils\PluginUpgraderSilentAjax();
		$installedPlugins = array_keys( get_plugins() );

		$plugins = [];
		foreach ( $pluginUpgrader->pluginSlugs as $key => $slug ) {
			$adminUrl        = admin_url( $pluginUpgrader->pluginAdminUrls[ $key ] );
			$networkAdminUrl = null;
			if (
				is_multisite() &&
				is_network_admin() &&
				! empty( $pluginUpgrader->hasNetworkAdmin[ $key ] )
			) {
				$networkAdminUrl = network_admin_url( $pluginUpgrader->hasNetworkAdmin[ $key ] );
				if ( aioseo()->helpers->isPluginNetworkActivated( $pluginUpgrader->pluginSlugs[ $key ] ) ) {
					$adminUrl = $networkAdminUrl;
				}
			}

			$plugins[ $key ] = [
				'basename'        => $slug,
				'installed'       => in_array( $slug, $installedPlugins, true ),
				'activated'       => is_plugin_active( $slug ),
				'adminUrl'        => $adminUrl,
				'networkAdminUrl' => $networkAdminUrl,
				'canInstall'      => aioseo()->addons->canInstall(),
				'canActivate'     => aioseo()->addons->canActivate(),
				'canUpdate'       => aioseo()->addons->canUpdate(),
				'wpLink'          => ! empty( $pluginUpgrader->wpPluginLinks[ $key ] ) ? $pluginUpgrader->wpPluginLinks[ $key ] : null
			];
		}

		return $plugins;
	}

	/**
	 * Returns all registered Post Statuses.
	 *
	 * @since 4.1.6
	 *
	 * @param  boolean $statusesOnly Whether or not to only return statuses.
	 * @return array              An array of post statuses.
	 */
	public function getPublicPostStatuses( $statusesOnly = false ) {
		$allStatuses = get_post_stati( [ 'show_in_admin_all_list' => true ], 'objects' );

		$postStatuses = [];
		foreach ( $allStatuses as $status => $data ) {
			if (
				! $data->public &&
				! $data->protected &&
				! $data->private
			) {
				continue;
			}

			if ( $statusesOnly ) {
				$postStatuses[] = $status;
				continue;
			}

			$postStatuses[] = [
				'label'  => $data->label,
				'status' => $status
			];
		}

		return $postStatuses;
	}

	/**
	 * Returns a list of public post types objects or names.
	 *
	 * @since 4.0.0
	 *
	 * @param  bool  $namesOnly       Whether only the names should be returned.
	 * @param  bool  $hasArchivesOnly Whether to only include post types which have archives.
	 * @param  bool  $rewriteType     Whether to rewrite the type slugs.
	 * @param  array $args            Additional arguments.
	 * @return array                  List of public post types.
	 */
	public function getPublicPostTypes( $namesOnly = false, $hasArchivesOnly = false, $rewriteType = false, $args = [] ) {
		$args = array_merge( [
			'include' => [] // Post types to include.
		], $args );

		$postTypes   = [];
		$postTypeObjects = get_post_types( [], 'objects' );
		foreach ( $postTypeObjects as $postTypeObject ) {
			if ( ! is_post_type_viewable( $postTypeObject ) ) {
				continue;
			}

			$postTypeArray = $this->getPostType( $postTypeObject, $namesOnly, $hasArchivesOnly, $rewriteType );
			if ( ! empty( $postTypeArray ) ) {
				$postTypes[] = $postTypeArray;
			}
		}

		if ( isset( aioseo()->standalone->buddyPress ) ) {
			aioseo()->standalone->buddyPress->maybeAddPostTypes( $postTypes, $namesOnly, $hasArchivesOnly, $args );
		}

		return apply_filters( 'aioseo_public_post_types', $postTypes, $namesOnly, $hasArchivesOnly, $args );
	}

	/**
	 * Returns the data for the given post type.
	 *
	 * @since 4.2.2
	 *
	 * @param  \WP_Post_Type $postTypeObject  The post type object.
	 * @param  bool          $namesOnly       Whether only the names should be returned.
	 * @param  bool          $hasArchivesOnly Whether to only include post types which have archives.
	 * @param  bool          $rewriteType     Whether to rewrite the type slugs.
	 * @return mixed                          Data for the post type.
	 */
	public function getPostType( $postTypeObject, $namesOnly = false, $hasArchivesOnly = false, $rewriteType = false ) {
		if ( empty( $postTypeObject->label ) ) {
			return $namesOnly ? null : [];
		}

		// We don't want to include archives for the WooCommerce shop page.
		if (
			$hasArchivesOnly &&
			(
				! $postTypeObject->has_archive ||
				( 'product' === $postTypeObject->name && $this->isWooCommerceActive() )
			)
		) {
			return $namesOnly ? null : [];
		}

		if ( $namesOnly ) {
			return $postTypeObject->name;
		}

		if ( 'attachment' === $postTypeObject->name ) {
			// We have to check if the 'init' action has been fired to avoid a PHP notice
			// in WP 6.7+ due to loading translations too early.
			if ( did_action( 'init' ) ) {
				$postTypeObject->label = __( 'Attachments', 'all-in-one-seo-pack' );
			}
		}

		if ( 'product' === $postTypeObject->name && $this->isWooCommerceActive() ) {
			$postTypeObject->menu_icon = 'dashicons-products';
		}

		$name = $postTypeObject->name;
		if ( 'type' === $postTypeObject->name && $rewriteType ) {
			$name = '_aioseo_type';
		}

		return [
			'name'         => $name,
			'label'        => ucwords( $postTypeObject->label ),
			'singular'     => ucwords( $postTypeObject->labels->singular_name ),
			'icon'         => $postTypeObject->menu_icon,
			'hasArchive'   => $postTypeObject->has_archive,
			'hierarchical' => $postTypeObject->hierarchical,
			'taxonomies'   => get_object_taxonomies( $name ),
			'slug'         => isset( $postTypeObject->rewrite['slug'] ) ? $postTypeObject->rewrite['slug'] : $name,
			'supports'     => get_all_post_type_supports( $name )
		];
	}

	/**
	 * Returns a list of public taxonomies objects or names.
	 *
	 * @since 4.0.0
	 *
	 * @param  bool  $namesOnly   Whether only the names should be returned.
	 * @param  bool  $rewriteType Whether to rewrite the type slugs.
	 * @return array              List of public taxonomies.
	 */
	public function getPublicTaxonomies( $namesOnly = false, $rewriteType = false ) {
		$taxonomies = [];
		if ( count( $taxonomies ) ) {
			return $taxonomies;
		}

		$taxObjects = get_taxonomies( [], 'objects' );
		foreach ( $taxObjects as $taxObject ) {
			if (
				empty( $taxObject->label ) ||
				! is_taxonomy_viewable( $taxObject ) ||
				aioseo()->helpers->isWooCommerceProductAttribute( $taxObject->name )
			) {
				continue;
			}

			if ( in_array( $taxObject->name, [
				'product_shipping_class',
				'post_format'
			], true ) ) {
				continue;
			}

			if ( $namesOnly ) {
				$taxonomies[] = $taxObject->name;
				continue;
			}

			$name = $taxObject->name;
			if ( 'type' === $taxObject->name && $rewriteType ) {
				$name = '_aioseo_type';
			}

			global $wp_taxonomies; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			$taxonomyPostTypes = ! empty( $wp_taxonomies[ $name ] ) // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				? $wp_taxonomies[ $name ]->object_type // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				: [];

			$taxonomies[] = [
				'name'               => $name,
				'label'              => ucwords( $taxObject->label ),
				'singular'           => ucwords( $taxObject->labels->singular_name ),
				'icon'               => strpos( $taxObject->label, 'categor' ) !== false ? 'dashicons-category' : 'dashicons-tag',
				'hierarchical'       => $taxObject->hierarchical,
				'slug'               => isset( $taxObject->rewrite['slug'] ) ? $taxObject->rewrite['slug'] : '',
				'primaryTermSupport' => (bool) $taxObject->hierarchical,
				'restBase'           => ( $taxObject->rest_base ) ? $taxObject->rest_base : $taxObject->name,
				'postTypes'          => $taxonomyPostTypes
			];
		}

		if ( $this->isWooCommerceActive() ) {
			// We inject a fake one for WooCommerce product attributes so that we can show a single set of settings
			// instead of having to duplicate them for each attribute.
			if ( $namesOnly ) {
				$taxonomies[] = 'product_attributes';
			} else {
				$taxonomies[] = [
					'name'               => 'product_attributes',
					'label'              => __( 'Product Attributes', 'all-in-one-seo-pack' ),
					'singular'           => __( 'Product Attribute', 'all-in-one-seo-pack' ),
					'icon'               => 'dashicons-products',
					'hierarchical'       => true,
					'slug'               => 'product_attributes',
					'primaryTermSupport' => true,
					'restBase'           => 'product_attributes_class',
					'postTypes'          => [ 'product' ]
				];
			}
		}

		return apply_filters( 'aioseo_public_taxonomies', $taxonomies, $namesOnly );
	}

	/**
	 * Retrieve a list of users that match passed in roles.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of user data.
	 */
	public function getSiteUsers( $roles ) {
		static $users = [];

		if ( ! empty( $users ) ) {
			return $users;
		}

		$rolesWhere = [];
		foreach ( $roles as $role ) {
			$rolesWhere[] = '(um.meta_key = \'' . aioseo()->core->db->db->prefix . 'capabilities\' AND um.meta_value LIKE \'%\"' . $role . '\"%\')';
		}
		// We get the table name from WPDB since multisites share the same table.
		$usersTableName    = aioseo()->core->db->db->users;
		$usermetaTableName = aioseo()->core->db->db->usermeta;
		$dbUsers           = aioseo()->core->db->start( "$usersTableName as u", true )
			->select( 'u.ID, u.display_name, u.user_nicename, u.user_email' )
			->join( "$usermetaTableName as um", 'u.ID = um.user_id', '', true )
			->whereRaw( '(' . implode( ' OR ', $rolesWhere ) . ')' )
			->orderBy( 'u.user_nicename' )
			->run()
			->result();

		foreach ( $dbUsers as $dbUser ) {
			$users[] = [
				'id'          => (int) $dbUser->ID,
				'displayName' => $dbUser->display_name,
				'niceName'    => $dbUser->user_nicename,
				'email'       => $dbUser->user_email,
				'gravatar'    => get_avatar_url( $dbUser->user_email )
			];
		}

		return $users;
	}

	/**
	 * Returns the ID of the site logo if it exists.
	 *
	 * @since 4.0.0
	 *
	 * @return int
	 */
	public function getSiteLogoId() {
		if ( ! get_theme_support( 'custom-logo' ) ) {
			return false;
		}

		return get_theme_mod( 'custom_logo' );
	}

	/**
	 * Returns the URL of the site logo if it exists.
	 *
	 * @since 4.0.0
	 *
	 * @return string
	 */
	public function getSiteLogoUrl() {
		$id = $this->getSiteLogoId();
		if ( ! $id ) {
			return false;
		}

		$image = wp_get_attachment_image_src( $id, 'full' );
		if ( empty( $image ) ) {
			return false;
		}

		return $image[0];
	}

	/**
	 * Returns noindexed post types.
	 *
	 * @since 4.0.0
	 *
	 * @return array A list of noindexed post types.
	 */
	public function getNoindexedPostTypes() {
		return $this->getNoindexedObjects( 'postTypes' );
	}

	/**
	 * Checks whether a given post type is noindexed.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $postType The post type.
	 * @return bool              Whether the post type is noindexed.
	 */
	public function isPostTypeNoindexed( $postType ) {
		$noindexedPostTypes = $this->getNoindexedPostTypes();

		return in_array( $postType, $noindexedPostTypes, true );
	}

	/**
	 * Checks whether a given post type is public.
	 *
	 * @since 4.2.2
	 *
	 * @param  string  $postType The post type.
	 * @return bool              Whether the post type is public.
	 */
	public function isPostTypePublic( $postType ) {
		$publicPostTypes = $this->getPublicPostTypes( true );

		return in_array( $postType, $publicPostTypes, true );
	}

	/**
	 * Returns noindexed taxonomies.
	 *
	 * @since 4.0.0
	 *
	 * @return array A list of noindexed taxonomies.
	 */
	public function getNoindexedTaxonomies() {
		return $this->getNoindexedObjects( 'taxonomies' );
	}

	/**
	 * Checks whether a given post type is noindexed.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $taxonomy The taxonomy.
	 * @return bool              Whether the taxonomy is noindexed.
	 */
	public function isTaxonomyNoindexed( $taxonomy ) {
		$noindexedTaxonomies = $this->getNoindexedTaxonomies();

		return in_array( $taxonomy, $noindexedTaxonomies, true );
	}

	/**
	 * Checks whether a given taxonomy is public.
	 *
	 * @since 4.2.2
	 *
	 * @param  string  $taxonomy The taxonomy.
	 * @return bool              Whether the taxonomy is public.
	 */
	public function isTaxonomyPublic( $taxonomy ) {
		$publicTaxonomies = $this->getPublicTaxonomies( true );

		return in_array( $taxonomy, $publicTaxonomies, true );
	}

	/**
	 * Returns noindexed object types of a given parent type.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $type The parent object type ("postTypes", "archives", "taxonomies").
	 * @return array        A list of noindexed objects types.
	 */
	public function getNoindexedObjects( $type ) {
		$noindexed = [];
		foreach ( aioseo()->dynamicOptions->searchAppearance->$type->all() as $name => $object ) {
			if (
				! $object['show'] ||
				( $object['advanced']['robotsMeta'] && ! $object['advanced']['robotsMeta']['default'] && $object['advanced']['robotsMeta']['noindex'] )
			) {
				$noindexed[] = $name;
			}
		}

		return $noindexed;
	}

	/**
	 * Returns all categories for a post.
	 *
	 * @since 4.1.4
	 *
	 * @param  int   $postId The post ID.
	 * @return array         The category names.
	 */
	public function getAllCategories( $postId = 0 ) {
		$names      = [];
		$categories = get_the_category( $postId );
		if ( $categories && count( $categories ) ) {
			foreach ( $categories as $category ) {
				$names[] = aioseo()->helpers->internationalize( $category->name );
			}
		}

		return $names;
	}

	/**
	 * Returns all tags for a post.
	 *
	 * @since 4.1.4
	 *
	 * @param  int   $postId The post ID.
	 * @return array $names  The tag names.
	 */
	public function getAllTags( $postId = 0 ) {
		$names = [];

		$tags = get_the_tags( $postId );
		if ( ! empty( $tags ) && ! is_wp_error( $tags ) ) {
			foreach ( $tags as $tag ) {
				if ( ! empty( $tag->name ) ) {
					$names[] = aioseo()->helpers->internationalize( $tag->name );
				}
			}
		}

		return $names;
	}

	/**
	 * Loads the translations for a given domain.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function loadTextDomain( $domain ) {
		if ( ! is_user_logged_in() ) {
			return;
		}

		// Unload the domain in case WordPress has enqueued the translations for the site language instead of profile language.
		// Reloading the text domain will otherwise not override the existing loaded translations.
		unload_textdomain( $domain );

		$mofile = $domain . '-' . get_user_locale() . '.mo';
		load_textdomain( $domain, WP_LANG_DIR . '/plugins/' . $mofile );
	}

	/**
	 * Get the page builder the given Post ID was built with.
	 *
	 * @since 4.1.7
	 *
	 * @param  int         $postId The Post ID.
	 * @return bool|string         The page builder or false if not built with page builders.
	 */
	public function getPostPageBuilderName( $postId ) {
		foreach ( aioseo()->standalone->pageBuilderIntegrations as $integration => $pageBuilder ) {
			if ( $pageBuilder->isBuiltWith( $postId ) ) {
				return $integration;
			}
		}

		return false;
	}

	/**
	 * Get the edit link for the given Post ID.
	 *
	 * @since 4.3.1
	 *
	 * @param  int         $postId The Post ID.
	 * @return bool|string         The edit link or false if not built with page builders.
	 */
	public function getPostEditLink( $postId ) {
		$pageBuilder = $this->getPostPageBuilderName( $postId );
		if ( ! empty( $pageBuilder ) ) {
			return aioseo()->standalone->pageBuilderIntegrations[ $pageBuilder ]->getEditUrl( $postId );
		}

		return get_edit_post_link( $postId );
	}

	/**
	 * Checks if the current user can edit posts of the given post type.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $postType The name of the post type.
	 * @return bool             Whether the user can edit posts of the given post type.
	 */
	public function canEditPostType( $postType ) {
		$capabilities = $this->getPostTypeCapabilities( $postType );

		return current_user_can( $capabilities['edit_posts'] );
	}

	/**
	 * Returns a list of capabilities for the given post type.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $postType The name of the post type.
	 * @return array            The capabilities.
	 */
	public function getPostTypeCapabilities( $postType ) {
		static $capabilities = [];
		if ( isset( $capabilities[ $postType ] ) ) {
			return $capabilities[ $postType ];
		}

		$postTypeObject = get_post_type_object( $postType );
		if ( ! is_a( $postTypeObject, 'WP_Post_Type' ) ) {
			$capabilities[ $postType ] = [];

			return $capabilities[ $postType ];
		}

		$capabilityType = $postTypeObject->capability_type;
		if ( ! is_array( $capabilityType ) ) {
			$capabilityType = [
				$capabilityType,
				$capabilityType . 's'
			];
		}

		// Singular base for meta capabilities, plural base for primitive capabilities.
		list( $singularBase, $pluralBase ) = $capabilityType;

		$capabilities[ $postType ] = [
			'edit_post'          => 'edit_' . $singularBase,
			'read_post'          => 'read_' . $singularBase,
			'delete_post'        => 'delete_' . $singularBase,
			'edit_posts'         => 'edit_' . $pluralBase,
			'edit_others_posts'  => 'edit_others_' . $pluralBase,
			'delete_posts'       => 'delete_' . $pluralBase,
			'publish_posts'      => 'publish_' . $pluralBase,
			'read_private_posts' => 'read_private_' . $pluralBase,
		];

		return $capabilities[ $postType ];
	}

	/**
	 * Checks if the current user can edit terms of the given taxonomy.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $taxonomy The name of the taxonomy.
	 * @return bool             Whether the user can edit posts of the given taxonomy.
	 */
	public function canEditTaxonomy( $taxonomy ) {
		$capabilities = $this->getTaxonomyCapabilities( $taxonomy );

		return current_user_can( $capabilities['edit_terms'] );
	}

	/**
	 * Returns a list of capabilities for the given taxonomy.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $taxonomy The name of the taxonomy.
	 * @return array            The capabilities.
	 */
	public function getTaxonomyCapabilities( $taxonomy ) {
		static $capabilities = [];
		if ( isset( $capabilities[ $taxonomy ] ) ) {
			return $capabilities[ $taxonomy ];
		}

		$taxonomyObject = get_taxonomy( $taxonomy );
		if ( ! is_a( $taxonomyObject, 'WP_Taxonomy' ) ) {
			$capabilities[ $taxonomy ] = [];

			return $capabilities[ $taxonomy ];
		}

		$capabilities[ $taxonomy ] = (array) $taxonomyObject->cap;

		return $capabilities[ $taxonomy ];
	}

	/**
	 * Returns the charset for the site.
	 *
	 * @since 4.2.3
	 *
	 * @return string The name of the charset.
	 */
	public function getCharset() {
		static $charset = null;
		if ( null !== $charset ) {
			return $charset;
		}

		$charset = get_option( 'blog_charset' );
		$charset = $charset ? $charset : 'UTF-8';

		return $charset;
	}

	/**
	 * Returns the given data as JSON.
	 * We temporarily change the floating point precision in order to prevent rounding errors.
	 * Otherwise e.g. 4.9 could be output as 4.90000004.
	 *
	 * @since 4.2.7
	 *
	 * @param  mixed  $data  The data.
	 * @param  int    $flags The flags.
	 * @return string        The JSON output.
	 */
	public function wpJsonEncode( $data, $flags = 0 ) {
		$originalPrecision          = false;
		$originalSerializePrecision = false;
		if ( version_compare( PHP_VERSION, '7.1', '>=' ) ) {
			$originalPrecision          = ini_get( 'precision' );
			$originalSerializePrecision = ini_get( 'serialize_precision' );
			ini_set( 'precision', 17 );
			ini_set( 'serialize_precision', -1 );
		}

		$json = wp_json_encode( $data, $flags );

		if ( version_compare( PHP_VERSION, '7.1', '>=' ) ) {
			ini_set( 'precision', $originalPrecision );
			ini_set( 'serialize_precision', $originalSerializePrecision );
		}

		return $json;
	}

	/**
	 * Returns the post title or a placeholder if there isn't one.
	 *
	 * @since 4.3.0
	 *
	 * @param  int    $postId The post ID.
	 * @return string         The post title.
	 */
	public function getPostTitle( $postId ) {
		static $titles = [];
		if ( isset( $titles[ $postId ] ) ) {
			return $titles[ $postId ];
		}

		$post = aioseo()->helpers->getPost( $postId );
		if ( ! is_a( $post, 'WP_Post' ) ) {
			$titles[ $postId ] = __( '(no title)', 'default' ); // phpcs:ignore AIOSEO.Wp.I18n.TextDomainMismatch, WordPress.WP.I18n.TextDomainMismatch

			return $titles[ $postId ];
		}

		$title = $post->post_title;
		$title = $title ? $title : __( '(no title)', 'default' ); // phpcs:ignore AIOSEO.Wp.I18n.TextDomainMismatch, WordPress.WP.I18n.TextDomainMismatch

		$titles[ $postId ] = aioseo()->helpers->decodeHtmlEntities( $title );

		return $titles[ $postId ];
	}

	/**
	 * Checks whether the post status should be considered viewable.
	 * This function is a copy of the WordPress core function is_post_status_viewable() which was introduced in WP 5.7.
	 *
	 * @since 4.5.0
	 *
	 * @param  string|\stdClass $postStatus The post status name or object.
	 * @return bool                         Whether the post status is viewable.
	 */
	public function isPostStatusViewable( $postStatus ) {
		if ( is_scalar( $postStatus ) ) {
			$postStatus = get_post_status_object( $postStatus );

			if ( ! $postStatus ) {
				return false;
			}
		}

		if (
			! is_object( $postStatus ) ||
			$postStatus->internal ||
			$postStatus->protected
		) {
			return false;
		}

		return $postStatus->publicly_queryable || ( $postStatus->_builtin && $postStatus->public );
	}

	/**
	 * Checks whether the given post is publicly viewable.
	 * This function is a copy of the WordPress core function is_post_publicly_viewable() which was introduced in WP 5.7.
	 *
	 * @since 4.5.0
	 *
	 * @param  int|\WP_Post  $post Optional. Post ID or post object. Defaults to global $post.
	 * @return boolean                      Whether the post is publicly viewable or not.
	 */
	public function isPostPubliclyViewable( $post = null ) {
		$post = get_post( $post );
		if ( empty( $post ) ) {
			return false;
		}

		$postType   = get_post_type( $post );
		$postStatus = get_post_status( $post );

		return is_post_type_viewable( $postType ) && $this->isPostStatusViewable( $postStatus );
	}

	/**
	 * Only register a legacy widget if the WP version is lower than 5.8 or the widget is being used.
	 * The "Block-based Widgets Editor" was released in WP 5.8, so for WP versions below 5.8 it's okay to register them.
	 * The main purpose here is to avoid blocks and widgets with the same name to be displayed on the Customizer,
	 * like e.g. the "Breadcrumbs" Block and Widget.
	 *
	 * @since 4.3.9
	 *
	 * @param string $idBase The base ID of a widget created by extending WP_Widget.
	 * @return bool          Whether the legacy widget can be registered.
	 */
	public function canRegisterLegacyWidget( $idBase ) {
		global $wp_version; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if (
			version_compare( $wp_version, '5.8', '<' ) || // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			is_active_widget( false, false, $idBase ) ||
			aioseo()->standalone->pageBuilderIntegrations['elementor']->isPluginActive()
		) {
			return true;
		}

		return false;
	}

	/**
	 * Parses blocks for a given post.
	 *
	 * @since 4.6.8
	 *
	 * @param  \WP_Post|int $post          The post or post ID.
	 * @param  bool         $flattenBlocks Whether to flatten the blocks.
	 * @return array                       The parsed blocks.
	 */
	public function parseBlocks( $post, $flattenBlocks = true ) {
		if ( ! is_a( $post, 'WP_Post' ) ) {
			$post = aioseo()->helpers->getPost( $post );
		}

		static $parsedBlocks = [];
		if ( isset( $parsedBlocks[ $post->ID ] ) ) {
			return $parsedBlocks[ $post->ID ];
		}

		$parsedBlocks = parse_blocks( $post->post_content );

		if ( $flattenBlocks ) {
			$parsedBlocks = $this->flattenBlocks( $parsedBlocks );
		}

		$parsedBlocks[ $post->ID ] = $parsedBlocks;

		return $parsedBlocks[ $post->ID ];
	}

	/**
	 * Flattens the given blocks.
	 *
	 * @since 4.6.8
	 *
	 * @param  array $blocks The blocks.
	 * @return array         The flattened blocks.
	 */
	public function flattenBlocks( $blocks ) {
		$flattenedBlocks = [];

		foreach ( $blocks as $block ) {
			if ( ! empty( $block['innerBlocks'] ) ) {
				// Flatten inner blocks first.
				$innerBlocks = $this->flattenBlocks( $block['innerBlocks'] );
				unset( $block['innerBlocks'] );

				// Add the current block to the result.
				$flattenedBlocks[] = $block;

				// Add the flattened inner blocks to the result.
				$flattenedBlocks = array_merge( $flattenedBlocks, $innerBlocks );
			} else {
				// If no inner blocks, just add the block to the result.
				$flattenedBlocks[] = $block;
			}
		}

		return $flattenedBlocks;
	}

	/**
	 * Checks if the Classic eEditor is active and if the Block Editor is disabled in its settings.
	 *
	 * @since 4.7.3
	 *
	 * @return bool Whether the Classic Editor is active.
	 */
	public function isClassicEditorActive() {
		include_once ABSPATH . 'wp-admin/includes/plugin.php';

		if ( ! is_plugin_active( 'classic-editor/classic-editor.php' ) ) {
			return false;
		}

		return 'classic' === get_option( 'classic-editor-replace' );
	}

	/**
	 * Redirects to a 404 Not Found page if the sitemap is disabled.
	 *
	 * @since 4.0.0
	 * @version 4.8.0 Moved from the Sitemap class.
	 *
	 * @return void
	 */
	public function notFoundPage() {
		global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$wp_query->set_404(); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		status_header( 404 );
		include_once get_404_template();
		exit;
	}

	/**
	 * Retrieves the post type labels for the given post type.
	 *
	 * @since 4.8.2
	 *
	 * @param  string $postType The name of a registered post type.
	 * @return object           Object with all the labels as member variables.
	 */
	public function getPostTypeLabels( $postType ) {
		static $postTypeLabels = [];
		if ( ! isset( $postTypeLabels[ $postType ] ) ) {
			$postTypeObject = get_post_type_object( $postType );
			if ( ! is_a( $postTypeObject, 'WP_Post_Type' ) ) {
				return null;
			}

			$postTypeLabels[ $postType ] = get_post_type_labels( $postTypeObject );
		}

		return $postTypeLabels[ $postType ];
	}
}Common/Traits/Helpers/Shortcodes.php000066600000014440151135505570013510 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Contains shortcode specific helper methods.
 *
 * @since 4.1.2
 */
trait Shortcodes {
	/**
	 * Shortcodes known to conflict with AIOSEO.
	 * NOTE: This is deprecated and only there for users who already were using the aioseo_conflicting_shortcodes_hook before 4.2.0.
	 *
	 * @since 4.1.2
	 *
	 * @var array
	 */
	private $conflictingShortcodes = [
		'WooCommerce Login'                => 'woocommerce_my_account',
		'WooCommerce Checkout'             => 'woocommerce_checkout',
		'WooCommerce Order Tracking'       => 'woocommerce_order_tracking',
		'WooCommerce Cart'                 => 'woocommerce_cart',
		'WooCommerce Registration'         => 'wwp_registration_form',
		'WISDM Group Registration'         => 'wdm_group_users',
		'WISDM Quiz Reporting'             => 'wdm_quiz_statistics_details',
		'WISDM Course Review'              => 'rrf_course_review',
		'Simple Membership Login'          => 'swpm_login_form',
		'Simple Membership Mini Login'     => 'swpm_mini_login',
		'Simple Membership Payment Button' => 'swpm_payment_button',
		'Simple Membership Thank You Page' => 'swpm_thank_you_page_registration',
		'Simple Membership Registration'   => 'swpm_registration_form',
		'Simple Membership Profile'        => 'swpm_profile_form',
		'Simple Membership Reset'          => 'swpm_reset_form',
		'Simple Membership Update Level'   => 'swpm_update_level_to',
		'Simple Membership Member Info'    => 'swpm_show_member_info',
		'Revslider'                        => 'rev_slider'
	];

	/**
	 * Returns the content with shortcodes replaced.
	 *
	 * @since 4.0.5
	 *
	 * @param  string $content  The post content.
	 * @param  bool   $override Whether shortcodes should be parsed regardless of the context. Needed for ActionScheduler actions.
	 * @param  int    $postId   The post ID (optional).
	 * @return string $content  The post content with shortcodes replaced.
	 */
	public function doShortcodes( $content, $override = false, $postId = 0 ) {
		// NOTE: This is_admin() check can never be removed because themes like Avada will otherwise load the wrong post.
		if ( ! $override && is_admin() ) {
			return $content;
		}

		if ( ! wp_doing_cron() && ! wp_doing_ajax() ) {
			if ( ! $override && apply_filters( 'aioseo_disable_shortcode_parsing', false ) ) {
				return $content;
			}

			if ( ! $override && ! aioseo()->options->searchAppearance->advanced->runShortcodes ) {
				return $this->doAllowedShortcodes( $content, $postId );
			}
		}

		$content = $this->doShortcodesHelper( $content, [], $postId );

		return $content;
	}

	/**
	 * Returns the content with only the allowed shortcodes and wildcards replaced.
	 *
	 * @since   4.1.2
	 * @version 4.6.6 Added the $allowedTags parameter.
	 *
	 * @param  string $content     The content.
	 * @param  int    $postId      The post ID (optional).
	 * @param  array  $allowedTags The shortcode tags to allow (optional).
	 * @return string              The content with shortcodes replaced.
	 */
	public function doAllowedShortcodes( $content, $postId = null, $allowedTags = [] ) {
		// Extract list of shortcodes from the post content.
		$tags = $this->getShortcodeTags( $content );
		if ( ! count( $tags ) ) {
			return $content;
		}

		$allowedTags  = apply_filters( 'aioseo_allowed_shortcode_tags', $allowedTags );
		$tagsToRemove = array_diff( $tags, $allowedTags );

		$content = $this->doShortcodesHelper( $content, $tagsToRemove, $postId );

		return $content;
	}

	/**
	 * Returns the content with only the allowed shortcodes and wildcards replaced.
	 *
	 * @since 4.1.2
	 *
	 * @param  string $content      The content.
	 * @param  array  $tagsToRemove The shortcode tags to remove (optional).
	 * @param  int    $postId       The post ID (optional).
	 * @return string               The content with shortcodes replaced.
	 */
	private function doShortcodesHelper( $content, $tagsToRemove = [], $postId = 0 ) {
		global $shortcode_tags; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$conflictingShortcodes = array_merge( $tagsToRemove, $this->conflictingShortcodes );
		$conflictingShortcodes = apply_filters( 'aioseo_conflicting_shortcodes', $conflictingShortcodes );

		$tagsToRemove = [];
		foreach ( $conflictingShortcodes as $shortcode ) {
			$shortcodeTag = str_replace( [ '[', ']' ], '', $shortcode );
			if ( array_key_exists( $shortcodeTag, $shortcode_tags ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				$tagsToRemove[ $shortcodeTag ] = $shortcode_tags[ $shortcodeTag ]; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			}
		}

		// Remove all conflicting shortcodes before parsing the content.
		foreach ( $tagsToRemove as $shortcodeTag => $shortcodeCallback ) {
			remove_shortcode( $shortcodeTag );
		}

		if ( $postId ) {
			global $post;
			$post = get_post( $postId );
			if ( is_a( $post, 'WP_Post' ) ) {
				// Add the current post to the loop so that shortcodes can use it if needed.
				setup_postdata( $post );
			}
		}

		// Set a flag to indicate Divi that it's processing internal content.

		$default = aioseo()->helpers->setDiviInternalRendering( true );

		$content = do_shortcode( $content );

		// Reset the Divi flag to its default value.
		aioseo()->helpers->setDiviInternalRendering( $default );

		if ( $postId ) {
			wp_reset_postdata();
		}

		// Add back shortcodes as remove_shortcode() disables them site-wide.
		foreach ( $tagsToRemove as $shortcodeTag => $shortcodeCallback ) {
			add_shortcode( $shortcodeTag, $shortcodeCallback );
		}

		return $content;
	}

	/**
	 * Extracts the shortcode tags from the content.
	 *
	 * @since 4.1.2
	 *
	 * @param  string $content The content.
	 * @return array  $tags    The shortcode tags.
	 */
	private function getShortcodeTags( $content ) {
		$tags    = [];
		$pattern = '\\[(\\[?)([^\s]*)(?![\\w-])([^\\]\\/]*(?:\\/(?!\\])[^\\]\\/]*)*?)(?:(\\/)\\]|\\](?:([^\\[]*+(?:\\[(?!\\/\\2\\])[^\\[]*+)*+)\\[\\/\\2\\])?)(\\]?)';
		if ( preg_match_all( "#$pattern#s", (string) $content, $matches ) && array_key_exists( 2, $matches ) ) {
			$tags = array_unique( $matches[2] );
		}

		if ( ! count( $tags ) ) {
			return $tags;
		}

		// Extract nested shortcodes.
		foreach ( $matches[5] as $innerContent ) {
			$tags = array_merge( $tags, $this->getShortcodeTags( $innerContent ) );
		}

		return $tags;
	}
}Common/Traits/Helpers/Svg.php000066600000002017151135505570012127 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Contains SVG specific helper methods.
 *
 * @since 4.1.4
 */
trait Svg {
	/**
	 * Sanitizes a SVG string.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $svgString The SVG to check.
	 * @return string            The sanitized SVG.
	 */
	public function escSvg( $svgString ) {
		if ( ! is_string( $svgString ) ) {
			return false;
		}

		$ksesDefaults = wp_kses_allowed_html( 'post' );

		$svgArgs = [
			'svg'   => [
				'class'           => true,
				'aria-hidden'     => true,
				'aria-labelledby' => true,
				'role'            => true,
				'xmlns'           => true,
				'width'           => true,
				'height'          => true,
				'viewbox'         => true, // <= Must be lower case!
			],
			'g'     => [ 'fill' => true ],
			'title' => [ 'title' => true ],
			'path'  => [
				'd'    => true,
				'fill' => true,
			]
		];

		return wp_kses( $svgString, array_merge( $ksesDefaults, $svgArgs ) );
	}
}Common/Traits/Helpers/Api.php000066600000004752151135505570012111 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Contains Action Scheduler specific helper methods.
 *
 * @since 4.2.4
 */
trait Api {
	/**
	 * Request the remote URL via wp_remote_post and return a json decoded response.
	 *
	 * @since 4.2.4
	 *
	 * @param  array       $body    The content to retrieve from the remote URL.
	 * @param  array       $headers The headers to send to the remote URL.
	 * @return object|null          JSON decoded response on success, false on failure.
	 */
	public function sendRequest( $url, $body = [], $headers = [] ) {
		$body = wp_json_encode( $body );

		// Build the headers of the request.
		$headers = wp_parse_args(
			$headers,
			[
				'Content-Type' => 'application/json'
			]
		);

		// Setup variable for wp_remote_post.
		$requestArgs = [
			'headers' => $headers,
			'body'    => $body,
			'timeout' => 20
		];

		// Perform the query and retrieve the response.
		$response     = $this->wpRemotePost( $url, $requestArgs );
		$responseBody = wp_remote_retrieve_body( $response );

		// Bail out early if there are any errors.
		if ( ! $responseBody ) {
			return null;
		}

		// Return the json decoded content.
		return json_decode( $responseBody );
	}

	/**
	 * Default arguments for wp_remote_get and wp_remote_post.
	 *
	 * @since 4.2.4
	 *
	 * @return array An array of default arguments for the request.
	 */
	private function getWpApiRequestDefaults() {
		return [
			'timeout'    => 10,
			'headers'    => aioseo()->helpers->getApiHeaders(),
			'user-agent' => aioseo()->helpers->getApiUserAgent()
		];
	}

	/**
	 * Sends a request using wp_remote_post.
	 *
	 * @since 4.2.4
	 *
	 * @param  string          $url  The URL to send the request to.
	 * @param  array           $args The args to use in the request.
	 * @return array|\WP_Error      The response as an array or WP_Error on failure.
	 */
	public function wpRemotePost( $url, $args = [] ) {
		return wp_remote_post( $url, array_replace_recursive( $this->getWpApiRequestDefaults(), $args ) );
	}

	/**
	 * Sends a request using wp_remote_get.
	 *
	 * @since 4.2.4
	 *
	 * @param  string          $url  The URL to send the request to.
	 * @param  array           $args The args to use in the request.
	 * @return array|\WP_Error      The response as an array or WP_Error on failure.
	 */
	public function wpRemoteGet( $url, $args = [] ) {
		return wp_remote_get( $url, array_replace_recursive( $this->getWpApiRequestDefaults(), $args ) );
	}
}Common/Traits/Helpers/Strings.php000066600000046236151135505570013034 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Contains string specific helper methods.
 *
 * @since 4.0.13
 */
trait Strings {
	/**
	 * Convert to snake case.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string The string to convert.
	 * @return string         The converted string.
	 */
	public function toSnakeCase( $string ) {
		$string[0] = strtolower( $string[0] );

		return preg_replace_callback( '/([A-Z])/', function ( $value ) {
			return '_' . strtolower( $value[1] );
		}, $string );
	}

	/**
	 * Convert to camel case.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string     The string to convert.
	 * @param  bool   $capitalize Whether or not to capitalize the first letter.
	 * @return string             The converted string.
	 */
	public function toCamelCase( $string, $capitalize = false ) {
		$string[0] = strtolower( $string[0] );
		if ( $capitalize ) {
			$string[0] = strtoupper( $string[0] );
		}

		return preg_replace_callback( '/_([a-z0-9])/', function ( $value ) {
			return strtoupper( $value[1] );
		}, $string );
	}

	/**
	 * Converts kebab case to camel case.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string                   The string to convert.
	 * @param  bool   $capitalizeFirstCharacter Whether to capitalize the first letter.
	 * @return string                           The converted string.
	 */
	public function dashesToCamelCase( $string, $capitalizeFirstCharacter = false ) {
		$string = str_replace( ' ', '', ucwords( str_replace( '-', ' ', $string ) ) );
		if ( ! $capitalizeFirstCharacter ) {
			$string[0] = strtolower( $string[0] );
		}

		return $string;
	}

	/**
	 * Truncates a given string.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $string             The string.
	 * @param  int     $maxCharacters      The max. amount of characters.
	 * @param  boolean $shouldHaveEllipsis Whether the string should have a trailing ellipsis (defaults to true).
	 * @return string                      The string.
	 */
	public function truncate( $string, $maxCharacters, $shouldHaveEllipsis = true ) {
		$length       = strlen( $string );
		$excessLength = $length - $maxCharacters;
		if ( 0 < $excessLength ) {
			// If the string is longer than 65535 characters, we first need to shorten it due to the character limit of the regex pattern quantifier.
			if ( 65535 < $length ) {
				$string = substr( $string, 0, 65534 );
			}
			$string = preg_replace( "#[^\pZ\pP]*.{{$excessLength}}$#", '', (string) $string );
			if ( $shouldHaveEllipsis ) {
				$string = $string . ' ...';
			}
		}

		return $string;
	}

	/**
	 * Escapes special regex characters.
	 *
	 * @since 4.0.5
	 *
	 * @param  string $string    The string.
	 * @param  string $delimiter The delimiter character.
	 * @return string            The escaped string.
	 */
	public function escapeRegex( $string, $delimiter = '/' ) {
		static $escapeRegex = [];
		if ( isset( $escapeRegex[ $string ] ) ) {
			return $escapeRegex[ $string ];
		}
		$escapeRegex[ $string ] = preg_quote( (string) $string, $delimiter );

		return $escapeRegex[ $string ];
	}

	/**
	 * Escapes special regex characters inside the replacement string.
	 *
	 * @since 4.0.7
	 *
	 * @param  string $string The string.
	 * @return string         The escaped string.
	 */
	public function escapeRegexReplacement( $string ) {
		static $escapeRegexReplacement = [];
		if ( isset( $escapeRegexReplacement[ $string ] ) ) {
			return $escapeRegexReplacement[ $string ];
		}

		$escapeRegexReplacement[ $string ] = str_replace( '$', '\$', $string );

		return $escapeRegexReplacement[ $string ];
	}

	/**
	 * preg_replace but with the replacement escaped.
	 *
	 * @since 4.0.10
	 *
	 * @param  string $pattern     The pattern to search for.
	 * @param  string $replacement The replacement string.
	 * @param  string $subject     The subject to search in.
	 * @return string              The subject with matches replaced.
	 */
	public function pregReplace( $pattern, $replacement, $subject ) {
		if ( ! $subject ) {
			return $subject;
		}

		$key = $pattern . $replacement . $subject;

		static $pregReplace = [];
		if ( isset( $pregReplace[ $key ] ) ) {
			return $pregReplace[ $key ];
		}

		// TODO: In the future, we should consider escaping the search pattern as well.
		// We can use the following pattern for this - (?<!\\)([\/.^$*+?|()[{}\]]{1})
		// The pattern above will only escape special characters if they're not escaped yet, which makes it compatible with all our patterns that are already escaped.
		// The caveat is that we'd need to first trim off slash delimiters and add them back later - otherwise they'd be escaped as well.

		$replacement         = $this->escapeRegexReplacement( $replacement );
		$pregReplace[ $key ] = preg_replace( $pattern, $replacement, (string) $subject );

		return $pregReplace[ $key ];
	}

	/**
	 * Returns string after converting it to lowercase.
	 *
	 * @since 4.0.13
	 *
	 * @param  string $string The original string.
	 * @return string         The string converted to lowercase.
	 */
	public function toLowerCase( $string ) {
		static $lowerCased = [];
		if ( isset( $lowerCased[ $string ] ) ) {
			return $lowerCased[ $string ];
		}
		$lowerCased[ $string ] = function_exists( 'mb_strtolower' ) ? mb_strtolower( $string, $this->getCharset() ) : strtolower( $string );

		return $lowerCased[ $string ];
	}

	/**
	 * Returns the index of a substring in a string.
	 *
	 * @since 4.1.6
	 *
	 * @param  string   $stack  The stack.
	 * @param  string   $needle The needle.
	 * @param  int      $offset The offset.
	 * @return int|bool         The index where the string starts or false if it does not exist.
	 */
	public function stringIndex( $stack, $needle, $offset = 0 ) {
		$key = $stack . $needle . $offset;

		static $stringIndex = [];
		if ( isset( $stringIndex[ $key ] ) ) {
			return $stringIndex[ $key ];
		}

		$stringIndex[ $key ] = function_exists( 'mb_strpos' ) ? mb_strpos( $stack, $needle, $offset, $this->getCharset() ) : strpos( $stack, $needle, $offset );

		return $stringIndex[ $key ];
	}

	/**
	 * Checks if the given string contains the given substring.
	 *
	 * @since 4.1.0.2
	 *
	 * @param  string   $stack  The stack.
	 * @param  string   $needle The needle.
	 * @param  int      $offset The offset.
	 * @return bool             Whether the substring occurs in the main string.
	 */
	public function stringContains( $stack, $needle, $offset = 0 ) {
		$key = $stack . $needle . $offset;

		static $stringContains = [];
		if ( isset( $stringContains[ $key ] ) ) {
			return $stringContains[ $key ];
		}

		$stringContains[ $key ] = false !== $this->stringIndex( $stack, $needle, $offset );

		return $stringContains[ $key ];
	}

	/**
	 * Check if a string is JSON encoded or not.
	 *
	 * @since 4.1.2
	 *
	 * @param  mixed $string The string to check.
	 * @return bool          True if it is JSON or false if not.
	 */
	public function isJsonString( $string ) {
		if ( ! is_string( $string ) ) {
			return false;
		}

		json_decode( $string );

		// Return a boolean whether or not the last error matches.
		return json_last_error() === JSON_ERROR_NONE;
	}

	/**
	 * Strips punctuation from a given string.
	 *
	 * @since 4.0.0
	 * @version 4.7.9 Added the $keepSpaces parameter.
	 *
	 * @param  string $string           The string.
	 * @param  array  $charactersToKeep The characters that can't be stripped (optional).
	 * @param  bool   $keepSpaces       Whether to keep spaces.
	 * @return string                   The string without punctuation.
	 */
	public function stripPunctuation( $string, $charactersToKeep = [], $keepSpaces = false ) {
		$characterRegexPattern = '';
		if ( ! empty( $charactersToKeep ) ) {
			$characterString       = implode( '', $charactersToKeep );
			$characterRegexPattern = "(?![$characterString])";
		}

		$string = aioseo()->helpers->decodeHtmlEntities( (string) $string );
		$string = preg_replace( "/{$characterRegexPattern}[\p{P}\d+]/u", '', $string );
		$string = aioseo()->helpers->encodeOutputHtml( $string );

		// Trim both internal and external whitespace.
		return $keepSpaces ? $string : preg_replace( '/\s\s+/u', ' ', trim( $string ) );
	}

	/**
	 * Returns the string after it is encoded with htmlspecialchars().
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string The string to encode.
	 * @return string         The encoded string.
	 */
	public function encodeOutputHtml( $string ) {
		if ( ! is_string( $string ) ) {
			return '';
		}

		return htmlspecialchars( $string, ENT_COMPAT | ENT_HTML401, $this->getCharset(), false );
	}

	/**
	 * Returns the string after all HTML entities have been decoded.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string The string to decode.
	 * @return string         The decoded string.
	 */
	public function decodeHtmlEntities( $string ) {
		static $decodeHtmlEntities = [];
		if ( isset( $decodeHtmlEntities[ $string ] ) ) {
			return $decodeHtmlEntities[ $string ];
		}

		// We must manually decode non-breaking spaces since html_entity_decode doesn't do this.
		$string                        = $this->pregReplace( '/&nbsp;/', ' ', $string );
		$decodeHtmlEntities[ $string ] = html_entity_decode( (string) $string, ENT_QUOTES );

		return $decodeHtmlEntities[ $string ];
	}

	/**
	 * Returns the string with script tags stripped.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string The string.
	 * @return string         The modified string.
	 */
	public function stripScriptTags( $string ) {
		static $stripScriptTags = [];
		if ( isset( $stripScriptTags[ $string ] ) ) {
			return $stripScriptTags[ $string ];
		}

		$stripScriptTags[ $string ] = $this->pregReplace( '/<script(.*?)>(.*?)<\/script>/is', '', $string );

		return $stripScriptTags[ $string ];
	}

	/**
	 * Returns the string with incomplete HTML tags stripped.
	 * Incomplete tags are not unopened/unclosed pairs but rather single tags that aren't properly formed.
	 * e.g. <a href='something'
	 * e.g. href='something' >
	 *
	 * @since 4.1.6
	 *
	 * @param  string $string The string.
	 * @return string         The modified string.
	 */
	public function stripIncompleteHtmlTags( $string ) {
		static $stripIncompleteHtmlTags = [];
		if ( isset( $stripIncompleteHtmlTags[ $string ] ) ) {
			return $stripIncompleteHtmlTags[ $string ];
		}

		$stripIncompleteHtmlTags[ $string ] = $this->pregReplace( '/(^(?!<).*?(\/>)|<[^>]*?(?!\/>)$)/is', '', $string );

		return $stripIncompleteHtmlTags[ $string ];
	}


	/**
	 * Returns the given JSON formatted data tags as a comma separated list with their values instead.
	 *
	 * @since 4.1.0
	 *
	 * @param  string|array $tags The Array or JSON formatted data tags.
	 * @return string             The comma separated values.
	 */
	public function jsonTagsToCommaSeparatedList( $tags ) {
		$tags = is_string( $tags ) ? json_decode( $tags ) : $tags;

		$values = [];
		foreach ( $tags as $k => $tag ) {
			$values[ $k ] = is_object( $tag ) ? $tag->value : $tag['value'];
		}

		return implode( ',', $values );
	}

	/**
	 * Returns the character length of the given string.
	 *
	 * @since 4.1.6
	 *
	 * @param  string $string The string.
	 * @return int            The string length.
	 */
	public function stringLength( $string ) {
		static $stringLength = [];
		if ( isset( $stringLength[ $string ] ) ) {
			return $stringLength[ $string ];
		}

		$stringLength[ $string ] = function_exists( 'mb_strlen' ) ? mb_strlen( $string, $this->getCharset() ) : strlen( $string );

		return $stringLength[ $string ];
	}

	/**
	 * Returns the word count of the given string.
	 *
	 * @since 4.1.6
	 *
	 * @param  string $string The string.
	 * @return int            The word count.
	 */
	public function stringWordCount( $string ) {
		static $stringWordCount = [];
		if ( isset( $stringWordCount[ $string ] ) ) {
			return $stringWordCount[ $string ];
		}

		$stringWordCount[ $string ] = str_word_count( $string );

		return $stringWordCount[ $string ];
	}

	/**
	 * Explodes the given string into an array.
	 *
	 * @since 4.1.6
	 *
	 * @param  string $delimiter The delimiter.
	 * @param  string $string    The string.
	 * @return array             The exploded words.
	 */
	public function explode( $delimiter, $string ) {
		$key = $delimiter . $string;

		static $exploded = [];
		if ( isset( $exploded[ $key ] ) ) {
			return $exploded[ $key ];
		}

		$exploded[ $key ] = explode( $delimiter, $string );

		return $exploded[ $key ];
	}

	/**
	 * Implodes an array into a WHEREIN clause useable string.
	 *
	 * @since 4.1.6
	 *
	 * @param  array  $array       The array.
	 * @param  bool   $outerQuotes Whether outer quotes should be added.
	 * @return string              The imploded array.
	 */
	public function implodeWhereIn( $array, $outerQuotes = false ) {
		// Reset the keys first in case there is no 0 index.
		$array = array_values( $array );

		if ( ! isset( $array[0] ) ) {
			return '';
		}

		if ( is_numeric( $array[0] ) ) {
			return implode( ', ', $array );
		}

		return $outerQuotes ? "'" . implode( "', '", $array ) . "'" : implode( "', '", $array );
	}

	/**
	 * Returns an imploded string of placeholders for usage in a WPDB prepare statement.
	 *
	 * @since 4.1.9
	 *
	 * @param  array  $array       The array.
	 * @param  string $placeholder The placeholder (e.g. "%s" or "%d").
	 * @return string              The imploded string with placeholders.
	 */
	public function implodePlaceholders( $array, $placeholder = '%s' ) {
		return implode( ', ', array_fill( 0, count( $array ), $placeholder ) );
	}

	/**
	 * Verifies that a string is indeed a valid regular expression.
	 *
	 * @since 4.2.1
	 *
	 * @return boolean True if the string is a valid regular expression.
	 */
	public function isValidRegex( $pattern ) {
		// Set a custom error handler to prevent throwing errors on a bad Regular Expression.
		set_error_handler( function() {}, E_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler

		$isValid = true;

		if ( false === preg_match( $pattern, '' ) ) {
			$isValid = false;
		}

		// Restore the error handler.
		restore_error_handler();

		return $isValid;
	}

	/**
	 * Removes the leading slash(es) from a string.
	 *
	 * @since 4.2.3
	 *
	 * @param  string $string The string.
	 * @return string         The modified string.
	 */
	public function unleadingSlashIt( $string ) {
		return ltrim( $string, '/' );
	}

	/**
	 * Convert the case of the given string.
	 *
	 * @since 4.2.4
	 *
	 * @param  string $string The string.
	 * @param  string $type   The casing ("lower", "title", "sentence").
	 * @return string         The converted string.
	 */
	public function convertCase( $string, $type ) {
		switch ( $type ) {
			case 'lower':
				return strtolower( $string );
			case 'title':
				return $this->toTitleCase( $string );
			case 'sentence':
				return $this->toSentenceCase( $string );
			default:
				return $string;
		}
	}

	/**
	 * Converts the given string to title case.
	 *
	 * @since 4.2.4
	 *
	 * @param  string $string The string.
	 * @return string         The converted string.
	 */
	public function toTitleCase( $string ) {
		// List of common English words that aren't typically modified.
		$exceptions = apply_filters( 'aioseo_title_case_exceptions', [
			'of',
			'a',
			'the',
			'and',
			'an',
			'or',
			'nor',
			'but',
			'is',
			'if',
			'then',
			'else',
			'when',
			'at',
			'from',
			'by',
			'on',
			'off',
			'for',
			'in',
			'out',
			'over',
			'to',
			'into',
			'with'
		] );

		$words = explode( ' ', strtolower( $string ) );

		foreach ( $words as $k => $word ) {
			if ( ! in_array( $word, $exceptions, true ) ) {
				$words[ $k ] = ucfirst( $word );
			}
		}

		$string = implode( ' ', $words );

		return $string;
	}

	/**
	 * Converts the given string to sentence case.
	 *
	 * @since 4.2.4
	 *
	 * @param  string $string The string.
	 * @return string         The converted string.
	 */
	public function toSentenceCase( $string ) {
		$phrases = preg_split( '/([.?!]+)/', (string) $string, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE );

		$convertedString = '';
		foreach ( $phrases as $index => $sentence ) {
			$convertedString .= ( $index & 1 ) === 0 ? ucfirst( strtolower( trim( $sentence ) ) ) : $sentence . ' ';
		}

		return trim( $convertedString );
	}

	/**
	 * Returns the substring with a given start index and length.
	 *
	 * @since 4.2.5
	 *
	 * @param  string $string     The string.
	 * @param  int    $startIndex The start index.
	 * @param  int    $length     The length.
	 * @return string             The substring.
	 */
	public function substring( $string, $startIndex, $length ) {
		return function_exists( 'mb_substr' ) ? mb_substr( $string, $startIndex, $length, $this->getCharset() ) : substr( $string, $startIndex, $length );
	}

	/**
	 * Strips emoji characters from a given string.
	 *
	 * @since 4.7.3
	 *
	 * @param  string $string The string.
	 * @return string         The string without emoji characters.
	 */
	public function stripEmoji( $string ) {
		// First, decode HTML entities to convert them to actual Unicode characters.
		$string = $this->decodeHtmlEntities( $string );

		// Pattern to match emoji characters.
		$emojiPattern = '/[\x{1F600}-\x{1F64F}' . // Emoticons
						'\x{1F300}-\x{1F5FF}' . // Misc Symbols and Pictographs
						'\x{1F680}-\x{1F6FF}' . // Transport and Map Symbols
						'\x{1F1E0}-\x{1F1FF}' . // Flags (iOS)
						'\x{2600}-\x{26FF}' . // Misc symbols
						'\x{2700}-\x{27BF}' . // Dingbats
						'\x{FE00}-\x{FE0F}' . // Variation Selectors
						'\x{1F900}-\x{1F9FF}' . // Supplemental Symbols and Pictographs
						']/u';

		$filteredString = preg_replace( $emojiPattern, '', (string) $string );

		// Re-encode special characters to HTML entities.
		return $this->encodeOutputHtml( $filteredString );
	}

	/**
	 * Creates a sha1 hash from the given arguments.
	 *
	 * @since 4.7.8
	 *
	 * @param  mixed  ...$args The arguments to create a sha1 hash from.
	 * @return string          The sha1 hash.
	 */
	public function createHash( ...$args ) {
		return sha1( wp_json_encode( $args ) );
	}

	/**
	 * Extracts URLs from a given string.
	 *
	 * @since 4.8.1
	 *
	 * @param  string $string The string.
	 * @return array          The extracted URLs.
	 */
	public function extractUrls( $string ) {
		$urls = wp_extract_urls( $string );

		if ( empty( $urls ) ) {
			return [];
		}

		$allUrls = [];

		// Attempt to split multiple URLs. Elementor does not always separate them properly.
		foreach ( $urls as $url ) {
			$splitUrls = preg_split( '/(?=https?:\/\/)/', $url, - 1, PREG_SPLIT_NO_EMPTY );
			$allUrls   = array_merge( $allUrls, $splitUrls );
		}

		return $allUrls;
	}

	/**
	 * Determines if a text string contains an emoji or not.
	 *
	 * @since 4.8.0
	 *
	 * @param  string $string The text string to detect emoji in.
	 * @return bool
	 */
	public function hasEmojis( $string ) {
		$emojisRegexPattern = '/[\x{1F600}-\x{1F64F}' . // Emoticons
							'\x{1F300}-\x{1F5FF}' . // Misc Symbols and Pictographs
							'\x{1F680}-\x{1F6FF}' . // Transport and Map Symbols
							'\x{1F1E0}-\x{1F1FF}' . // Flags (iOS)
							'\x{2600}-\x{26FF}' . // Misc symbols
							'\x{2700}-\x{27BF}' . // Dingbats
							'\x{FE00}-\x{FE0F}' . // Variation Selectors
							'\x{1F900}-\x{1F9FF}' . // Supplemental Symbols and Pictographs
							'\x{1F018}-\x{1F270}' . // Various Asian characters
							'\x{238C}-\x{2454}' . // Misc items
							'\x{20D0}-\x{20FF}' . // Combining Diacritical Marks for Symbols
							']/u';

		return preg_match( $emojisRegexPattern, $string );
	}
}Common/Traits/Helpers/Constants.php000066600000025544151135505570013356 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Contains constant specific helper methods.
 *
 * @since 4.0.17
 */
trait Constants {
	/**
	 * Returns the All in One SEO Logo
	 *
	 * @since 4.0.0
	 *
	 * @param  string $width     The width of the image.
	 * @param  string $height    The height of the image.
	 * @param  string $colorCode The color of the image.
	 * @return string            The logo as a string.
	 */
	public function logo( $width, $height, $colorCode ) {
		return '<svg viewBox="0 0 20 20" width="' . $width . '" height="' . $height . '" fill="none" xmlns="http://www.w3.org/2000/svg" class="aioseo-gear"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.98542 19.9708C15.5002 19.9708 19.9708 15.5002 19.9708 9.98542C19.9708 4.47063 15.5002 0 9.98542 0C4.47063 0 0 4.47063 0 9.98542C0 15.5002 4.47063 19.9708 9.98542 19.9708ZM8.39541 3.65464C8.26016 3.4485 8.0096 3.35211 7.77985 3.43327C7.51816 3.52572 7.26218 3.63445 7.01349 3.7588C6.79519 3.86796 6.68566 4.11731 6.73372 4.36049L6.90493 5.22694C6.949 5.44996 6.858 5.6763 6.68522 5.82009C6.41216 6.04734 6.16007 6.30426 5.93421 6.58864C5.79383 6.76539 5.57233 6.85907 5.35361 6.81489L4.50424 6.6433C4.26564 6.5951 4.02157 6.70788 3.91544 6.93121C3.85549 7.05738 3.79889 7.1862 3.74583 7.31758C3.69276 7.44896 3.64397 7.58105 3.59938 7.71369C3.52048 7.94847 3.61579 8.20398 3.81839 8.34133L4.53958 8.83027C4.72529 8.95617 4.81778 9.1819 4.79534 9.40826C4.75925 9.77244 4.76072 10.136 4.79756 10.4936C4.82087 10.7198 4.72915 10.9459 4.54388 11.0724L3.82408 11.5642C3.62205 11.7022 3.52759 11.9579 3.60713 12.1923C3.69774 12.4593 3.8043 12.7205 3.92615 12.9743C4.03313 13.1971 4.27749 13.3088 4.51581 13.2598L5.36495 13.0851C5.5835 13.0401 5.80533 13.133 5.94623 13.3093C6.16893 13.5879 6.42071 13.8451 6.6994 14.0756C6.87261 14.2188 6.96442 14.4448 6.92112 14.668L6.75296 15.5348C6.70572 15.7782 6.81625 16.0273 7.03511 16.1356C7.15876 16.1967 7.285 16.2545 7.41375 16.3086C7.54251 16.3628 7.67196 16.4126 7.80195 16.4581C8.18224 16.5912 8.71449 16.1147 9.108 15.7625C9.30205 15.5888 9.42174 15.343 9.42301 15.0798C9.42301 15.0784 9.42302 15.077 9.42302 15.0756L9.42301 13.6263C9.42301 13.6109 9.4236 13.5957 9.42476 13.5806C8.26248 13.2971 7.39838 12.2301 7.39838 10.9572V9.41823C7.39838 9.30125 7.49131 9.20642 7.60596 9.20642H8.32584V7.6922C8.32584 7.48312 8.49193 7.31364 8.69683 7.31364C8.90171 7.31364 9.06781 7.48312 9.06781 7.6922V9.20642H11.0155V7.6922C11.0155 7.48312 11.1816 7.31364 11.3865 7.31364C11.5914 7.31364 11.7575 7.48312 11.7575 7.6922V9.20642H12.4773C12.592 9.20642 12.6849 9.30125 12.6849 9.41823V10.9572C12.6849 12.2704 11.7653 13.3643 10.5474 13.6051C10.5477 13.6121 10.5478 13.6192 10.5478 13.6263L10.5478 15.0694C10.5478 15.3377 10.6711 15.5879 10.871 15.7622C11.2715 16.1115 11.8129 16.5837 12.191 16.4502C12.4527 16.3577 12.7086 16.249 12.9573 16.1246C13.1756 16.0155 13.2852 15.7661 13.2371 15.5229L13.0659 14.6565C13.0218 14.4334 13.1128 14.2071 13.2856 14.0633C13.5587 13.8361 13.8107 13.5792 14.0366 13.2948C14.177 13.118 14.3985 13.0244 14.6172 13.0685L15.4666 13.2401C15.7052 13.2883 15.9493 13.1756 16.0554 12.9522C16.1153 12.8261 16.1719 12.6972 16.225 12.5659C16.2781 12.4345 16.3269 12.3024 16.3714 12.1698C16.4503 11.935 16.355 11.6795 16.1524 11.5421L15.4312 11.0532C15.2455 10.9273 15.153 10.7015 15.1755 10.4752C15.2116 10.111 15.2101 9.74744 15.1733 9.38986C15.1499 9.16361 15.2417 8.93757 15.4269 8.811L16.1467 8.31927C16.3488 8.18126 16.4432 7.92558 16.3637 7.69115C16.2731 7.42411 16.1665 7.16292 16.0447 6.90915C15.9377 6.68638 15.6933 6.57462 15.455 6.62366L14.6059 6.79837C14.3873 6.84334 14.1655 6.75048 14.0246 6.57418C13.8019 6.29554 13.5501 6.03832 13.2714 5.80784C13.0982 5.6646 13.0064 5.43858 13.0497 5.2154L13.2179 4.34868C13.2651 4.10521 13.1546 3.85616 12.9357 3.74787C12.8121 3.68669 12.6858 3.62895 12.5571 3.5748C12.4283 3.52065 12.2989 3.47086 12.1689 3.42537C11.9388 3.34485 11.6884 3.44211 11.5538 3.64884L11.0746 4.38475C10.9513 4.57425 10.73 4.66862 10.5082 4.64573C10.1513 4.6089 9.79502 4.61039 9.44459 4.64799C9.22286 4.67177 9.00134 4.57818 8.87731 4.38913L8.39541 3.65464Z" fill="' . $colorCode . '" /></svg>'; // phpcs:ignore Generic.Files.LineLength.MaxExceeded
	}

	/**
	 * Returns the country name by code.
	 *
	 * @since 4.0.17
	 *
	 * @param  string $countryCode The country code.
	 * @return string              Country name.
	 */
	public function getCountryName( $countryCode ) {
		return isset( $this->countryList()[ $countryCode ] ) ? $this->countryList()[ $countryCode ] : '';
	}

	/**
	 * Returns a list of countries.
	 *
	 * @since 4.0.17
	 *
	 * @return array A list of countries.
	 */
	public function countryList() {
		return [
			'AF' => 'Afghanistan',
			'AL' => 'Albania',
			'DZ' => 'Algeria',
			'AS' => 'American Samoa',
			'AD' => 'Andorra',
			'AO' => 'Angola',
			'AI' => 'Anguilla',
			'AQ' => 'Antarctica',
			'AG' => 'Antigua and Barbuda',
			'AR' => 'Argentina',
			'AM' => 'Armenia',
			'AW' => 'Aruba',
			'AU' => 'Australia',
			'AT' => 'Austria',
			'AZ' => 'Azerbaijan',
			'BS' => 'Bahamas',
			'BH' => 'Bahrain',
			'BD' => 'Bangladesh',
			'BB' => 'Barbados',
			'BY' => 'Belarus',
			'BE' => 'Belgium',
			'BZ' => 'Belize',
			'BJ' => 'Benin',
			'BM' => 'Bermuda',
			'BT' => 'Bhutan',
			'BO' => 'Bolivia',
			'BQ' => 'Bonaire',
			'BA' => 'Bosnia and Herzegovina',
			'BW' => 'Botswana',
			'BV' => 'Bouvet Island',
			'BR' => 'Brazil',
			'IO' => 'British Indian Ocean Territory',
			'BN' => 'Brunei Darussalam',
			'BG' => 'Bulgaria',
			'BF' => 'Burkina Faso',
			'BI' => 'Burundi',
			'CV' => 'Cabo Verde',
			'KH' => 'Cambodia',
			'CM' => 'Cameroon',
			'CA' => 'Canada',
			'KY' => 'Cayman Islands',
			'CF' => 'Central African Republic',
			'TD' => 'Chad',
			'CL' => 'Chile',
			'CN' => 'China',
			'CX' => 'Christmas Island',
			'CC' => 'Cocos (Keeling) Islands',
			'CO' => 'Colombia',
			'KM' => 'Comoros',
			'CD' => 'Democratic Republic of the Congo',
			'CG' => 'Congo',
			'CK' => 'Cook Islands',
			'CR' => 'Costa Rica',
			'HR' => 'Croatia',
			'CU' => 'Cuba',
			'CW' => 'Curaçao',
			'CY' => 'Cyprus',
			'CZ' => 'Czechia',
			'CI' => 'Côte d\'Ivoire',
			'DK' => 'Denmark',
			'DJ' => 'Djibouti',
			'DM' => 'Dominica',
			'DO' => 'Dominican Republic',
			'EC' => 'Ecuador',
			'EG' => 'Egypt',
			'SV' => 'El Salvador',
			'GQ' => 'Equatorial Guinea',
			'ER' => 'Eritrea',
			'EE' => 'Estonia',
			'SZ' => 'Eswatini',
			'ET' => 'Ethiopia',
			'FK' => 'Falkland Islands',
			'FO' => 'Faroe Islands',
			'FJ' => 'Fiji',
			'FI' => 'Finland',
			'FR' => 'France',
			'GF' => 'French Guiana',
			'PF' => 'French Polynesia',
			'TF' => 'French Southern Territories',
			'GA' => 'Gabon',
			'GM' => 'Gambia',
			'GE' => 'Georgia',
			'DE' => 'Germany',
			'GH' => 'Ghana',
			'GI' => 'Gibraltar',
			'GR' => 'Greece',
			'GL' => 'Greenland',
			'GD' => 'Grenada',
			'GP' => 'Guadeloupe',
			'GU' => 'Guam',
			'GT' => 'Guatemala',
			'GG' => 'Guernsey',
			'GN' => 'Guinea',
			'GW' => 'Guinea-Bissau',
			'GY' => 'Guyana',
			'HT' => 'Haiti',
			'HM' => 'Heard Island and McDonald Islands',
			'VA' => 'Holy See',
			'HN' => 'Honduras',
			'HK' => 'Hong Kong',
			'HU' => 'Hungary',
			'IS' => 'Iceland',
			'IN' => 'India',
			'ID' => 'Indonesia',
			'IR' => 'Iran',
			'IQ' => 'Iraq',
			'IE' => 'Ireland',
			'IM' => 'Isle of Man',
			'IL' => 'Israel',
			'IT' => 'Italy',
			'JM' => 'Jamaica',
			'JP' => 'Japan',
			'JE' => 'Jersey',
			'JO' => 'Jordan',
			'KZ' => 'Kazakhstan',
			'KE' => 'Kenya',
			'KI' => 'Kiribati',
			'KR' => 'South Korea',
			'KW' => 'Kuwait',
			'KG' => 'Kyrgyzstan',
			'LA' => 'Lao People\'s Democratic Republic',
			'LV' => 'Latvia',
			'LB' => 'Lebanon',
			'LS' => 'Lesotho',
			'LR' => 'Liberia',
			'LY' => 'Libya',
			'LI' => 'Liechtenstein',
			'LT' => 'Lithuania',
			'LU' => 'Luxembourg',
			'MO' => 'Macao',
			'MG' => 'Madagascar',
			'MW' => 'Malawi',
			'MY' => 'Malaysia',
			'MV' => 'Maldives',
			'ML' => 'Mali',
			'MT' => 'Malta',
			'MH' => 'Marshall Islands',
			'MQ' => 'Martinique',
			'MR' => 'Mauritania',
			'MU' => 'Mauritius',
			'YT' => 'Mayotte',
			'MX' => 'Mexico',
			'FM' => 'Micronesia',
			'MD' => 'Moldova',
			'MC' => 'Monaco',
			'MN' => 'Mongolia',
			'ME' => 'Montenegro',
			'MS' => 'Montserrat',
			'MA' => 'Morocco',
			'MZ' => 'Mozambique',
			'MM' => 'Myanmar',
			'NA' => 'Namibia',
			'NR' => 'Nauru',
			'NP' => 'Nepal',
			'NL' => 'Netherlands',
			'NC' => 'New Caledonia',
			'NZ' => 'New Zealand',
			'NI' => 'Nicaragua',
			'NE' => 'Niger',
			'NG' => 'Nigeria',
			'NU' => 'Niue',
			'NF' => 'Norfolk Island',
			'MP' => 'Northern Mariana Islands',
			'NO' => 'Norway',
			'OM' => 'Oman',
			'PK' => 'Pakistan',
			'PW' => 'Palau',
			'PS' => 'Palestine, State of',
			'PA' => 'Panama',
			'PG' => 'Papua New Guinea',
			'PY' => 'Paraguay',
			'PE' => 'Peru',
			'PH' => 'Philippines',
			'PN' => 'Pitcairn',
			'PL' => 'Poland',
			'PT' => 'Portugal',
			'PR' => 'Puerto Rico',
			'QA' => 'Qatar',
			'MK' => 'Republic of North Macedonia',
			'RO' => 'Romania',
			'RU' => 'Russian Federation',
			'RW' => 'Rwanda',
			'RE' => 'Réunion',
			'BL' => 'Saint Barthélemy',
			'SH' => 'Saint Helena, Ascension and Tristan da Cunha',
			'KN' => 'Saint Kitts and Nevis',
			'LC' => 'Saint Lucia',
			'MF' => 'Saint Martin',
			'PM' => 'Saint Pierre and Miquelon',
			'VC' => 'Saint Vincent and the Grenadines',
			'WS' => 'Samoa',
			'SM' => 'San Marino',
			'ST' => 'Sao Tome and Principe',
			'SA' => 'Saudi Arabia',
			'SN' => 'Senegal',
			'RS' => 'Serbia',
			'SC' => 'Seychelles',
			'SL' => 'Sierra Leone',
			'SG' => 'Singapore',
			'SX' => 'Sint Maarten',
			'SK' => 'Slovakia',
			'SI' => 'Slovenia',
			'SB' => 'Solomon Islands',
			'SO' => 'Somalia',
			'ZA' => 'South Africa',
			'GS' => 'South Georgia and the South Sandwich Islands',
			'SS' => 'South Sudan',
			'ES' => 'Spain',
			'LK' => 'Sri Lanka',
			'SD' => 'Sudan',
			'SR' => 'Suriname',
			'SJ' => 'Svalbard and Jan Mayen',
			'SE' => 'Sweden',
			'CH' => 'Switzerland',
			'SY' => 'Syrian Arab Republic',
			'TW' => 'Taiwan',
			'TJ' => 'Tajikistan',
			'TZ' => 'Tanzania, United Republic of',
			'TH' => 'Thailand',
			'TL' => 'Timor-Leste',
			'TG' => 'Togo',
			'TK' => 'Tokelau',
			'TO' => 'Tonga',
			'TT' => 'Trinidad and Tobago',
			'TN' => 'Tunisia',
			'TR' => 'Turkey',
			'TM' => 'Turkmenistan',
			'TC' => 'Turks and Caicos Islands',
			'TV' => 'Tuvalu',
			'UG' => 'Uganda',
			'UA' => 'Ukraine',
			'AE' => 'United Arab Emirates',
			'GB' => 'United Kingdom of Great Britain and Northern Ireland',
			'UM' => 'United States Minor Outlying Islands',
			'US' => 'United States of America',
			'UY' => 'Uruguay',
			'UZ' => 'Uzbekistan',
			'VU' => 'Vanuatu',
			'VE' => 'Venezuela',
			'VN' => 'Vietnam',
			'VG' => 'Virgin Islands (British)',
			'VI' => 'Virgin Islands (U.S.)',
			'WF' => 'Wallis and Futuna',
			'EH' => 'Western Sahara',
			'YE' => 'Yemen',
			'ZM' => 'Zambia',
			'ZW' => 'Zimbabwe',
			'AX' => 'Åland Islands'
		];
	}
}Common/Traits/Helpers/Arrays.php000066600000020052151135505570012630 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Contains array specific helper methods.
 *
 * @since 4.1.4
 */
trait Arrays {
	/**
	 * Unsets a given value in a given array.
	 * This should only be used if the given value only appears once in the array.
	 *
	 * @since 4.0.0
	 *
	 * @param  array  $array The array.
	 * @param  string $value The value that needs to be removed from the array.
	 * @return array  $array The filtered array.
	 */
	public function unsetValue( $array, $value ) {
		if ( in_array( $value, $array, true ) ) {
			unset( $array[ array_search( $value, $array, true ) ] );
		}

		return $array;
	}

	/**
	 * Compares two multidimensional arrays to see if they're different.
	 *
	 * @since 4.0.0
	 *
	 * @param  array   $array1 The first array.
	 * @param  array   $array2 The second array.
	 * @return boolean         Whether the arrays are different.
	 */
	public function arraysDifferent( $array1, $array2 ) {
		foreach ( $array1 as $key => $value ) {
			// Check for non-existing values.
			if ( ! isset( $array2[ $key ] ) ) {
				return true;
			}
			if ( is_array( $value ) ) {
				if ( $this->arraysDifferent( $value, $array2[ $key ] ) ) {
					return true;
				}
			} else {
				if ( $value !== $array2[ $key ] ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Checks whether the given array is associative.
	 * Arrays that only have consecutive, sequential numeric keys are numeric.
	 * Otherwise they are associative.
	 *
	 * @since 4.1.4
	 *
	 * @param  array $array The array.
	 * @return bool         Whether the array is associative.
	 */
	public function isArrayAssociative( $array ) {
		return 0 < count( array_filter( array_keys( $array ), 'is_string' ) );
	}

	/**
	 * Checks whether the given array is numeric.
	 *
	 * @since 4.1.4
	 *
	 * @param  array $array The array.
	 * @return bool         Whether the array is numeric.
	 */
	public function isArrayNumeric( $array ) {
		return ! $this->isArrayAssociative( $array );
	}

	/**
	 * Recursively replaces the values from one array with the ones from another.
	 * This function should act identical to the built-in array_replace_recursive(), with the exception that it also replaces array values with empty arrays.
	 *
	 * @since 4.2.4
	 *
	 * @param  array $targetArray      The target array
	 * @param  array $replacementArray The array with values to replace in the target array.
	 * @return array                   The modified array.
	 */
	public function arrayReplaceRecursive( $targetArray, $replacementArray ) {
		// In some cases the target array isn't an array yet (due to e.g. race conditions in InternalOptions), so in that case we can just return the replacement array.
		if ( ! is_array( $targetArray ) ) {
			return $replacementArray;
		}

		foreach ( $replacementArray as $k => $v ) {
			// If the key does not exist yet on the target array, add it.
			if ( ! isset( $targetArray[ $k ] ) ) {
				$targetArray[ $k ] = $replacementArray[ $k ];
				continue;
			}

			// If the value is an array, only try to recursively replace it if the value isn't empty.
			// Otherwise empty arrays will be ignored and won't override the existing value of the target array.
			if ( is_array( $v ) && ! empty( $v ) ) {
				$targetArray[ $k ] = $this->arrayReplaceRecursive( $targetArray[ $k ], $v );
				continue;
			}

			// Replace with non-array value or empty array.
			$targetArray[ $k ] = $v;
		}

		return $targetArray;
	}

	/**
	 * Recursively intersects the two given arrays.
	 * You can pass in an optional argument (allowedKey) to restrict the intersect to arrays with a specific key.
	 * This is needed when we are e.g. sanitizing array values before setting/saving them to an option.
	 * This helper method was mainly built to support our complex options architecture.
	 *
	 * @since 4.2.5
	 *
	 * @param  array  $array1     The first array.
	 * @param  array  $array2     The second array.
	 * @param  string $allowedKey The only key the method should run for (optional).
	 * @param  string $parentKey  The parent key.
	 * @return array              The intersected array.
	 */
	public function arrayIntersectRecursive( $array1, $array2, $allowedKey = '', $parentKey = '' ) {
		if ( ! $allowedKey || $allowedKey === $parentKey ) {
			$array1 = $this->arrayIntersectRecursiveHelper( $array1, $array2 );
		}

		if ( empty( $array1 ) ) {
			return [];
		}

		foreach ( $array1 as $k => $v ) {
			if ( is_array( $v ) && isset( $array2[ $k ] ) ) {
				$array1[ $k ] = $this->arrayIntersectRecursive( $array1[ $k ], $array2[ $k ], $allowedKey, $k );
			}
		}

		if ( $this->isArrayNumeric( $array1 ) ) {
			$array1 = array_values( $array1 );
		}

		return $array1;
	}

	/**
	 * Recursively intersects the two given arrays. Supports arrays with a mix of nested arrays and primitive values.
	 * Helper function for arrayIntersectRecursive().
	 *
	 * @since 4.5.4
	 *
	 * @param  array $array1 The first array.
	 * @param  array $array2 The second array.
	 * @return array         The intersected array.
	 */
	private function arrayIntersectRecursiveHelper( $array1, $array2 ) {
		if ( null === $array2 ) {
			$array2 = [];
		}

		if ( is_array( $array1 ) ) {
			// First, check with keys are nested arrays and which are primitive values.
			$arrays     = [];
			$primitives = [];
			foreach ( $array1 as $k => $v ) {
				if ( is_array( $v ) ) {
					$arrays[ $k ] = $v;
				} else {
					$primitives[ $k ] = $v;
				}
			}

			// Then, intersect the primitive values.
			$intersectedPrimitives = array_intersect_assoc( $primitives, $array2 );

			// Finally, recursively intersect the nested arrays.
			$intersectedArrays = [];
			foreach ( $arrays as $k => $v ) {
				if ( isset( $array2[ $k ] ) ) {
					$intersectedArrays[ $k ] = $this->arrayIntersectRecursiveHelper( $v, $array2[ $k ] );
				} else {
					// If the nested array doesn't exist in the second array, we can just unset it.
					unset( $arrays[ $k ] );
				}
			}

			// Merge the intersected arrays and primitive values.
			return array_merge( $intersectedPrimitives, $intersectedArrays );
		}

		return array_intersect_assoc( $array1, $array2 );
	}

	/**
	 * Sorts the keys of an array alphabetically.
	 * The array is passed by reference, so it's not returned the same as in `ksort()`.
	 *
	 * @since 4.4.0.3
	 *
	 * @param array $array The array to sort, passed by reference.
	 */
	public function arrayRecursiveKsort( &$array ) {
		foreach ( $array as &$value ) {
			if ( is_array( $value ) ) {
				$this->arrayRecursiveKsort( $value );
			}
		}

		ksort( $array );
	}

	/**
	 * Creates a multidimensional array from a list of keys and a value.
	 *
	 * @since 4.5.3
	 *
	 * @param  array $keys  The keys to create the array from.
	 * @param  mixed $value The value to assign to the last key.
	 * @param  array $array The array when recursing.
	 * @return array        The multidimensional array.
	 */
	public function createMultidimensionalArray( $keys, $value, $array = [] ) {
		$key = array_shift( $keys );
		if ( empty( $array[ $key ] ) ) {
			$array[ $key ] = null;
		}

		if ( 0 < count( $keys ) ) {
			$array[ $key ] = $this->createMultidimensionalArray( $keys, $value, $array[ $key ] );
		} else {
			$array[ $key ] = $value;
		}

		return $array;
	}

	/**
	 * Sorts an array of arrays by a specific key.
	 *
	 * @since 4.7.4
	 *
	 * @param  array  $arr   The input array.
	 * @param  string $key   The key to sort by.
	 * @param  string $order Designates ascending or descending order. Default 'asc'. Accepts 'asc', 'desc'.
	 * @return void
	 */
	public function usortByKey( &$arr, $key, $order = 'asc' ) {
		if ( empty( $arr ) || ! is_array( $arr ) ) {
			return;
		}

		usort( $arr, function ( $a, $b ) use ( $key, $order ) {
			return 'asc' === $order ? $a[ $key ] <=> $b[ $key ] : $b[ $key ] <=> $a[ $key ];
		} );
	}

	/**
	 * Flattens a multidimensional array.
	 *
	 * @since 4.7.6
	 *
	 * @param  array $arr The input array.
	 * @return array      The flattened array.
	 */
	public function flatten( $arr ) {
		$result = [];
		array_walk_recursive( $arr, function ( $value ) use ( &$result ) {
			$result[] = $value;
		} );

		return $result;
	}
}Common/Traits/Helpers/WpMultisite.php000066600000017264151135505570013670 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Contains methods related to multisite.
 *
 * @since 4.2.5
 */
trait WpMultisite {
	/**
	 * Returns the ID of the network's main site.
	 *
	 * @since 4.2.5
	 *
	 * @return int The ID of the network's main site.
	 */
	public function getNetworkId() {
		if ( is_multisite() ) {
			return get_network()->site_id;
		}

		return get_current_blog_id();
	}

	/**
	 * Get a site (with aliases) by it's blog ID.
	 *
	 * @since 4.2.5
	 *
	 * @param  int          $blogId The blog ID.
	 * @return \WP_Site|null         The site.
	 */
	public function getSiteByBlogId( $blogId ) {
		$sites = $this->getSites();
		foreach ( $sites['sites'] as $site ) {
			if ( $site->blog_id === $blogId ) {
				return $site;
			}
		}

		return null;
	}

	/**
	 * Get the current site.
	 *
	 * @since 4.2.5
	 *
	 * @return \WP_Site|object A WP_Site instance of the current site or an object representing the same.
	 */
	public function getSite() {
		if ( is_multisite() ) {
			return get_site();
		}

		return (object) [
			'domain' => $this->getSiteDomain( true ),
			'path'   => $this->getHomePath( true )
		];
	}

	/**
	 * Get all sites in the multisite network.
	 *
	 * @since 4.2.5
	 *
	 * @param  int|string  $limit      The number of sites to get or 'all'.
	 * @param  int         $offset     The offset to start at.
	 * @param  null|string $searchTerm The search term to look for.
	 * @param  null|string $filter     A filter to look up sites by.
	 * @param  null|string $orderBy    The column to order results by. Defaults to null.
	 * @param  string      $orderDir   The direction to order results by. Defaults to 'DESC'.
	 * @return array                   An array of sites.
	 */
	public function getSites( $limit = 'all', $offset = 0, $searchTerm = null, $filter = 'all', $orderBy = null, $orderDir = 'DESC' ) {
		$countSites = $this->countSites();
		$sites      = get_sites( [
			'network_id' => get_current_network_id(),
			'number'     => $countSites['public'],
			'public'     => 1
		] );

		$allSites = [];
		foreach ( $sites as $site ) {
			$clonedSite           = clone $site;
			$clonedSite->adminUrl = get_admin_url( $site->blog_id );
			$clonedSite->homeUrl  = get_home_url( $site->blog_id );

			if ( $this->includeSite( $clonedSite, $filter ) ) {
				$allSites[] = $clonedSite;
			}

			// We need to look up aliases for Mercator, this checks to see if it's even enabled.
			if ( ! class_exists( '\Mercator\Mapping' ) ) {
				continue;
			}

			$aliases = $this->getSiteAliases( $site );
			foreach ( $aliases as $alias ) {
				$aliasSite               = clone $clonedSite;
				$aliasSite->domain       = $alias['domain'];
				$aliasSite->path         = '/';
				$aliasSite->alias        = $alias;
				$aliasSite->parentDomain = $site->domain;
				$aliasSite->parentPath   = $site->path;

				if ( $this->includeSite( $aliasSite, $filter ) ) {
					$allSites[] = $aliasSite;
				}
			}
		}

		// If we have a search term, let's filter down these results.
		if ( ! empty( $searchTerm ) ) {
			foreach ( $allSites as $key => $site ) {
				$keep = false;
				if (
					false !== stripos( $site->domain, $searchTerm ) ||
					false !== stripos( $site->path, $searchTerm ) ||
					false !== stripos( $site->parentDomain, $searchTerm ) ||
					false !== stripos( $site->parentPath, $searchTerm )
				) {
					$keep = true;
				}

				if ( ! $keep ) {
					unset( $allSites[ $key ] );
				}
			}
		}

		// Ordering the sites.
		if ( ! empty( $orderBy ) ) {
			usort( $allSites, function( $site1, $site2 ) use ( $orderBy, $orderDir ) {
				if ( empty( $site1->{ $orderBy } ) ) {
					return 0;
				}

				return 'ASC' === strtoupper( $orderDir )
					? ( $site1->{ $orderBy } > $site2->{ $orderBy } ? 1 : 0 )
					: ( $site1->{ $orderBy } < $site2->{ $orderBy } ? 1 : 0 );
			} );
		}

		return [
			'total' => count( $allSites ),
			'limit' => $limit,
			'sites' => 'all' === $limit ? $allSites : array_slice( $allSites, $offset, $limit )
		];
	}

	/**
	 * Count the number of sites in the network. A clone of wp_count_sites. We use this because
	 * we don't yet support WordPress 5.3. Once we do, we can revert to wp_count_sites.
	 *
	 * @since 4.4.5
	 *
	 * @return array          An array of aliases.
	 */
	private function countSites() {
		$networkId = get_current_network_id();

		$counts = [];
		$args   = [
			'network_id'    => $networkId,
			'number'        => 1,
			'fields'        => 'ids',
			'no_found_rows' => false,
		];

		$q             = new \WP_Site_Query( $args );
		$counts['all'] = $q->found_sites;

		$_args    = $args;
		$statuses = [ 'public', 'archived', 'mature', 'spam', 'deleted' ];

		foreach ( $statuses as $status ) {
			$_args            = $args;
			$_args[ $status ] = 1;

			$q                 = new \WP_Site_Query( $_args );
			$counts[ $status ] = $q->found_sites;
		}

		return $counts;
	}

	/**
	 * Filter sites based on a passed in filter. Options include 'all', 'activated' or 'deactivated'.
	 *
	 * @since 4.2.5
	 *
	 * @param  Object $site   The site object.
	 * @param  string $filter The filter to use.
	 * @return bool           The site if allowed or null if not.
	 */
	private function includeSite( $site, $filter ) {
		if ( 'all' === $filter ) {
			return true;
		}

		$siteIsActive = aioseo()->networkLicense->isSiteActive( $site );
		if (
			( 'deactivated' === $filter && ! $siteIsActive ) ||
			( 'activated' === $filter && $siteIsActive )
		) {
			return true;
		}

		return false;
	}

	/**
	 * Get an array of aliases for a WP_Site.
	 *
	 * @since 4.2.5
	 *
	 * @param  \WP_Site $site The Site.
	 * @return array          An array of aliases.
	 */
	public function getSiteAliases( $site ) {
		// We need to look up aliases for Mercator, this checks to see if it's even enabled.
		if ( ! class_exists( '\Mercator\Mapping' ) ) {
			return [];
		}

		$aliases = \Mercator\Mapping::get_by_site( $site->blog_id );
		if ( empty( $aliases ) ) {
			return [];
		}

		$aliasData = [];
		foreach ( $aliases as $alias ) {
			$aliasData[] = [
				'alias_id' => $alias->get_id(),
				'domain'   => $alias->get_domain(),
				'active'   => $alias->is_active()
			];
		}

		return $aliasData;
	}

	/**
	 * Wrapper for switch_to_blog especially for non-multisite setups.
	 *
	 * @since 4.2.5
	 *
	 * @param  int  $blogId The blog ID to switch to.
	 * @return bool         Whether the blog was switched to or not.
	 */
	public function switchToBlog( $blogId ) {
		if ( ! is_multisite() ) {
			return false;
		}

		switch_to_blog( $blogId );

		aioseo()->core->db->init();

		return true;
	}

	/**
	 * Wrapper for restore_current_blog especially for non-multisite setups.
	 *
	 * @since 4.2.5
	 *
	 * @return bool Whether the blog was restored or not.
	 */
	public function restoreCurrentBlog() {
		if ( ! is_multisite() ) {
			return false;
		}

		restore_current_blog();

		aioseo()->core->db->init();

		return true;
	}

	/**
	 * Checks if the current plugin is network activated.
	 *
	 * @since 4.2.8
	 *
	 * @param  string|null $plugin The plugin to check for network activation.
	 * @return bool                True if network activated, false if not.
	 */
	public function isPluginNetworkActivated( $plugin = null ) {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		if ( ! is_multisite() ) {
			return false;
		}

		$plugin = $plugin ? $plugin : plugin_basename( AIOSEO_FILE );

		// If the plugin is not network activated, then no it's not network licensed.
		if ( ! is_plugin_active_for_network( $plugin ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Returns the current site domain.
	 *
	 * @since 4.7.7
	 *
	 * @return string The site domain.
	 */
	public function getMultiSiteDomain() {
		$site = aioseo()->helpers->getSite();

		return $site->domain . $site->path;
	}
}Common/Traits/Helpers/DateTime.php000066600000012035151135505570013065 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Contains date/time specific helper methods.
 *
 * @since 4.1.2
 */
trait DateTime {
	/**
	 * Formats a date in ISO8601 format.
	 *
	 * @since 4.1.2
	 *
	 * @param  string $date The date.
	 * @return string       The date formatted in ISO8601 format.
	 */
	public function dateToIso8601( $date ) {
		return date( 'Y-m-d', strtotime( $date ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
	}

	/**
	 * Formats a date & time in ISO8601 format.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $dateTime The date.
	 * @return string           The date formatted in ISO8601 format.
	 */
	public function dateTimeToIso8601( $dateTime ) {
		return date( 'c', strtotime( $dateTime ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
	}

	/**
	 * Formats a date & time in RFC-822 format.
	 *
	 * @since 4.2.1
	 *
	 * @param  string $dateTime The date.
	 * @return string           The date formatted in RFC-822 format.
	 */
	public function dateTimeToRfc822( $dateTime ) {
		return date( 'D, d M Y H:i:s O', strtotime( $dateTime ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
	}

	/**
	 * Retrieves the timezone offset in seconds.
	 *
	 * @since   4.0.0
	 * @version 4.7.2 Returns the actual timezone offset.
	 *
	 * @return int The timezone offset in seconds.
	 */
	public function getTimeZoneOffset() {
		try {
			$timezone = get_option( 'timezone_string' );
			if ( $timezone ) {
				$timezone_object = new \DateTimeZone( $timezone ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName

				return $timezone_object->getOffset( new \DateTime( 'now' ) ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			}
		} catch ( \Exception $e ) {
			// Do nothing.
		}

		return intval( get_option( 'gmt_offset', 0 ) ) * HOUR_IN_SECONDS;
	}

	/**
	 * Formats an amount of days, hours and minutes in ISO8601 duration format.
	 * This is used in our JSON schema to adhere to Google's standards.
	 *
	 * @since 4.2.5
	 *
	 * @param  integer|string $days    The days.
	 * @param  integer|string $hours   The hours.
	 * @param  integer|string $minutes The minutes.
	 * @return string                  The days, hours and minutes formatted in ISO8601 duration format.
	 */
	public function timeToIso8601DurationFormat( $days, $hours, $minutes ) {
		$duration = 'P';
		if ( $days ) {
			$duration .= $days . 'D';
		}

		$duration .= 'T';
		if ( $hours ) {
			$duration .= $hours . 'H';
		}

		if ( $minutes ) {
			$duration .= $minutes . 'M';
		}

		return $duration;
	}

	/**
	 * Returns a MySQL formatted date.
	 *
	 * @since 4.1.5
	 *
	 * @param  int|string   $time Any format accepted by strtotime.
	 * @return false|string       The MySQL formatted string.
	 */
	public function timeToMysql( $time ) {
		$time = is_string( $time ) ? strtotime( $time ) : $time;

		return date( 'Y-m-d H:i:s', $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
	}

	/**
	 * Formats a date in WordPress format.
	 *
	 * @since 4.8.2
	 *
	 * @param  string      $dateTime          Same as you'd pass to `strtotime()`.
	 * @param  string      $dateTimeSeparator The separator between the date and time.
	 * @return string|null                    The date formatted in WordPress format. Null if the passed date is invalid.
	 */
	public function dateToWpFormat( $dateTime, $dateTimeSeparator = ', ' ) {
		static $format = null;
		if ( ! isset( $format ) ) {
			$dateFormat = get_option( 'date_format', 'd M' );
			$timeFormat = get_option( 'time_format', 'H:i' );
			$format     = $dateFormat . $dateTimeSeparator . $timeFormat;
		}

		$timestamp = strtotime( (string) $dateTime );

		return $timestamp && 0 < $timestamp ? date_i18n( $format, $timestamp ) : null;
	}

	/**
	 * Checks if a given string is a valid date.
	 *
	 * @since 4.8.3
	 *
	 * @param  string $date   The date string to check.
	 * @param  string $format The format of the date string.
	 * @return bool           True if the string is a valid date, false otherwise.
	 */
	public function isValidDate( $date, $format = null ) {
		if ( ! $date ) {
			return false;
		}

		if ( $format ) {
			$d = \DateTime::createFromFormat( $format, $date );

			return $d && $d->format( $format ) === $date;
		}

		$timestamp = strtotime( $date );

		return false !== $timestamp;
	}

	/**
	 * Generates a random (yet unique per identifier) time offset based on a site identifier.
	 *
	 * @since 4.7.9
	 *
	 * @param  string $identifier       Data such as the site URL, site ID, or a combination of both to serve as the seed for generating a random time offset.
	 * @param  int    $maxOffsetMinutes The range for the random offset in minutes.
	 * @return int                      The random (yet unique per identifier) time offset in minutes.
	 */
	public function generateRandomTimeOffset( $identifier, $maxOffsetMinutes ) {
		$hash = md5( strval( $identifier ) );

		// Convert part of the hash to an integer.
		$hashInteger = hexdec( substr( $hash, 0, 8 ) );

		return $hashInteger % $maxOffsetMinutes;
	}
}Common/Traits/Helpers/Buffer.php000066600000000605151135505570012602 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Contains buffer specific helper methods.
 *
 * @since 4.8.3
 */
trait Buffer {
	/**
	 * Clears all output buffers.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	public function clearBuffers() {
		while ( ob_get_level() > 0 ) {
			ob_end_clean();
		}
	}
}Common/Traits/Helpers/WpUri.php000066600000040107151135505570012440 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Contains all WordPress related URL, URI, path, slug, etc. related helper methods.
 *
 * @since 4.1.4
 */
trait WpUri {
	/**
	 * Returns the site domain.
	 *
	 * @since 4.0.0
	 *
	 * @param  bool   $unfiltered Whether to get the unfiltered value.
	 * @return string             The site's domain.
	 */
	public function getSiteDomain( $unfiltered = false ) {
		return wp_parse_url( $this->getHomeUrl( $unfiltered ), PHP_URL_HOST );
	}

	/**
	 * Returns the site URL.
	 * NOTE: For multisites inside a sub-directory, this returns the URL for the main site.
	 * This is intentional.
	 *
	 * @since 4.0.0
	 *
	 * @param  bool   $unfiltered Whether to get the unfiltered value.
	 * @return string             The site's domain.
	 */
	public function getSiteUrl( $unfiltered = false ) {
		$homeUrl = $this->getHomeUrl( $unfiltered );

		return wp_parse_url( $homeUrl, PHP_URL_SCHEME ) . '://' . wp_parse_url( $homeUrl, PHP_URL_HOST );
	}

	/**
	 * Returns the current URL.
	 *
	 * @since 4.0.0
	 *
	 * @param  boolean $canonical Whether or not to get the canonical URL.
	 * @return string             The URL.
	 */
	public function getUrl( $canonical = false ) {
		$url = '';
		if ( is_singular() ) {
			$objectId = aioseo()->helpers->getPostId();

			if ( $canonical ) {
				$url = aioseo()->helpers->wpGetCanonicalUrl( $objectId );
			}

			if ( ! $url ) {
				// wp_get_canonical_url() returns false if the post isn't published.
				// Therefore, we must to fall back to the permalink if the post isn't published, e.g. draft post or attachment (inherit).
				$url = get_permalink( $objectId );
			}
		}

		if ( $url ) {
			return $url;
		}

		global $wp;
		// Permalink url without the query string.
		$url = user_trailingslashit( home_url( $wp->request ) );

		// If permalinks are not being used we need to append the query string to the home url.
		if ( ! $this->usingPermalinks() ) {
			$url = home_url( ! empty( $wp->query_string ) ? '?' . $wp->query_string : '' );
		}

		return $url;
	}

	/**
	 * Gets the canonical URL for the current page/post.
	 *
	 * @since 4.0.0
	 *
	 * @return string $url The canonical URL.
	 */
	public function canonicalUrl() {
		$queriedObject = get_queried_object(); // Don't use our getTerm helper here.
		$hash          = md5( wp_json_encode( $queriedObject ?? [] ) );

		static $url = [];
		if ( isset( $url[ $hash ] ) ) {
			return $url[ $hash ];
		}

		if ( is_404() || is_search() ) {
			$url[ $hash ] = apply_filters( 'aioseo_canonical_url', '' );

			return $url[ $hash ];
		}

		$metaData = [];
		$post     = $this->getPost();
		if ( $post ) {
			$metaData = aioseo()->meta->metaData->getMetaData( $post );
		}

		if ( is_category() || is_tag() || is_tax() ) {
			$metaData     = aioseo()->meta->metaData->getMetaData( $queriedObject );
			$url[ $hash ] = get_term_link( $queriedObject, $queriedObject->taxonomy ?? '' );

			// If the term link is a WP_Error, set it to an empty string.
			if ( ! is_string( $url[ $hash ] ) ) {
				$url[ $hash ] = '';
			}

			// Add pagination to the URL. We need to do this here because get_term_link() doesn't handle pagination.
			// We'll strip it further down if no pagination for canonical is enabled.
			if ( $this->getPageNumber() > 1 ) {
				$url[ $hash ] = user_trailingslashit( rtrim( $url[ $hash ], '/' ) . '/page/' . $this->getPageNumber() );
			}
		}

		if ( $metaData && ! empty( $metaData->canonical_url ) ) {
			$url[ $hash ] = apply_filters( 'aioseo_canonical_url', $this->makeUrlAbsolute( $metaData->canonical_url ) );

			return $url[ $hash ];
		}

		if ( BuddyPressIntegration::isComponentPage() ) {
			$url[ $hash ] = aioseo()->standalone->buddyPress->component->getMeta( 'canonical' );
		}

		if ( empty( $url[ $hash ] ) || is_wp_error( $url[ $hash ] ) ) {
			$url[ $hash ] = $this->getUrl( true );
		}

		$pageNumber = $this->getPageNumber();
		if (
			in_array( 'noPaginationForCanonical', aioseo()->internalOptions->deprecatedOptions, true ) &&
			aioseo()->options->deprecated->searchAppearance->advanced->noPaginationForCanonical
		) {
			if ( 1 < $pageNumber ) {
				if ( $this->usingPermalinks() ) {
					// Replace /page/3 and /page/3/.
					$url[ $hash ] = preg_replace( "@(?<=/)page/$pageNumber(/|)$@", '', (string) $url[ $hash ] );
					// Replace /3 and /3/.
					$url[ $hash ] = preg_replace( "@(?<=/)$pageNumber(/|)$@", '', (string) $url[ $hash ] );
				} else {
					// Replace /?page_id=457&paged=1 and /?page_id=457&page=1.
					$url[ $hash ] = aioseo()->helpers->urlRemoveQueryParameter( $url[ $hash ], [ 'page', 'paged' ] );
				}
			}

			// Comment pages.
			$url[ $hash ] = preg_replace( '/(?<=\/)comment-page-\d+\/*(#comments)*$/', '', (string) $url[ $hash ] );
		}

		$url[ $hash ] = $this->maybeRemoveTrailingSlash( $url[ $hash ] );

		// Get rid of /amp at the end of the URL.
		if (
			aioseo()->helpers->isAmpPage() &&
			! apply_filters( 'aioseo_disable_canonical_url_amp', false )
		) {
			$url[ $hash ] = preg_replace( '/\/amp$/', '', (string) $url[ $hash ] );
			$url[ $hash ] = preg_replace( '/\/amp\/$/', '/', (string) $url[ $hash ] );
		}

		$url[ $hash ] = apply_filters( 'aioseo_canonical_url', $url[ $hash ] );

		return $url[ $hash ];
	}

	/**
	 * Sanitizes a given domain.
	 *
	 * @since 4.0.0
	 *
	 * @param  string       $domain The domain to sanitize.
	 * @return mixed|string         The sanitized domain.
	 */
	public function sanitizeDomain( $domain ) {
		$domain = trim( $domain );
		$domain = strtolower( $domain );
		if ( 0 === strpos( $domain, 'http://' ) ) {
			$domain = substr( $domain, 7 );
		} elseif ( 0 === strpos( $domain, 'https://' ) ) {
			$domain = substr( $domain, 8 );
		}
		$domain = untrailingslashit( $domain );

		return $domain;
	}

	/**
	 * Remove trailing slashes if not set in the permalink structure.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $url The original URL.
	 * @return string      The adjusted URL.
	 */
	public function maybeRemoveTrailingSlash( $url ) {
		$permalinks = get_option( 'permalink_structure' );
		if ( $permalinks && ( ! is_home() || ! is_front_page() ) ) {
			$trailing = substr( $permalinks, -1 );
			if ( '/' !== $trailing ) {
				$url = untrailingslashit( $url );
			}
		}

		// Don't slash urls with query args.
		if ( false !== strpos( $url, '?' ) ) {
			$url = untrailingslashit( $url );
		}

		return $url;
	}

	/**
	 * Removes image dimensions from the slug of a URL.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $url The image URL.
	 * @return string      The formatted image URL.
	 */
	public function removeImageDimensions( $url ) {
		return $this->isValidAttachment( $url ) ? preg_replace( '#(-[0-9]*x[0-9]*|-scaled)#', '', (string) $url ) : $url;
	}

	/**
	 * Returns the URL for the WP content folder.
	 *
	 * @since 4.0.5
	 *
	 * @return string The URL.
	 */
	public function getWpContentUrl() {
		$info = wp_get_upload_dir();

		return isset( $info['baseurl'] ) ? $info['baseurl'] : '';
	}

	/**
	* Retrieves a post by its given path.
	* Based on the built-in get_page_by_path() function, but only checks ancestry if the post type is actually hierarchical.
	*
	* @since 4.1.4
	*
	* @param  string       $path     The path.
	* @param  string       $output   The output type. OBJECT, ARRAY_A, or ARRAY_N.
	* @param  string|array $postType The post type(s) to check against.
	* @return object|false           The post or false on failure.
	*/
	public function getPostByPath( $path, $output = OBJECT, $postType = 'page' ) {
		$lastChanged = wp_cache_get_last_changed( 'aioseo_posts_by_path' );
		$hash        = md5( $path . serialize( $postType ) );
		$cacheKey    = "get_page_by_path:$hash:$lastChanged";
		$cached      = wp_cache_get( $cacheKey, 'aioseo_posts_by_path' );

		if ( false !== $cached ) {
			// Special case: '0' is a bad `$path`.
			if ( '0' === $cached || 0 === $cached ) {
				return false;
			}

			return get_post( $cached, $output );
		}

		$path          = rawurlencode( urldecode( $path ) );
		$path          = str_replace( '%2F', '/', $path );
		$path          = str_replace( '%20', ' ', $path );
		$parts         = explode( '/', trim( $path, '/' ) );
		$reversedParts = array_reverse( $parts );
		$postNames     = "'" . implode( "','", $parts ) . "'";

		$postTypes = is_array( $postType ) ? $postType : [ $postType, 'attachment' ];
		$postTypes = "'" . implode( "','", $postTypes ) . "'";

		$posts = aioseo()->core->db->start( 'posts' )
			->select( 'ID, post_name, post_parent, post_type' )
			->whereRaw( "post_name in ( $postNames )" )
			->whereRaw( "post_type in ( $postTypes )" )
			->run()
			->result();

		$foundId = 0;
		foreach ( $posts as $post ) {
			if ( $post->post_name === $reversedParts[0] ) {
				$count = 0;
				$p     = $post;

				// Loop through the given path parts from right to left, ensuring each matches the post ancestry.
				while ( 0 !== (int) $p->post_parent && isset( $posts[ $p->post_parent ] ) ) {
					$count++;
					$parent = $posts[ $p->post_parent ];
					if ( ! isset( $reversedParts[ $count ] ) || $parent->post_name !== $reversedParts[ $count ] ) {
						break;
					}
					$p = $parent;
				}

				if (
					0 === (int) $p->post_parent &&
					( ! is_post_type_hierarchical( $p->post_type ) || count( $reversedParts ) === $count + 1 ) &&
					$p->post_name === $reversedParts[ $count ]
				) {
					$foundId = $post->ID;
					if ( $post->post_type === $postType ) {
						break;
					}
				}
			}
		}

		// We cache misses as well as hits.
		wp_cache_set( $cacheKey, $foundId, 'aioseo_posts_by_path' );

		return $foundId ? get_post( $foundId, $output ) : false;
	}

	/**
	 * Validates a URL.
	 *
	 * @since 4.1.2
	 *
	 * @param  string $url The url.
	 * @return bool        Is it a valid/safe url.
	 */
	public function isUrl( $url ) {
		return esc_url_raw( $url ) === $url;
	}

	/**
	 * Retrieves the parameters for a given URL.
	 *
	 * @since 4.1.5
	 *
	 * @param  string $url          The url.
	 * @return array                The parameters.
	 */
	public function getParametersFromUrl( $url ) {
		$parsedUrl  = wp_parse_url( wp_unslash( $url ) );
		$parameters = [];

		if ( empty( $parsedUrl['query'] ) ) {
			return [];
		}

		wp_parse_str( $parsedUrl['query'], $parameters );

		return $parameters;
	}

	/**
	 * Adds a leading slash to an url.
	 *
	 * @since 4.1.8
	 *
	 * @param  string $url The url.
	 * @return string      The url with a leading slash.
	 */
	public function leadingSlashIt( $url ) {
		return '/' . ltrim( $url, '/' );
	}

	/**
	 * Returns the path from a permalink.
	 * This function will help get the correct path from WP installations in subfolders.
	 *
	 * @since 4.1.8
	 *
	 * @param  string $permalink A permalink from get_permalink().
	 * @return string            The path without the home_url().
	 */
	public function getPermalinkPath( $permalink ) {
		// We want to get this value straight from the DB to prevent plugins like WPML from filtering it.
		// This will otherwise mess with things like license activation requests and redirects.
		$homeUrl = $this->getHomeUrl( true );

		return $this->leadingSlashIt( str_replace( $homeUrl, '', $permalink ) );
	}

	/**
	 * Changed if permalinks are different and the before wasn't
	 * the site url (we don't want to redirect the site URL).
	 *
	 * @since 4.2.3
	 *
	 * @param  string  $before The URL before the change.
	 * @param  string  $after  The URL after the change.
	 * @return boolean         True if the permalink has changed.
	 */
	public function hasPermalinkChanged( $before, $after ) {
		// Check it's not redirecting from the root.
		if ( $this->getHomePath() === $before || '/' === $before ) {
			return false;
		}

		// Are the URLs the same?
		return ( $before !== $after );
	}

	/**
	 * Retrieve the home path.
	 *
	 * @since 4.2.3
	 *
	 * @param  bool   $unfiltered Whether to get the unfiltered value.
	 * @return string              The home path.
	 */
	public function getHomePath( $unfiltered = false ) {
		$path = wp_parse_url( $this->getHomeUrl( $unfiltered ), PHP_URL_PATH );

		return $path ? trailingslashit( $path ) : '/';
	}

	/**
	 * Returns the home URL.
	 *
	 * @since 4.7.3
	 *
	 * @param  bool   $unfiltered Whether to get the unfiltered value.
	 * @return string             The home URL.
	 */
	private function getHomeUrl( $unfiltered = false ) {
		$homeUrl = home_url();
		if ( $unfiltered ) {
			// We want to get this value straight from the DB to prevent plugins like WPML from filtering it.
			// This will otherwise mess with things like license activation requests and redirects.
			$homeUrl = get_option( 'home' );
		}

		return $homeUrl;
	}

	/**
	 * Checks if the given URL is an internal URL for the current site.
	 *
	 * @since 4.2.6
	 *
	 * @param  string $urlToCheck The URL to check.
	 * @return bool               Whether the given URL is an internal one.
	 */
	public function isInternalUrl( $urlToCheck ) {
		$parsedHomeUrl    = wp_parse_url( home_url() );
		$parsedUrlToCheck = wp_parse_url( $urlToCheck );

		return ! empty( $parsedHomeUrl['host'] ) && ! empty( $parsedUrlToCheck['host'] )
			? $parsedHomeUrl['host'] === $parsedUrlToCheck['host']
			: false;
	}

	/**
	 * Helper for the rest url.
	 *
	 * @since 4.4.9
	 *
	 * @return string
	 */
	public function getRestUrl() {
		$restUrl = get_rest_url();

		if ( aioseo()->helpers->isWpmlActive() ) {
			global $sitepress;

			// Replace the rest url 'all' language prefix so our rest calls don't fail.
			if (
				is_object( $sitepress ) &&
				method_exists( $sitepress, 'get_current_language' ) &&
				method_exists( $sitepress, 'get_default_language' ) &&
				'all' === $sitepress->get_current_language()
			) {
				$restUrl = str_replace(
					get_home_url( null, '/all/' ),
					get_home_url( null, '/' . $sitepress->get_default_language() . '/' ),
					$restUrl
				);
			}
		}

		return $restUrl;
	}

	/**
	 * Exclude the home path from a full path.
	 *
	 * @since   1.2.3 Moved from aioseo-redirects.
	 * @version 4.5.8
	 *
	 * @param  string $path The original path.
	 * @return string       The path without WP's home path.
	 */
	public function excludeHomePath( $path ) {
		return preg_replace( '@^' . $this->getHomePath() . '@', '/', (string) $path );
	}

	/**
	 * Get the canonical URL for a post.
	 * This is a duplicate of wp_get_canonical_url() with a fix for issue #6372 where
	 * posts with paginated comment pages return the wrong canonical URL due to how WordPress sets the cpage var.
	 * We can remove this once trac ticket 60806 is resolved.
	 *
	 * @since 4.6.9
	 *
	 * @param  \WP_Post|int|null $post The post object or ID.
	 * @return string|false            The post's canonical URL, or false if the post is not published.
	 */
	public function wpGetCanonicalUrl( $post = null ) {
		$post = get_post( $post );

		if ( ! $post ) {
			return false;
		}

		if ( 'publish' !== $post->post_status ) {
			return false;
		}

		$canonical_url = get_permalink( $post ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		// If a canonical is being generated for the current page, make sure it has pagination if needed.
		if ( get_queried_object_id() === $post->ID ) {
			$page = get_query_var( 'page', 0 );
			if ( $page >= 2 ) {
				if ( ! get_option( 'permalink_structure' ) ) {
					$canonical_url = add_query_arg( 'page', $page, $canonical_url ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				} else {
					$canonical_url = trailingslashit( $canonical_url ) . user_trailingslashit( $page, 'single_paged' ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				}
			}

			$cpage = aioseo()->helpers->getCommentPageNumber(); // We're calling our own function here to get the correct cpage number.
			if ( $cpage ) {
				$canonical_url = get_comments_pagenum_link( $cpage ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			}
		}

		return apply_filters( 'get_canonical_url', $canonical_url, $post ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
	}

	/**
	 * Checks if permalinks are enabled.
	 *
	 * @since 4.8.3
	 *
	 * @return bool Whether permalinks are enabled.
	 */
	public function usingPermalinks() {
		global $wp_rewrite; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		return $wp_rewrite->using_permalinks();  // phpcs:ignore Squiz.NamingConventions.ValidVariableName
	}
}Common/Traits/Helpers/Request.php000066600000003440151135505570013021 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Parse the current request.
 *
 * @since 4.2.1
 */
trait Request {
	/**
	 * Get the server port.
	 *
	 * @since 4.2.1
	 *
	 * @return string The server port.
	 */
	private function getServerPort() {
		if (
			empty( $_SERVER['SERVER_PORT'] ) ||
			80 === (int) $_SERVER['SERVER_PORT'] ||
			443 === (int) $_SERVER['SERVER_PORT']
		) {
			return '';
		}

		return ':' . (int) $_SERVER['SERVER_PORT'];
	}

	/**
	 * Get the protocol.
	 *
	 * @since 4.2.1
	 *
	 * @return string The protocol.
	 */
	private function getProtocol() {
		return is_ssl() ? 'https' : 'http';
	}

	/**
	 * Get the server name (from $_SERVER['SERVER_NAME]), or use the request name ($_SERVER['HTTP_HOST']) if not present.
	 *
	 * @since 4.2.1
	 *
	 * @return string The server name.
	 */
	private function getServerName() {
		$host = $this->getRequestServerName();

		if ( isset( $_SERVER['SERVER_NAME'] ) ) {
			$host = sanitize_text_field( wp_unslash( $_SERVER['SERVER_NAME'] ) ); // phpcs:ignore HM.Security.ValidatedSanitizedInput.InputNotSanitized
		}

		return $host;
	}

	/**
	 * Get the request server name (from $_SERVER['HTTP_HOST]).
	 *
	 * @since 4.2.1
	 *
	 * @return string The request server name.
	 */
	private function getRequestServerName() {
		$host = '';

		if ( isset( $_SERVER['HTTP_HOST'] ) ) {
			$host = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) );
		}

		return $host;
	}

	/**
	 * Retrieve the request URL.
	 *
	 * @since 4.2.1
	 *
	 * @return string The request URL.
	 */
	public function getRequestUrl() {
		$url = '';

		if ( isset( $_SERVER['REQUEST_URI'] ) ) {
			$url = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
		}

		return rawurldecode( $url );
	}
}Common/Traits/Helpers/Url.php000066600000022265151135505570012141 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits\Helpers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Contains URL helper methods.
 *
 * @since 4.2.5
 */
trait Url {
	/**
	 * Removes a query string parameter from a URL.
	 *
	 * @since 4.2.5
	 *
	 * @param  string $url        The url.
	 * @param  array  $parameters The parameter keys to remove.
	 * @return string             The url without the parameters removed.
	 */
	public function urlRemoveQueryParameter( $url, $parameters ) {
		$url = wp_parse_url( $url );
		if ( ! empty( $url['query'] ) ) {
			// Take the query string apart.
			parse_str( $url['query'], $queryStringArray );

			// Remove parameters.
			foreach ( $parameters as $parameter ) {
				if ( isset( $queryStringArray[ $parameter ] ) ) {
					unset( $queryStringArray[ $parameter ] );
				}
			}

			// Rebuild the query string.
			$url['query'] = build_query( $queryStringArray );

			// Rebuild the URL from parse_url.
			$url = $this->buildUrl( $url );
		}

		return $url;
	}

	/**
	 * Builds a URL from a parse_url array.
	 *
	 * @since 4.2.5
	 *
	 * @param  array  $params  The params array.
	 * @param  array  $include The keys to include [scheme, user, pass, host, port, path, query, fragment].
	 * @param  array  $exclude The keys to exclude [scheme, user, pass, host, port, path, query, fragment].
	 * @return string          The built url.
	 */
	public function buildUrl( $params, $include = [], $exclude = [] ) {
		if ( ! is_array( $params ) ) {
			return $params;
		}

		if ( ! empty( $include ) ) {
			foreach ( array_keys( $params ) as $includeKey ) {
				if ( ! in_array( $includeKey, $include, true ) ) {
					unset( $params[ $includeKey ] );
				}
			}
		}

		if ( ! empty( $exclude ) ) {
			foreach ( array_keys( $params ) as $excludeKey ) {
				if ( in_array( $excludeKey, $exclude, true ) ) {
					unset( $params[ $excludeKey ] );
				}
			}
		}

		$url = '';
		if ( ! empty( $params['scheme'] ) ) {
			$url .= $params['scheme'] . '://';
		}
		if ( ! empty( $params['user'] ) ) {
			$url .= $params['user'];

			if ( isset( $params['pass'] ) ) {
				$url .= ':' . $params['pass'];
			}

			$url .= '@';
		}

		if ( ! empty( $params['host'] ) ) {
			$url .= $params['host'];
		}

		if ( ! empty( $params['port'] ) ) {
			$url .= ':' . $params['port'];
		}

		if ( ! empty( $params['path'] ) ) {
			$url .= $params['path'];
		}

		if ( ! empty( $params['query'] ) ) {
			$url .= '?' . $params['query'];
		}

		if ( ! empty( $params['fragment'] ) ) {
			$url .= '#' . $params['fragment'];
		}

		return $url;
	}

	/**
	 * Checks if a URL is considered a local one.
	 *
	 * @since 4.5.9
	 *
	 * @param  string $url The URL.
	 * @return bool        Whether the URL is a local one or not.
	 */
	public function isLocalUrl( $url ) {
		$domain = wp_parse_url( $url, PHP_URL_HOST );
		if ( empty( $domain ) ) {
			return false;
		}

		if (
			false !== ip2long( $domain ) &&
			! filter_var( $domain, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE )
		) {
			return true;
		}

		if ( 'localhost' === $domain ) {
			return true;
		}

		if ( ! $this->isValidDomain( $domain ) ) {
			return true;
		}

		$tldsToCheck = [
			'.local',
			'.test',
		];

		foreach ( $tldsToCheck as $tld ) {
			if ( false !== strpos( $this->getTld( $domain ), $tld ) ) {
				return true;
			}
		}

		if ( substr_count( $domain, '.' ) > 1 ) {
			$subdomainsToCheck = [
				'dev',
				'development',
				'staging',
				'stage',
				'test',
				'staging*',
				'*staging',
				'dev*',
				'*dev',
				'test*',
				'*test'
			];

			foreach ( $subdomainsToCheck as $subdomain ) {
				foreach ( $this->getSubdomains( $domain ) as $sd ) {

					$subdomain = str_replace( '.', '(.)', $subdomain );
					$subdomain = str_replace( [ '*', '(.)' ], '(.*)', $subdomain );

					if ( preg_match( '/^(' . $subdomain . ')$/', (string) $sd ) ) {
						return true;
					}
				}
			}
		}

		return false;
	}

	/**
	 * Checks if a domain is valid.
	 *
	 * @since 4.5.9
	 *
	 * @param  string $domain The domain.
	 * @return bool           Whether the domain is valid or not.
	 */
	private function isValidDomain( $domain ) {
		// In case there are unicode characters, convert it into
		// IDNA ASCII URLs
		if ( function_exists( 'idn_to_ascii' ) ) {
			$domain = idn_to_ascii( $domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 );
		}

		if ( ! $domain ) {
			return false;
		}

		$domain = preg_replace( '/^\*\.+/', '', (string) $domain );

		return preg_match( '/^(?!\-)(?:[a-z\d\-]{0,62}[a-z\d]\.){1,126}(?!\d+)[a-z\d]{1,63}$/i', (string) $domain );
	}

	/**
	 * Checks if a domain is valid and optionally contains paths at the end.
	 *
	 * @since 4.7.7
	 *
	 * @param  string $domain The domain.
	 * @return bool           Whether the domain is valid or not.
	 */
	private function isDomainWithPaths( $domain ) {
		// In case there are unicode characters, convert it into IDNA ASCII URLs.
		if ( function_exists( 'idn_to_ascii' ) ) {
			$domain = idn_to_ascii( $domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 );
		}

		if ( ! $domain ) {
			return false;
		}

		$domain = preg_replace( '/^\*\.+/', '', $domain );

		return preg_match( '/^(?!\-)(?:[a-z\d\-]{0,62}[a-z\d]\.){1,126}(?!\d+)[a-z\d]{1,63}(\/[a-z\d\-\/]*)?$/i', $domain );
	}

	/**
	 * Returns a single string of all subdomains associated with this domain.
	 * Example 1: www
	 * Example 2: ww2.www
	 *
	 * @since 4.5.9
	 *
	 * @return array The subdomains associated with this domain.
	 */
	public function getSubdomains( $domain ) {
		// If we can't find a TLD, we won't be able to parse a subdomain.
		if ( empty( $this->getTld( $domain ) ) ) {
			return [];
		}

		// Return any subdomains as an array.
		return array_filter( explode( '.', rtrim( strstr( $domain, $this->getTld( $domain ), true ), '.' ) ) );
	}

	/**
	 * Returns the TLD associated with the given domain.
	 *
	 * @since 4.5.9
	 *
	 * @param  string $domain The domain.
	 * @return string         The TLD.
	 */
	public function getTld( $domain ) {
		if ( preg_match( '/(?P<tld>[a-z0-9][a-z0-9\-]{1,63}\.[a-z\.]{2,6})$/i', (string) $domain, $matches ) ) {
			return $matches['tld'];
		}

		return $domain;
	}

	/**
	 * Returns a decoded URL string.
	 *
	 * @since 4.6.7
	 *
	 * @param  string $url The URL string.
	 * @return string      The decoded URL.
	 */
	public function decodeUrl( $url ) {
		// Ensure input is a string to prevent errors.
		if ( ! is_string( $url ) ) {
			return $url;
		}

		// Set a reasonable iteration limit to prevent infinite loops.
		$maxIterations = 10;
		$iterations    = 0;

		$decodedUrl = rawurldecode( $url );
		while ( $decodedUrl !== $url && $iterations < $maxIterations ) {
			$url        = $decodedUrl;
			$decodedUrl = rawurldecode( $url );
			$iterations++;
		}

		return $decodedUrl;
	}

	/**
	 * Redirects to a specific URL.
	 *
	 * @since 4.8.0
	 *
	 * @param string $url    The URL to redirect to.
	 * @param int    $status The status code to use.
	 * @param string $reason The reason for redirecting.
	 *
	 * @return void
	 */
	public function redirect( $url, $status = 301, $reason = '' ) {
		$redirectBy = 'AIOSEO';
		if ( ! empty( $reason ) ) {
			$redirectBy .= ': ' . $reason;
		}

		wp_safe_redirect( $url, $status, $redirectBy );
		exit;
	}

	/**
	 * Checks if the given URL is external.
	 *
	 * @since 4.8.3
	 *
	 * @param  string $url The URL to check.
	 * @return bool        Whether the URL is external or not.
	 */
	public function isExternalUrl( $url ) {
		$parsedUrl = wp_parse_url( $url );
		if ( ! $parsedUrl ) {
			return false;
		}

		static $parsedSiteUrl = null;
		if ( ! $parsedSiteUrl ) {
			$parsedSiteUrl = wp_parse_url( site_url() );
		}

		return $parsedSiteUrl['host'] !== $parsedUrl['host'];
	}

	/**
	 * Checks if the given URL is relative.
	 *
	 * @since 4.8.3
	 *
	 * @param  string $url The URL to check.
	 * @return bool        Whether the URL is relative or not.
	 */
	public function isRelativeUrl( $url ) {
		$parsedUrl = wp_parse_url( $url );
		if ( ! $parsedUrl ) {
			return false;
		}

		return empty( $parsedUrl['scheme'] ) && empty( $parsedUrl['host'] );
	}

	/**
	 * Makes the given URL relative.
	 *
	 * @since 4.8.3
	 *
	 * @param  string $url The URL to make relative.
	 * @return string      The relative URL.
	 */
	public function makeUrlRelative( $url ) {
		$parsedUrl = wp_parse_url( $url );
		if ( ! $parsedUrl ) {
			return $url;
		}

		static $parsedSiteUrl = null;
		if ( ! $parsedSiteUrl ) {
			$parsedSiteUrl = wp_parse_url( site_url() );
		}

		if ( $parsedSiteUrl['host'] !== $parsedUrl['host'] ) {
			return $url;
		}

		return ! empty( $parsedUrl['path'] ) ? $parsedUrl['path'] : $url;
	}

	/**
	 * Formats a given URL as an absolute URL if it is relative.
	 *
	 * @since   4.0.0
	 * @version 4.8.3 Moved from WpUri trait to Url trait.
	 *
	 * @param  string $url The URL.
	 * @return string      The absolute URL.
	 */
	public function makeUrlAbsolute( $url ) {
		if ( 0 !== strpos( $url, 'http' ) && '/' !== $url ) {
			$url = $this->sanitizeDomain( $url );
			if ( $this->isDomainWithPaths( $url ) ) {
				$scheme = wp_parse_url( site_url(), PHP_URL_SCHEME );
				$url    = $scheme . '://' . $url;
			} elseif ( 0 === strpos( $url, '//' ) ) {
				$scheme = wp_parse_url( site_url(), PHP_URL_SCHEME );
				$url    = $scheme . ':' . $url;
			} else {
				$url = site_url( $url );
			}
		}

		return $url;
	}
}Common/Traits/Options.php000066600000067142151135505570011433 0ustar00<?php
namespace AIOSEO\Plugin\Common\Traits;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Options trait.
 *
 * @since 4.0.0
 */
trait Options {
	/**
	 * Whether or not this instance is a clone.
	 *
	 * @since 4.1.4
	 *
	 * @var boolean
	 */
	public $isClone = false;

	/**
	 * Whether or not the options need to be saved to the DB.
	 *
	 * @since 4.1.4
	 *
	 * @var string
	 */
	public $shouldSave = false;

	/**
	 * The name to lookup the options with.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	public $optionsName = '';

	/**
	 * Holds the localized options.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $localized = [];

	/**
	 * The group key we are working with.
	 *
	 * @since 4.0.0
	 *
	 * @var string|null
	 */
	protected $groupKey = null;

	/**
	 * Allows us to create unlimited number of sub groups.
	 * Like so: options->breadcrumbs->templates->taxonomies->tags->template
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $subGroups = [];

	/**
	 * Any arguments associated with a dynamic method.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $arguments = [];

	/**
	 * The value to set on an option.
	 *
	 * @since 4.0.0
	 *
	 * @var mixed
	 */
	protected $value = null;

	/**
	 * Holds all the defaults after they have been merged.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $defaultsMerged = [];

	/**
	 * Holds a redirect link or slug.
	 *
	 * @since 4.0.17
	 *
	 * @var string
	 */
	protected $screenRedirection = '';

	/**
	 * Retrieve an option or null if missing.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name      The name of the property that is missing on the class.
	 * @param  array  $arguments The arguments passed into the method.
	 * @return mixed             The value from the options or default/null.
	 */
	public function __call( $name, $arguments = [] ) {
		if ( $this->setGroupKey( $name, $arguments ) ) {
			return $this;
		}

		// If we need to set a sub-group, do that now.
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$defaults      = $cachedOptions[ $this->groupKey ];
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				$defaults = $defaults[ $subGroup ];
			}
		}

		if ( ! isset( $defaults[ $name ] ) ) {
			$this->resetGroups();

			return ! empty( $this->arguments[0] )
				? $this->arguments[0]
				: $this->getDefault( $name, false );
		}

		if ( empty( $defaults[ $name ]['type'] ) ) {
			return $this->setSubGroup( $name );
		}

		$value = isset( $cachedOptions[ $this->groupKey ][ $name ]['value'] )
			? $cachedOptions[ $this->groupKey ][ $name ]['value']
			: (
				! empty( $this->arguments[0] )
					? $this->arguments[0]
					: $this->getDefault( $name, false )
			);

		$this->resetGroups();

		return $value;
	}

	/**
	 * Retrieve an option or null if missing.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name The name of the property that is missing on the class.
	 * @return mixed        The value from the options or default/null.
	 */
	public function __get( $name ) {
		if ( 'type' === $name ) {
			$name = '_aioseo_type';
		}

		if ( $this->setGroupKey( $name ) ) {
			return $this;
		}

		// If we need to set a sub-group, do that now.
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$defaults      = $cachedOptions[ $this->groupKey ];
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				$defaults = $defaults[ $subGroup ];
			}
		}

		if ( ! isset( $defaults[ $name ] ) ) {
			$default = $this->getDefault( $name, false );
			$this->resetGroups();

			return $default;
		}

		if ( ! isset( $defaults[ $name ]['type'] ) ) {
			return $this->setSubGroup( $name );
		}

		$value = $this->getDefault( $name, false );

		if ( isset( $defaults[ $name ]['value'] ) ) {
			$preserveHtml = ! empty( $defaults[ $name ]['preserveHtml'] );
			if ( $preserveHtml ) {
				if ( is_array( $defaults[ $name ]['value'] ) ) {
					foreach ( $defaults[ $name ]['value'] as $k => $v ) {
						$defaults[ $name ]['value'][ $k ] = html_entity_decode( $v, ENT_NOQUOTES );
					}
				} else {
					$defaults[ $name ]['value'] = html_entity_decode( $defaults[ $name ]['value'], ENT_NOQUOTES );
				}
			}
			$value = $defaults[ $name ]['value'];

			// Localized value.
			if ( isset( $defaults[ $name ]['localized'] ) ) {
				$localizedKey = $this->groupKey;
				if ( ! empty( $this->subGroups ) ) {
					foreach ( $this->subGroups as $subGroup ) {
						$localizedKey .= '_' . $subGroup;
					}
				}

				$localizedKey .= '_' . $name;

				if ( ! empty( $this->localized[ $localizedKey ] ) ) {
					$value = $this->localized[ $localizedKey ];
					// We need to rebuild the keywords as a json string.
					if ( 'keywords' === $name ) {
						$keywords = explode( ',', $value );
						foreach ( $keywords as $k => $keyword ) {
							$keywords[ $k ] = [
								'label' => $keyword,
								'value' => $keyword
							];
						}

						$value = wp_json_encode( $keywords );
					}
				}
			}
		}

		$this->resetGroups();

		return $value;
	}

	/**
	 * Sets the option value and saves to the database.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name  The name of the option.
	 * @param  mixed  $value The value to set.
	 * @return void
	 */
	public function __set( $name, $value ) {
		if ( $this->setGroupKey( $name, null, $value ) ) {
			return $this;
		}

		// If we need to set a sub-group, do that now.
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$defaults      = json_decode( wp_json_encode( $cachedOptions[ $this->groupKey ] ), true );
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				$defaults = &$defaults[ $subGroup ];
			}
		}

		if ( ! isset( $defaults[ $name ] ) ) {
			$default = $this->getDefault( $name, false );
			$this->resetGroups();

			return $default;
		}

		if ( empty( $defaults[ $name ]['type'] ) ) {
			return $this->setSubGroup( $name );
		}

		$preserveHtml               = ! empty( $defaults[ $name ]['preserveHtml'] );
		$localized                  = ! empty( $defaults[ $name ]['localized'] );
		$defaults[ $name ]['value'] = $this->sanitizeField( $this->value, $defaults[ $name ]['type'], $preserveHtml );

		if ( $localized ) {
			$localizedKey = $this->groupKey;
			if ( ! empty( $this->subGroups ) ) {
				foreach ( $this->subGroups as $subGroup ) {
					$localizedKey .= '_' . $subGroup;
				}
			}

			$localizedKey  .= '_' . $name;
			$localizedValue = $defaults[ $name ]['value'];

			if ( 'keywords' === $name ) {
				$keywords = json_decode( $localizedValue ) ? json_decode( $localizedValue ) : [];
				foreach ( $keywords as $k => $keyword ) {
					$keywords[ $k ] = $keyword->value;
				}

				$localizedValue = implode( ',', $keywords );
			}

			$this->localized[ $localizedKey ] = $localizedValue;
			update_option( $this->optionsName . '_localized', $this->localized );
		}

		$originalDefaults = json_decode( wp_json_encode( $cachedOptions[ $this->groupKey ] ), true );
		$pointer          = &$originalDefaults; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		foreach ( $this->subGroups as $subGroup ) {
			$pointer = &$pointer[ $subGroup ];
		}
		$pointer = $defaults;

		$cachedOptions[ $this->groupKey ] = $originalDefaults;
		aioseo()->core->optionsCache->setOptions( $this->optionsName, $cachedOptions );

		$this->resetGroups();

		$this->update();
	}

	/**
	 * Checks if an option is set or returns null if not.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name The name of the option.
	 * @return mixed        True or null.
	 */
	public function __isset( $name ) {
		if ( $this->setGroupKey( $name ) ) {
			return $this;
		}

		// If we need to set a sub-group, do that now.
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$defaults      = $cachedOptions[ $this->groupKey ];
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				$defaults = &$defaults[ $subGroup ];
			}
		}

		if ( ! isset( $defaults[ $name ] ) ) {
			$this->resetGroups();

			return false;
		}

		if ( empty( $defaults[ $name ]['type'] ) ) {
			return $this->setSubGroup( $name );
		}

		$value = isset( $defaults[ $name ]['value'] )
			? false === empty( $defaults[ $name ]['value'] )
			: false;

			$this->resetGroups();

		return $value;
	}

	/**
	 * Unsets the option value and saves to the database.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name  The name of the option.
	 * @return void
	 */
	public function __unset( $name ) {
		if ( $this->setGroupKey( $name ) ) {
			return $this;
		}

		// If we need to set a sub-group, do that now.
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$defaults      = json_decode( wp_json_encode( $cachedOptions[ $this->groupKey ] ), true );
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				$defaults = &$defaults[ $subGroup ];
			}
		}

		if ( ! isset( $defaults[ $name ] ) ) {
			$this->groupKey  = null;
			$this->subGroups = [];

			return;
		}

		if ( empty( $defaults[ $name ]['type'] ) ) {
			return $this->setSubGroup( $name );
		}

		if ( ! isset( $defaults[ $name ]['value'] ) ) {
			return;
		}

		unset( $defaults[ $name ]['value'] );

		$cachedOptions[ $this->groupKey ] = $defaults;
		aioseo()->core->optionsCache->setOptions( $this->optionsName, $cachedOptions );

		$this->resetGroups();

		$this->update();
	}

	/**
	 * Retrieves all options.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $include Keys to include.
	 * @param  array $exclude Keys to exclude.
	 * @return array          An array of options.
	 */
	public function all( $include = [], $exclude = [] ) {
		$originalGroupKey  = $this->groupKey;
		$originalSubGroups = $this->subGroups;

		// Make sure our dynamic options have loaded.
		$this->init();

		// Refactor options.
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$refactored    = $this->convertOptionsToValues( $cachedOptions );

		$this->groupKey = null;

		if ( ! $originalGroupKey ) {
			return $this->allFiltered( $refactored, $include, $exclude );
		}

		if ( empty( $originalSubGroups ) ) {
			$all = $refactored[ $originalGroupKey ];

			return $this->allFiltered( $all, $include, $exclude );
		}

		$returnable = &$refactored[ $originalGroupKey ]; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		foreach ( $originalSubGroups as $subGroup ) {
			$returnable = &$returnable[ $subGroup ];
		}

		$this->resetGroups();

		return $this->allFiltered( $returnable, $include, $exclude );
	}

	/**
	 * Reset the current option to the defaults.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $include Keys to include.
	 * @param  array $exclude Keys to exclude.
	 * @return void
	 */
	public function reset( $include = [], $exclude = [] ) {
		$originalGroupKey  = $this->groupKey;
		$originalSubGroups = $this->subGroups;

		// Make sure our dynamic options have loaded.
		$this->init();

		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );

		// If we don't have a group key set, it means we want to reset everything.
		if ( empty( $originalGroupKey ) ) {
			$groupKeys = array_keys( $cachedOptions );
			foreach ( $groupKeys as $groupKey ) {
				$this->groupKey = $groupKey;
				$this->reset();
			}

			// Since we just finished resetting everything, we can return early.
			return;
		}

		// If we need to set a sub-group, do that now.
		$keys     = array_merge( [ $originalGroupKey ], $originalSubGroups );
		$defaults = json_decode( wp_json_encode( $cachedOptions[ $originalGroupKey ] ), true );
		if ( ! empty( $originalSubGroups ) ) {
			foreach ( $originalSubGroups as $subGroup ) {
				$defaults = $defaults[ $subGroup ];
			}
		}

		// Refactor options.
		$resetValues = $this->resetValues( $defaults, $this->defaultsMerged, $keys, $include, $exclude );
		// We need to call our helper method instead of the built-in array_replace_recursive() function here because we want values to be replaced with empty arrays.
		$defaults = aioseo()->helpers->arrayReplaceRecursive( $defaults, $resetValues );

		$originalDefaults = json_decode( wp_json_encode( $cachedOptions[ $originalGroupKey ] ), true );
		$pointer          = &$originalDefaults; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		foreach ( $originalSubGroups as $subGroup ) {
			$pointer = &$pointer[ $subGroup ];
		}
		$pointer = $defaults;

		$cachedOptions[ $originalGroupKey ] = $originalDefaults;
		aioseo()->core->optionsCache->setOptions( $this->optionsName, $cachedOptions );

		$this->resetGroups();

		$this->update();
	}

	/**
	 * Resets all values in a group.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $defaults The defaults array we are currently working with.
	 * @param  array $values   The values to adjust.
	 * @param  array $keys     Parent keys for the current group we are parsing.
	 * @param  array $include  Keys to include.
	 * @param  array $exclude  Keys to exclude.
	 * @return array           The modified values.
	 */
	protected function resetValues( $values, $defaults, $keys = [], $include = [], $exclude = [] ) {
		$values = $this->allFiltered( $values, $include, $exclude );
		foreach ( $values as $key => $value ) {
			$option = $this->isAnOption( $key, $defaults, $keys );
			if ( $option ) {
				$values[ $key ]['value'] = isset( $values[ $key ]['default'] ) ? $values[ $key ]['default'] : null;
				continue;
			}

			$keys[]         = $key;
			$values[ $key ] = $this->resetValues( $value, $defaults, $keys );
			array_pop( $keys );
		}

		return $values;
	}

	/**
	 * Checks if the current group has an option or group.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $optionOrGroup The option or group to look for.
	 * @param  bool   $resetGroups   Whether or not to reset the groups after.
	 * @return bool                  True if it does, false if not.
	 */
	public function has( $optionOrGroup = '', $resetGroups = true ) {
		if ( 'type' === $optionOrGroup ) {
			$optionOrGroup = '_aioseo_type';
		}

		$originalGroupKey  = $this->groupKey;
		$originalSubGroups = $this->subGroups;

		static $hasInitialized = false;
		if ( ! $hasInitialized ) {
			$hasInitialized = true;
			$this->init();
		}

		// If we need to set a sub-group, do that now.
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$defaults      = $originalGroupKey ? $cachedOptions[ $originalGroupKey ] : $cachedOptions;
		if ( ! empty( $originalSubGroups ) ) {
			foreach ( $originalSubGroups as $subGroup ) {
				$defaults = $defaults[ $subGroup ];
			}
		}

		if ( $resetGroups ) {
			$this->resetGroups();
		}

		if ( ! empty( $defaults[ $optionOrGroup ] ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Filters the results based on passed in array.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $all     All the options to filter.
	 * @param  array $include Keys to include.
	 * @param  array $exclude Keys to exclude.
	 * @return array          The filtered options.
	 */
	private function allFiltered( $all, $include, $exclude ) {
		if ( ! empty( $include ) ) {
			return array_intersect_ukey( $all, $include, function ( $key1, $key2 ) use ( $include ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
				if ( in_array( $key1, $include, true ) ) {
					return 0;
				}

				return -1;
			} );
		}

		if ( ! empty( $exclude ) ) {
			return array_diff_ukey( $all, $exclude, function ( $key1, $key2 ) use ( $exclude ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
				if ( ! in_array( $key1, $exclude, true ) ) {
					return 0;
				}

				return -1;
			} );
		}

		return $all;
	}

	/**
	 * Gets the default value for an option.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name The option name.
	 * @return mixed        The default value.
	 */
	public function getDefault( $name, $resetGroups = true ) {
		$defaults = $this->defaultsMerged[ $this->groupKey ];
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				if ( empty( $defaults[ $subGroup ] ) ) {
					return null;
				}
				$defaults = $defaults[ $subGroup ];
			}
		}

		if ( $resetGroups ) {
			$this->resetGroups();
		}

		if ( ! isset( $defaults[ $name ] ) ) {
			return null;
		}

		if ( empty( $defaults[ $name ]['type'] ) ) {
			return $this->setSubGroup( $name );
		}

		return isset( $defaults[ $name ]['default'] )
			? $defaults[ $name ]['default']
			: null;
	}

	/**
	 * Gets the defaults options.
	 *
	 * @since 4.1.3
	 *
	 * @return array An array of dafults.
	 */
	public function getDefaults() {
		return $this->defaults;
	}

	/**
	 * Updates the options in the database.
	 *
	 * @since 4.0.0
	 *
	 * @param  string     $optionsName An optional option name to update.
	 * @param  string     $defaults    The defaults to filter the options by.
	 * @param  array|null $options     An optional options array.
	 * @return void
	 */
	public function update( $optionsName = null, $defaults = null, $options = null ) {
		$optionsName = empty( $optionsName ) ? $this->optionsName : $optionsName;
		$defaults    = empty( $defaults ) ? $this->defaults : $defaults;

		// First, we need to filter our options.
		$options = $this->filterOptions( $defaults, $options );

		// Refactor options.
		$refactored = $this->convertOptionsToValues( $options );

		$this->resetGroups();

		// The following needs to happen here (possibly a clone) as well as in the main instance.
		$originalInstance = $this->getOriginalInstance();

		// Update the DB options.
		aioseo()->core->optionsCache->setDb( $optionsName, $refactored );

		// Force a save here and in the main class.
		$this->shouldSave             = true;
		$originalInstance->shouldSave = true;
	}

	/**
	 * Updates the options in the database.
	 *
	 * @since 4.1.4
	 *
	 * @param  boolean $force       Whether or not to force an immediate save.
	 * @param  string  $optionsName An optional option name to update.
	 * @param  string  $defaults    The defaults to filter the options by.
	 * @return void
	 */
	public function save( $force = false, $optionsName = null, $defaults = null ) {
		if ( ! $this->shouldSave && ! $force ) {
			return;
		}

		$optionsName = empty( $optionsName ) ? $this->optionsName : $optionsName;
		$defaults    = empty( $defaults ) ? $this->defaults : $defaults;

		$this->update( $optionsName );

		// First, we need to filter our options.
		$options = $this->filterOptions( $defaults );

		// Refactor options.
		$refactored = $this->convertOptionsToValues( $options );

		$this->resetGroups();

		update_option( $optionsName, wp_json_encode( $refactored ) );
	}

	/**
	 * Filter options to match our defaults.
	 *
	 * @since 4.0.0
	 *
	 * @param  array      $defaults The defaults to use in filtering.
	 * @param  array|null $options  An optional options array.
	 * @return array                An array of filtered options.
	 */
	public function filterOptions( $defaults, $options = null ) {
		$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
		$options       = ! empty( $options ) ? $options : json_decode( wp_json_encode( $cachedOptions ), true );

		return $this->filterRecursively( $options, $defaults );
	}

	/**
	 * Filters options in a loop.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $options  An array of options to filter.
	 * @param  array $defaults An array of defaults to filter against.
	 * @return array           A filtered array of options.
	 */
	public function filterRecursively( $options, $defaults ) {
		if ( ! is_array( $options ) ) {
			return $options;
		}

		foreach ( $options as $key => $value ) {
			if ( ! isset( $defaults[ $key ] ) ) {
				unset( $options[ $key ] );
				continue;
			}

			if ( ! isset( $value['type'] ) ) {
				$options[ $key ] = $this->filterRecursively( $options[ $key ], $defaults[ $key ] );
				continue;
			}
		}

		return $options;
	}

	/**
	 * Sanitizes the value before allowing it to be saved.
	 *
	 * @since 4.0.0
	 *
	 * @param  mixed  $value The value to sanitize.
	 * @param  string $type  The type of sanitization to do.
	 * @return mixed         The sanitized value.
	 */
	public function sanitizeField( $value, $type, $preserveHtml = false ) {
		switch ( $type ) {
			case 'boolean':
				return (bool) $value;
			case 'html':
				return sanitize_textarea_field( $value );
			case 'string':
				return sanitize_text_field( $value );
			case 'number':
				return intval( $value );
			case 'array':
				$array = [];
				foreach ( (array) $value as $k => $v ) {
					if ( is_array( $v ) ) {
						$array[ $k ] = $this->sanitizeField( $v, 'array' );
						continue;
					}

					$array[ $k ] = sanitize_text_field( $preserveHtml ? htmlspecialchars( $v, ENT_NOQUOTES, 'UTF-8' ) : $v );
				}

				return $array;
			case 'float':
				return floatval( $value );
		}
	}

	/**
	 * Checks to see if we need to set the group key. If so, will return true.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $name      The name of the option to set.
	 * @param  array   $arguments Any arguments needed if this was a method called.
	 * @param  mixed   $value     The value if we are setting an option.
	 * @return boolean            Whether or not we need to set the group key.
	 */
	private function setGroupKey( $name, $arguments = null, $value = null ) {
		$this->arguments = $arguments;
		$this->value     = $value;

		if ( empty( $this->groupKey ) ) {
			$groups = array_keys( $this->defaultsMerged );
			if ( in_array( $name, $groups, true ) ) {
				$this->groupKey = $name;

				return true;
			}

			$this->groupKey = $groups[0];
		}

		return false;
	}

	/**
	 * Sets the sub group key. Will set and return the instance.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $name      The name of the option to set.
	 * @param  array   $arguments Any arguments needed if this was a method called.
	 * @param  mixed   $value     The value if we are setting an option.
	 * @return object             The options object.
	 */
	private function setSubGroup( $name, $arguments = null, $value = null ) {
		if ( ! is_null( $arguments ) ) {
			$this->arguments = $arguments;
		}
		if ( ! is_null( $value ) ) {
			$this->value = $value;
		}

		$defaults = $this->defaultsMerged[ $this->groupKey ];
		if ( ! empty( $this->subGroups ) ) {
			foreach ( $this->subGroups as $subGroup ) {
				$defaults = $defaults[ $subGroup ];
			}
		}

		$groups = array_keys( $defaults );
		if ( in_array( $name, $groups, true ) ) {
			$this->subGroups[] = $name;
		}

		return $this;
	}

	/**
	 * Reset groups.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function resetGroups() {
		$this->groupKey  = null;
		$this->subGroups = [];
	}

	/**
	 * Converts an associative array of values into a structure
	 * that works with our defaults.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $defaults The defaults array we are currently working with.
	 * @param  array $values   The values to adjust.
	 * @param  array $keys     Parent keys for the current group we are parsing.
	 * @param  bool  $sanitize Whether or not we should sanitize the value.
	 * @return array           The modified values.
	 */
	protected function addValueToValuesArray( $defaults, $values, $keys = [], $sanitize = false ) {
		foreach ( $values as $key => $value ) {
			$option = $this->isAnOption( $key, $defaults, $keys );
			if ( $option ) {
				$preserveHtml   = ! empty( $option['preserveHtml'] );
				$newValue       = $sanitize ? $this->sanitizeField( $value, $option['type'], $preserveHtml ) : $value;
				$values[ $key ] = [
					'value' => $newValue
				];

				// If this is a localized string, let's save it to our localized options.
				if ( $sanitize && ! empty( $option['localized'] ) ) {
					$localizedKey = '';
					foreach ( $keys as $k ) {
						$localizedKey .= $k . '_';
					}

					$localizedKey  .= $key;
					$localizedValue = $newValue;
					if ( 'keywords' === $key ) {
						$keywords = json_decode( $localizedValue ) ? json_decode( $localizedValue ) : [];
						foreach ( $keywords as $k => $keyword ) {
							$keywords[ $k ] = $keyword->value;
						}

						$localizedValue = implode( ',', $keywords );
					}

					$this->localized[ $localizedKey ] = $localizedValue;
				}
				continue;
			}

			if ( ! is_array( $value ) ) {
				continue;
			}

			$keys[]         = $key;
			$values[ $key ] = $this->addValueToValuesArray( $defaults, $value, $keys, $sanitize );
			array_pop( $keys );
		}

		return $values;
	}

	/**
	 * Our options array has values (or defaults).
	 * This method converts them to how we would store them
	 * in the DB.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $options The options array.
	 * @return array           The converted options array.
	 */
	public function convertOptionsToValues( $options, $optionKey = 'type' ) {
		foreach ( $options as $key => $value ) {
			if ( ! is_array( $value ) ) {
				continue;
			}

			if ( ! isset( $value[ $optionKey ] ) ) {
				$options[ $key ] = $this->convertOptionsToValues( $value, $optionKey );
				continue;
			}

			$options[ $key ] = null;

			if ( isset( $value['value'] ) ) {
				$preserveHtml = ! empty( $value['preserveHtml'] );
				if ( $preserveHtml ) {
					if ( is_array( $value['value'] ) ) {
						foreach ( $value['value'] as $k => $v ) {
							$value['value'][ $k ] = html_entity_decode( $v, ENT_NOQUOTES );
						}
					} else {
						$value['value'] = html_entity_decode( $value['value'], ENT_NOQUOTES );
					}
				}
				$options[ $key ] = $value['value'];
				continue;
			}

			if ( isset( $value['default'] ) ) {
				$options[ $key ] = $value['default'];
			}
		}

		return $options;
	}

	/**
	 * This checks to see if the current array/option is really an option
	 * and not just another parent with a subgroup.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $key      The current array key we are working with.
	 * @param  array  $defaults The defaults array to check against.
	 * @param  array  $keys     The parent keys to loop through.
	 * @return bool             Whether or not this is an option.
	 */
	private function isAnOption( $key, $defaults, $keys ) {
		if ( ! empty( $keys ) ) {
			foreach ( $keys as $k ) {
				$defaults = isset( $defaults[ $k ] ) ? $defaults[ $k ] : [];
			}
		}

		if ( isset( $defaults[ $key ]['type'] ) ) {
			return $defaults[ $key ];
		}

		return false;
	}

	/**
	 * Refreshes the options from the database.
	 *
	 * We need this during the migration to update through clones.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function refresh() {
		// Reset DB options to clear the cache.
		aioseo()->core->optionsCache->resetDb();
		$this->init();
	}

	/**
	 * Returns the DB options.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $optionsName The options name.
	 * @return array               The options.
	 */
	public function getDbOptions( $optionsName ) {
		$cache = aioseo()->core->optionsCache->getDb( $optionsName );
		if ( empty( $cache ) ) {
			$options = json_decode( get_option( $optionsName ), true );
			$options = ! empty( $options ) ? $options : [];

			// Set the cache.
			aioseo()->core->optionsCache->setDb( $optionsName, $options );
		}

		return aioseo()->core->optionsCache->getDb( $optionsName );
	}

	/**
	 * In order to not have a conflict, we need to return a clone.
	 *
	 * @since 4.0.0
	 *
	 * @param  bool   $reInitialize Whether to reinitialize on the clone.
	 * @return object               The cloned Options object.
	 */
	public function noConflict( $reInitialize = false ) {
		$class          = clone $this;
		$class->isClone = true;

		if ( $reInitialize ) {
			$class->init();
		}

		return $class;
	}

	/**
	 * Get original instance. Since this could be a cloned object, let's get the original instance.
	 *
	 * @since 4.1.4
	 *
	 * @return self
	 */
	public function getOriginalInstance() {
		if ( ! $this->isClone ) {
			return $this;
		}

		$class      = new \ReflectionClass( get_called_class() );
		$optionName = aioseo()->helpers->toCamelCase( $class->getShortName() );

		if ( isset( aioseo()->{ $optionName } ) ) {
			return aioseo()->{ $optionName };
		}

		return $this;
	}
}Common/Core/Core.php000066600000005345151135505570010307 0ustar00<?php
namespace AIOSEO\Plugin\Common\Core;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Options;
use AIOSEO\Plugin\Common\Utils;

/**
 * Loads core classes.
 *
 * @since 4.1.9
 */
class Core {
	/**
	 * List of AIOSEO tables.
	 *
	 * @since 4.2.5
	 *
	 * @var array
	 */
	private $aioseoTables = [
		'aioseo_cache',
		'aioseo_crawl_cleanup_blocked_args',
		'aioseo_crawl_cleanup_logs',
		'aioseo_links',
		'aioseo_links_suggestions',
		'aioseo_notifications',
		'aioseo_posts',
		'aioseo_redirects',
		'aioseo_redirects_404',
		'aioseo_redirects_404_logs',
		'aioseo_redirects_hits',
		'aioseo_redirects_logs',
		'aioseo_terms',
		'aioseo_search_statistics_objects',
		'aioseo_revisions',
		'aioseo_seo_analyzer_results'
	];

	/**
	 * Filesystem class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Utils\Filesystem
	 */
	public $fs = null;

	/**
	 * Assets class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Utils\Assets
	 */
	public $assets = null;

	/**
	 * DB class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Utils\Database
	 */
	public $db = null;

	/**
	 * Cache class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Utils\Cache
	 */
	public $cache = null;

	/**
	 * NetworkCache class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Utils\NetworkCache
	 */
	public $networkCache = null;

	/**
	 * CachePrune class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Utils\CachePrune
	 */
	public $cachePrune = null;

	/**
	 * Options Cache class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Options\Cache
	 */
	public $optionsCache = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.1.9
	 */
	public function __construct() {
		$this->fs           = new Utils\Filesystem( $this );
		$this->assets       = new Utils\Assets( $this );
		$this->db           = new Utils\Database();
		$this->cache        = new Utils\Cache();
		$this->networkCache = new Utils\NetworkCache();
		$this->cachePrune   = new Utils\CachePrune();
		$this->optionsCache = new Options\Cache();
	}

	/**
	 * Get all the DB tables with prefix.
	 *
	 * @since 4.2.5
	 *
	 * @return array An array of tables.
	 */
	public function getDbTables() {
		global $wpdb;

		$tables = [];
		foreach ( $this->aioseoTables as $tableName ) {
			$tables[] = $wpdb->prefix . $tableName;
		}

		return $tables;
	}

	/**
	 * Check if the current request is uninstalling (deleting) AIOSEO.
	 *
	 * @since 4.3.7
	 *
	 * @return bool Whether AIOSEO is being uninstalled/deleted or not.
	 */
	public function isUninstalling() {
		if (
			defined( 'AIOSEO_FILE' ) &&
			defined( 'WP_UNINSTALL_PLUGIN' )
		) {
			// Make sure `plugin_basename()` exists.
			include_once ABSPATH . 'wp-admin/includes/plugin.php';

			return WP_UNINSTALL_PLUGIN === plugin_basename( AIOSEO_FILE );
		}

		return false;
	}
}Common/Breadcrumbs/Frontend.php000066600000021055151135505570012533 0ustar00<?php
namespace AIOSEO\Plugin\Common\Breadcrumbs;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Class Frontend.
 *
 * @since 4.1.1
 */
class Frontend {
	/**
	 * A local 'cached' crumb array.
	 *
	 * @since 4.1.1
	 *
	 * @var array
	 */
	public $breadcrumbs = [];

	/**
	 * Gets the current page's breadcrumbs.
	 *
	 * @since 4.1.1
	 *
	 * @return array
	 */
	public function getBreadcrumbs() {
		if ( ! empty( $this->breadcrumbs ) ) {
			return apply_filters( 'aioseo_breadcrumbs_trail', $this->breadcrumbs );
		}

		$reference = get_queried_object();
		$type      = '';
		if ( BuddyPressIntegration::isComponentPage() ) {
			$type = 'buddypress';
		}

		if ( ! $type ) {
			// These types need the queried object for reference.
			if ( is_object( $reference ) ) {
				if ( is_single() ) {
					$type = 'single';
				}

				if ( is_singular( 'post' ) ) {
					$type = 'post';
				}

				if ( is_page() && ! is_front_page() ) {
					$type = 'page';
				}

				if ( is_category() || is_tag() ) {
					$type = 'category';
				}

				if ( is_tax() ) {
					$type = 'taxonomy';
				}

				if ( is_post_type_archive() ) {
					$type = 'postTypeArchive';
				}

				if ( is_author() ) {
					$type = 'author';
				}

				if ( is_home() ) {
					$type = 'blog';
				}

				// Support WC shop page.
				if ( aioseo()->helpers->isWooCommerceShopPage() ) {
					$type = 'wcShop';
				}

				// Support WC products.
				if ( aioseo()->helpers->isWooCommerceProductPage() ) {
					$type = 'wcProduct';
				}
			}

			if ( is_date() ) {
				$type      = 'date';
				$reference = [
					'year'  => get_query_var( 'year' ),
					'month' => get_query_var( 'monthnum' ),
					'day'   => get_query_var( 'day' )
				];
			}

			if ( is_search() ) {
				$type      = 'search';
				$reference = htmlspecialchars( sanitize_text_field( get_search_query() ) );
			}

			if ( is_404() ) {
				$type = 'notFound';
			}
		}

		$paged = false;
		if ( is_paged() || ( is_singular() && 1 < get_query_var( 'page' ) ) ) {
			global $wp;
			$paged = [
				'paged' => get_query_var( 'paged' ) ? get_query_var( 'paged' ) : get_query_var( 'page' ),
				'link'  => home_url( $wp->request )
			];
		}

		return apply_filters( 'aioseo_breadcrumbs_trail', aioseo()->breadcrumbs->buildBreadcrumbs( $type, $reference, $paged ) );
	}

	/**
	 * Helper function to display breadcrumbs for a specific page.
	 *
	 * @since 4.1.1
	 *
	 * @param  bool        $echo      Print out the breadcrumb.
	 * @param  string      $type      The type for the breadcrumb.
	 * @param  string      $reference A reference to be used for rendering the breadcrumb.
	 * @return string|void            A html breadcrumb.
	 */
	public function sideDisplay( $echo = true, $type = '', $reference = '' ) {
		// Save previously built breadcrumbs.
		$previousCrumbs = $this->breadcrumbs;

		// Build and run the sideDisplay.
		$this->breadcrumbs = aioseo()->breadcrumbs->buildBreadcrumbs( $type, $reference );
		$sideDisplay       = $this->display( $echo );

		// Restore previously built breadcrumbs.
		$this->breadcrumbs = $previousCrumbs;

		return $sideDisplay;
	}

	/**
	 * Display a generic breadcrumb preview.
	 *
	 * @since 4.1.5
	 *
	 * @param  bool        $echo  Print out the breadcrumb.
	 * @param  string      $label The preview crumb label.
	 * @return string|void        A html breadcrumb.
	 */
	public function preview( $echo = true, $label = '' ) {
		// Translators: "Crumb" refers to a part of the breadcrumb trail.
		$label = empty( $label ) ? __( 'Sample Crumb', 'all-in-one-seo-pack' ) : $label;

		return $this->sideDisplay( $echo, 'preview', $label );
	}

	/**
	 * Display the breadcrumb in the frontend.
	 *
	 * @since 4.1.1
	 *
	 * @param  bool        $echo Print out the breadcrumb.
	 * @return string|void       A html breadcrumb.
	 */
	public function display( $echo = true ) {
		if (
			in_array( 'breadcrumbsEnable', aioseo()->internalOptions->deprecatedOptions, true ) &&
			! aioseo()->options->deprecated->breadcrumbs->enable
		) {
			return;
		}

		if ( ! apply_filters( 'aioseo_breadcrumbs_output', true ) ) {
			return;
		}

		// We can only run after this action because we need all post types loaded.
		if ( ! did_action( 'init' ) ) {
			return;
		}

		$breadcrumbs = $this->getBreadcrumbs();
		if ( empty( $breadcrumbs ) ) {
			return;
		}

		$breadcrumbsCount = count( $breadcrumbs );

		$display = '<div class="aioseo-breadcrumbs">';
		foreach ( $breadcrumbs as $breadcrumb ) {
			--$breadcrumbsCount;

			$breadcrumbDisplay = $this->breadcrumbToDisplay( $breadcrumb );

			// Strip link from Last crumb.
			if (
				0 === $breadcrumbsCount &&
				aioseo()->breadcrumbs->showCurrentItem() &&
				! $this->linkCurrentItem() &&
				'default' === $breadcrumbDisplay['templateType']
			) {
				$breadcrumbDisplay['template'] = $this->stripLink( $breadcrumbDisplay['template'] );
			}

			$display .= $breadcrumbDisplay['template'];

			if ( 0 < $breadcrumbsCount ) {
				$display .= $this->getSeparator();
			}
		}
		$display .= '</div>';

		// Final security cleaning.
		$display = wp_kses_post( $display );

		if ( $echo ) {
			echo $display; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}

		return $display;
	}

	/**
	 * Turns a crumb array into a rendered html crumb.
	 *
	 * @since 4.1.1
	 *
	 * @param  array       $item The crumb array.
	 * @return string|void       The crumb html.
	 */
	protected function breadcrumbToDisplay( $item ) {
		$templateItem = $this->getCrumbTemplate( $item );
		if ( empty( $templateItem['template'] ) ) {
			return;
		}

		// Do tags.
		$templateItem['template'] = aioseo()->breadcrumbs->tags->replaceTags( $templateItem['template'], $item );
		$templateItem['template'] = preg_replace_callback(
			'/>(?![^<]*>)(?![^>]*")([^<]*?)>/',
			function ( $matches ) {
				return '>' . $matches[1] . '>';
			},
			htmlentities( $templateItem['template'] )
		);

		// Restore html.
		$templateItem['template'] = aioseo()->helpers->decodeHtmlEntities( $templateItem['template'] );

		// Remove html link if it comes back from the template but we passed no links to it.
		if ( empty( $item['link'] ) ) {
			$templateItem['template'] = $this->stripLink( $templateItem['template'] );
		}

		// Allow shortcodes to run in the final html.
		$templateItem['template'] = do_shortcode( $templateItem['template'] );

		return $templateItem;
	}

	/**
	 * Helper function to get a crumb's template.
	 *
	 * @since 4.1.1
	 *
	 * @param  array $crumb The crumb array.
	 * @return string       The html template.
	 */
	protected function getTemplate( $crumb ) {
		return $this->getDefaultTemplate( $crumb );
	}

	/**
	 * Helper function to get a crumb's template.
	 *
	 * @since 4.1.1
	 *
	 * @param  array $crumb The crumb array.
	 * @return array        The template type and html.
	 */
	protected function getCrumbTemplate( $crumb ) {
		return [
			'templateType' => 'default',
			'template'     => $this->getTemplate( $crumb )
		];
	}

	/**
	 * Default html template.
	 *
	 * @since 4.1.1
	 *
	 * @param  string $type      The crumb's type.
	 * @param  mixed  $reference The crumb's reference.
	 * @return string            The default crumb template.
	 */
	public function getDefaultTemplate( $type = '', $reference = '' ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return <<<TEMPLATE
<span class="aioseo-breadcrumb">
	<a href="#breadcrumb_link" title="#breadcrumb_label">#breadcrumb_label</a>
</span>
TEMPLATE;
	}

	/**
	 * Helper function to strip a html link from the crumb.
	 *
	 * @since 4.1.1
	 *
	 * @param  string $html The crumb's html.
	 * @return string       A crumb html without links.
	 */
	public function stripLink( $html ) {
		return preg_replace( '/<a\s.*?>|<\/a>/is', '', (string) $html );
	}

	/**
	 * Get the breadcrumb configured separator.
	 *
	 * @since 4.1.1
	 *
	 * @return string The separator html.
	 */
	public function getSeparator() {
		$separator = aioseo()->options->breadcrumbs->separator;

		$separatorToOverride = aioseo()->breadcrumbs->getOverride( 'separator' );
		if ( ! empty( $separatorToOverride ) ) {
			$separator = $separatorToOverride;
		}

		$separator = apply_filters( 'aioseo_breadcrumbs_separator_symbol', $separator );

		return apply_filters( 'aioseo_breadcrumbs_separator', '<span class="aioseo-breadcrumb-separator">' . esc_html( $separator ) . '</span>' );
	}

	/**
	 * Function to filter the linkCurrentItem option.
	 *
	 * @since 4.1.3
	 *
	 * @return bool Link current item.
	 */
	public function linkCurrentItem() {
		return apply_filters( 'aioseo_breadcrumbs_link_current_item', aioseo()->options->breadcrumbs->linkCurrentItem );
	}
}Common/Breadcrumbs/Shortcode.php000066600000001037151135505570012704 0ustar00<?php
namespace AIOSEO\Plugin\Common\Breadcrumbs;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class Shortcode.
 *
 * @since 4.1.1
 */
class Shortcode {
	/**
	 * Shortcode constructor.
	 *
	 * @since 4.1.1
	 */
	public function __construct() {
		add_shortcode( 'aioseo_breadcrumbs', [ $this, 'display' ] );
	}

	/**
	 * Shortcode callback.
	 *
	 * @since 4.1.1
	 *
	 * @return string|void The breadcrumb html.
	 */
	public function display() {
		return aioseo()->breadcrumbs->frontend->display( false );
	}
}Common/Breadcrumbs/Block.php000066600000012762151135505570012013 0ustar00<?php
namespace AIOSEO\Plugin\Common\Breadcrumbs;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Breadcrumb Block.
 *
 * @since 4.1.1
 */
class Block {
	/**
	 * The primary term list.
	 *
	 * @since 4.3.6
	 *
	 * @var array
	 */
	private $primaryTerm = [];

	/**
	 * The breadcrumb settings.
	 *
	 * @since 4.8.3
	 *
	 * @var array
	 */
	private $breadcrumbSettings = [
		'default'            => true,
		'separator'          => '›',
		'showHomeCrumb'      => true,
		'showTaxonomyCrumbs' => true,
		'showParentCrumbs'   => true,
		'parentTemplate'     => 'default',
		'template'           => 'default',
		'taxonomy'           => ''
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.1
	 */
	public function __construct() {
		$this->register();
	}

	/**
	 * Registers the block.
	 *
	 * @since 4.1.1
	 *
	 * @return void
	 */
	public function register() {
		aioseo()->blocks->registerBlock(
			'aioseo/breadcrumbs', [
				'attributes'      => [
					'primaryTerm'        => [
						'type'    => 'string',
						'default' => null
					],
					'breadcrumbSettings' => [
						'type'    => 'object',
						'default' => $this->breadcrumbSettings
					]
				],
				'render_callback' => [ $this, 'render' ]
			]
		);
	}

	/**
	 * Renders the block.
	 *
	 * @since 4.1.1
	 *
	 * @param  array  $blockAttributes The block attributes.
	 * @return string                  The output from the output buffering.
	 */
	public function render( $blockAttributes ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		// phpcs:disable HM.Security.ValidatedSanitizedInput.InputNotSanitized, HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		$postId = ! empty( $_GET['post_id'] ) ? (int) sanitize_text_field( wp_unslash( $_GET['post_id'] ) ) : false;
		// phpcs:enable

		if ( ! empty( $blockAttributes['primaryTerm'] ) ) {
			$this->primaryTerm = json_decode( $blockAttributes['primaryTerm'], true );
		}

		if ( ! empty( $blockAttributes['breadcrumbSettings'] ) ) {
			$this->breadcrumbSettings = $blockAttributes['breadcrumbSettings'];
		}

		aioseo()->breadcrumbs->setOverride( $this->getBlockOverrides() );

		if ( aioseo()->blocks->isRenderingBlockInEditor() && ! empty( $postId ) ) {
			add_filter( 'get_object_terms', [ $this, 'temporarilyAddTerm' ], 10, 3 );
			$breadcrumbs = aioseo()->breadcrumbs->frontend->sideDisplay( false, 'post' === get_post_type( $postId ) ? 'post' : 'single', get_post( $postId ) );
			remove_filter( 'get_object_terms', [ $this, 'temporarilyAddTerm' ], 10 );

			if (
				in_array( 'breadcrumbsEnable', aioseo()->internalOptions->deprecatedOptions, true ) &&
				! aioseo()->options->deprecated->breadcrumbs->enable
			) {
				return '<p>' .
						sprintf(
							// Translators: 1 - The plugin short name ("AIOSEO"), 2 - Opening HTML link tag, 3 - Closing HTML link tag.
							__( 'Breadcrumbs are currently disabled, so this block will be rendered empty. You can enable %1$s\'s breadcrumb functionality under %2$sGeneral Settings > Breadcrumbs%3$s.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
							AIOSEO_PLUGIN_SHORT_NAME,
							'<a href="' . esc_url( admin_url( 'admin.php?page=aioseo-settings#/breadcrumbs' ) ) . '" target="_blank">',
							'</a>'
						) .
						'</p>';
			}

			return $breadcrumbs;
		}

		return aioseo()->breadcrumbs->frontend->display( false );
	}

	/**
	 * Temporarily adds the primary term to the list of terms.
	 *
	 * @since 4.3.6
	 *
	 * @param  array  $terms      The list of terms.
	 * @param  array  $objectIds  The object IDs.
	 * @param  array  $taxonomies The taxonomies.
	 * @return array              The list of terms.
	 */
	public function temporarilyAddTerm( $terms, $objectIds, $taxonomies ) {
		$taxonomy = $taxonomies[0];
		if ( empty( $this->primaryTerm ) || empty( $this->primaryTerm[ $taxonomy ] ) ) {
			return $terms;
		}

		$term = aioseo()->helpers->getTerm( $this->primaryTerm[ $taxonomy ] );
		if ( is_a( $term, 'WP_Term' ) ) {
			$terms[] = $term;
		}

		return $terms;
	}

	/**
	 * Get the block overrides.
	 *
	 * @since 4.8.3
	 *
	 * @return array
	 */
	private function getBlockOverrides() {
		$default = filter_var( $this->breadcrumbSettings['default'], FILTER_VALIDATE_BOOLEAN );
		if ( true === $default || ! aioseo()->pro ) {
			return [];
		}

		return [
			'default'            => false,
			'taxonomy'           => $this->breadcrumbSettings['taxonomy'] ?? '',
			'separator'          => $this->breadcrumbSettings['separator'] ?? '›',
			'showHomeCrumb'      => filter_var( $this->breadcrumbSettings['showHomeCrumb'], FILTER_VALIDATE_BOOLEAN ),
			'showTaxonomyCrumbs' => filter_var( $this->breadcrumbSettings['showTaxonomyCrumbs'], FILTER_VALIDATE_BOOLEAN ),
			'showParentCrumbs'   => filter_var( $this->breadcrumbSettings['showParentCrumbs'], FILTER_VALIDATE_BOOLEAN ),
			'template'           => empty( $this->breadcrumbSettings['template'] ) ? '' : [
				'templateType' => 'custom',
				'template'     => aioseo()->helpers->decodeHtmlEntities( aioseo()->helpers->encodeOutputHtml( $this->breadcrumbSettings['template'] ) )
			],
			'parentTemplate'     => empty( $this->breadcrumbSettings['parentTemplate'] ) ? '' : [
				'templateType' => 'custom',
				'template'     => aioseo()->helpers->decodeHtmlEntities( aioseo()->helpers->encodeOutputHtml( $this->breadcrumbSettings['parentTemplate'] ) )
			],
			'primaryTerm'        => ! empty( $this->primaryTerm[ $this->breadcrumbSettings['taxonomy'] ] ) ? $this->primaryTerm[ $this->breadcrumbSettings['taxonomy'] ] : null
		];
	}
}Common/Breadcrumbs/Breadcrumbs.php000066600000053445151135505570013215 0ustar00<?php
namespace AIOSEO\Plugin\Common\Breadcrumbs {
	// Exit if accessed directly.
	if ( ! defined( 'ABSPATH' ) ) {
		exit;
	}

	/**
	 * Class Breadcrumbs.
	 *
	 * @since 4.1.1
	 */
	class Breadcrumbs {
		/** Instance of the frontend class.
		 *
		 * @since 4.1.1
		 *
		 * @var \AIOSEO\Plugin\Common\Breadcrumbs\Frontend|\AIOSEO\Plugin\Pro\Breadcrumbs\Frontend
		 */
		public $frontend;

		/**
		 * Instance of the shortcode class.
		 *
		 * @since 4.1.1
		 *
		 * @var Shortcode
		 */
		public $shortcode;

		/**
		 * Instance of the block class.
		 *
		 * @since 4.1.1
		 *
		 * @var Block
		 */
		public $block;

		/**
		 * Instance of the tags class.
		 *
		 * @since 4.1.1
		 *
		 * @var Tags
		 */
		public $tags;

		/**
		 * Array of crumbs.
		 *
		 * @since 4.1.1
		 *
		 * @var array An array of crumbs.
		 */
		public $breadcrumbs;

		/**
		 * Array of options to override.
		 *
		 * @since 4.8.3
		 *
		 * @var array An array of options to override.
		 */
		protected $override = [];

		/**
		 * Breadcrumbs constructor.
		 *
		 * @since 4.1.1
		 */
		public function __construct() {
			$this->frontend  = new Frontend();
			$this->shortcode = new Shortcode();
			$this->block     = new Block();

			add_action( 'widgets_init', [ $this, 'registerWidget' ] );

			// Init Tags class later as we need post types registered.
			add_action( 'init', [ $this, 'init' ], 50 );
		}

		public function init() {
			$this->tags = new Tags();
		}

		/**
		 * Helper to add crumbs on the breadcrumb array.
		 *
		 * @since 4.1.1
		 *
		 * @param  array $crumbs A single crumb or an array of crumbs.
		 * @return void
		 */
		public function addCrumbs( $crumbs ) {
			if ( empty( $crumbs ) || ! is_array( $crumbs ) ) {
				return;
			}

			// If it's a single crumb put it inside an array to merge.
			if ( isset( $crumbs['label'] ) ) {
				$crumbs = [ $crumbs ];
			}

			$this->breadcrumbs = array_merge( $this->breadcrumbs, $crumbs );
		}

		/**
		 * Builds a crumb array based on a type and a reference.
		 *
		 * @since 4.1.1
		 *
		 * @param  string $type      The type of breadcrumb ( post, single, page, category, tag, taxonomy, postTypeArchive, date,
		 *                           author, search, notFound, blog ).
		 * @param  mixed  $reference The reference can be an object ( WP_Post | WP_Term | WP_Post_Type | WP_User ), an array, an int or a string.
		 * @param  array  $paged     A reference for a paged crumb.
		 * @return array             An array of breadcrumbs with their label, link, type and reference.
		 */
		public function buildBreadcrumbs( $type, $reference, $paged = [] ) {
			// Clear the breadcrumb array and build a new one.
			$this->breadcrumbs = [];

			// Add breadcrumb prefix.
			$this->addCrumbs( $this->getPrefixCrumb( $type, $reference ) );

			// Set a home page in the beginning of the breadcrumb.
			$this->addCrumbs( $this->maybeGetHomePageCrumb( $type, $reference ) );

			// Woocommerce shop page support.
			$this->addCrumbs( $this->maybeGetWooCommerceShopCrumb() );

			// Blog home.
			if (
				aioseo()->options->breadcrumbs->showBlogHome &&
				in_array( $type, [ 'category', 'tag', 'post', 'author', 'date' ], true )
			) {
				$this->addCrumbs( $this->getBlogCrumb() );
			}

			switch ( $type ) {
				case 'post':
				case 'single':
					$this->addCrumbs( $this->getPostArchiveCrumb( $reference ) );
					$this->addCrumbs( $this->getPostTaxonomyCrumbs( $reference ) );
					$this->addCrumbs( $this->getPostParentCrumbs( $reference ) );
					$this->addCrumbs( $this->getPostCrumb( $reference ) );
					break;
				case 'page':
					$this->addCrumbs( $this->getPostParentCrumbs( $reference, 'page' ) );
					$this->addCrumbs( $this->getPostCrumb( $reference, 'page' ) );
					break;
				case 'category':
				case 'tag':
				case 'taxonomy':
					$this->addCrumbs( $this->getTermTaxonomyParentCrumbs( $reference ) );
					$this->addCrumbs( $this->getTermTaxonomyCrumb( $reference ) );
					break;
				case 'postTypeArchive':
					$this->addCrumbs( $this->getPostTypeArchiveCrumb( $reference ) );
					break;
				case 'date':
					$this->addCrumbs( $this->getDateCrumb( $reference ) );
					break;
				case 'author':
					$this->addCrumbs( $this->getAuthorCrumb( $reference ) );
					break;
				case 'blog':
					$this->addCrumbs( $this->getBlogCrumb() );
					break;
				case 'search':
					$this->addCrumbs( $this->getSearchCrumb( $reference ) );
					break;
				case 'notFound':
					$this->addCrumbs( $this->getNotFoundCrumb() );
					break;
				case 'preview':
					$this->addCrumbs( $this->getPreviewCrumb( $reference ) );
					break;
				case 'wcProduct':
					$this->addCrumbs( $this->getPostTaxonomyCrumbs( $reference ) );
					$this->addCrumbs( $this->getPostParentCrumbs( $reference ) );
					$this->addCrumbs( $this->getPostCrumb( $reference ) );
					break;
				case 'buddypress':
					$this->addCrumbs( aioseo()->standalone->buddyPress->component->getCrumbs() );
					break;
			}

			// Paged crumb.
			if ( ! empty( $paged['paged'] ) ) {
				$this->addCrumbs( $this->getPagedCrumb( $paged ) );
			}

			// Maybe remove the last crumb.
			if ( ! $this->showCurrentItem( $type, $reference ) ) {
				array_pop( $this->breadcrumbs );
			}

			// Remove empty crumbs.
			$this->breadcrumbs = array_filter( $this->breadcrumbs );

			return $this->breadcrumbs;
		}

		/**
		 * Gets the prefix crumb.
		 *
		 * @since 4.1.1
		 *
		 * @param  string $type      The type of breadcrumb.
		 * @param  mixed  $reference The breadcrumb reference.
		 * @return array             A crumb.
		 */
		public function getPrefixCrumb( $type, $reference ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
			if ( 0 === strlen( aioseo()->options->breadcrumbs->breadcrumbPrefix ) ) {
				return [];
			}

			return $this->makeCrumb( aioseo()->options->breadcrumbs->breadcrumbPrefix, '', 'prefix' );
		}

		/**
		 * Gets the 404 crumb.
		 *
		 * @since 4.1.1
		 *
		 * @return array A crumb.
		 */
		public function getNotFoundCrumb() {
			return $this->makeCrumb( aioseo()->options->breadcrumbs->errorFormat404, '', 'notFound' );
		}

		/**
		 * Gets the search crumb.
		 *
		 * @since 4.1.1
		 *
		 * @param  string $searchQuery The search query for reference.
		 * @return array               A crumb.
		 */
		public function getSearchCrumb( $searchQuery ) {
			return $this->makeCrumb( aioseo()->options->breadcrumbs->searchResultFormat, get_search_link( $searchQuery ), 'search', $searchQuery );
		}

		/**
		 * Gets the preview crumb.
		 *
		 * @since 4.1.5
		 *
		 * @param  string $label The preview label.
		 * @return array         A crumb.
		 */
		public function getPreviewCrumb( $label ) {
			return $this->makeCrumb( $label, '', 'preview' );
		}

		/**
		 * Gets the post type archive crumb.
		 *
		 * @since 4.1.1
		 *
		 * @param  \WP_Post_Type $postType The post type object for reference.
		 * @return array                   A crumb.
		 */
		public function getPostTypeArchiveCrumb( $postType ) {
			return $this->makeCrumb( aioseo()->options->breadcrumbs->archiveFormat, get_post_type_archive_link( $postType->name ), 'postTypeArchive', $postType );
		}

		/**
		 * Gets a post crumb.
		 *
		 * @since 4.1.1
		 *
		 * @param  \WP_Post $post    A post object for reference.
		 * @param  string   $type    The breadcrumb type.
		 * @param  string   $subType The breadcrumb subType.
		 * @return array             A crumb.
		 */
		public function getPostCrumb( $post, $type = 'single', $subType = '' ) {
			return $this->makeCrumb( get_the_title( $post ), get_permalink( $post ), $type, $post, $subType );
		}

		/**
		 * Gets the term crumb.
		 *
		 * @since 4.1.1
		 *
		 * @param  \WP_Term $term    The term object for reference.
		 * @param  string   $subType The breadcrumb subType.
		 * @return array             A crumb.
		 */
		public function getTermTaxonomyCrumb( $term, $subType = '' ) {
			return $this->makeCrumb( $term->name, get_term_link( $term ), 'taxonomy', $term, $subType );
		}

		/**
		 * Gets the paged crumb.
		 *
		 * @since 4.1.1
		 *
		 * @param  array $reference The paged array for reference.
		 * @return array             A crumb.
		 */
		public function getPagedCrumb( $reference ) {
			return $this->makeCrumb( sprintf( '%1$s %2$s', __( 'Page', 'all-in-one-seo-pack' ), $reference['paged'] ), $reference['link'], 'paged', $reference );
		}

		/**
		 * Gets the author crumb.
		 *
		 * @since 4.1.1
		 *
		 * @param  \WP_User $wpUser A WP_User object.
		 * @return array            A crumb.
		 */
		public function getAuthorCrumb( $wpUser ) {
			return $this->makeCrumb( $wpUser->display_name, get_author_posts_url( $wpUser->ID ), 'author', $wpUser );
		}

		/**
		 * Gets the date crumb.
		 *
		 * @since 4.1.1
		 *
		 * @param  array $reference An array of year, month and day values.
		 * @return array            A crumb.
		 */
		public function getDateCrumb( $reference ) {
			$dateCrumb = [];
			$addMonth  = false;
			$addYear   = false;
			if ( ! empty( $reference['day'] ) ) {
				$addMonth    = true;
				$addYear     = true;
				$dateCrumb[] = $this->makeCrumb(
					zeroise( (int) $reference['day'], 2 ),
					get_day_link( $reference['year'], $reference['month'], $reference['day'] ),
					'day',
					$reference['day']
				);
			}
			if ( ! empty( $reference['month'] ) || $addMonth ) {
				$addYear     = true;
				$dateCrumb[] = $this->makeCrumb(
					zeroise( (int) $reference['month'], 2 ),
					get_month_link( $reference['year'], $reference['month'] ),
					'month',
					$reference['month']
				);

			}
			if ( ! empty( $reference['year'] ) || $addYear ) {
				$dateCrumb[] = $this->makeCrumb(
					$reference['year'],
					get_year_link( $reference['year'] ),
					'year',
					$reference['year']
				);
			}

			return array_reverse( $dateCrumb );
		}

		/**
		 * Gets an array of crumbs parents for the term.
		 *
		 * @since 4.1.1
		 *
		 * @param  \WP_Term $term A WP_Term object.
		 * @return array          An array of parent crumbs.
		 */
		public function getTermTaxonomyParentCrumbs( $term ) {
			$crumbs = [];

			$termHierarchy = $this->getTermHierarchy( $term->term_id, $term->taxonomy );
			if ( ! empty( $termHierarchy ) ) {
				foreach ( $termHierarchy as $parentTermId ) {
					$parentTerm = aioseo()->helpers->getTerm( $parentTermId, $term->taxonomy );
					$crumbs[]   = $this->getTermTaxonomyCrumb( $parentTerm, 'parent' );
				}
			}

			return $crumbs;
		}

		/**
		 * Helper function to create a standard crumb array.
		 *
		 * @since 4.1.1
		 *
		 * @param  string $label     The crumb label.
		 * @param  string $link      The crumb url.
		 * @param  null   $type      The crumb type.
		 * @param  null   $reference The crumb reference.
		 * @param  null   $subType   The crumb subType ( single/parent ).
		 * @return array             A crumb array.
		 */
		public function makeCrumb( $label, $link = '', $type = null, $reference = null, $subType = null ) {
			return [
				'label'     => $label,
				'link'      => $link,
				'type'      => $type,
				'subType'   => $subType,
				'reference' => $reference
			];
		}

		/**
		 * Gets a post archive crumb if it's post type has archives.
		 *
		 * @since 4.1.1
		 *
		 * @param  int|\WP_Post $post An ID or a WP_Post object.
		 * @return array              A crumb.
		 */
		public function getPostArchiveCrumb( $post ) {
			$postType = get_post_type_object( get_post_type( $post ) );
			if ( ! $postType || ! $postType->has_archive ) {
				return [];
			}

			return $this->makeCrumb( $postType->labels->name, get_post_type_archive_link( $postType->name ), 'postTypeArchive', $postType );
		}

		/**
		 * Gets a post's taxonomy crumbs.
		 *
		 * @since 4.1.1
		 *
		 * @param  int|\WP_Post $post     An ID or a WP_Post object.
		 * @param  null         $taxonomy A taxonomy to use. If none is provided the first one with terms selected will be used.
		 * @return array                  An array of term crumbs.
		 */
		public function getPostTaxonomyCrumbs( $post, $taxonomy = null ) {
			$crumbs = [];

			$overrideTaxonomy = $this->getOverride( 'taxonomy' );
			if ( ! empty( $overrideTaxonomy ) ) {
				$taxonomy = $overrideTaxonomy;
			}

			if ( $taxonomy && ! is_array( $taxonomy ) ) {
				$taxonomy = [ $taxonomy ];
			}

			$termHierarchy = $this->getPostTaxTermHierarchy( $post, $taxonomy );
			if ( ! empty( $termHierarchy['terms'] ) ) {
				foreach ( $termHierarchy['terms'] as $termId ) {
					$term     = aioseo()->helpers->getTerm( $termId, $termHierarchy['taxonomy'] );
					$crumbs[] = $this->makeCrumb( $term->name, get_term_link( $term, $termHierarchy['taxonomy'] ), 'taxonomy', $term, 'parent' );
				}
			}

			return $crumbs;
		}

		/**
		 * Gets the post's parent crumbs.
		 *
		 * @since 4.1.1
		 *
		 * @param  int|\WP_Post $post An ID or a WP_Post object.
		 * @param  string       $type The crumb type.
		 * @return array              An array of the post parent crumbs.
		 */
		public function getPostParentCrumbs( $post, $type = 'single' ) {
			$crumbs = [];
			if ( ! is_post_type_hierarchical( get_post_type( $post ) ) ) {
				return $crumbs;
			}

			$postHierarchy = $this->getPostHierarchy( $post );
			if ( ! empty( $postHierarchy ) ) {
				foreach ( $postHierarchy as $parentID ) {
					// Do not include the Home Page.
					if ( aioseo()->helpers->getHomePageId() === $parentID ) {
						continue;
					}

					$crumbs[] = $this->getPostCrumb( get_post( $parentID ), $type, 'parent' );
				}
			}

			return $crumbs;
		}

		/**
		 * Function to extend on pro for extra functionality.
		 *
		 * @since 4.1.1
		 *
		 * @param  string $type      The type of breadcrumb.
		 * @param  mixed  $reference The breadcrumb reference.
		 * @return bool              Show current item.
		 */
		public function showCurrentItem( $type = null, $reference = null ) {
			return apply_filters( 'aioseo_breadcrumbs_show_current_item', aioseo()->options->breadcrumbs->showCurrentItem, $type, $reference );
		}

		/**
		 * Gets a home page crumb.
		 *
		 * @since 4.1.1
		 *
		 * @param  string     $type      The type of breadcrumb.
		 * @param  mixed      $reference The breadcrumb reference.
		 * @return array|void            The home crumb.
		 */
		public function maybeGetHomePageCrumb( $type = null, $reference = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
			if ( aioseo()->options->breadcrumbs->homepageLink ) {
				return $this->getHomePageCrumb();
			}
		}

		/**
		 * Gets a home page crumb.
		 *
		 * @since 4.1.1
		 *
		 * @return array The home crumb.
		 */
		public function getHomePageCrumb() {
			$homePageId = aioseo()->helpers->getHomePageId();

			$label = '';
			if ( $homePageId ) {
				$label = get_the_title( $homePageId );
			}

			if ( 0 < strlen( aioseo()->options->breadcrumbs->homepageLabel ) ) {
				$label = aioseo()->options->breadcrumbs->homepageLabel;
			}

			// Label fallback.
			if ( empty( $label ) ) {
				$label = __( 'Home', 'all-in-one-seo-pack' );
			}

			return $this->makeCrumb( $label, get_home_url(), 'homePage', aioseo()->helpers->getHomePage() );
		}

		/**
		 * Gets the blog crumb.
		 *
		 * @since 4.1.1
		 *
		 * @return array The blog crumb.
		 */
		public function getBlogCrumb() {
			$crumb = [];

			$blogPage = aioseo()->helpers->getBlogPage();
			if ( null !== $blogPage ) {
				$crumb = $this->makeCrumb( $blogPage->post_title, get_permalink( $blogPage ), 'blog', $blogPage );
			}

			return $crumb;
		}

		/**
		 * Maybe add the shop crumb to products and product categories.
		 *
		 * @since 4.5.5
		 *
		 * @return array The shop crumb.
		 */
		public function maybeGetWooCommerceShopCrumb() {
			$crumb = [];
			if (
				aioseo()->helpers->isWooCommerceShopPage() ||
				aioseo()->helpers->isWooCommerceProductPage() ||
				aioseo()->helpers->isWooCommerceTaxonomyPage()
			) {
				$crumb = $this->getWooCommerceShopCrumb();
			}

			return $crumb;
		}

		/**
		 * Gets the shop crumb.
		 * @see WC_Breadcrumb::prepend_shop_page()
		 *
		 * @since 4.5.5
		 *
		 * @return array The shop crumb.
		 */
		public function getWooCommerceShopCrumb() {
			$crumb = [];

			if (
				! function_exists( 'wc_get_page_id' ) ||
				apply_filters( 'aioseo_woocommerce_breadcrumb_hide_shop', false )
			) {
				return $crumb;
			}

			$shopPageId = wc_get_page_id( 'shop' );
			$shopPage   = get_post( $shopPageId );

			// WC checks if the permalink contains the shop page in the URI, but we prefer to
			// always show the shop page as the first crumb if it exists and it's not the home page.
			if (
				$shopPageId &&
				$shopPage &&
				aioseo()->helpers->getHomePageId() !== $shopPageId
			) {
				$crumb = $this->makeCrumb( get_the_title( $shopPage ), get_permalink( $shopPage ), 'wcShop' );
			}

			return $crumb;
		}

		/**
		 * Gets a post's term hierarchy for a list of taxonomies selecting the one that has a lengthier hierarchy.
		 *
		 * @since 4.1.1
		 *
		 * @param  int|\WP_Post $post                An ID or a WP_Post object.
		 * @param  array        $taxonomies          An array of taxonomy names.
		 * @param  false        $skipUnselectedTerms Allow unselected terms to be filtered out from the crumbs.
		 * @return array                             An array of the taxonomy name + a term hierarchy.
		 */
		public function getPostTaxTermHierarchy( $post, $taxonomies = [], $skipUnselectedTerms = false ) {
			// Get all taxonomies attached to the post.
			if ( empty( $taxonomies ) ) {
				$taxonomies = get_object_taxonomies( get_post_type( $post ), 'objects' );
				$taxonomies = wp_filter_object_list( $taxonomies, [ 'public' => true ], 'and', 'name' );
			}

			foreach ( $taxonomies as $taxonomy ) {
				$primaryTerm         = aioseo()->standalone->primaryTerm->getPrimaryTerm( $post->ID, $taxonomy );
				$overridePrimaryTerm = $this->getOverride( 'primaryTerm' );
				if ( ! empty( $overridePrimaryTerm ) ) {
					$primaryTerm = ! is_a( $overridePrimaryTerm, 'WP_Term' ) ? get_term( $overridePrimaryTerm, $taxonomy ) : $overridePrimaryTerm;
				}

				$terms = wp_get_object_terms( $post->ID, $taxonomy, [
					'orderby' => 'term_id',
					'order'   => 'ASC',
				] );
				// Use the first taxonomy with terms.
				if ( empty( $terms ) || is_wp_error( $terms ) ) {
					continue;
				}

				// Determines the lengthier term hierarchy.
				$termHierarchy = [];
				foreach ( $terms as $term ) {
					// Gets our filtered ancestors.
					$ancestors = $this->getFilteredTermHierarchy( $term->term_id, $term->taxonomy, $skipUnselectedTerms ? $terms : [] );

					// Merge the current term to be used in the breadcrumbs.
					$ancestors = array_merge( $ancestors, [ $term->term_id ] );

					// If the current term is the primary term, use it.
					if ( is_a( $primaryTerm, 'WP_Term' ) && $primaryTerm->term_id === $term->term_id ) {
						$termHierarchy = $ancestors;
						break;
					}

					$termHierarchy = ( count( $termHierarchy ) < count( $ancestors ) ) ? $ancestors : $termHierarchy;
				}

				// Return a top to bottom hierarchy.
				return [
					'taxonomy' => $taxonomy,
					'terms'    => $termHierarchy
				];
			}

			return [];
		}

		/**
		 * Filters a term's parent hierarchy against other terms.
		 *
		 * @since 4.1.1
		 *
		 * @param  int    $termId               A term id.
		 * @param  string $taxonomy             The taxonomy name.
		 * @param  array  $termsToFilterAgainst Terms to filter out of the hierarchy.
		 * @return array                        The term's parent hierarchy.
		 */
		public function getFilteredTermHierarchy( $termId, $taxonomy, $termsToFilterAgainst = [] ) {
			$ancestors = $this->getTermHierarchy( $termId, $taxonomy );

			// Keep only selected terms in the hierarchy.
			if ( ! empty( $termsToFilterAgainst ) ) {
				// If it's a WP_Term array make it a term_id array.
				if ( is_a( current( $termsToFilterAgainst ), 'WP_Term' ) ) {
					$termsToFilterAgainst = wp_list_pluck( $termsToFilterAgainst, 'term_id' );
				}

				$ancestors = array_intersect( $ancestors, $termsToFilterAgainst );
			}

			return $ancestors;
		}

		/**
		 * Gets a term's parent hierarchy.
		 *
		 * @since 4.1.1
		 *
		 * @param  int    $termId   A term id.
		 * @param  string $taxonomy A taxonomy name.
		 * @return array            The term parent hierarchy.
		 */
		public function getTermHierarchy( $termId, $taxonomy ) {
			// Return a top to bottom hierarchy.
			return array_reverse( get_ancestors( $termId, $taxonomy, 'taxonomy' ) );
		}

		/**
		 * Gets a post's parent hierarchy.
		 *
		 * @since 4.1.1
		 *
		 * @param  int|\WP_Post $post An ID or a WP_Post object.
		 * @return array              The post parent hierarchy.
		 */
		public function getPostHierarchy( $post ) {
			$postId = ! empty( $post->ID ) ? $post->ID : $post;

			// Return a top to bottom hierarchy.
			return array_reverse( get_ancestors( $postId, '', 'post_type' ) );
		}

		/**
		 * Register our breadcrumb widget.
		 *
		 * @since 4.1.1
		 *
		 * @return void
		 */
		public function registerWidget() {
			if ( aioseo()->helpers->canRegisterLegacyWidget( 'aioseo-breadcrumb-widget' ) ) {
				register_widget( 'AIOSEO\Plugin\Common\Breadcrumbs\Widget' );
			}
		}

		/**
		 * Setter for the override property.
		 *
		 * @since 4.8.3
		 *
		 * @param  array $toOverride Array containing data to override.
		 * @return void
		 */
		public function setOverride( $toOverride = [] ) {
			$this->override = $toOverride;
		}

		/**
		 * Getter for the override property.
		 *
		 * @since 4.8.3
		 *
		 * @param  string $optionName Optional. The specific option name to retrieve.
		 * @return array              Array containing data to override.
		 */
		public function getOverride( $optionName = null ) {
			if ( empty( $this->override ) ) {
				return $optionName ? null : [];
			}

			$value = $this->override[ $optionName ] ?? null;

			return $optionName ? $value : $this->override;
		}
	}
}

namespace {
	// Exit if accessed directly.
	if ( ! defined( 'ABSPATH' ) ) {
		exit;
	}

	if ( ! function_exists( 'aioseo_breadcrumbs' ) ) {
		/**
		 * Global function for breadcrumbs output.
		 *
		 * @since 4.1.1
		 *
		 * @param  boolean     $echo Echo or return the output.
		 * @return string|void       The output.
		 */
		function aioseo_breadcrumbs( $echo = true ) {
			return aioseo()->breadcrumbs->frontend->display( $echo );
		}
	}
}Common/Breadcrumbs/Widget.php000066600000006410151135505570012175 0ustar00<?php
namespace AIOSEO\Plugin\Common\Breadcrumbs;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class Widget.
 *
 * @since 4.1.1
 */
class Widget extends \WP_Widget {
	/**
	 * The default attributes.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $defaults = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.1
	 */
	public function __construct() {
		// Widget defaults.
		$this->defaults = [
			'title' => ''
		];

		// Widget Slug.
		$widgetSlug = 'aioseo-breadcrumb-widget';

		// Widget basics.
		$widgetOps = [
			'classname'   => $widgetSlug,
			'description' => esc_html__( 'Display the current page breadcrumb.', 'all-in-one-seo-pack' ),
		];

		// Widget controls.
		$controlOps = [
			'id_base' => $widgetSlug,
		];

		// Translators: 1 - The plugin short name ("AIOSEO").
		$name = sprintf( esc_html__( '%1$s - Breadcrumbs', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME );
		$name .= ' ' . esc_html__( '(legacy)', 'all-in-one-seo-pack' );
		parent::__construct( $widgetSlug, $name, $widgetOps, $controlOps );
	}

	/**
	 * Widget callback.
	 *
	 * @since 4.1.1
	 *
	 * @param  array $args     Widget args.
	 * @param  array $instance The widget instance options.
	 * @return void
	 */
	public function widget( $args, $instance ) {
		// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
		// Merge with defaults.
		$instance = wp_parse_args( (array) $instance, $this->defaults );

		echo $args['before_widget'];

		// Title.
		if ( ! empty( $instance['title'] ) ) {
			echo $args['before_title'];
			echo apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base );
			echo $args['after_title'];
		}

		// If not being previewed in the Customizer maybe show the dummy preview.
		if (
			! is_customize_preview() &&
			(
				false !== strpos( wp_get_referer(), admin_url( 'widgets.php' ) ) ||
				false !== strpos( wp_get_referer(), admin_url( 'customize.php' ) )
			)
		) {
			aioseo()->breadcrumbs->frontend->preview();
		} else {
			aioseo()->breadcrumbs->frontend->display();
		}

		echo $args['after_widget'];
		// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Widget option update.
	 *
	 * @since 4.1.1
	 *
	 * @param array $newInstance New instance options.
	 * @param array $oldInstance Old instance options.
	 * @return array              Processed new instance options.
	 */
	public function update( $newInstance, $oldInstance ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$newInstance['title'] = wp_strip_all_tags( $newInstance['title'] );

		return $newInstance;
	}

	/**
	 * Widget options form.
	 *
	 * @since 4.1.1
	 *
	 * @param array $instance The widget instance options.
	 * @return void
	 */
	public function form( $instance ) {
		// Merge with defaults.
		$instance = wp_parse_args( (array) $instance, $this->defaults );
		?>
		<p>
			<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
				<?php echo esc_html( __( 'Title:', 'all-in-one-seo-pack' ) ); ?>
			</label>
			<input
					type="text"
					id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
					name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
					value="<?php echo esc_attr( $instance['title'] ); ?>"
					class="widefat"
			/>
		</p>
		<?php
	}
}Common/Breadcrumbs/Tags.php000066600000026211151135505570011651 0ustar00<?php
namespace AIOSEO\Plugin\Common\Breadcrumbs;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class to replace tag values with their data counterparts.
 *
 * @since 4.1.1
 */
class Tags {
	/**
	 * Tags constructor.
	 *
	 * @since 4.1.1
	 */
	public function __construct() {
		aioseo()->tags->addContext( $this->getContexts() );
		aioseo()->tags->addTags( $this->getTags() );
	}

	/**
	 * Replace the tags in the string provided.
	 *
	 * @since 4.1.1
	 *
	 * @param  string  $string           The string with tags.
	 * @param  array   $item             The breadcrumb item.
	 * @param  boolean $stripPunctuation Whether we should strip punctuation after the tags have been converted.
	 * @return string                    The string with tags replaced.
	 */
	public function replaceTags( $string, $item, $stripPunctuation = false ) {
		if ( ! $string || ! preg_match( '/#/', (string) $string ) ) {
			return $string;
		}

		// Replace separator tag so we don't strip it as punctuation.
		$separatorTag = aioseo()->tags->denotationChar . 'separator_sa';
		$string       = preg_replace( "/$separatorTag(?![a-zA-Z0-9_])/im", '>thisisjustarandomplaceholder<', (string) $string );

		// Replace custom breadcrumb tags.
		foreach ( $this->getTags() as $tag ) {
			$tagId   = aioseo()->tags->denotationChar . $tag['id'];
			$pattern = "/$tagId(?![a-zA-Z0-9_])/im";
			if ( preg_match( $pattern, (string) $string ) ) {
				$tagValue = str_replace( '$', '\$', (string) $this->getTagValue( $tag, $item ) );
				$string   = preg_replace( $pattern, $tagValue, (string) $string );
			}
		}

		if ( $stripPunctuation ) {
			$string = aioseo()->helpers->stripPunctuation( $string );
		}

		// Remove any remaining tags from the title attribute.
		$string = preg_replace_callback( '/title="([^"]*)"/i', function ( $matches ) {
			$sanitizedTitle = wp_strip_all_tags( html_entity_decode( $matches[1] ) );

			return 'title="' . esc_attr( $sanitizedTitle ) . '"';
		}, html_entity_decode( $string ) );

		return preg_replace(
			'/>thisisjustarandomplaceholder<(?![a-zA-Z0-9_])/im',
			aioseo()->helpers->decodeHtmlEntities( aioseo()->options->searchAppearance->global->separator ),
			(string) $string
		);
	}

	/**
	 * Get the value of the tag to replace.
	 *
	 * @since 4.1.1
	 *
	 * @param  string $tag  The tag to look for.
	 * @param  int    $item The crumb array.
	 * @return string       The value of the tag.
	 */
	public function getTagValue( $tag, $item ) {
		$product = false;
		if ( 0 === stripos( $tag['id'], 'breadcrumb_wc_product_' ) ) {
			$product = wc_get_product( $item['reference'] );
			if ( ! $product ) {
				return;
			}
		}

		switch ( $tag['id'] ) {
			case 'breadcrumb_link':
				return $item['link'];
			case 'breadcrumb_separator':
				return aioseo()->breadcrumbs->frontend->getSeparator();
			case 'breadcrumb_wc_product_price':
				return $product ? wc_price( $product->get_price() ) : '';
			case 'breadcrumb_wc_product_sku':
				return $product ? $product->get_sku() : '';
			case 'breadcrumb_wc_product_brand':
				return $product ? aioseo()->helpers->getWooCommerceBrand( $product->get_id() ) : '';
			case 'breadcrumb_author_first_name':
				return $item['reference']->first_name;
			case 'breadcrumb_author_last_name':
				return $item['reference']->last_name;
			case 'breadcrumb_archive_post_type_name':
				return $item['reference']->label;
			case 'breadcrumb_search_string':
				return $item['reference'];
			case 'breadcrumb_format_page_number':
				return $item['reference']['paged'];
			default:
				return $item['label'];
		}
	}

	/**
	 * Gets our breadcrumb custom tags.
	 *
	 * @since 4.1.1
	 *
	 * @return array An array of tags.
	 */
	public function getTags() {
		$tags = [
			[
				'id'          => 'breadcrumb_link',
				'name'        => __( 'Permalink', 'all-in-one-seo-pack' ),
				'description' => __( 'The permalink.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_label',
				'name'        => __( 'Label', 'all-in-one-seo-pack' ),
				'description' => __( 'The label.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_post_title',
				// Translators: 1 - The type of page (Post, Page, Category, Tag, etc.).
				'name'        => sprintf( __( '%1$s Title', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'The original title of the current post.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_taxonomy_title',
				// Translators: 1 - The type of page (Post, Page, Category, Tag, etc.).
				'name'        => sprintf( __( '%1$s Title', 'all-in-one-seo-pack' ), 'Category' ),
				// Translators: 1 - The name of a taxonomy.
				'description' => sprintf( __( 'The %1$s title.', 'all-in-one-seo-pack' ), 'Category' )
			],
			[
				'id'          => 'breadcrumb_separator',
				'name'        => __( 'Separator', 'all-in-one-seo-pack' ),
				'description' => __( 'The crumb separator.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_blog_page_title',
				'name'        => __( 'Blog Page Title', 'all-in-one-seo-pack' ),
				'description' => __( 'The blog page title.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_author_display_name',
				'name'        => __( 'Author Display Name', 'all-in-one-seo-pack' ),
				'description' => __( 'The author\'s display name.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_author_first_name',
				'name'        => __( 'Author First Name', 'all-in-one-seo-pack' ),
				'description' => __( 'The author\'s first name.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_author_last_name',
				'name'        => __( 'Author Last Name', 'all-in-one-seo-pack' ),
				'description' => __( 'The author\'s last name.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_search_result_format',
				'name'        => __( 'Search result format', 'all-in-one-seo-pack' ),
				'description' => __( 'The search result format.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_404_error_format',
				'name'        => __( '404 Error Format', 'all-in-one-seo-pack' ),
				'description' => __( 'The 404 error format.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_date_archive_year',
				'name'        => __( 'Year', 'all-in-one-seo-pack' ),
				'description' => __( 'The year.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_date_archive_month',
				'name'        => __( 'Month', 'all-in-one-seo-pack' ),
				'description' => __( 'The month.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_date_archive_day',
				'name'        => __( 'Day', 'all-in-one-seo-pack' ),
				'description' => __( 'The day.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_search_string',
				'name'        => __( 'Search String', 'all-in-one-seo-pack' ),
				'description' => __( 'The search string.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_format_page_number',
				'name'        => __( 'Page Number', 'all-in-one-seo-pack' ),
				'description' => __( 'The page number.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_archive_post_type_format',
				'name'        => __( 'Archive format', 'all-in-one-seo-pack' ),
				'description' => __( 'The archive format.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'breadcrumb_archive_post_type_name',
				'name'        => __( 'Post Type Name', 'all-in-one-seo-pack' ),
				'description' => __( 'The archive post type name.', 'all-in-one-seo-pack' )
			]
		];

		$postTypes = aioseo()->helpers->getPublicPostTypes();
		foreach ( $postTypes as $postType ) {
			if ( 'product' === $postType['name'] && aioseo()->helpers->isWoocommerceActive() ) {
				$tags[] = [
					'id'          => 'breadcrumb_wc_product_price',
					// Translators: 1 - The name of a post type.
					'name'        => sprintf( __( '%1$s Price', 'all-in-one-seo-pack' ), $postType['singular'] ),
					// Translators: 1 - The name of a post type.
					'description' => sprintf( __( 'The %1$s price.', 'all-in-one-seo-pack' ), $postType['singular'] )
				];
				$tags[] = [
					'id'          => 'breadcrumb_wc_product_sku',
					// Translators: 1 - The name of a post type.
					'name'        => sprintf( __( '%1$s SKU', 'all-in-one-seo-pack' ), $postType['singular'] ),
					// Translators: 1 - The name of a post type.
					'description' => sprintf( __( 'The %1$s SKU.', 'all-in-one-seo-pack' ), $postType['singular'] )
				];
				$tags[] = [
					'id'          => 'breadcrumb_wc_product_brand',
					// Translators: 1 - The name of a post type.
					'name'        => sprintf( __( '%1$s Brand', 'all-in-one-seo-pack' ), $postType['singular'] ),
					// Translators: 1 - The name of a post type.
					'description' => sprintf( __( 'The %1$s brand.', 'all-in-one-seo-pack' ), $postType['singular'] )
				];
			}
		}

		return $tags;
	}

	/**
	 * Gets our breadcrumb contexts.
	 *
	 * @since 4.1.1
	 *
	 * @return array An array of contexts.
	 */
	public function getContexts() {
		$contexts = [];

		$baseTags = [ 'breadcrumb_link', 'breadcrumb_separator' ];

		$postTypes = aioseo()->helpers->getPublicPostTypes();
		foreach ( $postTypes as $postType ) {
			$contexts[ 'breadcrumbs-post-type-' . $postType['name'] ] = array_merge( $baseTags, [ 'breadcrumb_post_title' ] );

			if ( 'product' === $postType['name'] && aioseo()->helpers->isWoocommerceActive() ) {
				$contexts[ 'breadcrumbs-post-type-' . $postType['name'] ] = array_merge( $contexts[ 'breadcrumbs-post-type-' . $postType['name'] ], [
					'breadcrumb_wc_product_price',
					'breadcrumb_wc_product_sku',
					'breadcrumb_wc_product_brand'
				] );
			}
		}

		$taxonomies = aioseo()->helpers->getPublicTaxonomies();
		foreach ( $taxonomies as $taxonomy ) {
			$contexts[ 'breadcrumbs-taxonomy-' . $taxonomy['name'] ] = array_merge( $baseTags, [ 'breadcrumb_taxonomy_title' ] );
		}

		$archives = aioseo()->helpers->getPublicPostTypes( false, true, true );
		foreach ( $archives as $archive ) {
			$contexts[ 'breadcrumbs-post-type-archive-' . $archive['name'] ] = array_merge( $baseTags, [
				'breadcrumb_archive_post_type_format',
				'breadcrumb_archive_post_type_name'
			] );
		}

		$contexts['breadcrumbs-blog-archive'] = array_merge( $baseTags, [ 'breadcrumb_blog_page_title' ] );

		$contexts['breadcrumbs-author'] = array_merge( $baseTags, [
			'breadcrumb_author_display_name',
			'breadcrumb_author_first_name',
			'breadcrumb_author_last_name'
		] );

		$contexts['breadcrumbs-search']             = array_merge( $baseTags, [ 'breadcrumb_search_result_format', 'breadcrumb_search_string' ] );
		$contexts['breadcrumbs-notFound']           = array_merge( $baseTags, [ 'breadcrumb_404_error_format' ] );
		$contexts['breadcrumbs-date-archive-year']  = array_merge( $baseTags, [ 'breadcrumb_date_archive_year' ] );
		$contexts['breadcrumbs-date-archive-month'] = array_merge( $baseTags, [ 'breadcrumb_date_archive_month' ] );
		$contexts['breadcrumbs-date-archive-day']   = array_merge( $baseTags, [ 'breadcrumb_date_archive_day' ] );

		$contexts['breadcrumbs-format-archive'] = [ 'breadcrumb_archive_post_type_name' ];
		$contexts['breadcrumbs-format-search']  = [ 'breadcrumb_search_string' ];
		$contexts['breadcrumbs-format-paged']   = [ 'breadcrumb_format_page_number' ];

		return $contexts;
	}
}Common/Utils/Features.php000066600000012002151135505570011371 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Contains helper methods specific to the addons.
 *
 * @since 4.3.0
 */
class Features {
	/**
	 * The features URL.
	 *
	 * @since 4.3.0
	 *
	 * @var string
	 */
	protected $featuresUrl = 'https://licensing-cdn.aioseo.com/keys/lite/all-in-one-seo-pack-pro-features.json';

	/**
	 * Returns our features.
	 *
	 * @since 4.3.0
	 *
	 * @param  boolean $flushCache Whether or not to flush the cache.
	 * @return array               An array of addon data.
	 */
	public function getFeatures( $flushCache = false ) {
		$features = aioseo()->core->networkCache->get( 'license_features' );
		if ( null === $features || $flushCache ) {
			$response = aioseo()->helpers->wpRemoteGet( $this->getFeaturesUrl() );
			if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
				$features = json_decode( wp_remote_retrieve_body( $response ), true );
			}

			if ( ! $features || ! empty( $features->error ) ) {
				$features = $this->getDefaultFeatures();
			}

			aioseo()->core->networkCache->update( 'license_features', $features );
		}

		// Convert the features array to objects using JSON. This is essential because we have lots of features that rely on this to be an object, and changing it to an array would break them.

		$features = json_decode( wp_json_encode( $features ) );

		return $features;
	}

	/**
	 * Get the URL to get features.
	 *
	 * @since 4.1.8
	 *
	 * @return string The URL.
	 */
	protected function getFeaturesUrl() {
		$url = $this->featuresUrl;
		if ( defined( 'AIOSEO_FEATURES_URL' ) ) {
			$url = AIOSEO_FEATURES_URL;
		}

		return $url;
	}

	/**
	 * Retrieves a default list of all external saas features available for the current user if the API cannot be reached.
	 *
	 * @since 4.3.0
	 *
	 * @return array An array of features.
	 */
	protected function getDefaultFeatures() {
		return json_decode( wp_json_encode( [
			[
				'license_level' => 'pro',
				'section'       => 'schema',
				'feature'       => 'event'
			],
			[
				'license_level' => 'elite',
				'section'       => 'schema',
				'feature'       => 'event'
			],
			[
				'license_level' => 'elite',
				'section'       => 'schema',
				'feature'       => 'job-posting'
			],
			[
				'license_level' => 'elite',
				'section'       => 'tools',
				'feature'       => 'network-tools-site-activation'
			],
			[
				'license_level' => 'elite',
				'section'       => 'tools',
				'feature'       => 'network-tools-database'
			],
			[
				'license_level' => 'elite',
				'section'       => 'tools',
				'feature'       => 'network-tools-import-export'
			],
			[
				'license_level' => 'elite',
				'section'       => 'tools',
				'feature'       => 'network-tools-robots'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'seo-statistics'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'keyword-rankings'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'keyword-rankings-pages'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'content-rankings'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'post-detail'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'post-detail-page-speed'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'post-detail-seo-statistics'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'post-detail-keywords'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'post-detail-focus-keyword-trend'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'keyword-tracking'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'post-detail-keyword-tracking'
			],
			[
				'license_level' => 'elite',
				'section'       => 'search-statistics',
				'feature'       => 'index-status'
			]
		] ), true );
	}

	/**
	 * Get the plans for a given feature.
	 *
	 * @since 4.3.0
	 *
	 * @param  string $sectionSlug The section name.
	 * @param  string $feature     The feature name.
	 * @return array               The plans for the feature.
	 */
	public function getPlansForFeature( $sectionSlug, $feature = '' ) {
		$plans = [];

		// Loop through all the features and find the plans that have access to the feature.
		foreach ( $this->getFeatures() as $featureArray ) {
			if ( $featureArray->section !== $sectionSlug ) {
				continue;
			}

			if ( ! empty( $feature ) && $featureArray->feature !== $feature ) {
				continue;
			}

			$plans[] = ucfirst( $featureArray->license_level );
		}

		return array_unique( $plans );
	}
}Common/Utils/PluginUpgraderSilentAjax.php000066600000023674151135505570014547 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use WP_Error;

/** \WP_Upgrader class */
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';

/** \Plugin_Upgrader class */
require_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php';

/**
 * In WP 5.3 a PHP 5.6 splat operator (...$args) was added to \WP_Upgrader_Skin::feedback().
 * We need to remove all calls to *Skin::feedback() method, as we can't override it in own Skins
 * without breaking support for PHP 5.3-5.5.
 *
 * @internal Please do not use this class outside of core AIOSEO development. May be removed at any time.
 *
 * @since 1.5.6.1
 */
class PluginUpgraderSilentAjax extends \Plugin_Upgrader {
	/**
	 * An array of links to install the plugins from.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $pluginLinks = [
		'brokenLinkChecker'    => 'https://downloads.wordpress.org/plugin/broken-link-checker-seo.zip',
		'optinMonster'         => 'https://downloads.wordpress.org/plugin/optinmonster.zip',
		'wpForms'              => 'https://downloads.wordpress.org/plugin/wpforms-lite.zip',
		'miLite'               => 'https://downloads.wordpress.org/plugin/google-analytics-for-wordpress.zip',
		'emLite'               => 'https://downloads.wordpress.org/plugin/google-analytics-dashboard-for-wp.zip',
		'wpMail'               => 'https://downloads.wordpress.org/plugin/wp-mail-smtp.zip',
		'rafflePress'          => 'https://downloads.wordpress.org/plugin/rafflepress.zip',
		'seedProd'             => 'https://downloads.wordpress.org/plugin/coming-soon.zip',
		'trustPulse'           => 'https://downloads.wordpress.org/plugin/trustpulse-api.zip',
		'instagramFeed'        => 'https://downloads.wordpress.org/plugin/instagram-feed.zip',
		'facebookFeed'         => 'https://downloads.wordpress.org/plugin/custom-facebook-feed.zip',
		'twitterFeed'          => 'https://downloads.wordpress.org/plugin/custom-twitter-feeds.zip',
		'youTubeFeed'          => 'https://downloads.wordpress.org/plugin/feeds-for-youtube.zip',
		'pushEngage'           => 'https://downloads.wordpress.org/plugins/pushengage.zip',
		'sugarCalendar'        => 'https://downloads.wordpress.org/plugins/sugar-calendar-lite.zip',
		'wpSimplePay'          => 'https://downloads.wordpress.org/plugins/stripe.zip',
		'easyDigitalDownloads' => 'https://downloads.wordpress.org/plugins/easy-digital-downloads.zip',
		'wpcode'               => 'https://downloads.wordpress.org/plugin/insert-headers-and-footers.zip',
		'searchWp'             => '',
		'affiliateWp'          => '',
		'charitable'           => 'https://downloads.wordpress.org/plugin/charitable.zip',
		'duplicator'           => 'https://downloads.wordpress.org/plugin/duplicator.zip'
	];

	/**
	 * An array of links to install the plugins from wordpress.org.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $wpPluginLinks = [
		'brokenLinkChecker' => 'https://wordpress.org/plugins/broken-link-checker-seo/',
		'optinMonster'      => 'https://wordpress.org/plugin/optinmonster/',
		'wpForms'           => 'https://wordpress.org/plugin/wpforms-lite/',
		'miLite'            => 'https://wordpress.org/plugin/google-analytics-for-wordpress/',
		'emLite'            => 'https://wordpress.org/plugin/google-analytics-dashboard-for-wp/',
		'wpMail'            => 'https://wordpress.org/plugin/wp-mail-smtp/',
		'rafflePress'       => 'https://wordpress.org/plugin/rafflepress/',
		'seedProd'          => 'https://wordpress.org/plugin/coming-soon/',
		'trustPulse'        => 'https://wordpress.org/plugin/trustpulse-api/',
		'instagramFeed'     => 'https://wordpress.org/plugin/instagram-feed/',
		'facebookFeed'      => 'https://wordpress.org/plugin/custom-facebook-feed/',
		'twitterFeed'       => 'https://wordpress.org/plugin/custom-twitter-feeds/',
		'youTubeFeed'       => 'https://wordpress.org/plugin/feeds-for-youtube/',
		'pushEngage'        => 'https://wordpress.org/plugins/pushengage/',
		'sugarCalendar'     => 'https://wordpress.org/plugins/sugar-calendar-lite/',
		'wpSimplePay'       => 'https://wordpress.org/plugins/stripe/',
		'searchWp'          => 'https://searchwp.com/',
		'affiliateWp'       => 'https://affiliatewp.com/',
		'wpcode'            => 'https://wordpress.org/plugins/insert-headers-and-footers/',
		'charitable'        => 'https://wordpress.org/plugins/charitable/',
		'duplicator'        => 'https://wordpress.org/plugins/duplicator/'
	];

	/**
	 * An array of slugs to check if plugins are activated.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $pluginSlugs = [
		'brokenLinkChecker'       => 'broken-link-checker-seo/aioseo-broken-link-checker.php',
		'optinMonster'            => 'optinmonster/optin-monster-wp-api.php',
		'wpForms'                 => 'wpforms-lite/wpforms.php',
		'wpFormsPro'              => 'wpforms/wpforms.php',
		'miLite'                  => 'google-analytics-for-wordpress/googleanalytics.php',
		'miPro'                   => 'google-analytics-premium/googleanalytics-premium.php',
		'emLite'                  => 'google-analytics-dashboard-for-wp/gadwp.php',
		'emPro'                   => 'exactmetrics-premium/exactmetrics-premium.php',
		'wpMail'                  => 'wp-mail-smtp/wp_mail_smtp.php',
		'wpMailPro'               => 'wp-mail-smtp-pro/wp_mail_smtp.php',
		'rafflePress'             => 'rafflepress/rafflepress.php',
		'rafflePressPro'          => 'rafflepress-pro/rafflepress-pro.php',
		'seedProd'                => 'coming-soon/coming-soon.php',
		'seedProdPro'             => 'seedprod-coming-soon-pro-5/seedprod-coming-soon-pro-5.php',
		'trustPulse'              => 'trustpulse-api/trustpulse.php',
		'instagramFeed'           => 'instagram-feed/instagram-feed.php',
		'instagramFeedPro'        => 'instagram-feed-pro/instagram-feed.php',
		'facebookFeed'            => 'custom-facebook-feed/custom-facebook-feed.php',
		'facebookFeedPro'         => 'custom-facebook-feed-pro/custom-facebook-feed.php',
		'twitterFeed'             => 'custom-twitter-feeds/custom-twitter-feed.php',
		'twitterFeedPro'          => 'custom-twitter-feeds-pro/custom-twitter-feed.php',
		'youTubeFeed'             => 'feeds-for-youtube/youtube-feed.php',
		'youTubeFeedPro'          => 'youtube-feed-pro/youtube-feed.php',
		'pushEngage'              => 'pushengage/main.php',
		'sugarCalendar'           => 'sugar-calendar-lite/sugar-calendar-lite.php',
		'sugarCalendarPro'        => 'sugar-calendar/sugar-calendar.php',
		'wpSimplePay'             => 'stripe/stripe-checkout.php',
		'wpSimplePayPro'          => 'wp-simple-pay-pro-3/simple-pay.php',
		'easyDigitalDownloads'    => 'easy-digital-downloads/easy-digital-downloads.php',
		'easyDigitalDownloadsPro' => 'easy-digital-downloads-pro/easy-digital-downloads.php',
		'searchWp'                => 'searchwp/index.php',
		'affiliateWp'             => 'affiliate-wp/affiliate-wp.php',
		'wpcode'                  => 'insert-headers-and-footers/ihaf.php',
		'wpcodePro'               => 'wpcode-premium/wpcode.php',
		'charitable'              => 'charitable/charitable.php',
		'duplicator'              => 'duplicator/duplicator.php'
	];

	/**
	 * An array of links for admin settings.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $pluginAdminUrls = [
		'brokenLinkChecker'       => 'admin.php?page=broken-link-checker#/settings',
		'optinMonster'            => 'admin.php?page=optin-monster-api-settings',
		'wpForms'                 => 'admin.php?page=wpforms-settings',
		'wpFormsPro'              => 'admin.php?page=wpforms-settings',
		'miLite'                  => 'admin.php?page=monsterinsights_settings#/',
		'miPro'                   => 'admin.php?page=monsterinsights_settings#/',
		'emLite'                  => 'admin.php?page=exactmetrics_settings#/',
		'emPro'                   => 'admin.php?page=exactmetrics_settings#/',
		'wpMail'                  => 'admin.php?page=wp-mail-smtp',
		'wpMailPro'               => 'admin.php?page=wp-mail-smtp',
		'seedProd'                => 'admin.php?page=seedprod_lite',
		'seedProdPro'             => 'admin.php?page=seedprod_pro',
		'rafflePress'             => 'admin.php?page=rafflepress_lite#/settings',
		'rafflePressPro'          => 'admin.php?page=rafflepress_pro#/settings',
		'trustPulse'              => 'admin.php?page=trustpulse',
		'instagramFeed'           => 'admin.php?page=sb-instagram-feed',
		'instagramFeedPro'        => 'admin.php?page=sb-instagram-feed',
		'facebookFeed'            => 'admin.php?page=cff-top',
		'facebookFeedPro'         => 'admin.php?page=cff-top',
		'twitterFeed'             => 'admin.php?page=ctf-settings',
		'twitterFeedPro'          => 'admin.php?page=ctf-settings',
		'youTubeFeed'             => 'admin.php?page=youtube-feed-settings',
		'youTubeFeedPro'          => 'admin.php?page=youtube-feed-settings',
		'pushEngage'              => 'admin.php?page=pushengage',
		'sugarCalendar'           => 'admin.php?page=sugar-calendar',
		'sugarCalendarPro'        => 'admin.php?page=sugar-calendar',
		'wpSimplePay'             => 'edit.php?post_type=simple-pay',
		'wpSimplePayPro'          => 'edit.php?post_type=simple-pay',
		'easyDigitalDownloads'    => 'edit.php?post_type=download&page=edd-settings',
		'easyDigitalDownloadsPro' => 'edit.php?post_type=download&page=edd-settings',
		'searchWp'                => 'options-general.php?page=searchwp',
		'affiliateWp'             => 'admin.php?page=affiliate-wp',
		'wpcode'                  => 'admin.php?page=wpcode',
		'wpcodePro'               => 'admin.php?page=wpcode',
		'charitable'              => 'admin.php?page=charitable-settings',
		'duplicator'              => 'admin.php?page=duplicator-settings'
	];

	/**
	 * An array of slugs that work in the network admin.
	 *
	 * @since 4.2.8
	 *
	 * @var array
	 */
	public $hasNetworkAdmin = [
		'miLite'    => 'admin.php?page=monsterinsights_network',
		'miPro'     => 'admin.php?page=monsterinsights_network',
		'emLite'    => 'admin.php?page=exactmetrics_network',
		'emPro'     => 'admin.php?page=exactmetrics_network',
		'wpMail'    => 'admin.php?page=wp-mail-smtp',
		'wpMailPro' => 'admin.php?page=wp-mail-smtp',
	];
}Common/Utils/Access.php000066600000017735151135505570011036 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;
	}
}Common/Utils/Assets.php000066600000004125151135505570011064 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Traits;

/**
 * Load file assets.
 *
 * @since 4.1.9
 */
class Assets {
	use Traits\Assets;

	/**
	 * Get the script handle to use for asset enqueuing.
	 *
	 * @since 4.1.9
	 *
	 * @var string
	 */
	private $scriptHandle = 'aioseo';

	/**
	 * Class constructor.
	 *
	 * @since 4.1.9
	 *
	 * @param \AIOSEO\Plugin\Common\Core\Core $core The AIOSEO Core class.
	 */
	public function __construct( $core ) {
		$this->core         = $core;
		$this->version      = aioseo()->version;
		$this->manifestFile = AIOSEO_DIR . '/dist/' . aioseo()->versionPath . '/manifest.php';
		$this->isDev        = aioseo()->isDev;

		if ( $this->isDev ) {
			$this->domain = getenv( 'VITE_AIOSEO_DOMAIN' );
			$this->port   = getenv( 'VITE_AIOSEO_DEV_PORT' );
		}

		add_filter( 'script_loader_tag', [ $this, 'scriptLoaderTag' ], 10, 3 );
		add_action( 'admin_head', [ $this, 'devRefreshRuntime' ] );
		add_action( 'wp_head', [ $this, 'devRefreshRuntime' ] );
	}

	/**
	 * Get the public URL base.
	 *
	 * @since 4.1.9
	 *
	 * @return string The URL base.
	 */
	private function getPublicUrlBase() {
		return $this->shouldLoadDev() ? $this->getDevUrl() . 'dist/' . aioseo()->versionPath . '/assets/' : $this->basePath();
	}

	/**
	 * Get the base path URL.
	 *
	 * @since 4.1.9
	 *
	 * @return string The base path URL.
	 */
	private function basePath() {
		return $this->normalizeAssetsHost( plugins_url( 'dist/' . aioseo()->versionPath . '/assets/', AIOSEO_FILE ) );
	}

	/**
	 * Adds the RefreshRuntime.
	 *
	 * @since 4.1.9
	 *
	 * @return void
	 */
	public function devRefreshRuntime() {
		if ( $this->shouldLoadDev() ) {
			echo sprintf( '<script type="module">
			import RefreshRuntime from "%1$s@react-refresh"
			RefreshRuntime.injectIntoGlobalHook(window)
			window.$RefreshReg$ = () => {}
			window.$RefreshSig$ = () => (type) => type
			window.__vite_plugin_react_preamble_installed__ = true
			</script>', $this->getDevUrl() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}
	}
}Common/Utils/Blocks.php000066600000010366151135505570011043 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Block helpers.
 *
 * @since 4.1.1
 */
class Blocks {
	/**
	 * Class constructor.
	 *
	 * @since 4.1.1
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'init' ] );
	}

	/**
	 * Initializes our blocks.
	 *
	 * @since 4.1.1
	 *
	 * @return void
	 */
	public function init() {
		add_action( 'enqueue_block_editor_assets', [ $this, 'registerBlockEditorAssets' ] );
	}

	/**
	 * Registers the block type with WordPress.
	 *
	 * @since 4.2.1
	 *
	 * @param  string               $slug Block type name including namespace.
	 * @param  array                $args Array of block type arguments with additional 'wp_min_version' arg.
	 * @return \WP_Block_Type|false       The registered block type on success, or false on failure.
	 */
	public function registerBlock( $slug = '', $args = [] ) {
		global $wp_version; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		if ( ! strpos( $slug, '/' ) ) {
			$slug = 'aioseo/' . $slug;
		}

		if ( ! $this->isBlockEditorActive() ) {
			return false;
		}

		// Check if the block requires a minimum WP version.
		if ( ! empty( $args['wp_min_version'] ) && version_compare( $wp_version, $args['wp_min_version'], '>' ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			return false;
		}

		// Checking whether block is registered to ensure it isn't registered twice.
		if ( $this->isRegistered( $slug ) ) {
			return false;
		}

		$defaults = [
			'render_callback' => null,
			'editor_script'   => aioseo()->core->assets->jsHandle( 'src/vue/standalone/blocks/main.js' ),
			'editor_style'    => aioseo()->core->assets->cssHandle( 'src/vue/assets/scss/blocks-editor.scss' ),
			'attributes'      => null,
			'supports'        => null
		];

		$args = wp_parse_args( $args, $defaults );

		return register_block_type( $slug, $args );
	}

	/**
	 * Registers Gutenberg editor assets.
	 *
	 * @since 4.2.1
	 *
	 * @return void
	 */
	public function registerBlockEditorAssets() {
		$postSettingJsAsset = 'src/vue/standalone/post-settings/main.js';
		if (
			aioseo()->helpers->isScreenBase( 'widgets' ) ||
			aioseo()->helpers->isScreenBase( 'customize' )
		) {
			/**
			 * Make sure the post settings JS asset is registered before adding it as a dependency below.
			 * This is needed because this asset is not loaded on widgets and customizer screens,
			 * {@see \AIOSEO\Plugin\Common\Admin\PostSettings::enqueuePostSettingsAssets}.
			 *

			 */
			aioseo()->core->assets->load( $postSettingJsAsset, [], aioseo()->helpers->getVueData() );
		}

		aioseo()->core->assets->loadCss( 'src/vue/standalone/blocks/main.js' );

		$dependencies = [
			'wp-annotations',
			'wp-block-editor',
			'wp-blocks',
			'wp-components',
			'wp-element',
			'wp-i18n',
			'wp-data',
			'wp-url',
			'wp-polyfill',
			aioseo()->core->assets->jsHandle( $postSettingJsAsset )
		];

		aioseo()->core->assets->enqueueJs( 'src/vue/standalone/blocks/main.js', $dependencies );
		aioseo()->core->assets->registerCss( 'src/vue/assets/scss/blocks-editor.scss' );
	}

	/**
	 * Check if a block is already registered.
	 *
	 * @since 4.2.1
	 *
	 * @param string $slug Name of block to check.
	 *
	 * @return bool
	 */
	public function isRegistered( $slug ) {
		if ( ! class_exists( 'WP_Block_Type_Registry' ) ) {
			return false;
		}

		return \WP_Block_Type_Registry::get_instance()->is_registered( $slug );
	}

	/**
	 * Helper function to determine if we're rendering the block inside Gutenberg.
	 *
	 * @since 4.1.1
	 *
	 * @return bool In gutenberg.
	 */
	public function isRenderingBlockInEditor() {
		// phpcs:disable HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) {
			return false;
		}

		$context = isset( $_REQUEST['context'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['context'] ) ) : '';
		// phpcs:enable HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended

		return 'edit' === $context;
	}

	/**
	 * Helper function to determine if we can register blocks.
	 *
	 * @since 4.1.1
	 *
	 * @return bool Can register block.
	 */
	public function isBlockEditorActive() {
		return function_exists( 'register_block_type' );
	}
}Common/Utils/Database.php000066600000136636151135505570011343 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Database utility class for AIOSEO.
 *
 * @since 4.0.0
 */
class Database {
	/**
	 * List of custom tables we support.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $customTables = [
		'aioseo_cache',
		'aioseo_crawl_cleanup_blocked_args',
		'aioseo_crawl_cleanup_logs',
		'aioseo_links',
		'aioseo_links_suggestions',
		'aioseo_notifications',
		'aioseo_posts',
		'aioseo_redirects',
		'aioseo_redirects_404',
		'aioseo_redirects_404_logs',
		'aioseo_redirects_hits',
		'aioseo_redirects_logs',
		'aioseo_terms',
		'aioseo_search_statistics_objects',
		'aioseo_revisions'
	];

	/**
	 * Holds $wpdb instance.
	 *
	 * @since 4.0.0
	 *
	 * @var \wpdb
	 */
	public $db = null;

	/**
	 * Holds $wpdb prefix.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	public $prefix = '';

	/**
	 * The database table in use by this query.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	public $table = '';

	/**
	 * The sql statement (SELECT, INSERT, UPDATE, DELETE, etc.).
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $statement = '';

	/**
	 * The limit clause for the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var string|int
	 */
	private $limit = '';

	/**
	 * The group clause for the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $group = [];

	/**
	 * The order by clause for the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $order = [];

	/**
	 * The select clause for the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $select = [];

	/**
	 * The set clause for the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $set = [];

	/**
	 * Duplicate clause for the INSERT query.
	 *
	 * @since 4.1.5
	 *
	 * @var array
	 */
	private $onDuplicate = [];

	/**
	 * Ignore clause for the INSERT query.
	 *
	 * @since 4.1.6
	 *
	 * @var array
	 */
	private $ignore = false;

	/**
	 * The where clause for the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $where = [];

	/**
	 * The union clause for the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $union = [];

	/**
	 * The join clause for the SQL query.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $join = [];

	/**
	 * Determines whether the select statement should be distinct.
	 *
	 * @since 4.0.0
	 *
	 * @var bool
	 */
	private $distinct = false;

	/**
	 * The order by direction for the query.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $orderDirection = 'ASC';

	/**
	 * The query string is populated after the __toString function is run.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $query = '';

	/**
	 * The sql query results are stored here.
	 *
	 * @since 4.0.0
	 *
	 * @var mixed
	 */
	private $result;

	/**
	 * The method in which $wpdb will output results.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $output = 'OBJECT';

	/**
	 * Whether or not to strip tags.
	 *
	 * @since 4.0.0
	 *
	 * @var bool
	 */
	private $stripTags = false;

	/**
	 * Set which option to use to escape the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var int
	 */
	protected $escapeOptions = 0;

	/**
	 * A cache of all queries and their results.
	 *
	 * @var array
	 */
	private $cache = [];

	/**
	 * Whether or not to reset the cached results.
	 *
	 * @var bool
	 */
	private $shouldResetCache = false;

	/**
	 * Constant for escape options.
	 *
	 * @since 4.0.0
	 *
	 * @var int
	 */
	const ESCAPE_FORCE = 2;

	/**
	 * Constant for escape options.
	 *
	 * @since 4.0.0
	 *
	 * @var int
	 */
	const ESCAPE_STRIP_HTML = 4;

	/**
	 * Constant for escape options.
	 *
	 * @since 4.0.0
	 *
	 * @var int
	 */
	const ESCAPE_QUOTE = 8;

	/**
	 * List of model class instances.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $models = [];

	/**
	 * The last query that ran, stringified.
	 *
	 * @since 4.3.0
	 */
	public $lastQuery = '';

	/**
	 * Prepares the database class for use.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->init();
	}

	/**
	 * Initializes the DB class.
	 * This needs to be called after the class is instantiated or when switching between sites in a multisite environment.
	 * The latter is important because the prefix otherwise isn't updated.
	 *
	 * @since 4.6.1
	 *
	 * @return void
	 */
	public function init() {
		global $wpdb;
		$this->db            = $wpdb;
		$this->prefix        = $wpdb->prefix;
		$this->escapeOptions = self::ESCAPE_STRIP_HTML | self::ESCAPE_QUOTE;
	}

	/**
	 * If this is a clone, lets reset all the data.
	 *
	 * @since 4.0.0
	 */
	public function __clone() {
		// We need to reset the result separately as well since it is not in the default array.
		$this->reset( [ 'result' ] );
		$this->reset();
	}

	/**
	 * Gets all AIOSEO installed tables.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of custom AIOSEO tables.
	 */
	public function getInstalledTables() {
		$results = $this->db->get_results( 'SHOW TABLES', 'ARRAY_N' );

		return ! empty( $results ) ? wp_list_pluck( $results, 0 ) : [];
	}

	/**
	 * Get all the database info such as data size, index size, table list.
	 *
	 * @since 4.4.5
	 *
	 * @return array An array of the database info.
	 */
	public function getDatabaseInfo() {
		$tables       = [];
		$databaseSize = [];

		if ( defined( 'DB_NAME' ) ) {
			$databaseTableInformation = $this->db->get_results(
				$this->db->prepare(
					"SELECT
						table_name AS 'name',
						table_collation AS 'collation',
						engine AS 'engine',
						round( ( data_length / 1024 / 1024 ), 2 ) 'data',
						round( ( index_length / 1024 / 1024 ), 2 ) 'index'
					FROM information_schema.TABLES
					WHERE table_schema = %s
					ORDER BY name ASC;",
					DB_NAME
				)
			);

			$databaseSize = [
				'data'  => 0,
				'index' => 0,
			];

			$siteTablesPrefix = $this->db->get_blog_prefix( get_current_blog_id() );
			$globalTables     = $this->db->tables( 'global', true );
			foreach ( $databaseTableInformation as $table ) {
				// Only include tables matching the prefix of the current site, this is to prevent displaying all tables on a MS install not relating to the current.
				if ( is_multisite() && 0 !== strpos( $table->name, $siteTablesPrefix ) && ! in_array( $table->name, $globalTables, true ) ) {
					continue;
				}

				$tableType = ( 0 === strpos( $table->name, aioseo()->core->db->prefix . 'aioseo' ) ) ? 'aioseo' : 'other';

				$tables[ $tableType ][ $table->name ] = [
					'data'      => $table->data,
					'index'     => $table->index,
					'engine'    => $table->engine,
					'collation' => $table->collation
				];

				$databaseSize['data']  += $table->data;
				$databaseSize['index'] += $table->index;
			}
		}

		return [
			'tables' => $tables,
			'size'   => $databaseSize,
		];
	}

	/**
	 * Gets all columns from a table.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $table The name of the table to lookup columns for.
	 * @return array         An array of custom AIOSEO tables.
	 */
	public function getColumns( $table ) {
		if ( ! $this->tableExists( $table ) ) {
			return [];
		}

		$table           = $this->prefix . $table;
		$installedTables = json_decode( aioseo()->internalOptions->database->installedTables, true );

		if ( empty( $installedTables[ $table ] ) ) {
			$installedTables[ $table ]                           = $this->db->get_col( 'SHOW COLUMNS FROM `' . $table . '`' );
			aioseo()->internalOptions->database->installedTables = wp_json_encode( $installedTables );
		}

		return $installedTables[ $table ];
	}

	/**
	 * Checks if a table exists.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $table The name of the table.
	 * @return bool          Whether or not the table exists.
	 */
	public function tableExists( $table ) {
		$table           = $this->prefix . $table;
		$installedTables = json_decode( aioseo()->internalOptions->database->installedTables ?? '[]', true ) ?: [];
		if ( isset( $installedTables[ $table ] ) ) {
			return true;
		}

		$results = $this->db->get_results( "SHOW TABLES LIKE '" . $table . "'" );
		if ( empty( $results ) ) {
			return false;
		}

		$installedTables[ $table ]                           = [];
		aioseo()->internalOptions->database->installedTables = wp_json_encode( $installedTables );

		return true;
	}

	/**
	 * Checks if a column exists on a given table.
	 *
	 * @since 4.0.5
	 *
	 * @param  string $table  The name of the table.
	 * @param  string $column The name of the column.
	 * @return bool           Whether or not the column exists.
	 */
	public function columnExists( $table, $column ) {
		if ( ! $this->tableExists( $table ) ) {
			return false;
		}

		$columns = $this->getColumns( $table );

		return in_array( $column, $columns, true );
	}

	/**
	 * Gets the size of a table in bytes.
	 *
	 * @since 4.1.0
	 *
	 * @param  string $table The table to check.
	 * @return int           The size of the table in bytes.
	 */
	public function getTableSize( $table ) {
		$this->db->query( 'ANALYZE TABLE ' . $this->prefix . $table );
		$results = $this->db->get_results( '
			SELECT
				TABLE_NAME AS `table`,
				ROUND(SUM(DATA_LENGTH + INDEX_LENGTH)) AS `size`
			FROM information_schema.TABLES
			WHERE TABLE_SCHEMA = "' . $this->db->dbname . '"
			AND TABLE_NAME = "' . $this->prefix . $table . '"
			ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC;
		' );

		return ! empty( $results ) ? $results[0]->size : 0;
	}

	/**
	 * The query string in all its glory.
	 *
	 * @since 4.0.0
	 *
	 * @return string The actual query string.
	 */
	public function __toString() {
		switch ( strtoupper( $this->statement ) ) {
			case 'INSERT':
				$insert = 'INSERT ';
				if ( $this->ignore ) {
					$insert .= 'IGNORE ';
				}
				$insert   .= 'INTO ' . $this->table;
				$clauses   = [];
				$clauses[] = $insert;
				$clauses[] = 'SET ' . implode( ', ', $this->set );
				if ( ! empty( $this->onDuplicate ) ) {
					$clauses[] = 'ON DUPLICATE KEY UPDATE ' . implode( ', ', $this->onDuplicate );
				}

				break;
			case 'REPLACE':
				$clauses   = [];
				$clauses[] = "REPLACE INTO $this->table";
				$clauses[] = 'SET ' . implode( ', ', $this->set );

				break;
			case 'UPDATE':
				$clauses   = [];
				$clauses[] = "UPDATE $this->table";

				if ( count( $this->join ) > 0 ) {
					foreach ( (array) $this->join as $join ) {
						if ( is_array( $join[1] ) ) {
							$join_on = []; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
							foreach ( (array) $join[1] as $left => $right ) {
								$join_on[] = "$this->table.`$left` = `{$join[0]}`.`$right`"; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
							}
							// phpcs:disable Squiz.NamingConventions.ValidVariableName
							$clauses[] = "\t" . ( ( 'LEFT' === $join[2] || 'RIGHT' === $join[2] ) ? $join[2] . ' JOIN ' : 'JOIN ' ) . $join[0] . ' ON ' . implode( ' AND ', $join_on );
							// phpcs:enable Squiz.NamingConventions.ValidVariableName
						} else {
							$clauses[] = "\t" . ( ( 'LEFT' === $join[2] || 'RIGHT' === $join[2] ) ? $join[2] . ' JOIN ' : 'JOIN ' ) . "{$join[0]} ON {$join[1]}";
						}
					}
				}

				$clauses[] = 'SET ' . implode( ', ', $this->set );

				if ( count( $this->where ) > 0 ) {
					$clauses[] = "WHERE 1 = 1 AND\n\t" . implode( "\n\tAND ", $this->where );
				}

				if ( count( $this->order ) > 0 ) {
					$clauses[] = 'ORDER BY ' . implode( ', ', $this->order );
				}

				if ( $this->limit ) {
					$clauses[] = 'LIMIT ' . $this->limit;
				}

				break;

			case 'TRUNCATE':
				$clauses   = [];
				$clauses[] = "TRUNCATE TABLE $this->table";
				break;

			case 'DELETE':
				$clauses   = [];
				$clauses[] = "DELETE FROM $this->table";

				if ( count( $this->where ) > 0 ) {
					$clauses[] = "WHERE 1 = 1 AND\n\t" . implode( "\n\tAND ", $this->where );
				}

				if ( count( $this->order ) > 0 ) {
					$clauses[] = 'ORDER BY ' . implode( ', ', $this->order );
				}

				if ( $this->limit ) {
					$clauses[] = 'LIMIT ' . $this->limit;
				}

				break;
			case 'SELECT':
			case 'SELECT DISTINCT':
			default:
				// Select fields.
				$clauses   = [];
				$distinct  = ( $this->distinct || stripos( $this->statement, 'DISTINCT' ) !== false ) ? 'DISTINCT ' : '';
				$select    = ( count( $this->select ) > 0 ) ? implode( ",\n\t", $this->select ) : '*';
				$clauses[] = "SELECT {$distinct}\n\t{$select}";

				// Select table.
				$clauses[] = "FROM $this->table";

				// Select joins.
				if ( ! empty( $this->join ) && count( $this->join ) > 0 ) {
					foreach ( (array) $this->join as $join ) {
						if ( is_array( $join[1] ) ) {
							$join_on = []; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
							foreach ( (array) $join[1] as $left => $right ) {
								$join_on[] = "$this->table.`$left` = `{$join[0]}`.`$right`"; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
							}
							// phpcs:disable Squiz.NamingConventions.ValidVariableName
							$clauses[] = "\t" . ( ( 'LEFT' === $join[2] || 'RIGHT' === $join[2] ) ? $join[2] . ' JOIN ' : 'JOIN ' ) . $join[0] . ' ON ' . implode( ' AND ', $join_on );
							// phpcs:enable Squiz.NamingConventions.ValidVariableName
						} else {
							$clauses[] = "\t" . ( ( 'LEFT' === $join[2] || 'RIGHT' === $join[2] ) ? $join[2] . ' JOIN ' : 'JOIN ' ) . "{$join[0]} ON {$join[1]}";
						}
					}
				}

				// Select conditions.
				if ( count( $this->where ) > 0 ) {
					$clauses[] = "WHERE 1 = 1 AND\n\t" . implode( "\n\tAND ", $this->where );
				}

				// Union queries.
				if ( count( $this->union ) > 0 ) {
					foreach ( $this->union as $union ) {
						$keyword   = ( $union[1] ) ? 'UNION' : 'UNION ALL';
						$clauses[] = "\n$keyword\n\n$union[0]";
					}

					$clauses[] = '';
				}

				// Select groups.
				if ( count( $this->group ) > 0 ) {
					$clauses[] = 'GROUP BY ' . implode( ', ', $this->escapeColNames( $this->group ) );
				}

				// Select order.
				if ( count( $this->order ) > 0 ) {
					$orderFragments = [];
					foreach ( $this->escapeColNames( $this->order ) as $col ) {
						$orderFragments[] = ( preg_match( '/ (ASC|DESC|RAND\(\))$/i', (string) $col ) ) ? $col : "$col $this->orderDirection";
					}

					$clauses[] = 'ORDER BY ' . implode( ', ', $orderFragments );
				}

				// Select limit.
				if ( $this->limit ) {
					$clauses[] = 'LIMIT ' . $this->limit;
				}

				break;
		}

		// @HACK for wpdb::prepare.
		$clauses[] = '/* %d = %d */';

		$this->query = str_replace( '%%d = %%d', '%d = %d', str_replace( '%', '%%', implode( "\n", $clauses ) ) );

		// Flag queries with double quotes down, but not if the double quotes are contained within a string value (like JSON).
		if ( aioseo()->isDev && preg_match( '/\{[^}]*\}(*SKIP)(*FAIL)|\[[^]]*\](*SKIP)(*FAIL)|\'[^\']*\'(*SKIP)(*FAIL)|\\"(*SKIP)(*FAIL)|"/i', (string) $this->query ) ) {
			// phpcs:disable WordPress.PHP.DevelopmentFunctions
			error_log(
				"Query with double quotes detected - this may cause isues when ANSI_QUOTES is enabled:\r\n" .
				$this->query . "\r\n" . wp_debug_backtrace_summary()
			);
			// phpcs:enable WordPress.PHP.DevelopmentFunctions
		}

		$this->lastQuery = $this->query;

		return $this->query;
	}

	/**
	 * Shortcut method to return the query string.
	 *
	 * @since 4.0.0
	 *
	 * @return string The query string.
	 */
	public function query() {
		return $this->__toString();
	}

	/**
	 * Start a new Database Query.
	 *
	 * @since 4.0.0
	 *
	 * @param  string   $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  bool     $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @param  string   $statement      The MySQL statement for the query.
	 * @return Database                 Returns the Database class which can then be method chained for building the query.
	 */
	public function start( $table = '', $includesPrefix = false, $statement = 'SELECT' ) {
		// Always reset everything when starting a new query.
		$this->reset();
		$this->table     = $includesPrefix ? $table : $this->prefix . $table;
		$this->statement = $statement;

		return $this;
	}

	/**
	 * Shortcut method for start with INSERT as the statement.
	 *
	 * @since 4.0.0
	 *
	 * @param  string   $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  bool     $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                 Returns the Database class which can then be method chained for building the query.
	 */
	public function insert( $table = '', $includesPrefix = false ) {
		return $this->start( $table, $includesPrefix, 'INSERT' );
	}

	/**
	 * Shortcut method for start with INSERT IGNORE as the statement.
	 *
	 * @since 4.1.6
	 *
	 * @param  string   $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  bool     $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                 Returns the Database class which can then be method chained for building the query.
	 */
	public function insertIgnore( $table = '', $includesPrefix = false ) {
		$this->ignore = true;

		return $this->start( $table, $includesPrefix, 'INSERT' );
	}

	/**
	 * Shortcut method for start with UPDATE as the statement.
	 *
	 * @since 4.0.0
	 *
	 * @param  string   $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  bool     $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                 Returns the Database class which can then be method chained for building the query.
	 */
	public function update( $table = '', $includesPrefix = false ) {
		return $this->start( $table, $includesPrefix, 'UPDATE' );
	}

	/**
	 * Shortcut method for start with REPLACE as the statement.
	 *
	 * @since 4.0.0
	 *
	 * @param  string   $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  bool     $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                 Returns the Database class which can then be method chained for building the query.
	 */
	public function replace( $table = '', $includesPrefix = false ) {
		return $this->start( $table, $includesPrefix, 'REPLACE' );
	}

	/**
	 * Shortcut method for start with TRUNCATE as the statement.
	 *
	 * @since 4.0.0
	 *
	 * @param  string   $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  bool     $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                 Returns the Database class which can then be method chained for building the query.
	 */
	public function truncate( $table = '', $includesPrefix = false ) {
		return $this->start( $table, $includesPrefix, 'TRUNCATE' );
	}

	/**
	 * Shortcut method for start with DELETE as the statement.
	 *
	 * @since 4.0.0
	 *
	 * @param  string   $table          The name of the table without the WordPress prefix unless includes_prefix is true.
	 * @param  bool     $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                 Returns the Database class which can then be method chained for building the query.
	 */
	public function delete( $table = '', $includesPrefix = false ) {
		return $this->start( $table, $includesPrefix, 'DELETE' );
	}

	/**
	 * Adds a SELECT clause.
	 *
	 * @since 4.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function select() {
		$args = (array) func_get_args();
		if ( count( $args ) === 1 && is_array( $args[0] ) ) {
			$args = $args[0];
		}

		$this->select = array_merge( $this->select, $this->escapeColNames( $args ) );

		return $this;
	}

	/**
	 * Adds a WHERE clause.
	 *
	 * @since 4.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function where() {
		$criteria = $this->prepArgs( func_get_args() );

		foreach ( (array) $criteria as $field => $value ) {
			if ( ! preg_match( '/[\(\)<=>!]+/', (string) $field ) && false === stripos( $field, ' IS ' ) ) {
				$operator = ( is_null( $value ) ) ? 'IS' : '=';
				$escaped  = $this->escapeColNames( $field );
				$field    = array_pop( $escaped ) . ' ' . $operator;
			}

			if ( is_null( $value ) && false !== stripos( $field, ' IS ' ) ) {
				// WHERE `field` IS NOT NULL.
				$this->where[] = "$field NULL";
				continue;
			}

			if ( is_null( $value ) ) {
				// WHERE `field` IS NULL.
				$this->where[] = "$field NULL";
				continue;
			}

			if ( is_array( $value ) ) {
				$wheres = [];
				foreach ( (array) $value as $val ) {
					$wheres[] = sprintf( "$field %s", $this->escape( $val, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
				}

				$this->where[] = '(' . implode( ' OR ', $wheres ) . ')';
				continue;
			}

			$this->where[] = sprintf( "$field %s", $this->escape( $value, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
		}

		return $this;
	}

	/**
	 * Adds a complex WHERE clause.
	 *
	 * @since 4.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function whereRaw() {
		$criteria = $this->prepArgs( func_get_args() );

		foreach ( (array) $criteria as $clause ) {
			$this->where[] = $clause;
		}

		return $this;
	}

	/**
	 * Adds a WHERE clause with all arguments sent separated by OR instead of AND inside a subclause.
	 * @example [ 'a' => 1, 'b' => 2 ] becomes "AND (a = 1 OR b = 2)"
	 *
	 * @since 4.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function whereOr() {
		$criteria = $this->prepArgs( func_get_args() );

		$or = [];
		foreach ( (array) $criteria as $field => $value ) {
			if ( ! preg_match( '/[\(\)<=>!]+/', (string) $field ) && false === stripos( $field, ' IS ' ) ) {
				$operator = ( is_null( $value ) ) ? 'IS' : '=';
				$field    = $this->escapeColNames( $field );
				$field    = array_pop( $field ) . ' ' . $operator;
			}

			if ( is_null( $value ) && false !== stripos( $field, ' IS ' ) ) {
				// WHERE `field` IS NOT NULL.
				$or[] = "$field NULL";
				continue;
			}

			if ( is_null( $value ) ) {
				// WHERE `field` IS NULL.
				$or[] = "$field NULL";
			}

			$or[] = sprintf( "$field %s", $this->escape( $value, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
		}

		// Create our subclause, and add it to the WHERE array.
		$this->where[] = '(' . implode( ' OR ', $or ) . ')';

		return $this;
	}

	/**
	 * Adds a WHERE IN() clause.
	 *
	 * @since 4.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function whereIn() {
		$criteria = $this->prepArgs( func_get_args() );

		foreach ( (array) $criteria as $field => $values ) {
			if ( ! is_array( $values ) ) {
				$values = [ $values ];
			}

			if ( count( $values ) === 0 ) {
				continue;
			}

			foreach ( $values as &$value ) {
				// Note: We can no longer check for `is_numeric` because a value like `61021e6242255` returns true and breaks the query.
				if ( is_int( $value ) || is_float( $value ) ) {
					// No change.
					continue;
				}

				if ( is_null( $value ) || 'null' === strtolower( $value ) ) {
					// Change to a true NULL value.
					$value = null;
					continue;
				}

				$value = sprintf( '%s', $this->escape( $value, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
			}

			$values = implode( ',', $values );
			$this->whereRaw( "$field IN ($values)" );
		}

		return $this;
	}

	/**
	 * Adds a WHERE NOT IN() clause.
	 *
	 * @since 4.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function whereNotIn() {
		$criteria = $this->prepArgs( func_get_args() );

		foreach ( (array) $criteria as $field => $values ) {
			if ( ! is_array( $values ) ) {
				$values = [ $values ];
			}

			if ( count( $values ) === 0 ) {
				continue;
			}

			foreach ( $values as &$value ) {
				if ( is_numeric( $value ) ) {
					// No change.
					continue;
				}

				if ( is_null( $value ) || false !== stristr( $value, 'NULL' ) ) {
					// Change to a true NULL value.
					$value = null;
					continue;
				}

				$value = sprintf( '%s', $this->escape( $value, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
			}

			$values = implode( ',', $values );
			$this->whereRaw( "$field NOT IN($values)" );
		}

		return $this;
	}

	/**
	 * Adds a WHERE BETWEEN clause.
	 *
	 * @since 4.3.0
	 *
	 * @return Database  Returns the Database class which can be method chained for more query building.
	 */
	public function whereBetween() {
		$criteria = $this->prepArgs( func_get_args() );

		foreach ( (array) $criteria as $field => $values ) {
			if ( ! is_array( $values ) ) {
				$values = [ $values ];
			}

			if ( count( $values ) === 0 ) {
				continue;
			}

			foreach ( $values as &$value ) {
				// Note: We can no longer check for `is_numeric` because a value like `61021e6242255` returns true and breaks the query.
				if ( is_int( $value ) || is_float( $value ) ) {
					// No change.
					continue;
				}

				if ( is_null( $value ) || false !== stristr( $value, 'NULL' ) ) {
					// Change to a true NULL value.
					$value = null;
					continue;
				}

				$value = sprintf( '%s', $this->escape( $value, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
			}

			$values = implode( ' AND ', $values );
			$this->whereRaw( "$field BETWEEN $values" );
		}

		return $this;
	}

	/**
	 * Adds a LEFT JOIN clause.
	 *
	 * @since 4.0.0
	 *
	 * @param  string       $table          The name of the table to join to this query.
	 * @param  string|array $conditions     The conditions of the join clause.
	 * @param  bool         $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                     Returns the Database class which can be method chained for more query building.
	 */
	public function leftJoin( $table = '', $conditions = '', $includesPrefix = false ) {
		return $this->join( $table, $conditions, 'LEFT', $includesPrefix );
	}

	/**
	 * Adds a JOIN clause.
	 *
	 * @since 4.0.0
	 *
	 * @param  string       $table          The name of the table to join to this query.
	 * @param  string|array $conditions     The conditions of the join clause.
	 * @param  string       $direction      This can take 'LEFT' or 'RIGHT' as arguments.
	 * @param  bool         $includesPrefix This determines if the table name includes the WordPress prefix or not.
	 * @return Database                     Returns the Database class which can be method chained for more query building.
	 */
	public function join( $table = '', $conditions = '', $direction = '', $includesPrefix = false ) {
		$this->join[] = [ $includesPrefix ? $table : $this->prefix . $table, $conditions, $direction ];

		return $this;
	}

	/**
	 * Add a UNION query.
	 *
	 * @since 4.0.0
	 *
	 * @param  Database|string $query    The query (Database object or query string) to be joined with.
	 * @param  bool            $distinct Set whether this union should be distinct or not.
	 * @return Database                  Returns the Database class which can be method chained for more query building.
	 */
	public function union( $query, $distinct = true ) {
		$this->union[] = [ $query, $distinct ];

		return $this;
	}

	/**
	 * Adds am GROUP BY clause.
	 *
	 * @since 4.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function groupBy() {
		$args = (array) func_get_args();
		if ( count( $args ) === 1 && is_array( $args[0] ) ) {
			$args = $args[0];
		}

		$this->group = array_merge( $this->group, $args );

		return $this;
	}

	/**
	 * Adds am ORDER BY clause.
	 *
	 * @since   4.0.0
	 * @version 4.8.2 Hardened against SQL injection.
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function orderBy() {
		// Normalize arguments.
		$args = (array) func_get_args();
		if ( count( $args ) === 1 && is_array( $args[0] ) ) {
			$args = $args[0];
		}

		$orderBy = [];
		// Separate commas to account for multiple orders.
		foreach ( $args as $argComma ) {
			$orderBy = array_map( 'trim', array_merge( $orderBy, explode( ',', $argComma ) ) );
		}

		// Validate and sanitize column names and sort directions.
		$sanitizedOrderBy = [];
		foreach ( $orderBy as $ordBy ) {
			$parts     = explode( ' ', $ordBy );
			$column    = str_replace( '`', '', $parts[0] ); // Strip existing ticks first.
			$column    = preg_replace( '/[^a-zA-Z0-9_.]/', '', $column ); // Strip invalid characters from the column name.
			$column    = $this->escapeColNames( $column )[0];
			$direction = isset( $parts[1] ) ? strtoupper( $parts[1] ) : 'ASC';

			// Validate the order direction.
			if ( ! in_array( $direction, [ 'ASC', 'DESC' ], true ) ) {
				$direction = 'ASC';
			}

			$sanitizedOrderBy[] = "$column $direction";
		}

		if ( ! empty( $sanitizedOrderBy ) ) {
			if ( ! empty( $args[0] ) && true !== $args[0] ) {
				$this->order = array_merge( $this->order, $sanitizedOrderBy );
			} else {
				// This allows for overwriting a preexisting order-by setting.
				array_shift( $sanitizedOrderBy );
				$this->order = $sanitizedOrderBy;
			}
		}

		return $this;
	}

	/**
	 * Adds a raw ORDER BY clause.
	 *
	 * @since 4.8.2
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function orderByRaw() {
		$args = (array) func_get_args();
		if ( count( $args ) === 1 && is_array( $args[0] ) ) {
			$args = $args[0];
		}

		$this->order = array_merge( $this->order, $args );

		return $this;
	}

	/**
	 * Sets the sort direction for ORDER BY clauses.
	 *
	 * @since 4.0.0
	 *
	 * @param  string    $direction This sets the direction of the order by clause, default is 'ASC'.
	 * @return Database             Returns the Database class which can be method chained for more query building.
	 */
	public function orderDirection( $direction = 'ASC' ) {
		$this->orderDirection = $direction;

		return $this;
	}

	/**
	 * Adds a LIMIT clause.
	 *
	 * @since 4.0.0
	 *
	 * @param  int      $limit  The amount of rows to limit the query to.
	 * @param  int      $offset The amount of rows the result of the query should be ofset with.
	 * @return Database         Returns the Database class which can be method chained for more query building.
	 */
	public function limit( $limit, $offset = -1 ) {
		if ( ! is_numeric( $limit ) || $limit <= 0 ) {
			return $this;
		}

		if ( ! is_numeric( $offset ) ) {
			$offset = -1;
		}

		$this->limit = ( -1 === $offset )
			? intval( $limit )
			: intval( $offset ) . ', ' . intval( $limit );

		return $this;
	}

	/**
	 * Converts associative arrays to a SET argument.
	 *
	 * @since 4.1.5
	 *
	 * @param  array $args The arguments.
	 * @return array       The prepared arguments.
	 */
	private function prepareSet( $args ) {
		$args = $this->prepArgs( $args );

		$preparedSet = [];
		foreach ( (array) $args as $field => $value ) {
			if ( is_null( $value ) ) {
				$preparedSet[] = "`$field` = NULL";
				continue;
			}

			if ( is_array( $value ) ) {
				throw new \Exception( 'Cannot save an unserialized array in the database. Data passed was: ' . wp_json_encode( $value ) );
			}

			if ( is_object( $value ) ) {
				throw new \Exception( 'Cannot save an unserialized object in the database. Data passed was: ' . esc_html( $value ) );
			}

			$preparedSet[] = sprintf( "`$field` = %s", $this->escape( $value, $this->getEscapeOptions() | self::ESCAPE_QUOTE ) );
		}

		return $preparedSet;
	}

	/**
	 * Adds a SET clause.
	 *
	 * @since 4.0.0
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function set() {
		$this->set = array_merge( $this->set, $this->prepareSet( func_get_args() ) );

		return $this;
	}

	/**
	 * Adds an ON DUPLICATE clause.
	 *
	 * @since 4.1.5
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function onDuplicate() {
		$this->onDuplicate = array_merge( $this->onDuplicate, $this->prepareSet( func_get_args() ) );

		return $this;
	}

	/**
	 * Set the output for the query.
	 *
	 * @since 4.0.0
	 *
	 * @param  string   $output This can be one of the following: ARRAY_A | ARRAY_N | OBJECT | OBJECT_K.
	 * @return Database         Returns the Database class which can be method chained for more query building.
	 */
	public function output( $output = 'OBJECT' ) {
		if ( ! $output ) {
			$output = 'OBJECT';
		}

		$this->output = $output;

		return $this;
	}

	/**
	 * Reset the cache so we make sure the query gets to the DB.
	 *
	 * @since 4.1.6
	 *
	 * @return Database Returns the Database class which can be method chained for more query building.
	 */
	public function resetCache() {
		$this->shouldResetCache = true;

		return $this;
	}

	/**
	 * Run this query.
	 *
	 * @since 4.0.0
	 *
	 * @param  bool     $reset  Whether to reset the results/query.
	 * @param  string   $return Determine which method to call on the $wpdb object
	 * @param  array    $params Optional extra parameters to pass to the db method call
	 * @return Database         Returns the Database class which can be method chained for more query building.
	 */
	public function run( $reset = true, $return = 'results', $params = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		if ( ! in_array( $return, [ 'results', 'col', 'var', 'row' ], true ) ) {
			$return = 'results';
		}

		$prepare        = $this->db->prepare( $this->query(), 1, 1 );
		$queryHash      = sha1( $this->query() );
		$cacheTableName = $this->getCacheTableName();

		// Pull the result from the in-memory cache if everything checks out.
		if (
			! $this->shouldResetCache &&
			! in_array( $this->statement, [ 'INSERT', 'REPLACE', 'UPDATE', 'DELETE' ], true ) &&
			isset( $this->cache[ $cacheTableName ][ $queryHash ][ $return ] ) &&
			empty( $this->join )
		) {
			$this->result = $this->cache[ $cacheTableName ][ $queryHash ][ $return ];

			return $this;
		}

		switch ( $return ) {
			case 'col':
				$this->result = $this->db->get_col( $prepare );
				break;
			case 'var':
				$this->result = $this->db->get_var( $prepare );
				break;
			case 'row':
				$this->result = $this->db->get_row( $prepare );
				break;
			default:
				$this->result = $this->db->get_results( $prepare, $this->output );
		}

		if ( $reset ) {
			$this->reset();
		}

		$this->cache[ $cacheTableName ][ $queryHash ][ $return ] = $this->result;

		// Reset the cache trigger for the next run.
		$this->shouldResetCache = false;

		return $this;
	}

	/**
	 * Inject a count select statement and return the result.
	 *
	 * @since 4.1.0
	 *
	 * @param  string $countColumn The column to count with. Defaults to '*' all.
	 * @return int                 The number of rows that were found.
	 */
	public function count( $countColumn = '*' ) {
		$usingGroup = ! empty( $this->group );
		$results    = $this->reset( [ 'select', 'order', 'limit' ] )
			->select( 'count(' . $countColumn . ') as count' )
			->run()
			->result();

		return 1 === $this->numRows() && ! $usingGroup
			? (int) $results[0]->count
			: $this->numRows();
	}

	/**
	 * Inject a count group select statement and return the result.
	 *
	 * @since 4.6.1
	 *
	 * @param  string $countDistinctColumn The column to count with. Defaults to '*' all.
	 * @return int                         The number of rows that were found.
	 */
	public function countDistinct( $countDistinctColumn = '*' ) {
		$countDistinctColumn = '*' !== $countDistinctColumn ? 'distinct( ' . $countDistinctColumn . ' )' : $countDistinctColumn;

		return $this->reset( [ 'select', 'order', 'limit' ] )
			->select( 'count(' . $countDistinctColumn . ') as count' )
			->run( true, 'var' )
			->result();
	}

	/**
	 * Returns the query results based on the value of the output property.
	 *
	 * @since 4.0.0
	 *
	 * @return mixed This depends on what was set in the output property.
	 */
	public function result() {
		return $this->result;
	}

	/**
	 * Return a model model from a row.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $class The name of the model class to call.
	 * @return object        The model class instance.
	 */
	public function model( $class ) {
		$result = $this->result();

		return ! empty( $result )
			? ( is_array( $result )
				? new $class( (array) current( $result ) )
				: $result )
			: new $class();
	}

	/**
	 * Return an array of model class instancnes from the result.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $class  The name of the model class to call.
	 * @param  string $id     The ID of the index to use.
	 * @param  bool   $toJson The index if necessary.
	 * @return array          An array of model class instances.
	 */
	public function models( $class, $id = null, $toJson = false ) {
		if ( ! empty( $this->models ) ) {
			return $this->models;
		}

		$i      = 0;
		$models = [];
		foreach ( $this->result() as $row ) {
			$var   = ( null === $id ) ? $row : $row[ $id ];
			$class = new $class( $var );
			// Lets add the class to the array using the class ID.
			$models[ $class->id ] = $toJson ? $class->jsonSerialize() : $class;
			$i++;
		}

		$this->models = $models;

		return $this->models;
	}

	/**
	 * Returns the last error reported by MySQL.
	 *
	 * @since 4.0.0
	 *
	 * @return string The last error message.
	 */
	public function lastError() {
		return $this->db->last_error;
	}

	/**
	 * Return the $wpdb insert_id from the last query.
	 *
	 * @since 4.0.0
	 *
	 * @return int The ID of the most recent INSERT query.
	 */
	public function insertId() {
		return $this->db->insert_id;
	}

	/**
	 * Return the $wpdb rows_affected from the last query.
	 *
	 * @since 4.0.0
	 *
	 * @return int The number of rows affected.
	 */
	public function rowsAffected() {
		return $this->db->rows_affected;
	}

	/**
	 * Return the $wpdb num_rows from the last query.
	 *
	 * @since 4.0.0
	 *
	 * @return int The count for the number of rows in the last query.
	 */
	public function numRows() {
		return $this->db->num_rows;
	}

	/**
	 * Check if the last query had any rows.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether there were any rows retrived by the last query.
	 */
	public function nullSet() {
		return ( $this->numRows() < 1 );
	}

	/**
	 * This will start a MySQL transaction. Be sure to commit or rollback!
	 *
	 * @since 4.0.0
	 */
	public function startTransaction() {
		$this->db->query( 'START TRANSACTION' );
	}

	/**
	 * This will commit a MySQL transaction. Used in conjunction with startTransaction.
	 *
	 * @since 4.0.0
	 */
	public function commit() {
		$this->db->query( 'COMMIT' );
	}

	/**
	 * This will rollback a MySQL transaction. Used in conjunction with startTransaction.
	 *
	 * @since 4.0.0
	 */
	public function rollback() {
		$this->db->query( 'ROLLBACK' );
	}

	/**
	 * Fast way to execute raw queries.
	 * NOTE: When using this method, all arguments must be sanitized manually!
	 *
	 * @since 4.0.0
	 *
	 * @param  string $sql      The sql query to execute.
	 * @param  bool   $results  Whether to return the results or not.
	 * @param  bool   $useCache Whether to use the cache or not.
	 * @return mixed            Could be an array or object depending on the result set.
	 */
	public function execute( $sql, $results = false, $useCache = false ) {
		$this->lastQuery = $sql;
		$queryHash       = sha1( $sql );
		$cacheTableName  = $this->getCacheTableName();

		// Pull the result from the in-memory cache if everything checks out.
		if (
			$useCache &&
			! $this->shouldResetCache &&
			isset( $this->cache[ $cacheTableName ][ $queryHash ] )
		) {
			if ( $results ) {
				$this->result = $this->cache[ $cacheTableName ][ $queryHash ];
			}

			return $this;
		}

		if ( $results ) {
			$this->result = $this->db->get_results( $sql, $this->output );

			if ( $useCache ) {
				$this->cache[ $cacheTableName ][ $queryHash ] = $this->result;

				// Reset the cache trigger for the next run.
				$this->shouldResetCache = false;
			}

			return $this;
		}

		return $this->db->query( $sql );
	}

	/**
	 * Escape a value for safe use in SQL queries.
	 *
	 * @param string   $value   The value to be escaped.
	 * @param int|null $options The escape options.
	 * @return string           The escaped SQL value.
	 */
	public function escape( $value, $options = null ) {
		if ( is_array( $value ) ) {
			foreach ( $value as &$val ) {
				$val = $this->escape( $val, $options );
			}

			return $value;
		}

		$options = ( is_null( $options ) ) ? $this->getEscapeOptions() : $options;
		if ( ( $options & self::ESCAPE_STRIP_HTML ) !== 0 && isset( $this->stripTags ) && true === $this->stripTags ) {
			$value = wp_strip_all_tags( $value );
		}

		if (
			( ( $options & self::ESCAPE_FORCE ) !== 0 || php_sapi_name() === 'cli' ) ||
			( ( $options & self::ESCAPE_QUOTE ) !== 0 && ! is_int( $value ) )
		) {
			$value = esc_sql( $value );
			if ( ! is_int( $value ) ) {
				$value = "'$value'";
			}
		}

		return $value;
	}

	/**
	 * Returns the current escape options value.
	 *
	 * @since 4.0.0
	 *
	 * @return int The current escape options value.
	 */
	public function getEscapeOptions() {
		return $this->escapeOptions;
	}


	/**
	 * Sets the current escape options value.
	 *
	 * @since 4.0.0
	 *
	 * @param int $options The escape options value.
	 */
	public function setEscapeOptions( $options ) {
		$this->escapeOptions = $options;
	}

	/**
	 * Backtick-escapes an array of column and/or table names.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $cols An array of column names to be escaped.
	 * @return array       An array of escaped column names.
	 */
	private function escapeColNames( $cols ) {
		if ( ! is_array( $cols ) ) {
			$cols = [ $cols ];
		}

		foreach ( $cols as &$col ) {
			if ( false === stripos( $col, '(' ) && false === stripos( $col, ' ' ) && false === stripos( $col, '*' ) ) {
				if ( stripos( $col, '.' ) ) {
					list( $table, $c ) = explode( '.', $col );
					$col = "`$table`.`$c`";
					continue;
				}

				$col = "`$col`";
			}
		}

		return $cols;
	}

	/**
	 * Gets a variable list of function arguments and reformats them as needed for many of the functions of this class.
	 *
	 * @since 4.0.0
	 *
	 * @param  mixed $values This could be anything, but if used properly it usually is a string or an array.
	 * @return mixed         If the preparation was successful, it will return an array of arguments. Otherwise it could be anything.
	 */
	private function prepArgs( $values ) {
		$values = (array) $values;
		if ( ! is_array( $values[0] ) && count( $values ) === 2 ) {
			$values = [ $values[0] => $values[1] ];
		} elseif ( is_array( $values[0] ) && count( $values ) === 1 ) {
			$values = $values[0];
		}

		return $values;
	}

	/**
	 * Resets all the variables that make up the query.
	 *
	 * @since 4.0.0
	 *
	 * @param  array    $what Set which properties you want to reset. All are selected by default.
	 * @return Database       Returns the Database instance.
	 */
	public function reset(
		$what = [
			'table',
			'statement',
			'limit',
			'group',
			'order',
			'select',
			'set',
			'onDuplicate',
			'ignore',
			'where',
			'union',
			'distinct',
			'orderDirection',
			'query',
			'output',
			'stripTags',
			'models',
			'join'
		]
	) {
		// If we are not running a select query, let's bust the cache for this table.
		$selectStatements = [ 'SELECT', 'SELECT DISTINCT' ];
		if (
			! empty( $this->statement ) &&
			! in_array( $this->statement, $selectStatements, true )
		) {
			$this->bustCache( $this->getCacheTableName() );
		}

		foreach ( (array) $what as $var ) {
			switch ( $var ) {
				case 'group':
				case 'order':
				case 'select':
				case 'set':
				case 'onDuplicate':
				case 'where':
				case 'union':
				case 'join':
					$this->$var = [];
					break;
				case 'orderDirection':
					$this->$var = 'ASC';
					break;
				case 'ignore':
				case 'stripTags':
					$this->$var = false;
					break;
				case 'output':
					$this->$var = 'OBJECT';
					break;
				default:
					if ( isset( $this->$var ) ) {
						$this->$var = null;
					}
					break;
			}
		}

		return $this;
	}

	/**
	 * Returns the current value of one or more query properties.
	 *
	 * @since 4.0.0
	 *
	 * @param  string|array  $what You can pass in an array of options to retrieve. By default it selects all if them.
	 * @return string|array       Returns the value of whichever variables are passed in.
	 */
	public function getQueryProperty(
		$what = [
			'table',
			'statement',
			'limit',
			'group',
			'order',
			'select',
			'set',
			'onDuplicate',
			'where',
			'union',
			'distinct',
			'orderDirection',
			'query',
			'output',
			'result'
		]
	) {
		if ( is_array( $what ) ) {
			$return = [];
			foreach ( (array) $what as $which ) {
				$return[ $which ] = $this->$which;
			}

			return $return;
		}

		return $this->$what;
	}

	/**
	 * Get a table name for the cache key.
	 *
	 * @since 4.1.6
	 *
	 * @param  string $cacheTableName The table name to check against.
	 * @return string                 The cache key table name.
	 */
	private function getCacheTableName( $cacheTableName = '' ) {
		$cacheTableName = empty( $cacheTableName ) ? $this->table : $cacheTableName;

		foreach ( $this->customTables as $tableName ) {
			if ( false !== stripos( (string) $cacheTableName, $this->prefix . $tableName ) ) {
				$cacheTableName = $tableName;
				break;
			}
		}

		return $cacheTableName;
	}

	/**
	 * Busts the cache for the given table name.
	 *
	 * @since 4.1.6
	 *
	 * @param  string $tableName The table name.
	 * @return void
	 */
	public function bustCache( $tableName = '' ) {
		if ( ! $tableName ) {
			// Bust all the cache.
			$this->cache = [];

			return;
		}

		unset( $this->cache[ $tableName ] );
	}

	/**
	 * In order to not have a conflict, we need to return a clone.
	 *
	 * @since 4.1.0
	 *
	 * @return Database The cloned Database instance.
	 */
	public function noConflict() {
		return clone $this;
	}

	/**
	 * Checks whether the given index exists on the given table.
	 *
	 * @since 4.4.8
	 *
	 * @param  string $tableName      The table name.
	 * @param  string $indexName      The index name.
	 * @param  bool   $includesPrefix Whether the table name includes the WordPress prefix or not.
	 * @return bool                   Whether the index exists or not.
	 */
	public function indexExists( $tableName, $indexName, $includesPrefix = false ) {
		$prefix    = $includesPrefix ? '' : $this->prefix;
		$tableName = strtolower( $prefix . $tableName );
		$indexName = strtolower( $indexName );

		$indexes = $this->db->get_results( "SHOW INDEX FROM `$tableName`" );
		foreach ( $indexes as $index ) {
			if ( empty( $index->Key_name ) ) {
				continue;
			}

			if ( strtolower( $index->Key_name ) === $indexName ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Acquires a database lock with the given name.
	 *
	 * @since 4.8.3
	 *
	 * @param  string  $lockName The name of the lock to acquire.
	 * @param  integer $timeout  The timeout in seconds. Default is 0 which means it will return immediately if the lock cannot be acquired.
	 * @return boolean           Whether the lock was acquired.
	 */
	public function acquireLock( $lockName, $timeout = 0 ) {
		$lockResult = $this->db->get_var( $this->db->prepare( 'SELECT GET_LOCK(%s, %d)', $lockName, $timeout ) );
		$acquired   = '1' === $lockResult;

		if ( $acquired ) {
			// Register a shutdown function to always release the lock even if a fatal error occurs.
			register_shutdown_function( function () use ( $lockName ) {
				$this->releaseLock( $lockName );
			} );
		}

		return $acquired;
	}

	/**
	 * Releases a database lock with the given name.
	 *
	 * @since 4.8.3
	 *
	 * @param  string  $lockName The name of the lock to release.
	 * @return boolean           Whether the lock was released.
	 */
	public function releaseLock( $lockName ) {
		$releaseResult = $this->db->query( $this->db->prepare( 'SELECT RELEASE_LOCK(%s)', $lockName ) );

		return false !== $releaseResult;
	}
}Common/Utils/Backup.php000066600000004413151135505570011027 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Backup for AIOSEO Settings.
 *
 * @since 4.0.0
 */
class Backup {
	/**
	 * A the name of the option to save backups with.
	 *
	 * @since 4.00
	 *
	 * @var string
	 */
	private $optionsName = 'aioseo_settings_backup';

	/**
	 * Get all backups.
	 *
	 * @return array An array of backups.
	 */
	public function all() {
		$backups = json_decode( get_option( $this->optionsName ), true );
		if ( empty( $backups ) ) {
			$backups = [];
		}

		return $backups;
	}

	/**
	 * Creates a backup of the settings state.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function create() {
		$backupTime = time();
		$options    = $this->getOptions();

		update_option( $this->optionsName . '_' . $backupTime, wp_json_encode( $options ), 'no' );

		$backups = $this->all();

		$backups[] = $backupTime;

		update_option( $this->optionsName, wp_json_encode( $backups ), 'no' );
	}

	/**
	 * Deletes a backup of the settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function delete( $backupTime ) {
		delete_option( $this->optionsName . '_' . $backupTime );

		$backups = $this->all();

		foreach ( $backups as $key => $backup ) {
			if ( $backup === $backupTime ) {
				unset( $backups[ $key ] );
			}
		}

		update_option( $this->optionsName, wp_json_encode( array_values( $backups ) ), 'no' );
	}

	/**
	 * Restores a backup of the settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function restore( $backupTime ) {
		$backup = json_decode( get_option( $this->optionsName . '_' . $backupTime ), true );
		if ( ! empty( $backup['options']['tools']['robots']['rules'] ) ) {
			$backup['options']['tools']['robots']['rules'] = array_merge(
				aioseo()->robotsTxt->extractSearchAppearanceRules(),
				$backup['options']['tools']['robots']['rules']
			);
		}

		aioseo()->options->sanitizeAndSave( $backup['options'] );
		aioseo()->internalOptions->sanitizeAndSave( $backup['internalOptions'] );
	}

	/**
	 * Get the options to save.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of options to save.
	 */
	private function getOptions() {
		return [
			'options'         => aioseo()->options->all(),
			'internalOptions' => aioseo()->internalOptions->all()
		];
	}
}Common/Utils/Helpers.php000066600000024225151135505570011227 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 );
	}
}Common/Utils/PluginUpgraderSkin.php000066600000003257151135505570013404 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

require_once ABSPATH . 'wp-admin/includes/plugin.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader-skin.php';

/**
 * Class PluginSilentUpgraderSkin.
 *
 * @internal Please do not use this class outside of core All in One SEO development. May be removed at any time.
 *
 * @since 4.0.0
 */
class PluginUpgraderSkin extends \WP_Upgrader_Skin {
	/**
	 * Empty out the header of its HTML content and only check to see if it has
	 * been performed or not.
	 *
	 * @since 4.0.0
	 */
	public function header() {}

	/**
	 * Empty out the footer of its HTML contents.
	 *
	 * @since 4.0.0
	 */
	public function footer() {}

	/**
	 * Instead of outputting HTML for errors, just return them.
	 * Ajax request will just ignore it.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $errors Array of errors with the install process.
	 * @return void
	 */
	public function error( $errors ) {
		if ( ! empty( $errors ) ) {
			wp_send_json_error( $errors );
		}
	}

	/**
	 * Empty out JavaScript output that calls function to decrement the update counts.
	 *
	 * @since 4.0.0
	 *
	 * @param string $type Type of update count to decrement.
	 */
	public function decrement_update_count( $type ) {} // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable, PSR1.Methods.CamelCapsMethodName.NotCamelCaps

	/**
	 * @since 4.2.5
	 *
	 * @param  string $feedback Message data.
	 * @param  mixed  ...$args  Optional text replacements.
	 * @return void
	 */
	public function feedback( $feedback, ...$args ) {} // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
}Common/Utils/ActionScheduler.php000066600000020774151135505570012706 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.
		}
	}
}Common/Utils/VueSettings.php000066600000023150151135505570012101 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 );
	}
}Common/Utils/Cache.php000066600000021126151135505570010625 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Handles our cache.
 *
 * @since 4.1.5
 */
class Cache {
	/**
	 * Our cache table.
	 *
	 * @since 4.1.5
	 *
	 * @var string
	 */
	private $table = 'aioseo_cache';

	/**
	 * Our cached cache.
	 *
	 * @since 4.1.5
	 *
	 * @var array
	 */
	private static $cache = [];

	/**
	 * The Cache Prune class.
	 *
	 * @since 4.1.5
	 *
	 * @var CachePrune
	 */
	public $prune;

	/**
	 * Prefix for this cache.
	 *
	 * @since 4.1.5
	 *
	 * @var string
	 */
	protected $prefix = '';

	/**
	 * Class constructor.
	 *
	 * @since 4.7.7.1
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'checkIfTableExists' ] ); // This needs to run on init because the DB
		// class gets instantiated along with the cache class.
	}

	/**
	 * Checks if the cache table exists and creates it if it doesn't.
	 *
	 * @since 4.7.7.1
	 *
	 * @return void
	 */
	public function checkIfTableExists() {
		if ( ! aioseo()->core->db->tableExists( $this->table ) ) {
			aioseo()->preUpdates->createCacheTable();
		}
	}

	/**
	 * Returns the cache value for a key if it exists and is not expired.
	 *
	 * @since 4.1.5
	 *
	 * @param  string     $key            The cache key name. Use a '%' for a like query.
	 * @param  bool|array $allowedClasses Whether to allow objects to be returned.
	 * @return mixed                      The value or null if the cache does not exist.
	 */
	public function get( $key, $allowedClasses = false ) {
		$key = $this->prepareKey( $key );
		if ( isset( self::$cache[ $key ] ) ) {
			return self::$cache[ $key ];
		}

		// Are we searching for a group of keys?
		$isLikeGet = preg_match( '/%/', (string) $key );

		$result = aioseo()->core->db
			->start( $this->table )
			->select( '`key`, `value`' )
			->whereRaw( '( `expiration` IS NULL OR `expiration` > \'' . aioseo()->helpers->timeToMysql( time() ) . '\' )' );

		$isLikeGet ?
			$result->whereRaw( '`key` LIKE \'' . $key . '\'' ) :
			$result->where( 'key', $key );

		$result->output( ARRAY_A )->run();

		// If we have nothing in the cache let's return a hard null.
		$values = $result->nullSet() ? null : $result->result();

		// If we have something let's normalize it.
		if ( $values ) {
			foreach ( $values as &$value ) {
				$value['value'] = aioseo()->helpers->maybeUnserialize( $value['value'], $allowedClasses );
			}
			// Return only the single cache value.
			if ( ! $isLikeGet ) {
				$values = $values[0]['value'];
			}
		}

		// Return values without a static cache.
		// This is here because clearing the like cache is not simple.
		if ( $isLikeGet ) {
			return $values;
		}

		self::$cache[ $key ] = $values;

		return self::$cache[ $key ];
	}

	/**
	 * Updates the given cache or creates it if it doesn't exist.
	 *
	 * @since 4.1.5
	 *
	 * @param  string $key        The cache key name.
	 * @param  mixed  $value      The value.
	 * @param  int    $expiration The expiration time in seconds. Defaults to 24 hours. 0 to no expiration.
	 * @return void
	 */
	public function update( $key, $value, $expiration = DAY_IN_SECONDS ) {
		// If the value is null we'll convert it and give it a shorter expiration.
		if ( null === $value ) {
			$value      = false;
			$expiration = 10 * MINUTE_IN_SECONDS;
		}

		$serializedValue = serialize( $value );
		$expiration      = 0 < $expiration ? aioseo()->helpers->timeToMysql( time() + $expiration ) : null;

		aioseo()->core->db->insert( $this->table )
			->set( [
				'key'        => $this->prepareKey( $key ),
				'value'      => $serializedValue,
				'expiration' => $expiration,
				'created'    => aioseo()->helpers->timeToMysql( time() ),
				'updated'    => aioseo()->helpers->timeToMysql( time() )
			] )
			->onDuplicate( [
				'value'      => $serializedValue,
				'expiration' => $expiration,
				'updated'    => aioseo()->helpers->timeToMysql( time() )
			] )
			->run();

		$this->updateStatic( $key, $value );
	}

	/**
	 * Deletes the given cache key.
	 *
	 * @since 4.1.5
	 *
	 * @param  string $key The cache key.
	 * @return void
	 */
	public function delete( $key ) {
		$key = $this->prepareKey( $key );

		aioseo()->core->db->delete( $this->table )
			->where( 'key', $key )
			->run();

		$this->clearStatic( $key );
	}

	/**
	 * Prepares the key before using the cache.
	 *
	 * @since 4.1.5
	 *
	 * @param  string $key The key to prepare.
	 * @return string      The prepared key.
	 */
	private function prepareKey( $key ) {
		$key = trim( $key );
		$key = $this->prefix && 0 !== strpos( $key, $this->prefix ) ? $this->prefix . $key : $key;

		if ( aioseo()->helpers->isDev() && 80 < mb_strlen( $key, 'UTF-8' ) ) {
			throw new \Exception( 'You are using a cache key that is too large, shorten your key and try again: [' . esc_html( $key ) . ']' );
		}

		return $key;
	}

	/**
	 * Clears all of our cache.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	public function clear() {
		// Bust the tableExists and columnExists cache.
		aioseo()->internalOptions->database->installedTables = '';

		if ( $this->prefix ) {
			$this->clearPrefix( '' );

			return;
		}

		// Try to acquire the lock.
		if ( ! aioseo()->core->db->acquireLock( 'aioseo_cache_clear_lock', 0 ) ) {
			// If we couldn't acquire the lock, exit early without doing anything.
			// This means another process is already clearing the cache.
			return;
		}

		// If we find the activation redirect, we'll need to reset it after clearing.
		$activationRedirect = $this->get( 'activation_redirect' );

		// Create a temporary table with the same structure.
		$table    = aioseo()->core->db->prefix . $this->table;
		$newTable = aioseo()->core->db->prefix . $this->table . '_new';
		$oldTable = aioseo()->core->db->prefix . $this->table . '_old';

		try {
			// Drop the temp table if it exists from a previous failed attempt.
			if ( false === aioseo()->core->db->execute( "DROP TABLE IF EXISTS {$newTable}" ) ) {
				throw new \Exception( 'Failed to drop temporary table' );
			}

			// Create the new empty table with the same structure.
			if ( false === aioseo()->core->db->execute( "CREATE TABLE {$newTable} LIKE {$table}" ) ) {
				throw new \Exception( 'Failed to create temporary table' );
			}

			// Rename tables (atomic operation in MySQL).
			if ( false === aioseo()->core->db->execute( "RENAME TABLE {$table} TO {$oldTable}, {$newTable} TO {$table}" ) ) {
				throw new \Exception( 'Failed to rename tables' );
			}

			// Drop the old table.
			if ( false === aioseo()->core->db->execute( "DROP TABLE {$oldTable}" ) ) {
				throw new \Exception( 'Failed to drop old table' );
			}
		} catch ( \Exception $e ) {
			// If something fails, ensure we clean up any temporary tables.
			aioseo()->core->db->execute( "DROP TABLE IF EXISTS {$newTable}" );
			aioseo()->core->db->execute( "DROP TABLE IF EXISTS {$oldTable}" );

			// Truncate table to clear the cache.
			aioseo()->core->db->truncate( $this->table )->run();
		}

		$this->clearStatic();

		if ( $activationRedirect ) {
			$this->update( 'activation_redirect', $activationRedirect, 30 );
		}
	}

	/**
	 * Clears all of our cache under a certain prefix.
	 *
	 * @since 4.1.5
	 *
	 * @param  string $prefix A prefix to clear or empty to clear everything.
	 * @return void
	 */
	public function clearPrefix( $prefix ) {
		$prefix = $this->prepareKey( $prefix );

		aioseo()->core->db->delete( $this->table )
			->whereRaw( "`key` LIKE '$prefix%'" )
			->run();

		$this->clearStaticPrefix( $prefix );
	}

	/**
	 * Clears all of our static in-memory cache of a prefix.
	 *
	 * @since 4.1.5
	 *
	 * @param  string $prefix A prefix to clear.
	 * @return void
	 */
	private function clearStaticPrefix( $prefix ) {
		$prefix = $this->prepareKey( $prefix );
		foreach ( array_keys( self::$cache ) as $key ) {
			if ( 0 === strpos( $key, $prefix ) ) {
				unset( self::$cache[ $key ] );
			}
		}
	}

	/**
	 * Clears all of our static in-memory cache.
	 *
	 * @since 4.1.5
	 *
	 * @param  string $key A key to clear.
	 * @return void
	 */
	private function clearStatic( $key = null ) {
		if ( empty( $key ) ) {
			self::$cache = [];

			return;
		}

		unset( self::$cache[ $this->prepareKey( $key ) ] );
	}

	/**
	 * Clears all of our static in-memory cache or the cache for a single given key.
	 *
	 * @since 4.7.1
	 *
	 * @param  string $key   A key to clear (optional).
	 * @param  string $value A value to update (optional).
	 * @return void
	 */
	private function updateStatic( $key = null, $value = null ) {
		if ( empty( $key ) ) {
			$this->clearStatic( $key );

			return;
		}

		self::$cache[ $this->prepareKey( $key ) ] = $value;
	}

	/**
	 * Returns the cache table name.
	 *
	 * @since 4.1.5
	 *
	 * @return string
	 */
	public function getTableName() {
		return $this->table;
	}
}Common/Utils/Addons.php000066600000071455151135505570011044 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Utils;

/**
 * Contains helper methods specific to the addons.
 *
 * @since 4.0.0
 */
class Addons {
	/**
	 * Holds our list of loaded addons.
	 *
	 * @since 4.1.0
	 *
	 * @var array
	 */
	protected $loadedAddons = [];

	/**
	 * The addons URL.
	 *
	 * @since 4.1.8
	 *
	 * @var string
	 */
	protected $addonsUrl = 'https://licensing-cdn.aioseo.com/keys/lite/all-in-one-seo-pack-pro.json';

	/**
	 * The main Image SEO addon class.
	 *
	 * @since 4.4.2
	 *
	 * @var \AIOSEO\Plugin\Addon\ImageSeo\ImageSeo
	 */
	private $imageSeo = null;

	/**
	 * The main Index Now addon class.
	 *
	 * @since 4.4.2
	 *
	 * @var \AIOSEO\Plugin\Addon\IndexNow\IndexNow
	 */
	private $indexNow = null;

	/**
	 * The main Local Business addon class.
	 *
	 * @since 4.4.2
	 *
	 * @var \AIOSEO\Plugin\Addon\LocalBusiness\LocalBusiness
	 */
	private $localBusiness = null;

	/**
	 * The main News Sitemap addon class.
	 *
	 * @since 4.4.2
	 *
	 * @var \AIOSEO\Plugin\Addon\NewsSitemap\NewsSitemap
	 */
	private $newsSitemap = null;

	/**
	 * The main Redirects addon class.
	 *
	 * @since 4.4.2
	 *
	 * @var \AIOSEO\Plugin\Addon\Redirects\Redirects
	 */
	private $redirects = null;

	/**
	 * The main REST API addon class.
	 *
	 * @since 4.4.2
	 *
	 * @var \AIOSEO\Plugin\Addon\RestApi\RestApi
	 */
	private $restApi = null;

	/**
	 * The main Video Sitemap addon class.
	 *
	 * @since 4.4.2
	 *
	 * @var \AIOSEO\Plugin\Addon\VideoSitemap\VideoSitemap
	 */
	private $videoSitemap = null;

	/**
	 * The main Link Assistant addon class.
	 *
	 * @since 4.4.2
	 *
	 * @var \AIOSEO\Plugin\Addon\LinkAssistant\LinkAssistant
	 */
	private $linkAssistant = null;

	/**
	 * The main EEAT addon class.
	 *
	 * @since 4.5.4
	 *
	 * @var \AIOSEO\Plugin\Addon\LinkAssistant\LinkAssistant
	 */
	private $eeat = null;

	/**
	 * Returns our addons.
	 *
	 * @since 4.0.0
	 *
	 * @param  boolean $flushCache Whether or not to flush the cache.
	 * @return array               An array of addon data.
	 */
	public function getAddons( $flushCache = false ) {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		$addons        = aioseo()->core->cache->get( 'addons' );
		$defaultAddons = $this->getDefaultAddons();
		if ( null === $addons || $flushCache ) {
			$response = aioseo()->helpers->wpRemoteGet( $this->getAddonsUrl() );
			if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
				$addons = json_decode( wp_remote_retrieve_body( $response ), true );
			}

			if ( ! $addons || ! empty( $addons->error ) ) {
				$addons = $defaultAddons;
			}

			aioseo()->core->cache->update( 'addons', $addons );
		}

		// Convert the addons array to objects using JSON. This is essential because we have lots of addons that rely on this to be an object, and changing it to an array would break them.

		$addons = json_decode( wp_json_encode( $addons ) );

		$installedPlugins = array_keys( get_plugins() );
		foreach ( $addons as $key => $addon ) {
			if ( ! is_object( $addon ) ) {
				continue;
			}

			$addons[ $key ]->basename          = $this->getAddonBasename( $addon->sku );
			$addons[ $key ]->installed         = in_array( $this->getAddonBasename( $addon->sku ), $installedPlugins, true );
			$addons[ $key ]->isActive          = is_plugin_active( $addons[ $key ]->basename );
			$addons[ $key ]->canInstall        = $this->canInstall();
			$addons[ $key ]->canActivate       = $this->canActivate();
			$addons[ $key ]->canUpdate         = $this->canUpdate();
			$addons[ $key ]->capability        = $this->getManageCapability( $addon->sku );
			$addons[ $key ]->minimumVersion    = '0.0.0';
			$addons[ $key ]->hasMinimumVersion = false;
			$addons[ $key ]->featured          = $this->setFeatured( $addon );
		}

		return $this->sortAddons( $addons );
	}

	/**
	 * Set the featured status for an addon.
	 *
	 * @since 4.6.9
	 *
	 * @param  object $addon The addon.
	 * @return bool          The featured status.
	 */
	protected function setFeatured( $addon ) {
		$defaultAddons = $this->getDefaultAddons();
		$featured      = false;

		// Find the addon in the default addons list and get the featured status.
		foreach ( $defaultAddons as $defaultAddon ) {
			if ( $addon->sku !== $defaultAddon['sku'] ) {
				continue;
			}

			$featured = ! empty( $addon->featured )
				? $addon->featured
				: (
					! empty( $defaultAddon['featured'] )
						? $defaultAddon['featured']
						: $featured
					);
			break;
		}

		return $featured;
	}

	/**
	 * Sort the addons by moving the featured ones to the top.
	 *
	 * @since 4.6.9
	 *
	 * @param  array $addons The addons to sort.
	 * @return array         The sorted addons.
	 */
	protected function sortAddons( $addons ) {
		if ( ! is_array( $addons ) ) {
			return $addons;
		}

		// Sort the addons by moving the featured ones to the top.
		usort( $addons, function( $a, $b ) {
			// Sort by featured value. It can be false, or numerical. If it's false, it will be moved to the bottom.
			// If it's numerical, it will be moved to the top. Numbers will be sorted in descending order.
			$featuredA = ! empty( $a->featured ) ? $a->featured : 0;
			$featuredB = ! empty( $b->featured ) ? $b->featured : 0;

			if ( $featuredA === $featuredB ) {
				return 0;
			}

			return $featuredA > $featuredB ? -1 : 1;
		} );

		return $addons;
	}

	/**
	 * Returns the required capability to manage the addon.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $sku The addon sku.
	 * @return string      The required capability.
	 */
	protected function getManageCapability( $sku ) {
		$capability = apply_filters( 'aioseo_manage_seo', 'aioseo_manage_seo' );

		switch ( $sku ) {
			case 'aioseo-image-seo':
				$capability = 'aioseo_search_appearance_settings';
				break;
			case 'aioseo-video-sitemap':
			case 'aioseo-news-sitemap':
				$capability = 'aioseo_sitemap_settings';
				break;
			case 'aioseo-redirects':
				$capability = 'aioseo_redirects_settings';
				break;
			case 'aioseo-local-business':
				$capability = 'aioseo_local_seo_settings';
				break;
			case 'aioseo-index-now':
				$capability = 'aioseo_general_settings';
				break;
		}

		return $capability;
	}

	/**
	 * Check to see if there are unlicensed addons installed and activated.
	 *
	 * @since 4.1.3
	 *
	 * @return boolean True if there are unlicensed addons, false if not.
	 */
	public function unlicensedAddons() {
		$unlicensed = [
			'addons'  => [],
			// Translators: 1 - Opening bold tag, 2 - Plugin short name ("AIOSEO"), 3 - "Pro", 4 - Closing bold tag.
			'message' => sprintf(
				// Translators: 1 - Opening HTML strong tag, 2 - The short plugin name ("AIOSEO"), 3 - "Pro", 4 - Closing HTML strong tag.
				__( 'The following addons cannot be used, because they require %1$s%2$s %3$s%4$s to work:', 'all-in-one-seo-pack' ),
				'<strong>',
				AIOSEO_PLUGIN_SHORT_NAME,
				'Pro',
				'</strong>'
			)
		];

		$addons = $this->getAddons();
		foreach ( $addons as $addon ) {
			if ( ! is_object( $addon ) ) {
				continue;
			}

			if ( $addon->isActive ) {
				$unlicensed['addons'][] = $addon;
			}
		}

		return $unlicensed;
	}

	/**
	 * Get the data for a specific addon.
	 *
	 * We need this function to refresh the data of a given addon because installation links expire after one hour.
	 *
	 * @since 4.0.0
	 *
	 * @param  string      $sku        The addon sku.
	 * @param  bool        $flushCache Whether or not to flush the cache.
	 * @return null|object             The addon.
	 */
	public function getAddon( $sku, $flushCache = false ) {
		$addon     = null;
		$allAddons = $this->getAddons( $flushCache );
		foreach ( $allAddons as $a ) {
			if ( $sku === $a->sku ) {
				$addon = $a;
			}
		}

		if ( ! $addon || ! empty( $addon->error ) ) {
			$addon = $this->getDefaultAddon( $sku );
			aioseo()->core->cache->update( 'addon_' . $sku, $addon, 10 * MINUTE_IN_SECONDS );
		}

		return $addon;
	}

	/**
	 * Checks if the specified addon is activated.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $sku The sku to check.
	 * @return string      The addon basename.
	 */
	public function getAddonBasename( $sku ) {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';
		$plugins = get_plugins();

		$keys = array_keys( $plugins );
		foreach ( $keys as $key ) {
			if ( preg_match( '|^' . $sku . '|', (string) $key ) ) {
				return $key;
			}
		}

		return $sku;
	}

	/**
	 * Returns an array of levels connected to an addon.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $addonName The addon name.
	 * @return array             The array of levels.
	 */
	public function getAddonLevels( $addonName ) {
		$addons = $this->getAddons();
		foreach ( $addons as $addon ) {
			if ( $addonName !== $addon->sku ) {
				continue;
			}

			if ( ! isset( $addon->levels ) ) {
				return [];
			}

			return $addon->levels;
		}

		return [];
	}

	/**
	 * Returns a list of addon SKUs.
	 *
	 * @since 4.5.6
	 *
	 * @return array The addon SKUs.
	 */
	public function getAddonSkus() {
		$addons = $this->getAddons();
		if ( empty( $addons ) ) {
			return [];
		}

		return array_map( function( $addon ) {
			return $addon->sku;
		}, $addons );
	}

	/**
	 * Get the URL to get addons.
	 *
	 * @since 4.1.8
	 *
	 * @return string The URL.
	 */
	protected function getAddonsUrl() {
		$url = $this->addonsUrl;
		if ( defined( 'AIOSEO_ADDONS_URL' ) ) {
			$url = AIOSEO_ADDONS_URL;
		}

		if ( defined( 'AIOSEO_INTERNAL_ADDONS' ) && AIOSEO_INTERNAL_ADDONS ) {
			$url = add_query_arg( 'internal', true, $url );
		}

		return $url;
	}

	/**
	 * Installs and activates a given addon or plugin.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name    The addon name/sku.
	 * @param  bool   $network Whether or not we are in a network environment.
	 * @return bool            Whether or not the installation was succesful.
	 */
	public function installAddon( $name, $network = false ) {
		if ( ! $this->canInstall() ) {
			return false;
		}

		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/template.php';
		require_once ABSPATH . 'wp-admin/includes/class-wp-screen.php';
		require_once ABSPATH . 'wp-admin/includes/screen.php';

		// Set the current screen to avoid undefined notices.
		set_current_screen( 'toplevel_page_aioseo' );

		// Prepare variables.
		$url = esc_url_raw(
			add_query_arg(
				[
					'page' => 'aioseo-settings',
				],
				admin_url( 'admin.php' )
			)
		);

		// Do not allow WordPress to search/download translations, as this will break JS output.
		remove_action( 'upgrader_process_complete', [ 'Language_Pack_Upgrader', 'async_upgrade' ], 20 );

		// Create the plugin upgrader with our custom skin.
		$installer = new Utils\PluginUpgraderSilentAjax( new Utils\PluginUpgraderSkin() );

		// Activate the plugin silently.
		$pluginUrl = ! empty( $installer->pluginSlugs[ $name ] ) ? $installer->pluginSlugs[ $name ] : $name;
		$activated = activate_plugin( $pluginUrl, '', $network );

		if ( ! is_wp_error( $activated ) ) {
			return $name;
		}

		// Using output buffering to prevent the FTP form from being displayed in the screen.
		ob_start();
		$creds = request_filesystem_credentials( $url, '', false, false, null );
		ob_end_clean();

		// Check for file system permissions.
		$fs = aioseo()->core->fs->noConflict();
		$fs->init( $creds );
		if ( false === $creds || ! $fs->isWpfsValid() ) {
			return false;
		}

		// Error check.
		if ( ! method_exists( $installer, 'install' ) ) {
			return false;
		}

		$installLink = ! empty( $installer->pluginLinks[ $name ] ) ? $installer->pluginLinks[ $name ] : null;

		// Check if this is an addon and if we have a download link.
		if ( empty( $installLink ) ) {
			$downloadUrl = aioseo()->addons->getDownloadUrl( $name );
			if ( empty( $downloadUrl ) ) {
				return false;
			}

			$installLink = $downloadUrl;
		}

		$installer->install( $installLink );

		// Flush the cache and return the newly installed plugin basename.
		wp_cache_flush();

		$pluginBasename = $installer->plugin_info();
		if ( ! $pluginBasename ) {
			return false;
		}

		// Activate the plugin silently.
		$activated = activate_plugin( $pluginBasename, '', $network );

		if ( is_wp_error( $activated ) ) {
			return false;
		}

		return $pluginBasename;
	}

	/**
	 * Determine if addons/plugins can be installed.
	 *
	 * @since 4.0.0
	 *
	 * @return bool True if yes, false if not.
	 */
	public function canInstall() {
		if (
			function_exists( 'wp_get_current_user' ) &&
			is_user_logged_in() &&
			! current_user_can( 'install_plugins' ) &&
			! aioseo()->helpers->isDoingWpCli()
		) {
			return false;
		}

		// Determine whether file modifications are allowed.
		if ( ! wp_is_file_mod_allowed( 'aioseo_can_install' ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Determine if addons/plugins can be updated.
	 *
	 * @since 4.1.6
	 *
	 * @return bool True if yes, false if not.
	 */
	public function canUpdate() {
		if (
			function_exists( 'wp_get_current_user' ) &&
			is_user_logged_in() &&
			! current_user_can( 'update_plugins' ) &&
			! aioseo()->helpers->isDoingWpCli()
		) {
			return false;
		}

		// Determine whether file modifications are allowed.
		if ( ! wp_is_file_mod_allowed( 'aioseo_can_update' ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Determine if addons/plugins can be activated.
	 *
	 * @since 4.1.3
	 *
	 * @return bool True if yes, false if not.
	 */
	public function canActivate() {
		if (
			function_exists( 'wp_get_current_user' ) &&
			is_user_logged_in() &&
			! current_user_can( 'activate_plugins' ) &&
			! aioseo()->helpers->isDoingWpCli()
		) {
			return false;
		}

		return true;
	}

	/**
	 * Load an addon into aioseo.
	 *
	 * @since 4.1.0
	 *
	 * @param  string $slug
	 * @param  object $addon Addon class instance.
	 * @return void
	 */
	public function loadAddon( $slug, $addon ) {
		$this->{$slug}        = $addon;
		$this->loadedAddons[] = $slug;
	}

	/**
	 * Return a loaded addon.
	 *
	 * @since 4.1.0
	 *
	 * @param  string $slug
	 * @return object|null
	 */
	public function getLoadedAddon( $slug ) {
		return isset( $this->{$slug} ) ? $this->{$slug} : null;
	}

	/**
	 * Returns loaded addons
	 *
	 * @since 4.1.0
	 *
	 * @return array
	 */
	public function getLoadedAddons() {
		$loadedAddonsList = [];
		if ( ! empty( $this->loadedAddons ) ) {
			foreach ( $this->loadedAddons as $addonSlug ) {
				$loadedAddonsList[ $addonSlug ] = $this->{$addonSlug};
			}
		}

		return $loadedAddonsList;
	}

	/**
	 * Run a function through all addons that support it.
	 *
	 * @since 4.2.3
	 *
	 * @param  string $class    The class name.
	 * @param  string $function The function name.
	 * @param  array  $args     The args for the function.
	 * @return array            The response from each addon.
	 */
	public function doAddonFunction( $class, $function, $args = [] ) {
		$addonResponses = [];

		foreach ( $this->getLoadedAddons() as $addonSlug => $addon ) {
			if ( isset( $addon->$class ) && method_exists( $addon->$class, $function ) ) {
				$addonResponses[ $addonSlug ] = call_user_func_array( [ $addon->$class, $function ], $args );
			}
		}

		return $addonResponses;
	}

	/**
	 * Merges the data for Vue.
	 *
	 * @since 4.4.1
	 *
	 * @param  array  $data The data to merge.
	 * @param  string $page The current page.
	 * @return array        The data.
	 */
	public function getVueData( $data = [], $page = null ) {
		foreach ( $this->getLoadedAddons() as $addon ) {
			if ( isset( $addon->helpers ) && method_exists( $addon->helpers, 'getVueData' ) ) {
				$data = array_merge( $data, $addon->helpers->getVueData( $data, $page ) );
			}
		}

		return $data;
	}

	/**
	 * Retrieves a default addon with whatever information is needed if the API cannot be reached.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $sku The sku of the addon.
	 * @return array       An array of addon data.
	 */
	public function getDefaultAddon( $sku ) {
		$addons = $this->getDefaultAddons();
		$addon  = [];
		foreach ( $addons as $a ) {
			if ( $a['sku'] === $sku ) {
				$addon = $a;
			}
		}

		return $addon;
	}

	/**
	 * Retrieves a default list of addons if the API cannot be reached.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of addons.
	 */
	protected function getDefaultAddons() {
		return json_decode( wp_json_encode( [
			[
				'sku'                => 'aioseo-eeat',
				'name'               => 'Author SEO (E-E-A-T)',
				'version'            => '1.0.0',
				'image'              => null,
				'icon'               => 'svg-eeat',
				'levels'             => [
					'plus',
					'pro',
					'elite',
				],
				'currentLevels'      => [
					'plus',
					'pro',
					'elite'
				],
				'requiresUpgrade'    => true,
				'description'        => '<p>Optimize your site for Google\'s E-E-A-T ranking factor by proving your writer\'s expertise through author schema markup and new UI elements.</p>',
				'descriptionVersion' => 0,
				'productUrl'         => 'https://aioseo.com/author-seo-eeat/',
				'learnMoreUrl'       => 'https://aioseo.com/author-seo-eeat/',
				'manageUrl'          => 'https://route#aioseo-search-appearance:author-seo',
				'basename'           => 'aioseo-eeat/aioseo-eeat.php',
				'installed'          => false,
				'isActive'           => false,
				'canInstall'         => false,
				'canActivate'        => false,
				'canUpdate'          => false,
				'capability'         => $this->getManageCapability( 'aioseo-eeat' ),
				'minimumVersion'     => '0.0.0',
				'hasMinimumVersion'  => false,
				'featured'           => 300
			],
			[
				'sku'                => 'aioseo-redirects',
				'name'               => 'Redirection Manager',
				'version'            => '1.0.0',
				'image'              => null,
				'icon'               => 'svg-redirect',
				'levels'             => [
					'agency',
					'business',
					'pro',
					'elite'
				],
				'currentLevels'      => [
					'pro',
					'elite'
				],
				'requiresUpgrade'    => true,
				'description'        => '<p>Our Redirection Manager allows you to easily create and manage redirects for your broken links to avoid confusing search engines and users, as well as losing valuable backlinks. It even automatically sends users and search engines from your old URLs to your new ones.</p>', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				'descriptionVersion' => 0,
				'productUrl'         => 'https://aioseo.com/features/redirection-manager/',
				'learnMoreUrl'       => 'https://aioseo.com/features/redirection-manager/',
				'manageUrl'          => 'https://route#aioseo-redirects:redirects',
				'basename'           => 'aioseo-redirects/aioseo-redirects.php',
				'installed'          => false,
				'isActive'           => false,
				'canInstall'         => false,
				'canActivate'        => false,
				'canUpdate'          => false,
				'capability'         => $this->getManageCapability( 'aioseo-redirects' ),
				'minimumVersion'     => '0.0.0',
				'hasMinimumVersion'  => false,
				'featured'           => 200
			],
			[
				'sku'                => 'aioseo-link-assistant',
				'name'               => 'Link Assistant',
				'version'            => '1.0.0',
				'image'              => null,
				'icon'               => 'svg-link-assistant',
				'levels'             => [
					'agency',
					'pro',
					'elite'
				],
				'currentLevels'      => [
					'pro',
					'elite'
				],
				'requiresUpgrade'    => true,
				'description'        => '<p>Super-charge your SEO with Link Assistant! Get relevant suggestions for adding internal links to older content as well as finding any orphaned posts that have no internal links. Use our reporting feature to see all link suggestions or add them directly from any page or post.</p>', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				'descriptionVersion' => 0,
				'productUrl'         => 'https://aioseo.com/feature/internal-link-assistant/',
				'learnMoreUrl'       => 'https://aioseo.com/feature/internal-link-assistant/',
				'manageUrl'          => 'https://route#aioseo-link-assistant:overview',
				'basename'           => 'aioseo-link-assistant/aioseo-link-assistant.php',
				'installed'          => false,
				'isActive'           => false,
				'canInstall'         => false,
				'canActivate'        => false,
				'canUpdate'          => false,
				'capability'         => $this->getManageCapability( 'aioseo-link-assistant' ),
				'minimumVersion'     => '0.0.0',
				'hasMinimumVersion'  => false,
				'featured'           => 100
			],
			[
				'sku'                => 'aioseo-video-sitemap',
				'name'               => 'Video Sitemap',
				'version'            => '1.0.0',
				'image'              => null,
				'icon'               => 'svg-sitemaps-pro',
				'levels'             => [
					'individual',
					'business',
					'agency',
					'pro',
					'elite'
				],
				'currentLevels'      => [
					'pro',
					'elite'
				],
				'requiresUpgrade'    => true,
				'description'        => '<p>The Video Sitemap works in much the same way as the XML Sitemap module, it generates an XML Sitemap specifically for video content on your site. Search engines use this information to display rich snippet information in search results.</p>', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				'descriptionVersion' => 0,
				'productUrl'         => 'https://aioseo.com/video-sitemap',
				'learnMoreUrl'       => 'https://aioseo.com/video-sitemap',
				'manageUrl'          => 'https://route#aioseo-sitemaps:video-sitemap',
				'basename'           => 'aioseo-video-sitemap/aioseo-video-sitemap.php',
				'installed'          => false,
				'isActive'           => false,
				'canInstall'         => false,
				'canActivate'        => false,
				'canUpdate'          => false,
				'capability'         => $this->getManageCapability( 'aioseo-video-sitemap' ),
				'minimumVersion'     => '0.0.0',
				'hasMinimumVersion'  => false
			],
			[
				'sku'                => 'aioseo-local-business',
				'name'               => 'Local Business SEO',
				'version'            => '1.0.0',
				'image'              => null,
				'icon'               => 'svg-local-business',
				'levels'             => [
					'business',
					'agency',
					'plus',
					'pro',
					'elite'
				],
				'currentLevels'      => [
					'plus',
					'pro',
					'elite'
				],
				'requiresUpgrade'    => true,
				'description'        => '<p>Local Business schema markup enables you to tell Google about your business, including your business name, address and phone number, opening hours and price range. This information may be displayed as a Knowledge Graph card or business carousel.</p>', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				'descriptionVersion' => 0,
				'productUrl'         => 'https://aioseo.com/local-business',
				'learnMoreUrl'       => 'https://aioseo.com/local-business',
				'manageUrl'          => 'https://route#aioseo-local-seo:locations',
				'basename'           => 'aioseo-local-business/aioseo-local-business.php',
				'installed'          => false,
				'isActive'           => false,
				'canInstall'         => false,
				'canActivate'        => false,
				'canUpdate'          => false,
				'capability'         => $this->getManageCapability( 'aioseo-local-business' ),
				'minimumVersion'     => '0.0.0',
				'hasMinimumVersion'  => false
			],
			[
				'sku'                => 'aioseo-news-sitemap',
				'name'               => 'News Sitemap',
				'version'            => '1.0.0',
				'image'              => null,
				'icon'               => 'svg-sitemaps-pro',
				'levels'             => [
					'business',
					'agency',
					'pro',
					'elite'
				],
				'currentLevels'      => [
					'pro',
					'elite'
				],
				'requiresUpgrade'    => true,
				'description'        => '<p>Our Google News Sitemap lets you control which content you submit to Google News and only contains articles that were published in the last 48 hours. In order to submit a News Sitemap to Google, you must have added your site to Google’s Publisher Center and had it approved.</p>', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				'descriptionVersion' => 0,
				'productUrl'         => 'https://aioseo.com/news-sitemap',
				'learnMoreUrl'       => 'https://aioseo.com/news-sitemap',
				'manageUrl'          => 'https://route#aioseo-sitemaps:news-sitemap',
				'basename'           => 'aioseo-news-sitemap/aioseo-news-sitemap.php',
				'installed'          => false,
				'isActive'           => false,
				'canInstall'         => false,
				'canActivate'        => false,
				'canUpdate'          => false,
				'capability'         => $this->getManageCapability( 'aioseo-news-sitemap' ),
				'minimumVersion'     => '0.0.0',
				'hasMinimumVersion'  => false
			],
			[
				'sku'                => 'aioseo-index-now',
				'name'               => 'IndexNow',
				'version'            => '1.0.0',
				'image'              => null,
				'icon'               => 'svg-sitemaps-pro',
				'levels'             => [
					'agency',
					'business',
					'basic',
					'plus',
					'pro',
					'elite'
				],
				'currentLevels'      => [
					'basic',
					'plus',
					'pro',
					'elite'
				],
				'requiresUpgrade'    => true,
				'description'        => '<p>Add IndexNow support to instantly notify search engines when your content has changed. This helps the search engines to prioritize the changes on your website and helps you rank faster.</p>', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				'descriptionVersion' => 0,
				'downloadUrl'        => '',
				'productUrl'         => 'https://aioseo.com/index-now/',
				'learnMoreUrl'       => 'https://aioseo.com/index-now/',
				'manageUrl'          => 'https://route#aioseo-settings:webmaster-tools',
				'basename'           => 'aioseo-index-now/aioseo-index-now.php',
				'installed'          => false,
				'isActive'           => false,
				'canInstall'         => false,
				'canActivate'        => false,
				'canUpdate'          => false,
				'capability'         => $this->getManageCapability( 'aioseo-index-now' ),
				'minimumVersion'     => '0.0.0',
				'hasMinimumVersion'  => false
			],
			[
				'sku'                => 'aioseo-rest-api',
				'name'               => 'REST API',
				'version'            => '1.0.0',
				'image'              => null,
				'icon'               => 'svg-code',
				'levels'             => [
					'plus',
					'pro',
					'elite'
				],
				'currentLevels'      => [
					'plus',
					'pro',
					'elite'
				],
				'requiresUpgrade'    => true,
				'description'        => '<p>Manage your post and term SEO meta via the WordPress REST API. This addon also works seamlessly with headless WordPress installs.</p>', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				'descriptionVersion' => 0,
				'downloadUrl'        => '',
				'productUrl'         => 'https://aioseo.com/feature/rest-api/',
				'learnMoreUrl'       => 'https://aioseo.com/feature/rest-api/',
				'manageUrl'          => null,
				'basename'           => 'aioseo-rest-api/aioseo-rest-api.php',
				'installed'          => false,
				'isActive'           => false,
				'canInstall'         => false,
				'canActivate'        => false,
				'canUpdate'          => false,
				'capability'         => null,
				'minimumVersion'     => '0.0.0',
				'hasMinimumVersion'  => false
			],
			[
				'sku'                => 'aioseo-image-seo',
				'name'               => 'Image SEO',
				'version'            => '1.0.0',
				'image'              => null,
				'icon'               => 'svg-image-seo',
				'levels'             => [
					'individual',
					'business',
					'agency',
					'plus',
					'pro',
					'elite',
				],
				'currentLevels'      => [
					'plus',
					'pro',
					'elite'
				],
				'requiresUpgrade'    => true,
				'description'        => '<p>Globally control the Title attribute and Alt text for images in your content. These attributes are essential for both accessibility and SEO.</p>',
				'descriptionVersion' => 0,
				'productUrl'         => 'https://aioseo.com/image-seo',
				'learnMoreUrl'       => 'https://aioseo.com/image-seo',
				'manageUrl'          => 'https://route#aioseo-search-appearance:media',
				'basename'           => 'aioseo-image-seo/aioseo-image-seo.php',
				'installed'          => false,
				'isActive'           => false,
				'canInstall'         => false,
				'canActivate'        => false,
				'canUpdate'          => false,
				'capability'         => $this->getManageCapability( 'aioseo-image-seo' ),
				'minimumVersion'     => '0.0.0',
				'hasMinimumVersion'  => false
			]
		] ), true );
	}

	/**
	 * Check for updates for all addons.
	 *
	 * @since 4.2.4
	 *
	 * @return void
	 */
	public function registerUpdateCheck() {}

	/**
	 * Updates a given addon or plugin.
	 *
	 * @since 4.4.3
	 *
	 * @param  string $name    The addon name/sku.
	 * @param  bool   $network Whether we are in a network environment.
	 * @return bool            Whether the installation was succesful.
	 */
	public function upgradeAddon( $name, $network ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return false;
	}

	/**
	 * Get the download URL for the given addon.
	 *
	 * @since 4.4.3
	 *
	 * @param  string $sku The addon sku.
	 * @return string      The download url for the addon.
	 */
	public function getDownloadUrl( $sku ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return '';
	}
}Common/Utils/Templates.php000066600000005504151135505570011562 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class Templates
 *
 * @since 4.0.17
 *
 * @package AIOSEO\Plugin\Common\Utils
 */
class Templates {
	/**
	 * This plugin absolute path.
	 *
	 * @since 4.0.17
	 *
	 * @var string
	 */
	protected $pluginPath = AIOSEO_DIR;

	/**
	 * Paths were our template files are located.
	 *
	 * @since 4.0.17
	 *
	 * @var string Array of paths.
	 */
	protected $paths = [
		'app/Common/Views'
	];

	/**
	 *
	 * The theme folder.
	 *
	 * @since 4.0.17
	 *
	 * @var string
	 */
	private $themeTemplatePath = 'aioseo/';

	/**
	 *
	 * A theme subfolder.
	 *
	 * @since 4.0.17
	 *
	 * @var string
	 */
	protected $themeTemplateSubpath = '';

	/**
	 * Locate a template file in the theme or our plugin paths.
	 *
	 * @since 4.0.17
	 *
	 * @param  string $templateName The template name.
	 * @return string               The template absolute path.
	 */
	public function locateTemplate( $templateName ) {
		// Try to find template file in the theme.
		$template = locate_template(
			[
				trailingslashit( $this->getThemeTemplatePath() ) . trailingslashit( $this->getThemeTemplateSubpath() ) . $templateName
			]
		);

		if ( ! $template ) {
			// Try paths, in order.
			foreach ( $this->paths as $path ) {
				$template = trailingslashit( $this->addPluginPath( $path ) ) . $templateName;
				if ( aioseo()->core->fs->exists( $template ) ) {
					break;
				}
			}
		}

		return apply_filters( 'aioseo_locate_template', $template, $templateName );
	}

	/**
	 * Includes a template if the file exists.
	 *
	 * @param  string $templateName The template path/name.php to be included.
	 * @param  null   $data         Data passed down to the template.
	 * @return void
	 */
	public function getTemplate( $templateName, $data = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$template = $this->locateTemplate( $templateName );
		if ( ! empty( $template ) and aioseo()->core->fs->exists( $template ) ) {
			include $template;
		}
	}

	/**
	 * Add this plugin path when trying the paths.
	 *
	 * @since 4.0.17
	 *
	 * @param  string $path A path.
	 * @return string       A path with the plugin absolute path.
	 */
	protected function addPluginPath( $path ) {
		return trailingslashit( $this->pluginPath ) . $path;
	}

	/**
	 * Returns the theme folder for templates.
	 *
	 * @since 4.0.17
	 *
	 * @return string The theme folder for templates.
	 */
	public function getThemeTemplatePath() {
		return apply_filters( 'aioseo_template_path', $this->themeTemplatePath );
	}

	/**
	 *
	 * Returns the theme subfolder for templates.
	 *
	 * @since 4.0.17
	 *
	 * @return string The theme subfolder for templates.
	 */
	public function getThemeTemplateSubpath() {
		return apply_filters( 'aioseo_template_subpath', $this->themeTemplateSubpath );
	}
}Common/Utils/NetworkCache.php000066600000005416151135505570012203 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Handles our network cache.
 *
 * @since 4.2.5
 */
class NetworkCache extends Cache {
	/**
	 * Returns the cache value for a key if it exists and is not expired.
	 *
	 * @since 4.2.5
	 *
	 * @param  string     $key            The cache key name. Use a '%' for a like query.
	 * @param  bool|array $allowedClasses Whether to allow objects to be returned.
	 * @return mixed                      The value or null if the cache does not exist.
	 */
	public function get( $key, $allowedClasses = false ) {
		if ( ! aioseo()->helpers->isPluginNetworkActivated() ) {
			return parent::get( $key, $allowedClasses );
		}

		aioseo()->helpers->switchToBlog( aioseo()->helpers->getNetworkId() );
		$value = parent::get( $key, $allowedClasses );
		aioseo()->helpers->restoreCurrentBlog();

		return $value;
	}

	/**
	 * Updates the given cache or creates it if it doesn't exist.
	 *
	 * @since 4.2.5
	 *
	 * @param  string $key        The cache key name.
	 * @param  mixed  $value      The value.
	 * @param  int    $expiration The expiration time in seconds. Defaults to 24 hours. 0 to no expiration.
	 * @return void
	 */
	public function update( $key, $value, $expiration = DAY_IN_SECONDS ) {
		if ( ! aioseo()->helpers->isPluginNetworkActivated() ) {
			parent::update( $key, $value, $expiration );

			return;
		}

		aioseo()->helpers->switchToBlog( aioseo()->helpers->getNetworkId() );
		parent::update( $key, $value, $expiration );
		aioseo()->helpers->restoreCurrentBlog();
	}

	/**
	 * Deletes the given cache key.
	 *
	 * @since 4.2.5
	 *
	 * @param  string $key The cache key.
	 * @return void
	 */
	public function delete( $key ) {
		if ( ! aioseo()->helpers->isPluginNetworkActivated() ) {
			parent::delete( $key );

			return;
		}

		aioseo()->helpers->switchToBlog( aioseo()->helpers->getNetworkId() );
		parent::delete( $key );
		aioseo()->helpers->restoreCurrentBlog();
	}

	/**
	 * Clears all of our cache.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	public function clear() {
		if ( ! aioseo()->helpers->isPluginNetworkActivated() ) {
			parent::clear();

			return;
		}

		aioseo()->helpers->switchToBlog( aioseo()->helpers->getNetworkId() );
		parent::clear();
		aioseo()->helpers->restoreCurrentBlog();
	}

	/**
	 * Clears all of our cache under a certain prefix.
	 *
	 * @since 4.2.5
	 *
	 * @param  string $prefix A prefix to clear or empty to clear everything.
	 * @return void
	 */
	public function clearPrefix( $prefix ) {
		if ( ! aioseo()->helpers->isPluginNetworkActivated() ) {
			parent::clearPrefix( $prefix );

			return;
		}

		aioseo()->helpers->switchToBlog( aioseo()->helpers->getNetworkId() );
		parent::clearPrefix( $prefix );
		aioseo()->helpers->restoreCurrentBlog();
	}
}Common/Utils/CachePrune.php000066600000004074151135505570011642 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Handles our cache pruning.
 *
 * @since 4.1.5
 */
class CachePrune {
	/**
	 * The action for the scheduled cache prune.
	 *
	 * @since 4.1.5
	 *
	 * @var string
	 */
	private $pruneAction = 'aioseo_cache_prune';

	/**
	 * The action for the scheduled old cache clean.
	 *
	 * @since 4.1.5
	 *
	 * @var string
	 */
	private $optionCacheCleanAction = 'aioseo_old_cache_clean';

	/**
	 * Class constructor.
	 *
	 * @since 4.1.5
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'init' ] );
	}

	/**
	 * Inits our class.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	public function init() {
		add_action( $this->pruneAction, [ $this, 'prune' ] );
		add_action( $this->optionCacheCleanAction, [ $this, 'optionCacheClean' ] );

		if ( ! is_admin() ) {
			return;
		}

		if ( ! aioseo()->actionScheduler->isScheduled( $this->pruneAction ) ) {
			aioseo()->actionScheduler->scheduleRecurrent( $this->pruneAction, 0, DAY_IN_SECONDS );
		}
	}

	/**
	 * Prunes our expired cache.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	public function prune() {
		aioseo()->core->db->delete( aioseo()->core->cache->getTableName() )
			->whereRaw( '( `expiration` IS NOT NULL AND expiration <= \'' . aioseo()->helpers->timeToMysql( time() ) . '\' )' )
			->run();
	}

	/**
	 * Cleans our old options cache.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	public function optionCacheClean() {
		$optionCache = aioseo()->core->db->delete( aioseo()->core->db->db->options, true )
			->whereRaw( "option_name LIKE '\_aioseo\_cache\_%'" )
			->limit( 10000 )
			->run();

		// Schedule a new run if we're not done cleaning.
		if ( 0 !== $optionCache->db->rows_affected ) {
			aioseo()->actionScheduler->scheduleSingle( $this->optionCacheCleanAction, MINUTE_IN_SECONDS, [], true );
		}
	}

	/**
	 * Returns the action name for the old cache clean.
	 *
	 * @since 4.1.5
	 *
	 * @return string
	 */
	public function getOptionCacheCleanAction() {
		return $this->optionCacheCleanAction;
	}
}Common/Utils/Filesystem.php000066600000014234151135505570011750 0ustar00<?php
// phpcs:disable WordPress.WP.AlternativeFunctions

namespace AIOSEO\Plugin\Common\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Load our manifest to use throughout the app.
 *
 * @since 4.1.9
 */
class Filesystem {
	/**
	 * Holds the WordPress filesystem object.
	 *
	 * @since 4.1.9
	 *
	 * @var \WP_Filesystem_Base
	 */
	public $fs = null;

	/**
	 * Core class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Core\Core
	 */
	private $core = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.1.9
	 *
	 * @param \AIOSEO\Plugin\Common\Core\Core $core The AIOSEO Core class.
	 * @param array                           $args Any arguments needed to construct the class with.
	 */
	public function __construct( $core, $args = [] ) {
		$this->core = $core;
		$this->init( $args );
	}

	/**
	 * Initialize the filesystem.
	 *
	 * @since 4.1.9
	 *
	 * @param  array $args An array of arguments for the WP_Filesystem
	 * @return void
	 */
	public function init( $args = [] ) {
		require_once ABSPATH . 'wp-admin/includes/file.php';
		WP_Filesystem( $args );

		global $wp_filesystem; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if ( is_object( $wp_filesystem ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			$this->fs = $wp_filesystem; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		}
	}

	/**
	 * Wrapper method to check if a file exists.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $filename The filename to check if it exists.
	 * @return bool             Returns true if the file or directory specified by filename exists; false otherwise.
	 */
	public function exists( $filename ) {
		if ( ! $this->isWpfsValid() ) {
			return @file_exists( $filename );
		}

		return $this->fs->exists( $filename );
	}

	/**
	 * Retrieve the contents of a file.
	 *
	 * @since 4.1.9
	 *
	 * @param  string      $filename The filename to get the contents for.
	 * @return string|bool           The function returns the read data or false on failure.
	 */
	public function getContents( $filename ) {
		if ( ! $this->exists( $filename ) ) {
			return false;
		}

		if ( ! $this->isWpfsValid() ) {
			return @file_get_contents( $filename );
		}

		return $this->fs->get_contents( $filename );
	}

	/**
	 * Reads entire file into an array.
	 *
	 * @since 4.1.9
	 *
	 * @param  string     $file Path to the file.
	 * @return array|bool       File contents in an array on success, false on failure.
	 */
	public function getContentsArray( $file ) {
		if ( ! $this->exists( $file ) ) {
			return false;
		}

		if ( ! $this->isWpfsValid() ) {
			return @file( $file );
		}

		return $this->fs->get_contents_array( $file );
	}

	/**
	 * Sets the access and modification times of a file.
	 * Note: If $file doesn't exist, it will be created.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $file  Path to file.
	 * @param  int    $time  Optional. Modified time to set for file. Default 0.
	 * @param  int    $atime Optional. Access time to set for file. Default 0.
	 * @return bool          True on success, false on failure.
	 */
	public function touch( $file, $time = 0, $atime = 0 ) {
		if ( 0 === $time ) {
			$time = time();
		}

		if ( 0 === $atime ) {
			$atime = time();
		}

		if ( ! $this->isWpfsValid() ) {
			return @touch( $file, $time, $atime );
		}

		return $this->fs->touch( $file, $time, $atime );
	}

	/**
	 * Writes a string to a file.
	 *
	 * @since 4.1.9
	 *
	 * @param  string    $file     Remote path to the file where to write the data.
	 * @param  string    $contents The data to write.
	 * @param  int|false $mode     Optional. The file permissions as octal number, usually 0644. Default false.
	 * @return int|bool            True on success, false on failure.
	 */
	public function putContents( $file, $contents, $mode = false ) {
		if ( ! $this->isWpfsValid() ) {
			return @file_put_contents( $file, $contents );
		}

		return $this->fs->put_contents( $file, $contents, $mode );
	}

	/**
	 * Checks if a file or directory is writable.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $file Path to file or directory.
	 * @return bool         Whether $file is writable.
	 */
	public function isWritable( $file ) {
		if ( ! $this->isWpfsValid() ) {
			return @is_writable( $file );
		}

		return $this->fs->is_writable( $file );
	}

	/**
	 * Checks if a file is readable.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $file Path to file.
	 * @return bool         Whether $file is readable.
	 */
	public function isReadable( $file ) {
		if ( ! $this->isWpfsValid() ) {
			return @is_readable( $file );
		}

		return $this->fs->is_readable( $file );
	}

	/**
	 * Gets the file size (in bytes).
	 *
	 * @since 4.1.9
	 *
	 * @param  string   $file Path to file.
	 * @return int|bool       Size of the file in bytes on success, false on failure.
	 */
	public function size( $file ) {
		if ( ! $this->isWpfsValid() ) {
			return @filesize( $file );
		}

		return $this->fs->size( $file );
	}

	/**
	 * Checks if resource is a file.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $file File path.
	 * @return bool         Whether $file is a file.
	 */
	public function isFile( $file ) {
		if ( ! $this->isWpfsValid() ) {
			return @is_file( $file );
		}

		return $this->fs->is_file( $file );
	}

	/**
	 * Checks if resource is a directory.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $path Directory path.
	 * @return bool         Whether $path is a directory.
	 */
	public function isDir( $path ) {
		if ( ! $this->isWpfsValid() ) {
			return @is_dir( $path );
		}

		return $this->fs->is_dir( $path );
	}

	/**
	 * A simple check to ensure that the WP_Filesystem is valid.
	 *
	 * @since 4.1.9
	 *
	 * @return bool True if valid, false if not.
	 */
	public function isWpfsValid() {
		if (
			! is_a( $this->fs, 'WP_Filesystem_Base' ) ||
			(
				// Errors is a WP_Error object.
				! empty( $this->fs->errors ) &&
				// We directly check if the errors array is empty for compatibility with WP < 5.1.
				! empty( $this->fs->errors->errors )
			)
		) {
			return false;
		}

		return true;
	}

	/**
	 * In order to not have a conflict, we need to return a clone.
	 *
	 * @since 4.1.9
	 *
	 * @return Filesystem The cloned Filesystem object.
	 */
	public function noConflict() {
		return clone $this;
	}
}Common/Utils/Tags.php000066600000124307151135505570010525 0ustar00<?php
namespace AIOSEO\Plugin\Common\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class to replace tag values with their data counterparts.
 *
 * @since 4.0.0
 */
class Tags {
	/**
	 * An array of tag values that we support.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $tags = [];

	/**
	 * Specifies the denotation character for the tags.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	public $denotationChar = '#';

	/**
	 * An array of contexts to separate tags.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $context = [
		'authorDescription'   => [
			'author_bio',
			'author_first_name',
			'author_last_name',
			'author_name',
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'separator_sa',
			'site_title',
			'tagline'
		],
		'authorTitle'         => [
			'author_first_name',
			'author_last_name',
			'author_name',
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'separator_sa',
			'site_title',
			'tagline'
		],
		'descriptionFormat'   => [
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'description',
			'post_date',
			'post_month',
			'post_title',
			'post_year',
			'separator_sa',
			'site_title',
			'tagline'
		],
		'dateDescription'     => [
			'archive_date',
			'archive_title',
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'post_day',
			'post_month',
			'post_year',
			'separator_sa',
			'site_title',
			'tagline'
		],
		'dateTitle'           => [
			'archive_date',
			'archive_title',
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'post_day',
			'post_month',
			'post_year',
			'separator_sa',
			'site_title',
			'tagline'
		],
		'homePage'            => [
			'author_first_name',
			'author_last_name',
			'author_name',
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'post_date',
			'post_day',
			'post_excerpt_only',
			'post_excerpt',
			'post_month',
			'post_title',
			'post_year',
			'separator_sa',
			'site_title',
			'tagline'
		],
		'knowledgeGraph'      => [
			'separator_sa',
			'site_title',
			'tagline'
		],
		'pagedFormat'         => [
			'page_number',
			'separator_sa'
		],
		'postDescription'     => [
			'author_first_name',
			'author_last_name',
			'author_name',
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'permalink',
			'post_content',
			'post_date',
			'post_day',
			'post_excerpt_only',
			'post_excerpt',
			'post_month',
			'post_title',
			'post_year',
			'separator_sa',
			'site_title',
			'tagline',
			'tax_name',
			'taxonomy_title'
		],
		'postTitle'           => [
			'author_first_name',
			'author_last_name',
			'author_name',
			'categories',
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'permalink',
			'post_content',
			'post_date',
			'post_day',
			'post_excerpt_only',
			'post_excerpt',
			'post_month',
			'post_title',
			'post_year',
			'separator_sa',
			'site_title',
			'tagline',
			'tax_name',
			'taxonomy_title'
		],
		'rss'                 => [
			'author_link',
			'author_link_alt',
			'author_name',
			'featured_image',
			'post_date',
			'post_link',
			'post_link_alt',
			'post_title',
			'site_link',
			'site_link_alt',
			'site_title',
			'taxonomy_title'
		],
		'schema'              => [
			'author_first_name',
			'author_last_name',
			'author_name',
			'author_url',
			'categories',
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'permalink',
			'post_content',
			'post_date',
			'post_day',
			'post_excerpt_only',
			'post_excerpt',
			'post_month',
			'post_title',
			'post_year',
			'separator_sa',
			'site_title',
			'tagline',
			'tax_name',
			'taxonomy_title'
		],
		'searchDescription'   => [
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'search_term',
			'separator_sa',
			'site_title',
			'tagline'
		],
		'searchTitle'         => [
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'search_term',
			'separator_sa',
			'site_title',
			'tagline'
		],
		'siteDescription'     => [
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'permalink',
			'post_date',
			'post_day',
			'post_month',
			'post_year',
			'search_term',
			'separator_sa',
			'tagline'
		],
		'siteTitle'           => [
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'permalink',
			'post_date',
			'post_day',
			'post_month',
			'post_year',
			'search_term',
			'separator_sa',
			'tagline'
		],
		'taxonomyDescription' => [
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'permalink',
			'separator_sa',
			'site_title',
			'tagline',
			'taxonomy_description',
			'taxonomy_title'
		],
		'taxonomyTitle'       => [
			'current_date',
			'current_day',
			'current_month',
			'current_year',
			'custom_field',
			'permalink',
			'separator_sa',
			'site_title',
			'tagline',
			'tax_parent_name',
			'taxonomy_description',
			'taxonomy_title'
		]
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		// Tags need to be registered on wp_loaded instead of init to ensure these are available during block rendering.
		add_action( 'wp_loaded', [ $this, 'registerTags' ] );
	}

	/**
	 * Register the tags.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	public function registerTags() {
		$this->tags = array_merge( $this->tags, [
			[
				'id'          => 'alt_tag',
				'name'        => __( 'Image Alt Tag', 'all-in-one-seo-pack' ),
				'description' => __( 'Your image\'s alt tag attribute.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'attachment_caption',
				'name'        => __( 'Media Caption', 'all-in-one-seo-pack' ),
				'description' => __( 'Caption for the current media file.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'attachment_description',
				'name'        => __( 'Media Description', 'all-in-one-seo-pack' ),
				'description' => __( 'Description for the current media file.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'archive_date',
				'name'        => __( 'Archive Date', 'all-in-one-seo-pack' ),
				'description' => __( 'The date of the current archive, localized.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'author_link',
				'name'        => __( 'Author Link', 'all-in-one-seo-pack' ),
				'description' => __( 'Author archive link (name as text).', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'author_link_alt',
				'name'        => __( 'Author Link (Alt)', 'all-in-one-seo-pack' ),
				'description' => __( 'Author archive link (link as text).', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'author_bio',
				'name'        => __( 'Author Biography', 'all-in-one-seo-pack' ),
				'description' => __( 'The biography of the author.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'author_name',
				'name'        => __( 'Author Name', 'all-in-one-seo-pack' ),
				'description' => __( 'The display name of the post author.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'author_first_name',
				'name'        => __( 'Author First Name', 'all-in-one-seo-pack' ),
				'description' => __( 'The first name of the post author.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'author_last_name',
				'name'        => __( 'Author Last Name', 'all-in-one-seo-pack' ),
				'description' => __( 'The last name of the post author.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'author_url',
				'name'        => __( 'Author URL', 'all-in-one-seo-pack' ),
				'description' => __( 'The URL of the author page.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'archive_title',
				'name'        => __( 'Archive Title', 'all-in-one-seo-pack' ),
				'description' => __( 'The title of the current archive.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'blog_link',
				'name'        => __( 'Site Link', 'all-in-one-seo-pack' ),
				'description' => __( 'Site link (link as text).', 'all-in-one-seo-pack' ),
				'html'        => true
			],
			[
				'id'          => 'blog_title',
				'name'        => __( 'Site Title', 'all-in-one-seo-pack' ),
				'description' => __( 'Your site title.', 'all-in-one-seo-pack' ),
				'deprecated'  => true
			],
			[
				'id'          => 'category',
				'name'        => __( 'Category', 'all-in-one-seo-pack' ),
				'description' => __( 'Current or first category title.', 'all-in-one-seo-pack' ),
				'deprecated'  => true
			],
			[
				'id'          => 'categories',
				'name'        => __( 'Categories', 'all-in-one-seo-pack' ),
				'description' => __( 'All categories that are assigned to the current post, comma-separated.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'category_link',
				// Translators: 1 - The type of page (Post, Page, Category, Tag, etc.).
				'name'        => sprintf( __( '%1$s Link', 'all-in-one-seo-pack' ), 'Category' ),
				'description' => __( 'Current or first term link (name as text).', 'all-in-one-seo-pack' ),
				'html'        => true
			],
			[
				'id'          => 'category_link_alt',
				// Translators: 1 - The type of page (Post, Page, Category, Tag, etc.).
				'name'        => sprintf( __( '%1$s Link (Alt)', 'all-in-one-seo-pack' ), 'Category' ),
				'description' => __( 'Current or first term link (link as text).', 'all-in-one-seo-pack' ),
				'html'        => true
			],
			[
				'id'          => 'current_date',
				'name'        => __( 'Current Date', 'all-in-one-seo-pack' ),
				'description' => __( 'The current date, localized.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'current_day',
				'name'        => __( 'Current Day', 'all-in-one-seo-pack' ),
				'description' => __( 'The current day of the month, localized.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'current_month',
				'name'        => __( 'Current Month', 'all-in-one-seo-pack' ),
				'description' => __( 'The current month, localized.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'current_year',
				'name'        => __( 'Current Year', 'all-in-one-seo-pack' ),
				'description' => __( 'The current year, localized.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'custom_field',
				'name'        => __( 'Custom Field', 'all-in-one-seo-pack' ),
				'description' => __( 'A custom field from the current page/post.', 'all-in-one-seo-pack' ),
				'custom'      => true
			],
			[
				'id'          => 'description',
				'name'        => __( 'Description', 'all-in-one-seo-pack' ),
				'description' => __( 'The meta description for the current page/post.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'featured_image',
				'name'        => __( 'Featured Image', 'all-in-one-seo-pack' ),
				'description' => __( 'The featured image of the current page/post.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'page_number',
				'name'        => __( 'Page Number', 'all-in-one-seo-pack' ),
				'description' => __( 'The page number for the current paginated page.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'parent_title',
				'name'        => __( 'Parent Title', 'all-in-one-seo-pack' ),
				'description' => __( 'The title of the parent post of the current page/post.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'permalink',
				'name'        => __( 'Permalink', 'all-in-one-seo-pack' ),
				'description' => __( 'The permalink for the current page/post.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'post_content',
				// Translators: 1 - The singular name of the post type.
				'name'        => sprintf( __( '%1$s Content', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'The content of your page/post.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'post_date',
				// Translators: 1 - The singular name of the post type.
				'name'        => sprintf( __( '%1$s Date', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'The date when the page/post was published, localized.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'post_day',
				// Translators: 1 - The singular name of the post type.
				'name'        => sprintf( __( '%1$s Day', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'The day of the month when the page/post was published, localized.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'post_excerpt',
				// Translators: 1 - The singular name of the post type.
				'name'        => sprintf( __( '%1$s Excerpt', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'The excerpt defined on your page/post.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'post_excerpt_only',
				// Translators: 1 - The singular name of the post type.
				'name'        => sprintf( __( '%1$s Excerpt Only', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'The excerpt defined on your page/post. Will not fall back to the post content.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'post_month',
				// Translators: 1 - The singular name of the post type.
				'name'        => sprintf( __( '%1$s Month', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'The month when the page/post was published, localized.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'post_year',
				// Translators: 1 - The singular name of the post type.
				'name'        => sprintf( __( '%1$s Year', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'The year when the page/post was published, localized.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'post_link',
				// Translators: 1 - The type of page (Post, Page, Category, Tag, etc.).
				'name'        => sprintf( __( '%1$s Link', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'Post link (name as anchor text).', 'all-in-one-seo-pack' ),
				'html'        => true
			],
			[
				'id'          => 'post_link_alt',
				// Translators: 1 - The type of page (Post, Page, Category, Tag, etc.).
				'name'        => sprintf( __( '%1$s Link (Alt)', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'Post link (link as anchor text).', 'all-in-one-seo-pack' ),
				'html'        => true
			],
			[
				'id'          => 'post_title',
				// Translators: 1 - The type of page (Post, Page, Category, Tag, etc.).
				'name'        => sprintf( __( '%1$s Title', 'all-in-one-seo-pack' ), 'Post' ),
				'description' => __( 'The original title of the current post.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'search_term',
				'name'        => __( 'Search Term', 'all-in-one-seo-pack' ),
				'description' => __( 'The term the user is searching for.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'separator_sa',
				'name'        => __( 'Separator', 'all-in-one-seo-pack' ),
				'description' => __( 'The separator defined in the search appearance settings.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'site_description',
				'name'        => __( 'Site Description', 'all-in-one-seo-pack' ),
				'description' => __( 'The description for your site.', 'all-in-one-seo-pack' ),
				'deprecated'  => true
			],
			[
				'id'          => 'site_link',
				'name'        => __( 'Site Link', 'all-in-one-seo-pack' ),
				'description' => __( 'Site link (name as text).', 'all-in-one-seo-pack' ),
				'html'        => true
			],
			[
				'id'          => 'site_link_alt',
				'name'        => __( 'Site Link (Alt)', 'all-in-one-seo-pack' ),
				'description' => __( 'Site link (link as text).', 'all-in-one-seo-pack' ),
				'html'        => true
			],
			[
				'id'          => 'site_title',
				'name'        => __( 'Site Title', 'all-in-one-seo-pack' ),
				'description' => __( 'Your site title.', 'all-in-one-seo-pack' ),
				'html'        => true
			],
			[
				'id'          => 'tagline',
				'name'        => __( 'Tagline', 'all-in-one-seo-pack' ),
				'description' => __( 'The tagline for your site, set in the general settings.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'tax_name',
				'name'        => __( 'Taxonomy Name', 'all-in-one-seo-pack' ),
				'description' => __( 'The name of the first term of a given taxonomy that is assigned to the current page/post.', 'all-in-one-seo-pack' ),
				'custom'      => true
			],
			[
				'id'          => 'tax_parent_name',
				'name'        => __( 'Parent Term', 'all-in-one-seo-pack' ),
				'description' => __( 'The name of the parent term of the current term.', 'all-in-one-seo-pack' ),
			],
			[
				'id'          => 'taxonomy_description',
				// Translators: 1 - The singular name of the current taxonomy.
				'name'        => sprintf( __( '%1$s Description', 'all-in-one-seo-pack' ), 'Category' ),
				'description' => __( 'The description of the primary term, first assigned term or the current term.', 'all-in-one-seo-pack' )
			],
			[
				'id'          => 'taxonomy_title',
				// Translators: 1 - The type of page (Post, Page, Category, Tag, etc.).
				'name'        => sprintf( __( '%1$s Title', 'all-in-one-seo-pack' ), 'Category' ),
				'description' => __( 'The title of the primary term, first assigned term or the current term.', 'all-in-one-seo-pack' )
			]
		] );
	}

	/**
	 * Returns all the tags.
	 *
	 * @since 4.0.0
	 *
	 * @param  bool  $sampleData Whether or not to fill empty values with sample data.
	 * @return array             An array of tags.
	 */
	public function all( $sampleData = false ) {
		$tags = $this->tags;
		foreach ( $tags as $key => $tag ) {
			$tags[ $key ]['value'] = ( $tag['instance'] ?? null )
				? $tag['instance']->getTagValue( $tag, null, $sampleData )
				: $this->getTagValue( $tag, null, $sampleData );
		}

		usort( $tags, function ( $a, $b ) {
			return $a['name'] < $b['name']
				? -1
				: ( $a['name'] > $b['name'] ? 1 : 0 );
		} );

		return [
			'tags'    => $tags,
			'context' => $this->getContext()
		];
	}

	/**
	 * Add the context for all the post/page types.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of contextual data.
	 */
	public function getContext() {
		$context = $this->context;

		// Post types including CPT's.
		foreach ( aioseo()->helpers->getPublicPostTypes() as $postType ) {
			if (
				'post' === $postType['name'] ||
				! empty( $postType['buddyPress'] )
			) {
				continue;
			}

			if ( $postType['hasArchive'] ) {
				$context[ $postType['name'] . 'ArchiveTitle' ]       = $context['dateTitle'];
				$context[ $postType['name'] . 'ArchiveDescription' ] = $context['dateDescription'];
			}

			$context[ $postType['name'] . 'Title' ]       = $context['postTitle'];
			$context[ $postType['name'] . 'Description' ] = $context['postDescription'];

			// Check if the post type has an excerpt.
			if ( empty( $postType['supports']['excerpt'] ) ) {
				$phpTitleKey = array_search( 'post_excerpt', $context[ $postType['name'] . 'Title' ], true );
				if ( false !== $phpTitleKey ) {
					unset( $context[ $postType['name'] . 'Title' ][ $phpTitleKey ] );
				}

				$phpTitleKey = array_search( 'post_excerpt_only', $context[ $postType['name'] . 'Title' ], true );
				if ( false !== $phpTitleKey ) {
					unset( $context[ $postType['name'] . 'Title' ][ $phpTitleKey ] );
				}

				$phpDescriptionKey = array_search( 'post_excerpt', $context[ $postType['name'] . 'Description' ], true );
				if ( false !== $phpDescriptionKey ) {
					unset( $context[ $postType['name'] . 'Description' ][ $phpDescriptionKey ] );
				}

				$phpDescriptionKey = array_search( 'post_excerpt_only', $context[ $postType['name'] . 'Description' ], true );
				if ( false !== $phpDescriptionKey ) {
					unset( $context[ $postType['name'] . 'Description' ][ $phpDescriptionKey ] );
				}

				asort( $context[ $postType['name'] . 'Title' ] );
				$context[ $postType['name'] . 'Title' ] = array_values( $context[ $postType['name'] . 'Title' ] );
				asort( $context[ $postType['name'] . 'Description' ] );
				$context[ $postType['name'] . 'Description' ] = array_values( $context[ $postType['name'] . 'Description' ] );
			}

			if ( 'page' === $postType['name'] ) {
				$phpTitleKey = array_search( 'taxonomy_title', $context['pageTitle'], true );
				if ( false !== $phpTitleKey ) {
					unset( $context['pageTitle'][ $phpTitleKey ] );
				}

				$phpTitleKey = array_search( 'category', $context['pageTitle'], true );
				if ( false !== $phpTitleKey ) {
					unset( $context['pageTitle'][ $phpTitleKey ] );
				}

				$phpDescriptionKey = array_search( 'taxonomy_title', $context['pageDescription'], true );
				if ( false !== $phpDescriptionKey ) {
					unset( $context['pageDescription'][ $phpDescriptionKey ] );
				}

				$phpDescriptionKey = array_search( 'category', $context['pageDescription'], true );
				if ( false !== $phpDescriptionKey ) {
					unset( $context['pageDescription'][ $phpDescriptionKey ] );
				}

				$context['pageTitle']       = array_values( $context['pageTitle'] );
				$context['pageDescription'] = array_values( $context['pageDescription'] );

				asort( $context['pageTitle'] );
				$context['pageTitle'] = array_values( $context['pageTitle'] );
				asort( $context['pageDescription'] );
				$context['pageDescription'] = array_values( $context['pageDescription'] );
			}

			if ( 'attachment' === $postType['name'] ) {
				$context['attachmentTitle'][] = 'alt_tag';
				asort( $context['attachmentTitle'] );
				$context['attachmentTitle'] = array_values( $context['attachmentTitle'] );
				$context['attachmentDescription'][] = 'alt_tag';
				asort( $context['attachmentDescription'] );
				$context['attachmentDescription'] = array_values( $context['attachmentDescription'] );

				$phpTitleKey = array_search( 'taxonomy_title', $context['attachmentTitle'], true );
				if ( false !== $phpTitleKey ) {
					unset( $context['attachmentTitle'][ $phpTitleKey ] );
				}

				$phpTitleKey = array_search( 'post_content', $context['attachmentTitle'], true );
				if ( false !== $phpTitleKey ) {
					unset( $context['attachmentTitle'][ $phpTitleKey ] );
				}

				$phpTitleKey = array_search( 'post_excerpt', $context['attachmentTitle'], true );
				if ( false !== $phpTitleKey ) {
					unset( $context['attachmentTitle'][ $phpTitleKey ] );
				}

				$phpTitleKey = array_search( 'post_excerpt_only', $context['attachmentTitle'], true );
				if ( false !== $phpTitleKey ) {
					unset( $context['attachmentTitle'][ $phpTitleKey ] );
				}

				$phpDescriptionKey = array_search( 'taxonomy_title', $context['attachmentDescription'], true );
				if ( false !== $phpDescriptionKey ) {
					unset( $context['attachmentDescription'][ $phpDescriptionKey ] );
				}

				$phpDescriptionKey = array_search( 'post_content', $context['attachmentDescription'], true );
				if ( false !== $phpDescriptionKey ) {
					unset( $context['attachmentDescription'][ $phpDescriptionKey ] );
				}

				$phpDescriptionKey = array_search( 'post_excerpt', $context['attachmentDescription'], true );
				if ( false !== $phpDescriptionKey ) {
					unset( $context['attachmentDescription'][ $phpDescriptionKey ] );
				}

				$phpDescriptionKey = array_search( 'post_excerpt_only', $context['attachmentDescription'], true );
				if ( false !== $phpDescriptionKey ) {
					unset( $context['attachmentDescription'][ $phpDescriptionKey ] );
				}

				$context['attachmentTitle']       = array_merge( $context['attachmentTitle'], [ 'attachment_caption', 'attachment_description' ] );
				$context['attachmentDescription'] = array_merge( $context['attachmentDescription'], [ 'attachment_caption', 'attachment_description' ] );

				asort( $context['attachmentTitle'] );
				$context['attachmentTitle'] = array_values( $context['attachmentTitle'] );
				asort( $context['attachmentDescription'] );
				$context['attachmentDescription'] = array_values( $context['attachmentDescription'] );
			}

			if ( ! in_array( 'category', get_object_taxonomies( $postType['name'] ), true ) ) {
				$phpTitleKey = array_search( 'categories', $context[ $postType['name'] . 'Title' ], true );
				if ( false !== $phpTitleKey ) {
					unset( $context[ $postType['name'] . 'Title' ][ $phpTitleKey ] );
				}

				$phpTitleKey = array_search( 'categories', $context[ $postType['name'] . 'Description' ], true );
				if ( false !== $phpTitleKey ) {
					unset( $context[ $postType['name'] . 'Description' ][ $phpTitleKey ] );
				}

				asort( $context[ $postType['name'] . 'Title' ] );
				$context[ $postType['name'] . 'Title' ] = array_values( $context[ $postType['name'] . 'Title' ] );
				asort( $context[ $postType['name'] . 'Description' ] );
				$context[ $postType['name'] . 'Description' ] = array_values( $context[ $postType['name'] . 'Description' ] );
			}

			if ( $postType['hierarchical'] ) {
				$context[ $postType['name'] . 'Title' ][] = 'parent_title';
			}
		}

		// Taxonomies including from CPT's.
		foreach ( aioseo()->helpers->getPublicTaxonomies() as $taxonomy ) {
			$context[ $taxonomy['name'] . 'Title' ]       = $context['taxonomyTitle'];
			$context[ $taxonomy['name'] . 'Description' ] = $context['taxonomyDescription'];
		}

		return $context;
	}

	/**
	 * Replace the tags in the string provided.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string The string to look for tags in.
	 * @param  int    $id     The page or post ID.
	 * @return string         The string with tags replaced.
	 */
	public function replaceTags( $string, $id = 0 ) {
		if ( ! $string || ! preg_match( '/' . $this->denotationChar . '/', (string) $string ) ) {
			return $string;
		}

		foreach ( $this->tags as $tag ) {
			if ( 'custom_field' === $tag['id'] || 'tax_name' === $tag['id'] ) {
				continue;
			}

			$tagId = $this->denotationChar . $tag['id'];
			// Pattern explained: Exact match of tag, not followed by any additional letter, number or underscore.
			// This allows us to have tags like: #post_link and #post_link_alt
			// and it will always replace the correct one.
			$pattern = "/$tagId(?![a-zA-Z0-9_])/im";
			if ( preg_match( $pattern, (string) $string ) ) {
				$tagValue = $this->getTagValue( $tag, $id );
				$string   = preg_replace( $pattern, '%|%' . aioseo()->helpers->escapeRegexReplacement( $tagValue ), (string) $string );
			}
		}

		$string = $this->parseTaxonomyNames( $string, $id );

		// Custom fields are parsed separately.
		$string = $this->parseCustomFields( $string, $id );

		return preg_replace( '/%\|%/im', '', (string) $string );
	}

	/**
	 * Get the value of the tag to replace.
	 *
	 * @since 4.0.0
	 *
	 * @param  array    $tag        The tag to look for.
	 * @param  int|null $id         The post ID.
	 * @param  bool     $sampleData Whether or not to fill empty values with sample data.
	 * @return mixed                The value of the tag.
	 */
	public function getTagValue( $tag, $id, $sampleData = false ) {
		$author   = new \WP_User();
		$post     = aioseo()->helpers->getPost( $id );
		$postId   = null;
		$category = null;
		if ( $post ) {
			$author   = new \WP_User( $post->post_author );
			$postId   = empty( $id ) ? $post->ID : $id;
			$category = get_the_category( $postId );
		} elseif ( is_author() && is_a( get_queried_object(), 'WP_User' ) ) {
			$author = get_queried_object();
		}

		switch ( $tag['id'] ) {
			case 'alt_tag':
				return empty( $id )
					? ( $sampleData ? __( 'A sample alt tag for your image', 'all-in-one-seo-pack' ) : '' )
					: get_post_meta( $id, '_wp_attachment_image_alt', true );
			case 'archive_date':
				$date = null;
				if ( is_year() ) {
					$date = get_the_date( 'Y' );
				}
				if ( is_month() ) {
					$date = get_the_date( 'F, Y' );
				}
				if ( is_day() ) {
					$date = get_the_date();
				}
				if ( $sampleData ) {
					$date = $this->formatDateAsI18n( date_i18n( 'U' ) );
				}
				if ( ! empty( $date ) ) {
					return $date;
				}

				break;
			case 'archive_title':
				$title = is_post_type_archive() ? post_type_archive_title( '', false ) : get_the_archive_title();

				return $sampleData ? __( 'Sample Archive Title', 'all-in-one-seo-pack' ) : wp_strip_all_tags( $title );
			case 'author_bio':
				$bio = get_the_author_meta( 'description', $author->ID );

				return empty( $bio ) && $sampleData ? __( 'Sample author biography', 'all-in-one-seo-pack' ) : $bio;
			case 'author_first_name':
				$name = $author->first_name;

				return empty( $name ) && $sampleData ? wp_get_current_user()->first_name : $author->first_name;
			case 'author_last_name':
				$name = $author->last_name;

				return empty( $name ) && $sampleData ? wp_get_current_user()->last_name : $author->last_name;
			case 'author_link':
				return '<a href="' . esc_url( get_author_posts_url( $author->ID ) ) . '">' . esc_html( $author->display_name ) . '</a>';
			case 'author_link_alt':
				return '<a href="' . esc_url( get_author_posts_url( $author->ID ) ) . '">' . esc_url( get_author_posts_url( $author->ID ) ) . '</a>';
			case 'author_name':
				$name = $author->display_name;

				return empty( $name ) && $sampleData ? wp_get_current_user()->display_name : $author->display_name;
			case 'author_url':
				$authorUrl = get_author_posts_url( $author->ID );

				return ! empty( $authorUrl ) ? $authorUrl : '';
			case 'attachment_caption':
				$caption = wp_get_attachment_caption( $postId );

				return empty( $caption ) && $sampleData ? __( 'Sample caption for media.', 'all-in-one-seo-pack' ) : $caption;
			case 'attachment_description':
				$description = ! empty( $post->post_content ) ? $post->post_content : '';

				return empty( $description ) && $sampleData ? __( 'Sample description for media.', 'all-in-one-seo-pack' ) : $description;
			case 'categories':
				if ( ! is_object( $post ) || 'post' !== $post->post_type ) {
					return ! is_object( $post ) && $sampleData ? __( 'Sample Category 1, Sample Category 2', 'all-in-one-seo-pack' ) : '';
				}
				$categories = get_the_terms( $post->ID, 'category' );

				$names = [];
				if ( ! is_array( $categories ) ) {
					return '';
				}

				foreach ( $categories as $category ) {
					$names[] = $category->name;
				}

				return implode( ', ', $names );
			case 'category_link':
				return '<a href="' . esc_url( get_category_link( $category ) ) . '">' . ( $category ? $category[0]->name : '' ) . '</a>';
			case 'category_link_alt':
				return '<a href="' . esc_url( get_category_link( $category ) ) . '">' . esc_url( get_category_link( $category ) ) . '</a>';
			case 'current_date':
				return $this->formatDateAsI18n( date_i18n( 'U' ) );
			case 'current_day':
				return date_i18n( 'd' );
			case 'current_month':
				return date_i18n( 'F' );
			case 'current_year':
				return date_i18n( 'Y' );
			case 'custom_field':
				return $sampleData ? __( 'Sample Custom Field Value', 'all-in-one-seo-pack' ) : '';
			case 'featured_image':
				if ( ! has_post_thumbnail( $postId ) ) {
					return $sampleData ? __( 'Sample featured image', 'all-in-one-seo-pack' ) : '';
				}

				$imageId = get_post_thumbnail_id( $postId );
				$image   = (array) wp_get_attachment_image_src( $imageId, 'full' );
				$image   = isset( $image[0] ) ? '<img src="' . $image[0] . '" style="display: block; margin: 1em auto">' : ''; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage

				return $sampleData ? __( 'Sample featured image', 'all-in-one-seo-pack' ) : $image;
			case 'page_number':
				return aioseo()->helpers->getPageNumber();
			case 'parent_title':
				if ( ! is_object( $post ) || ! $post->post_parent ) {
					return ! is_object( $post ) && $sampleData ? __( 'Sample Parent', 'all-in-one-seo-pack' ) : '';
				}
				$parent = get_post( $post->post_parent );

				return $parent ? $parent->post_title : '';
			case 'permalink':
				return aioseo()->helpers->getUrl();
			case 'post_date':
				$date = $this->formatDateAsI18n( get_the_date( 'U' ) );

				return empty( $date ) && $sampleData ? $this->formatDateAsI18n( date_i18n( 'U' ) ) : $date;
			case 'post_day':
				$day = get_the_date( 'd', $post );

				return empty( $day ) && $sampleData ? date_i18n( 'd' ) : $day;
			case 'post_excerpt_only':
				return empty( $postId ) ? ( $sampleData ? __( 'Sample excerpt from a page/post.', 'all-in-one-seo-pack' ) : '' ) : $post->post_excerpt;
			case 'post_excerpt':
				if ( empty( $postId ) ) {
					return $sampleData ? __( 'Sample excerpt from a page/post.', 'all-in-one-seo-pack' ) : '';
				}

				if ( $post->post_excerpt ) {
					return $post->post_excerpt;
				}

				// Fall through if the post doesn't have an excerpt set. In that case getDescriptionFromContent() will generate it for us.
			case 'post_content':
				return empty( $postId ) ? ( $sampleData ? __( 'An example of content from your page/post.', 'all-in-one-seo-pack' ) : '' ) : aioseo()->helpers->getDescriptionFromContent( $post );
			case 'post_link':
				return '<a href="' . esc_url( get_permalink( $post ) ) . '">' . esc_html( get_the_title( $post ) ) . '</a>';
			case 'post_link_alt':
				return '<a href="' . esc_url( get_permalink( $post ) ) . '">' . esc_url( get_permalink( $post ) ) . '</a>';
			case 'post_month':
				$month = get_the_date( 'F', $post );

				return empty( $month ) && $sampleData ? date_i18n( 'F' ) : $month;
			case 'post_title':
				$title = esc_html( get_the_title( $post ) );

				return empty( $title ) && $sampleData ? __( 'Sample Post', 'all-in-one-seo-pack' ) : $title;
			case 'post_year':
				$year = get_the_date( 'Y', $post );

				return empty( $year ) && $sampleData ? date_i18n( 'Y' ) : $year;
			case 'search_term':
				$search = get_search_query();

				return empty( $search ) && $sampleData ? __( 'Example search string', 'all-in-one-seo-pack' ) : esc_attr( stripslashes( $search ) );
			case 'separator_sa':
				return aioseo()->helpers->decodeHtmlEntities( aioseo()->options->searchAppearance->global->separator );
			case 'site_link':
			case 'blog_link':
				return '<a href="' . esc_url( get_bloginfo( 'url' ) ) . '">' . esc_html( get_bloginfo( 'name' ) ) . '</a>';
			case 'site_link_alt':
				return '<a href="' . esc_url( get_bloginfo( 'url' ) ) . '">' . esc_url( get_bloginfo( 'url' ) ) . '</a>';
			case 'tag':
				return single_term_title( '', false );
			case 'tax_name':
				return $sampleData ? __( 'Sample Taxonomy Name Value', 'all-in-one-seo-pack' ) : '';
			case 'tax_parent_name':
				$termObject       = get_term( $id ); // Don't use the getTerm() helper here. We need the actual Product Attribute tax.
				$parentTermObject = ! empty( $termObject->parent ) ? aioseo()->helpers->getTerm( $termObject->parent ) : '';
				$name             = $parentTermObject->name ?? '';

				if (
					is_a( $termObject, 'WP_Term' ) &&
					empty( $parentTermObject ) &&
					aioseo()->helpers->isWooCommerceProductAttribute( $termObject->taxonomy )
				) {
					$wcAttributeTaxonomiesTable = aioseo()->core->db->prefix . 'woocommerce_attribute_taxonomies';
					$attributeName              = str_replace( 'pa_', '', $termObject->taxonomy );

					$result = aioseo()->core->db->db->get_row(
						aioseo()->core->db->db->prepare(
							"SELECT attribute_label FROM $wcAttributeTaxonomiesTable WHERE attribute_name = %s",
							$attributeName
						)
					);

					return $result->attribute_label ?? '';
				}

				return $sampleData ? __( 'Sample Parent Term Name', 'all-in-one-seo-pack' ) : $name;
			case 'taxonomy_description':
				$description = term_description();

				return empty( $description ) && $sampleData ? __( 'Sample taxonomy description', 'all-in-one-seo-pack' ) : $description;
			case 'taxonomy_title':
			case 'category':
				$title = $this->getTaxonomyTitle( $postId );

				return ! $title && $sampleData ? __( 'Sample Taxonomy Title', 'all-in-one-seo-pack' ) : $title;
			case 'site_description':
			case 'blog_description':
			case 'tagline':
				return aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'description' ) );
			case 'site_title':
			case 'blog_title':
				return aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) );
			default:
				return '';
		}
	}

	/**
	 * Get the category title.
	 *
	 * @since 4.0.0
	 *
	 * @param  integer $postId The post ID if set.
	 * @return string          The category title.
	 */
	private function getTaxonomyTitle( $postId = null ) {
		$isWcActive = aioseo()->helpers->isWooCommerceActive();
		$title      = '';
		if ( $isWcActive && is_product_category() ) {
			$title = single_cat_title( '', false );
		} elseif ( is_category() ) {
			$title = single_cat_title( '', false );
		} elseif ( is_tag() ) {
			$title = single_tag_title( '', false );
		} elseif ( is_author() ) {
			$title = get_the_author();
		} elseif ( is_tax() ) {
			$title = single_term_title( '', false );
		} elseif ( is_post_type_archive() ) {
			$title = post_type_archive_title( '', false );
		} elseif ( is_archive() ) {
			$title = get_the_archive_title();
		}

		if ( $postId ) {
			$currentScreen  = aioseo()->helpers->getCurrentScreen();
			$isProduct      = $isWcActive && ( is_product() || 'product' === ( $currentScreen->post_type ?? '' ) );
			$post           = aioseo()->helpers->getPost( $postId );
			$postTaxonomies = get_object_taxonomies( $post, 'objects' );
			$postTerms      = [];
			foreach ( $postTaxonomies as $taxonomySlug => $taxonomy ) {
				if ( ! $taxonomy->hierarchical ) {
					continue;
				}

				$taxonomySlug = $isProduct ? 'product_cat' : $taxonomySlug;
				$primaryTerm  = aioseo()->standalone->primaryTerm->getPrimaryTerm( $postId, $taxonomySlug );
				if ( $primaryTerm ) {
					$postTerms[] = aioseo()->helpers->getTerm( $primaryTerm, $taxonomySlug );
					break;
				}

				$postTaxonomyTerms = get_the_terms( $postId, $taxonomySlug );
				if ( is_array( $postTaxonomyTerms ) ) {
					$postTerms = array_merge( $postTerms, $postTaxonomyTerms );
					break;
				}
			}

			$title = $postTerms ? $postTerms[0]->name : '';
		}

		return wp_strip_all_tags( (string) $title );
	}

	/**
	 * Formatted Date
	 *
	 * Get formatted date based on WP options.
	 *
	 * @since 4.0.0
	 *
	 * @param  null|int    $date   Date in UNIX timestamp format. Otherwise, current time.
	 * @return string              Date internationalized.
	 */
	public function formatDateAsI18n( $date = null ) {
		if ( ! $date ) {
			$date = time();
		}

		$format        = get_option( 'date_format' );
		$formattedDate = date_i18n( $format, $date );

		return apply_filters(
			'aioseo_format_date',
			$formattedDate,
			[
				$date,
				$format
			]
		);
	}

	/**
	 * Parses custom taxonomy tags by replacing them with the name of the first assigned term of the given taxonomy.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string The string to parse.
	 * @return mixed          The new title.
	 */
	private function parseTaxonomyNames( $string, $id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$pattern = '/' . $this->denotationChar . 'tax_name-([a-zA-Z0-9_-]+)/im';
		$string  = preg_replace_callback( $pattern, [ $this, 'replaceTaxonomyName' ], $string );
		$pattern = '/' . $this->denotationChar . 'tax_name(?![a-zA-Z0-9_-])/im';

		return preg_replace( $pattern, '', (string) $string );
	}

	/**
	 * Adds support for using #custom_field-[custom_field_title] for using
	 * custom fields / Advanced Custom Fields in titles / descriptions etc.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string The string to parse customs fields out of.
	 * @param  int    $postId The page or post ID.
	 * @return string         The new title.
	 */
	public function parseCustomFields( $string, $postId = 0 ) {
		$pattern = '/' . $this->denotationChar . 'custom_field-([a-zA-Z0-9_-]+)/im';
		$matches = [];
		preg_match_all( $pattern, (string) $string, $matches, PREG_SET_ORDER );

		$string  = $this->replaceCustomField( $string, $matches, $postId );
		$pattern = '/' . $this->denotationChar . 'custom_field(?![a-zA-Z0-9_-])/im';

		return preg_replace( $pattern, '', (string) $string );
	}

	/**
	 * Add context to our internal context.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $context A context array to append.
	 * @return void
	 */
	public function addContext( $context ) {
		$this->context = array_merge( $this->context, $context );
	}

	/**
	 * Add tags to our internal tags.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $tags A tags array to append.
	 * @return void
	 */
	public function addTags( $tags ) {
		$this->tags = array_merge( $this->tags, $tags );
	}

	/**
	 * Replaces a taxonomy name tag with its respective value.
	 *
	 * @since 4.0.0
	 *
	 * @param  array  $matches The matches.
	 * @return string          The replaced matches.
	 */
	private function replaceTaxonomyName( $matches ) {
		$termName = '';
		$post     = aioseo()->helpers->getPost();
		if ( ! empty( $matches[1] ) && $post ) {
			$taxonomy = get_taxonomy( $matches[1] );
			if ( ! $taxonomy ) {
				return '';
			}

			$term = aioseo()->standalone->primaryTerm->getPrimaryTerm( $post->ID, $taxonomy->name );
			if ( ! $term ) {
				$terms = get_the_terms( $post->ID, $taxonomy->name );
				if ( ! $terms || is_wp_error( $terms ) ) {
					return '';
				}

				$term = array_shift( $terms );
			}

			$termName = $term->name;
		}

		return '%|%' . $termName;
	}

	/**
	 * (ACF) Custom Field Replace.
	 *
	 * @since 4.0.0
	 *
	 * @param  string      $string  The string to parse customs fields out of.
	 * @param  array       $matches Array of matched values.
	 * @param  int         $postId  The page or post ID.
	 * @return bool|string          New title/text.
	 */
	private function replaceCustomField( $string, $matches, $postId ) {
		if ( empty( $matches ) ) {
			return $string;
		}

		$postId = get_queried_object() ?? $postId;

		foreach ( $matches as $match ) {
			$value = '';
			if ( ! empty( $match[1] ) ) {
				if ( function_exists( 'get_field' ) ) {
					$value = get_field( $match[1], $postId );
					if ( ! empty( $value['url'] ) && ! empty( $value['title'] ) ) {
						$value = "<a href='{$value['url']}'>{$value['title']}</a>";
					}
					if ( empty( $value ) ) {
						$value = aioseo()->helpers->getAcfFlexibleContentField( $match[1], $postId );
					}
				}

				if ( empty( $value ) ) {
					global $post;
					if ( ! empty( $post ) ) {
						$value = get_post_meta( $post->ID, $match[1], true );
					}
				}
			}

			$value  = is_scalar( $value ) ? wp_strip_all_tags( $value ) : '';
			$string = str_replace( $match[0], '%|%' . $value, $string );
		}

		return $string;
	}

	/**
	 * Get the default tags for the current post.
	 *
	 * @since 4.0.0
	 *
	 * @param  integer $postId The Post ID.
	 * @return array           An array of tags.
	 */
	public function getDefaultPostTags( $postId ) {
		$post = get_post( $postId );

		$title       = aioseo()->meta->title->getTitle( $post, true );
		$description = aioseo()->meta->description->getDescription( $post, true );

		return [
			'title'       => empty( $title ) ? '' : $title,
			'description' => empty( $description ) ? '' : $description
		];
	}
}Common/ThirdParty/WebStories.php000066600000002767151135505570012714 0ustar00<?php
namespace AIOSEO\Plugin\Common\ThirdParty;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Integrates with Google Web Stories plugin.
 *
 * @since 4.8.3
 */
class WebStories {
	/**
	 * Class constructor.
	 *
	 * @since 4.7.6
	 */
	public function __construct() {
		add_action( 'web_stories_story_head', [ $this, 'stripDefaultTags' ], 0 );
		add_action( 'web_stories_story_head', [ $this, 'outputAioseoTags' ] );
	}

	/**
	 * Strip all meta tags that are added by default by the Web Stories plugin.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	public function stripDefaultTags() {
		add_filter( 'web_stories_enable_metadata', '__return_false' );
		add_filter( 'web_stories_enable_schemaorg_metadata', '__return_false' );
		add_filter( 'web_stories_enable_open_graph_metadata', '__return_false' );
		add_filter( 'web_stories_enable_twitter_metadata', '__return_false' );

		remove_action( 'web_stories_story_head', 'rel_canonical' );
		remove_action( 'web_stories_story_head', 'wp_robots' );

		// This is needed to prevent multiple robots meta tags from being output.
		add_filter( 'wp_robots', '__return_empty_array' );
	}

	/**
	 * Output the AIOSEO tags.
	 *
	 * @since 4.7.6
	 *
	 * @return void
	 */
	public function outputAioseoTags() {
		aioseo()->head->wpHead();
	}

	/**
	 * Checks if the plugin is active.
	 *
	 * @since 4.7.6
	 *
	 * @return bool True if the plugin is active.
	 */
	public function isPluginActive() {
		return class_exists( 'Google\Web_Stories\Plugin' );
	}
}Common/ThirdParty/ThirdParty.php000066600000000672151135505570012711 0ustar00<?php
namespace AIOSEO\Plugin\Common\ThirdParty;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Instantiates our third-party classes.
 *
 * @since 4.7.6
 */
class ThirdParty {
	/**
	 * WebStories instance.
	 *
	 * @since 4.7.6
	 *
	 * @var WebStories
	 */
	public $webStories;

	/**
	 * Class constructor.
	 *
	 * @since 4.7.6
	 */
	public function __construct() {
		$this->webStories = new WebStories();
	}
}Common/Help/Help.php000066600000003001151135505570010272 0ustar00<?php
namespace AIOSEO\Plugin\Common\Help;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class Help {
	/**
	 * Source of the documentation content.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $url = 'https://cdn.aioseo.com/wp-content/docs.json';

	/**
	 * Settings.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $settings = [
		'docsUrl'          => 'https://aioseo.com/docs/',
		'supportTicketUrl' => 'https://aioseo.com/account/support/',
		'upgradeUrl'       => 'https://aioseo.com/pricing/'
	];

	/**
	 * Gets the URL for the notifications api.
	 *
	 * @since 4.0.0
	 *
	 * @return string The URL to use for the api requests.
	 */
	private function getUrl() {
		if ( defined( 'AIOSEO_DOCS_FEED_URL' ) ) {
			return AIOSEO_DOCS_FEED_URL;
		}

		return $this->url;
	}

	/**
	 * Returns the help docs for our menus.
	 *
	 * @since 4.0.0
	 *
	 * @return array The help docs.
	 */
	public function getDocs() {
		$helpDocs = aioseo()->core->networkCache->get( 'admin_help_docs' );
		if ( null !== $helpDocs ) {
			if ( is_array( $helpDocs ) ) {
				return $helpDocs;
			}

			return json_decode( $helpDocs, true );
		}

		$request = aioseo()->helpers->wpRemoteGet( $this->getUrl() );
		if ( is_wp_error( $request ) ) {
			aioseo()->core->networkCache->update( 'admin_help_docs', [], DAY_IN_SECONDS );
		}

		$helpDocs = wp_remote_retrieve_body( $request );

		aioseo()->core->networkCache->update( 'admin_help_docs', $helpDocs, WEEK_IN_SECONDS );

		return json_decode( $helpDocs, true );
	}
}Common/Migration/Migration.php000066600000014014151135505570012402 0ustar00<?php
namespace AIOSEO\Plugin\Common\Migration;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Handles the migration from V3 to V4.
 */
class Migration {
	/**
	 * The old V3 options.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $oldOptions = [];

	/**
	 * Meta class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Meta
	 */
	public $meta = null;

	/**
	 * Helpers class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Helpers
	 */
	public $helpers = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->meta    = new Meta();
		$this->helpers = new Helpers();

		// NOTE: This needs to go above the is_admin check in order for it to run at all.
		add_action( 'aioseo_migrate_post_meta', [ $this->meta, 'migratePostMeta' ] );

		if ( ! is_admin() ) {
			return;
		}

		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		add_action( 'init', [ $this, 'init' ], 2000 );
	}

	/**
	 * Initializes the class.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function init() {
		// Since the version numbers may vary, we only want to compare the first 3 numbers.
		$lastActiveVersion = aioseo()->internalOptions->internal->lastActiveVersion;
		$lastActiveVersion = $lastActiveVersion ? explode( '-', $lastActiveVersion ) : null;

		if ( version_compare( $lastActiveVersion[0], '4.0.0', '<' ) ) {
			aioseo()->internalOptions->internal->migratedVersion = $lastActiveVersion[0];
			add_action( 'wp_loaded', [ $this, 'doMigration' ] );
		}

		// Run our migration again for V4 users between v4.0.0 and v4.0.4.
		if (
			version_compare( $lastActiveVersion[0], '4.0.0', '>=' ) &&
			version_compare( $lastActiveVersion[0], '4.0.4', '<' ) &&
			get_option( 'aioseop_options' )
		) {
			add_action( 'wp_loaded', [ $this, 'redoMetaMigration' ] );
		}

		// Stop migration for new v4 users where it was incorrectly triggered.
		if ( version_compare( $lastActiveVersion[0], '4.0.4', '=' ) && ! get_option( 'aioseop_options' ) ) {
			aioseo()->core->cache->delete( 'v3_migration_in_progress_posts' );
			aioseo()->core->cache->delete( 'v3_migration_in_progress_terms' );

			try {
				aioseo()->actionScheduler->unschedule( 'aioseo_migrate_post_meta' );
				aioseo()->actionScheduler->unschedule( 'aioseo_migrate_term_meta' );
			} catch ( \Exception $e ) {
				// Do nothing.
			}
		}
	}

	/**
	 * Starts the migration.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function doMigration() {
		// If our tables do not exist, create them now.
		if ( ! aioseo()->core->db->tableExists( 'aioseo_posts' ) ) {
			aioseo()->updates->addInitialCustomTablesForV4();
		}

		$this->oldOptions = ( new OldOptions() )->oldOptions;

		if (
			! $this->oldOptions ||
			! is_array( $this->oldOptions ) ||
			! count( $this->oldOptions )
		) {
			return;
		}

		update_option( 'aioseo_options_v3', $this->oldOptions );

		aioseo()->core->cache->update( 'v3_migration_in_progress_posts', time(), WEEK_IN_SECONDS );

		$this->migrateSettings();
		$this->meta->migrateMeta();
	}

	/**
	 * Reruns the post meta migration.
	 *
	 * This is meant for users on v4.0.0, v4.0.1 or v4.0.2 where the migration might have failed.
	 *
	 * @since 4.0.3
	 *
	 * @return void
	 */
	public function redoMetaMigration() {
		aioseo()->core->cache->update( 'v3_migration_in_progress_posts', time(), WEEK_IN_SECONDS );
		$this->meta->migrateMeta();
	}

	/**
	 * Migrates the plugin settings.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $oldOptions The old options. We pass it in directly via the Importer/Exporter.
	 * @return void
	 */
	public function migrateSettings( $oldOptions = [] ) {
		if ( empty( $this->oldOptions ) && ! empty( $oldOptions ) ) {
			$this->oldOptions = ( new OldOptions( $oldOptions ) )->oldOptions;

			if (
				! $this->oldOptions ||
				! is_array( $this->oldOptions ) ||
				! count( $this->oldOptions )
			) {
				return;
			}
		}

		aioseo()->core->cache->update( 'v3_migration_in_progress_settings', time() );

		new GeneralSettings();

		if ( ! isset( $this->oldOptions['modules']['aiosp_feature_manager_options'] ) ) {
			new Sitemap();
			aioseo()->core->cache->delete( 'v3_migration_in_progress_settings' );

			return;
		}

		$this->migrateFeatureManager();

		if ( isset( $this->oldOptions['modules']['aiosp_feature_manager_options']['aiosp_feature_manager_enable_opengraph'] ) ) {
			new SocialMeta();
		}

		if ( isset( $this->oldOptions['modules']['aiosp_feature_manager_options']['aiosp_feature_manager_enable_sitemap'] ) ) {
			new Sitemap();
		}

		if ( isset( $this->oldOptions['modules']['aiosp_feature_manager_options']['aiosp_feature_manager_enable_robots'] ) ) {
			new RobotsTxt();
		}

		if ( aioseo()->helpers->isWpmlActive() ) {
			new Wpml();
		}

		aioseo()->core->cache->delete( 'v3_migration_in_progress_settings' );
	}

	/**
	 * Migrates the Feature Manager settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function migrateFeatureManager() {
		if ( empty( $this->oldOptions['modules']['aiosp_feature_manager_options'] ) ) {
			return;
		}

		if ( empty( $this->oldOptions['modules']['aiosp_feature_manager_options']['aiosp_feature_manager_enable_opengraph'] ) ) {
			aioseo()->options->social->facebook->general->enable = false;
			aioseo()->options->social->twitter->general->enable  = false;
		}

		if ( empty( $this->oldOptions['modules']['aiosp_feature_manager_options']['aiosp_feature_manager_enable_sitemap'] ) ) {
			aioseo()->options->sitemap->general->enable = false;
			aioseo()->options->sitemap->rss->enable     = false;
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_feature_manager_options']['aiosp_feature_manager_enable_robots'] ) ) {
			aioseo()->options->tools->robots->enable = true;
		}
	}

	/**
	 * Checks whether the V3 migration is running.
	 *
	 * @since 4.1.8
	 *
	 * @return bool Whether the V3 migration is running.
	 */
	public function isMigrationRunning() {
		return aioseo()->core->cache->get( 'v3_migration_in_progress_settings' ) || aioseo()->core->cache->get( 'v3_migration_in_progress_posts' );
	}
}Common/Migration/Sitemap.php000066600000035775151135505570012074 0ustar00<?php
namespace AIOSEO\Plugin\Common\Migration;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the XML Sitemap settings from V3.
 *
 * @since 4.0.0
 */
class Sitemap {
	/**
	 * The old V3 options.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $oldOptions = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->oldOptions = aioseo()->migration->oldOptions;

		if ( empty( $this->oldOptions['modules']['aiosp_sitemap_options'] ) ) {
			return;
		}

		$this->checkIfStatic();
		$this->migrateLinksPerIndex();
		$this->migrateIncludedObjects();
		$this->migratePrioFreq();
		$this->migrateAdditionalPages();
		$this->migrateExcludedPages();
		$this->regenerateSitemap();

		$settings = [
			'aiosp_sitemap_indexes'          => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'general', 'indexes' ] ],
			'aiosp_sitemap_archive'          => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'general', 'date' ] ],
			'aiosp_sitemap_author'           => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'general', 'author' ] ],
			'aiosp_sitemap_images'           => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'excludeImages' ] ],
			'aiosp_sitemap_rss_sitemap'      => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'rss', 'enable' ] ],
			'aiosp_sitemap_filename'         => [ 'type' => 'string', 'newOption' => [ 'sitemap', 'general', 'filename' ] ],
			'aiosp_sitemap_publication_name' => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'news', 'publicationName' ] ],
			'aiosp_sitemap_rewrite'          => [ 'type' => 'boolean', 'newOption' => [ 'deprecated', 'sitemap', 'general', 'advancedSettings', 'dynamic' ] ]
		];

		aioseo()->migration->helpers->mapOldToNew( $settings, $this->oldOptions['modules']['aiosp_sitemap_options'] );

		if (
			aioseo()->options->sitemap->general->advancedSettings->excludePosts ||
			aioseo()->options->sitemap->general->advancedSettings->excludeTerms ||
			aioseo()->options->sitemap->general->advancedSettings->excludeImages ||
			( in_array( 'staticSitemap', aioseo()->internalOptions->internal->deprecatedOptions, true ) && ! aioseo()->options->deprecated->sitemap->general->advancedSettings->dynamic )
		) {
			aioseo()->options->sitemap->general->advancedSettings->enable = true;
		}
	}

	/**
	 * Check if the sitemap is statically generated.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function checkIfStatic() {
		if (
			isset( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_rewrite'] ) &&
			empty( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_rewrite'] )
		) {
			$deprecatedOptions = aioseo()->internalOptions->internal->deprecatedOptions;
			array_push( $deprecatedOptions, 'staticSitemap' );
			aioseo()->internalOptions->internal->deprecatedOptions = $deprecatedOptions;

			aioseo()->options->deprecated->sitemap->general->advancedSettings->dynamic = false;
		}
	}

	/**
	 * Migrates the amount of links per sitemap index.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateLinksPerIndex() {
		if ( ! empty( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_max_posts'] ) ) {
			$value = intval( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_max_posts'] );
			if ( ! $value ) {
				return;
			}
			$value = $value > 50000 ? 50000 : $value;
			aioseo()->options->sitemap->general->linksPerIndex = $value;
		}
	}

	/**
	 * Migrates the excluded object settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function migrateExcludedPages() {
		if (
			empty( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_excl_terms'] ) &&
			empty( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_excl_pages'] )
		) {
			return;
		}

		$excludedPosts = aioseo()->options->sitemap->general->advancedSettings->excludePosts;
		if ( ! empty( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_excl_pages'] ) ) {
			$pages = explode( ',', $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_excl_pages'] );
			if ( count( $pages ) ) {
				foreach ( $pages as $page ) {
					$page = trim( $page );
					$id   = intval( $page );
					if ( ! $id ) {
						$post = get_page_by_path( $page, OBJECT, aioseo()->helpers->getPublicPostTypes( true ) );
						if ( $post && is_object( $post ) ) {
							$id = $post->ID;
						}
					}

					if ( $id ) {
						$post = get_post( $id );
						if ( ! is_object( $post ) ) {
							continue;
						}

						$excludedPost        = new \stdClass();
						$excludedPost->value = $id;
						$excludedPost->type  = $post->post_type;
						$excludedPost->label = $post->post_title;
						$excludedPost->link  = get_permalink( $id );

						array_push( $excludedPosts, wp_json_encode( $excludedPost ) );
					}
				}
			}
		}
		aioseo()->options->sitemap->general->advancedSettings->excludePosts = $excludedPosts;

		$excludedTerms = aioseo()->options->sitemap->general->advancedSettings->excludeTerms;
		if ( ! empty( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_excl_terms'] ) ) {
			foreach ( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_excl_terms'] as $taxonomy ) {
				foreach ( $taxonomy['terms'] as $id ) {
					$term = get_term( $id );
					if ( ! is_a( $term, 'WP_Term' ) ) {
						continue;
					}

					$excludedTerm        = new \stdClass();
					$excludedTerm->value = $id;
					$excludedTerm->type  = $term->taxonomy;
					$excludedTerm->label = $term->name;
					$excludedTerm->link  = get_term_link( $term );

					array_push( $excludedTerms, wp_json_encode( $excludedTerm ) );
				}
			}
		}
		aioseo()->options->sitemap->general->advancedSettings->excludeTerms = $excludedTerms;
	}

	/**
	 * Migrates the objects that are included in the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function migrateIncludedObjects() {
		if (
			! isset( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_posttypes'] ) &&
			! isset( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_taxonomies'] )
		) {
			return;
		}

		if ( ! is_array( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_posttypes'] ) ) {
			$this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_posttypes'] = [];
		}

		if ( ! is_array( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_taxonomies'] ) ) {
			$this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_taxonomies'] = [];
		}

		$publicPostTypes  = aioseo()->helpers->getPublicPostTypes( true );
		$publicTaxonomies = aioseo()->helpers->getPublicTaxonomies( true );

		if ( in_array( 'all', $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_posttypes'], true ) ) {
			aioseo()->options->sitemap->general->postTypes->all      = true;
			aioseo()->options->sitemap->general->postTypes->included = array_values( $publicPostTypes );
		} else {
			$allPostTypes = true;
			foreach ( $publicPostTypes as $postType ) {
				if ( ! in_array( $postType, $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_posttypes'], true ) ) {
					$allPostTypes = false;
				}
			}

			aioseo()->options->sitemap->general->postTypes->all      = $allPostTypes;
			aioseo()->options->sitemap->general->postTypes->included = array_values(
				array_intersect( $publicPostTypes, $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_posttypes'] )
			);
		}

		if ( in_array( 'all', $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_taxonomies'], true ) ) {
			aioseo()->options->sitemap->general->taxonomies->all      = true;
			aioseo()->options->sitemap->general->taxonomies->included = array_values( $publicTaxonomies );
		} else {
			$allTaxonomies = true;
			foreach ( $publicTaxonomies as $taxonomy ) {
				if ( ! in_array( $taxonomy, $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_taxonomies'], true ) ) {
					$allTaxonomies = false;
				}
			}

			aioseo()->options->sitemap->general->taxonomies->all      = $allTaxonomies;
			aioseo()->options->sitemap->general->taxonomies->included = array_values(
				array_intersect( $publicTaxonomies, $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_taxonomies'] )
			);
		}
	}

	/**
	 * Migrates the additional pages that are included in the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateAdditionalPages() {
		if ( empty( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_addl_pages'] ) ) {
			return;
		}

		$pages = [];
		foreach ( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_addl_pages'] as $url => $values ) {
			$page               = new \stdClass();
			$page->url          = esc_url( wp_strip_all_tags( $url ) );
			$page->priority     = [ 'label' => $values['prio'], 'value' => $values['prio'] ];
			$page->frequency    = [ 'label' => $values['freq'], 'value' => $values['freq'] ];
			$page->lastModified = gmdate( 'm/d/Y', strtotime( $values['mod'] ) );

			$pages[] = wp_json_encode( $page );
		}

		aioseo()->options->sitemap->general->additionalPages->enable = true;
		aioseo()->options->sitemap->general->additionalPages->pages  = $pages;
	}

	/**
	 * Migrates the priority/frequency settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migratePrioFreq() {
		$settings = [
			'aiosp_sitemap_prio_homepage'   => [ 'type' => 'float', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'homePage', 'priority' ] ],
			'aiosp_sitemap_freq_homepage'   => [ 'type' => 'string', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'homePage', 'frequency' ] ],
			'aiosp_sitemap_prio_post'       => [ 'type' => 'float', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'postTypes', 'priority' ] ],
			'aiosp_sitemap_freq_post'       => [ 'type' => 'string', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'postTypes', 'frequency' ] ],
			'aiosp_sitemap_prio_post_post'  => [ 'type' => 'float', 'newOption' => [ 'sitemap', 'priority', 'postTypes', 'post', 'priority' ], 'dynamic' => true ],
			'aiosp_sitemap_freq_post_post'  => [ 'type' => 'string', 'newOption' => [ 'sitemap', 'priority', 'postTypes', 'post', 'frequency' ], 'dynamic' => true ],
			'aiosp_sitemap_prio_taxonomies' => [ 'type' => 'float', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'taxonomies', 'priority' ] ],
			'aiosp_sitemap_freq_taxonomies' => [ 'type' => 'string', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'taxonomies', 'frequency' ] ],
			'aiosp_sitemap_prio_archive'    => [ 'type' => 'float', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'archive', 'priority' ] ],
			'aiosp_sitemap_freq_archive'    => [ 'type' => 'string', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'archive', 'frequency' ] ],
			'aiosp_sitemap_prio_author'     => [ 'type' => 'float', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'author', 'priority' ] ],
			'aiosp_sitemap_freq_author'     => [ 'type' => 'string', 'newOption' => [ 'sitemap', 'general', 'advancedSettings', 'priority', 'author', 'frequency' ] ],
		];

		foreach ( $this->oldOptions['modules']['aiosp_sitemap_options'] as $name => $value ) {
			// Ignore fixed settings.
			if ( in_array( $name, array_keys( $settings ), true ) ) {
				continue;
			}

			$type = false;
			$slug = '';
			if ( preg_match( '#aiosp_sitemap_prio_(.*)#', (string) $name, $slug ) ) {
				$type = 'priority';
			} elseif ( preg_match( '#aiosp_sitemap_freq_(.*)#', (string) $name, $slug ) ) {
				$type = 'frequency';
			}

			if ( empty( $slug ) || empty( $slug[1] ) ) {
				continue;
			}

			$objectSlug = aioseo()->helpers->pregReplace( '#post_(?!tag)|taxonomies_#', '', $slug[1] );

			if ( in_array( $objectSlug, aioseo()->helpers->getPublicPostTypes( true ), true ) ) {
				$settings[ $name ] = [
					'type'      => 'priority' === $type ? 'float' : 'string',
					'newOption' => [ 'sitemap', 'priority', 'postTypes', $objectSlug, $type ],
					'dynamic'   => true
				];
				continue;
			}

			if ( in_array( $objectSlug, aioseo()->helpers->getPublicTaxonomies( true ), true ) ) {
				$settings[ $name ] = [
					'type'      => 'priority' === $type ? 'float' : 'string',
					'newOption' => [ 'sitemap', 'priority', 'taxonomies', $objectSlug, $type ],
					'dynamic'   => true
				];
			}
		}

		$mainOptions    = aioseo()->options->noConflict();
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		foreach ( $settings as $name => $values ) {
			// If setting is set to default, do nothing.
			if (
				empty( $this->oldOptions['modules']['aiosp_sitemap_options'][ $name ] ) ||
				'no' === $this->oldOptions['modules']['aiosp_sitemap_options'][ $name ]
			) {
				unset( $settings[ $name ] );
				continue;
			}

			// If value is "Select Individual", set grouped to false.
			$value = $this->oldOptions['modules']['aiosp_sitemap_options'][ $name ];
			if ( 'sel' === $value ) {
				if ( preg_match( '#post$#', (string) $name ) ) {
					aioseo()->options->sitemap->general->advancedSettings->priority->postTypes->grouped = false;
				} else {
					aioseo()->options->sitemap->general->advancedSettings->priority->taxonomies->grouped = false;
				}
				continue;
			}

			$object        = new \stdClass();
			$object->label = $value;
			$object->value = $value;

			$error      = false;
			$options    = ! empty( $values['dynamic'] ) ? $dynamicOptions : $mainOptions;
			$lastOption = '';
			for ( $i = 0; $i < count( $values['newOption'] ); $i++ ) {
				$lastOption = $values['newOption'][ $i ];
				if ( ! $options->has( $lastOption, false ) ) {
					$error = true;
					break;
				}

				if ( count( $values['newOption'] ) - 1 !== $i ) {
					$options = $options->$lastOption;
				}
			}

			if ( $error ) {
				continue;
			}

			$options->$lastOption = wp_json_encode( $object );
		}

		if ( count( $settings ) ) {
			$mainOptions->sitemap->general->advancedSettings->enable = true;
		}
	}

	/**
	 * Regenerates the sitemap if it is static.
	 *
	 * We need to do this since the stylesheet URLs have changed.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function regenerateSitemap() {
		if (
			isset( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_rewrite'] ) &&
			empty( $this->oldOptions['modules']['aiosp_sitemap_options']['aiosp_sitemap_rewrite'] )
		) {
			$files         = aioseo()->sitemap->file->files();
			$detectedFiles = [];
			foreach ( $files as $filename ) {
				// We don't want to delete the video sitemap here at all.
				$isVideoSitemap = preg_match( '#.*video.*#', (string) $filename ) ? true : false;
				if ( ! $isVideoSitemap ) {
					$detectedFiles[] = $filename;
				}
			}

			$fs = aioseo()->core->fs;
			if ( count( $detectedFiles ) && $fs->isWpfsValid() ) {
				foreach ( $detectedFiles as $file ) {
					$fs->fs->delete( $file, false, 'f' );
				}
			}

			aioseo()->sitemap->file->generate( true );
		}
	}
}Common/Migration/Meta.php000066600000031130151135505570011335 0ustar00<?php
namespace AIOSEO\Plugin\Common\Migration;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

use AIOSEO\Plugin\Common\Models;

/**
 * Migrates the post meta from V3.
 *
 * @since 4.0.0
 */
class Meta {
	/**
	 * Holds the old options array.
	 *
	 * @since 4.0.3
	 *
	 * @var array|null
	 */
	protected static $oldOptions = null;

	/**
	 * Migrates the plugin meta data.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function migrateMeta() {
		try {
			if ( as_next_scheduled_action( 'aioseo_migrate_post_meta' ) ) {
				return;
			}

			as_schedule_single_action( time() + 30, 'aioseo_migrate_post_meta', [], 'aioseo' );
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Migrates the post meta data from V3.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function migratePostMeta() {
		if ( aioseo()->core->cache->get( 'v3_migration_in_progress_settings' ) ) {
			aioseo()->actionScheduler->scheduleSingle( 'aioseo_migrate_post_meta', 30, [], true );

			return;
		}

		$postsPerAction  = 50;
		$publicPostTypes = implode( "', '", aioseo()->helpers->getPublicPostTypes( true ) );
		$timeStarted     = gmdate( 'Y-m-d H:i:s', aioseo()->core->cache->get( 'v3_migration_in_progress_posts' ) );

		$postsToMigrate = aioseo()->core->db
			->start( 'posts' . ' as p' )
			->select( 'p.ID' )
			->leftJoin( 'aioseo_posts as ap', '`p`.`ID` = `ap`.`post_id`' )
			->whereRaw( "( ap.post_id IS NULL OR ap.updated < '$timeStarted' )" )
			->whereRaw( "( p.post_type IN ( '$publicPostTypes' ) )" )
			->whereRaw( 'p.post_status NOT IN( \'auto-draft\' )' )
			->orderBy( 'p.ID DESC' )
			->limit( $postsPerAction )
			->run()
			->result();

		if ( ! $postsToMigrate || ! count( $postsToMigrate ) ) {
			aioseo()->core->cache->delete( 'v3_migration_in_progress_posts' );

			return;
		}

		foreach ( $postsToMigrate as $post ) {
			$newPostMeta = $this->getMigratedPostMeta( $post->ID );

			$aioseoPost = Models\Post::getPost( $post->ID );
			$aioseoPost->set( $newPostMeta );
			$aioseoPost->save();

			$this->updateLocalizedPostMeta( $post->ID, $newPostMeta );
			$this->migrateAdditionalPostMeta( $post->ID );
		}

		if ( count( $postsToMigrate ) === $postsPerAction ) {
			try {
				as_schedule_single_action( time() + 30, 'aioseo_migrate_post_meta', [], 'aioseo' );
			} catch ( \Exception $e ) {
				// Do nothing.
			}
		} else {
			aioseo()->core->cache->delete( 'v3_migration_in_progress_posts' );
		}
	}

	/**
	 * Returns the migrated post meta for a given post.
	 *
	 * @since 4.0.3
	 *
	 * @param  int   $postId The post ID.
	 * @return array         The post meta.
	 */
	public function getMigratedPostMeta( $postId ) {
		if ( is_category() || is_tag() || is_tax() || ! is_numeric( $postId ) ) {
			return [];
		}

		if ( null === self::$oldOptions ) {
			self::$oldOptions = get_option( 'aioseop_options' );
		}

		if ( empty( self::$oldOptions ) ) {
			return [];
		}

		$postMeta = aioseo()->core->db
			->start( 'postmeta' . ' as pm' )
			->select( 'pm.meta_key, pm.meta_value' )
			->where( 'pm.post_id', $postId )
			->whereRaw( "`pm`.`meta_key` LIKE '_aioseop_%'" )
			->run()
			->result();

		$mappedMeta = [
			'_aioseop_title'              => 'title',
			'_aioseop_description'        => 'description',
			'_aioseop_custom_link'        => 'canonical_url',
			'_aioseop_sitemap_exclude'    => '',
			'_aioseop_disable'            => '',
			'_aioseop_noindex'            => 'robots_noindex',
			'_aioseop_nofollow'           => 'robots_nofollow',
			'_aioseop_sitemap_priority'   => 'priority',
			'_aioseop_sitemap_frequency'  => 'frequency',
			'_aioseop_keywords'           => 'keywords',
			'_aioseop_opengraph_settings' => ''
		];

		$meta = [
			'post_id' => $postId,
		];

		if ( ! $postMeta || ! count( $postMeta ) ) {
			return $meta;
		}

		foreach ( $postMeta as $record ) {
			$name  = $record->meta_key;
			$value = $record->meta_value;

			if ( ! in_array( $name, array_keys( $mappedMeta ), true ) ) {
				continue;
			}

			switch ( $name ) {
				case '_aioseop_description':
					$meta[ $mappedMeta[ $name ] ] = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $value ) );
					break;
				case '_aioseop_title':
					if ( ! empty( $value ) ) {
						$meta[ $mappedMeta[ $name ] ] = $this->getPostTitle( $postId, $value );
					}
					break;
				case '_aioseop_sitemap_exclude':
					if ( empty( $value ) ) {
						break;
					}
					$this->migrateExcludedPost( $postId );
					break;
				case '_aioseop_disable':
					if ( empty( $value ) ) {
						break;
					}
					$this->migrateSitemapExcludedPost( $postId );
					break;
				case '_aioseop_noindex':
				case '_aioseop_nofollow':
					if ( 'on' === (string) $value ) {
						$meta['robots_default']       = false;
						$meta[ $mappedMeta[ $name ] ] = true;
					} elseif ( 'off' === (string) $value ) {
						$meta['robots_default'] = false;
					}
					break;
				case '_aioseop_keywords':
					$meta[ $mappedMeta[ $name ] ] = aioseo()->migration->helpers->oldKeywordsToNewKeywords( $value );
					break;
				case '_aioseop_opengraph_settings':
					$meta += $this->convertOpenGraphMeta( $value );
					break;
				case '_aioseop_sitemap_priority':
				case '_aioseop_sitemap_frequency':
					if ( empty( $value ) ) {
						$meta[ $mappedMeta[ $name ] ] = 'default';
						break;
					}
					$meta[ $mappedMeta[ $name ] ] = $value;
					break;
				default:
					$meta[ $mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
					break;
			}
		}

		return $meta;
	}

	/**
	 * Migrates a given disabled post from V3.
	 *
	 * @since 4.0.3
	 *
	 * @param  int  $postId The post ID.
	 * @return void
	 */
	private function migrateExcludedPost( $postId ) {
		$post = get_post( $postId );
		if ( ! is_object( $post ) ) {
			return;
		}

		aioseo()->options->sitemap->general->advancedSettings->enable = true;
		$excludedPosts = aioseo()->options->sitemap->general->advancedSettings->excludePosts;

		foreach ( $excludedPosts as $excludedPost ) {
			$excludedPost = json_decode( $excludedPost );
			if ( $excludedPost->value === $postId ) {
				return;
			}
		}

		$excludedPost = [
			'value' => $post->ID,
			'type'  => $post->post_type,
			'label' => $post->post_title,
			'link'  => get_permalink( $post )
		];

		$excludedPosts[] = wp_json_encode( $excludedPost );
		aioseo()->options->sitemap->general->advancedSettings->excludePosts = $excludedPosts;

		$deprecatedOptions = aioseo()->internalOptions->internal->deprecatedOptions;
		if ( ! in_array( 'excludePosts', $deprecatedOptions, true ) ) {
			array_push( $deprecatedOptions, 'excludePosts' );
			aioseo()->internalOptions->internal->deprecatedOptions = $deprecatedOptions;
		}
	}

	/**
	 * Migrates a given sitemap excluded post from V3.
	 *
	 * @since 4.0.3
	 *
	 * @param  int  $postId The post ID.
	 * @return void
	 */
	private function migrateSitemapExcludedPost( $postId ) {
		$post = get_post( $postId );
		if ( ! is_object( $post ) ) {
			return;
		}

		$excludedPosts = aioseo()->options->deprecated->searchAppearance->advanced->excludePosts;
		foreach ( $excludedPosts as $excludedPost ) {
			$excludedPost = json_decode( $excludedPost );
			if ( $excludedPost->value === $postId ) {
				return;
			}
		}

		$excludedPost = [
			'value' => $post->ID,
			'type'  => $post->post_type,
			'label' => $post->post_title,
			'link'  => get_permalink( $post )
		];

		$excludedPosts[] = wp_json_encode( $excludedPost );
		aioseo()->options->deprecated->searchAppearance->advanced->excludePosts = $excludedPosts;
	}

	/**
	 * Updates the traditional post meta table with the new data.
	 *
	 * @since 4.1.0
	 *
	 * @param  int   $postId  The post ID.
	 * @param  array $newMeta The new meta data.
	 * @return void
	 */
	protected function updateLocalizedPostMeta( $postId, $newMeta ) {
		$localizedFields = [
			'title',
			'description',
			'keywords',
			'og_title',
			'og_description',
			'og_article_section',
			'og_article_tags',
			'twitter_title',
			'twitter_description'
		];

		foreach ( $newMeta as $k => $v ) {
			if ( ! in_array( $k, $localizedFields, true ) ) {
				continue;
			}

			if ( in_array( $k, [ 'keywords', 'og_article_tags' ], true ) ) {
				$v = ! empty( $v ) ? aioseo()->helpers->jsonTagsToCommaSeparatedList( $v ) : '';
			}

			update_post_meta( $postId, "_aioseo_{$k}", $v );
		}
	}

	/**
	 * Migrates additional post meta data.
	 *
	 * @since 4.0.2
	 *
	 * @param  int  $postId The post ID.
	 * @return void
	 */
	public function migrateAdditionalPostMeta( $postId ) {
		static $disabled = null;

		if ( null === $disabled ) {
			$disabled = (
				! aioseo()->options->sitemap->general->enable ||
				(
					aioseo()->options->sitemap->general->advancedSettings->enable &&
					aioseo()->options->sitemap->general->advancedSettings->excludeImages
				)
			);
		}
		if ( $disabled ) {
			return;
		}

		aioseo()->sitemap->image->scanPost( $postId );
	}

	/**
	 * Maps the old Open Graph meta to the social meta columns in V4.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $ogMeta The old V3 Open Graph meta.
	 * @return array $meta   The mapped meta.
	 */
	public function convertOpenGraphMeta( $ogMeta ) {
		$ogMeta = aioseo()->helpers->maybeUnserialize( $ogMeta );

		if ( ! is_array( $ogMeta ) ) {
			return [];
		}

		$mappedSocialMeta = [
			'aioseop_opengraph_settings_title'             => 'og_title',
			'aioseop_opengraph_settings_desc'              => 'og_description',
			'aioseop_opengraph_settings_image'             => 'og_image_custom_url',
			'aioseop_opengraph_settings_imagewidth'        => 'og_image_width',
			'aioseop_opengraph_settings_imageheight'       => 'og_image_height',
			'aioseop_opengraph_settings_video'             => 'og_video',
			'aioseop_opengraph_settings_videowidth'        => 'og_video_width',
			'aioseop_opengraph_settings_videoheight'       => 'og_video_height',
			'aioseop_opengraph_settings_category'          => 'og_object_type',
			'aioseop_opengraph_settings_section'           => 'og_article_section',
			'aioseop_opengraph_settings_tag'               => 'og_article_tags',
			'aioseop_opengraph_settings_setcard'           => 'twitter_card',
			'aioseop_opengraph_settings_customimg_twitter' => 'twitter_image_custom_url',
		];

		$meta = [];
		foreach ( $ogMeta as $name => $value ) {
			if ( ! in_array( $name, array_keys( $mappedSocialMeta ), true ) ) {
				continue;
			}

			switch ( $name ) {
				case 'aioseop_opengraph_settings_desc':
				case 'aioseop_opengraph_settings_title':
					$meta[ $mappedSocialMeta[ $name ] ] = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $value ) );
					break;
				case 'aioseop_opengraph_settings_image':
					$value = strval( $value );
					if ( empty( $value ) ) {
						break;
					}

					$meta['og_image_type']              = 'custom_image';
					$meta[ $mappedSocialMeta[ $name ] ] = strval( $value );
					break;
				case 'aioseop_opengraph_settings_video':
					$meta[ $mappedSocialMeta[ $name ] ] = esc_url( $value );
					break;
				case 'aioseop_opengraph_settings_customimg_twitter':
					$value = strval( $value );
					if ( empty( $value ) ) {
						break;
					}
					$meta['twitter_image_type']         = 'custom_image';
					$meta['twitter_use_og']             = false;
					$meta[ $mappedSocialMeta[ $name ] ] = strval( $value );
					break;
				case 'aioseop_opengraph_settings_imagewidth':
				case 'aioseop_opengraph_settings_imageheight':
				case 'aioseop_opengraph_settings_videowidth':
				case 'aioseop_opengraph_settings_videoheight':
					$value = intval( $value );
					if ( ! $value || $value <= 0 ) {
						break;
					}
					$meta[ $mappedSocialMeta[ $name ] ] = $value;
					break;
				case 'aioseop_opengraph_settings_tag':
					$meta[ $mappedSocialMeta[ $name ] ] = aioseo()->migration->helpers->oldKeywordsToNewKeywords( $value );
					break;
				default:
					$meta[ $mappedSocialMeta[ $name ] ] = esc_html( strval( $value ) );
					break;
			}
		}

		return $meta;
	}

	/**
	 * Returns the title as it was in V3.
	 *
	 * @since 4.0.0
	 *
	 * @param  int    $postId   The post ID.
	 * @param  string $seoTitle The old SEO title.
	 * @return string           The title.
	 */
	protected function getPostTitle( $postId, $seoTitle = '' ) {
		$post = get_post( $postId );
		if ( ! is_object( $post ) ) {
			return '';
		}

		$postType    = $post->post_type;
		$oldOptions  = get_option( 'aioseo_options_v3' );
		$titleFormat = isset( $oldOptions[ "aiosp_{$postType}_title_format" ] ) ? $oldOptions[ "aiosp_{$postType}_title_format" ] : '';

		$seoTitle = aioseo()->helpers->pregReplace( '/(%post_title%|%page_title%)/', $seoTitle, $titleFormat );

		return aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $seoTitle ) );
	}
}Common/Migration/OldOptions.php000066600000014772151135505570012556 0ustar00<?php
namespace AIOSEO\Plugin\Common\Migration;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Updates and holds the old options from V3.
 *
 * @since 4.0.0
 */
class OldOptions {
	/**
	 * The old options from V3.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $oldOptions = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 *
	 * @param array $oldOptions The old options. We pass it in directly via the Importer/Exporter.
	 */
	public function __construct( $oldOptions = [] ) {
		$this->oldOptions = ! empty( $oldOptions ) ? $oldOptions : get_option( 'aioseop_options' );

		if (
			! $this->oldOptions ||
			! is_array( $this->oldOptions ) ||
			! count( $this->oldOptions )
		) {
			return;
		}

		$this->runPreV4Migrations();
		$this->fixSettingValues();
	}

	/**
	 * Runs all pre-V4 migrations to update the old options to the latest state.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function runPreV4Migrations() {
		$lastActiveVersion = aioseo()->internalOptions->internal->lastActiveVersion;
		if ( version_compare( $lastActiveVersion, aioseo()->version, '<' ) ) {

			$this->doVersionUpdates( $lastActiveVersion );
			aioseo()->internalOptions->internal->lastActiveVersion = aioseo()->version;
		}
	}

	/**
	 * Runs all pre-V4 version-based migrations.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $oldVersion The old version number to compare against.
	 * @return void
	 */
	protected function doVersionUpdates( $oldVersion ) {
		if ( version_compare( $oldVersion, '3.0', '<' ) ) {
			$this->sitemapExclTerms201905();
		}

		if ( version_compare( $oldVersion, '3.1', '<' ) ) {
			$this->resetFlushRewriteRules201906();
		}

		if (
			version_compare( $oldVersion, '3.2', '<' ) ||
			version_compare( $oldVersion, '3.2.6', '<' )
		) {
			$this->updateSchemaMarkup201907();
		}

		if ( version_compare( $oldVersion, '4.0.0', '<' ) ) {
			$this->updateArchiveNoIndexSettings20200413();
			$this->updateArchiveTitleFormatSettings20200413();
		}
	}

	/**
	 * Converts "excl_categories" to "excl_terms".
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function sitemapExclTerms201905() {
		if (
			empty( $this->oldOptions['modules'] ) ||
			empty( $this->oldOptions['modules']['aiosp_sitemap_options'] )
		) {
			return;
		}

		$options = $this->oldOptions['modules']['aiosp_sitemap_options'];
		if ( ! empty( $options['aiosp_sitemap_excl_categories'] ) ) {
			$options['aiosp_sitemap_excl_terms']['category']['taxonomy'] = 'category';
			$options['aiosp_sitemap_excl_terms']['category']['terms']    = $options['aiosp_sitemap_excl_categories'];
			unset( $options['aiosp_sitemap_excl_categories'] );

			$this->oldOptions['modules']['aiosp_sitemap_options'] = $options;
		}
	}

	/**
	 * Flushes rewrite rules for XML Sitemap URL changes.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function resetFlushRewriteRules201906() {
		add_action( 'shutdown', 'flush_rewrite_rules' );
	}

	/**
	 * Adds a number of schema markup settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function updateSchemaMarkup201907() {
		$updateValues = [
			'aiosp_schema_markup'               => '1',
			'aiosp_schema_search_results_page'  => '1',
			'aiosp_schema_social_profile_links' => '',
			'aiosp_schema_site_represents'      => 'organization',
			'aiosp_schema_organization_name'    => '',
			'aiosp_schema_organization_logo'    => '',
			'aiosp_schema_person_user'          => '1',
			'aiosp_schema_phone_number'         => '',
			'aiosp_schema_contact_type'         => 'none',
		];

		if ( isset( $this->oldOptions['aiosp_schema_markup'] ) ) {
			if ( empty( $this->oldOptions['aiosp_schema_markup'] ) || 'off' === $this->oldOptions['aiosp_schema_markup'] ) {
				$updateValues['aiosp_schema_markup'] = '0';
			}
		}
		if ( isset( $this->oldOptions['aiosp_google_sitelinks_search'] ) ) {
			if ( empty( $this->oldOptions['aiosp_google_sitelinks_search'] ) || 'off' === $this->oldOptions['aiosp_google_sitelinks_search'] ) {
				$updateValues['aiosp_schema_search_results_page'] = '0';
			}
		}
		if ( isset( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_profile_links'] ) ) {
			$updateValues['aiosp_schema_social_profile_links'] = $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_profile_links'];
		}
		if ( isset( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_person_or_org'] ) ) {
			if ( 'person' === $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_person_or_org'] ) {
				$updateValues['aiosp_schema_site_represents'] = 'person';
			}
		}
		if ( isset( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_social_name'] ) ) {
			$updateValues['aiosp_schema_organization_name'] = $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_social_name'];
		}

		foreach ( $updateValues as $k => $v ) {
			$this->oldOptions[ $k ] = $v;
		}
	}

	/**
	 * Migrate setting for noindex archives.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function updateArchiveNoIndexSettings20200413() {
		if ( isset( $this->oldOptions['aiosp_archive_noindex'] ) ) {
			$this->oldOptions['aiosp_archive_date_noindex']   = $this->oldOptions['aiosp_archive_noindex'];
			$this->oldOptions['aiosp_archive_author_noindex'] = $this->oldOptions['aiosp_archive_noindex'];
			unset( $this->oldOptions['aiosp_archive_noindex'] );
		}
	}

	/**
	 * Migrate settings for archive title formats.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function updateArchiveTitleFormatSettings20200413() {
		if (
			isset( $this->oldOptions['aiosp_archive_title_format'] ) &&
			empty( $this->oldOptions['aiosp_date_title_format'] )
		) {
			$this->oldOptions['aiosp_date_title_format'] = $this->oldOptions['aiosp_archive_title_format'];
			unset( $this->oldOptions['aiosp_archive_title_format'] );
		}

		if (
			isset( $this->oldOptions['aiosp_archive_title_format'] ) &&
			'%date% | %site_title%' === $this->oldOptions['aiosp_archive_title_format']
		) {
			unset( $this->oldOptions['aiosp_archive_title_format'] );
		}
	}

	/**
	 * Corrects the value of a number of settings in V3 that are illogical.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function fixSettingValues() {
		$settingsToFix = [
			'aiosp_togglekeywords'
		];
		foreach ( $settingsToFix as $settingToFix ) {
			if ( isset( $this->oldOptions[ $settingToFix ] ) ) {
				if ( '1' === (string) $this->oldOptions[ $settingToFix ] ) {
					$this->oldOptions[ $settingToFix ] = '';
					continue;
				}
				$this->oldOptions[ $settingToFix ] = 'on';
			}
		}
	}
}Common/Migration/SocialMeta.php000066600000045565151135505570012511 0ustar00<?php
namespace AIOSEO\Plugin\Common\Migration;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Social Meta settings from V3.
 *
 * @since 4.0.0
 */
class SocialMeta {
	/**
	 * The old V3 options.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $oldOptions = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->oldOptions = aioseo()->migration->oldOptions;

		if ( empty( $this->oldOptions['modules']['aiosp_opengraph_options'] ) ) {
			return;
		}

		$this->migrateHomePageOgTitle();
		$this->migrateHomePageOgDescription();
		$this->migrateTwitterUsername();
		$this->migrateTwitterCardType();
		$this->migrateSocialPostImageSettings();
		$this->migrateDefaultObjectTypes();
		$this->migrateAdvancedSettings();
		$this->migrateProfileSocialUrls();

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_sitename'] ) ) {
			aioseo()->options->social->facebook->general->siteName = aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_sitename'] );
		}

		$settings = [
			'aiosp_opengraph_facebook_author' => [ 'type' => 'boolean', 'newOption' => [ 'social', 'facebook', 'general', 'showAuthor' ] ],
			'aiosp_opengraph_twitter_creator' => [ 'type' => 'boolean', 'newOption' => [ 'social', 'twitter', 'general', 'showAuthor' ] ],
		];

		aioseo()->migration->helpers->mapOldToNew( $settings, $this->oldOptions['modules']['aiosp_opengraph_options'] );

		$this->maybeShowOgNotices();
	}

	/**
	 * Check if we need to add a notice about the OG deprecated settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function maybeShowOgNotices() {
		$include = [];

		// Check if any of thw following are set to true.
		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_generate_descriptions'] ) ) {
			$include[] = __( 'Use Content for Autogenerated Descriptions', 'all-in-one-seo-pack' );
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_description_shortcodes'] ) ) {
			$include[] = __( 'Run Shortcodes in Description', 'all-in-one-seo-pack' );
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_title_shortcodes'] ) ) {
			$include[] = __( 'Run Shortcodes in Title', 'all-in-one-seo-pack' );
		}

		if ( empty( $include ) ) {
			return;
		}

		$content = __( 'Due to some changes in how our Open Graph integration works, your Facebook Titles and Descriptions may have changed. You were using the following options that have been removed:', 'all-in-one-seo-pack' ) . '<ul>'; // phpcs:ignore Generic.Files.LineLength.MaxExceeded

		foreach ( $include as $setting ) {
			$content .= '<li><strong>' . $setting . '</strong></li>';
		}

		$content .= '</ul>';

		$notification = Models\Notification::getNotificationByName( 'v3-migration-deprecated-opengraph' );
		if ( $notification->notification_name ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'v3-migration-deprecated-opengraph',
			'title'             => __( 'Review Your Facebook Open Graph Titles and Descriptions', 'all-in-one-seo-pack' ),
			'content'           => $content,
			'type'              => 'warning',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Learn More', 'all-in-one-seo-pack' ),
			'button1_action'    => aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . 'docs/deprecated-opengraph-settings', 'notifications-center', 'v3-migration-deprecated-opengraph' ),
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Migrates the Open Graph homepage title.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateHomePageOgTitle() {
		$showOnFront        = get_option( 'show_on_front' );
		$pageOnFront        = (int) get_option( 'page_on_front' );
		$useHomePageMeta    = ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_setmeta'] );
		$format             = $this->oldOptions['aiosp_home_page_title_format'];

		// Latest Posts.
		if ( 'posts' === $showOnFront ) {
			$ogTitle = aioseo()->helpers->pregReplace( '#%page_title%#', '#site_title', $format );
			if ( ! $useHomePageMeta ) {
				if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_hometitle'] ) ) {
					$ogTitle = $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_hometitle'];
				}
				aioseo()->options->social->facebook->homePage->title = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $ogTitle ) );
				aioseo()->options->social->twitter->homePage->title  = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $ogTitle ) );

				return;
			}
			$title   = aioseo()->options->searchAppearance->global->siteTitle;
			$ogTitle = $title ? $title : $ogTitle;
			aioseo()->options->social->facebook->homePage->title = aioseo()->helpers->sanitizeOption( $ogTitle );
			aioseo()->options->social->twitter->homePage->title  = aioseo()->helpers->sanitizeOption( $ogTitle );

			return;
		}

		// Static Home Page.
		$post       = 'page' === $showOnFront && $pageOnFront ? aioseo()->helpers->getPost( $pageOnFront ) : '';
		$aioseoPost = Models\Post::getPost( $post->ID );
		$seoTitle   = get_post_meta( $post->ID, '_aioseop_title', true );
		$ogMeta     = get_post_meta( $post->ID, '_aioseop_opengraph_settings', true );

		if ( ! $ogMeta ) {
			return;
		}

		$ogMeta = aioseo()->helpers->maybeUnserialize( $ogMeta );

		$ogTitle = '';
		if ( ! $useHomePageMeta ) {
			if ( empty( $this->oldOptions['aiosp_use_static_home_info'] ) ) {
				$ogTitle = ! empty( $this->oldOptions['aiosp_home_title'] ) ? $this->oldOptions['aiosp_home_title'] : $ogTitle;
				if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_hometitle'] ) ) {
					$ogTitle = $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_hometitle'];
				}
				if ( ! empty( $ogMeta['aioseop_opengraph_settings_title'] ) ) {
					$ogTitle = $ogMeta['aioseop_opengraph_settings_title'];
				} elseif ( ! empty( $seoTitle ) ) {
					if ( empty( $ogTitle ) ) {
						$ogTitle = $seoTitle;
					} elseif ( empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_hometitle'] ) ) {
						$ogTitle = $seoTitle;
					}
				}
			}
		} else {
			if ( empty( $this->oldOptions['aiosp_use_static_home_info'] ) ) {
				$ogTitle = $aioseoPost->title;
				if ( ! empty( $ogMeta['aioseop_opengraph_settings_title'] ) ) {
					$ogTitle = $ogMeta['aioseop_opengraph_settings_title'];
				}
				$ogTitle = ! empty( $this->oldOptions['aiosp_home_title'] ) ? $this->oldOptions['aiosp_home_title'] : $ogTitle;
				if ( ! empty( $seoTitle ) ) {
					$ogTitle = $seoTitle;
				}
			} else {
				$ogTitle = ! empty( $seoTitle ) ? $seoTitle : $ogTitle;
			}
		}

		$ogTitle = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $ogTitle ) );
		$aioseoPost->set( [
			'post_id'       => $post->ID,
			'og_title'      => $ogTitle,
			'twitter_title' => $ogTitle
		] );
		$aioseoPost->save();
	}

	/**
	 * Migrates the Open Graph homepage description.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateHomePageOgDescription() {
		$showOnFront        = get_option( 'show_on_front' );
		$pageOnFront        = (int) get_option( 'page_on_front' );
		$useHomePageMeta    = ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_setmeta'] );
		$format             = $this->oldOptions['aiosp_description_format'];

		if ( 'posts' === $showOnFront ) {
			$ogDescription = aioseo()->helpers->pregReplace( '#%description%#', '#tagline', $format );
			if ( ! $useHomePageMeta ) {
				if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_description'] ) ) {
					$ogDescription = $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_description'];
				}
				aioseo()->options->social->facebook->homePage->description = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $ogDescription ) );
				aioseo()->options->social->twitter->homePage->description = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $ogDescription ) );

				return;
			}
			$description   = aioseo()->options->searchAppearance->global->metaDescription;
			$ogDescription = $description ? $description : $ogDescription;
			aioseo()->options->social->facebook->homePage->description = aioseo()->helpers->sanitizeOption( $ogDescription );
			aioseo()->options->social->twitter->homePage->description  = aioseo()->helpers->sanitizeOption( $ogDescription );

			return;
		}

		$post           = 'page' === $showOnFront && $pageOnFront ? aioseo()->helpers->getPost( $pageOnFront ) : '';
		$aioseoPost     = Models\Post::getPost( $post->ID );
		$seoDescription = get_post_meta( $post->ID, '_aioseop_description', true );
		$ogMeta         = get_post_meta( $post->ID, '_aioseop_opengraph_settings', true );

		if ( ! $ogMeta ) {
			return;
		}

		$ogMeta = aioseo()->helpers->maybeUnserialize( $ogMeta );

		$ogDescription = '';
		if ( ! $useHomePageMeta ) {
			if ( empty( $this->oldOptions['aiosp_use_static_home_info'] ) ) {
				$ogDescription = ! empty( $this->oldOptions['aiosp_home_description'] ) ? $this->oldOptions['aiosp_home_description'] : $ogDescription;
				if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_description'] ) ) {
					$ogDescription = $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_description'];
				}
				if ( ! empty( $ogMeta['aioseop_opengraph_settings_desc'] ) ) {
					$ogDescription = $ogMeta['aioseop_opengraph_settings_desc'];
				} elseif ( ! empty( $seoDescription ) ) {
					if ( empty( $ogDescription ) ) {
						$ogDescription = $seoDescription;
					} elseif ( empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_description'] ) ) {
						$ogDescription = $seoDescription;
					}
				}
			}
		} else {
			if ( empty( $this->oldOptions['aiosp_use_static_home_info'] ) ) {
				$ogDescription = $aioseoPost->description;
				if ( ! empty( $ogMeta['aioseop_opengraph_settings_desc'] ) ) {
					$ogDescription = $ogMeta['aioseop_opengraph_settings_desc'];
				}
				$ogDescription = ! empty( $this->oldOptions['aiosp_home_description'] ) ? $this->oldOptions['aiosp_home_description'] : $ogDescription;
				if ( ! empty( $seoDescription ) ) {
					$ogDescription = $seoDescription;
				}
			} else {
				$ogDescription = ! empty( $seoDescription ) ? $seoDescription : $ogDescription;
			}
		}

		$ogDescription = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $ogDescription ) );
		$aioseoPost->set( [
			'post_id'             => $post->ID,
			'og_description'      => $ogDescription,
			'twitter_description' => $ogDescription
		] );
		$aioseoPost->save();
	}

	/**
	 * Migrates the Open Graph default post images.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateSocialPostImageSettings() {
		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_homeimage'] ) ) {
			$value = esc_url( wp_strip_all_tags( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_homeimage'] ) );
			aioseo()->options->social->facebook->homePage->image = $value;
			aioseo()->options->social->twitter->homePage->image  = $value;
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_defimg'] ) ) {
			$value = aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_defimg'] );
			aioseo()->options->social->facebook->general->defaultImageSourcePosts = $value;
			aioseo()->options->social->twitter->general->defaultImageSourcePosts  = $value;
		}

		if (
			! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_dimg'] ) &&
			! preg_match( '/default-user-image.png$/', (string) $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_dimg'] )
		) {
			$value = esc_url( wp_strip_all_tags( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_dimg'] ) );
			aioseo()->options->social->facebook->general->defaultImagePosts = $value;
			aioseo()->options->social->twitter->general->defaultImagePosts  = $value;
		} else {
			aioseo()->options->social->facebook->general->defaultImagePosts = '';
			aioseo()->options->social->twitter->general->defaultImagePosts  = '';
		}

		if (
			! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_dimgwidth'] ) ||
			! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_dimgheight'] )
		) {
			aioseo()->options->social->facebook->general->defaultImageWidthPosts =
				aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_dimgwidth'] );
			aioseo()->options->social->facebook->general->defaultImageHeightPosts =
				aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_dimgheight'] );
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_meta_key'] ) ) {
			$value = aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_meta_key'] );
			aioseo()->options->social->facebook->general->customFieldImagePosts = $value;
			aioseo()->options->social->twitter->general->customFieldImagePosts  = $value;
		}
	}

	/**
	 * Migrates the Twitter username.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateTwitterUsername() {
		if (
			! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_twitter_site'] ) &&
			! aioseo()->options->social->profiles->urls->twitterUrl
		) {
			$username = ltrim( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_twitter_site'], '@' );
			aioseo()->options->social->profiles->urls->twitterUrl =
				esc_url( 'https://x.com/' . aioseo()->social->twitter->prepareUsername( aioseo()->helpers->sanitizeOption( $username ), false ) );
		}
	}

	/**
	 * Migrates the Twitter card type.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateTwitterCardType() {
		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_defcard'] ) ) {
			aioseo()->options->social->twitter->general->defaultCardType =
				aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_defcard'] );
			aioseo()->options->social->twitter->homePage->cardType =
				aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_defcard'] );
		}
	}

	/**
	 * Migrates the default object types.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateDefaultObjectTypes() {
		foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
			$settingName = "aiosp_opengraph_{$postType}_fb_object_type";
			if ( ! in_array( $settingName, array_keys( $this->oldOptions['modules']['aiosp_opengraph_options'] ), true ) ) {
				continue;
			}

			$dynamicOptions = aioseo()->dynamicOptions->noConflict();
			if ( $dynamicOptions->social->facebook->general->postTypes->has( $postType ) ) {
				aioseo()->dynamicOptions->social->facebook->general->postTypes->$postType->objectType =
					aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options'][ $settingName ] );
			}

			if ( 'post' === $postType ) {
				aioseo()->options->social->facebook->homePage->objectType =
					aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options'][ $settingName ] );
			}
		}
	}

	/**
	 * Migrates a number of advanced settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateAdvancedSettings() {
		$advancedEnabled = false;

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_key'] ) ) {
			$advancedEnabled = true;
			aioseo()->options->social->facebook->advanced->adminId = aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_key'] );
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_appid'] ) ) {
			$advancedEnabled = true;
			aioseo()->options->social->facebook->advanced->appId  = aioseo()->helpers->sanitizeOption( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_appid'] );
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_gen_tags'] ) ) {
			$advancedEnabled = true;
			aioseo()->options->social->facebook->advanced->generateArticleTags = true;
		} else {
			aioseo()->options->social->facebook->advanced->generateArticleTags = false;
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_gen_keywords'] ) ) {
			$advancedEnabled = true;
			aioseo()->options->social->facebook->advanced->useKeywordsInTags = true;
		} else {
			aioseo()->options->social->facebook->advanced->useKeywordsInTags = false;
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_gen_categories'] ) ) {
			$advancedEnabled = true;
			aioseo()->options->social->facebook->advanced->useCategoriesInTags = true;
		} else {
			aioseo()->options->social->facebook->advanced->useCategoriesInTags = false;
		}

		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_gen_post_tags'] ) ) {
			$advancedEnabled = true;
			aioseo()->options->social->facebook->advanced->usePostTagsInTags = true;
		} else {
			aioseo()->options->social->facebook->advanced->usePostTagsInTags = false;
		}

		aioseo()->options->social->facebook->advanced->enable = $advancedEnabled;
	}

	/**
	 * Migrates the social URLs for the author users.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateProfileSocialUrls() {
		$records = aioseo()->core->db
			->start( aioseo()->core->db->db->usermeta, true )
			->select( '*' )
			->where( 'meta_key', 'facebook' )
			->run()
			->result();

		if ( count( $records ) ) {
			foreach ( $records as $record ) {
				if ( ! empty( $record->user_id ) && ! empty( $record->meta_value ) ) {
					update_user_meta(
						(int) $record->user_id,
						'aioseo_facebook',
						esc_url( $record->meta_value )
					);
				}
			}
		}

		$records = aioseo()->core->db
			->start( aioseo()->core->db->db->usermeta, true )
			->select( '*' )
			->where( 'meta_key', 'twitter' )
			->run()
			->result();

		if ( count( $records ) ) {
			foreach ( $records as $record ) {
				if ( ! empty( $record->user_id ) && ! empty( $record->meta_value ) ) {
					update_user_meta(
						(int) $record->user_id,
						'aioseo_twitter',
						sanitize_text_field( $record->meta_value )
					);
				}
			}
		}
	}
}Common/Migration/Wpml.php000066600000010215151135505570011367 0ustar00<?php
namespace AIOSEO\Plugin\Common\Migration;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Migrates the WPML settings from V3.
 *
 * @since 4.0.0
 */
class Wpml {
	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		// If the tables don't exist (could happen), return early.
		if ( ! aioseo()->core->db->tableExists( 'icl_strings' ) && ! aioseo()->core->db->tableExists( 'icl_string_translations' ) ) {
			return;
		}

		$strings = [
			'[aioseop_options]aiosp_home_title'       => '[aioseo_options_localized]searchAppearance_global_siteTitle',
			'[aioseop_options]aiosp_home_description' => '[aioseo_options_localized]searchAppearance_global_metaDescription',
			'[aioseop_options]aiosp_home_keywords'    => '[aioseo_options_localized]searchAppearance_global_keywords'
		];

		try {
			$v3Results = aioseo()->core->db->start( 'icl_strings' )
				->where( 'context', 'admin_texts_aioseop_options' )
				->whereIn( 'name', array_keys( $strings ) )
				->run()
				->result();

			$v4Results = aioseo()->core->db->start( 'icl_strings' )
				->where( 'context', 'admin_texts_aioseo_options_localized' )
				->whereIn( 'name', array_values( $strings ) )
				->run()
				->result();

			if ( ! empty( $v3Results ) ) {
				foreach ( $v3Results as $result ) {
					$translations = aioseo()->core->db->start( 'icl_string_translations' )
						->where( 'string_id', $result->id )
						->run()
						->result();

					if ( empty( $translations ) ) {
						continue;
					}

					$v4ResultId = null;
					if ( ! empty( $v4Results ) ) {
						foreach ( $v4Results as $r ) {
							if ( $r->name === $strings[ $result->name ] ) {
								$v4ResultId = $r->id;
								break;
							}
						}
					}

					if ( ! $v4ResultId ) {
						$v4ResultId = aioseo()->core->db
							->insert( 'icl_strings' )
							->set( [
								'language'                => $result->language,
								'context'                 => 'admin_texts_aioseo_options_localized',
								'name'                    => $strings[ $result->name ],
								'value'                   => $result->value,
								'string_package_id'       => $result->string_package_id,
								'location'                => $result->location,
								'wrap_tag'                => $result->wrap_tag,
								'type'                    => $result->type,
								'title'                   => $result->title,
								'status'                  => $result->status,
								'gettext_context'         => $result->gettext_context,
								'domain_name_context_md5' => md5( 'admin_texts_aioseo_options_localized' . $strings[ $result->name ] ),
								'translation_priority'    => $result->translation_priority,
								'word_count'              => $result->word_count
							] )
							->run()
							->insertId();
					}

					foreach ( $translations as $translation ) {
						// Check if the translation exists first or we'll get a DB error.
						$v4Translation = aioseo()->core->db->start( 'icl_string_translations' )
							->where( 'string_id', $v4ResultId )
							->where( 'language', $translation->language )
							->run()
							->result();

						if ( ! empty( $v4Translation ) ) {
							aioseo()->core->db->update( 'icl_string_translations' )
								->where( 'string_id', $v4ResultId )
								->where( 'language', $translation->language )
								->set( [
									'value' => $translation->value
								] )
								->run();
							continue;
						}

						aioseo()->core->db
							->insert( 'icl_string_translations' )
							->set( [
								'string_id'           => $v4ResultId,
								'language'            => $translation->language,
								'status'              => $translation->status,
								'value'               => $translation->value,
								'mo_string'           => $translation->mo_string,
								'translator_id'       => $translation->translator_id,
								'translation_service' => $translation->translation_service,
								'batch_id'            => $translation->batch_id,
								'translation_date'    => $translation->translation_date
							] )
							->run();
					}
				}
			}
		} catch ( \Exception $e ) {
			// If there are any errors, let's just abort. We dont' want to do anything more.
		}
	}
}Common/Migration/Helpers.php000066600000020076151135505570012060 0ustar00<?php
namespace AIOSEO\Plugin\Common\Migration;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models;

/**
 * Contains a number of helper functions for the V3 migration.
 *
 * @since 4.0.0
 */
class Helpers {
	/**
	 * Maps a list of old settings from V3 to their counterparts in V4.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $mappings      The old settings, mapped to their new settings.
	 * @param  array $group         The old settings group.
	 * @param  bool  $convertMacros Whether to convert the old V3 macros to V4 smart tags.
	 * @return void
	 */
	public function mapOldToNew( $mappings, $group, $convertMacros = false ) {
		if (
			! is_array( $mappings ) ||
			! is_array( $group ) ||
			! count( $mappings ) ||
			! count( $group )
		) {
			return;
		}

		$mainOptions    = aioseo()->options->noConflict();
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		foreach ( $mappings as $name => $values ) {
			if ( ! isset( $group[ $name ] ) ) {
				continue;
			}

			$error      = false;
			$options    = ! empty( $values['dynamic'] ) ? $dynamicOptions : $mainOptions;
			$lastOption = '';
			for ( $i = 0; $i < count( $values['newOption'] ); $i++ ) {
				$lastOption = $values['newOption'][ $i ];
				if ( ! $options->has( $lastOption, false ) ) {
					$error = true;
					break;
				}

				if ( count( $values['newOption'] ) - 1 !== $i ) {
					$options = $options->$lastOption;
				}
			}

			if ( $error ) {
				continue;
			}

			switch ( $values['type'] ) {
				case 'boolean':
					if ( ! empty( $group[ $name ] ) ) {
						$options->$lastOption = true;
						break;
					}
					$options->$lastOption = false;
					break;
				case 'integer':
				case 'float':
					$value = aioseo()->helpers->sanitizeOption( $group[ $name ] );
					if ( $value ) {
						$options->$lastOption = $value;
					}
					break;
				default:
					$value = $group[ $name ];
					if ( $convertMacros ) {
						$value = $this->macrosToSmartTags( $value );
					}
					$options->$lastOption = aioseo()->helpers->sanitizeOption( $value );
					break;
			}
		}
	}

	/**
	 * Replaces the macros from V3 with our new Smart Tags from V4.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string The string.
	 * @return string $string The converted string.
	 */
	public function macrosToSmartTags( $string ) {
		$macros = [
			'%site_title%'             => '#site_title',
			'%blog_title%'             => '#site_title',
			'%site_description%'       => '#tagline',
			'%blog_description%'       => '#tagline',
			'%wp_title%'               => '#post_title',
			'%post_title%'             => '#post_title',
			'%page_title%'             => '#post_title',
			'%post_date%'              => '#post_date',
			'%post_month%'             => '#post_month',
			'%post_year%'              => '#post_year',
			'%date%'                   => '#archive_date',
			'%day%'                    => '#post_day',
			'%month%'                  => '#post_month',
			'%monthnum%'               => '#post_month',
			'%year%'                   => '#post_year',
			'%current_date%'           => '#current_date',
			'%current_day%'            => '#current_day',
			'%current_month%'          => '#current_month',
			'%current_month_i18n%'     => '#current_month',
			'%current_year%'           => '#current_year',
			'%category_title%'         => '#taxonomy_title',
			'%tag%'                    => '#taxonomy_title',
			'%tag_title%'              => '#taxonomy_title',
			'%archive_title%'          => '#archive_title',
			'%taxonomy_title%'         => '#taxonomy_title',
			'%taxonomy_description%'   => '#taxonomy_description',
			'%tag_description%'        => '#taxonomy_description',
			'%category_description%'   => '#taxonomy_description',
			'%author%'                 => '#author_name',
			'%search%'                 => '#search_term',
			'%page%'                   => '#page_number',
			'%site_link%'              => '#site_link',
			'%site_link_raw%'          => '#site_link_alt',
			'%post_link%'              => '#post_link',
			'%post_link_raw%'          => '#post_link_alt',
			'%author_name%'            => '#author_name',
			'%author_link%'            => '#author_link',
			'%image_title%'            => '#image_title',
			'%image_seo_title%'        => '#image_seo_title',
			'%image_seo_description%'  => '#image_seo_description',
			'%post_seo_title%'         => '#post_seo_title',
			'%post_seo_description%'   => '#post_seo_description',
			'%alt_tag%'                => '#alt_tag',
			'%description%'            => '#description',
			// These need to run last so we don't replace other known tags.
			'%.*_title%'               => '#post_title',
			'%[^%]*_author_login%'     => '#author_first_name #author_last_name',
			'%[^%]*_author_nicename%'  => '#author_first_name #author_last_name',
			'%[^%]*_author_firstname%' => '#author_first_name',
			'%[^%]*_author_lastname%'  => '#author_last_name',
		];

		if ( preg_match_all( '#%cf_([^%]*)%#', (string) $string, $matches ) && ! empty( $matches[1] ) ) {
			foreach ( $matches[1] as $name ) {
				if ( preg_match( '#\s#', (string) $name ) ) {
					$notification = Models\Notification::getNotificationByName( 'v3-migration-custom-field' );
					if ( ! $notification->notification_name ) {
						Models\Notification::addNotification( [
							'slug'              => uniqid(),
							'notification_name' => 'v3-migration-custom-field',
							'title'             => __( 'Custom field names with spaces detected', 'all-in-one-seo-pack' ),
							'content'           => sprintf(
								// Translators: 1 - The plugin short name ("AIOSEO"), 2 - Same as previous.
								__( '%1$s has detected that you have one or more custom fields with spaces in their name.
								In order for %2$s to correctly parse these custom fields, their names cannot contain any spaces.', 'all-in-one-seo-pack' ),
								AIOSEO_PLUGIN_SHORT_NAME,
								AIOSEO_PLUGIN_SHORT_NAME
							),
							'type'              => 'warning',
							'level'             => [ 'all' ],
							'button1_label'     => __( 'Remind Me Later', 'all-in-one-seo-pack' ),
							'button1_action'    => 'http://action#notification/v3-migration-custom-field-reminder',
							'start'             => gmdate( 'Y-m-d H:i:s' )
						] );
					}
				} else {
					$string = aioseo()->helpers->pregReplace( "#%cf_$name%#", "#custom_field-$name", $string );
				}
			}
		}

		if ( preg_match_all( '#%tax_([^%]*)%#', (string) $string, $matches ) && ! empty( $matches[1] ) ) {
			foreach ( $matches[1] as $name ) {
				if ( ! preg_match( '#\s#', (string) $name ) ) {
					$string = aioseo()->helpers->pregReplace( "#%tax_$name%#", "#tax_name-$name", $string );
				}
			}
		}

		foreach ( $macros as $macro => $tag ) {
			$string = aioseo()->helpers->pregReplace( "#$macro(?![a-zA-Z0-9_])#im", $tag, $string );
		}

		$string = preg_replace( '/%([a-f0-9]{2}[^%]*)%/i', '#$1#', (string) $string );

		return $string;
	}

	/**
	 * Converts the old comma-separated keywords format to the new JSON format.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $keywords A comma-separated list of keywords.
	 * @return string $keywords The keywords formatted in JSON.
	 */
	public function oldKeywordsToNewKeywords( $keywords ) {
		if ( ! $keywords ) {
			return '';
		}

		$oldKeywords = array_filter( explode( ',', $keywords ) );
		if ( ! is_array( $oldKeywords ) ) {
			return '';
		}

		$keywords = [];
		foreach ( $oldKeywords as $oldKeyword ) {
			$oldKeyword = aioseo()->helpers->sanitizeOption( $oldKeyword );

			$keyword        = new \stdClass();
			$keyword->label = $oldKeyword;
			$keyword->value = $oldKeyword;

			$keywords[] = $keyword;
		}

		return $keywords;
	}

	/**
	 * Resets the plugin so that the migration can run again.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public static function redoMigration() {
		aioseo()->core->db->delete( 'options' )
			->whereRaw( "`option_name` LIKE 'aioseo_options_internal%'" )
			->run();

		aioseo()->core->cache->delete( 'v3_migration_in_progress_posts' );
		aioseo()->core->cache->delete( 'v3_migration_in_progress_terms' );

		aioseo()->actionScheduler->unschedule( 'aioseo_migrate_post_meta' );
		aioseo()->actionScheduler->unschedule( 'aioseo_migrate_term_meta' );
	}
}Common/Migration/RobotsTxt.php000066600000002723151135505570012425 0ustar00<?php
namespace AIOSEO\Plugin\Common\Migration;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Robots.txt settings from V3.
 *
 * @since 4.0.0
 */
class RobotsTxt {
	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$oldOptions = aioseo()->migration->oldOptions;

		$rules = aioseo()->options->tools->robots->rules;

		if (
			! empty( $oldOptions['modules']['aiosp_robots_options'] ) &&
			! empty( $oldOptions['modules']['aiosp_robots_options']['aiosp_robots_rules'] )
		) {
			$rules += $this->convertRules( $oldOptions['modules']['aiosp_robots_options']['aiosp_robots_rules'] );
		}

		aioseo()->options->tools->robots->rules = $rules;
	}

	/**
	 * Converts the old Robots.txt rules to the new format.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $oldRules The old rules.
	 * @return array $newRules The converted rules.
	 */
	private function convertRules( $oldRules ) {
		$newRules = [];
		foreach ( $oldRules as $oldRule ) {
			$newRule                = new \stdClass();
			$newRule->userAgent     = aioseo()->helpers->sanitizeOption( $oldRule['agent'] );
			$newRule->rule          = aioseo()->helpers->sanitizeOption( lcfirst( $oldRule['type'] ) );
			$newRule->directoryPath = aioseo()->helpers->sanitizeOption( $oldRule['path'] );

			array_push( $newRules, wp_json_encode( $newRule ) );
		}

		return $newRules;
	}
}Common/Migration/GeneralSettings.php000066600000103572151135505570013557 0ustar00<?php
namespace AIOSEO\Plugin\Common\Migration;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the General Settings from V3.
 *
 * @since 4.0.0
 */
class GeneralSettings {
	/**
	 * The old V3 options.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $oldOptions = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->oldOptions = aioseo()->migration->oldOptions;

		$this->migrateSeparatorCharacter();
		$this->setDefaultArticleType();
		$this->migrateHomePageMeta();
		$this->migrateTitleFormats();
		$this->migrateDescriptionFormat();
		$this->migrateNoindexSettings();
		$this->migrateNofollowSettings();
		$this->migratePostSeoColumns();
		$this->migrateSocialUrls();
		$this->migrateSchemaMarkupSettings();
		$this->migrateHomePageKeywords();
		$this->migrateDeprecatedAdvancedOptions();
		$this->migrateRssContentSettings();
		$this->migrateRedirectToParent();
		$this->migrateDisabledPosts();
		$this->migrateNoPaginationForCanonicalUrls();

		$settings = [
			'aiosp_admin_bar'                  => [ 'type' => 'boolean', 'newOption' => [ 'advanced', 'adminBarMenu' ] ],
			'aiosp_google_verify'              => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'google' ] ],
			'aiosp_bing_verify'                => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'bing' ] ],
			'aiosp_pinterest_verify'           => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'pinterest' ] ],
			'aiosp_yandex_verify'              => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'yandex' ] ],
			'aiosp_baidu_verify'               => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'baidu' ] ],
			'aiosp_schema_site_represents'     => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'siteRepresents' ] ],
			'aiosp_schema_organization_name'   => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'organizationName' ] ],
			'aiosp_schema_person_manual_name'  => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'personName' ] ],
			'aiosp_schema_organization_logo'   => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'organizationLogo' ] ],
			'aiosp_schema_person_manual_image' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'personLogo' ] ],
			'aiosp_togglekeywords'             => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'useKeywords' ] ],
			'aiosp_use_categories'             => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'useCategoriesForMetaKeywords' ] ],
			'aiosp_use_tags_as_keywords'       => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'useTagsForMetaKeywords' ] ],
			'aiosp_dynamic_postspage_keywords' => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'dynamicallyGenerateKeywords' ] ],
			'aiosp_run_shortcodes'             => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'runShortcodes' ] ]
		];

		aioseo()->migration->helpers->mapOldToNew( $settings, aioseo()->migration->oldOptions );
	}

	/**
	 * Migrates the separator character.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateSeparatorCharacter() {
		aioseo()->options->searchAppearance->global->separator = '|';
	}

	/**
	 * Set the default posts schema type to Article.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function setDefaultArticleType() {
		if ( aioseo()->dynamicOptions->searchAppearance->postTypes->has( 'post' ) ) {
			aioseo()->dynamicOptions->searchAppearance->postTypes->post->articleType = 'Article';
		}
	}

	/**
	 * Migrates the homepage meta.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateHomePageMeta() {
		$this->migrateHomePageTitle();
		$this->migrateHomePageDescription();

		// If the homepage is a static one, we should migrate the meta now.
		$showOnFront = get_option( 'show_on_front' );
		$pageOnFront = (int) get_option( 'page_on_front' );
		if ( 'page' !== $showOnFront || ! $pageOnFront ) {
			return;
		}

		$post       = 'page' === $showOnFront && $pageOnFront ? get_post( $pageOnFront ) : '';
		$aioseoPost = Models\Post::getPost( $post->ID );

		$postMeta = aioseo()->core->db
			->start( 'postmeta' . ' as pm' )
			->select( 'pm.meta_key, pm.meta_value' )
			->where( 'pm.post_id', $post->ID )
			->whereRaw( "`pm`.`meta_key` LIKE '_aioseop_%'" )
			->run()
			->result();

		$mappedMeta = [
			'_aioseop_nofollow'           => 'robots_nofollow',
			'_aioseop_sitemap_priority'   => 'priority',
			'_aioseop_sitemap_frequency'  => 'frequency',
			'_aioseop_keywords'           => 'keywords',
			'_aioseop_opengraph_settings' => '',
		];

		$meta = [
			'post_id' => $post->ID,
		];

		foreach ( $postMeta as $record ) {
			$name  = $record->meta_key;
			$value = $record->meta_value;

			if ( ! in_array( $name, array_keys( $mappedMeta ), true ) ) {
				continue;
			}

			switch ( $name ) {
				case '_aioseop_nofollow':
					$meta[ $mappedMeta[ $name ] ] = ! empty( $value );
					if ( ! empty( $value ) ) {
						$meta['robots_default'] = false;
					}
					break;
				case '_aioseop_keywords':
					$meta[ $mappedMeta[ $name ] ] = aioseo()->migration->helpers->oldKeywordsToNewKeywords( $value );
					break;
				case '_aioseop_opengraph_settings':
					$class = new Meta();
					$meta += $class->convertOpenGraphMeta( $value );

					// We'll deal with the OG title/description in the Social Meta migration class.
					if ( isset( $meta['og_title'] ) ) {
						unset( $meta['og_title'] );
					}
					if ( isset( $meta['og_description'] ) ) {
						unset( $meta['og_description'] );
					}
					break;
				default:
					$meta[ $mappedMeta[ $name ] ] = aioseo()->helpers->sanitizeOption( $value );
					break;
			}
		}

		$aioseoPost->set( $meta );
		$aioseoPost->save();
	}

	/**
	 * Migrates the homepage title.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateHomePageTitle() {
		$showOnFront   = get_option( 'show_on_front' );
		$pageOnFront   = (int) get_option( 'page_on_front' );

		$homePageTitle = ! empty( $this->oldOptions['aiosp_home_title'] ) ? $this->oldOptions['aiosp_home_title'] : '';
		$format        = $this->oldOptions['aiosp_home_page_title_format'];

		if ( 'posts' === $showOnFront ) {
			$homePageTitle = $homePageTitle ? $homePageTitle : get_bloginfo( 'name' );
			$title         = empty( $format ) ? $homePageTitle : aioseo()->helpers->pregReplace( '#%page_title%#', $homePageTitle, $format );
			$title         = aioseo()->migration->helpers->macrosToSmartTags( $title );
			aioseo()->options->searchAppearance->global->siteTitle = aioseo()->helpers->sanitizeOption( $title );

			return;
		}

		// Set the setting globally regardless of what happens below.
		if ( ! empty( $homePageTitle ) ) {
			$title = aioseo()->migration->helpers->macrosToSmartTags( aioseo()->helpers->pregReplace( '#%page_title%#', $homePageTitle, $format ) );
			aioseo()->options->searchAppearance->global->siteTitle = aioseo()->helpers->sanitizeOption( $title );
		}

		$post       = 'page' === $showOnFront && $pageOnFront ? get_post( $pageOnFront ) : '';
		$metaTitle  = get_post_meta( $post->ID, '_aioseop_title', true );

		$homePageTitle = '';
		if ( empty( $this->oldOptions['aiosp_use_static_home_info'] ) ) {
			$homePageTitle = ! empty( $this->oldOptions['aiosp_home_title'] ) ? $this->oldOptions['aiosp_home_title'] : '#site_title';
			$homePageTitle = ! empty( $metaTitle ) ? $metaTitle : $homePageTitle;
			$homePageTitle = empty( $format ) ? $homePageTitle : aioseo()->helpers->pregReplace( '#%page_title%#', $homePageTitle, $format );
			$homePageTitle = aioseo()->migration->helpers->macrosToSmartTags( $homePageTitle );
		} else {
			if ( ! empty( $metaTitle ) ) {
				$homePageTitle = empty( $format ) ? $metaTitle : aioseo()->helpers->pregReplace( '#%page_title%#', $metaTitle, $format );
				$homePageTitle = aioseo()->migration->helpers->macrosToSmartTags( $homePageTitle );
			}
		}

		$aioseoPost = Models\Post::getPost( $post->ID );
		$aioseoPost->set( [
			'post_id' => $post->ID,
			'title'   => aioseo()->helpers->sanitizeOption( $homePageTitle )
		] );
		$aioseoPost->save();

		$this->maybeShowHomePageTitleNotice( $post );
	}

	/**
	 * Check if we should display a notice warning users that their homepage title may have changed.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post The post object.
	 * @return void
	 */
	private function maybeShowHomePageTitleNotice( $post ) {
		$metaTitle     = get_post_meta( $post->ID, '_aioseop_title', true );
		$homePageTitle = ! empty( $this->oldOptions['aiosp_home_title'] ) ? $this->oldOptions['aiosp_home_title'] : '';

		if (
			empty( $this->oldOptions['aiosp_use_static_home_info'] ) &&
			$metaTitle &&
			( trim( $homePageTitle ) !== trim( $metaTitle ) )
		) {
			$this->showHomePageSettingsNotice();
		}
	}

	/**
	 * Migrates the homepage description.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateHomePageDescription() {
		$showOnFront         = get_option( 'show_on_front' );
		$pageOnFront         = (int) get_option( 'page_on_front' );

		$homePageDescription = ! empty( $this->oldOptions['aiosp_home_description'] ) ? $this->oldOptions['aiosp_home_description'] : '';
		$format              = $this->oldOptions['aiosp_description_format'];

		if ( 'posts' === $showOnFront ) {
			// If the description had the page_title macro, we want to replace it with the actual page title itself.
			$homePageDescription = $homePageDescription ? $homePageDescription : get_bloginfo( 'description' );
			$homePageTitle       = ! empty( $this->oldOptions['aiosp_home_title'] ) ? $this->oldOptions['aiosp_home_title'] : get_bloginfo( 'name' );
			$format              = aioseo()->helpers->pregReplace( '#%page_title%#', $homePageTitle, $format );
			$description         = empty( $format ) ? $homePageDescription : aioseo()->helpers->pregReplace( '#%description%#', $homePageDescription, $format );
			$description         = aioseo()->migration->helpers->macrosToSmartTags( $description );
			aioseo()->options->searchAppearance->global->metaDescription = aioseo()->helpers->sanitizeOption( $description );

			return;
		}

		// Set the setting globally regardless of what happens below.
		if ( ! empty( $homePageDescription ) ) {
			$homePageTitle = ! empty( $this->oldOptions['aiosp_home_title'] ) ? $this->oldOptions['aiosp_home_title'] : get_bloginfo( 'name' );
			$format        = aioseo()->helpers->pregReplace( '#%page_title%#', $homePageTitle, $format );
			$description   = aioseo()->migration->helpers->macrosToSmartTags( aioseo()->helpers->pregReplace( '#%description%#', $homePageDescription, $format ) );
			aioseo()->options->searchAppearance->global->metaDescription = aioseo()->helpers->sanitizeOption( $description );
		}

		$post             = 'page' === $showOnFront && $pageOnFront ? get_post( $pageOnFront ) : '';
		$metaDescription  = get_post_meta( $post->ID, '_aioseop_description', true );

		$homePageDescription = '';
		if ( empty( $this->oldOptions['aiosp_use_static_home_info'] ) ) {
			$homePageDescription = ! empty( $this->oldOptions['aiosp_home_description'] ) ? $this->oldOptions['aiosp_home_description'] : '';
			$homePageDescription = ! empty( $metaDescription ) ? $metaDescription : $homePageDescription;
		} else {
			if ( ! empty( $metaDescription ) ) {
				$homePageDescription = empty( $format ) ? $metaDescription : aioseo()->helpers->pregReplace( '#%description%#', $metaDescription, $format );
				$homePageDescription = aioseo()->migration->helpers->macrosToSmartTags( $homePageDescription );
			}
		}

		$homePageDescription = empty( $format ) ? $homePageDescription : aioseo()->helpers->pregReplace( '#(%description%|%page_title%)#', $homePageDescription, $format );
		$homePageDescription = aioseo()->migration->helpers->macrosToSmartTags( $homePageDescription );

		$aioseoPost = Models\Post::getPost( $post->ID );
		$aioseoPost->set( [
			'post_id'     => $post->ID,
			'description' => aioseo()->helpers->sanitizeOption( $homePageDescription )
		] );
		$aioseoPost->save();

		$this->maybeShowHomePageDescriptionNotice( $post );
	}

		/**
	 * Check if we should display a notice warning users that their homepage title may have changed.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post The post object.
	 * @return void
	 */
	private function maybeShowHomePageDescriptionNotice( $post ) {
		$metaDescription     = get_post_meta( $post->ID, '_aioseop_description', true );
		$homePageDescription = ! empty( $this->oldOptions['aiosp_home_description'] ) ? $this->oldOptions['aiosp_home_description'] : '';

		if (
			empty( $this->oldOptions['aiosp_use_static_home_info'] ) &&
			$metaDescription &&
			( trim( $homePageDescription ) !== trim( $metaDescription ) )
		) {
			$this->showHomePageSettingsNotice();
		}
	}

	/**
	 * Shows the homepage settings notice.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function showHomePageSettingsNotice() {
		$notification = Models\Notification::getNotificationByName( 'v3-migration-homepage-settings' );
		if ( $notification->notification_name ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'v3-migration-homepage-settings',
			'title'             => __( 'Review Your Homepage Title & Description', 'all-in-one-seo-pack' ),
			'content'           => sprintf(
				// Translators: 1 - All in One SEO.
				__( 'Due to a bug in the previous version of %1$s, your homepage title and description may have changed. Please take a minute to review your homepage settings to verify that they are correct.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				AIOSEO_PLUGIN_NAME
			),
			'type'              => 'warning',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Review Now', 'all-in-one-seo-pack' ),
			'button1_action'    => 'http://route#aioseo-search-appearance&aioseo-scroll=home-page-settings&aioseo-highlight=home-page-settings:global-settings',
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Migrates the title formats.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateTitleFormats() {
		if ( ! empty( $this->oldOptions['aiosp_archive_title_format'] ) ) {
			$archives = array_keys( aioseo()->dynamicOptions->searchAppearance->archives->all() );
			$format   = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $this->oldOptions['aiosp_archive_title_format'] ) );
			foreach ( $archives as $archive ) {
				aioseo()->dynamicOptions->searchAppearance->archives->$archive->title = $format;
			}
		}

		$settings = [
			'aiosp_post_title_format'       => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'postTypes', 'post', 'title' ], 'dynamic' => true ],
			'aiosp_page_title_format'       => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'postTypes', 'page', 'title' ], 'dynamic' => true ],
			'aiosp_attachment_title_format' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'postTypes', 'attachment', 'title' ], 'dynamic' => true ],
			'aiosp_category_title_format'   => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'taxonomies', 'category', 'title' ], 'dynamic' => true ],
			'aiosp_tag_title_format'        => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'taxonomies', 'post_tag', 'title' ], 'dynamic' => true ],
			'aiosp_date_title_format'       => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'date', 'title' ] ],
			'aiosp_author_title_format'     => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'author', 'title' ] ],
			'aiosp_search_title_format'     => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'search', 'title' ] ],
			'aiosp_paged_format'            => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'advanced', 'pagedFormat' ] ]
		];

		foreach ( $this->oldOptions as $name => $value ) {
			if (
				! in_array( $name, array_keys( $settings ), true ) &&
				preg_match( '#aiosp_(.*)_title_format#', (string) $name, $slug )
			) {
				if ( empty( $slug[1] ) ) {
					continue;
				}

				$objectSlug = aioseo()->helpers->pregReplace( '#_tax#', '', $slug[1] );
				if ( in_array( $objectSlug, aioseo()->helpers->getPublicPostTypes( true ), true ) ) {
					$settings[ $name ] = [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'postTypes', $objectSlug, 'title' ], 'dynamic' => true ];
					continue;
				}
				if ( in_array( $objectSlug, aioseo()->helpers->getPublicTaxonomies( true ), true ) ) {
					$settings[ $name ] = [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'taxonomies', $objectSlug, 'title' ], 'dynamic' => true ];
				}
			}
		}

		aioseo()->migration->helpers->mapOldToNew( $settings, $this->oldOptions, true );

		// Check if any of the title formats were empty and register a notification if so.
		$found = false;
		foreach ( $settings as $k => $v ) {
			if ( 'aiosp_home_page_title_format' === $k ) {
				continue;
			}

			if ( isset( $this->oldOptions[ $k ] ) && empty( $this->oldOptions[ $k ] ) ) {
				$found = true;
				break;
			}
		}

		if ( ! $found ) {
			Models\Notification::deleteNotificationByName( 'v3-migration-title-formats-blank' );

			return;
		}

		$notification = Models\Notification::getNotificationByName( 'v3-migration-title-formats-blank' );
		if ( $notification->notification_name ) {
			return;
		}

		$p1 = sprintf(
			// Translators: 1 - The plugin short name ("AIOSEO"), 2 - The plugin short name ("AIOSEO"), 3 - Opening link tag, 4 - Closing link tag.
			__( '%1$s migrated all your title formats, some of which were blank. If you were purposely using blank formats in the previous version of %2$s and want WordPress to handle your titles, you can safely dismiss this message. For more information, check out our documentation on %3$sblank title formats%4$s.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			AIOSEO_PLUGIN_SHORT_NAME,
			AIOSEO_PLUGIN_SHORT_NAME,
			'<a href="' . aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . '/docs/blank-title-formats-detected', 'notifications-center', 'v3-migration-title-formats-blank' ) . '">',
			'</a>'
		);

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'v3-migration-title-formats-blank',
			'title'             => __( 'Blank Title Formats Detected', 'all-in-one-seo-pack' ),
			'content'           => $p1,
			'type'              => 'warning',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Learn More', 'all-in-one-seo-pack' ),
			'button1_action'    => aioseo()->helpers->utmUrl( AIOSEO_MARKETING_URL . '/docs/blank-title-formats-detected', 'notifications-center', 'v3-migration-title-formats-blank' ),
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Migrates the description format.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateDescriptionFormat() {
		if (
			! empty( $this->oldOptions['aiosp_generate_descriptions'] ) &&
			empty( $this->oldOptions['aiosp_skip_excerpt'] )
		) {
			foreach ( aioseo()->helpers->getPublicPostTypes() as $postType ) {
				if ( empty( $postType['supports']['excerpt'] ) ) {
					continue;
				}

				if ( aioseo()->dynamicOptions->searchAppearance->postTypes->has( $postType['name'] ) ) {
					aioseo()->dynamicOptions->searchAppearance->postTypes->{$postType['name']}->metaDescription = '#post_excerpt';
				}
			}
		}

		if (
			empty( $this->oldOptions['aiosp_description_format'] ) ||
			'%description%' === trim( $this->oldOptions['aiosp_description_format'] )
		) {
			return;
		}

		$deprecatedOptions = aioseo()->internalOptions->internal->deprecatedOptions;
		array_push( $deprecatedOptions, 'descriptionFormat' );
		aioseo()->internalOptions->internal->deprecatedOptions = $deprecatedOptions;

		$format = aioseo()->migration->helpers->macrosToSmartTags( $this->oldOptions['aiosp_description_format'] );
		aioseo()->options->deprecated->searchAppearance->global->descriptionFormat = aioseo()->helpers->sanitizeOption( $format );
	}

	/**
	 * Migrates the noindex settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateNoindexSettings() {
		if ( ! isset( $this->oldOptions['aiosp_cpostnoindex'] ) && ! isset( $this->oldOptions['aiosp_tax_noindex'] ) ) {
			return;
		}

		$noindexedPostTypes = is_array( $this->oldOptions['aiosp_cpostnoindex'] ) ? $this->oldOptions['aiosp_cpostnoindex'] : explode( ', ', $this->oldOptions['aiosp_cpostnoindex'] );
		foreach ( array_intersect( aioseo()->helpers->getPublicPostTypes( true ), $noindexedPostTypes ) as $postType ) {
			if ( aioseo()->dynamicOptions->noConflict()->searchAppearance->postTypes->has( $postType ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->show = false;
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = false;
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->noindex = true;
			}
		}

		$noindexedTaxonomies = isset( $this->oldOptions['aiosp_tax_noindex'] ) ? (array) $this->oldOptions['aiosp_tax_noindex'] : [];
		if ( ! empty( $this->oldOptions['aiosp_category_noindex'] ) ) {
			$noindexedTaxonomies[] = 'category';
		}

		if ( ! empty( $this->oldOptions['aiosp_tags_noindex'] ) ) {
			$noindexedTaxonomies[] = 'post_tag';
		}

		if ( ! empty( $noindexedTaxonomies ) ) {
			foreach ( array_intersect( aioseo()->helpers->getPublicTaxonomies( true ), $noindexedTaxonomies ) as $taxonomy ) {
				if ( aioseo()->dynamicOptions->noConflict()->searchAppearance->taxonomies->has( $taxonomy ) ) {
					aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->show = false;
					aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->default = false;
					aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->noindex = true;
				}
			}
		}

		if ( ! empty( $this->oldOptions['aiosp_archive_date_noindex'] ) ) {
			aioseo()->options->searchAppearance->archives->date->show = false;
			aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->default = false;
			aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->noindex = true;
		}

		if ( ! empty( $this->oldOptions['aiosp_archive_author_noindex'] ) ) {
			aioseo()->options->searchAppearance->archives->author->show = false;
			aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->default = false;
			aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->noindex = true;
		}

		if ( ! empty( $this->oldOptions['aiosp_search_noindex'] ) ) {
			aioseo()->options->searchAppearance->archives->search->show = false;
			aioseo()->options->searchAppearance->archives->search->advanced->robotsMeta->default = false;
			aioseo()->options->searchAppearance->archives->search->advanced->robotsMeta->noindex = true;
		} else {
			// We need to do this as V4 will noindex the search page otherwise.
			aioseo()->options->searchAppearance->archives->search->show = true;
			aioseo()->options->searchAppearance->archives->search->advanced->robotsMeta->default = true;
			aioseo()->options->searchAppearance->archives->search->advanced->robotsMeta->noindex = false;
		}

		if ( ! empty( $this->oldOptions['aiosp_paginated_noindex'] ) ) {
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default          = false;
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindexPaginated = true;
		}
	}

	/**
	 * Migrates the nofollow settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateNofollowSettings() {
		if ( ! empty( $this->oldOptions['aiosp_cpostnofollow'] ) ) {
			foreach ( array_intersect( aioseo()->helpers->getPublicPostTypes( true ), $this->oldOptions['aiosp_cpostnofollow'] ) as $postType ) {
				if ( aioseo()->dynamicOptions->noConflict()->searchAppearance->postTypes->has( $postType ) ) {
					aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default  = false;
					aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->nofollow = true;
				}
			}
		}

		if ( ! empty( $this->oldOptions['aiosp_paginated_nofollow'] ) ) {
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->nofollowPaginated = true;
		}
	}

	/**
	 * Migrates the post SEO columns.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migratePostSeoColumns() {
		if ( ! isset( $this->oldOptions['aiosp_posttypecolumns'] ) ) {
			return;
		}

		$publicPostTypes = aioseo()->helpers->getPublicPostTypes( true );
		$postTypes       = array_intersect( (array) $this->oldOptions['aiosp_posttypecolumns'], $publicPostTypes );

		aioseo()->options->advanced->postTypes->included = array_values( $postTypes );
		if ( count( $publicPostTypes ) !== count( $postTypes ) ) {
			aioseo()->options->advanced->postTypes->all = false;
		}
	}

	/**
	 * Migrates the schema social URLs.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateSocialUrls() {
		if ( ! empty( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_facebook_publisher'] ) ) {
			aioseo()->options->social->profiles->urls->facebookPageUrl = esc_url( wp_strip_all_tags( $this->oldOptions['modules']['aiosp_opengraph_options']['aiosp_opengraph_facebook_publisher'] ) );
			aioseo()->options->social->profiles->sameUsername->enable = false;
		}

		if ( empty( $this->oldOptions['aiosp_schema_social_profile_links'] ) ) {
			return;
		}

		$socialUrls = aioseo()->helpers->pregReplace( '/\s/', '\r\n', $this->oldOptions['aiosp_schema_social_profile_links'] );
		$socialUrls = array_filter( explode( '\r\n', $socialUrls ) );

		if ( ! count( $socialUrls ) ) {
			return;
		}

		$supportedNetworks = [
			'facebook.com'   => 'facebookPageUrl',
			'twitter.com'    => 'twitterUrl',
			'instagram.com'  => 'instagramUrl',
			'tiktok.com'     => 'tiktokUrl',
			'pinterest.com'  => 'pinterestUrl',
			'youtube.com'    => 'youtubeUrl',
			'linkedin.com'   => 'linkedinUrl',
			'tumblr.com'     => 'tumblrUrl',
			'yelp.com'       => 'yelpPageUrl',
			'soundcloud.com' => 'soundCloudUrl',
			'wikipedia.org'  => 'wikipediaUrl',
			'myspace.com'    => 'myspaceUrl',
			'wordpress.org'  => 'wordpressUrl',
			'bsky.app'       => 'blueskyUrl',
			'threads.net'    => 'threadsUrl'
		];

		$found = false;
		foreach ( $supportedNetworks as $url => $settingName ) {
			$url = aioseo()->helpers->escapeRegex( $url );
			foreach ( $socialUrls as $socialUrl ) {
				if ( preg_match( "/.*$url.*/", (string) $socialUrl ) ) {
					aioseo()->options->social->profiles->urls->$settingName = esc_url( wp_strip_all_tags( $socialUrl ) );
					$found = true;
				}
			}
		}

		if ( $found ) {
			aioseo()->options->social->profiles->sameUsername->enable = false;
		}
	}

	/**
	 * Migrates the Schema Markup settings in the General Settings menu.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateSchemaMarkupSettings() {
		$this->migrateSchemaPhoneNumber();

		if (
			isset( $this->oldOptions['aiosp_schema_markup'] ) &&
			empty( $this->oldOptions['aiosp_schema_markup'] )
		) {
			$deprecatedOptions = aioseo()->internalOptions->internal->deprecatedOptions;
			array_push( $deprecatedOptions, 'enableSchemaMarkup' );
			aioseo()->internalOptions->internal->deprecatedOptions = $deprecatedOptions;
			aioseo()->options->deprecated->searchAppearance->global->schema->enableSchemaMarkup = false;
		}

		if ( ! empty( $this->oldOptions['aiosp_schema_person_user'] ) ) {
			if ( -1 === (int) $this->oldOptions['aiosp_schema_person_user'] ) {
				aioseo()->options->searchAppearance->global->schema->person = 'manual';
			} else {
				aioseo()->options->searchAppearance->global->schema->person = intval( $this->oldOptions['aiosp_schema_person_user'] );
			}
		}
	}

	/**
	 * Migrates the schema phone number.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateSchemaPhoneNumber() {
		if ( empty( $this->oldOptions['aiosp_schema_phone_number'] ) ) {
			return;
		}

		$phoneNumber = aioseo()->helpers->sanitizeOption( $this->oldOptions['aiosp_schema_phone_number'] );
		if ( ! preg_match( '#\+\d+#', (string) $phoneNumber ) ) {
			$notification = Models\Notification::getNotificationByName( 'v3-migration-schema-number' );
			if ( $notification->notification_name ) {
				return;
			}

			Models\Notification::addNotification( [
				'slug'              => uniqid(),
				'notification_name' => 'v3-migration-schema-number',
				'title'             => __( 'Invalid Phone Number for Knowledge Graph', 'all-in-one-seo-pack' ),
				'content'           => sprintf(
					// Translators: 1 - The phone number.
					__( 'The phone number that you previously entered for your Knowledge Graph schema markup is invalid. As it needs to be internationally formatted, please enter it (%1$s) again with the country code, e.g. +1 (555) 555-1234.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
					"<strong>$phoneNumber</strong>"
				),
				'type'              => 'warning',
				'level'             => [ 'all' ],
				'button1_label'     => __( 'Fix Now', 'all-in-one-seo-pack' ),
				'button1_action'    => 'http://route#aioseo-search-appearance&aioseo-scroll=schema-graph-phone&aioseo-highlight=schema-graph-phone:global-settings',
				'button2_label'     => __( 'Remind Me Later', 'all-in-one-seo-pack' ),
				'button2_action'    => 'http://action#notification/v3-migration-schema-number-reminder',
				'start'             => gmdate( 'Y-m-d H:i:s' )
			] );

			return;
		}
		aioseo()->options->searchAppearance->global->schema->phone = $phoneNumber;
	}

	/**
	 * Migrates the homepage keywords.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateHomePageKeywords() {
		if ( ! empty( $this->oldOptions['aiosp_home_keywords'] ) ) {
			aioseo()->options->searchAppearance->global->keywords = aioseo()->migration->helpers->oldKeywordsToNewKeywords( $this->oldOptions['aiosp_home_keywords'] );
		}
	}

	/**
	 * Migrates the deprecated V3 advanced General Settings options.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateDeprecatedAdvancedOptions() {
		$deprecatedOptions = aioseo()->internalOptions->internal->deprecatedOptions;

		if ( empty( $this->oldOptions['aiosp_generate_descriptions'] ) ) {
			array_push( $deprecatedOptions, 'autogenerateDescriptions' );
			aioseo()->options->deprecated->searchAppearance->advanced->autogenerateDescriptions = false;
		} else {
			if ( ! empty( $this->oldOptions['aiosp_skip_excerpt'] ) ) {
				array_push( $deprecatedOptions, 'useContentForAutogeneratedDescriptions' );
				aioseo()->options->deprecated->searchAppearance->advanced->useContentForAutogeneratedDescriptions = true;
			}
		}

		aioseo()->internalOptions->internal->deprecatedOptions = $deprecatedOptions;
	}

	/**
	 * Migrates the RSS content settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateRssContentSettings() {
		if ( isset( $this->oldOptions['aiosp_rss_content_before'] ) ) {
			aioseo()->options->rssContent->before = esc_html( aioseo()->migration->helpers->macrosToSmartTags( $this->oldOptions['aiosp_rss_content_before'] ) );
		}

		if ( isset( $this->oldOptions['aiosp_rss_content_after'] ) ) {
			aioseo()->options->rssContent->after = esc_html( aioseo()->migration->helpers->macrosToSmartTags( $this->oldOptions['aiosp_rss_content_after'] ) );
		}
	}

	/**
	 * Migrates the Redirect Attachment to Parent setting.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateRedirectToParent() {
		if ( isset( $this->oldOptions['aiosp_redirect_attachement_parent'] ) ) {
			if ( ! empty( $this->oldOptions['aiosp_redirect_attachement_parent'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'attachment_parent';
			} else {
				aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'disabled';
			}
		}
	}

	/**
	 * Migrates the excluded posts.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateDisabledPosts() {
		if ( empty( $this->oldOptions['aiosp_ex_pages'] ) ) {
			return;
		}

		$deprecatedOptions = aioseo()->internalOptions->internal->deprecatedOptions;
		if ( ! in_array( 'excludePosts', $deprecatedOptions, true ) ) {
			array_push( $deprecatedOptions, 'excludePosts' );
			aioseo()->internalOptions->internal->deprecatedOptions = $deprecatedOptions;
		}

		$excludedPosts = aioseo()->options->deprecated->searchAppearance->advanced->excludePosts;
		$pages         = explode( ',', $this->oldOptions['aiosp_ex_pages'] );
		if ( count( $pages ) ) {
			foreach ( $pages as $page ) {
				$page = trim( $page );
				$id   = intval( $page );
				if ( ! $id ) {
					$post = get_page_by_path( $page, OBJECT, aioseo()->helpers->getPublicPostTypes( true ) );
					if ( $post && is_object( $post ) ) {
						$id = $post->ID;
					}
				}

				if ( $id ) {
					$post = get_post( $id );
					if ( ! is_object( $post ) ) {
						continue;
					}

					$excludedPost        = new \stdClass();
					$excludedPost->value = $id;
					$excludedPost->type  = $post->post_type;
					$excludedPost->label = $post->post_title;
					$excludedPost->link  = get_permalink( $id );

					array_push( $excludedPosts, wp_json_encode( $excludedPost ) );
				}
			}
		}
		aioseo()->options->deprecated->searchAppearance->advanced->excludePosts = $excludedPosts;
	}

	/**
	 * Migrates the deprecated "No Pagination for Canonical URLs" setting.
	 *
	 * @since 4.5.9
	 *
	 * @return void
	 */
	private function migrateNoPaginationForCanonicalUrls() {
		if ( empty( $this->oldOptions['aiosp_no_paged_canonical_links'] ) ) {
			return;
		}

		$deprecatedOptions = aioseo()->internalOptions->deprecatedOptions;
		if ( ! in_array( 'noPaginationForCanonical', $deprecatedOptions, true ) ) {
			$deprecatedOptions[]                         = 'noPaginationForCanonical';
			aioseo()->internalOptions->deprecatedOptions = $deprecatedOptions;
		}

		aioseo()->options->deprecated->searchAppearance->advanced->noPaginationForCanonical = true;
	}
}Common/Main/PreUpdates.php000066600000004065151135505570011465 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * This class contains pre-updates necessary for the next updates class to run.
 *
 * @since 4.1.5
 */
class PreUpdates {
	/**
	 * Class constructor.
	 *
	 * @since 4.1.5
	 */
	public function __construct() {
		// We don't want an AJAX request check here since the plugin might be installed/activated for the first time via AJAX (e.g. EDD/BLC).
		// If that's the case, the cache table needs to be created before the activation hook runs.
		if ( wp_doing_cron() ) {
			return;
		}

		$lastActiveVersion = aioseo()->internalOptions->internal->lastActiveVersion;
		if ( aioseo()->version !== $lastActiveVersion ) {
			// Bust the table/columns cache so that we can start the update migrations with a fresh slate.
			aioseo()->internalOptions->database->installedTables = '';
		}

		if ( version_compare( $lastActiveVersion, '4.1.5', '<' ) ) {
			$this->createCacheTable();
		}

		if ( version_compare( $lastActiveVersion, AIOSEO_VERSION, '<' ) ) {
			aioseo()->core->cache->clear();
		}
	}

	/**
	 * Creates a new aioseo_cache table.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	public function createCacheTable() {
		$db             = aioseo()->core->db->db;
		$charsetCollate = '';

		if ( ! empty( $db->charset ) ) {
			$charsetCollate .= "DEFAULT CHARACTER SET {$db->charset}";
		}
		if ( ! empty( $db->collate ) ) {
			$charsetCollate .= " COLLATE {$db->collate}";
		}

		$tableName = aioseo()->core->cache->getTableName();
		if ( ! aioseo()->core->db->tableExists( $tableName ) ) {
			$tableName = $db->prefix . $tableName;

			aioseo()->core->db->execute(
				"CREATE TABLE {$tableName} (
					`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					`key` varchar(80) NOT NULL,
					`value` longtext NOT NULL,
					`expiration` datetime NULL,
					`created` datetime NOT NULL,
					`updated` datetime NOT NULL,
					PRIMARY KEY (`id`),
					UNIQUE KEY ndx_aioseo_cache_key (`key`),
					KEY ndx_aioseo_cache_expiration (`expiration`)
				) {$charsetCollate};"
			);
		}
	}
}Common/Main/Head.php000066600000006613151135505570010253 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Meta;

/**
 * Outputs anything we need to the head of the site.
 *
 * @since 4.0.0
 */
class Head {
	/**
	 * The page title.
	 *
	 * @since 4.0.5
	 *
	 * @var string
	 */
	private static $pageTitle = null;

	/**
	 * Title class instance.
	 *
	 * @since 4.3.9
	 *
	 * @var Title
	 */
	private $title;

	/**
	 * Links class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Meta\Links
	 */
	protected $links = null;

	/**
	 * Keywords class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Meta\Keywords
	 */
	protected $keywords = null;

	/**
	 * Verification class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Meta\SiteVerification
	 */
	protected $verification = null;

	/**
	 * The views to output.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	protected $views = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'wp', [ $this, 'registerTitleHooks' ], 1000 );
		add_action( 'wp_head', [ $this, 'wpHead' ], 1 );

		$this->title        = new Title();
		$this->links        = new Meta\Links();
		$this->keywords     = new Meta\Keywords();
		$this->verification = new Meta\SiteVerification();
		$this->views        = [
			'meta'    => AIOSEO_DIR . '/app/Common/Views/main/meta.php',
			'social'  => AIOSEO_DIR . '/app/Common/Views/main/social.php',
			'schema'  => AIOSEO_DIR . '/app/Common/Views/main/schema.php',
			'clarity' => AIOSEO_DIR . '/app/Common/Views/main/clarity.php'
		];
	}

	/**
	 * Registers our title hooks.
	 *
	 * @since 4.0.5
	 *
	 * @return void
	 */
	public function registerTitleHooks() {
		if ( apply_filters( 'aioseo_disable', false ) || apply_filters( 'aioseo_disable_title_rewrites', false ) ) {
			return;
		}

		add_filter( 'pre_get_document_title', [ $this, 'getTitle' ], 99999 );
		add_filter( 'wp_title', [ $this, 'getTitle' ], 99999 );
		if ( ! current_theme_supports( 'title-tag' ) ) {
			add_action( 'template_redirect', [ $this->title, 'startOutputBuffering' ], 99999 );
			add_action( 'wp_head', [ $this->title, 'endOutputBuffering' ], 99999 );
		}
	}

	/**
	 * Outputs the head.
	 *
	 * @since 4.0.5
	 * @version 4.6.1
	 *
	 * @return void
	 */
	public function wpHead() {
		$included = new Meta\Included();
		if ( is_admin() || wp_doing_ajax() || wp_doing_cron() || ! $included->isIncluded() ) {
			return;
		}

		$this->output();
	}

	/**
	 * Returns the page title.
	 *
	 * @since 4.0.5
	 *
	 * @param  string $wpTitle   The original page title from WordPress.
	 * @return string $pageTitle The page title.
	 */
	public function getTitle( $wpTitle = '' ) {
		if ( null !== self::$pageTitle ) {
			return self::$pageTitle;
		}
		self::$pageTitle = aioseo()->meta->title->filterPageTitle( $wpTitle );

		return self::$pageTitle;
	}

	/**
	 * The output function itself.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function output() {
		remove_action( 'wp_head', 'rel_canonical' );

		$views = apply_filters( 'aioseo_meta_views', $this->views );
		if ( empty( $views ) ) {
			return;
		}

		echo "\n\t\t<!-- " . sprintf(
			'%1$s %2$s',
			esc_html( AIOSEO_PLUGIN_NAME ),
			aioseo()->helpers->getAioseoVersion() // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		) . " - aioseo.com -->\n";

		foreach ( $views as $view ) {
			require $view;
		}

		echo "\t\t<!-- " . esc_html( AIOSEO_PLUGIN_NAME ) . " -->\n\n";
	}
}Common/Main/Filters.php000066600000042522151135505570011021 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models;
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;

/**
 * Abstract class that Pro and Lite both extend.
 *
 * @since 4.0.0
 */
abstract class Filters {
	/**
	 * The plugin we are checking.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $plugin;

	/**
	 * ID of the WooCommerce product that is being duplicated.
	 *
	 * @since 4.1.4
	 *
	 * @var integer
	 */
	private static $originalProductId;

	/**
	 * Construct method.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_filter( 'wp_optimize_get_tables', [ $this, 'wpOptimizeAioseoTables' ] );

		// This action needs to run on AJAX/cron for scheduled rewritten posts in Yoast Duplicate Post.
		add_action( 'duplicate_post_after_rewriting', [ $this, 'updateRescheduledPostMeta' ], 10, 2 );

		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		add_filter( 'plugin_row_meta', [ $this, 'pluginRowMeta' ], 10, 2 );
		add_filter( 'plugin_action_links_' . AIOSEO_PLUGIN_BASENAME, [ $this, 'pluginActionLinks' ], 10, 2 );

		// Genesis theme compatibility.
		add_filter( 'genesis_detect_seo_plugins', [ $this, 'genesisTheme' ] );

		// WeGlot compatibility.
		if ( isset( $_SERVER['REQUEST_URI'] ) && preg_match( '#(/default-sitemap\.xsl)$#i', (string) sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ) {
			add_filter( 'weglot_active_translation_before_treat_page', '__return_false' );
		}

		add_filter( 'wpml_tm_adjust_translation_fields', [ $this, 'defineMetaFieldsForWpml' ] );

		if ( isset( $_SERVER['REQUEST_URI'] ) && preg_match( '#(\.xml)$#i', (string) sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ) {
			add_filter( 'jetpack_boost_should_defer_js', '__return_false' );
		}

		// GoDaddy CDN compatibility.
		add_filter( 'wpaas_cdn_file_ext', [ $this, 'goDaddySitemapXml' ] );

		// Duplicate Post integration.
		add_action( 'dp_duplicate_post', [ $this, 'duplicatePost' ], 10, 2 );
		add_action( 'dp_duplicate_page', [ $this, 'duplicatePost' ], 10, 2 );
		add_action( 'woocommerce_product_duplicate_before_save', [ $this, 'scheduleDuplicateProduct' ], 10, 2 );
		add_action( 'add_post_meta', [ $this, 'rewriteAndRepublish' ], 10, 3 );

		// BBpress compatibility.
		add_action( 'init', [ $this, 'resetUserBBPress' ], -1 );
		add_filter( 'the_title', [ $this, 'maybeRemoveBBPressReplyFilter' ], 0, 2 );

		// Bypass the JWT Auth plugin's unnecessary restrictions. https://wordpress.org/plugins/jwt-auth/
		add_filter( 'jwt_auth_default_whitelist', [ $this, 'allowRestRoutes' ] );

		// Clear the site authors cache.
		add_action( 'profile_update', [ $this, 'clearAuthorsCache' ] );
		add_action( 'user_register', [ $this, 'clearAuthorsCache' ] );

		add_filter( 'aioseo_public_post_types', [ $this, 'removeInvalidPublicPostTypes' ] );
		add_filter( 'aioseo_public_taxonomies', [ $this, 'removeInvalidPublicTaxonomies' ] );

		add_action( 'admin_print_scripts', [ $this, 'removeEmojiDetectionScripts' ], 0 );

		// Disable Jetpack sitemaps module.
		if ( aioseo()->options->sitemap->general->enable ) {
			add_filter( 'jetpack_get_available_modules', [ $this, 'disableJetpackSitemaps' ] );
		}

		add_action( 'after_setup_theme', [ $this, 'removeHelloElementorDescriptionTag' ] );
		add_action( 'wp', [ $this, 'removeAvadaOgTags' ] );
		add_action( 'init', [ $this, 'declareAioseoFollowingConsentApi' ] );
	}

	/**
	 * Declares AIOSEO and its addons as following the Consent API.
	 *
	 * @since 4.6.5
	 *
	 * @return void
	 */
	public function declareAioseoFollowingConsentApi() {
		add_filter( 'wp_consent_api_registered_all-in-one-seo-pack/all_in_one_seo_pack.php', '__return_true' );
		add_filter( 'wp_consent_api_registered_all-in-one-seo-pack-pro/all_in_one_seo_pack.php', '__return_true' );

		foreach ( aioseo()->addons->getAddons() as $addon ) {
			if ( empty( $addon->installed ) || empty( $addon->basename ) ) {
				continue;
			}
			if ( isset( $addon->basename ) ) {
				add_filter( 'wp_consent_api_registered_' . $addon->basename, '__return_true' );
			}
		}
	}

	/**
	 * Removes emoji detection scripts on WP 6.2 which broke our Emojis.
	 *
	 * @since 4.3.4.1
	 *
	 * @return void
	 */
	public function removeEmojiDetectionScripts() {
		global $wp_version; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if ( version_compare( $wp_version, '6.2', '>=' ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
		}
	}

	/**
	 * Resets the current user if bbPress is active.
	 * We have to do this because our calls to wp_get_current_user() set the current user early and this breaks core functionality in bbPress.
	 *

	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	public function resetUserBBPress() {
		if ( function_exists( 'bbpress' ) ) {
			global $current_user; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			$current_user = null; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		}
	}

	/**
	 * Removes the bbPress title filter when adding a new reply with empty title to avoid fatal error.
	 *

	 *
	 * @since 4.3.1
	 *
	 * @param  string $title The post title.
	 * @param  int    $id    The post ID (optional - in order to fix an issue where other plugins/themes don't pass in the second arg).
	 * @return string        The post title.
	 */
	public function maybeRemoveBBPressReplyFilter( $title, $id = 0 ) {
		if (
			function_exists( 'bbp_get_reply_post_type' ) &&
			get_post_type( $id ) === bbp_get_reply_post_type() &&
			aioseo()->helpers->isScreenBase( 'post' )
		) {
			remove_filter( 'the_title', 'bbp_get_reply_title_fallback', 2 );
		}

		return $title;
	}

	/**
	 * Duplicates the model when duplicate post is triggered.
	 *
	 * @since 4.1.1
	 *
	 * @param  integer  $targetPostId The target post ID.
	 * @param  \WP_Post $sourcePost   The source post object.
	 * @return void
	 */
	public function duplicatePost( $targetPostId, $sourcePost = null ) {
		$sourcePostId     = ! empty( $sourcePost->ID ) ? $sourcePost->ID : $sourcePost;
		$sourceAioseoPost = Models\Post::getPost( $sourcePostId );
		$targetPost       = Models\Post::getPost( $targetPostId );

		$columns = $sourceAioseoPost->getColumns();
		foreach ( $columns as $column => $value ) {
			// Skip the ID column.
			if ( 'id' === $column ) {
				continue;
			}

			if ( 'post_id' === $column ) {
				$targetPost->$column = $targetPostId;
				continue;
			}

			$targetPost->$column = $sourceAioseoPost->$column;
		}

		$targetPost->save();
	}

	/**
	 * Duplicates the model when rewrite and republish is triggered.
	 *
	 * @since 4.3.4
	 *
	 * @param  integer $postId    The post ID.
	 * @param  string  $metaKey   The meta key.
	 * @param  mixed   $metaValue The meta value.
	 * @return void
	 */
	public function rewriteAndRepublish( $postId, $metaKey = '', $metaValue = '' ) {
		if ( '_dp_has_rewrite_republish_copy' !== $metaKey ) {
			return;
		}

		$originalPost = aioseo()->helpers->getPost( $postId );
		if ( ! is_object( $originalPost ) ) {
			return;
		}

		$this->duplicatePost( (int) $metaValue, $originalPost );
	}

	/**
	 * Updates the model when a post is republished.
	 * Yoast Duplicate Post doesn't do this since we store our data in a custom table.
	 *
	 * @since 4.6.7
	 *
	 * @param  int  $scheduledPostId The ID of the scheduled post.
	 * @param  int  $originalPostId  The ID of the original post.
	 * @return void
	 */
	public function updateRescheduledPostMeta( $scheduledPostId, $originalPostId ) {
		$this->duplicatePost( $originalPostId, $scheduledPostId );

		// Delete the AIOSEO post record for the scheduled post.
		$scheduledAioseoPost = Models\Post::getPost( $scheduledPostId );
		$scheduledAioseoPost->delete();
	}

	/**
	 * Schedules an action to duplicate our meta after the duplicated WooCommerce product has been saved.
	 *
	 * @since 4.1.4
	 *
	 * @param  \WC_Product $newProduct      The new, duplicated product.
	 * @param  \WC_Product $originalProduct The original product.
	 * @return void
	 */
	public function scheduleDuplicateProduct( $newProduct, $originalProduct = null ) {
		self::$originalProductId = $originalProduct->get_id();
		add_action( 'wp_insert_post', [ $this, 'duplicateProduct' ], 10, 2 );
	}

	/**
	 * Duplicates our meta for the new WooCommerce product.
	 *
	 * @since 4.1.4
	 *
	 * @param  integer  $postId The new post ID.
	 * @param  \WP_Post $post   The new post object.
	 * @return void
	 */
	public function duplicateProduct( $postId, $post = null ) {
		if ( ! self::$originalProductId || 'product' !== $post->post_type ) {
			return;
		}

		$this->duplicatePost( $postId, self::$originalProductId );
	}

	/**
	 * Disable SEO inside the Genesis theme if it's running.
	 *
	 * @since 4.0.3
	 *
	 * @param  array $array An array of checks.
	 * @return array        An array with our function added.
	 */
	public function genesisTheme( $array ) {
		if ( empty( $array ) || ! isset( $array['functions'] ) ) {
			return $array;
		}

		$array['functions'][] = 'aioseo';

		return $array;
	}

	/**
	 * Remove XML from the GoDaddy CDN so our urls remain intact.
	 *
	 * @since 4.0.5
	 *
	 * @param  array $extensions The original extensions list.
	 * @return array             The extensions list without xml.
	 */
	public function goDaddySitemapXml( $extensions ) {
		$key = array_search( 'xml', $extensions, true );
		unset( $extensions[ $key ] );

		return $extensions;
	}

	/**
	 * Registers our row meta for the plugins page.
	 *
	 * @since 4.0.0
	 *
	 * @param  array  $actions    List of existing actions.
	 * @param  string $pluginFile The plugin file.
	 * @return array              List of action links.
	 */
	abstract public function pluginRowMeta( $actions, $pluginFile = '' );

	/**
	 * Registers our action links for the plugins page.
	 *
	 * @since 4.0.0
	 *
	 * @param  array  $actions    List of existing actions.
	 * @param  string $pluginFile The plugin file.
	 * @return array              List of action links.
	 */
	abstract public function pluginActionLinks( $actions, $pluginFile = '' );

	/**
	 * Parses the action links.
	 *
	 * @since 4.0.0
	 *
	 * @param  array  $actions     The actions.
	 * @param  string $pluginFile  The plugin file.
	 * @param  array  $actionLinks The action links.
	 * @param  string $position    The position.
	 * @return array               The parsed actions.
	 */
	protected function parseActionLinks( $actions, $pluginFile, $actionLinks = [], $position = 'after' ) {
		if ( empty( $this->plugin ) ) {
			$this->plugin = AIOSEO_PLUGIN_BASENAME;
		}

		if ( $this->plugin === $pluginFile && ! empty( $actionLinks ) ) {
			foreach ( $actionLinks as $key => $value ) {
				$link = [
					$key => sprintf(
						'<a href="%1$s" %2$s target="_blank">%3$s</a>',
						esc_url( $value['url'] ),
						isset( $value['title'] ) ? 'title="' . esc_attr( $value['title'] ) . '"' : '',
						$value['label']
					)
				];

				$actions = 'after' === $position ? array_merge( $actions, $link ) : array_merge( $link, $actions );
			}
		}

		return $actions;
	}

	/**
	 * Add our routes to this plugins allow list.
	 *
	 * @since 4.1.4
	 *
	 * @param  array $allowList The original list.
	 * @return array            The modified list.
	 */
	public function allowRestRoutes( $allowList ) {
		return array_merge( $allowList, [
			'/aioseo/'
		] );
	}

	/**
	 * Clear the site authors cache when user is updated or registered.
	 *
	 * @since 4.1.8
	 *
	 * @return void
	 */
	public function clearAuthorsCache() {
		aioseo()->core->cache->delete( 'site_authors' );
	}

	/**
	 * Filters out post types that aren't really public when getPublicPostTypes() is called.
	 *
	 * @since 4.1.9
	 *
	 * @param  array[object]|array[string] $postTypes The post types.
	 * @return array[object]|array[string]            The filtered post types.
	 */
	public function removeInvalidPublicPostTypes( $postTypes ) {
		$postTypesToRemove = [
			'fusion_element', // Avada
			'elementor_library',
			'redirect_rule', // Safe Redirect Manager
			'seedprod',
			'tcb_lightbox',

			// Thrive Themes internal post types.
			'tva_module',
			'tvo_display',
			'tvo_capture',
			'tva_module',
			'tve_lead_1c_signup',
			'tve_form_type',
			'tvd_login_edit',
			'tve_global_cond_set',
			'tve_cond_display',
			'tve_lead_2s_lightbox',
			'tcb_symbol',
			'td_nm_notification',
			'tvd_content_set',
			'tve_saved_lp',
			'tve_notifications',
			'tve_user_template',
			'tve_video_data',
			'tva_course_type',
			'tva-acc-restriction',
			'tva_course_overview',
			'tve_ult_schedule',
			'tqb_optin',
			'tqb_splash',
			'tva_certificate',
			'tva_course_overview',

			// BuddyPress post types.
			BuddyPressIntegration::getEmailCptSlug()
		];

		foreach ( $postTypes as $index => $postType ) {
			if ( is_string( $postType ) && in_array( $postType, $postTypesToRemove, true ) ) {
				unset( $postTypes[ $index ] );
				continue;
			}

			if ( is_array( $postType ) && in_array( $postType['name'], $postTypesToRemove, true ) ) {
				unset( $postTypes[ $index ] );
			}
		}

		return array_values( $postTypes );
	}

	/**
	 * Filters out taxonomies that aren't really public when getPublicTaxonomies() is called.
	 *
	 * @since 4.2.4
	 *
	 * @param  array[object]|array[string] $taxonomies The taxonomies.
	 * @return array[object]|array[string]             The filtered taxonomies.
	 */
	public function removeInvalidPublicTaxonomies( $taxonomies ) {
		$taxonomiesToRemove = [
			'fusion_tb_category',
			'element_category',
			'template_category',

			// Thrive Themes internal taxonomies.
			'tcb_symbols_tax'
		];

		foreach ( $taxonomies as $index => $taxonomy ) {
			if ( is_string( $taxonomy ) && in_array( $taxonomy, $taxonomiesToRemove, true ) ) {
				unset( $taxonomies[ $index ] );
				continue;
			}

			if ( is_array( $taxonomy ) && in_array( $taxonomy['name'], $taxonomiesToRemove, true ) ) {
				unset( $taxonomies[ $index ] );
			}
		}

		return array_values( $taxonomies );
	}

	/**
	 * Disable Jetpack sitemaps module.
	 *
	 * @since 4.2.2
	 */
	public function disableJetpackSitemaps( $active ) {
		unset( $active['sitemaps'] );

		return $active;
	}

	/**
	 * Dequeues third-party scripts from the other plugins or themes that crashes our menu pages.
	 *
	 * @since   4.1.9
	 * @version 4.3.1
	 *
	 * @return void
	 */
	public function dequeueThirdPartyAssets() {
		// TagDiv Opt-in Builder plugin.
		wp_dequeue_script( 'tds_js_vue_files_last' );

		// MyListing theme.
		if ( function_exists( 'mylisting' ) ) {
			wp_dequeue_script( 'vuejs' );
			wp_dequeue_script( 'theme-script-vendor' );
			wp_dequeue_script( 'theme-script-main' );
		}

		// Voxel theme.
		if ( class_exists( '\Voxel\Controllers\Assets_Controller' ) ) {
			wp_dequeue_script( 'vue' );
			wp_dequeue_script( 'vx:backend.js' );
		}

		// Meta tags for seo plugin.
		if ( class_exists( '\Pagup\MetaTags\Settings' ) ) {
			wp_dequeue_script( 'pmt__vuejs' );
			wp_dequeue_script( 'pmt__script' );
		}

		// Plugin: Wpbingo Core (By TungHV).
		if ( strpos( wp_styles()->query( 'bwp-lookbook-css' )->src ?? '', 'wpbingo' ) !== false ) {
			wp_dequeue_style( 'bwp-lookbook-css' );
		}
	}

	/**
	 * Dequeues third-party scripts from the other plugins or themes that crashes our menu pages.
	 *
	 * @version 4.3.2
	 *
	 * @return void
	 */
	public function dequeueThirdPartyAssetsEarly() {
		// Disables scripts for plugins StmMotorsExtends and StmPostType.
		if ( class_exists( 'STM_Metaboxes' ) ) {
			remove_action( 'admin_enqueue_scripts', [ 'STM_Metaboxes', 'wpcfto_scripts' ] );
		}

		// Disables scripts for LearnPress plugin.
		if ( function_exists( 'learn_press_admin_assets' ) ) {
			remove_action( 'admin_enqueue_scripts', [ learn_press_admin_assets(), 'load_scripts' ] );
		}
	}

	/**
	 * Removes the duplicate meta description tag from the Hello Elementor theme.
	 *
	 * @since 4.4.3
	 *
	 * @link https://developers.elementor.com/docs/hello-elementor-theme/hello_elementor_add_description_meta_tag/
	 *
	 * @return void
	 */
	public function removeHelloElementorDescriptionTag() {
		remove_action( 'wp_head', 'hello_elementor_add_description_meta_tag' );
	}

	/**
	 * Removes the Avada OG tags.
	 *
	 * @since 4.6.5
	 *
	 * @return void
	 */
	public function removeAvadaOgTags() {
		if ( function_exists( 'Avada' ) ) {
			$avada = Avada();
			if ( is_object( $avada->head ?? null ) ) {
				remove_action( 'wp_head', [ $avada->head, 'insert_og_meta' ], 5 );
			}
		}
	}

	/**
	 * Prevent WP-Optimize from deleting our tables.
	 *
	 * @since 4.4.5
	 *
	 * @param  array $tables List of tables.
	 * @return array         Filtered tables.
	 */
	public function wpOptimizeAioseoTables( $tables ) {
		foreach ( $tables as &$table ) {
			if (
				is_object( $table ) &&
				property_exists( $table, 'Name' ) &&
				false !== stripos( $table->Name, 'aioseo_' )
			) {
				$table->is_using       = true;
				$table->can_be_removed = false;
			}
		}

		return $tables;
	}

	/**
	 * Defines specific meta fields for WPML so character limits can be applied when auto-translating fields.
	 *
	 * @since 4.8.3.2
	 *
	 * @param  array $fields The fields.
	 * @return array         The modified fields.
	 */
	public function defineMetaFieldsForWpml( $fields ) {
		foreach ( $fields as &$field ) {
			if ( empty( $field['field_type'] ) ) {
				continue;
			}

			$fieldKey = strtolower( preg_replace( '/^(field-)(.*)(-0)$/', '$2', $field['field_type'] ) );

			switch ( $fieldKey ) {
				case '_aioseo_title':
					$field['purpose'] = 'seo_title';
					break;
				case '_aioseo_description':
					$field['purpose'] = 'seo_meta_description';
					break;
			}
		}

		return $fields;
	}
}Common/Main/Title.php000066600000004366151135505570010476 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Document Title class.
 *
 * @since 4.3.9
 */
class Title {
	/**
	 * Keeps the buffer level.
	 *
	 * @since 4.3.9
	 *
	 * @var int
	 */
	private $bufferLevel = 0;

	/**
	 * Starts the output buffering.
	 *
	 * @since   4.3.2
	 * @version 4.3.9
	 *
	 * @return void
	 */
	public function startOutputBuffering() {
		ob_start();

		$this->bufferLevel = ob_get_level();
	}

	/**
	 * Ends the output buffering.
	 *
	 * @since   4.3.2
	 * @version 4.3.9
	 *
	 * @return void
	 */
	public function endOutputBuffering() {
		// Bail if our code didn't start the output buffering at all.
		if ( 0 === $this->bufferLevel ) {
			return;
		}

		/**
		 * In case the current buffer level is different from the one we kept earlier, then: either a plugin started a new buffer or ended our buffer earlier.
		 * If that's the case, we can't properly rewrite the document title anymore since we don't know what buffer content we'd parse below.
		 * In order to avoid conflicts/errors (blank/broken pages), we just bail.
		 * If we bail, the page won't have the title set by AIOSEO, but this can be fixed if the active theme starts supporting the "title-tag" feature {@link https://codex.wordpress.org/Title_Tag}.
		 */
		if ( ob_get_level() !== $this->bufferLevel ) {
			return;
		}

		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo $this->rewriteTitle( (string) ob_get_clean() );
	}

	/**
	 * Replace the page document title.
	 *
	 * @since   4.0.5
	 * @version 4.3.2
	 * @version 4.3.9
	 *
	 * @param  string $content The buffer content.
	 * @return string          The rewritten title.
	 */
	private function rewriteTitle( $content ) {
		if ( strpos( $content, '<!-- All in One SEO' ) === false ) {
			return $content;
		}

		// Remove all existing title tags.
		$content   = preg_replace( '#<title.*?/title>#s', '', (string) $content );
		$pageTitle = aioseo()->helpers->escapeRegexReplacement( aioseo()->head->getTitle() );

		// Return new output with our new title tag included in our own comment block.
		return preg_replace( '/(<!--\sAll\sin\sOne\sSEO[a-z0-9\s.]+\s-\saioseo\.com\s-->)/i', "$1\r\n\t\t<title>$pageTitle</title>", (string) $content, 1 );
	}
}Common/Main/Uninstall.php000066600000006005151135505570011356 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Utils;

/**
 * Handles plugin deinstallation.
 *
 * @since 4.8.1
 */
class Uninstall {
	/**
	 * Removes all data.
	 *
	 * @since 4.8.1
	 *
	 * @param  bool $force Whether we should ignore the uninstall option or not. We ignore it when we reset all data via the Debug Panel.
	 * @return void
	 */
	public function dropData( $force = false ) {
		// Don't call `aioseo()->options` as it's not loaded during uninstall.
		$aioseoOptions = get_option( 'aioseo_options', '' );
		$aioseoOptions = json_decode( $aioseoOptions, true );

		// Confirm that user has decided to remove all data, otherwise stop.
		if (
			! $force &&
			empty( $aioseoOptions['advanced']['uninstall'] )
		) {
			return;
		}

		// Drop our custom tables.
		$this->uninstallDb();

		// Delete all our custom capabilities.
		$this->uninstallCapabilities();
	}

	/**
	 * Removes all our tables and options.
	 *
	 * @since 4.2.3
	 * @version 4.8.1 Moved from Core to Uninstall.
	 *
	 * @return void
	 */
	private function uninstallDb() {
		// Delete all our custom tables.
		global $wpdb;

		// phpcs:disable WordPress.DB.DirectDatabaseQuery
		foreach ( aioseo()->core->getDbTables() as $tableName ) {
			$wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $tableName ) );
		}

		// Delete all AIOSEO Locations and Location Categories.
		$wpdb->delete( $wpdb->posts, [ 'post_type' => 'aioseo-location' ], [ '%s' ] );
		$wpdb->delete( $wpdb->term_taxonomy, [ 'taxonomy' => 'aioseo-location-category' ], [ '%s' ] );

		// Delete all the plugin settings.
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", 'aioseo\_%' ) );

		// Remove any transients we've left behind.
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", '\_aioseo\_%' ) );
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", 'aioseo\_%' ) );

		// Delete all entries from the action scheduler table.
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}actionscheduler_actions WHERE hook LIKE %s", 'aioseo\_%' ) );
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}actionscheduler_groups WHERE slug = %s", 'aioseo' ) );
		// phpcs:enable
	}

	/**
	 * Removes all our custom capabilities.
	 *
	 * @since 4.8.1
	 *
	 * @return void
	 */
	private function uninstallCapabilities() {
		$access             = new Utils\Access();
		$customCapabilities = $access->getCapabilityList() ?? [];
		$roles              = aioseo()->helpers->getUserRoles();

		// Loop through roles and remove custom capabilities.
		foreach ( $roles as $roleName => $roleInfo ) {
			$role = get_role( $roleName );

			if ( $role ) {
				$role->remove_cap( 'aioseo_admin' );
				$role->remove_cap( 'aioseo_manage_seo' );

				foreach ( $customCapabilities as $capability ) {
					$role->remove_cap( $capability );
				}
			}
		}

		remove_role( 'aioseo_manager' );
		remove_role( 'aioseo_editor' );
	}
}Common/Main/CategoryBase.php000066600000017205151135505570011761 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Main class with methods that are called.
 *
 * @since   4.2.0
 * @version 4.7.1 Moved from Pro to Common.
 */
class CategoryBase {
	/**
	 * Class constructor.
	 *
	 * @since 4.2.0
	 */
	public function __construct() {
		if ( ! aioseo()->options->searchAppearance->advanced->removeCategoryBase ) {
			return;
		}

		add_filter( 'query_vars', [ $this, 'queryVars' ] );
		add_filter( 'request', [ $this, 'maybeRedirectCategoryUrl' ] );
		add_filter( 'category_rewrite_rules', [ $this, 'categoryRewriteRules' ] );
		add_filter( 'term_link', [ $this, 'modifyTermLink' ], 10, 3 );

		// Flush rewrite rules on any of the following actions.
		add_action( 'created_category', [ $this, 'scheduleFlushRewrite' ] );
		add_action( 'delete_category', [ $this, 'scheduleFlushRewrite' ] );
		add_action( 'edited_category', [ $this, 'scheduleFlushRewrite' ] );
	}

	/**
	 * Add the redirect var to the query vars if the "strip category bases" option is enabled.
	 *
	 * @since 4.2.0
	 *
	 * @param  array $queryVars Query vars to filter.
	 * @return array            The filtered query vars.
	 */
	public function queryVars( $queryVars ) {
		$queryVars[] = 'aioseo_category_redirect';

		return $queryVars;
	}

	/**
	 * Redirect the category URL to the new one.
	 *
	 * @param  array $queryVars Query vars to check for redirect var.
	 * @return array            The original query vars.
	 */
	public function maybeRedirectCategoryUrl( $queryVars ) {
		if ( isset( $queryVars['aioseo_category_redirect'] ) ) {
			$categoryUrl = trailingslashit( get_option( 'home' ) ) . user_trailingslashit( $queryVars['aioseo_category_redirect'], 'category' );
			wp_redirect( $categoryUrl, 301, 'AIOSEO' );
			die;
		}

		return $queryVars;
	}

	/**
	 * Rewrite the category base.
	 *
	 * @since 4.2.0
	 *
	 * @return array The rewritten rules.
	 */
	public function categoryRewriteRules() {
		global $wp_rewrite; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		$categoryRewrite = $this->getCategoryRewriteRules();

		// Redirect from the old base.
		$categoryStructure = $wp_rewrite->get_category_permastruct(); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		$categoryBase      = trim( str_replace( '%category%', '(.+)', $categoryStructure ), '/' ) . '$';

		// Add the rewrite rules.
		$categoryRewrite[ $categoryBase ] = 'index.php?aioseo_category_redirect=$matches[1]';

		return $categoryRewrite;
	}

	/**
	 * Get the rewrite rules for the category.
	 *
	 * @since 4.2.0
	 *
	 * @return array An array of category rewrite rules.
	 */
	private function getCategoryRewriteRules() {
		global $wp_rewrite; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		$categoryRewrite = [];
		$categories      = get_categories( [ 'hide_empty' => false ] );

		if ( empty( $categories ) ) {
			return $categoryRewrite;
		}

		$blogPrefix      = $this->getBlogPrefix();
		$paginationBase = $wp_rewrite->pagination_base; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		foreach ( $categories as $category ) {
			$nicename        = $this->getCategoryParents( $category ) . $category->slug;
			$categoryRewrite = $this->addCategoryRewrites( $categoryRewrite, $nicename, $blogPrefix, $paginationBase );

			// Also add the rules for uppercase.
			$filteredNicename = $this->convertEncodedToUppercase( $nicename );

			if ( $nicename !== $filteredNicename ) {
				$categoryRewrite = $this->addCategoryRewrites( $categoryRewrite, $filteredNicename, $blogPrefix, $paginationBase );
			}
		}

		return $categoryRewrite;
	}

	/**
	 * Get the blog prefix.
	 *
	 * @since 4.2.0
	 *
	 * @return string The prefix for the blog.
	 */
	private function getBlogPrefix() {
		$permalinkStructure = get_option( 'permalink_structure' );
		if (
			is_multisite() &&
			! is_subdomain_install() &&
			is_main_site() &&
			0 === strpos( $permalinkStructure, '/blog/' )
		) {
			return 'blog/';
		}

		return '';
	}

	/**
	 * Retrieve category parents with separator.
	 *
	 * @since 4.2.0
	 *
	 * @param  \WP_Term $category the category instance.
	 * @return string             A list of category parents.
	 */
	private function getCategoryParents( $category ) {
		if (
			$category->parent === $category->term_id ||
			absint( $category->parent ) < 1
		) {
			return '';
		}

		$parents = get_category_parents( $category->parent, false, '/', true );

		return is_wp_error( $parents ) ? '' : $parents;
	}

	/**
	 * Walks through category nicename and convert encoded parts
	 * into uppercase using $this->encode_to_upper().
	 *
	 * @since 4.2.0
	 *
	 * @param  string $nicename The encoded category string.
	 * @return string           The converted category string.
	 */
	private function convertEncodedToUppercase( $nicename ) {
		// Checks if name has any encoding in it.
		if ( false === strpos( $nicename, '%' ) ) {
			return $nicename;
		}

		$nicenames = explode( '/', $nicename );
		$nicenames = array_map( [ $this, 'convertToUppercase' ], $nicenames );

		return implode( '/', $nicenames );
	}

	/**
	 * Converts the encoded URI string to uppercase.
	 *
	 * @since 4.2.0
	 *
	 * @param  string $encoded The encoded category string.
	 * @return string          The converted category string.
	 */
	private function convertToUppercase( $encoded ) {
		if ( false === strpos( $encoded, '%' ) ) {
			return $encoded;
		}

		return strtoupper( $encoded );
	}

	/**
	 * Adds the required category rewrites rules.
	 *
	 * @since 4.2.0
	 *
	 * @param  array  $categoryRewrite  The current set of rules.
	 * @param  string $categoryNicename The category nicename.
	 * @param  string $blogPrefix       Multisite blog prefix.
	 * @param  string $paginationBase   WP_Query pagination base.
	 * @return array                    The added set of rules.
	 */
	private function addCategoryRewrites( $categoryRewrite, $categoryNicename, $blogPrefix, $paginationBase ) {
		$categoryRewrite[ $blogPrefix . '(' . $categoryNicename . ')/(?:feed/)?(feed|rdf|rss|rss2|atom)/?$' ]   = 'index.php?category_name=$matches[1]&feed=$matches[2]';
		$categoryRewrite[ $blogPrefix . '(' . $categoryNicename . ')/' . $paginationBase . '/?([0-9]{1,})/?$' ] = 'index.php?category_name=$matches[1]&paged=$matches[2]';
		$categoryRewrite[ $blogPrefix . '(' . $categoryNicename . ')/?$' ]                                      = 'index.php?category_name=$matches[1]';

		return $categoryRewrite;
	}

	/**
	 * Remove the category base from the category link.
	 *
	 * @since 4.2.0
	 *
	 * @param  string $link     Term link.
	 * @param  object $term     The current Term Object.
	 * @param  string $taxonomy The current Taxonomy.
	 * @return string           The modified term link.
	 */
	public function modifyTermLink( $link, $term = null, $taxonomy = '' ) {
		if ( 'category' !== $taxonomy ) {
			return $link;
		}

		$categoryBase = get_option( 'category_base' );
		if ( empty( $categoryBase ) ) {
			global $wp_rewrite; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			$categoryStructure = $wp_rewrite->get_category_permastruct(); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			$categoryBase      = trim( str_replace( '%category%', '', $categoryStructure ), '/' );
		}

		// Remove initial slash, if there is one (we remove the trailing slash in the regex replacement and don't want to end up short a slash).
		if ( '/' === substr( $categoryBase, 0, 1 ) ) {
			$categoryBase = substr( $categoryBase, 1 );
		}

		$categoryBase .= '/';

		return preg_replace( '`' . preg_quote( (string) $categoryBase, '`' ) . '`u', '', (string) $link, 1 );
	}

	/**
	 * Flush the rewrite rules.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	public function scheduleFlushRewrite() {
		aioseo()->options->flushRewriteRules();
	}
}Common/Main/QueryArgs.php000066600000013015151135505570011326 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models\CrawlCleanupLog;
use AIOSEO\Plugin\Common\Models\CrawlCleanupBlockedArg;

/**
 * Query arguments class.
 *
 * @since   4.2.1
 * @version 4.5.8
 */
class QueryArgs {
	/**
	 * Construct method.
	 *
	 * @since 4.2.1
	 */
	public function __construct() {
		if (
			is_admin() ||
			aioseo()->helpers->isWpLoginPage() ||
			aioseo()->helpers->isAjaxCronRestRequest() ||
			aioseo()->helpers->isDoingWpCli()
		) {
			return;
		}

		add_action( 'template_redirect', [ $this, 'maybeRemoveQueryArgs' ], 1 );

		$this->removeReplyToCom();
	}

	/**
	 * Check if we can remove query args.
	 *
	 * @since 4.5.8
	 *
	 * @return boolean True if the query args can be removed.
	 */
	private function canRemoveQueryArgs() {
		if (
			! aioseo()->options->searchAppearance->advanced->blockArgs->enable ||
			is_user_logged_in() ||
			is_admin() ||
			is_robots() ||
			get_query_var( 'aiosp_sitemap_path' ) ||
			empty( $_GET ) // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		) {
			return false;
		}

		if ( is_singular() ) {
			global $post;
			$thePost = aioseo()->helpers->getPost( $post->ID );

			// Leave the preview query arguments intact.
			if (
				// phpcs:disable phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
				isset( $_GET['preview'] ) &&
				isset( $_GET['preview_nonce'] ) &&
				// phpcs:enable
				wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['preview_nonce'] ) ), 'post_preview_' . $thePost->ID ) &&
				current_user_can( 'edit_post', $thePost->ID )
			) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Maybe remove query args.
	 *
	 * @since 4.5.8
	 *
	 * @return void
	 */
	public function maybeRemoveQueryArgs() {
		if ( ! $this->canRemoveQueryArgs() ) {
			return;
		}

		$currentRequest = aioseo()->helpers->getRequestUrl();

		// Remove the home path from the url for subfolder installs.
		$currentRequest       = aioseo()->helpers->excludeHomePath( $currentRequest );
		$currentRequestParsed = wp_parse_url( $currentRequest );

		// No query args? Never mind!
		if ( empty( $currentRequestParsed['query'] ) ) {
			return;
		}

		parse_str( $currentRequestParsed['query'], $currentRequestQueryArgs );
		$notAllowed          = [];
		$recognizedQueryLogs = [];

		foreach ( $currentRequestQueryArgs as $key => $value ) {
			if ( ! is_string( $value ) ) {
				continue;
			}
			$this->addQueryLog( $currentRequestParsed['path'], $key, $value );

			$blocked = CrawlCleanupBlockedArg::getByKeyValue( $key, null );
			if ( ! $blocked->exists() ) {
				$blocked = CrawlCleanupBlockedArg::getByKeyValue( $key, $value );
			}

			if ( ! $blocked->exists() ) {
				$blocked = CrawlCleanupBlockedArg::matchRegex( $key, $value );
			}

			if ( $blocked->exists() ) {
				$queryArg = $key . ( $value ? '=' . $value : null );
				$notAllowed[] = $queryArg;
				$blocked->addHit();
				continue;
			}

			$recognizedQueryLogs[ $key ] = empty( $value ) ? true : $value;
		}

		if ( ! empty( $notAllowed ) ) {
			$newUrl = home_url( $currentRequestParsed['path'] );

			header( 'Content-Type: redirect', true );
			header_remove( 'Content-Type' );
			header_remove( 'Last-Modified' );
			header_remove( 'X-Pingback' );

			wp_safe_redirect( add_query_arg( $recognizedQueryLogs, $newUrl ), 301, AIOSEO_PLUGIN_SHORT_NAME . ' Crawl Cleanup' );
			exit;
		}
	}

	/**
	 * Remove ?replytocom.
	 *
	 * @since 4.5.8
	 *
	 * @return void
	 */
	private function removeReplyToCom() {
		if ( ! apply_filters( 'aioseo_remove_reply_to_com', true ) ) {
			return;
		}

		add_filter( 'comment_reply_link', [ $this, 'removeReplyToComLink' ] );
		add_action( 'template_redirect', [ $this, 'replyToComRedirect' ], 1 );
	}

	/**
	 * Remove ?replytocom.
	 *
	 * @since 4.7.3
	 *
	 * @param  string $link The comment link as a string.
	 * @return string       The modified link.
	 */
	public function removeReplyToComLink( $link ) {
		return preg_replace( '`href=(["\'])(?:.*(?:\?|&|&#038;)replytocom=(\d+)#respond)`', 'href=$1#comment-$2', (string) $link );
	}

	/**
	 * Redirects out the ?replytocom variables.
	 *
	 * @since 4.7.3
	 *
	 * @return void
	 */
	public function replyToComRedirect() {
		// phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
		$replyToCom = absint( sanitize_text_field( wp_unslash( $_GET['replytocom'] ?? null ) ) );

		if ( ! empty( $replyToCom ) && is_singular() ) {
			$url = get_permalink( $GLOBALS['post']->ID );
			if ( isset( $_SERVER['QUERY_STRING'] ) ) {
				$queryString = remove_query_arg( 'replytocom', sanitize_text_field( wp_unslash( $_SERVER['QUERY_STRING'] ) ) );
				if ( ! empty( $queryString ) ) {
					$url = add_query_arg( [], $url ) . '?' . $queryString;
				}
			}
			$url = add_query_arg( [], $url ) . '#comment-' . $replyToCom;

			wp_safe_redirect( $url, 301, AIOSEO_PLUGIN_SHORT_NAME );
			exit;
		}
	}

	/**
	 * Add query args log.
	 *
	 * @since 4.5.8
	 *
	 * @param string $path  A String of the path to create a slug.
	 * @param string $key   A String of key from query arg.
	 * @param string $value A String of value from query arg.
	 * @return void
	 */
	private function addQueryLog( $path, $key, $value = null ) {
		$slug = $path . '?' . $key . ( 0 < strlen( $value ) ? '=' . $value : '' );
		$log  = CrawlCleanupLog::getBySlug( $slug );

		$data = [
			'slug'  => $slug,
			'key'   => $key,
			'value' => $value
		];

		$log->set( $data );
		$log->create();
	}
}Common/Main/Activate.php000066600000010263151135505570011146 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Abstract class that Pro and Lite both extend.
 *
 * @since 4.0.0
 */
class Activate {
	/**
	 * Construct method.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		register_activation_hook( AIOSEO_FILE, [ $this, 'activate' ] );
		register_deactivation_hook( AIOSEO_FILE, [ $this, 'deactivate' ] );

		// The following only needs to happen when in the admin.
		if ( ! is_admin() ) {
			return;
		}

		// This needs to run on at least 1000 because we load the roles in the Access class on 999.
		add_action( 'init', [ $this, 'init' ], 1000 );
	}

	/**
	 * Initialize activation.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	public function init() {
		// If Pro just deactivated the lite version, we need to manually run the activation hook, because it doesn't run here.
		$proDeactivatedLite = (bool) aioseo()->core->cache->get( 'pro_just_deactivated_lite' );
		if ( ! $proDeactivatedLite ) {
			// Also check for the old transient in the options table (because a user might switch from an older Lite version that lacks the Cache class).
			$proDeactivatedLite = (bool) get_option( '_aioseo_cache_pro_just_deactivated_lite' );
		}

		if ( $proDeactivatedLite ) {
			aioseo()->core->cache->delete( 'pro_just_deactivated_lite' );
			$this->activate( false );
		}
	}

	/**
	 * Runs on activation.
	 *
	 * @since 4.0.17
	 *
	 * @param  bool $networkWide Whether or not this is a network wide activation.
	 * @return void
	 */
	public function activate( $networkWide ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		aioseo()->access->addCapabilities();

		// Make sure our tables exist.
		aioseo()->updates->addInitialCustomTablesForV4();

		// Set the activation timestamps.
		$time = time();
		aioseo()->internalOptions->internal->activated = $time;

		if ( ! aioseo()->internalOptions->internal->firstActivated ) {
			aioseo()->internalOptions->internal->firstActivated = $time;
		}

		aioseo()->core->cache->clear();

		$this->maybeRunSetupWizard();
	}

	/**
	 * Runs on deactivation.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function deactivate() {
		aioseo()->access->removeCapabilities();
	}

	/**
	 * Check if we should redirect on activation.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	private function maybeRunSetupWizard() {
		if ( '0.0' !== aioseo()->internalOptions->internal->lastActiveVersion ) {
			return;
		}

		$oldOptions = get_option( 'aioseop_options' );
		if ( ! empty( $oldOptions ) ) {
			return;
		}

		if ( is_network_admin() ) {
			return;
		}

		if ( isset( $_GET['activate-multi'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			return;
		}

		// Sets 30 second transient for welcome screen redirect on activation.
		aioseo()->core->cache->update( 'activation_redirect', true, 30 );
	}

	/**
	 * Adds our capabilities to all roles on the next request and the installing user on the current request after upgrading to Pro.
	 *


	 *
	 * @since 4.1.4.4
	 *
	 * @return void
	 */
	public function addCapabilitiesOnUpgrade() {
		// In case the user is switching to Pro via the AIOSEO Connect feature,
		// we need to set this transient here as the regular activation hooks won't run and Pro otherwise won't clear the cache and add the required capabilities.
		aioseo()->core->cache->update( 'pro_just_deactivated_lite', true );

		// Doing the above isn't sufficient because the current user will be lacking the capabilities on the first request. Therefore, we add them manually just for him.
		$userId = function_exists( 'get_current_user_id' ) && get_current_user_id()
			? get_current_user_id() // If there is a logged in user, the user is switching from Lite to Pro via the Plugins menu.
			: aioseo()->core->cache->get( 'connect_active_user' ); // If there is no logged in user, we're upgrading via AIOSEO Connect.

		$user = get_userdata( $userId );
		if ( is_object( $user ) ) {
			$capabilities = aioseo()->access->getCapabilityList();
			foreach ( $capabilities as $capability ) {
				$user->add_cap( $capability );
			}
		}

		aioseo()->core->cache->delete( 'connect_active_user' );
	}
}Common/Main/Media.php000066600000002224151135505570010423 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Media class.
 *
 * @since 4.0.0
 */
class Media {
	/**
	 * Construct method.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'template_redirect', [ $this, 'attachmentRedirect' ], 1 );
	}

	/**
	 * If the user wants to redirect attachment pages, this is where we do it.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function attachmentRedirect() {
		if ( ! is_attachment() ) {
			return;
		}

		if (
			! aioseo()->dynamicOptions->searchAppearance->postTypes->has( 'attachment' )
		) {
			return;
		}

		$redirect = aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls;
		if ( 'disabled' === $redirect ) {
			return;
		}

		if ( 'attachment' === $redirect ) {
			$url = wp_get_attachment_url( get_queried_object_id() );
			if ( empty( $url ) ) {
				return;
			}

			return wp_safe_redirect( $url, 301, AIOSEO_PLUGIN_SHORT_NAME );
		}

		global $post;
		if ( ! empty( $post->post_parent ) ) {
			wp_safe_redirect( urldecode( get_permalink( $post->post_parent ) ), 301 );
		}
	}
}Common/Main/Main.php000066600000003016151135505570010270 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models;

/**
 * Abstract class that Pro and Lite both extend.
 *
 * @since 4.0.0
 */
class Main {
	/**
	 * Construct method.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		new Media();
		new QueryArgs();

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueTranslations' ] );
		add_action( 'wp_enqueue_scripts', [ $this, 'enqueueFrontEndAssets' ] );
		add_action( 'admin_footer', [ $this, 'adminFooter' ] );
	}

	/**
	 * Enqueues the translations seperately so it can be called from anywhere.
	 *
	 * @since 4.1.9
	 *
	 * @return void
	 */
	public function enqueueTranslations() {
		aioseo()->core->assets->load( 'src/vue/standalone/app/main.js', [], [
			'translations' => aioseo()->helpers->getJedLocaleData( 'all-in-one-seo-pack' )
		], 'aioseoTranslations' );
	}

	/**
	 * Enqueue styles on the front-end.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function enqueueFrontEndAssets() {
		$canManageSeo = apply_filters( 'aioseo_manage_seo', 'aioseo_manage_seo' );
		if (
			! aioseo()->helpers->isAdminBarEnabled() ||
			! ( current_user_can( $canManageSeo ) || aioseo()->access->canManage() )
		) {
			return;
		}

		aioseo()->core->assets->enqueueCss( 'src/vue/assets/scss/app/admin-bar.scss' );
	}

	/**
	 * Enqueue the footer file to let vue attach.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function adminFooter() {
		echo '<div id="aioseo-admin"></div>';
	}
}Common/Main/Updates.php000066600000203224151135505570011014 0ustar00<?php
namespace AIOSEO\Plugin\Common\Main;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models;

/**
 * Updater class.
 *
 * @since 4.0.0
 */
class Updates {

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'aioseo_v4_migrate_post_schema', [ $this, 'migratePostSchema' ] );
		add_action( 'aioseo_v4_migrate_post_schema_default', [ $this, 'migratePostSchemaDefault' ] );
		add_action( 'aioseo_v419_remove_revision_records', [ $this, 'removeRevisionRecords' ] );

		if (
			wp_doing_ajax() ||
			wp_doing_cron()
		) {
			return;
		}

		add_action( 'init', [ $this, 'init' ], 1001 );
		add_action( 'init', [ $this, 'runUpdates' ], 1002 );
		add_action( 'init', [ $this, 'updateLatestVersion' ], 3000 );
	}

	/**
	 * Sets the latest active version if it is not set yet.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function init() {
		if ( '0.0' !== aioseo()->internalOptions->internal->lastActiveVersion ) {
			return;
		}

		// It's possible the user may not have capabilities. Let's add them now.
		aioseo()->access->addCapabilities();

		$oldOptions = get_option( 'aioseop_options' );
		if ( ! empty( $oldOptions['last_active_version'] ) ) {
			aioseo()->internalOptions->internal->lastActiveVersion = $oldOptions['last_active_version'];
		}

		$this->addInitialCustomTablesForV4();
		add_action( 'wp_loaded', [ $this, 'setDefaultSocialImages' ], 1001 );
	}

	/**
	 * Runs our migrations.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function runUpdates() {
		$lastActiveVersion = aioseo()->internalOptions->internal->lastActiveVersion;
		// Don't run updates if the last active version is the same as the current version.
		if ( aioseo()->version === $lastActiveVersion ) {
			// Allow addons to run their updates.
			do_action( 'aioseo_run_updates', $lastActiveVersion );

			return;
		}

		// Try to acquire the lock.
		if ( ! aioseo()->core->db->acquireLock( 'aioseo_run_updates_lock', 0 ) ) {
			// If we couldn't acquire the lock, exit early without doing anything.
			// This means another process is already running updates.
			return;
		}

		// The dynamic options have not yet fully loaded, so let's refresh here to force that to happen.
		aioseo()->dynamicOptions->refresh(); // TODO: Check if we still need this since it already runs on 999 in the main AIOSEO file.

		if ( version_compare( $lastActiveVersion, '4.0.5', '<' ) ) {
			$this->addImageScanDateColumn();
		}

		if ( version_compare( $lastActiveVersion, '4.0.6', '<' ) ) {
			$this->disableTwitterUseOgDefault();
			$this->updateMaxImagePreviewDefault();
		}

		if ( ! aioseo()->pro && version_compare( $lastActiveVersion, '4.0.6', '=' ) && 'posts' !== get_option( 'show_on_front' ) ) {
			aioseo()->migration->helpers->redoMigration();
		}

		if ( version_compare( $lastActiveVersion, '4.0.13', '<' ) ) {
			$this->removeDuplicateRecords();
		}

		if ( version_compare( $lastActiveVersion, '4.0.17', '<' ) ) {
			$this->removeLocationColumn();
		}

		if ( version_compare( $lastActiveVersion, '4.1.2', '<' ) ) {
			$this->clearProductImages();
		}

		if ( version_compare( $lastActiveVersion, '4.1.3', '<' ) ) {
			$this->addNotificationsNewColumn();
			$this->noindexWooCommercePages();
			$this->accessControlNewCapabilities();
		}

		if ( version_compare( $lastActiveVersion, '4.1.3.3', '<' ) ) {
			$this->accessControlNewCapabilities();
		}

		if ( version_compare( $lastActiveVersion, '4.1.4.3', '<' ) ) {
			$this->migrateDynamicSettings();
		}

		if ( version_compare( $lastActiveVersion, '4.1.5', '<' ) ) {
			aioseo()->actionScheduler->unschedule( 'aioseo_cleanup_action_scheduler' );
			// Schedule routine to remove our old transients from the options table.
			aioseo()->actionScheduler->scheduleSingle( aioseo()->core->cachePrune->getOptionCacheCleanAction(), MINUTE_IN_SECONDS );

			// Refresh with new Redirects capability.
			$this->accessControlNewCapabilities();

			// Regenerate the sitemap if using a static one to update the data for the new stylesheets.
			aioseo()->sitemap->regenerateStaticSitemap();

			$this->fixSchemaTypeDefault();
		}

		if ( version_compare( $lastActiveVersion, '4.1.6', '<' ) ) {
			// Remove the recurring scheduled action for notifications.
			aioseo()->actionScheduler->unschedule( 'aioseo_admin_notifications_update' );

			$this->migrateOgTwitterImageColumns();

			// Set the OG data to false for current installs.
			aioseo()->options->social->twitter->general->useOgData = false;
		}

		if ( version_compare( $lastActiveVersion, '4.1.8', '<' ) ) {
			$this->addLimitModifiedDateColumn();

			// Refresh with new Redirects Page capability.
			$this->accessControlNewCapabilities();
		}

		if ( version_compare( $lastActiveVersion, '4.1.9', '<' ) ) {
			$this->fixTaxonomyTags();
			$this->scheduleRemoveRevisionsRecords();
		}

		if ( version_compare( $lastActiveVersion, '4.0.0', '>=' ) && version_compare( $lastActiveVersion, '4.2.0', '<' ) ) {
			$this->migrateDeprecatedRunShortcodesSetting();
		}

		if ( version_compare( $lastActiveVersion, '4.2.1', '<' ) ) {
			// Force WordPress to flush the rewrite rules.
			aioseo()->options->flushRewriteRules();

			Models\Notification::deleteNotificationByName( 'deprecated-filters' );
			Models\Notification::deleteNotificationByName( 'deprecated-filters-v2' );
		}

		if ( version_compare( $lastActiveVersion, '4.2.2', '<' ) ) {
			aioseo()->internalOptions->database->installedTables = '';

			$this->addOptionsColumn();
			$this->removeTabsColumn();
			$this->migrateUserContactMethods();

			// Unschedule any static sitemap regeneration actions to remove any that failed and are still in-progress as a result.
			aioseo()->actionScheduler->unschedule( 'aioseo_static_sitemap_regeneration' );
		}

		if ( version_compare( $lastActiveVersion, '4.2.4', '<' ) ) {
			$this->addNotificationsAddonColumn();
		}

		if ( version_compare( $lastActiveVersion, '4.2.5', '<' ) ) {
			$this->addSchemaColumn();
			$this->schedulePostSchemaMigration();
		}

		if ( version_compare( $lastActiveVersion, '4.2.4.2', '>' ) && version_compare( $lastActiveVersion, '4.2.6', '<' ) ) {
			// The default graphs only need to be remigrated if the user was on 4.2.5 or 4.2.5.1.
			$this->schedulePostSchemaDefaultMigration();
		}

		if ( version_compare( $lastActiveVersion, '4.2.8', '<' ) ) {
			$this->migrateDashboardWidgetsOptions();
		}

		if ( version_compare( $lastActiveVersion, '4.3.6', '<' ) ) {
			$this->addPrimaryTermColumn();
		}

		if ( version_compare( $lastActiveVersion, '4.3.9', '<' ) ) {
			$this->migratePriorityColumn();
		}

		if ( version_compare( $lastActiveVersion, '4.4.2', '<' ) ) {
			$this->updateRobotsTxtRules();
		}

		if ( version_compare( $lastActiveVersion, '4.5.1', '<' ) ) {
			$this->checkForGaAnalyticsV3();
		}

		if ( version_compare( $lastActiveVersion, '4.5.8', '<' ) ) {
			$this->addQueryArgMonitorTables();
			$this->addQueryArgMonitorNotification();
		}

		if ( version_compare( $lastActiveVersion, '4.5.9', '<' ) ) {
			$this->deprecateNoPaginationForCanonicalUrlsSetting();
		}

		if ( version_compare( $lastActiveVersion, '4.6.5', '<' ) ) {
			$this->deprecateBreadcrumbsEnabledSetting();
		}

		if ( version_compare( $lastActiveVersion, '4.7.4', '<' ) ) {
			$this->addWritingAssistantTables();
			aioseo()->access->addCapabilities();
		}

		if ( version_compare( $lastActiveVersion, '4.7.5', '<' ) ) {
			$this->cancelScheduledSitemapPings();
		}

		if ( version_compare( $lastActiveVersion, '4.7.7', '<' ) ) {
			$this->disableEmailReports();
		}

		if ( version_compare( $lastActiveVersion, '4.7.9', '<' ) ) {
			$this->fixSavedHeadlines();
			$this->rescheduleEmailReport();
		}

		if ( version_compare( $lastActiveVersion, '4.8.3', '<' ) ) {
			$this->resetImageScanDate();
			$this->addSeoAnalyzerResultsTable();
			$this->migrateSeoAnalyzerResults();
			$this->migrateSeoAnalyzerCompetitors();
			$this->addBreadcrumbSettingsColumn();
		}

		if ( version_compare( $lastActiveVersion, '4.8.3.1', '<' ) ) {
			aioseo()->core->cache->delete( 'analyze_site_code' );
			aioseo()->core->cache->delete( 'analyze_site_body' );
		}

		do_action( 'aioseo_run_updates', $lastActiveVersion );

		// Always clear the cache if the last active version is different from our current.

		if ( version_compare( $lastActiveVersion, AIOSEO_VERSION, '<' ) ) {
			aioseo()->core->cache->clear();
		}
	}

	/**
	 * Retrieve the raw options from the database for migration.
	 *
	 * @since 4.1.4
	 *
	 * @return array An array of options.
	 */
	private function getRawOptions() {
		// Options from the DB.
		$commonOptions = json_decode( get_option( aioseo()->options->optionsName ), true );
		if ( empty( $commonOptions ) ) {
			$commonOptions = [];
		}

		return $commonOptions;
	}

	/**
	 * Updates the latest version after all migrations and updates have run.
	 *
	 * @since 4.0.3
	 *
	 * @return void
	 */
	public function updateLatestVersion() {
		if ( aioseo()->internalOptions->internal->lastActiveVersion === aioseo()->version ) {
			return;
		}

		aioseo()->internalOptions->internal->lastActiveVersion = aioseo()->version;

		// Bust the tableExists and columnExists cache.
		aioseo()->internalOptions->database->installedTables = '';

		// Bust the DB cache so we can make sure that everything is fresh.
		aioseo()->core->db->bustCache();
	}

	/**
	 * Adds our custom tables for V4.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addInitialCustomTablesForV4() {
		$db             = aioseo()->core->db->db;
		$charsetCollate = '';

		if ( ! empty( $db->charset ) ) {
			$charsetCollate .= "DEFAULT CHARACTER SET {$db->charset}";
		}
		if ( ! empty( $db->collate ) ) {
			$charsetCollate .= " COLLATE {$db->collate}";
		}

		// Check for notifications table.
		if ( ! aioseo()->core->db->tableExists( 'aioseo_notifications' ) ) {
			$tableName = $db->prefix . 'aioseo_notifications';

			aioseo()->core->db->execute(
				"CREATE TABLE {$tableName} (
					id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					slug varchar(13) NOT NULL,
					title text NOT NULL,
					content longtext NOT NULL,
					type varchar(64) NOT NULL,
					level text NOT NULL,
					notification_id bigint(20) unsigned DEFAULT NULL,
					notification_name varchar(255) DEFAULT NULL,
					start datetime DEFAULT NULL,
					end datetime DEFAULT NULL,
					button1_label varchar(255) DEFAULT NULL,
					button1_action varchar(255) DEFAULT NULL,
					button2_label varchar(255) DEFAULT NULL,
					button2_action varchar(255) DEFAULT NULL,
					dismissed tinyint(1) NOT NULL DEFAULT 0,
					created datetime NOT NULL,
					updated datetime NOT NULL,
					PRIMARY KEY (id),
					UNIQUE KEY ndx_aioseo_notifications_slug (slug),
					KEY ndx_aioseo_notifications_dates (start, end),
					KEY ndx_aioseo_notifications_type (type),
					KEY ndx_aioseo_notifications_dismissed (dismissed)
				) {$charsetCollate};"
			);
		}

		if ( ! aioseo()->core->db->tableExists( 'aioseo_posts' ) ) {
			$tableName = $db->prefix . 'aioseo_posts';

			// Incorrect defaults are adjusted below through migrations.
			aioseo()->core->db->execute(
				"CREATE TABLE {$tableName} (
					id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					post_id bigint(20) unsigned NOT NULL,
					title text DEFAULT NULL,
					description text DEFAULT NULL,
					keywords mediumtext DEFAULT NULL,
					keyphrases longtext DEFAULT NULL,
					page_analysis longtext DEFAULT NULL,
					canonical_url text DEFAULT NULL,
					og_title text DEFAULT NULL,
					og_description text DEFAULT NULL,
					og_object_type varchar(64) DEFAULT 'default',
					og_image_type varchar(64) DEFAULT 'default',
					og_image_custom_url text DEFAULT NULL,
					og_image_custom_fields text DEFAULT NULL,
					og_custom_image_width int(11) DEFAULT NULL,
					og_custom_image_height int(11) DEFAULT NULL,
					og_video varchar(255) DEFAULT NULL,
					og_custom_url text DEFAULT NULL,
					og_article_section text DEFAULT NULL,
					og_article_tags text DEFAULT NULL,
					twitter_use_og tinyint(1) DEFAULT 1,
					twitter_card varchar(64) DEFAULT 'default',
					twitter_image_type varchar(64) DEFAULT 'default',
					twitter_image_custom_url text DEFAULT NULL,
					twitter_image_custom_fields text DEFAULT NULL,
					twitter_title text DEFAULT NULL,
					twitter_description text DEFAULT NULL,
					seo_score int(11) DEFAULT 0 NOT NULL,
					schema_type varchar(20) DEFAULT NULL,
					schema_type_options longtext DEFAULT NULL,
					pillar_content tinyint(1) DEFAULT NULL,
					robots_default tinyint(1) DEFAULT 1 NOT NULL,
					robots_noindex tinyint(1) DEFAULT 0 NOT NULL,
					robots_noarchive tinyint(1) DEFAULT 0 NOT NULL,
					robots_nosnippet tinyint(1) DEFAULT 0 NOT NULL,
					robots_nofollow tinyint(1) DEFAULT 0 NOT NULL,
					robots_noimageindex tinyint(1) DEFAULT 0 NOT NULL,
					robots_noodp tinyint(1) DEFAULT 0 NOT NULL,
					robots_notranslate tinyint(1) DEFAULT 0 NOT NULL,
					robots_max_snippet int(11) DEFAULT NULL,
					robots_max_videopreview int(11) DEFAULT NULL,
					robots_max_imagepreview varchar(20) DEFAULT 'none',
					tabs mediumtext DEFAULT NULL,
					images longtext DEFAULT NULL,
					priority tinytext DEFAULT NULL,
					frequency tinytext DEFAULT NULL,
					videos longtext DEFAULT NULL,
					video_thumbnail text DEFAULT NULL,
					video_scan_date datetime DEFAULT NULL,
					local_seo longtext DEFAULT NULL,
					created datetime NOT NULL,
					updated datetime NOT NULL,
					PRIMARY KEY (id),
					KEY ndx_aioseo_posts_post_id (post_id)
				) {$charsetCollate};"
			);
		}

		// Reset the cache for the installed tables.
		aioseo()->internalOptions->database->installedTables = '';
	}

	/**
	 * Sets the default social images.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function setDefaultSocialImages() {
		$siteLogo = aioseo()->helpers->getSiteLogoUrl();
		if ( $siteLogo && ! aioseo()->internalOptions->internal->migratedVersion ) {
			if ( ! aioseo()->options->social->facebook->general->defaultImagePosts ) {
				aioseo()->options->social->facebook->general->defaultImagePosts = $siteLogo;
			}
			if ( ! aioseo()->options->social->twitter->general->defaultImagePosts ) {
				aioseo()->options->social->twitter->general->defaultImagePosts = $siteLogo;
			}
		}
	}

	/**
	 * Adds the image scan date column to our posts table.
	 *
	 * @since 4.0.5
	 *
	 * @return void
	 */
	public function addImageScanDateColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'image_scan_date' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				ADD image_scan_date datetime DEFAULT NULL AFTER images"
			);

			// Reset the cache for the installed tables.
			aioseo()->internalOptions->database->installedTables = '';
		}
	}

	/**
	 * Adds the breadcrumb settings column to our posts table.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	public function addBreadcrumbSettingsColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'breadcrumb_settings' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				ADD `breadcrumb_settings` longtext DEFAULT NULL AFTER local_seo"
			);

			// Reset the cache for the installed tables.
			aioseo()->internalOptions->database->installedTables = '';
		}
	}

	/**
	 * Modifes the default value of the twitter_use_og column.
	 *
	 * @since 4.0.6
	 *
	 * @return void
	 */
	protected function disableTwitterUseOgDefault() {
		if ( aioseo()->core->db->tableExists( 'aioseo_posts' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				MODIFY twitter_use_og tinyint(1) DEFAULT 0"
			);
		}
	}

	/**
	 * Modifes the default value of the robots_max_imagepreview column.
	 *
	 * @since 4.0.6
	 *
	 * @return void
	 */
	protected function updateMaxImagePreviewDefault() {
		if ( aioseo()->core->db->tableExists( 'aioseo_posts' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				MODIFY robots_max_imagepreview varchar(20) DEFAULT 'large'"
			);
		}
	}

	/**
	 * Deletes duplicate records in our custom tables.
	 *
	 * @since 4.0.13
	 *
	 * @return void
	 */
	public function removeDuplicateRecords() {
		$duplicates = aioseo()->core->db->start( 'aioseo_posts' )
			->select( 'post_id, min(id) as id' )
			->groupBy( 'post_id having count(post_id) > 1' )
			->orderByRaw( 'count(post_id) DESC' )
			->run()
			->result();

		if ( empty( $duplicates ) ) {
			return;
		}

		foreach ( $duplicates as $duplicate ) {
			$postId        = $duplicate->post_id;
			$firstRecordId = $duplicate->id;

			aioseo()->core->db->delete( 'aioseo_posts' )
				->whereRaw( "( id > $firstRecordId AND post_id = $postId )" )
				->run();
		}
	}

	/**
	 * Removes the location column.
	 *
	 * @since 4.0.17
	 *
	 * @return void
	 */
	public function removeLocationColumn() {
		if ( aioseo()->core->db->columnExists( 'aioseo_posts', 'location' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				DROP location"
			);
		}
	}

	/**
	 * Clears the image data for WooCommerce Products so that we scan them again and include product gallery images.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	public function clearProductImages() {
		if ( ! aioseo()->helpers->isWooCommerceActive() ) {
			return;
		}

		aioseo()->core->db->update( 'aioseo_posts as ap' )
			->join( 'posts as p', 'ap.post_id = p.ID' )
			->where( 'p.post_type', 'product' )
			->set(
				[
					'images'          => null,
					'image_scan_date' => null
				]
			)
			->run();
	}

	/**
	 * Adds the new flag to the notifications table.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	public function addNotificationsNewColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_notifications', 'new' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_notifications';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				ADD new tinyint(1) NOT NULL DEFAULT 1 AFTER dismissed"
			);

			// Reset the cache for the installed tables.
			aioseo()->internalOptions->database->installedTables = '';

			aioseo()->core->db
				->update( 'aioseo_notifications' )
				->where( 'new', 1 )
				->set( 'new', 0 )
				->run();
		}
	}

	/**
	 * Noindexes the WooCommerce cart, checkout and account pages.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	public function noindexWooCommercePages() {
		if ( ! aioseo()->helpers->isWooCommerceActive() ) {
			return;
		}

		$cartId     = (int) get_option( 'woocommerce_cart_page_id' );
		$checkoutId = (int) get_option( 'woocommerce_checkout_page_id' );
		$accountId  = (int) get_option( 'woocommerce_myaccount_page_id' );

		$cartPage     = Models\Post::getPost( $cartId );
		$checkoutPage = Models\Post::getPost( $checkoutId );
		$accountPage  = Models\Post::getPost( $accountId );

		$newMeta = [
			'robots_default' => false,
			'robots_noindex' => true
		];

		if ( $cartPage->exists() ) {
			$cartPage->set( $newMeta );
			$cartPage->save();
		}
		if ( $checkoutPage->exists() ) {
			$checkoutPage->set( $newMeta );
			$checkoutPage->save();
		}
		if ( $accountPage->exists() ) {
			$accountPage->set( $newMeta );
			$accountPage->save();
		}
	}

	/**
	 * Adds the new capabilities for all the roles.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	protected function accessControlNewCapabilities() {
		aioseo()->access->addCapabilities();
	}

	/**
	 * Migrate dynamic settings to a separate options structure.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	protected function migrateDynamicSettings() {
		$rawOptions = $this->getRawOptions();
		$options    = aioseo()->dynamicOptions->noConflict();

		// Sitemap post type priorities/frequencies.
		if (
			! empty( $rawOptions['sitemap']['dynamic']['priority']['postTypes'] )
		) {
			foreach ( $rawOptions['sitemap']['dynamic']['priority']['postTypes'] as $postTypeName => $data ) {
				if ( $options->sitemap->priority->postTypes->has( $postTypeName ) ) {
					$options->sitemap->priority->postTypes->$postTypeName->priority  = $data['priority'];
					$options->sitemap->priority->postTypes->$postTypeName->frequency = $data['frequency'];
				}
			}
		}

		// Sitemap taxonomy priorities/frequencies.
		if (
			! empty( $rawOptions['sitemap']['dynamic']['priority']['taxonomies'] )
		) {
			foreach ( $rawOptions['sitemap']['dynamic']['priority']['taxonomies'] as $taxonomyName => $data ) {
				if ( $options->sitemap->priority->taxonomies->has( $taxonomyName ) ) {
					$options->sitemap->priority->taxonomies->$taxonomyName->priority  = $data['priority'];
					$options->sitemap->priority->taxonomies->$taxonomyName->frequency = $data['frequency'];
				}
			}
		}

		// Facebook post type object types.
		if (
			! empty( $rawOptions['social']['facebook']['general']['dynamic']['postTypes'] )
		) {
			foreach ( $rawOptions['social']['facebook']['general']['dynamic']['postTypes'] as $postTypeName => $data ) {
				if ( $options->social->facebook->general->postTypes->has( $postTypeName ) ) {
					$options->social->facebook->general->postTypes->$postTypeName->objectType = $data['objectType'];
				}
			}
		}

		// Search appearance post type data.
		if (
			! empty( $rawOptions['searchAppearance']['dynamic']['postTypes'] )
		) {
			foreach ( $rawOptions['searchAppearance']['dynamic']['postTypes'] as $postTypeName => $data ) {
				if ( $options->searchAppearance->postTypes->has( $postTypeName ) ) {
					$options->searchAppearance->postTypes->$postTypeName->show            = $data['show'];
					$options->searchAppearance->postTypes->$postTypeName->title           = $data['title'];
					$options->searchAppearance->postTypes->$postTypeName->metaDescription = $data['metaDescription'];
					$options->searchAppearance->postTypes->$postTypeName->schemaType      = $data['schemaType'];
					$options->searchAppearance->postTypes->$postTypeName->webPageType     = $data['webPageType'];
					$options->searchAppearance->postTypes->$postTypeName->articleType     = $data['articleType'];
					$options->searchAppearance->postTypes->$postTypeName->customFields    = $data['customFields'];

					// Advanced settings.
					$advanced = ! empty( $data['advanced']['robotsMeta'] ) ? $data['advanced']['robotsMeta'] : null;
					if ( ! empty( $advanced ) ) {
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->default         = $data['advanced']['robotsMeta']['default'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->noindex         = $data['advanced']['robotsMeta']['noindex'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->nofollow        = $data['advanced']['robotsMeta']['nofollow'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->noarchive       = $data['advanced']['robotsMeta']['noarchive'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->noimageindex    = $data['advanced']['robotsMeta']['noimageindex'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->notranslate     = $data['advanced']['robotsMeta']['notranslate'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->nosnippet       = $data['advanced']['robotsMeta']['nosnippet'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->noodp           = $data['advanced']['robotsMeta']['noodp'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->maxSnippet      = $data['advanced']['robotsMeta']['maxSnippet'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->maxVideoPreview = $data['advanced']['robotsMeta']['maxVideoPreview'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->robotsMeta->maxImagePreview = $data['advanced']['robotsMeta']['maxImagePreview'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->showDateInGooglePreview     = $data['advanced']['showDateInGooglePreview'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->showPostThumbnailInSearch   = $data['advanced']['showPostThumbnailInSearch'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->showMetaBox                 = $data['advanced']['showMetaBox'];
						$options->searchAppearance->postTypes->$postTypeName->advanced->bulkEditing                 = $data['advanced']['bulkEditing'];
					}

					if ( 'attachment' === $postTypeName ) {
						$options->searchAppearance->postTypes->$postTypeName->redirectAttachmentUrls = $data['redirectAttachmentUrls'];
					}
				}
			}
		}

		// Search appearance taxonomy data.
		if (
			! empty( $rawOptions['searchAppearance']['dynamic']['taxonomies'] )
		) {
			foreach ( $rawOptions['searchAppearance']['dynamic']['taxonomies'] as $taxonomyName => $data ) {
				if ( $options->searchAppearance->taxonomies->has( $taxonomyName ) ) {
					$options->searchAppearance->taxonomies->$taxonomyName->show            = $data['show'];
					$options->searchAppearance->taxonomies->$taxonomyName->title           = $data['title'];
					$options->searchAppearance->taxonomies->$taxonomyName->metaDescription = $data['metaDescription'];

					// Advanced settings.
					$advanced = ! empty( $data['advanced']['robotsMeta'] ) ? $data['advanced']['robotsMeta'] : null;
					if ( ! empty( $advanced ) ) {
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->default         = $data['advanced']['robotsMeta']['default'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->noindex         = $data['advanced']['robotsMeta']['noindex'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->nofollow        = $data['advanced']['robotsMeta']['nofollow'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->noarchive       = $data['advanced']['robotsMeta']['noarchive'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->noimageindex    = $data['advanced']['robotsMeta']['noimageindex'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->notranslate     = $data['advanced']['robotsMeta']['notranslate'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->nosnippet       = $data['advanced']['robotsMeta']['nosnippet'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->noodp           = $data['advanced']['robotsMeta']['noodp'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->maxSnippet      = $data['advanced']['robotsMeta']['maxSnippet'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->maxVideoPreview = $data['advanced']['robotsMeta']['maxVideoPreview'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->robotsMeta->maxImagePreview = $data['advanced']['robotsMeta']['maxImagePreview'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->showDateInGooglePreview     = $data['advanced']['showDateInGooglePreview'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->showPostThumbnailInSearch   = $data['advanced']['showPostThumbnailInSearch'];
						$options->searchAppearance->taxonomies->$taxonomyName->advanced->showMetaBox                 = $data['advanced']['showMetaBox'];
					}
				}
			}
		}
	}

	/**
	 * Fixes the default value for the post schema type.
	 *
	 * @since 4.1.5
	 *
	 * @return void
	 */
	private function fixSchemaTypeDefault() {
		if ( aioseo()->core->db->tableExists( 'aioseo_posts' ) && aioseo()->core->db->columnExists( 'aioseo_posts', 'schema_type' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				MODIFY schema_type varchar(20) DEFAULT 'default'"
			);
		}
	}

	/**
	 * Add in image with/height columns and image URL for caching.
	 *
	 * @since 4.1.6
	 *
	 * @return void
	 */
	protected function migrateOgTwitterImageColumns() {
		if ( aioseo()->core->db->tableExists( 'aioseo_posts' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';

			// OG Columns.
			if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'og_image_url' ) ) {
				aioseo()->core->db->execute(
					"ALTER TABLE {$tableName} ADD og_image_url text DEFAULT NULL AFTER og_image_type"
				);
			}

			if ( aioseo()->core->db->columnExists( 'aioseo_posts', 'og_custom_image_height' ) ) {
				aioseo()->core->db->execute(
					"ALTER TABLE {$tableName} CHANGE COLUMN og_custom_image_height og_image_height int(11) DEFAULT NULL AFTER og_image_url"
				);
			} elseif ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'og_image_height' ) ) {
				aioseo()->core->db->execute(
					"ALTER TABLE {$tableName} ADD og_image_height int(11) DEFAULT NULL AFTER og_image_url"
				);
			}

			if ( aioseo()->core->db->columnExists( 'aioseo_posts', 'og_custom_image_width' ) ) {
				aioseo()->core->db->execute(
					"ALTER TABLE {$tableName} CHANGE COLUMN og_custom_image_width og_image_width int(11) DEFAULT NULL AFTER og_image_url"
				);
			} elseif ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'og_image_width' ) ) {
				aioseo()->core->db->execute(
					"ALTER TABLE {$tableName} ADD og_image_width int(11) DEFAULT NULL AFTER og_image_url"
				);
			}

			// Twitter image url columnn.
			if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'twitter_image_url' ) ) {
				aioseo()->core->db->execute(
					"ALTER TABLE {$tableName} ADD twitter_image_url text DEFAULT NULL AFTER twitter_image_type"
				);
			}

			// Reset the cache for the installed tables.
			aioseo()->internalOptions->database->installedTables = '';
		}
	}

	/**
	 * Adds the limit modified date column to our posts table.
	 *
	 * @since 4.1.8
	 *
	 * @return void
	 */
	private function addLimitModifiedDateColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'limit_modified_date' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				ADD limit_modified_date tinyint(1) NOT NULL DEFAULT 0 AFTER local_seo"
			);

			// Reset the cache for the installed tables.
			aioseo()->internalOptions->database->installedTables = '';
		}
	}

	/**
	 * Fixes tags that should not be in the search appearance taxonomy options.
	 *
	 * @since 4.1.9
	 *
	 * @return void
	 */
	protected function fixTaxonomyTags() {
		$searchAppearanceTaxonomies = aioseo()->dynamicOptions->searchAppearance->taxonomies->all();

		$replaces = [
			'#breadcrumb_separator' => '#separator_sa',
			'#breadcrumb_'          => '#',
			'#blog_title'           => '#site_title'
		];

		foreach ( $searchAppearanceTaxonomies as $taxonomy => $searchAppearanceTaxonomy ) {
			aioseo()->dynamicOptions->searchAppearance->taxonomies->{$taxonomy}->title = str_replace(
				array_keys( $replaces ),
				array_values( $replaces ),
				$searchAppearanceTaxonomy['title']
			);

			aioseo()->dynamicOptions->searchAppearance->taxonomies->{$taxonomy}->metaDescription = str_replace(
				array_keys( $replaces ),
				array_values( $replaces ),
				$searchAppearanceTaxonomy['metaDescription']
			);
		}
	}

	/**
	 * Removes any AIOSEO Post records for revisions.
	 *
	 * @since 4.1.9
	 *
	 * @return void
	 */
	public function removeRevisionRecords() {
		$postsTableName       = aioseo()->core->db->prefix . 'posts';
		$aioseoPostsTableName = aioseo()->core->db->prefix . 'aioseo_posts';
		$limit                = 5000;

		aioseo()->core->db->execute(
			"DELETE FROM `$aioseoPostsTableName`
			WHERE `post_id` IN (
				SELECT `ID`
				FROM `$postsTableName`
				WHERE `post_parent` != 0
				AND `post_type` = 'revision'
				AND `post_status` = 'inherit'
			)
			LIMIT {$limit}"
		);

		// If the limit equals the amount of post IDs found, there might be more revisions left, so we need a new scan.
		if ( aioseo()->core->db->rowsAffected() === $limit ) {
			$this->scheduleRemoveRevisionsRecords();
		}
	}

	/**
	 * Enables the new shortcodes parsing setting if it was already enabled before as a deprecated setting.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	private function migrateDeprecatedRunShortcodesSetting() {
		if (
			in_array( 'runShortcodesInDescription', aioseo()->internalOptions->deprecatedOptions, true ) &&
			! aioseo()->options->deprecated->searchAppearance->advanced->runShortcodesInDescription
		) {
			return;
		}

		aioseo()->options->searchAppearance->advanced->runShortcodes = true;
	}

	/**
	 * Add options column.
	 *
	 * @since 4.2.2
	 *
	 * @return void
	 */
	private function addOptionsColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'options' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				ADD `options` longtext DEFAULT NULL AFTER `limit_modified_date`"
			);

			// Reset the cache for the installed tables.
			aioseo()->internalOptions->database->installedTables = '';
		}
	}

	/**
	 * Remove the tabs column as it is unnecessary.
	 *
	 * @since 4.2.2
	 *
	 * @return void
	 */
	protected function removeTabsColumn() {
		if ( aioseo()->core->db->columnExists( 'aioseo_posts', 'tabs' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				DROP tabs"
			);
		}
	}

	/**
	 * Migrates the user contact methods to the new format.
	 *
	 * @since 4.2.2
	 *
	 * @return void
	 */
	private function migrateUserContactMethods() {
		$userMetaTableName = aioseo()->core->db->db->usermeta;

		aioseo()->core->db->execute(
			"UPDATE `$userMetaTableName`
			SET `meta_key` = 'aioseo_facebook_page_url'
			WHERE `meta_key` = 'aioseo_facebook'"
		);

		aioseo()->core->db->execute(
			"UPDATE `$userMetaTableName`
			SET `meta_key` = 'aioseo_twitter_url'
			WHERE `meta_key` = 'aioseo_twitter'"
		);
	}

	/**
	 * Add an addon column to the notifications table.
	 *
	 * @since 4.2.4
	 *
	 * @return void
	 */
	private function addNotificationsAddonColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_notifications', 'addon' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_notifications';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				ADD `addon` varchar(64) DEFAULT NULL AFTER `slug`"
			);

			// Reset the cache for the installed tables.
			aioseo()->internalOptions->database->installedTables = '';
		}
	}

	/**
	 * Adds the schema column.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	private function addSchemaColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'schema' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				ADD `schema` longtext DEFAULT NULL AFTER `seo_score`"
			);
		}
	}

	/**
	 * Schedules the post schema migration.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	private function schedulePostSchemaMigration() {
		aioseo()->actionScheduler->scheduleSingle( 'aioseo_v4_migrate_post_schema', 10 );

		if ( ! aioseo()->core->cache->get( 'v4_migrate_post_schema_default_date' ) ) {
			aioseo()->core->cache->update( 'v4_migrate_post_schema_default_date', gmdate( 'Y-m-d H:i:s' ), 3 * MONTH_IN_SECONDS );
		}
	}

	/**
	 * Migrates then post schema to the new JSON column.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	public function migratePostSchema() {
		$posts = aioseo()->core->db->start( 'aioseo_posts' )
			->select( '*' )
			->whereRaw( '`schema` IS NULL' )
			->limit( 40 )
			->run()
			->models( 'AIOSEO\\Plugin\\Common\\Models\\Post' );

		if ( empty( $posts ) ) {
			return;
		}

		foreach ( $posts as $post ) {
			$this->migratePostSchemaHelper( $post );
		}

		// Once done, schedule the next action.
		aioseo()->actionScheduler->scheduleSingle( 'aioseo_v4_migrate_post_schema', 30, [], true );
	}

	/**
	 * Schedules the post schema migration to fix the default graphs.
	 *
	 * @since 4.2.6
	 *
	 * @return void
	 */
	private function schedulePostSchemaDefaultMigration() {
		aioseo()->actionScheduler->scheduleSingle( 'aioseo_v4_migrate_post_schema_default', 30 );
	}

	/**
	 * Migrates the post schema to the new JSON column again for posts using the default.
	 * This is needed to fix an oversight because in 4.2.5 we didn't migrate any properties set to the default graph.
	 *
	 * @since 4.2.6
	 *
	 * @return void
	 */
	public function migratePostSchemaDefault() {
		$migrationStartDate = aioseo()->core->cache->get( 'v4_migrate_post_schema_default_date' );
		if ( ! $migrationStartDate ) {
			return;
		}

		$posts = aioseo()->core->db->start( 'aioseo_posts' )
			->select( '*' )
			->where( 'schema_type =', 'default' )
			->whereRaw( "updated < '$migrationStartDate'" )
			->limit( 40 )
			->run()
			->models( 'AIOSEO\\Plugin\\Common\\Models\\Post' );

		if ( empty( $posts ) ) {
			aioseo()->core->cache->delete( 'v4_migrate_post_schema_default_date' );

			return;
		}

		foreach ( $posts as $post ) {
			$this->migratePostSchemaHelper( $post );
		}

		// Once done, schedule the next action.
		aioseo()->actionScheduler->scheduleSingle( 'aioseo_v4_migrate_post_schema_default', 30, [], true );
	}

	/**
	 * Helper function for the schema migration.
	 *
	 * @since  4.2.5
	 *
	 * @param  Models\Post $aioseoPost The AIOSEO post object.
	 * @return Models\Post             The modified AIOSEO post object.
	 */
	public function migratePostSchemaHelper( $aioseoPost ) {
		$post              = aioseo()->helpers->getPost( $aioseoPost->post_id );
		$schemaType        = $aioseoPost->schema_type;
		$schemaTypeOptions = json_decode( (string) $aioseoPost->schema_type_options );
		$schemaOptions     = Models\Post::getDefaultSchemaOptions( '', $post );

		if ( empty( $schemaTypeOptions ) ) {
			$aioseoPost->schema = $schemaOptions;
			$aioseoPost->save();

			return $aioseoPost;
		}

		// If the post is set to the default schema type, set the default for post type but then also get the properties.
		$isDefault = 'default' === $schemaType;
		if ( $isDefault ) {
			$dynamicOptions = aioseo()->dynamicOptions->noConflict();
			if ( ! empty( $post->post_type ) && $dynamicOptions->searchAppearance->postTypes->has( $post->post_type ) ) {
				$schemaOptions->default->graphName = $dynamicOptions->searchAppearance->postTypes->{$post->post_type}->schemaType;
				$schemaType                        = $dynamicOptions->searchAppearance->postTypes->{$post->post_type}->schemaType;
			}
		}

		$graph = [];
		switch ( $schemaType ) {
			case 'Article':
				$graph = [
					'id'         => '#aioseo-article-' . uniqid(),
					'slug'       => 'article',
					'graphName'  => 'Article',
					'label'      => __( 'Article', 'all-in-one-seo-pack' ),
					'properties' => [
						'type'        => ! empty( $schemaTypeOptions->article->articleType ) ? $schemaTypeOptions->article->articleType : 'Article',
						'name'        => '#post_title',
						'headline'    => '#post_title',
						'description' => '#post_excerpt',
						'image'       => '',
						'keywords'    => '',
						'author'      => [
							'name' => '#author_name',
							'url'  => '#author_url'
						],
						'dates'       => [
							'include'       => true,
							'datePublished' => '',
							'dateModified'  => ''
						]
					]
				];
				break;
			case 'Course':
				$graph = [
					'id'         => '#aioseo-course-' . uniqid(),
					'slug'       => 'course',
					'graphName'  => 'Course',
					'label'      => __( 'Course', 'all-in-one-seo-pack' ),
					'properties' => [
						'name'        => ! empty( $schemaTypeOptions->course->name ) ? $schemaTypeOptions->course->name : '#post_title',
						'description' => ! empty( $schemaTypeOptions->course->description ) ? $schemaTypeOptions->course->description : '#post_excerpt',
						'provider'    => [
							'name'  => ! empty( $schemaTypeOptions->course->provider ) ? $schemaTypeOptions->course->provider : '',
							'url'   => '',
							'image' => ''
						]
					]
				];
				break;
			case 'Product':
				$graph = [
					'id'         => '#aioseo-product-' . uniqid(),
					'slug'       => 'product',
					'graphName'  => 'Product',
					'label'      => __( 'Product', 'all-in-one-seo-pack' ),
					'properties' => [
						'autogenerate' => true,
						'name'         => '#post_title',
						'description'  => ! empty( $schemaTypeOptions->product->description ) ? $schemaTypeOptions->product->description : '#post_excerpt',
						'brand'        => ! empty( $schemaTypeOptions->product->brand ) ? $schemaTypeOptions->product->brand : '',
						'image'        => '',
						'identifiers'  => [
							'sku'  => ! empty( $schemaTypeOptions->product->sku ) ? $schemaTypeOptions->product->sku : '',
							'gtin' => '',
							'mpn'  => ''
						],
						'offer'        => [
							'price'        => ! empty( $schemaTypeOptions->product->price ) ? (float) $schemaTypeOptions->product->price : '',
							'currency'     => ! empty( $schemaTypeOptions->product->currency ) ? $schemaTypeOptions->product->currency : '',
							'availability' => ! empty( $schemaTypeOptions->product->availability ) ? $schemaTypeOptions->product->availability : '',
							'validUntil'   => ! empty( $schemaTypeOptions->product->priceValidUntil ) ? $schemaTypeOptions->product->priceValidUntil : ''
						],
						'rating'       => [
							'minimum' => 1,
							'maximum' => 5
						],
						'reviews'      => []
					]
				];

				$identifierType = ! empty( $schemaTypeOptions->product->identifierType ) ? $schemaTypeOptions->product->identifierType : '';
				$identifier     = ! empty( $schemaTypeOptions->product->identifier ) ? $schemaTypeOptions->product->identifier : '';
				if ( preg_match( '/gtin/i', (string) $identifierType ) ) {
					$graph['properties']['identifiers']['gtin'] = $identifier;
				}

				if ( preg_match( '/mpn/i', (string) $identifierType ) ) {
					$graph['properties']['identifiers']['mpn'] = $identifier;
				}

				$reviews = ! empty( $schemaTypeOptions->product->reviews ) ? $schemaTypeOptions->product->reviews : [];
				if ( ! empty( $reviews ) ) {
					foreach ( $reviews as $reviewData ) {
						$reviewData = json_decode( $reviewData );
						if ( empty( $reviewData ) ) {
							continue;
						}

						$graph['properties']['reviews'][] = [
							'rating'   => $reviewData->rating,
							'headline' => $reviewData->headline,
							'content'  => $reviewData->content,
							'author'   => $reviewData->author
						];
					}
				}
				break;
			case 'Recipe':
				$graph = [
					'id'         => '#aioseo-recipe-' . uniqid(),
					'slug'       => 'recipe',
					'graphName'  => 'Recipe',
					'label'      => __( 'Recipe', 'all-in-one-seo-pack' ),
					'properties' => [
						'name'         => ! empty( $schemaTypeOptions->recipe->name ) ? $schemaTypeOptions->recipe->name : '#post_title',
						'description'  => ! empty( $schemaTypeOptions->recipe->description ) ? $schemaTypeOptions->recipe->description : '#post_excerpt',
						'author'       => ! empty( $schemaTypeOptions->recipe->author ) ? $schemaTypeOptions->recipe->author : '#author_name',
						'ingredients'  => ! empty( $schemaTypeOptions->recipe->ingredients ) ? $schemaTypeOptions->recipe->ingredients : '',
						'dishType'     => ! empty( $schemaTypeOptions->recipe->dishType ) ? $schemaTypeOptions->recipe->dishType : '',
						'cuisineType'  => ! empty( $schemaTypeOptions->recipe->cuisineType ) ? $schemaTypeOptions->recipe->cuisineType : '',
						'keywords'     => ! empty( $schemaTypeOptions->recipe->keywords ) ? $schemaTypeOptions->recipe->keywords : '',
						'image'        => ! empty( $schemaTypeOptions->recipe->image ) ? $schemaTypeOptions->recipe->image : '',
						'nutrition'    => [
							'servings' => ! empty( $schemaTypeOptions->recipe->servings ) ? $schemaTypeOptions->recipe->servings : '',
							'calories' => ! empty( $schemaTypeOptions->recipe->calories ) ? $schemaTypeOptions->recipe->calories : ''
						],
						'timeRequired' => [
							'preparation' => ! empty( $schemaTypeOptions->recipe->preparationTime ) ? $schemaTypeOptions->recipe->preparationTime : '',
							'cooking'     => ! empty( $schemaTypeOptions->recipe->cookingTime ) ? $schemaTypeOptions->recipe->cookingTime : ''
						],
						'instructions' => [],
						'rating'       => [
							'minimum' => 1,
							'maximum' => 5
						],
						'reviews'      => []
					]
				];

				$instructions = ! empty( $schemaTypeOptions->recipe->instructions ) ? $schemaTypeOptions->recipe->instructions : [];
				if ( ! empty( $instructions ) ) {
					foreach ( $instructions as $instructionData ) {
						$instructionData = json_decode( $instructionData );
						if ( empty( $instructionData ) ) {
							continue;
						}

						$graph['properties']['instructions'][] = [
							'name'  => '',
							'text'  => $instructionData->content,
							'image' => ''
						];
					}
				}

				$reviews = ! empty( $schemaTypeOptions->recipe->reviews ) ? $schemaTypeOptions->recipe->reviews : [];
				if ( ! empty( $reviews ) ) {
					foreach ( $reviews as $reviewData ) {
						$reviewData = json_decode( $reviewData );
						if ( empty( $reviewData ) ) {
							continue;
						}

						$graph['properties']['reviews'][] = [
							'rating'   => $reviewData->rating,
							'headline' => $reviewData->headline,
							'content'  => $reviewData->content,
							'author'   => $reviewData->author
						];
					}
				}
				break;
			case 'SoftwareApplication':
				$graph = [
					'id'         => '#aioseo-software-application-' . uniqid(),
					'slug'       => 'software-application',
					'graphName'  => 'SoftwareApplication',
					'label'      => __( 'Software', 'all-in-one-seo-pack' ),
					'properties' => [
						'name'            => ! empty( $schemaTypeOptions->software->name ) ? $schemaTypeOptions->software->name : '#post_title',
						'description'     => '#post_excerpt',
						'price'           => ! empty( $schemaTypeOptions->software->price ) ? (float) $schemaTypeOptions->software->price : '',
						'currency'        => ! empty( $schemaTypeOptions->software->currency ) ? $schemaTypeOptions->software->currency : '',
						'operatingSystem' => ! empty( $schemaTypeOptions->software->operatingSystems ) ? $schemaTypeOptions->software->operatingSystems : '',
						'category'        => ! empty( $schemaTypeOptions->software->category ) ? $schemaTypeOptions->software->category : '',
						'rating'          => [
							'value'   => '',
							'minimum' => 1,
							'maximum' => 5
						],
						'review'          => [
							'headline' => '',
							'content'  => '',
							'author'   => ''
						]
					]
				];

				$reviews = ! empty( $schemaTypeOptions->software->reviews ) ? $schemaTypeOptions->software->reviews : [];
				if ( ! empty( $reviews[0] ) ) {
					$reviewData = json_decode( $reviews[0] );
					if ( empty( $reviewData ) ) {
						break;
					}

					$graph['properties']['rating']['value'] = $reviewData->rating;
					$graph['properties']['review'] = [
						'headline' => $reviewData->headline,
						'content'  => $reviewData->content,
						'author'   => $reviewData->author
					];
				}
				break;
			case 'WebPage':
				if ( 'FAQPage' === $schemaTypeOptions->webPage->webPageType ) {
					$graph = [
						'id'         => '#aioseo-faq-page-' . uniqid(),
						'slug'       => 'faq-page',
						'graphName'  => 'FAQPage',
						'label'      => __( 'FAQ Page', 'all-in-one-seo-pack' ),
						'properties' => [
							'type'        => $schemaTypeOptions->webPage->webPageType,
							'name'        => '#post_title',
							'description' => '#post_excerpt',
							'questions'   => []
						]
					];

					$faqs = $schemaTypeOptions->faq->pages;
					if ( ! empty( $faqs ) ) {
						foreach ( $faqs as $faqData ) {
							$faqData = json_decode( $faqData );
							if ( empty( $faqData ) ) {
								continue;
							}

							$graph['properties']['questions'][] = [
								'question' => $faqData->question,
								'answer'   => $faqData->answer
							];
						}
					}
				} else {
					$graph = [
						'id'         => '#aioseo-web-page-' . uniqid(),
						'slug'       => 'web-page',
						'graphName'  => 'WebPage',
						'label'      => __( 'Web Page', 'all-in-one-seo-pack' ),
						'properties' => [
							'type'        => $schemaTypeOptions->webPage->webPageType,
							'name'        => '',
							'description' => ''
						]
					];
				}
				break;
			case 'default':
				$dynamicOptions = aioseo()->dynamicOptions->noConflict();
				if ( ! empty( $post->post_type ) && $dynamicOptions->searchAppearance->postTypes->has( $post->post_type ) ) {
					$schemaOptions->defaultGraph = $dynamicOptions->searchAppearance->postTypes->{$post->post_type}->schemaType;
				}
				break;
			case 'none':
				// If "none', we simply don't have to migrate anything.
			default:
				break;
		}

		if ( ! empty( $graph ) ) {
			if ( $isDefault ) {
				$schemaOptions->default->data->{$schemaType} = $graph;
			} else {
				$schemaOptions->graphs[]           = $graph;
				$schemaOptions->default->isEnabled = false;
			}
		}

		$aioseoPost->schema = $schemaOptions;
		$aioseoPost->save();

		return $aioseoPost;
	}

	/**
	 * Updates the dashboardWidgets with the new array format.
	 *
	 * @since 4.2.8
	 *
	 * @return void
	 */
	private function migrateDashboardWidgetsOptions() {
		$rawOptions = $this->getRawOptions();

		if ( empty( $rawOptions ) || ! is_bool( $rawOptions['advanced']['dashboardWidgets'] ) ) {
			return;
		}

		$widgets = [ 'seoNews' ];

		// If the dashboardWidgets was activated, let's turn on the other widgets.
		if ( ! empty( $rawOptions['advanced']['dashboardWidgets'] ) ) {
			$widgets[] = 'seoOverview';
			$widgets[] = 'seoSetup';
		}

		aioseo()->options->advanced->dashboardWidgets = $widgets;
	}

	/**
	 * Adds the primary_term column to the aioseo_posts table.
	 *
	 * @since 4.3.6
	 *
	 * @return void
	 */
	private function addPrimaryTermColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'primary_term' ) ) {
			$tableName = aioseo()->core->db->db->prefix . 'aioseo_posts';
			aioseo()->core->db->execute(
				"ALTER TABLE {$tableName}
				ADD `primary_term` longtext DEFAULT NULL AFTER `page_analysis`"
			);
		}
	}

	/**
	 * Schedules the revision records removal.
	 *
	 * @since 4.3.1
	 *
	 * @return void
	 */
	private function scheduleRemoveRevisionsRecords() {
		aioseo()->actionScheduler->scheduleSingle( 'aioseo_v419_remove_revision_records', 10, [], true );
	}

	/**
	 * Casts the priority column to a float.
	 *
	 * @since 4.3.9
	 *
	 * @return void
	 */
	private function migratePriorityColumn() {
		if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'priority' ) ) {
			return;
		}

		$prefix               = aioseo()->core->db->prefix;
		$aioseoPostsTableName = $prefix . 'aioseo_posts';

		// First, cast the default value to NULL since it's a string.
		aioseo()->core->db->execute( "UPDATE {$aioseoPostsTableName} SET priority = NULL WHERE priority = 'default'" );

		// Then, alter the column to a float.
		aioseo()->core->db->execute( "ALTER TABLE {$aioseoPostsTableName} MODIFY priority float" );
	}

	/**
	 * Update the custom robots.txt rules to the new format,
	 * by replacing `rule` and `directoryPath` with `directive` and `fieldValue`, respectively.
	 *
	 * @since 4.4.2
	 *
	 * @return void
	 */
	private function updateRobotsTxtRules() {
		$rawOptions   = $this->getRawOptions();
		$currentRules = $rawOptions && ! empty( $rawOptions['tools']['robots']['rules'] )
			? $rawOptions['tools']['robots']['rules']
			: [];
		if ( empty( $currentRules ) || ! is_array( $currentRules ) ) {
			return;
		}

		$newRules = [];
		foreach ( $currentRules as $oldRule ) {
			$parsedRule = json_decode( $oldRule, true );
			if ( empty( $parsedRule['rule'] ) && empty( $parsedRule['directoryPath'] ) ) {
				continue;
			}

			$newRule = [
				'userAgent'  => array_key_exists( 'userAgent', $parsedRule ) ? $parsedRule['userAgent'] : '',
				'directive'  => array_key_exists( 'rule', $parsedRule ) ? $parsedRule['rule'] : '',
				'fieldValue' => array_key_exists( 'directoryPath', $parsedRule ) ? $parsedRule['directoryPath'] : '',
			];

			$newRules[] = wp_json_encode( $newRule );
		}

		if ( $newRules ) {
			aioseo()->options->tools->robots->rules = $newRules;
		}
	}

	/**
	 * Checks if the user is currently using the old GA Analytics v3 integration and create a notification.
	 *
	 * @since 4.5.1
	 *
	 * @return void
	 */
	private function checkForGaAnalyticsV3() {
		// If either MonsterInsights or ExactMetrics is active, let's return early.
		$pluginData = aioseo()->helpers->getPluginData();
		if (
			$pluginData['miPro']['activated'] ||
			$pluginData['miLite']['activated'] ||
			$pluginData['emPro']['activated'] ||
			$pluginData['emLite']['activated']
		) {
			return;
		}

		$rawOptions = $this->getRawOptions();
		if ( empty( $rawOptions['deprecated']['webmasterTools']['googleAnalytics']['id'] ) ) {
			return;
		}

		// Let's clear the notification if the search is working again.
		$notification = Models\Notification::getNotificationByName( 'google-analytics-v3-deprecation' );
		if ( $notification->exists() ) {
			$notification->dismissed = false;
			$notification->save();

			return;
		}

		// Determine which plugin name to use.
		$pluginName = 'MonsterInsights';
		if (
			(
				$pluginData['emPro']['installed'] ||
				$pluginData['emLite']['installed']
			) &&
			! $pluginData['miPro']['installed'] &&
			! $pluginData['miLite']['installed']
		) {
			$pluginName = 'ExactMetrics';
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'google-analytics-v3-deprecation',
			'title'             => __( 'Universal Analytics V3 Deprecation Notice', 'all-in-one-seo-pack' ),
			'content'           => sprintf(
				// Translators: 1 - Line break HTML tags, 2 - Plugin short name ("AIOSEO"), Analytics plugin name (e.g. "MonsterInsights").
				__( 'You have been using the %2$s Google Analytics V3 (Universal Analytics) integration which has been deprecated by Google and is no longer supported. This may affect your website\'s data accuracy and performance.%1$sTo ensure a seamless analytics experience, we recommend migrating to %3$s, a powerful analytics solution.%1$s%3$s offers advanced features such as real-time tracking, enhanced e-commerce analytics, and easy-to-understand reports, helping you make informed decisions to grow your online presence effectively.%1$sClick the button below to be redirected to the %3$s setup process, where you can start benefiting from its robust analytics capabilities immediately.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				'<br><br>',
				AIOSEO_PLUGIN_SHORT_NAME,
				$pluginName
			),
			'type'              => 'error',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Fix Now', 'all-in-one-seo-pack' ),
			'button1_action'    => admin_url( 'admin.php?page=aioseo-monsterinsights' ),
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Adds our custom tables for the query arg monitor.
	 *
	 * @since 4.5.8
	 *
	 * @return void
	 */
	public function addQueryArgMonitorTables() {
		$db             = aioseo()->core->db->db;
		$charsetCollate = '';

		if ( ! empty( $db->charset ) ) {
			$charsetCollate .= "DEFAULT CHARACTER SET {$db->charset}";
		}
		if ( ! empty( $db->collate ) ) {
			$charsetCollate .= " COLLATE {$db->collate}";
		}

		// Check for crawl cleanup logs table.
		if ( ! aioseo()->core->db->tableExists( 'aioseo_crawl_cleanup_logs' ) ) {
			$tableName = $db->prefix . 'aioseo_crawl_cleanup_logs';

			aioseo()->core->db->execute(
				"CREATE TABLE {$tableName} (
					`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					`slug` text NOT NULL,
					`key` text NOT NULL,
					`value` text,
					`hash` varchar(40) NOT NULL,
					`hits` int(20) NOT NULL DEFAULT 1,
					`created` datetime NOT NULL,
					`updated` datetime NOT NULL,
					PRIMARY KEY (id),
					UNIQUE KEY ndx_aioseo_crawl_cleanup_logs_hash (hash)
				) {$charsetCollate};"
			);
		}

		// Check for crawl cleanup blocked table.
		if ( ! aioseo()->core->db->tableExists( 'aioseo_crawl_cleanup_blocked_args' ) ) {
			$tableName = $db->prefix . 'aioseo_crawl_cleanup_blocked_args';

			aioseo()->core->db->execute(
				"CREATE TABLE {$tableName} (
					`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					`key` text,
					`value` text,
					`key_value_hash` varchar(40),
					`regex` varchar(150),
					`hits` int(20) NOT NULL DEFAULT 0,
					`created` datetime NOT NULL,
					`updated` datetime NOT NULL,
					PRIMARY KEY (id),
					UNIQUE KEY ndx_aioseo_crawl_cleanup_blocked_args_key_value_hash (key_value_hash),
					UNIQUE KEY ndx_aioseo_crawl_cleanup_blocked_args_regex (regex)
				) {$charsetCollate};"
			);
		}
	}

	/**
	 * Adds a notification for the query arg monitor.
	 *
	 * @since 4.5.8
	 *
	 * @return void
	 */
	private function addQueryArgMonitorNotification() {
		$options = $this->getRawOptions();
		if (
			empty( $options['searchAppearance']['advanced']['crawlCleanup']['enable'] ) ||
			empty( $options['searchAppearance']['advanced']['crawlCleanup']['removeUnrecognizedQueryArgs'] )
		) {
			return;
		}

		$notification = Models\Notification::getNotificationByName( 'crawl-cleanup-updated' );
		if ( $notification->exists() ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'crawl-cleanup-updated',
			'title'             => __( 'Crawl Cleanup changes you should know about', 'all-in-one-seo-pack' ),
			'content'           => __( 'We\'ve made some significant changes to how we monitor Query Args for our Crawl Cleanup feature. Instead of DISABLING all query args and requiring you to add individual exceptions, we\'ve now changed it to ALLOW all query args by default with the option to easily block unrecognized ones through our new log table.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'type'              => 'info',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Learn More', 'all-in-one-seo-pack' ),
			'button1_action'    => 'http://route#aioseo-search-appearance&aioseo-scroll=aioseo-query-arg-monitoring&aioseo-highlight=aioseo-query-arg-monitoring:advanced',
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Deprecates the "No Pagination for Canonical URLs" setting.
	 *
	 * @since 4.5.9
	 *
	 * @return void
	 */
	public function deprecateNoPaginationForCanonicalUrlsSetting() {
		$options = $this->getRawOptions();
		if ( empty( $options['searchAppearance']['advanced']['noPaginationForCanonical'] ) ) {
			return;
		}

		$deprecatedOptions = aioseo()->internalOptions->deprecatedOptions;
		if ( ! in_array( 'noPaginationForCanonical', $deprecatedOptions, true ) ) {
			$deprecatedOptions[]                         = 'noPaginationForCanonical';
			aioseo()->internalOptions->deprecatedOptions = $deprecatedOptions;
		}

		aioseo()->options->deprecated->searchAppearance->advanced->noPaginationForCanonical = true;
	}

	/**
	 * Deprecates the "Breadcrumbs enabled" setting.
	 *
	 * @since 4.6.5
	 *
	 * @return void
	 */
	public function deprecateBreadcrumbsEnabledSetting() {
		$options = $this->getRawOptions();
		if ( ! isset( $options['breadcrumbs']['enable'] ) || 1 === intval( $options['breadcrumbs']['enable'] ) ) {
			return;
		}

		$deprecatedOptions = aioseo()->internalOptions->deprecatedOptions;
		if ( ! in_array( 'breadcrumbsEnable', $deprecatedOptions, true ) ) {
			$deprecatedOptions[]                         = 'breadcrumbsEnable';
			aioseo()->internalOptions->deprecatedOptions = $deprecatedOptions;
		}

		aioseo()->options->deprecated->breadcrumbs->enable = false;
	}

	/**
	 * Add tables for Writing Assistant.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	private function addWritingAssistantTables() {
		$db             = aioseo()->core->db->db;
		$charsetCollate = '';

		if ( ! empty( $db->charset ) ) {
			$charsetCollate .= "DEFAULT CHARACTER SET {$db->charset}";
		}
		if ( ! empty( $db->collate ) ) {
			$charsetCollate .= " COLLATE {$db->collate}";
		}

		if ( ! aioseo()->core->db->tableExists( 'aioseo_writing_assistant_posts' ) ) {
			$tableName = $db->prefix . 'aioseo_writing_assistant_posts';

			aioseo()->core->db->execute(
				"CREATE TABLE {$tableName} (
					`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					`post_id` bigint(20) unsigned DEFAULT NULL,
					`keyword_id` bigint(20) unsigned DEFAULT NULL,
					`content_analysis_hash` VARCHAR(40) DEFAULT NULL,
					`content_analysis` text DEFAULT NULL,
					`created` datetime NOT NULL,
					`updated` datetime NOT NULL,
					PRIMARY KEY (id),
					UNIQUE KEY ndx_aioseo_writing_assistant_posts_post_id (post_id),
					KEY ndx_aioseo_writing_assistant_posts_keyword_id (keyword_id)
				) {$charsetCollate};"
			);
		}

		if ( ! aioseo()->core->db->tableExists( 'aioseo_writing_assistant_keywords' ) ) {
			$tableName = $db->prefix . 'aioseo_writing_assistant_keywords';

			aioseo()->core->db->execute(
				"CREATE TABLE {$tableName} (
					`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					`uuid` varchar(40) NOT NULL,
					`keyword` varchar(255) NOT NULL,
					`country` varchar(10) NOT NULL DEFAULT 'us',
					`language` varchar(10) NOT NULL DEFAULT 'en',
					`progress` tinyint(3) DEFAULT 0,
					`keywords` mediumtext NULL,
					`competitors` mediumtext NULL,
					`created` datetime NOT NULL,
					`updated` datetime NOT NULL,
					PRIMARY KEY (id),
					UNIQUE KEY ndx_aioseo_writing_assistant_keywords_uuid (uuid),
					KEY ndx_aioseo_writing_assistant_keywords_keyword (keyword)
				) {$charsetCollate};"
			);
		}
	}

	/**
	 * Cancels all outstanding sitemap ping actions.
	 * This is needed because we've removed the Ping class.
	 *
	 * @since 4.7.5
	 *
	 * @return void
	 */
	private function cancelScheduledSitemapPings() {
		as_unschedule_all_actions( 'aioseo_sitemap_ping' );
		as_unschedule_all_actions( 'aioseo_sitemap_ping_recurring' );
	}

	/**
	 * Disable email reports.
	 *
	 * @since 4.7.7
	 *
	 * @return void
	 */
	private function disableEmailReports() {
		aioseo()->options->advanced->emailSummary->enable = false;

		// Schedule a notification to remind the user to enable email reports in 2 weeks.
		aioseo()->actionScheduler->scheduleSingle( 'aioseo_email_reports_enable_reminder', 2 * WEEK_IN_SECONDS );
	}

	/**
	 * Cancels all occurrences of the report summary task.
	 * This is needed in order to force the scheduled date to be reset.
	 *
	 * @since 4.7.9
	 *
	 * @return void
	 */
	private function rescheduleEmailReport() {
		as_unschedule_all_actions( aioseo()->emailReports->summary->actionHook );
	}

	/**
	 * Fixes headlines that could not be analyzed.
	 *
	 * @since 4.7.9
	 *
	 * @return void
	 */
	private function fixSavedHeadlines() {
		$headlines = aioseo()->internalOptions->internal->headlineAnalysis->headlines;
		if ( empty( $headlines ) ) {
			return;
		}

		foreach ( $headlines as $key => $headline ) {
			if ( ! json_decode( $headline ) ) {
				unset( $headlines[ $key ] );
			}
		}

		aioseo()->internalOptions->internal->headlineAnalysis->headlines = $headlines;
	}

	/**
	 * Resets the image scan date in order to force a new scan.
	 * This is needed because we're now storing relative URLs in order to support site migrations.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	private function resetImageScanDate() {
		aioseo()->core->db->update( 'aioseo_posts' )
			->set(
				[
					'image_scan_date' => null
				]
			)
			->run();
	}

	/**
	 * Adds our custom table for the SeoAnalysis/SeoAnalyzer homepage and competitor results.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	private function addSeoAnalyzerResultsTable() {
		$db             = aioseo()->core->db->db;
		$charsetCollate = '';

		if ( ! empty( $db->charset ) ) {
			$charsetCollate .= "DEFAULT CHARACTER SET {$db->charset}";
		}
		if ( ! empty( $db->collate ) ) {
			$charsetCollate .= " COLLATE {$db->collate}";
		}

		// Check for seo analyzer results table.
		if ( ! aioseo()->core->db->tableExists( 'aioseo_seo_analyzer_results' ) ) {
			$tableName = $db->prefix . 'aioseo_seo_analyzer_results';

			aioseo()->core->db->execute(
				"CREATE TABLE {$tableName} (
					`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
					`data` text NOT NULL,
					`score` varchar(255),
					`competitor_url` varchar(255),
					`created` datetime NOT NULL,
					`updated` datetime NOT NULL,
					PRIMARY KEY (id),
					KEY ndx_aioseo_seo_analyzer_results_competitor_url (competitor_url)
				) {$charsetCollate};"
			);

			// Reset the cache for the installed tables.
			aioseo()->internalOptions->database->installedTables = '';
		}
	}

	/**
	 * Migrate the SeoAnalyzer homepage results from the Internal Optinos to the new table.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	private function migrateSeoAnalyzerResults() {
		$internalOptions = $this->getRawInternalOptions();
		$results         = ! empty( $internalOptions['internal']['siteAnalysis']['results'] ) ? $internalOptions['internal']['siteAnalysis']['results'] : [];
		if ( empty( $results ) ) {
			return;
		}

		$parsedData = [
			'results' => is_string( $results ) ? json_decode( $results, true ) : $results,
			'score'   => $internalOptions['internal']['siteAnalysis']['score'],
		];

		Models\SeoAnalyzerResult::addResults( $parsedData );

		aioseo()->core->cache->delete( 'analyze_site_code' );
		aioseo()->core->cache->delete( 'analyze_site_body' );
	}

	/**
	 * Migrate the SeoAnalyzer competitors results from the Internal Optinos to the new table.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	private function migrateSeoAnalyzerCompetitors() {
		$internalOptions = $this->getRawInternalOptions();
		$competitors     = ! empty( $internalOptions['internal']['siteAnalysis']['competitors'] ) ? $internalOptions['internal']['siteAnalysis']['competitors'] : [];
		if ( empty( $competitors ) ) {
			return;
		}

		foreach ( $competitors as $url => $competitor ) {
			$parsedData = is_string( $competitor ) ? json_decode( $competitor, true ) : $competitor;
			$results    = empty( $parsedData['results'] ) ? [] : $parsedData['results'];
			if ( empty( $results ) ) {
				continue;
			}

			Models\SeoAnalyzerResult::addResults( [
				'results' => $results,
				'score'   => $parsedData['score'],
			], $url );
		}

		aioseo()->core->cache->delete( 'analyze_site_code' );
		aioseo()->core->cache->delete( 'analyze_site_body' );
	}

	/**
	 * Returns the raw options from the database.
	 *
	 * @since 4.8.3
	 *
	 * @return array
	 */
	private function getRawInternalOptions() {
		// Options from the DB.
		$internalOptions = json_decode( get_option( aioseo()->internalOptions->optionsName ), true );
		if ( empty( $internalOptions ) ) {
			$internalOptions = [];
		}

		return $internalOptions;
	}
}Common/ImportExport/RankMath/Sitemap.php000066600000011643151135505570014310 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\RankMath;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the sitemap settings.
 *
 * @since 4.0.0
 */
class Sitemap {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->options = get_option( 'rank-math-options-sitemap' );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->migrateIncludedObjects();
		$this->migrateIncludeImages();
		$this->migrateExcludedPosts();
		$this->migrateExcludedTerms();

		$settings = [
			'items_per_page' => [ 'type' => 'string', 'newOption' => [ 'sitemap', 'general', 'linksPerIndex' ] ],
		];

		aioseo()->options->sitemap->general->indexes = true;
		aioseo()->importExport->rankMath->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the included post types and taxonomies.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateIncludedObjects() {
		$includedPostTypes  = [];
		$includedTaxonomies = [];

		$allowedPostTypes = array_values( array_diff( aioseo()->helpers->getPublicPostTypes( true ), aioseo()->helpers->getNoindexedPostTypes() ) );
		foreach ( $allowedPostTypes as $postType ) {
			foreach ( $this->options as $name => $value ) {
				if ( preg_match( "#pt_{$postType}_sitemap$#", (string) $name, $match ) && 'on' === $this->options[ $name ] ) {
					$includedPostTypes[] = $postType;
				}
			}
		}

		$allowedTaxonomies = array_values( array_diff( aioseo()->helpers->getPublicTaxonomies( true ), aioseo()->helpers->getNoindexedTaxonomies() ) );
		foreach ( $allowedTaxonomies as $taxonomy ) {
			foreach ( $this->options as $name => $value ) {
				if ( preg_match( "#tax_{$taxonomy}_sitemap$#", (string) $name, $match ) && 'on' === $this->options[ $name ] ) {
					$includedTaxonomies[] = $taxonomy;
				}
			}
		}

		aioseo()->options->sitemap->general->postTypes->included = $includedPostTypes;
		if ( count( $allowedPostTypes ) !== count( $includedPostTypes ) ) {
			aioseo()->options->sitemap->general->postTypes->all = false;
		}

		aioseo()->options->sitemap->general->taxonomies->included = $includedTaxonomies;
		if ( count( $allowedTaxonomies ) !== count( $includedTaxonomies ) ) {
			aioseo()->options->sitemap->general->taxonomies->all = false;
		}
	}

	/**
	 * Migrates the Redirect Attachments setting.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateIncludeImages() {
		if ( ! empty( $this->options['include_images'] ) ) {
			if ( 'off' === $this->options['include_images'] ) {
				aioseo()->options->sitemap->general->advancedSettings->enable        = true;
				aioseo()->options->sitemap->general->advancedSettings->excludeImages = true;
			}
		}
	}

	/**
	 * Migrates the posts that are excluded from the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateExcludedPosts() {
		if ( empty( $this->options['exclude_posts'] ) ) {
			return;
		}

		$rmExcludedPosts = array_filter( explode( ',', $this->options['exclude_posts'] ) );
		$excludedPosts   = aioseo()->options->sitemap->general->advancedSettings->excludePosts;

		if ( count( $rmExcludedPosts ) ) {
			foreach ( $rmExcludedPosts as $rmExcludedPost ) {
				$post = get_post( trim( $rmExcludedPost ) );
				if ( ! is_object( $post ) ) {
					continue;
				}

				$excludedPost        = new \stdClass();
				$excludedPost->value = $post->ID;
				$excludedPost->type  = $post->post_type;
				$excludedPost->label = $post->post_title;
				$excludedPost->link  = get_permalink( $post->ID );

				array_push( $excludedPosts, wp_json_encode( $excludedPost ) );
			}
			aioseo()->options->sitemap->general->advancedSettings->enable = true;
		}
		aioseo()->options->sitemap->general->advancedSettings->excludePosts = $excludedPosts;
	}

	/**
	 * Migrates the terms that are excluded from the sitemap.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateExcludedTerms() {
		if ( empty( $this->options['exclude_terms'] ) ) {
			return;
		}

		$rmExcludedTerms = array_filter( explode( ',', $this->options['exclude_terms'] ) );
		$excludedTerms   = aioseo()->options->sitemap->general->advancedSettings->excludeTerms;

		if ( count( $rmExcludedTerms ) ) {
			foreach ( $rmExcludedTerms as $rmExcludedTerm ) {
				$term = get_term( trim( $rmExcludedTerm ) );
				if ( ! is_object( $term ) ) {
					continue;
				}

				$excludedTerm        = new \stdClass();
				$excludedTerm->value = $term->term_id;
				$excludedTerm->type  = $term->taxonomy;
				$excludedTerm->label = $term->name;
				$excludedTerm->link  = get_term_link( $term );

				array_push( $excludedTerms, wp_json_encode( $excludedTerm ) );
			}
			aioseo()->options->sitemap->general->advancedSettings->enable = true;
		}
		aioseo()->options->sitemap->general->advancedSettings->excludeTerms = $excludedTerms;
	}
}Common/ImportExport/RankMath/RankMath.php000066600000002063151135505570014407 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\RankMath;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\ImportExport;

class RankMath extends ImportExport\Importer {
	/**
	 * A list of plugins to look for to import.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $plugins = [
		[
			'name'     => 'Rank Math SEO',
			'version'  => '1.0',
			'basename' => 'seo-by-rank-math/rank-math.php',
			'slug'     => 'rank-math-seo'
		]
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 *
	 * @param ImportExport\ImportExport $importer the ImportExport class.
	 */
	public function __construct( $importer ) {
		$this->helpers  = new Helpers();
		$this->postMeta = new PostMeta();

		$plugins = $this->plugins;
		foreach ( $plugins as $key => $plugin ) {
			$plugins[ $key ]['class'] = $this;
		}
		$importer->addPlugins( $plugins );
	}


	/**
	 * Imports the settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function importSettings() {
		new GeneralSettings();
		new TitleMeta();
		new Sitemap();
	}
}Common/ImportExport/RankMath/TitleMeta.php000066600000047334151135505570014604 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\RankMath;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\ImportExport;
use AIOSEO\Plugin\Common\Models;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Search Appearance settings.
 *
 * @since 4.0.0
 */
class TitleMeta extends ImportExport\SearchAppearance {
	/**
	 * Our robot meta settings.
	 *
	 * @since 4.0.0
	 */
	private $robotMetaSettings = [
		'noindex',
		'nofollow',
		'noarchive',
		'noimageindex',
		'nosnippet'
	];

	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->options = get_option( 'rank-math-options-titles' );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->migrateHomePageSettings();
		$this->migratePostTypeSettings();
		$this->migratePostTypeArchiveSettings();
		$this->migrateArchiveSettings();
		$this->migrateRobotMetaSettings();
		$this->migrateKnowledgeGraphSettings();
		$this->migrateSocialMetaSettings();

		$settings = [
			'title_separator' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'separator' ] ],
		];

		aioseo()->importExport->rankMath->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the homepage settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateHomePageSettings() {
		if ( isset( $this->options['homepage_title'] ) ) {
			aioseo()->options->searchAppearance->global->siteTitle =
				aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options['homepage_title'] ) );
		}

		if ( isset( $this->options['homepage_description'] ) ) {
			aioseo()->options->searchAppearance->global->metaDescription =
				aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options['homepage_description'] ) );
		}

		if ( isset( $this->options['homepage_facebook_title'] ) ) {
			aioseo()->options->social->facebook->homePage->title = aioseo()->helpers->sanitizeOption( $this->options['homepage_facebook_title'] );
		}

		if ( isset( $this->options['homepage_facebook_description'] ) ) {
			aioseo()->options->social->facebook->homePage->description = aioseo()->helpers->sanitizeOption( $this->options['homepage_facebook_description'] );
		}

		if ( isset( $this->options['homepage_facebook_image'] ) ) {
			aioseo()->options->social->facebook->homePage->image = esc_url( $this->options['homepage_facebook_image'] );
		}
	}

	/**
	 * Migrates the archive settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateArchiveSettings() {
		$archives = [
			'author',
			'date'
		];

		foreach ( $archives as $archive ) {
			// Reset existing values first.
			foreach ( $this->robotMetaSettings as $robotsMetaName ) {
				aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->$robotsMetaName = false;
			}

			if ( isset( $this->options[ "disable_{$archive}_archives" ] ) ) {
				aioseo()->options->searchAppearance->archives->$archive->show                          = 'off' === $this->options[ "disable_{$archive}_archives" ];
				aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->default = 'on' === $this->options[ "disable_{$archive}_archives" ];
				aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->noindex = 'on' === $this->options[ "disable_{$archive}_archives" ];
			}

			if ( isset( $this->options[ "{$archive}_archive_title" ] ) ) {
				$value = aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options[ "{$archive}_archive_title" ], 'archive' ) );
				if ( 'date' !== $archive ) {
					// Archive Title tag needs to be stripped since we don't support it for author archives.
					$value = aioseo()->helpers->pregReplace( '/#archive_title/', '', $value );
				}
				aioseo()->options->searchAppearance->archives->$archive->title = $value;
			}

			if ( isset( $this->options[ "{$archive}_archive_description" ] ) ) {
				aioseo()->options->searchAppearance->archives->$archive->metaDescription =
					aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options[ "{$archive}_archive_description" ], 'archive' ) );
			}

			if ( ! empty( $this->options[ "{$archive}_custom_robots" ] ) ) {
				aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->default = 'off' === $this->options[ "{$archive}_custom_robots" ];
			}

			if ( ! empty( $this->options[ "{$archive}_robots" ] ) ) {
				foreach ( $this->options[ "{$archive}_robots" ] as $robotsName ) {
					if ( 'index' === $robotsName ) {
						continue;
					}

					if ( 'noindex' === $robotsName ) {
						aioseo()->options->searchAppearance->archives->{$archive}->show = false;
					}

					aioseo()->options->searchAppearance->archives->{$archive}->advanced->robotsMeta->{$robotsName} = true;
				}
			}

			if ( ! empty( $this->options[ "{$archive}_advanced_robots" ] ) ) {
				if ( isset( $this->options[ "{$archive}_advanced_robots" ]['max-snippet'] ) && is_numeric( $this->options[ "{$archive}_advanced_robots" ]['max-snippet'] ) ) {
					aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->maxSnippet = intval( $this->options[ "{$archive}_advanced_robots" ]['max-snippet'] );
				}
				if ( isset( $this->options[ "{$archive}_advanced_robots" ]['max-video-preview'] ) && is_numeric( isset( $this->options[ "{$archive}_advanced_robots" ]['max-video-preview'] ) ) ) {
					aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->maxVideoPreview = intval( $this->options[ "{$archive}_advanced_robots" ]['max-video-preview'] );
				}
				if ( ! empty( $this->options[ "{$archive}_advanced_robots" ]['max-image-preview'] ) ) {
					aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->maxImagePreview =
						aioseo()->helpers->sanitizeOption( lcfirst( $this->options[ "{$archive}_advanced_robots" ]['max-image-preview'] ) );
				}
			}
		}

		if ( isset( $this->options['search_title'] ) ) {
			// Archive Title tag needs to be stripped since we don't support it for search archives.
			$value = aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options['search_title'], 'archive' ) );
			aioseo()->options->searchAppearance->archives->search->title = aioseo()->helpers->pregReplace( '/#archive_title/', '', $value );
		}

		if ( ! empty( $this->options['noindex_search'] ) ) {
			aioseo()->options->searchAppearance->archives->search->show                          = 'off' === $this->options['noindex_search'];
			aioseo()->options->searchAppearance->archives->search->advanced->robotsMeta->default = 'on' === $this->options['noindex_search'];
			aioseo()->options->searchAppearance->archives->search->advanced->robotsMeta->noindex = 'on' === $this->options['noindex_search'];
		}
	}

	/**
	 * Migrates the post type settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migratePostTypeSettings() {
		$supportedSettings = [
			'title',
			'description',
			'custom_robots',
			'robots',
			'advanced_robots',
			'default_rich_snippet',
			'default_article_type',
			'add_meta_box'
		];

		foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
			// Reset existing values first.
			foreach ( $this->robotMetaSettings as $robotsMetaName ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->$robotsMetaName = false;
			}

			foreach ( $this->options as $name => $value ) {
				if ( ! preg_match( "#^pt_{$postType}_(.*)$#", (string) $name, $match ) || ! in_array( $match[1], $supportedSettings, true ) ) {
					continue;
				}

				switch ( $match[1] ) {
					case 'title':
						if ( 'page' === $postType ) {
							$value = aioseo()->helpers->pregReplace( '#%category%#', '', $value );
							$value = aioseo()->helpers->pregReplace( '#%excerpt%#', '', $value );
						}
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->title =
							aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $value ) );
						break;
					case 'description':
						if ( 'page' === $postType ) {
							$value = aioseo()->helpers->pregReplace( '#%category%#', '', $value );
							$value = aioseo()->helpers->pregReplace( '#%excerpt%#', '', $value );
						}
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->metaDescription =
							aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $value ) );
						break;
					case 'custom_robots':
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = 'off' === $value;
						break;
					case 'robots':
						if ( ! empty( $value ) ) {
							foreach ( $value as $robotsName ) {
								if ( 'index' === $robotsName ) {
									continue;
								}

								if ( 'noindex' === $robotsName ) {
									aioseo()->dynamicOptions->searchAppearance->postTypes->{$postType}->show = false;
								}

								aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->$robotsName = true;
							}
						}
						break;
					case 'advanced_robots':
						if ( isset( $value['max-snippet'] ) && is_numeric( $value['max-snippet'] ) ) {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->maxSnippet = intval( $value['max-snippet'] );
						}
						if ( isset( $value['max-video-preview'] ) && is_numeric( $value['max-video-preview'] ) ) {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->maxVideoPreview = intval( $value['max-video-preview'] );
						}
						if ( ! empty( $value['max-image-preview'] ) ) {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->maxImagePreview =
								aioseo()->helpers->sanitizeOption( $value['max-image-preview'] );
						}
						break;
					case 'add_meta_box':
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->showMetaBox = 'on' === $value;
						break;
					case 'default_rich_snippet':
						$value = aioseo()->helpers->pregReplace( '#\s#', '', $value );
						if ( 'off' === lcfirst( $value ) || in_array( $postType, [ 'page', 'attachment' ], true ) ) {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->schemaType = 'none';
							break;
						}
						if ( in_array( ucfirst( $value ), ImportExport\SearchAppearance::$supportedSchemaGraphs, true ) ) {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->schemaType = ucfirst( $value );
						}
						break;
					case 'default_article_type':
						if ( in_array( $postType, [ 'page', 'attachment' ], true ) ) {
							break;
						}
						$value = aioseo()->helpers->pregReplace( '#\s#', '', $value );
						if ( in_array( ucfirst( $value ), ImportExport\SearchAppearance::$supportedArticleGraphs, true ) ) {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->articleType = ucfirst( $value );
						} else {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->articleType = 'BlogPosting';
						}
						break;
					default:
						break;
				}
			}
		}
	}

	/**
	 * Migrates the post type archive settings.
	 *
	 * @since 4.0.16
	 *
	 * @return void
	 */
	private function migratePostTypeArchiveSettings() {
		$supportedSettings = [
			'title',
			'description'
		];

		foreach ( aioseo()->helpers->getPublicPostTypes( true, true ) as $postType ) {
			foreach ( $this->options as $name => $value ) {
				if ( ! preg_match( "#^pt_{$postType}_archive_(.*)$#", (string) $name, $match ) || ! in_array( $match[1], $supportedSettings, true ) ) {
					continue;
				}

				switch ( $match[1] ) {
					case 'title':
						aioseo()->dynamicOptions->searchAppearance->archives->$postType->title =
							aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $value, 'archive' ) );
						break;
					case 'description':
						aioseo()->dynamicOptions->searchAppearance->archives->$postType->metaDescription =
							aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $value, 'archive' ) );
						break;
					default:
						break;
				}
			}
		}
	}


	/**
	 * Migrates the robots meta settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateRobotMetaSettings() {
		// Reset existing values first.
		foreach ( $this->robotMetaSettings as $robotsMetaName ) {
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->$robotsMetaName = false;
		}

		if ( ! empty( $this->options['robots_global'] ) ) {
			foreach ( $this->options['robots_global'] as $robotsName ) {
				if ( 'index' === $robotsName ) {
					continue;
				}
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default     = false;
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->$robotsName = true;
			}
		}

		if ( ! empty( $this->options['advanced_robots_global'] ) ) {
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default = false;

			if ( isset( $this->options['robots_global']['max-snippet'] ) && is_numeric( $this->options['robots_global']['max-snippet'] ) ) {
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->maxSnippet = intval( $this->options['robots_global']['max-snippet'] );
			}
			if ( isset( $this->options['robots_global']['max-video-preview'] ) && is_numeric( $this->options['robots_global']['max-video-preview'] ) ) {
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->maxVideoPreview = intval( $this->options['robots_global']['max-video-preview'] );
			}
			if ( ! empty( $this->options['robots_global']['max-image-preview'] ) ) {
				aioseo()->options->searchAppearance->advanced->globalRobotsMeta->maxImagePreview =
					aioseo()->helpers->sanitizeOption( $this->options['robots_global']['max-image-preview'] );
			}
		}

		if ( ! empty( $this->options['noindex_paginated_pages'] ) ) {
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default          = false;
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindexPaginated = 'on' === $this->options['noindex_paginated_pages'];
		}
	}

	/**
	 * Migrates the Knowledge Graph settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateKnowledgeGraphSettings() {
		if ( empty( $this->options['knowledgegraph_type'] ) ) {
			return;
		}

		aioseo()->options->searchAppearance->global->schema->siteRepresents =
			'company' === $this->options['knowledgegraph_type'] ? 'organization' : 'person';

		if ( ! empty( $this->options['knowledgegraph_name'] ) && 'company' === $this->options['knowledgegraph_type'] ) {
			aioseo()->options->searchAppearance->global->schema->organizationName = aioseo()->helpers->sanitizeOption( $this->options['knowledgegraph_name'] );
		} elseif ( ! empty( $this->options['knowledgegraph_logo'] ) ) {
			aioseo()->options->searchAppearance->global->schema->person     = 'manual';
			aioseo()->options->searchAppearance->global->schema->personName = aioseo()->helpers->sanitizeOption( $this->options['knowledgegraph_name'] );
		}

		if ( ! empty( $this->options['knowledgegraph_logo'] ) && 'company' === $this->options['knowledgegraph_type'] ) {
			aioseo()->options->searchAppearance->global->schema->organizationLogo = esc_url( $this->options['knowledgegraph_logo'] );
		} elseif ( ! empty( $this->options['knowledgegraph_logo'] ) ) {
			aioseo()->options->searchAppearance->global->schema->person     = 'manual';
			aioseo()->options->searchAppearance->global->schema->personLogo = esc_url( $this->options['knowledgegraph_logo'] );
		}

		$this->migrateKnowledgeGraphPhoneNumber();
	}

	/**
	 * Migrates the Knowledge Graph phone number.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateKnowledgeGraphPhoneNumber() {
		if ( empty( $this->options['phone'] ) ) {
			return;
		}

		$phoneNumber = aioseo()->helpers->sanitizeOption( $this->options['phone'] );
		if ( ! preg_match( '#\+\d+#', (string) $phoneNumber ) ) {
			$notification = Models\Notification::getNotificationByName( 'v3-migration-schema-number' );
			if ( $notification->notification_name ) {
				return;
			}

			Models\Notification::addNotification( [
				'slug'              => uniqid(),
				'notification_name' => 'v3-migration-schema-number',
				'title'             => __( 'Invalid Phone Number for Knowledge Graph', 'all-in-one-seo-pack' ),
				'content'           => sprintf(
					// Translators: 1 - The phone number.
					__( 'We were unable to import the phone number that you previously entered for your Knowledge Graph schema markup.
					As it needs to be internationally formatted, please enter it (%1$s) with the country code, e.g. +1 (555) 555-1234.', 'all-in-one-seo-pack' ),
					"<strong>$phoneNumber</strong>"
				),
				'type'              => 'warning',
				'level'             => [ 'all' ],
				'button1_label'     => __( 'Fix Now', 'all-in-one-seo-pack' ),
				'button1_action'    => 'http://route#aioseo-search-appearance&aioseo-scroll=schema-graph-phone&aioseo-highlight=schema-graph-phone:schema-markup',
				'button2_label'     => __( 'Remind Me Later', 'all-in-one-seo-pack' ),
				'button2_action'    => 'http://action#notification/v3-migration-schema-number-reminder',
				'start'             => gmdate( 'Y-m-d H:i:s' )
			] );

			return;
		}
		aioseo()->options->searchAppearance->global->schema->phone = $phoneNumber;
	}

	/**
	 * Migrates the Social Meta settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateSocialMetaSettings() {
		if ( ! empty( $this->options['open_graph_image'] ) ) {
			$defaultImage = esc_url( $this->options['open_graph_image'] );
			aioseo()->options->social->facebook->general->defaultImagePosts = $defaultImage;
			aioseo()->options->social->twitter->general->defaultImagePosts  = $defaultImage;
		}

		if ( ! empty( $this->options['social_url_facebook'] ) ) {
			aioseo()->options->social->profiles->urls->facebookPageUrl = esc_url( $this->options['social_url_facebook'] );
		}

		if ( ! empty( $this->options['facebook_author_urls'] ) ) {
			aioseo()->options->social->facebook->advanced->enable    = true;
			aioseo()->options->social->facebook->advanced->authorUrl = esc_url( $this->options['facebook_author_urls'] );
		}

		if ( ! empty( $this->options['facebook_admin_id'] ) ) {
			aioseo()->options->social->facebook->advanced->enable  = true;
			aioseo()->options->social->facebook->advanced->adminId = aioseo()->helpers->sanitizeOption( $this->options['facebook_admin_id'] );
		}

		if ( ! empty( $this->options['facebook_app_id'] ) ) {
			aioseo()->options->social->facebook->advanced->enable = true;
			aioseo()->options->social->facebook->advanced->appId  = aioseo()->helpers->sanitizeOption( $this->options['facebook_app_id'] );
		}

		if ( ! empty( $this->options['twitter_author_names'] ) ) {
			aioseo()->options->social->profiles->urls->twitterUrl =
				'https://x.com/' . aioseo()->helpers->sanitizeOption( $this->options['twitter_author_names'] );
		}

		if ( ! empty( $this->options['twitter_card_type'] ) ) {
			preg_match( '#large#', $this->options['twitter_card_type'], $match );
			aioseo()->options->social->twitter->general->defaultCardType = ! empty( $match ) ? 'summary_large_image' : 'summary';
		}
	}

	/**
	 * Migrates the default social image for posts.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateDefaultPostSocialImage() {
		if ( ! empty( $this->options['open_graph_image'] ) ) {
			$defaultImage = esc_url( $this->options['open_graph_image'] );
			aioseo()->options->social->facebook->general->defaultImagePosts = $defaultImage;
			aioseo()->options->social->twitter->general->defaultImagePosts  = $defaultImage;
		}
	}
}Common/ImportExport/RankMath/Helpers.php000066600000007433151135505570014312 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\RankMath;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\ImportExport;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Contains helper methods for the import from Rank Math.
 *
 * @since 4.0.0
 */
class Helpers extends ImportExport\Helpers {
	/**
	 * Converts the macros from Rank Math to our own smart tags.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string   The string with macros.
	 * @param  string $pageType The page type.
	 * @return string $string   The string with smart tags.
	 */
	public function macrosToSmartTags( $string, $pageType = null ) {
		$macros = $this->getMacros( $pageType );

		if ( preg_match( '#%BLOGDESCLINK%#', (string) $string ) ) {
			$blogDescriptionLink = '<a href="' .
				aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'url' ) ) . '">' .
				aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) ) . ' - ' .
				aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'description' ) ) . '</a>';

			$string = str_replace( '%BLOGDESCLINK%', $blogDescriptionLink, $string );
		}

		if ( preg_match_all( '#%customfield\(([^%\s]*)\)%#', (string) $string, $matches ) && ! empty( $matches[1] ) ) {
			foreach ( $matches[1] as $name ) {
				$string = aioseo()->helpers->pregReplace( "#%customfield\($name\)%#", "#custom_field-$name", $string );
			}
		}

		if ( preg_match_all( '#%customterm\(([^%\s]*)\)%#', (string) $string, $matches ) && ! empty( $matches[1] ) ) {
			foreach ( $matches[1] as $name ) {
				$string = aioseo()->helpers->pregReplace( "#%customterm\($name\)%#", "#tax_name-$name", $string );
			}
		}

		foreach ( $macros as $macro => $tag ) {
			$string = aioseo()->helpers->pregReplace( "#$macro(?![a-zA-Z0-9_])#im", $tag, $string );
		}

		// Strip out all remaining tags.
		$string = aioseo()->helpers->pregReplace( '/%[^\%\s]*\([^\%]*\)%/i', '', aioseo()->helpers->pregReplace( '/%[^\%\s]*%/i', '', $string ) );

		return trim( $string );
	}

	/**
	 * Returns the macro mappings.
	 *
	 * @since 4.1.1
	 *
	 * @param  string $pageType The page type.
	 * @return array  $macros   The macros.
	 */
	protected function getMacros( $pageType = null ) {
		$macros = [
			'%sitename%'         => '#site_title',
			'%blog_title%'       => '#site_title',
			'%blog_description%' => '#tagline',
			'%sitedesc%'         => '#tagline',
			'%sep%'              => '#separator_sa',
			'%post_title%'       => '#post_title',
			'%page_title%'       => '#post_title',
			'%postname%'         => '#post_title',
			'%title%'            => '#post_title',
			'%seo_title%'        => '#post_title',
			'%excerpt%'          => '#post_excerpt',
			'%wc_shortdesc%'     => '#post_excerpt',
			'%category%'         => '#taxonomy_title',
			'%term%'             => '#taxonomy_title',
			'%term_description%' => '#taxonomy_description',
			'%currentdate%'      => '#current_date',
			'%currentday%'       => '#current_day',
			'%currentyear%'      => '#current_year',
			'%currentmonth%'     => '#current_month',
			'%name%'             => '#author_first_name #author_last_name',
			'%author%'           => '#author_first_name #author_last_name',
			'%date%'             => '#post_date',
			'%year%'             => '#current_year',
			'%search_query%'     => '#search_term',
			// RSS Content tags.
			'%AUTHORLINK%'       => '#author_link',
			'%POSTLINK%'         => '#post_link',
			'%BLOGLINK%'         => '#site_link',
			'%FEATUREDIMAGE%'    => '#featured_image'
		];

		switch ( $pageType ) {
			case 'archive':
				$macros['%title%'] = '#archive_title';
				break;
			case 'term':
				$macros['%title%'] = '#taxonomy_title';
				break;
			default:
				$macros['%title%'] = '#post_title';
				break;
		}

		// Strip all other tags.
		$macros['%[^%]*%'] = '';

		return $macros;
	}
}Common/ImportExport/RankMath/GeneralSettings.php000066600000005777151135505570016017 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\RankMath;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the General Settings.
 *
 * @since 4.0.0
 */
class GeneralSettings {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->options = get_option( 'rank-math-options-general' );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->isTruSeoDisabled();
		$this->migrateRedirectAttachments();
		$this->migrateStripCategoryBase();
		$this->migrateRssContentSettings();

		$settings = [
			'google_verify'    => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'google' ] ],
			'bing_verify'      => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'bing' ] ],
			'yandex_verify'    => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'yandex' ] ],
			'baidu_verify'     => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'baidu' ] ],
			'pinterest_verify' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'pinterest' ] ],
		];

		aioseo()->importExport->rankMath->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Checks whether TruSEO should be disabled.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function isTruSeoDisabled() {
		if ( ! empty( $this->options['frontend_seo_score'] ) ) {
			aioseo()->options->advanced->truSeo = 'on' === $this->options['frontend_seo_score'];
		}
	}

	/**
	 * Migrates the Redirect Attachments setting.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateRedirectAttachments() {
		if ( isset( $this->options['attachment_redirect_urls'] ) ) {
			if ( 'on' === $this->options['attachment_redirect_urls'] ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'attachment_parent';
			} else {
				aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'disabled';
			}
		}
	}

	/**
	 * Migrates the Strip Category Base setting.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	private function migrateStripCategoryBase() {
		if ( isset( $this->options['strip_category_base'] ) ) {
			aioseo()->options->searchAppearance->advanced->removeCategoryBase = 'on' === $this->options['strip_category_base'] ? true : false;
		}
	}

	/**
	 * Migrates the RSS content settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateRssContentSettings() {
		if ( isset( $this->options['rss_before_content'] ) ) {
			aioseo()->options->rssContent->before = esc_html( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options['rss_before_content'] ) );
		}

		if ( isset( $this->options['rss_after_content'] ) ) {
			aioseo()->options->rssContent->after = esc_html( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options['rss_after_content'] ) );
		}
	}
}Common/ImportExport/RankMath/PostMeta.php000066600000022432151135505570014440 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\RankMath;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Imports the post meta from Rank Math.
 *
 * @since 4.0.0
 */
class PostMeta {
	/**
	 * The batch import action name.
	 *
	 * @since 4.0.0
	 * @version 4.8.3 Moved from RankMath class to here.
	 *
	 * @var string
	 */
	public $postActionName = 'aioseo_import_post_meta_rank_math';

	/**
	 * The mapped meta
	 *
	 * @since 4.8.3
	 *
	 * @var array
	 */
	private $mappedMeta = [
		'rank_math_title'                => 'title',
		'rank_math_description'          => 'description',
		'rank_math_canonical_url'        => 'canonical_url',
		'rank_math_focus_keyword'        => 'keyphrases',
		'rank_math_robots'               => '',
		'rank_math_advanced_robots'      => '',
		'rank_math_facebook_title'       => 'og_title',
		'rank_math_facebook_description' => 'og_description',
		'rank_math_facebook_image'       => 'og_image_custom_url',
		'rank_math_twitter_use_facebook' => 'twitter_use_og',
		'rank_math_twitter_title'        => 'twitter_title',
		'rank_math_twitter_description'  => 'twitter_description',
		'rank_math_twitter_image'        => 'twitter_image_custom_url',
		'rank_math_twitter_card_type'    => 'twitter_card',
		'rank_math_primary_category'     => 'primary_term',
		'rank_math_pillar_content'       => 'pillar_content',
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.8.3
	 */
	public function __construct() {
		add_action( $this->postActionName, [ $this, 'importPostMeta' ] );
	}

	/**
	 * Schedules the post meta import.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function scheduleImport() {
		try {
			if ( as_next_scheduled_action( $this->postActionName ) ) {
				return;
			}

			if ( ! aioseo()->core->cache->get( 'import_post_meta_rank_math' ) ) {
				aioseo()->core->cache->update( 'import_post_meta_rank_math', time(), WEEK_IN_SECONDS );
			}

			as_schedule_single_action( time(), $this->postActionName, [], 'aioseo' );
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Get all posts to be imported
	 *
	 * @since 4.8.3
	 *
	 * @param  int   $postsPerAction The number of posts to import per action.
	 * @return array                 The posts to be imported.
	 */
	protected function getPostsToImport( $postsPerAction = 100 ) {
		$publicPostTypes = implode( "', '", aioseo()->helpers->getPublicPostTypes( true ) );
		$timeStarted     = gmdate( 'Y-m-d H:i:s', aioseo()->core->cache->get( 'import_post_meta_rank_math' ) );

		$posts = aioseo()->core->db
			->start( 'posts' . ' as p' )
			->select( 'p.ID, p.post_type' )
			->join( 'postmeta as pm', '`p`.`ID` = `pm`.`post_id`' )
			->leftJoin( 'aioseo_posts as ap', '`p`.`ID` = `ap`.`post_id`' )
			->whereRaw( "pm.meta_key LIKE 'rank_math_%'" )
			->whereRaw( "( p.post_type IN ( '$publicPostTypes' ) )" )
			->whereRaw( "( ap.post_id IS NULL OR ap.updated < '$timeStarted' )" )
			->orderBy( 'p.ID DESC' )
			->groupBy( 'p.ID' )
			->limit( $postsPerAction )
			->run()
			->result();

		return $posts;
	}

	/**
	 * Imports the post meta.
	 *
	 * @since 4.0.0
	 *
	 * @return array The posts that were imported.
	 */
	public function importPostMeta() {
		$postsPerAction = apply_filters( 'aioseo_import_rank_math_posts_per_action', 100 );
		$posts          = $this->getPostsToImport( $postsPerAction );
		if ( ! $posts || ! count( $posts ) ) {
			aioseo()->core->cache->delete( 'import_post_meta_rank_math' );

			return [];
		}

		foreach ( $posts as $post ) {
			$postMeta = aioseo()->core->db
				->start( 'postmeta' . ' as pm' )
				->select( 'pm.meta_key, pm.meta_value' )
				->where( 'pm.post_id', $post->ID )
				->whereRaw( "`pm`.`meta_key` LIKE 'rank_math_%'" )
				->run()
				->result();

			$meta = array_merge( [
				'post_id' => (int) $post->ID,
			], $this->getMetaData( $postMeta, $post ) );

			$aioseoPost = Models\Post::getPost( $post->ID );
			$aioseoPost->set( $meta );
			$aioseoPost->save();

			aioseo()->migration->meta->migrateAdditionalPostMeta( $post->ID );
		}

		// Clear the Overview cache.
		aioseo()->postSettings->clearPostTypeOverviewCache( $posts[0]->ID );

		if ( count( $posts ) === $postsPerAction ) {
			try {
				as_schedule_single_action( time() + 5, $this->postActionName, [], 'aioseo' );
			} catch ( \Exception $e ) {
				// Do nothing.
			}
		} else {
			aioseo()->core->cache->delete( 'import_post_meta_rank_math' );
		}

		return $posts;
	}

	/**
	 * Get the meta data by post meta.
	 *
	 * @since 4.8.3
	 *
	 * @param object $postMeta The post meta from database.
	 * @param object $post     The post object.
	 * @return array           The meta data.
	 */
	public function getMetaData( $postMeta, $post ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$meta = [
			'post_id'             => $post->ID,
			'robots_default'      => true,
			'robots_noarchive'    => false,
			'canonical_url'       => '',
			'robots_nofollow'     => false,
			'robots_noimageindex' => false,
			'robots_noindex'      => false,
			'robots_noodp'        => false,
			'robots_nosnippet'    => false,
			'keyphrases'          => [
				'focus'      => [ 'keyphrase' => '' ],
				'additional' => []
			],
		];
		foreach ( $postMeta as $record ) {
			$name  = $record->meta_key;
			$value = $record->meta_value;

			if (
				! in_array( $post->post_type, [ 'page', 'attachment' ], true ) &&
				preg_match( '#^rank_math_schema_([^\s]*)$#', (string) $name, $match ) && ! empty( $match[1] )
			) {
				switch ( $match[1] ) {
					case 'Article':
					case 'NewsArticle':
					case 'BlogPosting':
						$meta['schema_type'] = 'Article';
						$meta['schema_type_options'] = wp_json_encode(
							[ 'article' => [ 'articleType' => $match[1] ] ]
						);
						break;
					default:
						break;
				}
			}

			if ( ! in_array( $name, array_keys( $this->mappedMeta ), true ) ) {
				continue;
			}

			switch ( $name ) {
				case 'rank_math_focus_keyword':
					$keyphrases     = array_map( 'trim', explode( ',', $value ) );
					$keyphraseArray = [
						'focus'      => [ 'keyphrase' => aioseo()->helpers->sanitizeOption( $keyphrases[0] ) ],
						'additional' => []
					];
					unset( $keyphrases[0] );
					foreach ( $keyphrases as $keyphrase ) {
						$keyphraseArray['additional'][] = [ 'keyphrase' => aioseo()->helpers->sanitizeOption( $keyphrase ) ];
					}

					$meta['keyphrases'] = $keyphraseArray;
					break;
				case 'rank_math_robots':
					$value = aioseo()->helpers->maybeUnserialize( $value );
					if ( ! empty( $value ) ) {
						$supportedValues        = [ 'index', 'noindex', 'nofollow', 'noarchive', 'noimageindex', 'nosnippet' ];
						$meta['robots_default'] = false;

						foreach ( $supportedValues as $val ) {
							$meta[ "robots_$val" ] = false;
						}

						// This is a separated foreach as we can import any and all values.
						foreach ( $value as $robotsName ) {
							$meta[ "robots_$robotsName" ] = true;
						}
					}
					break;
				case 'rank_math_advanced_robots':
					$value = aioseo()->helpers->maybeUnserialize( $value );
					if ( isset( $value['max-snippet'] ) && is_numeric( $value['max-snippet'] ) ) {
						$meta['robots_default']     = false;
						$meta['robots_max_snippet'] = intval( $value['max-snippet'] );
					}
					if ( isset( $value['max-video-preview'] ) && is_numeric( $value['max-video-preview'] ) ) {
						$meta['robots_default']          = false;
						$meta['robots_max_videopreview'] = intval( $value['max-video-preview'] );
					}
					if ( ! empty( $value['max-image-preview'] ) ) {
						$meta['robots_default']          = false;
						$meta['robots_max_imagepreview'] = aioseo()->helpers->sanitizeOption( lcfirst( $value['max-image-preview'] ) );
					}
					break;
				case 'rank_math_facebook_image':
					$meta['og_image_type']        = 'custom_image';
					$meta[ $this->mappedMeta[ $name ] ] = esc_url( $value );
					break;
				case 'rank_math_twitter_image':
					$meta['twitter_image_type']   = 'custom_image';
					$meta[ $this->mappedMeta[ $name ] ] = esc_url( $value );
					break;
				case 'rank_math_twitter_card_type':
					preg_match( '#large#', (string) $value, $match );
					$meta[ $this->mappedMeta[ $name ] ] = ! empty( $match ) ? 'summary_large_image' : 'summary';
					break;
				case 'rank_math_twitter_use_facebook':
					$meta[ $this->mappedMeta[ $name ] ] = 'on' === $value;
					break;
				case 'rank_math_primary_category':
					$taxonomy                     = 'category';
					$options                      = new \stdClass();
					$options->$taxonomy           = (int) $value;
					$meta[ $this->mappedMeta[ $name ] ] = wp_json_encode( $options );
					break;
				case 'rank_math_title':
				case 'rank_math_description':
					if ( 'page' === $post->post_type ) {
						$value = aioseo()->helpers->pregReplace( '#%category%#', '', $value );
						$value = aioseo()->helpers->pregReplace( '#%excerpt%#', '', $value );
					}
					$value = aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $value );

					$meta[ $this->mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
					break;
				case 'rank_math_pillar_content':
					$meta['pillar_content'] = 'on' === $value ? 1 : 0;
					break;
				default:
					$meta[ $this->mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
					break;
			}
		}

		return $meta;
	}
}Common/ImportExport/ImportExport.php000066600000025310151135505570013651 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models;

/**
 * Handles the importing/exporting of settings and SEO data.
 *
 * @since 4.0.0
 */
class ImportExport {
	/**
	 * List of plugins for importing.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private $plugins = [];

	/**
	 * YoastSeo class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var YoastSeo\YoastSeo
	 */
	public $yoastSeo = null;

	/**
	 * RankMath class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var RankMath\RankMath
	 */
	public $rankMath = null;

	/**
	 * SeoPress class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var SeoPress\SeoPress
	 */
	public $seoPress = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->yoastSeo = new YoastSeo\YoastSeo( $this );
		$this->rankMath = new RankMath\RankMath( $this );
		$this->seoPress = new SeoPress\SeoPress( $this );
	}

	/**
	 * Converts the content of a given V3 .ini settings file to an array of settings.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $contents The .ini file contents.
	 * @return array            The settings.
	 */
	public function importIniData( $contents ) {
		$lines = array_filter( preg_split( '/\r\n|\r|\n/', (string) $contents ) );

		$sections     = [];
		$sectionLabel = '';
		$sectionCount = 0;

		foreach ( $lines as $line ) {
			$line = trim( $line );
			// Ignore comments.
			if ( preg_match( '#^;.*#', (string) $line ) || preg_match( '#\<(\?php|script)#', (string) $line ) ) {
				continue;
			}

			$matches = [];
			if ( preg_match( '#^\[(\S+)\]$#', (string) $line, $label ) ) {
				$sectionLabel = strval( $label[1] );
				if ( 'post_data' === $sectionLabel ) {
					$sectionCount++;
				}
				if ( ! isset( $sections[ $sectionLabel ] ) ) {
					$sections[ $sectionLabel ] = [];
				}
			} elseif ( preg_match( "#^(\S+)\s*=\s*'(.*)'$#", (string) $line, $matches ) ) {
				if ( 'post_data' === $sectionLabel ) {
					$sections[ $sectionLabel ][ $sectionCount ][ $matches[1] ] = $matches[2];
				} else {
					$sections[ $sectionLabel ][ $matches[1] ] = $matches[2];
				}
			} elseif ( preg_match( '#^(\S+)\s*=\s*NULL$#', (string) $line, $matches ) ) {
				if ( 'post_data' === $sectionLabel ) {
					$sections[ $sectionLabel ][ $sectionCount ][ $matches[1] ] = '';
				} else {
					$sections[ $sectionLabel ][ $matches[1] ] = '';
				}
			} else {
				continue;
			}
		}

		$sanitizedSections = [];
		foreach ( $sections as $section => $options ) {
			$sanitizedSection = [];
			foreach ( $options as $option => $value ) {
				$sanitizedSection[ $option ] = $this->convertAndSanitize( $value );
			}
			$sanitizedSections[ $section ] = $sanitizedSection;
		}

		$oldOptions = [];
		$postData   = [];
		foreach ( $sanitizedSections as $label => $data ) {
			switch ( $label ) {
				case 'aioseop_options':
					$oldOptions = array_merge( $oldOptions, $data );
					break;
				case 'aiosp_feature_manager_options':
				case 'aiosp_opengraph_options':
				case 'aiosp_sitemap_options':
				case 'aiosp_video_sitemap_options':
				case 'aiosp_schema_local_business_options':
				case 'aiosp_image_seo_options':
				case 'aiosp_robots_options':
				case 'aiosp_bad_robots_options':
					$oldOptions['modules'][ $label ] = $data;
					break;
				case 'post_data':
					$postData = $data;
					break;
				default:
					break;
			}
		}

		if ( ! empty( $oldOptions ) ) {
			aioseo()->migration->migrateSettings( $oldOptions );
		}

		if ( ! empty( $postData ) ) {
			$this->importOldPostMeta( $postData );
		}

		return true;
	}

	/**
	 * Imports the post meta from V3.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $postData The post data.
	 * @return void
	 */
	private function importOldPostMeta( $postData ) {
		$mappedMeta = [
			'_aioseop_title'              => 'title',
			'_aioseop_description'        => 'description',
			'_aioseop_custom_link'        => 'canonical_url',
			'_aioseop_sitemap_exclude'    => '',
			'_aioseop_disable'            => '',
			'_aioseop_noindex'            => 'robots_noindex',
			'_aioseop_nofollow'           => 'robots_nofollow',
			'_aioseop_sitemap_priority'   => 'priority',
			'_aioseop_sitemap_frequency'  => 'frequency',
			'_aioseop_keywords'           => 'keywords',
			'_aioseop_opengraph_settings' => ''
		];

		$excludedPosts        = [];
		$sitemapExcludedPosts = [];

		require_once ABSPATH . 'wp-admin/includes/post.php';
		foreach ( $postData as $post => $values ) {
			$postId = \post_exists( $values['post_title'], '', $values['post_date'] );
			if ( ! $postId ) {
				continue;
			}

			$meta = [
				'post_id' => $postId,
			];

			foreach ( $values as $name => $value ) {
				if ( ! in_array( $name, array_keys( $mappedMeta ), true ) ) {
					continue;
				}

				switch ( $name ) {
					case '_aioseop_sitemap_exclude':
						if ( empty( $value ) ) {
							break;
						}
						$sitemapExcludedPosts[] = $postId;
						break;
					case '_aioseop_disable':
						if ( empty( $value ) ) {
							break;
						}
						$excludedPosts[] = $postId;
						break;
					case '_aioseop_noindex':
					case '_aioseop_nofollow':
						$meta[ $mappedMeta[ $name ] ] = ! empty( $value );
						if ( ! empty( $value ) ) {
							$meta['robots_default'] = false;
						}
						break;
					case '_aioseop_keywords':
						$meta[ $mappedMeta[ $name ] ] = aioseo()->migration->helpers->oldKeywordsToNewKeywords( $value );
						break;
					case '_aioseop_opengraph_settings':
						$class = new \AIOSEO\Plugin\Common\Migration\Meta();
						$meta += $class->convertOpenGraphMeta( $value );
						break;
					default:
						$meta[ $mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
						break;
				}
			}
			$post = Models\Post::getPost( $postId );
			$post->set( $meta );
			$post->save();
		}

		if ( count( $excludedPosts ) ) {
			$deprecatedOptions = aioseo()->internalOptions->internal->deprecatedOptions;
			if ( ! in_array( 'excludePosts', $deprecatedOptions, true ) ) {
				array_push( $deprecatedOptions, 'excludePosts' );
				aioseo()->internalOptions->internal->deprecatedOptions = $deprecatedOptions;
			}

			$posts = aioseo()->options->deprecated->searchAppearance->advanced->excludePosts;

			foreach ( $excludedPosts as $id ) {
				if ( ! intval( $id ) ) {
					continue;
				}
				$post = get_post( $id );
				if ( ! is_object( $post ) ) {
					continue;
				}
				$excludedPost        = new \stdClass();
				$excludedPost->type  = $post->post_type;
				$excludedPost->value = $post->ID;
				$excludedPost->label = $post->post_title;
				$excludedPost->link  = get_permalink( $post );

				$posts[] = wp_json_encode( $excludedPost );
			}
			aioseo()->options->deprecated->searchAppearance->advanced->excludePosts = $posts;
		}

		if ( count( $sitemapExcludedPosts ) ) {
			aioseo()->options->sitemap->general->advancedSettings->enable = true;

			$posts = aioseo()->options->sitemap->general->advancedSettings->excludePosts;
			foreach ( $sitemapExcludedPosts as $id ) {
				if ( ! intval( $id ) ) {
					continue;
				}
				$post = get_post( $id );
				if ( ! is_object( $post ) ) {
					continue;
				}
				$excludedPost        = new \stdClass();
				$excludedPost->type  = $post->post_type;
				$excludedPost->value = $post->ID;
				$excludedPost->label = $post->post_title;
				$excludedPost->link  = get_permalink( $post );

				$posts[] = wp_json_encode( $excludedPost );
			}
			aioseo()->options->sitemap->general->advancedSettings->excludePosts = $posts;
		}
	}

	/**
	 * Unserializes an option value if needed and then sanitizes it.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $value The option value.
	 * @return mixed         The sanitized, converted option value.
	 */
	private function convertAndSanitize( $value ) {
		$value = aioseo()->helpers->maybeUnserialize( $value );

		switch ( gettype( $value ) ) {
			case 'boolean':
				return (bool) $value;
			case 'string':
				return esc_html( wp_strip_all_tags( wp_check_invalid_utf8( trim( $value ) ) ) );
			case 'integer':
				return intval( $value );
			case 'double':
				return floatval( $value );
			case 'array':
				$sanitized = [];
				foreach ( (array) $value as $k => $v ) {
					$sanitized[ $k ] = $this->convertAndSanitize( $v );
				}

				return $sanitized;
			default:
				return '';
		}
	}

	/**
	 * Starts an import.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $plugin  The slug of the plugin to import.
	 * @param  array $settings Which settings to import.
	 * @return void
	 */
	public function startImport( $plugin, $settings ) {
		// First cancel any scans running that might interfere with our import.
		$this->cancelScans();

		foreach ( $this->plugins as $pluginData ) {
			if ( $pluginData['slug'] === $plugin ) {
				$pluginData['class']->doImport( $settings );

				return;
			}
		}
	}

	/**
	 * Cancel scans that are currently running and could conflict with our migration.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function cancelScans() {
		// Figure out how to check if these addons are enabled and then get the action names that way.
		aioseo()->actionScheduler->unschedule( 'aioseo_video_sitemap_scan' );
		aioseo()->actionScheduler->unschedule( 'aioseo_image_sitemap_scan' );
	}

	/**
	 * Checks if an import is currently running.
	 *
	 * @since 4.1.4
	 *
	 * @return boolean True if an import is currently running.
	 */
	public function isImportRunning() {
		$importsRunning = aioseo()->core->cache->get( 'import_%_meta_%' );

		return ! empty( $importsRunning );
	}

	/**
	 * Adds plugins to the import/export.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $plugins The plugins to add.
	 * @return void
	 */
	public function addPlugins( $plugins ) {
		$this->plugins = array_merge( $this->plugins, $plugins );
	}

	/**
	 * Get the plugins we allow importing from.
	 *
	 * @since 4.0.0
	 *
	 * @return array
	 */
	public function plugins() {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';
		$plugins          = [];
		$installedPlugins = array_keys( get_plugins() );
		foreach ( $this->plugins as $importerPlugin ) {
			$data = [
				'slug'      => $importerPlugin['slug'],
				'name'      => $importerPlugin['name'],
				'version'   => null,
				'canImport' => false,
				'basename'  => $importerPlugin['basename'],
				'installed' => false
			];

			if ( in_array( $importerPlugin['basename'], $installedPlugins, true ) ) {
				$pluginData = get_file_data( trailingslashit( WP_PLUGIN_DIR ) . $importerPlugin['basename'], [
					'name'    => 'Plugin Name',
					'version' => 'Version',
				] );

				$canImport = false;
				if ( version_compare( $importerPlugin['version'], $pluginData['version'], '<=' ) ) {
					$canImport = true;
				}

				$data['name']      = $pluginData['name'];
				$data['version']   = $pluginData['version'];
				$data['canImport'] = $canImport;
				$data['installed'] = true;
			}

			$plugins[] = $data;
		}

		return $plugins;
	}
}Common/ImportExport/Importer.php000066600000002675151135505570013007 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Imports the settings and meta data from other plugins.
 *
 * @since 4.0.0
 */
abstract class Importer {
	/**
	 * Imports the settings.
	 *
	 * @since 4.2.7
	 *
	 * @return void
	 */
	protected function importSettings() {}

	/**
	 * Imports the post meta.
	 *
	 * @since 4.2.7
	 *
	 * @return void
	 */
	protected function importPostMeta() {}

	/**
	 * Imports the term meta.
	 *
	 * @since 4.2.7
	 *
	 * @return void
	 */
	protected function importTermMeta() {}

	/**
	 * PostMeta class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Object
	 */
	protected $postMeta = null;

	/**
	 * TermMeta class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Object
	 */
	protected $termMeta = null;

	/**
	 * Helpers class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Object
	 */
	public $helpers = null;

	/**
	 * Starts the import.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $options What the user wants to import.
	 * @return void
	 */
	public function doImport( $options = [] ) {
		if ( empty( $options ) ) {
			$this->importSettings();
			$this->importPostMeta();
			$this->importTermMeta();

			return;
		}

		foreach ( $options as $optionName ) {
			switch ( $optionName ) {
				case 'settings':
					$this->importSettings();
					break;
				case 'postMeta':
					$this->postMeta->scheduleImport();
					break;
				default:
					break;
			}
		}
	}
}Common/ImportExport/SeoPress/PostMeta.php000066600000015547151135505570014507 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Imports the post meta from SEOPress.
 *
 * @since 4.1.4
 */
class PostMeta {
	/**
	 * The mapped meta
	 *
	 * @since 4.1.4
	 *
	 * @var array
	 */
	private $mappedMeta = [
		'_seopress_analysis_target_kw'   => '',
		'_seopress_robots_archive'       => 'robots_noarchive',
		'_seopress_robots_canonical'     => 'canonical_url',
		'_seopress_robots_follow'        => 'robots_nofollow',
		'_seopress_robots_imageindex'    => 'robots_noimageindex',
		'_seopress_robots_index'         => 'robots_noindex',
		'_seopress_robots_odp'           => 'robots_noodp',
		'_seopress_robots_snippet'       => 'robots_nosnippet',
		'_seopress_social_twitter_desc'  => 'twitter_description',
		'_seopress_social_twitter_img'   => 'twitter_image_custom_url',
		'_seopress_social_twitter_title' => 'twitter_title',
		'_seopress_social_fb_desc'       => 'og_description',
		'_seopress_social_fb_img'        => 'og_image_custom_url',
		'_seopress_social_fb_title'      => 'og_title',
		'_seopress_titles_desc'          => 'description',
		'_seopress_titles_title'         => 'title',
		'_seopress_robots_primary_cat'   => 'primary_term'
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 */
	public function scheduleImport() {
		if ( aioseo()->actionScheduler->scheduleSingle( aioseo()->importExport->seoPress->postActionName, 0 ) ) {
			if ( ! aioseo()->core->cache->get( 'import_post_meta_seopress' ) ) {
				aioseo()->core->cache->update( 'import_post_meta_seopress', time(), WEEK_IN_SECONDS );
			}
		}
	}

	/**
	 * Imports the post meta.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function importPostMeta() {
		$postsPerAction  = apply_filters( 'aioseo_import_seopress_posts_per_action', 100 );
		$publicPostTypes = implode( "', '", aioseo()->helpers->getPublicPostTypes( true ) );
		$timeStarted     = gmdate( 'Y-m-d H:i:s', aioseo()->core->cache->get( 'import_post_meta_seopress' ) );

		$posts = aioseo()->core->db
			->start( 'posts as p' )
			->select( 'p.ID, p.post_type' )
			->join( 'postmeta as pm', '`p`.`ID` = `pm`.`post_id`' )
			->leftJoin( 'aioseo_posts as ap', '`p`.`ID` = `ap`.`post_id`' )
			->whereRaw( "pm.meta_key LIKE '_seopress_%'" )
			->whereRaw( "( p.post_type IN ( '$publicPostTypes' ) )" )
			->whereRaw( "( ap.post_id IS NULL OR ap.updated < '$timeStarted' )" )
			->groupBy( 'p.ID' )
			->orderBy( 'p.ID DESC' )
			->limit( $postsPerAction )
			->run()
			->result();

		if ( ! $posts || ! count( $posts ) ) {
			aioseo()->core->cache->delete( 'import_post_meta_seopress' );

			return;
		}

		foreach ( $posts as $post ) {
			$postMeta = aioseo()->core->db
				->start( 'postmeta' . ' as pm' )
				->select( 'pm.meta_key, pm.meta_value' )
				->where( 'pm.post_id', $post->ID )
				->whereRaw( "`pm`.`meta_key` LIKE '_seopress_%'" )
				->run()
				->result();

			$meta = array_merge( [
				'post_id' => (int) $post->ID,
			], $this->getMetaData( $postMeta, $post->ID ) );

			if ( ! $postMeta || ! count( $postMeta ) ) {
				$aioseoPost = Models\Post::getPost( (int) $post->ID );
				$aioseoPost->set( $meta );
				$aioseoPost->save();

				aioseo()->migration->meta->migrateAdditionalPostMeta( $post->ID );

				continue;
			}

			$aioseoPost = Models\Post::getPost( (int) $post->ID );
			$aioseoPost->set( $meta );
			$aioseoPost->save();

			aioseo()->migration->meta->migrateAdditionalPostMeta( $post->ID );

			// Clear the Overview cache.
			aioseo()->postSettings->clearPostTypeOverviewCache( $post->ID );
		}

		if ( count( $posts ) === $postsPerAction ) {
			aioseo()->actionScheduler->scheduleSingle( aioseo()->importExport->seoPress->postActionName, 5, [], true );
		} else {
			aioseo()->core->cache->delete( 'import_post_meta_seopress' );
		}
	}

	/**
	 * Get the meta data by post meta.
	 *
	 * @since 4.1.4
	 *
	 * @param object $postMeta The post meta from database.
	 * @return array           The meta data.
	 */
	public function getMetaData( $postMeta, $postId ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$meta = [
			'robots_default'      => true,
			'robots_noarchive'    => false,
			'canonical_url'       => '',
			'robots_nofollow'     => false,
			'robots_noimageindex' => false,
			'robots_noindex'      => false,
			'robots_noodp'        => false,
			'robots_nosnippet'    => false,
			'twitter_use_og'      => aioseo()->options->social->twitter->general->useOgData,
			'twitter_title'       => '',
			'twitter_description' => ''
		];
		foreach ( $postMeta as $record ) {
			$name  = $record->meta_key;
			$value = $record->meta_value;

			if ( ! in_array( $name, array_keys( $this->mappedMeta ), true ) ) {
				continue;
			}

			switch ( $name ) {
				case '_seopress_analysis_target_kw':
					$keyphrases     = array_map( 'trim', explode( ',', $value ) );
					$keyphraseArray = [
						'focus'      => [ 'keyphrase' => aioseo()->helpers->sanitizeOption( $keyphrases[0] ) ],
						'additional' => []
					];
					unset( $keyphrases[0] );
					foreach ( $keyphrases as $keyphrase ) {
						$keyphraseArray['additional'][] = [ 'keyphrase' => aioseo()->helpers->sanitizeOption( $keyphrase ) ];
					}

					$meta['keyphrases'] = $keyphraseArray;
					break;
				case '_seopress_robots_snippet':
				case '_seopress_robots_archive':
				case '_seopress_robots_imageindex':
				case '_seopress_robots_odp':
				case '_seopress_robots_follow':
				case '_seopress_robots_index':
					if ( 'yes' === $value ) {
						$meta['robots_default']             = false;
						$meta[ $this->mappedMeta[ $name ] ] = true;
					}
					break;
				case '_seopress_social_twitter_img':
					$meta['twitter_use_og']             = false;
					$meta['twitter_image_type']         = 'custom_image';
					$meta[ $this->mappedMeta[ $name ] ] = esc_url( $value );
					break;
				case '_seopress_social_twitter_desc':
				case '_seopress_social_twitter_title':
					$meta['twitter_use_og']             = false;
					$meta[ $this->mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
					break;
				case '_seopress_social_fb_img':
					$meta['og_image_type']              = 'custom_image';
					$meta[ $this->mappedMeta[ $name ] ] = esc_url( $value );
					break;
				case '_seopress_robots_primary_cat':
					$taxonomy                           = 'category';
					$options                            = new \stdClass();
					$options->$taxonomy                 = (int) $value;
					$meta[ $this->mappedMeta[ $name ] ] = wp_json_encode( $options );
					break;
				case '_seopress_titles_title':
				case '_seopress_titles_desc':
					$value = aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $value );
				default:
					$meta[ $this->mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
					break;
			}
		}

		return $meta;
	}
}Common/ImportExport/SeoPress/GeneralSettings.php000066600000010307151135505570016036 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the General Settings.
 *
 * @since 4.1.4
 */
class GeneralSettings {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * List of our access control roles.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $roles = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 */
	public function __construct() {
		$this->options = get_option( 'seopress_advanced_option_name' );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->roles = aioseo()->access->getRoles();

		$this->migrateBlockMetaboxRoles();
		$this->migrateBlockContentAnalysisRoles();
		$this->migrateAttachmentRedirects();

		$settings = [
			'seopress_advanced_advanced_google'    => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'google' ] ],
			'seopress_advanced_advanced_bing'      => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'bing' ] ],
			'seopress_advanced_advanced_pinterest' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'pinterest' ] ],
			'seopress_advanced_advanced_yandex'    => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'yandex' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates Block AIOSEO metabox setting.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateBlockMetaboxRoles() {
		$seoPressRoles = ! empty( $this->options['seopress_advanced_security_metaboxe_role'] ) ? $this->options['seopress_advanced_security_metaboxe_role'] : '';
		if ( empty( $seoPressRoles ) ) {
			return;
		}

		$roleSettings = [ 'useDefault', 'pageAnalysis', 'pageGeneralSettings', 'pageSocialSettings', 'pageSchemaSettings', 'pageAdvancedSettings' ];

		foreach ( $seoPressRoles as $wpRole => $value ) {
			$role = $this->roles[ $wpRole ];
			if ( empty( $role ) || aioseo()->access->isAdmin( $role ) ) {
				continue;
			}

			if ( aioseo()->options->accessControl->has( $role ) ) {
				foreach ( $roleSettings as $setting ) {
					aioseo()->options->accessControl->$role->$setting = false;
				}
			} elseif ( aioseo()->dynamicOptions->accessControl->has( $role ) ) {
				foreach ( $roleSettings as $setting ) {
					aioseo()->dynamicOptions->accessControl->$role->$setting = false;
				}
			}
		}
	}

	/**
	 * Migrates Block Content analysis metabox setting.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateBlockContentAnalysisRoles() {
		$seoPressRoles = ! empty( $this->options['seopress_advanced_security_metaboxe_ca_role'] ) ? $this->options['seopress_advanced_security_metaboxe_ca_role'] : '';
		if ( empty( $seoPressRoles ) ) {
			return;
		}

		$roleSettings = [ 'useDefault', 'pageAnalysis' ];

		foreach ( $seoPressRoles as $wpRole => $value ) {
			$role = $this->roles[ $wpRole ];
			if ( empty( $role ) || aioseo()->access->isAdmin( $role ) ) {
				continue;
			}

			if ( aioseo()->options->accessControl->has( $role ) ) {
				foreach ( $roleSettings as $setting ) {
					aioseo()->options->accessControl->$role->$setting = false;
				}
			} elseif ( aioseo()->dynamicOptions->accessControl->has( $role ) ) {
				foreach ( $roleSettings as $setting ) {
					aioseo()->dynamicOptions->accessControl->$role->$setting = false;
				}
			}
		}
	}

	/**
	 * Migrates redirect attachment pages settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateAttachmentRedirects() {
		if ( ! empty( $this->options['seopress_advanced_advanced_attachments'] ) ) {
			aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'attachment_parent';
		}

		if ( ! empty( $this->options['seopress_advanced_advanced_attachments_file'] ) ) {
			aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'attachment';
		}

		if ( empty( $this->options['seopress_advanced_advanced_attachments'] ) && empty( $this->options['seopress_advanced_advanced_attachments_file'] ) ) {
			aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'disabled';
		}
	}
}Common/ImportExport/SeoPress/SocialMeta.php000066600000012322151135505570014760 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Social Meta Settings.
 *
 * @since 4.1.4
 */
class SocialMeta {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 */
	public function __construct() {
		$this->options = get_option( 'seopress_social_option_name' );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->migrateSocialUrls();
		$this->migrateKnowledge();
		$this->migrateFacebookSettings();
		$this->migrateTwitterSettings();
	}

	/**
	 * Migrates Basic Social Profiles URLs.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateSocialUrls() {
		$settings = [
			'seopress_social_accounts_facebook'   => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'facebookPageUrl' ] ],
			'seopress_social_accounts_twitter'    => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'twitterUrl' ] ],
			'seopress_social_accounts_pinterest'  => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'pinterestUrl' ] ],
			'seopress_social_accounts_instagram'  => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'instagramUrl' ] ],
			'seopress_social_accounts_youtube'    => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'youtubeUrl' ] ],
			'seopress_social_accounts_linkedin'   => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'linkedinUrl' ] ],
			'seopress_social_accounts_myspace'    => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'myspaceUrl' ] ],
			'seopress_social_accounts_soundcloud' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'soundCloudUrl' ] ],
			'seopress_social_accounts_tumblr'     => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'tumblrUrl' ] ],
			'seopress_social_accounts_wordpress'  => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'wordPressUrl' ] ],
			'seopress_social_accounts_bluesky'    => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'blueskyUrl' ] ],
			'seopress_social_accounts_threads'    => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'threadsUrl' ] ]
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates Knowledge Graph data.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateKnowledge() {
		$type = 'organization';
		if ( ! empty( $this->options['seopress_social_knowledge_type'] ) ) {
			$type = strtolower( $this->options['seopress_social_knowledge_type'] );
			if ( 'person' === $type ) {
				aioseo()->options->searchAppearance->global->schema->person = 'manual';
			}
		}

		aioseo()->options->searchAppearance->global->schema->siteRepresents = $type;

		$settings = [
			'seopress_social_knowledge_img'   => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', $type . 'Logo' ] ],
			'seopress_social_knowledge_name'  => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', $type . 'Name' ] ],
			'seopress_social_knowledge_phone' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'phone' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the Facebook settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateFacebookSettings() {
		if ( ! empty( $this->options['seopress_social_facebook_admin_id'] ) || ! empty( $this->options['seopress_social_facebook_app_id'] ) ) {
			aioseo()->options->social->facebook->advanced->enable = true;
		}

		$settings = [
			'seopress_social_facebook_og'       => [ 'type' => 'boolean', 'newOption' => [ 'social', 'facebook', 'general', 'enable' ] ],
			'seopress_social_facebook_img'      => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'image' ] ],
			'seopress_social_facebook_admin_id' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'advanced', 'adminId' ] ],
			'seopress_social_facebook_app_id'   => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'advanced', 'appId' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the Twitter settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateTwitterSettings() {
		if ( ! empty( $this->options['seopress_social_twitter_card_img_size'] ) ) {
			$twitterCard = ( 'large' === $this->options['seopress_social_twitter_card_img_size'] ) ? 'summary-card' : 'summary';
			aioseo()->options->social->twitter->general->defaultCardType = $twitterCard;
		}

		$settings = [
			'seopress_social_twitter_card'     => [ 'type' => 'boolean', 'newOption' => [ 'social', 'twitter', 'general', 'enable' ] ],
			'seopress_social_twitter_card_img' => [ 'type' => 'string', 'newOption' => [ 'social', 'twitter', 'homePage', 'image' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}
}Common/ImportExport/SeoPress/Sitemap.php000066600000003600151135505570014340 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Sitemap Settings.
 *
 * @since 4.1.4
 */
class Sitemap {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 */
	public function __construct() {
		$this->options = get_option( 'seopress_xml_sitemap_option_name' );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->migratePostTypesInclude();
		$this->migrateTaxonomiesInclude();

		$settings = [
			'seopress_xml_sitemap_general_enable' => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'general', 'enable' ] ],
			'seopress_xml_sitemap_author_enable'  => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'general', 'author' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the post types to include in sitemap settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function migratePostTypesInclude() {
		$postTypesMigrate = $this->options['seopress_xml_sitemap_post_types_list'];
		$postTypesInclude = [];

		foreach ( $postTypesMigrate as $postType => $options ) {
			$postTypesInclude[] = $postType;
		}

		aioseo()->options->sitemap->general->postTypes->included = $postTypesInclude;
	}

	/**
	 * Migrates the taxonomies to include in sitemap settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function migrateTaxonomiesInclude() {
		$taxonomiesMigrate = $this->options['seopress_xml_sitemap_taxonomies_list'];
		$taxonomiesInclude = [];

		foreach ( $taxonomiesMigrate as $taxonomy => $options ) {
			$taxonomiesInclude[] = $taxonomy;
		}

		aioseo()->options->sitemap->general->taxonomies->included = $taxonomiesInclude;
	}
}Common/ImportExport/SeoPress/Rss.php000066600000002242151135505570013506 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the RSS settings.
 *
 * @since 4.1.4
 */
class Rss {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 */
	public function __construct() {
		$this->options = get_option( 'seopress_pro_option_name' );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->migrateRss();
	}

	/**
	 * Migrates the RSS settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function migrateRss() {
		if ( ! empty( $this->options['seopress_rss_before_html'] ) ) {
			aioseo()->options->rssContent->before = esc_html( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $this->options['seopress_rss_before_html'] ) );
		}

		if ( ! empty( $this->options['seopress_rss_after_html'] ) ) {
			aioseo()->options->rssContent->after = esc_html( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $this->options['seopress_rss_after_html'] ) );
		}
	}
}Common/ImportExport/SeoPress/Titles.php000066600000026226151135505570014213 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Titles Settings.
 *
 * @since 4.1.4
 */
class Titles {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 */
	public function __construct() {
		$this->options = get_option( 'seopress_titles_option_name' );
		if ( empty( $this->options ) ) {
			return;
		}

		if (
			! empty( $this->options['seopress_titles_archives_author_title'] ) ||
			! empty( $this->options['seopress_titles_archives_author_desc'] ) ||
			! empty( $this->options['seopress_titles_archives_author_noindex'] )
			) {
			aioseo()->options->searchAppearance->archives->author->show = true;
		}

		if (
			! empty( $this->options['seopress_titles_archives_date_title'] ) ||
			! empty( $this->options['seopress_titles_archives_date_desc'] ) ||
			! empty( $this->options['seopress_titles_archives_date_noindex'] )
			) {
			aioseo()->options->searchAppearance->archives->date->show = true;
		}

		if (
			! empty( $this->options['seopress_titles_archives_search_title'] ) ||
			! empty( $this->options['seopress_titles_archives_search_desc'] )
			) {
			aioseo()->options->searchAppearance->archives->search->show = true;
		}

		$this->migrateTitleFormats();
		$this->migrateDescriptionFormats();
		$this->migrateNoIndexFormats();
		$this->migratePostTypeSettings();
		$this->migrateTaxonomiesSettings();
		$this->migrateArchiveSettings();
		$this->migrateAdvancedSettings();

		$settings = [
			'seopress_titles_sep' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'separator' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options, true );
	}

	/**
	 * Migrates the title formats.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateTitleFormats() {
		$settings = [
			'seopress_titles_home_site_title'       => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'siteTitle' ] ],
			'seopress_titles_archives_author_title' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'author', 'title' ] ],
			'seopress_titles_archives_date_title'   => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'date', 'title' ] ],
			'seopress_titles_archives_search_title' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'search', 'title' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options, true );
	}

	/**
	 * Migrates the description formats.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateDescriptionFormats() {
		$settings = [
			'seopress_titles_home_site_desc'       => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'metaDescription' ] ],
			'seopress_titles_archives_author_desc' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'author', 'metaDescription' ] ],
			'seopress_titles_archives_date_desc'   => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'date', 'metaDescription' ] ],
			'seopress_titles_archives_search_desc' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'search', 'metaDescription' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options, true );
	}

	/**
	 * Migrates the NoIndex formats.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateNoIndexFormats() {
		$settings = [
			'seopress_titles_archives_author_noindex' => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'archives', 'author', 'show' ] ],
			'seopress_titles_archives_date_noindex'   => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'archives', 'date', 'show' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the post type settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migratePostTypeSettings() {
		$titles = $this->options['seopress_titles_single_titles'];
		if ( empty( $titles ) ) {
			return;
		}

		foreach ( $titles as $postType => $options ) {
			if ( ! aioseo()->dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
				continue;
			}

			if ( ! empty( $options['title'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->title =
					aioseo()->helpers->sanitizeOption( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $options['title'] ) );
			}

			if ( ! empty( $options['description'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->metaDescription =
					aioseo()->helpers->sanitizeOption( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $options['description'] ) );
			}

			if ( ! empty( $options['enable'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->showMetaBox = false;
			}

			if ( ! empty( $options['noindex'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->show = false;
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = false;
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->noindex = true;
			}

			if ( ! empty( $options['nofollow'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->show = false;
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = false;
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->nofollow = true;
			}

			if ( ! empty( $options['date'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->showDateInGooglePreview = false;
			}

			if ( ! empty( $options['thumb_gcs'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->showPostThumbnailInSearch = true;
			}
		}
	}

	/**
	 * Migrates the taxonomies settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateTaxonomiesSettings() {
		$titles = ! empty( $this->options['seopress_titles_tax_titles'] ) ? $this->options['seopress_titles_tax_titles'] : '';
		if ( empty( $titles ) ) {
			return;
		}

		foreach ( $titles as $taxonomy => $options ) {
			if ( ! aioseo()->dynamicOptions->searchAppearance->taxonomies->has( $taxonomy ) ) {
				continue;
			}

			if ( ! empty( $options['title'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->title =
					aioseo()->helpers->sanitizeOption( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $options['title'] ) );
			}

			if ( ! empty( $options['description'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->metaDescription =
					aioseo()->helpers->sanitizeOption( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $options['description'] ) );
			}

			if ( ! empty( $options['enable'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->showMetaBox = false;
			}

			if ( ! empty( $options['noindex'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->show = false;
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->default = false;
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->noindex = true;
			}

			if ( ! empty( $options['nofollow'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->show = false;
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->default = false;
				aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->nofollow = true;
			}
		}
	}

	/**
	 * Migrates the archives settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateArchiveSettings() {
		$titles = $this->options['seopress_titles_archive_titles'];
		if ( empty( $titles ) ) {
			return;
		}

		foreach ( $titles as $archive => $options ) {
			if ( ! aioseo()->dynamicOptions->searchAppearance->archives->has( $archive ) ) {
				continue;
			}

			if ( ! empty( $options['title'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->archives->$archive->title =
					aioseo()->helpers->sanitizeOption( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $options['title'] ) );
			}

			if ( ! empty( $options['description'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->archives->$archive->metaDescription =
					aioseo()->helpers->sanitizeOption( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $options['description'] ) );
			}

			if ( ! empty( $options['noindex'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->archives->$archive->show = false;
				aioseo()->dynamicOptions->searchAppearance->archives->$archive->advanced->robotsMeta->default = false;
				aioseo()->dynamicOptions->searchAppearance->archives->$archive->advanced->robotsMeta->noindex = true;
			}

			if ( ! empty( $options['nofollow'] ) ) {
				aioseo()->dynamicOptions->searchAppearance->archives->$archive->show = false;
				aioseo()->dynamicOptions->searchAppearance->archives->$archive->advanced->robotsMeta->default = false;
				aioseo()->dynamicOptions->searchAppearance->archives->$archive->advanced->robotsMeta->nofollow = true;
			}
		}
	}

	/**
	 * Migrates the advanced settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateAdvancedSettings() {
		if (
			! empty( $this->options['seopress_titles_noindex'] ) || ! empty( $this->options['seopress_titles_nofollow'] ) || ! empty( $this->options['seopress_titles_noodp'] ) ||
			! empty( $this->options['seopress_titles_noimageindex'] ) || ! empty( $this->options['seopress_titles_noarchive'] ) ||
			! empty( $this->options['seopress_titles_nosnippet'] ) || ! empty( $this->options['seopress_titles_paged_noindex'] )
		) {
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default = false;
		}

		$settings = [
			'seopress_titles_noindex'       => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'noindex' ] ],
			'seopress_titles_nofollow'      => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'nofollow' ] ],
			'seopress_titles_noodp'         => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'noodp' ] ],
			'seopress_titles_noimageindex'  => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'noimageindex' ] ],
			'seopress_titles_noarchive'     => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'noarchive' ] ],
			'seopress_titles_nosnippet'     => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'nosnippet' ] ],
			'seopress_titles_paged_noindex' => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'noindexPaginated' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}
}Common/ImportExport/SeoPress/Breadcrumbs.php000066600000003403151135505570015170 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Breadcrumb settings.
 *
 * @since 4.1.4
 */
class Breadcrumbs {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 */
	public function __construct() {
		$this->options = get_option( 'seopress_pro_option_name' );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->migrate();
	}

	/**
	 * Migrates the Breadcrumbs settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrate() {
		if ( ! empty( $this->options['seopress_breadcrumbs_i18n_search'] ) ) {
			aioseo()->options->breadcrumbs->searchResultFormat = sprintf( '%1$s #breadcrumb_archive_post_type_name', $this->options['seopress_breadcrumbs_i18n_search'] );
		}

		if ( ! empty( $this->options['seopress_breadcrumbs_remove_blog_page'] ) ) {
			aioseo()->options->breadcrumbs->showBlogHome = false;
		}

		$settings = [
			'seopress_breadcrumbs_enable'    => [ 'type' => 'boolean', 'newOption' => [ 'breadcrumbs', 'enable' ] ],
			'seopress_breadcrumbs_separator' => [ 'type' => 'string', 'newOption' => [ 'breadcrumbs', 'separator' ] ],
			'seopress_breadcrumbs_i18n_home' => [ 'type' => 'string', 'newOption' => [ 'breadcrumbs', 'homepageLabel' ] ],
			'seopress_breadcrumbs_i18n_here' => [ 'type' => 'string', 'newOption' => [ 'breadcrumbs', 'breadcrumbPrefix' ] ],
			'seopress_breadcrumbs_i18n_404'  => [ 'type' => 'string', 'newOption' => [ 'breadcrumbs', 'errorFormat404' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}
}Common/ImportExport/SeoPress/SeoPress.php000066600000002761151135505570014510 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\ImportExport;

class SeoPress extends ImportExport\Importer {
	/**
	 * A list of plugins to look for to import.
	 *
	 * @since 4.1.4
	 *
	 * @var array
	 */
	public $plugins = [
		[
			'name'     => 'SEOPress',
			'version'  => '4.0',
			'basename' => 'wp-seopress/seopress.php',
			'slug'     => 'seopress'
		],
		[
			'name'     => 'SEOPress PRO',
			'version'  => '4.0',
			'basename' => 'wp-seopress-pro/seopress-pro.php',
			'slug'     => 'seopress-pro'
		],
	];

	/**
	 * The post action name.
	 *
	 * @since 4.1.4
	 *
	 * @var string
	 */
	public $postActionName = 'aioseo_import_post_meta_seopress';

	/**
	 * The post action name.
	 *
	 * @since 4.1.4
	 *
	 * @param ImportExport\ImportExport $importer The main importer class.
	 */
	public function __construct( $importer ) {
		$this->helpers  = new Helpers();
		$this->postMeta = new PostMeta();
		add_action( $this->postActionName, [ $this->postMeta, 'importPostMeta' ] );

		$plugins = $this->plugins;
		foreach ( $plugins as $key => $plugin ) {
			$plugins[ $key ]['class'] = $this;
		}
		$importer->addPlugins( $plugins );
	}

	/**
	 * Imports the settings.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	protected function importSettings() {
		new GeneralSettings();
		new Analytics();
		new SocialMeta();
		new Titles();
		new Sitemap();
		new RobotsTxt();
		new Rss();
		new Breadcrumbs();
	}
}Common/ImportExport/SeoPress/Helpers.php000066600000007264151135505570014352 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\ImportExport;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Contains helper methods for the import from SEOPress.
 *
 * @since 4.1.4
 */
class Helpers extends ImportExport\Helpers {
	/**
	 * Converts the macros from SEOPress to our own smart tags.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $string   The string with macros.
	 * @param  string $postType The post type.
	 * @return string           The string with smart tags.
	 */
	public function macrosToSmartTags( $string, $postType = null ) {
		$macros = $this->getMacros( $postType );

		foreach ( $macros as $macro => $tag ) {
			$string = aioseo()->helpers->pregReplace( "#$macro(?![a-zA-Z0-9_])#im", $tag, $string );
		}

		return trim( $string );
	}

	/**
	 * Returns the macro mappings.
	 *
	 * @since 4.1.4
	 *
	 * @param  string $postType The post type.
	 * @param  string $pageType The page type.
	 * @return array  $macros   The macros.
	 */
	protected function getMacros( $postType = null, $pageType = null ) {
		$macros = [
			'%%sep%%'                   => '#separator_sa',
			'%%sitetitle%%'             => '#site_title',
			'%%sitename%%'              => '#site_title',
			'%%tagline%%'               => '#tagline',
			'%%sitedesc%%'              => '#tagline',
			'%%title%%'                 => '#site_title',
			'%%post_title%%'            => '#post_title',
			'%%post_excerpt%%'          => '#post_excerpt',
			'%%excerpt%%'               => '#post_excerpt',
			'%%post_content%%'          => '#post_content',
			'%%post_url%%'              => '#permalink',
			'%%post_date%%'             => '#post_date',
			'%%post_permalink%%'        => '#permalink',
			'%%date%%'                  => '#post_date',
			'%%post_author%%'           => '#author_name',
			'%%post_category%%'         => '#categories',
			'%%_category_title%%'       => '#taxonomy_title',
			'%%_category_description%%' => '#taxonomy_description',
			'%%tag_title%%'             => '#taxonomy_title',
			'%%tag_description%%'       => '#taxonomy_description',
			'%%term_title%%'            => '#taxonomy_title',
			'%%term_description%%'      => '#taxonomy_description',
			'%%search_keywords%%'       => '#search_term',
			'%%current_pagination%%'    => '#page_number',
			'%%page%%'                  => '#page_number',
			'%%archive_title%%'         => '#archive_title',
			'%%archive_date%%'          => '#archive_date',
			'%%wc_single_price%%'       => '#woocommerce_price',
			'%%wc_sku%%'                => '#woocommerce_sku',
			'%%currentday%%'            => '#current_day',
			'%%currentmonth%%'          => '#current_month',
			'%%currentmonth_short%%'    => '#current_month',
			'%%currentyear%%'           => '#current_year',
			'%%currentdate%%'           => '#current_date',
			'%%author_first_name%%'     => '#author_first_name',
			'%%author_last_name%%'      => '#author_last_name',
			'%%author_website%%'        => '#author_link',
			'%%author_nickname%%'       => '#author_first_name',
			'%%author_bio%%'            => '#author_bio',
			'%%currentmonth_num%%'      => '#current_month',
		];

		if ( $postType ) {
			$postType = get_post_type_object( $postType );
			if ( ! empty( $postType ) ) {
				$macros += [
					'%%cpt_plural%%' => $postType->labels->name,
				];
			}
		}

		switch ( $pageType ) {
			case 'archive':
				$macros['%%title%%'] = '#archive_title';
				break;
			case 'term':
				$macros['%%title%%'] = '#taxonomy_title';
				break;
			default:
				$macros['%%title%%'] = '#post_title';
				break;
		}

		// Strip all other tags.
		$macros['%%[^%]*%%'] = '';

		return $macros;
	}
}Common/ImportExport/SeoPress/RobotsTxt.php000066600000002357151135505570014716 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the robots.txt settings.
 *
 * @since 4.1.4
 */
class RobotsTxt {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 */
	public function __construct() {
		$this->options = get_option( 'seopress_pro_option_name', [] );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->migrateRobotsTxt();

		$settings = [
			'seopress_robots_enable' => [ 'type' => 'boolean', 'newOption' => [ 'tools', 'robots', 'enable' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the robots.txt.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function migrateRobotsTxt() {
		$lines = ! empty( $this->options['seopress_robots_file'] ) ? (string) $this->options['seopress_robots_file'] : '';

		if ( $lines ) {
			$allRules = aioseo()->robotsTxt->extractRules( $lines );

			aioseo()->options->tools->robots->rules = aioseo()->robotsTxt->prepareRobotsTxt( $allRules );
		}
	}
}Common/ImportExport/SeoPress/Analytics.php000066600000001527151135505570014673 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Analytics Settings.
 *
 * @since 4.1.4
 */
class Analytics {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.1.4
	 */
	public function __construct() {
		$this->options = get_option( 'seopress_google_analytics_option_name' );
		if ( empty( $this->options ) ) {
			return;
		}

		$settings = [
			'seopress_google_analytics_other_tracking' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'miscellaneousVerification' ] ],
		];

		aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
	}
}Common/ImportExport/YoastSeo/GeneralSettings.php000066600000002233151135505570016040 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the General Settings.
 *
 * @since 4.0.0
 */
class GeneralSettings {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->options = get_option( 'wpseo' );
		if ( empty( $this->options ) ) {
			return;
		}

		$settings = [
			'googleverify'       => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'google' ] ],
			'msverify'           => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'bing' ] ],
			'yandexverify'       => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'yandex' ] ],
			'baiduverify'        => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'baidu' ] ],
			'enable_xml_sitemap' => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'general', 'enable' ] ]
		];

		aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options );
	}
}Common/ImportExport/YoastSeo/YoastSeo.php000066600000004174151135505570014516 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\ImportExport;

class YoastSeo extends ImportExport\Importer {
	/**
	 * A list of plugins to look for to import.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public $plugins = [
		[
			'name'     => 'Yoast SEO',
			'version'  => '14.0',
			'basename' => 'wordpress-seo/wp-seo.php',
			'slug'     => 'yoast-seo'
		],
		[
			'name'     => 'Yoast SEO Premium',
			'version'  => '14.0',
			'basename' => 'wordpress-seo-premium/wp-seo-premium.php',
			'slug'     => 'yoast-seo-premium'
		],
	];

	/**
	 * The post action name.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	public $postActionName = 'aioseo_import_post_meta_yoast_seo';

	/**
	 * The user action name.
	 *
	 * @since 4.1.4
	 *
	 * @var string
	 */
	public $userActionName = 'aioseo_import_user_meta_yoast_seo';

	/**
	 * UserMeta class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var UserMeta
	 */
	private $userMeta = null;

	/**
	 * SearchAppearance class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var SearchAppearance
	 */
	public $searchAppearance = null;

	/**
	 * The post action name.
	 *
	 * @since 4.0.0
	 *
	 * @param ImportExport\ImportExport $importer The main importer class.
	 */
	public function __construct( $importer ) {
		$this->helpers  = new Helpers();
		$this->postMeta = new PostMeta();
		$this->userMeta = new UserMeta();

		add_action( $this->postActionName, [ $this->postMeta, 'importPostMeta' ] );
		add_action( $this->userActionName, [ $this->userMeta, 'importUserMeta' ] );

		$plugins = $this->plugins;
		foreach ( $plugins as $key => $plugin ) {
			$plugins[ $key ]['class'] = $this;
		}
		$importer->addPlugins( $plugins );
	}

	/**
	 * Imports the settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function importSettings() {
		new GeneralSettings();
		$this->searchAppearance = new SearchAppearance();
		// NOTE: The Social Meta settings need to be imported after the Search Appearance ones because some imports depend on what was imported there.
		new SocialMeta();
		$this->userMeta->scheduleImport();
	}
}Common/ImportExport/YoastSeo/UserMeta.php000066600000005176151135505570014500 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\ImportExport;
use AIOSEO\Plugin\Common\Models;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Imports the user meta from Yoast SEO.
 *
 * @since 4.0.0
 */
class UserMeta {
	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function scheduleImport() {
		aioseo()->actionScheduler->scheduleSingle( aioseo()->importExport->yoastSeo->userActionName, 30 );

		if ( ! aioseo()->core->cache->get( 'import_user_meta_yoast_seo' ) ) {
			aioseo()->core->cache->update( 'import_user_meta_yoast_seo', 0, WEEK_IN_SECONDS );
		}
	}

	/**
	 * Imports the post meta.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function importUserMeta() {
		$usersPerAction = 100;
		$offset         = aioseo()->core->cache->get( 'import_user_meta_yoast_seo' );

		$usersMeta = aioseo()->core->db
			->start( aioseo()->core->db->db->usermeta . ' as um', true )
			->whereRaw( "um.meta_key IN ('facebook', 'twitter', 'instagram', 'linkedin', 'myspace', 'pinterest', 'soundcloud', 'tumblr', 'wikipedia', 'youtube', 'mastodon', 'bluesky', 'threads')" )
			->whereRaw( "um.meta_value != ''" )
			->limit( $usersPerAction, $offset )
			->run()
			->result();

		if ( ! $usersMeta || ! count( $usersMeta ) ) {
			aioseo()->core->cache->delete( 'import_user_meta_yoast_seo' );

			return;
		}

		$mappedMeta = [
			'facebook'   => 'aioseo_facebook_page_url',
			'twitter'    => 'aioseo_twitter_url',
			'instagram'  => 'aioseo_instagram_url',
			'linkedin'   => 'aioseo_linkedin_url',
			'myspace'    => 'aioseo_myspace_url',
			'pinterest'  => 'aioseo_pinterest_url',
			'soundcloud' => 'aioseo_sound_cloud_url',
			'tumblr'     => 'aioseo_tumblr_url',
			'wikipedia'  => 'aioseo_wikipedia_url',
			'youtube'    => 'aioseo_youtube_url',
			'bluesky'    => 'aioseo_bluesky_url',
			'threads'    => 'aioseo_threads_url',
			'mastodon'   => 'aioseo_profiles_additional_urls'
		];

		foreach ( $usersMeta as $meta ) {
			if ( isset( $mappedMeta[ $meta->meta_key ] ) ) {
				$value = 'twitter' === $meta->meta_key ? 'https://x.com/' . $meta->meta_value : $meta->meta_value;
				update_user_meta( $meta->user_id, $mappedMeta[ $meta->meta_key ], $value );
			}
		}

		if ( count( $usersMeta ) === $usersPerAction ) {
			aioseo()->core->cache->update( 'import_user_meta_yoast_seo', 100 + $offset, WEEK_IN_SECONDS );
			aioseo()->actionScheduler->scheduleSingle( aioseo()->importExport->yoastSeo->userActionName, 5, [], true );
		} else {
			aioseo()->core->cache->delete( 'import_user_meta_yoast_seo' );
		}
	}
}Common/ImportExport/YoastSeo/PostMeta.php000066600000025720151135505570014504 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\ImportExport;
use AIOSEO\Plugin\Common\Models;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Imports the post meta from Yoast SEO.
 *
 * @since 4.0.0
 */
class PostMeta {
	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function scheduleImport() {
		try {
			if ( as_next_scheduled_action( aioseo()->importExport->yoastSeo->postActionName ) ) {
				return;
			}

			if ( ! aioseo()->core->cache->get( 'import_post_meta_yoast_seo' ) ) {
				aioseo()->core->cache->update( 'import_post_meta_yoast_seo', time(), WEEK_IN_SECONDS );
			}

			as_schedule_single_action( time(), aioseo()->importExport->yoastSeo->postActionName, [], 'aioseo' );
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Imports the post meta.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function importPostMeta() {
		$postsPerAction  = apply_filters( 'aioseo_import_yoast_seo_posts_per_action', 100 );
		$publicPostTypes = implode( "', '", aioseo()->helpers->getPublicPostTypes( true ) );
		$timeStarted     = gmdate( 'Y-m-d H:i:s', aioseo()->core->cache->get( 'import_post_meta_yoast_seo' ) );

		$posts = aioseo()->core->db
			->start( 'posts' . ' as p' )
			->select( 'p.ID, p.post_type' )
			->leftJoin( 'aioseo_posts as ap', '`p`.`ID` = `ap`.`post_id`' )
			->whereRaw( "( p.post_type IN ( '$publicPostTypes' ) )" )
			->whereRaw( "( ap.post_id IS NULL OR ap.updated < '$timeStarted' )" )
			->orderBy( 'p.ID DESC' )
			->limit( $postsPerAction )
			->run()
			->result();

		if ( ! $posts || ! count( $posts ) ) {
			aioseo()->core->cache->delete( 'import_post_meta_yoast_seo' );

			return;
		}

		$mappedMeta = [
			'_yoast_wpseo_title'                 => 'title',
			'_yoast_wpseo_metadesc'              => 'description',
			'_yoast_wpseo_canonical'             => 'canonical_url',
			'_yoast_wpseo_meta-robots-noindex'   => 'robots_noindex',
			'_yoast_wpseo_meta-robots-nofollow'  => 'robots_nofollow',
			'_yoast_wpseo_meta-robots-adv'       => '',
			'_yoast_wpseo_focuskw'               => '',
			'_yoast_wpseo_focuskeywords'         => '',
			'_yoast_wpseo_opengraph-title'       => 'og_title',
			'_yoast_wpseo_opengraph-description' => 'og_description',
			'_yoast_wpseo_opengraph-image'       => 'og_image_custom_url',
			'_yoast_wpseo_twitter-title'         => 'twitter_title',
			'_yoast_wpseo_twitter-description'   => 'twitter_description',
			'_yoast_wpseo_twitter-image'         => 'twitter_image_custom_url',
			'_yoast_wpseo_schema_page_type'      => '',
			'_yoast_wpseo_schema_article_type'   => '',
			'_yoast_wpseo_is_cornerstone'        => 'pillar_content'
		];

		foreach ( $posts as $post ) {
			$postMeta = aioseo()->core->db
				->start( 'postmeta' . ' as pm' )
				->select( 'pm.meta_key, pm.meta_value' )
				->where( 'pm.post_id', $post->ID )
				->whereRaw( "`pm`.`meta_key` LIKE '_yoast_wpseo_%'" )
				->run()
				->result();

			$featuredImage = get_the_post_thumbnail_url( $post->ID );
			$meta          = [
				'post_id'                  => (int) $post->ID,
				'twitter_use_og'           => true,
				'og_image_type'            => $featuredImage ? 'featured' : 'content',
				'pillar_content'           => 0,
				'canonical_url'            => '',
				'robots_default'           => true,
				'robots_noarchive'         => false,
				'robots_nofollow'          => false,
				'robots_noimageindex'      => false,
				'robots_noindex'           => false,
				'robots_noodp'             => false,
				'robots_nosnippet'         => false,
				'title'                    => '',
				'description'              => '',
				'og_title'                 => '',
				'og_description'           => '',
				'og_image_custom_url'      => '',
				'twitter_title'            => '',
				'twitter_description'      => '',
				'twitter_image_custom_url' => '',
				'twitter_image_type'       => 'default'
			];

			if ( ! $postMeta || ! count( $postMeta ) ) {
				$aioseoPost = Models\Post::getPost( (int) $post->ID );
				$aioseoPost->set( $meta );
				$aioseoPost->save();

				aioseo()->migration->meta->migrateAdditionalPostMeta( $post->ID );
				continue;
			}

			$title = '';
			foreach ( $postMeta as $record ) {
				$name  = $record->meta_key;
				$value = $record->meta_value;

				// Handles primary taxonomy terms.
				// We need to handle it separately because it's stored in a different format.
				if ( false !== stripos( $name, '_yoast_wpseo_primary_' ) ) {
					sscanf( $name, '_yoast_wpseo_primary_%s', $taxonomy );
					if ( null === $taxonomy ) {
						continue;
					}

					$options = new \stdClass();
					if ( isset( $meta['primary_term'] ) ) {
						$options = json_decode( $meta['primary_term'] );
					}

					$options->$taxonomy   = (int) $value;
					$meta['primary_term'] = wp_json_encode( $options );
				}

				if ( ! in_array( $name, array_keys( $mappedMeta ), true ) ) {
					continue;
				}

				switch ( $name ) {
					case '_yoast_wpseo_meta-robots-noindex':
					case '_yoast_wpseo_meta-robots-nofollow':
						if ( (bool) $value ) {
							$meta[ $mappedMeta[ $name ] ] = (bool) $value;
							$meta['robots_default']       = false;
						}
						break;
					case '_yoast_wpseo_meta-robots-adv':
						$supportedValues = [ 'index', 'noarchive', 'noimageindex', 'nosnippet' ];
						foreach ( $supportedValues as $val ) {
							$meta[ "robots_$val" ] = false;
						}

						// This is a separated foreach so we can import any and all values.
						$values = explode( ',', $value );
						if ( $values ) {
							$meta['robots_default'] = false;

							foreach ( $values as $value ) {
								$meta[ "robots_$value" ] = true;
							}
						}
						break;
					case '_yoast_wpseo_canonical':
						$meta[ $mappedMeta[ $name ] ] = esc_url( $value );
						break;
					case '_yoast_wpseo_opengraph-image':
						$meta['og_image_type']        = 'custom_image';
						$meta[ $mappedMeta[ $name ] ] = esc_url( $value );
						break;
					case '_yoast_wpseo_twitter-image':
						$meta['twitter_use_og']       = false;
						$meta['twitter_image_type']   = 'custom_image';
						$meta[ $mappedMeta[ $name ] ] = esc_url( $value );
						break;
					case '_yoast_wpseo_schema_page_type':
						$value = aioseo()->helpers->pregReplace( '#\s#', '', $value );
						if ( in_array( $post->post_type, [ 'post', 'page', 'attachment' ], true ) ) {
							break;
						}

						if ( ! in_array( $value, ImportExport\SearchAppearance::$supportedWebPageGraphs, true ) ) {
							break;
						}

						$meta[ $mappedMeta[ $name ] ] = 'WebPage';
						$meta['schema_type_options']  = wp_json_encode( [
							'webPage' => [
								'webPageType' => $value
							]
						] );
						break;
					case '_yoast_wpseo_schema_article_type':
						$value = aioseo()->helpers->pregReplace( '#\s#', '', $value );
						if ( 'none' === lcfirst( $value ) ) {
							$meta[ $mappedMeta[ $name ] ] = 'None';
							break;
						}

						if ( in_array( $post->post_type, [ 'page', 'attachment' ], true ) ) {
							break;
						}

						$options = new \stdClass();
						if ( isset( $meta['schema_type_options'] ) ) {
							$options = json_decode( $meta['schema_type_options'] );
						}

						$options->article = [ 'articleType' => 'Article' ];
						if ( in_array( $value, ImportExport\SearchAppearance::$supportedArticleGraphs, true ) ) {
							$options->article = [ 'articleType' => $value ];
						} else {
							$options->article = [ 'articleType' => 'BlogPosting' ];
						}

						$meta['schema_type']         = 'Article';
						$meta['schema_type_options'] = wp_json_encode( $options );
						break;
					case '_yoast_wpseo_focuskw':
						$focusKeyphrase = [
							'focus' => [ 'keyphrase' => aioseo()->helpers->sanitizeOption( $value ) ]
						];

						// Merge with existing keyphrases if the array key already exists.
						if ( ! empty( $meta['keyphrases'] ) ) {
							$meta['keyphrases'] = array_merge( $meta['keyphrases'], $focusKeyphrase );
						} else {
							$meta['keyphrases'] = $focusKeyphrase;
						}
						break;
					case '_yoast_wpseo_focuskeywords':
						$keyphrases = [];
						if ( ! empty( $meta[ $mappedMeta[ $name ] ] ) ) {
							$keyphrases = (array) json_decode( $meta[ $mappedMeta[ $name ] ] );
						}

						$yoastKeyphrases = json_decode( $value, true );
						if ( is_array( $yoastKeyphrases ) ) {
							foreach ( $yoastKeyphrases as $yoastKeyphrase ) {
								if ( ! empty( $yoastKeyphrase['keyword'] ) ) {
									$keyphrase = [ 'keyphrase' => aioseo()->helpers->sanitizeOption( $yoastKeyphrase['keyword'] ) ];

									if ( ! isset( $keyphrases['additional'] ) ) {
										$keyphrases['additional'] = [];
									}

									$keyphrases['additional'][] = $keyphrase;
								}
							}
						}

						if ( ! empty( $keyphrases ) ) {
							// Merge with existing keyphrases if the array key already exists.
							if ( ! empty( $meta['keyphrases'] ) ) {
								$meta['keyphrases'] = array_merge( $meta['keyphrases'], $keyphrases );
							} else {
								$meta['keyphrases'] = $keyphrases;
							}
						}
						break;
					case '_yoast_wpseo_title':
					case '_yoast_wpseo_metadesc':
					case '_yoast_wpseo_opengraph-title':
					case '_yoast_wpseo_opengraph-description':
					case '_yoast_wpseo_twitter-title':
					case '_yoast_wpseo_twitter-description':
						if ( 'page' === $post->post_type ) {
							$value = aioseo()->helpers->pregReplace( '#%%primary_category%%#', '', $value );
							$value = aioseo()->helpers->pregReplace( '#%%excerpt%%#', '', $value );
						}

						if ( '_yoast_wpseo_twitter-title' === $name || '_yoast_wpseo_twitter-description' === $name ) {
							$meta['twitter_use_og'] = false;
						}

						$value = aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $value, 'post', $post->post_type );

						if ( '_yoast_wpseo_title' === $name ) {
							$title = $value;
						}

						$meta[ $mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
						break;
					case '_yoast_wpseo_is_cornerstone':
						$meta['pillar_content'] = (bool) $value ? 1 : 0;
						break;
					default:
						$meta[ $mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
						break;
				}
			}

			// Resetting the `twitter_use_og` option if the user has a custom title and no twitter title.
			if ( $meta['twitter_use_og'] && $title && empty( $meta['twitter_title'] ) ) {
				$meta['twitter_use_og'] = false;
				$meta['twitter_title']  = $title;
			}

			$aioseoPost = Models\Post::getPost( (int) $post->ID );
			$aioseoPost->set( $meta );
			$aioseoPost->save();

			aioseo()->migration->meta->migrateAdditionalPostMeta( $post->ID );

			// Clear the Overview cache.
			aioseo()->postSettings->clearPostTypeOverviewCache( $post->ID );
		}

		if ( count( $posts ) === $postsPerAction ) {
			try {
				as_schedule_single_action( time() + 5, aioseo()->importExport->yoastSeo->postActionName, [], 'aioseo' );
			} catch ( \Exception $e ) {
				// Do nothing.
			}
		} else {
			aioseo()->core->cache->delete( 'import_post_meta_yoast_seo' );
		}
	}
}Common/ImportExport/YoastSeo/Helpers.php000066600000011315151135505570014345 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\ImportExport;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Contains helper methods for the import from Rank Math.
 *
 * @since 4.0.0
 */
class Helpers extends ImportExport\Helpers {
	/**
	 * Converts the macros from Yoast SEO to our own smart tags.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $string   The string with macros.
	 * @param  string $postType The post type.
	 * @param  string $pageType The page type.
	 * @return string $string   The string with smart tags.
	 */
	public function macrosToSmartTags( $string, $postType = null, $pageType = null ) {
		$macros = $this->getMacros( $postType, $pageType );

		if ( preg_match( '#%%BLOGDESCLINK%%#', (string) $string ) ) {
			$blogDescriptionLink = '<a href="' .
				aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'url' ) ) . '">' .
				aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) ) . ' - ' .
				aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'description' ) ) . '</a>';

			$string = str_replace( '%%BLOGDESCLINK%%', $blogDescriptionLink, $string );
		}

		if ( preg_match_all( '#%%cf_([^%]*)%%#', (string) $string, $matches ) && ! empty( $matches[1] ) ) {
			foreach ( $matches[1] as $name ) {
				if ( ! preg_match( '#\s#', (string) $name ) ) {
					$string = aioseo()->helpers->pregReplace( "#%%cf_$name%%#", "#custom_field-$name", $string );
				}
			}
		}

		if ( preg_match_all( '#%%tax_([^%]*)%%#', (string) $string, $matches ) && ! empty( $matches[1] ) ) {
			foreach ( $matches[1] as $name ) {
				if ( ! preg_match( '#\s#', (string) $name ) ) {
					$string = aioseo()->helpers->pregReplace( "#%%tax_$name%%#", "#tax_name-$name", $string );
				}
			}
		}

		foreach ( $macros as $macro => $tag ) {
			$string = aioseo()->helpers->pregReplace( "#$macro(?![a-zA-Z0-9_])#im", $tag, $string );
		}

		// Strip out all remaining tags.
		$string = aioseo()->helpers->pregReplace( '/%[^\%\s]*\([^\%]*\)%/i', '', aioseo()->helpers->pregReplace( '/%[^\%\s]*%/i', '', $string ) );

		return trim( $string );
	}

	/**
	 * Returns the macro mappings.
	 *
	 * @since 4.1.1
	 *
	 * @param  string $postType The post type.
	 * @param  string $pageType The page type.
	 * @return array  $macros   The macros.
	 */
	protected function getMacros( $postType = null, $pageType = null ) {
		$macros = [
			'%%sitename%%'             => '#site_title',
			'%%sitedesc%%'             => '#tagline',
			'%%sep%%'                  => '#separator_sa',
			'%%term_title%%'           => '#taxonomy_title',
			'%%term_description%%'     => '#taxonomy_description',
			'%%category_description%%' => '#taxonomy_description',
			'%%tag_description%%'      => '#taxonomy_description',
			'%%primary_category%%'     => '#taxonomy_title',
			'%%archive_title%%'        => '#archive_title',
			'%%pagenumber%%'           => '#page_number',
			'%%caption%%'              => '#attachment_caption',
			'%%name%%'                 => '#author_first_name #author_last_name',
			'%%user_description%%'     => '#author_bio',
			'%%date%%'                 => '#archive_date',
			'%%currentday%%'           => '#current_day',
			'%%currentmonth%%'         => '#current_month',
			'%%currentyear%%'          => '#current_year',
			'%%searchphrase%%'         => '#search_term',
			'%%AUTHORLINK%%'           => '#author_link',
			'%%POSTLINK%%'             => '#post_link',
			'%%BLOGLINK%%'             => '#site_link',
			'%%category%%'             => '#categories',
			'%%parent_title%%'         => '#parent_title',
			'%%wc_sku%%'               => '#woocommerce_sku',
			'%%wc_price%%'             => '#woocommerce_price',
			'%%wc_brand%%'             => '#woocommerce_brand',
			'%%excerpt%%'              => '#post_excerpt',
			'%%excerpt_only%%'         => '#post_excerpt_only'
			/* '%%tag%%'                  => '',
			'%%id%%'                   => '',
			'%%page%%'                 => '',
			'%%modified%%'             => '',
			'%%pagetotal%%'            => '',
			'%%focuskw%%'              => '',
			'%%term404%%'              => '',
			'%%ct_desc_[^%]*%%'        => '' */
		];

		if ( $postType ) {
			$postType = get_post_type_object( $postType );
			if ( ! empty( $postType ) ) {
				$macros += [
					'%%pt_single%%' => $postType->labels->singular_name,
					'%%pt_plural%%' => $postType->labels->name,
				];
			}
		}

		switch ( $pageType ) {
			case 'archive':
				$macros['%%title%%'] = '#archive_title';
				break;
			case 'term':
				$macros['%%title%%'] = '#taxonomy_title';
				break;
			default:
				$macros['%%title%%'] = '#post_title';
				break;
		}

		// Strip all other tags.
		$macros['%%[^%]*%%'] = '';

		return $macros;
	}
}Common/ImportExport/YoastSeo/SearchAppearance.php000066600000034145151135505570016136 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\ImportExport;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Search Appearance settings.
 *
 * @since 4.0.0
 */
class SearchAppearance {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Whether the homepage social settings have been imported here.
	 *
	 * @since 4.2.4
	 *
	 * @var bool
	 */
	public $hasImportedHomepageSocialSettings = false;

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->options = get_option( 'wpseo_titles' );
		if ( empty( $this->options ) ) {
			return;
		}

		$this->migrateSeparator();
		$this->migrateTitleFormats();
		$this->migrateDescriptionFormats();
		$this->migrateNoindexSettings();
		$this->migratePostTypeSettings();
		$this->migratePostTypeArchiveSettings();
		$this->migrateRedirectAttachments();
		$this->migrateKnowledgeGraphSettings();
		$this->migrateRssContentSettings();
		$this->migrateStripCategoryBase();
		$this->migrateHomepageSocialSettings();
	}

	/**
	 * Migrates the title/description separator.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateSeparator() {
		$separators = [
			'sc-dash'   => '-',
			'sc-ndash'  => '&ndash;',
			'sc-mdash'  => '&mdash;',
			'sc-colon'  => ':',
			'sc-middot' => '&middot;',
			'sc-bull'   => '&bull;',
			'sc-star'   => '*',
			'sc-smstar' => '&#8902;',
			'sc-pipe'   => '|',
			'sc-tilde'  => '~',
			'sc-laquo'  => '&laquo;',
			'sc-raquo'  => '&raquo;',
			'sc-lt'     => '&lt;',
			'sc-gt'     => '&gt;',
		];

		if ( ! empty( $this->options['separator'] ) && in_array( $this->options['separator'], array_keys( $separators ), true ) ) {
			aioseo()->options->searchAppearance->global->separator = $separators[ $this->options['separator'] ];
		}
	}

	/**
	 * Migrates the title formats.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateTitleFormats() {
		aioseo()->options->searchAppearance->global->siteTitle =
			aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $this->options['title-home-wpseo'] ) );

		aioseo()->options->searchAppearance->archives->date->title =
			aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $this->options['title-archive-wpseo'], null, 'archive' ) );

		// Archive Title tag needs to be stripped since we don't support it for these two archives.
		$value = aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $this->options['title-author-wpseo'], null, 'archive' ) );
		aioseo()->options->searchAppearance->archives->author->title = aioseo()->helpers->pregReplace( '/#archive_title/', '', $value );

		$value = aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $this->options['title-search-wpseo'], null, 'archive' ) );
		aioseo()->options->searchAppearance->archives->search->title = aioseo()->helpers->pregReplace( '/#archive_title/', '', $value );
	}

	/**
	 * Migrates the description formats.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateDescriptionFormats() {
		$settings = [
			'metadesc-home-wpseo'    => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'metaDescription' ] ],
			'metadesc-author-wpseo'  => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'author', 'metaDescription' ] ],
			'metadesc-archive-wpseo' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'date', 'metaDescription' ] ],
		];

		aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options, true );
	}

	/**
	 * Migrates the noindex settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateNoindexSettings() {
		if ( ! empty( $this->options['noindex-author-wpseo'] ) ) {
			aioseo()->options->searchAppearance->archives->author->show = false;
			aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->default = false;
			aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->noindex = true;
		} else {
			aioseo()->options->searchAppearance->archives->author->show = true;
		}

		if ( ! empty( $this->options['noindex-archive-wpseo'] ) ) {
			aioseo()->options->searchAppearance->archives->date->show = false;
			aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->default = false;
			aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->noindex = true;
		} else {
			aioseo()->options->searchAppearance->archives->date->show = true;
		}
	}

	/**
	 * Migrates the post type settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migratePostTypeSettings() {
		$supportedSettings = [
			'title',
			'metadesc',
			'noindex',
			'display-metabox-pt',
			'schema-page-type',
			'schema-article-type'
		];

		foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
			foreach ( $this->options as $name => $value ) {
				if ( ! preg_match( "#(.*)-$postType$#", (string) $name, $match ) || ! in_array( $match[1], $supportedSettings, true ) ) {
					continue;
				}

				switch ( $match[1] ) {
					case 'title':
						if ( 'page' === $postType ) {
							$value = aioseo()->helpers->pregReplace( '#%%primary_category%%#', '', $value );
							$value = aioseo()->helpers->pregReplace( '#%%excerpt%%#', '', $value );
						}
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->title =
							aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $value, $postType ) );
						break;
					case 'metadesc':
						if ( 'page' === $postType ) {
							$value = aioseo()->helpers->pregReplace( '#%%primary_category%%#', '', $value );
							$value = aioseo()->helpers->pregReplace( '#%%excerpt%%#', '', $value );
						}
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->metaDescription =
							aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $value, $postType ) );
						break;
					case 'noindex':
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->show = empty( $value ) ? true : false;
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = empty( $value ) ? true : false;
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->noindex = empty( $value ) ? false : true;
						break;
					case 'display-metabox-pt':
						if ( empty( $value ) ) {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->showMetaBox = false;
						}
						break;
					case 'schema-page-type':
						$value = aioseo()->helpers->pregReplace( '#\s#', '', $value );
						if ( in_array( $postType, [ 'post', 'page', 'attachment' ], true ) ) {
							break;
						}
						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->schemaType = 'WebPage';
						if ( in_array( $value, ImportExport\SearchAppearance::$supportedWebPageGraphs, true ) ) {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->webPageType = $value;
						}
						break;
					case 'schema-article-type':
						$value = aioseo()->helpers->pregReplace( '#\s#', '', $value );
						if ( 'none' === lcfirst( $value ) ) {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->articleType = 'none';
							break;
						}

						aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->articleType = 'Article';
						if ( in_array( $value, ImportExport\SearchAppearance::$supportedArticleGraphs, true ) ) {
							if ( ! in_array( $postType, [ 'page', 'attachment' ], true ) ) {
								aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->articleType = $value;
							}
						} else {
							aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->articleType = 'BlogPosting';
						}
						break;
					default:
						break;
				}
			}
		}
	}

	/**
	 * Migrates the post type archive settings.
	 *
	 * @since 4.0.16
	 *
	 * @return void
	 */
	private function migratePostTypeArchiveSettings() {
		$supportedSettings = [
			'title',
			'metadesc',
			'noindex'
		];

		foreach ( aioseo()->helpers->getPublicPostTypes( true, true ) as $postType ) {
			foreach ( $this->options as $name => $value ) {
				if ( ! preg_match( "#(.*)-ptarchive-$postType$#", (string) $name, $match ) || ! in_array( $match[1], $supportedSettings, true ) ) {
					continue;
				}

				switch ( $match[1] ) {
					case 'title':
						aioseo()->dynamicOptions->searchAppearance->archives->$postType->title =
							aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $value, $postType, 'archive' ) );
						break;
					case 'metadesc':
						aioseo()->dynamicOptions->searchAppearance->archives->$postType->metaDescription =
							aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $value, $postType, 'archive' ) );
						break;
					case 'noindex':
						aioseo()->dynamicOptions->searchAppearance->archives->$postType->show = empty( $value ) ? true : false;
						aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->default = empty( $value ) ? true : false;
						aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->noindex = empty( $value ) ? false : true;
						break;
					default:
						break;
				}
			}
		}
	}

	/**
	 * Migrates the Knowledge Graph settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateKnowledgeGraphSettings() {
		if ( ! empty( $this->options['company_or_person'] ) ) {
			aioseo()->options->searchAppearance->global->schema->siteRepresents =
				'company' === $this->options['company_or_person'] ? 'organization' : 'person';
		}

		$settings = [
			'company_or_person_user_id' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'person' ] ],
			'person_logo'               => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'personLogo' ] ],
			'person_name'               => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'personName' ] ],
			'company_name'              => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'organizationName' ] ],
			'company_logo'              => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'organizationLogo' ] ],
			'org-email'                 => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'email' ] ],
			'org-phone'                 => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'phone' ] ],
			'org-description'           => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'organizationDescription' ] ],
			'org-founding-date'         => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'foundingDate' ] ],
		];

		aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options );

		// Additional Info
		// Reset data
		aioseo()->options->noConflict()->searchAppearance->global->schema->numberOfEmployees->reset();

		$numberOfEmployees = $this->options['org-number-employees'];
		if ( ! empty( $numberOfEmployees ) ) {
			list( $num1, $num2 ) = explode( '-', $numberOfEmployees );

			if ( $num2 ) {
				aioseo()->options->noConflict()->searchAppearance->global->schema->numberOfEmployees->isRange = true;
				aioseo()->options->noConflict()->searchAppearance->global->schema->numberOfEmployees->from    = (int) $num1;
				aioseo()->options->noConflict()->searchAppearance->global->schema->numberOfEmployees->to      = (int) $num2;
			} else {
				aioseo()->options->noConflict()->searchAppearance->global->schema->numberOfEmployees->number = (int) $num1;
			}
		}
	}

	/**
	 * Migrates the RSS content settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateRssContentSettings() {
		if ( isset( $this->options['rssbefore'] ) ) {
			aioseo()->options->rssContent->before = esc_html( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $this->options['rssbefore'] ) );
		}

		if ( isset( $this->options['rssafter'] ) ) {
			aioseo()->options->rssContent->after = esc_html( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $this->options['rssafter'] ) );
		}
	}

	/**
	 * Migrates the Redirect Attachments setting.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateRedirectAttachments() {
		aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = empty( $this->options['disable-attachment'] ) ? 'disabled' : 'attachment';
	}

	/**
	 * Migrates the strip category base option.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	private function migrateStripCategoryBase() {
		aioseo()->options->searchAppearance->advanced->removeCategoryBase = empty( $this->options['stripcategorybase'] ) ? false : true;
	}

	/**
	 * Migrate the social settings for the homepage.
	 *
	 * @since 4.2.4
	 *
	 * @return void
	 */
	private function migrateHomepageSocialSettings() {
		if (
			empty( $this->options['open_graph_frontpage_title'] ) &&
			empty( $this->options['open_graph_frontpage_desc'] ) &&
			empty( $this->options['open_graph_frontpage_image'] )
		) {
			return;
		}

		$this->hasImportedHomepageSocialSettings = true;

		$settings = [
			// These settings can also be found in the SocialMeta class, but Yoast recently moved them here.
			// We'll still keep them in the other class for backwards compatibility.
			'open_graph_frontpage_title' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'title' ] ],
			'open_graph_frontpage_desc'  => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'description' ] ],
			'open_graph_frontpage_image' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'image' ] ]
		];

		aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options, true );
	}
}Common/ImportExport/YoastSeo/SocialMeta.php000066600000014250151135505570014765 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models;

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

/**
 * Migrates the Social Meta.
 *
 * @since 4.0.0
 */
class SocialMeta {
	/**
	 * List of options.
	 *
	 * @since 4.2.7
	 *
	 * @var array
	 */
	private $options = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		$this->options = get_option( 'wpseo_social' );

		if ( empty( $this->options ) ) {
			return;
		}

		$this->migrateSocialUrls();
		$this->migrateFacebookSettings();
		$this->migrateTwitterSettings();
		$this->migrateFacebookAdminId();
		$this->migrateSiteName();
		$this->migrateArticleTags();
		$this->migrateAdditionalTwitterData();

		$settings = [
			'pinterestverify' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'pinterest' ] ]
		];

		aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the Social URLs.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateSocialUrls() {
		$settings = [
			'facebook_site' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'facebookPageUrl' ] ],
			'instagram_url' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'instagramUrl' ] ],
			'linkedin_url'  => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'linkedinUrl' ] ],
			'myspace_url'   => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'myspaceUrl' ] ],
			'pinterest_url' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'pinterestUrl' ] ],
			'youtube_url'   => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'youtubeUrl' ] ],
			'wikipedia_url' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'wikipediaUrl' ] ],
			'wordpress_url' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'wordPressUrl' ] ],
		];

		if ( ! empty( $this->options['twitter_site'] ) ) {
			aioseo()->options->social->profiles->urls->twitterUrl =
				'https://x.com/' . aioseo()->helpers->sanitizeOption( $this->options['twitter_site'] );
		}

		aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the Facebook settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateFacebookSettings() {
		if ( ! empty( $this->options['og_default_image'] ) ) {
			$defaultImage = esc_url( $this->options['og_default_image'] );
			aioseo()->options->social->facebook->general->defaultImagePosts       = $defaultImage;
			aioseo()->options->social->facebook->general->defaultImageSourcePosts = 'default';

			aioseo()->options->social->twitter->general->defaultImagePosts       = $defaultImage;
			aioseo()->options->social->twitter->general->defaultImageSourcePosts = 'default';
		}

		$settings = [
			'opengraph' => [ 'type' => 'boolean', 'newOption' => [ 'social', 'facebook', 'general', 'enable' ] ],
		];

		if ( ! aioseo()->importExport->yoastSeo->searchAppearance->hasImportedHomepageSocialSettings ) {
			// These settings were moved to the Search Appearance tab of Yoast, but we'll leave this here to support older versions.
			// However, we want to make sure we import them only if the other ones aren't set.
			$settings = array_merge( $settings, [
				'og_frontpage_title' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'title' ] ],
				'og_frontpage_desc'  => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'description' ] ],
				'og_frontpage_image' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'image' ] ]
			] );
		}

		aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options, true );

		// Migrate home page object type.
		aioseo()->options->social->facebook->homePage->objectType = 'website';
		if ( 'page' === get_option( 'show_on_front' ) ) {
			$staticHomePageId = get_option( 'page_on_front' );

			// We must check if the ID exists because one might select the static homepage option but not actually set one.
			if ( ! $staticHomePageId ) {
				return;
			}

			$aioseoPost = Models\Post::getPost( (int) $staticHomePageId );
			$aioseoPost->set( [
				'og_object_type' => 'website'
			] );
			$aioseoPost->save();
		}
	}

	/**
	 * Migrates the Twitter settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateTwitterSettings() {
		$settings = [
			'twitter'           => [ 'type' => 'boolean', 'newOption' => [ 'social', 'twitter', 'general', 'enable' ] ],
			'twitter_card_type' => [ 'type' => 'string', 'newOption' => [ 'social', 'twitter', 'general', 'defaultCardType' ] ],
		];

		aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options );
	}

	/**
	 * Migrates the Facebook admin ID.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function migrateFacebookAdminId() {
		if ( ! empty( $this->options['fbadminapp'] ) ) {
			aioseo()->options->social->facebook->advanced->enable = true;
			aioseo()->options->social->facebook->advanced->adminId = aioseo()->helpers->sanitizeOption( $this->options['fbadminapp'] );
		}
	}

	/**
	 * Yoast sets the og:site_name to '#site_title';
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateSiteName() {
		aioseo()->options->social->facebook->general->siteName = '#site_title';
	}

	/**
	 * Yoast uses post tags by default, so we need to enable this.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateArticleTags() {
		aioseo()->options->social->facebook->advanced->enable              = true;
		aioseo()->options->social->facebook->advanced->generateArticleTags = true;
		aioseo()->options->social->facebook->advanced->usePostTagsInTags   = true;
		aioseo()->options->social->facebook->advanced->useKeywordsInTags   = false;
		aioseo()->options->social->facebook->advanced->useCategoriesInTags = false;
	}

	/**
	 * Enable additional Twitter Data.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	private function migrateAdditionalTwitterData() {
		aioseo()->options->social->twitter->general->additionalData = true;
	}
}Common/ImportExport/Helpers.php000066600000004430151135505570012577 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Contains helper methods for the import from other plugins.
 *
 * @since 4.0.0
 */
abstract class Helpers {
	/**
	 * Converts macros to smart tags.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $value The string with macros.
	 * @return string        The string with macros converted.
	 */
	abstract public function macrosToSmartTags( $value );

	/**
	 * Maps a list of old settings from V3 to their counterparts in V4.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $mappings      The old settings, mapped to their new settings.
	 * @param  array $group         The old settings group.
	 * @param  bool  $convertMacros Whether to convert the old V3 macros to V4 smart tags.
	 * @return void
	 */
	public function mapOldToNew( $mappings, $group, $convertMacros = false ) {
		if (
			! is_array( $mappings ) ||
			! is_array( $group ) ||
			! count( $mappings ) ||
			! count( $group )
		) {
			return;
		}

		$mainOptions    = aioseo()->options->noConflict();
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();
		foreach ( $mappings as $name => $values ) {
			if ( ! isset( $group[ $name ] ) ) {
				continue;
			}

			$error      = false;
			$options    = ! empty( $values['dynamic'] ) ? $dynamicOptions : $mainOptions;
			$lastOption = '';
			for ( $i = 0; $i < count( $values['newOption'] ); $i++ ) {
				$lastOption = $values['newOption'][ $i ];
				if ( ! $options->has( $lastOption, false ) ) {
					$error = true;
					break;
				}

				if ( count( $values['newOption'] ) - 1 !== $i ) {
					$options = $options->$lastOption;
				}
			}

			if ( $error ) {
				continue;
			}

			switch ( $values['type'] ) {
				case 'boolean':
					if ( ! empty( $group[ $name ] ) ) {
						$options->$lastOption = true;
						break;
					}
					$options->$lastOption = false;
					break;
				case 'integer':
				case 'float':
					$value = aioseo()->helpers->sanitizeOption( $group[ $name ] );
					if ( $value ) {
						$options->$lastOption = $value;
					}
					break;
				default:
					$value = $group[ $name ];
					if ( $convertMacros ) {
						$value = $this->macrosToSmartTags( $value );
					}
					$options->$lastOption = aioseo()->helpers->sanitizeOption( $value );
					break;
			}
		}
	}
}Common/ImportExport/SearchAppearance.php000066600000001535151135505570014365 0ustar00<?php
namespace AIOSEO\Plugin\Common\ImportExport;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Migrates the Search Appearance settings.
 *
 * @since 4.0.0
 */
abstract class SearchAppearance {
	/**
	 * The schema graphs we support.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public static $supportedSchemaGraphs = [
		'none',
		'WebPage',
		'Article'
	];

	/**
	 * The WebPage graphs we support.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public static $supportedWebPageGraphs = [
		'AboutPage',
		'CollectionPage',
		'ContactPage',
		'FAQPage',
		'ItemPage',
		'ProfilePage',
		'RealEstateListing',
		'SearchResultsPage',
		'WebPage'
	];

	/**
	 * The Article graphs we support.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	public static $supportedArticleGraphs = [
		'Article',
		'BlogPosting',
		'NewsArticle'
	];
}Common/Admin/ConflictingPlugins.php000066600000011250151135505570013350 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models;

/**
 * Checks for conflicting plugins.
 *
 * @since 4.0.0
 */
class ConflictingPlugins {
	/**
	 * A list of conflicting plugin slugs.
	 *
	 * @since 4.5.1
	 *
	 * @var array
	 */
	protected $conflictingPluginSlugs = [
		// Note: We should NOT add Jetpack here since they automatically disable their SEO module when ours is active.
		'wordpress-seo',
		'seo-by-rank-math',
		'wp-seopress',
		'autodescription',
		'slim-seo',
		'squirrly-seo',
		'google-sitemap-generator',
		'xml-sitemap-feed',
		'www-xml-sitemap-generator-org',
		'google-sitemap-plugin',
	];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		// We don't want to trigger our notices when not in the admin.
		if ( ! is_admin() ) {
			return;
		}

		add_action( 'init', [ $this, 'init' ], 20 );
	}

	/**
	 * Initialize the conflicting plugins check.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function init() {
		// Only do this for users who can install/deactivate plugins.
		if ( ! current_user_can( 'install_plugins' ) ) {
			return;
		}

		$conflictingPlugins = $this->getAllConflictingPlugins();

		$notification = Models\Notification::getNotificationByName( 'conflicting-plugins' );
		if ( empty( $conflictingPlugins ) ) {
			if ( ! $notification->exists() ) {
				return;
			}

			Models\Notification::deleteNotificationByName( 'conflicting-plugins' );

			return;
		}

		aioseo()->notices->conflictingPlugins( $conflictingPlugins );
	}

	/**
	 * Get a list of all conflicting plugins.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of conflicting plugins.
	 */
	public function getAllConflictingPlugins() {
		$conflictingSeoPlugins     = $this->getConflictingPlugins( 'seo' );
		$conflictingSitemapPlugins = [];

		if (
			aioseo()->options->sitemap->general->enable ||
			aioseo()->options->sitemap->rss->enable
		) {
			$conflictingSitemapPlugins = $this->getConflictingPlugins( 'sitemap' );
		}

		return array_merge( $conflictingSeoPlugins, $conflictingSitemapPlugins );
	}

	/**
	 * Get a list of conflicting plugins for AIOSEO.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $type A type to look for.
	 * @return array        An array of conflicting plugins.
	 */
	public function getConflictingPlugins( $type ) {
		$activePlugins = wp_get_active_and_valid_plugins();
		if ( is_multisite() ) {
			$activePlugins = array_merge( $activePlugins, wp_get_active_network_plugins() );
		}

		$conflictingPlugins = [];
		switch ( $type ) {
			// Note: We should NOT add Jetpack here since they automatically disable their SEO module when ours is active.
			case 'seo':
				$conflictingPlugins = [
					'Rank Math SEO'     => 'seo-by-rank-math/rank-math.php',
					'Rank Math SEO Pro' => 'seo-by-rank-math-pro/rank-math-pro.php',
					'SEOPress'          => 'wp-seopress/seopress.php',
					'The SEO Framework' => 'autodescription/autodescription.php',
					'Yoast SEO'         => 'wordpress-seo/wp-seo.php',
					'Yoast SEO Premium' => 'wordpress-seo-premium/wp-seo-premium.php'
				];
				break;
			case 'sitemap':
				$conflictingPlugins = [
					'Google XML Sitemaps'          => 'google-sitemap-generator/sitemap.php',
					'Google XML Sitemap Generator' => 'www-xml-sitemap-generator-org/www-xml-sitemap-generator-org.php',
					'Sitemap by BestWebSoft'       => 'google-sitemap-plugin/google-sitemap-plugin.php',
					'XML Sitemap & Google News'    => 'xml-sitemap-feed/xml-sitemap.php'
				];
				break;
		}

		$activeConflictingPlugins = [];
		foreach ( $activePlugins as $pluginFilePath ) {
			foreach ( $conflictingPlugins as $index => $pluginPath ) {
				if ( false !== strpos( $pluginFilePath, $pluginPath ) ) {
					$activeConflictingPlugins[ $index ] = $pluginPath;
				}
			}
		}

		return $activeConflictingPlugins;
	}

	/**
	 * Deactivate conflicting plugins.
	 *
	 * @since 4.5.1
	 *
	 * @param array $types An array of types to look for.
	 * @return void
	 */
	public function deactivateConflictingPlugins( $types ) {
		$seo     = in_array( 'seo', $types, true ) ? $this->getConflictingPlugins( 'seo' ) : [];
		$sitemap = in_array( 'sitemap', $types, true ) ? $this->getConflictingPlugins( 'sitemap' ) : [];
		$plugins = array_merge(
			$seo,
			$sitemap
		);

		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		foreach ( $plugins as $pluginPath ) {
			if ( is_plugin_active( $pluginPath ) ) {
				deactivate_plugins( $pluginPath );
			}
		}
	}

	/**
	 * Get a list of conflicting plugin slugs.
	 *
	 * @since 4.5.1
	 *
	 * @return array An array of conflicting plugin slugs.
	 */
	public function getConflictingPluginSlugs() {
		return $this->conflictingPluginSlugs;
	}
}Common/Admin/NetworkAdmin.php000066600000003222151135505570012151 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Abstract class that Pro and Lite both extend.
 *
 * @since 4.2.5
 */
class NetworkAdmin extends Admin {
	/**
	 * Construct method.
	 *
	 * @since 4.2.5
	 */
	public function __construct() {
		include_once ABSPATH . 'wp-admin/includes/plugin.php';
		if (
			is_network_admin() &&
			! is_plugin_active_for_network( plugin_basename( AIOSEO_FILE ) )
		) {
			return;
		}

		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		add_action( 'sanitize_comment_cookies', [ $this, 'init' ], 21 );
	}

	/**
	 * Initialize the admin.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	public function init() {
		add_action( 'network_admin_menu', [ $this, 'addNetworkMenu' ] );

		add_action( 'init', [ $this, 'setPages' ] );
	}

	/**
	 * Add the network menu inside of WordPress.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	public function addNetworkMenu() {
		$this->addMainMenu( 'aioseo' );

		foreach ( $this->pages as $slug => $page ) {
			if (
				'aioseo-settings' !== $slug &&
				'aioseo-tools' !== $slug &&
				'aioseo-about' !== $slug &&
				'aioseo-feature-manager' !== $slug
			) {
				continue;
			}

			$hook = add_submenu_page(
				$this->pageSlug,
				! empty( $page['page_title'] ) ? $page['page_title'] : $page['menu_title'],
				$page['menu_title'],
				$this->getPageRequiredCapability( $slug ),
				$slug,
				[ $this, 'page' ]
			);
			add_action( "load-{$hook}", [ $this, 'hooks' ] );
		}

		// Remove the "dashboard" submenu page that is not needed in the network admin.
		remove_submenu_page( $this->pageSlug, $this->pageSlug );
	}
}Common/Admin/SlugMonitor.php000066600000012421151135505570012032 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Monitors changes to post slugs.
 *
 * @since 4.2.3
 */
class SlugMonitor {
	/**
	 * Holds posts that have been updated.
	 *
	 * @since 4.2.3
	 *
	 * @var array
	 */
	private $updatedPosts = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.2.3
	 */
	public function __construct() {
		// We can't monitor changes without permalinks enabled.
		if ( ! get_option( 'permalink_structure' ) ) {
			return;
		}

		add_action( 'pre_post_update', [ $this, 'prePostUpdate' ] );

		// WP 5.6+.
		if ( function_exists( 'wp_after_insert_post' ) ) {
			add_action( 'wp_after_insert_post', [ $this, 'afterInsertPost' ], 11, 4 );
		} else {
			add_action( 'post_updated', [ $this, 'postUpdated' ], 11, 3 );
		}
	}

	/**
	 * Remember the previous post permalink.
	 *
	 * @since 4.2.3
	 *
	 * @param  integer $postId The post ID.
	 * @return void
	 */
	public function prePostUpdate( $postId ) {
		$this->updatedPosts[ $postId ] = get_permalink( $postId );
	}

	/**
	 * Called when a post has been completely inserted ( with categories and meta ).
	 *
	 * @since 4.2.3
	 *
	 * @param  integer       $postId     The post ID.
	 * @param  \WP_Post      $post       The post object.
	 * @param  bool          $update     Whether this is an existing post being updated.
	 * @param  null|\WP_Post $postBefore The post object before changes were made.
	 * @return void
	 */
	public function afterInsertPost( $postId, $post = null, $update = false, $postBefore = null ) {
		if ( ! $update ) {
			return;
		}

		$this->postUpdated( $postId, $post, $postBefore );
	}

	/**
	 * Called when a post has been updated - check if the slug has changed.
	 *
	 * @since 4.2.3
	 *
	 * @param  integer  $postId     The post ID.
	 * @param  \WP_Post $post       The post object.
	 * @param  \WP_Post $postBefore The post object before changes were made.
	 * @return void
	 */
	public function postUpdated( $postId, $post = null, $postBefore = null ) {
		if ( ! isset( $this->updatedPosts[ $postId ] ) ) {
			return;
		}

		$before = aioseo()->helpers->getPermalinkPath( $this->updatedPosts[ $postId ] );
		$after  = aioseo()->helpers->getPermalinkPath( get_permalink( $postId ) );
		if ( ! aioseo()->helpers->hasPermalinkChanged( $before, $after ) ) {
			return;
		}

		// Can we monitor this slug?
		if ( ! $this->canMonitorPost( $post, $postBefore ) ) {
			return;
		}

		// Ask aioseo-redirects if automatic redirects is monitoring it.
		if ( $this->automaticRedirect( $post->post_type, $before, $after ) ) {
			return;
		}

		// Filter to allow users to disable the slug monitor messages.
		if ( apply_filters( 'aioseo_redirects_disable_slug_monitor', false ) ) {
			return;
		}

		$redirectUrl = $this->manualRedirectUrl( [
			'url'    => $before,
			'target' => $after,
			'type'   => 301
		] );

		$message = __( 'The permalink for this post just changed! This could result in 404 errors for your site visitors.', 'all-in-one-seo-pack' );

		// Default notice redirecting to the Redirects screen.
		$action = [
			'url'    => $redirectUrl,
			'label'  => __( 'Add Redirect to improve SEO', 'all-in-one-seo-pack' ),
			'target' => '_blank',
			'class'  => 'aioseo-redirects-slug-changed'
		];

		// If redirects is active we'll show add-redirect in a modal.
		if ( aioseo()->addons->getLoadedAddon( 'redirects' ) ) {
			// We need to remove the target here so the action keeps the url used by the add-redirect modal.
			unset( $action['target'] );
		}

		aioseo()->wpNotices->addNotice( $message, 'warning', [ 'actions' => [ $action ] ], [ 'posts' ] );
	}

	/**
	 * Checks if this is a post we can monitor.
	 *
	 * @since 4.2.3
	 *
	 * @param  \WP_Post $post       The post object.
	 * @param  \WP_Post $postBefore The post object before changes were made.
	 * @return boolean              True if we can monitor this post.
	 */
	private function canMonitorPost( $post, $postBefore ) {
		// Check that this is for the expected post.
		if ( ! isset( $post->ID ) || ! isset( $this->updatedPosts[ $post->ID ] ) ) {
			return false;
		}

		// Don't do anything if we're not published.
		if ( 'publish' !== $post->post_status || 'publish' !== $postBefore->post_status ) {
			return false;
		}

		// Don't do anything is the post type is not public.
		if ( ! is_post_type_viewable( $post->post_type ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Tries to add a automatic redirect.
	 *
	 * @since 4.2.3
	 *
	 * @param  string $postType The post type.
	 * @param  string $before   The url before.
	 * @param  string $after    The url after.
	 * @return bool             True if an automatic redirect was added.
	 */
	private function automaticRedirect( $postType, $before, $after ) {
		if ( ! aioseo()->addons->getLoadedAddon( 'redirects' ) ) {
			return false;
		}

		return aioseoRedirects()->monitor->automaticRedirect( $postType, $before, $after );
	}

	/**
	 * Generates a URL for adding manual redirects.
	 *
	 * @since 4.2.3
	 *
	 * @param  array  $urls An array of [url, target, type, slash, case, regex].
	 * @return string       The redirect link.
	 */
	public function manualRedirectUrl( $urls ) {
		if ( ! aioseo()->addons->getLoadedAddon( 'redirects' ) ) {
			return admin_url( 'admin.php?page=aioseo-redirects' );
		}

		return aioseoRedirects()->helpers->manualRedirectUrl( $urls );
	}
}Common/Admin/Pointers.php000066600000010011151135505570011344 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models;

/**
 * Handles the pointers for the admin.
 *
 * @since 4.8.3
 */
class Pointers {
	/**
	 * Class constructor.
	 *
	 * @since 4.8.3
	 */
	public function __construct() {
		if ( ! is_admin() ) {
			return;
		}

		add_action( 'admin_init', [ $this, 'maybeDismissPointer' ] );
		add_action( 'in_admin_header', [ $this, 'init' ] );
	}

	/**
	 * Initializes the pointers.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	public function init() {
		$this->registerKwRankTracker();
	}

	/**
	 * Checks if a pointer should be dismissed.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	public function maybeDismissPointer() {
		if (
			! isset( $_GET['aioseo-dismiss-pointer'] ) ||
			! isset( $_GET['aioseo-dismiss-pointer-nonce'] ) ||
			! wp_verify_nonce( $_GET['aioseo-dismiss-pointer-nonce'], 'aioseo-dismiss-pointer' )
		) {
			return;
		}

		$pointer = sanitize_text_field( wp_unslash( $_GET['aioseo-dismiss-pointer'] ) );
		update_user_meta( get_current_user_id(), "_aioseo-$pointer-dismissed", true );
	}

	/**
	 * Registers a pointer.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	public function registerPointer( $id, $pageSlug, $args ) {
		if ( get_user_meta( get_current_user_id(), "_aioseo-$id-dismissed", true ) ) {
			return;
		}

		if ( "all-in-one-seo_page_aioseo-{$pageSlug}" === aioseo()->helpers->getCurrentScreen()->id ) {
			return;
		}

		wp_enqueue_style( 'wp-pointer' );
		wp_enqueue_script( 'wp-pointer' );

		// phpcs:disable AIOSEO.Wp.I18n.NonSingularStringLiteralText, Squiz.PHP.EmbeddedPhp, Generic.WhiteSpace.ScopeIndent.IncorrectExact
		?>
		<script>
			jQuery( document ).ready( function( $ ) {
				var isClosed = false;
				var pointer  = $( '#toplevel_page_aioseo > a' ).pointer( {
					content :
						"<h3><?php esc_html_e( $args['title'], 'all-in-one-seo-pack' ); ?><\/h3>" +
						"<h4><?php esc_html_e( $args['subtitle'], 'all-in-one-seo-pack' ); ?><\/h4>" +
						"<p><?php esc_html_e( $args['content'], 'all-in-one-seo-pack' ); ?><\/p>" +
						"<?php
							echo sprintf(
								'<p><a class=\"button button-primary\" href=\"%s\">%s</a></p>',
								esc_attr( esc_url( $args['url'] ) ),
								esc_html__( $args['button'], 'all-in-one-seo-pack' )
							);
						?>",
					position : {
						edge  : <?php echo is_rtl() ? "'right'" : "'left'"; ?>,
						align : 'center'
					},
					pointerWidth : 420,
					show: function(event, el) {
						el.pointer.css({'position':'fixed'});
						el.pointer.addClass('aioseo-wp-pointer');
					},
					close : function() {
						isClosed = true;
						jQuery.get(
							window.location.href,
							{
								'aioseo-dismiss-pointer'       : '<?php echo esc_js( $id ); ?>',
								'aioseo-dismiss-pointer-nonce' : '<?php echo esc_js( wp_create_nonce( 'aioseo-dismiss-pointer' ) ); ?>'
							}
						);
					}
				} ).pointer('open');
			} );
		</script>
		<?php
		// phpcs:enable
	}

	/**
	 * Registers the KW Rank Tracker pointer.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	public function registerKwRankTracker() {
		if (
			! current_user_can( 'aioseo_search_statistics_settings' ) ||
			(
				is_object( aioseo()->license ) &&
				aioseo()->license->hasCoreFeature( 'search-statistics', 'keyword-rank-tracker' ) &&
				aioseo()->searchStatistics->api->auth->isConnected()
			)
		) {
			return;
		}

		$nonce = wp_create_nonce( 'aioseo-dismiss-pointer' );

		$args = [
			'title'    => 'NEW! Keyword Rank Tracker',
			'subtitle' => 'Get insights into how your site is performing for your most important keywords',
			'content'  => 'Track keywords and combine them into groups to see how your site is performing for key topics in Google search results.',
			'url'      => admin_url( 'admin.php?aioseo-dismiss-pointer=kw-rank-tracker&aioseo-dismiss-pointer-nonce=' . $nonce . '&page=aioseo-search-statistics#/keyword-rank-tracker' ),
			'button'   => 'Unlock Keyword Rank Tracker'
		];

		$this->registerPointer( 'kw-rank-tracker', 'search-statistics', $args );
	}
}Common/Admin/Admin.php000066600000115413151135505570010605 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models;
use AIOSEO\Plugin\Common\Migration;

/**
 * Abstract class that Pro and Lite both extend.
 *
 * @since 4.0.0
 */
class Admin {
	/**
	 * The page slug for the sidebar.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $pageSlug = 'aioseo';

	/**
	 * Sidebar menu name.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	public $menuName = 'All in One SEO';

	/**
	 * An array of pages for the admin.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $pages = [];

	/**
	 * The current page we are enqueuing.
	 *
	 * @since 4.1.3
	 *
	 * @var string
	 */
	protected $currentPage;

	/**
	 * An array of items to add to the admin bar.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $adminBarMenuItems = [];

	/**
	 * An array of asset slugs to use.
	 *
	 * @since 4.1.9
	 *
	 * @var array
	 */
	protected $assetSlugs = [
		'plugins' => 'src/app/plugins/main.js',
		'pages'   => 'src/vue/pages/{page}/main.js'
	];

	/**
	 * Connect class instance.
	 *
	 * @since 4.4.3
	 *
	 * @var \AIOSEO\Plugin\Lite\Admin\Connect|null
	 */
	public $connect = null;

	/**
	 * Pointers class instance.
	 *
	 * @since 4.8.3
	 *
	 * @var \AIOSEO\Plugin\Common\Admin\Pointers|null
	 */
	public $pointers = null;

	/**
	 * Whether we're editing a post or term.
	 *
	 * @since 4.7.7
	 *
	 * @var bool
	 */
	private $isEditor = false;

	/**
	 * Construct method.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		new Pointers();
		new SeoAnalysis();
		new WritingAssistant();

		include_once ABSPATH . 'wp-admin/includes/plugin.php';
		if (
			is_network_admin() &&
			! is_plugin_active_for_network( plugin_basename( AIOSEO_FILE ) )
		) {
			return;
		}

		add_action( 'aioseo_unslash_escaped_data_posts', [ $this, 'unslashEscapedDataPosts' ] );

		add_action( 'wp_ajax_aioseo-dismiss-active-menu-tooltip', [ $this, 'dismissActiveMenuTooltips' ] );

		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		add_filter( 'language_attributes', [ $this, 'alwaysAddHtmlDirAttribute' ], 3000 );

		add_action( 'sanitize_comment_cookies', [ $this, 'init' ], 20 );

		add_action( 'admin_menu', [ $this, 'deactivationSurvey' ], 100 );
	}

	/**
	 * Runs the deactivation survey.
	 *
	 * @since 4.5.5
	 *
	 * @return void
	 */
	public function deactivationSurvey() {
		new DeactivationSurvey( AIOSEO_PLUGIN_NAME, dirname( plugin_basename( AIOSEO_FILE ) ) );
	}

	/**
	 * Always add dir attribute to HTML tag.
	 *
	 * @since 4.1.9
	 *
	 * @param  string $output The HTML language attribute.
	 * @return string         The possibly modified HTML language attribute.
	 */
	public function alwaysAddHtmlDirAttribute( $output ) {
		if ( is_rtl() || preg_match( '/dir=[\'"](ltr|rtl|auto)[\'"]/i', (string) $output ) ) {
			return $output;
		}

		return 'dir="ltr" ' . $output;
	}

	/**
	 * Initialize the admin.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function init() {
		// Add the admin bar menu.
		if ( is_user_logged_in() && ( ! is_multisite() || ! is_network_admin() ) ) {
			add_action( 'admin_bar_menu', [ $this, 'adminBarMenu' ], 1000 );
		}

		if ( is_admin() ) {
			// Add the menu to the sidebar.
			add_action( 'admin_menu', [ $this, 'addMenu' ] );
			add_action( 'admin_menu', [ $this, 'hideScheduledActionsMenu' ], 99999 );

			// Add Score to Publish metabox.
			add_action( 'post_submitbox_misc_actions', [ $this, 'addPublishScore' ] );

			add_action( 'admin_init', [ $this, 'addPluginScripts' ] );

			// Add redirects messages to trashed posts.
			add_filter( 'bulk_post_updated_messages', [ $this, 'appendTrashedMessage' ], PHP_INT_MAX );

			$this->registerLinkFormatHooks();

			add_action( 'admin_footer', [ $this, 'addAioseoModalPortal' ] );
		}

		$this->loadTextDomain();

		add_action( 'init', [ $this, 'setPages' ] );
	}

	/**
	 * Sets our menu pages.
	 * It is important this runs AFTER we've loaded the text domain.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function setPages() {
		// TODO: Remove this after a couple months.
		$newIndicator = '<span class="aioseo-menu-new-indicator">&nbsp;NEW!</span>';

		$this->pages = [
			$this->pageSlug            => [
				'menu_title' => esc_html__( 'Dashboard', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-settings'          => [
				'menu_title' => is_network_admin()
					? esc_html__( 'Network Settings', 'all-in-one-seo-pack' )
					: esc_html__( 'General Settings', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-search-appearance' => [
				'menu_title' => esc_html__( 'Search Appearance', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-social-networks'   => [
				'menu_title' => esc_html__( 'Social Networks', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-sitemaps'          => [
				'menu_title' => esc_html__( 'Sitemaps', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-link-assistant'    => [
				'menu_title' => esc_html__( 'Link Assistant', 'all-in-one-seo-pack' ),
				'capability' => 'aioseo_link_assistant_settings',
				'parent'     => $this->pageSlug
			],
			'aioseo-redirects'         => [
				'menu_title' => esc_html__( 'Redirects', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-local-seo'         => [
				'menu_title' => esc_html__( 'Local SEO', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-seo-analysis'      => [
				'menu_title' => esc_html__( 'SEO Analysis', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-search-statistics' => [
				'menu_title' => esc_html__( 'Search Statistics', 'all-in-one-seo-pack' ) . $newIndicator,
				'page_title' => esc_html__( 'Search Statistics', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-tools'             => [
				'menu_title' => is_network_admin()
					? esc_html__( 'Network Tools', 'all-in-one-seo-pack' )
					: esc_html__( 'Tools', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-feature-manager'   => [
				'menu_title' => esc_html__( 'Feature Manager', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-monsterinsights'   => [
				'menu_title'          => esc_html__( 'Analytics', 'all-in-one-seo-pack' ),
				'parent'              => 'aioseo-monsterinsights',
				'hide_admin_bar_menu' => true
			],
			'aioseo-about'             => [
				'menu_title' => esc_html__( 'About Us', 'all-in-one-seo-pack' ),
				'parent'     => $this->pageSlug
			],
			'aioseo-seo-revisions'     => [
				'menu_title'          => esc_html__( 'SEO Revisions', 'all-in-one-seo-pack' ),
				'parent'              => 'aioseo-seo-revisions',
				'hide_admin_bar_menu' => true
			],
		];
	}

	/**
	 * Registers our custom link format hooks.
	 *
	 * @since 4.0.16
	 *
	 * @return void
	 */
	private function registerLinkFormatHooks() {
		if ( apply_filters( 'aioseo_disable_link_format', false ) ) {
			return;
		}

		add_action( 'wp_enqueue_editor', [ $this, 'addClassicLinkFormatScript' ], 999999 );

		global $wp_version; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		if ( version_compare( $wp_version, '5.3', '>=' ) || is_plugin_active( 'gutenberg/gutenberg.php' ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			add_action( 'current_screen', [ $this, 'addGutenbergLinkFormatScript' ] );
			add_action( 'enqueue_block_editor_assets', [ $this, 'enqueueBlockEditorLinkFormat' ] );
		}
	}

	/**
	 * Enqueues the link format script for the Block Editor.
	 *
	 * @since 4.1.8
	 *
	 * @return void
	 */
	public function enqueueBlockEditorLinkFormat() {
		wp_enqueue_script( 'aioseo-link-format' );

		if ( ! wp_style_is( 'aioseo-link-format', 'enqueued' ) ) {
			wp_enqueue_style(
				'aioseo-link-format',
				aioseo()->core->assets->getAssetsPath( false ) . '/link-format/link-format-block.css',
				[],
				aioseo()->version
			);
		}
	}

	/**
	 * Enqueues the plugins script.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addPluginScripts() {
		global $pagenow;

		if ( 'plugins.php' !== $pagenow && 'plugin-install.php' !== $pagenow ) {
			return;
		}

		aioseo()->core->assets->load( $this->assetSlugs['plugins'], [], [
			'basename'           => AIOSEO_PLUGIN_BASENAME,
			'conflictingPlugins' => aioseo()->conflictingPlugins->getConflictingPluginSlugs()
		], 'aioseoPlugins' );
	}

	/**
	 * Enqueues our link format for the Classic Editor.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addClassicLinkFormatScript() {
		wp_deregister_script( 'wplink' );

		wp_enqueue_script(
			'wplink',
			aioseo()->core->assets->getAssetsPath( false ) . '/link-format/link-format-classic.js',
			[ 'jquery', 'wp-a11y' ],
			aioseo()->version,
			true
		);

		wp_localize_script(
			'wplink',
			'aioseoL10n',
			[
				'title'          => esc_html__( 'Insert/edit link', 'all-in-one-seo-pack' ),
				'update'         => esc_html__( 'Update', 'all-in-one-seo-pack' ),
				'save'           => esc_html__( 'Add Link', 'all-in-one-seo-pack' ),
				'noTitle'        => esc_html__( '(no title)', 'default' ), // phpcs:ignore AIOSEO.Wp.I18n.TextDomainMismatch, WordPress.WP.I18n.TextDomainMismatch
				'labelTitle'     => esc_html__( 'Title', 'all-in-one-seo-pack' ),
				'noMatchesFound' => esc_html__( 'No results found.', 'all-in-one-seo-pack' ),
				'linkSelected'   => esc_html__( 'Link selected.', 'all-in-one-seo-pack' ),
				'linkInserted'   => esc_html__( 'Link has been inserted.', 'all-in-one-seo-pack' ),
				// Translators: 1 - HTML whitespace character, 2 - Opening HTML code tag, 3 - Closing HTML code tag.
				'noFollow'       => sprintf( esc_html__( '%1$sAdd %2$srel="nofollow"%3$s to link', 'all-in-one-seo-pack' ), '&nbsp;', '<code>', '</code>' ),
				// Translators: 1 - HTML whitespace character, 2 - Opening HTML code tag, 3 - Closing HTML code tag.
				'sponsored'      => sprintf( esc_html__( '%1$sAdd %2$srel="sponsored"%3$s to link', 'all-in-one-seo-pack' ), '&nbsp;', '<code>', '</code>' ),
				// Translators: 1 - HTML whitespace character, 2 - Opening HTML code tag, 3 - Closing HTML code tag.
				'ugc'            => sprintf( esc_html__( '%1$sAdd %2$srel="UGC"%3$s to link', 'all-in-one-seo-pack' ), '&nbsp;', '<code>', '</code>' ),
				// Translators: Minimum input length in characters to start searching posts in the "Insert/edit link" modal.
				'minInputLength' => (int) _x( '3', 'minimum input length for searching post links', 'all-in-one-seo-pack' ),
			]
		);
	}

	/**
	 * Registers our link format for the Block Editor.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addGutenbergLinkFormatScript() {
		if ( ! aioseo()->helpers->isScreenBase( 'post' ) ) {
			return;
		}

		$linkFormat = 'block';
		if ( is_plugin_active( 'gutenberg/gutenberg.php' ) ) {
			$data = get_plugin_data( WP_CONTENT_DIR . '/plugins/gutenberg/gutenberg.php', false, false );
			if ( version_compare( $data['Version'], '7.4.0', '<' ) ) {
				$linkFormat = 'block-old';
			}
		} else {
			if ( version_compare( get_bloginfo( 'version' ), '5.4', '<' ) ) {
				$linkFormat = 'block-old';
			}
		}

		wp_register_script(
			'aioseo-link-format',
			aioseo()->core->assets->getAssetsPath( false ) . "link-format/link-format-$linkFormat.js",
			[
				'wp-blocks',
				'wp-i18n',
				'wp-element',
				'wp-plugins',
				'wp-components',
				'wp-edit-post',
				'wp-api',
				'wp-editor',
				'wp-hooks',
				'lodash'
			],
			aioseo()->version,
			true
		);
	}

	/**
	 * Adds All in One SEO to the Admin Bar.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function adminBarMenu() {
		if ( false === apply_filters( 'aioseo_show_in_admin_bar', true ) ) {
			return;
		}

		$firstPageSlug = $this->getFirstAvailablePageSlug();
		if ( ! $firstPageSlug ) {
			return;
		}

		$classes           = is_admin()
			? 'wp-core-ui wp-ui-notification aioseo-menu-notification-counter'
			: 'aioseo-menu-notification-counter aioseo-menu-notification-counter-frontend';
		$notificationCount = count( Models\Notification::getAllActiveNotifications() );
		$htmlCount         = 10 > $notificationCount ? $notificationCount : '!';
		$htmlCount         = $htmlCount ? "<div class=\"{$classes}\">" . $htmlCount . '</div>' : '';
		$htmlCount        .= '<div id="aioseo-menu-new-notifications"></div>';

		$this->adminBarMenuItems[] = [
			'id'    => 'aioseo-main',
			'title' => '<div class="ab-item aioseo-logo svg"></div><span class="text">' . esc_html__( 'SEO', 'all-in-one-seo-pack' ) . '</span>' . wp_kses_post( $htmlCount ),
			'href'  => esc_url( admin_url( 'admin.php?page=' . $firstPageSlug ) )
		];

		if ( $notificationCount ) {
			$this->adminBarMenuItems[] = [
				'parent' => 'aioseo-main',
				'id'     => 'aioseo-notifications',
				'title'  => esc_html__( 'Notifications', 'all-in-one-seo-pack' ) . ' <div class="aioseo-menu-notification-indicator"></div>',
				'href'   => admin_url( 'admin.php?page=' . $firstPageSlug . '&notifications=true' ),
			];
		}

		$this->adminBarMenuItems[] = aioseo()->standalone->seoPreview->getAdminBarMenuItemNode();

		$currentScreen = aioseo()->helpers->getCurrentScreen();
		if (
			is_admin() &&
			( 'post' === $currentScreen->base || 'term' === $currentScreen->base )
		) {
			$this->isEditor = true;
		}

		$htmlSitemapRequested = aioseo()->htmlSitemap->isDedicatedPage;
		if ( $htmlSitemapRequested || ! is_admin() || $this->isEditor ) {
			$this->addPageAnalyzerMenuItems();
		}

		if ( $htmlSitemapRequested ) {
			global $wp_admin_bar; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			$wp_admin_bar->remove_node( 'edit' ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		}

		$this->addSettingsMenuItems();
		$this->addEditSeoMenuItem();

		// Actually add in the menu bar items.
		$this->addAdminBarMenuItems();
	}

	/**
	 * Actually adds the menu items to the admin bar.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function addAdminBarMenuItems() {
		global $wp_admin_bar; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		foreach ( $this->adminBarMenuItems as $item ) {
			$wp_admin_bar->add_menu( $item ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		}
	}

	/**
	 * Adds the Analyze this Page menu item to the admin bar.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addPageAnalyzerMenuItems() {
		$url           = '';
		$currentScreen = aioseo()->helpers->getCurrentScreen();
		if (
			is_singular() ||
			( is_admin() && 'post' === $currentScreen->base )
		) {
			$post = aioseo()->helpers->getPost();
			if ( is_a( $post, 'WP_Post' ) && 'publish' === $post->post_status && '' !== $post->post_name ) {
				$url = get_permalink( $post->ID );
			}
		}

		if (
			is_category() ||
			is_tag() ||
			is_tax() ||
			( is_admin() && 'term' === $currentScreen->base )
		) {
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended, HM.Security.NonceVerification.Recommended
			$termId = ! empty( $_REQUEST['tag_ID'] ) ? intval( $_REQUEST['tag_ID'] ) : 0;
			$term   = is_admin() && $termId ? get_term( $termId ) : get_queried_object();
			if ( is_a( $term, 'WP_Term' ) ) {
				$url = get_term_link( $term );
			}
		}

		if ( ! $url ) {
			return;
		}

		$this->adminBarMenuItems[] = [
			'id'     => 'aioseo-analyze-page',
			'parent' => 'aioseo-main',
			'title'  => esc_html__( 'Analyze this page', 'all-in-one-seo-pack' )
		];

		$url = urlencode( $url );

		$submenuItems = [
			[
				'id'    => 'aioseo-analyze-page-pagespeed',
				'title' => esc_html__( 'Google Page Speed Test', 'all-in-one-seo-pack' ),
				'href'  => 'https://pagespeed.web.dev/report?url=' . $url
			],
			[
				'id'    => 'aioseo-analyze-page-rich-results-test',
				'title' => esc_html__( 'Google Rich Results Test', 'all-in-one-seo-pack' ),
				'href'  => 'https://search.google.com/test/rich-results?url=' . $url
			],
			[
				'id'    => 'aioseo-analyze-page-schema-org-validator',
				'title' => esc_html__( 'Schema.org Validator', 'all-in-one-seo-pack' ),
				'href'  => 'https://validator.schema.org/?url=' . $url
			],
			[
				'id'    => 'aioseo-analyze-page-inlinks',
				'title' => esc_html__( 'Inbound Links', 'all-in-one-seo-pack' ),
				'href'  => 'https://search.google.com/search-console/links/drilldown?resource_id=' . urlencode( get_option( 'siteurl' ) ) . '&type=EXTERNAL&target=' . $url . '&domain='
			],
			[
				'id'    => 'aioseo-analyze-page-facebookdebug',
				'title' => esc_html__( 'Facebook Debugger', 'all-in-one-seo-pack' ),
				'href'  => 'https://developers.facebook.com/tools/debug/?q=' . $url
			],
			[
				'id'    => 'aioseo-external-tools-linkedin-post-inspector',
				'title' => esc_html__( 'LinkedIn Post Inspector', 'all-in-one-seo-pack' ),
				'href'  => "https://www.linkedin.com/post-inspector/inspect/$url"
			],
			[
				'id'    => 'aioseo-analyze-page-htmlvalidation',
				'title' => esc_html__( 'HTML Validator', 'all-in-one-seo-pack' ),
				'href'  => '//validator.w3.org/check?uri=' . $url
			],
			[
				'id'    => 'aioseo-analyze-page-cssvalidation',
				'title' => esc_html__( 'CSS Validator', 'all-in-one-seo-pack' ),
				'href'  => '//jigsaw.w3.org/css-validator/validator?uri=' . $url
			]
		];

		foreach ( $submenuItems as $item ) {
			$this->adminBarMenuItems[] = [
				'parent' => 'aioseo-analyze-page',
				'id'     => $item['id'],
				'title'  => $item['title'],
				'href'   => $item['href'],
				'meta'   => [ 'target' => '_blank' ]
			];
		}
	}

	/**
	 * Adds the current post menu items to the admin bar.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	protected function addEditSeoMenuItem() {
		// Don't show if we're on the home page and the home page is the latest posts or if we're not in a singular context.
		if ( aioseo()->helpers->isDynamicHomePage() || ! is_singular() ) {
			return;
		}

		$post = aioseo()->helpers->getPost();
		if ( empty( $post ) ) {
			return;
		}

		$href = get_edit_post_link( $post->ID );
		if ( ! $href ) {
			return;
		}

		$this->adminBarMenuItems[] = [
			'id'     => 'aioseo-edit-' . $post->ID,
			'parent' => 'aioseo-main',
			'title'  => esc_html__( 'Edit SEO', 'all-in-one-seo-pack' ),
			'href'   => $href . '#aioseo-settings',
		];
	}

	/**
	 * Add the settings items to the menu bar.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function addSettingsMenuItems() {
		if ( ! is_admin() || $this->isEditor ) {
			$this->adminBarMenuItems[] = [
				'id'     => 'aioseo-settings-main',
				'parent' => 'aioseo-main',
				// Translators: This is an action link users can click to open the General Settings menu.
				'title'  => esc_html__( 'SEO Settings', 'all-in-one-seo-pack' )
			];
		}

		$parent = is_admin() && ! $this->isEditor ? 'aioseo-main' : 'aioseo-settings-main';
		foreach ( $this->pages as $id => $page ) {
			// Remove page from admin bar menu.
			if ( ! empty( $page['hide_admin_bar_menu'] ) ) {
				continue;
			}

			if ( ! current_user_can( $this->getPageRequiredCapability( $id ) ) ) {
				continue;
			}

			$this->adminBarMenuItems[] = [
				'id'     => $id,
				'parent' => $parent,
				'title'  => $page['menu_title'],
				'href'   => esc_url( admin_url( 'admin.php?page=' . $id ) )
			];
		}
	}

	/**
	 * Get the required capability for given admin page.
	 *
	 * @since 4.1.3
	 *
	 * @param  string $pageSlug The slug of the page.
	 * @return string           The required capability.
	 */
	public function getPageRequiredCapability( $pageSlug ) { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return apply_filters( 'aioseo_manage_seo', 'aioseo_manage_seo' );
	}

	/**
	 * Add the menu inside of WordPress.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addMenu() {
		$this->addMainMenu();

		foreach ( $this->pages as $slug => $page ) {
			$hook = add_submenu_page(
				$page['parent'],
				! empty( $page['page_title'] ) ? $page['page_title'] : $page['menu_title'],
				$page['menu_title'],
				$this->getPageRequiredCapability( $slug ),
				$slug,
				[ $this, 'page' ]
			);

			add_action( "load-{$hook}", [ $this, 'hooks' ] );
		}

		if ( ! current_user_can( $this->getPageRequiredCapability( $this->pageSlug ) ) ) {
			remove_submenu_page( $this->pageSlug, $this->pageSlug );
		}

		global $submenu;
		if ( current_user_can( $this->getPageRequiredCapability( 'aioseo-redirects' ) ) ) {
			$submenu['tools.php'][] = [
				esc_html__( 'Redirection Manager', 'all-in-one-seo-pack' ),
				$this->getPageRequiredCapability( 'aioseo-redirects' ),
				admin_url( '/admin.php?page=aioseo-redirects' )
			];
		}

		if ( current_user_can( $this->getPageRequiredCapability( 'aioseo-search-appearance' ) ) ) {
			$submenu['users.php'][] = [
				esc_html__( 'Author SEO', 'all-in-one-seo-pack' ),
				$this->getPageRequiredCapability( 'aioseo-search-appearance' ),
				admin_url( '/admin.php?page=aioseo-search-appearance/#author-seo' )
			];
		}

		// We use the global submenu, because we are adding an external link here.
		$count         = count( Models\Notification::getAllActiveNotifications() );
		$firstPageSlug = $this->getFirstAvailablePageSlug();
		if (
			$count &&
			! empty( $submenu[ $this->pageSlug ] ) &&
			! empty( $firstPageSlug )
		) {
			array_unshift( $submenu[ $this->pageSlug ], [
				esc_html__( 'Notifications', 'all-in-one-seo-pack' ) . '<div class="aioseo-menu-notification-indicator"></div>',
				$this->getPageRequiredCapability( $firstPageSlug ),
				admin_url( 'admin.php?page=' . $firstPageSlug . '&notifications=true' )
			] );
		}
	}

	/**
	 * Add the main menu.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $slug which slug to use.
	 * @return void
	 */
	protected function addMainMenu( $slug = 'aioseo' ) {
		add_menu_page(
			$this->menuName,
			$this->menuName,
			$this->getPageRequiredCapability( $slug ),
			$slug,
			'__return_true',
			'data:image/svg+xml;base64,' . base64_encode( aioseo()->helpers->logo( 16, 16, '#A0A5AA' ) ),
			'80.01234567890'
		);
	}

	/**
	 * Hides the Scheduled Actions menu.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	public function hideScheduledActionsMenu() {
		if ( ! apply_filters( 'aioseo_hide_action_scheduler_menu', true ) ) {
			return;
		}

		global $submenu;
		if ( ! isset( $submenu['tools.php'] ) ) {
			return;
		}

		foreach ( $submenu['tools.php'] as $index => $props ) {
			if ( ! empty( $props[2] ) && 'action-scheduler' === $props[2] ) {
				unset( $submenu['tools.php'][ $index ] );

				return;
			}
		}
	}

	/**
	 * Output the HTML for the page.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function page() {
		echo '<div id="aioseo-app">';
		aioseo()->templates->getTemplate( 'admin/settings-page.php' );
		echo '</div>';

		if ( aioseo()->standalone->flyoutMenu->isEnabled() ) {
			echo '<div id="aioseo-flyout-menu"></div>';
		}
	}

	/**
	 * Hooks for loading our pages.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function hooks() {
		$currentScreen = aioseo()->helpers->getCurrentScreen();
		global $admin_page_hooks; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		if ( ! is_object( $currentScreen ) || empty( $currentScreen->id ) || empty( $admin_page_hooks ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			return;
		}

		$pages = [
			'dashboard',
			'settings',
			'search-appearance',
			'social-networks',
			'sitemaps',
			'link-assistant',
			'redirects',
			'local-seo',
			'seo-analysis',
			'search-statistics',
			'tools',
			'feature-manager',
			'monsterinsights',
			'about',
			'seo-revisions'
		];

		foreach ( $pages as $page ) {
			$addScripts = false;

			if ( 'toplevel_page_aioseo' === $currentScreen->id ) {
				$addScripts = true;
			}

			if ( ! empty( $admin_page_hooks['aioseo'] ) && $currentScreen->id === $admin_page_hooks['aioseo'] ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				$addScripts = true;
			}

			if ( strpos( $currentScreen->id, 'aioseo-' . $page ) !== false ) {
				$addScripts = true;
			}

			if ( ! $addScripts ) {
				continue;
			}

			if ( 'tools' === $page ) {
				$this->checkForRedirects();
			}

			// Redirect our Analytics page to the appropriate plugin page.
			if ( 'monsterinsights' === $page ) {

				$pluginData = aioseo()->helpers->getPluginData();

				if (
					(
						$pluginData['miLite']['activated'] ||
						$pluginData['miPro']['activated']
					) &&
					function_exists( 'MonsterInsights' ) &&
					function_exists( 'monsterinsights_get_ua' )
				) {
					if ( (bool) monsterinsights_get_ua() ) {
						wp_safe_redirect( $pluginData['miLite']['adminUrl'] );
						exit;
					}
				}

				if (
					(
						$pluginData['emLite']['activated'] ||
						$pluginData['emPro']['activated']
					) &&
					function_exists( 'ExactMetrics' ) &&
					function_exists( 'exactmetrics_get_ua' )
				) {
					if ( (bool) exactmetrics_get_ua() ) {
						wp_safe_redirect( $pluginData['emLite']['adminUrl'] );
						exit;
					}
				}
			}

			// We don't want any plugin adding notices to our screens. Let's clear them out here.
			remove_all_actions( 'admin_notices' );
			remove_all_actions( 'network_admin_notices' );
			remove_all_actions( 'all_admin_notices' );
			remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );

			$this->currentPage = $page;
			add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAssets' ], 11 );
			add_action( 'admin_enqueue_scripts', [ aioseo()->filters, 'dequeueThirdPartyAssets' ], 99999 );
			add_action( 'admin_enqueue_scripts', [ aioseo()->filters, 'dequeueThirdPartyAssetsEarly' ], 0 );

			add_action( 'in_admin_footer', [ $this, 'addFooterPromotion' ] );
			add_filter( 'admin_footer_text', [ $this, 'addFooterText' ] );

			// Only enqueue the media library if we need it in our module
			if ( in_array( $page, [
				'social-networks',
				'search-appearance',
				'local-seo'
			], true ) ) {
				wp_enqueue_media();
			}

			break;
		}
	}

	/**
	 * Checks whether the current page is an AIOSEO menu page.
	 *
	 * @since 4.2.0
	 *
	 * @return bool Whether the current page is an AIOSEO menu page.
	 */
	public function isAioseoScreen() {
		$currentScreen = aioseo()->helpers->getCurrentScreen();
		if ( empty( $currentScreen->id ) ) {
			return false;
		}

		$adminPages = array_keys( $this->pages );
		$adminPages = array_map( function( $slug ) {
			if ( 'aioseo' === $slug ) {
				return 'toplevel_page_aioseo';
			}

			return 'all-in-one-seo_page_' . $slug;
		}, $adminPages );

		return in_array( $currentScreen->id, $adminPages, true );
	}

	/**
	 * Enqueue admin assets for the current page.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	public function enqueueAssets() {
		$page = str_replace( '{page}', $this->currentPage, $this->assetSlugs['pages'] );
		aioseo()->core->assets->load( $page, [], aioseo()->helpers->getVueData( $this->currentPage ) );
	}

	/**
	 * Add footer text to the WordPress admin screens.
	 *
	 * @since 4.0.0
	 *
	 * @return string The footer text.
	 */
	public function addFooterText() {
		$linkText = esc_html__( 'Give us a 5-star rating!', 'all-in-one-seo-pack' );
		$href     = 'https://wordpress.org/support/plugin/all-in-one-seo-pack/reviews/?filter=5#new-post';

		$link1 = sprintf(
			'<a href="%1$s" target="_blank" title="%2$s">&#9733;&#9733;&#9733;&#9733;&#9733;</a>',
			$href,
			$linkText
		);

		$link2 = sprintf(
			'<a href="%1$s" target="_blank" title="%2$s">WordPress.org</a>',
			$href,
			$linkText
		);

		printf(
			// Translators: 1 - The plugin name ("All in One SEO"), - 2 - This placeholder will be replaced with star icons, - 3 - "WordPress.org" - 4 - The plugin name ("All in One SEO").
			esc_html__( 'Please rate %1$s %2$s on %3$s to help us spread the word. Thank you!', 'all-in-one-seo-pack' ),
			sprintf( '<strong>%1$s</strong>', esc_html( AIOSEO_PLUGIN_NAME ) ),
			wp_kses_post( $link1 ),
			wp_kses_post( $link2 )
		);

		// Stop WP Core from outputting its version number and instead add both theirs & ours.
		global $wp_version; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
		printf(
			wp_kses_post( '<p class="alignright">%1$s</p>' ),
			sprintf(
				// Translators: 1 - WP Core version number, 2 - AIOSEO version number.
				esc_html__( 'WordPress %1$s | AIOSEO %2$s', 'all-in-one-seo-pack' ),
				esc_html( $wp_version ), // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				esc_html( AIOSEO_VERSION )
			)
		);

		remove_filter( 'update_footer', 'core_update_footer' );

		return '';
	}

	/**
	 * Renders the SEO Score button in the Publish metabox.
	 *
	 * @since 4.0.0
	 *
	 * @param  \WP_Post $post The post object.
	 * @return void
	 */
	public function addPublishScore( $post ) {
		$pageAnalysisCapability = aioseo()->access->hasCapability( 'aioseo_page_analysis' );
		if ( empty( $pageAnalysisCapability ) ) {
			return;
		}

		if ( aioseo()->helpers->isTruSeoEligible( $post->ID ) ) {
			$score = (int) Models\Post::getPost( $post->ID )->seo_score;
			$path  = 'M10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47716 0 0 4.47715 0 10C0 15.5228 4.47716 20 10 20ZM8.40767 3.65998C8.27222 3.45353 8.02129 3.357 7.79121 3.43828C7.52913 3.53087 7.27279 3.63976 7.02373 3.76429C6.80511 3.87361 6.69542 4.12332 6.74355 4.36686L6.91501 5.23457C6.95914 5.45792 6.86801 5.68459 6.69498 5.82859C6.42152 6.05617 6.16906 6.31347 5.94287 6.59826C5.80229 6.77526 5.58046 6.86908 5.36142 6.82484L4.51082 6.653C4.27186 6.60473 4.02744 6.71767 3.92115 6.94133C3.86111 7.06769 3.80444 7.19669 3.75129 7.32826C3.69815 7.45983 3.64929 7.59212 3.60464 7.72495C3.52562 7.96007 3.62107 8.21596 3.82396 8.35351L4.54621 8.84316C4.73219 8.96925 4.82481 9.19531 4.80234 9.42199C4.7662 9.78671 4.76767 10.1508 4.80457 10.5089C4.82791 10.7355 4.73605 10.9619 4.55052 11.0886L3.82966 11.5811C3.62734 11.7193 3.53274 11.9753 3.61239 12.2101C3.70314 12.4775 3.80985 12.7391 3.93188 12.9932C4.03901 13.2163 4.28373 13.3282 4.5224 13.2791L5.37279 13.1042C5.59165 13.0591 5.8138 13.1521 5.95491 13.3287C6.17794 13.6077 6.43009 13.8653 6.70918 14.0961C6.88264 14.2396 6.97459 14.4659 6.93122 14.6894L6.76282 15.5574C6.71551 15.8013 6.8262 16.0507 7.04538 16.1591C7.16921 16.2204 7.29563 16.2782 7.42457 16.3324C7.55352 16.3867 7.68316 16.4365 7.81334 16.4821C8.19418 16.6154 8.72721 16.1383 9.1213 15.7855C9.31563 15.6116 9.4355 15.3654 9.43677 15.1018C9.43677 15.1004 9.43678 15.099 9.43678 15.0976L9.43677 13.6462C9.43677 13.6308 9.43736 13.6155 9.43852 13.6004C8.27454 13.3165 7.40918 12.248 7.40918 10.9732V9.43198C7.40918 9.31483 7.50224 9.21986 7.61706 9.21986H8.338V7.70343C8.338 7.49405 8.50433 7.32432 8.70952 7.32432C8.9147 7.32432 9.08105 7.49405 9.08105 7.70343V9.21986H11.0316V7.70343C11.0316 7.49405 11.1979 7.32432 11.4031 7.32432C11.6083 7.32432 11.7746 7.49405 11.7746 7.70343V9.21986H12.4956C12.6104 9.21986 12.7034 9.31483 12.7034 9.43198V10.9732C12.7034 12.2883 11.7825 13.3838 10.5628 13.625C10.5631 13.632 10.5632 13.6391 10.5632 13.6462L10.5632 15.0914C10.5632 15.36 10.6867 15.6107 10.8868 15.7853C11.2879 16.1351 11.8302 16.6079 12.2088 16.4742C12.4708 16.3816 12.7272 16.2727 12.9762 16.1482C13.1949 16.0389 13.3046 15.7891 13.2564 15.5456L13.085 14.6779C13.0408 14.4545 13.132 14.2278 13.305 14.0838C13.5785 13.8563 13.8309 13.599 14.0571 13.3142C14.1977 13.1372 14.4195 13.0434 14.6385 13.0876L15.4892 13.2595C15.7281 13.3077 15.9725 13.1948 16.0788 12.9711C16.1389 12.8448 16.1955 12.7158 16.2487 12.5842C16.3018 12.4526 16.3507 12.3204 16.3953 12.1875C16.4744 11.9524 16.3789 11.6965 16.176 11.559L15.4537 11.0693C15.2678 10.9432 15.1752 10.7171 15.1976 10.4905C15.2338 10.1258 15.2323 9.76167 15.1954 9.40357C15.1721 9.17699 15.2639 8.95062 15.4495 8.82387L16.1703 8.33141C16.3726 8.1932 16.4672 7.93715 16.3876 7.70238C16.2968 7.43495 16.1901 7.17337 16.0681 6.91924C15.961 6.69615 15.7162 6.58422 15.4776 6.63333L14.6272 6.8083C14.4083 6.85333 14.1862 6.76033 14.0451 6.58377C13.822 6.30474 13.5699 6.04713 13.2908 5.81632C13.1173 5.67287 13.0254 5.44652 13.0688 5.22301L13.2372 4.35503C13.2845 4.11121 13.1738 3.86179 12.9546 3.75334C12.8308 3.69208 12.7043 3.63424 12.5754 3.58002C12.4465 3.52579 12.3168 3.47593 12.1866 3.43037C11.9562 3.34974 11.7055 3.44713 11.5707 3.65416L11.0908 4.39115C10.9672 4.58093 10.7457 4.67543 10.5235 4.65251C10.1661 4.61563 9.80932 4.61712 9.45837 4.65477C9.23633 4.6786 9.01448 4.58486 8.89027 4.39554L8.40767 3.65998Z'; // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			?>
			<div class="misc-pub-section aioseo-score-settings">
				<svg viewBox="0 0 20 20" width="20" height="20" xmlns="http://www.w3.org/2000/svg">
					<path fill-rule="evenodd" clip-rule="evenodd" d="<?php echo esc_attr( $path ); ?>" fill="#82878C" />
				</svg>
				<span>
					<?php
						echo sprintf(
							// Translators: 1 - The short plugin name ("AIOSEO").
							esc_html__( '%1$s Score', 'all-in-one-seo-pack' ),
							esc_html( AIOSEO_PLUGIN_SHORT_NAME )
						);
					?>
				</span>
				<div id="aioseo-post-settings-sidebar-button" class="aioseo-score-button classic-editor <?php echo esc_attr( $this->getScoreClass( $score ) ); ?>">
					<span id="aioseo-post-score"><?php echo esc_attr( $score . '/100' ); ?></span>
				</div>
			</div>
			<?php
		}
	}

	/**
	 * Check the query args to see if we need to redirect to an external URL.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	protected function checkForRedirects() {}

	/**
	 * Starts the cleaning procedure to fix escaped, corrupted data.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	public function scheduleUnescapeData() {
		aioseo()->core->cache->update( 'unslash_escaped_data_posts', time(), WEEK_IN_SECONDS );
		aioseo()->actionScheduler->scheduleSingle( 'aioseo_unslash_escaped_data_posts', 120 );
	}

	/**
	 * Unlashes corrupted escaped data in posts.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	public function unslashEscapedDataPosts() {
		$postsToUnslash = apply_filters( 'aioseo_debug_unslash_escaped_posts', 200 );
		$timeStarted    = gmdate( 'Y-m-d H:i:s', aioseo()->core->cache->get( 'unslash_escaped_data_posts' ) );

		$posts = aioseo()->core->db->start( 'aioseo_posts' )
			->select( '*' )
			->whereRaw( "updated < '$timeStarted'" )
			->orderBy( 'updated ASC' )
			->limit( $postsToUnslash )
			->run()
			->result();

		if ( empty( $posts ) ) {
			aioseo()->core->cache->delete( 'unslash_escaped_data_posts' );

			return;
		}

		aioseo()->actionScheduler->scheduleSingle( 'aioseo_unslash_escaped_data_posts', 120, [], true );

		foreach ( $posts as $post ) {
			$aioseoPost = Models\Post::getPost( $post->post_id );
			foreach ( $this->getColumnsToUnslash() as $columnName ) {
				// Remove backslashes but preserve encoded unicode characters in JSON data.
				$aioseoPost->$columnName = aioseo()->helpers->pregReplace( '/\\\(?![uU][+]?[a-zA-Z0-9]{4})/', '', $post->$columnName );
			}
			$aioseoPost->images          = null;
			$aioseoPost->image_scan_date = null;
			$aioseoPost->videos          = null;
			$aioseoPost->video_scan_date = null;
			$aioseoPost->save();
		}
	}

	/**
	 * Returns a list of names of database columns that should be unslashed when cleaning the corrupted data.
	 *
	 * @since 4.1.2
	 *
	 * @return array The list of column names.
	 */
	protected function getColumnsToUnslash() {
		return [
			'title',
			'description',
			'keywords',
			'keyphrases',
			'page_analysis',
			'canonical_url',
			'og_title',
			'og_description',
			'og_image_custom_url',
			'og_image_custom_fields',
			'og_video',
			'og_custom_url',
			'og_article_section',
			'og_article_tags',
			'twitter_title',
			'twitter_description',
			'twitter_image_custom_url',
			'twitter_image_custom_fields',
			'schema_type_options',
			'local_seo',
			'options'
		];
	}

	/**
	 * Get the first available page item for the current user.
	 *
	 * @since 4.1.3
	 *
	 * @return bool|string The page slug.
	 */
	public function getFirstAvailablePageSlug() {
		foreach ( $this->pages as $slug => $page ) {
			// Ignore other pages.
			if ( $this->pageSlug !== $page['parent'] ) {
				continue;
			}

			if ( current_user_can( $this->getPageRequiredCapability( $slug ) ) ) {
				return $slug;
			}
		}

		return false;
	}

	/**
	 * Appends a message to the default WordPress "trashed" message.
	 *
	 * @since 4.1.2
	 *
	 * @param  array $messages The original messages.
	 * @return array           The modified messages.
	 */
	public function appendTrashedMessage( $messages ) {
		// Let advanced users override this.

		if ( apply_filters( 'aioseo_redirects_disable_trashed_posts_suggestions', false ) ) {
			return $messages;
		}

		if ( function_exists( 'aioseoRedirects' ) && aioseoRedirects()->options->monitor->trash ) {
			return $messages;
		}

		if ( empty( $_GET['ids'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended  
			return $messages;
		}

		$ids = array_map( 'intval', explode( ',', sanitize_text_field( wp_unslash( $_GET['ids'] ) ) ) ); // phpcs:ignore HM.Security.NonceVerification.Recommended, HM.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended, Generic.Files.LineLength.MaxExceeded

		$posts = [];
		foreach ( $ids as $id ) {
			// We need to clone the post here so we can get a real permalink for the post even if it is not published already.
			$post = aioseo()->helpers->getPost( $id );
			if ( ! is_a( $post, 'WP_Post' ) ) {
				continue;
			}

			$post->post_status = 'publish';
			$post->post_name   = sanitize_title(
				$post->post_name ? $post->post_name : $post->post_title,
				$post->ID
			);

			$posts[] = [
				'url'    => str_replace( '__trashed', '', get_permalink( $post ) ),
				'target' => '/',
				'type'   => 301
			];
		}

		if ( empty( $posts ) ) {
			return $messages;
		}

		$url         = aioseo()->slugMonitor->manualRedirectUrl( $posts );
		$addRedirect = _n( 'Add Redirect to improve SEO', 'Add Redirects to improve SEO', count( $posts ), 'all-in-one-seo-pack' );

		$postType = get_post_type( $id );
		if ( empty( $messages[ $postType ]['trashed'] ) ) {
			$messages[ $postType ]['trashed'] = $messages['post']['trashed'];
		}

		$messages[ $postType ]['trashed'] = $messages[ $postType ]['trashed'] . '&nbsp;<a href="' . $url . '" class="aioseo-redirects-trashed-post">' . $addRedirect . '</a> |';

		return $messages;
	}

	/**
	* Get the class name for the Score button.
	* Depending on the score the button should have different color.
	*
	* @since 4.0.0
	*
	* @param  int    $score The content to retrieve from the remote URL.
	* @return string        The class name for Score button.
	*/
	private function getScoreClass( $score ) {
		$scoreClass = 50 < $score ? 'score-orange' : 'score-red';

		if ( 0 === $score ) {
			$scoreClass = 'score-none';
		}

		if ( $score >= 80 ) {
			$scoreClass = 'score-green';
		}

		return $scoreClass;
	}

	/**
	 * Loads the plugin text domain.
	 *
	 * @since 4.1.4
	 *
	 * @return void
	 */
	public function loadTextDomain() {
		aioseo()->helpers->loadTextDomain( 'all-in-one-seo-pack' );
	}

	/**
	 * Add the div for the modal portal.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	public function addAioseoModalPortal() {
		echo '<div id="aioseo-modal-portal"></div>';
	}

	/**
	 * Outputs the element we can mount our footer promotion standalone Vue app on.
	 * Also enqueues the assets.
	 *
	 * @since   4.3.6
	 * @version 4.4.3
	 *
	 * @return void
	 */
	public function addFooterPromotion() {
		echo wp_kses_post( '<div id="aioseo-footer-links"></div>' );

		aioseo()->core->assets->load( 'src/vue/standalone/footer-links/main.js' );
	}
}Common/Admin/WritingAssistant.php000066600000005005151135505570013065 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models;

/**
 * The Admin class.
 *
 * @since 4.7.4
 */
class WritingAssistant {
	/**
	 * Class constructor.
	 *
	 * @since 4.7.4
	 */
	public function __construct() {
		add_action( 'add_meta_boxes', [ $this, 'addMetabox' ] );
		add_action( 'delete_post', [ $this, 'deletePost' ] );
	}

	/**
	 * Deletes the writing assistant post.
	 *
	 * @since 4.7.4
	 *
	 * @param  int  $postId The post id.
	 * @return void
	 */
	public function deletePost( $postId ) {
		Models\WritingAssistantPost::getPost( $postId )->delete();
	}

	/**
	 * Adds a meta box to the page/posts screens.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	public function addMetabox() {
		if ( ! aioseo()->access->hasCapability( 'aioseo_page_writing_assistant_settings' ) ) {
			return;
		}

		$postType = get_post_type();
		if (
			(
				! aioseo()->options->writingAssistant->postTypes->all &&
				! in_array( $postType, aioseo()->options->writingAssistant->postTypes->included, true )
			) ||
			! in_array( $postType, aioseo()->helpers->getPublicPostTypes( true ), true )
		) {
			return;
		}

		// Skip post types that do not support an editor.
		if ( ! post_type_supports( $postType, 'editor' ) ) {
			return;
		}

		// Ignore certain plugins.
		if (
			aioseo()->thirdParty->webStories->isPluginActive() &&
			'web-story' === $postType
		) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAssets' ] );

		// Translators: 1 - The plugin short name ("AIOSEO").
		$aioseoMetaboxTitle = sprintf( esc_html__( '%1$s Writing Assistant', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME );

		add_meta_box(
			'aioseo-writing-assistant-metabox',
			$aioseoMetaboxTitle,
			[ $this, 'renderMetabox' ],
			null,
			'normal',
			'low'
		);
	}

	/**
	 * Render the on-page settings metabox with the Vue App wrapper.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	public function renderMetabox() {
		?>
		<div id="aioseo-writing-assistant-metabox-app">
			<?php aioseo()->templates->getTemplate( 'parts/loader.php' ); ?>
		</div>
		<?php
	}

	/**
	 * Enqueues the JS/CSS for the standalone.
	 *
	 * @since 4.7.4
	 *
	 * @return void
	 */
	public function enqueueAssets() {
		if ( ! aioseo()->helpers->isScreenBase( 'post' ) ) {
			return;
		}

		aioseo()->core->assets->load(
			'src/vue/standalone/writing-assistant/main.js',
			[],
			aioseo()->writingAssistant->helpers->getStandaloneVueData(),
			'aioseoWritingAssistant'
		);
	}
}Common/Admin/PostSettings.php000066600000031172151135505570012222 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models;

/**
 * Abstract class that Pro and Lite both extend.
 *
 * @since 4.0.0
 */
class PostSettings {
	/**
	 * The integrations instance.
	 *
	 * @since 4.4.3
	 *
	 * @var array[object]
	 */
	public $integrations;

	/**
	 * Initialize the admin.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function __construct() {
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}

		// Clear the Post Type Overview cache.
		add_action( 'save_post', [ $this, 'clearPostTypeOverviewCache' ], 100 );
		add_action( 'delete_post', [ $this, 'clearPostTypeOverviewCache' ], 100 );
		add_action( 'wp_trash_post', [ $this, 'clearPostTypeOverviewCache' ], 100 );

		if ( wp_doing_ajax() || wp_doing_cron() || ! is_admin() ) {
			return;
		}

		// Load Vue APP.
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueuePostSettingsAssets' ] );

		// Add metabox.
		add_action( 'add_meta_boxes', [ $this, 'addPostSettingsMetabox' ] );

		// Add metabox (upsell) to terms on init hook.
		add_action( 'init', [ $this, 'init' ], 1000 );

		// Save metabox.
		add_action( 'save_post', [ $this, 'saveSettingsMetabox' ] );
		add_action( 'edit_attachment', [ $this, 'saveSettingsMetabox' ] );
		add_action( 'add_attachment', [ $this, 'saveSettingsMetabox' ] );

		// Filter the sql clauses to show posts filtered by our params.
		add_filter( 'posts_clauses', [ $this, 'changeClausesToFilterPosts' ], 10, 2 );
	}

	/**
	 * Enqueues the JS/CSS for the on page/posts settings.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function enqueuePostSettingsAssets() {
		if (
			aioseo()->helpers->isScreenBase( 'event-espresso' ) ||
			aioseo()->helpers->isScreenBase( 'post' ) ||
			aioseo()->helpers->isScreenBase( 'term' ) ||
			aioseo()->helpers->isScreenBase( 'edit-tags' ) ||
			aioseo()->helpers->isScreenBase( 'site-editor' )
		) {
			$page = null;
			if (
				aioseo()->helpers->isScreenBase( 'event-espresso' ) ||
				aioseo()->helpers->isScreenBase( 'post' )
			) {
				$page = 'post';
			}

			aioseo()->core->assets->load( 'src/vue/standalone/post-settings/main.js', [], aioseo()->helpers->getVueData( $page ) );
			aioseo()->core->assets->load( 'src/vue/standalone/link-format/main.js', [], aioseo()->helpers->getVueData( $page ) );
		}

		$screen = aioseo()->helpers->getCurrentScreen();
		if ( empty( $screen->id ) ) {
			return;
		}

		if ( 'attachment' === $screen->id ) {
			wp_enqueue_media();
		}
	}

	/**
	 * Check whether or not we can add the metabox.
	 *
	 * @since 4.1.7
	 *
	 * @param  string  $postType The post type to check.
	 * @return boolean           Whether or not can add the Metabox.
	 */
	public function canAddPostSettingsMetabox( $postType ) {
		$dynamicOptions = aioseo()->dynamicOptions->noConflict();

		$pageAnalysisSettingsCapability = aioseo()->access->hasCapability( 'aioseo_page_analysis' );
		$generalSettingsCapability      = aioseo()->access->hasCapability( 'aioseo_page_general_settings' );
		$socialSettingsCapability       = aioseo()->access->hasCapability( 'aioseo_page_social_settings' );
		$schemaSettingsCapability       = aioseo()->access->hasCapability( 'aioseo_page_schema_settings' );
		$linkAssistantCapability        = aioseo()->access->hasCapability( 'aioseo_page_link_assistant_settings' );
		$redirectsCapability            = aioseo()->access->hasCapability( 'aioseo_page_redirects_manage' );
		$advancedSettingsCapability     = aioseo()->access->hasCapability( 'aioseo_page_advanced_settings' );
		$seoRevisionsSettingsCapability = aioseo()->access->hasCapability( 'aioseo_page_seo_revisions_settings' );

		if (
			$dynamicOptions->searchAppearance->postTypes->has( $postType ) &&
			$dynamicOptions->searchAppearance->postTypes->$postType->advanced->showMetaBox &&
			! (
				empty( $pageAnalysisSettingsCapability ) &&
				empty( $generalSettingsCapability ) &&
				empty( $socialSettingsCapability ) &&
				empty( $schemaSettingsCapability ) &&
				empty( $linkAssistantCapability ) &&
				empty( $redirectsCapability ) &&
				empty( $advancedSettingsCapability ) &&
				empty( $seoRevisionsSettingsCapability )
			)
		) {
			return true;
		}

		return false;
	}

	/**
	 * Adds a meta box to page/posts screens.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function addPostSettingsMetabox() {
		$screen = aioseo()->helpers->getCurrentScreen();
		if ( empty( $screen->post_type ) ) {
			return;
		}

		$postType = $screen->post_type;
		if ( $this->canAddPostSettingsMetabox( $postType ) ) {
			// Translators: 1 - The plugin short name ("AIOSEO").
			$aioseoMetaboxTitle = sprintf( esc_html__( '%1$s Settings', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME );

			add_meta_box(
				'aioseo-settings',
				$aioseoMetaboxTitle,
				[ $this, 'postSettingsMetabox' ],
				[ $postType ],
				'normal',
				apply_filters( 'aioseo_post_metabox_priority', 'high' )
			);
		}
	}

	/**
	 * Render the on page/posts settings metabox with Vue App wrapper.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function postSettingsMetabox() {
		$this->postSettingsHiddenField();
		?>
		<div id="aioseo-post-settings-metabox">
			<?php aioseo()->templates->getTemplate( 'parts/loader.php' ); ?>
		</div>
		<?php
	}

	/**
	 * Adds the hidden field where all the metabox data goes.
	 *
	 * @since 4.0.17
	 *
	 * @return void
	 */
	public function postSettingsHiddenField() {
		static $fieldExists = false;
		if ( $fieldExists ) {
			return;
		}

		$fieldExists = true;

		?>
		<div id="aioseo-post-settings-field">
			<input type="hidden" name="aioseo-post-settings" id="aioseo-post-settings" value=""/>
			<?php wp_nonce_field( 'aioseoPostSettingsNonce', 'PostSettingsNonce' ); ?>
		</div>
		<?php
	}

	/**
	 * Handles metabox saving.
	 *
	 * @since 4.0.3
	 *
	 * @param  int  $postId Post ID.
	 * @return void
	 */
	public function saveSettingsMetabox( $postId ) {
		if ( ! aioseo()->helpers->isValidPost( $postId, [ 'all' ] ) ) {
			return;
		}

		// Security check.
		if ( ! isset( $_POST['PostSettingsNonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['PostSettingsNonce'] ) ), 'aioseoPostSettingsNonce' ) ) {
			return;
		}

		// If we don't have our post settings input, we can safely skip.
		if ( ! isset( $_POST['aioseo-post-settings'] ) ) {
			return;
		}

		// Check user permissions.
		if ( ! current_user_can( 'edit_post', $postId ) ) {
			return;
		}

		$currentPost = json_decode( wp_unslash( ( $_POST['aioseo-post-settings'] ) ), true );
		$currentPost = aioseo()->helpers->sanitize( $currentPost );

		// If there is no data, there likely was an error, e.g. if the hidden field wasn't populated on load and the user saved the post without making changes in the metabox.
		// In that case we should return to prevent a complete reset of the data.

		if ( empty( $currentPost ) ) {
			return;
		}

		Models\Post::savePost( $postId, $currentPost );
	}

	/**
	 * Clear the Post Type Overview cache from our cache table.
	 *
	 * @since 4.2.0
	 *
	 * @param  int  $postId The Post ID being updated/deleted.
	 * @return void
	 */
	public function clearPostTypeOverviewCache( $postId ) {
		$postType = get_post_type( $postId );
		if ( empty( $postType ) ) {
			return;
		}

		aioseo()->core->cache->delete( $postType . '_overview_data' );
	}

	/**
	 * Get a list of post types with an overview showing how many posts are good, okay and so on.
	 *
	 * @since 4.2.0
	 *
	 * @return array The list of post types with the overview.
	 */
	public function getPostTypesOverview() {
		$overviewData      = [];
		$eligiblePostTypes = aioseo()->helpers->getTruSeoEligiblePostTypes();
		foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
			if ( ! in_array( $postType, $eligiblePostTypes, true ) ) {
				continue;
			}

			$overviewData[ $postType ] = $this->getPostTypeOverview( $postType );
		}

		return $overviewData;
	}

	/**
	 * Get how many posts are good, okay, needs improvement or are missing the focus keyphrase for the given post type.
	 *
	 * @since 4.2.0
	 *
	 * @param  string $postType The post type name.
	 * @return array            The overview data for the given post type.
	 */
	public function getPostTypeOverview( $postType ) {
		$overviewData = aioseo()->core->cache->get( $postType . '_overview_data' );
		if ( null !== $overviewData ) {
			return $overviewData;
		}

		$eligiblePostTypes = aioseo()->helpers->getTruSeoEligiblePostTypes();
		if ( ! in_array( $postType, $eligiblePostTypes, true ) ) {
			return [
				'total'               => 0,
				'withoutFocusKeyword' => 0,
				'needsImprovement'    => 0,
				'okay'                => 0,
				'good'                => 0
			];
		}

		$specialPageIds             = aioseo()->helpers->getSpecialPageIds();
		$implodedPageIdPlaceholders = array_fill( 0, count( $specialPageIds ), '%d' );
		$implodedPageIdPlaceholders = implode( ', ', $implodedPageIdPlaceholders );

		global $wpdb;

		// phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
		$overviewData = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT
					COUNT(*) as total,
					COALESCE( SUM(CASE WHEN ap.keyphrases = '' OR ap.keyphrases IS NULL OR ap.keyphrases LIKE %s THEN 1 ELSE 0 END), 0) as withoutFocusKeyword,
					COALESCE( SUM(CASE WHEN ap.seo_score IS NULL OR ap.seo_score = 0 THEN 1 ELSE 0 END), 0) as withoutTruSeoScore,
					COALESCE( SUM(CASE WHEN ap.seo_score > 0 AND ap.seo_score < 50 THEN 1 ELSE 0 END), 0) as needsImprovement,
					COALESCE( SUM(CASE WHEN ap.seo_score BETWEEN 50 AND 79 THEN 1 ELSE 0 END), 0) as okay,
					COALESCE( SUM(CASE WHEN ap.seo_score >= 80 THEN 1 ELSE 0 END), 0) as good
				FROM {$wpdb->posts} as p
				LEFT JOIN {$wpdb->prefix}aioseo_posts as ap ON ap.post_id = p.ID
				WHERE p.post_status = 'publish'
				AND p.post_type = %s
				AND p.ID NOT IN ( $implodedPageIdPlaceholders )",
				'{"focus":{"keyphrase":""%',
				$postType,
				...array_values( $specialPageIds )
			),
			ARRAY_A
		);

		// Ensure sure all the values are integers.
		foreach ( $overviewData as $key => $value ) {
			$overviewData[ $key ] = (int) $value;
		}

		// Give me the raw SQL of the query.
		aioseo()->core->cache->update( $postType . '_overview_data', $overviewData, HOUR_IN_SECONDS );

		return $overviewData;
	}

	/**
	 * Change the JOIN and WHERE clause to filter just the posts we need to show depending on the query string.
	 *
	 * @since 4.2.0
	 *
	 * @param  array     $clauses Associative array of the clauses for the query.
	 * @param  \WP_Query $query   The WP_Query instance (passed by reference).
	 * @return array              The clauses array updated.
	 */
	public function changeClausesToFilterPosts( $clauses, $query = null ) {
		if ( ! is_admin() || ! $query->is_main_query() ) {
			return $clauses;
		}

		$filter = filter_input( INPUT_GET, 'aioseo-filter' );
		if ( empty( $filter ) ) {
			return $clauses;
		}

		$whereClause        = '';
		$noKeyphrasesClause = "(aioseo_p.keyphrases = '' OR aioseo_p.keyphrases IS NULL OR aioseo_p.keyphrases LIKE '{\"focus\":{\"keyphrase\":\"\"%')";
		switch ( $filter ) {
			case 'withoutFocusKeyword':
				$whereClause = " AND $noKeyphrasesClause ";
				break;
			case 'withoutTruSeoScore':
				$whereClause = ' AND ( aioseo_p.seo_score IS NULL OR aioseo_p.seo_score = 0 ) ';
				break;
			case 'needsImprovement':
				$whereClause = ' AND ( aioseo_p.seo_score > 0 AND aioseo_p.seo_score < 50 ) ';
				break;
			case 'okay':
				$whereClause = ' AND aioseo_p.seo_score BETWEEN 50 AND 80 ';
				break;
			case 'good':
				$whereClause = ' AND aioseo_p.seo_score > 80 ';
				break;
		}

		$prefix            = aioseo()->core->db->prefix;
		$postsTable        = aioseo()->core->db->db->posts;
		$clauses['join']  .= " LEFT JOIN {$prefix}aioseo_posts AS aioseo_p ON ({$postsTable}.ID = aioseo_p.post_id) ";
		$clauses['where'] .= $whereClause;

		add_action( 'wp', [ $this, 'filterPostsAfterChangingClauses' ] );

		return $clauses;
	}

	/**
	 * Filter the posts array to remove the ones that are not eligible for page analysis.
	 * Hooked into `wp` action hook.
	 *
	 * @since 4.7.1
	 *
	 * @return void
	 */
	public function filterPostsAfterChangingClauses() {
		remove_action( 'wp', [ $this, 'filterPostsAfterChangingClauses' ] );
		// phpcs:disable Squiz.NamingConventions.ValidVariableName
		global $wp_query;
		if ( ! empty( $wp_query->posts ) && is_array( $wp_query->posts ) ) {
			$wp_query->posts = array_filter( $wp_query->posts, function ( $post ) {
				return aioseo()->helpers->isTruSeoEligible( $post->ID );
			} );

			// Update `post_count` for pagination.
			if ( isset( $wp_query->post_count ) ) {
				$wp_query->post_count = count( $wp_query->posts );
			}
		}
		// phpcs:enable Squiz.NamingConventions.ValidVariableName
	}
}Common/Admin/DeactivationSurvey.php000066600000022720151135505570013403 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Deactivation survey.
 *
 * @since 4.5.5
 */
class DeactivationSurvey {
	/**
	 * The API URL we are calling.
	 *
	 * @since 4.5.5
	 *
	 * @var string
	 */
	public $apiUrl = 'https://plugin.aioseo.com/wp-json/am-deactivate-survey/v1/deactivation-data';

	/**
	 * Name for this plugin.
	 *
	 * @since 4.5.5
	 *
	 * @var string
	 */
	public $name;

	/**
	 * Unique slug for this plugin.
	 *
	 * @since 4.5.5
	 *
	 * @var string
	 */
	public $plugin;

	/**
	 * Primary class constructor.
	 *
	 * @since 4.5.5
	 *
	 * @param string $name Plugin name.
	 * @param string $plugin Plugin slug.
	 */
	public function __construct( $name = '', $plugin = '' ) {
		$this->name   = $name;
		$this->plugin = $plugin;

		// Don't run deactivation survey on dev sites.
		if ( aioseo()->helpers->isDev() ) {
			// return;
		}

		add_action( 'admin_print_scripts', [ $this, 'js' ], 20 );
		add_action( 'admin_print_scripts', [ $this, 'css' ] );
		add_action( 'admin_footer', [ $this, 'modal' ] );
	}

	/**
	 * Returns the URL of the remote endpoint.
	 *
	 * @since 4.5.5
	 *
	 * @return string The URL.
	 */
	public function getApiUrl() {
		if ( defined( 'AIOSEO_DEACTIVATION_SURVEY_URL' ) ) {
			return AIOSEO_DEACTIVATION_SURVEY_URL;
		}

		return $this->apiUrl;
	}

	/**
	 * Checks if current admin screen is the plugins page.
	 *
	 * @since 4.5.5
	 *
	 * @return bool True if it is, false if not.
	 */
	public function isPluginPage() {
		$screen = aioseo()->helpers->getCurrentScreen();
		if ( empty( $screen->id ) ) {
			return false;
		}

		return in_array( $screen->id, [ 'plugins', 'plugins-network' ], true );
	}

	/**
	 * Survey javascript.
	 *
	 * @since 4.5.5
	 *
	 * @return void
	 */
	public function js() {
		if ( ! $this->isPluginPage() ) {
			return;
		}

		?>
		<script type="text/javascript">
		window.addEventListener("load", function() {
			var deactivateLink = document.querySelector('#the-list [data-slug="<?php echo esc_html( $this->plugin ); ?>"] span.deactivate a') ||
				document.querySelector('#deactivate-<?php echo esc_html( $this->plugin ); ?>'),
				overlay = document.querySelector('#am-deactivate-survey-<?php echo esc_html( $this->plugin ); ?>'),
				form = overlay.querySelector('form'),
				formOpen = false;

			deactivateLink.addEventListener('click', function(event) {
				event.preventDefault();
				overlay.style.display = 'table';
				formOpen = true;
				form.querySelector('.am-deactivate-survey-option:first-of-type input[type=radio]').focus();
			});

			form.addEventListener('change', function(event) {
				if (event.target.matches('input[type=radio]')) {
					event.preventDefault();
					Array.from(form.querySelectorAll('input[type=text], .error')).forEach(function(el) { el.style.display = 'none'; });
					Array.from(form.querySelectorAll('.am-deactivate-survey-option')).forEach(function(el) { el.classList.remove('selected'); });
					var option = event.target.closest('.am-deactivate-survey-option');
					option.classList.add('selected');
					
					var otherField = option.querySelector('input[type=text]');
					if (otherField) {
						otherField.style.display = 'block';
						otherField.focus();
					}
				}
			});

			form.addEventListener('click', function(event) {
				if (event.target.matches('.am-deactivate-survey-deactivate')) {
					event.preventDefault();
					window.location.href = deactivateLink.getAttribute('href');
				}
			});

			form.addEventListener('submit', function(event) {
				event.preventDefault();
				if (!form.querySelector('input[type=radio]:checked')) {
					if(!form.querySelector('span[class="error"]')) {
						form.querySelector('.am-deactivate-survey-footer')
						.insertAdjacentHTML('afterbegin', '<span class="error"><?php echo esc_js( __( 'Please select an option', 'all-in-one-seo-pack' ) ); ?></span>');
					}
					return;
				}

				var selected = form.querySelector('.selected');
				var otherField = selected.querySelector('input[type=text]');
				var data = {
					code: selected.querySelector('input[type=radio]').value,
					reason: selected.querySelector('.am-deactivate-survey-option-reason').textContent,
					details: otherField ? otherField.value : '',
					site: '<?php echo esc_url( home_url() ); ?>',
					plugin: '<?php echo esc_html( $this->plugin ); ?>'
				}

				var submitSurvey = fetch('<?php echo esc_url( $this->getApiUrl() ); ?>', {
					method: 'POST',
					body: JSON.stringify(data),
					headers: { 'Content-Type': 'application/json' }
				});

				submitSurvey.finally(function() {
					window.location.href = deactivateLink.getAttribute('href');
				});
			});

			document.addEventListener('keyup', function(event) {
				if (27 === event.keyCode && formOpen) {
					overlay.style.display = 'none';
					formOpen = false;
					deactivateLink.focus();
				}
			});
		});
		</script>
		<?php
	}

	/**
	 * Survey CSS.
	 *
	 * @since 4.5.5
	 *
	 * @return void
	 */
	public function css() {
		if ( ! $this->isPluginPage() ) {
			return;
		}

		?>
		<style type="text/css">
		.am-deactivate-survey-modal {
			display: none;
			table-layout: fixed;
			position: fixed;
			z-index: 9999;
			width: 100%;
			height: 100%;
			text-align: center;
			font-size: 14px;
			top: 0;
			left: 0;
			background: rgba(0,0,0,0.8);
		}
		.am-deactivate-survey-wrap {
			display: table-cell;
			vertical-align: middle;
		}
		.am-deactivate-survey {
			background-color: #fff;
			max-width: 550px;
			margin: 0 auto;
			padding: 30px;
			text-align: left;
		}
		.am-deactivate-survey .error {
			display: block;
			color: red;
			margin: 0 0 10px 0;
		}
		.am-deactivate-survey-title {
			display: block;
			font-size: 18px;
			font-weight: 700;
			text-transform: uppercase;
			border-bottom: 1px solid #ddd;
			padding: 0 0 18px 0;
			margin: 0 0 18px 0;
		}
		.am-deactivate-survey-title span {
			color: #999;
			margin-right: 10px;
		}
		.am-deactivate-survey-desc {
			display: block;
			font-weight: 600;
			margin: 0 0 18px 0;
		}
		.am-deactivate-survey-option {
			margin: 0 0 10px 0;
		}
		.am-deactivate-survey-option-input {
			margin-right: 10px !important;
		}
		.am-deactivate-survey-option-details {
			display: none;
			width: 90%;
			margin: 10px 0 0 30px;
		}
		.am-deactivate-survey-footer {
			margin-top: 18px;
		}
		.am-deactivate-survey-deactivate {
			float: right;
			font-size: 13px;
			color: #ccc;
			text-decoration: none;
			padding-top: 7px;
		}
		</style>
		<?php
	}

	/**
	 * Survey modal.
	 *
	 * @since 4.5.5
	 *
	 * @return void
	 */
	public function modal() {
		if ( ! $this->isPluginPage() ) {
			return;
		}

		$options = [
			1 => [
				'title' => esc_html__( 'I no longer need the plugin', 'all-in-one-seo-pack' ),
			],
			2 => [
				'title'   => esc_html__( 'I\'m switching to a different plugin', 'all-in-one-seo-pack' ),
				'details' => esc_html__( 'Please share which plugin', 'all-in-one-seo-pack' ),
			],
			3 => [
				'title' => esc_html__( 'I couldn\'t get the plugin to work', 'all-in-one-seo-pack' ),
			],
			4 => [
				'title' => esc_html__( 'It\'s a temporary deactivation', 'all-in-one-seo-pack' ),
			],
			5 => [
				'title'   => esc_html__( 'Other', 'all-in-one-seo-pack' ),
				'details' => esc_html__( 'Please share the reason', 'all-in-one-seo-pack' ),
			],
		];
		?>

		<div class="am-deactivate-survey-modal" id="am-deactivate-survey-<?php echo esc_html( $this->plugin ); ?>">
			<div class="am-deactivate-survey-wrap">
				<form class="am-deactivate-survey" method="post">
					<span class="am-deactivate-survey-title"><span class="dashicons dashicons-testimonial"></span><?php echo ' ' . esc_html__( 'Quick Feedback', 'all-in-one-seo-pack' ); ?></span>
					<span class="am-deactivate-survey-desc">
						<?php
						echo esc_html(
							sprintf(
								// Translators: 1 - The plugin name.
								__( 'If you have a moment, please share why you are deactivating %1$s:', 'all-in-one-seo-pack' ),
								$this->name
							)
						);
						?>
					</span>
					<div class="am-deactivate-survey-options">
						<?php foreach ( $options as $id => $option ) : ?>
							<div class="am-deactivate-survey-option">
								<label for="am-deactivate-survey-option-<?php echo esc_html( $this->plugin ); ?>-<?php echo intval( $id ); ?>" class="am-deactivate-survey-option-label">
									<input
										id="am-deactivate-survey-option-<?php echo esc_html( $this->plugin ); ?>-<?php echo intval( $id ); ?>"
										class="am-deactivate-survey-option-input"
										type="radio"
										name="code"
										value="<?php echo intval( $id ); ?>"
									/>
									<span class="am-deactivate-survey-option-reason"><?php echo esc_html( $option['title'] ); ?></span>
								</label>
								<?php if ( ! empty( $option['details'] ) ) : ?>
									<input class="am-deactivate-survey-option-details" type="text" placeholder="<?php echo esc_html( $option['details'] ); ?>" />
								<?php endif; ?>
							</div>
						<?php endforeach; ?>
					</div>
					<div class="am-deactivate-survey-footer">
						<button type="submit" class="am-deactivate-survey-submit button button-primary button-large">
							<?php
							echo sprintf(
								// Translators: 1 - & symbol.
								esc_html__( 'Submit %1$s Deactivate', 'all-in-one-seo-pack' ),
								'&amp;'
							);
							?>
						</button>
						<a href="#" class="am-deactivate-survey-deactivate">
						<?php
						echo sprintf(
							// Translators: 1 - & symbol.
							esc_html__( 'Skip %1$s Deactivate', 'all-in-one-seo-pack' ),
							'&amp;'
						);
						?>
						</a>
					</div>
				</form>
			</div>
		</div>
		<?php
	}
}Common/Admin/SeoAnalysis.php000066600000001661151135505570012006 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models\SeoAnalyzerResult;

/**
 * Handles all admin code for the SEO Analysis menu.
 *
 * @since 4.2.6
 */
class SeoAnalysis {
	/**
	 * Class constructor.
	 *
	 * @since 4.2.6
	 */
	public function __construct() {
		add_action( 'save_post', [ $this, 'bustStaticHomepageResults' ] );
	}

	/**
	 * Busts the SEO Analysis for the static homepage when it is updated.
	 *
	 * @since 4.2.6
	 *
	 * @param  int  $postId The post ID.
	 * @return void
	 */
	public function bustStaticHomepageResults( $postId ) {
		if ( ! aioseo()->helpers->isStaticHomePage( $postId ) ) {
			return;
		}

		aioseo()->internalOptions->internal->siteAnalysis->score = 0;
		SeoAnalyzerResult::deleteByUrl( null );

		aioseo()->core->cache->delete( 'analyze_site_code' );
		aioseo()->core->cache->delete( 'analyze_site_body' );
	}
}Common/Admin/Notices/DeprecatedWordPress.php000066600000011266151135505570015073 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin\Notices;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WordPress Deprecated Notice.
 *
 * @since 4.1.2
 */
class DeprecatedWordPress {
	/**
	 * Class Constructor.
	 *
	 * @since 4.1.2
	 */
	public function __construct() {
		add_action( 'wp_ajax_aioseo-dismiss-deprecated-wordpress-notice', [ $this, 'dismissNotice' ] );
	}

	/**
	 * Go through all the checks to see if we should show the notice.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	public function maybeShowNotice() {
		global $wp_version; // phpcs:ignore Squiz.NamingConventions.ValidVariableName

		$dismissed = get_option( '_aioseo_deprecated_wordpress_dismissed', true );
		if ( '1' === $dismissed ) {
			return;
		}

		// Show to users that interact with our pluign.
		if ( ! current_user_can( 'publish_posts' ) ) {
			return;
		}

		// Show if WordPress version is deprecated.
		if ( version_compare( $wp_version, '5.4', '>=' ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
			return;
		}

		$this->showNotice();

		// Print the script to the footer.
		add_action( 'admin_footer', [ $this, 'printScript' ] );
	}

	/**
	 * Actually show the review plugin.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	public function showNotice() {
		$medium = false !== strpos( AIOSEO_PHP_VERSION_DIR, 'pro' ) ? 'proplugin' : 'liteplugin';
		?>
		<div class="notice notice-warning aioseo-deprecated-wordpress-notice is-dismissible">
			<p>
				<?php
				echo wp_kses(
					sprintf(
						// Translators: 1 - Opening HTML bold tag, 2 - Closing HTML bold tag.
						__( 'Your site is running an %1$soutdated version%2$s of WordPress. We recommend using the latest version of WordPress in order to keep your site secure.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'<strong>',
						'</strong>'
					),
					[
						'strong' => [],
					]
				);
				?>
				<br><br>
				<?php
				echo wp_kses(
					sprintf(
						// phpcs:ignore Generic.Files.LineLength.MaxExceeded
						// Translators: 1 - Opening HTML bold tag, 2 - Closing HTML bold tag, 3 - The short plugin name ("AIOSEO"), 4 - The current year, 5 - Opening HTML link tag, 6 - Closing HTML link tag.
						__( '%1$sNote:%2$s %3$s will be discontinuing support for WordPress versions older than version 5.7 by the end of %4$s. %5$sRead more for additional information.%6$s', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'<strong>',
						'</strong>',
						'AIOSEO',
						gmdate( 'Y' ),
						'<a href="https://aioseo.com/docs/update-wordpress/?utm_source=WordPress&utm_medium=' . $medium . '&utm_campaign=outdated-wordpress-notice" target="_blank" rel="noopener noreferrer">', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						'</a>'
					),
					[
						'a'      => [
							'href'   => [],
							'target' => [],
							'rel'    => [],
						],
						'strong' => [],
					]
				);
				?>
			</p>
		</div>

		<?php
		// In case this is on plugin activation.
		if ( isset( $_GET['activate'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
			unset( $_GET['activate'] );
		}
	}

	/**
	 * Print the script for dismissing the notice.
	 *
	 * @since 4.1.2
	 *
	 * @return void
	 */
	public function printScript() {
		// Create a nonce.
		$nonce = wp_create_nonce( 'aioseo-dismiss-deprecated-wordpress' );
		?>
		<script>
			window.addEventListener('load', function () {
				var dismissBtn

				// Add an event listener to the dismiss button.
				dismissBtn = document.querySelector('.aioseo-deprecated-wordpress-notice .notice-dismiss')
				dismissBtn.addEventListener('click', function (event) {
					var httpRequest = new XMLHttpRequest(),
						postData    = ''

					// Build the data to send in our request.
					postData += '&action=aioseo-dismiss-deprecated-wordpress-notice'
					postData += '&nonce=<?php echo esc_html( $nonce ); ?>'

					httpRequest.open('POST', '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>')
					httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
					httpRequest.send(postData)
				})
			});
		</script>
		<?php
	}

	/**
	 * Dismiss the deprecated WordPress notice.
	 *
	 * @since 4.1.2
	 *
	 * @return string The successful response.
	 */
	public function dismissNotice() {
		// Early exit if we're not on a aioseo-dismiss-deprecated-wordpress-notice action.
		if ( ! isset( $_POST['action'] ) || 'aioseo-dismiss-deprecated-wordpress-notice' !== $_POST['action'] ) {
			return;
		}

		check_ajax_referer( 'aioseo-dismiss-deprecated-wordpress', 'nonce' );

		update_option( '_aioseo_deprecated_wordpress_dismissed', true );

		return wp_send_json_success();
	}
}Common/Admin/Notices/Notices.php000066600000041164151135505570012566 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin\Notices;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models;

/**
 * Abstract class that Pro and Lite both extend.
 *
 * @since 4.0.0
 */
class Notices {
	/**
	 * Source of notifications content.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $url = 'https://plugin-cdn.aioseo.com/wp-content/notifications.json';

	/**
	 * Review class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Review
	 */
	private $review = null;

	/**
	 * Migration class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Migration
	 */
	private $migration = null;

	/**
	 * Import class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var Import
	 */
	private $import = null;

	/**
	 * DeprecatedWordPress class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var DeprecatedWordPress
	 */
	private $deprecatedWordPress = null;

	/**
	 * ConflictingPlugins class instance.
	 *
	 * @since 4.5.1
	 *
	 * @var ConflictingPlugins
	 */
	private $conflictingPlugins = null;

	/**
	 * Class Constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'aioseo_admin_notifications_update', [ $this, 'update' ] );

		if ( ! is_admin() ) {
			return;
		}

		add_action( 'updated_option', [ $this, 'maybeResetBlogVisibility' ], 10, 3 );
		add_action( 'init', [ $this, 'init' ], 2 );

		$this->review              = new Review();
		$this->migration           = new Migration();
		$this->import              = new Import();
		$this->deprecatedWordPress = new DeprecatedWordPress();
		$this->conflictingPlugins  = new ConflictingPlugins();

		add_action( 'admin_notices', [ $this, 'notices' ] );
	}

	/**
	 * Initialize notifications.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function init() {
		// If our tables do not exist, create them now.
		if ( ! aioseo()->core->db->tableExists( 'aioseo_notifications' ) ) {
			aioseo()->updates->addInitialCustomTablesForV4();
		}

		$this->maybeUpdate();
		$this->initInternalNotices();
		$this->deleteInternalNotices();
	}

	/**
	 * Checks if we should update our notifications.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function maybeUpdate() {
		$nextRun = aioseo()->core->networkCache->get( 'admin_notifications_update' );
		if ( null !== $nextRun && time() < $nextRun ) {
			return;
		}

		// Schedule the action.
		aioseo()->actionScheduler->scheduleAsync( 'aioseo_admin_notifications_update' );

		// Update the cache.
		aioseo()->core->networkCache->update( 'admin_notifications_update', time() + DAY_IN_SECONDS );
	}

	/**
	 * Update Notifications from the server.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function update() {
		$notifications = $this->fetch();
		foreach ( $notifications as $notification ) {
			// First, let's check to see if this notification already exists. If so, we want to override it.
			$n = aioseo()->core->db
				->start( 'aioseo_notifications' )
				->where( 'notification_id', $notification->id )
				->run()
				->model( 'AIOSEO\\Plugin\\Common\\Models\\Notification' );

			$buttons = [
				'button1' => [
					'label' => ! empty( $notification->btns->main->text ) ? $notification->btns->main->text : null,
					'url'   => ! empty( $notification->btns->main->url ) ? $notification->btns->main->url : null
				],
				'button2' => [
					'label' => ! empty( $notification->btns->alt->text ) ? $notification->btns->alt->text : null,
					'url'   => ! empty( $notification->btns->alt->url ) ? $notification->btns->alt->url : null
				]
			];

			if ( $n->exists() ) {
				$n->title           = $notification->title;
				$n->content         = $notification->content;
				$n->type            = ! empty( $notification->notification_type ) ? $notification->notification_type : 'info';
				$n->level           = $notification->type;
				$n->notification_id = $notification->id;
				$n->start           = ! empty( $notification->start ) ? $notification->start : null;
				$n->end             = ! empty( $notification->end ) ? $notification->end : null;
				$n->button1_label   = $buttons['button1']['label'];
				$n->button1_action  = $buttons['button1']['url'];
				$n->button2_label   = $buttons['button2']['label'];
				$n->button2_action  = $buttons['button2']['url'];
				$n->save();
				continue;
			}

			$n                  = new Models\Notification();
			$n->slug            = uniqid();
			$n->title           = $notification->title;
			$n->content         = $notification->content;
			$n->type            = ! empty( $notification->notification_type ) ? $notification->notification_type : 'info';
			$n->level           = $notification->type;
			$n->notification_id = $notification->id;
			$n->start           = ! empty( $notification->start ) ? $notification->start : null;
			$n->end             = ! empty( $notification->end ) ? $notification->end : null;
			$n->button1_label   = $buttons['button1']['label'];
			$n->button1_action  = $buttons['button1']['url'];
			$n->button2_label   = $buttons['button2']['label'];
			$n->button2_action  = $buttons['button2']['url'];
			$n->dismissed       = 0;
			$n->save();

			// Since we've added a new remote notification, let's show the notification drawer.
			aioseo()->core->cache->update( 'show_notifications_drawer', true );
		}
	}

	/**
	 * Fetches the feed of notifications.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of notifications.
	 */
	private function fetch() {
		$response = aioseo()->helpers->wpRemoteGet( $this->getUrl() );

		if ( is_wp_error( $response ) ) {
			return [];
		}

		$body = wp_remote_retrieve_body( $response );

		if ( empty( $body ) ) {
			return [];
		}

		return $this->verify( json_decode( $body ) );
	}

	/**
	 * Verify notification data before it is saved.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $notifications Array of notifications items to verify.
	 * @return array                An array of verified notifications.
	 */
	private function verify( $notifications ) {
		$data = [];
		if ( ! is_array( $notifications ) || empty( $notifications ) ) {
			return $data;
		}

		foreach ( $notifications as $notification ) {
			// The message and license should never be empty, if they are, ignore.
			if ( empty( $notification->content ) || empty( $notification->type ) ) {
				continue;
			}

			if ( ! is_array( $notification->type ) ) {
				$notification->type = [ $notification->type ];
			}
			foreach ( $notification->type as $type ) {
				// Ignore if type does not match.
				if ( ! $this->validateType( $type ) ) {
					continue 2;
				}
			}

			// Ignore if expired.
			if ( ! empty( $notification->end ) && time() > strtotime( $notification->end ) ) {
				continue;
			}

			// Ignore if notification existed before installing AIOSEO.
			// Prevents bombarding the user with notifications after activation.
			$activated = aioseo()->internalOptions->internal->firstActivated( time() );
			if (
				! empty( $notification->start ) &&
				$activated > strtotime( $notification->start )
			) {
				continue;
			}

			$data[] = $notification;
		}

		return $data;
	}

	/**
	 * Validates the notification type.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $type The notification type we are targeting.
	 * @return boolean       True if yes, false if no.
	 */
	public function validateType( $type ) {
		$validated = false;

		if ( 'all' === $type ) {
			$validated = true;
		}

		// Store notice if version matches.
		if ( $this->versionMatch( aioseo()->version, $type ) ) {
			$validated = true;
		}

		return $validated;
	}

	/**
	 * Version Compare.
	 *
	 * @since 4.0.0
	 *
	 * @param  string       $currentVersion The current version being used.
	 * @param  string|array $compareVersion The version to compare with.
	 * @return bool                         True if we match, false if not.
	 */
	public function versionMatch( $currentVersion, $compareVersion ) {
		if ( is_array( $compareVersion ) ) {
			foreach ( $compareVersion as $compare_single ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				$recursiveResult = $this->versionMatch( $currentVersion, $compare_single ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
				if ( $recursiveResult ) {
					return true;
				}
			}

			return false;
		}

		$currentParse = explode( '.', $currentVersion );
		if ( strpos( $compareVersion, '-' ) ) {
			$compareParse = explode( '-', $compareVersion );
		} elseif ( strpos( $compareVersion, '.' ) ) {
			$compareParse = explode( '.', $compareVersion );
		} else {
			return false;
		}

		$currentCount = count( $currentParse );
		$compareCount = count( $compareParse );
		for ( $i = 0; $i < $currentCount || $i < $compareCount; $i++ ) {
			if ( isset( $compareParse[ $i ] ) && 'x' === strtolower( $compareParse[ $i ] ) ) {
				unset( $compareParse[ $i ] );
			}

			if ( ! isset( $currentParse[ $i ] ) ) {
				unset( $compareParse[ $i ] );
			} elseif ( ! isset( $compareParse[ $i ] ) ) {
				unset( $currentParse[ $i ] );
			}
		}

		foreach ( $compareParse as $index => $subNumber ) {
			if ( $currentParse[ $index ] !== $subNumber ) {
				return false;
			}
		}

		return true;
	}


	/**
	 * Gets the URL for the notifications api.
	 *
	 * @since 4.0.0
	 *
	 * @return string The URL to use for the api requests.
	 */
	private function getUrl() {
		if ( defined( 'AIOSEO_NOTIFICATIONS_URL' ) ) {
			return AIOSEO_NOTIFICATIONS_URL;
		}

		return $this->url;
	}

	/**
	 * Add notices.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function notices() {
		// Double check we're actually in the admin before outputting anything.
		if ( ! is_admin() ) {
			return;
		}

		$this->review->maybeShowNotice();
		$this->migration->maybeShowNotice();
		$this->import->maybeShowNotice();
		$this->deprecatedWordPress->maybeShowNotice();
		$this->conflictingPlugins->maybeShowNotice();
	}

	/**
	 * Initialize the internal notices.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function initInternalNotices() {
		$this->blogVisibility();
		$this->descriptionFormat();
	}

	/**
	 * Deletes internal notices we no longer need.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	protected function deleteInternalNotices() {
		$pluginData = aioseo()->helpers->getPluginData();
		if ( $pluginData['miPro']['installed'] || $pluginData['miLite']['installed'] ) {
			$notification = Models\Notification::getNotificationByName( 'install-mi' );
			if ( ! $notification->exists() ) {
				return;
			}

			Models\Notification::deleteNotificationByName( 'install-mi' );
		}

		if ( $pluginData['optinMonster']['installed'] ) {
			$notification = Models\Notification::getNotificationByName( 'install-om' );
			if ( ! $notification->exists() ) {
				return;
			}

			Models\Notification::deleteNotificationByName( 'install-om' );
		}
	}

	/**
	 * Extends a notice by a (default) 1 week start date.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $notice The notice to extend.
	 * @param  string $start  How long to extend.
	 * @return void
	 */
	public function remindMeLater( $notice, $start = '+1 week' ) {
		$notification = Models\Notification::getNotificationByName( $notice );
		if ( ! $notification->exists() ) {
			return;
		}

		$notification->start = gmdate( 'Y-m-d H:i:s', strtotime( $start ) );
		$notification->save();
	}

	/**
	 * Add a notice if the blog is set to hidden.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	private function blogVisibility() {
		$notification = Models\Notification::getNotificationByName( 'blog-visibility' );
		if ( get_option( 'blog_public' ) ) {
			if ( $notification->exists() ) {
				Models\Notification::deleteNotificationByName( 'blog-visibility' );
			}

			return;
		}

		if ( $notification->exists() || ! current_user_can( 'manage_options' ) ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'blog-visibility',
			'title'             => __( 'Search Engines Blocked', 'all-in-one-seo-pack' ),
			'content'           => sprintf(
				// Translators: 1 - The plugin short name ("AIOSEO").
				__( 'Warning: %1$s has detected that you are blocking access to search engines. You can change this in Settings > Reading if this was unintended.', 'all-in-one-seo-pack' ),
				AIOSEO_PLUGIN_SHORT_NAME
			),
			'type'              => 'error',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Fix Now', 'all-in-one-seo-pack' ),
			'button1_action'    => admin_url( 'options-reading.php' ),
			'button2_label'     => __( 'Remind Me Later', 'all-in-one-seo-pack' ),
			'button2_action'    => 'http://action#notification/blog-visibility-reminder',
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Add a notice if the description format is missing the Description tag.
	 *
	 * @since 4.0.5
	 *
	 * @return void
	 */
	private function descriptionFormat() {
		$notification = Models\Notification::getNotificationByName( 'description-format' );
		if ( ! in_array( 'descriptionFormat', aioseo()->internalOptions->deprecatedOptions, true ) ) {
			if ( $notification->exists() ) {
				Models\Notification::deleteNotificationByName( 'description-format' );
			}

			return;
		}

		$descriptionFormat = aioseo()->options->deprecated->searchAppearance->global->descriptionFormat;
		if ( false !== strpos( $descriptionFormat, '#description' ) ) {
			if ( $notification->exists() ) {
				Models\Notification::deleteNotificationByName( 'description-format' );
			}

			return;
		}

		if ( $notification->exists() ) {
			return;
		}

		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'description-format',
			'title'             => __( 'Invalid Description Format', 'all-in-one-seo-pack' ),
			'content'           => sprintf(
				// Translators: 1 - The plugin short name ("AIOSEO").
				__( 'Warning: %1$s has detected that you may have an invalid description format. This could lead to descriptions not being properly applied to your content.', 'all-in-one-seo-pack' ),
				AIOSEO_PLUGIN_SHORT_NAME
			) . ' ' . __( 'A Description tag is required in order to properly display your meta descriptions on your site.', 'all-in-one-seo-pack' ),
			'type'              => 'error',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Fix Now', 'all-in-one-seo-pack' ),
			'button1_action'    => 'http://route#aioseo-search-appearance&aioseo-scroll=description-format&aioseo-highlight=description-format:advanced',
			'button2_label'     => __( 'Remind Me Later', 'all-in-one-seo-pack' ),
			'button2_action'    => 'http://action#notification/description-format-reminder',
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}

	/**
	 * Check if blog visibility is changing and add/delete the appropriate notification.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $optionName The name of the option we are checking.
	 * @param  mixed  $oldValue   The old value.
	 * @param  mixed  $newValue   The new value.
	 * @return void
	 */
	public function maybeResetBlogVisibility( $optionName, $oldValue = '', $newValue = '' ) {
		if ( 'blog_public' === $optionName ) {
			if ( 1 === intval( $newValue ) ) {
				$notification = Models\Notification::getNotificationByName( 'blog-visibility' );
				if ( ! $notification->exists() ) {
					return;
				}

				Models\Notification::deleteNotificationByName( 'blog-visibility' );

				return;
			}

			$this->blogVisibility();
		}
	}

	/**
	 * Add a notice if the blog is set to hidden.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function conflictingPlugins( $plugins = [] ) {
		if ( empty( $plugins ) ) {
			return;
		}

		$content = sprintf(
			// Translators: 1 - The plugin short name ("AIOSEO").
			__( 'Warning: %1$s has detected other active SEO or sitemap plugins. We recommend that you deactivate the following plugins to prevent any conflicts:', 'all-in-one-seo-pack' ),
			AIOSEO_PLUGIN_SHORT_NAME
		) . '<ul>';

		foreach ( $plugins as $pluginName => $pluginPath ) {
			$content .= '<li><strong>' . $pluginName . '</strong></li>';
		}

		$content .= '</ul>';

		// Update an existing notice.
		$notification = Models\Notification::getNotificationByName( 'conflicting-plugins' );
		if ( $notification->exists() ) {
			$notification->content = $content;
			$notification->save();

			return;
		}

		// Create a new one if it doesn't exist.
		Models\Notification::addNotification( [
			'slug'              => uniqid(),
			'notification_name' => 'conflicting-plugins',
			'title'             => __( 'Conflicting Plugins Detected', 'all-in-one-seo-pack' ),
			'content'           => $content,
			'type'              => 'error',
			'level'             => [ 'all' ],
			'button1_label'     => __( 'Fix Now', 'all-in-one-seo-pack' ),
			'button1_action'    => 'http://action#sitemap/deactivate-conflicting-plugins?refresh',
			'button2_label'     => __( 'Remind Me Later', 'all-in-one-seo-pack' ),
			'button2_action'    => 'http://action#notification/conflicting-plugins-reminder',
			'start'             => gmdate( 'Y-m-d H:i:s' )
		] );
	}
}Common/Admin/Notices/Import.php000066600000002375151135505570012435 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin\Notices;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Plugin import notice.
 *
 * @since 4.0.0
 */
class Import {
	/**
	 * Go through all the checks to see if we should show the notice.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function maybeShowNotice() {
		if ( ! aioseo()->importExport->isImportRunning() ) {
			return;
		}

		$this->showNotice();
	}

	/**
	 * Register the notice so that it appears.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function showNotice() {
		$string1 = __( 'SEO Meta Import In Progress', 'all-in-one-seo-pack' );
		// Translators: 1 - The plugin name ("All in One SEO").
		$string2 = sprintf( __( '%1$s is importing your existing SEO data in the background.', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_NAME );
		$string3 = __( 'This notice will automatically disappear as soon as the import has completed. Meanwhile, everything should continue to work as expected.', 'all-in-one-seo-pack' );
		?>
		<div class="notice notice-info aioseo-migration">
			<p><strong><?php echo esc_html( $string1 ); ?></strong></p>
			<p><?php echo esc_html( $string2 ); ?></p>
			<p><?php echo esc_html( $string3 ); ?></p>
		</div>
		<style>
		</style>
		<?php
	}
}Common/Admin/Notices/Review.php000066600000024427151135505570012426 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin\Notices;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Review Plugin Notice.
 *
 * @since 4.0.0
 */
class Review {
	/**
	 * Class Constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'wp_ajax_aioseo-dismiss-review-plugin-cta', [ $this, 'dismissNotice' ] );
	}

	/**
	 * Go through all the checks to see if we should show the notice.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function maybeShowNotice() {
		$dismissed = get_user_meta( get_current_user_id(), '_aioseo_plugin_review_dismissed', true );
		if ( '3' === $dismissed || '4' === $dismissed ) {
			return;
		}

		if ( ! empty( $dismissed ) && $dismissed > time() ) {
			return;
		}

		// Only show to users that interact with our pluign.
		if ( ! current_user_can( 'publish_posts' ) ) {
			return;
		}

		// Only show if plugin has been active for over 10 days.
		if ( ! aioseo()->internalOptions->internal->firstActivated ) {
			aioseo()->internalOptions->internal->firstActivated = time();
		}

		$activated = aioseo()->internalOptions->internal->firstActivated( time() );
		if ( $activated > strtotime( '-10 days' ) ) {
			return;
		}

		if ( get_option( 'aioseop_options' ) || get_option( 'aioseo_options_v3' ) ) {
			$this->showNotice();
		} else {
			$this->showNotice2();
		}

		// Print the script to the footer.
		add_action( 'admin_footer', [ $this, 'printScript' ] );
	}

	/**
	 * Actually show the review plugin.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function showNotice() {
		$feedbackUrl = add_query_arg(
			[
				'wpf7528_24'   => untrailingslashit( home_url() ),
				'wpf7528_26'   => aioseo()->options->has( 'general' ) && aioseo()->options->general->has( 'licenseKey' )
					? aioseo()->options->general->licenseKey
					: '',
				'wpf7528_27'   => aioseo()->pro ? 'pro' : 'lite',
				'wpf7528_28'   => AIOSEO_VERSION,
				'utm_source'   => aioseo()->pro ? 'proplugin' : 'liteplugin',
				'utm_medium'   => 'review-notice',
				'utm_campaign' => 'feedback',
				'utm_content'  => AIOSEO_VERSION,
			],
			'https://aioseo.com/plugin-feedback/'
		);

		$string1 = sprintf(
			// Translators: 1 - The plugin short name ("AIOSEO").
			__( 'Are you enjoying %1$s?', 'all-in-one-seo-pack' ),
			AIOSEO_PLUGIN_NAME
		);
		$string2  = __( 'Yes I love it', 'all-in-one-seo-pack' );
		$string3  = __( 'Not Really...', 'all-in-one-seo-pack' );
		$string4  = sprintf(
					// Translators: 1 - The plugin name ("All in One SEO").
			__( 'We\'re sorry to hear you aren\'t enjoying %1$s. We would love a chance to improve. Could you take a minute and let us know what we can do better?', 'all-in-one-seo-pack' ),
			AIOSEO_PLUGIN_NAME
		); // phpcs:ignore Generic.Files.LineLength.MaxExceeded
		$string5  = __( 'Give feedback', 'all-in-one-seo-pack' );
		$string6  = __( 'No thanks', 'all-in-one-seo-pack' );
		$string7  = __( 'That\'s awesome! Could you please do us a BIG favor and give it a 5-star rating on WordPress to help us spread the word and boost our motivation?', 'all-in-one-seo-pack' );
		// Translators: 1 - The plugin name ("All in One SEO").
		$string9  = __( 'Ok, you deserve it', 'all-in-one-seo-pack' );
		$string10 = __( 'Nope, maybe later', 'all-in-one-seo-pack' );
		$string11 = __( 'I already did', 'all-in-one-seo-pack' );

		?>
		<div class="notice notice-info aioseo-review-plugin-cta is-dismissible">
			<div class="step-1">
				<p><?php echo esc_html( $string1 ); ?></p>
				<p>
					<a href="#" class="aioseo-review-switch-step-3" data-step="3"><?php echo esc_html( $string2 ); ?></a> 🙂 |
					<a href="#" class="aioseo-review-switch-step-2" data-step="2"><?php echo esc_html( $string3 ); ?></a>
				</p>
			</div>
			<div class="step-2" style="display:none;">
				<p><?php echo esc_html( $string4 ); ?></p>
				<p>
					<a href="<?php echo esc_url( $feedbackUrl ); ?>" class="aioseo-dismiss-review-notice" target="_blank" rel="noopener noreferrer"><?php echo esc_html( $string5 ); ?></a>&nbsp;&nbsp;
					<a href="#" class="aioseo-dismiss-review-notice" target="_blank" rel="noopener noreferrer"><?php echo esc_html( $string6 ); ?></a>
				</p>
			</div>
			<div class="step-3" style="display:none;">
				<p><?php echo esc_html( $string7 ); ?></p>
				<p>
					<a href="https://wordpress.org/support/plugin/all-in-one-seo-pack/reviews/?filter=5#new-post" class="aioseo-dismiss-review-notice" target="_blank" rel="noopener noreferrer">
						<?php echo esc_html( $string9 ); ?>
					</a>&nbsp;&bull;&nbsp;
					<a href="#" class="aioseo-dismiss-review-notice-delay" target="_blank" rel="noopener noreferrer">
						<?php echo esc_html( $string10 ); ?>
					</a>&nbsp;&bull;&nbsp;
					<a href="#" class="aioseo-dismiss-review-notice" target="_blank" rel="noopener noreferrer">
						<?php echo esc_html( $string11 ); ?>
					</a>
				</p>
			</div>
		</div>
		<?php
	}

	/**
	 * Actually show the review plugin 2.0.
	 *
	 * @since 4.2.2
	 *
	 * @return void
	 */
	public function showNotice2() {
		$string1 = sprintf(
			// Translators: 1 - The plugin name ("All in One SEO").
			__( 'Hey, we noticed you have been using %1$s for some time - that’s awesome! Could you please do us a BIG favor and give it a 5-star rating on WordPress to help us spread the word and boost our motivation?', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			'<strong>' . esc_html( AIOSEO_PLUGIN_NAME ) . '</strong>'
		);

		// Translators: 1 - The plugin name ("All in One SEO").
		$string9  = __( 'Ok, you deserve it', 'all-in-one-seo-pack' );
		$string10 = __( 'Nope, maybe later', 'all-in-one-seo-pack' );
		$string11 = __( 'I already did', 'all-in-one-seo-pack' );

		?>
		<div class="notice notice-info aioseo-review-plugin-cta is-dismissible">
			<div class="step-3">
				<p><?php echo $string1; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p>
				<p>
					<a href="https://wordpress.org/support/plugin/all-in-one-seo-pack/reviews/?filter=5#new-post" class="aioseo-dismiss-review-notice" target="_blank" rel="noopener noreferrer">
						<?php echo esc_html( $string9 ); ?>
					</a>&nbsp;&bull;&nbsp;
					<a href="#" class="aioseo-dismiss-review-notice-delay" target="_blank" rel="noopener noreferrer">
						<?php echo esc_html( $string10 ); ?>
					</a>&nbsp;&bull;&nbsp;
					<a href="#" class="aioseo-dismiss-review-notice" target="_blank" rel="noopener noreferrer">
						<?php echo esc_html( $string11 ); ?>
					</a>
				</p>
			</div>
		</div>
		<?php
	}

	/**
	 * Print the script for dismissing the notice.
	 *
	 * @since 4.0.13
	 *
	 * @return void
	 */
	public function printScript() {
		// Create a nonce.
		$nonce = wp_create_nonce( 'aioseo-dismiss-review' );
		?>
		<style>
			.aioseop-notice-review_plugin_cta .aioseo-action-buttons {
				display: none;
			}
			@keyframes dismissBtnVisible {
				from { opacity: 0.99; }
				to { opacity: 1; }
			}
			.aioseo-review-plugin-cta button.notice-dismiss {
				animation-duration: 0.001s;
				animation-name: dismissBtnVisible;
			}
		</style>
		<script>
			window.addEventListener('load', function () {
				var aioseoSetupButton,
					dismissBtn

				aioseoSetupButton = function (dismissBtn) {
					var notice      = document.querySelector('.notice.aioseo-review-plugin-cta'),
						delay       = false,
						relay       = true,
						stepOne     = notice.querySelector('.step-1'),
						stepTwo     = notice.querySelector('.step-2'),
						stepThree   = notice.querySelector('.step-3')

					// Add an event listener to the dismiss button.
					dismissBtn.addEventListener('click', function (event) {
						var httpRequest = new XMLHttpRequest(),
							postData    = ''

						// Build the data to send in our request.
						postData += '&delay=' + delay
						postData += '&relay=' + relay
						postData += '&action=aioseo-dismiss-review-plugin-cta'
						postData += '&nonce=<?php echo esc_html( $nonce ); ?>'

						httpRequest.open('POST', '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>')
						httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
						httpRequest.send(postData)
					})

					notice.addEventListener('click', function (event) {
						if (event.target.matches('.aioseo-review-switch-step-3')) {
							event.preventDefault()
							stepOne.style.display   = 'none'
							stepTwo.style.display   = 'none'
							stepThree.style.display = 'block'
						}
						if (event.target.matches('.aioseo-review-switch-step-2')) {
							event.preventDefault()
							stepOne.style.display   = 'none'
							stepThree.style.display = 'none'
							stepTwo.style.display   = 'block'
						}
						if (event.target.matches('.aioseo-dismiss-review-notice-delay')) {
							event.preventDefault()
							delay = true
							relay = false
							dismissBtn.click()
						}
						if (event.target.matches('.aioseo-dismiss-review-notice')) {
							if ('#' === event.target.getAttribute('href')) {
								event.preventDefault()
							}
							relay = false
							dismissBtn.click()
						}
					})
				}

				dismissBtn = document.querySelector('.aioseo-review-plugin-cta .notice-dismiss')
				if (!dismissBtn) {
					document.addEventListener('animationstart', function (event) {
						if (event.animationName == 'dismissBtnVisible') {
							dismissBtn = document.querySelector('.aioseo-review-plugin-cta .notice-dismiss')
							if (dismissBtn) {
								aioseoSetupButton(dismissBtn)
							}
						}
					}, false)

				} else {
					aioseoSetupButton(dismissBtn)
				}
			});
		</script>
		<?php
	}

	/**
	 * Dismiss the review plugin CTA.
	 *
	 * @since 4.0.0
	 *
	 * @return string The successful response.
	 */
	public function dismissNotice() {
		// Early exit if we're not on a aioseo-dismiss-review-plugin-cta action.
		if ( ! isset( $_POST['action'] ) || 'aioseo-dismiss-review-plugin-cta' !== $_POST['action'] ) {
			return;
		}

		check_ajax_referer( 'aioseo-dismiss-review', 'nonce' );
		$delay = isset( $_POST['delay'] ) ? 'true' === sanitize_text_field( wp_unslash( $_POST['delay'] ) ) : false;
		$relay = isset( $_POST['relay'] ) ? 'true' === sanitize_text_field( wp_unslash( $_POST['relay'] ) ) : false;

		if ( ! $delay ) {
			update_user_meta( get_current_user_id(), '_aioseo_plugin_review_dismissed', $relay ? '4' : '3' );

			return wp_send_json_success();
		}

		update_user_meta( get_current_user_id(), '_aioseo_plugin_review_dismissed', strtotime( '+1 week' ) );

		return wp_send_json_success();
	}
}Common/Admin/Notices/Migration.php000066600000003044151135505570013106 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin\Notices;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * V3 to V4 migration notice.
 *
 * @since 4.0.0
 */
class Migration {
	/**
	 * Go through all the checks to see if we should show the notice.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function maybeShowNotice() {
		$transientPosts = aioseo()->core->cache->get( 'v3_migration_in_progress_posts' );
		$transientTerms = aioseo()->core->cache->get( 'v3_migration_in_progress_terms' );
		if ( ! $transientPosts && ! $transientTerms ) {
			return;
		}

		$this->showNotice();
	}

	/**
	 * Register the notice so that it appears.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function showNotice() {
		// Translators: 1 - The plugin name ("AIOSEO).
		$string1 = sprintf( __( '%1$s V3->V4 Migration In Progress', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME );
		// Translators: 1 - The plugin name ("All in One SEO").
		$string2 = sprintf( __( '%1$s is currently upgrading your database and migrating your SEO data in the background.', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_NAME );
		$string3 = __( 'This notice will automatically disappear as soon as the migration has completed. Meanwhile, everything should continue to work as expected.', 'all-in-one-seo-pack' );
		?>
		<div class="notice notice-info aioseo-migration">
			<p><strong><?php echo esc_html( $string1 ); ?></strong></p>
			<p><?php echo esc_html( $string2 ); ?></p>
			<p><?php echo esc_html( $string3 ); ?></p>
		</div>
		<style>
		</style>
		<?php
	}
}Common/Admin/Notices/ConflictingPlugins.php000066600000012466151135505570014766 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin\Notices;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Handles the Conflicting Plugins notice..
 *
 * @since 4.5.1
 */
class ConflictingPlugins {
	/**
	 * Class constructor.
	 *
	 * @since 4.5.1
	 */
	public function __construct() {
		add_action( 'wp_ajax_aioseo-dismiss-conflicting-plugins-notice', [ $this, 'dismissNotice' ] );
		add_action( 'wp_ajax_aioseo-deactivate-conflicting-plugins-notice', [ $this, 'deactivateConflictingPlugins' ] );
	}

	/**
	 * Go through all the checks to see if we should show the notice.
	 *
	 * @since 4.5.1
	 *
	 * @return void
	 */
	public function maybeShowNotice() {
		$dismissed = get_option( '_aioseo_conflicting_plugins_dismissed', true );
		if ( '1' === $dismissed ) {
			return;
		}

		if ( ! current_user_can( 'activate_plugins' ) ) {
			return;
		}

		// Only show if there are conflicting plugins.
		$conflictingPlugins = aioseo()->conflictingPlugins->getAllConflictingPlugins();
		if ( empty( $conflictingPlugins ) ) {
			return;
		}

		$this->showNotice();

		// Print the script to the footer.
		add_action( 'admin_footer', [ $this, 'printScript' ] );
	}

	/**
	 * Renders the notice.
	 *
	 * @since 4.5.1
	 *
	 * @return void
	 */
	public function showNotice() {
		$type = ! empty( aioseo()->conflictingPlugins->getConflictingPlugins( 'seo' ) ) ? 'SEO' : 'sitemap';
		?>
		<div class="notice notice-error aioseo-conflicting-plugin-notice is-dismissible">
			<p>
				<?php
				echo wp_kses(
					sprintf(
						// phpcs:ignore Generic.Files.LineLength.MaxExceeded
						// Translators: 1 - Type of conflicting plugin (i.e. SEO or Sitemap), 2 - Opening HTML link tag, 3 - Closing HTML link tag.
						__( 'Please keep only one %1$s plugin active, otherwise, you might lose your rankings and traffic. %2$sClick here to Deactivate.%3$s', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
						$type,
						'<a href="#" rel="noopener noreferrer" class="deactivate-conflicting-plugins">',
						'</a>'
					),
					[
						'a'      => [
							'href'  => [],
							'rel'   => [],
							'class' => []
						],
						'strong' => [],
					]
				);
				?>
			</p>
		</div>

		<style>
			#conflicting_seo_plugins.rank-math-notice {
				display: none;
			}
		</style>

		<?php
	}

	/**
	 * Print the script for dismissing the notice.
	 *
	 * @since 4.5.1
	 *
	 * @return void
	 */
	public function printScript() {
		// Create a nonce.
		$nonce1 = wp_create_nonce( 'aioseo-dismiss-conflicting-plugins' );
		$nonce2 = wp_create_nonce( 'aioseo-deactivate-conflicting-plugins' );
		?>
		<script>
			window.addEventListener('load', function () {
				var dismissBtn,
					deactivateBtn

				// Add an event listener to the dismiss button.
				dismissBtn = document.querySelector('.aioseo-conflicting-plugin-notice .notice-dismiss')
				dismissBtn.addEventListener('click', function (event) {
					var httpRequest = new XMLHttpRequest(),
						postData    = ''

					// Build the data to send in our request.
					postData += '&action=aioseo-dismiss-conflicting-plugins-notice'
					postData += '&nonce=<?php echo esc_html( $nonce1 ); ?>'

					httpRequest.open('POST', '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>')
					httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
					httpRequest.send(postData)
				})

				deactivateBtn = document.querySelector('.aioseo-conflicting-plugin-notice .deactivate-conflicting-plugins')
				deactivateBtn.addEventListener('click', function (event) {
					event.preventDefault()

					var httpRequest = new XMLHttpRequest(),
						postData    = ''

					// Build the data to send in our request.
					postData += '&action=aioseo-deactivate-conflicting-plugins-notice'
					postData += '&nonce=<?php echo esc_html( $nonce2 ); ?>'

					httpRequest.open('POST', '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>')
					httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
					httpRequest.onerror = function () {
						window.location.reload()
					}
					httpRequest.onload = function () {
						window.location.reload()
					}
					httpRequest.send(postData)
				})
			});
		</script>
		<?php
	}

	/**
	 * Dismiss the notice.
	 *
	 * @since 4.5.1
	 *
	 * @return string The successful response.
	 */
	public function dismissNotice() {
		// Early exit if we're not on a aioseo-dismiss-conflicting-plugins-notice action.
		if ( ! isset( $_POST['action'] ) || 'aioseo-dismiss-conflicting-plugins-notice' !== $_POST['action'] ) {
			return wp_send_json_error( 'invalid-action' );
		}

		check_ajax_referer( 'aioseo-dismiss-conflicting-plugins', 'nonce' );

		update_option( '_aioseo_conflicting_plugins_dismissed', true );

		return wp_send_json_success();
	}

	/**
	 * Deactivates the conflicting plugins.
	 *
	 * @since 4.5.1
	 *
	 * @return string The successful response.
	 */
	public function deactivateConflictingPlugins() {
		// Early exit if we're not on a aioseo-dismiss-conflicting-plugins-notice action.
		if ( ! isset( $_POST['action'] ) || 'aioseo-deactivate-conflicting-plugins-notice' !== $_POST['action'] ) {
			return wp_send_json_error( 'invalid-action' );
		}

		check_ajax_referer( 'aioseo-deactivate-conflicting-plugins', 'nonce' );

		aioseo()->conflictingPlugins->deactivateConflictingPlugins( [ 'seo', 'sitemap' ] );

		return wp_send_json_success();
	}
}Common/Admin/Notices/WpNotices.php000066600000015515151135505570013076 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin\Notices;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WpNotices class.
 *
 * @since 4.2.3
 */
class WpNotices {
	/**
	 * Notices array
	 *
	 * @since 4.2.3
	 *
	 * @var array
	 */
	private $notices = [];

	/**
	 * The cache key.
	 *
	 * @since 4.2.3
	 *
	 * @var string
	 */
	private $cacheKey = 'wp_notices';

	/**
	 * Class Constructor.
	 *
	 * @since 4.2.3
	 */
	public function __construct() {
		add_action( 'rest_api_init', [ $this, 'registerApiField' ] );
		add_action( 'enqueue_block_editor_assets', [ $this, 'enqueueScripts' ] );
		add_action( 'admin_notices', [ $this, 'adminNotices' ] );
	}

	/**
	 * Enqueue notices scripts.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	public function enqueueScripts() {
		aioseo()->core->assets->load( 'src/vue/standalone/wp-notices/main.js' );
	}

	/**
	 * Registers an API field with notices.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	public function registerApiField() {
		foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
			register_rest_field( $postType, 'aioseo_notices', [
				'get_callback' => [ $this, 'apiGetNotices' ]
			] );
		}
	}

	/**
	 * API field callback.
	 *
	 * @since 4.2.3
	 *
	 * @return array Notices array
	 */
	public function apiGetNotices() {
		$notices = $this->getNoticesInContext();

		// Notices show only one time.
		$this->removeNotices( $notices );

		return $notices;
	}

	/**
	 * Get all notices.
	 *
	 * @since 4.2.3
	 *
	 * @return array Notices array
	 */
	public function getNotices() {
		if ( empty( $this->notices ) ) {
			$this->notices = (array) aioseo()->core->cache->get( $this->cacheKey );
		}

		return ! empty( $this->notices ) ? $this->notices : [];
	}

	/**
	 * Get all notices in the current context.
	 *
	 * @since 4.2.6
	 *
	 * @return array Notices array
	 */
	public function getNoticesInContext() {
		$contextNotices = $this->getNotices();
		foreach ( $contextNotices as $key => $notice ) {
			if ( empty( $notice['allowedContexts'] ) ) {
				continue;
			}

			$allowed = false;
			foreach ( $notice['allowedContexts'] as $allowedContext ) {
				if ( $this->isAllowedContext( $allowedContext ) ) {
					$allowed = true;
					break;
				}
			}

			if ( ! $allowed ) {
				unset( $contextNotices[ $key ] );
			}
		}

		return $contextNotices;
	}

	/**
	 * Test if we are in the current context.
	 *
	 * @since 4.2.6
	 *
	 * @param  string $context The context to test. (posts)
	 * @return bool            Is the required context.
	 */
	private function isAllowedContext( $context ) {
		switch ( $context ) {
			case 'posts':
				return aioseo()->helpers->isScreenPostList() ||
						aioseo()->helpers->isScreenPostEdit() ||
						aioseo()->helpers->isAjaxCronRestRequest();
		}

		return false;
	}

	/**
	 * Finds a notice by message.
	 *
	 * @since 4.2.3
	 *
	 * @param  string     $message The message string.
	 * @param  string     $type    The message type.
	 * @return void|array          The found notice.
	 */
	public function getNotice( $message, $type = '' ) {
		$notices = $this->getNotices();
		foreach ( $notices as $notice ) {
			if ( $notice['options']['id'] === $this->getNoticeId( $message, $type ) ) {
				return $notice;
			}
		}
	}

	/**
	 * Generates a notice id.
	 *
	 * @since 4.2.3
	 *
	 * @param  string $message The message string.
	 * @param  string $type    The message type.
	 * @return string          The notice id.
	 */
	public function getNoticeId( $message, $type = '' ) {
		return md5( $message . $type );
	}

	/**
	 * Clear notices.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	public function clearNotices() {
		$this->notices = [];
		$this->updateCache();
	}

	/**
	 * Remove certain notices.
	 *
	 * @since 4.2.6
	 *
	 * @param  array $notices A list of notices to remove.
	 * @return void
	 */
	public function removeNotices( $notices ) {
		foreach ( array_keys( $notices ) as $noticeKey ) {
			unset( $this->notices[ $noticeKey ] );
		}
		$this->updateCache();
	}

	/**
	 * Adds a notice.
	 *
	 * @since 4.2.3
	 *
	 * @param  string $message         The message.
	 * @param  string $status          The message status [success, info, warning, error]
	 * @param  array  $options         Options for the message. https://developer.wordpress.org/block-editor/reference-guides/data/data-core-notices/#createnotice
	 * @param  array  $allowedContexts The contexts where this notice will show.
	 * @return void
	 */
	public function addNotice( $message, $status = 'warning', $options = [], $allowedContexts = [] ) {
		$type = ! empty( $options['type'] ) ? $options['type'] : '';
		$foundNotice = $this->getNotice( $message, $type );
		if ( empty( $message ) || ! empty( $foundNotice ) ) {
			return;
		}

		$notice = [
			'message'         => $message,
			'status'          => $status,
			'options'         => wp_parse_args( $options, [
				'id'            => $this->getNoticeId( $message, $type ),
				'isDismissible' => true
			] ),
			'allowedContexts' => $allowedContexts
		];

		$this->notices[] = $notice;
		$this->updateCache();
	}

	/**
	 * Show notices on classic editor.
	 *
	 * @since 4.2.3
	 *
	 * @return void
	 */
	public function adminNotices() {
		// Double check we're actually in the admin before outputting anything.
		if ( ! is_admin() ) {
			return;
		}

		$notices = $this->getNoticesInContext();
		foreach ( $notices as $notice ) {
			// Hide snackbar notices on classic editor.
			if ( ! empty( $notice['options']['type'] ) && 'snackbar' === $notice['options']['type'] ) {
				continue;
			}

			$status = ! empty( $notice['status'] ) ? $notice['status'] : 'warning';
			$class  = ! empty( $notice['options']['class'] ) ? $notice['options']['class'] : '';
			?>
			<div
				class="notice notice-<?php echo esc_attr( $status ) ?> <?php echo esc_attr( $class ) ?>">
				<?php echo '<p>' . $notice['message'] . '</p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
				<?php
				if ( ! empty( $notice['options']['actions'] ) ) {
					foreach ( $notice['options']['actions'] as $action ) {
						echo '<p>';
						if ( ! empty( $action['url'] ) ) {
							$class  = ! empty( $action['class'] ) ? $action['class'] : '';
							$target = ! empty( $action['target'] ) ? $action['target'] : '';
							echo '<a 
								href="' . esc_attr( $action['url'] ) . '" 
								class="' . esc_attr( $class ) . '"
								target="' . esc_attr( $target ) . '"
							>';
						}
						echo $action['label']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
						if ( ! empty( $action['url'] ) ) {
							echo '</a>';
						}
						echo '</p>';
					}
					?>
				<?php } ?>
			</div>
			<?php
		}

		// Notices show only one time.
		$this->removeNotices( $notices );
	}

	/**
	 * Helper to update the cache with the current notices array.
	 *
	 * @since 4.2.6
	 *
	 * @return void
	 */
	private function updateCache() {
		aioseo()->core->cache->update( $this->cacheKey, $this->notices );
	}
}Common/Admin/Usage.php000066600000014117151135505570010620 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Usage tracking class.
 *
 * @since 4.0.0
 */
abstract class Usage {
	/**
	 * Returns the current plugin version type ("lite" or "pro").
	 *
	 * @since 4.1.3
	 *
	 * @return string The version type.
	 */
	abstract public function getType();

	/**
	 * Source of notifications content.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	private $url = 'https://aiousage.com/v1/track';

	/**
	 * Whether or not usage tracking is enabled.
	 *
	 * @since 4.0.0
	 *
	 * @var bool
	 */
	protected $enabled = false;

	/**
	 * Class Constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'init', [ $this, 'init' ], 2 );
	}

	/**
	 * Runs on the init action.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function init() {
		try {
			$action = 'aioseo_send_usage_data';
			if ( ! $this->enabled ) {
				aioseo()->actionScheduler->unschedule( $action );

				return;
			}

			// Register the action handler.
			add_action( $action, [ $this, 'process' ] );

			if ( ! as_next_scheduled_action( $action ) ) {
				as_schedule_recurring_action( $this->generateStartDate(), WEEK_IN_SECONDS, $action, [], 'aioseo' );

				// Run the task immediately using an async action.
				as_enqueue_async_action( $action, [], 'aioseo' );
			}
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Processes the usage tracking.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function process() {
		if ( ! $this->enabled ) {
			return;
		}

		wp_remote_post(
			$this->getUrl(),
			[
				'timeout'    => 10,
				'headers'    => array_merge( [
					'Content-Type' => 'application/json; charset=utf-8'
				], aioseo()->helpers->getApiHeaders() ),
				'user-agent' => aioseo()->helpers->getApiUserAgent(),
				'body'       => wp_json_encode( $this->getData() )
			]
		);
	}

	/**
	 * Gets the URL for the notifications api.
	 *
	 * @since 4.0.0
	 *
	 * @return string The URL to use for the api requests.
	 */
	private function getUrl() {
		if ( defined( 'AIOSEO_USAGE_TRACKING_URL' ) ) {
			return AIOSEO_USAGE_TRACKING_URL;
		}

		return $this->url;
	}

	/**
	 * Retrieves the data to send in the usage tracking.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of data to send.
	 */
	protected function getData() {
		$themeData = wp_get_theme();
		$type      = $this->getType();

		return [
			// Generic data (environment).
			'url'                           => home_url(),
			'php_version'                   => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION,
			'wp_version'                    => get_bloginfo( 'version' ),
			'mysql_version'                 => aioseo()->core->db->db->db_version(),
			'server_version'                => isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '',
			'is_ssl'                        => is_ssl(),
			'is_multisite'                  => is_multisite(),
			'sites_count'                   => function_exists( 'get_blog_count' ) ? (int) get_blog_count() : 1,
			'active_plugins'                => $this->getActivePlugins(),
			'theme_name'                    => $themeData->name,
			'theme_version'                 => $themeData->version,
			'user_count'                    => function_exists( 'get_user_count' ) ? get_user_count() : null,
			'locale'                        => get_locale(),
			'timezone_offset'               => wp_timezone_string(),
			'email'                         => get_bloginfo( 'admin_email' ),
			// AIOSEO specific data.
			'aioseo_version'                => AIOSEO_VERSION,
			'aioseo_license_key'            => null,
			'aioseo_license_type'           => null,
			'aioseo_is_pro'                 => false,
			"aioseo_{$type}_installed_date" => aioseo()->internalOptions->internal->installed,
			'aioseo_settings'               => $this->getSettings()
		];
	}

	/**
	 * Get the settings and escape the quotes so it can be JSON encoded.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of settings data.
	 */
	private function getSettings() {
		$settings = aioseo()->options->all();
		array_walk_recursive( $settings, function( &$v ) {
			if ( is_string( $v ) && strpos( $v, '&quot' ) !== false ) {
				$v = str_replace( '&quot', '&#x5c;&quot', $v );
			}
		});

		$settings = $this->filterPrivateSettings( $settings );

		$internal = aioseo()->internalOptions->all();
		array_walk_recursive( $internal, function( &$v ) {
			if ( is_string( $v ) && strpos( $v, '&quot' ) !== false ) {
				$v = str_replace( '&quot', '&#x5c;&quot', $v );
			}
		});

		return [
			'options'  => $settings,
			'internal' => $internal
		];
	}

	/**
	 * Return a list of active plugins.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of active plugin data.
	 */
	private function getActivePlugins() {
		if ( ! function_exists( 'get_plugins' ) ) {
			include ABSPATH . '/wp-admin/includes/plugin.php';
		}
		$active  = get_option( 'active_plugins', [] );
		$plugins = array_intersect_key( get_plugins(), array_flip( $active ) );

		return array_map(
			static function ( $plugin ) {
				if ( isset( $plugin['Version'] ) ) {
					return $plugin['Version'];
				}

				return 'Not Set';
			},
			$plugins
		);
	}

	/**
	 * Generate a random start date for usage tracking.
	 *
	 * @since 4.0.0
	 *
	 * @return integer The randomized start date.
	 */
	private function generateStartDate() {
		$tracking = [
			'days'    => wp_rand( 0, 6 ) * DAY_IN_SECONDS,
			'hours'   => wp_rand( 0, 23 ) * HOUR_IN_SECONDS,
			'minutes' => wp_rand( 0, 23 ) * HOUR_IN_SECONDS,
			'seconds' => wp_rand( 0, 59 )
		];

		return strtotime( 'next sunday' ) + array_sum( $tracking );
	}

	/**
	 * Anonimizes or obfuscates the value of certain settings.
	 *
	 * @since 4.3.2
	 *
	 * @param  array $settings The settings.
	 * @return array           The altered settings.
	 */
	private function filterPrivateSettings( $settings ) {
		if ( ! empty( $settings['advanced']['openAiKey'] ) ) {
			$settings['advanced']['openAiKey'] = true;
		}

		if ( ! empty( $settings['localBusiness']['maps']['apiKey'] ) ) {
			$settings['localBusiness']['maps']['apiKey'] = true;
		}

		return $settings;
	}
}Common/Admin/Dashboard.php000066600000010721151135505570011440 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class that holds our dashboard widget.
 *
 * @since 4.0.0
 */
class Dashboard {
	/**
	 * Class Constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_action( 'wp_dashboard_setup', [ $this, 'addDashboardWidgets' ] );
	}

	/**
	 * Registers our dashboard widgets.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	public function addDashboardWidgets() {
		// Add the SEO Setup widget.
		if (
			$this->canShowWidget( 'seoSetup' ) &&
			apply_filters( 'aioseo_show_seo_setup', true ) &&
			( aioseo()->access->isAdmin() || aioseo()->access->hasCapability( 'aioseo_setup_wizard' ) ) &&
			! aioseo()->standalone->setupWizard->isCompleted()
		) {
			wp_add_dashboard_widget(
				'aioseo-seo-setup',
				// Translators: 1 - The plugin short name ("AIOSEO").
				sprintf( esc_html__( '%s Setup', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME ),
				[
					$this,
					'outputSeoSetup',
				],
				null,
				null,
				'normal',
				'high'
			);
		}

		// Add the Overview widget.
		if (
			$this->canShowWidget( 'seoOverview' ) &&
			apply_filters( 'aioseo_show_seo_overview', true ) &&
			( aioseo()->access->isAdmin() || aioseo()->access->hasCapability( 'aioseo_page_analysis' ) ) &&
			aioseo()->options->advanced->truSeo
		) {
			wp_add_dashboard_widget(
				'aioseo-overview',
				// Translators: 1 - The plugin short name ("AIOSEO").
				sprintf( esc_html__( '%s Overview', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME ),
				[
					$this,
					'outputSeoOverview',
				]
			);
		}

		// Add the News widget.
		if (
			$this->canShowWidget( 'seoNews' ) &&
			apply_filters( 'aioseo_show_seo_news', true ) &&
			aioseo()->access->isAdmin()
		) {
			wp_add_dashboard_widget(
				'aioseo-rss-feed',
				esc_html__( 'SEO News', 'all-in-one-seo-pack' ),
				[
					$this,
					'displayRssDashboardWidget',
				]
			);
		}
	}

	/**
	 * Whether or not to show the widget.
	 *
	 * @since   4.0.0
	 * @version 4.2.8
	 *
	 * @param  string  $widget The widget to check if can show.
	 * @return boolean True if yes, false otherwise.
	 */
	protected function canShowWidget( $widget ) { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return true;
	}

	/**
	 * Output the SEO Setup widget.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	public function outputSeoSetup() {
		$this->output( 'aioseo-seo-setup-app' );
	}

	/**
	 * Output the SEO Overview widget.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	public function outputSeoOverview() {
		$this->output( 'aioseo-overview-app' );
	}

	/**
	 * Output the widget wrapper for the Vue App.
	 *
	 * @since 4.2.0
	 *
	 * @param  string $appId The App ID to print out.
	 * @return void
	 */
	private function output( $appId ) {
		// Enqueue the scripts for the widget.
		$this->enqueue();

		// Opening tag.
		echo '<div id="' . esc_attr( $appId ) . '">';

		// Loader element.
		require AIOSEO_DIR . '/app/Common/Views/parts/loader.php';

		// Closing tag.
		echo '</div>';
	}

	/**
	 * Enqueue the scripts and styles.
	 *
	 * @since 4.2.0
	 *
	 * @return void
	 */
	private function enqueue() {
		aioseo()->core->assets->load( 'src/vue/standalone/dashboard-widgets/main.js', [], aioseo()->helpers->getVueData( 'dashboard' ) );
	}

	/**
	 * Display RSS Dashboard Widget
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function displayRssDashboardWidget() {
		// Check if the user has chosen not to display this widget through screen options.
		$currentScreen = aioseo()->helpers->getCurrentScreen();
		if ( empty( $currentScreen->id ) ) {
			return;
		}

		$hiddenWidgets = get_user_meta( get_current_user_id(), 'metaboxhidden_' . $currentScreen->id );
		if ( $hiddenWidgets && count( $hiddenWidgets ) > 0 && is_array( $hiddenWidgets[0] ) && in_array( 'aioseo-rss-feed', $hiddenWidgets[0], true ) ) {
			return;
		}

		$rssItems = aioseo()->helpers->fetchAioseoArticles();
		if ( ! $rssItems ) {
			esc_html_e( 'Temporarily unable to load feed.', 'all-in-one-seo-pack' );

			return;
		}
		?>
		<ul>
			<?php
			foreach ( $rssItems as $item ) {
				?>
				<li>
					<a target="_blank" href="<?php echo esc_url( $item['url'] ); ?>" rel="noopener noreferrer">
						<?php echo esc_html( $item['title'] ); ?>
					</a>
					<span><?php echo esc_html( $item['date'] ); ?></span>
					<div>
						<?php echo esc_html( wp_strip_all_tags( $item['content'] ) ) . '...'; ?>
					</div>
				</li>
				<?php
			}

			?>
		</ul>
		<?php
	}
}Common/Admin/SiteHealth.php000066600000040333151135505570011605 0ustar00<?php
namespace AIOSEO\Plugin\Common\Admin;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WP Site Health class.
 *
 * @since 4.0.0
 */
class SiteHealth {
	/**
	 * Class Constructor.
	 *
	 * @since 4.0.0
	 */
	public function __construct() {
		add_filter( 'site_status_tests', [ $this, 'registerTests' ], 0 );
		add_filter( 'debug_information', [ $this, 'addDebugInfo' ], 0 );
	}

	/**
	 * Add AIOSEO WP Site Health tests.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $tests The current filters array.
	 * @return array
	 */
	public function registerTests( $tests ) {
		$tests['direct']['aioseo_site_public'] = [
			'label' => 'AIOSEO Site Public',
			'test'  => [ $this, 'testCheckSitePublic' ],
		];
		$tests['direct']['aioseo_site_info'] = [
			'label' => 'AIOSEO Site Info',
			'test'  => [ $this, 'testCheckSiteInfo' ],
		];
		$tests['direct']['aioseo_google_search_console'] = [
			'label' => 'AIOSEO Google Search Console',
			'test'  => [ $this, 'testCheckGoogleSearchConsole' ],
		];
		$tests['direct']['aioseo_plugin_update'] = [
			'label' => 'AIOSEO Plugin Update',
			'test'  => [ $this, 'testCheckPluginUpdate' ],
		];

		$tests['direct']['aioseo_schema_markup'] = [
			'label' => 'AIOSEO Schema Markup',
			'test'  => [ $this, 'testCheckSchemaMarkup' ],
		];

		return $tests;
	}

	/**
	 * Adds our site health debug info.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $debugInfo The debug info.
	 * @return array $debugInfo The debug info.
	 */
	public function addDebugInfo( $debugInfo ) {
		$fields = [];

		$noindexed = $this->noindexed();
		if ( $noindexed ) {
			$fields['noindexed'] = $this->field(
				__( 'Noindexed content', 'all-in-one-seo-pack' ),
				implode( ', ', $noindexed )
			);
		}

		$nofollowed = $this->nofollowed();
		if ( $nofollowed ) {
			$fields['nofollowed'] = $this->field(
				__( 'Nofollowed content', 'all-in-one-seo-pack' ),
				implode( ', ', $nofollowed )
			);
		}

		if ( ! count( $fields ) ) {
			return $debugInfo;
		}

		$debugInfo['aioseo'] = [
			'label'       => __( 'SEO', 'all-in-one-seo-pack' ),
			'description' => sprintf(
				// Translators: 1 - The plugin short name ("AIOSEO").
				__( 'The fields below contain important SEO information from %1$s that may effect your site.', 'all-in-one-seo-pack' ),
				AIOSEO_PLUGIN_SHORT_NAME
			),
			'private'     => false,
			'show_count'  => true,
			'fields'      => $fields,
		];

		return $debugInfo;
	}

	/**
	 * Checks whether the site is public.
	 *
	 * @since 4.0.0
	 *
	 * @return array The test result.
	 */
	public function testCheckSitePublic() {
		$test = 'aioseo_site_public';

		if ( ! get_option( 'blog_public' ) ) {
			return $this->result(
				$test,
				'critical',
				__( 'Your site does not appear in search results', 'all-in-one-seo-pack' ),
				__( 'Your site is set to private. This means WordPress asks search engines to exclude your website from search results.', 'all-in-one-seo-pack' ),
				$this->actionLink( admin_url( 'options-reading.php' ), __( 'Go to Settings > Reading', 'all-in-one-seo-pack' ) )
			);
		}

		return $this->result(
			$test,
			'good',
			__( 'Your site appears in search results', 'all-in-one-seo-pack' ),
			__( 'Your site is set to public. Search engines will index your website and it will appear in search results.', 'all-in-one-seo-pack' )
		);
	}

	/**
	 * Checks whether the site title and tagline are set.
	 *
	 * @since 4.0.0
	 *
	 * @return array The test result.
	 */
	public function testCheckSiteInfo() {
		$siteTitle   = get_bloginfo( 'name' );
		$siteTagline = get_bloginfo( 'description' );

		if ( ! $siteTitle || ! $siteTagline ) {
			return $this->result(
				'aioseo_site_info',
				'recommended',
				__( 'Your Site Title and/or Tagline are blank', 'all-in-one-seo-pack' ),
				sprintf(
					// Translators: 1 - The plugin short name ("AIOSEO").
					__(
						'Your Site Title and/or Tagline are blank. We recommend setting both of these values as %1$s requires these for various features, including our schema markup',
						'all-in-one-seo-pack'
					),
					AIOSEO_PLUGIN_SHORT_NAME
				),
				$this->actionLink( admin_url( 'options-general.php' ), __( 'Go to Settings > General', 'all-in-one-seo-pack' ) )
			);
		}

		return $this->result(
			'aioseo_site_info',
			'good',
			__( 'Your Site Title and Tagline are set', 'all-in-one-seo-pack' ),
			sprintf(
				// Translators: 1 - The plugin short name ("AIOSEO").
				__( 'Great! These are required for %1$s\'s schema markup and are often used as fallback values for various other features.', 'all-in-one-seo-pack' ),
				AIOSEO_PLUGIN_SHORT_NAME
			)
		);
	}

	/**
	 * Checks whether Google Search Console is connected.
	 *
	 * @since 4.6.2
	 *
	 * @return array The test result.
	 */
	public function testCheckGoogleSearchConsole() {
		$googleSearchConsole = aioseo()->searchStatistics->api->auth->isConnected();

		if ( ! $googleSearchConsole ) {
			return $this->result(
				'aioseo_google_search_console',
				'recommended',
				__( 'Connect Your Site with Google Search Console', 'all-in-one-seo-pack' ),
				__( 'Sync your site with Google Search Console and get valuable insights right inside your WordPress dashboard. Track keyword rankings and search performance for individual posts with actionable insights to help you rank higher in search results!', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
				$this->actionLink( admin_url( 'admin.php?page=aioseo-settings&aioseo-scroll=google-search-console-settings&aioseo-highlight=google-search-console-settings#/webmaster-tools?activetool=googleSearchConsole' ), __( 'Connect to Google Search Console', 'all-in-one-seo-pack' ) ) // phpcs:ignore Generic.Files.LineLength.MaxExceeded
			);
		}

		return $this->result(
			'aioseo_google_search_console',
			'good',
			__( 'Google Search Console is Connected', 'all-in-one-seo-pack' ),
			__( 'Awesome! Google Search Console is connected to your site. This will help you monitor and maintain your site\'s presence in Google Search results.', 'all-in-one-seo-pack' )
		);
	}

	/**
	 * Checks whether the required settings for our schema markup are set.
	 *
	 * @since 4.0.0
	 *
	 * @return array The test result.
	 */
	public function testCheckSchemaMarkup() {
		$menuPath = admin_url( 'admin.php?page=aioseo-search-appearance' );

		if ( 'organization' === aioseo()->options->searchAppearance->global->schema->siteRepresents ) {
			if (
				! aioseo()->options->searchAppearance->global->schema->organizationName ||
				(
					! aioseo()->options->searchAppearance->global->schema->organizationLogo &&
					! aioseo()->helpers->getSiteLogoUrl()
				)
			) {
				return $this->result(
					'aioseo_schema_markup',
					'recommended',
					__( 'Your Organization Name and/or Logo are blank', 'all-in-one-seo-pack' ),
					sprintf(
						// Translators: 1 - The plugin short name ("AIOSEO").
						__( 'Your Organization Name and/or Logo are blank. These values are required for %1$s\'s Organization schema markup.', 'all-in-one-seo-pack' ),
						AIOSEO_PLUGIN_SHORT_NAME
					),
					$this->actionLink( $menuPath, __( 'Go to Schema Settings', 'all-in-one-seo-pack' ) )
				);
			}

			return $this->result(
				'aioseo_schema_markup',
				'good',
				__( 'Your Organization Name and Logo are set', 'all-in-one-seo-pack' ),
				sprintf(
					// Translators: 1 - The plugin short name ("AIOSEO").
					__( 'Awesome! These are required for %1$s\'s Organization schema markup.', 'all-in-one-seo-pack' ),
					AIOSEO_PLUGIN_SHORT_NAME
				)
			);
		}

		if (
			! aioseo()->options->searchAppearance->global->schema->person ||
			(
				'manual' === aioseo()->options->searchAppearance->global->schema->person &&
				(
					! aioseo()->options->searchAppearance->global->schema->personName ||
					! aioseo()->options->searchAppearance->global->schema->personLogo
				)
			)
		) {
			return $this->result(
				'aioseo_schema_markup',
				'recommended',
				__( 'Your Person Name and/or Image are blank', 'all-in-one-seo-pack' ),
				sprintf(
					// Translators: 1 - The plugin short name ("AIOSEO").
					__( 'Your Person Name and/or Image are blank. These values are required for %1$s\'s Person schema markup.', 'all-in-one-seo-pack' ),
					AIOSEO_PLUGIN_SHORT_NAME
				),
				$this->actionLink( $menuPath, __( 'Go to Schema Settings', 'all-in-one-seo-pack' ) )
			);
		}

		return $this->result(
			'aioseo_schema_markup',
			'good',
			__( 'Your Person Name and Image are set', 'all-in-one-seo-pack' ),
			sprintf(
				// Translators: 1 - The plugin short name ("AIOSEO").
				__( 'Awesome! These are required for %1$s\'s Person schema markup.', 'all-in-one-seo-pack' ),
				AIOSEO_PLUGIN_SHORT_NAME
			)
		);
	}

	/**
	 * Checks if the plugin should be updated.
	 *
	 * @since 4.7.2
	 *
	 * @return bool Whether the plugin should be updated.
	 */
	public function shouldUpdate() {
		$response = wp_remote_get( 'https://api.wordpress.org/plugins/info/1.0/all-in-one-seo-pack.json' );
		$body     = wp_remote_retrieve_body( $response );
		if ( ! $body ) {
			// Something went wrong.
			return false;
		}

		$pluginData = json_decode( $body );

		return version_compare( AIOSEO_VERSION, $pluginData->version, '<' );
	}

	/**
	 * Checks whether the required settings for our schema markup are set.
	 *
	 * @since 4.0.0
	 *
	 * @return array The test result.
	 */
	public function testCheckPluginUpdate() {
		if ( $this->shouldUpdate() ) {
			return $this->result(
				'aioseo_plugin_update',
				'critical',
				sprintf(
					// Translators: 1 - The plugin short name ("AIOSEO").
					__( '%1$s needs to be updated', 'all-in-one-seo-pack' ),
					AIOSEO_PLUGIN_SHORT_NAME
				),
				sprintf(
					// Translators: 1 - The plugin short name ("AIOSEO").
					__( 'An update is available for %1$s. Upgrade to the latest version to receive all the latest features, bug fixes and security improvements.', 'all-in-one-seo-pack' ),
					AIOSEO_PLUGIN_SHORT_NAME
				),
				$this->actionLink( admin_url( 'plugins.php' ), __( 'Go to Plugins', 'all-in-one-seo-pack' ) )
			);
		}

		return $this->result(
			'aioseo_plugin_update',
			'good',
			sprintf(
				// Translators: 1 - The plugin short name ("AIOSEO").
				__( '%1$s is updated to the latest version', 'all-in-one-seo-pack' ),
				AIOSEO_PLUGIN_SHORT_NAME
			),
			__( 'Fantastic! By updating to the latest version, you have access to all the latest features, bug fixes and security improvements.', 'all-in-one-seo-pack' )
		);
	}

	/**
	 * Returns a list of noindexed content.
	 *
	 * @since 4.0.0
	 *
	 * @return array $noindexed A list of noindexed content.
	 */
	protected function noindexed() {
		$globalDefault = aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default;
		if (
			! $globalDefault &&
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindex
		) {
			return [
				__( 'Your entire site is set to globally noindex content.', 'all-in-one-seo-pack' )
			];
		}

		$noindexed = [];

		if (
			! $globalDefault &&
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindexPaginated
		) {
			$noindexed[] = __( 'Paginated Content', 'all-in-one-seo-pack' );
		}

		$archives = [
			'author' => __( 'Author Archives', 'all-in-one-seo-pack' ),
			'date'   => __( 'Date Archives', 'all-in-one-seo-pack' ),
			'search' => __( 'Search Page', 'all-in-one-seo-pack' )
		];

		// Archives.
		foreach ( $archives as $name => $type ) {
			if (
				! aioseo()->options->searchAppearance->archives->{ $name }->advanced->robotsMeta->default &&
				aioseo()->options->searchAppearance->archives->{ $name }->advanced->robotsMeta->noindex
			) {
				$noindexed[] = $type;
			}
		}

		foreach ( aioseo()->helpers->getPublicPostTypes() as $postType ) {
			if (
				aioseo()->dynamicOptions->searchAppearance->postTypes->has( $postType['name'] ) &&
				! aioseo()->dynamicOptions->searchAppearance->postTypes->{ $postType['name'] }->advanced->robotsMeta->default &&
				aioseo()->dynamicOptions->searchAppearance->postTypes->{ $postType['name'] }->advanced->robotsMeta->noindex
			) {
				$noindexed[] = $postType['label'] . ' (' . $postType['name'] . ')';
			}
		}

		foreach ( aioseo()->helpers->getPublicTaxonomies() as $taxonomy ) {
			if (
				aioseo()->dynamicOptions->searchAppearance->taxonomies->has( $taxonomy['name'] ) &&
				! aioseo()->dynamicOptions->searchAppearance->taxonomies->{ $taxonomy['name'] }->advanced->robotsMeta->default &&
				aioseo()->dynamicOptions->searchAppearance->taxonomies->{ $taxonomy['name'] }->advanced->robotsMeta->noindex
			) {
				$noindexed[] = $taxonomy['label'] . ' (' . $taxonomy['name'] . ')';
			}
		}

		return $noindexed;
	}

	/**
	 * Returns a list of nofollowed content.
	 *
	 * @since 4.0.0
	 *
	 * @return array $nofollowed A list of nofollowed content.
	 */
	protected function nofollowed() {
		$globalDefault = aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default;
		if (
			! $globalDefault &&
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->nofollow
		) {
			return [
				__( 'Your entire site is set to globally nofollow content.', 'all-in-one-seo-pack' )
			];
		}

		$nofollowed = [];

		if (
			! $globalDefault &&
			aioseo()->options->searchAppearance->advanced->globalRobotsMeta->nofollowPaginated
		) {
			$nofollowed[] = __( 'Paginated Content', 'all-in-one-seo-pack' );
		}

		$archives = [
			'author' => __( 'Author Archives', 'all-in-one-seo-pack' ),
			'date'   => __( 'Date Archives', 'all-in-one-seo-pack' ),
			'search' => __( 'Search Page', 'all-in-one-seo-pack' )
		];

		// Archives.
		foreach ( $archives as $name => $type ) {
			if (
				! aioseo()->options->searchAppearance->archives->{ $name }->advanced->robotsMeta->default &&
				aioseo()->options->searchAppearance->archives->{ $name }->advanced->robotsMeta->nofollow
			) {
				$nofollowed[] = $type;
			}
		}

		foreach ( aioseo()->helpers->getPublicPostTypes() as $postType ) {
			if (
				aioseo()->dynamicOptions->searchAppearance->postTypes->has( $postType['name'] ) &&
				! aioseo()->dynamicOptions->searchAppearance->postTypes->{ $postType['name'] }->advanced->robotsMeta->default &&
				aioseo()->dynamicOptions->searchAppearance->postTypes->{ $postType['name'] }->advanced->robotsMeta->nofollow
			) {
				$nofollowed[] = $postType['label'] . ' (' . $postType['name'] . ')';
			}
		}

		foreach ( aioseo()->helpers->getPublicTaxonomies() as $taxonomy ) {
			if (
				aioseo()->dynamicOptions->searchAppearance->taxonomies->has( $taxonomy['name'] ) &&
				! aioseo()->dynamicOptions->searchAppearance->taxonomies->{ $taxonomy['name'] }->advanced->robotsMeta->default &&
				aioseo()->dynamicOptions->searchAppearance->taxonomies->{ $taxonomy['name'] }->advanced->robotsMeta->nofollow
			) {
				$nofollowed[] = $taxonomy['label'] . ' (' . $taxonomy['name'] . ')';
			}
		}

		return $nofollowed;
	}

	/**
	 * Returns a debug info data field.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $label   The field label.
	 * @param  string  $value   The field value.
	 * @param  boolean $private Whether the field shouldn't be included if the debug info is copied.
	 * @return array            The debug info data field.
	 */
	private function field( $label, $value, $private = false ) {
		return [
			'label'   => $label,
			'value'   => $value,
			'private' => $private,
		];
	}

	/**
	 * Returns the test result.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name        The test name.
	 * @param  string $status      The result status.
	 * @param  string $header      The test header.
	 * @param  string $description The result description.
	 * @param  string $actions     The result actions.
	 * @return array               The test result.
	 */
	protected function result( $name, $status, $header, $description, $actions = '' ) {
		$color = 'blue';
		switch ( $status ) {
			case 'good':
				break;
			case 'recommended':
				$color = 'orange';
				break;
			case 'critical':
				$color = 'red';
				break;
			default:
				break;
		}

		return [
			'test'        => $name,
			'status'      => $status,
			'label'       => $header,
			'description' => $description,
			'actions'     => $actions,
			'badge'       => [
				'label' => AIOSEO_PLUGIN_SHORT_NAME,
				'color' => $color,
			],
		];
	}

	/**
	 * Returns an action link.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $path   The path.
	 * @param  string $anchor The anchor text.
	 * @return string         The action link.
	 */
	protected function actionLink( $path, $anchor ) {
		return sprintf(
			'<p><a href="%1$s">%2$s</a></p>',
			$path,
			$anchor
		);
	}
}AIOSEO.php000066600000031046151135505570006253 0ustar00<?php
namespace AIOSEO\Plugin {
	// Exit if accessed directly.
	if ( ! defined( 'ABSPATH' ) ) {
		exit;
	}

	/**
	 * Main AIOSEO class.
	 * We extend the abstract class as that one holds all the class properties.
	 *
	 * @since 4.0.0
	 */
	final class AIOSEO extends \AIOSEOAbstract {

		/**
		 * Holds the instance of the plugin currently in use.
		 *
		 * @since 4.0.0
		 *
		 * @var AIOSEO
		 */
		private static $instance;

		/**
		 * Plugin version for enqueueing, etc.
		 * The value is retrieved from the AIOSEO_VERSION constant.
		 *
		 * @since 4.0.0
		 *
		 * @var string
		 */
		public $version = '';

		/**
		 * Paid returns true, free (Lite) returns false.
		 *
		 * @since 4.0.0
		 *
		 * @var boolean
		 */
		public $pro = false;

		/**
		 * Returns 'Pro' or 'Lite'.
		 *
		 * @since 4.0.0
		 *
		 * @var boolean
		 */
		public $versionPath = 'Lite';

		/**
		 * Whether we're in a dev environment.
		 *
		 * @since 4.1.9
		 *
		 * @var bool
		 */
		public $isDev = false;

		/**
		 * Uninstall class instance.
		 *
		 * @since 4.8.1
		 *
		 * @var Common\Main\Uninstall
		 */
		public $uninstall = null;

		/**
		 * Main AIOSEO Instance.
		 *
		 * Insures that only one instance of AIOSEO exists in memory at any one
		 * time. Also prevents needing to define globals all over the place.
		 *
		 * @since 4.0.0
		 *
		 * @return AIOSEO The aioseo instance.
		 */
		public static function instance() {
			if ( null === self::$instance || ! self::$instance instanceof self ) {
				self::$instance = new self();

				self::$instance->init();

				// Load our addons on the action right after plugins_loaded.
				add_action( 'sanitize_comment_cookies', [ self::$instance, 'loadAddons' ] );
			}

			return self::$instance;
		}

		/**
		 * Initialize All in One SEO!
		 *
		 * @since 4.0.0
		 *
		 * @return void
		 */
		private function init() {
			$this->constants();
			$this->includes();
			$this->preLoad();
			if ( ! $this->core->isUninstalling() ) {
				$this->load();
			}
		}

		/**
		 * Setup plugin constants.
		 * All the path/URL related constants are defined in main plugin file.
		 *
		 * @since 4.0.0
		 *
		 * @return void
		 */
		private function constants() {
			$defaultHeaders = [
				'name'    => 'Plugin Name',
				'version' => 'Version',
			];

			$pluginData = get_file_data( AIOSEO_FILE, $defaultHeaders );

			$constants = [
				'AIOSEO_PLUGIN_BASENAME'   => plugin_basename( AIOSEO_FILE ),
				'AIOSEO_PLUGIN_NAME'       => $pluginData['name'],
				'AIOSEO_PLUGIN_SHORT_NAME' => 'AIOSEO',
				'AIOSEO_PLUGIN_URL'        => plugin_dir_url( AIOSEO_FILE ),
				'AIOSEO_VERSION'           => $pluginData['version'],
				'AIOSEO_MARKETING_URL'     => 'https://aioseo.com/',
				'AIOSEO_MARKETING_DOMAIN'  => 'aioseo.com'
			];

			foreach ( $constants as $constant => $value ) {
				if ( ! defined( $constant ) ) {
					define( $constant, $value );
				}
			}

			$this->version = AIOSEO_VERSION;
		}

		/**
		 * Including the new files with PHP 5.3 style.
		 *
		 * @since 4.0.0
		 *
		 * @return void
		 */
		private function includes() {
			$dependencies = [
				'/vendor/autoload.php'                                      => true,
				'/vendor/woocommerce/action-scheduler/action-scheduler.php' => true,
				'/vendor/jwhennessey/phpinsight/autoload.php'               => false,
				'/vendor_prefixed/monolog/monolog/src/Monolog/Logger.php'   => false
			];

			foreach ( $dependencies as $path => $shouldRequire ) {
				if ( ! file_exists( AIOSEO_DIR . $path ) ) {
					// Something is not right.
					status_header( 500 );
					wp_die( esc_html__( 'Plugin is missing required dependencies. Please contact support for more information.', 'all-in-one-seo-pack' ) );
				}

				if ( $shouldRequire ) {
					require_once AIOSEO_DIR . $path;
				}
			}

			$this->loadVersion();
		}

		/**
		 * Load the version of the plugin we are currently using.
		 *
		 * @since 4.1.9
		 *
		 * @return void
		 */
		private function loadVersion() {
			$proDir = is_dir( plugin_dir_path( AIOSEO_FILE ) . 'app/Pro' );

			if (
				! class_exists( '\Dotenv\Dotenv' ) ||
				! file_exists( AIOSEO_DIR . '/build/.env' )
			) {
				$this->pro         = $proDir;
				$this->versionPath = $proDir ? 'Pro' : 'Lite';

				return;
			}

			$dotenv = \Dotenv\Dotenv::createUnsafeImmutable( AIOSEO_DIR, '/build/.env' );
			$dotenv->load();

			$version = defined( 'AIOSEO_DEV_VERSION' )
				? strtolower( AIOSEO_DEV_VERSION )
				: strtolower( getenv( 'VITE_VERSION' ) );
			if ( ! empty( $version ) ) {
				$this->isDev = true;

				if ( file_exists( AIOSEO_DIR . '/build/filters.php' ) ) {
					require_once AIOSEO_DIR . '/build/filters.php';
				}
			}

			if ( $proDir && 'pro' === $version ) {
				$this->pro         = true;
				$this->versionPath = 'Pro';
			}
		}

		/**
		 * Runs before we load the plugin.
		 *
		 * @since 4.0.0
		 *
		 * @return void
		 */
		private function preLoad() {
			$this->core = new Common\Core\Core();

			$this->backwardsCompatibility();

			// Internal Options.
			$this->helpers                = $this->pro ? new Pro\Utils\Helpers() : new Lite\Utils\Helpers();
			$this->internalNetworkOptions = ( $this->pro && $this->helpers->isPluginNetworkActivated() ) ? new Pro\Options\InternalNetworkOptions() : new Common\Options\InternalNetworkOptions();
			$this->internalOptions        = $this->pro ? new Pro\Options\InternalOptions() : new Lite\Options\InternalOptions();
			$this->uninstall              = new Common\Main\Uninstall();

			// Run pre-updates.
			$this->preUpdates = $this->pro ? new Pro\Main\PreUpdates() : new Common\Main\PreUpdates();
		}

		/**
		 * To prevent errors and bugs from popping up,
		 * we will run this backwards compatibility method.
		 *
		 * @since 4.1.9
		 *
		 * @return void
		 */
		private function backwardsCompatibility() {
			$this->db           = $this->core->db;
			$this->cache        = $this->core->cache;
			$this->transients   = $this->cache;
			$this->cachePrune   = $this->core->cachePrune;
			$this->optionsCache = $this->core->optionsCache;
		}

		/**
		 * To prevent errors and bugs from popping up,
		 * we will run this backwards compatibility method.
		 *
		 * @since 4.2.0
		 *
		 * @return void
		 */
		private function backwardsCompatibilityLoad() {
			$this->postSettings->integrations = $this->standalone->pageBuilderIntegrations;
		}

		/**
		 * Load our classes.
		 *
		 * @since 4.0.0
		 *
		 * @return void
		 */
		public function load() {
			// Load external translations if this is a Pro install.
			if ( $this->pro ) {
				$translations = new Pro\Main\Translations(
					'plugin',
					'all-in-one-seo-pack',
					'https://aioseo.com/aioseo-plugin/all-in-one-seo-pack/packages.json'
				);
				$translations->init();

				$translations = new Pro\Main\Translations(
					'plugin',
					'aioseo-pro',
					'https://aioseo.com/aioseo-plugin/aioseo-pro/packages.json'
				);
				$translations->init();
			}

			$this->addons             = $this->pro ? new Pro\Utils\Addons() : new Common\Utils\Addons();
			$this->features           = $this->pro ? new Pro\Utils\Features() : new Common\Utils\Features();
			$this->tags               = $this->pro ? new Pro\Utils\Tags() : new Common\Utils\Tags();
			$this->blocks             = new Common\Utils\Blocks();
			$this->breadcrumbs        = $this->pro ? new Pro\Breadcrumbs\Breadcrumbs() : new Common\Breadcrumbs\Breadcrumbs();
			$this->dynamicBackup      = $this->pro ? new Pro\Options\DynamicBackup() : new Common\Options\DynamicBackup();
			$this->options            = $this->pro ? new Pro\Options\Options() : new Lite\Options\Options();
			$this->networkOptions     = ( $this->pro && $this->helpers->isPluginNetworkActivated() ) ? new Pro\Options\NetworkOptions() : new Common\Options\NetworkOptions();
			$this->dynamicOptions     = $this->pro ? new Pro\Options\DynamicOptions() : new Common\Options\DynamicOptions();
			$this->backup             = new Common\Utils\Backup();
			$this->access             = $this->pro ? new Pro\Utils\Access() : new Common\Utils\Access();
			$this->usage              = $this->pro ? new Pro\Admin\Usage() : new Lite\Admin\Usage();
			$this->siteHealth         = $this->pro ? new Pro\Admin\SiteHealth() : new Common\Admin\SiteHealth();
			$this->networkLicense     = $this->pro && $this->helpers->isPluginNetworkActivated() ? new Pro\Admin\NetworkLicense() : null;
			$this->license            = $this->pro ? new Pro\Admin\License() : null;
			$this->autoUpdates        = $this->pro ? new Pro\Admin\AutoUpdates() : null;
			$this->updates            = $this->pro ? new Pro\Main\Updates() : new Common\Main\Updates();
			$this->meta               = $this->pro ? new Pro\Meta\Meta() : new Common\Meta\Meta();
			$this->social             = $this->pro ? new Pro\Social\Social() : new Common\Social\Social();
			$this->robotsTxt          = new Common\Tools\RobotsTxt();
			$this->htaccess           = new Common\Tools\Htaccess();
			$this->term               = $this->pro ? new Pro\Admin\Term() : null;
			$this->notices            = $this->pro ? new Pro\Admin\Notices\Notices() : new Lite\Admin\Notices\Notices();
			$this->wpNotices          = new Common\Admin\Notices\WpNotices();
			$this->admin              = $this->pro ? new Pro\Admin\Admin() : new Lite\Admin\Admin();
			$this->networkAdmin       = $this->helpers->isPluginNetworkActivated() ? ( $this->pro ? new Pro\Admin\NetworkAdmin() : new Common\Admin\NetworkAdmin() ) : null;
			$this->activate           = $this->pro ? new Pro\Main\Activate() : new Common\Main\Activate();
			$this->conflictingPlugins = $this->pro ? new Pro\Admin\ConflictingPlugins() : new Common\Admin\ConflictingPlugins();
			$this->migration          = $this->pro ? new Pro\Migration\Migration() : new Common\Migration\Migration();
			$this->importExport       = $this->pro ? new Pro\ImportExport\ImportExport() : new Common\ImportExport\ImportExport();
			$this->sitemap            = $this->pro ? new Pro\Sitemap\Sitemap() : new Common\Sitemap\Sitemap();
			$this->htmlSitemap        = new Common\Sitemap\Html\Sitemap();
			$this->templates          = $this->pro ? new Pro\Utils\Templates() : new Common\Utils\Templates();
			$this->categoryBase       = new Common\Main\CategoryBase();
			$this->postSettings       = $this->pro ? new Pro\Admin\PostSettings() : new Lite\Admin\PostSettings();
			$this->standalone         = new Common\Standalone\Standalone();
			$this->searchStatistics   = $this->pro ? new Pro\SearchStatistics\SearchStatistics() : new Common\SearchStatistics\SearchStatistics();
			$this->slugMonitor        = new Common\Admin\SlugMonitor();
			$this->schema             = $this->pro ? new Pro\Schema\Schema() : new Common\Schema\Schema();
			$this->actionScheduler    = new Common\Utils\ActionScheduler();
			$this->seoRevisions       = $this->pro ? new Pro\SeoRevisions\SeoRevisions() : new Common\SeoRevisions\SeoRevisions();
			$this->ai                 = $this->pro ? new Pro\Ai\Ai() : null;
			$this->filters            = $this->pro ? new Pro\Main\Filters() : new Lite\Main\Filters();
			$this->crawlCleanup       = new Common\QueryArgs\CrawlCleanup();
			$this->searchCleanup      = new Common\SearchCleanup\SearchCleanup();
			$this->emailReports       = new Common\EmailReports\EmailReports();
			$this->thirdParty         = new Common\ThirdParty\ThirdParty();
			$this->writingAssistant   = new Common\WritingAssistant\WritingAssistant();

			if ( ! wp_doing_ajax() && ! wp_doing_cron() ) {
				$this->rss       = new Common\Rss();
				$this->main      = $this->pro ? new Pro\Main\Main() : new Common\Main\Main();
				$this->head      = $this->pro ? new Pro\Main\Head() : new Common\Main\Head();
				$this->dashboard = $this->pro ? new Pro\Admin\Dashboard() : new Common\Admin\Dashboard();
				$this->api       = $this->pro ? new Pro\Api\Api() : new Lite\Api\Api();
				$this->help      = new Common\Help\Help();
			}

			$this->backwardsCompatibilityLoad();

			add_action( 'init', [ $this, 'loadInit' ], 999 );
		}

		/**
		 * Things that need to load after init.
		 *
		 * @since 4.0.0
		 *
		 * @return void
		 */
		public function loadInit() {
			$this->settings = new Common\Utils\VueSettings( '_aioseo_settings' );
			$this->sitemap->init();

			// We call this again to reset any post types/taxonomies that have not yet been set up.
			$this->dynamicOptions->refresh();
		}

		/**
		 * Loads our addons.
		 *
		 * Runs right after the plugins_loaded hook.
		 *
		 * @since 4.0.0
		 *
		 * @return void
		 */
		public function loadAddons() {
			do_action( 'aioseo_loaded' );
		}
	}
}

namespace {
	// Exit if accessed directly.
	if ( ! defined( 'ABSPATH' ) ) {
		exit;
	}

	/**
	 * The function which returns the one AIOSEO instance.
	 *
	 * @since 4.0.0
	 *
	 * @return AIOSEO\Plugin\AIOSEO The instance.
	 */
	function aioseo() {
		return AIOSEO\Plugin\AIOSEO::instance();
	}
}AIOSEOAbstract.php000066600000024577151135505570007752 0ustar00<?php
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Abstract class holding the class properties of our main AIOSEO class.
 *
 * @since 4.2.7
 */
abstract class AIOSEOAbstract {
	/**
	 * Core class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Core\Core
	 */
	public $core = null;

	/**
	 * Helpers class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Lite\Utils\Helpers|\AIOSEO\Plugin\Pro\Utils\Helpers
	 */
	public $helpers = null;

	/**
	 * InternalNetworkOptions class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Options\InternalNetworkOptions|\AIOSEO\Plugin\Pro\Options\InternalNetworkOptions
	 */
	public $internalNetworkOptions = null;

	/**
	 * InternalOptions class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Lite\Options\InternalOptions|\AIOSEO\Plugin\Pro\Options\InternalOptions
	 */
	public $internalOptions = null;

	/**
	 * PreUpdates class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Main\PreUpdates|\AIOSEO\Plugin\Pro\Main\PreUpdates
	 */
	public $preUpdates = null;

	/**
	 * Db class instance.
	 * This prop is set for backwards compatibility.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\Database
	 */
	public $db = null;

	/**
	 * Transients class instance.
	 * This prop is set for backwards compatibility.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\Cache
	 */
	public $transients = null;

	/**
	 * OptionsCache class instance.
	 * This prop is set for backwards compatibility.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Options\Cache
	 */
	public $optionsCache = null;

	/**
	 * PostSettings class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Lite\Admin\PostSettings|\AIOSEO\Plugin\Pro\Admin\PostSettings
	 */
	public $postSettings = null;

	/**
	 * Standalone class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Standalone\Standalone
	 */
	public $standalone = null;

	/**
	 * Search Statistics class instance.
	 *
	 * @since 4.3.0
	 *
	 * @var \AIOSEO\Plugin\Pro\SearchStatistics\SearchStatistics
	 */
	public $searchStatistics = null;

	/**
	 * Tags class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Pro\Utils\Tags
	 */
	public $tags = null;

	/**
	 * Addons class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\Blocks
	 */
	public $blocks = null;

	/**
	 * Breadcrumbs class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Breadcrumbs\Breadcrumbs|\AIOSEO\Plugin\Pro\Breadcrumbs\Breadcrumbs
	 */
	public $breadcrumbs = null;

	/**
	 * DynamicBackup class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Options\DynamicBackup|\AIOSEO\Plugin\Pro\Options\DynamicBackup
	 */
	public $dynamicBackup = null;

	/**
	 * NetworkOptions class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Options\NetworkOptions|\AIOSEO\Plugin\Pro\Options\NetworkOptions
	 */
	public $networkOptions = null;

	/**
	 * Backup class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\Backup
	 */
	public $backup = null;

	/**
	 * Access class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\Access|\AIOSEO\Plugin\Pro\Utils\Access
	 */
	public $access = null;

	/**
	 * NetworkLicense class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var null|\AIOSEO\Plugin\Pro\Admin\NetworkLicense
	 */
	public $networkLicense = null;

	/**
	 * License class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var null|\AIOSEO\Plugin\Pro\Admin\License
	 */
	public $license = null;

	/**
	 * Updates class isntance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Main\Updates|\AIOSEO\Plugin\Pro\Main\Updates
	 */
	public $updates = null;

	/**
	 * Meta class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Pro\Meta\Meta
	 */
	public $meta = null;

	/**
	 * Social class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Social\Social|\AIOSEO\Plugin\Pro\Social\Social
	 */
	public $social = null;

	/**
	 * RobotsTxt class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Tools\RobotsTxt
	 */
	public $robotsTxt = null;

	/**
	 * Htaccess class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Tools\Htaccess
	 */
	public $htaccess = null;

	/**
	 * Term class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var null|\AIOSEO\Plugin\Pro\Admin\Term
	 */
	public $term = null;

	/**
	 * Notices class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Lite\Admin\Notices\Notices|\AIOSEO\Plugin\Pro\Admin\Notices\Notices
	 */
	public $notices = null;

	/**
	 * WpNotices class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Admin\Notices\WpNotices
	 */
	public $wpNotices = null;

	/**
	 * Admin class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Lite\Admin\Admin|\AIOSEO\Plugin\Pro\Admin\Admin
	 */
	public $admin = null;

	/**
	 * NetworkAdmin class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Admin\NetworkAdmin|\AIOSEO\Plugin\Pro\Admin\NetworkAdmin
	 */
	public $networkAdmin = null;

	/**
	 * Activate class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Main\Activate|\AIOSEO\Plugin\Pro\Main\Activate
	 */
	public $activate = null;

	/**
	 * ConflictingPlugins class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Admin\ConflictingPlugins|\AIOSEO\Plugin\Pro\Admin\ConflictingPlugins
	 */
	public $conflictingPlugins = null;

	/**
	 * Migration class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Pro\Migration\Migration
	 */
	public $migration = null;

	/**
	 * ImportExport class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\ImportExport\ImportExport
	 */
	public $importExport = null;

	/**
	 * Sitemap class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Sitemap\Sitemap|\AIOSEO\Plugin\Pro\Sitemap\Sitemap
	 */
	public $sitemap = null;

	/**
	 * HtmlSitemap class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Sitemap\Html\Sitemap
	 */
	public $htmlSitemap = null;

	/**
	 * CategoryBase class instance.
	 *
	 * @since   4.2.7
	 * @version 4.7.1 Moved from Pro to Common.
	 *
	 * @var null|\AIOSEO\Plugin\Common\Main\CategoryBase
	 */
	public $categoryBase = null;

	/**
	 * SlugMonitor class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Admin\SlugMonitor
	 */
	public $slugMonitor = null;

	/**
	 * Schema class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Pro\Schema\Schema
	 */
	public $schema = null;

	/**
	 * Rss class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Rss
	 */
	public $rss = null;

	/**
	 * Main class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Main\Main|\AIOSEO\Plugin\Pro\Main\Main
	 */
	public $main = null;

	/**
	 * Head class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Main\Head|\AIOSEO\Plugin\Pro\Main\Head
	 */
	public $head = null;

	/**
	 * Dashboard class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Admin\Dashboard|\AIOSEO\Plugin\Pro\Admin\Dashboard
	 */
	public $dashboard = null;

	/**
	 * API class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Lite\Api\Api|\AIOSEO\Plugin\Pro\Api\Api
	 */
	public $api = null;

	/**
	 * Help class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Help\Help
	 */
	public $help = null;

	/**
	 * Settings class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\VueSettings
	 */
	public $settings = null;

	/**
	 * Cache class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\Cache
	 */
	public $cache = null;

	/**
	 * CachePrune class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\CachePrune
	 */
	public $cachePrune = null;

	/**
	 * Addons class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Pro\Utils\Addons|\AIOSEO\Plugin\Common\Utils\Addons
	 */
	public $addons = null;

	/**
	 * Addons class instance.
	 *
	 * @since 4.3.0
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\Features|\AIOSEO\Plugin\Pro\Utils\Features
	 */
	public $features = null;

	/**
	 * Options class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Options\Options|\AIOSEO\Plugin\Pro\Options\Options
	 */
	public $options = null;

	/**
	 * DynamicOptions class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Options\DynamicOptions|\AIOSEO\Plugin\Pro\Options\DynamicOptions
	 */
	public $dynamicOptions = null;

	/**
	 * Usage class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Lite\Admin\Usage|\AIOSEO\Plugin\Pro\Admin\Usage
	 */
	public $usage = null;

	/**
	 * SiteHealth class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Admin\SiteHealth|\AIOSEO\Plugin\Pro\Admin\SiteHealth
	 */
	public $siteHealth = null;

	/**
	 * AutoUpdates class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Pro\Admin\AutoUpdates
	 */
	public $autoUpdates = null;

	/**
	 * Templates class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\Templates|\AIOSEO\Plugin\Pro\Utils\Templates
	 */
	public $templates = null;

	/**
	 * Filters class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Lite\Main\Filters|\AIOSEO\Plugin\Pro\Main\Filters
	 */
	public $filters = null;

	/**
	 * ActionScheduler class instance.
	 *
	 * @since 4.2.7
	 *
	 * @var \AIOSEO\Plugin\Common\Utils\ActionScheduler
	 */
	public $actionScheduler = null;

	/**
	 * AI class instance.
	 *
	 * @since 4.3.3
	 *
	 * @var null|\AIOSEO\Plugin\Pro\Ai\Ai
	 */
	public $ai = null;

	/**
	 * SeoRevisions class instance.
	 *
	 * @since 4.4.0
	 *
	 * @var null|\AIOSEO\Plugin\Pro\SeoRevisions\SeoRevisions
	 */
	public $seoRevisions = null;

	/**
	 * Crawl Cleanup class instance.
	 *
	 * @since 4.5.8
	 *
	 * @var \AIOSEO\Plugin\Common\QueryArgs\CrawlCleanup
	 */
	public $crawlCleanup = null;

	/**
	 * Search Cleanup class instance.
	 *
	 * @since 4.8.0
	 *
	 * @var \AIOSEO\Plugin\Common\SearchCleanup\SearchCleanup
	 */
	public $searchCleanup = null;

	/**
	 * EmailReports class instance.
	 *
	 * @since 4.7.2
	 *
	 * @var null|\AIOSEO\Plugin\Common\EmailReports\EmailReports
	 */
	public $emailReports = null;

	/**
	 * ThirdParty class instance.
	 *
	 * @since 4.7.6
	 *
	 * @var \AIOSEO\Plugin\Common\ThirdParty\ThirdParty
	 */
	public $thirdParty = null;

	/**
	 * WritingAssistant class instance.
	 *
	 * @since 4.7.4
	 *
	 * @var \AIOSEO\Plugin\Common\WritingAssistant\WritingAssistant
	 */
	public $writingAssistant = null;
}WordPressSDK.php000066600000017636151143706510007573 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK;

use YoastSEO_Vendor\WordProof\SDK\Config\DefaultAppConfig;
use YoastSEO_Vendor\WordProof\SDK\Config\AppConfigInterface;
use YoastSEO_Vendor\WordProof\SDK\Controllers\NoticeController;
use YoastSEO_Vendor\WordProof\SDK\Controllers\PostEditorDataController;
use YoastSEO_Vendor\WordProof\SDK\Controllers\PostEditorTimestampController;
use YoastSEO_Vendor\WordProof\SDK\Controllers\RestApiController;
use YoastSEO_Vendor\WordProof\SDK\Controllers\AuthenticationController;
use YoastSEO_Vendor\WordProof\SDK\Controllers\CertificateController;
use YoastSEO_Vendor\WordProof\SDK\Controllers\SettingsController;
use YoastSEO_Vendor\WordProof\SDK\Controllers\TimestampController;
use YoastSEO_Vendor\WordProof\SDK\Support\Loader;
use YoastSEO_Vendor\WordProof\SDK\Translations\DefaultTranslations;
use YoastSEO_Vendor\WordProof\SDK\Translations\TranslationsInterface;
class WordPressSDK
{
    /**
     * The version of this SDK
     * @var string
     */
    public $version = '1.3.2';
    /**
     * @var null|WordPressSDK
     */
    private static $instance = null;
    /**
     * Loader responsible for the WordPress hooks
     * @var Loader
     */
    private $loader;
    /**
     * Appconfig object
     * @var AppConfigInterface
     */
    public $appConfig;
    /**
     * Translations object
     * @var TranslationsInterface
     */
    private $translations;
    /**
     * WordPressSDK constructor.
     *
     * @return WordPressSDK|void
     *
     * @throws \Exception
     */
    public function __construct(\YoastSEO_Vendor\WordProof\SDK\Config\AppConfigInterface $appConfig = null, \YoastSEO_Vendor\WordProof\SDK\Translations\TranslationsInterface $translations = null)
    {
        if (\defined('WORDPROOF_TIMESTAMP_SDK_VERSION')) {
            return;
        }
        $this->loader = new \YoastSEO_Vendor\WordProof\SDK\Support\Loader();
        $this->appConfig = $appConfig ?: new \YoastSEO_Vendor\WordProof\SDK\Config\DefaultAppConfig();
        $this->translations = $translations ?: new \YoastSEO_Vendor\WordProof\SDK\Translations\DefaultTranslations();
        $this->authentication();
        $this->api();
        $this->timestamp();
        $this->settings();
        $this->postEditorData();
        $this->notices();
        if (!\defined('WORDPROOF_TIMESTAMP_SDK_FILE')) {
            \define('WORDPROOF_TIMESTAMP_SDK_FILE', __FILE__);
        }
        if (!\defined('WORDPROOF_TIMESTAMP_SDK_VERSION')) {
            \define('WORDPROOF_TIMESTAMP_SDK_VERSION', $this->version);
        }
        return $this;
    }
    /**
     * Singleton implementation of WordPress SDK.
     *
     * @param AppConfigInterface|null $appConfig
     * @param TranslationsInterface|null $translations
     * @return WordPressSDK|null Returns the WordPress SDK instance.
     * @throws \Exception
     */
    public static function getInstance(\YoastSEO_Vendor\WordProof\SDK\Config\AppConfigInterface $appConfig = null, \YoastSEO_Vendor\WordProof\SDK\Translations\TranslationsInterface $translations = null)
    {
        if (self::$instance === null) {
            self::$instance = new \YoastSEO_Vendor\WordProof\SDK\WordPressSDK($appConfig, $translations);
        }
        return self::$instance;
    }
    /**
     * Runs the loader and initializes the class.
     *
     * @return $this
     */
    public function initialize()
    {
        $this->loader->run();
        return $this;
    }
    /**
     * Initializes the authentication feature.
     */
    private function authentication()
    {
        $class = new \YoastSEO_Vendor\WordProof\SDK\Controllers\AuthenticationController();
        $this->loader->addAction('wordproof_authenticate', $class, 'authenticate');
        $this->loader->addAction('admin_menu', $class, 'addRedirectPage');
        $this->loader->addAction('admin_menu', $class, 'addSelfDestructPage');
        $this->loader->addAction('load-admin_page_wordproof-redirect-authenticate', $class, 'redirectOnLoad');
    }
    /**
     * Initializes the api feature.
     */
    private function api()
    {
        $class = new \YoastSEO_Vendor\WordProof\SDK\Controllers\RestApiController();
        $this->loader->addAction('rest_api_init', $class, 'init');
    }
    /**
     * Adds hooks to timestamp posts on new inserts or on a custom action.
     */
    private function timestamp()
    {
        $class = new \YoastSEO_Vendor\WordProof\SDK\Controllers\TimestampController();
        $this->loader->addAction('added_post_meta', $class, 'syncPostMetaTimestampOverrides', \PHP_INT_MAX, 4);
        $this->loader->addAction('updated_post_meta', $class, 'syncPostMetaTimestampOverrides', \PHP_INT_MAX, 4);
        $this->loader->addAction('rest_after_insert_post', $class, 'timestampAfterRestApiRequest');
        $this->loader->addAction('wp_insert_post', $class, 'timestampAfterPostRequest', \PHP_INT_MAX, 2);
        $this->loader->addAction('edit_attachment', $class, 'timestampAfterAttachmentRequest', \PHP_INT_MAX);
        $this->loader->addAction('add_attachment', $class, 'timestampAfterAttachmentRequest', \PHP_INT_MAX);
        $this->loader->addAction('wordproof_timestamp', $class, 'timestamp');
        $this->loader->addAction('elementor/document/before_save', $class, 'beforeElementorSave');
    }
    /**
     * Adds admin pages that redirect to the WordProof My settings page.
     */
    private function settings()
    {
        $class = new \YoastSEO_Vendor\WordProof\SDK\Controllers\SettingsController();
        $this->loader->addAction('wordproof_settings', $class, 'redirect');
        $this->loader->addAction('admin_menu', $class, 'addRedirectPage');
        $this->loader->addAction('load-admin_page_wordproof-redirect-settings', $class, 'redirectOnLoad');
    }
    /**
     * Registers and localizes post editor scripts.
     */
    private function postEditorData()
    {
        $class = new \YoastSEO_Vendor\WordProof\SDK\Controllers\PostEditorDataController($this->translations);
        $this->loader->addAction('admin_enqueue_scripts', $class, 'addScript');
        $this->loader->addAction('elementor/editor/before_enqueue_scripts', $class, 'addScriptForElementor');
    }
    /**
     * Initializes the notices feature.
     */
    private function notices()
    {
        $class = new \YoastSEO_Vendor\WordProof\SDK\Controllers\NoticeController($this->translations);
        $this->loader->addAction('admin_notices', $class, 'show');
    }
    /**
     * Optional feature to include the schema and certificate to the page.
     *
     * @return $this
     */
    public function certificate()
    {
        $class = new \YoastSEO_Vendor\WordProof\SDK\Controllers\CertificateController();
        $this->loader->addAction('wp_head', $class, 'head');
        $this->loader->addFilter('the_content', $class, 'certificateTag');
        return $this;
    }
    /**
     * Optional feature to timestamp with JS in the post editor.
     *
     * @return $this
     */
    public function timestampInPostEditor()
    {
        $class = new \YoastSEO_Vendor\WordProof\SDK\Controllers\PostEditorTimestampController();
        // Gutenberg
        $this->loader->addAction('init', $class, 'registerPostMeta', \PHP_INT_MAX);
        $this->loader->addAction('enqueue_block_editor_assets', $class, 'enqueueBlockEditorScript');
        // Classic editor
        $this->loader->addAction('add_meta_boxes', $class, 'addMetaboxToClassicEditor');
        $this->loader->addAction('save_post', $class, 'saveClassicMetaboxPostMeta');
        $this->loader->addAction('edit_attachment', $class, 'saveClassicMetaboxPostMeta');
        $this->loader->addAction('admin_enqueue_scripts', $class, 'enqueueClassicEditorScript');
        // Elementor
        $this->loader->addAction('elementor/editor/after_enqueue_scripts', $class, 'enqueueElementorEditorScript');
        $this->loader->addAction('elementor/documents/register_controls', $class, 'registerControl');
        $this->loader->addAction('elementor/editor/after_save', $class, 'elementorSave');
        return $this;
    }
}
Translations/.htaccess000066600000000424151143706510011032 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>Translations/TranslationsInterface.php000066600000000760151143706510014252 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Translations;

interface TranslationsInterface
{
    public function getNoBalanceNotice();
    public function getTimestampSuccessNotice();
    public function getTimestampFailedNotice();
    public function getWebhookFailedNotice();
    public function getNotAuthenticatedNotice();
    public function getOpenSettingsButtonText();
    public function getOpenAuthenticationButtonText();
    public function getContactWordProofSupportButtonText();
}
Translations/DefaultTranslations.php000066600000003721151143706510013736 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Translations;

class DefaultTranslations implements \YoastSEO_Vendor\WordProof\SDK\Translations\TranslationsInterface
{
    public function getNoBalanceNotice()
    {
        return \sprintf(
            /* translators: %s expands to WordProof. */
            __('You are out of timestamps. Please upgrade your account by opening the %s settings.', 'wordproof'),
            'WordProof'
        );
    }
    public function getTimestampFailedNotice()
    {
        return \sprintf(
            /* translators: %s expands to WordProof. */
            __('%1$s failed to timestamp this page. Please check if you\'re correctly authenticated with %1$s and try to save this page again.', 'wordproof'),
            'WordProof'
        );
    }
    public function getTimestampSuccessNotice()
    {
        return \sprintf(
            /* translators: %s expands to WordProof. */
            __('%s has successfully timestamped this page.', 'wordproof'),
            'WordProof'
        );
    }
    public function getWebhookFailedNotice()
    {
        /* translators: %s expands to WordProof. */
        return \sprintf(__('The timestamp is not retrieved by your site. Please try again or contact %1$s support.', 'wordproof'), 'WordProof');
    }
    public function getNotAuthenticatedNotice()
    {
        /* translators: %s expands to WordProof. */
        return \sprintf(__('The timestamp is not created because you need to authenticate with %s first.', 'wordproof'), 'WordProof');
    }
    public function getOpenAuthenticationButtonText()
    {
        return __('Authenticate', 'wordproof');
    }
    public function getOpenSettingsButtonText()
    {
        return __('Open settings', 'wordproof');
    }
    public function getContactWordProofSupportButtonText()
    {
        return \sprintf(
            /* translators: %s expands to WordProof. */
            __('Contact %s support.', 'wordproof'),
            'WordProof'
        );
    }
}
DataTransferObjects/TimestampData.php000066600000001354151143706510013714 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\DataTransferObjects;

class TimestampData
{
    /**
     * Get timestamp data from post object.
     *
     * @param \WP_Post $post
     * @return array
     */
    public static function fromPost($post)
    {
        if ($post->post_type === 'attachment') {
            $file = \get_attached_file($post->ID);
            $content = '';
            if ($file) {
                $content = \hash_file('sha256', $file);
            }
        } else {
            $content = $post->post_content;
        }
        return ['uid' => $post->ID, 'date_modified' => \get_post_modified_time('c', \false, $post->ID), 'title' => $post->post_title, 'url' => \get_permalink($post), 'content' => $content];
    }
}
DataTransferObjects/.htaccess000066600000000424151143706510012241 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>Config/Config.php000066600000001670151143706510007702 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Config;

abstract class Config
{
    /**
     * Try to return config values using the dot syntax.
     *
     * @param string|null $key The key of the config using the dot syntax.
     * @return array|mixed Returns the entire config array if not found, otherwise the value itself.
     */
    public static function get($key = null)
    {
        if (!isset($key)) {
            return static::values();
        }
        $keys = \explode('.', $key);
        $value = static::values();
        foreach ($keys as $key) {
            if (isset($value[$key])) {
                $value = $value[$key];
            } else {
                return \false;
            }
        }
        return $value;
    }
    /**
     * Should return an array with the config.
     *
     * @return array An array containing the config values.
     */
    protected static function values()
    {
        return [];
    }
}
Config/RoutesConfig.php000066600000002012151143706510011073 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Config;

class RoutesConfig extends \YoastSEO_Vendor\WordProof\SDK\Config\Config
{
    /**
     * Returns an array with the environment config.
     *
     * @return array
     */
    protected static function values()
    {
        return ['hashInput' => ['endpoint' => '/posts/(?P<id>\\d+)/hashinput/(?P<hash>[a-fA-F0-9]{64})', 'method' => 'get'], 'authenticate' => ['endpoint' => '/oauth/authenticate', 'method' => 'post'], 'timestamp' => ['endpoint' => '/posts/(?P<id>\\d+)/timestamp', 'method' => 'post'], 'timestamp.transaction.latest' => ['endpoint' => '/posts/(?P<id>\\d+)/timestamp/transaction/latest', 'method' => 'get'], 'webhook' => ['endpoint' => '/webhook', 'method' => 'get'], 'settings' => ['endpoint' => '/settings', 'method' => 'get'], 'saveSettings' => ['endpoint' => '/settings', 'method' => 'POST'], 'authentication' => ['endpoint' => '/authentication', 'method' => 'post'], 'authentication.destroy' => ['endpoint' => '/oauth/destroy', 'method' => 'post']];
    }
}
Config/.htaccess000066600000000424151143706510007556 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>Config/OptionsConfig.php000066600000001537151143706510011260 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Config;

class OptionsConfig extends \YoastSEO_Vendor\WordProof\SDK\Config\Config
{
    /**
     * Returns an array with the settings config.
     *
     * @return array
     */
    protected static function values()
    {
        return ['source_id' => ['escape' => 'integer', 'default' => null], 'access_token' => ['escape' => 'text_field', 'default' => null], 'balance' => ['escape' => 'integer', 'default' => 0], 'settings' => ['cast' => 'object', 'options' => ['certificate_link_text' => ['escape' => 'text_field', 'default' => __('View this content\'s Timestamp certificate', 'wordproof')], 'hide_certificate_link' => ['escape' => 'boolean', 'default' => \false], 'selected_post_types' => ['escape' => 'text_field', 'default' => []], 'show_revisions' => ['escape' => 'boolean', 'default' => \true]]]];
    }
}
Config/DefaultAppConfig.php000066600000001465151143706510011652 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Config;

class DefaultAppConfig implements \YoastSEO_Vendor\WordProof\SDK\Config\AppConfigInterface
{
    /**
     * @return string
     */
    public function getPartner()
    {
        return 'wordproof';
    }
    /**
     * @return string
     */
    public function getEnvironment()
    {
        return 'production';
    }
    /**
     * @return boolean
     */
    public function getLoadUikitFromCdn()
    {
        return \true;
    }
    /**
     * @return null
     */
    public function getOauthClient()
    {
        return null;
    }
    /**
     * @return null
     */
    public function getWordProofUrl()
    {
        return null;
    }
    /**
     * @return null
     */
    public function getScriptsFileOverwrite()
    {
        return null;
    }
}
Config/ScriptsConfig.php000066600000001651151143706510011251 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Config;

class ScriptsConfig extends \YoastSEO_Vendor\WordProof\SDK\Config\Config
{
    /**
     * Returns an array with the environment config.
     *
     * @return array
     */
    protected static function values()
    {
        return ['data' => ['dependencies' => ['wp-data', 'lodash', 'wp-api-fetch'], 'type' => 'js'], 'wordproof-block-editor' => ['dependencies' => ['wp-i18n', 'wp-element', 'wp-components', 'wp-editor', 'wp-edit-post', 'wp-data', 'lodash', 'wordproof-data'], 'type' => 'js'], 'wordproof-elementor-editor' => ['dependencies' => ['wp-i18n', 'wp-element', 'wp-components', 'wp-editor', 'wp-edit-post', 'wp-data', 'lodash', 'wordproof-data', 'elementor-common'], 'type' => 'js'], 'wordproof-classic-editor' => ['dependencies' => ['wp-i18n', 'wp-element', 'wp-components', 'wp-editor', 'wp-edit-post', 'wp-data', 'lodash', 'wordproof-data'], 'type' => 'js']];
    }
}
Config/AppConfigInterface.php000066600000001676151143706510012172 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Config;

interface AppConfigInterface
{
    /**
     * Your partner name.
     *
     * @default wordproof
     * @return string
     */
    public function getPartner();
    /**
     * The WordProof environment used. Either staging or production.
     *
     * @default production
     * @return string
     */
    public function getEnvironment();
    /**
     * The WordProof environment used. Either staging or production.
     *
     * @default true
     * @return boolean
     */
    public function getLoadUikitFromCdn();
    /**
     * Only used for local development.
     *
     * @return integer
     */
    public function getOauthClient();
    /**
     * Only used for local development.
     *
     * @return string
     */
    public function getWordProofUrl();
    /**
     * Only used for local development.
     *
     * @return string
     */
    public function getScriptsFileOverwrite();
}
Config/EnvironmentConfig.php000066600000000676151143706510012134 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Config;

class EnvironmentConfig extends \YoastSEO_Vendor\WordProof\SDK\Config\Config
{
    /**
     * Returns an array with the environment config.
     *
     * @return array
     */
    protected static function values()
    {
        return ['staging' => ['url' => 'https://staging.wordproof.com', 'client' => 78], 'production' => ['url' => 'https://my.wordproof.com', 'client' => 79]];
    }
}
Controllers/AuthenticationController.php000066600000003066151143706510014622 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Controllers;

use YoastSEO_Vendor\WordProof\SDK\Support\Authentication;
class AuthenticationController
{
    /**
     * Triggers the authentication flow.
     *
     * @param null $redirectUrl
     */
    public function authenticate($redirectUrl = null)
    {
        return \YoastSEO_Vendor\WordProof\SDK\Support\Authentication::authorize($redirectUrl);
    }
    /**
     * Adds admin page that redirects to the authentication flow.
     */
    public function addRedirectPage()
    {
        \add_submenu_page(null, 'WordProof Authenticate', 'WordProof Authenticate', 'publish_pages', 'wordproof-redirect-authenticate', [$this, 'redirectPageContent']);
    }
    /**
     * The content for the redirect page.
     */
    public function redirectPageContent()
    {
    }
    /**
     * Gets triggered by the 'load-admin_page_' hook of the redirect page
     */
    public function redirectOnLoad()
    {
        \do_action('wordproof_authenticate', \admin_url('admin.php?page=wordproof-close-after-redirect'));
    }
    /**
     * Adds self destruct admin page.
     */
    public function addSelfDestructPage()
    {
        \add_submenu_page(null, 'WordProof After Authenticate', 'WordProof After Authenticate', 'publish_pages', 'wordproof-close-after-redirect', [$this, 'closeOnLoadContent']);
    }
    /**
     * Adds a script to the loaded page to close on load.
     */
    public function closeOnLoadContent()
    {
        echo '<script type="text/javascript">';
        echo 'window.close();';
        echo '</script>';
    }
}
Controllers/.htaccess000066600000000424151143706510010657 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>Controllers/SettingsController.php000066600000002521151143706510013436 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Controllers;

use YoastSEO_Vendor\WordProof\SDK\Support\Settings;
class SettingsController
{
    /**
     * Redirects user to the settings page. Returns false if not authenticated.
     *
     * @param null|string $redirectUrl
     * @return false
     */
    public function redirect($redirectUrl = null)
    {
        return \YoastSEO_Vendor\WordProof\SDK\Support\Settings::redirect($redirectUrl);
    }
    /**
     * Adds admin page that will redirect the user to a predefined url.
     *
     * @action admin_menu
     */
    public function addRedirectPage()
    {
        \add_submenu_page(null, 'WordProof Settings', 'WordProof Settings', 'publish_pages', 'wordproof-redirect-settings', [$this, 'redirectPageContent']);
    }
    /**
     * The content for the redirect page. Triggered by addRedirectPage().
     */
    public function redirectPageContent()
    {
        return;
    }
    /**
     * Redirects user on admin page load to the settings page on the WordProof My.
     *
     * @action load-admin_page_settings
     */
    public function redirectOnLoad()
    {
        $closeWindowUrl = \admin_url('admin.php?page=wordproof-close-after-redirect');
        if ($this->redirect($closeWindowUrl) === \false) {
            \do_action('wordproof_authenticate', $closeWindowUrl);
        }
    }
}
Controllers/PostEditorTimestampController.php000066600000014037151143706510015623 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Controllers;

use YoastSEO_Vendor\WordProof\SDK\Helpers\AssetHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\PostEditorHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\PostTypeHelper;
class PostEditorTimestampController
{
    private $metaKey = '_wordproof_timestamp';
    private $classicEditorNonceKey = 'wordproof_timestamp_classic_nonce';
    /**
     * Registers post meta for all public post types.
     *
     * @action init
     */
    public function registerPostMeta()
    {
        foreach (\YoastSEO_Vendor\WordProof\SDK\Helpers\PostTypeHelper::getPublicPostTypes() as $postType) {
            register_post_meta($postType, $this->metaKey, ['show_in_rest' => \true, 'single' => \true, 'type' => 'boolean', 'default' => \false, 'supports' => ['editor', 'title', 'custom-fields'], 'auth_callback' => [$this, 'userCanEditPosts']]);
        }
    }
    /**
     * Returns if the current user can edit posts.
     *
     * @return boolean
     */
    public function userCanEditPosts()
    {
        return \current_user_can('edit_posts');
    }
    /**
     * Enqueues the wordproof-block-editor script.
     *
     * @action enqueue_block_editor_assets
     * @script wordproof-block-editor
     */
    public function enqueueBlockEditorScript()
    {
        \YoastSEO_Vendor\WordProof\SDK\Helpers\AssetHelper::enqueue('wordproof-block-editor');
    }
    /**
     * Enqueues the wordproof-elementor-editor script.
     *
     * @action elementor/editor/after_enqueue_scripts
     * @script wordproof-elementor-editor
     */
    public function enqueueElementorEditorScript()
    {
        \YoastSEO_Vendor\WordProof\SDK\Helpers\AssetHelper::enqueue('wordproof-elementor-editor');
    }
    /**
     * Enqueues the wordproof-classic-editor script.
     *
     * @action admin_enqueue_scripts
     * @script wordproof-classic-editor
     */
    public function enqueueClassicEditorScript($hook)
    {
        if (!\YoastSEO_Vendor\WordProof\SDK\Helpers\PostEditorHelper::isPostEdit($hook)) {
            return;
        }
        if (\YoastSEO_Vendor\WordProof\SDK\Helpers\PostEditorHelper::getPostEditor() === 'classic') {
            \YoastSEO_Vendor\WordProof\SDK\Helpers\AssetHelper::enqueue('wordproof-classic-editor');
        }
    }
    /**
     * Add Metabox to classic editor.
     *
     * @action add_meta_boxes
     */
    public function addMetaboxToClassicEditor()
    {
        foreach (\YoastSEO_Vendor\WordProof\SDK\Helpers\PostTypeHelper::getPublicPostTypes() as $postType) {
            \add_meta_box('wordproof_timestamp_metabox', 'WordProof Timestamp', [$this, 'classicMetaboxHtml'], $postType, 'side', 'default', ['__back_compat_meta_box' => \true]);
        }
    }
    /**
     * Save the meta box meta value for the classic editor.
     *
     * @param integer $postId The post ID.
     * @action save_post
     */
    public function saveClassicMetaboxPostMeta($postId)
    {
        if (\array_key_exists($this->classicEditorNonceKey, $_POST)) {
            if (\wp_verify_nonce(\sanitize_key($_POST[$this->classicEditorNonceKey]), 'save_post')) {
                \update_post_meta($postId, $this->metaKey, \array_key_exists($this->metaKey, $_POST));
            }
        }
    }
    /**
     * Display the meta box HTML to Classic Editor users.
     *
     * @param \WP_Post $post Post object.
     */
    public function classicMetaboxHtml($post)
    {
        $value = \YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper::get($post->ID, $this->metaKey);
        \wp_nonce_field('save_post', $this->classicEditorNonceKey);
        ?>
    
        <div id="wordproof-toggle">
            <input type="checkbox" id="<?php 
        echo \esc_attr($this->metaKey);
        ?>" name="<?php 
        echo \esc_attr($this->metaKey);
        ?>"
                   value="1" <?php 
        echo \boolval($value) ? 'checked' : '';
        ?>>
            <label for="<?php 
        echo \esc_attr($this->metaKey);
        ?>">Timestamp this post</label>
            <div id="wordproof-action-link"></div>
        </div>
        <?php 
    }
    /**
     * Registers control for the Elementor editor.
     *
     * @param \Elementor\Core\DocumentTypes\PageBase $document The PageBase document instance.
     *
     * @action elementor/documents/register_controls
     */
    public function registerControl($document)
    {
        if (!$document instanceof \Elementor\Core\DocumentTypes\PageBase || !$document::get_property('has_elements')) {
            return;
        }
        // Add Metabox
        $document->start_controls_section('wordproof_timestamp_section', ['label' => \esc_html__('WordProof Timestamp', 'wordproof'), 'tab' => \Elementor\Controls_Manager::TAB_SETTINGS]);
        // Get meta value
        $postId = $document->get_id();
        $metaValue = \YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper::get($postId, $this->metaKey, \true);
        // Override elementor value
        $pageSettingsManager = \Elementor\Core\Settings\Manager::get_settings_managers('page');
        $pageSettingsModel = $pageSettingsManager->get_model($postId);
        $pageSettingsModel->set_settings($this->metaKey, \boolval($metaValue) ? 'yes' : '');
        // Add Switcher
        $document->add_control($this->metaKey, ['label' => \esc_html__('Timestamp this post', 'wordproof'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'default' => 'no']);
        $document->end_controls_section();
    }
    /**
     * @param integer $postId
     * @action elementor/document/save/data
     */
    public function elementorSave($postId)
    {
        if (\get_post_type($postId) !== 'page') {
            return;
        }
        $pageSettingsManager = \Elementor\Core\Settings\Manager::get_settings_managers('page');
        $pageSettingsModel = $pageSettingsManager->get_model($postId);
        $value = $pageSettingsModel->get_settings($this->metaKey);
        // Update meta key with Elementor value.
        \YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper::update($postId, $this->metaKey, $value === 'yes');
    }
}
Controllers/RestApiController.php000066600000024741151143706510013215 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Controllers;

use YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\SchemaHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\SettingsHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\AuthenticationHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\StringHelper;
use YoastSEO_Vendor\WordProof\SDK\Support\Authentication;
class RestApiController
{
    /**
     * Registers the rest api endpoints.
     *
     * @action rest_api_init
     * @throws \Exception
     */
    public function init()
    {
        \register_rest_route(\YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::getNamespace(), \YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::endpoint('authenticate'), ['methods' => 'POST', 'callback' => [$this, 'authenticate'], 'permission_callback' => [$this, 'canPublishPermission']]);
        \register_rest_route(\YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::getNamespace(), \YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::endpoint('webhook'), ['methods' => 'POST', 'callback' => [$this, 'webhook'], 'permission_callback' => [$this, 'isValidWebhookRequest']]);
        \register_rest_route(\YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::getNamespace(), \YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::endpoint('hashInput'), ['methods' => 'GET', 'callback' => [$this, 'hashInput'], 'permission_callback' => function () {
            return \true;
        }]);
        \register_rest_route(\YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::getNamespace(), \YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::endpoint('timestamp'), ['methods' => 'POST', 'callback' => [$this, 'timestamp'], 'permission_callback' => [$this, 'canPublishPermission']]);
        \register_rest_route(\YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::getNamespace(), \YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::endpoint('timestamp.transaction.latest'), ['methods' => 'GET', 'callback' => [$this, 'showLatestTimestampTransaction'], 'permission_callback' => [$this, 'canPublishPermission']]);
        \register_rest_route(\YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::getNamespace(), \YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::endpoint('settings'), ['methods' => 'GET', 'callback' => [$this, 'settings'], 'permission_callback' => [$this, 'canPublishPermission']]);
        \register_rest_route(\YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::getNamespace(), \YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::endpoint('saveSettings'), ['methods' => 'POST', 'callback' => [$this, 'saveSettings'], 'permission_callback' => [$this, 'canPublishPermission']]);
        \register_rest_route(\YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::getNamespace(), \YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::endpoint('authentication'), ['methods' => 'GET', 'callback' => [$this, 'authentication'], 'permission_callback' => [$this, 'canPublishPermission']]);
        \register_rest_route(\YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::getNamespace(), \YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::endpoint('authentication.destroy'), ['methods' => 'POST', 'callback' => [$this, 'destroyAuthentication'], 'permission_callback' => [$this, 'canPublishPermission']]);
    }
    /**
     * Returns an object containing the settings.
     *
     * @return \WP_REST_Response Returns the settings.
     */
    public function settings()
    {
        $data = \YoastSEO_Vendor\WordProof\SDK\Helpers\SettingsHelper::get();
        $data->status = 200;
        return new \WP_REST_Response($data, $data->status);
    }
    /**
     * Save the settings.
     *
     * @return \WP_REST_Response Returns the settings.
     */
    public function saveSettings(\WP_REST_Request $request)
    {
        $data = $request->get_params();
        $settings = $data['settings'];
        $snakeCaseSettings = [];
        foreach ($settings as $key => $value) {
            $key = \YoastSEO_Vendor\WordProof\SDK\Helpers\StringHelper::toUnderscore($key);
            $snakeCaseSettings[$key] = $value;
        }
        \YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper::set('settings', $snakeCaseSettings);
        $data = (object) [];
        $data->status = 200;
        return new \WP_REST_Response($data, $data->status);
    }
    /**
     * Returns if the user is authenticated.
     *
     * @return \WP_REST_Response Returns if the user is authenticated.
     */
    public function authentication()
    {
        $data = (object) ['is_authenticated' => \YoastSEO_Vendor\WordProof\SDK\Helpers\AuthenticationHelper::isAuthenticated(), 'status' => 200];
        return new \WP_REST_Response($data, $data->status);
    }
    /**
     * Logout the user and return if the user is authenticated.
     *
     * @return \WP_REST_Response Returns if the user is authenticated.
     */
    public function destroyAuthentication()
    {
        \YoastSEO_Vendor\WordProof\SDK\Helpers\AuthenticationHelper::logout();
        return $this->authentication();
    }
    /**
     * Send a post request to WordProof to timestamp a post.
     *
     * @param \WP_REST_Request $request The Rest Request.
     * @return \WP_REST_Response
     */
    public function timestamp(\WP_REST_Request $request)
    {
        $data = $request->get_params();
        $postId = \intval($data['id']);
        return \YoastSEO_Vendor\WordProof\SDK\Controllers\TimestampController::timestamp($postId);
    }
    /**
     * The latest timestamp transaction is returned.
     *
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response
     */
    public function showLatestTimestampTransaction(\WP_REST_Request $request)
    {
        $data = $request->get_params();
        $postId = \intval($data['id']);
        $transactions = \YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper::get($postId, '_wordproof_blockchain_transaction', \false);
        $transaction = \array_pop($transactions);
        $response = new \WP_REST_Response((object) $transaction);
        $response->header('X-Robots-Tag', 'noindex');
        return $response;
    }
    /**
     * Returns the hash input of a post.
     *
     * @param \WP_REST_Request $request The Rest Request.
     * @return \WP_REST_Response The hash input of a post.
     */
    public function hashInput(\WP_REST_Request $request)
    {
        $data = $request->get_params();
        $postId = \intval($data['id']);
        $hash = \sanitize_text_field($data['hash']);
        $hashInput = \YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper::get($postId, '_wordproof_hash_input_' . $hash);
        $response = new \WP_REST_Response((object) $hashInput);
        $response->header('X-Robots-Tag', 'noindex');
        return $response;
    }
    /**
     * Retrieves the access token when the code and state are retrieved in the frontend.
     *
     * @throws \Exception
     */
    public function authenticate(\WP_REST_Request $request)
    {
        $state = \sanitize_text_field($request->get_param('state'));
        $code = \sanitize_text_field($request->get_param('code'));
        return \YoastSEO_Vendor\WordProof\SDK\Support\Authentication::token($state, $code);
    }
    /**
     * Handles webhooks sent by WordProof.
     *
     * @param \WP_REST_Request $request The Rest Request.
     * @return bool|null|\WP_REST_Response|void The value returned by the action undertaken.
     *
     * TODO: Improve
     */
    public function webhook(\WP_REST_Request $request)
    {
        $response = \json_decode($request->get_body());
        /**
         * Handle webhooks with type and data
         */
        if (isset($response->type) && isset($response->data)) {
            switch ($response->type) {
                case 'source_settings':
                    return \YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper::set('settings', $response->data);
                case 'ping':
                    $data = (object) ['status' => 200, 'source_id' => \YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper::sourceId()];
                    return new \WP_REST_Response($data, $data->status);
                case 'logout':
                    \YoastSEO_Vendor\WordProof\SDK\Helpers\AuthenticationHelper::logout();
                    break;
                case 'dump_item':
                    $key = '_wordproof_hash_input_' . $response->data->hash;
                    \YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper::update($response->data->uid, $key, \json_decode($response->data->hash_input));
                    $this->setBlockchainTransaction($response->data);
                    break;
                default:
                    break;
            }
        }
        /**
         * Handle timestamping webhooks without type
         */
        if (isset($response->uid) && isset($response->schema)) {
            $this->setBlockchainTransaction($response);
        }
    }
    /**
     * @param $response
     *
     * TODO: Improve
     */
    private function setBlockchainTransaction($response)
    {
        $postId = \intval($response->uid);
        $blockchainTransaction = \YoastSEO_Vendor\WordProof\SDK\Helpers\SchemaHelper::getBlockchainTransaction($response);
        \YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper::add($postId, '_wordproof_blockchain_transaction', $blockchainTransaction);
        $schema = \YoastSEO_Vendor\WordProof\SDK\Helpers\SchemaHelper::getSchema($postId);
        \YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper::update($postId, '_wordproof_schema', $schema);
    }
    /**
     * Checks if the user has permission to publish a post.
     *
     * @return bool Returns if a user has permission to publish.
     */
    public function canPublishPermission()
    {
        return \current_user_can('publish_posts') && \current_user_can('publish_pages');
    }
    /**
     * Validates if the webhook is valid and signed with the correct secret.
     *
     * @param \WP_REST_Request $request The Rest Request.
     * @return bool If the webhook can be accepted.
     */
    public static function isValidWebhookRequest(\WP_REST_Request $request)
    {
        if (!\YoastSEO_Vendor\WordProof\SDK\Helpers\AuthenticationHelper::isAuthenticated()) {
            return \false;
        }
        $hashedToken = \hash('sha256', \YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper::accessToken());
        $hmac = \hash_hmac('sha256', $request->get_body(), $hashedToken);
        return $request->get_header('signature') === $hmac;
    }
}
Controllers/TimestampController.php000066600000006606151143706510013611 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Controllers;

use YoastSEO_Vendor\WordProof\SDK\Helpers\ClassicNoticeHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\TimestampHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\TransientHelper;
class TimestampController
{
    /**
     * Timestamp an post triggered by custom action.
     *
     * @param integer $postId The post id to be timestamped.
     * @action wordproof_timestamp
     */
    public static function timestamp($postId)
    {
        $post = \get_post(\intval($postId));
        return \YoastSEO_Vendor\WordProof\SDK\Helpers\TimestampHelper::debounce($post);
    }
    /**
     * Timestamp new posts except those inserted by the API.
     *
     * @param integer $postId The post id to be timestamped.
     * @param \WP_Post $post The post to be timestamped.
     * @action wp_insert_post
     */
    public function timestampAfterPostRequest($postId, $post)
    {
        if (\defined('REST_REQUEST') && \REST_REQUEST) {
            return;
        }
        $response = \YoastSEO_Vendor\WordProof\SDK\Helpers\TimestampHelper::debounce($post);
        \YoastSEO_Vendor\WordProof\SDK\Helpers\ClassicNoticeHelper::addTimestampNotice($response);
        return $response;
    }
    /**
     * Timestamp new attachments.
     *
     * @param integer $postId The post id to be timestamped.
     *
     * @action add_attachment|edit_attachment
     */
    public function timestampAfterAttachmentRequest($postId)
    {
        $post = \get_post($postId);
        $this->timestampAfterPostRequest($postId, $post);
    }
    /**
     * Timestamp posts inserted by the API.
     *
     * @param \WP_Post $post The post to be timestamped.
     * @action rest_after_insert_post
     */
    public function timestampAfterRestApiRequest($post)
    {
        return \YoastSEO_Vendor\WordProof\SDK\Helpers\TimestampHelper::debounce($post);
    }
    /**
     * Removes action to timestamp post on insert if Elementor is used.
     */
    public function beforeElementorSave()
    {
        \remove_action('rest_after_insert_post', [$this, 'timestampAfterRestApiRequest']);
        \remove_action('wp_insert_post', [$this, 'timestampAfterPostRequest'], \PHP_INT_MAX);
    }
    /**
     * Syncs timestamp override post meta keys.
     *
     * @param $metaId
     * @param $postId
     * @param $metaKey
     * @param $metaValue
     */
    public function syncPostMetaTimestampOverrides($metaId, $postId, $metaKey, $metaValue)
    {
        $timestampablePostMetaKeys = \apply_filters('wordproof_timestamp_post_meta_key_overrides', ['_wordproof_timestamp']);
        if (\in_array($metaKey, $timestampablePostMetaKeys, \true) && \count($timestampablePostMetaKeys) > 1) {
            $arrayKey = \array_search($metaKey, $timestampablePostMetaKeys, \true);
            unset($timestampablePostMetaKeys[$arrayKey]);
            \YoastSEO_Vendor\WordProof\SDK\Helpers\TransientHelper::set('wordproof_debounce_post_meta_sync_' . $metaKey . '_' . $postId, \true, 5);
            foreach ($timestampablePostMetaKeys as $key) {
                \YoastSEO_Vendor\WordProof\SDK\Helpers\TransientHelper::debounce($postId, 'post_meta_sync_' . $key, function () use($postId, $key, $metaValue) {
                    return \YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper::update($postId, $key, $metaValue);
                });
            }
        }
    }
}
Controllers/CertificateController.php000066600000005623151143706510014066 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Controllers;

use YoastSEO_Vendor\WordProof\SDK\Helpers\AppConfigHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\CertificateHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\EnvironmentHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\SettingsHelper;
class CertificateController
{
    /**
     * Add scripts and schema to the head of the current page.
     *
     * @action wp_head
     */
    public function head()
    {
        if (!\YoastSEO_Vendor\WordProof\SDK\Helpers\CertificateHelper::show()) {
            return;
        }
        global $post;
        $schema = "\n";
        if (\YoastSEO_Vendor\WordProof\SDK\Helpers\AppConfigHelper::getLoadUikitFromCdn() === \true) {
            $schema .= '<script type="module" src="https://unpkg.com/@wordproof/uikit@1.0.*/dist/uikit/uikit.esm.js"></script>';
            $schema .= "\n";
            $schema .= '<script nomodule src="https://unpkg.com/@wordproof/uikit@1.0.*/dist/uikit/uikit.js"></script>';
            $schema .= "\n";
        }
        $schema .= '<script type="application/ld+json" class="' . \esc_attr('wordproof-schema-graph') . '">';
        $schema .= \json_encode(\YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper::get($post->ID, '_wordproof_schema'), \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
        $schema .= "</script>";
        $schema .= "\n";
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        echo $schema;
    }
    /**
     * Adds the certificate tag to the content before rendering it.
     *
     * @param $content
     * @return mixed|string Content string from 'the_content' filter
     * @filter the_content
     */
    public function certificateTag($content)
    {
        if (!\YoastSEO_Vendor\WordProof\SDK\Helpers\CertificateHelper::show()) {
            return $content;
        }
        if (\YoastSEO_Vendor\WordProof\SDK\Helpers\SettingsHelper::hideCertificateLink()) {
            return $content;
        }
        global $post;
        $identifier = $post->ID;
        $text = \YoastSEO_Vendor\WordProof\SDK\Helpers\SettingsHelper::certificateLinkText();
        $showRevisions = \YoastSEO_Vendor\WordProof\SDK\Helpers\SettingsHelper::showRevisions() ? 'true' : 'false';
        $debug = \YoastSEO_Vendor\WordProof\SDK\Helpers\EnvironmentHelper::development() ? 'true' : 'false';
        $lastModified = \get_the_modified_date('c', $post->ID);
        $content .= "\n" . '<w-certificate debug="' . $debug . '" shared-identifier="' . $identifier . '" render-without-button="true" show-revisions="' . $showRevisions . '" last-modified="' . $lastModified . '"></w-certificate>';
        $content .= "\n" . '<p><w-certificate-button shared-identifier="' . $identifier . '" icon="shield" shape="text" text="' . $text . '"></w-certificate-button></p>';
        $content .= "\n";
        return $content;
    }
}
Controllers/NoticeController.php000066600000005403151143706510013061 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Controllers;

use YoastSEO_Vendor\WordProof\SDK\Helpers\ClassicNoticeHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\TransientHelper;
use YoastSEO_Vendor\WordProof\SDK\Translations\TranslationsInterface;
class NoticeController
{
    /**
     * @var string[] The screens on which notices should be rendered.
     */
    private $screens = ['post'];
    /**
     * @var TranslationsInterface The translations objects,
     */
    private $translations;
    public function __construct(\YoastSEO_Vendor\WordProof\SDK\Translations\TranslationsInterface $translations)
    {
        $this->translations = $translations;
    }
    /**
     * Showing notices for the classic editor and delete them so they are only shown once.
     *
     * @action admin_notices
     */
    public function show()
    {
        $screen = \get_current_screen();
        if (!\in_array($screen->base, $this->screens, \true)) {
            return;
        }
        $notice = \YoastSEO_Vendor\WordProof\SDK\Helpers\TransientHelper::getOnce(\YoastSEO_Vendor\WordProof\SDK\Helpers\ClassicNoticeHelper::$transientKey);
        if (!isset($notice) || !$notice) {
            return;
        }
        switch ($notice) {
            case 'no_balance':
                $type = 'error';
                $message = $this->translations->getNoBalanceNotice();
                $buttonText = $this->translations->getOpenSettingsButtonText();
                $buttonEventName = 'wordproof:open_settings';
                break;
            case 'timestamp_success':
                $type = 'success';
                $message = $this->translations->getTimestampSuccessNotice();
                break;
            case 'timestamp_failed':
                $type = 'error';
                $message = $this->translations->getTimestampFailedNotice();
                break;
            case 'not_authenticated':
                $type = 'error';
                $message = $this->translations->getNotAuthenticatedNotice();
                $buttonText = $this->translations->getOpenAuthenticationButtonText();
                $buttonEventName = 'wordproof:open_authentication';
                break;
            default:
                break;
        }
        if (isset($message) && isset($type)) {
            $noticeClass = 'notice-' . $type;
            echo \sprintf('<div class="notice %1$s is-dismissible"><p>%2$s</p>', \esc_attr($noticeClass), \esc_html($message));
            if (isset($buttonText) && isset($buttonEventName)) {
                echo \sprintf('<p><button class="button button-primary" onclick="window.dispatchEvent( new window.CustomEvent( \'%2$s\' ) )">%1$s</button></p>', \esc_html($buttonText), \esc_attr($buttonEventName));
            }
            echo '</div>';
        }
    }
}
Controllers/PostEditorDataController.php000066600000003337151143706510014532 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Controllers;

use YoastSEO_Vendor\WordProof\SDK\Helpers\AssetHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\PostEditorHelper;
use YoastSEO_Vendor\WordProof\SDK\Translations\TranslationsInterface;
class PostEditorDataController
{
    /**
     * @var TranslationsInterface The translations objects,
     */
    private $translations;
    /**
     * PostEditorDataController constructor.
     *
     * @param TranslationsInterface $translations The implemented translations interface.
     */
    public function __construct(\YoastSEO_Vendor\WordProof\SDK\Translations\TranslationsInterface $translations)
    {
        $this->translations = $translations;
    }
    /**
     * Add script for post edit pages.
     *
     * @param string $hook The current page.
     */
    public function addScript($hook)
    {
        $loadWordProofData = \apply_filters('wordproof_load_data_on_pages', \YoastSEO_Vendor\WordProof\SDK\Helpers\PostEditorHelper::getPostEditPages());
        if (\in_array($hook, $loadWordProofData, \true)) {
            $this->enqueueAndLocalizeScript();
        }
    }
    /**
     * Localizes the elementor script.
     */
    public function addScriptForElementor()
    {
        $this->enqueueAndLocalizeScript();
    }
    /**
     * Enqueues and localizes data script.
     */
    private function enqueueAndLocalizeScript()
    {
        $data = \YoastSEO_Vendor\WordProof\SDK\Helpers\PostEditorHelper::getPostEditorData($this->translations);
        $data = \apply_filters('wordproof_data', $data);
        \YoastSEO_Vendor\WordProof\SDK\Helpers\AssetHelper::enqueue('data');
        \YoastSEO_Vendor\WordProof\SDK\Helpers\AssetHelper::localize('data', 'wordproofSdk', $data);
    }
}
Support/Template.php000066600000010405151143706510010513 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Support;

class Template
{
    private static $blocks = [];
    private static $cache_path = 'cache/';
    private static $template_path = 'templates/';
    private static $cache_enabled = \true;
    private static $store_cache = \false;
    public static function setOptions(array $options)
    {
        foreach ($options as $optionName => $optionValue) {
            if (\property_exists(__CLASS__, $optionName)) {
                self::${$optionName} = $optionValue;
            }
        }
    }
    public static function setCachePath($path)
    {
        self::$cache_path = $path;
    }
    public static function setTemplatePath($path)
    {
        self::$template_path = $path;
    }
    public static function render($file, $data = [])
    {
        \ob_start();
        self::view($file, $data);
        return \ob_get_contents();
    }
    public static function view($file, $data = [])
    {
        if (self::$store_cache) {
            $cached_file = self::cache($file);
            \extract($data, \EXTR_SKIP);
            require $cached_file;
        } else {
            $code = self::includeFiles($file);
            $code = self::compileCode($code);
            $code = '?>' . \PHP_EOL . $code . \PHP_EOL . "<?php";
            \extract($data, \EXTR_SKIP);
            eval($code);
        }
    }
    private static function cache($file)
    {
        if (!\file_exists(self::$cache_path)) {
            \mkdir(self::$cache_path, 0744);
        }
        $cached_file = self::$cache_path . \str_replace(['/', '.html'], ['_', ''], $file . '.php');
        if (!self::$cache_enabled || !\file_exists($cached_file) || \filemtime($cached_file) < \filemtime($file)) {
            $code = self::includeFiles($file);
            $code = self::compileCode($code);
            \file_put_contents($cached_file, '<?php class_exists(\'' . __CLASS__ . '\') or exit; ?>' . \PHP_EOL . $code);
        }
        return $cached_file;
    }
    public static function clearCache()
    {
        foreach (\glob(self::$cache_path . '*') as $file) {
            \unlink($file);
        }
    }
    private static function compileCode($code)
    {
        $code = self::compileBlock($code);
        $code = self::compileYield($code);
        $code = self::compileEscapedEchos($code);
        $code = self::compileEchos($code);
        $code = self::compilePHP($code);
        return $code;
    }
    private static function includeFiles($file)
    {
        $code = \file_get_contents(self::$template_path . $file);
        \preg_match_all('/{% ?(extends|include) ?\'?(.*?)\'? ?%}/i', $code, $matches, \PREG_SET_ORDER);
        foreach ($matches as $value) {
            $code = \str_replace($value[0], self::includeFiles($value[2]), $code);
        }
        $code = \preg_replace('/{% ?(extends|include) ?\'?(.*?)\'? ?%}/i', '', $code);
        return $code;
    }
    private static function compilePHP($code)
    {
        return \preg_replace('~{%\\s*(.+?)\\s*%}~is', '<?php $1 ?>', $code);
    }
    private static function compileEchos($code)
    {
        return \preg_replace('~{{\\s*(.+?)\\s*}}~is', '<?php echo $1 ?>', $code);
    }
    private static function compileEscapedEchos($code)
    {
        return \preg_replace('~{{{\\s*(.+?)\\s*}}}~is', '<?php echo htmlentities($1, ENT_QUOTES, \'UTF-8\') ?>', $code);
    }
    private static function compileBlock($code)
    {
        \preg_match_all('/{% ?block ?(.*?) ?%}(.*?){% ?endblock ?%}/is', $code, $matches, \PREG_SET_ORDER);
        foreach ($matches as $value) {
            if (!\array_key_exists($value[1], self::$blocks)) {
                self::$blocks[$value[1]] = '';
            }
            if (\strpos($value[2], '@parent') === \false) {
                self::$blocks[$value[1]] = $value[2];
            } else {
                self::$blocks[$value[1]] = \str_replace('@parent', self::$blocks[$value[1]], $value[2]);
            }
            $code = \str_replace($value[0], '', $code);
        }
        return $code;
    }
    private static function compileYield($code)
    {
        foreach (self::$blocks as $block => $value) {
            $code = \preg_replace('/{% ?yield ?' . $block . ' ?%}/', $value, $code);
        }
        $code = \preg_replace('/{% ?yield ?(.*?) ?%}/i', '', $code);
        return $code;
    }
}
Support/.htaccess000066600000000424151143706510010025 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>Support/Loader.php000066600000004502151143706510010147 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Support;

class Loader
{
    protected $actions;
    protected $filters;
    public function __construct()
    {
        $this->actions = [];
        $this->filters = [];
    }
    /**
     * @param string $hook The name of the WordPress action that is being registered.
     * @param object $component A reference to the instance of the object on which the action is defined.
     * @param string $callback The name of the function definition on the $component.
     * @param int $priority Optional. The priority at which the function should be fired. Default is 10.
     * @param int $accepted_args Optional. The number of arguments that should be passed to the $callback. Default is 1.
     */
    public function addAction($hook, $component, $callback, $priority = 10, $accepted_args = 1)
    {
        $this->actions = $this->add($this->actions, $hook, $component, $callback, $priority, $accepted_args);
    }
    /**
     * @param string $hook The name of the WordPress filter that is being registered.
     * @param object $component A reference to the instance of the object on which the filter is defined.
     * @param string $callback The name of the function definition on the $component.
     * @param int $priority Optional. The priority at which the function should be fired. Default is 10.
     * @param int $accepted_args Optional. The number of arguments that should be passed to the $callback. Default is 1
     */
    public function addFilter($hook, $component, $callback, $priority = 10, $accepted_args = 1)
    {
        $this->filters = $this->add($this->filters, $hook, $component, $callback, $priority, $accepted_args);
    }
    public function run()
    {
        foreach ($this->filters as $hook) {
            \add_filter($hook['hook'], [$hook['component'], $hook['callback']], $hook['priority'], $hook['accepted_args']);
        }
        foreach ($this->actions as $hook) {
            \add_action($hook['hook'], [$hook['component'], $hook['callback']], $hook['priority'], $hook['accepted_args']);
        }
    }
    private function add($hooks, $hook, $component, $callback, $priority, $accepted_args)
    {
        $hooks[] = ['hook' => $hook, 'component' => $component, 'callback' => $callback, 'priority' => $priority, 'accepted_args' => $accepted_args];
        return $hooks;
    }
}
Support/Timestamp.php000066600000002241151143706510010702 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Support;

use YoastSEO_Vendor\WordProof\SDK\Helpers\AuthenticationHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper;
class Timestamp
{
    /**
     * @param array $data
     *
     * @return mixed
     */
    public static function sendPostRequest($data)
    {
        $sourceId = \YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper::sourceId();
        $endpoint = '/api/sources/' . $sourceId . '/timestamps';
        $response = \YoastSEO_Vendor\WordProof\SDK\Support\Api::post($endpoint, $data);
        if (!$response || !isset($response->hash)) {
            //            AuthenticationHelper::logout(); // TODO Only if response is unauthenticated
            return \false;
        }
        if (isset($response->balance)) {
            \YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper::set('balance', $response->balance);
        }
        $key = '_wordproof_hash_input_' . $response->hash;
        \YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper::update($data['uid'], $key, \json_decode($response->hash_input));
        return $response;
    }
}
Support/Api.php000066600000002405151143706510007452 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Support;

use YoastSEO_Vendor\WordProof\SDK\Helpers\EnvironmentHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper;
class Api
{
    /**
     * @param string $endpoint
     * @param array $body
     * @return mixed
     */
    public static function post($endpoint, $body = [])
    {
        $location = \YoastSEO_Vendor\WordProof\SDK\Helpers\EnvironmentHelper::url() . $endpoint;
        $body = \wp_json_encode($body);
        $accessToken = \YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper::accessToken();
        $headers = ['Content-Type' => 'application/json', 'Accept' => 'application/json'];
        $headers = $accessToken ? \array_merge($headers, ['Authorization' => 'Bearer ' . $accessToken]) : $headers;
        $options = ['body' => $body, 'headers' => $headers, 'timeout' => 60, 'redirection' => 5, 'blocking' => \true, 'data_format' => 'body', 'sslverify' => \YoastSEO_Vendor\WordProof\SDK\Helpers\EnvironmentHelper::sslVerify()];
        $request = \wp_remote_post($location, $options);
        $status = \wp_remote_retrieve_response_code($request);
        if ($status < 200 || $status >= 300) {
            return \false;
        }
        return \json_decode(\wp_remote_retrieve_body($request));
    }
}
Support/Authentication.php000066600000012440151143706510011720 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Support;

use YoastSEO_Vendor\WordProof\SDK\Helpers\AdminHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\AuthenticationHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\EnvironmentHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\PostTypeHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\SettingsHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\TransientHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\AppConfigHelper;
class Authentication
{
    private static $callbackEndpoint = 'wordproof/v1/oauth/callback';
    public static function authorize($redirectUrl = null)
    {
        $state = \wp_generate_password(40, \false);
        $codeVerifier = \wp_generate_password(128, \false);
        $originalUrl = \YoastSEO_Vendor\WordProof\SDK\Helpers\AdminHelper::currentUrl();
        \YoastSEO_Vendor\WordProof\SDK\Helpers\TransientHelper::set('wordproof_authorize_state', $state, 1200);
        \YoastSEO_Vendor\WordProof\SDK\Helpers\TransientHelper::set('wordproof_authorize_code_verifier', $codeVerifier, 1200);
        \YoastSEO_Vendor\WordProof\SDK\Helpers\TransientHelper::set('wordproof_authorize_current_url', $redirectUrl ?: $originalUrl);
        $encoded = \base64_encode(\hash('sha256', $codeVerifier, \true));
        $codeChallenge = \strtr(\rtrim($encoded, '='), '+/', '-_');
        $data = ['client_id' => \YoastSEO_Vendor\WordProof\SDK\Helpers\EnvironmentHelper::client(), 'redirect_uri' => self::getCallbackUrl(), 'response_type' => 'code', 'scope' => '', 'state' => $state, 'code_challenge' => $codeChallenge, 'code_challenge_method' => 'S256', 'partner' => \YoastSEO_Vendor\WordProof\SDK\Helpers\AppConfigHelper::getPartner()];
        /**
         * Login with user if v2 plugin data exist.
         */
        $accessToken = \YoastSEO_Vendor\WordProof\SDK\Helpers\TransientHelper::get('wordproof_v2_authenticate_with_token');
        if ($accessToken) {
            $data = \array_merge($data, ['access_token_login' => $accessToken]);
        } else {
            $data = \array_merge($data, ['confirm_account' => \true]);
        }
        self::redirect('/wordpress-sdk/authorize', $data);
    }
    /**
     * Retrieve the access token with the state and code.
     *
     * @param string $state The state from remote
     * @param string $code The code from remote
     * @return \WP_REST_Response
     * @throws \Exception
     */
    public static function token($state, $code)
    {
        $localState = \YoastSEO_Vendor\WordProof\SDK\Helpers\TransientHelper::getOnce('wordproof_authorize_state');
        $codeVerifier = \YoastSEO_Vendor\WordProof\SDK\Helpers\TransientHelper::getOnce('wordproof_authorize_code_verifier');
        if (\strlen($localState) <= 0 || $localState !== $state) {
            throw new \Exception('WordProof: No state found.');
        }
        $data = ['grant_type' => 'authorization_code', 'client_id' => \YoastSEO_Vendor\WordProof\SDK\Helpers\EnvironmentHelper::client(), 'redirect_uri' => self::getCallbackUrl(), 'code_verifier' => $codeVerifier, 'code' => $code];
        $response = \YoastSEO_Vendor\WordProof\SDK\Support\Api::post('/api/wordpress-sdk/token', $data);
        if (isset($response->error) && $response->error === 'invalid_grant') {
            $data = (object) ['status' => 401, 'message' => 'invalid_grant'];
            return new \WP_REST_Response($data, $data->status);
        }
        if (!isset($response->access_token)) {
            $data = (object) ['status' => 401, 'message' => 'no_access_token'];
            return new \WP_REST_Response($data, $data->status);
        }
        \YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper::setAccessToken($response->access_token);
        $data = ['webhook_url' => \get_rest_url(null, 'wordproof/v1/webhook'), 'url' => \get_site_url(), 'available_post_types' => \YoastSEO_Vendor\WordProof\SDK\Helpers\PostTypeHelper::getPublicPostTypes(), 'partner' => \YoastSEO_Vendor\WordProof\SDK\Helpers\AppConfigHelper::getPartner(), 'local_settings' => (array) \YoastSEO_Vendor\WordProof\SDK\Helpers\SettingsHelper::get()];
        /**
         * Use existing source if user was authenticated in v2 of the plugin.
         */
        $sourceId = \YoastSEO_Vendor\WordProof\SDK\Helpers\TransientHelper::getOnce('wordproof_v2_get_existing_source');
        if ($sourceId) {
            $data = \array_merge($data, ['source_id' => \intval($sourceId)]);
        }
        $response = \YoastSEO_Vendor\WordProof\SDK\Support\Api::post('/api/wordpress-sdk/source', $data);
        \YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper::setSourceId($response->source_id);
        $data = (object) ['status' => 200, 'message' => 'authentication_success', 'source_id' => \YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper::get('source_id'), 'is_authenticated' => \YoastSEO_Vendor\WordProof\SDK\Helpers\AuthenticationHelper::isAuthenticated()];
        return new \WP_REST_Response($data, $data->status);
    }
    private static function getCallbackUrl()
    {
        return \get_rest_url(null, self::$callbackEndpoint);
    }
    public static function redirect($endpoint, $parameters)
    {
        $location = \YoastSEO_Vendor\WordProof\SDK\Helpers\EnvironmentHelper::url() . $endpoint . '?' . \http_build_query($parameters);
        \header("Location: " . $location);
    }
}
Support/Settings.php000066600000002132151143706510010536 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Support;

use YoastSEO_Vendor\WordProof\SDK\Helpers\AuthenticationHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\AppConfigHelper;
class Settings
{
    public static function redirect($redirectUrl = null)
    {
        if (!\YoastSEO_Vendor\WordProof\SDK\Helpers\AuthenticationHelper::isAuthenticated()) {
            return \false;
        }
        $options = \YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper::all();
        if (!$options->source_id) {
            return \false;
        }
        $endpoint = "/sources/" . $options->source_id . "/settings";
        if (\YoastSEO_Vendor\WordProof\SDK\Helpers\AppConfigHelper::getPartner() === 'yoast') {
            $endpoint = '/yoast/dashboard';
        }
        \YoastSEO_Vendor\WordProof\SDK\Support\Authentication::redirect($endpoint, ['redirect_uri' => $redirectUrl, 'partner' => \YoastSEO_Vendor\WordProof\SDK\Helpers\AppConfigHelper::getPartner(), 'source_id' => $options->source_id, 'access_token_login' => $options->access_token]);
    }
}
Helpers/RedirectHelper.php000066600000000512151143706510011565 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

class RedirectHelper
{
    /**
     * Does a safe redirect to an admin page.
     *
     * @param string $url The url to be redirected to.
     */
    public static function safe($url)
    {
        nocache_headers();
        \wp_safe_redirect($url);
        exit;
    }
}
Helpers/ReflectionHelper.php000066600000001000151143706510012107 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

use YoastSEO_Vendor\WordProof\SDK\WordPressSDK;
class ReflectionHelper
{
    /**
     * @param class $instance The class from which to get the name.
     * @return false|string
     */
    public static function name($instance)
    {
        if ($instance instanceof \YoastSEO_Vendor\WordProof\SDK\WordPressSDK) {
            $reflector = new \ReflectionClass($instance);
            return $reflector->getName();
        }
        return \false;
    }
}
Helpers/EscapeHelper.php000066600000004056151143706510011233 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

class EscapeHelper
{
    /**
     * Returns the value escaped according to the escape function set in the class.
     *
     * @param mixed $value The value to be sanitized.
     * @param string $escapeKey The escape function to be used.
     *
     * @return array|bool|int|string
     */
    public static function escape($value, $escapeKey)
    {
        if (\is_array($value)) {
            return self::escapeArray($value, $escapeKey);
        }
        if (\is_object($value)) {
            return (object) self::escapeArray((array) $value, $escapeKey);
        }
        return self::escapeSingleValue($value, $escapeKey);
    }
    /**
     * Loops through the array to escape the values inside.
     *
     * @param array $array The array with values to be escaped.
     * @param string $escapeKey The escape function to be used.
     * @return array Array with escapes values.
     */
    private static function escapeArray($array, $escapeKey)
    {
        $values = [];
        foreach ($array as $key => $value) {
            $values[$key] = self::escapeSingleValue($value, $escapeKey);
        }
        return $values;
    }
    /**
     * Escapes a single value using an escape function set in the class.
     *
     * @param string $value The value to be escaped.
     * @param string $escapeKey The escape function to be used.
     * @return bool|int|string The escaped value.
     */
    private static function escapeSingleValue($value, $escapeKey)
    {
        switch ($escapeKey) {
            case 'integer':
                return \intval($value);
            case 'boolean':
                return \boolval($value);
            case 'html_class':
                return \esc_html_class($value);
            case 'email':
                return \esc_email($value);
            case 'url':
                return \esc_url_raw($value);
            case 'key':
                return \esc_key($value);
            case 'text_field':
            default:
                return \esc_html($value);
        }
    }
}
Helpers/TimestampHelper.php000066600000006611151143706510011775 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

use YoastSEO_Vendor\WordProof\SDK\DataTransferObjects\TimestampData;
use YoastSEO_Vendor\WordProof\SDK\Support\Timestamp;
class TimestampHelper
{
    public static function debounce(\WP_Post $post)
    {
        $key = 'wordproof_timestamped_debounce_' . $post->id;
        $data = \YoastSEO_Vendor\WordProof\SDK\DataTransferObjects\TimestampData::fromPost($post);
        $transient = \YoastSEO_Vendor\WordProof\SDK\Helpers\TransientHelper::get($key);
        if ($transient) {
            return new \WP_REST_Response($transient, $transient->status);
        }
        $response = self::shouldBeTimestamped($post, $data);
        if (\is_bool($response) && $response === \false) {
            $response = (object) ['status' => 200, 'message' => 'Post should not be timestamped'];
            return new \WP_REST_Response($response, $response->status);
        }
        if (\is_array($response) && $response['timestamp'] === \false) {
            $response = (object) ['status' => 400, 'message' => 'Post should not be timestamped', 'error' => 'not_authenticated'];
            return new \WP_REST_Response($response, $response->status);
        }
        $response = \YoastSEO_Vendor\WordProof\SDK\Support\Timestamp::sendPostRequest($data);
        if ($response === \false) {
            $response = (object) ['status' => 400, 'message' => 'Something went wrong.', 'error' => 'timestamp_failed'];
            return new \WP_REST_Response($response, $response->status);
        }
        $response->status = 201;
        \YoastSEO_Vendor\WordProof\SDK\Helpers\TransientHelper::set($key, $response, 5);
        return new \WP_REST_Response($response, $response->status);
    }
    public static function shouldBeTimestamped(\WP_Post $post, $data)
    {
        if (!\YoastSEO_Vendor\WordProof\SDK\Helpers\AuthenticationHelper::isAuthenticated()) {
            if (self::hasPostMetaOverrideSetToTrue($post)) {
                return ['timestamp' => \false, 'notice' => 'not_authenticated'];
            }
            return \false;
        }
        if ($post->post_type !== 'attachment' && $post->post_content === '') {
            return \false;
        }
        if ($post->post_type === 'attachment' && \get_attached_file($post->ID) === \false) {
            return \false;
        }
        if (!\in_array($post->post_status, ['publish', 'inherit'], \true)) {
            return \false;
        }
        if (\YoastSEO_Vendor\WordProof\SDK\Helpers\SettingsHelper::postTypeIsInSelectedPostTypes($post->post_type)) {
            return \true;
        }
        if (self::hasPostMetaOverrideSetToTrue($post)) {
            return \true;
        }
        return \false;
    }
    private static function hasPostMetaOverrideSetToTrue(\WP_Post $post)
    {
        $timestampablePostMetaKeys = \apply_filters('wordproof_timestamp_post_meta_key_overrides', ['_wordproof_timestamp']);
        //Do not use PostMeta helper
        $meta = \get_post_meta($post->ID);
        foreach ($timestampablePostMetaKeys as $key) {
            if (!isset($meta[$key])) {
                continue;
            }
            if (\is_array($meta[$key])) {
                $value = \boolval($meta[$key][0]);
            } else {
                $value = \boolval($meta[$key]);
            }
            if (!$value) {
                continue;
            }
            return \true;
        }
        return \false;
    }
}
Helpers/TransientHelper.php000066600000002711151143706510011776 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

use YoastSEO_Vendor\WordProof\SDK\DataTransferObjects\TimestampData;
class TransientHelper
{
    /**
     * Set transient.
     *
     * @param $key
     * @param $value
     * @param int $expiration
     * @return bool
     */
    public static function set($key, $value, $expiration = 0)
    {
        return \set_transient($key, $value, $expiration);
    }
    /**
     * Returns and deletes site transient by key.
     *
     * @param $key
     * @return mixed
     */
    public static function getOnce($key)
    {
        $value = \get_transient($key);
        \delete_transient($key);
        return $value;
    }
    /**
     * Returns the transient by key.
     *
     * @param $key
     * @return mixed
     */
    public static function get($key)
    {
        return \get_transient($key);
    }
    /**
     * Debounce callback for post id.
     *
     * @param $postId
     * @param $action
     * @param $callback
     * @return mixed
     */
    public static function debounce($postId, $action, $callback)
    {
        $key = 'wordproof_debounce_' . $action . '_' . $postId;
        $transient = \YoastSEO_Vendor\WordProof\SDK\Helpers\TransientHelper::get($key);
        if ($transient) {
            return $transient;
        } else {
            \YoastSEO_Vendor\WordProof\SDK\Helpers\TransientHelper::set($key, \true, 4);
            $result = $callback();
            return $result;
        }
    }
}
Helpers/StringHelper.php000066600000001370151143706510011275 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

class StringHelper
{
    /**
     * Replace the last occurrence.
     *
     * @param string $search
     * @param string $replace
     * @param string $subject
     * @return string
     */
    public static function lastReplace($search, $replace, $subject)
    {
        $pos = \strrpos($subject, $search);
        if ($pos !== \false) {
            $subject = \substr_replace($subject, $replace, $pos, \strlen($search));
        }
        return $subject;
    }
    /**
     * PascalCase to snake_case
     *
     * @param $string
     * @return string
     */
    public static function toUnderscore($string)
    {
        return \strtolower(\preg_replace('/(?<!^)[A-Z]/', '_$0', $string));
    }
}
Helpers/SchemaHelper.php000066600000003351151143706510011230 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

class SchemaHelper
{
    /**
     * Builds an blockchain transaction schema object as array.
     *
     * @param object $response The response by WordProof.
     * @return array The blockchain transaction in the correct schema format.
     */
    public static function getBlockchainTransaction($response)
    {
        $postId = $response->uid;
        $hashLink = \YoastSEO_Vendor\WordProof\SDK\Helpers\RestApiHelper::getRestRoute('hashInput', [$postId, $response->hash]);
        $identifier = null;
        if (isset($response->transaction)) {
            $transaction = $response->transaction;
            if (isset($transaction->transactionId)) {
                $identifier = $transaction->transactionId;
            }
            if (isset($transaction->tx)) {
                $identifier = $transaction->tx;
            }
        }
        return ['@type' => 'BlockchainTransaction', 'identifier' => $identifier, 'hash' => $response->hash, 'hashLink' => $hashLink, 'recordedIn' => ['@type' => 'Blockchain', 'name' => $response->transaction->blockchain]];
    }
    /**
     * Retrieves the schema as array for a post.
     *
     * @param integer $postId The post id for which the schema should be returned.
     * @return array The schema as array.
     */
    public static function getSchema($postId)
    {
        $transactions = \YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper::get($postId, '_wordproof_blockchain_transaction', \false);
        $latest = \array_pop($transactions);
        if (\count($transactions) === 0) {
            return ['timestamp' => $latest];
        }
        return ['timestamp' => \array_merge($latest, ['revisions' => \array_reverse($transactions)])];
    }
}
Helpers/AppConfigHelper.php000066600000002362151143706510011677 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

use YoastSEO_Vendor\WordProof\SDK\WordPressSDK;
class AppConfigHelper
{
    /**
     * Returns the partner set during initialization.
     *
     * @return string|null
     */
    public static function getPartner()
    {
        $appConfig = self::getAppConfig();
        if ($appConfig) {
            return $appConfig->getPartner();
        }
        return null;
    }
    /**
     * Returns the environment set during initialization.
     * @return string|null
     */
    public static function getEnvironment()
    {
        $appConfig = self::getAppConfig();
        if ($appConfig) {
            return $appConfig->getEnvironment();
        }
        return null;
    }
    /**
     * Returns the environment set during initialization.
     * @return boolean
     */
    public static function getLoadUikitFromCdn()
    {
        $appConfig = self::getAppConfig();
        if ($appConfig) {
            return $appConfig->getLoadUikitFromCdn();
        }
        return null;
    }
    public static function getAppConfig()
    {
        $sdk = \YoastSEO_Vendor\WordProof\SDK\WordPressSDK::getInstance();
        if ($sdk) {
            return $sdk->appConfig;
        }
        return null;
    }
}
Helpers/AuthenticationHelper.php000066600000001166151143706510013011 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

class AuthenticationHelper
{
    /**
     * Removes all the options set by WordProof.
     *
     * @return void
     */
    public static function logout()
    {
        \YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper::resetAuthentication();
    }
    /**
     * Returns if the user is authenticated.
     *
     * @return bool If the user is authenticated.
     */
    public static function isAuthenticated()
    {
        $options = \YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper::all();
        return $options->access_token && $options->source_id;
    }
}
Helpers/AdminHelper.php000066600000000760151143706510011061 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

class AdminHelper
{
    /**
     * Returns the current admin url of the user.
     *
     * @return null|string The current admin url of the logged in user.
     */
    public static function currentUrl()
    {
        if (isset($_SERVER['REQUEST_URI'])) {
            $requestUri = \esc_url_raw(\wp_unslash($_SERVER['REQUEST_URI']));
            return \admin_url(\sprintf(\basename($requestUri)));
        }
        return null;
    }
}
Helpers/PostEditorHelper.php000066600000007720151143706510012130 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

use YoastSEO_Vendor\WordProof\SDK\Translations\TranslationsInterface;
class PostEditorHelper
{
    /**
     * Returns the post editor that is in use.
     *
     * @return bool The post editor the user is using..
     */
    public static function getPostEditor()
    {
        if (!\function_exists('YoastSEO_Vendor\\get_current_screen')) {
            return null;
        }
        $screen = \get_current_screen();
        if (!self::isPostEdit($screen->base)) {
            return null;
        }
        // Start with Elementor, otherwise the block editor will be returned.
        $action = \filter_input(\INPUT_GET, 'action', \FILTER_SANITIZE_STRING);
        if ($action === 'elementor') {
            return 'elementor';
        }
        if (\method_exists($screen, 'is_block_editor') && $screen->is_block_editor()) {
            return 'block';
        }
        return 'classic';
    }
    /**
     * Returns if the page is a post edit page.
     *
     * @param string $page The page to check.
     * @return bool If the current page is a post edit page.
     */
    public static function isPostEdit($page)
    {
        return \in_array($page, self::getPostEditPages(), \true);
    }
    /**
     * Returns an array of edit page hooks.
     *
     * @return array Post edit page hooks.
     */
    public static function getPostEditPages()
    {
        return ['post.php', 'post', 'post-new.php', 'post-new'];
    }
    /**
     * Returns the data that should be added to the post editor.
     *
     * @param TranslationsInterface $translations The implemented translations interface.
     *
     * @return array[] The post editor data.
     */
    public static function getPostEditorData(\YoastSEO_Vendor\WordProof\SDK\Translations\TranslationsInterface $translations)
    {
        global $post;
        $postId = isset($post->ID) ? $post->ID : null;
        $postType = isset($post->post_type) ? $post->post_type : null;
        $translations = ['no_balance' => $translations->getNoBalanceNotice(), 'timestamp_success' => $translations->getTimestampSuccessNotice(), 'timestamp_failed' => $translations->getTimestampFailedNotice(), 'webhook_failed' => $translations->getWebhookFailedNotice(), 'not_authenticated' => $translations->getNotAuthenticatedNotice(), 'open_authentication_button_text' => $translations->getOpenAuthenticationButtonText(), 'open_settings_button_text' => $translations->getOpenSettingsButtonText(), 'contact_wordproof_support_button_text' => $translations->getContactWordProofSupportButtonText()];
        return ['data' => ['origin' => \YoastSEO_Vendor\WordProof\SDK\Helpers\EnvironmentHelper::url(), 'is_authenticated' => \YoastSEO_Vendor\WordProof\SDK\Helpers\AuthenticationHelper::isAuthenticated(), 'popup_redirect_authentication_url' => \admin_url('admin.php?page=wordproof-redirect-authenticate'), 'popup_redirect_settings_url' => \admin_url('admin.php?page=wordproof-redirect-settings'), 'settings' => \YoastSEO_Vendor\WordProof\SDK\Helpers\SettingsHelper::get(), 'current_post_id' => $postId, 'current_post_type' => $postType, 'post_editor' => self::getPostEditor(), 'translations' => $translations, 'balance' => \YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper::get('balance')]];
    }
    /**
     * Returns the current post type.
     *
     * @return null|string The current post type.
     */
    public static function getCurrentPostType()
    {
        global $post, $typenow, $current_screen;
        if ($post && $post->post_type) {
            return $post->post_type;
        }
        if ($typenow) {
            return $typenow;
        }
        if ($current_screen && $current_screen->post_type) {
            return $current_screen->post_type;
        }
        // phpcs:disable WordPress.Security.NonceVerification
        if (isset($_REQUEST['post_type'])) {
            return \sanitize_key($_REQUEST['post_type']);
        }
        // phpcs:enable WordPress.Security.NonceVerification
        return null;
    }
}
Helpers/AssetHelper.php000066600000005270151143706510011111 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

use YoastSEO_Vendor\WordProof\SDK\Config\ScriptsConfig;
class AssetHelper
{
    private static $prefix = 'wordproof-';
    private static $filePath = 'app/';
    private static $buildPath = 'build/';
    /**
     * Localizes script by name.
     *
     * @param string $name Name of the script
     * @param string $objectName The name of the object in Javascript.
     * @param array $data The data to be included.
     * @return bool|void
     */
    public static function localize($name, $objectName, $data)
    {
        $config = \YoastSEO_Vendor\WordProof\SDK\Config\ScriptsConfig::get($name);
        if (!isset($config)) {
            return;
        }
        return \wp_localize_script(self::getHandle($name), $objectName, $data);
    }
    /**
     * Enqueues a script defined in the scripts config.
     *
     * @param string $name The name of the script to enqueue.
     * @return false|mixed|void
     */
    public static function enqueue($name)
    {
        $config = \YoastSEO_Vendor\WordProof\SDK\Config\ScriptsConfig::get($name);
        if (!isset($config)) {
            return;
        }
        $path = self::getPathUrl($name, $config['type']);
        if ($config['type'] === 'css') {
            \wp_enqueue_style(self::getHandle($name), $path, $config['dependencies'], self::getVersion());
        } else {
            \wp_enqueue_script(self::getHandle($name), $path, $config['dependencies'], self::getVersion(), \false);
        }
    }
    /**
     * Returns the prefixed script handle.
     *
     * @param string $name The name of the script.
     * @return string Handle of the script.
     */
    private static function getHandle($name)
    {
        return self::$prefix . $name;
    }
    /**
     * Get path url of the script.
     *
     * @param string $name The name of the script.
     * @return string The url of the script.
     */
    private static function getPathUrl($name, $extension)
    {
        $appConfig = \YoastSEO_Vendor\WordProof\SDK\Helpers\AppConfigHelper::getAppConfig();
        if ($appConfig->getScriptsFileOverwrite()) {
            $url = $appConfig->getScriptsFileOverwrite();
        } else {
            $url = \plugin_dir_url(WORDPROOF_TIMESTAMP_SDK_FILE);
        }
        $base = \YoastSEO_Vendor\WordProof\SDK\Helpers\StringHelper::lastReplace(self::$filePath, self::$buildPath, $url);
        return $base . $name . '.' . $extension;
    }
    /**
     * Returns version for file.
     *
     * @return false|string
     */
    private static function getVersion()
    {
        return \YoastSEO_Vendor\WordProof\SDK\Helpers\EnvironmentHelper::development() ? \false : WORDPROOF_TIMESTAMP_SDK_VERSION;
    }
}
Helpers/CertificateHelper.php000066600000001260151143706510012247 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

class CertificateHelper
{
    /**
     * Returns if the certificate should be displayed for this page.
     *
     * @return false If the certificate should be shown.
     */
    public static function show()
    {
        if (!\is_singular()) {
            return \false;
        }
        if (!\is_main_query()) {
            return \false;
        }
        if (\post_password_required()) {
            return \false;
        }
        global $post;
        return \apply_filters('wordproof_timestamp_show_certificate', \YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper::has($post->ID, '_wordproof_schema'), $post);
    }
}
Helpers/PostTypeHelper.php000066600000001322151143706510011613 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

class PostTypeHelper
{
    /**
     * Returns public post types.
     *
     * @return array The public post types.
     */
    public static function getPublicPostTypes()
    {
        return \array_values(\get_post_types(['public' => \true]));
    }
    public static function getUnprotectedPosts($postType)
    {
        $query = ['post_type' => [$postType], 'fields' => 'ids', 'posts_per_page' => -1, 'post_status' => ['publish', 'inherit'], 'meta_query' => [['key' => '_wordproof_blockchain_transaction', 'compare' => 'NOT EXISTS']]];
        $query = new \WP_Query($query);
        return ['count' => $query->found_posts, 'postIds' => $query->posts];
    }
}
Helpers/EnvironmentHelper.php000066600000002653151143706510012340 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

use YoastSEO_Vendor\WordProof\SDK\Config\EnvironmentConfig;
class EnvironmentHelper
{
    public static function url()
    {
        $appConfig = \YoastSEO_Vendor\WordProof\SDK\Helpers\AppConfigHelper::getAppConfig();
        if ($appConfig->getWordProofUrl()) {
            return $appConfig->getWordProofUrl();
        }
        return self::get('url');
    }
    public static function client()
    {
        $appConfig = \YoastSEO_Vendor\WordProof\SDK\Helpers\AppConfigHelper::getAppConfig();
        if ($appConfig->getOauthClient()) {
            return $appConfig->getOauthClient();
        }
        return self::get('client');
    }
    public static function sslVerify()
    {
        return !\YoastSEO_Vendor\WordProof\SDK\Helpers\EnvironmentHelper::development();
    }
    public static function development()
    {
        return \YoastSEO_Vendor\WordProof\SDK\Helpers\AppConfigHelper::getEnvironment() === 'development';
    }
    public static function get($key)
    {
        $envConfig = self::environmentConfig();
        if ($envConfig && isset($envConfig[$key])) {
            return $envConfig[$key];
        }
        return null;
    }
    private static function environmentConfig()
    {
        $env = \YoastSEO_Vendor\WordProof\SDK\Helpers\AppConfigHelper::getEnvironment();
        return \YoastSEO_Vendor\WordProof\SDK\Config\EnvironmentConfig::get($env);
    }
}
Helpers/SettingsHelper.php000066600000002741151143706510011632 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

use YoastSEO_Vendor\WordProof\SDK\Config\OptionsConfig;
class SettingsHelper
{
    private static $key = 'settings';
    /**
     * Retrieving settings from the option.
     *
     * @param null $setting The key for the setting
     * @return array|bool|int|mixed|object|string|null
     */
    public static function get($setting = null)
    {
        $settings = \YoastSEO_Vendor\WordProof\SDK\Helpers\OptionsHelper::get(self::$key);
        if ($setting) {
            $option = \YoastSEO_Vendor\WordProof\SDK\Config\OptionsConfig::get('settings.options.' . $setting);
            if (isset($settings->{$setting}) && $option) {
                return $settings->{$setting};
            }
            return $option['default'];
        }
        return (object) $settings;
    }
    public static function showRevisions()
    {
        return self::get('show_revisions');
    }
    public static function certificateLinkText()
    {
        return self::get('certificate_link_text');
    }
    public static function hideCertificateLink()
    {
        return self::get('hide_certificate_link');
    }
    public static function selectedPostTypes()
    {
        return \apply_filters('wordproof_timestamp_post_types', self::get('selected_post_types'));
    }
    public static function postTypeIsInSelectedPostTypes($postType)
    {
        $postTypes = self::selectedPostTypes();
        return \in_array($postType, $postTypes, \true);
    }
}
Helpers/PostMetaHelper.php000066600000003163151143706510011565 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

class PostMetaHelper
{
    /**
     * @param integer $postId The post id for which the meta should be set.
     * @param string $key The key for the post meta.
     * @param mixed $value The value for the post meta.
     * @return integer|boolean Returns the post meta id or false on failure.
     */
    public static function add($postId, $key, $value, $single = \false)
    {
        return \add_post_meta($postId, $key, $value, $single);
    }
    /**
     * @param integer $postId The post id for which the meta should be set.
     * @param string $key The key for the post meta.
     * @param mixed $value The value for the post meta.
     * @return integer|boolean Returns the post meta id or false on failure.
     */
    public static function update($postId, $key, $value)
    {
        return \update_post_meta($postId, $key, $value);
    }
    /**
     * @param integer $postId The post id for which the meta should be set.
     * @param string $key The key for the post meta.
     * @param bool $single If a single result should be returned.
     * @return mixed Returns the post meta data or false on failure.
     */
    public static function get($postId, $key, $single = \true)
    {
        return \get_post_meta($postId, $key, $single);
    }
    /**
     * @param integer $postId The post id for which the meta should be set.
     * @param string $key The key for the post meta.
     * @return boolean Returns if the post meta key exists for the post id.
     */
    public static function has($postId, $key)
    {
        return \boolval(self::get($postId, $key));
    }
}
Helpers/RestApiHelper.php000066600000002576151143706510011407 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

use YoastSEO_Vendor\WordProof\SDK\Config\RoutesConfig;
class RestApiHelper
{
    private static function buildPath($endpoint)
    {
        return 'wordproof/v1' . $endpoint;
    }
    public static function getNamespace()
    {
        return self::buildPath('');
    }
    public static function route($slug)
    {
        $routes = \YoastSEO_Vendor\WordProof\SDK\Config\RoutesConfig::get();
        if (isset($routes[$slug])) {
            return $routes[$slug];
        }
        throw new \Exception('Route slug does not exist.');
    }
    public static function endpoint($slug)
    {
        $route = self::route($slug);
        if (isset($route['endpoint'])) {
            return $route['endpoint'];
        }
        throw new \Exception('Endpoint for route does not exist.');
    }
    public static function getRestRoute($slug, $params = [])
    {
        $url = \get_rest_url(null, self::buildPath(self::endpoint($slug)));
        \preg_match_all("/\\(.+?\\)/", $url, $matches);
        if (!isset($matches) || !isset($matches[0])) {
            return $url;
        }
        if (!\is_array($params) || \count($params) !== \count($matches[0])) {
            return $url;
        }
        foreach ($matches[0] as $index => $match) {
            $url = \str_replace($match, $params[$index], $url);
        }
        return $url;
    }
}
Helpers/OptionsHelper.php000066600000013000151143706510011453 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

use YoastSEO_Vendor\WordProof\SDK\Config\OptionsConfig;
class OptionsHelper
{
    private static $prefix = 'wordproof_';
    /**
     * Sets site option while properly sanitizing the data.
     *
     * @param string $key The key to set.
     * @param mixed $value The value to save.
     * @return bool If update_option succeeded.
     */
    public static function set($key, $value)
    {
        if (self::optionContainsOptions($key)) {
            $sanitizedValue = self::secureOptionWithOptions($key, $value, 'sanitize');
            return \update_option(self::$prefix . $key, (object) $sanitizedValue);
        } else {
            $option = self::getOptionFromConfig($key);
            $sanitizedValue = \YoastSEO_Vendor\WordProof\SDK\Helpers\SanitizeHelper::sanitize($value, $option['escape']);
            return \update_option(self::$prefix . $key, $sanitizedValue);
        }
    }
    /**
     * Deletes the site options.
     *
     * @param string $key The key to be deleted.
     * @return mixed
     */
    public static function delete($key)
    {
        return \delete_option(self::$prefix . $key);
    }
    /**
     * Retrieves the site option while properly escaping the data.
     *
     * @param string $key The site option.
     * @return array|bool|int|object|string
     */
    public static function get($key)
    {
        $option = self::getOptionFromConfig($key);
        $value = \get_option(self::$prefix . $key);
        if (self::optionContainsOptions($key)) {
            return self::secureOptionWithOptions($key, $value, 'escape');
        } else {
            return \YoastSEO_Vendor\WordProof\SDK\Helpers\EscapeHelper::escape($value, $option['escape']);
        }
    }
    /**
     * Returns all site options as object.
     *
     * @return object
     */
    public static function all()
    {
        $optionKeys = \array_keys(\YoastSEO_Vendor\WordProof\SDK\Config\OptionsConfig::get());
        foreach ($optionKeys as $key) {
            $options[$key] = self::get($key);
        }
        return (object) $options;
    }
    /**
     * Deletes all site options.
     */
    public static function reset()
    {
        $optionKeys = \array_keys(\YoastSEO_Vendor\WordProof\SDK\Config\OptionsConfig::get());
        foreach ($optionKeys as $key) {
            self::delete($key);
        }
    }
    /**
     * Deletes authentication options.
     */
    public static function resetAuthentication()
    {
        $optionKeys = ['access_token', 'source_id'];
        foreach ($optionKeys as $key) {
            self::delete($key);
        }
    }
    /**
     * Retrieves the access token.
     *
     * @return string|null
     */
    public static function accessToken()
    {
        return self::get('access_token');
    }
    /**
     * Retrieves the source id.
     *
     * @return integer|null
     */
    public static function sourceId()
    {
        return self::get('source_id');
    }
    /**
     * Sets the access token.
     *
     * @param string|null $value The access token to be set.
     * @return bool
     */
    public static function setAccessToken($value)
    {
        return self::set('access_token', $value);
    }
    /**
     * Sets the source id.
     *
     * @param integer|null $value The source id to be set.
     * @return bool
     */
    public static function setSourceId($value)
    {
        return self::set('source_id', $value);
    }
    /**
     * Retrieves the option settings from the config.
     *
     * @param string $key The option key.
     * @return array|false|mixed
     */
    private static function getOptionFromConfig($key)
    {
        $option = \YoastSEO_Vendor\WordProof\SDK\Config\OptionsConfig::get($key);
        if ($option && \array_key_exists('escape', $option) && \array_key_exists('default', $option)) {
            return $option;
        }
        return \false;
    }
    /**
     * Returns if the given option key contains options itself.
     *
     * @param string $key The option key to be checked.
     * @return bool
     */
    private static function optionContainsOptions($key)
    {
        $option = \YoastSEO_Vendor\WordProof\SDK\Config\OptionsConfig::get($key);
        return $option && \array_key_exists('options', $option);
    }
    /**
     * Loops through an option that contains options to either sanitize or escape the result.
     *
     * @param $key
     * @param $value
     * @param string $method
     * @return array|object
     */
    private static function secureOptionWithOptions($key, $value, $method = 'sanitize')
    {
        $isObject = \is_object($value);
        if (\is_object($value)) {
            $value = (array) $value;
        }
        if (\is_array($value)) {
            $values = [];
            foreach ($value as $optionKey => $optionValue) {
                $optionConfig = self::getOptionFromConfig($key . '.options.' . $optionKey);
                if (!$optionConfig) {
                    continue;
                }
                if ($method === 'escape') {
                    $securedValue = \YoastSEO_Vendor\WordProof\SDK\Helpers\EscapeHelper::escape($optionValue, $optionConfig['escape']);
                } else {
                    $securedValue = \YoastSEO_Vendor\WordProof\SDK\Helpers\SanitizeHelper::sanitize($optionValue, $optionConfig['escape']);
                }
                $values[$optionKey] = $securedValue;
            }
            if ($isObject) {
                return (object) $values;
            }
            return $values;
        }
        return [];
    }
}
Helpers/ClassicNoticeHelper.php000066600000003154151143706510012554 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

class ClassicNoticeHelper
{
    /**
     * @var string The key used for the transient to save the single notice.
     */
    public static $transientKey = 'wordproof_notice';
    /**
     * Add a new transient with a notice key.
     *
     * @param string $noticeKey The noticeKey that should be displayed to the user.
     */
    public static function add($noticeKey)
    {
        \YoastSEO_Vendor\WordProof\SDK\Helpers\TransientHelper::set(self::$transientKey, $noticeKey);
    }
    /**
     * Add new notice depending on the timestamp response.
     *
     * @param \WP_REST_Response $response The timestamp response.
     */
    public static function addTimestampNotice($response)
    {
        $notice = self::getNoticeKeyForTimestampResponse($response->get_data());
        if ($notice) {
            self::add($notice);
        }
    }
    /**
     * Retrieve notice key for the timestamp response data.
     *
     * @param object $data The timestamp response data.
     * @return string The notice key for this response data.
     */
    private static function getNoticeKeyForTimestampResponse($data)
    {
        if (isset($data->error) && $data->error === 'not_authenticated') {
            return 'not_authenticated';
        }
        if (isset($data->balance) && $data->balance === 0) {
            return 'no_balance';
        }
        if (isset($data->hash)) {
            return 'timestamp_success';
        }
        if (isset($data->error) && $data->error === 'timestamp_failed') {
            return 'timestamp_failed';
        }
        return null;
    }
}
Helpers/SanitizeHelper.php000066600000004175151143706510011623 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Helpers;

class SanitizeHelper
{
    /**
     * Returns the value sanitized according to the escape function set in the class.
     *
     * @param mixed $value The value to be sanitized.
     * @param string $sanitizeKey The sanitize function to be used.
     *
     * @return array|bool|int|string
     */
    public static function sanitize($value, $sanitizeKey)
    {
        if (\is_array($value)) {
            return self::sanitizeArray($value, $sanitizeKey);
        }
        if (\is_object($value)) {
            return (object) self::sanitizeArray((array) $value, $sanitizeKey);
        }
        return self::sanitizeSingleValue($value, $sanitizeKey);
    }
    /**
     * Loops through the array to sanitize the values inside.
     *
     * @param array $array The array with values to be escaped.
     * @param string $sanitizeKey The sanitize function to be used.
     * @return array Array with escapes values.
     */
    private static function sanitizeArray($array, $sanitizeKey)
    {
        $values = [];
        foreach ($array as $key => $value) {
            $values[$key] = self::sanitizeSingleValue($value, $sanitizeKey);
        }
        return $values;
    }
    /**
     * Sanitize a single value using an escape function set in the class.
     *
     * @param string $value The value to be sanitized.
     * @param string $sanitizeKey The sanitize function to be used.
     * @return bool|int|string The sanitized value.
     */
    private static function sanitizeSingleValue($value, $sanitizeKey)
    {
        switch ($sanitizeKey) {
            case 'integer':
                return \intval($value);
            case 'boolean':
                return \boolval($value);
            case 'html_class':
                return \sanitize_html_class($value);
            case 'email':
                return \sanitize_email($value);
            case 'url':
                return \esc_url_raw($value);
            case 'key':
                return \sanitize_key($value);
            case 'text_field':
            default:
                return \sanitize_text_field($value);
        }
    }
}
Helpers/.htaccess000066600000000424151143706510007753 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>Exceptions/ValidationException.php000066600000000174151143706510013360 0ustar00<?php

namespace YoastSEO_Vendor\WordProof\SDK\Exceptions;

use Exception;
class ValidationException extends \Exception
{
}
Exceptions/.htaccess000066600000000424151143706510010472 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>