Phase 5: Content and SEO - Yoast SEO, Schema.org markup, Open Graph, favicon support, XML sitemap
This commit is contained in:
Executable
+86
@@ -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' );
|
||||
}
|
||||
}
|
||||
}
|
||||
+131
@@ -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 {
|
||||
|
||||
public 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.
|
||||
*
|
||||
* @param 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;
|
||||
}
|
||||
}
|
||||
Executable
+344
@@ -0,0 +1,344 @@
|
||||
<?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.
|
||||
*/
|
||||
public 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 = [];
|
||||
$old_values = $this->get_old_values( self::SITE_REPRESENTATION_FIELDS );
|
||||
|
||||
foreach ( self::SITE_REPRESENTATION_FIELDS as $field_name ) {
|
||||
if ( isset( $params[ $field_name ] ) ) {
|
||||
$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 );
|
||||
|
||||
/**
|
||||
* Action: 'wpseo_post_update_site_representation' - Allows for Hiive event tracking.
|
||||
*
|
||||
* @param array $params The new values of the options.
|
||||
* @param array $old_values The old values of the options.
|
||||
* @param array $failures The options that failed to be saved.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
\do_action( 'wpseo_ftc_post_update_site_representation', $params, $old_values, $failures );
|
||||
|
||||
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 ) {
|
||||
$old_values = $this->get_old_values( \array_keys( $this->social_profiles_helper->get_organization_social_profile_fields() ) );
|
||||
$failures = $this->social_profiles_helper->set_organization_social_profiles( $params );
|
||||
|
||||
/**
|
||||
* Action: 'wpseo_post_update_social_profiles' - Allows for Hiive event tracking.
|
||||
*
|
||||
* @param array $params The new values of the options.
|
||||
* @param array $old_values The old values of the options.
|
||||
* @param array $failures The options that failed to be saved.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
\do_action( 'wpseo_ftc_post_update_social_profiles', $params, $old_values, $failures );
|
||||
|
||||
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'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Action: 'wpseo_post_update_enable_tracking' - Allows for Hiive event tracking.
|
||||
*
|
||||
* @param array $new_value The new value.
|
||||
* @param array $old_value The old value.
|
||||
* @param bool $failure Whether the option failed to be stored.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
// $success is negated to be aligned with the other two actions which pass $failures.
|
||||
\do_action( 'wpseo_ftc_post_update_enable_tracking', $params['tracking'], $option_value, ! $success );
|
||||
|
||||
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 ( $configuration_option !== null ) {
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the old values for the given fields.
|
||||
*
|
||||
* @param array $fields_names The fields to get the old values for.
|
||||
*
|
||||
* @return array The old values.
|
||||
*/
|
||||
private function get_old_values( array $fields_names ): array {
|
||||
$old_values = [];
|
||||
|
||||
foreach ( $fields_names as $field_name ) {
|
||||
$old_values[ $field_name ] = $this->options_helper->get( $field_name );
|
||||
}
|
||||
|
||||
return $old_values;
|
||||
}
|
||||
}
|
||||
Executable
+266
@@ -0,0 +1,266 @@
|
||||
<?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
|
||||
*/
|
||||
public const PLUGIN = null;
|
||||
|
||||
/**
|
||||
* The type the class deals with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public 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.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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 = static::class;
|
||||
$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 = static::class;
|
||||
$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 );
|
||||
}
|
||||
}
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
<?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
|
||||
*/
|
||||
public const PLUGIN = null;
|
||||
|
||||
/**
|
||||
* The type the class deals with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE = null;
|
||||
|
||||
/**
|
||||
* The option_name of the AIOSEO option that contains the settings.
|
||||
*/
|
||||
public 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.
|
||||
*
|
||||
* @see https://yoast.com/help/list-available-snippet-variables-yoast-seo/
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param int $import_cursor 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.
|
||||
*
|
||||
* @param int $max_posts 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 [];
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
<?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.
|
||||
*/
|
||||
public const PLUGIN = 'aioseo';
|
||||
|
||||
/**
|
||||
* The type of the action.
|
||||
*/
|
||||
public const TYPE = 'cleanup';
|
||||
|
||||
/**
|
||||
* The AIOSEO meta_keys to be cleaned up.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param int $max_posts 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;
|
||||
}
|
||||
}
|
||||
+111
@@ -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.
|
||||
*/
|
||||
public const PLUGIN = 'aioseo';
|
||||
|
||||
/**
|
||||
* The type of the action.
|
||||
*/
|
||||
public const TYPE = 'custom_archive_settings';
|
||||
|
||||
/**
|
||||
* The option_name of the AIOSEO option that contains the settings.
|
||||
*/
|
||||
public 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+103
@@ -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.
|
||||
*/
|
||||
public const PLUGIN = 'aioseo';
|
||||
|
||||
/**
|
||||
* The type of the action.
|
||||
*/
|
||||
public const TYPE = 'default_archive_settings';
|
||||
|
||||
/**
|
||||
* The option_name of the AIOSEO option that contains the settings.
|
||||
*/
|
||||
public 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 [];
|
||||
}
|
||||
}
|
||||
+213
@@ -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.
|
||||
*/
|
||||
public const PLUGIN = 'aioseo';
|
||||
|
||||
/**
|
||||
* The type of the action.
|
||||
*/
|
||||
public const TYPE = 'general_settings';
|
||||
|
||||
/**
|
||||
* The option_name of the AIOSEO option that contains the settings.
|
||||
*/
|
||||
public 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+625
@@ -0,0 +1,625 @@
|
||||
<?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.
|
||||
*/
|
||||
public const PLUGIN = 'aioseo';
|
||||
|
||||
/**
|
||||
* The type of the action.
|
||||
*/
|
||||
public const TYPE = 'posts';
|
||||
|
||||
/**
|
||||
* The map of aioseo to yoast meta.
|
||||
*
|
||||
* @var array<string, array<string, string|bool|array<string, string|bool>>>
|
||||
*/
|
||||
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 );
|
||||
$this->indexable_helper->save_indexable( $indexable );
|
||||
|
||||
// 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.
|
||||
*
|
||||
* @param int $max_posts 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|false $limit The maximum number of unimported objects to be returned.
|
||||
* False for "no limit".
|
||||
* @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.
|
||||
*
|
||||
* @param int $import_cursor 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 );
|
||||
}
|
||||
}
|
||||
+99
@@ -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.
|
||||
*/
|
||||
public const PLUGIN = 'aioseo';
|
||||
|
||||
/**
|
||||
* The type of the action.
|
||||
*/
|
||||
public const TYPE = 'posttype_default_settings';
|
||||
|
||||
/**
|
||||
* The option_name of the AIOSEO option that contains the settings.
|
||||
*/
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+108
@@ -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.
|
||||
*/
|
||||
public const PLUGIN = 'aioseo';
|
||||
|
||||
/**
|
||||
* The type of the action.
|
||||
*/
|
||||
public const TYPE = 'taxonomy_settings';
|
||||
|
||||
/**
|
||||
* The option_name of the AIOSEO option that contains the settings.
|
||||
*/
|
||||
public 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.
|
||||
*
|
||||
* @see https://yoast.com/help/list-available-snippet-variables-yoast-seo/
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
Executable
+255
@@ -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.
|
||||
*/
|
||||
public const PLUGIN = 'aioseo';
|
||||
|
||||
/**
|
||||
* The type of the action.
|
||||
*/
|
||||
public 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.
|
||||
*
|
||||
* @param int $limit The maximum number of validations.
|
||||
*/
|
||||
$limit = \apply_filters( 'wpseo_aioseo_validation_limit', 25 );
|
||||
|
||||
if ( ! \is_int( $limit ) || $limit < 1 ) {
|
||||
$limit = 25;
|
||||
}
|
||||
|
||||
return $limit;
|
||||
}
|
||||
}
|
||||
Executable
+145
@@ -0,0 +1,145 @@
|
||||
<?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
|
||||
*/
|
||||
public const PLUGIN = 'conflicting-plugins';
|
||||
|
||||
/**
|
||||
* The type the class deals with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public 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.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
+32
@@ -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 );
|
||||
}
|
||||
Executable
+34
@@ -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();
|
||||
}
|
||||
+174
@@ -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 ];
|
||||
}
|
||||
}
|
||||
+103
@@ -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
|
||||
*/
|
||||
public const UNINDEXED_COUNT_TRANSIENT = null;
|
||||
|
||||
/**
|
||||
* The transient cache key for limited counts.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public 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 = ( $query === '' ) ? [] : $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 = ( $query === '' ) ? 0 : $this->wpdb->get_var( $query );
|
||||
|
||||
if ( $count === null ) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Indexing;
|
||||
|
||||
use wpdb;
|
||||
use Yoast\WP\SEO\Builders\Indexable_Link_Builder;
|
||||
use Yoast\WP\SEO\Helpers\Indexable_Helper;
|
||||
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 helper.
|
||||
*
|
||||
* @var Indexable_Helper
|
||||
*/
|
||||
protected $indexable_helper;
|
||||
|
||||
/**
|
||||
* 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_Helper $indexable_helper The indexable repository.
|
||||
* @param Indexable_Repository $repository The indexable repository.
|
||||
* @param wpdb $wpdb The WordPress database instance.
|
||||
*/
|
||||
public function __construct(
|
||||
Indexable_Link_Builder $link_builder,
|
||||
Indexable_Helper $indexable_helper,
|
||||
Indexable_Repository $repository,
|
||||
wpdb $wpdb
|
||||
) {
|
||||
$this->link_builder = $link_builder;
|
||||
$this->indexable_helper = $indexable_helper;
|
||||
$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 );
|
||||
$this->indexable_helper->save_indexable( $indexable );
|
||||
|
||||
$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.
|
||||
*
|
||||
* @param int $limit 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();
|
||||
}
|
||||
Executable
+138
@@ -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.
|
||||
*/
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
Executable
+36
@@ -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();
|
||||
}
|
||||
}
|
||||
Executable
+207
@@ -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
|
||||
*/
|
||||
public const UNINDEXED_COUNT_TRANSIENT = 'wpseo_total_unindexed_posts';
|
||||
|
||||
/**
|
||||
* The transient cache key for limited counts.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public 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.
|
||||
*
|
||||
* @param int $limit 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
|
||||
);
|
||||
}
|
||||
}
|
||||
+212
@@ -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.
|
||||
*/
|
||||
public 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|false $limit Limit the number of counted objects.
|
||||
* False for "no limit".
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @param int $limit 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 );
|
||||
}
|
||||
}
|
||||
Executable
+197
@@ -0,0 +1,197 @@
|
||||
<?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.
|
||||
*/
|
||||
public const UNINDEXED_COUNT_TRANSIENT = 'wpseo_total_unindexed_terms';
|
||||
|
||||
/**
|
||||
* The transient cache key for limited counts.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public 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 = ( $query === '' ) ? [] : $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.
|
||||
*
|
||||
* @param int $limit 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();
|
||||
|
||||
if ( empty( $public_taxonomies ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$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();
|
||||
|
||||
if ( empty( $public_taxonomies ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$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
|
||||
);
|
||||
}
|
||||
}
|
||||
+34
@@ -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();
|
||||
}
|
||||
+36
@@ -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();
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Executable
+18
@@ -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 );
|
||||
}
|
||||
+142
@@ -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
|
||||
*/
|
||||
public const UNINDEXED_COUNT_TRANSIENT = 'wpseo_unindexed_post_link_count';
|
||||
|
||||
/**
|
||||
* The transient cache key for limited counts.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public 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
|
||||
);
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
<?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
|
||||
*/
|
||||
public const UNINDEXED_COUNT_TRANSIENT = 'wpseo_unindexed_term_link_count';
|
||||
|
||||
/**
|
||||
* The transient cache key for limited counts.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public 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() );
|
||||
|
||||
if ( $query === '' ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
if ( empty( $public_taxonomies ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$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();
|
||||
|
||||
if ( empty( $public_taxonomies ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$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',
|
||||
];
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?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.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function login() {
|
||||
if ( $this->client->has_valid_tokens() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prompt with login screen.
|
||||
}
|
||||
}
|
||||
+51
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
+96
@@ -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.
|
||||
*/
|
||||
public const TRANSIENT_CACHE_KEY = 'wpseo_semrush_related_keyphrases_%s_%s';
|
||||
|
||||
/**
|
||||
* The SEMrush keyphrase URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public 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 && isset( $transient['data']['columnNames'] ) && \count( $transient['data']['columnNames'] ) === 5 ) {
|
||||
return $this->to_result_object( $transient );
|
||||
}
|
||||
|
||||
$options = [
|
||||
'params' => [
|
||||
'phrase' => $keyphrase,
|
||||
'database' => $database,
|
||||
'export_columns' => 'Ph,Nq,Td,In,Kd',
|
||||
'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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Actions\Wincher;
|
||||
|
||||
use Exception;
|
||||
use Yoast\WP\SEO\Config\Wincher_Client;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
|
||||
/**
|
||||
* Class Wincher_Account_Action
|
||||
*/
|
||||
class Wincher_Account_Action {
|
||||
|
||||
public const ACCOUNT_URL = 'https://api.wincher.com/beta/account';
|
||||
public 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'] ?? null );
|
||||
$limit = ( $results['limits']['keywords']['limit'] ?? null );
|
||||
$history = ( $results['limits']['history_days'] ?? null );
|
||||
|
||||
return (object) [
|
||||
'canTrack' => ( $limit === null || $usage < $limit ),
|
||||
'limit' => $limit,
|
||||
'usage' => $usage,
|
||||
'historyDays' => $history,
|
||||
'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'] ?? null );
|
||||
$months = ( $result['months'] ?? null );
|
||||
$discount = ( $result['value'] ?? null );
|
||||
|
||||
// We display upgrade discount only if it's a rate discount and positive months/discount.
|
||||
if ( $type === 'RATE' && $months && $discount ) {
|
||||
|
||||
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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
+363
@@ -0,0 +1,363 @@
|
||||
<?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
|
||||
*/
|
||||
public const KEYPHRASES_ADD_URL = 'https://api.wincher.com/beta/websites/%s/keywords/bulk';
|
||||
|
||||
/**
|
||||
* The Wincher tracked keyphrase retrieval URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const KEYPHRASES_URL = 'https://api.wincher.com/beta/yoast/%s';
|
||||
|
||||
/**
|
||||
* The Wincher delete tracked keyphrase URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public 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.
|
||||
* @param string|null $start_at The position start date. Optional.
|
||||
*
|
||||
* @return object The keyphrase chart data.
|
||||
*/
|
||||
public function get_tracked_keyphrases( $used_keyphrases = null, $permalink = null, $start_at = 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,
|
||||
'start_at' => $start_at,
|
||||
]
|
||||
),
|
||||
[
|
||||
'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 ( $limits->limit === null ) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
+73
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Authorization\Application;
|
||||
|
||||
use Yoast\WP\SEO\AI_Authorization\Domain\Code_Verifier;
|
||||
|
||||
/**
|
||||
* Interface Code_Verifier_Handler_Interface
|
||||
*
|
||||
* This interface defines the methods for handling code verifier.
|
||||
*/
|
||||
interface Code_Verifier_Handler_Interface {
|
||||
|
||||
/**
|
||||
* Generate a code verifier for a user.
|
||||
*
|
||||
* @param string $user_email The user email.
|
||||
*
|
||||
* @return Code_Verifier The generated code verifier.
|
||||
*/
|
||||
public function generate( string $user_email ): Code_Verifier;
|
||||
|
||||
/**
|
||||
* Validate the code verifier for a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return string The code verifier.
|
||||
*
|
||||
* @throws RuntimeException If the code verifier is expired or invalid.
|
||||
*/
|
||||
public function validate( int $user_id ): string;
|
||||
}
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Authorization\Application;
|
||||
|
||||
use RuntimeException;
|
||||
use Yoast\WP\SEO\AI_Authorization\Domain\Code_Verifier;
|
||||
use Yoast\WP\SEO\AI_Authorization\Infrastructure\Code_Verifier_User_Meta_Repository;
|
||||
use Yoast\WP\SEO\Helpers\Date_Helper;
|
||||
/**
|
||||
* Class Code_Verifier_Service
|
||||
* Handles the generation and validation of code verifiers for users.
|
||||
*/
|
||||
class Code_Verifier_Handler implements Code_Verifier_Handler_Interface {
|
||||
private const VALIDITY_IN_SECONDS = 300; // 5 minutes
|
||||
|
||||
/**
|
||||
* The date helper.
|
||||
*
|
||||
* @var Date_Helper
|
||||
*/
|
||||
private $date_helper;
|
||||
|
||||
/**
|
||||
* The code verifier repository.
|
||||
*
|
||||
* @var Code_Verifier_User_Meta_Repository
|
||||
*/
|
||||
private $code_verifier_repository;
|
||||
|
||||
/**
|
||||
* Code_Verifier_Service constructor.
|
||||
*
|
||||
* @param Date_Helper $date_helper The date helper.
|
||||
* @param Code_Verifier_User_Meta_Repository $code_verifier_repository The code verifier repository.
|
||||
*/
|
||||
public function __construct( Date_Helper $date_helper, Code_Verifier_User_Meta_Repository $code_verifier_repository ) {
|
||||
$this->date_helper = $date_helper;
|
||||
$this->code_verifier_repository = $code_verifier_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a code verifier for a user.
|
||||
*
|
||||
* @param string $user_email The user email.
|
||||
*
|
||||
* @return Code_Verifier The generated code verifier.
|
||||
*/
|
||||
public function generate( string $user_email ): Code_Verifier {
|
||||
$random_string = \substr( \str_shuffle( '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' ), 1, 10 );
|
||||
$code = \hash( 'sha256', $user_email . $random_string );
|
||||
$created_at = $this->date_helper->current_time();
|
||||
|
||||
return new Code_Verifier( $code, $created_at );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the code verifier for a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return string The code verifier.
|
||||
*
|
||||
* @throws RuntimeException If the code verifier is expired or invalid.
|
||||
*/
|
||||
public function validate( int $user_id ): string {
|
||||
$code_verifier = $this->code_verifier_repository->get_code_verifier( $user_id );
|
||||
|
||||
if ( $code_verifier === null || $code_verifier->is_expired( self::VALIDITY_IN_SECONDS ) ) {
|
||||
$this->code_verifier_repository->delete_code_verifier( $user_id );
|
||||
throw new RuntimeException( 'Code verifier has expired or is invalid.' );
|
||||
}
|
||||
|
||||
return $code_verifier->get_code();
|
||||
}
|
||||
}
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Authorization\Application;
|
||||
|
||||
use RuntimeException;
|
||||
use WP_User;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Bad_Request_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Forbidden_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Internal_Server_Error_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Not_Found_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Payment_Required_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Request_Timeout_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Service_Unavailable_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Too_Many_Requests_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Unauthorized_Exception;
|
||||
|
||||
/**
|
||||
* Interface Token_Manager_Interface
|
||||
*/
|
||||
interface Token_Manager_Interface {
|
||||
|
||||
/**
|
||||
* Invalidates the access token.
|
||||
*
|
||||
* @param string $user_id The user ID.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Bad_Request_Exception Bad_Request_Exception.
|
||||
* @throws Internal_Server_Error_Exception Internal_Server_Error_Exception.
|
||||
* @throws Not_Found_Exception Not_Found_Exception.
|
||||
* @throws Payment_Required_Exception Payment_Required_Exception.
|
||||
* @throws Request_Timeout_Exception Request_Timeout_Exception.
|
||||
* @throws Service_Unavailable_Exception Service_Unavailable_Exception.
|
||||
* @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
|
||||
* @throws RuntimeException Unable to retrieve the access token.
|
||||
*/
|
||||
public function token_invalidate( string $user_id ): void;
|
||||
|
||||
/**
|
||||
* Requests a new set of JWT tokens.
|
||||
*
|
||||
* Requests a new JWT access and refresh token for a user from the Yoast AI Service and stores it in the database
|
||||
* under usermeta. The storing of the token happens in a HTTP callback that is triggered by this request.
|
||||
*
|
||||
* @param WP_User $user The WP user.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Bad_Request_Exception Bad_Request_Exception.
|
||||
* @throws Forbidden_Exception Forbidden_Exception.
|
||||
* @throws Internal_Server_Error_Exception Internal_Server_Error_Exception.
|
||||
* @throws Not_Found_Exception Not_Found_Exception.
|
||||
* @throws Payment_Required_Exception Payment_Required_Exception.
|
||||
* @throws Request_Timeout_Exception Request_Timeout_Exception.
|
||||
* @throws Service_Unavailable_Exception Service_Unavailable_Exception.
|
||||
* @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
|
||||
* @throws Unauthorized_Exception Unauthorized_Exception.
|
||||
*/
|
||||
public function token_request( WP_User $user ): void;
|
||||
|
||||
/**
|
||||
* Refreshes the JWT access token.
|
||||
*
|
||||
* Refreshes a stored JWT access token for a user with the Yoast AI Service and stores it in the database under
|
||||
* usermeta. The storing of the token happens in a HTTP callback that is triggered by this request.
|
||||
*
|
||||
* @param WP_User $user The WP user.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Bad_Request_Exception Bad_Request_Exception.
|
||||
* @throws Forbidden_Exception Forbidden_Exception.
|
||||
* @throws Internal_Server_Error_Exception Internal_Server_Error_Exception.
|
||||
* @throws Not_Found_Exception Not_Found_Exception.
|
||||
* @throws Payment_Required_Exception Payment_Required_Exception.
|
||||
* @throws Request_Timeout_Exception Request_Timeout_Exception.
|
||||
* @throws Service_Unavailable_Exception Service_Unavailable_Exception.
|
||||
* @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
|
||||
* @throws Unauthorized_Exception Unauthorized_Exception.
|
||||
* @throws RuntimeException Unable to retrieve the refresh token.
|
||||
*/
|
||||
public function token_refresh( WP_User $user ): void;
|
||||
|
||||
/**
|
||||
* Checks whether the token has expired.
|
||||
*
|
||||
* @param string $jwt The JWT.
|
||||
*
|
||||
* @return bool Whether the token has expired.
|
||||
*/
|
||||
public function has_token_expired( string $jwt ): bool;
|
||||
|
||||
/**
|
||||
* Retrieves the access token.
|
||||
*
|
||||
* @param WP_User $user The WP user.
|
||||
*
|
||||
* @return string The access token.
|
||||
*
|
||||
* @throws Bad_Request_Exception Bad_Request_Exception.
|
||||
* @throws Forbidden_Exception Forbidden_Exception.
|
||||
* @throws Internal_Server_Error_Exception Internal_Server_Error_Exception.
|
||||
* @throws Not_Found_Exception Not_Found_Exception.
|
||||
* @throws Payment_Required_Exception Payment_Required_Exception.
|
||||
* @throws Request_Timeout_Exception Request_Timeout_Exception.
|
||||
* @throws Service_Unavailable_Exception Service_Unavailable_Exception.
|
||||
* @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
|
||||
* @throws Unauthorized_Exception Unauthorized_Exception.
|
||||
* @throws RuntimeException Unable to retrieve the access or refresh token.
|
||||
*/
|
||||
public function get_or_request_access_token( WP_User $user ): string;
|
||||
}
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Authorization\Application;
|
||||
|
||||
use RuntimeException;
|
||||
use WP_User;
|
||||
use WPSEO_Utils;
|
||||
use Yoast\WP\SEO\AI_Authorization\Infrastructure\Access_Token_User_Meta_Repository_Interface;
|
||||
use Yoast\WP\SEO\AI_Authorization\Infrastructure\Code_Verifier_User_Meta_Repository;
|
||||
use Yoast\WP\SEO\AI_Authorization\Infrastructure\Refresh_Token_User_Meta_Repository_Interface;
|
||||
use Yoast\WP\SEO\AI_Consent\Application\Consent_Handler;
|
||||
use Yoast\WP\SEO\AI_Generator\Infrastructure\WordPress_URLs;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Application\Request_Handler;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Bad_Request_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Forbidden_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Internal_Server_Error_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Not_Found_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Payment_Required_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Request_Timeout_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Service_Unavailable_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Too_Many_Requests_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Unauthorized_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Request;
|
||||
use Yoast\WP\SEO\Helpers\User_Helper;
|
||||
|
||||
/**
|
||||
* Class Token_Manager
|
||||
* Handles the management of JWT tokens used in the authorization process.
|
||||
*
|
||||
* @makePublic
|
||||
*/
|
||||
class Token_Manager implements Token_Manager_Interface {
|
||||
|
||||
/**
|
||||
* The access token repository.
|
||||
*
|
||||
* @var Access_Token_User_Meta_Repository_Interface
|
||||
*/
|
||||
private $access_token_repository;
|
||||
|
||||
/**
|
||||
* The code verifier service.
|
||||
*
|
||||
* @var Code_Verifier_Handler
|
||||
*/
|
||||
private $code_verifier;
|
||||
|
||||
/**
|
||||
* The consent handler.
|
||||
*
|
||||
* @var Consent_Handler
|
||||
*/
|
||||
private $consent_handler;
|
||||
|
||||
/**
|
||||
* The refresh token repository.
|
||||
*
|
||||
* @var Refresh_Token_User_Meta_Repository_Interface
|
||||
*/
|
||||
private $refresh_token_repository;
|
||||
|
||||
/**
|
||||
* The user helper.
|
||||
*
|
||||
* @var User_Helper
|
||||
*/
|
||||
private $user_helper;
|
||||
|
||||
/**
|
||||
* The code verifier repository.
|
||||
*
|
||||
* @var Code_Verifier_User_Meta_Repository
|
||||
*/
|
||||
private $code_verifier_repository;
|
||||
|
||||
/**
|
||||
* The URLs service.
|
||||
*
|
||||
* @var WordPress_URLs
|
||||
*/
|
||||
private $urls;
|
||||
|
||||
/**
|
||||
* The request handler.
|
||||
*
|
||||
* @var Request_Handler
|
||||
*/
|
||||
private $request_handler;
|
||||
|
||||
/**
|
||||
* Token_Manager constructor.
|
||||
*
|
||||
* @param Access_Token_User_Meta_Repository_Interface $access_token_repository The access token repository.
|
||||
* @param Code_Verifier_Handler $code_verifier The code verifier service.
|
||||
* @param Consent_Handler $consent_handler The consent handler.
|
||||
* @param Refresh_Token_User_Meta_Repository_Interface $refresh_token_repository The refresh token repository.
|
||||
* @param User_Helper $user_helper The user helper.
|
||||
* @param Request_Handler $request_handler The request handler.
|
||||
* @param Code_Verifier_User_Meta_Repository $code_verifier_repository The code verifier repository.
|
||||
* @param WordPress_URLs $urls The URLs service.
|
||||
*/
|
||||
public function __construct(
|
||||
Access_Token_User_Meta_Repository_Interface $access_token_repository,
|
||||
Code_Verifier_Handler $code_verifier,
|
||||
Consent_Handler $consent_handler,
|
||||
Refresh_Token_User_Meta_Repository_Interface $refresh_token_repository,
|
||||
User_Helper $user_helper,
|
||||
Request_Handler $request_handler,
|
||||
Code_Verifier_User_Meta_Repository $code_verifier_repository,
|
||||
WordPress_URLs $urls
|
||||
) {
|
||||
$this->access_token_repository = $access_token_repository;
|
||||
$this->code_verifier = $code_verifier;
|
||||
$this->consent_handler = $consent_handler;
|
||||
$this->refresh_token_repository = $refresh_token_repository;
|
||||
$this->user_helper = $user_helper;
|
||||
$this->request_handler = $request_handler;
|
||||
$this->code_verifier_repository = $code_verifier_repository;
|
||||
$this->urls = $urls;
|
||||
}
|
||||
|
||||
// phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods.
|
||||
|
||||
/**
|
||||
* Invalidates the access token.
|
||||
*
|
||||
* @param string $user_id The user ID.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Bad_Request_Exception Bad_Request_Exception.
|
||||
* @throws Internal_Server_Error_Exception Internal_Server_Error_Exception.
|
||||
* @throws Not_Found_Exception Not_Found_Exception.
|
||||
* @throws Payment_Required_Exception Payment_Required_Exception.
|
||||
* @throws Request_Timeout_Exception Request_Timeout_Exception.
|
||||
* @throws Service_Unavailable_Exception Service_Unavailable_Exception.
|
||||
* @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
|
||||
* @throws RuntimeException Unable to retrieve the access token.
|
||||
*/
|
||||
public function token_invalidate( string $user_id ): void {
|
||||
try {
|
||||
$access_jwt = $this->access_token_repository->get_token( $user_id );
|
||||
} catch ( RuntimeException $e ) {
|
||||
$access_jwt = '';
|
||||
}
|
||||
|
||||
$request_body = [
|
||||
'user_id' => (string) $user_id,
|
||||
];
|
||||
$request_headers = [
|
||||
'Authorization' => "Bearer $access_jwt",
|
||||
];
|
||||
|
||||
try {
|
||||
$this->request_handler->handle(
|
||||
new Request(
|
||||
'/token/invalidate',
|
||||
$request_body,
|
||||
$request_headers
|
||||
)
|
||||
);
|
||||
} catch ( Unauthorized_Exception | Forbidden_Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Reason: Ignored on purpose.
|
||||
// If the credentials in our request were already invalid, our job is done and we continue to remove the tokens client-side.
|
||||
}
|
||||
|
||||
// Delete the stored JWT tokens.
|
||||
$this->user_helper->delete_meta( $user_id, '_yoast_wpseo_ai_generator_access_jwt' );
|
||||
$this->user_helper->delete_meta( $user_id, '_yoast_wpseo_ai_generator_refresh_jwt' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a new set of JWT tokens.
|
||||
*
|
||||
* Requests a new JWT access and refresh token for a user from the Yoast AI Service and stores it in the database
|
||||
* under usermeta. The storing of the token happens in a HTTP callback that is triggered by this request.
|
||||
*
|
||||
* @param WP_User $user The WP user.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Bad_Request_Exception Bad_Request_Exception.
|
||||
* @throws Forbidden_Exception Forbidden_Exception.
|
||||
* @throws Internal_Server_Error_Exception Internal_Server_Error_Exception.
|
||||
* @throws Not_Found_Exception Not_Found_Exception.
|
||||
* @throws Payment_Required_Exception Payment_Required_Exception.
|
||||
* @throws Request_Timeout_Exception Request_Timeout_Exception.
|
||||
* @throws Service_Unavailable_Exception Service_Unavailable_Exception.
|
||||
* @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
|
||||
* @throws Unauthorized_Exception Unauthorized_Exception.
|
||||
*/
|
||||
public function token_request( WP_User $user ): void {
|
||||
// Ensure the user has given consent.
|
||||
if ( $this->user_helper->get_meta( $user->ID, '_yoast_wpseo_ai_consent', true ) !== '1' ) {
|
||||
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
|
||||
$this->consent_handler->revoke_consent( $user->ID );
|
||||
throw new Forbidden_Exception( 'CONSENT_REVOKED', 403 );
|
||||
|
||||
// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
|
||||
// Generate a code verifier and store it in the database.
|
||||
$code_verifier = $this->code_verifier->generate( $user->user_email );
|
||||
$this->code_verifier_repository->store_code_verifier( $user->ID, $code_verifier->get_code(), $code_verifier->get_created_at() );
|
||||
|
||||
$request_body = [
|
||||
'service' => 'openai',
|
||||
'code_challenge' => \hash( 'sha256', $code_verifier->get_code() ),
|
||||
'license_site_url' => WPSEO_Utils::get_home_url(),
|
||||
'user_id' => (string) $user->ID,
|
||||
'callback_url' => $this->urls->get_callback_url(),
|
||||
'refresh_callback_url' => $this->urls->get_refresh_callback_url(),
|
||||
];
|
||||
|
||||
$this->request_handler->handle( new Request( '/token/request', $request_body ) );
|
||||
|
||||
// The callback saves the metadata. Because that is in another session, we need to delete the current cache here. Or we may get the old token.
|
||||
\wp_cache_delete( $user->ID, 'user_meta' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the JWT access token.
|
||||
*
|
||||
* Refreshes a stored JWT access token for a user with the Yoast AI Service and stores it in the database under
|
||||
* usermeta. The storing of the token happens in a HTTP callback that is triggered by this request.
|
||||
*
|
||||
* @param WP_User $user The WP user.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Bad_Request_Exception Bad_Request_Exception.
|
||||
* @throws Forbidden_Exception Forbidden_Exception.
|
||||
* @throws Internal_Server_Error_Exception Internal_Server_Error_Exception.
|
||||
* @throws Not_Found_Exception Not_Found_Exception.
|
||||
* @throws Payment_Required_Exception Payment_Required_Exception.
|
||||
* @throws Request_Timeout_Exception Request_Timeout_Exception.
|
||||
* @throws Service_Unavailable_Exception Service_Unavailable_Exception.
|
||||
* @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
|
||||
* @throws Unauthorized_Exception Unauthorized_Exception.
|
||||
* @throws RuntimeException Unable to retrieve the refresh token.
|
||||
*/
|
||||
public function token_refresh( WP_User $user ): void {
|
||||
$refresh_jwt = $this->refresh_token_repository->get_token( $user->ID );
|
||||
|
||||
// Generate a code verifier and store it in the database.
|
||||
$code_verifier = $this->code_verifier->generate( $user->ID, $user->user_email );
|
||||
$this->code_verifier_repository->store_code_verifier( $user->ID, $code_verifier->get_code(), $code_verifier->get_created_at() );
|
||||
|
||||
$request_body = [
|
||||
'code_challenge' => \hash( 'sha256', $code_verifier->get_code() ),
|
||||
];
|
||||
$request_headers = [
|
||||
'Authorization' => "Bearer $refresh_jwt",
|
||||
];
|
||||
|
||||
$this->request_handler->handle( new Request( '/token/refresh', $request_body, $request_headers ) );
|
||||
|
||||
// The callback saves the metadata. Because that is in another session, we need to delete the current cache here. Or we may get the old token.
|
||||
\wp_cache_delete( $user->ID, 'user_meta' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the token has expired.
|
||||
*
|
||||
* @param string $jwt The JWT.
|
||||
*
|
||||
* @return bool Whether the token has expired.
|
||||
*/
|
||||
public function has_token_expired( string $jwt ): bool {
|
||||
$parts = \explode( '.', $jwt );
|
||||
if ( \count( $parts ) !== 3 ) {
|
||||
// Headers, payload and signature parts are not detected.
|
||||
return true;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Reason: Decoding the payload of the JWT.
|
||||
$payload = \base64_decode( $parts[1] );
|
||||
$json = \json_decode( $payload );
|
||||
if ( $json === null || ! isset( $json->exp ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ensure exp is a valid numeric value.
|
||||
if ( ! \is_numeric( $json->exp ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $json->exp < \time();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the access token.
|
||||
*
|
||||
* @param WP_User $user The WP user.
|
||||
*
|
||||
* @return string The access token.
|
||||
*
|
||||
* @throws Bad_Request_Exception Bad_Request_Exception.
|
||||
* @throws Forbidden_Exception Forbidden_Exception.
|
||||
* @throws Internal_Server_Error_Exception Internal_Server_Error_Exception.
|
||||
* @throws Not_Found_Exception Not_Found_Exception.
|
||||
* @throws Payment_Required_Exception Payment_Required_Exception.
|
||||
* @throws Request_Timeout_Exception Request_Timeout_Exception.
|
||||
* @throws Service_Unavailable_Exception Service_Unavailable_Exception.
|
||||
* @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
|
||||
* @throws Unauthorized_Exception Unauthorized_Exception.
|
||||
* @throws RuntimeException Unable to retrieve the access or refresh token.
|
||||
*/
|
||||
public function get_or_request_access_token( WP_User $user ): string {
|
||||
$access_jwt = $this->user_helper->get_meta( $user->ID, '_yoast_wpseo_ai_generator_access_jwt', true );
|
||||
if ( ! \is_string( $access_jwt ) || $access_jwt === '' ) {
|
||||
$this->token_request( $user );
|
||||
$access_jwt = $this->access_token_repository->get_token( $user->ID );
|
||||
}
|
||||
elseif ( $this->has_token_expired( $access_jwt ) ) {
|
||||
try {
|
||||
$this->token_refresh( $user );
|
||||
} catch ( Unauthorized_Exception $exception ) {
|
||||
$this->token_request( $user );
|
||||
} catch ( Forbidden_Exception $exception ) {
|
||||
// Follow the API in the consent being revoked (Use case: user sent an e-mail to revoke?).
|
||||
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
|
||||
$this->consent_handler->revoke_consent( $user->ID );
|
||||
throw new Forbidden_Exception( 'CONSENT_REVOKED', 403 );
|
||||
// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
$access_jwt = $this->access_token_repository->get_token( $user->ID );
|
||||
}
|
||||
|
||||
return $access_jwt;
|
||||
}
|
||||
|
||||
// phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Authorization\Domain;
|
||||
|
||||
/**
|
||||
* Class Code_Verifier representing a challenge code and its creation time.
|
||||
* This is used during the authorization process to verify the user requesting a token.
|
||||
*/
|
||||
class Code_Verifier {
|
||||
|
||||
/**
|
||||
* The code.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $code;
|
||||
|
||||
/**
|
||||
* The time the code was created.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $created_at;
|
||||
|
||||
/**
|
||||
* Code_Verifier constructor.
|
||||
*
|
||||
* @param string $code The code.
|
||||
* @param int $created_at The time the code was created.
|
||||
*/
|
||||
public function __construct( string $code, int $created_at ) {
|
||||
$this->code = $code;
|
||||
$this->created_at = $created_at;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the code.
|
||||
*
|
||||
* @return string The code.
|
||||
*/
|
||||
public function get_code(): string {
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the creation time of the code.
|
||||
*
|
||||
* @return int The creation time of the code.
|
||||
*/
|
||||
public function get_created_at(): int {
|
||||
return $this->created_at;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the code is expired.
|
||||
*
|
||||
* @param int $validity_in_seconds The validity of the code in seconds.
|
||||
*
|
||||
* @return bool True if the code is expired, false otherwise.
|
||||
*/
|
||||
public function is_expired( int $validity_in_seconds ): bool {
|
||||
return $this->created_at < ( \time() - $validity_in_seconds );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Authorization\Domain;
|
||||
|
||||
/**
|
||||
* Class Token
|
||||
* Represents a token used for authentication with the AI Generator API.
|
||||
*/
|
||||
class Token {
|
||||
|
||||
/**
|
||||
* The token value.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* The expiration time.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $expiration;
|
||||
|
||||
/**
|
||||
* Token constructor.
|
||||
*
|
||||
* @param string $value The token value.
|
||||
* @param int $expiration The expiration time.
|
||||
*/
|
||||
public function __construct( string $value, int $expiration ) {
|
||||
$this->value = $value;
|
||||
$this->expiration = $expiration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the token value.
|
||||
*
|
||||
* @return string The token value.
|
||||
*/
|
||||
public function get_value(): string {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the token is expired.
|
||||
*
|
||||
* @return bool True if the token is expired, false otherwise.
|
||||
*/
|
||||
public function is_expired(): bool {
|
||||
return $this->expiration < \time();
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Authorization\Infrastructure;
|
||||
|
||||
/**
|
||||
* Interface Access_Token_User_Meta_Repository_Interface
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
*/
|
||||
interface Access_Token_User_Meta_Repository_Interface extends Token_User_Meta_Repository_Interface {
|
||||
public const META_KEY = '_yoast_wpseo_ai_generator_access_jwt';
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Authorization\Infrastructure;
|
||||
|
||||
use RuntimeException;
|
||||
use Yoast\WP\SEO\Helpers\User_Helper;
|
||||
|
||||
/**
|
||||
* Class Access_Token_Repository
|
||||
* Handles the storage and retrieval of access tokens for users.
|
||||
*/
|
||||
class Access_Token_User_Meta_Repository implements Access_Token_User_Meta_Repository_Interface {
|
||||
|
||||
/**
|
||||
* The user helper.
|
||||
*
|
||||
* @var User_Helper
|
||||
*/
|
||||
private $user_helper;
|
||||
|
||||
/**
|
||||
* Access_Token_Repository constructor.
|
||||
*
|
||||
* @param User_Helper $user_helper The user helper.
|
||||
*/
|
||||
public function __construct( User_Helper $user_helper ) {
|
||||
$this->user_helper = $user_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the token for a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return string The token data.
|
||||
*
|
||||
* @throws RuntimeException If the token is not found or invalid.
|
||||
*/
|
||||
public function get_token( int $user_id ): string {
|
||||
$access_jwt = $this->user_helper->get_meta( $user_id, self::META_KEY, true );
|
||||
if ( ! \is_string( $access_jwt ) || $access_jwt === '' ) {
|
||||
throw new RuntimeException( 'Unable to retrieve the access token.' );
|
||||
}
|
||||
|
||||
return $access_jwt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the token for a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
* @param string $value The token value.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function store_token( int $user_id, string $value ): void {
|
||||
$this->user_helper->update_meta(
|
||||
$user_id,
|
||||
self::META_KEY,
|
||||
$value
|
||||
); }
|
||||
|
||||
/**
|
||||
* Delete the token for a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete_token( int $user_id ): void {
|
||||
$this->user_helper->delete_meta( $user_id, self::META_KEY );
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Authorization\Infrastructure;
|
||||
|
||||
use RuntimeException;
|
||||
use Yoast\WP\SEO\AI_Authorization\Domain\Code_Verifier;
|
||||
|
||||
// phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
/**
|
||||
* Interface for the Code Verifier User Meta Repository.
|
||||
*
|
||||
* This interface defines methods for managing code verifiers associated with users.
|
||||
*/
|
||||
interface Code_Verifier_User_Meta_Repository_Interface {
|
||||
|
||||
/**
|
||||
* Get the verification code for a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @throws RuntimeException If the code verifier is not found or has expired.
|
||||
* @return Code_Verifier The verification code or null if not found.
|
||||
*/
|
||||
public function get_code_verifier( int $user_id ): ?Code_Verifier;
|
||||
|
||||
/**
|
||||
* Store the verification code for a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
* @param string $code The code verifier.
|
||||
* @param int $created_at The time the code was created.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function store_code_verifier( int $user_id, string $code, int $created_at ): void;
|
||||
|
||||
/**
|
||||
* Delete the verification code for a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete_code_verifier( int $user_id ): void;
|
||||
}
|
||||
//phpcs:enable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Authorization\Infrastructure;
|
||||
|
||||
use RuntimeException;
|
||||
use Yoast\WP\SEO\AI_Authorization\Domain\Code_Verifier;
|
||||
use Yoast\WP\SEO\Helpers\Date_Helper;
|
||||
use Yoast\WP\SEO\Helpers\User_Helper;
|
||||
/**
|
||||
* Class Code_Verifier_Repository
|
||||
*/
|
||||
class Code_Verifier_User_Meta_Repository implements Code_Verifier_User_Meta_Repository_Interface {
|
||||
|
||||
private const CODE_VERIFIER_VALIDITY = 300; // 5 minutes
|
||||
|
||||
/**
|
||||
* The date helper.
|
||||
*
|
||||
* @var Date_Helper
|
||||
*/
|
||||
private $date_helper;
|
||||
|
||||
/**
|
||||
* The user helper.
|
||||
*
|
||||
* @var User_Helper
|
||||
*/
|
||||
private $user_helper;
|
||||
|
||||
/**
|
||||
* Code_Verifier_Repository constructor.
|
||||
*
|
||||
* @param Date_Helper $date_helper The date helper.
|
||||
* @param User_Helper $user_helper The user helper.
|
||||
*/
|
||||
public function __construct( Date_Helper $date_helper, User_Helper $user_helper ) {
|
||||
$this->date_helper = $date_helper;
|
||||
$this->user_helper = $user_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the verification code for a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
* @param string $code The code verifier.
|
||||
* @param int $created_at The time the code was created.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function store_code_verifier( int $user_id, string $code, int $created_at ): void {
|
||||
$this->user_helper->update_meta(
|
||||
$user_id,
|
||||
'yoast_wpseo_ai_generator_code_verifier_for_blog_' . \get_current_blog_id(),
|
||||
[
|
||||
'code' => $code,
|
||||
'created_at' => $created_at,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the verification code for a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @throws RuntimeException If the code verifier is not found or has expired.
|
||||
* @return Code_Verifier The verification code or null if not found.
|
||||
*/
|
||||
public function get_code_verifier( int $user_id ): ?Code_Verifier {
|
||||
$data = $this->user_helper->get_meta( $user_id, 'yoast_wpseo_ai_generator_code_verifier_for_blog_' . \get_current_blog_id(), true );
|
||||
|
||||
if ( ! \is_array( $data ) || ! isset( $data['code'] ) || $data['code'] === '' ) {
|
||||
throw new RuntimeException( 'Unable to retrieve the verification code.' );
|
||||
}
|
||||
|
||||
if ( ! isset( $data['created_at'] ) || $data['created_at'] < ( $this->date_helper->current_time() - self::CODE_VERIFIER_VALIDITY ) ) {
|
||||
$this->delete_code_verifier( $user_id );
|
||||
throw new RuntimeException( 'Code verifier has expired.' );
|
||||
}
|
||||
|
||||
return new Code_Verifier( $data['code'], $data['created_at'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the verification code for a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete_code_verifier( int $user_id ): void {
|
||||
$this->user_helper->delete_meta( $user_id, 'yoast_wpseo_ai_generator_code_verifier_for_blog_' . \get_current_blog_id() );
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Authorization\Infrastructure;
|
||||
|
||||
/**
|
||||
* Interface Refresh_Token_User_Meta_Repository_Interface
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
*/
|
||||
interface Refresh_Token_User_Meta_Repository_Interface extends Token_User_Meta_Repository_Interface {
|
||||
public const META_KEY = '_yoast_wpseo_ai_generator_refresh_jwt';
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Authorization\Infrastructure;
|
||||
|
||||
use RuntimeException;
|
||||
use Yoast\WP\SEO\Helpers\User_Helper;
|
||||
|
||||
/**
|
||||
* Class Refresh_Token_Repository
|
||||
* Handles the storage and retrieval of refresh tokens for users.
|
||||
*/
|
||||
class Refresh_Token_User_Meta_Repository implements Refresh_Token_User_Meta_Repository_Interface {
|
||||
|
||||
/**
|
||||
* The user helper.
|
||||
*
|
||||
* @var User_Helper
|
||||
*/
|
||||
private $user_helper;
|
||||
|
||||
/**
|
||||
* Refresh_Token_Repository constructor.
|
||||
*
|
||||
* @param User_Helper $user_helper The user helper.
|
||||
*/
|
||||
public function __construct( User_Helper $user_helper ) {
|
||||
$this->user_helper = $user_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the token for a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return string The token data.
|
||||
*
|
||||
* @throws RuntimeException If the token is not found or invalid.
|
||||
*/
|
||||
public function get_token( int $user_id ): string {
|
||||
$refresh_jwt = $this->user_helper->get_meta( $user_id, self::META_KEY, true );
|
||||
if ( ! \is_string( $refresh_jwt ) || $refresh_jwt === '' ) {
|
||||
throw new RuntimeException( 'Unable to retrieve the refresh token.' );
|
||||
}
|
||||
|
||||
return $refresh_jwt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the token for a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
* @param string $value The token value.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function store_token( int $user_id, string $value ): void {
|
||||
$this->user_helper->update_meta(
|
||||
$user_id,
|
||||
self::META_KEY,
|
||||
$value
|
||||
); }
|
||||
|
||||
/**
|
||||
* Delete the token for a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete_token( int $user_id ): void {
|
||||
$this->user_helper->delete_meta( $user_id, self::META_KEY );
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Authorization\Infrastructure;
|
||||
|
||||
/**
|
||||
* Interface Token_Repository_Interface
|
||||
*/
|
||||
interface Token_User_Meta_Repository_Interface {
|
||||
|
||||
/**
|
||||
* Get the token for a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return string The token data.
|
||||
*/
|
||||
public function get_token( int $user_id ): string;
|
||||
|
||||
/**
|
||||
* Store the token for a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
* @param string $value The token value.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function store_token( int $user_id, string $value ): void;
|
||||
|
||||
/**
|
||||
* Delete the token for a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete_token( int $user_id ): void;
|
||||
}
|
||||
Executable
+108
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Authorization\User_Interface;
|
||||
|
||||
use RuntimeException;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use Yoast\WP\SEO\AI_Authorization\Infrastructure\Access_Token_User_Meta_Repository_Interface;
|
||||
use Yoast\WP\SEO\AI_Authorization\Infrastructure\Code_Verifier_User_Meta_Repository_Interface;
|
||||
use Yoast\WP\SEO\AI_Authorization\Infrastructure\Refresh_Token_User_Meta_Repository_Interface;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Unauthorized_Exception;
|
||||
use Yoast\WP\SEO\Conditionals\AI_Conditional;
|
||||
use Yoast\WP\SEO\Main;
|
||||
use Yoast\WP\SEO\Routes\Route_Interface;
|
||||
|
||||
/**
|
||||
* The base class for the callback routes.
|
||||
*/
|
||||
abstract class Abstract_Callback_Route implements Route_Interface {
|
||||
|
||||
/**
|
||||
* The namespace for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
|
||||
|
||||
/**
|
||||
* The access token repository instance.
|
||||
*
|
||||
* @var Access_Token_User_Meta_Repository_Interface
|
||||
*/
|
||||
protected $access_token_repository;
|
||||
|
||||
/**
|
||||
* The refresh token repository instance.
|
||||
*
|
||||
* @var Refresh_Token_User_Meta_Repository_Interface
|
||||
*/
|
||||
protected $refresh_token_repository;
|
||||
|
||||
/**
|
||||
* The code verifier instance.
|
||||
*
|
||||
* @var Code_Verifier_User_Meta_Repository_Interface
|
||||
*/
|
||||
protected $code_verifier_repository;
|
||||
|
||||
/**
|
||||
* Returns the conditionals based in which this loadable should be active.
|
||||
*
|
||||
* @return array<string> The conditionals.
|
||||
*/
|
||||
public static function get_conditionals() {
|
||||
return [ AI_Conditional::class ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback_Route constructor.
|
||||
*
|
||||
* @param Access_Token_User_Meta_Repository_Interface $access_token_repository The access token repository instance.
|
||||
* @param Refresh_Token_User_Meta_Repository_Interface $refresh_token_repository The refresh token repository instance.
|
||||
* @param Code_Verifier_User_Meta_Repository_Interface $code_verifier_repository The code verifier instance.
|
||||
*/
|
||||
public function __construct( Access_Token_User_Meta_Repository_Interface $access_token_repository, Refresh_Token_User_Meta_Repository_Interface $refresh_token_repository, Code_Verifier_User_Meta_Repository_Interface $code_verifier_repository ) {
|
||||
$this->access_token_repository = $access_token_repository;
|
||||
$this->refresh_token_repository = $refresh_token_repository;
|
||||
$this->code_verifier_repository = $code_verifier_repository;
|
||||
}
|
||||
|
||||
// phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods.
|
||||
|
||||
/**
|
||||
* Runs the callback to store connection credentials and the tokens locally.
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response The response of the callback action.
|
||||
*
|
||||
* @throws Unauthorized_Exception If the code challenge is not valid.
|
||||
* @throws RuntimeException If the verification code is not found.
|
||||
*/
|
||||
public function callback( WP_REST_Request $request ): WP_REST_Response {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
try {
|
||||
$code_verifier = $this->code_verifier_repository->get_code_verifier( $user_id );
|
||||
|
||||
if ( $request->get_param( 'code_challenge' ) !== \hash( 'sha256', $code_verifier->get_code() ) ) {
|
||||
throw new Unauthorized_Exception( 'Unauthorized' );
|
||||
}
|
||||
|
||||
$this->access_token_repository->store_token( $user_id, $request->get_param( 'access_jwt' ) );
|
||||
$this->refresh_token_repository->store_token( $user_id, $request->get_param( 'refresh_jwt' ) );
|
||||
$this->code_verifier_repository->delete_code_verifier( $user_id );
|
||||
} catch ( Unauthorized_Exception | RuntimeException $e ) {
|
||||
return new WP_REST_Response( 'Unauthorized.', 401 );
|
||||
}
|
||||
|
||||
return new WP_REST_Response(
|
||||
[
|
||||
'message' => 'Tokens successfully stored.',
|
||||
'code_verifier' => $code_verifier->get_code(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods.
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Authorization\User_Interface;
|
||||
|
||||
/**
|
||||
* Registers the callback route used in the authorization process.
|
||||
*
|
||||
* @makePublic
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
*/
|
||||
class Callback_Route extends Abstract_Callback_Route {
|
||||
/**
|
||||
* The prefix for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_PREFIX = '/ai_generator/callback';
|
||||
|
||||
/**
|
||||
* Registers routes with WordPress.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_routes() {
|
||||
\register_rest_route(
|
||||
parent::ROUTE_NAMESPACE,
|
||||
self::ROUTE_PREFIX,
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'args' => [
|
||||
'access_jwt' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'description' => 'The access JWT.',
|
||||
],
|
||||
'refresh_jwt' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'description' => 'The JWT to be used when the access JWT needs to be refreshed.',
|
||||
],
|
||||
'code_challenge' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'description' => 'The SHA266 of the verification code used to check the authenticity of a callback call.',
|
||||
],
|
||||
'user_id' => [
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
'description' => 'The id of the user associated to the code verifier.',
|
||||
],
|
||||
],
|
||||
'callback' => [ $this, 'callback' ],
|
||||
'permission_callback' => '__return_true',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Authorization\User_Interface;
|
||||
|
||||
/**
|
||||
* Registers the callback route used in the authorization process.
|
||||
*
|
||||
* @makePublic
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
*/
|
||||
class Refresh_Callback_Route extends Abstract_Callback_Route {
|
||||
/**
|
||||
* The prefix for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_PREFIX = '/ai_generator/refresh_callback';
|
||||
|
||||
/**
|
||||
* Registers routes with WordPress.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_routes() {
|
||||
\register_rest_route(
|
||||
parent::ROUTE_NAMESPACE,
|
||||
self::ROUTE_PREFIX,
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'args' => [
|
||||
'access_jwt' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'description' => 'The access JWT.',
|
||||
],
|
||||
'refresh_jwt' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'description' => 'The JWT to be used when the access JWT needs to be refreshed.',
|
||||
],
|
||||
'code_challenge' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'description' => 'The SHA266 of the verification code used to check the authenticity of a callback call.',
|
||||
],
|
||||
'user_id' => [
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
'description' => 'The id of the user associated to the code verifier.',
|
||||
],
|
||||
],
|
||||
'callback' => [ $this, 'callback' ],
|
||||
'permission_callback' => '__return_true',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Consent\Application;
|
||||
|
||||
/**
|
||||
* Interface Consent_Handler_Interface
|
||||
*
|
||||
* This interface defines the methods for handling user consent.
|
||||
*/
|
||||
interface Consent_Handler_Interface {
|
||||
|
||||
/**
|
||||
* Handles consent revoked by deleting the consent user metadata from the database.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function revoke_consent( int $user_id );
|
||||
|
||||
/**
|
||||
* Handles consent granted by adding the consent user metadata to the database.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function grant_consent( int $user_id );
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Consent\Application;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\User_Helper;
|
||||
|
||||
/**
|
||||
* Class Consent_Handler
|
||||
* Handles the consent given or revoked by the user.
|
||||
*
|
||||
* @makePublic
|
||||
*/
|
||||
class Consent_Handler implements Consent_Handler_Interface {
|
||||
|
||||
/**
|
||||
* Holds the user helper instance.
|
||||
*
|
||||
* @var User_Helper
|
||||
*/
|
||||
private $user_helper;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param User_Helper $user_helper The user helper.
|
||||
*/
|
||||
public function __construct( User_Helper $user_helper ) {
|
||||
$this->user_helper = $user_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles consent revoked by deleting the consent user metadata from the database.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function revoke_consent( int $user_id ) {
|
||||
$this->user_helper->delete_meta( $user_id, '_yoast_wpseo_ai_consent' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles consent granted by adding the consent user metadata to the database.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function grant_consent( int $user_id ) {
|
||||
$this->user_helper->update_meta( $user_id, '_yoast_wpseo_ai_consent', true );
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Consent\Domain\Endpoint;
|
||||
|
||||
interface Endpoint_Interface {
|
||||
|
||||
/**
|
||||
* Gets the name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string;
|
||||
|
||||
/**
|
||||
* Gets the namespace.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_namespace(): string;
|
||||
|
||||
/**
|
||||
* Gets the route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_route(): string;
|
||||
|
||||
/**
|
||||
* Gets the URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_url(): string;
|
||||
}
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Consent\Infrastructure\Endpoints;
|
||||
|
||||
use Exception;
|
||||
use Yoast\WP\SEO\AI_Consent\Domain\Endpoint\Endpoint_Interface;
|
||||
use Yoast\WP\SEO\AI_Consent\User_Interface\Consent_Route;
|
||||
|
||||
/**
|
||||
* Represents the setup steps tracking endpoint.
|
||||
*/
|
||||
class Consent_Endpoint implements Endpoint_Interface {
|
||||
|
||||
/**
|
||||
* Gets the name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return 'consent';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the namespace.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_namespace(): string {
|
||||
return Consent_Route::ROUTE_NAMESPACE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the route.
|
||||
*
|
||||
* @throws Exception If the route prefix is not overwritten this throws.
|
||||
* @return string
|
||||
*/
|
||||
public function get_route(): string {
|
||||
return Consent_Route::ROUTE_PREFIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_url(): string {
|
||||
return \rest_url( $this->get_namespace() . $this->get_route() );
|
||||
}
|
||||
}
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Consent\User_Interface;
|
||||
|
||||
use WPSEO_Admin_Asset_Manager;
|
||||
use Yoast\WP\SEO\Conditionals\User_Profile_Conditional;
|
||||
use Yoast\WP\SEO\Helpers\Short_Link_Helper;
|
||||
use Yoast\WP\SEO\Helpers\User_Helper;
|
||||
use Yoast\WP\SEO\Integrations\Integration_Interface;
|
||||
|
||||
/**
|
||||
* Ai_Consent_Integration class.
|
||||
*/
|
||||
class Ai_Consent_Integration implements Integration_Interface {
|
||||
|
||||
/**
|
||||
* Represents the admin asset manager.
|
||||
*
|
||||
* @var WPSEO_Admin_Asset_Manager
|
||||
*/
|
||||
private $asset_manager;
|
||||
|
||||
/**
|
||||
* Represents the user helper.
|
||||
*
|
||||
* @var User_Helper
|
||||
*/
|
||||
private $user_helper;
|
||||
|
||||
/**
|
||||
* The short link helper.
|
||||
*
|
||||
* @var Short_Link_Helper
|
||||
*/
|
||||
protected $short_link_helper;
|
||||
|
||||
/**
|
||||
* Returns the conditionals based in which this loadable should be active.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function get_conditionals(): array {
|
||||
return [ User_Profile_Conditional::class ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the class.
|
||||
*
|
||||
* @param WPSEO_Admin_Asset_Manager $asset_manager The admin asset manager.
|
||||
* @param User_Helper $user_helper The user helper.
|
||||
* @param Short_Link_Helper $short_link_helper The short link helper.
|
||||
*/
|
||||
public function __construct(
|
||||
WPSEO_Admin_Asset_Manager $asset_manager,
|
||||
User_Helper $user_helper,
|
||||
Short_Link_Helper $short_link_helper
|
||||
) {
|
||||
$this->asset_manager = $asset_manager;
|
||||
$this->user_helper = $user_helper;
|
||||
$this->short_link_helper = $short_link_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the integration.
|
||||
*
|
||||
* This is the place to register hooks and filters.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_hooks() {
|
||||
// Hide AI feature option in user profile if the user is not allowed to use it.
|
||||
if ( \current_user_can( 'edit_posts' ) ) {
|
||||
\add_action( 'wpseo_user_profile_additions', [ $this, 'render_user_profile' ], 12 );
|
||||
}
|
||||
\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ], 11 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the script data for the AI consent button.
|
||||
*
|
||||
* @return array<string, string|bool>
|
||||
*/
|
||||
public function get_script_data(): array {
|
||||
return [
|
||||
'hasConsent' => $this->user_helper->get_meta( $this->user_helper->get_current_user_id(), '_yoast_wpseo_ai_consent', true ),
|
||||
'pluginUrl' => \plugins_url( '', \WPSEO_FILE ),
|
||||
'linkParams' => $this->short_link_helper->get_query_params(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues the required assets.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_assets() {
|
||||
$this->asset_manager->enqueue_style( 'ai-generator' );
|
||||
$this->asset_manager->localize_script( 'ai-consent', 'wpseoAiConsent', $this->get_script_data() );
|
||||
$this->asset_manager->enqueue_script( 'ai-consent' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the AI consent button for the user profile.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render_user_profile() {
|
||||
echo '<label for="ai-generator-consent-button">',
|
||||
\esc_html__( 'AI features', 'wordpress-seo' ),
|
||||
'</label>',
|
||||
'<div id="ai-generator-consent" style="display:inline-block; margin-top: 28px; padding-left:5px;"></div>';
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Consent\User_Interface;
|
||||
|
||||
use RuntimeException;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use Yoast\WP\SEO\AI_Authorization\Application\Token_Manager;
|
||||
use Yoast\WP\SEO\AI_Consent\Application\Consent_Handler;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Bad_Request_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Forbidden_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Internal_Server_Error_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Not_Found_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Payment_Required_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Request_Timeout_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Service_Unavailable_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Too_Many_Requests_Exception;
|
||||
use Yoast\WP\SEO\Conditionals\AI_Conditional;
|
||||
use Yoast\WP\SEO\Main;
|
||||
use Yoast\WP\SEO\Routes\Route_Interface;
|
||||
|
||||
/**
|
||||
* Registers a route toget suggestions from the AI API
|
||||
*
|
||||
* @makePublic
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
*/
|
||||
class Consent_Route implements Route_Interface {
|
||||
/**
|
||||
* The namespace for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
|
||||
|
||||
/**
|
||||
* The prefix for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_PREFIX = '/ai_generator/consent';
|
||||
|
||||
/**
|
||||
* The consent handler instance.
|
||||
*
|
||||
* @var Consent_Handler
|
||||
*/
|
||||
private $consent_handler;
|
||||
|
||||
/**
|
||||
* The token manager instance.
|
||||
*
|
||||
* @var Token_Manager
|
||||
*/
|
||||
private $token_manager;
|
||||
|
||||
/**
|
||||
* Returns the conditionals based in which this loadable should be active.
|
||||
*
|
||||
* @return array<string> The conditionals.
|
||||
*/
|
||||
public static function get_conditionals() {
|
||||
return [ AI_Conditional::class ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param Consent_Handler $consent_handler The consent handler.
|
||||
* @param Token_Manager $token_manager The token manager.
|
||||
*/
|
||||
public function __construct( Consent_Handler $consent_handler, Token_Manager $token_manager ) {
|
||||
$this->consent_handler = $consent_handler;
|
||||
$this->token_manager = $token_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers routes with WordPress.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_routes() {
|
||||
\register_rest_route(
|
||||
self::ROUTE_NAMESPACE,
|
||||
self::ROUTE_PREFIX,
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'args' => [
|
||||
'consent' => [
|
||||
'required' => true,
|
||||
'type' => 'boolean',
|
||||
'description' => 'Whether the consent to use AI-based services has been given by the user.',
|
||||
],
|
||||
],
|
||||
'callback' => [ $this, 'consent' ],
|
||||
'permission_callback' => [ $this, 'check_permissions' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the callback to store the consent given by the user to use AI-based services.
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response The response of the callback action.
|
||||
*/
|
||||
public function consent( WP_REST_Request $request ): WP_REST_Response {
|
||||
$user_id = \get_current_user_id();
|
||||
$consent = \boolval( $request->get_param( 'consent' ) );
|
||||
|
||||
try {
|
||||
if ( $consent ) {
|
||||
// Store the consent at user level.
|
||||
$this->consent_handler->grant_consent( $user_id );
|
||||
}
|
||||
else {
|
||||
// Delete the consent at user level.
|
||||
$this->consent_handler->revoke_consent( $user_id );
|
||||
// Invalidate the token if the user revoked the consent.
|
||||
$this->token_manager->token_invalidate( $user_id );
|
||||
}
|
||||
} catch ( Bad_Request_Exception | Forbidden_Exception | Internal_Server_Error_Exception | Not_Found_Exception | Payment_Required_Exception | Request_Timeout_Exception | Service_Unavailable_Exception | Too_Many_Requests_Exception | RuntimeException $e ) {
|
||||
return new WP_REST_Response( ( $consent ) ? 'Failed to store consent.' : 'Failed to revoke consent.', 500 );
|
||||
}
|
||||
|
||||
return new WP_REST_Response( ( $consent ) ? 'Consent successfully stored.' : 'Consent successfully revoked.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks:
|
||||
* - if the user is logged
|
||||
* - if the user can edit posts
|
||||
*
|
||||
* @return bool Whether the user is logged in, can edit posts and the feature is active.
|
||||
*/
|
||||
public function check_permissions(): bool {
|
||||
$user = \wp_get_current_user();
|
||||
if ( $user === null || $user->ID < 1 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return \user_can( $user, 'edit_posts' );
|
||||
}
|
||||
}
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Free_Sparks\Application;
|
||||
|
||||
/**
|
||||
* Interface Consent_Handler_Interface
|
||||
*
|
||||
* This interface defines the methods for handling user consent.
|
||||
*/
|
||||
interface Free_Sparks_Handler_Interface {
|
||||
|
||||
/**
|
||||
* Retrieves the timestamp.
|
||||
*
|
||||
* @param string $format The format in which to return the timestamp. Defaults to 'Y-m-d H:i:s'.
|
||||
*
|
||||
* @return ?string The timestamp when the user started using free sparks, or null if not set.
|
||||
*/
|
||||
public function get( string $format = 'Y-m-d H:i:s' ): ?string;
|
||||
|
||||
/**
|
||||
* Registers the starting of the free sparks.
|
||||
*
|
||||
* @param ?int $timestamp The timestamp when the user started using free sparks. If null, the current time will be
|
||||
* used.
|
||||
*
|
||||
* @return bool True if the operation was successful, false otherwise.
|
||||
*/
|
||||
public function start( ?int $timestamp = null ): bool;
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Free_Sparks\Application;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
|
||||
/**
|
||||
* Handles the free sparks started on timestamp.
|
||||
*/
|
||||
class Free_Sparks_Handler implements Free_Sparks_Handler_Interface {
|
||||
|
||||
/**
|
||||
* The key used to store the timestamp when the user started using free sparks.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const OPTION_KEY = 'ai_free_sparks_started_on';
|
||||
|
||||
/**
|
||||
* Holds the options helper instance.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
private $options_helper;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param Options_Helper $options_helper The options helper.
|
||||
*/
|
||||
public function __construct( Options_Helper $options_helper ) {
|
||||
$this->options_helper = $options_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the timestamp.
|
||||
*
|
||||
* @param string $format The format in which to return the timestamp. Defaults to 'Y-m-d H:i:s'.
|
||||
*
|
||||
* @return ?string The timestamp when the user started using free sparks, or null if not set.
|
||||
*/
|
||||
public function get( string $format = 'Y-m-d H:i:s' ): ?string {
|
||||
$timestamp = $this->options_helper->get( self::OPTION_KEY, null );
|
||||
if ( $timestamp === null ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return \gmdate( $format, (int) $timestamp );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the starting of the free sparks.
|
||||
*
|
||||
* @param ?int $timestamp The timestamp when the user started using free sparks. If null, the current time will be
|
||||
* used.
|
||||
*
|
||||
* @return bool True if the operation was successful, false otherwise.
|
||||
*/
|
||||
public function start( ?int $timestamp = null ): bool {
|
||||
return (bool) $this->options_helper->set( self::OPTION_KEY, ( $timestamp === null ) ? \time() : $timestamp, 'wpseo' );
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Free_Sparks\Infrastructure\Endpoints;
|
||||
|
||||
use Yoast\WP\SEO\AI_Free_Sparks\User_Interface\Free_Sparks_Route;
|
||||
use Yoast\WP\SEO\Routes\Endpoint_Interface;
|
||||
|
||||
/**
|
||||
* Represents the free sparks endpoint.
|
||||
*/
|
||||
class Free_Sparks_Endpoint implements Endpoint_Interface {
|
||||
|
||||
/**
|
||||
* Gets the name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return 'free_sparks';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the namespace.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_namespace(): string {
|
||||
return Free_Sparks_Route::ROUTE_NAMESPACE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_route(): string {
|
||||
return Free_Sparks_Route::ROUTE_PREFIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_url(): string {
|
||||
return \rest_url( $this->get_namespace() . $this->get_route() );
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Free_Sparks\User_Interface;
|
||||
|
||||
use WP_REST_Response;
|
||||
use Yoast\WP\SEO\AI_Free_Sparks\Application\Free_Sparks_Handler_Interface;
|
||||
use Yoast\WP\SEO\Conditionals\AI_Conditional;
|
||||
use Yoast\WP\SEO\Main;
|
||||
use Yoast\WP\SEO\Routes\Route_Interface;
|
||||
|
||||
/**
|
||||
* Registers a route to start free sparks.
|
||||
*/
|
||||
class Free_Sparks_Route implements Route_Interface {
|
||||
|
||||
/**
|
||||
* The namespace for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
|
||||
|
||||
/**
|
||||
* The prefix for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_PREFIX = '/ai/free_sparks';
|
||||
|
||||
/**
|
||||
* The free sparks handler instance.
|
||||
*
|
||||
* @var Free_Sparks_Handler_Interface
|
||||
*/
|
||||
private $free_sparks_handler;
|
||||
|
||||
/**
|
||||
* Returns the conditionals based in which this loadable should be active.
|
||||
*
|
||||
* @return array<string> The conditionals.
|
||||
*/
|
||||
public static function get_conditionals() {
|
||||
return [ AI_Conditional::class ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param Free_Sparks_Handler_Interface $free_sparks_handler The free sparks handler instance.
|
||||
*/
|
||||
public function __construct( Free_Sparks_Handler_Interface $free_sparks_handler ) {
|
||||
$this->free_sparks_handler = $free_sparks_handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers routes with WordPress.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_routes() {
|
||||
\register_rest_route(
|
||||
self::ROUTE_NAMESPACE,
|
||||
self::ROUTE_PREFIX,
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'callback' => [ $this, 'start' ],
|
||||
'permission_callback' => [ $this, 'can_edit_posts' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the callback to start the free sparks.
|
||||
*
|
||||
* @return WP_REST_Response The response of the callback action.
|
||||
*/
|
||||
public function start(): WP_REST_Response {
|
||||
$result = $this->free_sparks_handler->start( null );
|
||||
if ( ! $result ) {
|
||||
new WP_REST_Response( 'Failed to start free sparks.', 500 );
|
||||
}
|
||||
|
||||
return new WP_REST_Response( 'Free sparks successfully started.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the user is logged in and can edit posts.
|
||||
*
|
||||
* @return bool Whether the user is logged in and can edit posts.
|
||||
*/
|
||||
public function can_edit_posts(): bool {
|
||||
$user = \wp_get_current_user();
|
||||
if ( $user === null || $user->ID < 1 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return \user_can( $user, 'edit_posts' );
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Generator\Application;
|
||||
|
||||
use RuntimeException;
|
||||
use WP_User;
|
||||
use Yoast\WP\SEO\AI_Authorization\Application\Token_Manager;
|
||||
use Yoast\WP\SEO\AI_Consent\Application\Consent_Handler;
|
||||
use Yoast\WP\SEO\AI_Generator\Domain\Suggestion;
|
||||
use Yoast\WP\SEO\AI_Generator\Domain\Suggestions_Bucket;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Application\Request_Handler;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Bad_Request_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Forbidden_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Internal_Server_Error_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Not_Found_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Payment_Required_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Request_Timeout_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Service_Unavailable_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Too_Many_Requests_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Unauthorized_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Request;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Response;
|
||||
use Yoast\WP\SEO\Helpers\User_Helper;
|
||||
|
||||
/**
|
||||
* The class that handles the suggestions from the AI API.
|
||||
*/
|
||||
class Suggestions_Provider {
|
||||
|
||||
/**
|
||||
* The consent handler instance.
|
||||
*
|
||||
* @var Consent_Handler
|
||||
*/
|
||||
private $consent_handler;
|
||||
|
||||
/**
|
||||
* The request handler instance.
|
||||
*
|
||||
* @var Request_Handler
|
||||
*/
|
||||
private $request_handler;
|
||||
|
||||
/**
|
||||
* The token manager instance.
|
||||
*
|
||||
* @var Token_Manager
|
||||
*/
|
||||
private $token_manager;
|
||||
|
||||
/**
|
||||
* The user helper instance.
|
||||
*
|
||||
* @var User_Helper
|
||||
*/
|
||||
private $user_helper;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param Consent_Handler $consent_handler The consent handler instance.
|
||||
* @param Request_Handler $request_handler The request handler instance.
|
||||
* @param Token_Manager $token_manager The token manager instance.
|
||||
* @param User_Helper $user_helper The user helper instance.
|
||||
*/
|
||||
public function __construct(
|
||||
Consent_Handler $consent_handler,
|
||||
Request_Handler $request_handler,
|
||||
Token_Manager $token_manager,
|
||||
User_Helper $user_helper
|
||||
) {
|
||||
$this->consent_handler = $consent_handler;
|
||||
$this->request_handler = $request_handler;
|
||||
$this->token_manager = $token_manager;
|
||||
$this->user_helper = $user_helper;
|
||||
}
|
||||
|
||||
// phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods.
|
||||
|
||||
/**
|
||||
* Method used to generate suggestions through AI.
|
||||
*
|
||||
* @param WP_User $user The WP user.
|
||||
* @param string $suggestion_type The type of the requested suggestion.
|
||||
* @param string $prompt_content The excerpt taken from the post.
|
||||
* @param string $focus_keyphrase The focus keyphrase associated to the post.
|
||||
* @param string $language The language of the post.
|
||||
* @param string $platform The platform the post is intended for.
|
||||
* @param string $editor The current editor.
|
||||
* @param bool $retry_on_unauthorized Whether to retry when unauthorized (mechanism to retry once).
|
||||
*
|
||||
* @throws Bad_Request_Exception Bad_Request_Exception.
|
||||
* @throws Forbidden_Exception Forbidden_Exception.
|
||||
* @throws Internal_Server_Error_Exception Internal_Server_Error_Exception.
|
||||
* @throws Not_Found_Exception Not_Found_Exception.
|
||||
* @throws Payment_Required_Exception Payment_Required_Exception.
|
||||
* @throws Request_Timeout_Exception Request_Timeout_Exception.
|
||||
* @throws Service_Unavailable_Exception Service_Unavailable_Exception.
|
||||
* @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
|
||||
* @throws Unauthorized_Exception Unauthorized_Exception.
|
||||
* @throws RuntimeException Unable to retrieve the access token.
|
||||
* @return string[] The suggestions.
|
||||
*/
|
||||
public function get_suggestions(
|
||||
WP_User $user,
|
||||
string $suggestion_type,
|
||||
string $prompt_content,
|
||||
string $focus_keyphrase,
|
||||
string $language,
|
||||
string $platform,
|
||||
string $editor,
|
||||
bool $retry_on_unauthorized = true
|
||||
): array {
|
||||
$token = $this->token_manager->get_or_request_access_token( $user );
|
||||
|
||||
$request_body = [
|
||||
'service' => 'openai',
|
||||
'user_id' => (string) $user->ID,
|
||||
'subject' => [
|
||||
'content' => $prompt_content,
|
||||
'focus_keyphrase' => $focus_keyphrase,
|
||||
'language' => $language,
|
||||
'platform' => $platform,
|
||||
],
|
||||
];
|
||||
$request_headers = [
|
||||
'Authorization' => "Bearer $token",
|
||||
'X-Yst-Cohort' => $editor,
|
||||
];
|
||||
|
||||
try {
|
||||
$response = $this->request_handler->handle( new Request( "/openai/suggestions/$suggestion_type", $request_body, $request_headers ) );
|
||||
} catch ( Unauthorized_Exception $exception ) {
|
||||
// Delete the stored JWT tokens, as they appear to be no longer valid.
|
||||
$this->user_helper->delete_meta( $user->ID, '_yoast_wpseo_ai_generator_access_jwt' );
|
||||
$this->user_helper->delete_meta( $user->ID, '_yoast_wpseo_ai_generator_refresh_jwt' );
|
||||
|
||||
if ( ! $retry_on_unauthorized ) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
// Try again once more by fetching a new set of tokens and trying the suggestions endpoint again.
|
||||
return $this->get_suggestions( $user, $suggestion_type, $prompt_content, $focus_keyphrase, $language, $platform, $editor, false );
|
||||
} catch ( Forbidden_Exception $exception ) {
|
||||
// Follow the API in the consent being revoked (Use case: user sent an e-mail to revoke?).
|
||||
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
|
||||
$this->consent_handler->revoke_consent( $user->ID );
|
||||
throw new Forbidden_Exception( 'CONSENT_REVOKED', $exception->getCode() );
|
||||
// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
|
||||
return $this->build_suggestions_array( $response )->to_array();
|
||||
}
|
||||
|
||||
// phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
|
||||
|
||||
/**
|
||||
* Generates the list of 5 suggestions to return.
|
||||
*
|
||||
* @param Response $response The response from the API.
|
||||
*
|
||||
* @return Suggestions_Bucket The array of suggestions.
|
||||
*/
|
||||
public function build_suggestions_array( Response $response ): Suggestions_Bucket {
|
||||
$suggestions_bucket = new Suggestions_Bucket();
|
||||
$json = \json_decode( $response->get_body() );
|
||||
if ( $json === null || ! isset( $json->choices ) ) {
|
||||
return $suggestions_bucket;
|
||||
}
|
||||
foreach ( $json->choices as $suggestion ) {
|
||||
$suggestions_bucket->add_suggestion( new Suggestion( $suggestion->text ) );
|
||||
}
|
||||
|
||||
return $suggestions_bucket;
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Generator\Domain\Endpoint;
|
||||
|
||||
interface Endpoint_Interface {
|
||||
|
||||
/**
|
||||
* Gets the name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string;
|
||||
|
||||
/**
|
||||
* Gets the namespace.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_namespace(): string;
|
||||
|
||||
/**
|
||||
* Gets the route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_route(): string;
|
||||
|
||||
/**
|
||||
* Gets the URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_url(): string;
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Generator\Domain\Endpoint;
|
||||
|
||||
/**
|
||||
* List of endpoints.
|
||||
*/
|
||||
class Endpoint_List {
|
||||
|
||||
/**
|
||||
* Holds the endpoints.
|
||||
*
|
||||
* @var array<Endpoint_Interface>
|
||||
*/
|
||||
private $endpoints = [];
|
||||
|
||||
/**
|
||||
* Adds an endpoint to the list.
|
||||
*
|
||||
* @param Endpoint_Interface $endpoint An endpoint.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_endpoint( Endpoint_Interface $endpoint ): void {
|
||||
$this->endpoints[] = $endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the list to an array.
|
||||
*
|
||||
* @return array<string, string> The array of endpoints.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
$result = [];
|
||||
foreach ( $this->endpoints as $endpoint ) {
|
||||
$result[ $endpoint->get_name() ] = $endpoint->get_url();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Generator\Domain;
|
||||
|
||||
/**
|
||||
* Class Suggestion
|
||||
* Represents a suggestion from the AI Generator API.
|
||||
*/
|
||||
class Suggestion {
|
||||
|
||||
/**
|
||||
* The suggestion text.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param string $value The suggestion text.
|
||||
*/
|
||||
public function __construct( string $value ) {
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the suggestion text.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_value(): string {
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Generator\Domain;
|
||||
|
||||
/**
|
||||
* Class Suggestion_Bucket
|
||||
* Represents a collection of Suggestion objects.
|
||||
*/
|
||||
class Suggestions_Bucket {
|
||||
|
||||
/**
|
||||
* The suggestions.
|
||||
*
|
||||
* @var array<Suggestion>
|
||||
*/
|
||||
private $suggestions;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->suggestions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a suggestion to the bucket.
|
||||
*
|
||||
* @param Suggestion $suggestion The suggestion to add.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_suggestion( Suggestion $suggestion ) {
|
||||
$this->suggestions[] = $suggestion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the suggestions as an array.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function to_array() {
|
||||
return \array_map(
|
||||
static function ( $item ) {
|
||||
return $item->get_value();
|
||||
},
|
||||
$this->suggestions
|
||||
);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Generator\Domain;
|
||||
|
||||
/**
|
||||
* Helper class to get the URLs needed for the AI Generator API.
|
||||
*/
|
||||
interface URLs_Interface {
|
||||
|
||||
/**
|
||||
* Gets the licence URL.
|
||||
*
|
||||
* @return string The license URL.
|
||||
*/
|
||||
public function get_license_url(): string;
|
||||
|
||||
/**
|
||||
* Gets the callback URL to be used by the API to send back the access token, refresh token and code challenge.
|
||||
*
|
||||
* @return string The callback URL.
|
||||
*/
|
||||
public function get_callback_url(): string;
|
||||
|
||||
/**
|
||||
* Gets the callback URL to be used by the API to send back the refreshed JWTs once they expire.
|
||||
*
|
||||
* @return string The refresh callback URL.
|
||||
*/
|
||||
public function get_refresh_callback_url(): string;
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Generator\Infrastructure\Endpoints;
|
||||
|
||||
use Exception;
|
||||
use Yoast\WP\SEO\AI_Generator\Domain\Endpoint\Endpoint_Interface;
|
||||
use Yoast\WP\SEO\AI_Generator\User_Interface\Get_Suggestions_Route;
|
||||
|
||||
/**
|
||||
* Represents the setup steps tracking endpoint.
|
||||
*/
|
||||
class Get_Suggestions_Endpoint implements Endpoint_Interface {
|
||||
|
||||
/**
|
||||
* Gets the name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return 'getSuggestions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the namespace.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_namespace(): string {
|
||||
return Get_Suggestions_Route::ROUTE_NAMESPACE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the route.
|
||||
*
|
||||
* @throws Exception If the route prefix is not overwritten this throws.
|
||||
* @return string
|
||||
*/
|
||||
public function get_route(): string {
|
||||
return Get_Suggestions_Route::ROUTE_PREFIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_url(): string {
|
||||
return \rest_url( $this->get_namespace() . $this->get_route() );
|
||||
}
|
||||
}
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Generator\Infrastructure\Endpoints;
|
||||
|
||||
use Exception;
|
||||
use Yoast\WP\SEO\AI_Generator\Domain\Endpoint\Endpoint_Interface;
|
||||
use Yoast\WP\SEO\AI_Generator\User_Interface\Get_Usage_Route;
|
||||
|
||||
/**
|
||||
* Represents the setup steps tracking endpoint.
|
||||
*/
|
||||
class Get_Usage_Endpoint implements Endpoint_Interface {
|
||||
|
||||
/**
|
||||
* Gets the name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return 'getUsage';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the namespace.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_namespace(): string {
|
||||
return Get_Usage_Route::ROUTE_NAMESPACE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the route.
|
||||
*
|
||||
* @throws Exception If the route prefix is not overwritten this throws.
|
||||
* @return string
|
||||
*/
|
||||
public function get_route(): string {
|
||||
return Get_Usage_Route::ROUTE_PREFIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_url(): string {
|
||||
return \rest_url( $this->get_namespace() . $this->get_route() );
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Generator\Infrastructure;
|
||||
|
||||
use WPSEO_Utils;
|
||||
use Yoast\WP\SEO\AI_Generator\Domain\URLs_Interface;
|
||||
|
||||
/**
|
||||
* Class WordPress_URLs
|
||||
* Provides URLs for the AI Generator API in a WordPress context.
|
||||
*/
|
||||
class WordPress_URLs implements URLs_Interface {
|
||||
|
||||
/**
|
||||
* Gets the license URL.
|
||||
*
|
||||
* @return string The license URL.
|
||||
*/
|
||||
public function get_license_url(): string {
|
||||
return WPSEO_Utils::get_home_url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the callback URL to be used by the API to send back the access token, refresh token and code challenge.
|
||||
*
|
||||
* @return string The callbacks URL.
|
||||
*/
|
||||
public function get_callback_url(): string {
|
||||
return \get_rest_url( null, 'yoast/v1/ai_generator/callback' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the callback URL to be used by the API to send back the refreshed JWTs once they expire.
|
||||
*
|
||||
* @return string The callbacks URL.
|
||||
*/
|
||||
public function get_refresh_callback_url(): string {
|
||||
return \get_rest_url( null, 'yoast/v1/ai_generator/refresh_callback' );
|
||||
}
|
||||
}
|
||||
Executable
+162
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Generator\User_Interface;
|
||||
|
||||
use WPSEO_Addon_Manager;
|
||||
use WPSEO_Admin_Asset_Manager;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Infrastructure\API_Client;
|
||||
use Yoast\WP\SEO\Conditionals\AI_Conditional;
|
||||
use Yoast\WP\SEO\Conditionals\AI_Editor_Conditional;
|
||||
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
use Yoast\WP\SEO\Helpers\User_Helper;
|
||||
use Yoast\WP\SEO\Integrations\Integration_Interface;
|
||||
use Yoast\WP\SEO\Introductions\Application\Ai_Fix_Assessments_Upsell;
|
||||
use Yoast\WP\SEO\Introductions\Infrastructure\Introductions_Seen_Repository;
|
||||
|
||||
/**
|
||||
* Ai_Generator_Integration class.
|
||||
*/
|
||||
class Ai_Generator_Integration implements Integration_Interface {
|
||||
|
||||
/**
|
||||
* Represents the admin asset manager.
|
||||
*
|
||||
* @var WPSEO_Admin_Asset_Manager
|
||||
*/
|
||||
private $asset_manager;
|
||||
|
||||
/**
|
||||
* Represents the add-on manager.
|
||||
*
|
||||
* @var WPSEO_Addon_Manager
|
||||
*/
|
||||
private $addon_manager;
|
||||
|
||||
/**
|
||||
* Holds the API client instance.
|
||||
*
|
||||
* @var API_Client
|
||||
*/
|
||||
private $api_client;
|
||||
|
||||
/**
|
||||
* Represents the current page helper.
|
||||
*
|
||||
* @var Current_Page_Helper
|
||||
*/
|
||||
private $current_page_helper;
|
||||
|
||||
/**
|
||||
* Represents the options manager.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
private $options_helper;
|
||||
|
||||
/**
|
||||
* Represents the user helper.
|
||||
*
|
||||
* @var User_Helper
|
||||
*/
|
||||
private $user_helper;
|
||||
|
||||
/**
|
||||
* Represents the introductions seen repository.
|
||||
*
|
||||
* @var Introductions_Seen_Repository
|
||||
*/
|
||||
private $introductions_seen_repository;
|
||||
|
||||
/**
|
||||
* Returns the conditionals based in which this loadable should be active.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function get_conditionals() {
|
||||
return [ AI_Conditional::class, AI_Editor_Conditional::class ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the class.
|
||||
*
|
||||
* @param WPSEO_Admin_Asset_Manager $asset_manager The admin asset manager.
|
||||
* @param WPSEO_Addon_Manager $addon_manager The addon manager.
|
||||
* @param API_Client $api_client The API client.
|
||||
* @param Current_Page_Helper $current_page_helper The current page helper.
|
||||
* @param Options_Helper $options_helper The options helper.
|
||||
* @param User_Helper $user_helper The user helper.
|
||||
* @param Introductions_Seen_Repository $introductions_seen_repository The introductions seen repository.
|
||||
*/
|
||||
public function __construct(
|
||||
WPSEO_Admin_Asset_Manager $asset_manager,
|
||||
WPSEO_Addon_Manager $addon_manager,
|
||||
API_Client $api_client,
|
||||
Current_Page_Helper $current_page_helper,
|
||||
Options_Helper $options_helper,
|
||||
User_Helper $user_helper,
|
||||
Introductions_Seen_Repository $introductions_seen_repository
|
||||
) {
|
||||
$this->asset_manager = $asset_manager;
|
||||
$this->addon_manager = $addon_manager;
|
||||
$this->api_client = $api_client;
|
||||
$this->current_page_helper = $current_page_helper;
|
||||
$this->options_helper = $options_helper;
|
||||
$this->user_helper = $user_helper;
|
||||
$this->introductions_seen_repository = $introductions_seen_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the integration.
|
||||
*
|
||||
* This is the place to register hooks and filters.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_hooks() {
|
||||
\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
|
||||
// Enqueue after Elementor_Premium integration, which re-registers the assets.
|
||||
\add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'enqueue_assets' ], 11 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the subscription status for Yoast SEO Premium and Yoast WooCommerce SEO.
|
||||
*
|
||||
* @return array<string, bool>
|
||||
*/
|
||||
public function get_product_subscriptions() {
|
||||
return [
|
||||
'premiumSubscription' => $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ),
|
||||
'wooCommerceSubscription' => $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data that should be passed to the script.
|
||||
*
|
||||
* @return array<string|array<string>>
|
||||
*/
|
||||
public function get_script_data() {
|
||||
$user_id = $this->user_helper->get_current_user_id();
|
||||
|
||||
return [
|
||||
'hasConsent' => $this->user_helper->get_meta( $user_id, '_yoast_wpseo_ai_consent', true ),
|
||||
'productSubscriptions' => $this->get_product_subscriptions(),
|
||||
'hasSeenIntroduction' => $this->introductions_seen_repository->is_introduction_seen( $user_id, AI_Fix_Assessments_Upsell::ID ),
|
||||
'requestTimeout' => $this->api_client->get_request_timeout(),
|
||||
'isFreeSparks' => $this->options_helper->get( 'ai_free_sparks_started_on', null ) !== null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues the required assets.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_assets() {
|
||||
|
||||
$this->asset_manager->enqueue_script( 'ai-generator' );
|
||||
$this->asset_manager->localize_script( 'ai-generator', 'wpseoAiGenerator', $this->get_script_data() );
|
||||
$this->asset_manager->enqueue_style( 'ai-generator' );
|
||||
}
|
||||
}
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Generator\User_Interface;
|
||||
|
||||
use WP_REST_Response;
|
||||
use WPSEO_Addon_Manager;
|
||||
use Yoast\WP\SEO\Conditionals\AI_Conditional;
|
||||
use Yoast\WP\SEO\Main;
|
||||
use Yoast\WP\SEO\Routes\Route_Interface;
|
||||
|
||||
/**
|
||||
* Registers a route to bust the subscription cache.
|
||||
*
|
||||
* @makePublic
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
*/
|
||||
class Bust_Subscription_Cache_Route implements Route_Interface {
|
||||
|
||||
use Route_Permission_Trait;
|
||||
|
||||
/**
|
||||
* The namespace for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
|
||||
|
||||
/**
|
||||
* The prefix for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_PREFIX = '/ai_generator/bust_subscription_cache';
|
||||
|
||||
/**
|
||||
* The addon manager instance.
|
||||
*
|
||||
* @var WPSEO_Addon_Manager
|
||||
*/
|
||||
private $addon_manager;
|
||||
|
||||
/**
|
||||
* Returns the conditionals based in which this loadable should be active.
|
||||
*
|
||||
* @return array<string> The conditionals.
|
||||
*/
|
||||
public static function get_conditionals() {
|
||||
return [ AI_Conditional::class ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param WPSEO_Addon_Manager $addon_manager The addon manager instance.
|
||||
*/
|
||||
public function __construct( WPSEO_Addon_Manager $addon_manager ) {
|
||||
$this->addon_manager = $addon_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers routes with WordPress.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_routes() {
|
||||
\register_rest_route(
|
||||
self::ROUTE_NAMESPACE,
|
||||
self::ROUTE_PREFIX,
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'args' => [],
|
||||
'callback' => [ $this, 'bust_subscription_cache' ],
|
||||
'permission_callback' => [ $this, 'check_permissions' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the callback that busts the subscription cache.
|
||||
*
|
||||
* @return WP_REST_Response The response of the callback action.
|
||||
*/
|
||||
public function bust_subscription_cache(): WP_REST_Response {
|
||||
$this->addon_manager->remove_site_information_transients();
|
||||
|
||||
return new WP_REST_Response( 'Subscription cache successfully busted.' );
|
||||
}
|
||||
}
|
||||
Executable
+174
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Generator\User_Interface;
|
||||
|
||||
use RuntimeException;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use Yoast\WP\SEO\AI_Generator\Application\Suggestions_Provider;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Payment_Required_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Remote_Request_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Too_Many_Requests_Exception;
|
||||
use Yoast\WP\SEO\Conditionals\AI_Conditional;
|
||||
use Yoast\WP\SEO\Conditionals\No_Conditionals;
|
||||
use Yoast\WP\SEO\Main;
|
||||
use Yoast\WP\SEO\Routes\Route_Interface;
|
||||
|
||||
/**
|
||||
* Registers a route to get suggestions from the AI API
|
||||
*
|
||||
* @makePublic
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
*/
|
||||
class Get_Suggestions_Route implements Route_Interface {
|
||||
|
||||
use No_Conditionals;
|
||||
use Route_Permission_Trait;
|
||||
|
||||
/**
|
||||
* The namespace for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
|
||||
|
||||
/**
|
||||
* The prefix for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_PREFIX = '/ai_generator/get_suggestions';
|
||||
|
||||
/**
|
||||
* The suggestions provider instance.
|
||||
*
|
||||
* @var Suggestions_Provider
|
||||
*/
|
||||
private $suggestions_provider;
|
||||
|
||||
/**
|
||||
* Returns the conditionals based in which this loadable should be active.
|
||||
*
|
||||
* @return array<string> The conditionals.
|
||||
*/
|
||||
public static function get_conditionals() {
|
||||
return [ AI_Conditional::class ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param Suggestions_Provider $suggestions_provider The suggestions provider instance.
|
||||
*/
|
||||
public function __construct( Suggestions_Provider $suggestions_provider ) {
|
||||
$this->suggestions_provider = $suggestions_provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers routes with WordPress.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_routes() {
|
||||
\register_rest_route(
|
||||
self::ROUTE_NAMESPACE,
|
||||
self::ROUTE_PREFIX,
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'args' => [
|
||||
'type' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'enum' => [
|
||||
'seo-title',
|
||||
'meta-description',
|
||||
'product-seo-title',
|
||||
'product-meta-description',
|
||||
'product-taxonomy-seo-title',
|
||||
'product-taxonomy-meta-description',
|
||||
'taxonomy-seo-title',
|
||||
'taxonomy-meta-description',
|
||||
],
|
||||
'description' => 'The type of suggestion requested.',
|
||||
],
|
||||
'prompt_content' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'description' => 'The content needed by the prompt to ask for suggestions.',
|
||||
],
|
||||
'focus_keyphrase' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'description' => 'The focus keyphrase associated to the post.',
|
||||
],
|
||||
'language' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'description' => 'The language the post is written in.',
|
||||
],
|
||||
'platform' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'enum' => [
|
||||
'Google',
|
||||
'Facebook',
|
||||
'Twitter',
|
||||
],
|
||||
'description' => 'The platform the post is intended for.',
|
||||
],
|
||||
'editor' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'enum' => [
|
||||
'classic',
|
||||
'elementor',
|
||||
'gutenberg',
|
||||
],
|
||||
'description' => 'The current editor.',
|
||||
],
|
||||
],
|
||||
'callback' => [ $this, 'get_suggestions' ],
|
||||
'permission_callback' => [ $this, 'check_permissions' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the callback to get AI-generated suggestions.
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response The response of the get_suggestions action.
|
||||
*/
|
||||
public function get_suggestions( WP_REST_Request $request ): WP_REST_Response {
|
||||
try {
|
||||
$user = \wp_get_current_user();
|
||||
$data = $this->suggestions_provider->get_suggestions(
|
||||
$user,
|
||||
$request->get_param( 'type' ),
|
||||
$request->get_param( 'prompt_content' ),
|
||||
$request->get_param( 'focus_keyphrase' ),
|
||||
$request->get_param( 'language' ),
|
||||
$request->get_param( 'platform' ),
|
||||
$request->get_param( 'editor' )
|
||||
);
|
||||
} catch ( Remote_Request_Exception $e ) {
|
||||
$message = [
|
||||
'message' => $e->getMessage(),
|
||||
'errorIdentifier' => $e->get_error_identifier(),
|
||||
];
|
||||
if ( $e instanceof Payment_Required_Exception || $e instanceof Too_Many_Requests_Exception ) {
|
||||
$message['missingLicenses'] = $e->get_missing_licenses();
|
||||
}
|
||||
return new WP_REST_Response(
|
||||
$message,
|
||||
$e->getCode()
|
||||
);
|
||||
} catch ( RuntimeException $e ) {
|
||||
return new WP_REST_Response( 'Failed to get suggestions.', 500 );
|
||||
}
|
||||
|
||||
return new WP_REST_Response( $data );
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Generator\User_Interface;
|
||||
|
||||
use WP_REST_Response;
|
||||
use WPSEO_Addon_Manager;
|
||||
use Yoast\WP\SEO\AI_Authorization\Application\Token_Manager;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Application\Request_Handler;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Remote_Request_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Too_Many_Requests_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\WP_Request_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Request;
|
||||
use Yoast\WP\SEO\Conditionals\AI_Conditional;
|
||||
use Yoast\WP\SEO\Main;
|
||||
use Yoast\WP\SEO\Routes\Route_Interface;
|
||||
|
||||
/**
|
||||
* Registers a route to get suggestions from the AI API
|
||||
*
|
||||
* @makePublic
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
*/
|
||||
class Get_Usage_Route implements Route_Interface {
|
||||
|
||||
use Route_Permission_Trait;
|
||||
|
||||
/**
|
||||
* The namespace for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
|
||||
|
||||
/**
|
||||
* The prefix for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_PREFIX = '/ai_generator/get_usage';
|
||||
|
||||
/**
|
||||
* The token manager instance.
|
||||
*
|
||||
* @var Token_Manager
|
||||
*/
|
||||
private $token_manager;
|
||||
|
||||
/**
|
||||
* The request handler instance.
|
||||
*
|
||||
* @var Request_Handler
|
||||
*/
|
||||
private $request_handler;
|
||||
|
||||
/**
|
||||
* Represents the add-on manager.
|
||||
*
|
||||
* @var WPSEO_Addon_Manager
|
||||
*/
|
||||
private $addon_manager;
|
||||
|
||||
/**
|
||||
* Returns the conditionals based in which this loadable should be active.
|
||||
*
|
||||
* @return array<string> The conditionals.
|
||||
*/
|
||||
public static function get_conditionals() {
|
||||
return [ AI_Conditional::class ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param Token_Manager $token_manager The token manager instance.
|
||||
* @param Request_Handler $request_handler The request handler instance.
|
||||
* @param WPSEO_Addon_Manager $addon_manager The add-on manager instance.
|
||||
*/
|
||||
public function __construct( Token_Manager $token_manager, Request_Handler $request_handler, WPSEO_Addon_Manager $addon_manager ) {
|
||||
$this->addon_manager = $addon_manager;
|
||||
$this->token_manager = $token_manager;
|
||||
$this->request_handler = $request_handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers routes with WordPress.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_routes() {
|
||||
\register_rest_route(
|
||||
self::ROUTE_NAMESPACE,
|
||||
self::ROUTE_PREFIX,
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'args' => [
|
||||
'is_woo_product_entity' => [
|
||||
'type' => 'boolean',
|
||||
'default' => false,
|
||||
],
|
||||
],
|
||||
'callback' => [ $this, 'get_usage' ],
|
||||
'permission_callback' => [ $this, 'check_permissions' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the callback that gets the monthly usage of the user.
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response The response of the callback action.
|
||||
*/
|
||||
public function get_usage( $request ): WP_REST_Response {
|
||||
$is_woo_product_entity = $request->get_param( 'is_woo_product_entity' );
|
||||
$user = \wp_get_current_user();
|
||||
try {
|
||||
$token = $this->token_manager->get_or_request_access_token( $user );
|
||||
$request_headers = [
|
||||
'Authorization' => "Bearer $token",
|
||||
];
|
||||
$action_path = $this->get_action_path( $is_woo_product_entity );
|
||||
$response = $this->request_handler->handle( new Request( $action_path, [], $request_headers, false ) );
|
||||
$data = \json_decode( $response->get_body() );
|
||||
|
||||
} catch ( Remote_Request_Exception | WP_Request_Exception $e ) {
|
||||
$message = [
|
||||
'errorMessage' => $e->getMessage(),
|
||||
'errorIdentifier' => $e->get_error_identifier(),
|
||||
'errorCode' => $e->getCode(),
|
||||
];
|
||||
if ( $e instanceof Too_Many_Requests_Exception ) {
|
||||
$message['missingLicenses'] = $e->get_missing_licenses();
|
||||
}
|
||||
return new WP_REST_Response(
|
||||
$message,
|
||||
$e->getCode()
|
||||
);
|
||||
}
|
||||
|
||||
return new WP_REST_Response( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action path for the request.
|
||||
*
|
||||
* @param bool $is_woo_product_entity Whether the request is for a WooCommerce product entity.
|
||||
*
|
||||
* @return string The action path.
|
||||
*/
|
||||
public function get_action_path( $is_woo_product_entity = false ): string {
|
||||
$unlimited = '/usage/' . \gmdate( 'Y-m' );
|
||||
if ( $is_woo_product_entity && $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ) ) {
|
||||
return $unlimited;
|
||||
}
|
||||
if ( ! $is_woo_product_entity && $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ) ) {
|
||||
return $unlimited;
|
||||
}
|
||||
return '/usage/free-usages';
|
||||
}
|
||||
}
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Generator\User_Interface;
|
||||
|
||||
/**
|
||||
* Trait for common permission checks in route classes.
|
||||
*/
|
||||
trait Route_Permission_Trait {
|
||||
|
||||
/**
|
||||
* Checks:
|
||||
* - if the user is logged
|
||||
* - if the user can edit posts
|
||||
*
|
||||
* @return bool Whether the user is logged in, can edit posts and the feature is active.
|
||||
*/
|
||||
public function check_permissions(): bool {
|
||||
$user = \wp_get_current_user();
|
||||
if ( $user === null || $user->ID < 1 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return \user_can( $user, 'edit_posts' );
|
||||
}
|
||||
}
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Application;
|
||||
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Bad_Request_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Forbidden_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Internal_Server_Error_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Not_Found_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Payment_Required_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Request_Timeout_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Service_Unavailable_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Too_Many_Requests_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Unauthorized_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Request;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Response;
|
||||
|
||||
interface Request_Handler_Interface {
|
||||
|
||||
/**
|
||||
* Executes the request to the API.
|
||||
*
|
||||
* @param Request $request The request to execute.
|
||||
*
|
||||
* @return Response The response from the API.
|
||||
*
|
||||
* @throws Bad_Request_Exception When the request fails for any other reason.
|
||||
* @throws Forbidden_Exception When the response code is 403.
|
||||
* @throws Internal_Server_Error_Exception When the response code is 500.
|
||||
* @throws Not_Found_Exception When the response code is 404.
|
||||
* @throws Payment_Required_Exception When the response code is 402.
|
||||
* @throws Request_Timeout_Exception When the response code is 408.
|
||||
* @throws Service_Unavailable_Exception When the response code is 503.
|
||||
* @throws Too_Many_Requests_Exception When the response code is 429.
|
||||
* @throws Unauthorized_Exception When the response code is 401.
|
||||
*/
|
||||
public function handle( Request $request ): Response;
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Application;
|
||||
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Bad_Request_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Forbidden_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Internal_Server_Error_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Not_Found_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Payment_Required_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Request_Timeout_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Service_Unavailable_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Too_Many_Requests_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Unauthorized_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\WP_Request_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Request;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Response;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Infrastructure\API_Client;
|
||||
|
||||
/**
|
||||
* Class Request_Handler
|
||||
* Handles the request to Yoast AI API.
|
||||
*
|
||||
* @makePublic
|
||||
*/
|
||||
class Request_Handler implements Request_Handler_Interface {
|
||||
|
||||
private const TIMEOUT = 60;
|
||||
|
||||
/**
|
||||
* The API client.
|
||||
*
|
||||
* @var API_Client
|
||||
*/
|
||||
private $api_client;
|
||||
|
||||
/**
|
||||
* The response parser.
|
||||
*
|
||||
* @var Response_Parser
|
||||
*/
|
||||
private $response_parser;
|
||||
|
||||
/**
|
||||
* Request_Handler constructor.
|
||||
*
|
||||
* @param API_Client $api_client The API client.
|
||||
* @param Response_Parser $response_parser The response parser.
|
||||
*/
|
||||
public function __construct( API_Client $api_client, Response_Parser $response_parser ) {
|
||||
$this->api_client = $api_client;
|
||||
$this->response_parser = $response_parser;
|
||||
}
|
||||
|
||||
// phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods.
|
||||
|
||||
/**
|
||||
* Executes the request to the API.
|
||||
*
|
||||
* @param Request $request The request to execute.
|
||||
*
|
||||
* @return Response The response from the API.
|
||||
*
|
||||
* @throws Bad_Request_Exception When the request fails for any other reason.
|
||||
* @throws Forbidden_Exception When the response code is 403.
|
||||
* @throws Internal_Server_Error_Exception When the response code is 500.
|
||||
* @throws Not_Found_Exception When the response code is 404.
|
||||
* @throws Payment_Required_Exception When the response code is 402.
|
||||
* @throws Request_Timeout_Exception When the response code is 408.
|
||||
* @throws Service_Unavailable_Exception When the response code is 503.
|
||||
* @throws Too_Many_Requests_Exception When the response code is 429.
|
||||
* @throws Unauthorized_Exception When the response code is 401.
|
||||
* @throws WP_Request_Exception When the request fails for any other reason.
|
||||
*/
|
||||
public function handle( Request $request ): Response {
|
||||
$api_response = $this->api_client->perform_request(
|
||||
$request->get_action_path(),
|
||||
$request->get_body(),
|
||||
$request->get_headers(),
|
||||
$request->is_post()
|
||||
);
|
||||
|
||||
$response = $this->response_parser->parse( $api_response );
|
||||
|
||||
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
|
||||
switch ( $response->get_response_code() ) {
|
||||
case 200:
|
||||
return $response;
|
||||
case 401:
|
||||
throw new Unauthorized_Exception( $response->get_message(), $response->get_response_code(), $response->get_error_code() );
|
||||
case 402:
|
||||
throw new Payment_Required_Exception( $response->get_message(), $response->get_response_code(), $response->get_error_code(), null, $response->get_missing_licenses() );
|
||||
case 403:
|
||||
throw new Forbidden_Exception( $response->get_message(), $response->get_response_code(), $response->get_error_code() );
|
||||
case 404:
|
||||
throw new Not_Found_Exception( $response->get_message(), $response->get_response_code(), $response->get_error_code() );
|
||||
case 408:
|
||||
throw new Request_Timeout_Exception( $response->get_message(), $response->get_response_code(), $response->get_error_code() );
|
||||
case 429:
|
||||
throw new Too_Many_Requests_Exception( $response->get_message(), $response->get_response_code(), $response->get_error_code(), null, $response->get_missing_licenses() );
|
||||
case 500:
|
||||
throw new Internal_Server_Error_Exception( $response->get_message(), $response->get_response_code(), $response->get_error_code() );
|
||||
case 503:
|
||||
throw new Service_Unavailable_Exception( $response->get_message(), $response->get_response_code(), $response->get_error_code() );
|
||||
default:
|
||||
throw new Bad_Request_Exception( $response->get_message(), $response->get_response_code(), $response->get_error_code() );
|
||||
}
|
||||
// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
|
||||
// phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
|
||||
}
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Application;
|
||||
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Response;
|
||||
|
||||
interface Response_Parser_Interface {
|
||||
|
||||
/**
|
||||
* Parses the response from the API.
|
||||
*
|
||||
* @param array<int|string|array<string>> $response The response from the API.
|
||||
*
|
||||
* @return Response The parsed response.
|
||||
*/
|
||||
public function parse( $response ): Response;
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Application;
|
||||
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Response;
|
||||
|
||||
/**
|
||||
* Class Response_Parser
|
||||
* Parses the response from the AI API and creates a Response object.
|
||||
*/
|
||||
class Response_Parser implements Response_Parser_Interface {
|
||||
|
||||
/**
|
||||
* Parses the response from the API.
|
||||
*
|
||||
* @param array<int|string|array<string>> $response The response from the API.
|
||||
*
|
||||
* @return Response The parsed response.
|
||||
*/
|
||||
public function parse( $response ): Response {
|
||||
$response_code = ( \wp_remote_retrieve_response_code( $response ) !== '' ) ? \wp_remote_retrieve_response_code( $response ) : 0;
|
||||
$response_message = \esc_html( \wp_remote_retrieve_response_message( $response ) );
|
||||
$error_code = '';
|
||||
$missing_licenses = [];
|
||||
|
||||
if ( $response_code !== 200 && $response_code !== 0 ) {
|
||||
$json_body = \json_decode( \wp_remote_retrieve_body( $response ) );
|
||||
if ( $json_body !== null ) {
|
||||
$response_message = ( $json_body->message ?? $response_message );
|
||||
$error_code = ( $json_body->error_code ?? $this->map_message_to_code( $response_message ) );
|
||||
if ( $response_code === 402 || $response_code === 429 ) {
|
||||
$missing_licenses = isset( $json_body->missing_licenses ) ? (array) $json_body->missing_licenses : [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Response( $response['body'], $response_code, $response_message, $error_code, $missing_licenses );
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the error message to a code.
|
||||
*
|
||||
* @param string $message The error message.
|
||||
*
|
||||
* @return string The mapped code.
|
||||
*/
|
||||
private function map_message_to_code( string $message ): string {
|
||||
if ( \strpos( $message, 'must NOT have fewer than 1 characters' ) !== false ) {
|
||||
return 'NOT_ENOUGH_CONTENT';
|
||||
}
|
||||
if ( \strpos( $message, 'Client timeout' ) !== false ) {
|
||||
return 'CLIENT_TIMEOUT';
|
||||
}
|
||||
if ( \strpos( $message, 'Server timeout' ) !== false ) {
|
||||
return 'SERVER_TIMEOUT';
|
||||
}
|
||||
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
}
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions;
|
||||
|
||||
/**
|
||||
* Class to manage a 400 - Bad request response.
|
||||
*/
|
||||
class Bad_Request_Exception extends Remote_Request_Exception {
|
||||
|
||||
}
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions;
|
||||
|
||||
/**
|
||||
* Class to manage a 403 - Forbidden response.
|
||||
*/
|
||||
class Forbidden_Exception extends Remote_Request_Exception {
|
||||
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions;
|
||||
|
||||
/**
|
||||
* Class to manage a 500 - Internal server error response.
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
*/
|
||||
class Internal_Server_Error_Exception extends Remote_Request_Exception {
|
||||
|
||||
}
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions;
|
||||
|
||||
/**
|
||||
* Class to manage a 404 - not found response.
|
||||
*/
|
||||
class Not_Found_Exception extends Remote_Request_Exception {
|
||||
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Class to manage a 402 - payment required response.
|
||||
*/
|
||||
class Payment_Required_Exception extends Remote_Request_Exception {
|
||||
|
||||
/**
|
||||
* The missing plugin licenses.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $missing_licenses;
|
||||
|
||||
/**
|
||||
* Payment_Required_Exception constructor.
|
||||
*
|
||||
* @param string $message The error message.
|
||||
* @param int $code The error status code.
|
||||
* @param string $error_identifier The error code identifier, used to identify a type of error.
|
||||
* @param Throwable| null $previous The previously thrown exception.
|
||||
* @param string[] $missing_licenses The missing plugin licenses.
|
||||
*/
|
||||
public function __construct( $message = '', $code = 0, $error_identifier = '', $previous = null, $missing_licenses = [] ) {
|
||||
$this->missing_licenses = $missing_licenses;
|
||||
parent::__construct( $message, $code, $error_identifier, $previous );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the missing plugin licences.
|
||||
*
|
||||
* @return string[] The missing plugin licenses.
|
||||
*/
|
||||
public function get_missing_licenses() {
|
||||
return $this->missing_licenses;
|
||||
}
|
||||
}
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Class Remote_Request_Exception
|
||||
*/
|
||||
abstract class Remote_Request_Exception extends Exception {
|
||||
|
||||
/**
|
||||
* A string error code that can be used to identify a particular type of error.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $error_identifier;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $message The error message.
|
||||
* @param int $code The error status code.
|
||||
* @param string $error_identifier The error code identifier, used to identify a type of error.
|
||||
* @param Throwable|null $previous The previously thrown exception.
|
||||
*/
|
||||
public function __construct( $message = '', $code = 0, $error_identifier = '', ?Throwable $previous = null ) {
|
||||
parent::__construct( $message, $code, $previous );
|
||||
$this->error_identifier = (string) $error_identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error identifier.
|
||||
*
|
||||
* @return string The error identifier.
|
||||
*/
|
||||
public function get_error_identifier(): string {
|
||||
return $this->error_identifier;
|
||||
}
|
||||
}
|
||||
wp-content/plugins/wordpress-seo/src/ai-http-request/domain/exceptions/request-timeout-exception.php
Executable
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions;
|
||||
|
||||
/**
|
||||
* Class to manage a 408 - request timeout exception
|
||||
*/
|
||||
class Request_Timeout_Exception extends Remote_Request_Exception {
|
||||
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions;
|
||||
|
||||
/**
|
||||
* Class to manage a 503 - service unavailable response.
|
||||
*/
|
||||
class Service_Unavailable_Exception extends Remote_Request_Exception {
|
||||
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions;
|
||||
|
||||
/**
|
||||
* Class to manage a 429 - Too many requests response.
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
*/
|
||||
class Too_Many_Requests_Exception extends Payment_Required_Exception {
|
||||
|
||||
}
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions;
|
||||
|
||||
/**
|
||||
* Class to manage a 401 - unauthorized response.
|
||||
*/
|
||||
class Unauthorized_Exception extends Remote_Request_Exception {
|
||||
|
||||
}
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions;
|
||||
|
||||
use Throwable;
|
||||
/**
|
||||
* Class to manage an error response in wp_remote_*() requests.
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
*/
|
||||
class WP_Request_Exception extends Remote_Request_Exception {
|
||||
|
||||
/**
|
||||
* WP_Request_Exception constructor.
|
||||
*
|
||||
* @param string $message The error message.
|
||||
* @param Throwable| null $previous The previously thrown exception.
|
||||
*/
|
||||
public function __construct( $message = '', $previous = null ) {
|
||||
parent::__construct( $message, 400, 'WP_HTTP_REQUEST_ERROR', $previous );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Domain;
|
||||
|
||||
/**
|
||||
* Class Request
|
||||
* Represents a request to the AI Generator API.
|
||||
*/
|
||||
class Request {
|
||||
|
||||
/**
|
||||
* The action path for the request.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $action_path;
|
||||
|
||||
/**
|
||||
* The body of the request.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
private $body;
|
||||
|
||||
/**
|
||||
* The headers for the request.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
private $headers;
|
||||
|
||||
/**
|
||||
* Whether the request is a POST request.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_post;
|
||||
|
||||
/**
|
||||
* Constructor for the Request class.
|
||||
*
|
||||
* @param string $action_path The action path for the request.
|
||||
* @param array<string> $body The body of the request.
|
||||
* @param array<string> $headers The headers for the request.
|
||||
* @param bool $is_post Whether the request is a POST request. Default is true.
|
||||
*/
|
||||
public function __construct( string $action_path, array $body = [], array $headers = [], bool $is_post = true ) {
|
||||
$this->action_path = $action_path;
|
||||
$this->body = $body;
|
||||
$this->headers = $headers;
|
||||
$this->is_post = $is_post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the action path for the request.
|
||||
*
|
||||
* @return string The action path for the request.
|
||||
*/
|
||||
public function get_action_path(): string {
|
||||
return $this->action_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the body of the request.
|
||||
*
|
||||
* @return array<string> The body of the request.
|
||||
*/
|
||||
public function get_body(): array {
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the headers for the request.
|
||||
*
|
||||
* @return array<string> The headers for the request.
|
||||
*/
|
||||
public function get_headers(): array {
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the request is a POST request.
|
||||
*
|
||||
* @return bool True if the request is a POST request, false otherwise.
|
||||
*/
|
||||
public function is_post(): bool {
|
||||
return $this->is_post;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Domain;
|
||||
|
||||
/**
|
||||
* Class Response
|
||||
* Represents a response from the AI Generator API.
|
||||
*/
|
||||
class Response {
|
||||
|
||||
/**
|
||||
* The response body.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $body;
|
||||
|
||||
/**
|
||||
* The response code.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $response_code;
|
||||
|
||||
/**
|
||||
* The response message.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $message;
|
||||
|
||||
/**
|
||||
* The error code.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $error_code;
|
||||
|
||||
/**
|
||||
* The missing licenses.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
private $missing_licenses;
|
||||
|
||||
/**
|
||||
* Response constructor.
|
||||
*
|
||||
* @param string $body The response body.
|
||||
* @param int $response_code The response code.
|
||||
* @param string $message The response message.
|
||||
* @param string $error_code The error code.
|
||||
* @param array<string> $missing_licenses The missing licenses.
|
||||
*/
|
||||
public function __construct( string $body, int $response_code, string $message, string $error_code = '', $missing_licenses = [] ) {
|
||||
$this->body = $body;
|
||||
$this->response_code = $response_code;
|
||||
$this->message = $message;
|
||||
$this->error_code = $error_code;
|
||||
$this->missing_licenses = $missing_licenses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the response body.
|
||||
*
|
||||
* @return string The response body.
|
||||
*/
|
||||
public function get_body() {
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the response code.
|
||||
*
|
||||
* @return int The response code.
|
||||
*/
|
||||
public function get_response_code(): int {
|
||||
return $this->response_code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the response message.
|
||||
*
|
||||
* @return string The response message.
|
||||
*/
|
||||
public function get_message(): string {
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the error code.
|
||||
*
|
||||
* @return string The error code.
|
||||
*/
|
||||
public function get_error_code(): string {
|
||||
return $this->error_code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the missing licenses.
|
||||
*
|
||||
* @return array<string> The missing licenses.
|
||||
*/
|
||||
public function get_missing_licenses(): array {
|
||||
return $this->missing_licenses;
|
||||
}
|
||||
}
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Infrastructure;
|
||||
|
||||
use Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\WP_Request_Exception;
|
||||
|
||||
/**
|
||||
* Interface for the API client.
|
||||
*/
|
||||
|
||||
interface API_Client_Interface {
|
||||
|
||||
/**
|
||||
* Performs a request to the API.
|
||||
*
|
||||
* @param string $action_path The action path for the request.
|
||||
* @param array<string> $body The body of the request.
|
||||
* @param array<string> $headers The headers for the request.
|
||||
* @param bool $is_post Whether the request is a POST request.
|
||||
*
|
||||
* @return array<int|string|array<string>> The response from the API.
|
||||
*
|
||||
* @throws WP_Request_Exception When the wp_remote_post() returns an error.
|
||||
*/
|
||||
public function perform_request( string $action_path, $body, $headers, bool $is_post ): array;
|
||||
|
||||
/**
|
||||
* Gets the timeout of the requests in seconds.
|
||||
*
|
||||
* @return int The timeout of the suggestion requests in seconds.
|
||||
*/
|
||||
public function get_request_timeout(): int;
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_HTTP_Request\Infrastructure;
|
||||
|
||||
use WPSEO_Utils;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\WP_Request_Exception;
|
||||
|
||||
/**
|
||||
* Class API_Client
|
||||
* Handles the API requests to the AI Generator API.
|
||||
*
|
||||
* @makePublic
|
||||
*/
|
||||
class API_Client implements API_Client_Interface {
|
||||
|
||||
/**
|
||||
* The base URL for the API.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $base_url = 'https://ai.yoa.st/api/v1';
|
||||
|
||||
/**
|
||||
* Performs a request to the API.
|
||||
*
|
||||
* @param string $action_path The action path for the request.
|
||||
* @param array<string> $body The body of the request.
|
||||
* @param array<string> $headers The headers for the request.
|
||||
* @param bool $is_post Whether the request is a POST request.
|
||||
*
|
||||
* @return array<int|string|array<string>> The response from the API.
|
||||
*
|
||||
* @throws WP_Request_Exception When the wp_remote_post() returns an error.
|
||||
*/
|
||||
public function perform_request( string $action_path, $body, $headers, bool $is_post ): array {
|
||||
// Our API expects JSON.
|
||||
// The request times out after 30 seconds.
|
||||
$headers = \array_merge( $headers, [ 'Content-Type' => 'application/json' ] );
|
||||
$arguments = [
|
||||
'timeout' => $this->get_request_timeout(),
|
||||
'headers' => $headers,
|
||||
];
|
||||
|
||||
if ( $is_post ) {
|
||||
// phpcs:ignore Yoast.Yoast.JsonEncodeAlternative.Found -- Reason: We don't want the debug/pretty possibility.
|
||||
$arguments['body'] = WPSEO_Utils::format_json_encode( $body );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter: 'Yoast\WP\SEO\ai_api_url' - Replaces the default URL for the AI API with a custom one.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @param string $url The default URL for the AI API.
|
||||
*/
|
||||
$url = \apply_filters( 'Yoast\WP\SEO\ai_api_url', $this->base_url );
|
||||
$response = ( $is_post ) ? \wp_remote_post( $url . $action_path, $arguments ) : \wp_remote_get( $url . $action_path, $arguments );
|
||||
|
||||
if ( \is_wp_error( $response ) ) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
|
||||
throw new WP_Request_Exception( $response->get_error_message() );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the timeout of the requests in seconds.
|
||||
*
|
||||
* @return int The timeout of the suggestion requests in seconds.
|
||||
*/
|
||||
public function get_request_timeout(): int {
|
||||
/**
|
||||
* Filter: 'Yoast\WP\SEO\ai_suggestions_timeout' - Replaces the default timeout with a custom one, for testing purposes.
|
||||
*
|
||||
* @since 22.7
|
||||
* @internal
|
||||
*
|
||||
* @param int $timeout The default timeout in seconds.
|
||||
*/
|
||||
return (int) \apply_filters( 'Yoast\WP\SEO\ai_suggestions_timeout', 60 );
|
||||
}
|
||||
}
|
||||
Executable
+202
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\Alerts\Application\Default_SEO_Data;
|
||||
|
||||
use Yoast\WP\SEO\Alerts\Infrastructure\Default_SEO_Data\Default_SEO_Data_Collector;
|
||||
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
|
||||
use Yoast\WP\SEO\Helpers\Indexable_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Product_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Short_Link_Helper;
|
||||
use Yoast\WP\SEO\Integrations\Integration_Interface;
|
||||
use Yoast_Notification;
|
||||
use Yoast_Notification_Center;
|
||||
|
||||
/**
|
||||
* Default_SEO_Data_Alert class.
|
||||
*/
|
||||
class Default_SEO_Data_Alert implements Integration_Interface {
|
||||
|
||||
public const NOTIFICATION_ID = 'wpseo-default-seo-data';
|
||||
|
||||
/**
|
||||
* The notifications center.
|
||||
*
|
||||
* @var Yoast_Notification_Center
|
||||
*/
|
||||
private $notification_center;
|
||||
|
||||
/**
|
||||
* The default SEO data collector.
|
||||
*
|
||||
* @var Default_SEO_Data_Collector
|
||||
*/
|
||||
private $default_seo_data_collector;
|
||||
|
||||
/**
|
||||
* The short link helper.
|
||||
*
|
||||
* @var Short_Link_Helper
|
||||
*/
|
||||
private $short_link_helper;
|
||||
|
||||
/**
|
||||
* The product helper.
|
||||
*
|
||||
* @var Product_Helper
|
||||
*/
|
||||
private $product_helper;
|
||||
|
||||
/**
|
||||
* The indexable helper.
|
||||
*
|
||||
* @var Indexable_Helper
|
||||
*/
|
||||
private $indexable_helper;
|
||||
|
||||
/**
|
||||
* The post type helper.
|
||||
*
|
||||
* @var Post_Type_Helper
|
||||
*/
|
||||
private $post_type_helper;
|
||||
|
||||
/**
|
||||
* Default_SEO_Data_Alert constructor.
|
||||
*
|
||||
* @param Yoast_Notification_Center $notification_center The notification center.
|
||||
* @param Default_SEO_Data_Collector $default_seo_data_collector The default SEO data collector.
|
||||
* @param Short_Link_Helper $short_link_helper The short link helper.
|
||||
* @param Product_Helper $product_helper The product helper.
|
||||
* @param Indexable_Helper $indexable_helper The indexable helper.
|
||||
* @param Post_Type_Helper $post_type_helper The post type helper.
|
||||
*/
|
||||
public function __construct(
|
||||
Yoast_Notification_Center $notification_center,
|
||||
Default_SEO_Data_Collector $default_seo_data_collector,
|
||||
Short_Link_Helper $short_link_helper,
|
||||
Product_Helper $product_helper,
|
||||
Indexable_Helper $indexable_helper,
|
||||
Post_Type_Helper $post_type_helper
|
||||
) {
|
||||
$this->notification_center = $notification_center;
|
||||
$this->default_seo_data_collector = $default_seo_data_collector;
|
||||
$this->short_link_helper = $short_link_helper;
|
||||
$this->product_helper = $product_helper;
|
||||
$this->indexable_helper = $indexable_helper;
|
||||
$this->post_type_helper = $post_type_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the conditionals based on which this loadable should be active.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function get_conditionals() {
|
||||
return [ Admin_Conditional::class ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the integration.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_hooks() {
|
||||
\add_action( 'admin_init', [ $this, 'add_notifications' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds notifications (when necessary).
|
||||
*
|
||||
* We want to show this notification only when there are enough posts that have the default SEO title or meta description, or both.
|
||||
* If this is not the case we will not show the notification at all since it does not serve a purpose yet.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_notifications() {
|
||||
if ( ! $this->indexable_helper->should_index_indexables() ) {
|
||||
// Do not show the notification when indexables are disabled.
|
||||
$this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->post_type_helper->is_indexable( 'post' ) || ! $this->post_type_helper->has_metabox( 'post' ) ) {
|
||||
// Do not show the notification when posts are not indexable or have no metabox.
|
||||
$this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID );
|
||||
return;
|
||||
}
|
||||
|
||||
$posts_with_default_seo_title = $this->default_seo_data_collector->get_posts_with_default_seo_title();
|
||||
$posts_with_default_seo_description = $this->default_seo_data_collector->get_posts_with_default_seo_description();
|
||||
|
||||
$has_enough_posts_with_default_title = \count( $posts_with_default_seo_title ) > 4;
|
||||
$has_enough_posts_with_default_desc = \count( $posts_with_default_seo_description ) > 4;
|
||||
|
||||
if ( ! $has_enough_posts_with_default_title && ! $has_enough_posts_with_default_desc ) {
|
||||
$this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID );
|
||||
return;
|
||||
}
|
||||
|
||||
$notification = $this->get_default_seo_data_notification( $has_enough_posts_with_default_title, $has_enough_posts_with_default_desc );
|
||||
|
||||
$this->notification_center->add_notification( $notification );
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the default SEO data notification.
|
||||
*
|
||||
* @param bool $has_enough_posts_with_default_title Whether there are content types with default SEO title in their most recent posts.
|
||||
* @param bool $has_enough_posts_with_default_desc Whether there are content types with default SEO description in their most recent posts.
|
||||
*
|
||||
* @return Yoast_Notification The notification containing the suggested plugin.
|
||||
*/
|
||||
private function get_default_seo_data_notification( $has_enough_posts_with_default_title, $has_enough_posts_with_default_desc ): Yoast_Notification {
|
||||
$message = $this->get_default_seo_data_message( $has_enough_posts_with_default_title, $has_enough_posts_with_default_desc );
|
||||
|
||||
return new Yoast_Notification(
|
||||
$message,
|
||||
[
|
||||
'id' => self::NOTIFICATION_ID,
|
||||
'type' => Yoast_Notification::WARNING,
|
||||
'capabilities' => [ 'wpseo_manage_options' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a message to inform users that they are using only default SEO data lately.
|
||||
*
|
||||
* @param bool $has_enough_posts_with_default_title Whether there are content types with default SEO title in their most recent posts.
|
||||
* @param bool $has_enough_posts_with_default_desc Whether there are content types with default SEO description in their most recent posts.
|
||||
*
|
||||
* @return string The default SEO data message.
|
||||
*/
|
||||
private function get_default_seo_data_message( $has_enough_posts_with_default_title, $has_enough_posts_with_default_desc ): string {
|
||||
$shortlink = ( $this->product_helper->is_premium() ) ? $this->short_link_helper->get( 'https://yoa.st/ai-generate-alert-premium/' ) : $this->short_link_helper->get( 'https://yoa.st/ai-generate-alert-free/' );
|
||||
|
||||
if ( $has_enough_posts_with_default_title && $has_enough_posts_with_default_desc ) {
|
||||
$default_seo_data = \esc_html__( 'SEO titles and meta descriptions', 'wordpress-seo' );
|
||||
}
|
||||
elseif ( $has_enough_posts_with_default_title ) {
|
||||
$default_seo_data = \esc_html__( 'SEO titles', 'wordpress-seo' );
|
||||
}
|
||||
elseif ( $has_enough_posts_with_default_desc ) {
|
||||
$default_seo_data = \esc_html__( 'meta descriptions', 'wordpress-seo' );
|
||||
}
|
||||
else {
|
||||
$default_seo_data = \esc_html__( 'SEO data', 'wordpress-seo' );
|
||||
}
|
||||
|
||||
/* translators: %1$s expands to "SEO title" or "meta description", %2$s expands to an opening link tag, %3$s expands to an opening strong tag, %4$s expands to a closing strong tag, %5$s expands to a closing link tag. */
|
||||
$message = ( $this->product_helper->is_premium() ) ? \esc_html__( 'Your recent posts are using default %1$s, which can make them easy to overlook in search results. Update them manually or %2$sfind out how %3$sYoast AI Generate%4$s can improve them for you.%5$s', 'wordpress-seo' ) : \esc_html__( 'Your recent posts are using default %1$s, which can make them easy to overlook in search results. Update them for better visibility or %2$stry %3$sYoast AI Generate%4$s for free to do it faster.%5$s', 'wordpress-seo' );
|
||||
|
||||
return \sprintf(
|
||||
$message,
|
||||
$default_seo_data,
|
||||
'<a href="' . \esc_url( $shortlink ) . '" target="_blank">',
|
||||
'<strong>',
|
||||
'</strong>',
|
||||
'</a>'
|
||||
);
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\Alerts\Application\Ping_Other_Admins;
|
||||
|
||||
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Product_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Short_Link_Helper;
|
||||
use Yoast\WP\SEO\Helpers\User_Helper;
|
||||
use Yoast\WP\SEO\Integrations\Integration_Interface;
|
||||
use Yoast_Notification;
|
||||
use Yoast_Notification_Center;
|
||||
|
||||
/**
|
||||
* Ping_Other_Admins_Alert class.
|
||||
*/
|
||||
class Ping_Other_Admins_Alert implements Integration_Interface {
|
||||
|
||||
public const NOTIFICATION_ID = 'wpseo-ping-other-admins';
|
||||
|
||||
/**
|
||||
* The notifications center.
|
||||
*
|
||||
* @var Yoast_Notification_Center
|
||||
*/
|
||||
private $notification_center;
|
||||
|
||||
/**
|
||||
* The short link helper.
|
||||
*
|
||||
* @var Short_Link_Helper
|
||||
*/
|
||||
private $short_link_helper;
|
||||
|
||||
/**
|
||||
* The product helper.
|
||||
*
|
||||
* @var Product_Helper
|
||||
*/
|
||||
private $product_helper;
|
||||
|
||||
/**
|
||||
* The options helper.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
private $options_helper;
|
||||
|
||||
/**
|
||||
* The user helper.
|
||||
*
|
||||
* @var User_Helper
|
||||
*/
|
||||
private $user_helper;
|
||||
|
||||
/**
|
||||
* Ping_Other_Admins_Alert constructor.
|
||||
*
|
||||
* @param Yoast_Notification_Center $notification_center The notification center.
|
||||
* @param Short_Link_Helper $short_link_helper The short link helper.
|
||||
* @param Product_Helper $product_helper The product helper.
|
||||
* @param Options_Helper $options_helper The options helper.
|
||||
* @param User_Helper $user_helper The user helper.
|
||||
*/
|
||||
public function __construct(
|
||||
Yoast_Notification_Center $notification_center,
|
||||
Short_Link_Helper $short_link_helper,
|
||||
Product_Helper $product_helper,
|
||||
Options_Helper $options_helper,
|
||||
User_Helper $user_helper
|
||||
) {
|
||||
$this->notification_center = $notification_center;
|
||||
$this->short_link_helper = $short_link_helper;
|
||||
$this->product_helper = $product_helper;
|
||||
$this->options_helper = $options_helper;
|
||||
$this->user_helper = $user_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the conditionals based on which this loadable should be active.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function get_conditionals() {
|
||||
return [ Admin_Conditional::class ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the integration.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_hooks() {
|
||||
// @phpcs:ignore Squiz.PHP.CommentedOutCode.Found, Squiz.Commenting.InlineComment.InvalidEndChar -- we're gonna postpone this notification until we're actually ready for it.
|
||||
// \add_action( 'admin_init', [ $this, 'add_notifications' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds notification when user has not installed Yoast SEO themselves and has not resolved the notification yet.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_notifications() {
|
||||
if ( $this->has_user_installed_yoast() ) {
|
||||
$this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $this->has_notification_been_resolved() ) {
|
||||
$this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID );
|
||||
return;
|
||||
}
|
||||
|
||||
$notification = $this->get_ping_other_admins_notification();
|
||||
|
||||
$this->notification_center->add_notification( $notification );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether user has installed Yoast SEO themselves.
|
||||
*
|
||||
* @return bool Whether the user has installed Yoast SEO themselves.
|
||||
*/
|
||||
private function has_user_installed_yoast(): bool {
|
||||
$first_activated_by = $this->options_helper->get( 'first_activated_by', 0 );
|
||||
|
||||
if ( $first_activated_by === 0 ) {
|
||||
return true; // We cannot be sure, so we assume they did.
|
||||
}
|
||||
|
||||
if ( \get_current_user_id() === $first_activated_by ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the alert has been resolved before.
|
||||
*
|
||||
* @return bool Whether the alert has been resolved before.
|
||||
*/
|
||||
private function has_notification_been_resolved(): bool {
|
||||
return $this->user_helper->get_meta( \get_current_user_id(), self::NOTIFICATION_ID . '_resolved', true ) === '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the ping-other-admins notification.
|
||||
*
|
||||
* @return Yoast_Notification The ping-other-admins notification.
|
||||
*/
|
||||
private function get_ping_other_admins_notification(): Yoast_Notification {
|
||||
$message = $this->get_message();
|
||||
|
||||
return new Yoast_Notification(
|
||||
$message,
|
||||
[
|
||||
'id' => self::NOTIFICATION_ID,
|
||||
'type' => Yoast_Notification::WARNING,
|
||||
'capabilities' => [ 'wpseo_manage_options' ],
|
||||
'priority' => 20,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the notification as an HTML string.
|
||||
*
|
||||
* @return string The HTML string representation of the notification.
|
||||
*/
|
||||
private function get_message() {
|
||||
$shortlink = $this->short_link_helper->get( 'https://yoa.st/new-admin-newsletter-sign-up/' );
|
||||
|
||||
$message = \sprintf(
|
||||
/* translators: %1$s and %3$s expands to "Yoast SEO" , %2$s expands to an opening link tag, %4$s expands to a closing link tag. */
|
||||
\esc_html__( 'Looks like you’re new here. %1$s makes it easy to optimize your website for search engines. Want to keep your site healthy and easier to find? %2$sSign up for the %3$s newsletter for short, practical weekly tips%4$s.', 'wordpress-seo' ),
|
||||
'Yoast SEO',
|
||||
'<a href="' . \esc_url( $shortlink ) . '" target="_blank">',
|
||||
'Yoast SEO',
|
||||
'</a>'
|
||||
);
|
||||
|
||||
$notification_text = '<p>' . $message . '</p>';
|
||||
$notification_text .= '<a class="button wpseo-resolve-alert" href="#" data-alert-id="' . \esc_attr( self::NOTIFICATION_ID ) . '" data-nonce="' . \esc_attr( \wp_create_nonce( 'wpseo-resolve-alert-nonce' ) ) . '">';
|
||||
$notification_text .= \esc_html__( 'Dismiss', 'wordpress-seo' );
|
||||
$notification_text .= '</a>';
|
||||
|
||||
return $notification_text;
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
|
||||
namespace Yoast\WP\SEO\Alerts\Infrastructure\Default_SEO_Data;
|
||||
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
|
||||
/**
|
||||
* Class that collects default SEO data.
|
||||
*/
|
||||
class Default_SEO_Data_Collector {
|
||||
|
||||
/**
|
||||
* Holds the Options_Helper instance.
|
||||
*
|
||||
* @var 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the posts with default SEO title in their most recent.
|
||||
*
|
||||
* @return string[] The posts with default SEO title in their most recent.
|
||||
*/
|
||||
public function get_posts_with_default_seo_title(): array {
|
||||
return $this->options_helper->get( 'default_seo_title', [] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the posts with default SEO description in their most recent.
|
||||
*
|
||||
* @return string[] The posts with default SEO description in their most recent.
|
||||
*/
|
||||
public function get_posts_with_default_seo_description(): array {
|
||||
return $this->options_helper->get( 'default_seo_meta_desc', [] );
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user