first
This commit is contained in:
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Addon_Installation;
|
||||
|
||||
use WPSEO_Addon_Manager;
|
||||
use Yoast\WP\SEO\Exceptions\Addon_Installation\Addon_Activation_Error_Exception;
|
||||
use Yoast\WP\SEO\Exceptions\Addon_Installation\User_Cannot_Activate_Plugins_Exception;
|
||||
use Yoast\WP\SEO\Helpers\Require_File_Helper;
|
||||
|
||||
/**
|
||||
* Represents the endpoint for activating a specific Yoast Plugin on WordPress.
|
||||
*/
|
||||
class Addon_Activate_Action {
|
||||
|
||||
/**
|
||||
* The addon manager.
|
||||
*
|
||||
* @var WPSEO_Addon_Manager
|
||||
*/
|
||||
protected $addon_manager;
|
||||
|
||||
/**
|
||||
* The require file helper.
|
||||
*
|
||||
* @var Require_File_Helper
|
||||
*/
|
||||
protected $require_file_helper;
|
||||
|
||||
/**
|
||||
* Addon_Activate_Action constructor.
|
||||
*
|
||||
* @param WPSEO_Addon_Manager $addon_manager The addon manager.
|
||||
* @param Require_File_Helper $require_file_helper A file helper.
|
||||
*/
|
||||
public function __construct(
|
||||
WPSEO_Addon_Manager $addon_manager,
|
||||
Require_File_Helper $require_file_helper
|
||||
) {
|
||||
$this->addon_manager = $addon_manager;
|
||||
$this->require_file_helper = $require_file_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the plugin based on the given plugin file.
|
||||
*
|
||||
* @param string $plugin_slug The plugin slug to get download url for.
|
||||
*
|
||||
* @return bool True when activation is successful.
|
||||
*
|
||||
* @throws Addon_Activation_Error_Exception Exception when the activation encounters an error.
|
||||
* @throws User_Cannot_Activate_Plugins_Exception Exception when the user is not allowed to activate.
|
||||
*/
|
||||
public function activate_addon( $plugin_slug ) {
|
||||
if ( ! \current_user_can( 'activate_plugins' ) ) {
|
||||
throw new User_Cannot_Activate_Plugins_Exception();
|
||||
}
|
||||
|
||||
if ( $this->addon_manager->is_installed( $plugin_slug ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->load_wordpress_classes();
|
||||
|
||||
$plugin_file = $this->addon_manager->get_plugin_file( $plugin_slug );
|
||||
$activation_result = \activate_plugin( $plugin_file );
|
||||
|
||||
if ( $activation_result !== null && \is_wp_error( $activation_result ) ) {
|
||||
throw new Addon_Activation_Error_Exception( $activation_result->get_error_message() );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the files needed from WordPress itself.
|
||||
*
|
||||
* @codeCoverageIgnore Only loads a WordPress file.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function load_wordpress_classes() {
|
||||
if ( ! \function_exists( 'get_plugins' ) ) {
|
||||
$this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/plugin.php' );
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Addon_Installation;
|
||||
|
||||
use Plugin_Upgrader;
|
||||
use WP_Error;
|
||||
use WPSEO_Addon_Manager;
|
||||
use Yoast\WP\SEO\Exceptions\Addon_Installation\Addon_Already_Installed_Exception;
|
||||
use Yoast\WP\SEO\Exceptions\Addon_Installation\Addon_Installation_Error_Exception;
|
||||
use Yoast\WP\SEO\Exceptions\Addon_Installation\User_Cannot_Install_Plugins_Exception;
|
||||
use Yoast\WP\SEO\Helpers\Require_File_Helper;
|
||||
|
||||
/**
|
||||
* Represents the endpoint for downloading and installing a zip-file from MyYoast.
|
||||
*/
|
||||
class Addon_Install_Action {
|
||||
|
||||
/**
|
||||
* The addon manager.
|
||||
*
|
||||
* @var WPSEO_Addon_Manager
|
||||
*/
|
||||
protected $addon_manager;
|
||||
|
||||
/**
|
||||
* The require file helper.
|
||||
*
|
||||
* @var Require_File_Helper
|
||||
*/
|
||||
protected $require_file_helper;
|
||||
|
||||
/**
|
||||
* Addon_Activate_Action constructor.
|
||||
*
|
||||
* @param WPSEO_Addon_Manager $addon_manager The addon manager.
|
||||
* @param Require_File_Helper $require_file_helper A helper that can require files.
|
||||
*/
|
||||
public function __construct(
|
||||
WPSEO_Addon_Manager $addon_manager,
|
||||
Require_File_Helper $require_file_helper
|
||||
) {
|
||||
$this->addon_manager = $addon_manager;
|
||||
$this->require_file_helper = $require_file_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the plugin based on the given slug.
|
||||
*
|
||||
* @param string $plugin_slug The plugin slug to install.
|
||||
* @param string $download_url The plugin download URL.
|
||||
*
|
||||
* @return bool True when install is successful.
|
||||
*
|
||||
* @throws Addon_Already_Installed_Exception When the addon is already installed.
|
||||
* @throws Addon_Installation_Error_Exception When the installation encounters an error.
|
||||
* @throws User_Cannot_Install_Plugins_Exception When the user does not have the permissions to install plugins.
|
||||
*/
|
||||
public function install_addon( $plugin_slug, $download_url ) {
|
||||
if ( ! \current_user_can( 'install_plugins' ) ) {
|
||||
throw new User_Cannot_Install_Plugins_Exception( $plugin_slug );
|
||||
}
|
||||
|
||||
if ( $this->is_installed( $plugin_slug ) ) {
|
||||
throw new Addon_Already_Installed_Exception( $plugin_slug );
|
||||
}
|
||||
|
||||
$this->load_wordpress_classes();
|
||||
|
||||
$install_result = $this->install( $download_url );
|
||||
if ( \is_wp_error( $install_result ) ) {
|
||||
throw new Addon_Installation_Error_Exception( $install_result->get_error_message() );
|
||||
}
|
||||
|
||||
return $install_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the files needed from WordPress itself.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function load_wordpress_classes() {
|
||||
if ( ! \class_exists( 'WP_Upgrader' ) ) {
|
||||
$this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' );
|
||||
}
|
||||
|
||||
if ( ! \class_exists( 'Plugin_Upgrader' ) ) {
|
||||
$this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php' );
|
||||
}
|
||||
|
||||
if ( ! \class_exists( 'WP_Upgrader_Skin' ) ) {
|
||||
$this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/class-wp-upgrader-skin.php' );
|
||||
}
|
||||
|
||||
if ( ! \function_exists( 'get_plugin_data' ) ) {
|
||||
$this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/plugin.php' );
|
||||
}
|
||||
|
||||
if ( ! \function_exists( 'request_filesystem_credentials' ) ) {
|
||||
$this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/file.php' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks is a plugin is installed.
|
||||
*
|
||||
* @param string $plugin_slug The plugin to check.
|
||||
*
|
||||
* @return bool True when plugin is installed.
|
||||
*/
|
||||
protected function is_installed( $plugin_slug ) {
|
||||
return $this->addon_manager->get_plugin_file( $plugin_slug ) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the installation by using the WordPress installation routine.
|
||||
*
|
||||
* @codeCoverageIgnore Contains WordPress specific logic.
|
||||
*
|
||||
* @param string $plugin_download The url to the download.
|
||||
*
|
||||
* @return bool|WP_Error True when success, WP_Error when something went wrong.
|
||||
*/
|
||||
protected function install( $plugin_download ) {
|
||||
$plugin_upgrader = new Plugin_Upgrader();
|
||||
|
||||
return $plugin_upgrader->install( $plugin_download );
|
||||
}
|
||||
}
|
@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\User_Helper;
|
||||
|
||||
/**
|
||||
* Class Alert_Dismissal_Action.
|
||||
*/
|
||||
class Alert_Dismissal_Action {
|
||||
|
||||
const USER_META_KEY = '_yoast_alerts_dismissed';
|
||||
|
||||
/**
|
||||
* Holds the user helper instance.
|
||||
*
|
||||
* @var User_Helper
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* Constructs Alert_Dismissal_Action.
|
||||
*
|
||||
* @param User_Helper $user User helper.
|
||||
*/
|
||||
public function __construct( User_Helper $user ) {
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismisses an alert.
|
||||
*
|
||||
* @param string $alert_identifier Alert identifier.
|
||||
*
|
||||
* @return bool Whether the dismiss was successful or not.
|
||||
*/
|
||||
public function dismiss( $alert_identifier ) {
|
||||
$user_id = $this->user->get_current_user_id();
|
||||
if ( $user_id === 0 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->is_allowed( $alert_identifier ) === false ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$dismissed_alerts = $this->get_dismissed_alerts( $user_id );
|
||||
if ( $dismissed_alerts === false ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( \array_key_exists( $alert_identifier, $dismissed_alerts ) === true ) {
|
||||
// The alert is already dismissed.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add this alert to the dismissed alerts.
|
||||
$dismissed_alerts[ $alert_identifier ] = true;
|
||||
|
||||
// Save.
|
||||
return $this->user->update_meta( $user_id, static::USER_META_KEY, $dismissed_alerts ) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets an alert.
|
||||
*
|
||||
* @param string $alert_identifier Alert identifier.
|
||||
*
|
||||
* @return bool Whether the reset was successful or not.
|
||||
*/
|
||||
public function reset( $alert_identifier ) {
|
||||
$user_id = $this->user->get_current_user_id();
|
||||
if ( $user_id === 0 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->is_allowed( $alert_identifier ) === false ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$dismissed_alerts = $this->get_dismissed_alerts( $user_id );
|
||||
if ( $dismissed_alerts === false ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$amount_of_dismissed_alerts = \count( $dismissed_alerts );
|
||||
if ( $amount_of_dismissed_alerts === 0 ) {
|
||||
// No alerts: nothing to reset.
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( \array_key_exists( $alert_identifier, $dismissed_alerts ) === false ) {
|
||||
// Alert not found: nothing to reset.
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( $amount_of_dismissed_alerts === 1 ) {
|
||||
// The 1 remaining dismissed alert is the alert to reset: delete the alerts user meta row.
|
||||
return $this->user->delete_meta( $user_id, static::USER_META_KEY, $dismissed_alerts );
|
||||
}
|
||||
|
||||
// Remove this alert from the dismissed alerts.
|
||||
unset( $dismissed_alerts[ $alert_identifier ] );
|
||||
|
||||
// Save.
|
||||
return $this->user->update_meta( $user_id, static::USER_META_KEY, $dismissed_alerts ) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if an alert is dismissed or not.
|
||||
*
|
||||
* @param string $alert_identifier Alert identifier.
|
||||
*
|
||||
* @return bool Whether the alert has been dismissed.
|
||||
*/
|
||||
public function is_dismissed( $alert_identifier ) {
|
||||
$user_id = $this->user->get_current_user_id();
|
||||
if ( $user_id === 0 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->is_allowed( $alert_identifier ) === false ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$dismissed_alerts = $this->get_dismissed_alerts( $user_id );
|
||||
if ( $dismissed_alerts === false ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return \array_key_exists( $alert_identifier, $dismissed_alerts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object with all alerts dismissed by current user.
|
||||
*
|
||||
* @return array|false An array with the keys of all Alerts that have been dismissed
|
||||
* by the current user or `false`.
|
||||
*/
|
||||
public function all_dismissed() {
|
||||
$user_id = $this->user->get_current_user_id();
|
||||
if ( $user_id === 0 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$dismissed_alerts = $this->get_dismissed_alerts( $user_id );
|
||||
if ( $dismissed_alerts === false ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $dismissed_alerts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if an alert is allowed or not.
|
||||
*
|
||||
* @param string $alert_identifier Alert identifier.
|
||||
*
|
||||
* @return bool Whether the alert is allowed.
|
||||
*/
|
||||
public function is_allowed( $alert_identifier ) {
|
||||
return \in_array( $alert_identifier, $this->get_allowed_dismissable_alerts(), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the dismissed alerts.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return string[]|false The dismissed alerts. False for an invalid $user_id.
|
||||
*/
|
||||
protected function get_dismissed_alerts( $user_id ) {
|
||||
$dismissed_alerts = $this->user->get_meta( $user_id, static::USER_META_KEY, true );
|
||||
if ( $dismissed_alerts === false ) {
|
||||
// Invalid user ID.
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $dismissed_alerts === '' ) {
|
||||
/*
|
||||
* When no database row exists yet, an empty string is returned because of the `single` parameter.
|
||||
* We do want a single result returned, but the default should be an empty array instead.
|
||||
*/
|
||||
return [];
|
||||
}
|
||||
|
||||
return $dismissed_alerts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the allowed dismissable alerts.
|
||||
*
|
||||
* @return string[] The allowed dismissable alerts.
|
||||
*/
|
||||
protected function get_allowed_dismissable_alerts() {
|
||||
/**
|
||||
* Filter: 'wpseo_allowed_dismissable_alerts' - List of allowed dismissable alerts.
|
||||
*
|
||||
* @api string[] $allowed_dismissable_alerts Allowed dismissable alerts list.
|
||||
*/
|
||||
$allowed_dismissable_alerts = \apply_filters( 'wpseo_allowed_dismissable_alerts', [] );
|
||||
|
||||
if ( \is_array( $allowed_dismissable_alerts ) === false ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Only allow strings.
|
||||
$allowed_dismissable_alerts = \array_filter( $allowed_dismissable_alerts, 'is_string' );
|
||||
|
||||
// Filter unique and reorder indices.
|
||||
$allowed_dismissable_alerts = \array_values( \array_unique( $allowed_dismissable_alerts ) );
|
||||
|
||||
return $allowed_dismissable_alerts;
|
||||
}
|
||||
}
|
@ -0,0 +1,298 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Configuration;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Social_Profiles_Helper;
|
||||
|
||||
/**
|
||||
* Class First_Time_Configuration_Action.
|
||||
*/
|
||||
class First_Time_Configuration_Action {
|
||||
|
||||
/**
|
||||
* The fields for the site representation payload.
|
||||
*/
|
||||
const SITE_REPRESENTATION_FIELDS = [
|
||||
'company_or_person',
|
||||
'company_name',
|
||||
'website_name',
|
||||
'company_logo',
|
||||
'company_logo_id',
|
||||
'person_logo',
|
||||
'person_logo_id',
|
||||
'company_or_person_user_id',
|
||||
'description',
|
||||
];
|
||||
|
||||
/**
|
||||
* The Options_Helper instance.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
protected $options_helper;
|
||||
|
||||
/**
|
||||
* The Social_Profiles_Helper instance.
|
||||
*
|
||||
* @var Social_Profiles_Helper
|
||||
*/
|
||||
protected $social_profiles_helper;
|
||||
|
||||
/**
|
||||
* First_Time_Configuration_Action constructor.
|
||||
*
|
||||
* @param Options_Helper $options_helper The WPSEO options helper.
|
||||
* @param Social_Profiles_Helper $social_profiles_helper The social profiles helper.
|
||||
*/
|
||||
public function __construct( Options_Helper $options_helper, Social_Profiles_Helper $social_profiles_helper ) {
|
||||
$this->options_helper = $options_helper;
|
||||
$this->social_profiles_helper = $social_profiles_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the values for the site representation.
|
||||
*
|
||||
* @param array $params The values to store.
|
||||
*
|
||||
* @return object The response object.
|
||||
*/
|
||||
public function set_site_representation( $params ) {
|
||||
$failures = [];
|
||||
|
||||
foreach ( self::SITE_REPRESENTATION_FIELDS as $field_name ) {
|
||||
if ( isset( $params[ $field_name ] ) ) {
|
||||
if ( $field_name === 'description' && \current_user_can( 'manage_options' ) ) {
|
||||
$result = \update_option( 'blogdescription', $params['description'] );
|
||||
if ( ! $result && $params['description'] === \get_option( 'blogdescription' ) ) {
|
||||
$result = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$result = $this->options_helper->set( $field_name, $params[ $field_name ] );
|
||||
}
|
||||
if ( ! $result ) {
|
||||
$failures[] = $field_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete cached logos in the db.
|
||||
$this->options_helper->set( 'company_logo_meta', false );
|
||||
$this->options_helper->set( 'person_logo_meta', false );
|
||||
|
||||
if ( \count( $failures ) === 0 ) {
|
||||
return (object) [
|
||||
'success' => true,
|
||||
'status' => 200,
|
||||
];
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'success' => false,
|
||||
'status' => 500,
|
||||
'error' => 'Could not save some options in the database',
|
||||
'failures' => $failures,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the values for the social profiles.
|
||||
*
|
||||
* @param array $params The values to store.
|
||||
*
|
||||
* @return object The response object.
|
||||
*/
|
||||
public function set_social_profiles( $params ) {
|
||||
$failures = $this->social_profiles_helper->set_organization_social_profiles( $params );
|
||||
|
||||
if ( empty( $failures ) ) {
|
||||
return (object) [
|
||||
'success' => true,
|
||||
'status' => 200,
|
||||
];
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'success' => false,
|
||||
'status' => 200,
|
||||
'error' => 'Could not save some options in the database',
|
||||
'failures' => $failures,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the values for the social profiles.
|
||||
*
|
||||
* @param array $params The values to store.
|
||||
*
|
||||
* @return object The response object.
|
||||
*/
|
||||
public function set_person_social_profiles( $params ) {
|
||||
$social_profiles = \array_filter(
|
||||
$params,
|
||||
static function ( $key ) {
|
||||
return $key !== 'user_id';
|
||||
},
|
||||
\ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
|
||||
$failures = $this->social_profiles_helper->set_person_social_profiles( $params['user_id'], $social_profiles );
|
||||
|
||||
if ( \count( $failures ) === 0 ) {
|
||||
return (object) [
|
||||
'success' => true,
|
||||
'status' => 200,
|
||||
];
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'success' => false,
|
||||
'status' => 200,
|
||||
'error' => 'Could not save some options in the database',
|
||||
'failures' => $failures,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the values for the social profiles.
|
||||
*
|
||||
* @param int $user_id The person ID.
|
||||
*
|
||||
* @return object The response object.
|
||||
*/
|
||||
public function get_person_social_profiles( $user_id ) {
|
||||
|
||||
return (object) [
|
||||
'success' => true,
|
||||
'status' => 200,
|
||||
'social_profiles' => $this->social_profiles_helper->get_person_social_profiles( $user_id ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the values to enable/disable tracking.
|
||||
*
|
||||
* @param array $params The values to store.
|
||||
*
|
||||
* @return object The response object.
|
||||
*/
|
||||
public function set_enable_tracking( $params ) {
|
||||
$success = true;
|
||||
$option_value = $this->options_helper->get( 'tracking' );
|
||||
|
||||
if ( $option_value !== $params['tracking'] ) {
|
||||
$this->options_helper->set( 'toggled_tracking', true );
|
||||
$success = $this->options_helper->set( 'tracking', $params['tracking'] );
|
||||
}
|
||||
|
||||
if ( $success ) {
|
||||
return (object) [
|
||||
'success' => true,
|
||||
'status' => 200,
|
||||
];
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'success' => false,
|
||||
'status' => 500,
|
||||
'error' => 'Could not save the option in the database',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current user has the capability a specific user.
|
||||
*
|
||||
* @param int $user_id The id of the user to be edited.
|
||||
*
|
||||
* @return object The response object.
|
||||
*/
|
||||
public function check_capability( $user_id ) {
|
||||
if ( $this->can_edit_profile( $user_id ) ) {
|
||||
return (object) [
|
||||
'success' => true,
|
||||
'status' => 200,
|
||||
];
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'success' => false,
|
||||
'status' => 403,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the first time configuration state.
|
||||
*
|
||||
* @param array $params The values to store.
|
||||
*
|
||||
* @return object The response object.
|
||||
*/
|
||||
public function save_configuration_state( $params ) {
|
||||
// If the finishedSteps param is not present in the REST request, it's a malformed request.
|
||||
if ( ! isset( $params['finishedSteps'] ) ) {
|
||||
return (object) [
|
||||
'success' => false,
|
||||
'status' => 400,
|
||||
'error' => 'Bad request',
|
||||
];
|
||||
}
|
||||
|
||||
// Sanitize input.
|
||||
$finished_steps = \array_map( '\sanitize_text_field', \wp_unslash( $params['finishedSteps'] ) );
|
||||
|
||||
$success = $this->options_helper->set( 'configuration_finished_steps', $finished_steps );
|
||||
|
||||
if ( ! $success ) {
|
||||
return (object) [
|
||||
'success' => false,
|
||||
'status' => 500,
|
||||
'error' => 'Could not save the option in the database',
|
||||
];
|
||||
}
|
||||
|
||||
// If all the five steps of the configuration have been completed, set first_time_install option to false.
|
||||
if ( \count( $params['finishedSteps'] ) === 3 ) {
|
||||
$this->options_helper->set( 'first_time_install', false );
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'success' => true,
|
||||
'status' => 200,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first time configuration state.
|
||||
*
|
||||
* @return object The response object.
|
||||
*/
|
||||
public function get_configuration_state() {
|
||||
$configuration_option = $this->options_helper->get( 'configuration_finished_steps' );
|
||||
|
||||
if ( ! \is_null( $configuration_option ) ) {
|
||||
return (object) [
|
||||
'success' => true,
|
||||
'status' => 200,
|
||||
'data' => $configuration_option,
|
||||
];
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'success' => false,
|
||||
'status' => 500,
|
||||
'error' => 'Could not get data from the database',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current user has the capability to edit a specific user.
|
||||
*
|
||||
* @param int $person_id The id of the person to edit.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function can_edit_profile( $person_id ) {
|
||||
return \current_user_can( 'edit_user', $person_id );
|
||||
}
|
||||
}
|
@ -0,0 +1,264 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Importing;
|
||||
|
||||
use Exception;
|
||||
use Yoast\WP\SEO\Helpers\Aioseo_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Import_Cursor_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Sanitization_Helper;
|
||||
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service;
|
||||
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Provider_Service;
|
||||
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Transformer_Service;
|
||||
|
||||
/**
|
||||
* Importing action interface.
|
||||
*/
|
||||
abstract class Abstract_Aioseo_Importing_Action implements Importing_Action_Interface {
|
||||
|
||||
/**
|
||||
* The plugin the class deals with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const PLUGIN = null;
|
||||
|
||||
/**
|
||||
* The type the class deals with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TYPE = null;
|
||||
|
||||
/**
|
||||
* The AIOSEO helper.
|
||||
*
|
||||
* @var Aioseo_Helper
|
||||
*/
|
||||
protected $aioseo_helper;
|
||||
|
||||
/**
|
||||
* The import cursor helper.
|
||||
*
|
||||
* @var Import_Cursor_Helper
|
||||
*/
|
||||
protected $import_cursor;
|
||||
|
||||
/**
|
||||
* The options helper.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* The sanitization helper.
|
||||
*
|
||||
* @var Sanitization_Helper
|
||||
*/
|
||||
protected $sanitization;
|
||||
|
||||
/**
|
||||
* The replacevar handler.
|
||||
*
|
||||
* @var Aioseo_Replacevar_Service
|
||||
*/
|
||||
protected $replacevar_handler;
|
||||
|
||||
/**
|
||||
* The robots provider service.
|
||||
*
|
||||
* @var Aioseo_Robots_Provider_Service
|
||||
*/
|
||||
protected $robots_provider;
|
||||
|
||||
/**
|
||||
* The robots transformer service.
|
||||
*
|
||||
* @var Aioseo_Robots_Transformer_Service
|
||||
*/
|
||||
protected $robots_transformer;
|
||||
|
||||
/**
|
||||
* Abstract_Aioseo_Importing_Action constructor.
|
||||
*
|
||||
* @param Import_Cursor_Helper $import_cursor The import cursor helper.
|
||||
* @param Options_Helper $options The options helper.
|
||||
* @param Sanitization_Helper $sanitization The sanitization helper.
|
||||
* @param Aioseo_Replacevar_Service $replacevar_handler The replacevar handler.
|
||||
* @param Aioseo_Robots_Provider_Service $robots_provider The robots provider service.
|
||||
* @param Aioseo_Robots_Transformer_Service $robots_transformer The robots transfomer service.
|
||||
*/
|
||||
public function __construct(
|
||||
Import_Cursor_Helper $import_cursor,
|
||||
Options_Helper $options,
|
||||
Sanitization_Helper $sanitization,
|
||||
Aioseo_Replacevar_Service $replacevar_handler,
|
||||
Aioseo_Robots_Provider_Service $robots_provider,
|
||||
Aioseo_Robots_Transformer_Service $robots_transformer
|
||||
) {
|
||||
$this->import_cursor = $import_cursor;
|
||||
$this->options = $options;
|
||||
$this->sanitization = $sanitization;
|
||||
$this->replacevar_handler = $replacevar_handler;
|
||||
$this->robots_provider = $robots_provider;
|
||||
$this->robots_transformer = $robots_transformer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the AIOSEO helper.
|
||||
*
|
||||
* @required
|
||||
*
|
||||
* @param Aioseo_Helper $aioseo_helper The AIOSEO helper.
|
||||
*/
|
||||
public function set_aioseo_helper( Aioseo_Helper $aioseo_helper ) {
|
||||
$this->aioseo_helper = $aioseo_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the plugin we import from.
|
||||
*
|
||||
* @return string The plugin we import from.
|
||||
*
|
||||
* @throws Exception If the PLUGIN constant is not set in the child class.
|
||||
*/
|
||||
public function get_plugin() {
|
||||
$class = \get_class( $this );
|
||||
$plugin = $class::PLUGIN;
|
||||
|
||||
if ( $plugin === null ) {
|
||||
throw new Exception( 'Importing action without explicit plugin' );
|
||||
}
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* The data type we import from the plugin.
|
||||
*
|
||||
* @return string The data type we import from the plugin.
|
||||
*
|
||||
* @throws Exception If the TYPE constant is not set in the child class.
|
||||
*/
|
||||
public function get_type() {
|
||||
$class = \get_class( $this );
|
||||
$type = $class::TYPE;
|
||||
|
||||
if ( $type === null ) {
|
||||
throw new Exception( 'Importing action without explicit type' );
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the current action import the data from plugin $plugin of type $type?
|
||||
*
|
||||
* @param string|null $plugin The plugin to import from.
|
||||
* @param string|null $type The type of data to import.
|
||||
*
|
||||
* @return bool True if this action can handle the combination of Plugin and Type.
|
||||
*
|
||||
* @throws Exception If the TYPE constant is not set in the child class.
|
||||
*/
|
||||
public function is_compatible_with( $plugin = null, $type = null ) {
|
||||
if ( empty( $plugin ) && empty( $type ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( $plugin === $this->get_plugin() && empty( $type ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( empty( $plugin ) && $type === $this->get_type() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( $plugin === $this->get_plugin() && $type === $this->get_type() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the completed id (to be used as a key for the importing_completed option).
|
||||
*
|
||||
* @return string The completed id.
|
||||
*/
|
||||
public function get_completed_id() {
|
||||
return $this->get_cursor_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored state of completedness.
|
||||
*
|
||||
* @return int The stored state of completedness.
|
||||
*/
|
||||
public function get_completed() {
|
||||
$completed_id = $this->get_completed_id();
|
||||
$importers_completions = $this->options->get( 'importing_completed', [] );
|
||||
|
||||
return ( isset( $importers_completions[ $completed_id ] ) ) ? $importers_completions[ $completed_id ] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the current state of completedness.
|
||||
*
|
||||
* @param bool $completed Whether the importer is completed.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set_completed( $completed ) {
|
||||
$completed_id = $this->get_completed_id();
|
||||
$current_importers_completions = $this->options->get( 'importing_completed', [] );
|
||||
|
||||
$current_importers_completions[ $completed_id ] = $completed;
|
||||
$this->options->set( 'importing_completed', $current_importers_completions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the importing action is enabled.
|
||||
*
|
||||
* @return bool True by default unless a child class overrides it.
|
||||
*/
|
||||
public function is_enabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cursor id.
|
||||
*
|
||||
* @return string The cursor id.
|
||||
*/
|
||||
protected function get_cursor_id() {
|
||||
return $this->get_plugin() . '_' . $this->get_type();
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimally transforms data to be imported.
|
||||
*
|
||||
* @param string $meta_data The meta data to be imported.
|
||||
*
|
||||
* @return string The transformed meta data.
|
||||
*/
|
||||
public function simple_import( $meta_data ) {
|
||||
// Transform the replace vars into Yoast replace vars.
|
||||
$transformed_data = $this->replacevar_handler->transform( $meta_data );
|
||||
|
||||
return $this->sanitization->sanitize_text_field( \html_entity_decode( $transformed_data ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms URL to be imported.
|
||||
*
|
||||
* @param string $meta_data The meta data to be imported.
|
||||
*
|
||||
* @return string The transformed URL.
|
||||
*/
|
||||
public function url_import( $meta_data ) {
|
||||
// We put null as the allowed protocols here, to have the WP default allowed protocols, see https://developer.wordpress.org/reference/functions/wp_allowed_protocols.
|
||||
return $this->sanitization->sanitize_url( $meta_data, null );
|
||||
}
|
||||
}
|
@ -0,0 +1,340 @@
|
||||
<?php
|
||||
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
|
||||
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
|
||||
|
||||
use Exception;
|
||||
use Yoast\WP\SEO\Actions\Importing\Abstract_Aioseo_Importing_Action;
|
||||
use Yoast\WP\SEO\Helpers\Import_Helper;
|
||||
|
||||
/**
|
||||
* Abstract class for importing AIOSEO settings.
|
||||
*/
|
||||
abstract class Abstract_Aioseo_Settings_Importing_Action extends Abstract_Aioseo_Importing_Action {
|
||||
|
||||
/**
|
||||
* The plugin the class deals with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const PLUGIN = null;
|
||||
|
||||
/**
|
||||
* The type the class deals with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TYPE = null;
|
||||
|
||||
/**
|
||||
* The option_name of the AIOSEO option that contains the settings.
|
||||
*/
|
||||
const SOURCE_OPTION_NAME = null;
|
||||
|
||||
/**
|
||||
* The map of aioseo_options to yoast settings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $aioseo_options_to_yoast_map = [];
|
||||
|
||||
/**
|
||||
* The tab of the aioseo settings we're working with, eg. taxonomies, posttypes.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $settings_tab = '';
|
||||
|
||||
/**
|
||||
* Additional mapping between AiOSEO replace vars and Yoast replace vars.
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
* @see https://yoast.com/help/list-available-snippet-variables-yoast-seo/
|
||||
*/
|
||||
protected $replace_vars_edited_map = [];
|
||||
|
||||
/**
|
||||
* The import helper.
|
||||
*
|
||||
* @var Import_Helper
|
||||
*/
|
||||
protected $import_helper;
|
||||
|
||||
/**
|
||||
* Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function build_mapping();
|
||||
|
||||
/**
|
||||
* Sets the import helper.
|
||||
*
|
||||
* @required
|
||||
*
|
||||
* @param Import_Helper $import_helper The import helper.
|
||||
*/
|
||||
public function set_import_helper( Import_Helper $import_helper ) {
|
||||
$this->import_helper = $import_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the source option_name.
|
||||
*
|
||||
* @return string The source option_name.
|
||||
*
|
||||
* @throws Exception If the SOURCE_OPTION_NAME constant is not set in the child class.
|
||||
*/
|
||||
public function get_source_option_name() {
|
||||
$source_option_name = static::SOURCE_OPTION_NAME;
|
||||
|
||||
if ( empty( $source_option_name ) ) {
|
||||
throw new Exception( 'Importing settings action without explicit source option_name' );
|
||||
}
|
||||
|
||||
return $source_option_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of unimported objects.
|
||||
*
|
||||
* @return int The total number of unimported objects.
|
||||
*/
|
||||
public function get_total_unindexed() {
|
||||
return $this->get_unindexed_count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the limited number of unimported objects.
|
||||
*
|
||||
* @param int $limit The maximum number of unimported objects to be returned.
|
||||
*
|
||||
* @return int The limited number of unindexed posts.
|
||||
*/
|
||||
public function get_limited_unindexed_count( $limit ) {
|
||||
return $this->get_unindexed_count( $limit );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of unimported objects (limited if limit is applied).
|
||||
*
|
||||
* @param int|null $limit The maximum number of unimported objects to be returned.
|
||||
*
|
||||
* @return int The number of unindexed posts.
|
||||
*/
|
||||
protected function get_unindexed_count( $limit = null ) {
|
||||
if ( ! \is_int( $limit ) || $limit < 1 ) {
|
||||
$limit = null;
|
||||
}
|
||||
|
||||
$settings_to_create = $this->query( $limit );
|
||||
|
||||
$number_of_settings_to_create = \count( $settings_to_create );
|
||||
$completed = $number_of_settings_to_create === 0;
|
||||
$this->set_completed( $completed );
|
||||
|
||||
return $number_of_settings_to_create;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports AIOSEO settings.
|
||||
*
|
||||
* @return array|false An array of the AIOSEO settings that were imported or false if aioseo data was not found.
|
||||
*/
|
||||
public function index() {
|
||||
$limit = $this->get_limit();
|
||||
$aioseo_settings = $this->query( $limit );
|
||||
$created_settings = [];
|
||||
|
||||
$completed = \count( $aioseo_settings ) === 0;
|
||||
$this->set_completed( $completed );
|
||||
|
||||
// Prepare the setting keys mapping.
|
||||
$this->build_mapping();
|
||||
|
||||
// Prepare the replacement var mapping.
|
||||
foreach ( $this->replace_vars_edited_map as $aioseo_var => $yoast_var ) {
|
||||
$this->replacevar_handler->compose_map( $aioseo_var, $yoast_var );
|
||||
}
|
||||
|
||||
$last_imported_setting = '';
|
||||
try {
|
||||
foreach ( $aioseo_settings as $setting => $setting_value ) {
|
||||
// Map and import the values of the setting we're working with (eg. post, book-category, etc.) to the respective Yoast option.
|
||||
$this->map( $setting_value, $setting );
|
||||
|
||||
// Save the type of the settings that were just imported, so that we can allow chunked imports.
|
||||
$last_imported_setting = $setting;
|
||||
|
||||
$created_settings[] = $setting;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$cursor_id = $this->get_cursor_id();
|
||||
$this->import_cursor->set_cursor( $cursor_id, $last_imported_setting );
|
||||
}
|
||||
|
||||
return $created_settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the settings tab subsetting is set in the AIOSEO option.
|
||||
*
|
||||
* @param string $aioseo_settings The AIOSEO option.
|
||||
*
|
||||
* @return bool Whether the settings are set.
|
||||
*/
|
||||
public function isset_settings_tab( $aioseo_settings ) {
|
||||
return isset( $aioseo_settings['searchAppearance'][ $this->settings_tab ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the database and retrieves unimported AiOSEO settings (in chunks if a limit is applied).
|
||||
*
|
||||
* @param int|null $limit The maximum number of unimported objects to be returned.
|
||||
*
|
||||
* @return array The (maybe chunked) unimported AiOSEO settings to import.
|
||||
*/
|
||||
protected function query( $limit = null ) {
|
||||
$aioseo_settings = \json_decode( \get_option( $this->get_source_option_name(), '' ), true );
|
||||
|
||||
if ( empty( $aioseo_settings ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// We specifically want the setttings of the tab we're working with, eg. postTypes, taxonomies, etc.
|
||||
$settings_values = $aioseo_settings['searchAppearance'][ $this->settings_tab ];
|
||||
if ( ! \is_array( $settings_values ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$flattened_settings = $this->import_helper->flatten_settings( $settings_values );
|
||||
|
||||
return $this->get_unimported_chunk( $flattened_settings, $limit );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves (a chunk of, if limit is applied) the unimported AIOSEO settings.
|
||||
* To apply a chunk, we manipulate the cursor to the keys of the AIOSEO settings.
|
||||
*
|
||||
* @param array $importable_data All of the available AIOSEO settings.
|
||||
* @param int $limit The maximum number of unimported objects to be returned.
|
||||
*
|
||||
* @return array The (chunk of, if limit is applied)) unimported AIOSEO settings.
|
||||
*/
|
||||
protected function get_unimported_chunk( $importable_data, $limit ) {
|
||||
\ksort( $importable_data );
|
||||
|
||||
$cursor_id = $this->get_cursor_id();
|
||||
$cursor = $this->import_cursor->get_cursor( $cursor_id, '' );
|
||||
|
||||
/**
|
||||
* Filter 'wpseo_aioseo_<identifier>_import_cursor' - Allow filtering the value of the aioseo settings import cursor.
|
||||
*
|
||||
* @api int The value of the aioseo posttype default settings import cursor.
|
||||
*/
|
||||
$cursor = \apply_filters( 'wpseo_aioseo_' . $this->get_type() . '_import_cursor', $cursor );
|
||||
|
||||
if ( $cursor === '' ) {
|
||||
return \array_slice( $importable_data, 0, $limit, true );
|
||||
}
|
||||
|
||||
// Let's find the position of the cursor in the alphabetically sorted importable data, so we can return only the unimported data.
|
||||
$keys = \array_flip( \array_keys( $importable_data ) );
|
||||
// If the stored cursor now no longer exists in the data, we have no choice but to start over.
|
||||
$position = ( isset( $keys[ $cursor ] ) ) ? ( $keys[ $cursor ] + 1 ) : 0;
|
||||
|
||||
return \array_slice( $importable_data, $position, $limit, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of objects that will be imported in a single importing pass.
|
||||
*
|
||||
* @return int The limit.
|
||||
*/
|
||||
public function get_limit() {
|
||||
/**
|
||||
* Filter 'wpseo_aioseo_<identifier>_indexation_limit' - Allow filtering the number of settings imported during each importing pass.
|
||||
*
|
||||
* @api int The maximum number of posts indexed.
|
||||
*/
|
||||
$limit = \apply_filters( 'wpseo_aioseo_' . $this->get_type() . '_indexation_limit', 25 );
|
||||
|
||||
if ( ! \is_int( $limit ) || $limit < 1 ) {
|
||||
$limit = 25;
|
||||
}
|
||||
|
||||
return $limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps/imports AIOSEO settings into the respective Yoast settings.
|
||||
*
|
||||
* @param string|array $setting_value The value of the AIOSEO setting at hand.
|
||||
* @param string $setting The setting at hand, eg. post or movie-category, separator etc.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function map( $setting_value, $setting ) {
|
||||
$aioseo_options_to_yoast_map = $this->aioseo_options_to_yoast_map;
|
||||
|
||||
if ( isset( $aioseo_options_to_yoast_map[ $setting ] ) ) {
|
||||
$this->import_single_setting( $setting, $setting_value, $aioseo_options_to_yoast_map[ $setting ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a single setting in the db after transforming it to adhere to Yoast conventions.
|
||||
*
|
||||
* @param string $setting The name of the setting.
|
||||
* @param string $setting_value The values of the setting.
|
||||
* @param array $setting_mapping The mapping of the setting to Yoast formats.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function import_single_setting( $setting, $setting_value, $setting_mapping ) {
|
||||
$yoast_key = $setting_mapping['yoast_name'];
|
||||
|
||||
// Check if we're supposed to save the setting.
|
||||
if ( $this->options->get_default( 'wpseo_titles', $yoast_key ) !== null ) {
|
||||
// Then, do any needed data transfomation before actually saving the incoming data.
|
||||
$transformed_data = \call_user_func( [ $this, $setting_mapping['transform_method'] ], $setting_value, $setting_mapping );
|
||||
|
||||
$this->options->set( $yoast_key, $transformed_data );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimally transforms boolean data to be imported.
|
||||
*
|
||||
* @param bool $meta_data The boolean meta data to be imported.
|
||||
*
|
||||
* @return bool The transformed boolean meta data.
|
||||
*/
|
||||
public function simple_boolean_import( $meta_data ) {
|
||||
return $meta_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports the noindex setting, taking into consideration whether they defer to global defaults.
|
||||
*
|
||||
* @param bool $noindex The noindex of the type, without taking into consideration whether the type defers to global defaults.
|
||||
* @param array $mapping The mapping of the setting we're working with.
|
||||
*
|
||||
* @return bool The noindex setting.
|
||||
*/
|
||||
public function import_noindex( $noindex, $mapping ) {
|
||||
return $this->robots_transformer->transform_robot_setting( 'noindex', $noindex, $mapping );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a setting map of the robot setting for one subset of post types/taxonomies/archives.
|
||||
* For custom archives, it returns an empty array because AIOSEO excludes some custom archives from this option structure, eg. WooCommerce's products and we don't want to raise a false alarm.
|
||||
*
|
||||
* @return array The setting map of the robot setting for one subset of post types/taxonomies/archives or an empty array.
|
||||
*/
|
||||
public function pluck_robot_setting_from_mapping() {
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
|
||||
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
|
||||
|
||||
use wpdb;
|
||||
use Yoast\WP\SEO\Actions\Importing\Abstract_Aioseo_Importing_Action;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
|
||||
/**
|
||||
* Importing action for cleaning up AIOSEO data.
|
||||
*/
|
||||
class Aioseo_Cleanup_Action extends Abstract_Aioseo_Importing_Action {
|
||||
|
||||
/**
|
||||
* The plugin of the action.
|
||||
*/
|
||||
const PLUGIN = 'aioseo';
|
||||
|
||||
/**
|
||||
* The type of the action.
|
||||
*/
|
||||
const TYPE = 'cleanup';
|
||||
|
||||
/**
|
||||
* The AIOSEO meta_keys to be cleaned up.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $aioseo_postmeta_keys = [
|
||||
'_aioseo_title',
|
||||
'_aioseo_description',
|
||||
'_aioseo_og_title',
|
||||
'_aioseo_og_description',
|
||||
'_aioseo_twitter_title',
|
||||
'_aioseo_twitter_description',
|
||||
];
|
||||
|
||||
/**
|
||||
* The WordPress database instance.
|
||||
*
|
||||
* @var wpdb
|
||||
*/
|
||||
protected $wpdb;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param wpdb $wpdb The WordPress database instance.
|
||||
* @param Options_Helper $options The options helper.
|
||||
*/
|
||||
public function __construct(
|
||||
wpdb $wpdb,
|
||||
Options_Helper $options
|
||||
) {
|
||||
$this->wpdb = $wpdb;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the postmeta along with the db prefix.
|
||||
*
|
||||
* @return string The postmeta table name along with the db prefix.
|
||||
*/
|
||||
protected function get_postmeta_table() {
|
||||
return $this->wpdb->prefix . 'postmeta';
|
||||
}
|
||||
|
||||
/**
|
||||
* Just checks if the cleanup has been completed in the past.
|
||||
*
|
||||
* @return int The total number of unimported objects.
|
||||
*/
|
||||
public function get_total_unindexed() {
|
||||
if ( ! $this->aioseo_helper->aioseo_exists() ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ( ! $this->get_completed() ) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Just checks if the cleanup has been completed in the past.
|
||||
*
|
||||
* @param int $limit The maximum number of unimported objects to be returned.
|
||||
*
|
||||
* @return int|false The limited number of unindexed posts. False if the query fails.
|
||||
*/
|
||||
public function get_limited_unindexed_count( $limit ) {
|
||||
if ( ! $this->aioseo_helper->aioseo_exists() ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ( ! $this->get_completed() ) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up AIOSEO data.
|
||||
*
|
||||
* @return Indexable[]|false An array of created indexables or false if aioseo data was not found.
|
||||
*/
|
||||
public function index() {
|
||||
if ( $this->get_completed() ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: There is no unescaped user input.
|
||||
$meta_data = $this->wpdb->query( $this->cleanup_postmeta_query() );
|
||||
$aioseo_table_truncate_done = $this->wpdb->query( $this->truncate_query() );
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
|
||||
|
||||
if ( $meta_data === false && $aioseo_table_truncate_done === false ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->set_completed( true );
|
||||
|
||||
return [
|
||||
'metadata_cleanup' => $meta_data,
|
||||
'indexables_cleanup' => $aioseo_table_truncate_done,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DELETE query string for deleting AIOSEO postmeta data.
|
||||
*
|
||||
* @return string The query to use for importing or counting the number of items to import.
|
||||
*/
|
||||
public function cleanup_postmeta_query() {
|
||||
$table = $this->get_postmeta_table();
|
||||
$meta_keys_to_delete = $this->aioseo_postmeta_keys;
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
|
||||
return $this->wpdb->prepare(
|
||||
"DELETE FROM {$table} WHERE meta_key IN (" . \implode( ', ', \array_fill( 0, \count( $meta_keys_to_delete ), '%s' ) ) . ')',
|
||||
$meta_keys_to_delete
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TRUNCATE query string for emptying the AIOSEO indexable table, if it exists.
|
||||
*
|
||||
* @return string The query to use for importing or counting the number of items to import.
|
||||
*/
|
||||
public function truncate_query() {
|
||||
if ( ! $this->aioseo_helper->aioseo_exists() ) {
|
||||
// If the table doesn't exist, we need a string that will amount to a quick query that doesn't return false when ran.
|
||||
return 'SELECT 1';
|
||||
}
|
||||
|
||||
$table = $this->aioseo_helper->get_table();
|
||||
|
||||
return "TRUNCATE TABLE {$table}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Used nowhere. Exists to comply with the interface.
|
||||
*
|
||||
* @return int The limit.
|
||||
*/
|
||||
public function get_limit() {
|
||||
/**
|
||||
* Filter 'wpseo_aioseo_cleanup_limit' - Allow filtering the number of posts indexed during each indexing pass.
|
||||
*
|
||||
* @api int The maximum number of posts cleaned up.
|
||||
*/
|
||||
$limit = \apply_filters( 'wpseo_aioseo_cleanup_limit', 25 );
|
||||
|
||||
if ( ! \is_int( $limit ) || $limit < 1 ) {
|
||||
$limit = 25;
|
||||
}
|
||||
|
||||
return $limit;
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
|
||||
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Import_Cursor_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Sanitization_Helper;
|
||||
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service;
|
||||
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Provider_Service;
|
||||
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Transformer_Service;
|
||||
|
||||
/**
|
||||
* Importing action for AIOSEO custom archive settings data.
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
*/
|
||||
class Aioseo_Custom_Archive_Settings_Importing_Action extends Abstract_Aioseo_Settings_Importing_Action {
|
||||
|
||||
/**
|
||||
* The plugin of the action.
|
||||
*/
|
||||
const PLUGIN = 'aioseo';
|
||||
|
||||
/**
|
||||
* The type of the action.
|
||||
*/
|
||||
const TYPE = 'custom_archive_settings';
|
||||
|
||||
/**
|
||||
* The option_name of the AIOSEO option that contains the settings.
|
||||
*/
|
||||
const SOURCE_OPTION_NAME = 'aioseo_options_dynamic';
|
||||
|
||||
/**
|
||||
* The map of aioseo_options to yoast settings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $aioseo_options_to_yoast_map = [];
|
||||
|
||||
/**
|
||||
* The tab of the aioseo settings we're working with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $settings_tab = 'archives';
|
||||
|
||||
/**
|
||||
* The post type helper.
|
||||
*
|
||||
* @var Post_Type_Helper
|
||||
*/
|
||||
protected $post_type;
|
||||
|
||||
/**
|
||||
* Aioseo_Custom_Archive_Settings_Importing_Action constructor.
|
||||
*
|
||||
* @param Import_Cursor_Helper $import_cursor The import cursor helper.
|
||||
* @param Options_Helper $options The options helper.
|
||||
* @param Sanitization_Helper $sanitization The sanitization helper.
|
||||
* @param Post_Type_Helper $post_type The post type helper.
|
||||
* @param Aioseo_Replacevar_Service $replacevar_handler The replacevar handler.
|
||||
* @param Aioseo_Robots_Provider_Service $robots_provider The robots provider service.
|
||||
* @param Aioseo_Robots_Transformer_Service $robots_transformer The robots transfomer service.
|
||||
*/
|
||||
public function __construct(
|
||||
Import_Cursor_Helper $import_cursor,
|
||||
Options_Helper $options,
|
||||
Sanitization_Helper $sanitization,
|
||||
Post_Type_Helper $post_type,
|
||||
Aioseo_Replacevar_Service $replacevar_handler,
|
||||
Aioseo_Robots_Provider_Service $robots_provider,
|
||||
Aioseo_Robots_Transformer_Service $robots_transformer
|
||||
) {
|
||||
parent::__construct( $import_cursor, $options, $sanitization, $replacevar_handler, $robots_provider, $robots_transformer );
|
||||
|
||||
$this->post_type = $post_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function build_mapping() {
|
||||
$post_type_objects = \get_post_types( [ 'public' => true ], 'objects' );
|
||||
|
||||
foreach ( $post_type_objects as $pt ) {
|
||||
// Use all the custom post types that have archives.
|
||||
if ( ! $pt->_builtin && $this->post_type->has_archive( $pt ) ) {
|
||||
$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/title' ] = [
|
||||
'yoast_name' => 'title-ptarchive-' . $pt->name,
|
||||
'transform_method' => 'simple_import',
|
||||
];
|
||||
$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/metaDescription' ] = [
|
||||
'yoast_name' => 'metadesc-ptarchive-' . $pt->name,
|
||||
'transform_method' => 'simple_import',
|
||||
];
|
||||
$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/advanced/robotsMeta/noindex' ] = [
|
||||
'yoast_name' => 'noindex-ptarchive-' . $pt->name,
|
||||
'transform_method' => 'import_noindex',
|
||||
'type' => 'archives',
|
||||
'subtype' => $pt->name,
|
||||
'option_name' => 'aioseo_options_dynamic',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
|
||||
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
|
||||
|
||||
/**
|
||||
* Importing action for AIOSEO default archive settings data.
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
*/
|
||||
class Aioseo_Default_Archive_Settings_Importing_Action extends Abstract_Aioseo_Settings_Importing_Action {
|
||||
|
||||
/**
|
||||
* The plugin of the action.
|
||||
*/
|
||||
const PLUGIN = 'aioseo';
|
||||
|
||||
/**
|
||||
* The type of the action.
|
||||
*/
|
||||
const TYPE = 'default_archive_settings';
|
||||
|
||||
/**
|
||||
* The option_name of the AIOSEO option that contains the settings.
|
||||
*/
|
||||
const SOURCE_OPTION_NAME = 'aioseo_options';
|
||||
|
||||
/**
|
||||
* The map of aioseo_options to yoast settings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $aioseo_options_to_yoast_map = [];
|
||||
|
||||
/**
|
||||
* The tab of the aioseo settings we're working with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $settings_tab = 'archives';
|
||||
|
||||
/**
|
||||
* Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function build_mapping() {
|
||||
$this->aioseo_options_to_yoast_map = [
|
||||
'/author/title' => [
|
||||
'yoast_name' => 'title-author-wpseo',
|
||||
'transform_method' => 'simple_import',
|
||||
],
|
||||
'/author/metaDescription' => [
|
||||
'yoast_name' => 'metadesc-author-wpseo',
|
||||
'transform_method' => 'simple_import',
|
||||
],
|
||||
'/date/title' => [
|
||||
'yoast_name' => 'title-archive-wpseo',
|
||||
'transform_method' => 'simple_import',
|
||||
],
|
||||
'/date/metaDescription' => [
|
||||
'yoast_name' => 'metadesc-archive-wpseo',
|
||||
'transform_method' => 'simple_import',
|
||||
],
|
||||
'/search/title' => [
|
||||
'yoast_name' => 'title-search-wpseo',
|
||||
'transform_method' => 'simple_import',
|
||||
],
|
||||
'/author/advanced/robotsMeta/noindex' => [
|
||||
'yoast_name' => 'noindex-author-wpseo',
|
||||
'transform_method' => 'import_noindex',
|
||||
'type' => 'archives',
|
||||
'subtype' => 'author',
|
||||
'option_name' => 'aioseo_options',
|
||||
],
|
||||
'/date/advanced/robotsMeta/noindex' => [
|
||||
'yoast_name' => 'noindex-archive-wpseo',
|
||||
'transform_method' => 'import_noindex',
|
||||
'type' => 'archives',
|
||||
'subtype' => 'date',
|
||||
'option_name' => 'aioseo_options',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a setting map of the robot setting for author archives.
|
||||
*
|
||||
* @return array The setting map of the robot setting for author archives.
|
||||
*/
|
||||
public function pluck_robot_setting_from_mapping() {
|
||||
$this->build_mapping();
|
||||
|
||||
foreach ( $this->aioseo_options_to_yoast_map as $setting ) {
|
||||
// Return the first archive setting map.
|
||||
if ( $setting['transform_method'] === 'import_noindex' && isset( $setting['subtype'] ) && $setting['subtype'] === 'author' ) {
|
||||
return $setting;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
|
||||
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Image_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Import_Cursor_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Sanitization_Helper;
|
||||
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service;
|
||||
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Provider_Service;
|
||||
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Transformer_Service;
|
||||
|
||||
/**
|
||||
* Importing action for AIOSEO general settings.
|
||||
*/
|
||||
class Aioseo_General_Settings_Importing_Action extends Abstract_Aioseo_Settings_Importing_Action {
|
||||
|
||||
/**
|
||||
* The plugin of the action.
|
||||
*/
|
||||
const PLUGIN = 'aioseo';
|
||||
|
||||
/**
|
||||
* The type of the action.
|
||||
*/
|
||||
const TYPE = 'general_settings';
|
||||
|
||||
/**
|
||||
* The option_name of the AIOSEO option that contains the settings.
|
||||
*/
|
||||
const SOURCE_OPTION_NAME = 'aioseo_options';
|
||||
|
||||
/**
|
||||
* The map of aioseo_options to yoast settings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $aioseo_options_to_yoast_map = [];
|
||||
|
||||
/**
|
||||
* The tab of the aioseo settings we're working with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $settings_tab = 'global';
|
||||
|
||||
/**
|
||||
* The image helper.
|
||||
*
|
||||
* @var Image_Helper
|
||||
*/
|
||||
protected $image;
|
||||
|
||||
/**
|
||||
* Aioseo_General_Settings_Importing_Action constructor.
|
||||
*
|
||||
* @param Import_Cursor_Helper $import_cursor The import cursor helper.
|
||||
* @param Options_Helper $options The options helper.
|
||||
* @param Sanitization_Helper $sanitization The sanitization helper.
|
||||
* @param Image_Helper $image The image helper.
|
||||
* @param Aioseo_Replacevar_Service $replacevar_handler The replacevar handler.
|
||||
* @param Aioseo_Robots_Provider_Service $robots_provider The robots provider service.
|
||||
* @param Aioseo_Robots_Transformer_Service $robots_transformer The robots transfomer service.
|
||||
*/
|
||||
public function __construct(
|
||||
Import_Cursor_Helper $import_cursor,
|
||||
Options_Helper $options,
|
||||
Sanitization_Helper $sanitization,
|
||||
Image_Helper $image,
|
||||
Aioseo_Replacevar_Service $replacevar_handler,
|
||||
Aioseo_Robots_Provider_Service $robots_provider,
|
||||
Aioseo_Robots_Transformer_Service $robots_transformer
|
||||
) {
|
||||
parent::__construct( $import_cursor, $options, $sanitization, $replacevar_handler, $robots_provider, $robots_transformer );
|
||||
|
||||
$this->image = $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function build_mapping() {
|
||||
$this->aioseo_options_to_yoast_map = [
|
||||
'/separator' => [
|
||||
'yoast_name' => 'separator',
|
||||
'transform_method' => 'transform_separator',
|
||||
],
|
||||
'/siteTitle' => [
|
||||
'yoast_name' => 'title-home-wpseo',
|
||||
'transform_method' => 'simple_import',
|
||||
],
|
||||
'/metaDescription' => [
|
||||
'yoast_name' => 'metadesc-home-wpseo',
|
||||
'transform_method' => 'simple_import',
|
||||
],
|
||||
'/schema/siteRepresents' => [
|
||||
'yoast_name' => 'company_or_person',
|
||||
'transform_method' => 'transform_site_represents',
|
||||
],
|
||||
'/schema/person' => [
|
||||
'yoast_name' => 'company_or_person_user_id',
|
||||
'transform_method' => 'simple_import',
|
||||
],
|
||||
'/schema/organizationName' => [
|
||||
'yoast_name' => 'company_name',
|
||||
'transform_method' => 'simple_import',
|
||||
],
|
||||
'/schema/organizationLogo' => [
|
||||
'yoast_name' => 'company_logo',
|
||||
'transform_method' => 'import_company_logo',
|
||||
],
|
||||
'/schema/personLogo' => [
|
||||
'yoast_name' => 'person_logo',
|
||||
'transform_method' => 'import_person_logo',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports the organization logo while also accounting for the id of the log to be saved in the separate Yoast option.
|
||||
*
|
||||
* @param string $logo_url The company logo url coming from AIOSEO settings.
|
||||
*
|
||||
* @return string The transformed company logo url.
|
||||
*/
|
||||
public function import_company_logo( $logo_url ) {
|
||||
$logo_id = $this->image->get_attachment_by_url( $logo_url );
|
||||
$this->options->set( 'company_logo_id', $logo_id );
|
||||
|
||||
$this->options->set( 'company_logo_meta', false );
|
||||
$logo_meta = $this->image->get_attachment_meta_from_settings( 'company_logo' );
|
||||
$this->options->set( 'company_logo_meta', $logo_meta );
|
||||
|
||||
return $this->url_import( $logo_url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports the person logo while also accounting for the id of the log to be saved in the separate Yoast option.
|
||||
*
|
||||
* @param string $logo_url The person logo url coming from AIOSEO settings.
|
||||
*
|
||||
* @return string The transformed person logo url.
|
||||
*/
|
||||
public function import_person_logo( $logo_url ) {
|
||||
$logo_id = $this->image->get_attachment_by_url( $logo_url );
|
||||
$this->options->set( 'person_logo_id', $logo_id );
|
||||
|
||||
$this->options->set( 'person_logo_meta', false );
|
||||
$logo_meta = $this->image->get_attachment_meta_from_settings( 'person_logo' );
|
||||
$this->options->set( 'person_logo_meta', $logo_meta );
|
||||
|
||||
return $this->url_import( $logo_url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the site represents setting.
|
||||
*
|
||||
* @param string $site_represents The site represents setting.
|
||||
*
|
||||
* @return string The transformed site represents setting.
|
||||
*/
|
||||
public function transform_site_represents( $site_represents ) {
|
||||
switch ( $site_represents ) {
|
||||
case 'person':
|
||||
return 'person';
|
||||
|
||||
case 'organization':
|
||||
default:
|
||||
return 'company';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the separator setting.
|
||||
*
|
||||
* @param string $separator The separator setting.
|
||||
*
|
||||
* @return string The transformed separator.
|
||||
*/
|
||||
public function transform_separator( $separator ) {
|
||||
switch ( $separator ) {
|
||||
case '-':
|
||||
return 'sc-dash';
|
||||
|
||||
case '–':
|
||||
return 'sc-ndash';
|
||||
|
||||
case '—':
|
||||
return 'sc-mdash';
|
||||
|
||||
case '»':
|
||||
return 'sc-raquo';
|
||||
|
||||
case '«':
|
||||
return 'sc-laquo';
|
||||
|
||||
case '>':
|
||||
return 'sc-gt';
|
||||
|
||||
case '•':
|
||||
return 'sc-bull';
|
||||
|
||||
case '|':
|
||||
return 'sc-pipe';
|
||||
|
||||
default:
|
||||
return 'sc-dash';
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,623 @@
|
||||
<?php
|
||||
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
|
||||
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
|
||||
|
||||
use wpdb;
|
||||
use Yoast\WP\SEO\Actions\Importing\Abstract_Aioseo_Importing_Action;
|
||||
use Yoast\WP\SEO\Helpers\Image_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Import_Cursor_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Indexable_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Indexable_To_Postmeta_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Sanitization_Helper;
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
use Yoast\WP\SEO\Repositories\Indexable_Repository;
|
||||
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service;
|
||||
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Provider_Service;
|
||||
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Transformer_Service;
|
||||
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Social_Images_Provider_Service;
|
||||
|
||||
/**
|
||||
* Importing action for AIOSEO post data.
|
||||
*/
|
||||
class Aioseo_Posts_Importing_Action extends Abstract_Aioseo_Importing_Action {
|
||||
|
||||
/**
|
||||
* The plugin of the action.
|
||||
*/
|
||||
const PLUGIN = 'aioseo';
|
||||
|
||||
/**
|
||||
* The type of the action.
|
||||
*/
|
||||
const TYPE = 'posts';
|
||||
|
||||
/**
|
||||
* The map of aioseo to yoast meta.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $aioseo_to_yoast_map = [
|
||||
'title' => [
|
||||
'yoast_name' => 'title',
|
||||
'transform_method' => 'simple_import_post',
|
||||
],
|
||||
'description' => [
|
||||
'yoast_name' => 'description',
|
||||
'transform_method' => 'simple_import_post',
|
||||
],
|
||||
'og_title' => [
|
||||
'yoast_name' => 'open_graph_title',
|
||||
'transform_method' => 'simple_import_post',
|
||||
],
|
||||
'og_description' => [
|
||||
'yoast_name' => 'open_graph_description',
|
||||
'transform_method' => 'simple_import_post',
|
||||
],
|
||||
'twitter_title' => [
|
||||
'yoast_name' => 'twitter_title',
|
||||
'transform_method' => 'simple_import_post',
|
||||
'twitter_import' => true,
|
||||
],
|
||||
'twitter_description' => [
|
||||
'yoast_name' => 'twitter_description',
|
||||
'transform_method' => 'simple_import_post',
|
||||
'twitter_import' => true,
|
||||
],
|
||||
'canonical_url' => [
|
||||
'yoast_name' => 'canonical',
|
||||
'transform_method' => 'url_import_post',
|
||||
],
|
||||
'keyphrases' => [
|
||||
'yoast_name' => 'primary_focus_keyword',
|
||||
'transform_method' => 'keyphrase_import',
|
||||
],
|
||||
'og_image_url' => [
|
||||
'yoast_name' => 'open_graph_image',
|
||||
'social_image_import' => true,
|
||||
'social_setting_prefix_aioseo' => 'og_',
|
||||
'social_setting_prefix_yoast' => 'open_graph_',
|
||||
'transform_method' => 'social_image_url_import',
|
||||
],
|
||||
'twitter_image_url' => [
|
||||
'yoast_name' => 'twitter_image',
|
||||
'social_image_import' => true,
|
||||
'social_setting_prefix_aioseo' => 'twitter_',
|
||||
'social_setting_prefix_yoast' => 'twitter_',
|
||||
'transform_method' => 'social_image_url_import',
|
||||
],
|
||||
'robots_noindex' => [
|
||||
'yoast_name' => 'is_robots_noindex',
|
||||
'transform_method' => 'post_robots_noindex_import',
|
||||
'robots_import' => true,
|
||||
],
|
||||
'robots_nofollow' => [
|
||||
'yoast_name' => 'is_robots_nofollow',
|
||||
'transform_method' => 'post_general_robots_import',
|
||||
'robots_import' => true,
|
||||
'robot_type' => 'nofollow',
|
||||
],
|
||||
'robots_noarchive' => [
|
||||
'yoast_name' => 'is_robots_noarchive',
|
||||
'transform_method' => 'post_general_robots_import',
|
||||
'robots_import' => true,
|
||||
'robot_type' => 'noarchive',
|
||||
],
|
||||
'robots_nosnippet' => [
|
||||
'yoast_name' => 'is_robots_nosnippet',
|
||||
'transform_method' => 'post_general_robots_import',
|
||||
'robots_import' => true,
|
||||
'robot_type' => 'nosnippet',
|
||||
],
|
||||
'robots_noimageindex' => [
|
||||
'yoast_name' => 'is_robots_noimageindex',
|
||||
'transform_method' => 'post_general_robots_import',
|
||||
'robots_import' => true,
|
||||
'robot_type' => 'noimageindex',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Represents the indexables repository.
|
||||
*
|
||||
* @var Indexable_Repository
|
||||
*/
|
||||
protected $indexable_repository;
|
||||
|
||||
/**
|
||||
* The WordPress database instance.
|
||||
*
|
||||
* @var wpdb
|
||||
*/
|
||||
protected $wpdb;
|
||||
|
||||
/**
|
||||
* The image helper.
|
||||
*
|
||||
* @var Image_Helper
|
||||
*/
|
||||
protected $image;
|
||||
|
||||
/**
|
||||
* The indexable_to_postmeta helper.
|
||||
*
|
||||
* @var Indexable_To_Postmeta_Helper
|
||||
*/
|
||||
protected $indexable_to_postmeta;
|
||||
|
||||
/**
|
||||
* The indexable helper.
|
||||
*
|
||||
* @var Indexable_Helper
|
||||
*/
|
||||
protected $indexable_helper;
|
||||
|
||||
/**
|
||||
* The social images provider service.
|
||||
*
|
||||
* @var Aioseo_Social_Images_Provider_Service
|
||||
*/
|
||||
protected $social_images_provider;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param Indexable_Repository $indexable_repository The indexables repository.
|
||||
* @param wpdb $wpdb The WordPress database instance.
|
||||
* @param Import_Cursor_Helper $import_cursor The import cursor helper.
|
||||
* @param Indexable_Helper $indexable_helper The indexable helper.
|
||||
* @param Indexable_To_Postmeta_Helper $indexable_to_postmeta The indexable_to_postmeta helper.
|
||||
* @param Options_Helper $options The options helper.
|
||||
* @param Image_Helper $image The image helper.
|
||||
* @param Sanitization_Helper $sanitization The sanitization helper.
|
||||
* @param Aioseo_Replacevar_Service $replacevar_handler The replacevar handler.
|
||||
* @param Aioseo_Robots_Provider_Service $robots_provider The robots provider service.
|
||||
* @param Aioseo_Robots_Transformer_Service $robots_transformer The robots transfomer service.
|
||||
* @param Aioseo_Social_Images_Provider_Service $social_images_provider The social images provider service.
|
||||
*/
|
||||
public function __construct(
|
||||
Indexable_Repository $indexable_repository,
|
||||
wpdb $wpdb,
|
||||
Import_Cursor_Helper $import_cursor,
|
||||
Indexable_Helper $indexable_helper,
|
||||
Indexable_To_Postmeta_Helper $indexable_to_postmeta,
|
||||
Options_Helper $options,
|
||||
Image_Helper $image,
|
||||
Sanitization_Helper $sanitization,
|
||||
Aioseo_Replacevar_Service $replacevar_handler,
|
||||
Aioseo_Robots_Provider_Service $robots_provider,
|
||||
Aioseo_Robots_Transformer_Service $robots_transformer,
|
||||
Aioseo_Social_Images_Provider_Service $social_images_provider ) {
|
||||
parent::__construct( $import_cursor, $options, $sanitization, $replacevar_handler, $robots_provider, $robots_transformer );
|
||||
|
||||
$this->indexable_repository = $indexable_repository;
|
||||
$this->wpdb = $wpdb;
|
||||
$this->image = $image;
|
||||
$this->indexable_helper = $indexable_helper;
|
||||
$this->indexable_to_postmeta = $indexable_to_postmeta;
|
||||
$this->social_images_provider = $social_images_provider;
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: They are already prepared.
|
||||
|
||||
/**
|
||||
* Returns the total number of unimported objects.
|
||||
*
|
||||
* @return int The total number of unimported objects.
|
||||
*/
|
||||
public function get_total_unindexed() {
|
||||
if ( ! $this->aioseo_helper->aioseo_exists() ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$limit = false;
|
||||
$just_detect = true;
|
||||
$indexables_to_create = $this->wpdb->get_col( $this->query( $limit, $just_detect ) );
|
||||
|
||||
$number_of_indexables_to_create = \count( $indexables_to_create );
|
||||
$completed = $number_of_indexables_to_create === 0;
|
||||
$this->set_completed( $completed );
|
||||
|
||||
return $number_of_indexables_to_create;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the limited number of unimported objects.
|
||||
*
|
||||
* @param int $limit The maximum number of unimported objects to be returned.
|
||||
*
|
||||
* @return int|false The limited number of unindexed posts. False if the query fails.
|
||||
*/
|
||||
public function get_limited_unindexed_count( $limit ) {
|
||||
if ( ! $this->aioseo_helper->aioseo_exists() ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$just_detect = true;
|
||||
$indexables_to_create = $this->wpdb->get_col( $this->query( $limit, $just_detect ) );
|
||||
|
||||
$number_of_indexables_to_create = \count( $indexables_to_create );
|
||||
$completed = $number_of_indexables_to_create === 0;
|
||||
$this->set_completed( $completed );
|
||||
|
||||
return $number_of_indexables_to_create;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports AIOSEO meta data and creates the respective Yoast indexables and postmeta.
|
||||
*
|
||||
* @return Indexable[]|false An array of created indexables or false if aioseo data was not found.
|
||||
*/
|
||||
public function index() {
|
||||
if ( ! $this->aioseo_helper->aioseo_exists() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$limit = $this->get_limit();
|
||||
$aioseo_indexables = $this->wpdb->get_results( $this->query( $limit ), \ARRAY_A );
|
||||
$created_indexables = [];
|
||||
|
||||
$completed = \count( $aioseo_indexables ) === 0;
|
||||
$this->set_completed( $completed );
|
||||
|
||||
// Let's build the list of fields to check their defaults, to identify whether we're gonna import AIOSEO data in the indexable or not.
|
||||
$check_defaults_fields = [];
|
||||
foreach ( $this->aioseo_to_yoast_map as $yoast_mapping ) {
|
||||
// We don't want to check all the imported fields.
|
||||
if ( ! \in_array( $yoast_mapping['yoast_name'], [ 'open_graph_image', 'twitter_image' ], true ) ) {
|
||||
$check_defaults_fields[] = $yoast_mapping['yoast_name'];
|
||||
}
|
||||
}
|
||||
|
||||
$last_indexed_aioseo_id = 0;
|
||||
foreach ( $aioseo_indexables as $aioseo_indexable ) {
|
||||
$last_indexed_aioseo_id = $aioseo_indexable['id'];
|
||||
|
||||
$indexable = $this->indexable_repository->find_by_id_and_type( $aioseo_indexable['post_id'], 'post' );
|
||||
|
||||
// Let's ensure that the current post id represents something that we want to index (eg. *not* shop_order).
|
||||
if ( ! \is_a( $indexable, 'Yoast\WP\SEO\Models\Indexable' ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $this->indexable_helper->check_if_default_indexable( $indexable, $check_defaults_fields ) ) {
|
||||
$indexable = $this->map( $indexable, $aioseo_indexable );
|
||||
$indexable->save();
|
||||
|
||||
// To ensure that indexables can be rebuild after a reset, we have to store the data in the postmeta table too.
|
||||
$this->indexable_to_postmeta->map_to_postmeta( $indexable );
|
||||
}
|
||||
|
||||
$last_indexed_aioseo_id = $aioseo_indexable['id'];
|
||||
|
||||
$created_indexables[] = $indexable;
|
||||
}
|
||||
|
||||
$cursor_id = $this->get_cursor_id();
|
||||
$this->import_cursor->set_cursor( $cursor_id, $last_indexed_aioseo_id );
|
||||
|
||||
return $created_indexables;
|
||||
}
|
||||
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
|
||||
|
||||
/**
|
||||
* Maps AIOSEO meta data to Yoast meta data.
|
||||
*
|
||||
* @param Indexable $indexable The Yoast indexable.
|
||||
* @param array $aioseo_indexable The AIOSEO indexable.
|
||||
*
|
||||
* @return Indexable The created indexables.
|
||||
*/
|
||||
public function map( $indexable, $aioseo_indexable ) {
|
||||
foreach ( $this->aioseo_to_yoast_map as $aioseo_key => $yoast_mapping ) {
|
||||
// For robots import.
|
||||
if ( isset( $yoast_mapping['robots_import'] ) && $yoast_mapping['robots_import'] ) {
|
||||
$yoast_mapping['subtype'] = $indexable->object_sub_type;
|
||||
$indexable->{$yoast_mapping['yoast_name']} = $this->transform_import_data( $yoast_mapping['transform_method'], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable );
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// For social images, like open graph and twitter image.
|
||||
if ( isset( $yoast_mapping['social_image_import'] ) && $yoast_mapping['social_image_import'] ) {
|
||||
$image_url = $this->transform_import_data( $yoast_mapping['transform_method'], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable );
|
||||
|
||||
// Update the indexable's social image only where there's actually a url to import, so as not to lose the social images that we came up with when we originally built the indexable.
|
||||
if ( ! empty( $image_url ) ) {
|
||||
$indexable->{$yoast_mapping['yoast_name']} = $image_url;
|
||||
|
||||
$image_source_key = $yoast_mapping['social_setting_prefix_yoast'] . 'image_source';
|
||||
$indexable->$image_source_key = 'imported';
|
||||
|
||||
$image_id_key = $yoast_mapping['social_setting_prefix_yoast'] . 'image_id';
|
||||
$indexable->$image_id_key = $this->image->get_attachment_by_url( $image_url );
|
||||
|
||||
if ( $yoast_mapping['yoast_name'] === 'open_graph_image' ) {
|
||||
$indexable->open_graph_image_meta = null;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// For twitter import, take the respective open graph data if the appropriate setting is enabled.
|
||||
if ( isset( $yoast_mapping['twitter_import'] ) && $yoast_mapping['twitter_import'] && $aioseo_indexable['twitter_use_og'] ) {
|
||||
$aioseo_indexable['twitter_title'] = $aioseo_indexable['og_title'];
|
||||
$aioseo_indexable['twitter_description'] = $aioseo_indexable['og_description'];
|
||||
}
|
||||
|
||||
if ( ! empty( $aioseo_indexable[ $aioseo_key ] ) ) {
|
||||
$indexable->{$yoast_mapping['yoast_name']} = $this->transform_import_data( $yoast_mapping['transform_method'], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable );
|
||||
}
|
||||
}
|
||||
|
||||
return $indexable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the data to be imported.
|
||||
*
|
||||
* @param string $transform_method The method that is going to be used for transforming the data.
|
||||
* @param array $aioseo_indexable The data of the AIOSEO indexable data that is being imported.
|
||||
* @param string $aioseo_key The name of the specific set of data that is going to be transformed.
|
||||
* @param array $yoast_mapping Extra details for the import of the specific data that is going to be transformed.
|
||||
* @param Indexable $indexable The Yoast indexable that we are going to import the transformed data into.
|
||||
*
|
||||
* @return string|bool|null The transformed data to be imported.
|
||||
*/
|
||||
protected function transform_import_data( $transform_method, $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable ) {
|
||||
return \call_user_func( [ $this, $transform_method ], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of objects that will be imported in a single importing pass.
|
||||
*
|
||||
* @return int The limit.
|
||||
*/
|
||||
public function get_limit() {
|
||||
/**
|
||||
* Filter 'wpseo_aioseo_post_indexation_limit' - Allow filtering the number of posts indexed during each indexing pass.
|
||||
*
|
||||
* @api int The maximum number of posts indexed.
|
||||
*/
|
||||
$limit = \apply_filters( 'wpseo_aioseo_post_indexation_limit', 25 );
|
||||
|
||||
if ( ! \is_int( $limit ) || $limit < 1 ) {
|
||||
$limit = 25;
|
||||
}
|
||||
|
||||
return $limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the needed data array based on which columns we use from the AIOSEO indexable table.
|
||||
*
|
||||
* @return array The needed data array that contains all the needed columns.
|
||||
*/
|
||||
public function get_needed_data() {
|
||||
$needed_data = \array_keys( $this->aioseo_to_yoast_map );
|
||||
\array_push( $needed_data, 'id', 'post_id', 'robots_default', 'og_image_custom_url', 'og_image_type', 'twitter_image_custom_url', 'twitter_image_type', 'twitter_use_og' );
|
||||
|
||||
return $needed_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the needed robot data array to be used in validating against its structure.
|
||||
*
|
||||
* @return array The needed data array that contains all the needed columns.
|
||||
*/
|
||||
public function get_needed_robot_data() {
|
||||
$needed_robot_data = [];
|
||||
|
||||
foreach ( $this->aioseo_to_yoast_map as $yoast_mapping ) {
|
||||
if ( isset( $yoast_mapping['robot_type'] ) ) {
|
||||
$needed_robot_data[] = $yoast_mapping['robot_type'];
|
||||
}
|
||||
}
|
||||
|
||||
return $needed_robot_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a query for gathering AiOSEO data from the database.
|
||||
*
|
||||
* @param int $limit The maximum number of unimported objects to be returned.
|
||||
* @param bool $just_detect Whether we want to just detect if there are unimported objects. If false, we want to actually import them too.
|
||||
*
|
||||
* @return string The query to use for importing or counting the number of items to import.
|
||||
*/
|
||||
public function query( $limit = false, $just_detect = false ) {
|
||||
$table = $this->aioseo_helper->get_table();
|
||||
|
||||
$select_statement = 'id';
|
||||
if ( ! $just_detect ) {
|
||||
// If we want to import too, we need the actual needed data from AIOSEO indexables.
|
||||
$needed_data = $this->get_needed_data();
|
||||
|
||||
$select_statement = \implode( ', ', $needed_data );
|
||||
}
|
||||
|
||||
$cursor_id = $this->get_cursor_id();
|
||||
$cursor = $this->import_cursor->get_cursor( $cursor_id );
|
||||
|
||||
/**
|
||||
* Filter 'wpseo_aioseo_post_cursor' - Allow filtering the value of the aioseo post import cursor.
|
||||
*
|
||||
* @api int The value of the aioseo post import cursor.
|
||||
*/
|
||||
$cursor = \apply_filters( 'wpseo_aioseo_post_import_cursor', $cursor );
|
||||
|
||||
$replacements = [ $cursor ];
|
||||
|
||||
$limit_statement = '';
|
||||
if ( ! empty( $limit ) ) {
|
||||
$replacements[] = $limit;
|
||||
$limit_statement = ' LIMIT %d';
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
|
||||
return $this->wpdb->prepare(
|
||||
"SELECT {$select_statement} FROM {$table} WHERE id > %d ORDER BY id{$limit_statement}",
|
||||
$replacements
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimally transforms data to be imported.
|
||||
*
|
||||
* @param array $aioseo_data All of the AIOSEO data to be imported.
|
||||
* @param string $aioseo_key The AIOSEO key that contains the setting we're working with.
|
||||
*
|
||||
* @return string The transformed meta data.
|
||||
*/
|
||||
public function simple_import_post( $aioseo_data, $aioseo_key ) {
|
||||
return $this->simple_import( $aioseo_data[ $aioseo_key ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms URL to be imported.
|
||||
*
|
||||
* @param array $aioseo_data All of the AIOSEO data to be imported.
|
||||
* @param string $aioseo_key The AIOSEO key that contains the setting we're working with.
|
||||
*
|
||||
* @return string The transformed URL.
|
||||
*/
|
||||
public function url_import_post( $aioseo_data, $aioseo_key ) {
|
||||
return $this->url_import( $aioseo_data[ $aioseo_key ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Plucks the keyphrase to be imported from the AIOSEO array of keyphrase meta data.
|
||||
*
|
||||
* @param array $aioseo_data All of the AIOSEO data to be imported.
|
||||
* @param string $aioseo_key The AIOSEO key that contains the setting we're working with, aka keyphrases.
|
||||
*
|
||||
* @return string|null The plucked keyphrase.
|
||||
*/
|
||||
public function keyphrase_import( $aioseo_data, $aioseo_key ) {
|
||||
$meta_data = \json_decode( $aioseo_data[ $aioseo_key ], true );
|
||||
if ( ! isset( $meta_data['focus']['keyphrase'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->sanitization->sanitize_text_field( $meta_data['focus']['keyphrase'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports the post's noindex setting.
|
||||
*
|
||||
* @param bool $aioseo_robots_settings AIOSEO's set of robot settings for the post.
|
||||
*
|
||||
* @return bool|null The value of Yoast's noindex setting for the post.
|
||||
*/
|
||||
public function post_robots_noindex_import( $aioseo_robots_settings ) {
|
||||
// If robot settings defer to default settings, we have null in the is_robots_noindex field.
|
||||
if ( $aioseo_robots_settings['robots_default'] ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $aioseo_robots_settings['robots_noindex'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports the post's robots setting.
|
||||
*
|
||||
* @param bool $aioseo_robots_settings AIOSEO's set of robot settings for the post.
|
||||
* @param string $aioseo_key The AIOSEO key that contains the robot setting we're working with.
|
||||
* @param array $mapping The mapping of the setting we're working with.
|
||||
*
|
||||
* @return bool|null The value of Yoast's noindex setting for the post.
|
||||
*/
|
||||
public function post_general_robots_import( $aioseo_robots_settings, $aioseo_key, $mapping ) {
|
||||
$mapping = $this->enhance_mapping( $mapping );
|
||||
|
||||
if ( $aioseo_robots_settings['robots_default'] ) {
|
||||
// Let's first get the subtype's setting value and then transform it taking into consideration whether it defers to global defaults.
|
||||
$subtype_setting = $this->robots_provider->get_subtype_robot_setting( $mapping );
|
||||
return $this->robots_transformer->transform_robot_setting( $mapping['robot_type'], $subtype_setting, $mapping );
|
||||
}
|
||||
|
||||
return $aioseo_robots_settings[ $aioseo_key ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances the mapping of the setting we're working with, with type and the option name, so that we can retrieve the settings for the object we're working with.
|
||||
*
|
||||
* @param array $mapping The mapping of the setting we're working with.
|
||||
*
|
||||
* @return array The enhanced mapping.
|
||||
*/
|
||||
public function enhance_mapping( $mapping = [] ) {
|
||||
$mapping['type'] = 'postTypes';
|
||||
$mapping['option_name'] = 'aioseo_options_dynamic';
|
||||
|
||||
return $mapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports the og and twitter image url.
|
||||
*
|
||||
* @param bool $aioseo_social_image_settings AIOSEO's set of social image settings for the post.
|
||||
* @param string $aioseo_key The AIOSEO key that contains the robot setting we're working with.
|
||||
* @param array $mapping The mapping of the setting we're working with.
|
||||
* @param Indexable $indexable The Yoast indexable we're importing into.
|
||||
*
|
||||
* @return bool|null The url of the social image we're importing, null if there's none.
|
||||
*/
|
||||
public function social_image_url_import( $aioseo_social_image_settings, $aioseo_key, $mapping, $indexable ) {
|
||||
if ( $mapping['social_setting_prefix_aioseo'] === 'twitter_' && $aioseo_social_image_settings['twitter_use_og'] ) {
|
||||
$mapping['social_setting_prefix_aioseo'] = 'og_';
|
||||
}
|
||||
|
||||
$social_setting = \rtrim( $mapping['social_setting_prefix_aioseo'], '_' );
|
||||
|
||||
$image_type = $aioseo_social_image_settings[ $mapping['social_setting_prefix_aioseo'] . 'image_type' ];
|
||||
|
||||
if ( $image_type === 'default' ) {
|
||||
$image_type = $this->social_images_provider->get_default_social_image_source( $social_setting );
|
||||
}
|
||||
|
||||
switch ( $image_type ) {
|
||||
case 'attach':
|
||||
$image_url = $this->social_images_provider->get_first_attached_image( $indexable->object_id );
|
||||
break;
|
||||
case 'auto':
|
||||
if ( $this->social_images_provider->get_featured_image( $indexable->object_id ) ) {
|
||||
// If there's a featured image, lets not import it, as our indexable calculation has already set that as active social image. That way we achieve dynamicality.
|
||||
return null;
|
||||
}
|
||||
$image_url = $this->social_images_provider->get_auto_image( $indexable->object_id );
|
||||
break;
|
||||
case 'content':
|
||||
$image_url = $this->social_images_provider->get_first_image_in_content( $indexable->object_id );
|
||||
break;
|
||||
case 'custom_image':
|
||||
$image_url = $aioseo_social_image_settings[ $mapping['social_setting_prefix_aioseo'] . 'image_custom_url' ];
|
||||
break;
|
||||
case 'featured':
|
||||
return null; // Our auto-calculation when the indexable was built/updated has taken care of it, so it's not needed to transfer any data now.
|
||||
case 'author':
|
||||
return null;
|
||||
case 'custom':
|
||||
return null;
|
||||
case 'default':
|
||||
$image_url = $this->social_images_provider->get_default_custom_social_image( $social_setting );
|
||||
break;
|
||||
default:
|
||||
$image_url = $aioseo_social_image_settings[ $mapping['social_setting_prefix_aioseo'] . 'image_url' ];
|
||||
break;
|
||||
}
|
||||
|
||||
if ( empty( $image_url ) ) {
|
||||
$image_url = $this->social_images_provider->get_default_custom_social_image( $social_setting );
|
||||
}
|
||||
|
||||
if ( empty( $image_url ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->sanitization->sanitize_url( $image_url, null );
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
|
||||
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
|
||||
|
||||
/**
|
||||
* Importing action for AIOSEO posttype defaults settings data.
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
*/
|
||||
class Aioseo_Posttype_Defaults_Settings_Importing_Action extends Abstract_Aioseo_Settings_Importing_Action {
|
||||
|
||||
/**
|
||||
* The plugin of the action.
|
||||
*/
|
||||
const PLUGIN = 'aioseo';
|
||||
|
||||
/**
|
||||
* The type of the action.
|
||||
*/
|
||||
const TYPE = 'posttype_default_settings';
|
||||
|
||||
/**
|
||||
* The option_name of the AIOSEO option that contains the settings.
|
||||
*/
|
||||
const SOURCE_OPTION_NAME = 'aioseo_options_dynamic';
|
||||
|
||||
/**
|
||||
* The map of aioseo_options to yoast settings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $aioseo_options_to_yoast_map = [];
|
||||
|
||||
/**
|
||||
* The tab of the aioseo settings we're working with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $settings_tab = 'postTypes';
|
||||
|
||||
/**
|
||||
* Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function build_mapping() {
|
||||
$post_type_objects = \get_post_types( [ 'public' => true ], 'objects' );
|
||||
|
||||
foreach ( $post_type_objects as $pt ) {
|
||||
// Use all the custom post types that are public.
|
||||
$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/title' ] = [
|
||||
'yoast_name' => 'title-' . $pt->name,
|
||||
'transform_method' => 'simple_import',
|
||||
];
|
||||
$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/metaDescription' ] = [
|
||||
'yoast_name' => 'metadesc-' . $pt->name,
|
||||
'transform_method' => 'simple_import',
|
||||
];
|
||||
$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/advanced/showMetaBox' ] = [
|
||||
'yoast_name' => 'display-metabox-pt-' . $pt->name,
|
||||
'transform_method' => 'simple_boolean_import',
|
||||
];
|
||||
$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/advanced/robotsMeta/noindex' ] = [
|
||||
'yoast_name' => 'noindex-' . $pt->name,
|
||||
'transform_method' => 'import_noindex',
|
||||
'type' => 'postTypes',
|
||||
'subtype' => $pt->name,
|
||||
'option_name' => 'aioseo_options_dynamic',
|
||||
];
|
||||
|
||||
if ( $pt->name === 'attachment' ) {
|
||||
$this->aioseo_options_to_yoast_map['/attachment/redirectAttachmentUrls'] = [
|
||||
'yoast_name' => 'disable-attachment',
|
||||
'transform_method' => 'import_redirect_attachment',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the redirect_attachment setting.
|
||||
*
|
||||
* @param string $redirect_attachment The redirect_attachment setting.
|
||||
*
|
||||
* @return bool The transformed redirect_attachment setting.
|
||||
*/
|
||||
public function import_redirect_attachment( $redirect_attachment ) {
|
||||
switch ( $redirect_attachment ) {
|
||||
case 'disabled':
|
||||
return false;
|
||||
|
||||
case 'attachment':
|
||||
case 'attachment_parent':
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
|
||||
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
|
||||
|
||||
/**
|
||||
* Importing action for AIOSEO taxonomies settings data.
|
||||
*/
|
||||
class Aioseo_Taxonomy_Settings_Importing_Action extends Abstract_Aioseo_Settings_Importing_Action {
|
||||
|
||||
/**
|
||||
* The plugin of the action.
|
||||
*/
|
||||
const PLUGIN = 'aioseo';
|
||||
|
||||
/**
|
||||
* The type of the action.
|
||||
*/
|
||||
const TYPE = 'taxonomy_settings';
|
||||
|
||||
/**
|
||||
* The option_name of the AIOSEO option that contains the settings.
|
||||
*/
|
||||
const SOURCE_OPTION_NAME = 'aioseo_options_dynamic';
|
||||
|
||||
/**
|
||||
* The map of aioseo_options to yoast settings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $aioseo_options_to_yoast_map = [];
|
||||
|
||||
/**
|
||||
* The tab of the aioseo settings we're working with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $settings_tab = 'taxonomies';
|
||||
|
||||
/**
|
||||
* Additional mapping between AiOSEO replace vars and Yoast replace vars.
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
* @see https://yoast.com/help/list-available-snippet-variables-yoast-seo/
|
||||
*/
|
||||
protected $replace_vars_edited_map = [
|
||||
'#breadcrumb_404_error_format' => '', // Empty string, as AIOSEO shows nothing for that tag.
|
||||
'#breadcrumb_archive_post_type_format' => '', // Empty string, as AIOSEO shows nothing for that tag.
|
||||
'#breadcrumb_archive_post_type_name' => '', // Empty string, as AIOSEO shows nothing for that tag.
|
||||
'#breadcrumb_author_display_name' => '', // Empty string, as AIOSEO shows nothing for that tag.
|
||||
'#breadcrumb_author_first_name' => '', // Empty string, as AIOSEO shows nothing for that tag.
|
||||
'#breadcrumb_blog_page_title' => '', // Empty string, as AIOSEO shows nothing for that tag.
|
||||
'#breadcrumb_label' => '', // Empty string, as AIOSEO shows nothing for that tag.
|
||||
'#breadcrumb_link' => '', // Empty string, as AIOSEO shows nothing for that tag.
|
||||
'#breadcrumb_search_result_format' => '', // Empty string, as AIOSEO shows nothing for that tag.
|
||||
'#breadcrumb_search_string' => '', // Empty string, as AIOSEO shows nothing for that tag.
|
||||
'#breadcrumb_separator' => '', // Empty string, as AIOSEO shows nothing for that tag.
|
||||
'#breadcrumb_taxonomy_title' => '', // Empty string, as AIOSEO shows nothing for that tag.
|
||||
'#taxonomy_title' => '%%term_title%%',
|
||||
];
|
||||
|
||||
/**
|
||||
* Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function build_mapping() {
|
||||
$taxonomy_objects = \get_taxonomies( [ 'public' => true ], 'object' );
|
||||
|
||||
foreach ( $taxonomy_objects as $tax ) {
|
||||
// Use all the public taxonomies.
|
||||
$this->aioseo_options_to_yoast_map[ '/' . $tax->name . '/title' ] = [
|
||||
'yoast_name' => 'title-tax-' . $tax->name,
|
||||
'transform_method' => 'simple_import',
|
||||
];
|
||||
$this->aioseo_options_to_yoast_map[ '/' . $tax->name . '/metaDescription' ] = [
|
||||
'yoast_name' => 'metadesc-tax-' . $tax->name,
|
||||
'transform_method' => 'simple_import',
|
||||
];
|
||||
$this->aioseo_options_to_yoast_map[ '/' . $tax->name . '/advanced/robotsMeta/noindex' ] = [
|
||||
'yoast_name' => 'noindex-tax-' . $tax->name,
|
||||
'transform_method' => 'import_noindex',
|
||||
'type' => 'taxonomies',
|
||||
'subtype' => $tax->name,
|
||||
'option_name' => 'aioseo_options_dynamic',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a setting map of the robot setting for post category taxonomies.
|
||||
*
|
||||
* @return array The setting map of the robot setting for post category taxonomies.
|
||||
*/
|
||||
public function pluck_robot_setting_from_mapping() {
|
||||
$this->build_mapping();
|
||||
|
||||
foreach ( $this->aioseo_options_to_yoast_map as $setting ) {
|
||||
// Return the first archive setting map.
|
||||
if ( $setting['transform_method'] === 'import_noindex' && isset( $setting['subtype'] ) && $setting['subtype'] === 'category' ) {
|
||||
return $setting;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
|
||||
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
|
||||
|
||||
use wpdb;
|
||||
use Yoast\WP\SEO\Actions\Importing\Abstract_Aioseo_Importing_Action;
|
||||
use Yoast\WP\SEO\Exceptions\Importing\Aioseo_Validation_Exception;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
|
||||
/**
|
||||
* Importing action for validating AIOSEO data before the import occurs.
|
||||
*/
|
||||
class Aioseo_Validate_Data_Action extends Abstract_Aioseo_Importing_Action {
|
||||
|
||||
/**
|
||||
* The plugin of the action.
|
||||
*/
|
||||
const PLUGIN = 'aioseo';
|
||||
|
||||
/**
|
||||
* The type of the action.
|
||||
*/
|
||||
const TYPE = 'validate_data';
|
||||
|
||||
/**
|
||||
* The WordPress database instance.
|
||||
*
|
||||
* @var wpdb
|
||||
*/
|
||||
protected $wpdb;
|
||||
|
||||
/**
|
||||
* The Post Importing action.
|
||||
*
|
||||
* @var Aioseo_Posts_Importing_Action
|
||||
*/
|
||||
protected $post_importing_action;
|
||||
|
||||
/**
|
||||
* The settings importing actions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $settings_importing_actions;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param wpdb $wpdb The WordPress database instance.
|
||||
* @param Options_Helper $options The options helper.
|
||||
* @param Aioseo_Custom_Archive_Settings_Importing_Action $custom_archive_action The Custom Archive Settings importing action.
|
||||
* @param Aioseo_Default_Archive_Settings_Importing_Action $default_archive_action The Default Archive Settings importing action.
|
||||
* @param Aioseo_General_Settings_Importing_Action $general_settings_action The General Settings importing action.
|
||||
* @param Aioseo_Posttype_Defaults_Settings_Importing_Action $posttype_defaults_settings_action The Posttype Defaults Settings importing action.
|
||||
* @param Aioseo_Taxonomy_Settings_Importing_Action $taxonomy_settings_action The Taxonomy Settings importing action.
|
||||
* @param Aioseo_Posts_Importing_Action $post_importing_action The Post importing action.
|
||||
*/
|
||||
public function __construct(
|
||||
wpdb $wpdb,
|
||||
Options_Helper $options,
|
||||
Aioseo_Custom_Archive_Settings_Importing_Action $custom_archive_action,
|
||||
Aioseo_Default_Archive_Settings_Importing_Action $default_archive_action,
|
||||
Aioseo_General_Settings_Importing_Action $general_settings_action,
|
||||
Aioseo_Posttype_Defaults_Settings_Importing_Action $posttype_defaults_settings_action,
|
||||
Aioseo_Taxonomy_Settings_Importing_Action $taxonomy_settings_action,
|
||||
Aioseo_Posts_Importing_Action $post_importing_action
|
||||
) {
|
||||
$this->wpdb = $wpdb;
|
||||
$this->options = $options;
|
||||
$this->post_importing_action = $post_importing_action;
|
||||
$this->settings_importing_actions = [
|
||||
$custom_archive_action,
|
||||
$default_archive_action,
|
||||
$general_settings_action,
|
||||
$posttype_defaults_settings_action,
|
||||
$taxonomy_settings_action,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Just checks if the action has been completed in the past.
|
||||
*
|
||||
* @return int 1 if it hasn't been completed in the past, 0 if it has.
|
||||
*/
|
||||
public function get_total_unindexed() {
|
||||
return ( ! $this->get_completed() ) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Just checks if the action has been completed in the past.
|
||||
*
|
||||
* @param int $limit The maximum number of unimported objects to be returned. Not used, exists to comply with the interface.
|
||||
*
|
||||
* @return int 1 if it hasn't been completed in the past, 0 if it has.
|
||||
*/
|
||||
public function get_limited_unindexed_count( $limit ) {
|
||||
return ( ! $this->get_completed() ) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates AIOSEO data.
|
||||
*
|
||||
* @return array An array of validated data or false if aioseo data did not pass validation.
|
||||
*
|
||||
* @throws Aioseo_Validation_Exception If the validation fails.
|
||||
*/
|
||||
public function index() {
|
||||
if ( $this->get_completed() ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$validated_aioseo_table = $this->validate_aioseo_table();
|
||||
$validated_aioseo_settings = $this->validate_aioseo_settings();
|
||||
$validated_robot_settings = $this->validate_robot_settings();
|
||||
|
||||
if ( $validated_aioseo_table === false || $validated_aioseo_settings === false || $validated_robot_settings === false ) {
|
||||
throw new Aioseo_Validation_Exception();
|
||||
}
|
||||
|
||||
$this->set_completed( true );
|
||||
|
||||
return [
|
||||
'validated_aioseo_table' => $validated_aioseo_table,
|
||||
'validated_aioseo_settings' => $validated_aioseo_settings,
|
||||
'validated_robot_settings' => $validated_robot_settings,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the AIOSEO indexable table.
|
||||
*
|
||||
* @return bool Whether the AIOSEO table exists and has the structure we expect.
|
||||
*/
|
||||
public function validate_aioseo_table() {
|
||||
if ( ! $this->aioseo_helper->aioseo_exists() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$table = $this->aioseo_helper->get_table();
|
||||
$needed_data = $this->post_importing_action->get_needed_data();
|
||||
|
||||
$aioseo_columns = $this->wpdb->get_col(
|
||||
"SHOW COLUMNS FROM {$table}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
|
||||
0
|
||||
);
|
||||
|
||||
return $needed_data === \array_intersect( $needed_data, $aioseo_columns );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the AIOSEO settings from the options table.
|
||||
*
|
||||
* @return bool Whether the AIOSEO settings from the options table exist and have the structure we expect.
|
||||
*/
|
||||
public function validate_aioseo_settings() {
|
||||
foreach ( $this->settings_importing_actions as $settings_import_action ) {
|
||||
$aioseo_settings = \json_decode( \get_option( $settings_import_action->get_source_option_name(), '' ), true );
|
||||
|
||||
if ( ! $settings_import_action->isset_settings_tab( $aioseo_settings ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the AIOSEO robots settings from the options table.
|
||||
*
|
||||
* @return bool Whether the AIOSEO robots settings from the options table exist and have the structure we expect.
|
||||
*/
|
||||
public function validate_robot_settings() {
|
||||
if ( $this->validate_post_robot_settings() && $this->validate_default_robot_settings() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the post AIOSEO robots settings from the options table.
|
||||
*
|
||||
* @return bool Whether the post AIOSEO robots settings from the options table exist and have the structure we expect.
|
||||
*/
|
||||
public function validate_post_robot_settings() {
|
||||
$post_robot_mapping = $this->post_importing_action->enhance_mapping();
|
||||
// We're gonna validate against posttype robot settings only for posts, assuming the robot settings stay the same for other post types.
|
||||
$post_robot_mapping['subtype'] = 'post';
|
||||
|
||||
// Let's get both the aioseo_options and the aioseo_options_dynamic options.
|
||||
$aioseo_global_settings = $this->aioseo_helper->get_global_option();
|
||||
$aioseo_posts_settings = \json_decode( \get_option( $post_robot_mapping['option_name'], '' ), true );
|
||||
|
||||
$needed_robots_data = $this->post_importing_action->get_needed_robot_data();
|
||||
\array_push( $needed_robots_data, 'default', 'noindex' );
|
||||
|
||||
foreach ( $needed_robots_data as $robot_setting ) {
|
||||
// Validate against global settings.
|
||||
if ( ! isset( $aioseo_global_settings['searchAppearance']['advanced']['globalRobotsMeta'][ $robot_setting ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate against posttype settings.
|
||||
if ( ! isset( $aioseo_posts_settings['searchAppearance'][ $post_robot_mapping['type'] ][ $post_robot_mapping['subtype'] ]['advanced']['robotsMeta'][ $robot_setting ] ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the default AIOSEO robots settings for search appearance settings from the options table.
|
||||
*
|
||||
* @return bool Whether the AIOSEO robots settings for search appearance settings from the options table exist and have the structure we expect.
|
||||
*/
|
||||
public function validate_default_robot_settings() {
|
||||
|
||||
foreach ( $this->settings_importing_actions as $settings_import_action ) {
|
||||
$robot_setting_map = $settings_import_action->pluck_robot_setting_from_mapping();
|
||||
|
||||
// Some actions return empty robot settings, let's not validate against those.
|
||||
if ( ! empty( $robot_setting_map ) ) {
|
||||
$aioseo_settings = \json_decode( \get_option( $robot_setting_map['option_name'], '' ), true );
|
||||
|
||||
if ( ! isset( $aioseo_settings['searchAppearance'][ $robot_setting_map['type'] ][ $robot_setting_map['subtype'] ]['advanced']['robotsMeta']['default'] ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used nowhere. Exists to comply with the interface.
|
||||
*
|
||||
* @return int The limit.
|
||||
*/
|
||||
public function get_limit() {
|
||||
/**
|
||||
* Filter 'wpseo_aioseo_cleanup_limit' - Allow filtering the number of validations during each action pass.
|
||||
*
|
||||
* @api int The maximum number of validations.
|
||||
*/
|
||||
$limit = \apply_filters( 'wpseo_aioseo_validation_limit', 25 );
|
||||
|
||||
if ( ! \is_int( $limit ) || $limit < 1 ) {
|
||||
$limit = 25;
|
||||
}
|
||||
|
||||
return $limit;
|
||||
}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Importing;
|
||||
|
||||
use Yoast\WP\SEO\Conditionals\Updated_Importer_Framework_Conditional;
|
||||
use Yoast\WP\SEO\Config\Conflicting_Plugins;
|
||||
use Yoast\WP\SEO\Helpers\Import_Cursor_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Sanitization_Helper;
|
||||
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service;
|
||||
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Provider_Service;
|
||||
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Transformer_Service;
|
||||
use Yoast\WP\SEO\Services\Importing\Conflicting_Plugins_Service;
|
||||
|
||||
/**
|
||||
* Deactivates plug-ins that cause conflicts with Yoast SEO.
|
||||
*/
|
||||
class Deactivate_Conflicting_Plugins_Action extends Abstract_Aioseo_Importing_Action {
|
||||
|
||||
/**
|
||||
* The plugin the class deals with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const PLUGIN = 'conflicting-plugins';
|
||||
|
||||
/**
|
||||
* The type the class deals with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TYPE = 'deactivation';
|
||||
|
||||
/**
|
||||
* The replacevar handler.
|
||||
*
|
||||
* @var Aioseo_Replacevar_Service
|
||||
*/
|
||||
protected $replacevar_handler;
|
||||
|
||||
/**
|
||||
* Knows all plugins that might possibly conflict.
|
||||
*
|
||||
* @var Conflicting_Plugins_Service
|
||||
*/
|
||||
protected $conflicting_plugins;
|
||||
|
||||
/**
|
||||
* The list of conflicting plugins
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $detected_plugins;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param Import_Cursor_Helper $import_cursor The import cursor helper.
|
||||
* @param Options_Helper $options The options helper.
|
||||
* @param Sanitization_Helper $sanitization The sanitization helper.
|
||||
* @param Aioseo_Replacevar_Service $replacevar_handler The replacevar handler.
|
||||
* @param Aioseo_Robots_Provider_Service $robots_provider The robots provider service.
|
||||
* @param Aioseo_Robots_Transformer_Service $robots_transformer The robots transfomer service.
|
||||
* @param Conflicting_Plugins_Service $conflicting_plugins_service The Conflicting plugins Service.
|
||||
*/
|
||||
public function __construct(
|
||||
Import_Cursor_Helper $import_cursor,
|
||||
Options_Helper $options,
|
||||
Sanitization_Helper $sanitization,
|
||||
Aioseo_Replacevar_Service $replacevar_handler,
|
||||
Aioseo_Robots_Provider_Service $robots_provider,
|
||||
Aioseo_Robots_Transformer_Service $robots_transformer,
|
||||
Conflicting_Plugins_Service $conflicting_plugins_service
|
||||
) {
|
||||
parent::__construct( $import_cursor, $options, $sanitization, $replacevar_handler, $robots_provider, $robots_transformer );
|
||||
|
||||
$this->conflicting_plugins = $conflicting_plugins_service;
|
||||
$this->detected_plugins = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of conflicting plugins.
|
||||
*/
|
||||
public function get_total_unindexed() {
|
||||
return \count( $this->get_detected_plugins() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the updated importer framework is enabled.
|
||||
*
|
||||
* @return bool True if the updated importer framework is enabled.
|
||||
*/
|
||||
public function is_enabled() {
|
||||
$updated_importer_framework_conditional = \YoastSEO()->classes->get( Updated_Importer_Framework_Conditional::class );
|
||||
|
||||
return $updated_importer_framework_conditional->is_met();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate conflicting plugins.
|
||||
*/
|
||||
public function index() {
|
||||
$detected_plugins = $this->get_detected_plugins();
|
||||
$this->conflicting_plugins->deactivate_conflicting_plugins( $detected_plugins );
|
||||
|
||||
// We need to conform to the interface, so we report that no indexables were created.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function get_limit() {
|
||||
return \count( Conflicting_Plugins::all_plugins() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of unindexed objects up to a limit.
|
||||
*
|
||||
* @param int $limit The maximum.
|
||||
*
|
||||
* @return int The total number of unindexed objects.
|
||||
*/
|
||||
public function get_limited_unindexed_count( $limit ) {
|
||||
$count = \count( $this->get_detected_plugins() );
|
||||
return ( $count <= $limit ) ? $count : $limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all detected plugins.
|
||||
*
|
||||
* @return array The detected plugins.
|
||||
*/
|
||||
protected function get_detected_plugins() {
|
||||
// The active plugins won't change much. We can reuse the result for the duration of the request.
|
||||
if ( \count( $this->detected_plugins ) < 1 ) {
|
||||
$this->detected_plugins = $this->conflicting_plugins->detect_conflicting_plugins();
|
||||
}
|
||||
return $this->detected_plugins;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Importing;
|
||||
|
||||
use Yoast\WP\SEO\Actions\Indexing\Limited_Indexing_Action_Interface;
|
||||
|
||||
interface Importing_Action_Interface extends Importing_Indexation_Action_Interface, Limited_Indexing_Action_Interface {
|
||||
|
||||
/**
|
||||
* Returns the name of the plugin we import from.
|
||||
*
|
||||
* @return string The plugin name.
|
||||
*/
|
||||
public function get_plugin();
|
||||
|
||||
/**
|
||||
* Returns the type of data we import.
|
||||
*
|
||||
* @return string The type of data.
|
||||
*/
|
||||
public function get_type();
|
||||
|
||||
/**
|
||||
* Whether or not this action is capable of importing given a specific plugin and type.
|
||||
*
|
||||
* @param string|null $plugin The name of the plugin being imported.
|
||||
* @param string|null $type The component of the plugin being imported.
|
||||
*
|
||||
* @return bool True if the action can import the given plugin's data of the given type.
|
||||
*/
|
||||
public function is_compatible_with( $plugin = null, $type = null );
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Importing;
|
||||
|
||||
/**
|
||||
* Interface definition of reindexing action for indexables.
|
||||
*/
|
||||
interface Importing_Indexation_Action_Interface {
|
||||
|
||||
/**
|
||||
* Returns the total number of unindexed objects.
|
||||
*
|
||||
* @return int The total number of unindexed objects.
|
||||
*/
|
||||
public function get_total_unindexed();
|
||||
|
||||
/**
|
||||
* Indexes a number of objects.
|
||||
*
|
||||
* NOTE: ALWAYS use limits, this method is intended to be called multiple times over several requests.
|
||||
*
|
||||
* For indexing that requires JavaScript simply return the objects that should be indexed.
|
||||
*
|
||||
* @return array The reindexed objects.
|
||||
*/
|
||||
public function index();
|
||||
|
||||
/**
|
||||
* Returns the number of objects that will be indexed in a single indexing pass.
|
||||
*
|
||||
* @return int The limit.
|
||||
*/
|
||||
public function get_limit();
|
||||
}
|
@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Indexables;
|
||||
|
||||
use Yoast\WP\SEO\Surfaces\Meta_Surface;
|
||||
use Yoast\WP\SEO\Surfaces\Values\Meta;
|
||||
|
||||
/**
|
||||
* Get head action for indexables.
|
||||
*/
|
||||
class Indexable_Head_Action {
|
||||
|
||||
/**
|
||||
* Caches the output.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* The meta surface.
|
||||
*
|
||||
* @var Meta_Surface
|
||||
*/
|
||||
private $meta_surface;
|
||||
|
||||
/**
|
||||
* Indexable_Head_Action constructor.
|
||||
*
|
||||
* @param Meta_Surface $meta_surface The meta surface.
|
||||
*/
|
||||
public function __construct( Meta_Surface $meta_surface ) {
|
||||
$this->meta_surface = $meta_surface;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the head for a url.
|
||||
*
|
||||
* @param string $url The url to get the head for.
|
||||
*
|
||||
* @return object Object with head and status properties.
|
||||
*/
|
||||
public function for_url( $url ) {
|
||||
if ( $url === \trailingslashit( \get_home_url() ) ) {
|
||||
return $this->with_404_fallback( $this->with_cache( 'home_page' ) );
|
||||
}
|
||||
return $this->with_404_fallback( $this->with_cache( 'url', $url ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the head for a post.
|
||||
*
|
||||
* @param int $id The id.
|
||||
*
|
||||
* @return object Object with head and status properties.
|
||||
*/
|
||||
public function for_post( $id ) {
|
||||
return $this->with_404_fallback( $this->with_cache( 'post', $id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the head for a term.
|
||||
*
|
||||
* @param int $id The id.
|
||||
*
|
||||
* @return object Object with head and status properties.
|
||||
*/
|
||||
public function for_term( $id ) {
|
||||
return $this->with_404_fallback( $this->with_cache( 'term', $id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the head for an author.
|
||||
*
|
||||
* @param int $id The id.
|
||||
*
|
||||
* @return object Object with head and status properties.
|
||||
*/
|
||||
public function for_author( $id ) {
|
||||
return $this->with_404_fallback( $this->with_cache( 'author', $id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the head for a post type archive.
|
||||
*
|
||||
* @param int $type The id.
|
||||
*
|
||||
* @return object Object with head and status properties.
|
||||
*/
|
||||
public function for_post_type_archive( $type ) {
|
||||
return $this->with_404_fallback( $this->with_cache( 'post_type_archive', $type ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the head for the posts page.
|
||||
*
|
||||
* @return object Object with head and status properties.
|
||||
*/
|
||||
public function for_posts_page() {
|
||||
return $this->with_404_fallback( $this->with_cache( 'posts_page' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the head for the 404 page. Always sets the status to 404.
|
||||
*
|
||||
* @return object Object with head and status properties.
|
||||
*/
|
||||
public function for_404() {
|
||||
$meta = $this->with_cache( '404' );
|
||||
|
||||
if ( ! $meta ) {
|
||||
return (object) [
|
||||
'html' => '',
|
||||
'json' => [],
|
||||
'status' => 404,
|
||||
];
|
||||
}
|
||||
|
||||
$head = $meta->get_head();
|
||||
|
||||
return (object) [
|
||||
'html' => $head->html,
|
||||
'json' => $head->json,
|
||||
'status' => 404,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the head for a successful page load.
|
||||
*
|
||||
* @param object $head The calculated Yoast head.
|
||||
*
|
||||
* @return object The presentations and status code 200.
|
||||
*/
|
||||
protected function for_200( $head ) {
|
||||
return (object) [
|
||||
'html' => $head->html,
|
||||
'json' => $head->json,
|
||||
'status' => 200,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the head with 404 fallback
|
||||
*
|
||||
* @param Meta|false $meta The meta object.
|
||||
*
|
||||
* @return object The head response.
|
||||
*/
|
||||
protected function with_404_fallback( $meta ) {
|
||||
if ( $meta === false ) {
|
||||
return $this->for_404();
|
||||
}
|
||||
else {
|
||||
return $this->for_200( $meta->get_head() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a value from the meta surface cached.
|
||||
*
|
||||
* @param string $type The type of value to retrieve.
|
||||
* @param string $argument Optional. The argument for the value.
|
||||
*
|
||||
* @return Meta The meta object.
|
||||
*/
|
||||
protected function with_cache( $type, $argument = '' ) {
|
||||
if ( ! isset( $this->cache[ $type ][ $argument ] ) ) {
|
||||
$this->cache[ $type ][ $argument ] = \call_user_func( [ $this->meta_surface, "for_$type" ], $argument );
|
||||
}
|
||||
|
||||
return $this->cache[ $type ][ $argument ];
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Indexing;
|
||||
|
||||
/**
|
||||
* Base class of indexing actions.
|
||||
*/
|
||||
abstract class Abstract_Indexing_Action implements Indexation_Action_Interface, Limited_Indexing_Action_Interface {
|
||||
|
||||
/**
|
||||
* The transient name.
|
||||
*
|
||||
* This is a trick to force derived classes to define a transient themselves.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const UNINDEXED_COUNT_TRANSIENT = null;
|
||||
|
||||
/**
|
||||
* The transient cache key for limited counts.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const UNINDEXED_LIMITED_COUNT_TRANSIENT = self::UNINDEXED_COUNT_TRANSIENT . '_limited';
|
||||
|
||||
/**
|
||||
* Builds a query for selecting the ID's of unindexed posts.
|
||||
*
|
||||
* @param bool $limit The maximum number of post IDs to return.
|
||||
*
|
||||
* @return string The prepared query string.
|
||||
*/
|
||||
abstract protected function get_select_query( $limit );
|
||||
|
||||
/**
|
||||
* Builds a query for counting the number of unindexed posts.
|
||||
*
|
||||
* @return string The prepared query string.
|
||||
*/
|
||||
abstract protected function get_count_query();
|
||||
|
||||
/**
|
||||
* Returns a limited number of unindexed posts.
|
||||
*
|
||||
* @param int $limit Limit the maximum number of unindexed posts that are counted.
|
||||
*
|
||||
* @return int The limited number of unindexed posts. 0 if the query fails.
|
||||
*/
|
||||
public function get_limited_unindexed_count( $limit ) {
|
||||
$transient = \get_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT );
|
||||
if ( $transient !== false ) {
|
||||
return (int) $transient;
|
||||
}
|
||||
|
||||
\set_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT, 0, ( \MINUTE_IN_SECONDS * 15 ) );
|
||||
|
||||
$query = $this->get_select_query( $limit );
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_count_query returns a prepared query.
|
||||
$unindexed_object_ids = $this->wpdb->get_col( $query );
|
||||
$count = (int) \count( $unindexed_object_ids );
|
||||
|
||||
\set_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT, $count, ( \MINUTE_IN_SECONDS * 15 ) );
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of unindexed posts.
|
||||
*
|
||||
* @return int|false The total number of unindexed posts. False if the query fails.
|
||||
*/
|
||||
public function get_total_unindexed() {
|
||||
$transient = \get_transient( static::UNINDEXED_COUNT_TRANSIENT );
|
||||
if ( $transient !== false ) {
|
||||
return (int) $transient;
|
||||
}
|
||||
|
||||
// Store transient before doing the query so multiple requests won't make multiple queries.
|
||||
// Only store this for 15 minutes to ensure that if the query doesn't complete a wrong count is not kept too long.
|
||||
\set_transient( static::UNINDEXED_COUNT_TRANSIENT, 0, ( \MINUTE_IN_SECONDS * 15 ) );
|
||||
|
||||
$query = $this->get_count_query();
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_count_query returns a prepared query.
|
||||
$count = $this->wpdb->get_var( $query );
|
||||
|
||||
if ( \is_null( $count ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
\set_transient( static::UNINDEXED_COUNT_TRANSIENT, $count, \DAY_IN_SECONDS );
|
||||
|
||||
/**
|
||||
* Action: 'wpseo_indexables_unindexed_calculated' - sets an option to timestamp when there are no unindexed indexables left.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
\do_action( 'wpseo_indexables_unindexed_calculated', static::UNINDEXED_COUNT_TRANSIENT, $count );
|
||||
|
||||
return (int) $count;
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Indexing;
|
||||
|
||||
use wpdb;
|
||||
use Yoast\WP\SEO\Builders\Indexable_Link_Builder;
|
||||
use Yoast\WP\SEO\Models\SEO_Links;
|
||||
use Yoast\WP\SEO\Repositories\Indexable_Repository;
|
||||
|
||||
/**
|
||||
* Reindexing action for link indexables.
|
||||
*/
|
||||
abstract class Abstract_Link_Indexing_Action extends Abstract_Indexing_Action {
|
||||
|
||||
/**
|
||||
* The link builder.
|
||||
*
|
||||
* @var Indexable_Link_Builder
|
||||
*/
|
||||
protected $link_builder;
|
||||
|
||||
/**
|
||||
* The indexable repository.
|
||||
*
|
||||
* @var Indexable_Repository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* The WordPress database instance.
|
||||
*
|
||||
* @var wpdb
|
||||
*/
|
||||
protected $wpdb;
|
||||
|
||||
/**
|
||||
* Indexable_Post_Indexing_Action constructor
|
||||
*
|
||||
* @param Indexable_Link_Builder $link_builder The indexable link builder.
|
||||
* @param Indexable_Repository $repository The indexable repository.
|
||||
* @param wpdb $wpdb The WordPress database instance.
|
||||
*/
|
||||
public function __construct(
|
||||
Indexable_Link_Builder $link_builder,
|
||||
Indexable_Repository $repository,
|
||||
wpdb $wpdb
|
||||
) {
|
||||
$this->link_builder = $link_builder;
|
||||
$this->repository = $repository;
|
||||
$this->wpdb = $wpdb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds links for indexables which haven't had their links indexed yet.
|
||||
*
|
||||
* @return SEO_Links[] The created SEO links.
|
||||
*/
|
||||
public function index() {
|
||||
$objects = $this->get_objects();
|
||||
|
||||
$indexables = [];
|
||||
foreach ( $objects as $object ) {
|
||||
$indexable = $this->repository->find_by_id_and_type( $object->id, $object->type );
|
||||
if ( $indexable ) {
|
||||
$this->link_builder->build( $indexable, $object->content );
|
||||
$indexable->save();
|
||||
|
||||
$indexables[] = $indexable;
|
||||
}
|
||||
}
|
||||
|
||||
if ( \count( $indexables ) > 0 ) {
|
||||
\delete_transient( static::UNINDEXED_COUNT_TRANSIENT );
|
||||
\delete_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT );
|
||||
}
|
||||
|
||||
return $indexables;
|
||||
}
|
||||
|
||||
/**
|
||||
* In the case of term-links and post-links we want to use the total unindexed count, because using
|
||||
* the limited unindexed count actually leads to worse performance.
|
||||
*
|
||||
* @param int|bool $limit Unused.
|
||||
*
|
||||
* @return int The total number of unindexed links.
|
||||
*/
|
||||
public function get_limited_unindexed_count( $limit = false ) {
|
||||
return $this->get_total_unindexed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of texts that will be indexed in a single link indexing pass.
|
||||
*
|
||||
* @return int The limit.
|
||||
*/
|
||||
public function get_limit() {
|
||||
/**
|
||||
* Filter 'wpseo_link_indexing_limit' - Allow filtering the number of texts indexed during each link indexing pass.
|
||||
*
|
||||
* @api int The maximum number of texts indexed.
|
||||
*/
|
||||
return \apply_filters( 'wpseo_link_indexing_limit', 5 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns objects to be indexed.
|
||||
*
|
||||
* @return array Objects to be indexed, should be an array of objects with object_id, object_type and content.
|
||||
*/
|
||||
abstract protected function get_objects();
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Indexing;
|
||||
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
use Yoast\WP\SEO\Repositories\Indexable_Repository;
|
||||
|
||||
/**
|
||||
* General reindexing action for indexables.
|
||||
*/
|
||||
class Indexable_General_Indexation_Action implements Indexation_Action_Interface, Limited_Indexing_Action_Interface {
|
||||
|
||||
/**
|
||||
* The transient cache key.
|
||||
*/
|
||||
const UNINDEXED_COUNT_TRANSIENT = 'wpseo_total_unindexed_general_items';
|
||||
|
||||
/**
|
||||
* Represents the indexables repository.
|
||||
*
|
||||
* @var Indexable_Repository
|
||||
*/
|
||||
protected $indexable_repository;
|
||||
|
||||
/**
|
||||
* Indexable_General_Indexation_Action constructor.
|
||||
*
|
||||
* @param Indexable_Repository $indexable_repository The indexables repository.
|
||||
*/
|
||||
public function __construct( Indexable_Repository $indexable_repository ) {
|
||||
$this->indexable_repository = $indexable_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of unindexed objects.
|
||||
*
|
||||
* @return int The total number of unindexed objects.
|
||||
*/
|
||||
public function get_total_unindexed() {
|
||||
$transient = \get_transient( static::UNINDEXED_COUNT_TRANSIENT );
|
||||
if ( $transient !== false ) {
|
||||
return (int) $transient;
|
||||
}
|
||||
|
||||
$indexables_to_create = $this->query();
|
||||
|
||||
$result = \count( $indexables_to_create );
|
||||
|
||||
\set_transient( static::UNINDEXED_COUNT_TRANSIENT, $result, \DAY_IN_SECONDS );
|
||||
|
||||
/**
|
||||
* Action: 'wpseo_indexables_unindexed_calculated' - sets an option to timestamp when there are no unindexed indexables left.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
\do_action( 'wpseo_indexables_unindexed_calculated', static::UNINDEXED_COUNT_TRANSIENT, $result );
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a limited number of unindexed posts.
|
||||
*
|
||||
* @param int $limit Limit the maximum number of unindexed posts that are counted.
|
||||
*
|
||||
* @return int|false The limited number of unindexed posts. False if the query fails.
|
||||
*/
|
||||
public function get_limited_unindexed_count( $limit ) {
|
||||
return $this->get_total_unindexed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates indexables for unindexed system pages, the date archive, and the homepage.
|
||||
*
|
||||
* @return Indexable[] The created indexables.
|
||||
*/
|
||||
public function index() {
|
||||
$indexables = [];
|
||||
$indexables_to_create = $this->query();
|
||||
|
||||
if ( isset( $indexables_to_create['404'] ) ) {
|
||||
$indexables[] = $this->indexable_repository->find_for_system_page( '404' );
|
||||
}
|
||||
|
||||
if ( isset( $indexables_to_create['search'] ) ) {
|
||||
$indexables[] = $this->indexable_repository->find_for_system_page( 'search-result' );
|
||||
}
|
||||
|
||||
if ( isset( $indexables_to_create['date_archive'] ) ) {
|
||||
$indexables[] = $this->indexable_repository->find_for_date_archive();
|
||||
}
|
||||
if ( isset( $indexables_to_create['home_page'] ) ) {
|
||||
$indexables[] = $this->indexable_repository->find_for_home_page();
|
||||
}
|
||||
|
||||
\set_transient( static::UNINDEXED_COUNT_TRANSIENT, 0, \DAY_IN_SECONDS );
|
||||
|
||||
return $indexables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of objects that will be indexed in a single indexing pass.
|
||||
*
|
||||
* @return int The limit.
|
||||
*/
|
||||
public function get_limit() {
|
||||
// This matches the maximum number of indexables created by this action.
|
||||
return 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check which indexables already exist and return the values of the ones to create.
|
||||
*
|
||||
* @return array The indexable types to create.
|
||||
*/
|
||||
private function query() {
|
||||
$indexables_to_create = [];
|
||||
if ( ! $this->indexable_repository->find_for_system_page( '404', false ) ) {
|
||||
$indexables_to_create['404'] = true;
|
||||
}
|
||||
|
||||
if ( ! $this->indexable_repository->find_for_system_page( 'search-result', false ) ) {
|
||||
$indexables_to_create['search'] = true;
|
||||
}
|
||||
|
||||
if ( ! $this->indexable_repository->find_for_date_archive( false ) ) {
|
||||
$indexables_to_create['date_archive'] = true;
|
||||
}
|
||||
|
||||
$need_home_page_indexable = ( (int) \get_option( 'page_on_front' ) === 0 && \get_option( 'show_on_front' ) === 'posts' );
|
||||
|
||||
if ( $need_home_page_indexable && ! $this->indexable_repository->find_for_home_page( false ) ) {
|
||||
$indexables_to_create['home_page'] = true;
|
||||
}
|
||||
|
||||
return $indexables_to_create;
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Indexing;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Indexable_Helper;
|
||||
|
||||
/**
|
||||
* Indexing action to call when the indexable indexing process is completed.
|
||||
*/
|
||||
class Indexable_Indexing_Complete_Action {
|
||||
|
||||
/**
|
||||
* The options helper.
|
||||
*
|
||||
* @var Indexable_Helper
|
||||
*/
|
||||
protected $indexable_helper;
|
||||
|
||||
/**
|
||||
* Indexable_Indexing_Complete_Action constructor.
|
||||
*
|
||||
* @param Indexable_Helper $indexable_helper The indexable helper.
|
||||
*/
|
||||
public function __construct( Indexable_Helper $indexable_helper ) {
|
||||
$this->indexable_helper = $indexable_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps up the indexing process.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function complete() {
|
||||
$this->indexable_helper->finish_indexing();
|
||||
}
|
||||
}
|
@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Indexing;
|
||||
|
||||
use wpdb;
|
||||
use Yoast\WP\Lib\Model;
|
||||
use Yoast\WP\SEO\Helpers\Post_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
use Yoast\WP\SEO\Repositories\Indexable_Repository;
|
||||
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
|
||||
|
||||
/**
|
||||
* Reindexing action for post indexables.
|
||||
*/
|
||||
class Indexable_Post_Indexation_Action extends Abstract_Indexing_Action {
|
||||
|
||||
/**
|
||||
* The transient cache key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const UNINDEXED_COUNT_TRANSIENT = 'wpseo_total_unindexed_posts';
|
||||
|
||||
/**
|
||||
* The transient cache key for limited counts.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const UNINDEXED_LIMITED_COUNT_TRANSIENT = self::UNINDEXED_COUNT_TRANSIENT . '_limited';
|
||||
|
||||
/**
|
||||
* The post type helper.
|
||||
*
|
||||
* @var Post_Type_Helper
|
||||
*/
|
||||
protected $post_type_helper;
|
||||
|
||||
/**
|
||||
* The post helper.
|
||||
*
|
||||
* @var Post_Helper
|
||||
*/
|
||||
protected $post_helper;
|
||||
|
||||
/**
|
||||
* The indexable repository.
|
||||
*
|
||||
* @var Indexable_Repository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* The WordPress database instance.
|
||||
*
|
||||
* @var wpdb
|
||||
*/
|
||||
protected $wpdb;
|
||||
|
||||
/**
|
||||
* The latest version of Post Indexables.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $version;
|
||||
|
||||
/**
|
||||
* Indexable_Post_Indexing_Action constructor
|
||||
*
|
||||
* @param Post_Type_Helper $post_type_helper The post type helper.
|
||||
* @param Indexable_Repository $repository The indexable repository.
|
||||
* @param wpdb $wpdb The WordPress database instance.
|
||||
* @param Indexable_Builder_Versions $builder_versions The latest versions for each Indexable type.
|
||||
* @param Post_Helper $post_helper The post helper.
|
||||
*/
|
||||
public function __construct(
|
||||
Post_Type_Helper $post_type_helper,
|
||||
Indexable_Repository $repository,
|
||||
wpdb $wpdb,
|
||||
Indexable_Builder_Versions $builder_versions,
|
||||
Post_Helper $post_helper
|
||||
) {
|
||||
$this->post_type_helper = $post_type_helper;
|
||||
$this->repository = $repository;
|
||||
$this->wpdb = $wpdb;
|
||||
$this->version = $builder_versions->get_latest_version_for_type( 'post' );
|
||||
$this->post_helper = $post_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates indexables for unindexed posts.
|
||||
*
|
||||
* @return Indexable[] The created indexables.
|
||||
*/
|
||||
public function index() {
|
||||
$query = $this->get_select_query( $this->get_limit() );
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query.
|
||||
$post_ids = $this->wpdb->get_col( $query );
|
||||
|
||||
$indexables = [];
|
||||
foreach ( $post_ids as $post_id ) {
|
||||
$indexables[] = $this->repository->find_by_id_and_type( (int) $post_id, 'post' );
|
||||
}
|
||||
|
||||
if ( \count( $indexables ) > 0 ) {
|
||||
\delete_transient( static::UNINDEXED_COUNT_TRANSIENT );
|
||||
\delete_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT );
|
||||
}
|
||||
|
||||
return $indexables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of posts that will be indexed in a single indexing pass.
|
||||
*
|
||||
* @return int The limit.
|
||||
*/
|
||||
public function get_limit() {
|
||||
/**
|
||||
* Filter 'wpseo_post_indexation_limit' - Allow filtering the amount of posts indexed during each indexing pass.
|
||||
*
|
||||
* @api int The maximum number of posts indexed.
|
||||
*/
|
||||
$limit = \apply_filters( 'wpseo_post_indexation_limit', 25 );
|
||||
|
||||
if ( ! \is_int( $limit ) || $limit < 1 ) {
|
||||
$limit = 25;
|
||||
}
|
||||
|
||||
return $limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a query for counting the number of unindexed posts.
|
||||
*
|
||||
* @return string The prepared query string.
|
||||
*/
|
||||
protected function get_count_query() {
|
||||
$indexable_table = Model::get_table_name( 'Indexable' );
|
||||
|
||||
$post_types = $this->post_type_helper->get_indexable_post_types();
|
||||
$excluded_post_statuses = $this->post_helper->get_excluded_post_statuses();
|
||||
$replacements = \array_merge(
|
||||
$post_types,
|
||||
$excluded_post_statuses
|
||||
);
|
||||
|
||||
$replacements[] = $this->version;
|
||||
|
||||
// Warning: If this query is changed, makes sure to update the query in get_select_query as well.
|
||||
// @phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
return $this->wpdb->prepare(
|
||||
"
|
||||
SELECT COUNT(P.ID)
|
||||
FROM {$this->wpdb->posts} AS P
|
||||
WHERE P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $post_types ), '%s' ) ) . ')
|
||||
AND P.post_status NOT IN (' . \implode( ', ', \array_fill( 0, \count( $excluded_post_statuses ), '%s' ) ) . ")
|
||||
AND P.ID not in (
|
||||
SELECT I.object_id from $indexable_table as I
|
||||
WHERE I.object_type = 'post'
|
||||
AND I.version = %d )",
|
||||
$replacements
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a query for selecting the ID's of unindexed posts.
|
||||
*
|
||||
* @param bool $limit The maximum number of post IDs to return.
|
||||
*
|
||||
* @return string The prepared query string.
|
||||
*/
|
||||
protected function get_select_query( $limit = false ) {
|
||||
$indexable_table = Model::get_table_name( 'Indexable' );
|
||||
|
||||
$post_types = $this->post_type_helper->get_indexable_post_types();
|
||||
$excluded_post_statuses = $this->post_helper->get_excluded_post_statuses();
|
||||
$replacements = \array_merge(
|
||||
$post_types,
|
||||
$excluded_post_statuses
|
||||
);
|
||||
$replacements[] = $this->version;
|
||||
|
||||
$limit_query = '';
|
||||
if ( $limit ) {
|
||||
$limit_query = 'LIMIT %d';
|
||||
$replacements[] = $limit;
|
||||
}
|
||||
|
||||
// Warning: If this query is changed, makes sure to update the query in get_count_query as well.
|
||||
// @phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
return $this->wpdb->prepare(
|
||||
"
|
||||
SELECT P.ID
|
||||
FROM {$this->wpdb->posts} AS P
|
||||
WHERE P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $post_types ), '%s' ) ) . ')
|
||||
AND P.post_status NOT IN (' . \implode( ', ', \array_fill( 0, \count( $excluded_post_statuses ), '%s' ) ) . ")
|
||||
AND P.ID not in (
|
||||
SELECT I.object_id from $indexable_table as I
|
||||
WHERE I.object_type = 'post'
|
||||
AND I.version = %d )
|
||||
$limit_query",
|
||||
$replacements
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Indexing;
|
||||
|
||||
use Yoast\WP\SEO\Builders\Indexable_Builder;
|
||||
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
use Yoast\WP\SEO\Repositories\Indexable_Repository;
|
||||
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
|
||||
|
||||
/**
|
||||
* Reindexing action for post type archive indexables.
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
*/
|
||||
class Indexable_Post_Type_Archive_Indexation_Action implements Indexation_Action_Interface, Limited_Indexing_Action_Interface {
|
||||
|
||||
|
||||
/**
|
||||
* The transient cache key.
|
||||
*/
|
||||
const UNINDEXED_COUNT_TRANSIENT = 'wpseo_total_unindexed_post_type_archives';
|
||||
|
||||
/**
|
||||
* The post type helper.
|
||||
*
|
||||
* @var Post_Type_Helper
|
||||
*/
|
||||
protected $post_type;
|
||||
|
||||
/**
|
||||
* The indexable repository.
|
||||
*
|
||||
* @var Indexable_Repository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* The indexable builder.
|
||||
*
|
||||
* @var Indexable_Builder
|
||||
*/
|
||||
protected $builder;
|
||||
|
||||
/**
|
||||
* The current version of the post type archive indexable builder.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $version;
|
||||
|
||||
/**
|
||||
* Indexation_Post_Type_Archive_Action constructor.
|
||||
*
|
||||
* @param Indexable_Repository $repository The indexable repository.
|
||||
* @param Indexable_Builder $builder The indexable builder.
|
||||
* @param Post_Type_Helper $post_type The post type helper.
|
||||
* @param Indexable_Builder_Versions $versions The current versions of all indexable builders.
|
||||
*/
|
||||
public function __construct(
|
||||
Indexable_Repository $repository,
|
||||
Indexable_Builder $builder,
|
||||
Post_Type_Helper $post_type,
|
||||
Indexable_Builder_Versions $versions
|
||||
) {
|
||||
$this->repository = $repository;
|
||||
$this->builder = $builder;
|
||||
$this->post_type = $post_type;
|
||||
$this->version = $versions->get_latest_version_for_type( 'post-type-archive' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of unindexed post type archives.
|
||||
*
|
||||
* @param int $limit Limit the number of counted objects.
|
||||
*
|
||||
* @return int The total number of unindexed post type archives.
|
||||
*/
|
||||
public function get_total_unindexed( $limit = false ) {
|
||||
$transient = \get_transient( static::UNINDEXED_COUNT_TRANSIENT );
|
||||
if ( $transient !== false ) {
|
||||
return (int) $transient;
|
||||
}
|
||||
|
||||
\set_transient( static::UNINDEXED_COUNT_TRANSIENT, 0, \DAY_IN_SECONDS );
|
||||
|
||||
$result = \count( $this->get_unindexed_post_type_archives( $limit ) );
|
||||
|
||||
\set_transient( static::UNINDEXED_COUNT_TRANSIENT, $result, \DAY_IN_SECONDS );
|
||||
|
||||
/**
|
||||
* Action: 'wpseo_indexables_unindexed_calculated' - sets an option to timestamp when there are no unindexed indexables left.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
\do_action( 'wpseo_indexables_unindexed_calculated', static::UNINDEXED_COUNT_TRANSIENT, $result );
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates indexables for post type archives.
|
||||
*
|
||||
* @return Indexable[] The created indexables.
|
||||
*/
|
||||
public function index() {
|
||||
$unindexed_post_type_archives = $this->get_unindexed_post_type_archives( $this->get_limit() );
|
||||
|
||||
$indexables = [];
|
||||
foreach ( $unindexed_post_type_archives as $post_type_archive ) {
|
||||
$indexables[] = $this->builder->build_for_post_type_archive( $post_type_archive );
|
||||
}
|
||||
|
||||
if ( \count( $indexables ) > 0 ) {
|
||||
\delete_transient( static::UNINDEXED_COUNT_TRANSIENT );
|
||||
}
|
||||
|
||||
return $indexables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of post type archives that will be indexed in a single indexing pass.
|
||||
*
|
||||
* @return int The limit.
|
||||
*/
|
||||
public function get_limit() {
|
||||
/**
|
||||
* Filter 'wpseo_post_type_archive_indexation_limit' - Allow filtering the number of posts indexed during each indexing pass.
|
||||
*
|
||||
* @api int The maximum number of posts indexed.
|
||||
*/
|
||||
$limit = \apply_filters( 'wpseo_post_type_archive_indexation_limit', 25 );
|
||||
|
||||
if ( ! \is_int( $limit ) || $limit < 1 ) {
|
||||
$limit = 25;
|
||||
}
|
||||
|
||||
return $limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of post types for which no indexable for its archive page has been made yet.
|
||||
*
|
||||
* @param int|false $limit Limit the number of retrieved indexables to this number.
|
||||
*
|
||||
* @return array The list of post types for which no indexable for its archive page has been made yet.
|
||||
*/
|
||||
protected function get_unindexed_post_type_archives( $limit = false ) {
|
||||
$post_types_with_archive_pages = $this->get_post_types_with_archive_pages();
|
||||
$indexed_post_types = $this->get_indexed_post_type_archives();
|
||||
|
||||
$unindexed_post_types = \array_diff( $post_types_with_archive_pages, $indexed_post_types );
|
||||
|
||||
if ( $limit ) {
|
||||
return \array_slice( $unindexed_post_types, 0, $limit );
|
||||
}
|
||||
|
||||
return $unindexed_post_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of all the post types that have archive pages.
|
||||
*
|
||||
* @return array The list of names of all post types that have archive pages.
|
||||
*/
|
||||
protected function get_post_types_with_archive_pages() {
|
||||
// We only want to index archive pages of public post types that have them.
|
||||
$post_types_with_archive = $this->post_type->get_indexable_post_archives();
|
||||
|
||||
// We only need the post type names, not the objects.
|
||||
$post_types = [];
|
||||
foreach ( $post_types_with_archive as $post_type_with_archive ) {
|
||||
$post_types[] = $post_type_with_archive->name;
|
||||
}
|
||||
|
||||
return $post_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of post type names for which an archive indexable exists.
|
||||
*
|
||||
* @return array The list of names of post types with unindexed archive pages.
|
||||
*/
|
||||
protected function get_indexed_post_type_archives() {
|
||||
$results = $this->repository->query()
|
||||
->select( 'object_sub_type' )
|
||||
->where( 'object_type', 'post-type-archive' )
|
||||
->where_equal( 'version', $this->version )
|
||||
->find_array();
|
||||
|
||||
if ( $results === false ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$callback = static function( $result ) {
|
||||
return $result['object_sub_type'];
|
||||
};
|
||||
|
||||
return \array_map( $callback, $results );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a limited number of unindexed posts.
|
||||
*
|
||||
* @param int $limit Limit the maximum number of unindexed posts that are counted.
|
||||
*
|
||||
* @return int|false The limited number of unindexed posts. False if the query fails.
|
||||
*/
|
||||
public function get_limited_unindexed_count( $limit ) {
|
||||
return $this->get_total_unindexed( $limit );
|
||||
}
|
||||
}
|
@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Indexing;
|
||||
|
||||
use wpdb;
|
||||
use Yoast\WP\Lib\Model;
|
||||
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
use Yoast\WP\SEO\Repositories\Indexable_Repository;
|
||||
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
|
||||
|
||||
/**
|
||||
* Reindexing action for term indexables.
|
||||
*/
|
||||
class Indexable_Term_Indexation_Action extends Abstract_Indexing_Action {
|
||||
|
||||
/**
|
||||
* The transient cache key.
|
||||
*/
|
||||
const UNINDEXED_COUNT_TRANSIENT = 'wpseo_total_unindexed_terms';
|
||||
|
||||
/**
|
||||
* The transient cache key for limited counts.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const UNINDEXED_LIMITED_COUNT_TRANSIENT = self::UNINDEXED_COUNT_TRANSIENT . '_limited';
|
||||
|
||||
/**
|
||||
* The post type helper.
|
||||
*
|
||||
* @var Taxonomy_Helper
|
||||
*/
|
||||
protected $taxonomy;
|
||||
|
||||
/**
|
||||
* The indexable repository.
|
||||
*
|
||||
* @var Indexable_Repository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* The WordPress database instance.
|
||||
*
|
||||
* @var wpdb
|
||||
*/
|
||||
protected $wpdb;
|
||||
|
||||
/**
|
||||
* The latest version of the Indexable term builder
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $version;
|
||||
|
||||
/**
|
||||
* Indexable_Term_Indexation_Action constructor
|
||||
*
|
||||
* @param Taxonomy_Helper $taxonomy The taxonomy helper.
|
||||
* @param Indexable_Repository $repository The indexable repository.
|
||||
* @param wpdb $wpdb The WordPress database instance.
|
||||
* @param Indexable_Builder_Versions $builder_versions The latest versions of all indexable builders.
|
||||
*/
|
||||
public function __construct(
|
||||
Taxonomy_Helper $taxonomy,
|
||||
Indexable_Repository $repository,
|
||||
wpdb $wpdb,
|
||||
Indexable_Builder_Versions $builder_versions
|
||||
) {
|
||||
$this->taxonomy = $taxonomy;
|
||||
$this->repository = $repository;
|
||||
$this->wpdb = $wpdb;
|
||||
$this->version = $builder_versions->get_latest_version_for_type( 'term' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates indexables for unindexed terms.
|
||||
*
|
||||
* @return Indexable[] The created indexables.
|
||||
*/
|
||||
public function index() {
|
||||
$query = $this->get_select_query( $this->get_limit() );
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query.
|
||||
$term_ids = $this->wpdb->get_col( $query );
|
||||
|
||||
$indexables = [];
|
||||
foreach ( $term_ids as $term_id ) {
|
||||
$indexables[] = $this->repository->find_by_id_and_type( (int) $term_id, 'term' );
|
||||
}
|
||||
|
||||
if ( \count( $indexables ) > 0 ) {
|
||||
\delete_transient( static::UNINDEXED_COUNT_TRANSIENT );
|
||||
\delete_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT );
|
||||
}
|
||||
|
||||
return $indexables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of terms that will be indexed in a single indexing pass.
|
||||
*
|
||||
* @return int The limit.
|
||||
*/
|
||||
public function get_limit() {
|
||||
/**
|
||||
* Filter 'wpseo_term_indexation_limit' - Allow filtering the number of terms indexed during each indexing pass.
|
||||
*
|
||||
* @api int The maximum number of terms indexed.
|
||||
*/
|
||||
$limit = \apply_filters( 'wpseo_term_indexation_limit', 25 );
|
||||
|
||||
if ( ! \is_int( $limit ) || $limit < 1 ) {
|
||||
$limit = 25;
|
||||
}
|
||||
|
||||
return $limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a query for counting the number of unindexed terms.
|
||||
*
|
||||
* @return string The prepared query string.
|
||||
*/
|
||||
protected function get_count_query() {
|
||||
$indexable_table = Model::get_table_name( 'Indexable' );
|
||||
$taxonomy_table = $this->wpdb->term_taxonomy;
|
||||
$public_taxonomies = $this->taxonomy->get_indexable_taxonomies();
|
||||
|
||||
$taxonomies_placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) );
|
||||
|
||||
$replacements = [ $this->version ];
|
||||
\array_push( $replacements, ...$public_taxonomies );
|
||||
|
||||
// Warning: If this query is changed, makes sure to update the query in get_count_query as well.
|
||||
return $this->wpdb->prepare(
|
||||
"
|
||||
SELECT COUNT(term_id)
|
||||
FROM {$taxonomy_table} AS T
|
||||
LEFT JOIN $indexable_table AS I
|
||||
ON T.term_id = I.object_id
|
||||
AND I.object_type = 'term'
|
||||
AND I.version = %d
|
||||
WHERE I.object_id IS NULL
|
||||
AND taxonomy IN ($taxonomies_placeholders)",
|
||||
$replacements
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a query for selecting the ID's of unindexed terms.
|
||||
*
|
||||
* @param bool $limit The maximum number of term IDs to return.
|
||||
*
|
||||
* @return string The prepared query string.
|
||||
*/
|
||||
protected function get_select_query( $limit = false ) {
|
||||
$indexable_table = Model::get_table_name( 'Indexable' );
|
||||
$taxonomy_table = $this->wpdb->term_taxonomy;
|
||||
$public_taxonomies = $this->taxonomy->get_indexable_taxonomies();
|
||||
$placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) );
|
||||
|
||||
$replacements = [ $this->version ];
|
||||
\array_push( $replacements, ...$public_taxonomies );
|
||||
|
||||
$limit_query = '';
|
||||
if ( $limit ) {
|
||||
$limit_query = 'LIMIT %d';
|
||||
$replacements[] = $limit;
|
||||
}
|
||||
|
||||
// Warning: If this query is changed, makes sure to update the query in get_count_query as well.
|
||||
return $this->wpdb->prepare(
|
||||
"
|
||||
SELECT term_id
|
||||
FROM {$taxonomy_table} AS T
|
||||
LEFT JOIN $indexable_table AS I
|
||||
ON T.term_id = I.object_id
|
||||
AND I.object_type = 'term'
|
||||
AND I.version = %d
|
||||
WHERE I.object_id IS NULL
|
||||
AND taxonomy IN ($placeholders)
|
||||
$limit_query",
|
||||
$replacements
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Indexing;
|
||||
|
||||
/**
|
||||
* Interface definition of reindexing action for indexables.
|
||||
*/
|
||||
interface Indexation_Action_Interface {
|
||||
|
||||
/**
|
||||
* Returns the total number of unindexed objects.
|
||||
*
|
||||
* @return int The total number of unindexed objects.
|
||||
*/
|
||||
public function get_total_unindexed();
|
||||
|
||||
/**
|
||||
* Indexes a number of objects.
|
||||
*
|
||||
* NOTE: ALWAYS use limits, this method is intended to be called multiple times over several requests.
|
||||
*
|
||||
* For indexing that requires JavaScript simply return the objects that should be indexed.
|
||||
*
|
||||
* @return array The reindexed objects.
|
||||
*/
|
||||
public function index();
|
||||
|
||||
/**
|
||||
* Returns the number of objects that will be indexed in a single indexing pass.
|
||||
*
|
||||
* @return int The limit.
|
||||
*/
|
||||
public function get_limit();
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Indexing;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Indexing_Helper;
|
||||
|
||||
/**
|
||||
* Indexing action to call when the indexing is completed.
|
||||
*/
|
||||
class Indexing_Complete_Action {
|
||||
|
||||
/**
|
||||
* The indexing helper.
|
||||
*
|
||||
* @var Indexing_Helper
|
||||
*/
|
||||
protected $indexing_helper;
|
||||
|
||||
/**
|
||||
* Indexing_Complete_Action constructor.
|
||||
*
|
||||
* @param Indexing_Helper $indexing_helper The indexing helper.
|
||||
*/
|
||||
public function __construct( Indexing_Helper $indexing_helper ) {
|
||||
$this->indexing_helper = $indexing_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps up the indexing process.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function complete() {
|
||||
$this->indexing_helper->complete();
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Indexing;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Indexing_Helper;
|
||||
|
||||
/**
|
||||
* Class Indexing_Prepare_Action.
|
||||
*
|
||||
* Action for preparing the indexing routine.
|
||||
*/
|
||||
class Indexing_Prepare_Action {
|
||||
|
||||
/**
|
||||
* The indexing helper.
|
||||
*
|
||||
* @var Indexing_Helper
|
||||
*/
|
||||
protected $indexing_helper;
|
||||
|
||||
/**
|
||||
* Action for preparing the indexing routine.
|
||||
*
|
||||
* @param Indexing_Helper $indexing_helper The indexing helper.
|
||||
*/
|
||||
public function __construct(
|
||||
Indexing_Helper $indexing_helper
|
||||
) {
|
||||
$this->indexing_helper = $indexing_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the indexing routine.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function prepare() {
|
||||
$this->indexing_helper->prepare();
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Indexing;
|
||||
|
||||
/**
|
||||
* Interface definition of a reindexing action for indexables that have a limited unindexed count.
|
||||
*/
|
||||
interface Limited_Indexing_Action_Interface {
|
||||
|
||||
/**
|
||||
* Returns a limited number of unindexed posts.
|
||||
*
|
||||
* @param int $limit Limit the maximum number of unindexed posts that are counted.
|
||||
*
|
||||
* @return int|false The limited number of unindexed posts. False if the query fails.
|
||||
*/
|
||||
public function get_limited_unindexed_count( $limit );
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Indexing;
|
||||
|
||||
use Yoast\WP\Lib\Model;
|
||||
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
|
||||
|
||||
/**
|
||||
* Reindexing action for post link indexables.
|
||||
*/
|
||||
class Post_Link_Indexing_Action extends Abstract_Link_Indexing_Action {
|
||||
|
||||
/**
|
||||
* The transient name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const UNINDEXED_COUNT_TRANSIENT = 'wpseo_unindexed_post_link_count';
|
||||
|
||||
/**
|
||||
* The transient cache key for limited counts.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const UNINDEXED_LIMITED_COUNT_TRANSIENT = self::UNINDEXED_COUNT_TRANSIENT . '_limited';
|
||||
|
||||
/**
|
||||
* The post type helper.
|
||||
*
|
||||
* @var Post_Type_Helper
|
||||
*/
|
||||
protected $post_type_helper;
|
||||
|
||||
/**
|
||||
* Sets the required helper.
|
||||
*
|
||||
* @required
|
||||
*
|
||||
* @param Post_Type_Helper $post_type_helper The post type helper.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set_helper( Post_Type_Helper $post_type_helper ) {
|
||||
$this->post_type_helper = $post_type_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns objects to be indexed.
|
||||
*
|
||||
* @return array Objects to be indexed.
|
||||
*/
|
||||
protected function get_objects() {
|
||||
$query = $this->get_select_query( $this->get_limit() );
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query.
|
||||
$posts = $this->wpdb->get_results( $query );
|
||||
|
||||
return \array_map(
|
||||
static function ( $post ) {
|
||||
return (object) [
|
||||
'id' => (int) $post->ID,
|
||||
'type' => 'post',
|
||||
'content' => $post->post_content,
|
||||
];
|
||||
},
|
||||
$posts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a query for counting the number of unindexed post links.
|
||||
*
|
||||
* @return string The prepared query string.
|
||||
*/
|
||||
protected function get_count_query() {
|
||||
$public_post_types = $this->post_type_helper->get_indexable_post_types();
|
||||
$indexable_table = Model::get_table_name( 'Indexable' );
|
||||
$links_table = Model::get_table_name( 'SEO_Links' );
|
||||
|
||||
// Warning: If this query is changed, makes sure to update the query in get_select_query as well.
|
||||
return $this->wpdb->prepare(
|
||||
"SELECT COUNT(P.ID)
|
||||
FROM {$this->wpdb->posts} AS P
|
||||
LEFT JOIN $indexable_table AS I
|
||||
ON P.ID = I.object_id
|
||||
AND I.link_count IS NOT NULL
|
||||
AND I.object_type = 'post'
|
||||
LEFT JOIN $links_table AS L
|
||||
ON L.post_id = P.ID
|
||||
AND L.target_indexable_id IS NULL
|
||||
AND L.type = 'internal'
|
||||
AND L.target_post_id IS NOT NULL
|
||||
AND L.target_post_id != 0
|
||||
WHERE ( I.object_id IS NULL OR L.post_id IS NOT NULL )
|
||||
AND P.post_status = 'publish'
|
||||
AND P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $public_post_types ), '%s' ) ) . ')',
|
||||
$public_post_types
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a query for selecting the ID's of unindexed post links.
|
||||
*
|
||||
* @param int|false $limit The maximum number of post link IDs to return.
|
||||
*
|
||||
* @return string The prepared query string.
|
||||
*/
|
||||
protected function get_select_query( $limit = false ) {
|
||||
$public_post_types = $this->post_type_helper->get_indexable_post_types();
|
||||
$indexable_table = Model::get_table_name( 'Indexable' );
|
||||
$links_table = Model::get_table_name( 'SEO_Links' );
|
||||
$replacements = $public_post_types;
|
||||
|
||||
$limit_query = '';
|
||||
if ( $limit ) {
|
||||
$limit_query = 'LIMIT %d';
|
||||
$replacements[] = $limit;
|
||||
}
|
||||
|
||||
// Warning: If this query is changed, makes sure to update the query in get_count_query as well.
|
||||
return $this->wpdb->prepare(
|
||||
"
|
||||
SELECT P.ID, P.post_content
|
||||
FROM {$this->wpdb->posts} AS P
|
||||
LEFT JOIN $indexable_table AS I
|
||||
ON P.ID = I.object_id
|
||||
AND I.link_count IS NOT NULL
|
||||
AND I.object_type = 'post'
|
||||
LEFT JOIN $links_table AS L
|
||||
ON L.post_id = P.ID
|
||||
AND L.target_indexable_id IS NULL
|
||||
AND L.type = 'internal'
|
||||
AND L.target_post_id IS NOT NULL
|
||||
AND L.target_post_id != 0
|
||||
WHERE ( I.object_id IS NULL OR L.post_id IS NOT NULL )
|
||||
AND P.post_status = 'publish'
|
||||
AND P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $public_post_types ), '%s' ) ) . ")
|
||||
$limit_query",
|
||||
$replacements
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Indexing;
|
||||
|
||||
use Yoast\WP\Lib\Model;
|
||||
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
|
||||
|
||||
/**
|
||||
* Reindexing action for term link indexables.
|
||||
*/
|
||||
class Term_Link_Indexing_Action extends Abstract_Link_Indexing_Action {
|
||||
|
||||
/**
|
||||
* The transient name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const UNINDEXED_COUNT_TRANSIENT = 'wpseo_unindexed_term_link_count';
|
||||
|
||||
/**
|
||||
* The transient cache key for limited counts.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const UNINDEXED_LIMITED_COUNT_TRANSIENT = self::UNINDEXED_COUNT_TRANSIENT . '_limited';
|
||||
|
||||
/**
|
||||
* The post type helper.
|
||||
*
|
||||
* @var Taxonomy_Helper
|
||||
*/
|
||||
protected $taxonomy_helper;
|
||||
|
||||
/**
|
||||
* Sets the required helper.
|
||||
*
|
||||
* @required
|
||||
*
|
||||
* @param Taxonomy_Helper $taxonomy_helper The taxonomy helper.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set_helper( Taxonomy_Helper $taxonomy_helper ) {
|
||||
$this->taxonomy_helper = $taxonomy_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns objects to be indexed.
|
||||
*
|
||||
* @return array Objects to be indexed.
|
||||
*/
|
||||
protected function get_objects() {
|
||||
$query = $this->get_select_query( $this->get_limit() );
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query.
|
||||
$terms = $this->wpdb->get_results( $query );
|
||||
|
||||
return \array_map(
|
||||
static function ( $term ) {
|
||||
return (object) [
|
||||
'id' => (int) $term->term_id,
|
||||
'type' => 'term',
|
||||
'content' => $term->description,
|
||||
];
|
||||
},
|
||||
$terms
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a query for counting the number of unindexed term links.
|
||||
*
|
||||
* @return string The prepared query string.
|
||||
*/
|
||||
protected function get_count_query() {
|
||||
$public_taxonomies = $this->taxonomy_helper->get_indexable_taxonomies();
|
||||
$placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) );
|
||||
$indexable_table = Model::get_table_name( 'Indexable' );
|
||||
|
||||
// Warning: If this query is changed, makes sure to update the query in get_select_query as well.
|
||||
return $this->wpdb->prepare(
|
||||
"
|
||||
SELECT COUNT(T.term_id)
|
||||
FROM {$this->wpdb->term_taxonomy} AS T
|
||||
LEFT JOIN $indexable_table AS I
|
||||
ON T.term_id = I.object_id
|
||||
AND I.object_type = 'term'
|
||||
AND I.link_count IS NOT NULL
|
||||
WHERE I.object_id IS NULL
|
||||
AND T.taxonomy IN ($placeholders)",
|
||||
$public_taxonomies
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a query for selecting the ID's of unindexed term links.
|
||||
*
|
||||
* @param int|false $limit The maximum number of term link IDs to return.
|
||||
*
|
||||
* @return string The prepared query string.
|
||||
*/
|
||||
protected function get_select_query( $limit = false ) {
|
||||
$public_taxonomies = $this->taxonomy_helper->get_indexable_taxonomies();
|
||||
|
||||
$indexable_table = Model::get_table_name( 'Indexable' );
|
||||
$replacements = $public_taxonomies;
|
||||
|
||||
$limit_query = '';
|
||||
if ( $limit ) {
|
||||
$limit_query = 'LIMIT %d';
|
||||
$replacements[] = $limit;
|
||||
}
|
||||
|
||||
// Warning: If this query is changed, makes sure to update the query in get_count_query as well.
|
||||
return $this->wpdb->prepare(
|
||||
"
|
||||
SELECT T.term_id, T.description
|
||||
FROM {$this->wpdb->term_taxonomy} AS T
|
||||
LEFT JOIN $indexable_table AS I
|
||||
ON T.term_id = I.object_id
|
||||
AND I.object_type = 'term'
|
||||
AND I.link_count IS NOT NULL
|
||||
WHERE I.object_id IS NULL
|
||||
AND T.taxonomy IN (" . \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) ) . ")
|
||||
$limit_query",
|
||||
$replacements
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
|
||||
/**
|
||||
* Class Integrations_Action.
|
||||
*/
|
||||
class Integrations_Action {
|
||||
|
||||
/**
|
||||
* The Options_Helper instance.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
protected $options_helper;
|
||||
|
||||
/**
|
||||
* Integrations_Action constructor.
|
||||
*
|
||||
* @param Options_Helper $options_helper The WPSEO options helper.
|
||||
*/
|
||||
public function __construct( Options_Helper $options_helper ) {
|
||||
$this->options_helper = $options_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an integration state.
|
||||
*
|
||||
* @param string $integration_name The name of the integration to activate/deactivate.
|
||||
* @param bool $value The value to store.
|
||||
*
|
||||
* @return object The response object.
|
||||
*/
|
||||
public function set_integration_active( $integration_name, $value ) {
|
||||
$option_name = $integration_name . '_integration_active';
|
||||
$success = true;
|
||||
$option_value = $this->options_helper->get( $option_name );
|
||||
|
||||
if ( $option_value !== $value ) {
|
||||
$success = $this->options_helper->set( $option_name, $value );
|
||||
}
|
||||
|
||||
if ( $success ) {
|
||||
return (object) [
|
||||
'success' => true,
|
||||
'status' => 200,
|
||||
];
|
||||
}
|
||||
return (object) [
|
||||
'success' => false,
|
||||
'status' => 500,
|
||||
'error' => 'Could not save the option in the database',
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\SEMrush;
|
||||
|
||||
use Yoast\WP\SEO\Config\SEMrush_Client;
|
||||
use Yoast\WP\SEO\Exceptions\OAuth\Authentication_Failed_Exception;
|
||||
|
||||
/**
|
||||
* Class SEMrush_Login_Action
|
||||
*/
|
||||
class SEMrush_Login_Action {
|
||||
|
||||
/**
|
||||
* The SEMrush_Client instance.
|
||||
*
|
||||
* @var SEMrush_Client
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* SEMrush_Login_Action constructor.
|
||||
*
|
||||
* @param SEMrush_Client $client The API client.
|
||||
*/
|
||||
public function __construct( SEMrush_Client $client ) {
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates with SEMrush to request the necessary tokens.
|
||||
*
|
||||
* @param string $code The authentication code to use to request a token with.
|
||||
*
|
||||
* @return object The response object.
|
||||
*/
|
||||
public function authenticate( $code ) {
|
||||
// Code has already been validated at this point. No need to do that again.
|
||||
try {
|
||||
$tokens = $this->client->request_tokens( $code );
|
||||
|
||||
return (object) [
|
||||
'tokens' => $tokens->to_array(),
|
||||
'status' => 200,
|
||||
];
|
||||
} catch ( Authentication_Failed_Exception $e ) {
|
||||
return $e->get_response();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the login request, if necessary.
|
||||
*/
|
||||
public function login() {
|
||||
if ( $this->client->has_valid_tokens() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prompt with login screen.
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\SEMrush;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
|
||||
/**
|
||||
* Class SEMrush_Options_Action
|
||||
*/
|
||||
class SEMrush_Options_Action {
|
||||
|
||||
/**
|
||||
* The Options_Helper instance.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
protected $options_helper;
|
||||
|
||||
/**
|
||||
* SEMrush_Options_Action constructor.
|
||||
*
|
||||
* @param Options_Helper $options_helper The WPSEO options helper.
|
||||
*/
|
||||
public function __construct( Options_Helper $options_helper ) {
|
||||
$this->options_helper = $options_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores SEMrush country code in the WPSEO options.
|
||||
*
|
||||
* @param string $country_code The country code to store.
|
||||
*
|
||||
* @return object The response object.
|
||||
*/
|
||||
public function set_country_code( $country_code ) {
|
||||
// The country code has already been validated at this point. No need to do that again.
|
||||
$success = $this->options_helper->set( 'semrush_country_code', $country_code );
|
||||
|
||||
if ( $success ) {
|
||||
return (object) [
|
||||
'success' => true,
|
||||
'status' => 200,
|
||||
];
|
||||
}
|
||||
return (object) [
|
||||
'success' => false,
|
||||
'status' => 500,
|
||||
'error' => 'Could not save option in the database',
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\SEMrush;
|
||||
|
||||
use Exception;
|
||||
use Yoast\WP\SEO\Config\SEMrush_Client;
|
||||
|
||||
/**
|
||||
* Class SEMrush_Phrases_Action
|
||||
*/
|
||||
class SEMrush_Phrases_Action {
|
||||
|
||||
/**
|
||||
* The transient cache key.
|
||||
*/
|
||||
const TRANSIENT_CACHE_KEY = 'wpseo_semrush_related_keyphrases_%s_%s';
|
||||
|
||||
/**
|
||||
* The SEMrush keyphrase URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const KEYPHRASES_URL = 'https://oauth.semrush.com/api/v1/keywords/phrase_fullsearch';
|
||||
|
||||
/**
|
||||
* The SEMrush_Client instance.
|
||||
*
|
||||
* @var SEMrush_Client
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* SEMrush_Phrases_Action constructor.
|
||||
*
|
||||
* @param SEMrush_Client $client The API client.
|
||||
*/
|
||||
public function __construct( SEMrush_Client $client ) {
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the related keyphrases and data based on the passed keyphrase and database country code.
|
||||
*
|
||||
* @param string $keyphrase The keyphrase to search for.
|
||||
* @param string $database The database's country code.
|
||||
*
|
||||
* @return object The response object.
|
||||
*/
|
||||
public function get_related_keyphrases( $keyphrase, $database ) {
|
||||
try {
|
||||
$transient_key = \sprintf( static::TRANSIENT_CACHE_KEY, $keyphrase, $database );
|
||||
$transient = \get_transient( $transient_key );
|
||||
|
||||
if ( $transient !== false ) {
|
||||
return $this->to_result_object( $transient );
|
||||
}
|
||||
|
||||
$options = [
|
||||
'params' => [
|
||||
'phrase' => $keyphrase,
|
||||
'database' => $database,
|
||||
'export_columns' => 'Ph,Nq,Td',
|
||||
'display_limit' => 10,
|
||||
'display_offset' => 0,
|
||||
'display_sort' => 'nq_desc',
|
||||
'display_filter' => '%2B|Nq|Lt|1000',
|
||||
],
|
||||
];
|
||||
|
||||
$results = $this->client->get( self::KEYPHRASES_URL, $options );
|
||||
|
||||
\set_transient( $transient_key, $results, \DAY_IN_SECONDS );
|
||||
|
||||
return $this->to_result_object( $results );
|
||||
} catch ( Exception $e ) {
|
||||
return (object) [
|
||||
'error' => $e->getMessage(),
|
||||
'status' => $e->getCode(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the passed dataset to an object.
|
||||
*
|
||||
* @param array $result The result dataset to convert to an object.
|
||||
*
|
||||
* @return object The result object.
|
||||
*/
|
||||
protected function to_result_object( $result ) {
|
||||
return (object) [
|
||||
'results' => $result['data'],
|
||||
'status' => $result['status'],
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Wincher;
|
||||
|
||||
use Yoast\WP\SEO\Config\Wincher_Client;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
|
||||
/**
|
||||
* Class Wincher_Account_Action
|
||||
*/
|
||||
class Wincher_Account_Action {
|
||||
|
||||
const ACCOUNT_URL = 'https://api.wincher.com/beta/account';
|
||||
const UPGRADE_CAMPAIGN_URL = 'https://api.wincher.com/v1/yoast/upgrade-campaign';
|
||||
|
||||
/**
|
||||
* The Wincher_Client instance.
|
||||
*
|
||||
* @var Wincher_Client
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* The Options_Helper instance.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
protected $options_helper;
|
||||
|
||||
/**
|
||||
* Wincher_Account_Action constructor.
|
||||
*
|
||||
* @param Wincher_Client $client The API client.
|
||||
* @param Options_Helper $options_helper The options helper.
|
||||
*/
|
||||
public function __construct( Wincher_Client $client, Options_Helper $options_helper ) {
|
||||
$this->client = $client;
|
||||
$this->options_helper = $options_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the account limit for tracking keyphrases.
|
||||
*
|
||||
* @return object The response object.
|
||||
*/
|
||||
public function check_limit() {
|
||||
// Code has already been validated at this point. No need to do that again.
|
||||
try {
|
||||
$results = $this->client->get( self::ACCOUNT_URL );
|
||||
|
||||
$usage = $results['limits']['keywords']['usage'];
|
||||
$limit = $results['limits']['keywords']['limit'];
|
||||
|
||||
return (object) [
|
||||
'canTrack' => \is_null( $limit ) || $usage < $limit,
|
||||
'limit' => $limit,
|
||||
'usage' => $usage,
|
||||
'status' => 200,
|
||||
];
|
||||
} catch ( \Exception $e ) {
|
||||
return (object) [
|
||||
'status' => $e->getCode(),
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the upgrade campaign.
|
||||
*
|
||||
* @return object The response object.
|
||||
*/
|
||||
public function get_upgrade_campaign() {
|
||||
try {
|
||||
$result = $this->client->get( self::UPGRADE_CAMPAIGN_URL );
|
||||
$type = $result['type'];
|
||||
$months = $result['months'];
|
||||
|
||||
// We display upgrade discount only if it's a rate discount and positive months.
|
||||
if ( $type === 'RATE' && $months && $months > 0 ) {
|
||||
$discount = $result['value'];
|
||||
|
||||
return (object) [
|
||||
'discount' => $discount,
|
||||
'months' => $months,
|
||||
'status' => 200,
|
||||
];
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'discount' => null,
|
||||
'months' => null,
|
||||
'status' => 200,
|
||||
];
|
||||
} catch ( \Exception $e ) {
|
||||
return (object) [
|
||||
'status' => $e->getCode(),
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,361 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Wincher;
|
||||
|
||||
use Exception;
|
||||
use WP_Post;
|
||||
use WPSEO_Utils;
|
||||
use Yoast\WP\SEO\Config\Wincher_Client;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
use Yoast\WP\SEO\Repositories\Indexable_Repository;
|
||||
|
||||
/**
|
||||
* Class Wincher_Keyphrases_Action
|
||||
*/
|
||||
class Wincher_Keyphrases_Action {
|
||||
|
||||
/**
|
||||
* The Wincher keyphrase URL for bulk addition.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const KEYPHRASES_ADD_URL = 'https://api.wincher.com/beta/websites/%s/keywords/bulk';
|
||||
|
||||
/**
|
||||
* The Wincher tracked keyphrase retrieval URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const KEYPHRASES_URL = 'https://api.wincher.com/beta/yoast/%s';
|
||||
|
||||
/**
|
||||
* The Wincher delete tracked keyphrase URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const KEYPHRASE_DELETE_URL = 'https://api.wincher.com/beta/websites/%s/keywords/%s';
|
||||
|
||||
/**
|
||||
* The Wincher_Client instance.
|
||||
*
|
||||
* @var Wincher_Client
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* The Options_Helper instance.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
protected $options_helper;
|
||||
|
||||
/**
|
||||
* The Indexable_Repository instance.
|
||||
*
|
||||
* @var Indexable_Repository
|
||||
*/
|
||||
protected $indexable_repository;
|
||||
|
||||
/**
|
||||
* Wincher_Keyphrases_Action constructor.
|
||||
*
|
||||
* @param Wincher_Client $client The API client.
|
||||
* @param Options_Helper $options_helper The options helper.
|
||||
* @param Indexable_Repository $indexable_repository The indexables repository.
|
||||
*/
|
||||
public function __construct(
|
||||
Wincher_Client $client,
|
||||
Options_Helper $options_helper,
|
||||
Indexable_Repository $indexable_repository
|
||||
) {
|
||||
$this->client = $client;
|
||||
$this->options_helper = $options_helper;
|
||||
$this->indexable_repository = $indexable_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the tracking API request for one or more keyphrases.
|
||||
*
|
||||
* @param string|array $keyphrases One or more keyphrases that should be tracked.
|
||||
* @param Object $limits The limits API call response data.
|
||||
*
|
||||
* @return Object The reponse object.
|
||||
*/
|
||||
public function track_keyphrases( $keyphrases, $limits ) {
|
||||
try {
|
||||
$endpoint = \sprintf(
|
||||
self::KEYPHRASES_ADD_URL,
|
||||
$this->options_helper->get( 'wincher_website_id' )
|
||||
);
|
||||
|
||||
// Enforce arrrays to ensure a consistent way of preparing the request.
|
||||
if ( ! \is_array( $keyphrases ) ) {
|
||||
$keyphrases = [ $keyphrases ];
|
||||
}
|
||||
|
||||
// Calculate if the user would exceed their limit.
|
||||
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- To ensure JS code style, this can be ignored.
|
||||
if ( ! $limits->canTrack || $this->would_exceed_limits( $keyphrases, $limits ) ) {
|
||||
$response = [
|
||||
'limit' => $limits->limit,
|
||||
'error' => 'Account limit exceeded',
|
||||
'status' => 400,
|
||||
];
|
||||
|
||||
return $this->to_result_object( $response );
|
||||
}
|
||||
|
||||
$formatted_keyphrases = \array_values(
|
||||
\array_map(
|
||||
static function ( $keyphrase ) {
|
||||
return [
|
||||
'keyword' => $keyphrase,
|
||||
'groups' => [],
|
||||
];
|
||||
},
|
||||
$keyphrases
|
||||
)
|
||||
);
|
||||
|
||||
$results = $this->client->post( $endpoint, WPSEO_Utils::format_json_encode( $formatted_keyphrases ) );
|
||||
|
||||
if ( ! \array_key_exists( 'data', $results ) ) {
|
||||
return $this->to_result_object( $results );
|
||||
}
|
||||
|
||||
// The endpoint returns a lot of stuff that we don't want/need.
|
||||
$results['data'] = \array_map(
|
||||
static function( $keyphrase ) {
|
||||
return [
|
||||
'id' => $keyphrase['id'],
|
||||
'keyword' => $keyphrase['keyword'],
|
||||
];
|
||||
},
|
||||
$results['data']
|
||||
);
|
||||
|
||||
$results['data'] = \array_combine(
|
||||
\array_column( $results['data'], 'keyword' ),
|
||||
\array_values( $results['data'] )
|
||||
);
|
||||
|
||||
return $this->to_result_object( $results );
|
||||
} catch ( Exception $e ) {
|
||||
return (object) [
|
||||
'error' => $e->getMessage(),
|
||||
'status' => $e->getCode(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an untrack request for the passed keyword ID.
|
||||
*
|
||||
* @param int $keyphrase_id The ID of the keyphrase to untrack.
|
||||
*
|
||||
* @return object The response object.
|
||||
*/
|
||||
public function untrack_keyphrase( $keyphrase_id ) {
|
||||
try {
|
||||
$endpoint = \sprintf(
|
||||
self::KEYPHRASE_DELETE_URL,
|
||||
$this->options_helper->get( 'wincher_website_id' ),
|
||||
$keyphrase_id
|
||||
);
|
||||
|
||||
$this->client->delete( $endpoint );
|
||||
|
||||
return (object) [
|
||||
'status' => 200,
|
||||
];
|
||||
} catch ( Exception $e ) {
|
||||
return (object) [
|
||||
'error' => $e->getMessage(),
|
||||
'status' => $e->getCode(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the keyphrase data for the passed keyphrases.
|
||||
* Retrieves all available data if no keyphrases are provided.
|
||||
*
|
||||
* @param array|null $used_keyphrases The currently used keyphrases. Optional.
|
||||
* @param string|null $permalink The current permalink. Optional.
|
||||
*
|
||||
* @return object The keyphrase chart data.
|
||||
*/
|
||||
public function get_tracked_keyphrases( $used_keyphrases = null, $permalink = null ) {
|
||||
try {
|
||||
if ( $used_keyphrases === null ) {
|
||||
$used_keyphrases = $this->collect_all_keyphrases();
|
||||
}
|
||||
|
||||
// If we still have no keyphrases the API will return an error, so
|
||||
// don't even bother sending a request.
|
||||
if ( empty( $used_keyphrases ) ) {
|
||||
return $this->to_result_object(
|
||||
[
|
||||
'data' => [],
|
||||
'status' => 200,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$endpoint = \sprintf(
|
||||
self::KEYPHRASES_URL,
|
||||
$this->options_helper->get( 'wincher_website_id' )
|
||||
);
|
||||
|
||||
$results = $this->client->post(
|
||||
$endpoint,
|
||||
WPSEO_Utils::format_json_encode(
|
||||
[
|
||||
'keywords' => $used_keyphrases,
|
||||
'url' => $permalink,
|
||||
]
|
||||
),
|
||||
[
|
||||
'timeout' => 60,
|
||||
]
|
||||
);
|
||||
|
||||
if ( ! \array_key_exists( 'data', $results ) ) {
|
||||
return $this->to_result_object( $results );
|
||||
}
|
||||
|
||||
$results['data'] = $this->filter_results_by_used_keyphrases( $results['data'], $used_keyphrases );
|
||||
|
||||
// Extract the positional data and assign it to the keyphrase.
|
||||
$results['data'] = \array_combine(
|
||||
\array_column( $results['data'], 'keyword' ),
|
||||
\array_values( $results['data'] )
|
||||
);
|
||||
|
||||
return $this->to_result_object( $results );
|
||||
} catch ( Exception $e ) {
|
||||
return (object) [
|
||||
'error' => $e->getMessage(),
|
||||
'status' => $e->getCode(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects the keyphrases associated with the post.
|
||||
*
|
||||
* @param WP_Post $post The post object.
|
||||
*
|
||||
* @return array The keyphrases.
|
||||
*/
|
||||
public function collect_keyphrases_from_post( $post ) {
|
||||
$keyphrases = [];
|
||||
$primary_keyphrase = $this->indexable_repository
|
||||
->query()
|
||||
->select( 'primary_focus_keyword' )
|
||||
->where( 'object_id', $post->ID )
|
||||
->find_one();
|
||||
|
||||
if ( $primary_keyphrase ) {
|
||||
$keyphrases[] = $primary_keyphrase->primary_focus_keyword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the keyphrases collected by the Wincher integration from the post.
|
||||
*
|
||||
* @param array $keyphrases The keyphrases array.
|
||||
* @param int $post_id The ID of the post.
|
||||
*/
|
||||
return \apply_filters( 'wpseo_wincher_keyphrases_from_post', $keyphrases, $post->ID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all keyphrases known to Yoast.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function collect_all_keyphrases() {
|
||||
// Collect primary keyphrases first.
|
||||
$keyphrases = \array_column(
|
||||
$this->indexable_repository
|
||||
->query()
|
||||
->select( 'primary_focus_keyword' )
|
||||
->where_not_null( 'primary_focus_keyword' )
|
||||
->where( 'object_type', 'post' )
|
||||
->where_not_equal( 'post_status', 'trash' )
|
||||
->distinct()
|
||||
->find_array(),
|
||||
'primary_focus_keyword'
|
||||
);
|
||||
|
||||
/**
|
||||
* Filters the keyphrases collected by the Wincher integration from all the posts.
|
||||
*
|
||||
* @param array $keyphrases The keyphrases array.
|
||||
*/
|
||||
$keyphrases = \apply_filters( 'wpseo_wincher_all_keyphrases', $keyphrases );
|
||||
|
||||
// Filter out empty entries.
|
||||
return \array_filter( $keyphrases );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the results based on the passed keyphrases.
|
||||
*
|
||||
* @param array $results The results to filter.
|
||||
* @param array $used_keyphrases The used keyphrases.
|
||||
*
|
||||
* @return array The filtered results.
|
||||
*/
|
||||
protected function filter_results_by_used_keyphrases( $results, $used_keyphrases ) {
|
||||
return \array_filter(
|
||||
$results,
|
||||
static function( $result ) use ( $used_keyphrases ) {
|
||||
return \in_array( $result['keyword'], \array_map( 'strtolower', $used_keyphrases ), true );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the amount of keyphrases would mean the user exceeds their account limits.
|
||||
*
|
||||
* @param string|array $keyphrases The keyphrases to be added.
|
||||
* @param object $limits The current account limits.
|
||||
*
|
||||
* @return bool Whether the limit is exceeded.
|
||||
*/
|
||||
protected function would_exceed_limits( $keyphrases, $limits ) {
|
||||
if ( ! \is_array( $keyphrases ) ) {
|
||||
$keyphrases = [ $keyphrases ];
|
||||
}
|
||||
|
||||
if ( \is_null( $limits->limit ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ( \count( $keyphrases ) + $limits->usage ) > $limits->limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the passed dataset to an object.
|
||||
*
|
||||
* @param array $result The result dataset to convert to an object.
|
||||
*
|
||||
* @return object The result object.
|
||||
*/
|
||||
protected function to_result_object( $result ) {
|
||||
if ( \array_key_exists( 'data', $result ) ) {
|
||||
$result['results'] = (object) $result['data'];
|
||||
|
||||
unset( $result['data'] );
|
||||
}
|
||||
|
||||
if ( \array_key_exists( 'message', $result ) ) {
|
||||
$result['error'] = $result['message'];
|
||||
|
||||
unset( $result['message'] );
|
||||
}
|
||||
|
||||
return (object) $result;
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Wincher;
|
||||
|
||||
use Yoast\WP\SEO\Config\Wincher_Client;
|
||||
use Yoast\WP\SEO\Exceptions\OAuth\Authentication_Failed_Exception;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
|
||||
/**
|
||||
* Class Wincher_Login_Action
|
||||
*/
|
||||
class Wincher_Login_Action {
|
||||
|
||||
/**
|
||||
* The Wincher_Client instance.
|
||||
*
|
||||
* @var Wincher_Client
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* The Options_Helper instance.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
protected $options_helper;
|
||||
|
||||
/**
|
||||
* Wincher_Login_Action constructor.
|
||||
*
|
||||
* @param Wincher_Client $client The API client.
|
||||
* @param Options_Helper $options_helper The options helper.
|
||||
*/
|
||||
public function __construct( Wincher_Client $client, Options_Helper $options_helper ) {
|
||||
$this->client = $client;
|
||||
$this->options_helper = $options_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authorization URL.
|
||||
*
|
||||
* @return object The response object.
|
||||
*/
|
||||
public function get_authorization_url() {
|
||||
return (object) [
|
||||
'status' => 200,
|
||||
'url' => $this->client->get_authorization_url(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates with Wincher to request the necessary tokens.
|
||||
*
|
||||
* @param string $code The authentication code to use to request a token with.
|
||||
* @param string $website_id The website id associated with the code.
|
||||
*
|
||||
* @return object The response object.
|
||||
*/
|
||||
public function authenticate( $code, $website_id ) {
|
||||
// Code has already been validated at this point. No need to do that again.
|
||||
try {
|
||||
$tokens = $this->client->request_tokens( $code );
|
||||
$this->options_helper->set( 'wincher_website_id', $website_id );
|
||||
|
||||
return (object) [
|
||||
'tokens' => $tokens->to_array(),
|
||||
'status' => 200,
|
||||
];
|
||||
} catch ( Authentication_Failed_Exception $e ) {
|
||||
return $e->get_response();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Analytics\Application;
|
||||
|
||||
use WPSEO_Collection;
|
||||
use Yoast\WP\SEO\Actions\Indexing\Indexation_Action_Interface;
|
||||
use Yoast\WP\SEO\Analytics\Domain\Missing_Indexable_Bucket;
|
||||
use Yoast\WP\SEO\Analytics\Domain\Missing_Indexable_Count;
|
||||
|
||||
/**
|
||||
* Manages the collection of the missing indexable data.
|
||||
*
|
||||
* @makePublic
|
||||
*/
|
||||
class Missing_Indexables_Collector implements WPSEO_Collection {
|
||||
|
||||
/**
|
||||
* All the indexation actions.
|
||||
*
|
||||
* @var array<Indexation_Action_Interface>
|
||||
*/
|
||||
private $indexation_actions;
|
||||
|
||||
/**
|
||||
* The collector constructor.
|
||||
*
|
||||
* @param Indexation_Action_Interface ...$indexation_actions All the Indexation actions.
|
||||
*/
|
||||
public function __construct( Indexation_Action_Interface ...$indexation_actions ) {
|
||||
$this->indexation_actions = $indexation_actions;
|
||||
$this->add_additional_indexing_actions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data for the tracking collector.
|
||||
*
|
||||
* @return array The list of missing indexables.
|
||||
*/
|
||||
public function get() {
|
||||
$missing_indexable_bucket = new Missing_Indexable_Bucket();
|
||||
foreach ( $this->indexation_actions as $indexation_action ) {
|
||||
$missing_indexable_count = new Missing_Indexable_Count( \get_class( $indexation_action ), $indexation_action->get_total_unindexed() );
|
||||
$missing_indexable_bucket->add_missing_indexable_count( $missing_indexable_count );
|
||||
}
|
||||
|
||||
return [ 'missing_indexables' => $missing_indexable_bucket->to_array() ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds additional indexing actions to count from the 'wpseo_indexable_collector_add_indexation_actions' filter.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function add_additional_indexing_actions() {
|
||||
/**
|
||||
* Filter: Adds the possibility to add additional indexation actions to be included in the count routine.
|
||||
*
|
||||
* @internal
|
||||
* @api Indexation_Action_Interface This filter expects a list of Indexation_Action_Interface instances and expects only Indexation_Action_Interface implementations to be added to the list.
|
||||
*/
|
||||
$indexing_actions = (array) \apply_filters( 'wpseo_indexable_collector_add_indexation_actions', $this->indexation_actions );
|
||||
|
||||
$this->indexation_actions = \array_filter(
|
||||
$indexing_actions,
|
||||
static function ( $indexing_action ) {
|
||||
return \is_a( $indexing_action, Indexation_Action_Interface::class );
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Analytics\Application;
|
||||
|
||||
use WPSEO_Collection;
|
||||
use Yoast\WP\SEO\Analytics\Domain\To_Be_Cleaned_Indexable_Bucket;
|
||||
use Yoast\WP\SEO\Analytics\Domain\To_Be_Cleaned_Indexable_Count;
|
||||
use Yoast\WP\SEO\Repositories\Indexable_Cleanup_Repository;
|
||||
|
||||
/**
|
||||
* Collects data about to-be-cleaned indexables.
|
||||
*
|
||||
* @makePublic
|
||||
*/
|
||||
class To_Be_Cleaned_Indexables_Collector implements WPSEO_Collection {
|
||||
|
||||
/**
|
||||
* The cleanup query repository.
|
||||
*
|
||||
* @var Indexable_Cleanup_Repository
|
||||
*/
|
||||
private $indexable_cleanup_repository;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param Indexable_Cleanup_Repository $indexable_cleanup_repository The Indexable cleanup repository.
|
||||
*/
|
||||
public function __construct( Indexable_Cleanup_Repository $indexable_cleanup_repository ) {
|
||||
$this->indexable_cleanup_repository = $indexable_cleanup_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data for the collector.
|
||||
*/
|
||||
public function get() {
|
||||
$to_be_cleaned_indexable_bucket = new To_Be_Cleaned_Indexable_Bucket();
|
||||
$cleanup_tasks = [
|
||||
'indexables_with_post_object_type_and_shop_order_object_sub_type' => $this->indexable_cleanup_repository->count_indexables_with_object_type_and_object_sub_type( 'post', 'shop_order' ),
|
||||
'indexables_with_auto-draft_post_status' => $this->indexable_cleanup_repository->count_indexables_with_post_status( 'auto-draft' ),
|
||||
'indexables_for_non_publicly_viewable_post' => $this->indexable_cleanup_repository->count_indexables_for_non_publicly_viewable_post(),
|
||||
'indexables_for_non_publicly_viewable_taxonomies' => $this->indexable_cleanup_repository->count_indexables_for_non_publicly_viewable_taxonomies(),
|
||||
'indexables_for_non_publicly_viewable_post_type_archive_pages' => $this->indexable_cleanup_repository->count_indexables_for_non_publicly_post_type_archive_pages(),
|
||||
'indexables_for_authors_archive_disabled' => $this->indexable_cleanup_repository->count_indexables_for_authors_archive_disabled(),
|
||||
'indexables_for_authors_without_archive' => $this->indexable_cleanup_repository->count_indexables_for_authors_without_archive(),
|
||||
'indexables_for_object_type_and_source_table_users' => $this->indexable_cleanup_repository->count_indexables_for_orphaned_users(),
|
||||
'indexables_for_object_type_and_source_table_posts' => $this->indexable_cleanup_repository->count_indexables_for_object_type_and_source_table( 'posts', 'ID', 'post' ),
|
||||
'indexables_for_object_type_and_source_table_terms' => $this->indexable_cleanup_repository->count_indexables_for_object_type_and_source_table( 'terms', 'term_id', 'term' ),
|
||||
'orphaned_from_table_indexable_hierarchy' => $this->indexable_cleanup_repository->count_orphaned_from_table( 'Indexable_Hierarchy', 'indexable_id' ),
|
||||
'orphaned_from_table_indexable_id' => $this->indexable_cleanup_repository->count_orphaned_from_table( 'SEO_Links', 'indexable_id' ),
|
||||
'orphaned_from_table_target_indexable_id' => $this->indexable_cleanup_repository->count_orphaned_from_table( 'SEO_Links', 'target_indexable_id' ),
|
||||
];
|
||||
|
||||
foreach ( $cleanup_tasks as $name => $count ) {
|
||||
if ( $count !== null ) {
|
||||
$count_object = new To_Be_Cleaned_Indexable_Count( $name, $count );
|
||||
$to_be_cleaned_indexable_bucket->add_to_be_cleaned_indexable_count( $count_object );
|
||||
}
|
||||
}
|
||||
|
||||
$this->add_additional_counts( $to_be_cleaned_indexable_bucket );
|
||||
|
||||
return [ 'to_be_cleaned_indexables' => $to_be_cleaned_indexable_bucket->to_array() ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows additional tasks to be added via the 'wpseo_add_cleanup_counts_to_indexable_bucket' action.
|
||||
*
|
||||
* @param To_Be_Cleaned_Indexable_Bucket $to_be_cleaned_indexable_bucket The current bucket with data.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function add_additional_counts( $to_be_cleaned_indexable_bucket ) {
|
||||
/**
|
||||
* Action: Adds the possibility to add additional to be cleaned objects.
|
||||
*
|
||||
* @internal
|
||||
* @api To_Be_Cleaned_Indexable_Bucket An indexable cleanup bucket. New values are instances of To_Be_Cleaned_Indexable_Count.
|
||||
*/
|
||||
\do_action( 'wpseo_add_cleanup_counts_to_indexable_bucket', $to_be_cleaned_indexable_bucket );
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Analytics\Domain;
|
||||
|
||||
/**
|
||||
* A collection domain object.
|
||||
*/
|
||||
class Missing_Indexable_Bucket {
|
||||
|
||||
/**
|
||||
* All the missing indexable count objects.
|
||||
*
|
||||
* @var array<Missing_Indexable_Count>
|
||||
*/
|
||||
private $missing_indexable_counts;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->missing_indexable_counts = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a missing indexable count object to this bucket.
|
||||
*
|
||||
* @param Missing_Indexable_Count $missing_indexable_count The missing indexable count object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_missing_indexable_count( Missing_Indexable_Count $missing_indexable_count ): void {
|
||||
$this->missing_indexable_counts[] = $missing_indexable_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation of all indexable counts.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_array() {
|
||||
return \array_map(
|
||||
static function ( $item ) {
|
||||
return $item->to_array();
|
||||
},
|
||||
$this->missing_indexable_counts
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Analytics\Domain;
|
||||
|
||||
/**
|
||||
* Domain object that holds indexable count information.
|
||||
*/
|
||||
class Missing_Indexable_Count {
|
||||
|
||||
/**
|
||||
* The indexable type that is represented by this.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $indexable_type;
|
||||
|
||||
/**
|
||||
* The amount of missing indexables.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $count;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param string $indexable_type The indexable type that is represented by this.
|
||||
* @param int $count The amount of missing indexables.
|
||||
*/
|
||||
public function __construct( $indexable_type, $count ) {
|
||||
$this->indexable_type = $indexable_type;
|
||||
$this->count = $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array representation of the data.
|
||||
*
|
||||
* @return array Returns both values in an array format.
|
||||
*/
|
||||
public function to_array() {
|
||||
return [
|
||||
'indexable_type' => $this->get_indexable_type(),
|
||||
'count' => $this->get_count(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the indexable type.
|
||||
*
|
||||
* @return string Returns the indexable type.
|
||||
*/
|
||||
public function get_indexable_type() {
|
||||
return $this->indexable_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the count.
|
||||
*
|
||||
* @return int Returns the amount of missing indexables.
|
||||
*/
|
||||
public function get_count() {
|
||||
return $this->count;
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Analytics\Domain;
|
||||
|
||||
/**
|
||||
* A collection domain object.
|
||||
*/
|
||||
class To_Be_Cleaned_Indexable_Bucket {
|
||||
|
||||
/**
|
||||
* All the to be cleaned indexable count objects.
|
||||
*
|
||||
* @var array<To_Be_Cleaned_Indexable_Count>
|
||||
*/
|
||||
private $to_be_cleaned_indexable_counts;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->to_be_cleaned_indexable_counts = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a 'to be cleaned' indexable count object to this bucket.
|
||||
*
|
||||
* @param To_Be_Cleaned_Indexable_Count $to_be_cleaned_indexable_counts The to be cleaned indexable count object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_to_be_cleaned_indexable_count( To_Be_Cleaned_Indexable_Count $to_be_cleaned_indexable_counts ) {
|
||||
$this->to_be_cleaned_indexable_counts[] = $to_be_cleaned_indexable_counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation of all indexable counts.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_array() {
|
||||
return \array_map(
|
||||
static function ( $item ) {
|
||||
return $item->to_array();
|
||||
},
|
||||
$this->to_be_cleaned_indexable_counts
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Analytics\Domain;
|
||||
|
||||
/**
|
||||
* The to be cleaned indexable domain object.
|
||||
*/
|
||||
class To_Be_Cleaned_Indexable_Count {
|
||||
|
||||
/**
|
||||
* The cleanup task that is represented by this.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $cleanup_name;
|
||||
|
||||
/**
|
||||
* The amount of missing indexables.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $count;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param string $cleanup_name The indexable type that is represented by this.
|
||||
* @param int $count The amount of missing indexables.
|
||||
*/
|
||||
public function __construct( $cleanup_name, $count ) {
|
||||
$this->cleanup_name = $cleanup_name;
|
||||
$this->count = $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array representation of the data.
|
||||
*
|
||||
* @return array Returns both values in an array format.
|
||||
*/
|
||||
public function to_array() {
|
||||
return [
|
||||
'cleanup_name' => $this->get_cleanup_name(),
|
||||
'count' => $this->get_count(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_cleanup_name() {
|
||||
return $this->cleanup_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the count.
|
||||
*
|
||||
* @return int Returns the amount of missing indexables.
|
||||
*/
|
||||
public function get_count() {
|
||||
return $this->count;
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Analytics\User_Interface;
|
||||
|
||||
use Yoast\WP\SEO\Conditionals\No_Conditionals;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
use Yoast\WP\SEO\Integrations\Integration_Interface;
|
||||
|
||||
/**
|
||||
* Handles setting a timestamp when the indexation of a specific indexation action is completed.
|
||||
*/
|
||||
class Last_Completed_Indexation_Integration implements Integration_Interface {
|
||||
|
||||
use No_Conditionals;
|
||||
|
||||
/**
|
||||
* The options helper.
|
||||
*
|
||||
* @var Options_Helper The options helper.
|
||||
*/
|
||||
private $options_helper;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param Options_Helper $options_helper The options helper.
|
||||
*/
|
||||
public function __construct( Options_Helper $options_helper ) {
|
||||
$this->options_helper = $options_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers action hook to maybe save an option.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_hooks(): void {
|
||||
\add_action(
|
||||
'wpseo_indexables_unindexed_calculated',
|
||||
[
|
||||
$this,
|
||||
'maybe_set_indexables_unindexed_calculated',
|
||||
],
|
||||
10,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a timestamp option when there are no unindexed indexables.
|
||||
*
|
||||
* @param string $indexable_name The name of the indexable that is being checked.
|
||||
* @param int $count The amount of missing indexables.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function maybe_set_indexables_unindexed_calculated( string $indexable_name, int $count ): void {
|
||||
if ( $count === 0 ) {
|
||||
$no_index = $this->options_helper->get( 'last_known_no_unindexed', [] );
|
||||
$no_index[ $indexable_name ] = \time();
|
||||
$this->options_helper->set( 'last_known_no_unindexed', $no_index );
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Builders;
|
||||
|
||||
use wpdb;
|
||||
use Yoast\WP\SEO\Exceptions\Indexable\Author_Not_Built_Exception;
|
||||
use Yoast\WP\SEO\Helpers\Author_Archive_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Post_Helper;
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
|
||||
|
||||
/**
|
||||
* Author Builder for the indexables.
|
||||
*
|
||||
* Formats the author meta to indexable format.
|
||||
*/
|
||||
class Indexable_Author_Builder {
|
||||
|
||||
use Indexable_Social_Image_Trait;
|
||||
|
||||
/**
|
||||
* The author archive helper.
|
||||
*
|
||||
* @var Author_Archive_Helper
|
||||
*/
|
||||
private $author_archive;
|
||||
|
||||
/**
|
||||
* The latest version of the Indexable_Author_Builder.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $version;
|
||||
|
||||
/**
|
||||
* Holds the taxonomy helper instance.
|
||||
*
|
||||
* @var Post_Helper
|
||||
*/
|
||||
protected $post_helper;
|
||||
|
||||
/**
|
||||
* The WPDB instance.
|
||||
*
|
||||
* @var wpdb
|
||||
*/
|
||||
protected $wpdb;
|
||||
|
||||
/**
|
||||
* Indexable_Author_Builder constructor.
|
||||
*
|
||||
* @param Author_Archive_Helper $author_archive The author archive helper.
|
||||
* @param Indexable_Builder_Versions $versions The Indexable version manager.
|
||||
* @param Post_Helper $post_helper The post helper.
|
||||
* @param wpdb $wpdb The WPDB instance.
|
||||
*/
|
||||
public function __construct(
|
||||
Author_Archive_Helper $author_archive,
|
||||
Indexable_Builder_Versions $versions,
|
||||
Post_Helper $post_helper,
|
||||
wpdb $wpdb
|
||||
) {
|
||||
$this->author_archive = $author_archive;
|
||||
$this->version = $versions->get_latest_version_for_type( 'user' );
|
||||
$this->post_helper = $post_helper;
|
||||
$this->wpdb = $wpdb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data.
|
||||
*
|
||||
* @param int $user_id The user to retrieve the indexable for.
|
||||
* @param Indexable $indexable The indexable to format.
|
||||
*
|
||||
* @return Indexable The extended indexable.
|
||||
*
|
||||
* @throws Author_Not_Built_Exception When author is not built.
|
||||
*/
|
||||
public function build( $user_id, Indexable $indexable ) {
|
||||
$exception = $this->check_if_user_should_be_indexed( $user_id );
|
||||
if ( $exception ) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$meta_data = $this->get_meta_data( $user_id );
|
||||
|
||||
$indexable->object_id = $user_id;
|
||||
$indexable->object_type = 'user';
|
||||
$indexable->permalink = \get_author_posts_url( $user_id );
|
||||
$indexable->title = $meta_data['wpseo_title'];
|
||||
$indexable->description = $meta_data['wpseo_metadesc'];
|
||||
$indexable->is_cornerstone = false;
|
||||
$indexable->is_robots_noindex = ( $meta_data['wpseo_noindex_author'] === 'on' );
|
||||
$indexable->is_robots_nofollow = null;
|
||||
$indexable->is_robots_noarchive = null;
|
||||
$indexable->is_robots_noimageindex = null;
|
||||
$indexable->is_robots_nosnippet = null;
|
||||
$indexable->is_public = ( $indexable->is_robots_noindex ) ? false : null;
|
||||
$indexable->has_public_posts = $this->author_archive->author_has_public_posts( $user_id );
|
||||
$indexable->blog_id = \get_current_blog_id();
|
||||
|
||||
$this->reset_social_images( $indexable );
|
||||
$this->handle_social_images( $indexable );
|
||||
|
||||
$timestamps = $this->get_object_timestamps( $user_id );
|
||||
$indexable->object_published_at = $timestamps->published_at;
|
||||
$indexable->object_last_modified = $timestamps->last_modified;
|
||||
|
||||
$indexable->version = $this->version;
|
||||
|
||||
return $indexable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the meta data for this indexable.
|
||||
*
|
||||
* @param int $user_id The user to retrieve the meta data for.
|
||||
*
|
||||
* @return array List of meta entries.
|
||||
*/
|
||||
protected function get_meta_data( $user_id ) {
|
||||
$keys = [
|
||||
'wpseo_title',
|
||||
'wpseo_metadesc',
|
||||
'wpseo_noindex_author',
|
||||
];
|
||||
|
||||
$output = [];
|
||||
foreach ( $keys as $key ) {
|
||||
$output[ $key ] = $this->get_author_meta( $user_id, $key );
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the author meta.
|
||||
*
|
||||
* @param int $user_id The user to retrieve the indexable for.
|
||||
* @param string $key The meta entry to retrieve.
|
||||
*
|
||||
* @return string|null The value of the meta field.
|
||||
*/
|
||||
protected function get_author_meta( $user_id, $key ) {
|
||||
$value = \get_the_author_meta( $key, $user_id );
|
||||
if ( \is_string( $value ) && $value === '' ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an alternative image for the social image.
|
||||
*
|
||||
* @param Indexable $indexable The indexable.
|
||||
*
|
||||
* @return array|bool False when not found, array with data when found.
|
||||
*/
|
||||
protected function find_alternative_image( Indexable $indexable ) {
|
||||
$gravatar_image = \get_avatar_url(
|
||||
$indexable->object_id,
|
||||
[
|
||||
'size' => 500,
|
||||
'scheme' => 'https',
|
||||
]
|
||||
);
|
||||
if ( $gravatar_image ) {
|
||||
return [
|
||||
'image' => $gravatar_image,
|
||||
'source' => 'gravatar-image',
|
||||
];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamps for a given author.
|
||||
*
|
||||
* @param int $author_id The author ID.
|
||||
*
|
||||
* @return object An object with last_modified and published_at timestamps.
|
||||
*/
|
||||
protected function get_object_timestamps( $author_id ) {
|
||||
$post_statuses = $this->post_helper->get_public_post_statuses();
|
||||
|
||||
$sql = "
|
||||
SELECT MAX(p.post_modified_gmt) AS last_modified, MIN(p.post_date_gmt) AS published_at
|
||||
FROM {$this->wpdb->posts} AS p
|
||||
WHERE p.post_status IN (" . \implode( ', ', \array_fill( 0, \count( $post_statuses ), '%s' ) ) . ")
|
||||
AND p.post_password = ''
|
||||
AND p.post_author = %d
|
||||
";
|
||||
|
||||
$replacements = \array_merge( $post_statuses, [ $author_id ] );
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- We are using wpdb prepare.
|
||||
return $this->wpdb->get_row( $this->wpdb->prepare( $sql, $replacements ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user should be indexed.
|
||||
* Returns an exception with an appropriate message if not.
|
||||
*
|
||||
* @param string $user_id The user id.
|
||||
*
|
||||
* @return Author_Not_Built_Exception|null The exception if it should not be indexed, or `null` if it should.
|
||||
*/
|
||||
protected function check_if_user_should_be_indexed( $user_id ) {
|
||||
$exception = null;
|
||||
|
||||
if ( $this->author_archive->are_disabled() ) {
|
||||
$exception = Author_Not_Built_Exception::author_archives_are_disabled( $user_id );
|
||||
}
|
||||
|
||||
// We will check if the author has public posts the WP way, instead of the indexable way, to make sure we get proper results even if SEO optimization is not run.
|
||||
if ( $this->author_archive->author_has_public_posts_wp( $user_id ) === false ) {
|
||||
$exception = Author_Not_Built_Exception::author_archives_are_not_indexed_for_users_without_posts( $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter: Include or exclude a user from being build and saved as an indexable.
|
||||
* Return an `Author_Not_Built_Exception` when the indexable should not be build, with an appropriate message telling why it should not be built.
|
||||
* Return `null` if the indexable should be build.
|
||||
*
|
||||
* @param Author_Not_Built_Exception|null $exception An exception if the indexable is not being built, `null` if the indexable should be built.
|
||||
* @param string $user_id The ID of the user that should or should not be excluded.
|
||||
*/
|
||||
return \apply_filters( 'wpseo_should_build_and_save_user_indexable', $exception, $user_id );
|
||||
}
|
||||
}
|
@ -0,0 +1,460 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Builders;
|
||||
|
||||
use Yoast\WP\SEO\Exceptions\Indexable\Not_Built_Exception;
|
||||
use Yoast\WP\SEO\Exceptions\Indexable\Source_Exception;
|
||||
use Yoast\WP\SEO\Helpers\Indexable_Helper;
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
use Yoast\WP\SEO\Repositories\Indexable_Repository;
|
||||
use Yoast\WP\SEO\Services\Indexables\Indexable_Version_Manager;
|
||||
|
||||
/**
|
||||
* Builder for the indexables.
|
||||
*
|
||||
* Creates all the indexables.
|
||||
*/
|
||||
class Indexable_Builder {
|
||||
|
||||
/**
|
||||
* The author builder.
|
||||
*
|
||||
* @var Indexable_Author_Builder
|
||||
*/
|
||||
private $author_builder;
|
||||
|
||||
/**
|
||||
* The post builder.
|
||||
*
|
||||
* @var Indexable_Post_Builder
|
||||
*/
|
||||
private $post_builder;
|
||||
|
||||
/**
|
||||
* The term builder.
|
||||
*
|
||||
* @var Indexable_Term_Builder
|
||||
*/
|
||||
private $term_builder;
|
||||
|
||||
/**
|
||||
* The home page builder.
|
||||
*
|
||||
* @var Indexable_Home_Page_Builder
|
||||
*/
|
||||
private $home_page_builder;
|
||||
|
||||
/**
|
||||
* The post type archive builder.
|
||||
*
|
||||
* @var Indexable_Post_Type_Archive_Builder
|
||||
*/
|
||||
private $post_type_archive_builder;
|
||||
|
||||
/**
|
||||
* The data archive builder.
|
||||
*
|
||||
* @var Indexable_Date_Archive_Builder
|
||||
*/
|
||||
private $date_archive_builder;
|
||||
|
||||
/**
|
||||
* The system page builder.
|
||||
*
|
||||
* @var Indexable_System_Page_Builder
|
||||
*/
|
||||
private $system_page_builder;
|
||||
|
||||
/**
|
||||
* The indexable hierarchy builder.
|
||||
*
|
||||
* @var Indexable_Hierarchy_Builder
|
||||
*/
|
||||
private $hierarchy_builder;
|
||||
|
||||
/**
|
||||
* The primary term builder
|
||||
*
|
||||
* @var Primary_Term_Builder
|
||||
*/
|
||||
private $primary_term_builder;
|
||||
|
||||
/**
|
||||
* The link builder
|
||||
*
|
||||
* @var Indexable_Link_Builder
|
||||
*/
|
||||
private $link_builder;
|
||||
|
||||
/**
|
||||
* The indexable repository.
|
||||
*
|
||||
* @var Indexable_Repository
|
||||
*/
|
||||
private $indexable_repository;
|
||||
|
||||
/**
|
||||
* The indexable helper.
|
||||
*
|
||||
* @var Indexable_Helper
|
||||
*/
|
||||
protected $indexable_helper;
|
||||
|
||||
/**
|
||||
* The Indexable Version Manager.
|
||||
*
|
||||
* @var Indexable_Version_Manager
|
||||
*/
|
||||
protected $version_manager;
|
||||
|
||||
/**
|
||||
* Returns the instance of this class constructed through the ORM Wrapper.
|
||||
*
|
||||
* @param Indexable_Author_Builder $author_builder The author builder for creating missing indexables.
|
||||
* @param Indexable_Post_Builder $post_builder The post builder for creating missing indexables.
|
||||
* @param Indexable_Term_Builder $term_builder The term builder for creating missing indexables.
|
||||
* @param Indexable_Home_Page_Builder $home_page_builder The front page builder for creating missing indexables.
|
||||
* @param Indexable_Post_Type_Archive_Builder $post_type_archive_builder The post type archive builder for creating missing indexables.
|
||||
* @param Indexable_Date_Archive_Builder $date_archive_builder The date archive builder for creating missing indexables.
|
||||
* @param Indexable_System_Page_Builder $system_page_builder The search result builder for creating missing indexables.
|
||||
* @param Indexable_Hierarchy_Builder $hierarchy_builder The hierarchy builder for creating the indexable hierarchy.
|
||||
* @param Primary_Term_Builder $primary_term_builder The primary term builder for creating primary terms for posts.
|
||||
* @param Indexable_Helper $indexable_helper The indexable helper.
|
||||
* @param Indexable_Version_Manager $version_manager The indexable version manager.
|
||||
* @param Indexable_Link_Builder $link_builder The link builder for creating missing SEO links.
|
||||
*/
|
||||
public function __construct(
|
||||
Indexable_Author_Builder $author_builder,
|
||||
Indexable_Post_Builder $post_builder,
|
||||
Indexable_Term_Builder $term_builder,
|
||||
Indexable_Home_Page_Builder $home_page_builder,
|
||||
Indexable_Post_Type_Archive_Builder $post_type_archive_builder,
|
||||
Indexable_Date_Archive_Builder $date_archive_builder,
|
||||
Indexable_System_Page_Builder $system_page_builder,
|
||||
Indexable_Hierarchy_Builder $hierarchy_builder,
|
||||
Primary_Term_Builder $primary_term_builder,
|
||||
Indexable_Helper $indexable_helper,
|
||||
Indexable_Version_Manager $version_manager,
|
||||
Indexable_Link_Builder $link_builder
|
||||
) {
|
||||
$this->author_builder = $author_builder;
|
||||
$this->post_builder = $post_builder;
|
||||
$this->term_builder = $term_builder;
|
||||
$this->home_page_builder = $home_page_builder;
|
||||
$this->post_type_archive_builder = $post_type_archive_builder;
|
||||
$this->date_archive_builder = $date_archive_builder;
|
||||
$this->system_page_builder = $system_page_builder;
|
||||
$this->hierarchy_builder = $hierarchy_builder;
|
||||
$this->primary_term_builder = $primary_term_builder;
|
||||
$this->indexable_helper = $indexable_helper;
|
||||
$this->version_manager = $version_manager;
|
||||
$this->link_builder = $link_builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the indexable repository. Done to avoid circular dependencies.
|
||||
*
|
||||
* @required
|
||||
*
|
||||
* @param Indexable_Repository $indexable_repository The indexable repository.
|
||||
*/
|
||||
public function set_indexable_repository( Indexable_Repository $indexable_repository ) {
|
||||
$this->indexable_repository = $indexable_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a clean copy of an Indexable to allow for later database operations.
|
||||
*
|
||||
* @param Indexable $indexable The Indexable to copy.
|
||||
*
|
||||
* @return bool|Indexable
|
||||
*/
|
||||
protected function deep_copy_indexable( $indexable ) {
|
||||
return $this->indexable_repository
|
||||
->query()
|
||||
->create( $indexable->as_array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an indexable by its ID and type.
|
||||
*
|
||||
* @param int $object_id The indexable object ID.
|
||||
* @param string $object_type The indexable object type.
|
||||
* @param Indexable|bool $indexable Optional. An existing indexable to overwrite.
|
||||
*
|
||||
* @return bool|Indexable Instance of indexable. False when unable to build.
|
||||
*/
|
||||
public function build_for_id_and_type( $object_id, $object_type, $indexable = false ) {
|
||||
$defaults = [
|
||||
'object_type' => $object_type,
|
||||
'object_id' => $object_id,
|
||||
];
|
||||
|
||||
$indexable = $this->build( $indexable, $defaults );
|
||||
|
||||
return $indexable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an indexable for the homepage.
|
||||
*
|
||||
* @param Indexable|bool $indexable Optional. An existing indexable to overwrite.
|
||||
*
|
||||
* @return Indexable The home page indexable.
|
||||
*/
|
||||
public function build_for_home_page( $indexable = false ) {
|
||||
return $this->build( $indexable, [ 'object_type' => 'home-page' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an indexable for the date archive.
|
||||
*
|
||||
* @param Indexable|bool $indexable Optional. An existing indexable to overwrite.
|
||||
*
|
||||
* @return Indexable The date archive indexable.
|
||||
*/
|
||||
public function build_for_date_archive( $indexable = false ) {
|
||||
return $this->build( $indexable, [ 'object_type' => 'date-archive' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an indexable for a post type archive.
|
||||
*
|
||||
* @param string $post_type The post type.
|
||||
* @param Indexable|bool $indexable Optional. An existing indexable to overwrite.
|
||||
*
|
||||
* @return Indexable The post type archive indexable.
|
||||
*/
|
||||
public function build_for_post_type_archive( $post_type, $indexable = false ) {
|
||||
$defaults = [
|
||||
'object_type' => 'post-type-archive',
|
||||
'object_sub_type' => $post_type,
|
||||
];
|
||||
return $this->build( $indexable, $defaults );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an indexable for a system page.
|
||||
*
|
||||
* @param string $page_type The type of system page.
|
||||
* @param Indexable|bool $indexable Optional. An existing indexable to overwrite.
|
||||
*
|
||||
* @return Indexable The search result indexable.
|
||||
*/
|
||||
public function build_for_system_page( $page_type, $indexable = false ) {
|
||||
$defaults = [
|
||||
'object_type' => 'system-page',
|
||||
'object_sub_type' => $page_type,
|
||||
];
|
||||
return $this->build( $indexable, $defaults );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures we have a valid indexable. Creates one if false is passed.
|
||||
*
|
||||
* @param Indexable|false $indexable The indexable.
|
||||
* @param array $defaults The initial properties of the Indexable.
|
||||
*
|
||||
* @return Indexable The indexable.
|
||||
*/
|
||||
protected function ensure_indexable( $indexable, $defaults = [] ) {
|
||||
if ( ! $indexable ) {
|
||||
return $this->indexable_repository->query()->create( $defaults );
|
||||
}
|
||||
|
||||
return $indexable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves and returns an indexable (on production environments only).
|
||||
*
|
||||
* @param Indexable $indexable The indexable.
|
||||
* @param Indexable|null $indexable_before The indexable before possible changes.
|
||||
*
|
||||
* @return Indexable The indexable.
|
||||
*/
|
||||
protected function save_indexable( $indexable, $indexable_before = null ) {
|
||||
$intend_to_save = $this->indexable_helper->should_index_indexables();
|
||||
|
||||
/**
|
||||
* Filter: 'wpseo_should_save_indexable' - Allow developers to enable / disable
|
||||
* saving the indexable when the indexable is updated. Warning: overriding
|
||||
* the intended action may cause problems when moving from a staging to a
|
||||
* production environment because indexable permalinks may get set incorrectly.
|
||||
*
|
||||
* @param Indexable $indexable The indexable to be saved.
|
||||
*
|
||||
* @api bool $intend_to_save True if YoastSEO intends to save the indexable.
|
||||
*/
|
||||
$intend_to_save = \apply_filters( 'wpseo_should_save_indexable', $intend_to_save, $indexable );
|
||||
|
||||
if ( ! $intend_to_save ) {
|
||||
return $indexable;
|
||||
}
|
||||
|
||||
// Save the indexable before running the WordPress hook.
|
||||
$indexable->save();
|
||||
|
||||
if ( $indexable_before ) {
|
||||
/**
|
||||
* Action: 'wpseo_save_indexable' - Allow developers to perform an action
|
||||
* when the indexable is updated.
|
||||
*
|
||||
* @param Indexable $indexable_before The indexable before saving.
|
||||
*
|
||||
* @api Indexable $indexable The saved indexable.
|
||||
*/
|
||||
\do_action( 'wpseo_save_indexable', $indexable, $indexable_before );
|
||||
}
|
||||
|
||||
return $indexable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and author indexable from an author id if it does not exist yet, or if the author indexable needs to be upgraded.
|
||||
*
|
||||
* @param int $author_id The author id.
|
||||
*
|
||||
* @return Indexable|false The author indexable if it has been built, `false` if it could not be built.
|
||||
*/
|
||||
protected function maybe_build_author_indexable( $author_id ) {
|
||||
$author_indexable = $this->indexable_repository->find_by_id_and_type(
|
||||
$author_id,
|
||||
'user',
|
||||
false
|
||||
);
|
||||
if ( ! $author_indexable || $this->version_manager->indexable_needs_upgrade( $author_indexable ) ) {
|
||||
// Try to build the author.
|
||||
$author_defaults = [
|
||||
'object_type' => 'user',
|
||||
'object_id' => $author_id,
|
||||
];
|
||||
$author_indexable = $this->build( $author_indexable, $author_defaults );
|
||||
}
|
||||
return $author_indexable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the indexable type is one that is not supposed to have object ID for.
|
||||
*
|
||||
* @param string $type The type of the indexable.
|
||||
*
|
||||
* @return bool Whether the indexable type is one that is not supposed to have object ID for.
|
||||
*/
|
||||
protected function is_type_with_no_id( $type ) {
|
||||
return \in_array( $type, [ 'home-page', 'date-archive', 'post-type-archive', 'system-page' ], true );
|
||||
}
|
||||
|
||||
// phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.Missing -- Exceptions are handled by the catch statement in the method.
|
||||
|
||||
/**
|
||||
* Rebuilds an Indexable from scratch.
|
||||
*
|
||||
* @param Indexable $indexable The Indexable to (re)build.
|
||||
* @param array|null $defaults The object type of the Indexable.
|
||||
*
|
||||
* @return Indexable|false The resulting Indexable.
|
||||
*/
|
||||
public function build( $indexable, $defaults = null ) {
|
||||
// Backup the previous Indexable, if there was one.
|
||||
$indexable_before = ( $indexable ) ? $this->deep_copy_indexable( $indexable ) : null;
|
||||
|
||||
// Make sure we have an Indexable to work with.
|
||||
$indexable = $this->ensure_indexable( $indexable, $defaults );
|
||||
|
||||
try {
|
||||
if ( $indexable->object_id === 0 ) {
|
||||
throw Not_Built_Exception::invalid_object_id( $indexable->object_id );
|
||||
}
|
||||
switch ( $indexable->object_type ) {
|
||||
|
||||
case 'post':
|
||||
$indexable = $this->post_builder->build( $indexable->object_id, $indexable );
|
||||
|
||||
// Save indexable, to make sure it can be queried when building related objects like the author indexable and hierarchy.
|
||||
$indexable = $this->save_indexable( $indexable, $indexable_before );
|
||||
|
||||
// For attachments, we have to make sure to patch any potentially previously cleaned up SEO links.
|
||||
if ( \is_a( $indexable, Indexable::class ) && $indexable->object_sub_type === 'attachment' ) {
|
||||
$this->link_builder->patch_seo_links( $indexable );
|
||||
}
|
||||
|
||||
// Always rebuild the primary term.
|
||||
$this->primary_term_builder->build( $indexable->object_id );
|
||||
|
||||
// Always rebuild the hierarchy; this needs the primary term to run correctly.
|
||||
$this->hierarchy_builder->build( $indexable );
|
||||
|
||||
$this->maybe_build_author_indexable( $indexable->author_id );
|
||||
|
||||
// The indexable is already saved, so return early.
|
||||
return $indexable;
|
||||
|
||||
case 'user':
|
||||
$indexable = $this->author_builder->build( $indexable->object_id, $indexable );
|
||||
break;
|
||||
|
||||
case 'term':
|
||||
$indexable = $this->term_builder->build( $indexable->object_id, $indexable );
|
||||
|
||||
// Save indexable, to make sure it can be queried when building hierarchy.
|
||||
$indexable = $this->save_indexable( $indexable, $indexable_before );
|
||||
|
||||
$this->hierarchy_builder->build( $indexable );
|
||||
|
||||
// The indexable is already saved, so return early.
|
||||
return $indexable;
|
||||
|
||||
case 'home-page':
|
||||
$indexable = $this->home_page_builder->build( $indexable );
|
||||
break;
|
||||
|
||||
case 'date-archive':
|
||||
$indexable = $this->date_archive_builder->build( $indexable );
|
||||
break;
|
||||
|
||||
case 'post-type-archive':
|
||||
$indexable = $this->post_type_archive_builder->build( $indexable->object_sub_type, $indexable );
|
||||
break;
|
||||
|
||||
case 'system-page':
|
||||
$indexable = $this->system_page_builder->build( $indexable->object_sub_type, $indexable );
|
||||
break;
|
||||
}
|
||||
|
||||
return $this->save_indexable( $indexable, $indexable_before );
|
||||
}
|
||||
catch ( Source_Exception $exception ) {
|
||||
if ( ! $this->is_type_with_no_id( $indexable->object_type ) && ( ! isset( $indexable->object_id ) || \is_null( $indexable->object_id ) ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current indexable could not be indexed. Create a placeholder indexable, so we can
|
||||
* skip this indexable in future indexing runs.
|
||||
*
|
||||
* @var Indexable $indexable
|
||||
*/
|
||||
$indexable = $this->ensure_indexable(
|
||||
$indexable,
|
||||
[
|
||||
'object_id' => $indexable->object_id,
|
||||
'object_type' => $indexable->object_type,
|
||||
'post_status' => 'unindexed',
|
||||
'version' => 0,
|
||||
]
|
||||
);
|
||||
// If we already had an existing indexable, mark it as unindexed. We cannot rely on its validity anymore.
|
||||
$indexable->post_status = 'unindexed';
|
||||
// Make sure that the indexing process doesn't get stuck in a loop on this broken indexable.
|
||||
$indexable = $this->version_manager->set_latest( $indexable );
|
||||
|
||||
return $this->save_indexable( $indexable, $indexable_before );
|
||||
}
|
||||
catch ( Not_Built_Exception $exception ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:enable
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Builders;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
|
||||
|
||||
/**
|
||||
* Date Archive Builder for the indexables.
|
||||
*
|
||||
* Formats the date archive meta to indexable format.
|
||||
*/
|
||||
class Indexable_Date_Archive_Builder {
|
||||
|
||||
/**
|
||||
* The options helper.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* The latest version of the Indexable_Date_Archive_Builder.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $version;
|
||||
|
||||
/**
|
||||
* Indexable_Date_Archive_Builder constructor.
|
||||
*
|
||||
* @param Options_Helper $options The options helper.
|
||||
* @param Indexable_Builder_Versions $versions The latest version for all indexable builders.
|
||||
*/
|
||||
public function __construct(
|
||||
Options_Helper $options,
|
||||
Indexable_Builder_Versions $versions
|
||||
) {
|
||||
$this->options = $options;
|
||||
$this->version = $versions->get_latest_version_for_type( 'date-archive' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data.
|
||||
*
|
||||
* @param Indexable $indexable The indexable to format.
|
||||
*
|
||||
* @return Indexable The extended indexable.
|
||||
*/
|
||||
public function build( $indexable ) {
|
||||
$indexable->object_type = 'date-archive';
|
||||
$indexable->title = $this->options->get( 'title-archive-wpseo' );
|
||||
$indexable->description = $this->options->get( 'metadesc-archive-wpseo' );
|
||||
$indexable->is_robots_noindex = $this->options->get( 'noindex-archive-wpseo' );
|
||||
$indexable->is_public = ( (int) $indexable->is_robots_noindex !== 1 );
|
||||
$indexable->blog_id = \get_current_blog_id();
|
||||
$indexable->permalink = null;
|
||||
$indexable->version = $this->version;
|
||||
|
||||
return $indexable;
|
||||
}
|
||||
}
|
@ -0,0 +1,400 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Builders;
|
||||
|
||||
use WP_Post;
|
||||
use WP_Term;
|
||||
use WPSEO_Meta;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Post_Helper;
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
use Yoast\WP\SEO\Repositories\Indexable_Hierarchy_Repository;
|
||||
use Yoast\WP\SEO\Repositories\Indexable_Repository;
|
||||
use Yoast\WP\SEO\Repositories\Primary_Term_Repository;
|
||||
|
||||
/**
|
||||
* Builder for the indexables hierarchy.
|
||||
*
|
||||
* Builds the indexable hierarchy for indexables.
|
||||
*/
|
||||
class Indexable_Hierarchy_Builder {
|
||||
|
||||
/**
|
||||
* Holds a list of indexables where the ancestors are saved for.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $saved_ancestors = [];
|
||||
|
||||
/**
|
||||
* The indexable repository.
|
||||
*
|
||||
* @var Indexable_Repository
|
||||
*/
|
||||
private $indexable_repository;
|
||||
|
||||
/**
|
||||
* The indexable hierarchy repository.
|
||||
*
|
||||
* @var Indexable_Hierarchy_Repository
|
||||
*/
|
||||
private $indexable_hierarchy_repository;
|
||||
|
||||
/**
|
||||
* The primary term repository.
|
||||
*
|
||||
* @var Primary_Term_Repository
|
||||
*/
|
||||
private $primary_term_repository;
|
||||
|
||||
/**
|
||||
* The options helper.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* Holds the Post_Helper instance.
|
||||
*
|
||||
* @var Post_Helper
|
||||
*/
|
||||
private $post;
|
||||
|
||||
/**
|
||||
* Indexable_Author_Builder constructor.
|
||||
*
|
||||
* @param Indexable_Hierarchy_Repository $indexable_hierarchy_repository The indexable hierarchy repository.
|
||||
* @param Primary_Term_Repository $primary_term_repository The primary term repository.
|
||||
* @param Options_Helper $options The options helper.
|
||||
* @param Post_Helper $post The post helper.
|
||||
*/
|
||||
public function __construct(
|
||||
Indexable_Hierarchy_Repository $indexable_hierarchy_repository,
|
||||
Primary_Term_Repository $primary_term_repository,
|
||||
Options_Helper $options,
|
||||
Post_Helper $post
|
||||
) {
|
||||
$this->indexable_hierarchy_repository = $indexable_hierarchy_repository;
|
||||
$this->primary_term_repository = $primary_term_repository;
|
||||
$this->options = $options;
|
||||
$this->post = $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the indexable repository. Done to avoid circular dependencies.
|
||||
*
|
||||
* @required
|
||||
*
|
||||
* @param Indexable_Repository $indexable_repository The indexable repository.
|
||||
*/
|
||||
public function set_indexable_repository( Indexable_Repository $indexable_repository ) {
|
||||
$this->indexable_repository = $indexable_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the ancestor hierarchy for an indexable.
|
||||
*
|
||||
* @param Indexable $indexable The indexable.
|
||||
*
|
||||
* @return Indexable The indexable.
|
||||
*/
|
||||
public function build( Indexable $indexable ) {
|
||||
if ( $this->hierarchy_is_built( $indexable ) ) {
|
||||
return $indexable;
|
||||
}
|
||||
|
||||
$this->indexable_hierarchy_repository->clear_ancestors( $indexable->id );
|
||||
|
||||
$indexable_id = $this->get_indexable_id( $indexable );
|
||||
$ancestors = [];
|
||||
if ( $indexable->object_type === 'post' ) {
|
||||
$this->add_ancestors_for_post( $indexable_id, $indexable->object_id, $ancestors );
|
||||
}
|
||||
|
||||
if ( $indexable->object_type === 'term' ) {
|
||||
$this->add_ancestors_for_term( $indexable_id, $indexable->object_id, $ancestors );
|
||||
}
|
||||
$indexable->ancestors = \array_reverse( \array_values( $ancestors ) );
|
||||
$indexable->has_ancestors = ! empty( $ancestors );
|
||||
if ( $indexable->id ) {
|
||||
$this->save_ancestors( $indexable );
|
||||
}
|
||||
|
||||
return $indexable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a hierarchy is built already for the given indexable.
|
||||
*
|
||||
* @param Indexable $indexable The indexable to check.
|
||||
*
|
||||
* @return bool True when indexable has a built hierarchy.
|
||||
*/
|
||||
protected function hierarchy_is_built( Indexable $indexable ) {
|
||||
if ( \in_array( $indexable->id, $this->saved_ancestors, true ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->saved_ancestors[] = $indexable->id;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the ancestors.
|
||||
*
|
||||
* @param Indexable $indexable The indexable.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function save_ancestors( $indexable ) {
|
||||
if ( empty( $indexable->ancestors ) ) {
|
||||
$this->indexable_hierarchy_repository->add_ancestor( $indexable->id, 0, 0 );
|
||||
return;
|
||||
}
|
||||
$depth = \count( $indexable->ancestors );
|
||||
foreach ( $indexable->ancestors as $ancestor ) {
|
||||
$this->indexable_hierarchy_repository->add_ancestor( $indexable->id, $ancestor->id, $depth );
|
||||
--$depth;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds ancestors for a post.
|
||||
*
|
||||
* @param int $indexable_id The indexable id, this is the id of the original indexable.
|
||||
* @param int $post_id The post id, this is the id of the post currently being evaluated.
|
||||
* @param int[] $parents The indexable IDs of all parents.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function add_ancestors_for_post( $indexable_id, $post_id, &$parents ) {
|
||||
$post = $this->post->get_post( $post_id );
|
||||
|
||||
if ( ! isset( $post->post_parent ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $post->post_parent !== 0 && $this->post->get_post( $post->post_parent ) !== null ) {
|
||||
$ancestor = $this->indexable_repository->find_by_id_and_type( $post->post_parent, 'post' );
|
||||
if ( $this->is_invalid_ancestor( $ancestor, $indexable_id, $parents ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parents[ $this->get_indexable_id( $ancestor ) ] = $ancestor;
|
||||
|
||||
$this->add_ancestors_for_post( $indexable_id, $ancestor->object_id, $parents );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$primary_term_id = $this->find_primary_term_id_for_post( $post );
|
||||
|
||||
if ( $primary_term_id === 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ancestor = $this->indexable_repository->find_by_id_and_type( $primary_term_id, 'term' );
|
||||
if ( $this->is_invalid_ancestor( $ancestor, $indexable_id, $parents ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parents[ $this->get_indexable_id( $ancestor ) ] = $ancestor;
|
||||
|
||||
$this->add_ancestors_for_term( $indexable_id, $ancestor->object_id, $parents );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds ancestors for a term.
|
||||
*
|
||||
* @param int $indexable_id The indexable id, this is the id of the original indexable.
|
||||
* @param int $term_id The term id, this is the id of the term currently being evaluated.
|
||||
* @param int[] $parents The indexable IDs of all parents.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function add_ancestors_for_term( $indexable_id, $term_id, &$parents = [] ) {
|
||||
$term = \get_term( $term_id );
|
||||
$term_parents = $this->get_term_parents( $term );
|
||||
|
||||
foreach ( $term_parents as $parent ) {
|
||||
$ancestor = $this->indexable_repository->find_by_id_and_type( $parent->term_id, 'term' );
|
||||
if ( $this->is_invalid_ancestor( $ancestor, $indexable_id, $parents ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parents[ $this->get_indexable_id( $ancestor ) ] = $ancestor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the primary term ID for a post.
|
||||
*
|
||||
* @param WP_Post $post The post.
|
||||
*
|
||||
* @return int The primary term ID. 0 if none exists.
|
||||
*/
|
||||
private function find_primary_term_id_for_post( $post ) {
|
||||
$main_taxonomy = $this->options->get( 'post_types-' . $post->post_type . '-maintax' );
|
||||
|
||||
if ( ! $main_taxonomy || $main_taxonomy === '0' ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$primary_term_id = $this->get_primary_term_id( $post->ID, $main_taxonomy );
|
||||
|
||||
if ( $primary_term_id ) {
|
||||
$term = \get_term( $primary_term_id );
|
||||
if ( $term !== null && ! \is_wp_error( $term ) ) {
|
||||
return $primary_term_id;
|
||||
}
|
||||
}
|
||||
|
||||
$terms = \get_the_terms( $post->ID, $main_taxonomy );
|
||||
|
||||
if ( ! \is_array( $terms ) || empty( $terms ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $this->find_deepest_term_id( $terms );
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the deepest term in an array of term objects.
|
||||
*
|
||||
* @param array $terms Terms set.
|
||||
*
|
||||
* @return int The deepest term ID.
|
||||
*/
|
||||
private function find_deepest_term_id( $terms ) {
|
||||
/*
|
||||
* Let's find the deepest term in this array, by looping through and then
|
||||
* unsetting every term that is used as a parent by another one in the array.
|
||||
*/
|
||||
$terms_by_id = [];
|
||||
foreach ( $terms as $term ) {
|
||||
$terms_by_id[ $term->term_id ] = $term;
|
||||
}
|
||||
foreach ( $terms as $term ) {
|
||||
unset( $terms_by_id[ $term->parent ] );
|
||||
}
|
||||
|
||||
/*
|
||||
* As we could still have two subcategories, from different parent categories,
|
||||
* let's pick the one with the lowest ordered ancestor.
|
||||
*/
|
||||
$parents_count = -1;
|
||||
$term_order = 9999; // Because ASC.
|
||||
$deepest_term = \reset( $terms_by_id );
|
||||
foreach ( $terms_by_id as $term ) {
|
||||
$parents = $this->get_term_parents( $term );
|
||||
|
||||
$new_parents_count = \count( $parents );
|
||||
|
||||
if ( $new_parents_count < $parents_count ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parent_order = 9999; // Set default order.
|
||||
foreach ( $parents as $parent ) {
|
||||
if ( $parent->parent === 0 && isset( $parent->term_order ) ) {
|
||||
$parent_order = $parent->term_order;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if parent has lowest order.
|
||||
if ( $new_parents_count > $parents_count || $parent_order < $term_order ) {
|
||||
$term_order = $parent_order;
|
||||
$deepest_term = $term;
|
||||
}
|
||||
|
||||
$parents_count = $new_parents_count;
|
||||
}
|
||||
|
||||
return $deepest_term->term_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a term's parents.
|
||||
*
|
||||
* @param WP_Term $term Term to get the parents for.
|
||||
*
|
||||
* @return WP_Term[] An array of all this term's parents.
|
||||
*/
|
||||
private function get_term_parents( $term ) {
|
||||
$tax = $term->taxonomy;
|
||||
$parents = [];
|
||||
while ( (int) $term->parent !== 0 ) {
|
||||
$term = \get_term( $term->parent, $tax );
|
||||
$parents[] = $term;
|
||||
}
|
||||
|
||||
return $parents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an ancestor is valid to add.
|
||||
*
|
||||
* @param Indexable $ancestor The ancestor (presumed indexable) to check.
|
||||
* @param int $indexable_id The indexable id we're adding ancestors for.
|
||||
* @param int[] $parents The indexable ids of the parents already added.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_invalid_ancestor( $ancestor, $indexable_id, $parents ) {
|
||||
// If the ancestor is not an Indexable, it is invalid by default.
|
||||
if ( ! \is_a( $ancestor, 'Yoast\WP\SEO\Models\Indexable' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Don't add ancestors if they're unindexed, already added or the same as the main object.
|
||||
if ( $ancestor->post_status === 'unindexed' ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$ancestor_id = $this->get_indexable_id( $ancestor );
|
||||
if ( \array_key_exists( $ancestor_id, $parents ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( $ancestor_id === $indexable_id ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID for an indexable. Catches situations where the id is null due to errors.
|
||||
*
|
||||
* @param Indexable $indexable The indexable.
|
||||
*
|
||||
* @return string|int A unique ID for the indexable.
|
||||
*/
|
||||
private function get_indexable_id( Indexable $indexable ) {
|
||||
if ( $indexable->id === 0 ) {
|
||||
return "{$indexable->object_type}:{$indexable->object_id}";
|
||||
}
|
||||
|
||||
return $indexable->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the primary term id of a post.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @param string $main_taxonomy The main taxonomy.
|
||||
*
|
||||
* @return int The ID of the primary term.
|
||||
*/
|
||||
private function get_primary_term_id( $post_id, $main_taxonomy ) {
|
||||
$primary_term = $this->primary_term_repository->find_by_post_id_and_taxonomy( $post_id, $main_taxonomy, false );
|
||||
|
||||
if ( $primary_term ) {
|
||||
return $primary_term->term_id;
|
||||
}
|
||||
|
||||
return \get_post_meta( $post_id, WPSEO_Meta::$meta_prefix . 'primary_' . $main_taxonomy, true );
|
||||
}
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Builders;
|
||||
|
||||
use wpdb;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Post_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Url_Helper;
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
|
||||
|
||||
/**
|
||||
* Homepage Builder for the indexables.
|
||||
*
|
||||
* Formats the homepage meta to indexable format.
|
||||
*/
|
||||
class Indexable_Home_Page_Builder {
|
||||
|
||||
use Indexable_Social_Image_Trait;
|
||||
|
||||
/**
|
||||
* The options helper.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* The URL helper.
|
||||
*
|
||||
* @var Url_Helper
|
||||
*/
|
||||
protected $url_helper;
|
||||
|
||||
/**
|
||||
* The latest version of the Indexable-Home-Page-Builder.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $version;
|
||||
|
||||
/**
|
||||
* Holds the taxonomy helper instance.
|
||||
*
|
||||
* @var Post_Helper
|
||||
*/
|
||||
protected $post_helper;
|
||||
|
||||
/**
|
||||
* The WPDB instance.
|
||||
*
|
||||
* @var wpdb
|
||||
*/
|
||||
protected $wpdb;
|
||||
|
||||
/**
|
||||
* Indexable_Home_Page_Builder constructor.
|
||||
*
|
||||
* @param Options_Helper $options The options helper.
|
||||
* @param Url_Helper $url_helper The url helper.
|
||||
* @param Indexable_Builder_Versions $versions Knows the latest version of each Indexable type.
|
||||
* @param Post_Helper $post_helper The post helper.
|
||||
* @param wpdb $wpdb The WPDB instance.
|
||||
*/
|
||||
public function __construct(
|
||||
Options_Helper $options,
|
||||
Url_Helper $url_helper,
|
||||
Indexable_Builder_Versions $versions,
|
||||
Post_Helper $post_helper,
|
||||
wpdb $wpdb
|
||||
) {
|
||||
$this->options = $options;
|
||||
$this->url_helper = $url_helper;
|
||||
$this->version = $versions->get_latest_version_for_type( 'home-page' );
|
||||
$this->post_helper = $post_helper;
|
||||
$this->wpdb = $wpdb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data.
|
||||
*
|
||||
* @param Indexable $indexable The indexable to format.
|
||||
*
|
||||
* @return Indexable The extended indexable.
|
||||
*/
|
||||
public function build( $indexable ) {
|
||||
$indexable->object_type = 'home-page';
|
||||
$indexable->title = $this->options->get( 'title-home-wpseo' );
|
||||
$indexable->breadcrumb_title = $this->options->get( 'breadcrumbs-home' );
|
||||
$indexable->permalink = $this->url_helper->home();
|
||||
$indexable->blog_id = \get_current_blog_id();
|
||||
$indexable->description = $this->options->get( 'metadesc-home-wpseo' );
|
||||
if ( empty( $indexable->description ) ) {
|
||||
$indexable->description = \get_bloginfo( 'description' );
|
||||
}
|
||||
|
||||
$indexable->is_robots_noindex = \get_option( 'blog_public' ) === '0';
|
||||
|
||||
$indexable->open_graph_title = $this->options->get( 'open_graph_frontpage_title' );
|
||||
$indexable->open_graph_image = $this->options->get( 'open_graph_frontpage_image' );
|
||||
$indexable->open_graph_image_id = $this->options->get( 'open_graph_frontpage_image_id' );
|
||||
$indexable->open_graph_description = $this->options->get( 'open_graph_frontpage_desc' );
|
||||
|
||||
// Reset the OG image source & meta.
|
||||
$indexable->open_graph_image_source = null;
|
||||
$indexable->open_graph_image_meta = null;
|
||||
|
||||
// When the image or image id is set.
|
||||
if ( $indexable->open_graph_image || $indexable->open_graph_image_id ) {
|
||||
$indexable->open_graph_image_source = 'set-by-user';
|
||||
|
||||
$this->set_open_graph_image_meta_data( $indexable );
|
||||
}
|
||||
|
||||
$timestamps = $this->get_object_timestamps();
|
||||
$indexable->object_published_at = $timestamps->published_at;
|
||||
$indexable->object_last_modified = $timestamps->last_modified;
|
||||
|
||||
$indexable->version = $this->version;
|
||||
|
||||
return $indexable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamps for the homepage.
|
||||
*
|
||||
* @return object An object with last_modified and published_at timestamps.
|
||||
*/
|
||||
protected function get_object_timestamps() {
|
||||
$post_statuses = $this->post_helper->get_public_post_statuses();
|
||||
|
||||
$sql = "
|
||||
SELECT MAX(p.post_modified_gmt) AS last_modified, MIN(p.post_date_gmt) AS published_at
|
||||
FROM {$this->wpdb->posts} AS p
|
||||
WHERE p.post_status IN (" . \implode( ', ', \array_fill( 0, \count( $post_statuses ), '%s' ) ) . ")
|
||||
AND p.post_password = ''
|
||||
AND p.post_type = 'post'
|
||||
";
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- We are using wpdb prepare.
|
||||
return $this->wpdb->get_row( $this->wpdb->prepare( $sql, $post_statuses ) );
|
||||
}
|
||||
}
|
@ -0,0 +1,577 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Builders;
|
||||
|
||||
use WPSEO_Image_Utils;
|
||||
use Yoast\WP\SEO\Helpers\Image_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Post_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Url_Helper;
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
use Yoast\WP\SEO\Models\SEO_Links;
|
||||
use Yoast\WP\SEO\Repositories\Indexable_Repository;
|
||||
use Yoast\WP\SEO\Repositories\SEO_Links_Repository;
|
||||
|
||||
/**
|
||||
* Indexable link builder.
|
||||
*/
|
||||
class Indexable_Link_Builder {
|
||||
|
||||
/**
|
||||
* The SEO links repository.
|
||||
*
|
||||
* @var SEO_Links_Repository
|
||||
*/
|
||||
protected $seo_links_repository;
|
||||
|
||||
/**
|
||||
* The url helper.
|
||||
*
|
||||
* @var Url_Helper
|
||||
*/
|
||||
protected $url_helper;
|
||||
|
||||
/**
|
||||
* The image helper.
|
||||
*
|
||||
* @var Image_Helper
|
||||
*/
|
||||
protected $image_helper;
|
||||
|
||||
/**
|
||||
* The post helper.
|
||||
*
|
||||
* @var Post_Helper
|
||||
*/
|
||||
protected $post_helper;
|
||||
|
||||
/**
|
||||
* The options helper.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
protected $options_helper;
|
||||
|
||||
/**
|
||||
* The indexable repository.
|
||||
*
|
||||
* @var Indexable_Repository
|
||||
*/
|
||||
protected $indexable_repository;
|
||||
|
||||
/**
|
||||
* Indexable_Link_Builder constructor.
|
||||
*
|
||||
* @param SEO_Links_Repository $seo_links_repository The SEO links repository.
|
||||
* @param Url_Helper $url_helper The URL helper.
|
||||
* @param Post_Helper $post_helper The post helper.
|
||||
* @param Options_Helper $options_helper The options helper.
|
||||
*/
|
||||
public function __construct(
|
||||
SEO_Links_Repository $seo_links_repository,
|
||||
Url_Helper $url_helper,
|
||||
Post_Helper $post_helper,
|
||||
Options_Helper $options_helper
|
||||
) {
|
||||
$this->seo_links_repository = $seo_links_repository;
|
||||
$this->url_helper = $url_helper;
|
||||
$this->post_helper = $post_helper;
|
||||
$this->options_helper = $options_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the indexable repository.
|
||||
*
|
||||
* @required
|
||||
*
|
||||
* @param Indexable_Repository $indexable_repository The indexable repository.
|
||||
* @param Image_Helper $image_helper The image helper.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set_dependencies(
|
||||
Indexable_Repository $indexable_repository,
|
||||
Image_Helper $image_helper
|
||||
) {
|
||||
$this->indexable_repository = $indexable_repository;
|
||||
$this->image_helper = $image_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the links for a post.
|
||||
*
|
||||
* @param Indexable $indexable The indexable.
|
||||
* @param string $content The content. Expected to be unfiltered.
|
||||
*
|
||||
* @return SEO_Links[] The created SEO links.
|
||||
*/
|
||||
public function build( $indexable, $content ) {
|
||||
global $post;
|
||||
if ( $indexable->object_type === 'post' ) {
|
||||
$post_backup = $post;
|
||||
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- To setup the post we need to do this explicitly.
|
||||
$post = $this->post_helper->get_post( $indexable->object_id );
|
||||
\setup_postdata( $post );
|
||||
$content = \apply_filters( 'the_content', $content );
|
||||
\wp_reset_postdata();
|
||||
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- To setup the post we need to do this explicitly.
|
||||
$post = $post_backup;
|
||||
}
|
||||
|
||||
$content = \str_replace( ']]>', ']]>', $content );
|
||||
$links = $this->gather_links( $content );
|
||||
$images = $this->gather_images( $content );
|
||||
|
||||
if ( empty( $links ) && empty( $images ) ) {
|
||||
$indexable->link_count = 0;
|
||||
$this->update_related_indexables( $indexable, [] );
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$links = $this->create_links( $indexable, $links, $images );
|
||||
|
||||
$this->update_related_indexables( $indexable, $links );
|
||||
|
||||
$indexable->link_count = $this->get_internal_link_count( $links );
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all SEO links for an indexable.
|
||||
*
|
||||
* @param Indexable $indexable The indexable.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete( $indexable ) {
|
||||
$links = ( $this->seo_links_repository->find_all_by_indexable_id( $indexable->id ) );
|
||||
$this->seo_links_repository->delete_all_by_indexable_id( $indexable->id );
|
||||
|
||||
$linked_indexable_ids = [];
|
||||
foreach ( $links as $link ) {
|
||||
if ( $link->target_indexable_id ) {
|
||||
$linked_indexable_ids[] = $link->target_indexable_id;
|
||||
}
|
||||
}
|
||||
|
||||
$this->update_incoming_links_for_related_indexables( $linked_indexable_ids );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes existing SEO links that are supposed to have a target indexable but don't, because of prior indexable cleanup.
|
||||
*
|
||||
* @param Indexable $indexable The indexable to be the target of SEO Links.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function patch_seo_links( Indexable $indexable ) {
|
||||
if ( ! empty( $indexable->id ) && ! empty( $indexable->object_id ) ) {
|
||||
$links = $this->seo_links_repository->find_all_by_target_post_id( $indexable->object_id );
|
||||
|
||||
$updated_indexable = false;
|
||||
foreach ( $links as $link ) {
|
||||
if ( \is_a( $link, SEO_Links::class ) && empty( $link->target_indexable_id ) ) {
|
||||
// Since that post ID exists in an SEO link but has no target_indexable_id, it's probably because of prior indexable cleanup.
|
||||
$this->seo_links_repository->update_target_indexable_id( $link->id, $indexable->id );
|
||||
$updated_indexable = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $updated_indexable ) {
|
||||
$updated_indexable_id = [ $indexable->id ];
|
||||
$this->update_incoming_links_for_related_indexables( $updated_indexable_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers all links from content.
|
||||
*
|
||||
* @param string $content The content.
|
||||
*
|
||||
* @return string[] An array of urls.
|
||||
*/
|
||||
protected function gather_links( $content ) {
|
||||
if ( \strpos( $content, 'href' ) === false ) {
|
||||
// Nothing to do.
|
||||
return [];
|
||||
}
|
||||
|
||||
$links = [];
|
||||
$regexp = '<a\s[^>]*href=("??)([^" >]*?)\1[^>]*>';
|
||||
// Used modifiers iU to match case insensitive and make greedy quantifiers lazy.
|
||||
if ( \preg_match_all( "/$regexp/iU", $content, $matches, \PREG_SET_ORDER ) ) {
|
||||
foreach ( $matches as $match ) {
|
||||
$links[] = \trim( $match[2], "'" );
|
||||
}
|
||||
}
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers all images from content.
|
||||
*
|
||||
* @param string $content The content.
|
||||
*
|
||||
* @return string[] An array of urls.
|
||||
*/
|
||||
protected function gather_images( $content ) {
|
||||
if ( \strpos( $content, 'src' ) === false ) {
|
||||
// Nothing to do.
|
||||
return [];
|
||||
}
|
||||
|
||||
$images = [];
|
||||
$regexp = '<img\s[^>]*src=("??)([^" >]*?)\\1[^>]*>';
|
||||
// Used modifiers iU to match case insensitive and make greedy quantifiers lazy.
|
||||
if ( \preg_match_all( "/$regexp/iU", $content, $matches, \PREG_SET_ORDER ) ) {
|
||||
foreach ( $matches as $match ) {
|
||||
$images[] = \trim( $match[2], "'" );
|
||||
}
|
||||
}
|
||||
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates link models from lists of URLs and image sources.
|
||||
*
|
||||
* @param Indexable $indexable The indexable.
|
||||
* @param string[] $links The link URLs.
|
||||
* @param string[] $images The image sources.
|
||||
*
|
||||
* @return SEO_Links[] The link models.
|
||||
*/
|
||||
protected function create_links( $indexable, $links, $images ) {
|
||||
$home_url = \wp_parse_url( \home_url() );
|
||||
$current_url = \wp_parse_url( $indexable->permalink );
|
||||
$links = \array_map(
|
||||
function( $link ) use ( $home_url, $indexable ) {
|
||||
return $this->create_internal_link( $link, $home_url, $indexable );
|
||||
},
|
||||
$links
|
||||
);
|
||||
// Filter out links to the same page with a fragment or query.
|
||||
$links = \array_filter(
|
||||
$links,
|
||||
function ( $link ) use ( $current_url ) {
|
||||
return $this->filter_link( $link, $current_url );
|
||||
}
|
||||
);
|
||||
|
||||
$images = \array_map(
|
||||
function( $link ) use ( $home_url, $indexable ) {
|
||||
return $this->create_internal_link( $link, $home_url, $indexable, true );
|
||||
},
|
||||
$images
|
||||
);
|
||||
return \array_merge( $links, $images );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the post ID based on the link's type and its target's permalink.
|
||||
*
|
||||
* @param string $type The type of link (either SEO_Links::TYPE_INTERNAL or SEO_Links::TYPE_INTERNAL_IMAGE).
|
||||
* @param string $permalink The permalink of the link's target.
|
||||
*
|
||||
* @return int The post ID.
|
||||
*/
|
||||
protected function get_post_id( $type, $permalink ) {
|
||||
if ( $type === SEO_Links::TYPE_INTERNAL ) {
|
||||
return \url_to_postid( $permalink );
|
||||
}
|
||||
|
||||
return $this->image_helper->get_attachment_by_url( $permalink );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an internal link.
|
||||
*
|
||||
* @param string $url The url of the link.
|
||||
* @param array $home_url The home url, as parsed by wp_parse_url.
|
||||
* @param Indexable $indexable The indexable of the post containing the link.
|
||||
* @param bool $is_image Whether or not the link is an image.
|
||||
*
|
||||
* @return SEO_Links The created link.
|
||||
*/
|
||||
protected function create_internal_link( $url, $home_url, $indexable, $is_image = false ) {
|
||||
$parsed_url = \wp_parse_url( $url );
|
||||
$link_type = $this->url_helper->get_link_type( $parsed_url, $home_url, $is_image );
|
||||
|
||||
/**
|
||||
* ORM representing a link in the SEO Links table.
|
||||
*
|
||||
* @var SEO_Links $model
|
||||
*/
|
||||
$model = $this->seo_links_repository->query()->create(
|
||||
[
|
||||
'url' => $url,
|
||||
'type' => $link_type,
|
||||
'indexable_id' => $indexable->id,
|
||||
'post_id' => $indexable->object_id,
|
||||
]
|
||||
);
|
||||
|
||||
$model->parsed_url = $parsed_url;
|
||||
|
||||
if ( $model->type === SEO_Links::TYPE_INTERNAL ) {
|
||||
$permalink = $this->build_permalink( $url, $home_url );
|
||||
|
||||
return $this->enhance_link_from_indexable( $model, $permalink );
|
||||
}
|
||||
|
||||
if ( $model->type === SEO_Links::TYPE_INTERNAL_IMAGE ) {
|
||||
$permalink = $this->build_permalink( $url, $home_url );
|
||||
|
||||
if ( ! $this->options_helper->get( 'disable-attachment' ) ) {
|
||||
$model = $this->enhance_link_from_indexable( $model, $permalink );
|
||||
}
|
||||
else {
|
||||
$target_post_id = WPSEO_Image_Utils::get_attachment_by_url( $permalink );
|
||||
|
||||
if ( ! empty( $target_post_id ) ) {
|
||||
$model->target_post_id = $target_post_id;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $model->target_post_id ) {
|
||||
$file = \get_attached_file( $model->target_post_id );
|
||||
|
||||
if ( $file ) {
|
||||
if ( \file_exists( $file ) ) {
|
||||
$model->size = \filesize( $file );
|
||||
}
|
||||
else {
|
||||
$model->size = null;
|
||||
}
|
||||
|
||||
list( , $width, $height ) = \wp_get_attachment_image_src( $model->target_post_id, 'full' );
|
||||
$model->width = $width;
|
||||
$model->height = $height;
|
||||
}
|
||||
else {
|
||||
$model->width = 0;
|
||||
$model->height = 0;
|
||||
$model->size = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances the link model with information from its indexable.
|
||||
*
|
||||
* @param SEO_Links $model The link's model.
|
||||
* @param string $permalink The link's permalink.
|
||||
*
|
||||
* @return SEO_Links The enhanced link model.
|
||||
*/
|
||||
protected function enhance_link_from_indexable( $model, $permalink ) {
|
||||
$target = $this->indexable_repository->find_by_permalink( $permalink );
|
||||
|
||||
if ( ! $target ) {
|
||||
// If target indexable cannot be found, create one based on the post's post ID.
|
||||
$post_id = $this->get_post_id( $model->type, $permalink );
|
||||
if ( $post_id && $post_id !== 0 ) {
|
||||
$target = $this->indexable_repository->find_by_id_and_type( $post_id, 'post' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $target ) {
|
||||
return $model;
|
||||
}
|
||||
|
||||
$model->target_indexable_id = $target->id;
|
||||
if ( $target->object_type === 'post' ) {
|
||||
$model->target_post_id = $target->object_id;
|
||||
}
|
||||
|
||||
if ( $model->target_indexable_id ) {
|
||||
$model->language = $target->language;
|
||||
$model->region = $target->region;
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the link's permalink.
|
||||
*
|
||||
* @param string $url The url of the link.
|
||||
* @param array $home_url The home url, as parsed by wp_parse_url.
|
||||
*
|
||||
* @return string The link's permalink.
|
||||
*/
|
||||
protected function build_permalink( $url, $home_url ) {
|
||||
$permalink = $this->get_permalink( $url, $home_url );
|
||||
|
||||
if ( $this->url_helper->is_relative( $permalink ) ) {
|
||||
// Make sure we're checking against the absolute URL, and add a trailing slash if the site has a trailing slash in its permalink settings.
|
||||
$permalink = $this->url_helper->ensure_absolute_url( \user_trailingslashit( $permalink ) );
|
||||
}
|
||||
|
||||
return $permalink;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out links that point to the same page with a fragment or query.
|
||||
*
|
||||
* @param SEO_Links $link The link.
|
||||
* @param array $current_url The url of the page the link is on, as parsed by wp_parse_url.
|
||||
*
|
||||
* @return bool Whether or not the link should be filtered.
|
||||
*/
|
||||
protected function filter_link( SEO_Links $link, $current_url ) {
|
||||
$url = $link->parsed_url;
|
||||
|
||||
// Always keep external links.
|
||||
if ( $link->type === SEO_Links::TYPE_EXTERNAL ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Always keep links with an empty path or pointing to other pages.
|
||||
if ( isset( $url['path'] ) ) {
|
||||
return empty( $url['path'] ) || $url['path'] !== $current_url['path'];
|
||||
}
|
||||
|
||||
// Only keep links to the current page without a fragment or query.
|
||||
return ( ! isset( $url['fragment'] ) && ! isset( $url['query'] ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the link counts for related indexables.
|
||||
*
|
||||
* @param Indexable $indexable The indexable.
|
||||
* @param SEO_Links[] $links The link models.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function update_related_indexables( $indexable, $links ) {
|
||||
// Old links were only stored by post id, so remove all old seo links for this post that have no indexable id.
|
||||
// This can be removed if we ever fully clear all seo links.
|
||||
if ( $indexable->object_type === 'post' ) {
|
||||
$this->seo_links_repository->delete_all_by_post_id_where_indexable_id_null( $indexable->object_id );
|
||||
}
|
||||
|
||||
$updated_indexable_ids = [];
|
||||
$old_links = $this->seo_links_repository->find_all_by_indexable_id( $indexable->id );
|
||||
|
||||
$links_to_remove = $this->links_diff( $old_links, $links );
|
||||
$links_to_add = $this->links_diff( $links, $old_links );
|
||||
|
||||
if ( ! empty( $links_to_remove ) ) {
|
||||
$this->seo_links_repository->delete_many_by_id( \wp_list_pluck( $links_to_remove, 'id' ) );
|
||||
}
|
||||
|
||||
if ( ! empty( $links_to_add ) ) {
|
||||
$this->seo_links_repository->insert_many( $links_to_add );
|
||||
}
|
||||
|
||||
foreach ( $links_to_add as $link ) {
|
||||
if ( $link->target_indexable_id ) {
|
||||
$updated_indexable_ids[] = $link->target_indexable_id;
|
||||
}
|
||||
}
|
||||
foreach ( $links_to_remove as $link ) {
|
||||
if ( $link->target_indexable_id ) {
|
||||
$updated_indexable_ids[] = $link->target_indexable_id;
|
||||
}
|
||||
}
|
||||
|
||||
$this->update_incoming_links_for_related_indexables( $updated_indexable_ids );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a diff between two arrays of SEO links, based on urls.
|
||||
*
|
||||
* @param SEO_Links[] $links_a The array to compare.
|
||||
* @param SEO_Links[] $links_b The array to compare against.
|
||||
*
|
||||
* @return SEO_Links[] Links that are in $links_a, but not in $links_b.
|
||||
*/
|
||||
protected function links_diff( $links_a, $links_b ) {
|
||||
return \array_udiff(
|
||||
$links_a,
|
||||
$links_b,
|
||||
static function( SEO_Links $link_a, SEO_Links $link_b ) {
|
||||
return \strcmp( $link_a->url, $link_b->url );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of internal links in an array of link models.
|
||||
*
|
||||
* @param SEO_Links[] $links The link models.
|
||||
*
|
||||
* @return int The number of internal links.
|
||||
*/
|
||||
protected function get_internal_link_count( $links ) {
|
||||
$internal_link_count = 0;
|
||||
|
||||
foreach ( $links as $link ) {
|
||||
if ( $link->type === SEO_Links::TYPE_INTERNAL ) {
|
||||
++$internal_link_count;
|
||||
}
|
||||
}
|
||||
|
||||
return $internal_link_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a cleaned permalink for a given link.
|
||||
*
|
||||
* @param string $link The raw URL.
|
||||
* @param array $home_url The home URL, as parsed by wp_parse_url.
|
||||
*
|
||||
* @return string The cleaned permalink.
|
||||
*/
|
||||
protected function get_permalink( $link, $home_url ) {
|
||||
// Get rid of the #anchor.
|
||||
$url_split = \explode( '#', $link );
|
||||
$link = $url_split[0];
|
||||
|
||||
// Get rid of URL ?query=string.
|
||||
$url_split = \explode( '?', $link );
|
||||
$link = $url_split[0];
|
||||
|
||||
// Set the correct URL scheme.
|
||||
$link = \set_url_scheme( $link, $home_url['scheme'] );
|
||||
|
||||
// Add 'www.' if it is absent and should be there.
|
||||
if ( \strpos( $home_url['host'], 'www.' ) === 0 && \strpos( $link, '://www.' ) === false ) {
|
||||
$link = \str_replace( '://', '://www.', $link );
|
||||
}
|
||||
|
||||
// Strip 'www.' if it is present and shouldn't be.
|
||||
if ( \strpos( $home_url['host'], 'www.' ) !== 0 ) {
|
||||
$link = \str_replace( '://www.', '://', $link );
|
||||
}
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates incoming link counts for related indexables.
|
||||
*
|
||||
* @param int[] $related_indexable_ids The IDs of all related indexables.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function update_incoming_links_for_related_indexables( $related_indexable_ids ) {
|
||||
if ( empty( $related_indexable_ids ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$counts = $this->seo_links_repository->get_incoming_link_counts_for_indexable_ids( $related_indexable_ids );
|
||||
foreach ( $counts as $count ) {
|
||||
|
||||
$this->indexable_repository->update_incoming_link_count( $count['target_indexable_id'], $count['incoming'] );
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,431 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Builders;
|
||||
|
||||
use WP_Error;
|
||||
use WP_Post;
|
||||
use Yoast\WP\SEO\Exceptions\Indexable\Post_Not_Built_Exception;
|
||||
use Yoast\WP\SEO\Exceptions\Indexable\Post_Not_Found_Exception;
|
||||
use Yoast\WP\SEO\Helpers\Meta_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Post_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
use Yoast\WP\SEO\Repositories\Indexable_Repository;
|
||||
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
|
||||
|
||||
/**
|
||||
* Post Builder for the indexables.
|
||||
*
|
||||
* Formats the post meta to indexable format.
|
||||
*/
|
||||
class Indexable_Post_Builder {
|
||||
|
||||
use Indexable_Social_Image_Trait;
|
||||
|
||||
/**
|
||||
* The indexable repository.
|
||||
*
|
||||
* @var Indexable_Repository
|
||||
*/
|
||||
protected $indexable_repository;
|
||||
|
||||
/**
|
||||
* Holds the Post_Helper instance.
|
||||
*
|
||||
* @var Post_Helper
|
||||
*/
|
||||
protected $post_helper;
|
||||
|
||||
/**
|
||||
* The post type helper.
|
||||
*
|
||||
* @var Post_Type_Helper
|
||||
*/
|
||||
protected $post_type_helper;
|
||||
|
||||
/**
|
||||
* Knows the latest version of the Indexable post builder type.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $version;
|
||||
|
||||
/**
|
||||
* The meta helper.
|
||||
*
|
||||
* @var Meta_Helper
|
||||
*/
|
||||
protected $meta;
|
||||
|
||||
/**
|
||||
* Indexable_Post_Builder constructor.
|
||||
*
|
||||
* @param Post_Helper $post_helper The post helper.
|
||||
* @param Post_Type_Helper $post_type_helper The post type helper.
|
||||
* @param Indexable_Builder_Versions $versions The indexable builder versions.
|
||||
* @param Meta_Helper $meta The meta helper.
|
||||
*/
|
||||
public function __construct(
|
||||
Post_Helper $post_helper,
|
||||
Post_Type_Helper $post_type_helper,
|
||||
Indexable_Builder_Versions $versions,
|
||||
Meta_Helper $meta
|
||||
) {
|
||||
$this->post_helper = $post_helper;
|
||||
$this->post_type_helper = $post_type_helper;
|
||||
$this->version = $versions->get_latest_version_for_type( 'post' );
|
||||
$this->meta = $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the indexable repository. Done to avoid circular dependencies.
|
||||
*
|
||||
* @required
|
||||
*
|
||||
* @param Indexable_Repository $indexable_repository The indexable repository.
|
||||
*/
|
||||
public function set_indexable_repository( Indexable_Repository $indexable_repository ) {
|
||||
$this->indexable_repository = $indexable_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data.
|
||||
*
|
||||
* @param int $post_id The post ID to use.
|
||||
* @param Indexable $indexable The indexable to format.
|
||||
*
|
||||
* @return bool|Indexable The extended indexable. False when unable to build.
|
||||
*
|
||||
* @throws Post_Not_Found_Exception When the post could not be found.
|
||||
* @throws Post_Not_Built_Exception When the post should not be indexed.
|
||||
*/
|
||||
public function build( $post_id, $indexable ) {
|
||||
if ( ! $this->post_helper->is_post_indexable( $post_id ) ) {
|
||||
throw Post_Not_Built_Exception::because_not_indexable( $post_id );
|
||||
}
|
||||
|
||||
$post = $this->post_helper->get_post( $post_id );
|
||||
|
||||
if ( $post === null ) {
|
||||
throw new Post_Not_Found_Exception();
|
||||
}
|
||||
|
||||
if ( $this->should_exclude_post( $post ) ) {
|
||||
throw Post_Not_Built_Exception::because_post_type_excluded( $post_id );
|
||||
}
|
||||
|
||||
$indexable->object_id = $post_id;
|
||||
$indexable->object_type = 'post';
|
||||
$indexable->object_sub_type = $post->post_type;
|
||||
$indexable->permalink = $this->get_permalink( $post->post_type, $post_id );
|
||||
|
||||
$indexable->primary_focus_keyword_score = $this->get_keyword_score(
|
||||
$this->meta->get_value( 'focuskw', $post_id ),
|
||||
(int) $this->meta->get_value( 'linkdex', $post_id )
|
||||
);
|
||||
|
||||
$indexable->readability_score = (int) $this->meta->get_value( 'content_score', $post_id );
|
||||
|
||||
$indexable->inclusive_language_score = (int) $this->meta->get_value( 'inclusive_language_score', $post_id );
|
||||
|
||||
$indexable->is_cornerstone = ( $this->meta->get_value( 'is_cornerstone', $post_id ) === '1' );
|
||||
$indexable->is_robots_noindex = $this->get_robots_noindex(
|
||||
(int) $this->meta->get_value( 'meta-robots-noindex', $post_id )
|
||||
);
|
||||
|
||||
// Set additional meta-robots values.
|
||||
$indexable->is_robots_nofollow = ( $this->meta->get_value( 'meta-robots-nofollow', $post_id ) === '1' );
|
||||
$noindex_advanced = $this->meta->get_value( 'meta-robots-adv', $post_id );
|
||||
$meta_robots = \explode( ',', $noindex_advanced );
|
||||
|
||||
foreach ( $this->get_robots_options() as $meta_robots_option ) {
|
||||
$indexable->{'is_robots_' . $meta_robots_option} = \in_array( $meta_robots_option, $meta_robots, true ) ? 1 : null;
|
||||
}
|
||||
|
||||
$this->reset_social_images( $indexable );
|
||||
|
||||
foreach ( $this->get_indexable_lookup() as $meta_key => $indexable_key ) {
|
||||
$indexable->{$indexable_key} = $this->empty_string_to_null( $this->meta->get_value( $meta_key, $post_id ) );
|
||||
}
|
||||
|
||||
if ( empty( $indexable->breadcrumb_title ) ) {
|
||||
$indexable->breadcrumb_title = \wp_strip_all_tags( \get_the_title( $post_id ), true );
|
||||
}
|
||||
|
||||
$this->handle_social_images( $indexable );
|
||||
|
||||
$indexable->author_id = $post->post_author;
|
||||
$indexable->post_parent = $post->post_parent;
|
||||
|
||||
$indexable->number_of_pages = $this->get_number_of_pages_for_post( $post );
|
||||
$indexable->post_status = $post->post_status;
|
||||
$indexable->is_protected = $post->post_password !== '';
|
||||
$indexable->is_public = $this->is_public( $indexable );
|
||||
$indexable->has_public_posts = $this->has_public_posts( $indexable );
|
||||
$indexable->blog_id = \get_current_blog_id();
|
||||
|
||||
$indexable->schema_page_type = $this->empty_string_to_null( $this->meta->get_value( 'schema_page_type', $post_id ) );
|
||||
$indexable->schema_article_type = $this->empty_string_to_null( $this->meta->get_value( 'schema_article_type', $post_id ) );
|
||||
|
||||
$indexable->object_last_modified = $post->post_modified_gmt;
|
||||
$indexable->object_published_at = $post->post_date_gmt;
|
||||
|
||||
$indexable->version = $this->version;
|
||||
|
||||
return $indexable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the permalink for a post with the given post type and ID.
|
||||
*
|
||||
* @param string $post_type The post type.
|
||||
* @param int $post_id The post ID.
|
||||
*
|
||||
* @return false|string|WP_Error The permalink.
|
||||
*/
|
||||
protected function get_permalink( $post_type, $post_id ) {
|
||||
if ( $post_type !== 'attachment' ) {
|
||||
return \get_permalink( $post_id );
|
||||
}
|
||||
|
||||
return \wp_get_attachment_url( $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the value of is_public.
|
||||
*
|
||||
* @param Indexable $indexable The indexable.
|
||||
*
|
||||
* @return bool|null Whether or not the post type is public. Null if no override is set.
|
||||
*/
|
||||
protected function is_public( $indexable ) {
|
||||
if ( $indexable->is_protected === true ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $indexable->is_robots_noindex === true ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Attachments behave differently than the other post types, since they inherit from their parent.
|
||||
if ( $indexable->object_sub_type === 'attachment' ) {
|
||||
return $this->is_public_attachment( $indexable );
|
||||
}
|
||||
|
||||
if ( ! \in_array( $indexable->post_status, $this->post_helper->get_public_post_statuses(), true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $indexable->is_robots_noindex === false ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the value of is_public for attachments.
|
||||
*
|
||||
* @param Indexable $indexable The indexable.
|
||||
*
|
||||
* @return bool|null False when it has no parent. Null when it has a parent.
|
||||
*/
|
||||
protected function is_public_attachment( $indexable ) {
|
||||
// If the attachment has no parent, it should not be public.
|
||||
if ( empty( $indexable->post_parent ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the attachment has a parent, the is_public should be NULL.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the value of has_public_posts.
|
||||
*
|
||||
* @param Indexable $indexable The indexable.
|
||||
*
|
||||
* @return bool|null Whether the attachment has a public parent, can be true, false and null. Null when it is not an attachment.
|
||||
*/
|
||||
protected function has_public_posts( $indexable ) {
|
||||
// Only attachments (and authors) have this value.
|
||||
if ( $indexable->object_sub_type !== 'attachment' ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The attachment should have a post parent.
|
||||
if ( empty( $indexable->post_parent ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The attachment should inherit the post status.
|
||||
if ( $indexable->post_status !== 'inherit' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The post parent should be public.
|
||||
$post_parent_indexable = $this->indexable_repository->find_by_id_and_type( $indexable->post_parent, 'post' );
|
||||
if ( $post_parent_indexable !== false ) {
|
||||
return $post_parent_indexable->is_public;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the meta robots noindex value to the indexable value.
|
||||
*
|
||||
* @param int $value Meta value to convert.
|
||||
*
|
||||
* @return bool|null True for noindex, false for index, null for default of parent/type.
|
||||
*/
|
||||
protected function get_robots_noindex( $value ) {
|
||||
$value = (int) $value;
|
||||
|
||||
switch ( $value ) {
|
||||
case 1:
|
||||
return true;
|
||||
case 2:
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the robot options to search for.
|
||||
*
|
||||
* @return array List of robots values.
|
||||
*/
|
||||
protected function get_robots_options() {
|
||||
return [ 'noimageindex', 'noarchive', 'nosnippet' ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the focus keyword score.
|
||||
*
|
||||
* @param string $keyword The focus keyword that is set.
|
||||
* @param int $score The score saved on the meta data.
|
||||
*
|
||||
* @return int|null Score to use.
|
||||
*/
|
||||
protected function get_keyword_score( $keyword, $score ) {
|
||||
if ( empty( $keyword ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the lookup table.
|
||||
*
|
||||
* @return array Lookup table for the indexable fields.
|
||||
*/
|
||||
protected function get_indexable_lookup() {
|
||||
return [
|
||||
'focuskw' => 'primary_focus_keyword',
|
||||
'canonical' => 'canonical',
|
||||
'title' => 'title',
|
||||
'metadesc' => 'description',
|
||||
'bctitle' => 'breadcrumb_title',
|
||||
'opengraph-title' => 'open_graph_title',
|
||||
'opengraph-image' => 'open_graph_image',
|
||||
'opengraph-image-id' => 'open_graph_image_id',
|
||||
'opengraph-description' => 'open_graph_description',
|
||||
'twitter-title' => 'twitter_title',
|
||||
'twitter-image' => 'twitter_image',
|
||||
'twitter-image-id' => 'twitter_image_id',
|
||||
'twitter-description' => 'twitter_description',
|
||||
'estimated-reading-time-minutes' => 'estimated_reading_time_minutes',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an alternative image for the social image.
|
||||
*
|
||||
* @param Indexable $indexable The indexable.
|
||||
*
|
||||
* @return array|bool False when not found, array with data when found.
|
||||
*/
|
||||
protected function find_alternative_image( Indexable $indexable ) {
|
||||
if (
|
||||
$indexable->object_sub_type === 'attachment'
|
||||
&& $this->image->is_valid_attachment( $indexable->object_id )
|
||||
) {
|
||||
return [
|
||||
'image_id' => $indexable->object_id,
|
||||
'source' => 'attachment-image',
|
||||
];
|
||||
}
|
||||
|
||||
$featured_image_id = $this->image->get_featured_image_id( $indexable->object_id );
|
||||
if ( $featured_image_id ) {
|
||||
return [
|
||||
'image_id' => $featured_image_id,
|
||||
'source' => 'featured-image',
|
||||
];
|
||||
}
|
||||
|
||||
$gallery_image = $this->image->get_gallery_image( $indexable->object_id );
|
||||
if ( $gallery_image ) {
|
||||
return [
|
||||
'image' => $gallery_image,
|
||||
'source' => 'gallery-image',
|
||||
];
|
||||
}
|
||||
|
||||
$content_image = $this->image->get_post_content_image( $indexable->object_id );
|
||||
if ( $content_image ) {
|
||||
return [
|
||||
'image' => $content_image,
|
||||
'source' => 'first-content-image',
|
||||
];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of pages for a post.
|
||||
*
|
||||
* @param object $post The post object.
|
||||
*
|
||||
* @return int|null The number of pages or null if the post isn't paginated.
|
||||
*/
|
||||
protected function get_number_of_pages_for_post( $post ) {
|
||||
$number_of_pages = ( \substr_count( $post->post_content, '<!--nextpage-->' ) + 1 );
|
||||
|
||||
if ( $number_of_pages <= 1 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $number_of_pages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an indexable should be built for this post.
|
||||
*
|
||||
* @param WP_Post $post The post for which an indexable should be built.
|
||||
*
|
||||
* @return bool `true` if the post should be excluded from building, `false` if not.
|
||||
*/
|
||||
protected function should_exclude_post( $post ) {
|
||||
return $this->post_type_helper->is_excluded( $post->post_type );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms an empty string into null. Leaves non-empty strings intact.
|
||||
*
|
||||
* @param string $text The string.
|
||||
*
|
||||
* @return string|null The input string or null.
|
||||
*/
|
||||
protected function empty_string_to_null( $text ) {
|
||||
if ( ! \is_string( $text ) || $text === '' ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Builders;
|
||||
|
||||
use wpdb;
|
||||
use Yoast\WP\SEO\Exceptions\Indexable\Post_Type_Not_Built_Exception;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Post_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
|
||||
|
||||
/**
|
||||
* Post type archive builder for the indexables.
|
||||
*
|
||||
* Formats the post type archive meta to indexable format.
|
||||
*/
|
||||
class Indexable_Post_Type_Archive_Builder {
|
||||
|
||||
/**
|
||||
* The options helper.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* The latest version of the Indexable_Post_Type_Archive_Builder.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $version;
|
||||
|
||||
/**
|
||||
* Holds the post helper instance.
|
||||
*
|
||||
* @var Post_Helper
|
||||
*/
|
||||
protected $post_helper;
|
||||
|
||||
/**
|
||||
* Holds the post type helper instance.
|
||||
*
|
||||
* @var Post_Type_Helper
|
||||
*/
|
||||
protected $post_type_helper;
|
||||
|
||||
/**
|
||||
* The WPDB instance.
|
||||
*
|
||||
* @var wpdb
|
||||
*/
|
||||
protected $wpdb;
|
||||
|
||||
/**
|
||||
* Indexable_Post_Type_Archive_Builder constructor.
|
||||
*
|
||||
* @param Options_Helper $options The options helper.
|
||||
* @param Indexable_Builder_Versions $versions The latest version of each Indexable builder.
|
||||
* @param Post_Helper $post_helper The post helper.
|
||||
* @param Post_Type_Helper $post_type_helper The post type helper.
|
||||
* @param wpdb $wpdb The WPDB instance.
|
||||
*/
|
||||
public function __construct(
|
||||
Options_Helper $options,
|
||||
Indexable_Builder_Versions $versions,
|
||||
Post_Helper $post_helper,
|
||||
Post_Type_Helper $post_type_helper,
|
||||
wpdb $wpdb
|
||||
) {
|
||||
$this->options = $options;
|
||||
$this->version = $versions->get_latest_version_for_type( 'post-type-archive' );
|
||||
$this->post_helper = $post_helper;
|
||||
$this->post_type_helper = $post_type_helper;
|
||||
$this->wpdb = $wpdb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data.
|
||||
*
|
||||
* @param string $post_type The post type to build the indexable for.
|
||||
* @param Indexable $indexable The indexable to format.
|
||||
*
|
||||
* @return Indexable The extended indexable.
|
||||
* @throws Post_Type_Not_Built_Exception Throws exception if the post type is excluded.
|
||||
*/
|
||||
public function build( $post_type, Indexable $indexable ) {
|
||||
if ( ! $this->post_type_helper->is_post_type_archive_indexable( $post_type ) ) {
|
||||
throw Post_Type_Not_Built_Exception::because_not_indexable( $post_type );
|
||||
}
|
||||
|
||||
$indexable->object_type = 'post-type-archive';
|
||||
$indexable->object_sub_type = $post_type;
|
||||
$indexable->title = $this->options->get( 'title-ptarchive-' . $post_type );
|
||||
$indexable->description = $this->options->get( 'metadesc-ptarchive-' . $post_type );
|
||||
$indexable->breadcrumb_title = $this->get_breadcrumb_title( $post_type );
|
||||
$indexable->permalink = \get_post_type_archive_link( $post_type );
|
||||
$indexable->is_robots_noindex = $this->options->get( 'noindex-ptarchive-' . $post_type );
|
||||
$indexable->is_public = ( (int) $indexable->is_robots_noindex !== 1 );
|
||||
$indexable->blog_id = \get_current_blog_id();
|
||||
$indexable->version = $this->version;
|
||||
|
||||
$timestamps = $this->get_object_timestamps( $post_type );
|
||||
$indexable->object_published_at = $timestamps->published_at;
|
||||
$indexable->object_last_modified = $timestamps->last_modified;
|
||||
|
||||
return $indexable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fallback breadcrumb title for a given post.
|
||||
*
|
||||
* @param string $post_type The post type to get the fallback breadcrumb title for.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_breadcrumb_title( $post_type ) {
|
||||
$options_breadcrumb_title = $this->options->get( 'bctitle-ptarchive-' . $post_type );
|
||||
|
||||
if ( $options_breadcrumb_title !== '' ) {
|
||||
return $options_breadcrumb_title;
|
||||
}
|
||||
|
||||
$post_type_obj = \get_post_type_object( $post_type );
|
||||
|
||||
if ( ! \is_object( $post_type_obj ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( isset( $post_type_obj->label ) && $post_type_obj->label !== '' ) {
|
||||
return $post_type_obj->label;
|
||||
}
|
||||
|
||||
if ( isset( $post_type_obj->labels->menu_name ) && $post_type_obj->labels->menu_name !== '' ) {
|
||||
return $post_type_obj->labels->menu_name;
|
||||
}
|
||||
|
||||
return $post_type_obj->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamps for a given post type.
|
||||
*
|
||||
* @param string $post_type The post type.
|
||||
*
|
||||
* @return object An object with last_modified and published_at timestamps.
|
||||
*/
|
||||
protected function get_object_timestamps( $post_type ) {
|
||||
$post_statuses = $this->post_helper->get_public_post_statuses();
|
||||
|
||||
$sql = "
|
||||
SELECT MAX(p.post_modified_gmt) AS last_modified, MIN(p.post_date_gmt) AS published_at
|
||||
FROM {$this->wpdb->posts} AS p
|
||||
WHERE p.post_status IN (" . \implode( ', ', \array_fill( 0, \count( $post_statuses ), '%s' ) ) . ")
|
||||
AND p.post_password = ''
|
||||
AND p.post_type = %s
|
||||
";
|
||||
|
||||
$replacements = \array_merge( $post_statuses, [ $post_type ] );
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- We are using wpdb prepare.
|
||||
return $this->wpdb->get_row( $this->wpdb->prepare( $sql, $replacements ) );
|
||||
}
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Builders;
|
||||
|
||||
use WPSEO_Utils;
|
||||
use Yoast\WP\SEO\Helpers\Image_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Open_Graph\Image_Helper as Open_Graph_Image_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Twitter\Image_Helper as Twitter_Image_Helper;
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
|
||||
/**
|
||||
* Trait for determine the social image to use in the indexable.
|
||||
*
|
||||
* Represents the trait used in builders for handling social images.
|
||||
*/
|
||||
trait Indexable_Social_Image_Trait {
|
||||
|
||||
/**
|
||||
* The image helper.
|
||||
*
|
||||
* @var Image_Helper
|
||||
*/
|
||||
protected $image;
|
||||
|
||||
/**
|
||||
* The Open Graph image helper.
|
||||
*
|
||||
* @var Open_Graph_Image_Helper
|
||||
*/
|
||||
protected $open_graph_image;
|
||||
|
||||
/**
|
||||
* The Twitter image helper.
|
||||
*
|
||||
* @var Twitter_Image_Helper
|
||||
*/
|
||||
protected $twitter_image;
|
||||
|
||||
/**
|
||||
* Sets the helpers for the trait.
|
||||
*
|
||||
* @required
|
||||
*
|
||||
* @param Image_Helper $image The image helper.
|
||||
* @param Open_Graph_Image_Helper $open_graph_image The Open Graph image helper.
|
||||
* @param Twitter_Image_Helper $twitter_image The Twitter image helper.
|
||||
*/
|
||||
public function set_social_image_helpers(
|
||||
Image_Helper $image,
|
||||
Open_Graph_Image_Helper $open_graph_image,
|
||||
Twitter_Image_Helper $twitter_image
|
||||
) {
|
||||
$this->image = $image;
|
||||
$this->open_graph_image = $open_graph_image;
|
||||
$this->twitter_image = $twitter_image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the alternative on an indexable.
|
||||
*
|
||||
* @param array $alternative_image The alternative image to set.
|
||||
* @param Indexable $indexable The indexable to set image for.
|
||||
*/
|
||||
protected function set_alternative_image( array $alternative_image, Indexable $indexable ) {
|
||||
if ( ! empty( $alternative_image['image_id'] ) ) {
|
||||
if ( ! $indexable->open_graph_image_source && ! $indexable->open_graph_image_id ) {
|
||||
$indexable->open_graph_image_id = $alternative_image['image_id'];
|
||||
$indexable->open_graph_image_source = $alternative_image['source'];
|
||||
|
||||
$this->set_open_graph_image_meta_data( $indexable );
|
||||
}
|
||||
|
||||
if ( ! $indexable->twitter_image && ! $indexable->twitter_image_id ) {
|
||||
$indexable->twitter_image = $this->twitter_image->get_by_id( $alternative_image['image_id'] );
|
||||
$indexable->twitter_image_id = $alternative_image['image_id'];
|
||||
$indexable->twitter_image_source = $alternative_image['source'];
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $alternative_image['image'] ) ) {
|
||||
if ( ! $indexable->open_graph_image_source && ! $indexable->open_graph_image_id ) {
|
||||
$indexable->open_graph_image = $alternative_image['image'];
|
||||
$indexable->open_graph_image_source = $alternative_image['source'];
|
||||
}
|
||||
|
||||
if ( ! $indexable->twitter_image && ! $indexable->twitter_image_id ) {
|
||||
$indexable->twitter_image = $alternative_image['image'];
|
||||
$indexable->twitter_image_source = $alternative_image['source'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Open Graph image meta data for an og image
|
||||
*
|
||||
* @param Indexable $indexable The indexable.
|
||||
*/
|
||||
protected function set_open_graph_image_meta_data( Indexable $indexable ) {
|
||||
if ( ! $indexable->open_graph_image_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$image = $this->open_graph_image->get_image_by_id( $indexable->open_graph_image_id );
|
||||
|
||||
if ( ! empty( $image ) ) {
|
||||
$indexable->open_graph_image = $image['url'];
|
||||
$indexable->open_graph_image_meta = WPSEO_Utils::format_json_encode( $image );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the social images.
|
||||
*
|
||||
* @param Indexable $indexable The indexable to handle.
|
||||
*/
|
||||
protected function handle_social_images( Indexable $indexable ) {
|
||||
// When the image or image id is set.
|
||||
if ( $indexable->open_graph_image || $indexable->open_graph_image_id ) {
|
||||
$indexable->open_graph_image_source = 'set-by-user';
|
||||
|
||||
$this->set_open_graph_image_meta_data( $indexable );
|
||||
}
|
||||
|
||||
if ( $indexable->twitter_image || $indexable->twitter_image_id ) {
|
||||
$indexable->twitter_image_source = 'set-by-user';
|
||||
}
|
||||
|
||||
if ( $indexable->twitter_image_id ) {
|
||||
$indexable->twitter_image = $this->twitter_image->get_by_id( $indexable->twitter_image_id );
|
||||
}
|
||||
|
||||
// When image sources are set already.
|
||||
if ( $indexable->open_graph_image_source && $indexable->twitter_image_source ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$alternative_image = $this->find_alternative_image( $indexable );
|
||||
if ( ! empty( $alternative_image ) ) {
|
||||
$this->set_alternative_image( $alternative_image, $indexable );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the social images.
|
||||
*
|
||||
* @param Indexable $indexable The indexable to set images for.
|
||||
*/
|
||||
protected function reset_social_images( Indexable $indexable ) {
|
||||
$indexable->open_graph_image = null;
|
||||
$indexable->open_graph_image_id = null;
|
||||
$indexable->open_graph_image_source = null;
|
||||
$indexable->open_graph_image_meta = null;
|
||||
|
||||
$indexable->twitter_image = null;
|
||||
$indexable->twitter_image_id = null;
|
||||
$indexable->twitter_image_source = null;
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Builders;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
|
||||
|
||||
/**
|
||||
* System page builder for the indexables.
|
||||
*
|
||||
* Formats system pages ( search and error ) meta to indexable format.
|
||||
*/
|
||||
class Indexable_System_Page_Builder {
|
||||
|
||||
/**
|
||||
* Mapping of object type to title option keys.
|
||||
*/
|
||||
const OPTION_MAPPING = [
|
||||
'search-result' => [
|
||||
'title' => 'title-search-wpseo',
|
||||
],
|
||||
'404' => [
|
||||
'title' => 'title-404-wpseo',
|
||||
'breadcrumb_title' => 'breadcrumbs-404crumb',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* The options helper.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* The latest version of the Indexable_System_Page_Builder.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $version;
|
||||
|
||||
/**
|
||||
* Indexable_System_Page_Builder constructor.
|
||||
*
|
||||
* @param Options_Helper $options The options helper.
|
||||
* @param Indexable_Builder_Versions $versions The latest version of each Indexable Builder.
|
||||
*/
|
||||
public function __construct(
|
||||
Options_Helper $options,
|
||||
Indexable_Builder_Versions $versions
|
||||
) {
|
||||
$this->options = $options;
|
||||
$this->version = $versions->get_latest_version_for_type( 'system-page' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data.
|
||||
*
|
||||
* @param string $object_sub_type The object sub type of the system page.
|
||||
* @param Indexable $indexable The indexable to format.
|
||||
*
|
||||
* @return Indexable The extended indexable.
|
||||
*/
|
||||
public function build( $object_sub_type, Indexable $indexable ) {
|
||||
$indexable->object_type = 'system-page';
|
||||
$indexable->object_sub_type = $object_sub_type;
|
||||
$indexable->title = $this->options->get( static::OPTION_MAPPING[ $object_sub_type ]['title'] );
|
||||
$indexable->is_robots_noindex = true;
|
||||
$indexable->blog_id = \get_current_blog_id();
|
||||
|
||||
if ( \array_key_exists( 'breadcrumb_title', static::OPTION_MAPPING[ $object_sub_type ] ) ) {
|
||||
$indexable->breadcrumb_title = $this->options->get( static::OPTION_MAPPING[ $object_sub_type ]['breadcrumb_title'] );
|
||||
}
|
||||
|
||||
$indexable->version = $this->version;
|
||||
|
||||
return $indexable;
|
||||
}
|
||||
}
|
@ -0,0 +1,279 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Builders;
|
||||
|
||||
use wpdb;
|
||||
use Yoast\WP\SEO\Exceptions\Indexable\Invalid_Term_Exception;
|
||||
use Yoast\WP\SEO\Exceptions\Indexable\Term_Not_Built_Exception;
|
||||
use Yoast\WP\SEO\Exceptions\Indexable\Term_Not_Found_Exception;
|
||||
use Yoast\WP\SEO\Helpers\Post_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
|
||||
|
||||
/**
|
||||
* Term Builder for the indexables.
|
||||
*
|
||||
* Formats the term meta to indexable format.
|
||||
*/
|
||||
class Indexable_Term_Builder {
|
||||
|
||||
use Indexable_Social_Image_Trait;
|
||||
|
||||
/**
|
||||
* Holds the taxonomy helper instance.
|
||||
*
|
||||
* @var Taxonomy_Helper
|
||||
*/
|
||||
protected $taxonomy_helper;
|
||||
|
||||
/**
|
||||
* The latest version of the Indexable_Term_Builder.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $version;
|
||||
|
||||
/**
|
||||
* Holds the taxonomy helper instance.
|
||||
*
|
||||
* @var Post_Helper
|
||||
*/
|
||||
protected $post_helper;
|
||||
|
||||
/**
|
||||
* The WPDB instance.
|
||||
*
|
||||
* @var wpdb
|
||||
*/
|
||||
protected $wpdb;
|
||||
|
||||
/**
|
||||
* Indexable_Term_Builder constructor.
|
||||
*
|
||||
* @param Taxonomy_Helper $taxonomy_helper The taxonomy helper.
|
||||
* @param Indexable_Builder_Versions $versions The latest version of each Indexable Builder.
|
||||
* @param Post_Helper $post_helper The post helper.
|
||||
* @param wpdb $wpdb The WPDB instance.
|
||||
*/
|
||||
public function __construct(
|
||||
Taxonomy_Helper $taxonomy_helper,
|
||||
Indexable_Builder_Versions $versions,
|
||||
Post_Helper $post_helper,
|
||||
wpdb $wpdb
|
||||
) {
|
||||
$this->taxonomy_helper = $taxonomy_helper;
|
||||
$this->version = $versions->get_latest_version_for_type( 'term' );
|
||||
$this->post_helper = $post_helper;
|
||||
$this->wpdb = $wpdb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data.
|
||||
*
|
||||
* @param int $term_id ID of the term to save data for.
|
||||
* @param Indexable $indexable The indexable to format.
|
||||
*
|
||||
* @return bool|Indexable The extended indexable. False when unable to build.
|
||||
*
|
||||
* @throws Invalid_Term_Exception When the term is invalid.
|
||||
* @throws Term_Not_Built_Exception When the term is not viewable.
|
||||
* @throws Term_Not_Found_Exception When the term is not found.
|
||||
*/
|
||||
public function build( $term_id, $indexable ) {
|
||||
$term = \get_term( $term_id );
|
||||
|
||||
if ( $term === null ) {
|
||||
throw new Term_Not_Found_Exception();
|
||||
}
|
||||
|
||||
if ( \is_wp_error( $term ) ) {
|
||||
throw new Invalid_Term_Exception( $term->get_error_message() );
|
||||
}
|
||||
|
||||
$indexable_taxonomies = $this->taxonomy_helper->get_indexable_taxonomies();
|
||||
if ( ! \in_array( $term->taxonomy, $indexable_taxonomies, true ) ) {
|
||||
throw Term_Not_Built_Exception::because_not_indexable( $term_id );
|
||||
}
|
||||
|
||||
$term_link = \get_term_link( $term, $term->taxonomy );
|
||||
|
||||
if ( \is_wp_error( $term_link ) ) {
|
||||
throw new Invalid_Term_Exception( $term_link->get_error_message() );
|
||||
}
|
||||
|
||||
$term_meta = $this->taxonomy_helper->get_term_meta( $term );
|
||||
|
||||
$indexable->object_id = $term_id;
|
||||
$indexable->object_type = 'term';
|
||||
$indexable->object_sub_type = $term->taxonomy;
|
||||
$indexable->permalink = $term_link;
|
||||
$indexable->blog_id = \get_current_blog_id();
|
||||
|
||||
$indexable->primary_focus_keyword_score = $this->get_keyword_score(
|
||||
$this->get_meta_value( 'wpseo_focuskw', $term_meta ),
|
||||
$this->get_meta_value( 'wpseo_linkdex', $term_meta )
|
||||
);
|
||||
|
||||
$indexable->is_robots_noindex = $this->get_noindex_value( $this->get_meta_value( 'wpseo_noindex', $term_meta ) );
|
||||
$indexable->is_public = ( $indexable->is_robots_noindex === null ) ? null : ! $indexable->is_robots_noindex;
|
||||
|
||||
$this->reset_social_images( $indexable );
|
||||
|
||||
foreach ( $this->get_indexable_lookup() as $meta_key => $indexable_key ) {
|
||||
$indexable->{$indexable_key} = $this->get_meta_value( $meta_key, $term_meta );
|
||||
}
|
||||
|
||||
if ( empty( $indexable->breadcrumb_title ) ) {
|
||||
$indexable->breadcrumb_title = $term->name;
|
||||
}
|
||||
|
||||
$this->handle_social_images( $indexable );
|
||||
|
||||
$indexable->is_cornerstone = $this->get_meta_value( 'wpseo_is_cornerstone', $term_meta );
|
||||
|
||||
// Not implemented yet.
|
||||
$indexable->is_robots_nofollow = null;
|
||||
$indexable->is_robots_noarchive = null;
|
||||
$indexable->is_robots_noimageindex = null;
|
||||
$indexable->is_robots_nosnippet = null;
|
||||
|
||||
$timestamps = $this->get_object_timestamps( $term_id, $term->taxonomy );
|
||||
$indexable->object_published_at = $timestamps->published_at;
|
||||
$indexable->object_last_modified = $timestamps->last_modified;
|
||||
|
||||
$indexable->version = $this->version;
|
||||
|
||||
return $indexable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the meta noindex value to the indexable value.
|
||||
*
|
||||
* @param string $meta_value Term meta to base the value on.
|
||||
*
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function get_noindex_value( $meta_value ) {
|
||||
if ( $meta_value === 'noindex' ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( $meta_value === 'index' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the focus keyword score.
|
||||
*
|
||||
* @param string $keyword The focus keyword that is set.
|
||||
* @param int $score The score saved on the meta data.
|
||||
*
|
||||
* @return int|null Score to use.
|
||||
*/
|
||||
protected function get_keyword_score( $keyword, $score ) {
|
||||
if ( empty( $keyword ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the lookup table.
|
||||
*
|
||||
* @return array Lookup table for the indexable fields.
|
||||
*/
|
||||
protected function get_indexable_lookup() {
|
||||
return [
|
||||
'wpseo_canonical' => 'canonical',
|
||||
'wpseo_focuskw' => 'primary_focus_keyword',
|
||||
'wpseo_title' => 'title',
|
||||
'wpseo_desc' => 'description',
|
||||
'wpseo_content_score' => 'readability_score',
|
||||
'wpseo_inclusive_language_score' => 'inclusive_language_score',
|
||||
'wpseo_bctitle' => 'breadcrumb_title',
|
||||
'wpseo_opengraph-title' => 'open_graph_title',
|
||||
'wpseo_opengraph-description' => 'open_graph_description',
|
||||
'wpseo_opengraph-image' => 'open_graph_image',
|
||||
'wpseo_opengraph-image-id' => 'open_graph_image_id',
|
||||
'wpseo_twitter-title' => 'twitter_title',
|
||||
'wpseo_twitter-description' => 'twitter_description',
|
||||
'wpseo_twitter-image' => 'twitter_image',
|
||||
'wpseo_twitter-image-id' => 'twitter_image_id',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a meta value from the given meta data.
|
||||
*
|
||||
* @param string $meta_key The key to extract.
|
||||
* @param array $term_meta The meta data.
|
||||
*
|
||||
* @return string|null The meta value.
|
||||
*/
|
||||
protected function get_meta_value( $meta_key, $term_meta ) {
|
||||
if ( ! $term_meta || ! \array_key_exists( $meta_key, $term_meta ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = $term_meta[ $meta_key ];
|
||||
if ( \is_string( $value ) && $value === '' ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an alternative image for the social image.
|
||||
*
|
||||
* @param Indexable $indexable The indexable.
|
||||
*
|
||||
* @return array|bool False when not found, array with data when found.
|
||||
*/
|
||||
protected function find_alternative_image( Indexable $indexable ) {
|
||||
$content_image = $this->image->get_term_content_image( $indexable->object_id );
|
||||
if ( $content_image ) {
|
||||
return [
|
||||
'image' => $content_image,
|
||||
'source' => 'first-content-image',
|
||||
];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamps for a given term.
|
||||
*
|
||||
* @param int $term_id The term ID.
|
||||
* @param string $taxonomy The taxonomy.
|
||||
*
|
||||
* @return object An object with last_modified and published_at timestamps.
|
||||
*/
|
||||
protected function get_object_timestamps( $term_id, $taxonomy ) {
|
||||
$post_statuses = $this->post_helper->get_public_post_statuses();
|
||||
|
||||
$sql = "
|
||||
SELECT MAX(p.post_modified_gmt) AS last_modified, MIN(p.post_date_gmt) AS published_at
|
||||
FROM {$this->wpdb->posts} AS p
|
||||
INNER JOIN {$this->wpdb->term_relationships} AS term_rel
|
||||
ON term_rel.object_id = p.ID
|
||||
INNER JOIN {$this->wpdb->term_taxonomy} AS term_tax
|
||||
ON term_tax.term_taxonomy_id = term_rel.term_taxonomy_id
|
||||
AND term_tax.taxonomy = %s
|
||||
AND term_tax.term_id = %d
|
||||
WHERE p.post_status IN (" . \implode( ', ', \array_fill( 0, \count( $post_statuses ), '%s' ) ) . ")
|
||||
AND p.post_password = ''
|
||||
";
|
||||
|
||||
$replacements = \array_merge( [ $taxonomy, $term_id ], $post_statuses );
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- We are using wpdb prepare.
|
||||
return $this->wpdb->get_row( $this->wpdb->prepare( $sql, $replacements ) );
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Builders;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Meta_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Primary_Term_Helper;
|
||||
use Yoast\WP\SEO\Repositories\Primary_Term_Repository;
|
||||
|
||||
/**
|
||||
* Primary term builder.
|
||||
*
|
||||
* Creates the primary term for a post.
|
||||
*/
|
||||
class Primary_Term_Builder {
|
||||
|
||||
/**
|
||||
* The primary term repository.
|
||||
*
|
||||
* @var Primary_Term_Repository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* The primary term helper.
|
||||
*
|
||||
* @var Primary_Term_Helper
|
||||
*/
|
||||
private $primary_term;
|
||||
|
||||
/**
|
||||
* The meta helper.
|
||||
*
|
||||
* @var Meta_Helper
|
||||
*/
|
||||
private $meta;
|
||||
|
||||
/**
|
||||
* Primary_Term_Builder constructor.
|
||||
*
|
||||
* @param Primary_Term_Repository $repository The primary term repository.
|
||||
* @param Primary_Term_Helper $primary_term The primary term helper.
|
||||
* @param Meta_Helper $meta The meta helper.
|
||||
*/
|
||||
public function __construct(
|
||||
Primary_Term_Repository $repository,
|
||||
Primary_Term_Helper $primary_term,
|
||||
Meta_Helper $meta
|
||||
) {
|
||||
$this->repository = $repository;
|
||||
$this->primary_term = $primary_term;
|
||||
$this->meta = $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats and saves the primary terms for the post with the given post id.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function build( $post_id ) {
|
||||
foreach ( $this->primary_term->get_primary_term_taxonomies( $post_id ) as $taxonomy ) {
|
||||
$this->save_primary_term( $post_id, $taxonomy->name );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the primary term for a specific taxonomy.
|
||||
*
|
||||
* @param int $post_id Post ID to save primary term for.
|
||||
* @param string $taxonomy Taxonomy to save primary term for.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function save_primary_term( $post_id, $taxonomy ) {
|
||||
$term_id = $this->meta->get_value( 'primary_' . $taxonomy, $post_id );
|
||||
|
||||
$term_selected = ! empty( $term_id );
|
||||
$primary_term = $this->repository->find_by_post_id_and_taxonomy( $post_id, $taxonomy, $term_selected );
|
||||
|
||||
// Removes the indexable when no term found.
|
||||
if ( ! $term_selected ) {
|
||||
if ( $primary_term ) {
|
||||
$primary_term->delete();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$primary_term->term_id = $term_id;
|
||||
$primary_term->post_id = $post_id;
|
||||
$primary_term->taxonomy = $taxonomy;
|
||||
$primary_term->blog_id = \get_current_blog_id();
|
||||
$primary_term->save();
|
||||
}
|
||||
}
|
@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Commands;
|
||||
|
||||
use WP_CLI;
|
||||
use WP_CLI\ExitException;
|
||||
use WP_CLI\Utils;
|
||||
use Yoast\WP\SEO\Integrations\Cleanup_Integration;
|
||||
use Yoast\WP\SEO\Main;
|
||||
|
||||
/**
|
||||
* A WP CLI command that helps with cleaning up unwanted records from our custom tables.
|
||||
*/
|
||||
final class Cleanup_Command implements Command_Interface {
|
||||
|
||||
/**
|
||||
* The integration that cleans up on cron.
|
||||
*
|
||||
* @var Cleanup_Integration
|
||||
*/
|
||||
private $cleanup_integration;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param Cleanup_Integration $cleanup_integration The integration that cleans up on cron.
|
||||
*/
|
||||
public function __construct( Cleanup_Integration $cleanup_integration ) {
|
||||
$this->cleanup_integration = $cleanup_integration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the namespace of this command.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_namespace() {
|
||||
return Main::WP_CLI_NAMESPACE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a cleanup of custom Yoast tables.
|
||||
*
|
||||
* This removes unused, unwanted or orphaned database records, which ensures the best performance. Including:
|
||||
* - Indexables
|
||||
* - Indexable hierarchy
|
||||
* - SEO links
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--batch-size=<batch-size>]
|
||||
* : The number of database records to clean up in a single sql query.
|
||||
* ---
|
||||
* default: 1000
|
||||
* ---
|
||||
*
|
||||
* [--interval=<interval>]
|
||||
* : The number of microseconds (millionths of a second) to wait between cleanup batches.
|
||||
* ---
|
||||
* default: 500000
|
||||
* ---
|
||||
*
|
||||
* [--network]
|
||||
* : Performs the cleanup on all sites within the network.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* wp yoast cleanup
|
||||
*
|
||||
* @when after_wp_load
|
||||
*
|
||||
* @param array|null $args The arguments.
|
||||
* @param array|null $assoc_args The associative arguments.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws ExitException When the input args are invalid.
|
||||
*/
|
||||
public function cleanup( $args = null, $assoc_args = null ) {
|
||||
if ( isset( $assoc_args['interval'] ) && (int) $assoc_args['interval'] < 0 ) {
|
||||
WP_CLI::error( \__( 'The value for \'interval\' must be a positive integer.', 'wordpress-seo' ) );
|
||||
}
|
||||
if ( isset( $assoc_args['batch-size'] ) && (int) $assoc_args['batch-size'] < 1 ) {
|
||||
WP_CLI::error( \__( 'The value for \'batch-size\' must be a positive integer higher than equal to 1.', 'wordpress-seo' ) );
|
||||
}
|
||||
|
||||
if ( isset( $assoc_args['network'] ) && \is_multisite() ) {
|
||||
$total_removed = $this->cleanup_network( $assoc_args );
|
||||
}
|
||||
else {
|
||||
$total_removed = $this->cleanup_current_site( $assoc_args );
|
||||
}
|
||||
|
||||
WP_CLI::success(
|
||||
\sprintf(
|
||||
/* translators: %1$d is the number of records that are removed. */
|
||||
\_n(
|
||||
'Cleaned up %1$d record.',
|
||||
'Cleaned up %1$d records.',
|
||||
$total_removed,
|
||||
'wordpress-seo'
|
||||
),
|
||||
$total_removed
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the cleanup for the entire network.
|
||||
*
|
||||
* @param array|null $assoc_args The associative arguments.
|
||||
*
|
||||
* @return int The number of cleaned up records.
|
||||
*/
|
||||
private function cleanup_network( $assoc_args ) {
|
||||
$criteria = [
|
||||
'fields' => 'ids',
|
||||
'spam' => 0,
|
||||
'deleted' => 0,
|
||||
'archived' => 0,
|
||||
];
|
||||
$blog_ids = \get_sites( $criteria );
|
||||
$total_removed = 0;
|
||||
foreach ( $blog_ids as $blog_id ) {
|
||||
\switch_to_blog( $blog_id );
|
||||
$total_removed += $this->cleanup_current_site( $assoc_args );
|
||||
\restore_current_blog();
|
||||
}
|
||||
|
||||
return $total_removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the cleanup for a single site.
|
||||
*
|
||||
* @param array|null $assoc_args The associative arguments.
|
||||
*
|
||||
* @return int The number of cleaned up records.
|
||||
*/
|
||||
private function cleanup_current_site( $assoc_args ) {
|
||||
$site_url = \site_url();
|
||||
$total_removed = 0;
|
||||
|
||||
if ( ! \is_plugin_active( \WPSEO_BASENAME ) ) {
|
||||
/* translators: %1$s is the site url of the site that is skipped. %2$s is Yoast SEO. */
|
||||
WP_CLI::warning( \sprintf( \__( 'Skipping %1$s. %2$s is not active on this site.', 'wordpress-seo' ), $site_url, 'Yoast SEO' ) );
|
||||
|
||||
return $total_removed;
|
||||
}
|
||||
|
||||
// Make sure the DB is up to date first.
|
||||
\do_action( '_yoast_run_migrations' );
|
||||
|
||||
$tasks = $this->cleanup_integration->get_cleanup_tasks();
|
||||
$limit = (int) $assoc_args['batch-size'];
|
||||
$interval = (int) $assoc_args['interval'];
|
||||
|
||||
/* translators: %1$s is the site url of the site that is cleaned up. %2$s is the name of the cleanup task that is currently running. */
|
||||
$progress_bar_title_format = \__( 'Cleaning up %1$s [%2$s]', 'wordpress-seo' );
|
||||
$progress = Utils\make_progress_bar( \sprintf( $progress_bar_title_format, $site_url, \key( $tasks ) ), \count( $tasks ) );
|
||||
|
||||
foreach ( $tasks as $task_name => $task ) {
|
||||
// Update the progressbar title with the current task name.
|
||||
$progress->tick( 0, \sprintf( $progress_bar_title_format, $site_url, $task_name ) );
|
||||
do {
|
||||
$items_cleaned = $task( $limit );
|
||||
if ( \is_int( $items_cleaned ) ) {
|
||||
$total_removed += $items_cleaned;
|
||||
}
|
||||
\usleep( $interval );
|
||||
|
||||
// Update the timer.
|
||||
$progress->tick( 0 );
|
||||
} while ( $items_cleaned !== false && $items_cleaned > 0 );
|
||||
$progress->tick();
|
||||
}
|
||||
$progress->finish();
|
||||
|
||||
$this->cleanup_integration->reset_cleanup();
|
||||
WP_CLI::log(
|
||||
\sprintf(
|
||||
/* translators: %1$d is the number of records that were removed. %2$s is the site url. */
|
||||
\_n(
|
||||
'Cleaned up %1$d record from %2$s.',
|
||||
'Cleaned up %1$d records from %2$s.',
|
||||
$total_removed,
|
||||
'wordpress-seo'
|
||||
),
|
||||
$total_removed,
|
||||
$site_url
|
||||
)
|
||||
);
|
||||
|
||||
return $total_removed;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Commands;
|
||||
|
||||
/**
|
||||
* Interface definition for WP CLI commands.
|
||||
*
|
||||
* An interface for registering integrations with WordPress.
|
||||
*/
|
||||
interface Command_Interface {
|
||||
|
||||
/**
|
||||
* Returns the namespace of this command.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_namespace();
|
||||
}
|
294
wp-content/plugins/wordpress-seo/src/commands/index-command.php
Normal file
294
wp-content/plugins/wordpress-seo/src/commands/index-command.php
Normal file
@ -0,0 +1,294 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Commands;
|
||||
|
||||
use WP_CLI;
|
||||
use WP_CLI\Utils;
|
||||
use Yoast\WP\Lib\Model;
|
||||
use Yoast\WP\SEO\Actions\Indexing\Indexable_General_Indexation_Action;
|
||||
use Yoast\WP\SEO\Actions\Indexing\Indexable_Indexing_Complete_Action;
|
||||
use Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Indexation_Action;
|
||||
use Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Type_Archive_Indexation_Action;
|
||||
use Yoast\WP\SEO\Actions\Indexing\Indexable_Term_Indexation_Action;
|
||||
use Yoast\WP\SEO\Actions\Indexing\Indexation_Action_Interface;
|
||||
use Yoast\WP\SEO\Actions\Indexing\Indexing_Prepare_Action;
|
||||
use Yoast\WP\SEO\Actions\Indexing\Post_Link_Indexing_Action;
|
||||
use Yoast\WP\SEO\Actions\Indexing\Term_Link_Indexing_Action;
|
||||
use Yoast\WP\SEO\Helpers\Indexable_Helper;
|
||||
use Yoast\WP\SEO\Main;
|
||||
|
||||
/**
|
||||
* Command to generate indexables for all posts and terms.
|
||||
*/
|
||||
class Index_Command implements Command_Interface {
|
||||
|
||||
/**
|
||||
* The post indexation action.
|
||||
*
|
||||
* @var Indexable_Post_Indexation_Action
|
||||
*/
|
||||
private $post_indexation_action;
|
||||
|
||||
/**
|
||||
* The term indexation action.
|
||||
*
|
||||
* @var Indexable_Term_Indexation_Action
|
||||
*/
|
||||
private $term_indexation_action;
|
||||
|
||||
/**
|
||||
* The post type archive indexation action.
|
||||
*
|
||||
* @var Indexable_Post_Type_Archive_Indexation_Action
|
||||
*/
|
||||
private $post_type_archive_indexation_action;
|
||||
|
||||
/**
|
||||
* The general indexation action.
|
||||
*
|
||||
* @var Indexable_General_Indexation_Action
|
||||
*/
|
||||
private $general_indexation_action;
|
||||
|
||||
/**
|
||||
* The term link indexing action.
|
||||
*
|
||||
* @var Term_Link_Indexing_Action
|
||||
*/
|
||||
private $term_link_indexing_action;
|
||||
|
||||
/**
|
||||
* The post link indexing action.
|
||||
*
|
||||
* @var Post_Link_Indexing_Action
|
||||
*/
|
||||
private $post_link_indexing_action;
|
||||
|
||||
/**
|
||||
* The complete indexation action.
|
||||
*
|
||||
* @var Indexable_Indexing_Complete_Action
|
||||
*/
|
||||
private $complete_indexation_action;
|
||||
|
||||
/**
|
||||
* The indexing prepare action.
|
||||
*
|
||||
* @var Indexing_Prepare_Action
|
||||
*/
|
||||
private $prepare_indexing_action;
|
||||
|
||||
/**
|
||||
* Represents the indexable helper.
|
||||
*
|
||||
* @var Indexable_Helper
|
||||
*/
|
||||
protected $indexable_helper;
|
||||
|
||||
/**
|
||||
* Generate_Indexables_Command constructor.
|
||||
*
|
||||
* @param Indexable_Post_Indexation_Action $post_indexation_action The post indexation
|
||||
* action.
|
||||
* @param Indexable_Term_Indexation_Action $term_indexation_action The term indexation
|
||||
* action.
|
||||
* @param Indexable_Post_Type_Archive_Indexation_Action $post_type_archive_indexation_action The post type archive
|
||||
* indexation action.
|
||||
* @param Indexable_General_Indexation_Action $general_indexation_action The general indexation
|
||||
* action.
|
||||
* @param Indexable_Indexing_Complete_Action $complete_indexation_action The complete indexation
|
||||
* action.
|
||||
* @param Indexing_Prepare_Action $prepare_indexing_action The prepare indexing
|
||||
* action.
|
||||
* @param Post_Link_Indexing_Action $post_link_indexing_action The post link indexation
|
||||
* action.
|
||||
* @param Term_Link_Indexing_Action $term_link_indexing_action The term link indexation
|
||||
* action.
|
||||
* @param Indexable_Helper $indexable_helper The indexable helper.
|
||||
*/
|
||||
public function __construct(
|
||||
Indexable_Post_Indexation_Action $post_indexation_action,
|
||||
Indexable_Term_Indexation_Action $term_indexation_action,
|
||||
Indexable_Post_Type_Archive_Indexation_Action $post_type_archive_indexation_action,
|
||||
Indexable_General_Indexation_Action $general_indexation_action,
|
||||
Indexable_Indexing_Complete_Action $complete_indexation_action,
|
||||
Indexing_Prepare_Action $prepare_indexing_action,
|
||||
Post_Link_Indexing_Action $post_link_indexing_action,
|
||||
Term_Link_Indexing_Action $term_link_indexing_action,
|
||||
Indexable_Helper $indexable_helper
|
||||
) {
|
||||
$this->post_indexation_action = $post_indexation_action;
|
||||
$this->term_indexation_action = $term_indexation_action;
|
||||
$this->post_type_archive_indexation_action = $post_type_archive_indexation_action;
|
||||
$this->general_indexation_action = $general_indexation_action;
|
||||
$this->complete_indexation_action = $complete_indexation_action;
|
||||
$this->prepare_indexing_action = $prepare_indexing_action;
|
||||
$this->post_link_indexing_action = $post_link_indexing_action;
|
||||
$this->term_link_indexing_action = $term_link_indexing_action;
|
||||
$this->indexable_helper = $indexable_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the namespace.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_namespace() {
|
||||
return Main::WP_CLI_NAMESPACE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indexes all your content to ensure the best performance.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--network]
|
||||
* : Performs the indexation on all sites within the network.
|
||||
*
|
||||
* [--reindex]
|
||||
* : Removes all existing indexables and then reindexes them.
|
||||
*
|
||||
* [--skip-confirmation]
|
||||
* : Skips the confirmations (for automated systems).
|
||||
*
|
||||
* [--interval=<interval>]
|
||||
* : The number of microseconds (millionths of a second) to wait between index actions.
|
||||
* ---
|
||||
* default: 500000
|
||||
* ---
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* wp yoast index
|
||||
*
|
||||
* @when after_wp_load
|
||||
*
|
||||
* @param array|null $args The arguments.
|
||||
* @param array|null $assoc_args The associative arguments.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function index( $args = null, $assoc_args = null ) {
|
||||
if ( ! $this->indexable_helper->should_index_indexables() ) {
|
||||
WP_CLI::log(
|
||||
\__( 'Your WordPress environment is running on a non-production site. Indexables can only be created on production environments. Please check your `WP_ENVIRONMENT_TYPE` settings.', 'wordpress-seo' )
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! isset( $assoc_args['network'] ) ) {
|
||||
$this->run_indexation_actions( $assoc_args );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$criteria = [
|
||||
'fields' => 'ids',
|
||||
'spam' => 0,
|
||||
'deleted' => 0,
|
||||
'archived' => 0,
|
||||
];
|
||||
$blog_ids = \get_sites( $criteria );
|
||||
|
||||
foreach ( $blog_ids as $blog_id ) {
|
||||
\switch_to_blog( $blog_id );
|
||||
\do_action( '_yoast_run_migrations' );
|
||||
$this->run_indexation_actions( $assoc_args );
|
||||
\restore_current_blog();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs all indexation actions.
|
||||
*
|
||||
* @param array $assoc_args The associative arguments.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function run_indexation_actions( $assoc_args ) {
|
||||
// See if we need to clear all indexables before repopulating.
|
||||
if ( isset( $assoc_args['reindex'] ) ) {
|
||||
|
||||
// Argument --skip-confirmation to prevent confirmation (for automated systems).
|
||||
if ( ! isset( $assoc_args['skip-confirmation'] ) ) {
|
||||
WP_CLI::confirm( 'This will clear all previously indexed objects. Are you certain you wish to proceed?' );
|
||||
}
|
||||
|
||||
// Truncate the tables.
|
||||
$this->clear();
|
||||
|
||||
// Delete the transients to make sure re-indexing runs every time.
|
||||
\delete_transient( Indexable_Post_Indexation_Action::UNINDEXED_COUNT_TRANSIENT );
|
||||
\delete_transient( Indexable_Post_Type_Archive_Indexation_Action::UNINDEXED_COUNT_TRANSIENT );
|
||||
\delete_transient( Indexable_Term_Indexation_Action::UNINDEXED_COUNT_TRANSIENT );
|
||||
}
|
||||
|
||||
$indexation_actions = [
|
||||
'posts' => $this->post_indexation_action,
|
||||
'terms' => $this->term_indexation_action,
|
||||
'post type archives' => $this->post_type_archive_indexation_action,
|
||||
'general objects' => $this->general_indexation_action,
|
||||
'post links' => $this->post_link_indexing_action,
|
||||
'term links' => $this->term_link_indexing_action,
|
||||
];
|
||||
|
||||
$this->prepare_indexing_action->prepare();
|
||||
|
||||
$interval = (int) $assoc_args['interval'];
|
||||
foreach ( $indexation_actions as $name => $indexation_action ) {
|
||||
$this->run_indexation_action( $name, $indexation_action, $interval );
|
||||
}
|
||||
|
||||
$this->complete_indexation_action->complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an indexation action.
|
||||
*
|
||||
* @param string $name The name of the object to be indexed.
|
||||
* @param Indexation_Action_Interface $indexation_action The indexation action.
|
||||
* @param int $interval Number of microseconds (millionths of a second) to wait between index actions.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function run_indexation_action( $name, Indexation_Action_Interface $indexation_action, $interval ) {
|
||||
$total = $indexation_action->get_total_unindexed();
|
||||
if ( $total > 0 ) {
|
||||
$limit = $indexation_action->get_limit();
|
||||
$progress = Utils\make_progress_bar( 'Indexing ' . $name, $total );
|
||||
do {
|
||||
$indexables = $indexation_action->index();
|
||||
$count = \count( $indexables );
|
||||
$progress->tick( $count );
|
||||
\usleep( $interval );
|
||||
Utils\wp_clear_object_cache();
|
||||
} while ( $count >= $limit );
|
||||
$progress->finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the database related to the indexables.
|
||||
*/
|
||||
protected function clear() {
|
||||
global $wpdb;
|
||||
|
||||
// For the PreparedSQLPlaceholders issue, see: https://github.com/WordPress/WordPress-Coding-Standards/issues/1903.
|
||||
// For the DirectDBQuery issue, see: https://github.com/WordPress/WordPress-Coding-Standards/issues/1947.
|
||||
// phpcs:disable WordPress.DB -- Table names should not be quoted and truncate queries can not be cached.
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
'TRUNCATE TABLE %1$s',
|
||||
Model::get_table_name( 'Indexable' )
|
||||
)
|
||||
);
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
'TRUNCATE TABLE %1$s',
|
||||
Model::get_table_name( 'Indexable_Hierarchy' )
|
||||
)
|
||||
);
|
||||
// phpcs:enable
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Checks if the Addon_Installation constant is set.
|
||||
*/
|
||||
class Addon_Installation_Conditional extends Feature_Flag_Conditional {
|
||||
|
||||
/**
|
||||
* Returns the name of the feature flag.
|
||||
* 'YOAST_SEO_' is automatically prepended to it and it will be uppercased.
|
||||
*
|
||||
* @return string the name of the feature flag.
|
||||
*/
|
||||
protected function get_feature_flag() {
|
||||
return 'ADDON_INSTALLATION';
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when in the admin.
|
||||
*/
|
||||
class Admin_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns whether or not this conditional is met.
|
||||
*
|
||||
* @return bool Whether or not the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
return \is_admin();
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals\Admin;
|
||||
|
||||
use Yoast\WP\SEO\Conditionals\Conditional;
|
||||
|
||||
/**
|
||||
* Checks if the post is saved by inline-save. This is the case when doing quick edit.
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded -- Base class can't be written shorter without abbreviating.
|
||||
*/
|
||||
class Doing_Post_Quick_Edit_Save_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Checks if the current request is ajax and the action is inline-save.
|
||||
*
|
||||
* @return bool True when the quick edit action is executed.
|
||||
*/
|
||||
public function is_met() {
|
||||
if ( ! \wp_doing_ajax() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Do the same nonce check as is done in wp_ajax_inline_save because we hook into that request.
|
||||
if ( ! \check_ajax_referer( 'inlineeditnonce', '_inline_edit', false ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! isset( $_POST['action'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sanitized_action = \sanitize_text_field( \wp_unslash( $_POST['action'] ) );
|
||||
|
||||
return ( $sanitized_action === 'inline-save' );
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals\Admin;
|
||||
|
||||
use Yoast\WP\SEO\Conditionals\Conditional;
|
||||
|
||||
/**
|
||||
* Conditional that is only when we want the Estimated Reading Time.
|
||||
*/
|
||||
class Estimated_Reading_Time_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* The Post Conditional.
|
||||
*
|
||||
* @var Post_Conditional
|
||||
*/
|
||||
protected $post_conditional;
|
||||
|
||||
/**
|
||||
* Constructs the Estimated Reading Time Conditional.
|
||||
*
|
||||
* @param Post_Conditional $post_conditional The post conditional.
|
||||
*/
|
||||
public function __construct( Post_Conditional $post_conditional ) {
|
||||
$this->post_conditional = $post_conditional;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this conditional is met.
|
||||
*
|
||||
* @return bool Whether the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing -- Reason: Nonce verification should not be done in a conditional but rather in the classes using the conditional.
|
||||
// Check if we are in our Elementor ajax request (for saving).
|
||||
if ( \wp_doing_ajax() && isset( $_POST['action'] ) && \is_string( $_POST['action'] ) ) {
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are only strictly comparing the variable.
|
||||
$post_action = \wp_unslash( $_POST['action'] );
|
||||
if ( $post_action === 'wpseo_elementor_save' ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $this->post_conditional->is_met() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We don't support Estimated Reading Time on the attachment post type.
|
||||
if ( isset( $_GET['post'] ) && \is_string( $_GET['post'] ) ) {
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are casting to an integer.
|
||||
$post_id = (int) \wp_unslash( $_GET['post'] );
|
||||
if ( $post_id !== 0 && \get_post_type( $post_id ) === 'attachment' ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals\Admin;
|
||||
|
||||
use Yoast\WP\SEO\Conditionals\Conditional;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when current page is the tools page.
|
||||
*/
|
||||
class Licenses_Page_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns whether or not this conditional is met.
|
||||
*
|
||||
* @return bool Whether or not the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
global $pagenow;
|
||||
|
||||
if ( $pagenow !== 'admin.php' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification -- This is not a form.
|
||||
if ( isset( $_GET['page'] ) && $_GET['page'] === 'wpseo_licenses' ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals\Admin;
|
||||
|
||||
use Yoast\WP\SEO\Conditionals\Conditional;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when not on a network admin page.
|
||||
*/
|
||||
class Non_Network_Admin_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns whether or not this conditional is met.
|
||||
*
|
||||
* @return bool Whether or not the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
return ! \is_network_admin();
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals\Admin;
|
||||
|
||||
use Yoast\WP\SEO\Conditionals\Conditional;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when on a post edit or new post page.
|
||||
*/
|
||||
class Post_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns whether or not this conditional is met.
|
||||
*
|
||||
* @return bool Whether or not the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
global $pagenow;
|
||||
|
||||
// Current page is the creation of a new post (type, i.e. post, page, custom post or attachment).
|
||||
if ( $pagenow === 'post-new.php' ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Current page is the edit page of an existing post (type, i.e. post, page, custom post or attachment).
|
||||
if ( $pagenow === 'post.php' ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals\Admin;
|
||||
|
||||
use Yoast\WP\SEO\Conditionals\Conditional;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when on a post overview page or during an ajax request.
|
||||
*/
|
||||
class Posts_Overview_Or_Ajax_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns whether or not this conditional is met.
|
||||
*
|
||||
* @return bool Whether or not the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
global $pagenow;
|
||||
return $pagenow === 'edit.php' || \wp_doing_ajax();
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
<?php // phpcs:ignore Yoast.Files.FileName.InvalidClassFileName -- Reason: this explicitly concerns the Yoast admin.
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals\Admin;
|
||||
|
||||
use Yoast\WP\SEO\Conditionals\Conditional;
|
||||
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when on a Yoast SEO admin page.
|
||||
*/
|
||||
class Yoast_Admin_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Holds the Current_Page_Helper.
|
||||
*
|
||||
* @var Current_Page_Helper
|
||||
*/
|
||||
private $current_page_helper;
|
||||
|
||||
/**
|
||||
* Constructs the conditional.
|
||||
*
|
||||
* @param \Yoast\WP\SEO\Helpers\Current_Page_Helper $current_page_helper The current page helper.
|
||||
*/
|
||||
public function __construct( Current_Page_Helper $current_page_helper ) {
|
||||
$this->current_page_helper = $current_page_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` when on the admin dashboard, update or Yoast SEO pages.
|
||||
*
|
||||
* @return bool `true` when on the admin dashboard, update or Yoast SEO pages.
|
||||
*/
|
||||
public function is_met() {
|
||||
if ( ! \is_admin() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->current_page_helper->is_yoast_seo_page();
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when the 'Redirect attachment URLs to the attachment itself' setting is enabled.
|
||||
*/
|
||||
class Attachment_Redirections_Enabled_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* The options helper.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* Attachment_Redirections_Enabled_Conditional constructor.
|
||||
*
|
||||
* @param Options_Helper $options The options helper.
|
||||
*/
|
||||
public function __construct( Options_Helper $options ) {
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the 'Redirect attachment URLs to the attachment itself' setting has been enabled.
|
||||
*
|
||||
* @return bool `true` when the 'Redirect attachment URLs to the attachment itself' setting has been enabled.
|
||||
*/
|
||||
public function is_met() {
|
||||
return $this->options->get( 'disable-attachment' );
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Conditional interface, used to prevent integrations from loading.
|
||||
*/
|
||||
interface Conditional {
|
||||
|
||||
/**
|
||||
* Returns whether or not this conditional is met.
|
||||
*
|
||||
* @return bool Whether or not the conditional is met.
|
||||
*/
|
||||
public function is_met();
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when on deactivating Yoast SEO.
|
||||
*/
|
||||
class Deactivating_Yoast_Seo_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns whether this conditional is met.
|
||||
*
|
||||
* @return bool Whether the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification -- We can't verify nonce since this might run from any user.
|
||||
if ( isset( $_GET['action'] ) && \sanitize_text_field( \wp_unslash( $_GET['action'] ) ) === 'deactivate' && isset( $_GET['plugin'] ) && \sanitize_text_field( \wp_unslash( $_GET['plugin'] === 'wordpress-seo/wp-seo.php' ) ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
use WPSEO_Utils;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when in development mode.
|
||||
*/
|
||||
class Development_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns whether or not this conditional is met.
|
||||
*
|
||||
* @return bool Whether or not the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
return WPSEO_Utils::is_development_mode();
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Abstract class for creating conditionals based on feature flags.
|
||||
*/
|
||||
abstract class Feature_Flag_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns whether or not this conditional is met.
|
||||
*
|
||||
* @return bool Whether or not the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
$feature_flag = \strtoupper( $this->get_feature_flag() );
|
||||
|
||||
return \defined( 'YOAST_SEO_' . $feature_flag ) && \constant( 'YOAST_SEO_' . $feature_flag ) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the feature flag.
|
||||
* 'YOAST_SEO_' is automatically prepended to it and it will be uppercased.
|
||||
*
|
||||
* @return string the name of the feature flag.
|
||||
*/
|
||||
abstract protected function get_feature_flag();
|
||||
|
||||
/**
|
||||
* Returns the feature name.
|
||||
*
|
||||
* @return string the name of the feature flag.
|
||||
*/
|
||||
public function get_feature_name() {
|
||||
return $this->get_feature_flag();
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when NOT in the admin.
|
||||
*/
|
||||
class Front_End_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns `true` when NOT on an admin page.
|
||||
*
|
||||
* @return bool `true` when NOT on an admin page.
|
||||
*/
|
||||
public function is_met() {
|
||||
return ! \is_admin();
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when the current request uses the GET method.
|
||||
*/
|
||||
class Get_Request_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns whether or not this conditional is met.
|
||||
*
|
||||
* @return bool Whether or not the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
if ( isset( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] === 'GET' ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when the headless rest endpoints are enabled.
|
||||
*/
|
||||
class Headless_Rest_Endpoints_Enabled_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* The options helper.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* Headless_Rest_Endpoints_Enabled_Conditional constructor.
|
||||
*
|
||||
* @param Options_Helper $options The options helper.
|
||||
*/
|
||||
public function __construct( Options_Helper $options ) {
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` whether the headless REST endpoints have been enabled.
|
||||
*
|
||||
* @return bool `true` when the headless REST endpoints have been enabled.
|
||||
*/
|
||||
public function is_met() {
|
||||
return $this->options->get( 'enable_headless_rest_endpoints' );
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when current page is not a specific tool's page.
|
||||
*/
|
||||
class Import_Tool_Selected_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns whether or not this conditional is met.
|
||||
*
|
||||
* @return bool Whether or not the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We just check whether a URL parameter does not exist.
|
||||
return ( isset( $_GET['tool'] ) && $_GET['tool'] === 'import-export' );
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when Jetpack exists.
|
||||
*/
|
||||
class Jetpack_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns `true` when the Jetpack plugin exists on this
|
||||
* WordPress installation.
|
||||
*
|
||||
* @return bool `true` when the Jetpack plugin exists on this WordPress installation.
|
||||
*/
|
||||
public function is_met() {
|
||||
return \class_exists( 'Jetpack' );
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
use Yoast\WP\SEO\Config\Migration_Status;
|
||||
|
||||
/**
|
||||
* Class for integrations that depend on having all migrations run.
|
||||
*/
|
||||
class Migrations_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* The migration status.
|
||||
*
|
||||
* @var Migration_Status
|
||||
*/
|
||||
protected $migration_status;
|
||||
|
||||
/**
|
||||
* Migrations_Conditional constructor.
|
||||
*
|
||||
* @param Migration_Status $migration_status The migration status object.
|
||||
*/
|
||||
public function __construct( Migration_Status $migration_status ) {
|
||||
$this->migration_status = $migration_status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` when all database migrations have been run.
|
||||
*
|
||||
* @return bool `true` when all database migrations have been run.
|
||||
*/
|
||||
public function is_met() {
|
||||
return $this->migration_status->is_version( 'free', \WPSEO_VERSION );
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Feature flag conditional for the new settings UI.
|
||||
*/
|
||||
class New_Settings_Ui_Conditional extends Feature_Flag_Conditional {
|
||||
|
||||
/**
|
||||
* Returns the name of the feature flag.
|
||||
*
|
||||
* @return string The name of the feature flag.
|
||||
*/
|
||||
protected function get_feature_flag() {
|
||||
return 'NEW_SETTINGS_UI';
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when news SEO is activated.
|
||||
*/
|
||||
class News_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns whether or not this conditional is met.
|
||||
*
|
||||
* @return bool Whether or not the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
return \defined( 'WPSEO_NEWS_VERSION' );
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Trait for integrations that do not have any conditionals.
|
||||
*/
|
||||
trait No_Conditionals {
|
||||
|
||||
/**
|
||||
* Returns an empty array, meaning no conditionals are required to load whatever uses this trait.
|
||||
*
|
||||
* @return array The conditionals that must be met to load this.
|
||||
*/
|
||||
public static function get_conditionals() {
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when current page is not a specific tool's page.
|
||||
*/
|
||||
class No_Tool_Selected_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns whether or not this conditional is met.
|
||||
*
|
||||
* @return bool Whether or not the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We just check whether a URL parameter does not exist.
|
||||
return ! isset( $_GET['tool'] );
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when we aren't in a multisite setup.
|
||||
*/
|
||||
class Non_Multisite_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns `true` when we aren't in a multisite setup.
|
||||
*
|
||||
* @return bool `true` when we aren't in a multisite setup.
|
||||
*/
|
||||
public function is_met() {
|
||||
return ! \is_multisite();
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when not in a admin-ajax request.
|
||||
*/
|
||||
class Not_Admin_Ajax_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns whether or not this conditional is met.
|
||||
*
|
||||
* @return bool Whether or not the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
return ( ! \wp_doing_ajax() );
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when the Open Graph feature is enabled.
|
||||
*/
|
||||
class Open_Graph_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* The options helper.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* Open_Graph_Conditional constructor.
|
||||
*
|
||||
* @param Options_Helper $options The options helper.
|
||||
*/
|
||||
public function __construct( Options_Helper $options ) {
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` when the Open Graph feature is enabled.
|
||||
*
|
||||
* @return bool `true` when the Open Graph feature is enabled.
|
||||
*/
|
||||
public function is_met() {
|
||||
return $this->options->get( 'opengraph' ) === true;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Class Premium_Active_Conditional.
|
||||
*/
|
||||
class Premium_Active_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns whether or not this conditional is met.
|
||||
*
|
||||
* @return bool Whether or not the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
return \YoastSEO()->helpers->product->is_premium();
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Abstract class for creating conditionals based on feature flags.
|
||||
*/
|
||||
class Premium_Inactive_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns whether or not this conditional is met.
|
||||
*
|
||||
* @return bool Whether or not the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
return ! \YoastSEO()->helpers->product->is_premium();
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when in frontend or page is a post overview or post add/edit form.
|
||||
*/
|
||||
class Primary_Category_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* The current page helper.
|
||||
*
|
||||
* @var Current_Page_Helper
|
||||
*/
|
||||
private $current_page;
|
||||
|
||||
/**
|
||||
* Primary_Category_Conditional constructor.
|
||||
*
|
||||
* @param Current_Page_Helper $current_page The current page helper.
|
||||
*/
|
||||
public function __construct( Current_Page_Helper $current_page ) {
|
||||
$this->current_page = $current_page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` when on the frontend,
|
||||
* or when on the post overview, post edit or new post admin page,
|
||||
* or when on additional admin pages, allowed by filter.
|
||||
*
|
||||
* @return bool `true` when on the frontend, or when on the post overview,
|
||||
* post edit, new post admin page or additional admin pages, allowed by filter.
|
||||
*/
|
||||
public function is_met() {
|
||||
if ( ! \is_admin() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter: Adds the possibility to use primary category at additional admin pages.
|
||||
*
|
||||
* @param array $admin_pages List of additional admin pages.
|
||||
*/
|
||||
$additional_pages = \apply_filters( 'wpseo_primary_category_admin_pages', [] );
|
||||
return \in_array( $this->current_page->get_current_admin_page(), \array_merge( [ 'edit.php', 'post.php', 'post-new.php' ], $additional_pages ), true );
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when on the front end or Yoast file editor page.
|
||||
*/
|
||||
class Robots_Txt_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Holds the Front_End_Conditional instance.
|
||||
*
|
||||
* @var Front_End_Conditional
|
||||
*/
|
||||
protected $front_end_conditional;
|
||||
|
||||
/**
|
||||
* Constructs the class.
|
||||
*
|
||||
* @param Front_End_Conditional $front_end_conditional The front end conditional.
|
||||
*/
|
||||
public function __construct( Front_End_Conditional $front_end_conditional ) {
|
||||
$this->front_end_conditional = $front_end_conditional;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this conditional is met.
|
||||
*
|
||||
* @return bool Whether or not the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
return $this->front_end_conditional->is_met() || $this->is_file_editor_page();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current page is the file editor page.
|
||||
*
|
||||
* This checks for two locations:
|
||||
* - Multisite network admin file editor page
|
||||
* - Single site file editor page (under tools)
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_file_editor_page() {
|
||||
global $pagenow;
|
||||
|
||||
if ( $pagenow !== 'admin.php' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification -- This is not a form.
|
||||
if ( isset( $_GET['page'] ) && $_GET['page'] === 'wpseo_files' && \is_multisite() && \is_network_admin() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification -- This is not a form.
|
||||
if ( ! ( isset( $_GET['page'] ) && $_GET['page'] === 'wpseo_tools' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification -- This is not a form.
|
||||
if ( isset( $_GET['tool'] ) && $_GET['tool'] === 'file-editor' ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when the SEMrush integration is enabled.
|
||||
*/
|
||||
class SEMrush_Enabled_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* The options helper.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* SEMrush_Enabled_Conditional constructor.
|
||||
*
|
||||
* @param Options_Helper $options The options helper.
|
||||
*/
|
||||
public function __construct( Options_Helper $options ) {
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this conditional is met.
|
||||
*
|
||||
* @return bool Whether or not the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
return $this->options->get( 'semrush_integration_active', false );
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Class Settings_Conditional.
|
||||
*/
|
||||
class Settings_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Holds User_Can_Manage_Wpseo_Options_Conditional.
|
||||
*
|
||||
* @var User_Can_Manage_Wpseo_Options_Conditional
|
||||
*/
|
||||
protected $user_can_manage_wpseo_options_conditional;
|
||||
|
||||
/**
|
||||
* Constructs Settings_Conditional.
|
||||
*
|
||||
* @param User_Can_Manage_Wpseo_Options_Conditional $user_can_manage_wpseo_options_conditional The User_Can_Manage_Wpseo_Options_Conditional.
|
||||
*/
|
||||
public function __construct(
|
||||
User_Can_Manage_Wpseo_Options_Conditional $user_can_manage_wpseo_options_conditional
|
||||
) {
|
||||
$this->user_can_manage_wpseo_options_conditional = $user_can_manage_wpseo_options_conditional;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this conditional is met.
|
||||
*
|
||||
* @return bool Whether or not the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
if ( ! $this->user_can_manage_wpseo_options_conditional->is_met() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
|
||||
/**
|
||||
* Should_Index_Links_Conditional class.
|
||||
*/
|
||||
class Should_Index_Links_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* The options helper.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
protected $options_helper;
|
||||
|
||||
/**
|
||||
* Should_Index_Links_Conditional constructor.
|
||||
*
|
||||
* @param Options_Helper $options_helper The options helper.
|
||||
*/
|
||||
public function __construct( Options_Helper $options_helper ) {
|
||||
$this->options_helper = $options_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` when the links on this website should be indexed.
|
||||
*
|
||||
* @return bool `true` when the links on this website should be indexed.
|
||||
*/
|
||||
public function is_met() {
|
||||
$should_index_links = $this->options_helper->get( 'enable_text_link_counter' );
|
||||
|
||||
/**
|
||||
* Filter: 'wpseo_should_index_links' - Allows disabling of Yoast's links indexation.
|
||||
*
|
||||
* @api bool To disable the indexation, return false.
|
||||
*/
|
||||
return \apply_filters( 'wpseo_should_index_links', $should_index_links );
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals;
|
||||
|
||||
/**
|
||||
* Checks if the YOAST_SEO_TEXT_FORMALITY constant is set.
|
||||
*/
|
||||
class Text_Formality_Conditional extends Feature_Flag_Conditional {
|
||||
|
||||
/**
|
||||
* Returns the name of the feature flag.
|
||||
* 'YOAST_SEO_' is automatically prepended to it and it will be uppercased.
|
||||
*
|
||||
* @return string the name of the feature flag.
|
||||
*/
|
||||
public function get_feature_flag() {
|
||||
return 'TEXT_FORMALITY';
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals\Third_Party;
|
||||
|
||||
use Yoast\WP\SEO\Conditionals\Conditional;
|
||||
|
||||
/**
|
||||
* Conditional that is met when the Elementor plugin is installed and activated.
|
||||
*/
|
||||
class Elementor_Activated_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Checks if the Elementor plugins is installed and activated.
|
||||
*
|
||||
* @return bool `true` when the Elementor plugin is installed and activated.
|
||||
*/
|
||||
public function is_met() {
|
||||
return \defined( 'ELEMENTOR__FILE__' );
|
||||
}
|
||||
}
|
39
wp-content/plugins/wordpress-seo/src/conditionals/third-party/elementor-edit-conditional.php
vendored
Normal file
39
wp-content/plugins/wordpress-seo/src/conditionals/third-party/elementor-edit-conditional.php
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals\Third_Party;
|
||||
|
||||
use Yoast\WP\SEO\Conditionals\Conditional;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when on an Elementor edit page or when the current
|
||||
* request is an ajax request for saving our post meta data.
|
||||
*/
|
||||
class Elementor_Edit_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns whether this conditional is met.
|
||||
*
|
||||
* @return bool Whether the conditional is met.
|
||||
*/
|
||||
public function is_met() {
|
||||
global $pagenow;
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
|
||||
if ( isset( $_GET['action'] ) && \is_string( $_GET['action'] ) ) {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are only strictly comparing.
|
||||
$get_action = \wp_unslash( $_GET['action'] );
|
||||
if ( $pagenow === 'post.php' && $get_action === 'elementor' ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information.
|
||||
if ( isset( $_POST['action'] ) && \is_string( $_POST['action'] ) ) {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are only strictly comparing.
|
||||
$post_action = \wp_unslash( $_POST['action'] );
|
||||
return \wp_doing_ajax() && $post_action === 'wpseo_elementor_save';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals\Third_Party;
|
||||
|
||||
use Yoast\WP\SEO\Conditionals\Conditional;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when Jetpack_Boost exists.
|
||||
*/
|
||||
class Jetpack_Boost_Active_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Returns `true` when the Jetpack_Boost class exists within this WordPress installation.
|
||||
*
|
||||
* @return bool `true` when the Jetpack_Boost class exists within this WordPress installation.
|
||||
*/
|
||||
public function is_met() {
|
||||
return \class_exists( '\Automattic\Jetpack_Boost\Jetpack_Boost', false );
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals\Third_Party;
|
||||
|
||||
use Automattic\Jetpack_Boost\Lib\Premium_Features;
|
||||
use Yoast\WP\SEO\Conditionals\Conditional;
|
||||
|
||||
/**
|
||||
* Conditional that is met when Jetpack Boost is not installed, activated or premium.
|
||||
*/
|
||||
class Jetpack_Boost_Not_Premium_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Whether Jetpack Boost is not premium.
|
||||
*
|
||||
* @return bool Whether Jetpack Boost is not premium.
|
||||
*/
|
||||
public function is_met() {
|
||||
return ! $this->is_premium();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves, if available, if Jetpack Boost has priority feature available.
|
||||
*
|
||||
* @return bool Whether Jetpack Boost is premium.
|
||||
*/
|
||||
private function is_premium() {
|
||||
if ( \class_exists( '\Automattic\Jetpack_Boost\Lib\Premium_Features', false ) ) {
|
||||
return Premium_Features::has_feature(
|
||||
Premium_Features::PRIORITY_SUPPORT
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
20
wp-content/plugins/wordpress-seo/src/conditionals/third-party/polylang-conditional.php
vendored
Normal file
20
wp-content/plugins/wordpress-seo/src/conditionals/third-party/polylang-conditional.php
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Conditionals\Third_Party;
|
||||
|
||||
use Yoast\WP\SEO\Conditionals\Conditional;
|
||||
|
||||
/**
|
||||
* Conditional that is only met when the Polylang plugin is active.
|
||||
*/
|
||||
class Polylang_Conditional implements Conditional {
|
||||
|
||||
/**
|
||||
* Checks whether the Polylang plugin is installed and active.
|
||||
*
|
||||
* @return bool Whether Polylang is installed and active.
|
||||
*/
|
||||
public function is_met() {
|
||||
return \defined( 'POLYLANG_FILE' );
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user