Current File : /home/campusedgeraleigh/webapps/campusedgeraleigh//actions.tar
wincher/wincher-account-action.php 0000644 00000004473 14672432024 0013274 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\Wincher;
use Yoast\WP\SEO\Config\Wincher_Client;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Class Wincher_Account_Action
*/
class Wincher_Account_Action {
const ACCOUNT_URL = 'https://api.wincher.com/beta/account';
const UPGRADE_CAMPAIGN_URL = 'https://api.wincher.com/v1/yoast/upgrade-campaign';
/**
* The Wincher_Client instance.
*
* @var Wincher_Client
*/
protected $client;
/**
* The Options_Helper instance.
*
* @var Options_Helper
*/
protected $options_helper;
/**
* Wincher_Account_Action constructor.
*
* @param Wincher_Client $client The API client.
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Wincher_Client $client, Options_Helper $options_helper ) {
$this->client = $client;
$this->options_helper = $options_helper;
}
/**
* Checks the account limit for tracking keyphrases.
*
* @return object The response object.
*/
public function check_limit() {
// Code has already been validated at this point. No need to do that again.
try {
$results = $this->client->get( self::ACCOUNT_URL );
$usage = $results['limits']['keywords']['usage'];
$limit = $results['limits']['keywords']['limit'];
return (object) [
'canTrack' => \is_null( $limit ) || $usage < $limit,
'limit' => $limit,
'usage' => $usage,
'status' => 200,
];
} catch ( \Exception $e ) {
return (object) [
'status' => $e->getCode(),
'error' => $e->getMessage(),
];
}
}
/**
* Gets the upgrade campaign.
*
* @return object The response object.
*/
public function get_upgrade_campaign() {
try {
$result = $this->client->get( self::UPGRADE_CAMPAIGN_URL );
$type = $result['type'];
$months = $result['months'];
// We display upgrade discount only if it's a rate discount and positive months.
if ( $type === 'RATE' && $months && $months > 0 ) {
$discount = $result['value'];
return (object) [
'discount' => $discount,
'months' => $months,
'status' => 200,
];
}
return (object) [
'discount' => null,
'months' => null,
'status' => 200,
];
} catch ( \Exception $e ) {
return (object) [
'status' => $e->getCode(),
'error' => $e->getMessage(),
];
}
}
}
wincher/wincher-keyphrases-action.php 0000644 00000022004 14672432024 0014004 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\Wincher;
use Exception;
use WP_Post;
use WPSEO_Utils;
use Yoast\WP\SEO\Config\Wincher_Client;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* Class Wincher_Keyphrases_Action
*/
class Wincher_Keyphrases_Action {
/**
* The Wincher keyphrase URL for bulk addition.
*
* @var string
*/
const KEYPHRASES_ADD_URL = 'https://api.wincher.com/beta/websites/%s/keywords/bulk';
/**
* The Wincher tracked keyphrase retrieval URL.
*
* @var string
*/
const KEYPHRASES_URL = 'https://api.wincher.com/beta/yoast/%s';
/**
* The Wincher delete tracked keyphrase URL.
*
* @var string
*/
const KEYPHRASE_DELETE_URL = 'https://api.wincher.com/beta/websites/%s/keywords/%s';
/**
* The Wincher_Client instance.
*
* @var Wincher_Client
*/
protected $client;
/**
* The Options_Helper instance.
*
* @var Options_Helper
*/
protected $options_helper;
/**
* The Indexable_Repository instance.
*
* @var Indexable_Repository
*/
protected $indexable_repository;
/**
* Wincher_Keyphrases_Action constructor.
*
* @param Wincher_Client $client The API client.
* @param Options_Helper $options_helper The options helper.
* @param Indexable_Repository $indexable_repository The indexables repository.
*/
public function __construct(
Wincher_Client $client,
Options_Helper $options_helper,
Indexable_Repository $indexable_repository
) {
$this->client = $client;
$this->options_helper = $options_helper;
$this->indexable_repository = $indexable_repository;
}
/**
* Sends the tracking API request for one or more keyphrases.
*
* @param string|array $keyphrases One or more keyphrases that should be tracked.
* @param Object $limits The limits API call response data.
*
* @return Object The reponse object.
*/
public function track_keyphrases( $keyphrases, $limits ) {
try {
$endpoint = \sprintf(
self::KEYPHRASES_ADD_URL,
$this->options_helper->get( 'wincher_website_id' )
);
// Enforce arrrays to ensure a consistent way of preparing the request.
if ( ! \is_array( $keyphrases ) ) {
$keyphrases = [ $keyphrases ];
}
// Calculate if the user would exceed their limit.
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- To ensure JS code style, this can be ignored.
if ( ! $limits->canTrack || $this->would_exceed_limits( $keyphrases, $limits ) ) {
$response = [
'limit' => $limits->limit,
'error' => 'Account limit exceeded',
'status' => 400,
];
return $this->to_result_object( $response );
}
$formatted_keyphrases = \array_values(
\array_map(
static function ( $keyphrase ) {
return [
'keyword' => $keyphrase,
'groups' => [],
];
},
$keyphrases
)
);
$results = $this->client->post( $endpoint, WPSEO_Utils::format_json_encode( $formatted_keyphrases ) );
if ( ! \array_key_exists( 'data', $results ) ) {
return $this->to_result_object( $results );
}
// The endpoint returns a lot of stuff that we don't want/need.
$results['data'] = \array_map(
static function( $keyphrase ) {
return [
'id' => $keyphrase['id'],
'keyword' => $keyphrase['keyword'],
];
},
$results['data']
);
$results['data'] = \array_combine(
\array_column( $results['data'], 'keyword' ),
\array_values( $results['data'] )
);
return $this->to_result_object( $results );
} catch ( Exception $e ) {
return (object) [
'error' => $e->getMessage(),
'status' => $e->getCode(),
];
}
}
/**
* Sends an untrack request for the passed keyword ID.
*
* @param int $keyphrase_id The ID of the keyphrase to untrack.
*
* @return object The response object.
*/
public function untrack_keyphrase( $keyphrase_id ) {
try {
$endpoint = \sprintf(
self::KEYPHRASE_DELETE_URL,
$this->options_helper->get( 'wincher_website_id' ),
$keyphrase_id
);
$this->client->delete( $endpoint );
return (object) [
'status' => 200,
];
} catch ( Exception $e ) {
return (object) [
'error' => $e->getMessage(),
'status' => $e->getCode(),
];
}
}
/**
* Gets the keyphrase data for the passed keyphrases.
* Retrieves all available data if no keyphrases are provided.
*
* @param array|null $used_keyphrases The currently used keyphrases. Optional.
* @param string|null $permalink The current permalink. Optional.
*
* @return object The keyphrase chart data.
*/
public function get_tracked_keyphrases( $used_keyphrases = null, $permalink = null ) {
try {
if ( $used_keyphrases === null ) {
$used_keyphrases = $this->collect_all_keyphrases();
}
// If we still have no keyphrases the API will return an error, so
// don't even bother sending a request.
if ( empty( $used_keyphrases ) ) {
return $this->to_result_object(
[
'data' => [],
'status' => 200,
]
);
}
$endpoint = \sprintf(
self::KEYPHRASES_URL,
$this->options_helper->get( 'wincher_website_id' )
);
$results = $this->client->post(
$endpoint,
WPSEO_Utils::format_json_encode(
[
'keywords' => $used_keyphrases,
'url' => $permalink,
]
),
[
'timeout' => 60,
]
);
if ( ! \array_key_exists( 'data', $results ) ) {
return $this->to_result_object( $results );
}
$results['data'] = $this->filter_results_by_used_keyphrases( $results['data'], $used_keyphrases );
// Extract the positional data and assign it to the keyphrase.
$results['data'] = \array_combine(
\array_column( $results['data'], 'keyword' ),
\array_values( $results['data'] )
);
return $this->to_result_object( $results );
} catch ( Exception $e ) {
return (object) [
'error' => $e->getMessage(),
'status' => $e->getCode(),
];
}
}
/**
* Collects the keyphrases associated with the post.
*
* @param WP_Post $post The post object.
*
* @return array The keyphrases.
*/
public function collect_keyphrases_from_post( $post ) {
$keyphrases = [];
$primary_keyphrase = $this->indexable_repository
->query()
->select( 'primary_focus_keyword' )
->where( 'object_id', $post->ID )
->find_one();
if ( $primary_keyphrase ) {
$keyphrases[] = $primary_keyphrase->primary_focus_keyword;
}
/**
* Filters the keyphrases collected by the Wincher integration from the post.
*
* @param array $keyphrases The keyphrases array.
* @param int $post_id The ID of the post.
*/
return \apply_filters( 'wpseo_wincher_keyphrases_from_post', $keyphrases, $post->ID );
}
/**
* Collects all keyphrases known to Yoast.
*
* @return array
*/
protected function collect_all_keyphrases() {
// Collect primary keyphrases first.
$keyphrases = \array_column(
$this->indexable_repository
->query()
->select( 'primary_focus_keyword' )
->where_not_null( 'primary_focus_keyword' )
->where( 'object_type', 'post' )
->where_not_equal( 'post_status', 'trash' )
->distinct()
->find_array(),
'primary_focus_keyword'
);
/**
* Filters the keyphrases collected by the Wincher integration from all the posts.
*
* @param array $keyphrases The keyphrases array.
*/
$keyphrases = \apply_filters( 'wpseo_wincher_all_keyphrases', $keyphrases );
// Filter out empty entries.
return \array_filter( $keyphrases );
}
/**
* Filters the results based on the passed keyphrases.
*
* @param array $results The results to filter.
* @param array $used_keyphrases The used keyphrases.
*
* @return array The filtered results.
*/
protected function filter_results_by_used_keyphrases( $results, $used_keyphrases ) {
return \array_filter(
$results,
static function( $result ) use ( $used_keyphrases ) {
return \in_array( $result['keyword'], \array_map( 'strtolower', $used_keyphrases ), true );
}
);
}
/**
* Determines whether the amount of keyphrases would mean the user exceeds their account limits.
*
* @param string|array $keyphrases The keyphrases to be added.
* @param object $limits The current account limits.
*
* @return bool Whether the limit is exceeded.
*/
protected function would_exceed_limits( $keyphrases, $limits ) {
if ( ! \is_array( $keyphrases ) ) {
$keyphrases = [ $keyphrases ];
}
if ( \is_null( $limits->limit ) ) {
return false;
}
return ( \count( $keyphrases ) + $limits->usage ) > $limits->limit;
}
/**
* Converts the passed dataset to an object.
*
* @param array $result The result dataset to convert to an object.
*
* @return object The result object.
*/
protected function to_result_object( $result ) {
if ( \array_key_exists( 'data', $result ) ) {
$result['results'] = (object) $result['data'];
unset( $result['data'] );
}
if ( \array_key_exists( 'message', $result ) ) {
$result['error'] = $result['message'];
unset( $result['message'] );
}
return (object) $result;
}
}
wincher/wincher-login-action.php 0000644 00000003336 14672432024 0012745 0 ustar 00 <?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();
}
}
}
addon-installation/addon-install-action.php 0000644 00000007562 14672432024 0015063 0 ustar 00 <?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 );
}
}
addon-installation/addon-activate-action.php 0000644 00000004522 14672432024 0015206 0 ustar 00 <?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' );
}
}
}
importing/abstract-aioseo-importing-action.php 0000644 00000015266 14672432024 0015644 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\Importing;
use Exception;
use Yoast\WP\SEO\Helpers\Aioseo_Helper;
use Yoast\WP\SEO\Helpers\Import_Cursor_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Sanitization_Helper;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Provider_Service;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Transformer_Service;
/**
* Importing action interface.
*/
abstract class Abstract_Aioseo_Importing_Action implements Importing_Action_Interface {
/**
* The plugin the class deals with.
*
* @var string
*/
const PLUGIN = null;
/**
* The type the class deals with.
*
* @var string
*/
const TYPE = null;
/**
* The AIOSEO helper.
*
* @var Aioseo_Helper
*/
protected $aioseo_helper;
/**
* The import cursor helper.
*
* @var Import_Cursor_Helper
*/
protected $import_cursor;
/**
* The options helper.
*
* @var Options_Helper
*/
protected $options;
/**
* The sanitization helper.
*
* @var Sanitization_Helper
*/
protected $sanitization;
/**
* The replacevar handler.
*
* @var Aioseo_Replacevar_Service
*/
protected $replacevar_handler;
/**
* The robots provider service.
*
* @var Aioseo_Robots_Provider_Service
*/
protected $robots_provider;
/**
* The robots transformer service.
*
* @var Aioseo_Robots_Transformer_Service
*/
protected $robots_transformer;
/**
* Abstract_Aioseo_Importing_Action constructor.
*
* @param Import_Cursor_Helper $import_cursor The import cursor helper.
* @param Options_Helper $options The options helper.
* @param Sanitization_Helper $sanitization The sanitization helper.
* @param Aioseo_Replacevar_Service $replacevar_handler The replacevar handler.
* @param Aioseo_Robots_Provider_Service $robots_provider The robots provider service.
* @param Aioseo_Robots_Transformer_Service $robots_transformer The robots transfomer service.
*/
public function __construct(
Import_Cursor_Helper $import_cursor,
Options_Helper $options,
Sanitization_Helper $sanitization,
Aioseo_Replacevar_Service $replacevar_handler,
Aioseo_Robots_Provider_Service $robots_provider,
Aioseo_Robots_Transformer_Service $robots_transformer
) {
$this->import_cursor = $import_cursor;
$this->options = $options;
$this->sanitization = $sanitization;
$this->replacevar_handler = $replacevar_handler;
$this->robots_provider = $robots_provider;
$this->robots_transformer = $robots_transformer;
}
/**
* Sets the AIOSEO helper.
*
* @required
*
* @param Aioseo_Helper $aioseo_helper The AIOSEO helper.
*/
public function set_aioseo_helper( Aioseo_Helper $aioseo_helper ) {
$this->aioseo_helper = $aioseo_helper;
}
/**
* The name of the plugin we import from.
*
* @return string The plugin we import from.
*
* @throws Exception If the PLUGIN constant is not set in the child class.
*/
public function get_plugin() {
$class = \get_class( $this );
$plugin = $class::PLUGIN;
if ( $plugin === null ) {
throw new Exception( 'Importing action without explicit plugin' );
}
return $plugin;
}
/**
* The data type we import from the plugin.
*
* @return string The data type we import from the plugin.
*
* @throws Exception If the TYPE constant is not set in the child class.
*/
public function get_type() {
$class = \get_class( $this );
$type = $class::TYPE;
if ( $type === null ) {
throw new Exception( 'Importing action without explicit type' );
}
return $type;
}
/**
* Can the current action import the data from plugin $plugin of type $type?
*
* @param string|null $plugin The plugin to import from.
* @param string|null $type The type of data to import.
*
* @return bool True if this action can handle the combination of Plugin and Type.
*
* @throws Exception If the TYPE constant is not set in the child class.
*/
public function is_compatible_with( $plugin = null, $type = null ) {
if ( empty( $plugin ) && empty( $type ) ) {
return true;
}
if ( $plugin === $this->get_plugin() && empty( $type ) ) {
return true;
}
if ( empty( $plugin ) && $type === $this->get_type() ) {
return true;
}
if ( $plugin === $this->get_plugin() && $type === $this->get_type() ) {
return true;
}
return false;
}
/**
* Gets the completed id (to be used as a key for the importing_completed option).
*
* @return string The completed id.
*/
public function get_completed_id() {
return $this->get_cursor_id();
}
/**
* Returns the stored state of completedness.
*
* @return int The stored state of completedness.
*/
public function get_completed() {
$completed_id = $this->get_completed_id();
$importers_completions = $this->options->get( 'importing_completed', [] );
return ( isset( $importers_completions[ $completed_id ] ) ) ? $importers_completions[ $completed_id ] : false;
}
/**
* Stores the current state of completedness.
*
* @param bool $completed Whether the importer is completed.
*
* @return void
*/
public function set_completed( $completed ) {
$completed_id = $this->get_completed_id();
$current_importers_completions = $this->options->get( 'importing_completed', [] );
$current_importers_completions[ $completed_id ] = $completed;
$this->options->set( 'importing_completed', $current_importers_completions );
}
/**
* Returns whether the importing action is enabled.
*
* @return bool True by default unless a child class overrides it.
*/
public function is_enabled() {
return true;
}
/**
* Gets the cursor id.
*
* @return string The cursor id.
*/
protected function get_cursor_id() {
return $this->get_plugin() . '_' . $this->get_type();
}
/**
* Minimally transforms data to be imported.
*
* @param string $meta_data The meta data to be imported.
*
* @return string The transformed meta data.
*/
public function simple_import( $meta_data ) {
// Transform the replace vars into Yoast replace vars.
$transformed_data = $this->replacevar_handler->transform( $meta_data );
return $this->sanitization->sanitize_text_field( \html_entity_decode( $transformed_data ) );
}
/**
* Transforms URL to be imported.
*
* @param string $meta_data The meta data to be imported.
*
* @return string The transformed URL.
*/
public function url_import( $meta_data ) {
// We put null as the allowed protocols here, to have the WP default allowed protocols, see https://developer.wordpress.org/reference/functions/wp_allowed_protocols.
return $this->sanitization->sanitize_url( $meta_data, null );
}
}
importing/aioseo/aioseo-default-archive-settings-importing-action.php 0000644 00000005563 14672432024 0022220 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
/**
* Importing action for AIOSEO default archive settings data.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Aioseo_Default_Archive_Settings_Importing_Action extends Abstract_Aioseo_Settings_Importing_Action {
/**
* The plugin of the action.
*/
const PLUGIN = 'aioseo';
/**
* The type of the action.
*/
const TYPE = 'default_archive_settings';
/**
* The option_name of the AIOSEO option that contains the settings.
*/
const SOURCE_OPTION_NAME = 'aioseo_options';
/**
* The map of aioseo_options to yoast settings.
*
* @var array
*/
protected $aioseo_options_to_yoast_map = [];
/**
* The tab of the aioseo settings we're working with.
*
* @var string
*/
protected $settings_tab = 'archives';
/**
* Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method.
*
* @return void
*/
protected function build_mapping() {
$this->aioseo_options_to_yoast_map = [
'/author/title' => [
'yoast_name' => 'title-author-wpseo',
'transform_method' => 'simple_import',
],
'/author/metaDescription' => [
'yoast_name' => 'metadesc-author-wpseo',
'transform_method' => 'simple_import',
],
'/date/title' => [
'yoast_name' => 'title-archive-wpseo',
'transform_method' => 'simple_import',
],
'/date/metaDescription' => [
'yoast_name' => 'metadesc-archive-wpseo',
'transform_method' => 'simple_import',
],
'/search/title' => [
'yoast_name' => 'title-search-wpseo',
'transform_method' => 'simple_import',
],
'/author/advanced/robotsMeta/noindex' => [
'yoast_name' => 'noindex-author-wpseo',
'transform_method' => 'import_noindex',
'type' => 'archives',
'subtype' => 'author',
'option_name' => 'aioseo_options',
],
'/date/advanced/robotsMeta/noindex' => [
'yoast_name' => 'noindex-archive-wpseo',
'transform_method' => 'import_noindex',
'type' => 'archives',
'subtype' => 'date',
'option_name' => 'aioseo_options',
],
];
}
/**
* Returns a setting map of the robot setting for author archives.
*
* @return array The setting map of the robot setting for author archives.
*/
public function pluck_robot_setting_from_mapping() {
$this->build_mapping();
foreach ( $this->aioseo_options_to_yoast_map as $setting ) {
// Return the first archive setting map.
if ( $setting['transform_method'] === 'import_noindex' && isset( $setting['subtype'] ) && $setting['subtype'] === 'author' ) {
return $setting;
}
}
return [];
}
}
importing/aioseo/aioseo-taxonomy-settings-importing-action.php 0000644 00000007620 14672432024 0021027 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
/**
* Importing action for AIOSEO taxonomies settings data.
*/
class Aioseo_Taxonomy_Settings_Importing_Action extends Abstract_Aioseo_Settings_Importing_Action {
/**
* The plugin of the action.
*/
const PLUGIN = 'aioseo';
/**
* The type of the action.
*/
const TYPE = 'taxonomy_settings';
/**
* The option_name of the AIOSEO option that contains the settings.
*/
const SOURCE_OPTION_NAME = 'aioseo_options_dynamic';
/**
* The map of aioseo_options to yoast settings.
*
* @var array
*/
protected $aioseo_options_to_yoast_map = [];
/**
* The tab of the aioseo settings we're working with.
*
* @var string
*/
protected $settings_tab = 'taxonomies';
/**
* Additional mapping between AiOSEO replace vars and Yoast replace vars.
*
* @var array
*
* @see https://yoast.com/help/list-available-snippet-variables-yoast-seo/
*/
protected $replace_vars_edited_map = [
'#breadcrumb_404_error_format' => '', // Empty string, as AIOSEO shows nothing for that tag.
'#breadcrumb_archive_post_type_format' => '', // Empty string, as AIOSEO shows nothing for that tag.
'#breadcrumb_archive_post_type_name' => '', // Empty string, as AIOSEO shows nothing for that tag.
'#breadcrumb_author_display_name' => '', // Empty string, as AIOSEO shows nothing for that tag.
'#breadcrumb_author_first_name' => '', // Empty string, as AIOSEO shows nothing for that tag.
'#breadcrumb_blog_page_title' => '', // Empty string, as AIOSEO shows nothing for that tag.
'#breadcrumb_label' => '', // Empty string, as AIOSEO shows nothing for that tag.
'#breadcrumb_link' => '', // Empty string, as AIOSEO shows nothing for that tag.
'#breadcrumb_search_result_format' => '', // Empty string, as AIOSEO shows nothing for that tag.
'#breadcrumb_search_string' => '', // Empty string, as AIOSEO shows nothing for that tag.
'#breadcrumb_separator' => '', // Empty string, as AIOSEO shows nothing for that tag.
'#breadcrumb_taxonomy_title' => '', // Empty string, as AIOSEO shows nothing for that tag.
'#taxonomy_title' => '%%term_title%%',
];
/**
* Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method.
*
* @return void
*/
protected function build_mapping() {
$taxonomy_objects = \get_taxonomies( [ 'public' => true ], 'object' );
foreach ( $taxonomy_objects as $tax ) {
// Use all the public taxonomies.
$this->aioseo_options_to_yoast_map[ '/' . $tax->name . '/title' ] = [
'yoast_name' => 'title-tax-' . $tax->name,
'transform_method' => 'simple_import',
];
$this->aioseo_options_to_yoast_map[ '/' . $tax->name . '/metaDescription' ] = [
'yoast_name' => 'metadesc-tax-' . $tax->name,
'transform_method' => 'simple_import',
];
$this->aioseo_options_to_yoast_map[ '/' . $tax->name . '/advanced/robotsMeta/noindex' ] = [
'yoast_name' => 'noindex-tax-' . $tax->name,
'transform_method' => 'import_noindex',
'type' => 'taxonomies',
'subtype' => $tax->name,
'option_name' => 'aioseo_options_dynamic',
];
}
}
/**
* Returns a setting map of the robot setting for post category taxonomies.
*
* @return array The setting map of the robot setting for post category taxonomies.
*/
public function pluck_robot_setting_from_mapping() {
$this->build_mapping();
foreach ( $this->aioseo_options_to_yoast_map as $setting ) {
// Return the first archive setting map.
if ( $setting['transform_method'] === 'import_noindex' && isset( $setting['subtype'] ) && $setting['subtype'] === 'category' ) {
return $setting;
}
}
return [];
}
}
importing/aioseo/aioseo-posts-importing-action.php 0000644 00000054622 14672432024 0016467 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
use wpdb;
use Yoast\WP\SEO\Actions\Importing\Abstract_Aioseo_Importing_Action;
use Yoast\WP\SEO\Helpers\Image_Helper;
use Yoast\WP\SEO\Helpers\Import_Cursor_Helper;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Indexable_To_Postmeta_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Sanitization_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Provider_Service;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Transformer_Service;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Social_Images_Provider_Service;
/**
* Importing action for AIOSEO post data.
*/
class Aioseo_Posts_Importing_Action extends Abstract_Aioseo_Importing_Action {
/**
* The plugin of the action.
*/
const PLUGIN = 'aioseo';
/**
* The type of the action.
*/
const TYPE = 'posts';
/**
* The map of aioseo to yoast meta.
*
* @var array
*/
protected $aioseo_to_yoast_map = [
'title' => [
'yoast_name' => 'title',
'transform_method' => 'simple_import_post',
],
'description' => [
'yoast_name' => 'description',
'transform_method' => 'simple_import_post',
],
'og_title' => [
'yoast_name' => 'open_graph_title',
'transform_method' => 'simple_import_post',
],
'og_description' => [
'yoast_name' => 'open_graph_description',
'transform_method' => 'simple_import_post',
],
'twitter_title' => [
'yoast_name' => 'twitter_title',
'transform_method' => 'simple_import_post',
'twitter_import' => true,
],
'twitter_description' => [
'yoast_name' => 'twitter_description',
'transform_method' => 'simple_import_post',
'twitter_import' => true,
],
'canonical_url' => [
'yoast_name' => 'canonical',
'transform_method' => 'url_import_post',
],
'keyphrases' => [
'yoast_name' => 'primary_focus_keyword',
'transform_method' => 'keyphrase_import',
],
'og_image_url' => [
'yoast_name' => 'open_graph_image',
'social_image_import' => true,
'social_setting_prefix_aioseo' => 'og_',
'social_setting_prefix_yoast' => 'open_graph_',
'transform_method' => 'social_image_url_import',
],
'twitter_image_url' => [
'yoast_name' => 'twitter_image',
'social_image_import' => true,
'social_setting_prefix_aioseo' => 'twitter_',
'social_setting_prefix_yoast' => 'twitter_',
'transform_method' => 'social_image_url_import',
],
'robots_noindex' => [
'yoast_name' => 'is_robots_noindex',
'transform_method' => 'post_robots_noindex_import',
'robots_import' => true,
],
'robots_nofollow' => [
'yoast_name' => 'is_robots_nofollow',
'transform_method' => 'post_general_robots_import',
'robots_import' => true,
'robot_type' => 'nofollow',
],
'robots_noarchive' => [
'yoast_name' => 'is_robots_noarchive',
'transform_method' => 'post_general_robots_import',
'robots_import' => true,
'robot_type' => 'noarchive',
],
'robots_nosnippet' => [
'yoast_name' => 'is_robots_nosnippet',
'transform_method' => 'post_general_robots_import',
'robots_import' => true,
'robot_type' => 'nosnippet',
],
'robots_noimageindex' => [
'yoast_name' => 'is_robots_noimageindex',
'transform_method' => 'post_general_robots_import',
'robots_import' => true,
'robot_type' => 'noimageindex',
],
];
/**
* Represents the indexables repository.
*
* @var Indexable_Repository
*/
protected $indexable_repository;
/**
* The WordPress database instance.
*
* @var wpdb
*/
protected $wpdb;
/**
* The image helper.
*
* @var Image_Helper
*/
protected $image;
/**
* The indexable_to_postmeta helper.
*
* @var Indexable_To_Postmeta_Helper
*/
protected $indexable_to_postmeta;
/**
* The indexable helper.
*
* @var Indexable_Helper
*/
protected $indexable_helper;
/**
* The social images provider service.
*
* @var Aioseo_Social_Images_Provider_Service
*/
protected $social_images_provider;
/**
* Class constructor.
*
* @param Indexable_Repository $indexable_repository The indexables repository.
* @param wpdb $wpdb The WordPress database instance.
* @param Import_Cursor_Helper $import_cursor The import cursor helper.
* @param Indexable_Helper $indexable_helper The indexable helper.
* @param Indexable_To_Postmeta_Helper $indexable_to_postmeta The indexable_to_postmeta helper.
* @param Options_Helper $options The options helper.
* @param Image_Helper $image The image helper.
* @param Sanitization_Helper $sanitization The sanitization helper.
* @param Aioseo_Replacevar_Service $replacevar_handler The replacevar handler.
* @param Aioseo_Robots_Provider_Service $robots_provider The robots provider service.
* @param Aioseo_Robots_Transformer_Service $robots_transformer The robots transfomer service.
* @param Aioseo_Social_Images_Provider_Service $social_images_provider The social images provider service.
*/
public function __construct(
Indexable_Repository $indexable_repository,
wpdb $wpdb,
Import_Cursor_Helper $import_cursor,
Indexable_Helper $indexable_helper,
Indexable_To_Postmeta_Helper $indexable_to_postmeta,
Options_Helper $options,
Image_Helper $image,
Sanitization_Helper $sanitization,
Aioseo_Replacevar_Service $replacevar_handler,
Aioseo_Robots_Provider_Service $robots_provider,
Aioseo_Robots_Transformer_Service $robots_transformer,
Aioseo_Social_Images_Provider_Service $social_images_provider ) {
parent::__construct( $import_cursor, $options, $sanitization, $replacevar_handler, $robots_provider, $robots_transformer );
$this->indexable_repository = $indexable_repository;
$this->wpdb = $wpdb;
$this->image = $image;
$this->indexable_helper = $indexable_helper;
$this->indexable_to_postmeta = $indexable_to_postmeta;
$this->social_images_provider = $social_images_provider;
}
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: They are already prepared.
/**
* Returns the total number of unimported objects.
*
* @return int The total number of unimported objects.
*/
public function get_total_unindexed() {
if ( ! $this->aioseo_helper->aioseo_exists() ) {
return 0;
}
$limit = false;
$just_detect = true;
$indexables_to_create = $this->wpdb->get_col( $this->query( $limit, $just_detect ) );
$number_of_indexables_to_create = \count( $indexables_to_create );
$completed = $number_of_indexables_to_create === 0;
$this->set_completed( $completed );
return $number_of_indexables_to_create;
}
/**
* Returns the limited number of unimported objects.
*
* @param int $limit The maximum number of unimported objects to be returned.
*
* @return int|false The limited number of unindexed posts. False if the query fails.
*/
public function get_limited_unindexed_count( $limit ) {
if ( ! $this->aioseo_helper->aioseo_exists() ) {
return 0;
}
$just_detect = true;
$indexables_to_create = $this->wpdb->get_col( $this->query( $limit, $just_detect ) );
$number_of_indexables_to_create = \count( $indexables_to_create );
$completed = $number_of_indexables_to_create === 0;
$this->set_completed( $completed );
return $number_of_indexables_to_create;
}
/**
* Imports AIOSEO meta data and creates the respective Yoast indexables and postmeta.
*
* @return Indexable[]|false An array of created indexables or false if aioseo data was not found.
*/
public function index() {
if ( ! $this->aioseo_helper->aioseo_exists() ) {
return false;
}
$limit = $this->get_limit();
$aioseo_indexables = $this->wpdb->get_results( $this->query( $limit ), \ARRAY_A );
$created_indexables = [];
$completed = \count( $aioseo_indexables ) === 0;
$this->set_completed( $completed );
// Let's build the list of fields to check their defaults, to identify whether we're gonna import AIOSEO data in the indexable or not.
$check_defaults_fields = [];
foreach ( $this->aioseo_to_yoast_map as $yoast_mapping ) {
// We don't want to check all the imported fields.
if ( ! \in_array( $yoast_mapping['yoast_name'], [ 'open_graph_image', 'twitter_image' ], true ) ) {
$check_defaults_fields[] = $yoast_mapping['yoast_name'];
}
}
$last_indexed_aioseo_id = 0;
foreach ( $aioseo_indexables as $aioseo_indexable ) {
$last_indexed_aioseo_id = $aioseo_indexable['id'];
$indexable = $this->indexable_repository->find_by_id_and_type( $aioseo_indexable['post_id'], 'post' );
// Let's ensure that the current post id represents something that we want to index (eg. *not* shop_order).
if ( ! \is_a( $indexable, 'Yoast\WP\SEO\Models\Indexable' ) ) {
continue;
}
if ( $this->indexable_helper->check_if_default_indexable( $indexable, $check_defaults_fields ) ) {
$indexable = $this->map( $indexable, $aioseo_indexable );
$indexable->save();
// To ensure that indexables can be rebuild after a reset, we have to store the data in the postmeta table too.
$this->indexable_to_postmeta->map_to_postmeta( $indexable );
}
$last_indexed_aioseo_id = $aioseo_indexable['id'];
$created_indexables[] = $indexable;
}
$cursor_id = $this->get_cursor_id();
$this->import_cursor->set_cursor( $cursor_id, $last_indexed_aioseo_id );
return $created_indexables;
}
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
/**
* Maps AIOSEO meta data to Yoast meta data.
*
* @param Indexable $indexable The Yoast indexable.
* @param array $aioseo_indexable The AIOSEO indexable.
*
* @return Indexable The created indexables.
*/
public function map( $indexable, $aioseo_indexable ) {
foreach ( $this->aioseo_to_yoast_map as $aioseo_key => $yoast_mapping ) {
// For robots import.
if ( isset( $yoast_mapping['robots_import'] ) && $yoast_mapping['robots_import'] ) {
$yoast_mapping['subtype'] = $indexable->object_sub_type;
$indexable->{$yoast_mapping['yoast_name']} = $this->transform_import_data( $yoast_mapping['transform_method'], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable );
continue;
}
// For social images, like open graph and twitter image.
if ( isset( $yoast_mapping['social_image_import'] ) && $yoast_mapping['social_image_import'] ) {
$image_url = $this->transform_import_data( $yoast_mapping['transform_method'], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable );
// Update the indexable's social image only where there's actually a url to import, so as not to lose the social images that we came up with when we originally built the indexable.
if ( ! empty( $image_url ) ) {
$indexable->{$yoast_mapping['yoast_name']} = $image_url;
$image_source_key = $yoast_mapping['social_setting_prefix_yoast'] . 'image_source';
$indexable->$image_source_key = 'imported';
$image_id_key = $yoast_mapping['social_setting_prefix_yoast'] . 'image_id';
$indexable->$image_id_key = $this->image->get_attachment_by_url( $image_url );
if ( $yoast_mapping['yoast_name'] === 'open_graph_image' ) {
$indexable->open_graph_image_meta = null;
}
}
continue;
}
// For twitter import, take the respective open graph data if the appropriate setting is enabled.
if ( isset( $yoast_mapping['twitter_import'] ) && $yoast_mapping['twitter_import'] && $aioseo_indexable['twitter_use_og'] ) {
$aioseo_indexable['twitter_title'] = $aioseo_indexable['og_title'];
$aioseo_indexable['twitter_description'] = $aioseo_indexable['og_description'];
}
if ( ! empty( $aioseo_indexable[ $aioseo_key ] ) ) {
$indexable->{$yoast_mapping['yoast_name']} = $this->transform_import_data( $yoast_mapping['transform_method'], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable );
}
}
return $indexable;
}
/**
* Transforms the data to be imported.
*
* @param string $transform_method The method that is going to be used for transforming the data.
* @param array $aioseo_indexable The data of the AIOSEO indexable data that is being imported.
* @param string $aioseo_key The name of the specific set of data that is going to be transformed.
* @param array $yoast_mapping Extra details for the import of the specific data that is going to be transformed.
* @param Indexable $indexable The Yoast indexable that we are going to import the transformed data into.
*
* @return string|bool|null The transformed data to be imported.
*/
protected function transform_import_data( $transform_method, $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable ) {
return \call_user_func( [ $this, $transform_method ], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable );
}
/**
* Returns the number of objects that will be imported in a single importing pass.
*
* @return int The limit.
*/
public function get_limit() {
/**
* Filter 'wpseo_aioseo_post_indexation_limit' - Allow filtering the number of posts indexed during each indexing pass.
*
* @api int The maximum number of posts indexed.
*/
$limit = \apply_filters( 'wpseo_aioseo_post_indexation_limit', 25 );
if ( ! \is_int( $limit ) || $limit < 1 ) {
$limit = 25;
}
return $limit;
}
/**
* Populates the needed data array based on which columns we use from the AIOSEO indexable table.
*
* @return array The needed data array that contains all the needed columns.
*/
public function get_needed_data() {
$needed_data = \array_keys( $this->aioseo_to_yoast_map );
\array_push( $needed_data, 'id', 'post_id', 'robots_default', 'og_image_custom_url', 'og_image_type', 'twitter_image_custom_url', 'twitter_image_type', 'twitter_use_og' );
return $needed_data;
}
/**
* Populates the needed robot data array to be used in validating against its structure.
*
* @return array The needed data array that contains all the needed columns.
*/
public function get_needed_robot_data() {
$needed_robot_data = [];
foreach ( $this->aioseo_to_yoast_map as $yoast_mapping ) {
if ( isset( $yoast_mapping['robot_type'] ) ) {
$needed_robot_data[] = $yoast_mapping['robot_type'];
}
}
return $needed_robot_data;
}
/**
* Creates a query for gathering AiOSEO data from the database.
*
* @param int $limit The maximum number of unimported objects to be returned.
* @param bool $just_detect Whether we want to just detect if there are unimported objects. If false, we want to actually import them too.
*
* @return string The query to use for importing or counting the number of items to import.
*/
public function query( $limit = false, $just_detect = false ) {
$table = $this->aioseo_helper->get_table();
$select_statement = 'id';
if ( ! $just_detect ) {
// If we want to import too, we need the actual needed data from AIOSEO indexables.
$needed_data = $this->get_needed_data();
$select_statement = \implode( ', ', $needed_data );
}
$cursor_id = $this->get_cursor_id();
$cursor = $this->import_cursor->get_cursor( $cursor_id );
/**
* Filter 'wpseo_aioseo_post_cursor' - Allow filtering the value of the aioseo post import cursor.
*
* @api int The value of the aioseo post import cursor.
*/
$cursor = \apply_filters( 'wpseo_aioseo_post_import_cursor', $cursor );
$replacements = [ $cursor ];
$limit_statement = '';
if ( ! empty( $limit ) ) {
$replacements[] = $limit;
$limit_statement = ' LIMIT %d';
}
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
return $this->wpdb->prepare(
"SELECT {$select_statement} FROM {$table} WHERE id > %d ORDER BY id{$limit_statement}",
$replacements
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
/**
* Minimally transforms data to be imported.
*
* @param array $aioseo_data All of the AIOSEO data to be imported.
* @param string $aioseo_key The AIOSEO key that contains the setting we're working with.
*
* @return string The transformed meta data.
*/
public function simple_import_post( $aioseo_data, $aioseo_key ) {
return $this->simple_import( $aioseo_data[ $aioseo_key ] );
}
/**
* Transforms URL to be imported.
*
* @param array $aioseo_data All of the AIOSEO data to be imported.
* @param string $aioseo_key The AIOSEO key that contains the setting we're working with.
*
* @return string The transformed URL.
*/
public function url_import_post( $aioseo_data, $aioseo_key ) {
return $this->url_import( $aioseo_data[ $aioseo_key ] );
}
/**
* Plucks the keyphrase to be imported from the AIOSEO array of keyphrase meta data.
*
* @param array $aioseo_data All of the AIOSEO data to be imported.
* @param string $aioseo_key The AIOSEO key that contains the setting we're working with, aka keyphrases.
*
* @return string|null The plucked keyphrase.
*/
public function keyphrase_import( $aioseo_data, $aioseo_key ) {
$meta_data = \json_decode( $aioseo_data[ $aioseo_key ], true );
if ( ! isset( $meta_data['focus']['keyphrase'] ) ) {
return null;
}
return $this->sanitization->sanitize_text_field( $meta_data['focus']['keyphrase'] );
}
/**
* Imports the post's noindex setting.
*
* @param bool $aioseo_robots_settings AIOSEO's set of robot settings for the post.
*
* @return bool|null The value of Yoast's noindex setting for the post.
*/
public function post_robots_noindex_import( $aioseo_robots_settings ) {
// If robot settings defer to default settings, we have null in the is_robots_noindex field.
if ( $aioseo_robots_settings['robots_default'] ) {
return null;
}
return $aioseo_robots_settings['robots_noindex'];
}
/**
* Imports the post's robots setting.
*
* @param bool $aioseo_robots_settings AIOSEO's set of robot settings for the post.
* @param string $aioseo_key The AIOSEO key that contains the robot setting we're working with.
* @param array $mapping The mapping of the setting we're working with.
*
* @return bool|null The value of Yoast's noindex setting for the post.
*/
public function post_general_robots_import( $aioseo_robots_settings, $aioseo_key, $mapping ) {
$mapping = $this->enhance_mapping( $mapping );
if ( $aioseo_robots_settings['robots_default'] ) {
// Let's first get the subtype's setting value and then transform it taking into consideration whether it defers to global defaults.
$subtype_setting = $this->robots_provider->get_subtype_robot_setting( $mapping );
return $this->robots_transformer->transform_robot_setting( $mapping['robot_type'], $subtype_setting, $mapping );
}
return $aioseo_robots_settings[ $aioseo_key ];
}
/**
* Enhances the mapping of the setting we're working with, with type and the option name, so that we can retrieve the settings for the object we're working with.
*
* @param array $mapping The mapping of the setting we're working with.
*
* @return array The enhanced mapping.
*/
public function enhance_mapping( $mapping = [] ) {
$mapping['type'] = 'postTypes';
$mapping['option_name'] = 'aioseo_options_dynamic';
return $mapping;
}
/**
* Imports the og and twitter image url.
*
* @param bool $aioseo_social_image_settings AIOSEO's set of social image settings for the post.
* @param string $aioseo_key The AIOSEO key that contains the robot setting we're working with.
* @param array $mapping The mapping of the setting we're working with.
* @param Indexable $indexable The Yoast indexable we're importing into.
*
* @return bool|null The url of the social image we're importing, null if there's none.
*/
public function social_image_url_import( $aioseo_social_image_settings, $aioseo_key, $mapping, $indexable ) {
if ( $mapping['social_setting_prefix_aioseo'] === 'twitter_' && $aioseo_social_image_settings['twitter_use_og'] ) {
$mapping['social_setting_prefix_aioseo'] = 'og_';
}
$social_setting = \rtrim( $mapping['social_setting_prefix_aioseo'], '_' );
$image_type = $aioseo_social_image_settings[ $mapping['social_setting_prefix_aioseo'] . 'image_type' ];
if ( $image_type === 'default' ) {
$image_type = $this->social_images_provider->get_default_social_image_source( $social_setting );
}
switch ( $image_type ) {
case 'attach':
$image_url = $this->social_images_provider->get_first_attached_image( $indexable->object_id );
break;
case 'auto':
if ( $this->social_images_provider->get_featured_image( $indexable->object_id ) ) {
// If there's a featured image, lets not import it, as our indexable calculation has already set that as active social image. That way we achieve dynamicality.
return null;
}
$image_url = $this->social_images_provider->get_auto_image( $indexable->object_id );
break;
case 'content':
$image_url = $this->social_images_provider->get_first_image_in_content( $indexable->object_id );
break;
case 'custom_image':
$image_url = $aioseo_social_image_settings[ $mapping['social_setting_prefix_aioseo'] . 'image_custom_url' ];
break;
case 'featured':
return null; // Our auto-calculation when the indexable was built/updated has taken care of it, so it's not needed to transfer any data now.
case 'author':
return null;
case 'custom':
return null;
case 'default':
$image_url = $this->social_images_provider->get_default_custom_social_image( $social_setting );
break;
default:
$image_url = $aioseo_social_image_settings[ $mapping['social_setting_prefix_aioseo'] . 'image_url' ];
break;
}
if ( empty( $image_url ) ) {
$image_url = $this->social_images_provider->get_default_custom_social_image( $social_setting );
}
if ( empty( $image_url ) ) {
return null;
}
return $this->sanitization->sanitize_url( $image_url, null );
}
}
importing/aioseo/aioseo-validate-data-action.php 0000644 00000021130 14672432024 0015775 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
use wpdb;
use Yoast\WP\SEO\Actions\Importing\Abstract_Aioseo_Importing_Action;
use Yoast\WP\SEO\Exceptions\Importing\Aioseo_Validation_Exception;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Importing action for validating AIOSEO data before the import occurs.
*/
class Aioseo_Validate_Data_Action extends Abstract_Aioseo_Importing_Action {
/**
* The plugin of the action.
*/
const PLUGIN = 'aioseo';
/**
* The type of the action.
*/
const TYPE = 'validate_data';
/**
* The WordPress database instance.
*
* @var wpdb
*/
protected $wpdb;
/**
* The Post Importing action.
*
* @var Aioseo_Posts_Importing_Action
*/
protected $post_importing_action;
/**
* The settings importing actions.
*
* @var array
*/
protected $settings_importing_actions;
/**
* Class constructor.
*
* @param wpdb $wpdb The WordPress database instance.
* @param Options_Helper $options The options helper.
* @param Aioseo_Custom_Archive_Settings_Importing_Action $custom_archive_action The Custom Archive Settings importing action.
* @param Aioseo_Default_Archive_Settings_Importing_Action $default_archive_action The Default Archive Settings importing action.
* @param Aioseo_General_Settings_Importing_Action $general_settings_action The General Settings importing action.
* @param Aioseo_Posttype_Defaults_Settings_Importing_Action $posttype_defaults_settings_action The Posttype Defaults Settings importing action.
* @param Aioseo_Taxonomy_Settings_Importing_Action $taxonomy_settings_action The Taxonomy Settings importing action.
* @param Aioseo_Posts_Importing_Action $post_importing_action The Post importing action.
*/
public function __construct(
wpdb $wpdb,
Options_Helper $options,
Aioseo_Custom_Archive_Settings_Importing_Action $custom_archive_action,
Aioseo_Default_Archive_Settings_Importing_Action $default_archive_action,
Aioseo_General_Settings_Importing_Action $general_settings_action,
Aioseo_Posttype_Defaults_Settings_Importing_Action $posttype_defaults_settings_action,
Aioseo_Taxonomy_Settings_Importing_Action $taxonomy_settings_action,
Aioseo_Posts_Importing_Action $post_importing_action
) {
$this->wpdb = $wpdb;
$this->options = $options;
$this->post_importing_action = $post_importing_action;
$this->settings_importing_actions = [
$custom_archive_action,
$default_archive_action,
$general_settings_action,
$posttype_defaults_settings_action,
$taxonomy_settings_action,
];
}
/**
* Just checks if the action has been completed in the past.
*
* @return int 1 if it hasn't been completed in the past, 0 if it has.
*/
public function get_total_unindexed() {
return ( ! $this->get_completed() ) ? 1 : 0;
}
/**
* Just checks if the action has been completed in the past.
*
* @param int $limit The maximum number of unimported objects to be returned. Not used, exists to comply with the interface.
*
* @return int 1 if it hasn't been completed in the past, 0 if it has.
*/
public function get_limited_unindexed_count( $limit ) {
return ( ! $this->get_completed() ) ? 1 : 0;
}
/**
* Validates AIOSEO data.
*
* @return array An array of validated data or false if aioseo data did not pass validation.
*
* @throws Aioseo_Validation_Exception If the validation fails.
*/
public function index() {
if ( $this->get_completed() ) {
return [];
}
$validated_aioseo_table = $this->validate_aioseo_table();
$validated_aioseo_settings = $this->validate_aioseo_settings();
$validated_robot_settings = $this->validate_robot_settings();
if ( $validated_aioseo_table === false || $validated_aioseo_settings === false || $validated_robot_settings === false ) {
throw new Aioseo_Validation_Exception();
}
$this->set_completed( true );
return [
'validated_aioseo_table' => $validated_aioseo_table,
'validated_aioseo_settings' => $validated_aioseo_settings,
'validated_robot_settings' => $validated_robot_settings,
];
}
/**
* Validates the AIOSEO indexable table.
*
* @return bool Whether the AIOSEO table exists and has the structure we expect.
*/
public function validate_aioseo_table() {
if ( ! $this->aioseo_helper->aioseo_exists() ) {
return false;
}
$table = $this->aioseo_helper->get_table();
$needed_data = $this->post_importing_action->get_needed_data();
$aioseo_columns = $this->wpdb->get_col(
"SHOW COLUMNS FROM {$table}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
0
);
return $needed_data === \array_intersect( $needed_data, $aioseo_columns );
}
/**
* Validates the AIOSEO settings from the options table.
*
* @return bool Whether the AIOSEO settings from the options table exist and have the structure we expect.
*/
public function validate_aioseo_settings() {
foreach ( $this->settings_importing_actions as $settings_import_action ) {
$aioseo_settings = \json_decode( \get_option( $settings_import_action->get_source_option_name(), '' ), true );
if ( ! $settings_import_action->isset_settings_tab( $aioseo_settings ) ) {
return false;
}
}
return true;
}
/**
* Validates the AIOSEO robots settings from the options table.
*
* @return bool Whether the AIOSEO robots settings from the options table exist and have the structure we expect.
*/
public function validate_robot_settings() {
if ( $this->validate_post_robot_settings() && $this->validate_default_robot_settings() ) {
return true;
}
return false;
}
/**
* Validates the post AIOSEO robots settings from the options table.
*
* @return bool Whether the post AIOSEO robots settings from the options table exist and have the structure we expect.
*/
public function validate_post_robot_settings() {
$post_robot_mapping = $this->post_importing_action->enhance_mapping();
// We're gonna validate against posttype robot settings only for posts, assuming the robot settings stay the same for other post types.
$post_robot_mapping['subtype'] = 'post';
// Let's get both the aioseo_options and the aioseo_options_dynamic options.
$aioseo_global_settings = $this->aioseo_helper->get_global_option();
$aioseo_posts_settings = \json_decode( \get_option( $post_robot_mapping['option_name'], '' ), true );
$needed_robots_data = $this->post_importing_action->get_needed_robot_data();
\array_push( $needed_robots_data, 'default', 'noindex' );
foreach ( $needed_robots_data as $robot_setting ) {
// Validate against global settings.
if ( ! isset( $aioseo_global_settings['searchAppearance']['advanced']['globalRobotsMeta'][ $robot_setting ] ) ) {
return false;
}
// Validate against posttype settings.
if ( ! isset( $aioseo_posts_settings['searchAppearance'][ $post_robot_mapping['type'] ][ $post_robot_mapping['subtype'] ]['advanced']['robotsMeta'][ $robot_setting ] ) ) {
return false;
}
}
return true;
}
/**
* Validates the default AIOSEO robots settings for search appearance settings from the options table.
*
* @return bool Whether the AIOSEO robots settings for search appearance settings from the options table exist and have the structure we expect.
*/
public function validate_default_robot_settings() {
foreach ( $this->settings_importing_actions as $settings_import_action ) {
$robot_setting_map = $settings_import_action->pluck_robot_setting_from_mapping();
// Some actions return empty robot settings, let's not validate against those.
if ( ! empty( $robot_setting_map ) ) {
$aioseo_settings = \json_decode( \get_option( $robot_setting_map['option_name'], '' ), true );
if ( ! isset( $aioseo_settings['searchAppearance'][ $robot_setting_map['type'] ][ $robot_setting_map['subtype'] ]['advanced']['robotsMeta']['default'] ) ) {
return false;
}
}
}
return true;
}
/**
* Used nowhere. Exists to comply with the interface.
*
* @return int The limit.
*/
public function get_limit() {
/**
* Filter 'wpseo_aioseo_cleanup_limit' - Allow filtering the number of validations during each action pass.
*
* @api int The maximum number of validations.
*/
$limit = \apply_filters( 'wpseo_aioseo_validation_limit', 25 );
if ( ! \is_int( $limit ) || $limit < 1 ) {
$limit = 25;
}
return $limit;
}
}
importing/aioseo/aioseo-posttype-defaults-settings-importing-action.php 0000644 00000005513 14672432024 0022644 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
/**
* Importing action for AIOSEO posttype defaults settings data.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Aioseo_Posttype_Defaults_Settings_Importing_Action extends Abstract_Aioseo_Settings_Importing_Action {
/**
* The plugin of the action.
*/
const PLUGIN = 'aioseo';
/**
* The type of the action.
*/
const TYPE = 'posttype_default_settings';
/**
* The option_name of the AIOSEO option that contains the settings.
*/
const SOURCE_OPTION_NAME = 'aioseo_options_dynamic';
/**
* The map of aioseo_options to yoast settings.
*
* @var array
*/
protected $aioseo_options_to_yoast_map = [];
/**
* The tab of the aioseo settings we're working with.
*
* @var string
*/
protected $settings_tab = 'postTypes';
/**
* Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method.
*
* @return void
*/
protected function build_mapping() {
$post_type_objects = \get_post_types( [ 'public' => true ], 'objects' );
foreach ( $post_type_objects as $pt ) {
// Use all the custom post types that are public.
$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/title' ] = [
'yoast_name' => 'title-' . $pt->name,
'transform_method' => 'simple_import',
];
$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/metaDescription' ] = [
'yoast_name' => 'metadesc-' . $pt->name,
'transform_method' => 'simple_import',
];
$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/advanced/showMetaBox' ] = [
'yoast_name' => 'display-metabox-pt-' . $pt->name,
'transform_method' => 'simple_boolean_import',
];
$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/advanced/robotsMeta/noindex' ] = [
'yoast_name' => 'noindex-' . $pt->name,
'transform_method' => 'import_noindex',
'type' => 'postTypes',
'subtype' => $pt->name,
'option_name' => 'aioseo_options_dynamic',
];
if ( $pt->name === 'attachment' ) {
$this->aioseo_options_to_yoast_map['/attachment/redirectAttachmentUrls'] = [
'yoast_name' => 'disable-attachment',
'transform_method' => 'import_redirect_attachment',
];
}
}
}
/**
* Transforms the redirect_attachment setting.
*
* @param string $redirect_attachment The redirect_attachment setting.
*
* @return bool The transformed redirect_attachment setting.
*/
public function import_redirect_attachment( $redirect_attachment ) {
switch ( $redirect_attachment ) {
case 'disabled':
return false;
case 'attachment':
case 'attachment_parent':
default:
return true;
}
}
}
importing/aioseo/abstract-aioseo-settings-importing-action.php 0000644 00000024500 14672432024 0020750 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
use Exception;
use Yoast\WP\SEO\Actions\Importing\Abstract_Aioseo_Importing_Action;
use Yoast\WP\SEO\Helpers\Import_Helper;
/**
* Abstract class for importing AIOSEO settings.
*/
abstract class Abstract_Aioseo_Settings_Importing_Action extends Abstract_Aioseo_Importing_Action {
/**
* The plugin the class deals with.
*
* @var string
*/
const PLUGIN = null;
/**
* The type the class deals with.
*
* @var string
*/
const TYPE = null;
/**
* The option_name of the AIOSEO option that contains the settings.
*/
const SOURCE_OPTION_NAME = null;
/**
* The map of aioseo_options to yoast settings.
*
* @var array
*/
protected $aioseo_options_to_yoast_map = [];
/**
* The tab of the aioseo settings we're working with, eg. taxonomies, posttypes.
*
* @var string
*/
protected $settings_tab = '';
/**
* Additional mapping between AiOSEO replace vars and Yoast replace vars.
*
* @var array
*
* @see https://yoast.com/help/list-available-snippet-variables-yoast-seo/
*/
protected $replace_vars_edited_map = [];
/**
* The import helper.
*
* @var Import_Helper
*/
protected $import_helper;
/**
* Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method.
*
* @return void
*/
abstract protected function build_mapping();
/**
* Sets the import helper.
*
* @required
*
* @param Import_Helper $import_helper The import helper.
*/
public function set_import_helper( Import_Helper $import_helper ) {
$this->import_helper = $import_helper;
}
/**
* Retrieves the source option_name.
*
* @return string The source option_name.
*
* @throws Exception If the SOURCE_OPTION_NAME constant is not set in the child class.
*/
public function get_source_option_name() {
$source_option_name = static::SOURCE_OPTION_NAME;
if ( empty( $source_option_name ) ) {
throw new Exception( 'Importing settings action without explicit source option_name' );
}
return $source_option_name;
}
/**
* Returns the total number of unimported objects.
*
* @return int The total number of unimported objects.
*/
public function get_total_unindexed() {
return $this->get_unindexed_count();
}
/**
* Returns the limited number of unimported objects.
*
* @param int $limit The maximum number of unimported objects to be returned.
*
* @return int The limited number of unindexed posts.
*/
public function get_limited_unindexed_count( $limit ) {
return $this->get_unindexed_count( $limit );
}
/**
* Returns the number of unimported objects (limited if limit is applied).
*
* @param int|null $limit The maximum number of unimported objects to be returned.
*
* @return int The number of unindexed posts.
*/
protected function get_unindexed_count( $limit = null ) {
if ( ! \is_int( $limit ) || $limit < 1 ) {
$limit = null;
}
$settings_to_create = $this->query( $limit );
$number_of_settings_to_create = \count( $settings_to_create );
$completed = $number_of_settings_to_create === 0;
$this->set_completed( $completed );
return $number_of_settings_to_create;
}
/**
* Imports AIOSEO settings.
*
* @return array|false An array of the AIOSEO settings that were imported or false if aioseo data was not found.
*/
public function index() {
$limit = $this->get_limit();
$aioseo_settings = $this->query( $limit );
$created_settings = [];
$completed = \count( $aioseo_settings ) === 0;
$this->set_completed( $completed );
// Prepare the setting keys mapping.
$this->build_mapping();
// Prepare the replacement var mapping.
foreach ( $this->replace_vars_edited_map as $aioseo_var => $yoast_var ) {
$this->replacevar_handler->compose_map( $aioseo_var, $yoast_var );
}
$last_imported_setting = '';
try {
foreach ( $aioseo_settings as $setting => $setting_value ) {
// Map and import the values of the setting we're working with (eg. post, book-category, etc.) to the respective Yoast option.
$this->map( $setting_value, $setting );
// Save the type of the settings that were just imported, so that we can allow chunked imports.
$last_imported_setting = $setting;
$created_settings[] = $setting;
}
}
finally {
$cursor_id = $this->get_cursor_id();
$this->import_cursor->set_cursor( $cursor_id, $last_imported_setting );
}
return $created_settings;
}
/**
* Checks if the settings tab subsetting is set in the AIOSEO option.
*
* @param string $aioseo_settings The AIOSEO option.
*
* @return bool Whether the settings are set.
*/
public function isset_settings_tab( $aioseo_settings ) {
return isset( $aioseo_settings['searchAppearance'][ $this->settings_tab ] );
}
/**
* Queries the database and retrieves unimported AiOSEO settings (in chunks if a limit is applied).
*
* @param int|null $limit The maximum number of unimported objects to be returned.
*
* @return array The (maybe chunked) unimported AiOSEO settings to import.
*/
protected function query( $limit = null ) {
$aioseo_settings = \json_decode( \get_option( $this->get_source_option_name(), '' ), true );
if ( empty( $aioseo_settings ) ) {
return [];
}
// We specifically want the setttings of the tab we're working with, eg. postTypes, taxonomies, etc.
$settings_values = $aioseo_settings['searchAppearance'][ $this->settings_tab ];
if ( ! \is_array( $settings_values ) ) {
return [];
}
$flattened_settings = $this->import_helper->flatten_settings( $settings_values );
return $this->get_unimported_chunk( $flattened_settings, $limit );
}
/**
* Retrieves (a chunk of, if limit is applied) the unimported AIOSEO settings.
* To apply a chunk, we manipulate the cursor to the keys of the AIOSEO settings.
*
* @param array $importable_data All of the available AIOSEO settings.
* @param int $limit The maximum number of unimported objects to be returned.
*
* @return array The (chunk of, if limit is applied)) unimported AIOSEO settings.
*/
protected function get_unimported_chunk( $importable_data, $limit ) {
\ksort( $importable_data );
$cursor_id = $this->get_cursor_id();
$cursor = $this->import_cursor->get_cursor( $cursor_id, '' );
/**
* Filter 'wpseo_aioseo_<identifier>_import_cursor' - Allow filtering the value of the aioseo settings import cursor.
*
* @api int The value of the aioseo posttype default settings import cursor.
*/
$cursor = \apply_filters( 'wpseo_aioseo_' . $this->get_type() . '_import_cursor', $cursor );
if ( $cursor === '' ) {
return \array_slice( $importable_data, 0, $limit, true );
}
// Let's find the position of the cursor in the alphabetically sorted importable data, so we can return only the unimported data.
$keys = \array_flip( \array_keys( $importable_data ) );
// If the stored cursor now no longer exists in the data, we have no choice but to start over.
$position = ( isset( $keys[ $cursor ] ) ) ? ( $keys[ $cursor ] + 1 ) : 0;
return \array_slice( $importable_data, $position, $limit, true );
}
/**
* Returns the number of objects that will be imported in a single importing pass.
*
* @return int The limit.
*/
public function get_limit() {
/**
* Filter 'wpseo_aioseo_<identifier>_indexation_limit' - Allow filtering the number of settings imported during each importing pass.
*
* @api int The maximum number of posts indexed.
*/
$limit = \apply_filters( 'wpseo_aioseo_' . $this->get_type() . '_indexation_limit', 25 );
if ( ! \is_int( $limit ) || $limit < 1 ) {
$limit = 25;
}
return $limit;
}
/**
* Maps/imports AIOSEO settings into the respective Yoast settings.
*
* @param string|array $setting_value The value of the AIOSEO setting at hand.
* @param string $setting The setting at hand, eg. post or movie-category, separator etc.
*
* @return void
*/
protected function map( $setting_value, $setting ) {
$aioseo_options_to_yoast_map = $this->aioseo_options_to_yoast_map;
if ( isset( $aioseo_options_to_yoast_map[ $setting ] ) ) {
$this->import_single_setting( $setting, $setting_value, $aioseo_options_to_yoast_map[ $setting ] );
}
}
/**
* Imports a single setting in the db after transforming it to adhere to Yoast conventions.
*
* @param string $setting The name of the setting.
* @param string $setting_value The values of the setting.
* @param array $setting_mapping The mapping of the setting to Yoast formats.
*
* @return void
*/
protected function import_single_setting( $setting, $setting_value, $setting_mapping ) {
$yoast_key = $setting_mapping['yoast_name'];
// Check if we're supposed to save the setting.
if ( $this->options->get_default( 'wpseo_titles', $yoast_key ) !== null ) {
// Then, do any needed data transfomation before actually saving the incoming data.
$transformed_data = \call_user_func( [ $this, $setting_mapping['transform_method'] ], $setting_value, $setting_mapping );
$this->options->set( $yoast_key, $transformed_data );
}
}
/**
* Minimally transforms boolean data to be imported.
*
* @param bool $meta_data The boolean meta data to be imported.
*
* @return bool The transformed boolean meta data.
*/
public function simple_boolean_import( $meta_data ) {
return $meta_data;
}
/**
* Imports the noindex setting, taking into consideration whether they defer to global defaults.
*
* @param bool $noindex The noindex of the type, without taking into consideration whether the type defers to global defaults.
* @param array $mapping The mapping of the setting we're working with.
*
* @return bool The noindex setting.
*/
public function import_noindex( $noindex, $mapping ) {
return $this->robots_transformer->transform_robot_setting( 'noindex', $noindex, $mapping );
}
/**
* Returns a setting map of the robot setting for one subset of post types/taxonomies/archives.
* For custom archives, it returns an empty array because AIOSEO excludes some custom archives from this option structure, eg. WooCommerce's products and we don't want to raise a false alarm.
*
* @return array The setting map of the robot setting for one subset of post types/taxonomies/archives or an empty array.
*/
public function pluck_robot_setting_from_mapping() {
return [];
}
}
importing/aioseo/aioseo-custom-archive-settings-importing-action.php 0000644 00000007341 14672432024 0022102 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
use Yoast\WP\SEO\Helpers\Import_Cursor_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Helpers\Sanitization_Helper;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Provider_Service;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Transformer_Service;
/**
* Importing action for AIOSEO custom archive settings data.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Aioseo_Custom_Archive_Settings_Importing_Action extends Abstract_Aioseo_Settings_Importing_Action {
/**
* The plugin of the action.
*/
const PLUGIN = 'aioseo';
/**
* The type of the action.
*/
const TYPE = 'custom_archive_settings';
/**
* The option_name of the AIOSEO option that contains the settings.
*/
const SOURCE_OPTION_NAME = 'aioseo_options_dynamic';
/**
* The map of aioseo_options to yoast settings.
*
* @var array
*/
protected $aioseo_options_to_yoast_map = [];
/**
* The tab of the aioseo settings we're working with.
*
* @var string
*/
protected $settings_tab = 'archives';
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
protected $post_type;
/**
* Aioseo_Custom_Archive_Settings_Importing_Action constructor.
*
* @param Import_Cursor_Helper $import_cursor The import cursor helper.
* @param Options_Helper $options The options helper.
* @param Sanitization_Helper $sanitization The sanitization helper.
* @param Post_Type_Helper $post_type The post type helper.
* @param Aioseo_Replacevar_Service $replacevar_handler The replacevar handler.
* @param Aioseo_Robots_Provider_Service $robots_provider The robots provider service.
* @param Aioseo_Robots_Transformer_Service $robots_transformer The robots transfomer service.
*/
public function __construct(
Import_Cursor_Helper $import_cursor,
Options_Helper $options,
Sanitization_Helper $sanitization,
Post_Type_Helper $post_type,
Aioseo_Replacevar_Service $replacevar_handler,
Aioseo_Robots_Provider_Service $robots_provider,
Aioseo_Robots_Transformer_Service $robots_transformer
) {
parent::__construct( $import_cursor, $options, $sanitization, $replacevar_handler, $robots_provider, $robots_transformer );
$this->post_type = $post_type;
}
/**
* Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method.
*
* @return void
*/
protected function build_mapping() {
$post_type_objects = \get_post_types( [ 'public' => true ], 'objects' );
foreach ( $post_type_objects as $pt ) {
// Use all the custom post types that have archives.
if ( ! $pt->_builtin && $this->post_type->has_archive( $pt ) ) {
$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/title' ] = [
'yoast_name' => 'title-ptarchive-' . $pt->name,
'transform_method' => 'simple_import',
];
$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/metaDescription' ] = [
'yoast_name' => 'metadesc-ptarchive-' . $pt->name,
'transform_method' => 'simple_import',
];
$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/advanced/robotsMeta/noindex' ] = [
'yoast_name' => 'noindex-ptarchive-' . $pt->name,
'transform_method' => 'import_noindex',
'type' => 'archives',
'subtype' => $pt->name,
'option_name' => 'aioseo_options_dynamic',
];
}
}
}
}
importing/aioseo/aioseo-general-settings-importing-action.php 0000644 00000013725 14672432024 0020571 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
use Yoast\WP\SEO\Helpers\Image_Helper;
use Yoast\WP\SEO\Helpers\Import_Cursor_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Sanitization_Helper;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Provider_Service;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Transformer_Service;
/**
* Importing action for AIOSEO general settings.
*/
class Aioseo_General_Settings_Importing_Action extends Abstract_Aioseo_Settings_Importing_Action {
/**
* The plugin of the action.
*/
const PLUGIN = 'aioseo';
/**
* The type of the action.
*/
const TYPE = 'general_settings';
/**
* The option_name of the AIOSEO option that contains the settings.
*/
const SOURCE_OPTION_NAME = 'aioseo_options';
/**
* The map of aioseo_options to yoast settings.
*
* @var array
*/
protected $aioseo_options_to_yoast_map = [];
/**
* The tab of the aioseo settings we're working with.
*
* @var string
*/
protected $settings_tab = 'global';
/**
* The image helper.
*
* @var Image_Helper
*/
protected $image;
/**
* Aioseo_General_Settings_Importing_Action constructor.
*
* @param Import_Cursor_Helper $import_cursor The import cursor helper.
* @param Options_Helper $options The options helper.
* @param Sanitization_Helper $sanitization The sanitization helper.
* @param Image_Helper $image The image helper.
* @param Aioseo_Replacevar_Service $replacevar_handler The replacevar handler.
* @param Aioseo_Robots_Provider_Service $robots_provider The robots provider service.
* @param Aioseo_Robots_Transformer_Service $robots_transformer The robots transfomer service.
*/
public function __construct(
Import_Cursor_Helper $import_cursor,
Options_Helper $options,
Sanitization_Helper $sanitization,
Image_Helper $image,
Aioseo_Replacevar_Service $replacevar_handler,
Aioseo_Robots_Provider_Service $robots_provider,
Aioseo_Robots_Transformer_Service $robots_transformer
) {
parent::__construct( $import_cursor, $options, $sanitization, $replacevar_handler, $robots_provider, $robots_transformer );
$this->image = $image;
}
/**
* Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method.
*
* @return void
*/
protected function build_mapping() {
$this->aioseo_options_to_yoast_map = [
'/separator' => [
'yoast_name' => 'separator',
'transform_method' => 'transform_separator',
],
'/siteTitle' => [
'yoast_name' => 'title-home-wpseo',
'transform_method' => 'simple_import',
],
'/metaDescription' => [
'yoast_name' => 'metadesc-home-wpseo',
'transform_method' => 'simple_import',
],
'/schema/siteRepresents' => [
'yoast_name' => 'company_or_person',
'transform_method' => 'transform_site_represents',
],
'/schema/person' => [
'yoast_name' => 'company_or_person_user_id',
'transform_method' => 'simple_import',
],
'/schema/organizationName' => [
'yoast_name' => 'company_name',
'transform_method' => 'simple_import',
],
'/schema/organizationLogo' => [
'yoast_name' => 'company_logo',
'transform_method' => 'import_company_logo',
],
'/schema/personLogo' => [
'yoast_name' => 'person_logo',
'transform_method' => 'import_person_logo',
],
];
}
/**
* Imports the organization logo while also accounting for the id of the log to be saved in the separate Yoast option.
*
* @param string $logo_url The company logo url coming from AIOSEO settings.
*
* @return string The transformed company logo url.
*/
public function import_company_logo( $logo_url ) {
$logo_id = $this->image->get_attachment_by_url( $logo_url );
$this->options->set( 'company_logo_id', $logo_id );
$this->options->set( 'company_logo_meta', false );
$logo_meta = $this->image->get_attachment_meta_from_settings( 'company_logo' );
$this->options->set( 'company_logo_meta', $logo_meta );
return $this->url_import( $logo_url );
}
/**
* Imports the person logo while also accounting for the id of the log to be saved in the separate Yoast option.
*
* @param string $logo_url The person logo url coming from AIOSEO settings.
*
* @return string The transformed person logo url.
*/
public function import_person_logo( $logo_url ) {
$logo_id = $this->image->get_attachment_by_url( $logo_url );
$this->options->set( 'person_logo_id', $logo_id );
$this->options->set( 'person_logo_meta', false );
$logo_meta = $this->image->get_attachment_meta_from_settings( 'person_logo' );
$this->options->set( 'person_logo_meta', $logo_meta );
return $this->url_import( $logo_url );
}
/**
* Transforms the site represents setting.
*
* @param string $site_represents The site represents setting.
*
* @return string The transformed site represents setting.
*/
public function transform_site_represents( $site_represents ) {
switch ( $site_represents ) {
case 'person':
return 'person';
case 'organization':
default:
return 'company';
}
}
/**
* Transforms the separator setting.
*
* @param string $separator The separator setting.
*
* @return string The transformed separator.
*/
public function transform_separator( $separator ) {
switch ( $separator ) {
case '-':
return 'sc-dash';
case '–':
return 'sc-ndash';
case '—':
return 'sc-mdash';
case '»':
return 'sc-raquo';
case '«':
return 'sc-laquo';
case '>':
return 'sc-gt';
case '•':
return 'sc-bull';
case '|':
return 'sc-pipe';
default:
return 'sc-dash';
}
}
}
importing/aioseo/aioseo-cleanup-action.php 0000644 00000010767 14672432024 0014742 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
use wpdb;
use Yoast\WP\SEO\Actions\Importing\Abstract_Aioseo_Importing_Action;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Importing action for cleaning up AIOSEO data.
*/
class Aioseo_Cleanup_Action extends Abstract_Aioseo_Importing_Action {
/**
* The plugin of the action.
*/
const PLUGIN = 'aioseo';
/**
* The type of the action.
*/
const TYPE = 'cleanup';
/**
* The AIOSEO meta_keys to be cleaned up.
*
* @var array
*/
protected $aioseo_postmeta_keys = [
'_aioseo_title',
'_aioseo_description',
'_aioseo_og_title',
'_aioseo_og_description',
'_aioseo_twitter_title',
'_aioseo_twitter_description',
];
/**
* The WordPress database instance.
*
* @var wpdb
*/
protected $wpdb;
/**
* Class constructor.
*
* @param wpdb $wpdb The WordPress database instance.
* @param Options_Helper $options The options helper.
*/
public function __construct(
wpdb $wpdb,
Options_Helper $options
) {
$this->wpdb = $wpdb;
$this->options = $options;
}
/**
* Retrieves the postmeta along with the db prefix.
*
* @return string The postmeta table name along with the db prefix.
*/
protected function get_postmeta_table() {
return $this->wpdb->prefix . 'postmeta';
}
/**
* Just checks if the cleanup has been completed in the past.
*
* @return int The total number of unimported objects.
*/
public function get_total_unindexed() {
if ( ! $this->aioseo_helper->aioseo_exists() ) {
return 0;
}
return ( ! $this->get_completed() ) ? 1 : 0;
}
/**
* Just checks if the cleanup has been completed in the past.
*
* @param int $limit The maximum number of unimported objects to be returned.
*
* @return int|false The limited number of unindexed posts. False if the query fails.
*/
public function get_limited_unindexed_count( $limit ) {
if ( ! $this->aioseo_helper->aioseo_exists() ) {
return 0;
}
return ( ! $this->get_completed() ) ? 1 : 0;
}
/**
* Cleans up AIOSEO data.
*
* @return Indexable[]|false An array of created indexables or false if aioseo data was not found.
*/
public function index() {
if ( $this->get_completed() ) {
return [];
}
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: There is no unescaped user input.
$meta_data = $this->wpdb->query( $this->cleanup_postmeta_query() );
$aioseo_table_truncate_done = $this->wpdb->query( $this->truncate_query() );
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
if ( $meta_data === false && $aioseo_table_truncate_done === false ) {
return false;
}
$this->set_completed( true );
return [
'metadata_cleanup' => $meta_data,
'indexables_cleanup' => $aioseo_table_truncate_done,
];
}
/**
* Creates a DELETE query string for deleting AIOSEO postmeta data.
*
* @return string The query to use for importing or counting the number of items to import.
*/
public function cleanup_postmeta_query() {
$table = $this->get_postmeta_table();
$meta_keys_to_delete = $this->aioseo_postmeta_keys;
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
return $this->wpdb->prepare(
"DELETE FROM {$table} WHERE meta_key IN (" . \implode( ', ', \array_fill( 0, \count( $meta_keys_to_delete ), '%s' ) ) . ')',
$meta_keys_to_delete
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
/**
* Creates a TRUNCATE query string for emptying the AIOSEO indexable table, if it exists.
*
* @return string The query to use for importing or counting the number of items to import.
*/
public function truncate_query() {
if ( ! $this->aioseo_helper->aioseo_exists() ) {
// If the table doesn't exist, we need a string that will amount to a quick query that doesn't return false when ran.
return 'SELECT 1';
}
$table = $this->aioseo_helper->get_table();
return "TRUNCATE TABLE {$table}";
}
/**
* Used nowhere. Exists to comply with the interface.
*
* @return int The limit.
*/
public function get_limit() {
/**
* Filter 'wpseo_aioseo_cleanup_limit' - Allow filtering the number of posts indexed during each indexing pass.
*
* @api int The maximum number of posts cleaned up.
*/
$limit = \apply_filters( 'wpseo_aioseo_cleanup_limit', 25 );
if ( ! \is_int( $limit ) || $limit < 1 ) {
$limit = 25;
}
return $limit;
}
}
importing/importing-indexation-action-interface.php 0000644 00000001445 14672432024 0016656 0 ustar 00 <?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();
}
importing/importing-action-interface.php 0000644 00000001614 14672432024 0014514 0 ustar 00 <?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 );
}
importing/deactivate-conflicting-plugins-action.php 0000644 00000010327 14672432024 0016634 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\Importing;
use Yoast\WP\SEO\Conditionals\Updated_Importer_Framework_Conditional;
use Yoast\WP\SEO\Config\Conflicting_Plugins;
use Yoast\WP\SEO\Helpers\Import_Cursor_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Sanitization_Helper;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Provider_Service;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Transformer_Service;
use Yoast\WP\SEO\Services\Importing\Conflicting_Plugins_Service;
/**
* Deactivates plug-ins that cause conflicts with Yoast SEO.
*/
class Deactivate_Conflicting_Plugins_Action extends Abstract_Aioseo_Importing_Action {
/**
* The plugin the class deals with.
*
* @var string
*/
const PLUGIN = 'conflicting-plugins';
/**
* The type the class deals with.
*
* @var string
*/
const TYPE = 'deactivation';
/**
* The replacevar handler.
*
* @var Aioseo_Replacevar_Service
*/
protected $replacevar_handler;
/**
* Knows all plugins that might possibly conflict.
*
* @var Conflicting_Plugins_Service
*/
protected $conflicting_plugins;
/**
* The list of conflicting plugins
*
* @var array
*/
protected $detected_plugins;
/**
* Class constructor.
*
* @param Import_Cursor_Helper $import_cursor The import cursor helper.
* @param Options_Helper $options The options helper.
* @param Sanitization_Helper $sanitization The sanitization helper.
* @param Aioseo_Replacevar_Service $replacevar_handler The replacevar handler.
* @param Aioseo_Robots_Provider_Service $robots_provider The robots provider service.
* @param Aioseo_Robots_Transformer_Service $robots_transformer The robots transfomer service.
* @param Conflicting_Plugins_Service $conflicting_plugins_service The Conflicting plugins Service.
*/
public function __construct(
Import_Cursor_Helper $import_cursor,
Options_Helper $options,
Sanitization_Helper $sanitization,
Aioseo_Replacevar_Service $replacevar_handler,
Aioseo_Robots_Provider_Service $robots_provider,
Aioseo_Robots_Transformer_Service $robots_transformer,
Conflicting_Plugins_Service $conflicting_plugins_service
) {
parent::__construct( $import_cursor, $options, $sanitization, $replacevar_handler, $robots_provider, $robots_transformer );
$this->conflicting_plugins = $conflicting_plugins_service;
$this->detected_plugins = [];
}
/**
* Get the total number of conflicting plugins.
*/
public function get_total_unindexed() {
return \count( $this->get_detected_plugins() );
}
/**
* Returns whether the updated importer framework is enabled.
*
* @return bool True if the updated importer framework is enabled.
*/
public function is_enabled() {
$updated_importer_framework_conditional = \YoastSEO()->classes->get( Updated_Importer_Framework_Conditional::class );
return $updated_importer_framework_conditional->is_met();
}
/**
* Deactivate conflicting plugins.
*/
public function index() {
$detected_plugins = $this->get_detected_plugins();
$this->conflicting_plugins->deactivate_conflicting_plugins( $detected_plugins );
// We need to conform to the interface, so we report that no indexables were created.
return [];
}
/**
* {@inheritDoc}
*/
public function get_limit() {
return \count( Conflicting_Plugins::all_plugins() );
}
/**
* Returns the total number of unindexed objects up to a limit.
*
* @param int $limit The maximum.
*
* @return int The total number of unindexed objects.
*/
public function get_limited_unindexed_count( $limit ) {
$count = \count( $this->get_detected_plugins() );
return ( $count <= $limit ) ? $count : $limit;
}
/**
* Returns all detected plugins.
*
* @return array The detected plugins.
*/
protected function get_detected_plugins() {
// The active plugins won't change much. We can reuse the result for the duration of the request.
if ( \count( $this->detected_plugins ) < 1 ) {
$this->detected_plugins = $this->conflicting_plugins->detect_conflicting_plugins();
}
return $this->detected_plugins;
}
}
indexables/indexable-head-action.php 0000644 00000007417 14672432024 0013515 0 ustar 00 <?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 ];
}
}
alert-dismissal-action.php 0000644 00000012565 14672432024 0011642 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions;
use Yoast\WP\SEO\Helpers\User_Helper;
/**
* Class Alert_Dismissal_Action.
*/
class Alert_Dismissal_Action {
const USER_META_KEY = '_yoast_alerts_dismissed';
/**
* Holds the user helper instance.
*
* @var User_Helper
*/
protected $user;
/**
* Constructs Alert_Dismissal_Action.
*
* @param User_Helper $user User helper.
*/
public function __construct( User_Helper $user ) {
$this->user = $user;
}
/**
* Dismisses an alert.
*
* @param string $alert_identifier Alert identifier.
*
* @return bool Whether the dismiss was successful or not.
*/
public function dismiss( $alert_identifier ) {
$user_id = $this->user->get_current_user_id();
if ( $user_id === 0 ) {
return false;
}
if ( $this->is_allowed( $alert_identifier ) === false ) {
return false;
}
$dismissed_alerts = $this->get_dismissed_alerts( $user_id );
if ( $dismissed_alerts === false ) {
return false;
}
if ( \array_key_exists( $alert_identifier, $dismissed_alerts ) === true ) {
// The alert is already dismissed.
return true;
}
// Add this alert to the dismissed alerts.
$dismissed_alerts[ $alert_identifier ] = true;
// Save.
return $this->user->update_meta( $user_id, static::USER_META_KEY, $dismissed_alerts ) !== false;
}
/**
* Resets an alert.
*
* @param string $alert_identifier Alert identifier.
*
* @return bool Whether the reset was successful or not.
*/
public function reset( $alert_identifier ) {
$user_id = $this->user->get_current_user_id();
if ( $user_id === 0 ) {
return false;
}
if ( $this->is_allowed( $alert_identifier ) === false ) {
return false;
}
$dismissed_alerts = $this->get_dismissed_alerts( $user_id );
if ( $dismissed_alerts === false ) {
return false;
}
$amount_of_dismissed_alerts = \count( $dismissed_alerts );
if ( $amount_of_dismissed_alerts === 0 ) {
// No alerts: nothing to reset.
return true;
}
if ( \array_key_exists( $alert_identifier, $dismissed_alerts ) === false ) {
// Alert not found: nothing to reset.
return true;
}
if ( $amount_of_dismissed_alerts === 1 ) {
// The 1 remaining dismissed alert is the alert to reset: delete the alerts user meta row.
return $this->user->delete_meta( $user_id, static::USER_META_KEY, $dismissed_alerts );
}
// Remove this alert from the dismissed alerts.
unset( $dismissed_alerts[ $alert_identifier ] );
// Save.
return $this->user->update_meta( $user_id, static::USER_META_KEY, $dismissed_alerts ) !== false;
}
/**
* Returns if an alert is dismissed or not.
*
* @param string $alert_identifier Alert identifier.
*
* @return bool Whether the alert has been dismissed.
*/
public function is_dismissed( $alert_identifier ) {
$user_id = $this->user->get_current_user_id();
if ( $user_id === 0 ) {
return false;
}
if ( $this->is_allowed( $alert_identifier ) === false ) {
return false;
}
$dismissed_alerts = $this->get_dismissed_alerts( $user_id );
if ( $dismissed_alerts === false ) {
return false;
}
return \array_key_exists( $alert_identifier, $dismissed_alerts );
}
/**
* Returns an object with all alerts dismissed by current user.
*
* @return array|false An array with the keys of all Alerts that have been dismissed
* by the current user or `false`.
*/
public function all_dismissed() {
$user_id = $this->user->get_current_user_id();
if ( $user_id === 0 ) {
return false;
}
$dismissed_alerts = $this->get_dismissed_alerts( $user_id );
if ( $dismissed_alerts === false ) {
return false;
}
return $dismissed_alerts;
}
/**
* Returns if an alert is allowed or not.
*
* @param string $alert_identifier Alert identifier.
*
* @return bool Whether the alert is allowed.
*/
public function is_allowed( $alert_identifier ) {
return \in_array( $alert_identifier, $this->get_allowed_dismissable_alerts(), true );
}
/**
* Retrieves the dismissed alerts.
*
* @param int $user_id User ID.
*
* @return string[]|false The dismissed alerts. False for an invalid $user_id.
*/
protected function get_dismissed_alerts( $user_id ) {
$dismissed_alerts = $this->user->get_meta( $user_id, static::USER_META_KEY, true );
if ( $dismissed_alerts === false ) {
// Invalid user ID.
return false;
}
if ( $dismissed_alerts === '' ) {
/*
* When no database row exists yet, an empty string is returned because of the `single` parameter.
* We do want a single result returned, but the default should be an empty array instead.
*/
return [];
}
return $dismissed_alerts;
}
/**
* Retrieves the allowed dismissable alerts.
*
* @return string[] The allowed dismissable alerts.
*/
protected function get_allowed_dismissable_alerts() {
/**
* Filter: 'wpseo_allowed_dismissable_alerts' - List of allowed dismissable alerts.
*
* @api string[] $allowed_dismissable_alerts Allowed dismissable alerts list.
*/
$allowed_dismissable_alerts = \apply_filters( 'wpseo_allowed_dismissable_alerts', [] );
if ( \is_array( $allowed_dismissable_alerts ) === false ) {
return [];
}
// Only allow strings.
$allowed_dismissable_alerts = \array_filter( $allowed_dismissable_alerts, 'is_string' );
// Filter unique and reorder indices.
$allowed_dismissable_alerts = \array_values( \array_unique( $allowed_dismissable_alerts ) );
return $allowed_dismissable_alerts;
}
}
configuration/first-time-configuration-action.php 0000644 00000016045 14672432024 0016341 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\Configuration;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Social_Profiles_Helper;
/**
* Class First_Time_Configuration_Action.
*/
class First_Time_Configuration_Action {
/**
* The fields for the site representation payload.
*/
const SITE_REPRESENTATION_FIELDS = [
'company_or_person',
'company_name',
'website_name',
'company_logo',
'company_logo_id',
'person_logo',
'person_logo_id',
'company_or_person_user_id',
'description',
];
/**
* The Options_Helper instance.
*
* @var Options_Helper
*/
protected $options_helper;
/**
* The Social_Profiles_Helper instance.
*
* @var Social_Profiles_Helper
*/
protected $social_profiles_helper;
/**
* First_Time_Configuration_Action constructor.
*
* @param Options_Helper $options_helper The WPSEO options helper.
* @param Social_Profiles_Helper $social_profiles_helper The social profiles helper.
*/
public function __construct( Options_Helper $options_helper, Social_Profiles_Helper $social_profiles_helper ) {
$this->options_helper = $options_helper;
$this->social_profiles_helper = $social_profiles_helper;
}
/**
* Stores the values for the site representation.
*
* @param array $params The values to store.
*
* @return object The response object.
*/
public function set_site_representation( $params ) {
$failures = [];
foreach ( self::SITE_REPRESENTATION_FIELDS as $field_name ) {
if ( isset( $params[ $field_name ] ) ) {
if ( $field_name === 'description' && \current_user_can( 'manage_options' ) ) {
$result = \update_option( 'blogdescription', $params['description'] );
if ( ! $result && $params['description'] === \get_option( 'blogdescription' ) ) {
$result = true;
}
}
else {
$result = $this->options_helper->set( $field_name, $params[ $field_name ] );
}
if ( ! $result ) {
$failures[] = $field_name;
}
}
}
// Delete cached logos in the db.
$this->options_helper->set( 'company_logo_meta', false );
$this->options_helper->set( 'person_logo_meta', false );
if ( \count( $failures ) === 0 ) {
return (object) [
'success' => true,
'status' => 200,
];
}
return (object) [
'success' => false,
'status' => 500,
'error' => 'Could not save some options in the database',
'failures' => $failures,
];
}
/**
* Stores the values for the social profiles.
*
* @param array $params The values to store.
*
* @return object The response object.
*/
public function set_social_profiles( $params ) {
$failures = $this->social_profiles_helper->set_organization_social_profiles( $params );
if ( empty( $failures ) ) {
return (object) [
'success' => true,
'status' => 200,
];
}
return (object) [
'success' => false,
'status' => 200,
'error' => 'Could not save some options in the database',
'failures' => $failures,
];
}
/**
* Stores the values for the social profiles.
*
* @param array $params The values to store.
*
* @return object The response object.
*/
public function set_person_social_profiles( $params ) {
$social_profiles = \array_filter(
$params,
static function ( $key ) {
return $key !== 'user_id';
},
\ARRAY_FILTER_USE_KEY
);
$failures = $this->social_profiles_helper->set_person_social_profiles( $params['user_id'], $social_profiles );
if ( \count( $failures ) === 0 ) {
return (object) [
'success' => true,
'status' => 200,
];
}
return (object) [
'success' => false,
'status' => 200,
'error' => 'Could not save some options in the database',
'failures' => $failures,
];
}
/**
* Gets the values for the social profiles.
*
* @param int $user_id The person ID.
*
* @return object The response object.
*/
public function get_person_social_profiles( $user_id ) {
return (object) [
'success' => true,
'status' => 200,
'social_profiles' => $this->social_profiles_helper->get_person_social_profiles( $user_id ),
];
}
/**
* Stores the values to enable/disable tracking.
*
* @param array $params The values to store.
*
* @return object The response object.
*/
public function set_enable_tracking( $params ) {
$success = true;
$option_value = $this->options_helper->get( 'tracking' );
if ( $option_value !== $params['tracking'] ) {
$this->options_helper->set( 'toggled_tracking', true );
$success = $this->options_helper->set( 'tracking', $params['tracking'] );
}
if ( $success ) {
return (object) [
'success' => true,
'status' => 200,
];
}
return (object) [
'success' => false,
'status' => 500,
'error' => 'Could not save the option in the database',
];
}
/**
* Checks if the current user has the capability a specific user.
*
* @param int $user_id The id of the user to be edited.
*
* @return object The response object.
*/
public function check_capability( $user_id ) {
if ( $this->can_edit_profile( $user_id ) ) {
return (object) [
'success' => true,
'status' => 200,
];
}
return (object) [
'success' => false,
'status' => 403,
];
}
/**
* Stores the first time configuration state.
*
* @param array $params The values to store.
*
* @return object The response object.
*/
public function save_configuration_state( $params ) {
// If the finishedSteps param is not present in the REST request, it's a malformed request.
if ( ! isset( $params['finishedSteps'] ) ) {
return (object) [
'success' => false,
'status' => 400,
'error' => 'Bad request',
];
}
// Sanitize input.
$finished_steps = \array_map( '\sanitize_text_field', \wp_unslash( $params['finishedSteps'] ) );
$success = $this->options_helper->set( 'configuration_finished_steps', $finished_steps );
if ( ! $success ) {
return (object) [
'success' => false,
'status' => 500,
'error' => 'Could not save the option in the database',
];
}
// If all the five steps of the configuration have been completed, set first_time_install option to false.
if ( \count( $params['finishedSteps'] ) === 3 ) {
$this->options_helper->set( 'first_time_install', false );
}
return (object) [
'success' => true,
'status' => 200,
];
}
/**
* Gets the first time configuration state.
*
* @return object The response object.
*/
public function get_configuration_state() {
$configuration_option = $this->options_helper->get( 'configuration_finished_steps' );
if ( ! \is_null( $configuration_option ) ) {
return (object) [
'success' => true,
'status' => 200,
'data' => $configuration_option,
];
}
return (object) [
'success' => false,
'status' => 500,
'error' => 'Could not get data from the database',
];
}
/**
* Checks if the current user has the capability to edit a specific user.
*
* @param int $person_id The id of the person to edit.
*
* @return bool
*/
private function can_edit_profile( $person_id ) {
return \current_user_can( 'edit_user', $person_id );
}
}
indexing/post-link-indexing-action.php 0000644 00000007514 14672432024 0014073 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\Indexing;
use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
/**
* Reindexing action for post link indexables.
*/
class Post_Link_Indexing_Action extends Abstract_Link_Indexing_Action {
/**
* The transient name.
*
* @var string
*/
const UNINDEXED_COUNT_TRANSIENT = 'wpseo_unindexed_post_link_count';
/**
* The transient cache key for limited counts.
*
* @var string
*/
const UNINDEXED_LIMITED_COUNT_TRANSIENT = self::UNINDEXED_COUNT_TRANSIENT . '_limited';
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
protected $post_type_helper;
/**
* Sets the required helper.
*
* @required
*
* @param Post_Type_Helper $post_type_helper The post type helper.
*
* @return void
*/
public function set_helper( Post_Type_Helper $post_type_helper ) {
$this->post_type_helper = $post_type_helper;
}
/**
* Returns objects to be indexed.
*
* @return array Objects to be indexed.
*/
protected function get_objects() {
$query = $this->get_select_query( $this->get_limit() );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query.
$posts = $this->wpdb->get_results( $query );
return \array_map(
static function ( $post ) {
return (object) [
'id' => (int) $post->ID,
'type' => 'post',
'content' => $post->post_content,
];
},
$posts
);
}
/**
* Builds a query for counting the number of unindexed post links.
*
* @return string The prepared query string.
*/
protected function get_count_query() {
$public_post_types = $this->post_type_helper->get_indexable_post_types();
$indexable_table = Model::get_table_name( 'Indexable' );
$links_table = Model::get_table_name( 'SEO_Links' );
// Warning: If this query is changed, makes sure to update the query in get_select_query as well.
return $this->wpdb->prepare(
"SELECT COUNT(P.ID)
FROM {$this->wpdb->posts} AS P
LEFT JOIN $indexable_table AS I
ON P.ID = I.object_id
AND I.link_count IS NOT NULL
AND I.object_type = 'post'
LEFT JOIN $links_table AS L
ON L.post_id = P.ID
AND L.target_indexable_id IS NULL
AND L.type = 'internal'
AND L.target_post_id IS NOT NULL
AND L.target_post_id != 0
WHERE ( I.object_id IS NULL OR L.post_id IS NOT NULL )
AND P.post_status = 'publish'
AND P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $public_post_types ), '%s' ) ) . ')',
$public_post_types
);
}
/**
* Builds a query for selecting the ID's of unindexed post links.
*
* @param int|false $limit The maximum number of post link IDs to return.
*
* @return string The prepared query string.
*/
protected function get_select_query( $limit = false ) {
$public_post_types = $this->post_type_helper->get_indexable_post_types();
$indexable_table = Model::get_table_name( 'Indexable' );
$links_table = Model::get_table_name( 'SEO_Links' );
$replacements = $public_post_types;
$limit_query = '';
if ( $limit ) {
$limit_query = 'LIMIT %d';
$replacements[] = $limit;
}
// Warning: If this query is changed, makes sure to update the query in get_count_query as well.
return $this->wpdb->prepare(
"
SELECT P.ID, P.post_content
FROM {$this->wpdb->posts} AS P
LEFT JOIN $indexable_table AS I
ON P.ID = I.object_id
AND I.link_count IS NOT NULL
AND I.object_type = 'post'
LEFT JOIN $links_table AS L
ON L.post_id = P.ID
AND L.target_indexable_id IS NULL
AND L.type = 'internal'
AND L.target_post_id IS NOT NULL
AND L.target_post_id != 0
WHERE ( I.object_id IS NULL OR L.post_id IS NOT NULL )
AND P.post_status = 'publish'
AND P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $public_post_types ), '%s' ) ) . ")
$limit_query",
$replacements
);
}
}
indexing/indexable-indexing-complete-action.php 0000644 00000001316 14672432024 0015706 0 ustar 00 <?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();
}
}
indexing/abstract-indexing-action.php 0000644 00000006077 14672432024 0013761 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\Indexing;
/**
* Base class of indexing actions.
*/
abstract class Abstract_Indexing_Action implements Indexation_Action_Interface, Limited_Indexing_Action_Interface {
/**
* The transient name.
*
* This is a trick to force derived classes to define a transient themselves.
*
* @var string
*/
const UNINDEXED_COUNT_TRANSIENT = null;
/**
* The transient cache key for limited counts.
*
* @var string
*/
const UNINDEXED_LIMITED_COUNT_TRANSIENT = self::UNINDEXED_COUNT_TRANSIENT . '_limited';
/**
* Builds a query for selecting the ID's of unindexed posts.
*
* @param bool $limit The maximum number of post IDs to return.
*
* @return string The prepared query string.
*/
abstract protected function get_select_query( $limit );
/**
* Builds a query for counting the number of unindexed posts.
*
* @return string The prepared query string.
*/
abstract protected function get_count_query();
/**
* Returns a limited number of unindexed posts.
*
* @param int $limit Limit the maximum number of unindexed posts that are counted.
*
* @return int The limited number of unindexed posts. 0 if the query fails.
*/
public function get_limited_unindexed_count( $limit ) {
$transient = \get_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT );
if ( $transient !== false ) {
return (int) $transient;
}
\set_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT, 0, ( \MINUTE_IN_SECONDS * 15 ) );
$query = $this->get_select_query( $limit );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_count_query returns a prepared query.
$unindexed_object_ids = $this->wpdb->get_col( $query );
$count = (int) \count( $unindexed_object_ids );
\set_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT, $count, ( \MINUTE_IN_SECONDS * 15 ) );
return $count;
}
/**
* Returns the total number of unindexed posts.
*
* @return int|false The total number of unindexed posts. False if the query fails.
*/
public function get_total_unindexed() {
$transient = \get_transient( static::UNINDEXED_COUNT_TRANSIENT );
if ( $transient !== false ) {
return (int) $transient;
}
// Store transient before doing the query so multiple requests won't make multiple queries.
// Only store this for 15 minutes to ensure that if the query doesn't complete a wrong count is not kept too long.
\set_transient( static::UNINDEXED_COUNT_TRANSIENT, 0, ( \MINUTE_IN_SECONDS * 15 ) );
$query = $this->get_count_query();
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_count_query returns a prepared query.
$count = $this->wpdb->get_var( $query );
if ( \is_null( $count ) ) {
return false;
}
\set_transient( static::UNINDEXED_COUNT_TRANSIENT, $count, \DAY_IN_SECONDS );
/**
* Action: 'wpseo_indexables_unindexed_calculated' - sets an option to timestamp when there are no unindexed indexables left.
*
* @internal
*/
\do_action( 'wpseo_indexables_unindexed_calculated', static::UNINDEXED_COUNT_TRANSIENT, $count );
return (int) $count;
}
}
indexing/limited-indexing-action-interface.php 0000644 00000000763 14672432024 0015537 0 ustar 00 <?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 );
}
indexing/abstract-link-indexing-action.php 0000644 00000005403 14672432024 0014704 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\Indexing;
use wpdb;
use Yoast\WP\SEO\Builders\Indexable_Link_Builder;
use Yoast\WP\SEO\Models\SEO_Links;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* Reindexing action for link indexables.
*/
abstract class Abstract_Link_Indexing_Action extends Abstract_Indexing_Action {
/**
* The link builder.
*
* @var Indexable_Link_Builder
*/
protected $link_builder;
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
protected $repository;
/**
* The WordPress database instance.
*
* @var wpdb
*/
protected $wpdb;
/**
* Indexable_Post_Indexing_Action constructor
*
* @param Indexable_Link_Builder $link_builder The indexable link builder.
* @param Indexable_Repository $repository The indexable repository.
* @param wpdb $wpdb The WordPress database instance.
*/
public function __construct(
Indexable_Link_Builder $link_builder,
Indexable_Repository $repository,
wpdb $wpdb
) {
$this->link_builder = $link_builder;
$this->repository = $repository;
$this->wpdb = $wpdb;
}
/**
* Builds links for indexables which haven't had their links indexed yet.
*
* @return SEO_Links[] The created SEO links.
*/
public function index() {
$objects = $this->get_objects();
$indexables = [];
foreach ( $objects as $object ) {
$indexable = $this->repository->find_by_id_and_type( $object->id, $object->type );
if ( $indexable ) {
$this->link_builder->build( $indexable, $object->content );
$indexable->save();
$indexables[] = $indexable;
}
}
if ( \count( $indexables ) > 0 ) {
\delete_transient( static::UNINDEXED_COUNT_TRANSIENT );
\delete_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT );
}
return $indexables;
}
/**
* In the case of term-links and post-links we want to use the total unindexed count, because using
* the limited unindexed count actually leads to worse performance.
*
* @param int|bool $limit Unused.
*
* @return int The total number of unindexed links.
*/
public function get_limited_unindexed_count( $limit = false ) {
return $this->get_total_unindexed();
}
/**
* Returns the number of texts that will be indexed in a single link indexing pass.
*
* @return int The limit.
*/
public function get_limit() {
/**
* Filter 'wpseo_link_indexing_limit' - Allow filtering the number of texts indexed during each link indexing pass.
*
* @api int The maximum number of texts indexed.
*/
return \apply_filters( 'wpseo_link_indexing_limit', 5 );
}
/**
* Returns objects to be indexed.
*
* @return array Objects to be indexed, should be an array of objects with object_id, object_type and content.
*/
abstract protected function get_objects();
}
indexing/indexing-complete-action.php 0000644 00000001227 14672432024 0013756 0 ustar 00 <?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();
}
}
indexing/term-link-indexing-action.php 0000644 00000006424 14672432024 0014054 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\Indexing;
use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
/**
* Reindexing action for term link indexables.
*/
class Term_Link_Indexing_Action extends Abstract_Link_Indexing_Action {
/**
* The transient name.
*
* @var string
*/
const UNINDEXED_COUNT_TRANSIENT = 'wpseo_unindexed_term_link_count';
/**
* The transient cache key for limited counts.
*
* @var string
*/
const UNINDEXED_LIMITED_COUNT_TRANSIENT = self::UNINDEXED_COUNT_TRANSIENT . '_limited';
/**
* The post type helper.
*
* @var Taxonomy_Helper
*/
protected $taxonomy_helper;
/**
* Sets the required helper.
*
* @required
*
* @param Taxonomy_Helper $taxonomy_helper The taxonomy helper.
*
* @return void
*/
public function set_helper( Taxonomy_Helper $taxonomy_helper ) {
$this->taxonomy_helper = $taxonomy_helper;
}
/**
* Returns objects to be indexed.
*
* @return array Objects to be indexed.
*/
protected function get_objects() {
$query = $this->get_select_query( $this->get_limit() );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query.
$terms = $this->wpdb->get_results( $query );
return \array_map(
static function ( $term ) {
return (object) [
'id' => (int) $term->term_id,
'type' => 'term',
'content' => $term->description,
];
},
$terms
);
}
/**
* Builds a query for counting the number of unindexed term links.
*
* @return string The prepared query string.
*/
protected function get_count_query() {
$public_taxonomies = $this->taxonomy_helper->get_indexable_taxonomies();
$placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) );
$indexable_table = Model::get_table_name( 'Indexable' );
// Warning: If this query is changed, makes sure to update the query in get_select_query as well.
return $this->wpdb->prepare(
"
SELECT COUNT(T.term_id)
FROM {$this->wpdb->term_taxonomy} AS T
LEFT JOIN $indexable_table AS I
ON T.term_id = I.object_id
AND I.object_type = 'term'
AND I.link_count IS NOT NULL
WHERE I.object_id IS NULL
AND T.taxonomy IN ($placeholders)",
$public_taxonomies
);
}
/**
* Builds a query for selecting the ID's of unindexed term links.
*
* @param int|false $limit The maximum number of term link IDs to return.
*
* @return string The prepared query string.
*/
protected function get_select_query( $limit = false ) {
$public_taxonomies = $this->taxonomy_helper->get_indexable_taxonomies();
$indexable_table = Model::get_table_name( 'Indexable' );
$replacements = $public_taxonomies;
$limit_query = '';
if ( $limit ) {
$limit_query = 'LIMIT %d';
$replacements[] = $limit;
}
// Warning: If this query is changed, makes sure to update the query in get_count_query as well.
return $this->wpdb->prepare(
"
SELECT T.term_id, T.description
FROM {$this->wpdb->term_taxonomy} AS T
LEFT JOIN $indexable_table AS I
ON T.term_id = I.object_id
AND I.object_type = 'term'
AND I.link_count IS NOT NULL
WHERE I.object_id IS NULL
AND T.taxonomy IN (" . \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) ) . ")
$limit_query",
$replacements
);
}
}
indexing/indexation-action-interface.php 0000644 00000001432 14672432024 0014441 0 ustar 00 <?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();
}
indexing/indexable-general-indexation-action.php 0000644 00000007572 14672432024 0016062 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\Indexing;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* General reindexing action for indexables.
*/
class Indexable_General_Indexation_Action implements Indexation_Action_Interface, Limited_Indexing_Action_Interface {
/**
* The transient cache key.
*/
const UNINDEXED_COUNT_TRANSIENT = 'wpseo_total_unindexed_general_items';
/**
* Represents the indexables repository.
*
* @var Indexable_Repository
*/
protected $indexable_repository;
/**
* Indexable_General_Indexation_Action constructor.
*
* @param Indexable_Repository $indexable_repository The indexables repository.
*/
public function __construct( Indexable_Repository $indexable_repository ) {
$this->indexable_repository = $indexable_repository;
}
/**
* Returns the total number of unindexed objects.
*
* @return int The total number of unindexed objects.
*/
public function get_total_unindexed() {
$transient = \get_transient( static::UNINDEXED_COUNT_TRANSIENT );
if ( $transient !== false ) {
return (int) $transient;
}
$indexables_to_create = $this->query();
$result = \count( $indexables_to_create );
\set_transient( static::UNINDEXED_COUNT_TRANSIENT, $result, \DAY_IN_SECONDS );
/**
* Action: 'wpseo_indexables_unindexed_calculated' - sets an option to timestamp when there are no unindexed indexables left.
*
* @internal
*/
\do_action( 'wpseo_indexables_unindexed_calculated', static::UNINDEXED_COUNT_TRANSIENT, $result );
return $result;
}
/**
* Returns a limited number of unindexed posts.
*
* @param int $limit Limit the maximum number of unindexed posts that are counted.
*
* @return int|false The limited number of unindexed posts. False if the query fails.
*/
public function get_limited_unindexed_count( $limit ) {
return $this->get_total_unindexed();
}
/**
* Creates indexables for unindexed system pages, the date archive, and the homepage.
*
* @return Indexable[] The created indexables.
*/
public function index() {
$indexables = [];
$indexables_to_create = $this->query();
if ( isset( $indexables_to_create['404'] ) ) {
$indexables[] = $this->indexable_repository->find_for_system_page( '404' );
}
if ( isset( $indexables_to_create['search'] ) ) {
$indexables[] = $this->indexable_repository->find_for_system_page( 'search-result' );
}
if ( isset( $indexables_to_create['date_archive'] ) ) {
$indexables[] = $this->indexable_repository->find_for_date_archive();
}
if ( isset( $indexables_to_create['home_page'] ) ) {
$indexables[] = $this->indexable_repository->find_for_home_page();
}
\set_transient( static::UNINDEXED_COUNT_TRANSIENT, 0, \DAY_IN_SECONDS );
return $indexables;
}
/**
* Returns the number of objects that will be indexed in a single indexing pass.
*
* @return int The limit.
*/
public function get_limit() {
// This matches the maximum number of indexables created by this action.
return 4;
}
/**
* Check which indexables already exist and return the values of the ones to create.
*
* @return array The indexable types to create.
*/
private function query() {
$indexables_to_create = [];
if ( ! $this->indexable_repository->find_for_system_page( '404', false ) ) {
$indexables_to_create['404'] = true;
}
if ( ! $this->indexable_repository->find_for_system_page( 'search-result', false ) ) {
$indexables_to_create['search'] = true;
}
if ( ! $this->indexable_repository->find_for_date_archive( false ) ) {
$indexables_to_create['date_archive'] = true;
}
$need_home_page_indexable = ( (int) \get_option( 'page_on_front' ) === 0 && \get_option( 'show_on_front' ) === 'posts' );
if ( $need_home_page_indexable && ! $this->indexable_repository->find_for_home_page( false ) ) {
$indexables_to_create['home_page'] = true;
}
return $indexables_to_create;
}
}
indexing/indexable-post-indexation-action.php 0000644 00000013264 14672432024 0015425 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\Indexing;
use wpdb;
use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Helpers\Post_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
/**
* Reindexing action for post indexables.
*/
class Indexable_Post_Indexation_Action extends Abstract_Indexing_Action {
/**
* The transient cache key.
*
* @var string
*/
const UNINDEXED_COUNT_TRANSIENT = 'wpseo_total_unindexed_posts';
/**
* The transient cache key for limited counts.
*
* @var string
*/
const UNINDEXED_LIMITED_COUNT_TRANSIENT = self::UNINDEXED_COUNT_TRANSIENT . '_limited';
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
protected $post_type_helper;
/**
* The post helper.
*
* @var Post_Helper
*/
protected $post_helper;
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
protected $repository;
/**
* The WordPress database instance.
*
* @var wpdb
*/
protected $wpdb;
/**
* The latest version of Post Indexables.
*
* @var int
*/
protected $version;
/**
* Indexable_Post_Indexing_Action constructor
*
* @param Post_Type_Helper $post_type_helper The post type helper.
* @param Indexable_Repository $repository The indexable repository.
* @param wpdb $wpdb The WordPress database instance.
* @param Indexable_Builder_Versions $builder_versions The latest versions for each Indexable type.
* @param Post_Helper $post_helper The post helper.
*/
public function __construct(
Post_Type_Helper $post_type_helper,
Indexable_Repository $repository,
wpdb $wpdb,
Indexable_Builder_Versions $builder_versions,
Post_Helper $post_helper
) {
$this->post_type_helper = $post_type_helper;
$this->repository = $repository;
$this->wpdb = $wpdb;
$this->version = $builder_versions->get_latest_version_for_type( 'post' );
$this->post_helper = $post_helper;
}
/**
* Creates indexables for unindexed posts.
*
* @return Indexable[] The created indexables.
*/
public function index() {
$query = $this->get_select_query( $this->get_limit() );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query.
$post_ids = $this->wpdb->get_col( $query );
$indexables = [];
foreach ( $post_ids as $post_id ) {
$indexables[] = $this->repository->find_by_id_and_type( (int) $post_id, 'post' );
}
if ( \count( $indexables ) > 0 ) {
\delete_transient( static::UNINDEXED_COUNT_TRANSIENT );
\delete_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT );
}
return $indexables;
}
/**
* Returns the number of posts that will be indexed in a single indexing pass.
*
* @return int The limit.
*/
public function get_limit() {
/**
* Filter 'wpseo_post_indexation_limit' - Allow filtering the amount of posts indexed during each indexing pass.
*
* @api int The maximum number of posts indexed.
*/
$limit = \apply_filters( 'wpseo_post_indexation_limit', 25 );
if ( ! \is_int( $limit ) || $limit < 1 ) {
$limit = 25;
}
return $limit;
}
/**
* Builds a query for counting the number of unindexed posts.
*
* @return string The prepared query string.
*/
protected function get_count_query() {
$indexable_table = Model::get_table_name( 'Indexable' );
$post_types = $this->post_type_helper->get_indexable_post_types();
$excluded_post_statuses = $this->post_helper->get_excluded_post_statuses();
$replacements = \array_merge(
$post_types,
$excluded_post_statuses
);
$replacements[] = $this->version;
// Warning: If this query is changed, makes sure to update the query in get_select_query as well.
// @phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
return $this->wpdb->prepare(
"
SELECT COUNT(P.ID)
FROM {$this->wpdb->posts} AS P
WHERE P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $post_types ), '%s' ) ) . ')
AND P.post_status NOT IN (' . \implode( ', ', \array_fill( 0, \count( $excluded_post_statuses ), '%s' ) ) . ")
AND P.ID not in (
SELECT I.object_id from $indexable_table as I
WHERE I.object_type = 'post'
AND I.version = %d )",
$replacements
);
}
/**
* Builds a query for selecting the ID's of unindexed posts.
*
* @param bool $limit The maximum number of post IDs to return.
*
* @return string The prepared query string.
*/
protected function get_select_query( $limit = false ) {
$indexable_table = Model::get_table_name( 'Indexable' );
$post_types = $this->post_type_helper->get_indexable_post_types();
$excluded_post_statuses = $this->post_helper->get_excluded_post_statuses();
$replacements = \array_merge(
$post_types,
$excluded_post_statuses
);
$replacements[] = $this->version;
$limit_query = '';
if ( $limit ) {
$limit_query = 'LIMIT %d';
$replacements[] = $limit;
}
// Warning: If this query is changed, makes sure to update the query in get_count_query as well.
// @phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
return $this->wpdb->prepare(
"
SELECT P.ID
FROM {$this->wpdb->posts} AS P
WHERE P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $post_types ), '%s' ) ) . ')
AND P.post_status NOT IN (' . \implode( ', ', \array_fill( 0, \count( $excluded_post_statuses ), '%s' ) ) . ")
AND P.ID not in (
SELECT I.object_id from $indexable_table as I
WHERE I.object_type = 'post'
AND I.version = %d )
$limit_query",
$replacements
);
}
}
indexing/indexing-prepare-action.php 0000644 00000001264 14672432024 0013605 0 ustar 00 <?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();
}
}
indexing/indexable-post-type-archive-indexation-action.php 0000644 00000013716 14672432024 0020025 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\Indexing;
use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
/**
* Reindexing action for post type archive indexables.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Indexable_Post_Type_Archive_Indexation_Action implements Indexation_Action_Interface, Limited_Indexing_Action_Interface {
/**
* The transient cache key.
*/
const UNINDEXED_COUNT_TRANSIENT = 'wpseo_total_unindexed_post_type_archives';
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
protected $post_type;
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
protected $repository;
/**
* The indexable builder.
*
* @var Indexable_Builder
*/
protected $builder;
/**
* The current version of the post type archive indexable builder.
*
* @var int
*/
protected $version;
/**
* Indexation_Post_Type_Archive_Action constructor.
*
* @param Indexable_Repository $repository The indexable repository.
* @param Indexable_Builder $builder The indexable builder.
* @param Post_Type_Helper $post_type The post type helper.
* @param Indexable_Builder_Versions $versions The current versions of all indexable builders.
*/
public function __construct(
Indexable_Repository $repository,
Indexable_Builder $builder,
Post_Type_Helper $post_type,
Indexable_Builder_Versions $versions
) {
$this->repository = $repository;
$this->builder = $builder;
$this->post_type = $post_type;
$this->version = $versions->get_latest_version_for_type( 'post-type-archive' );
}
/**
* Returns the total number of unindexed post type archives.
*
* @param int $limit Limit the number of counted objects.
*
* @return int The total number of unindexed post type archives.
*/
public function get_total_unindexed( $limit = false ) {
$transient = \get_transient( static::UNINDEXED_COUNT_TRANSIENT );
if ( $transient !== false ) {
return (int) $transient;
}
\set_transient( static::UNINDEXED_COUNT_TRANSIENT, 0, \DAY_IN_SECONDS );
$result = \count( $this->get_unindexed_post_type_archives( $limit ) );
\set_transient( static::UNINDEXED_COUNT_TRANSIENT, $result, \DAY_IN_SECONDS );
/**
* Action: 'wpseo_indexables_unindexed_calculated' - sets an option to timestamp when there are no unindexed indexables left.
*
* @internal
*/
\do_action( 'wpseo_indexables_unindexed_calculated', static::UNINDEXED_COUNT_TRANSIENT, $result );
return $result;
}
/**
* Creates indexables for post type archives.
*
* @return Indexable[] The created indexables.
*/
public function index() {
$unindexed_post_type_archives = $this->get_unindexed_post_type_archives( $this->get_limit() );
$indexables = [];
foreach ( $unindexed_post_type_archives as $post_type_archive ) {
$indexables[] = $this->builder->build_for_post_type_archive( $post_type_archive );
}
if ( \count( $indexables ) > 0 ) {
\delete_transient( static::UNINDEXED_COUNT_TRANSIENT );
}
return $indexables;
}
/**
* Returns the number of post type archives that will be indexed in a single indexing pass.
*
* @return int The limit.
*/
public function get_limit() {
/**
* Filter 'wpseo_post_type_archive_indexation_limit' - Allow filtering the number of posts indexed during each indexing pass.
*
* @api int The maximum number of posts indexed.
*/
$limit = \apply_filters( 'wpseo_post_type_archive_indexation_limit', 25 );
if ( ! \is_int( $limit ) || $limit < 1 ) {
$limit = 25;
}
return $limit;
}
/**
* Retrieves the list of post types for which no indexable for its archive page has been made yet.
*
* @param int|false $limit Limit the number of retrieved indexables to this number.
*
* @return array The list of post types for which no indexable for its archive page has been made yet.
*/
protected function get_unindexed_post_type_archives( $limit = false ) {
$post_types_with_archive_pages = $this->get_post_types_with_archive_pages();
$indexed_post_types = $this->get_indexed_post_type_archives();
$unindexed_post_types = \array_diff( $post_types_with_archive_pages, $indexed_post_types );
if ( $limit ) {
return \array_slice( $unindexed_post_types, 0, $limit );
}
return $unindexed_post_types;
}
/**
* Returns the names of all the post types that have archive pages.
*
* @return array The list of names of all post types that have archive pages.
*/
protected function get_post_types_with_archive_pages() {
// We only want to index archive pages of public post types that have them.
$post_types_with_archive = $this->post_type->get_indexable_post_archives();
// We only need the post type names, not the objects.
$post_types = [];
foreach ( $post_types_with_archive as $post_type_with_archive ) {
$post_types[] = $post_type_with_archive->name;
}
return $post_types;
}
/**
* Retrieves the list of post type names for which an archive indexable exists.
*
* @return array The list of names of post types with unindexed archive pages.
*/
protected function get_indexed_post_type_archives() {
$results = $this->repository->query()
->select( 'object_sub_type' )
->where( 'object_type', 'post-type-archive' )
->where_equal( 'version', $this->version )
->find_array();
if ( $results === false ) {
return [];
}
$callback = static function( $result ) {
return $result['object_sub_type'];
};
return \array_map( $callback, $results );
}
/**
* Returns a limited number of unindexed posts.
*
* @param int $limit Limit the maximum number of unindexed posts that are counted.
*
* @return int|false The limited number of unindexed posts. False if the query fails.
*/
public function get_limited_unindexed_count( $limit ) {
return $this->get_total_unindexed( $limit );
}
}
indexing/indexable-term-indexation-action.php 0000644 00000011723 14672432024 0015405 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\Indexing;
use wpdb;
use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
/**
* Reindexing action for term indexables.
*/
class Indexable_Term_Indexation_Action extends Abstract_Indexing_Action {
/**
* The transient cache key.
*/
const UNINDEXED_COUNT_TRANSIENT = 'wpseo_total_unindexed_terms';
/**
* The transient cache key for limited counts.
*
* @var string
*/
const UNINDEXED_LIMITED_COUNT_TRANSIENT = self::UNINDEXED_COUNT_TRANSIENT . '_limited';
/**
* The post type helper.
*
* @var Taxonomy_Helper
*/
protected $taxonomy;
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
protected $repository;
/**
* The WordPress database instance.
*
* @var wpdb
*/
protected $wpdb;
/**
* The latest version of the Indexable term builder
*
* @var int
*/
protected $version;
/**
* Indexable_Term_Indexation_Action constructor
*
* @param Taxonomy_Helper $taxonomy The taxonomy helper.
* @param Indexable_Repository $repository The indexable repository.
* @param wpdb $wpdb The WordPress database instance.
* @param Indexable_Builder_Versions $builder_versions The latest versions of all indexable builders.
*/
public function __construct(
Taxonomy_Helper $taxonomy,
Indexable_Repository $repository,
wpdb $wpdb,
Indexable_Builder_Versions $builder_versions
) {
$this->taxonomy = $taxonomy;
$this->repository = $repository;
$this->wpdb = $wpdb;
$this->version = $builder_versions->get_latest_version_for_type( 'term' );
}
/**
* Creates indexables for unindexed terms.
*
* @return Indexable[] The created indexables.
*/
public function index() {
$query = $this->get_select_query( $this->get_limit() );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query.
$term_ids = $this->wpdb->get_col( $query );
$indexables = [];
foreach ( $term_ids as $term_id ) {
$indexables[] = $this->repository->find_by_id_and_type( (int) $term_id, 'term' );
}
if ( \count( $indexables ) > 0 ) {
\delete_transient( static::UNINDEXED_COUNT_TRANSIENT );
\delete_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT );
}
return $indexables;
}
/**
* Returns the number of terms that will be indexed in a single indexing pass.
*
* @return int The limit.
*/
public function get_limit() {
/**
* Filter 'wpseo_term_indexation_limit' - Allow filtering the number of terms indexed during each indexing pass.
*
* @api int The maximum number of terms indexed.
*/
$limit = \apply_filters( 'wpseo_term_indexation_limit', 25 );
if ( ! \is_int( $limit ) || $limit < 1 ) {
$limit = 25;
}
return $limit;
}
/**
* Builds a query for counting the number of unindexed terms.
*
* @return string The prepared query string.
*/
protected function get_count_query() {
$indexable_table = Model::get_table_name( 'Indexable' );
$taxonomy_table = $this->wpdb->term_taxonomy;
$public_taxonomies = $this->taxonomy->get_indexable_taxonomies();
$taxonomies_placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) );
$replacements = [ $this->version ];
\array_push( $replacements, ...$public_taxonomies );
// Warning: If this query is changed, makes sure to update the query in get_count_query as well.
return $this->wpdb->prepare(
"
SELECT COUNT(term_id)
FROM {$taxonomy_table} AS T
LEFT JOIN $indexable_table AS I
ON T.term_id = I.object_id
AND I.object_type = 'term'
AND I.version = %d
WHERE I.object_id IS NULL
AND taxonomy IN ($taxonomies_placeholders)",
$replacements
);
}
/**
* Builds a query for selecting the ID's of unindexed terms.
*
* @param bool $limit The maximum number of term IDs to return.
*
* @return string The prepared query string.
*/
protected function get_select_query( $limit = false ) {
$indexable_table = Model::get_table_name( 'Indexable' );
$taxonomy_table = $this->wpdb->term_taxonomy;
$public_taxonomies = $this->taxonomy->get_indexable_taxonomies();
$placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) );
$replacements = [ $this->version ];
\array_push( $replacements, ...$public_taxonomies );
$limit_query = '';
if ( $limit ) {
$limit_query = 'LIMIT %d';
$replacements[] = $limit;
}
// Warning: If this query is changed, makes sure to update the query in get_count_query as well.
return $this->wpdb->prepare(
"
SELECT term_id
FROM {$taxonomy_table} AS T
LEFT JOIN $indexable_table AS I
ON T.term_id = I.object_id
AND I.object_type = 'term'
AND I.version = %d
WHERE I.object_id IS NULL
AND taxonomy IN ($placeholders)
$limit_query",
$replacements
);
}
}
semrush/semrush-phrases-action.php 0000644 00000004252 14672432024 0013356 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\SEMrush;
use Exception;
use Yoast\WP\SEO\Config\SEMrush_Client;
/**
* Class SEMrush_Phrases_Action
*/
class SEMrush_Phrases_Action {
/**
* The transient cache key.
*/
const TRANSIENT_CACHE_KEY = 'wpseo_semrush_related_keyphrases_%s_%s';
/**
* The SEMrush keyphrase URL.
*
* @var string
*/
const KEYPHRASES_URL = 'https://oauth.semrush.com/api/v1/keywords/phrase_fullsearch';
/**
* The SEMrush_Client instance.
*
* @var SEMrush_Client
*/
protected $client;
/**
* SEMrush_Phrases_Action constructor.
*
* @param SEMrush_Client $client The API client.
*/
public function __construct( SEMrush_Client $client ) {
$this->client = $client;
}
/**
* Gets the related keyphrases and data based on the passed keyphrase and database country code.
*
* @param string $keyphrase The keyphrase to search for.
* @param string $database The database's country code.
*
* @return object The response object.
*/
public function get_related_keyphrases( $keyphrase, $database ) {
try {
$transient_key = \sprintf( static::TRANSIENT_CACHE_KEY, $keyphrase, $database );
$transient = \get_transient( $transient_key );
if ( $transient !== false ) {
return $this->to_result_object( $transient );
}
$options = [
'params' => [
'phrase' => $keyphrase,
'database' => $database,
'export_columns' => 'Ph,Nq,Td',
'display_limit' => 10,
'display_offset' => 0,
'display_sort' => 'nq_desc',
'display_filter' => '%2B|Nq|Lt|1000',
],
];
$results = $this->client->get( self::KEYPHRASES_URL, $options );
\set_transient( $transient_key, $results, \DAY_IN_SECONDS );
return $this->to_result_object( $results );
} catch ( Exception $e ) {
return (object) [
'error' => $e->getMessage(),
'status' => $e->getCode(),
];
}
}
/**
* Converts the passed dataset to an object.
*
* @param array $result The result dataset to convert to an object.
*
* @return object The result object.
*/
protected function to_result_object( $result ) {
return (object) [
'results' => $result['data'],
'status' => $result['status'],
];
}
}
semrush/semrush-login-action.php 0000644 00000002333 14672432024 0013017 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\SEMrush;
use Yoast\WP\SEO\Config\SEMrush_Client;
use Yoast\WP\SEO\Exceptions\OAuth\Authentication_Failed_Exception;
/**
* Class SEMrush_Login_Action
*/
class SEMrush_Login_Action {
/**
* The SEMrush_Client instance.
*
* @var SEMrush_Client
*/
protected $client;
/**
* SEMrush_Login_Action constructor.
*
* @param SEMrush_Client $client The API client.
*/
public function __construct( SEMrush_Client $client ) {
$this->client = $client;
}
/**
* Authenticates with SEMrush to request the necessary tokens.
*
* @param string $code The authentication code to use to request a token with.
*
* @return object The response object.
*/
public function authenticate( $code ) {
// Code has already been validated at this point. No need to do that again.
try {
$tokens = $this->client->request_tokens( $code );
return (object) [
'tokens' => $tokens->to_array(),
'status' => 200,
];
} catch ( Authentication_Failed_Exception $e ) {
return $e->get_response();
}
}
/**
* Performs the login request, if necessary.
*/
public function login() {
if ( $this->client->has_valid_tokens() ) {
return;
}
// Prompt with login screen.
}
}
semrush/semrush-options-action.php 0000644 00000002140 14672432024 0013376 0 ustar 00 <?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',
];
}
}
integrations-action.php 0000644 00000002366 14672432024 0011251 0 ustar 00 <?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',
];
}
}
With our new Self-Guided Tour option, you can tour Campus Edge on your time - whether it's after-hours, during the day or on the weekends. All you need to do is register and download the Tour24 app to tour our property without a leasing agent whenever it's convenient for you!
Located near the entrance of NC State’s Centennial Campus, Campus Edge is amongst the top of the off-campus housing choices in Raleigh, NC. We offer spacious 1, 2, and 3 bedroom apartments with fully furnished options and all-inclusive rates. Each pet-friendly apartment features large private bedrooms, private bathrooms, individual leases, walk-in closets, full-size washer and dryer, and a patio or balcony. We offer both furnished and unfurnished options to best fit your needs. Upgrade your new home with our modern furniture package and luxury finishes such as vinyl wood flooring, quartz kitchen countertops, faux stainless steel appliances, and garage parking. High-speed internet, cable, water, trash and sewer service, and uncovered parking are all included in your regular monthly rental installment.
Whether you go to NC State, Meredith or Wake Tech, our premium apartments are the place for you! You’ll love our community amenities, including not 1, but 2 resort-style pools, basketball court, volleyball court, hammock area, 24-hour fitness center with Wellbeats Virtual Fitness, 24-hour computer lab with both PC and Mac computers and high-speed printer, large game room with 4 LED curved TVs and 60″ flat screens, dog park, car wash station, and more! Stop by our office at 3551 Cum Laude Court, Raleigh, NC to speak with our team and to learn more about our community. We can’t wait to show you around!
Browse through our community gallery and picture yourself living the ultimate off-campus experience at Campus Edge! Loving what you see? Check out our virtual tour for a closer look of our apartments and community amenities, or schedule an in-person tour with one of our leasing professionals.
Explore the apartment home at your own pace through a video conference with one of our agents.
Self-Guided Tour
Explore the apartment home a your own pace. Pick up the key from our staff and return it when you're finished.
Available Times
*Please be sure to bring a form of identification with you in order to take your scheduled tour. You'll receive an email 24 hours before your tour as a reminder.
Welcome to our website!
We have updated that you are using an outdated version of your browser. Please download the update.