This commit is contained in:
KhaiNguyen
2020-02-13 10:39:37 +07:00
commit 59401cb805
12867 changed files with 4646216 additions and 0 deletions

View File

@@ -0,0 +1,518 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Inc
*/
/**
* Represents the addon manager.
*/
class WPSEO_Addon_Manager {
/**
* Holds the name of the transient.
*
* @var string
*/
const SITE_INFORMATION_TRANSIENT = 'wpseo_site_information';
/**
* Holds the slug for YoastSEO free.
*
* @var string
*/
const FREE_SLUG = 'yoast-seo-wordpress';
/**
* Holds the slug for YoastSEO Premium.
*
* @var string
*/
const PREMIUM_SLUG = 'yoast-seo-wordpress-premium';
/**
* Holds the slug for Yoast News.
*
* @var string
*/
const NEWS_SLUG = 'yoast-seo-news';
/**
* Holds the slug for Video.
*
* @var string
*/
const VIDEO_SLUG = 'yoast-seo-video';
/**
* Holds the slug for WooCommerce.
*
* @var string
*/
const WOOCOMMERCE_SLUG = 'yoast-seo-woocommerce';
/**
* Holds the slug for Local.
*
* @var string
*/
const LOCAL_SLUG = 'yoast-seo-local';
/**
* The expected addon data.
*
* @var array
*/
protected static $addons = [
'wp-seo-premium.php' => self::PREMIUM_SLUG,
'wpseo-news.php' => self::NEWS_SLUG,
'video-seo.php' => self::VIDEO_SLUG,
'wpseo-woocommerce.php' => self::WOOCOMMERCE_SLUG,
'local-seo.php' => self::LOCAL_SLUG,
];
/**
* Holds the site information data.
*
* @var object
*/
private $site_information;
/**
* Hooks into WordPress.
*
* @codeCoverageIgnore
*
* @return void
*/
public function register_hooks() {
add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'check_for_updates' ] );
add_filter( 'plugins_api', [ $this, 'get_plugin_information' ], 10, 3 );
}
/**
* Gets the subscriptions for current site.
*
* @return stdClass The subscriptions.
*/
public function get_subscriptions() {
return $this->get_site_information()->subscriptions;
}
/**
* Retrieves the subscription for the given slug.
*
* @param string $slug The plugin slug to retrieve.
*
* @return stdClass|false Subscription data when found, false when not found.
*/
public function get_subscription( $slug ) {
foreach ( $this->get_subscriptions() as $subscription ) {
if ( $subscription->product->slug === $slug ) {
return $subscription;
}
}
return false;
}
/**
* Retrieves a list of (subscription) slugs by the active addons.
*
* @return array The slugs.
*/
public function get_subscriptions_for_active_addons() {
$active_addons = array_keys( $this->get_active_addons() );
$subscription_slugs = array_map( [ $this, 'get_slug_by_plugin_file' ], $active_addons );
$subscriptions = [];
foreach ( $subscription_slugs as $subscription_slug ) {
$subscriptions[ $subscription_slug ] = $this->get_subscription( $subscription_slug );
}
return $subscriptions;
}
/**
* Retrieves a list of versions for each addon.
*
* @return array The addon versions.
*/
public function get_installed_addons_versions() {
$addon_versions = [];
foreach ( $this->get_installed_addons() as $plugin_file => $installed_addon ) {
$addon_versions[ $this->get_slug_by_plugin_file( $plugin_file ) ] = $installed_addon['Version'];
}
return $addon_versions;
}
/**
* Retrieves the plugin information from the subscriptions.
*
* @param stdClass|false $data The result object. Default false.
* @param string $action The type of information being requested from the Plugin Installation API.
* @param stdClass $args Plugin API arguments.
*
* @return object Extended plugin data.
*/
public function get_plugin_information( $data, $action, $args ) {
if ( $action !== 'plugin_information' ) {
return $data;
}
if ( ! isset( $args->slug ) ) {
return $data;
}
$subscription = $this->get_subscription( $args->slug );
if ( ! $subscription || $this->has_subscription_expired( $subscription ) ) {
return $data;
}
return $this->convert_subscription_to_plugin( $subscription );
}
/**
* Checks if the subscription for the given slug is valid.
*
* @param string $slug The plugin slug to retrieve.
*
* @return bool True when the subscription is valid.
*/
public function has_valid_subscription( $slug ) {
$subscription = $this->get_subscription( $slug );
// An non-existing subscription is never valid.
if ( $subscription === false ) {
return false;
}
return ! $this->has_subscription_expired( $subscription );
}
/**
* Checks if there are addon updates.
*
* @param stdClass|mixed $data The current data for update_plugins.
*
* @return stdClass Extended data for update_plugins.
*/
public function check_for_updates( $data ) {
if ( empty( $data ) ) {
return $data;
}
foreach ( $this->get_installed_addons() as $plugin_file => $installed_plugin ) {
$subscription_slug = $this->get_slug_by_plugin_file( $plugin_file );
$subscription = $this->get_subscription( $subscription_slug );
if ( ! $subscription || $this->has_subscription_expired( $subscription ) ) {
continue;
}
if ( version_compare( $installed_plugin['Version'], $subscription->product->version, '<' ) ) {
$data->response[ $plugin_file ] = $this->convert_subscription_to_plugin( $subscription );
}
}
return $data;
}
/**
* Checks whether a plugin expiry date has been passed.
*
* @param stdClass $subscription Plugin subscription.
*
* @return bool Has the plugin expired.
*/
protected function has_subscription_expired( $subscription ) {
return ( strtotime( $subscription->expiry_date ) - time() ) < 0;
}
/**
* Converts a subscription to plugin based format.
*
* @param stdClass $subscription The subscription to convert.
*
* @return stdClass The converted subscription.
*/
protected function convert_subscription_to_plugin( $subscription ) {
return (object) [
'new_version' => $subscription->product->version,
'name' => $subscription->product->name,
'slug' => $subscription->product->slug,
'url' => $subscription->product->store_url,
'last_update' => $subscription->product->last_updated,
'homepage' => $subscription->product->store_url,
'download_link' => $subscription->product->download,
'package' => $subscription->product->download,
'sections' =>
[
'changelog' => $subscription->product->changelog,
],
];
}
/**
* Checks if the given plugin_file belongs to a Yoast addon.
*
* @param string $plugin_file Path to the plugin.
*
* @return bool True when plugin file is for a Yoast addon.
*/
protected function is_yoast_addon( $plugin_file ) {
return $this->get_slug_by_plugin_file( $plugin_file ) !== '';
}
/**
* Retrieves the addon slug by given plugin file path.
*
* @param string $plugin_file The file path to the plugin.
*
* @return string The slug when found or empty string when not.
*/
protected function get_slug_by_plugin_file( $plugin_file ) {
$addons = self::$addons;
// Yoast SEO Free isn't an addon, but we needed it in Premium to fetch translations.
if ( WPSEO_Utils::is_yoast_seo_premium() ) {
$addons['wp-seo.php'] = self::FREE_SLUG;
}
foreach ( $addons as $addon => $addon_slug ) {
if ( strpos( $plugin_file, $addon ) !== false ) {
return $addon_slug;
}
}
return '';
}
/**
* Retrieves the installed Yoast addons.
*
* @return array The installed plugins.
*/
protected function get_installed_addons() {
return $this->filter_by_key( $this->get_plugins(), [ $this, 'is_yoast_addon' ] );
}
/**
* Retrieves a list of active addons.
*
* @return array The active addons.
*/
protected function get_active_addons() {
return $this->filter_by_key( $this->get_installed_addons(), [ $this, 'is_plugin_active' ] );
}
/**
* Retrieves the current sites from the API.
*
* @codeCoverageIgnore
*
* @return bool|stdClass Object when request is successful. False if not.
*/
protected function request_current_sites() {
$api_request = new WPSEO_MyYoast_Api_Request( 'sites/current' );
if ( $api_request->fire() ) {
return $api_request->get_response();
}
return $this->get_site_information_default();
}
/**
* Retrieves the transient value with the site information.
*
* @codeCoverageIgnore
*
* @return stdClass|false The transient value.
*/
protected function get_site_information_transient() {
global $pagenow;
// Force re-check on license & dashboard pages.
$current_page = $this->get_current_page();
// Check whether the licenses are valid or whether we need to show notifications.
$exclude_cache = ( $current_page === 'wpseo_licenses' || $current_page === 'wpseo_dashboard' );
// Also do a fresh request on Plugins & Core Update pages.
$exclude_cache = $exclude_cache || $pagenow === 'plugins.php';
$exclude_cache = $exclude_cache || $pagenow === 'update-core.php';
if ( $exclude_cache ) {
return false;
}
return get_transient( self::SITE_INFORMATION_TRANSIENT );
}
/**
* Returns the current page.
*
* @codeCoverageIgnore
*
* @return string The current page.
*/
protected function get_current_page() {
return filter_input( INPUT_GET, 'page' );
}
/**
* Sets the site information transient.
*
* @codeCoverageIgnore
*
* @param stdClass $site_information The site information to save.
*
* @return void
*/
protected function set_site_information_transient( $site_information ) {
set_transient( self::SITE_INFORMATION_TRANSIENT, $site_information, DAY_IN_SECONDS );
}
/**
* Retrieves all installed WordPress plugins.
*
* @codeCoverageIgnore
*
* @return array The plugins.
*/
protected function get_plugins() {
return get_plugins();
}
/**
* Checks if the given plugin file belongs to an active plugin.
*
* @codeCoverageIgnore
*
* @param string $plugin_file The file path to the plugin.
*
* @return bool True when plugin is active.
*/
protected function is_plugin_active( $plugin_file ) {
return is_plugin_active( $plugin_file );
}
/**
* Returns an object with no subscriptions.
*
* @codeCoverageIgnore
*
* @return stdClass Site information.
*/
protected function get_site_information_default() {
return (object) [
'url' => WPSEO_Utils::get_home_url(),
'subscriptions' => [],
];
}
/**
* Checks if there are any installed addons.
*
* @return bool True when there are installed Yoast addons.
*/
protected function has_installed_addons() {
$installed_addons = $this->get_installed_addons();
return ! empty( $installed_addons );
}
/**
* Filters the given array by its keys.
*
* This method is temporary. When WordPress has minimal PHP 5.6 support we can change this to:
*
* array_filter( $array_to_filter, $filter, ARRAY_FILTER_USE_KEY )
*
* @codeCoverageIgnore
*
* @param array $array_to_filter The array to filter.
* @param callable $callback The filter callback.
*
* @return array The filtered array,
*/
private function filter_by_key( $array_to_filter, $callback ) {
$keys_to_filter = array_filter( array_keys( $array_to_filter ), $callback );
$filtered_array = [];
foreach ( $keys_to_filter as $filtered_key ) {
$filtered_array[ $filtered_key ] = $array_to_filter[ $filtered_key ];
}
return $filtered_array;
}
/**
* Maps the plugin API response.
*
* @param object $site_information Site information as received from the API.
*
* @return object Mapped site information.
*/
protected function map_site_information( $site_information ) {
return (object) [
'url' => $site_information->url,
'subscriptions' => array_map( [ $this, 'map_subscription' ], $site_information->subscriptions ),
];
}
/**
* Maps a plugin subscription.
*
* @param object $subscription Subscription information as received from the API.
*
* @return object Mapped subscription.
*/
protected function map_subscription( $subscription ) {
// @codingStandardsIgnoreStart
return (object) array(
'renewal_url' => $subscription->renewalUrl,
'expiry_date' => $subscription->expiryDate,
'product' => (object) array(
'version' => $subscription->product->version,
'name' => $subscription->product->name,
'slug' => $subscription->product->slug,
'last_updated' => $subscription->product->lastUpdated,
'store_url' => $subscription->product->storeUrl,
// Ternary operator is necessary because download can be undefined.
'download' => isset( $subscription->product->download ) ? $subscription->product->download : null,
'changelog' => $subscription->product->changelog,
),
);
// @codingStandardsIgnoreStop
}
/**
* Retrieves the site information.
*
* @return stdClass The site information.
*/
private function get_site_information() {
if ( ! $this->has_installed_addons() ) {
return $this->get_site_information_default();
}
if ( $this->site_information === null ) {
$this->site_information = $this->get_site_information_transient();
}
if ( $this->site_information ) {
return $this->site_information;
}
$this->site_information = $this->request_current_sites();
if ( $this->site_information ) {
$this->site_information = $this->map_site_information( $this->site_information );
$this->set_site_information_transient( $this->site_information );
return $this->site_information;
}
return $this->get_site_information_default();
}
}

View File

@@ -0,0 +1,363 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Inc
*/
/**
* Handles requests to MyYoast.
*/
class WPSEO_MyYoast_Api_Request {
/**
* The Request URL.
*
* @var string
*/
protected $url;
/**
* The request parameters.
*
* @var array
*/
protected $args = [
'method' => 'GET',
'timeout' => 5,
'headers' => [
'Accept-Encoding' => '*',
],
];
/**
* Contains the fetched response.
*
* @var stdClass
*/
protected $response;
/**
* Contains the error message when request went wrong.
*
* @var string
*/
protected $error_message = '';
/**
* The MyYoast client object.
*
* @var WPSEO_MyYoast_Client
*/
protected $client;
/**
* Constructor.
*
* @codeCoverageIgnore
*
* @param string $url The request url.
* @param array $args The request arguments.
*/
public function __construct( $url, array $args = [] ) {
$this->url = 'https://my.yoast.com/api/' . $url;
$this->args = wp_parse_args( $args, $this->args );
}
/**
* Fires the request.
*
* @return bool True when request is successful.
*/
public function fire() {
try {
$response = $this->do_request( $this->url, $this->args );
$this->response = $this->decode_response( $response );
return true;
}
/**
* The Authentication exception only occurs when using Access Tokens (>= PHP 5.6).
* In other case this exception won't be thrown.
*
* When authentication failed just try to get a new access token based
* on the refresh token. If that request also has an authentication issue
* we just invalidate the access token by removing it.
*/
catch ( WPSEO_MyYoast_Authentication_Exception $authentication_exception ) {
try {
$access_token = $this->get_access_token();
if ( $access_token !== false ) {
$response = $this->do_request( $this->url, $this->args );
$this->response = $this->decode_response( $response );
}
return true;
}
catch ( WPSEO_MyYoast_Authentication_Exception $authentication_exception ) {
$this->error_message = $authentication_exception->getMessage();
$this->remove_access_token( $this->get_current_user_id() );
return false;
}
catch ( WPSEO_MyYoast_Bad_Request_Exception $bad_request_exception ) {
$this->error_message = $bad_request_exception->getMessage();
return false;
}
}
catch ( WPSEO_MyYoast_Bad_Request_Exception $bad_request_exception ) {
$this->error_message = $bad_request_exception->getMessage();
return false;
}
}
/**
* Retrieves the error message.
*
* @return string The set error message.
*/
public function get_error_message() {
return $this->error_message;
}
/**
* Retrieves the response.
*
* @return stdClass The response object.
*/
public function get_response() {
return $this->response;
}
/**
* Performs the request using WordPress internals.
*
* @codeCoverageIgnore
*
* @param string $url The request URL.
* @param array $request_arguments The request arguments.
*
* @return string The retrieved body.
* @throws WPSEO_MyYoast_Authentication_Exception When authentication has failed.
* @throws WPSEO_MyYoast_Bad_Request_Exception When request is invalid.
*/
protected function do_request( $url, $request_arguments ) {
$request_arguments = $this->enrich_request_arguments( $request_arguments );
$response = wp_remote_request( $url, $request_arguments );
if ( is_wp_error( $response ) ) {
throw new WPSEO_MyYoast_Bad_Request_Exception( $response->get_error_message() );
}
$response_code = wp_remote_retrieve_response_code( $response );
$response_message = wp_remote_retrieve_response_message( $response );
// Do nothing, response code is okay.
if ( $response_code === 200 || strpos( $response_code, '200' ) !== false ) {
return wp_remote_retrieve_body( $response );
}
// Authentication failed, throw an exception.
if ( strpos( $response_code, '401' ) && $this->has_oauth_support() ) {
throw new WPSEO_MyYoast_Authentication_Exception( esc_html( $response_message ), 401 );
}
throw new WPSEO_MyYoast_Bad_Request_Exception( esc_html( $response_message ), (int) $response_code );
}
/**
* Decodes the JSON encoded response.
*
* @param string $response The response to decode.
*
* @return stdClass The json decoded response.
* @throws WPSEO_MyYoast_Invalid_JSON_Exception When decoded string is not a JSON object.
*/
protected function decode_response( $response ) {
$response = json_decode( $response );
if ( ! is_object( $response ) ) {
throw new WPSEO_MyYoast_Invalid_JSON_Exception(
esc_html__( 'No JSON object was returned.', 'wordpress-seo' )
);
}
return $response;
}
/**
* Checks if MyYoast tokens are allowed and adds the token to the request body.
*
* When tokens are disallowed it will add the url to the request body.
*
* @param array $request_arguments The arguments to enrich.
*
* @return array The enriched arguments.
*/
protected function enrich_request_arguments( array $request_arguments ) {
$request_arguments = wp_parse_args( $request_arguments, [ 'headers' => [] ] );
$addon_version_headers = $this->get_installed_addon_versions();
foreach ( $addon_version_headers as $addon => $version ) {
$request_arguments['headers'][ $addon . '-version' ] = $version;
}
$request_body = $this->get_request_body();
if ( $request_body !== [] ) {
$request_arguments['body'] = $request_body;
}
return $request_arguments;
}
/**
* Retrieves the request body based on URL or access token support.
*
* @codeCoverageIgnore
*
* @return array The request body.
*/
public function get_request_body() {
if ( ! $this->has_oauth_support() ) {
return [ 'url' => WPSEO_Utils::get_home_url() ];
}
try {
$access_token = $this->get_access_token();
if ( $access_token ) {
return [ 'token' => $access_token->getToken() ];
}
}
// @codingStandardsIgnoreLine Generic.CodeAnalysis.EmptyStatement.DetectedCATCH -- There is nothing to do.
catch ( WPSEO_MyYoast_Bad_Request_Exception $bad_request ) {
// Do nothing.
}
return [];
}
/**
* Retrieves the access token.
*
* @codeCoverageIgnore
*
* @return bool|WPSEO_MyYoast_AccessToken_Interface The AccessToken when valid.
* @throws WPSEO_MyYoast_Bad_Request_Exception When something went wrong in getting the access token.
*/
protected function get_access_token() {
$client = $this->get_client();
if ( ! $client ) {
return false;
}
$access_token = $client->get_access_token();
if ( ! $access_token ) {
return false;
}
if ( ! $access_token->hasExpired() ) {
return $access_token;
}
try {
$access_token = $client
->get_provider()
->getAccessToken(
'refresh_token',
[
'refresh_token' => $access_token->getRefreshToken(),
]
);
$client->save_access_token( $this->get_current_user_id(), $access_token );
return $access_token;
}
catch ( Exception $e ) {
$error_code = $e->getCode();
if ( $error_code >= 400 && $error_code < 500 ) {
$this->remove_access_token( $this->get_current_user_id() );
}
throw new WPSEO_MyYoast_Bad_Request_Exception( $e->getMessage() );
}
}
/**
* Retrieves an instance of the MyYoast client.
*
* @codeCoverageIgnore
*
* @return WPSEO_MyYoast_Client Instance of the client.
*/
protected function get_client() {
if ( $this->client === null ) {
$this->client = new WPSEO_MyYoast_Client();
}
return $this->client;
}
/**
* Wraps the get current user id function.
*
* @codeCoverageIgnore
*
* @return int The user id.
*/
protected function get_current_user_id() {
return get_current_user_id();
}
/**
* Removes the access token for given user id.
*
* @codeCoverageIgnore
*
* @param int $user_id The user id.
*
* @return void
*/
protected function remove_access_token( $user_id ) {
if ( ! $this->has_oauth_support() ) {
return;
}
// Remove the access token entirely.
$this->get_client()->remove_access_token( $user_id );
}
/**
* Retrieves the installed addons as http headers.
*
* @codeCoverageIgnore
*
* @return array The installed addon versions.
*/
protected function get_installed_addon_versions() {
$addon_manager = new WPSEO_Addon_Manager();
return $addon_manager->get_installed_addons_versions();
}
/**
* Wraps the has_access_token support method.
*
* @codeCoverageIgnore
*
* @return bool False to disable the support.
*/
protected function has_oauth_support() {
return false;
// @todo: Uncomment the following statement when we are implementing the oAuth flow.
// return WPSEO_Utils::has_access_token_support();
}
}

View File

@@ -0,0 +1,120 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Inc
*/
/**
* Represents the post type utils.
*/
class WPSEO_Post_Type {
/**
* Returns an array with the accessible post types.
*
* An accessible post type is a post type that is public and isn't set as no-index (robots).
*
* @return array Array with all the accessible post_types.
*/
public static function get_accessible_post_types() {
$post_types = get_post_types( [ 'public' => true ] );
$post_types = array_filter( $post_types, 'is_post_type_viewable' );
/**
* Filter: 'wpseo_accessible_post_types' - Allow changing the accessible post types.
*
* @api array $post_types The public post types.
*/
$post_types = apply_filters( 'wpseo_accessible_post_types', $post_types );
// When the array gets messed up somewhere.
if ( ! is_array( $post_types ) ) {
return [];
}
return $post_types;
}
/**
* Returns whether the passed post type is considered accessible.
*
* @param string $post_type The post type to check.
*
* @return bool Whether or not the post type is considered accessible.
*/
public static function is_post_type_accessible( $post_type ) {
return in_array( $post_type, self::get_accessible_post_types(), true );
}
/**
* Checks if the request post type is public and indexable.
*
* @param string $post_type_name The name of the post type to lookup.
*
* @return bool True when post type is set to index.
*/
public static function is_post_type_indexable( $post_type_name ) {
if ( WPSEO_Options::get( 'disable-' . $post_type_name, false ) ) {
return false;
}
return ( WPSEO_Options::get( 'noindex-' . $post_type_name, false ) === false );
}
/**
* Filters the attachment post type from an array with post_types.
*
* @param array $post_types The array to filter the attachment post type from.
*
* @return array The filtered array.
*/
public static function filter_attachment_post_type( array $post_types ) {
unset( $post_types['attachment'] );
return $post_types;
}
/**
* Checks if the post type is enabled in the REST API.
*
* @param string $post_type The post type to check.
*
* @return bool Whether or not the post type is available in the REST API.
*/
public static function is_rest_enabled( $post_type ) {
$post_type_object = get_post_type_object( $post_type );
if ( $post_type_object === null ) {
return false;
}
return $post_type_object->show_in_rest === true;
}
/**
* Checks if the current post type has an archive.
*
* Context: The has_archive value can be a string or a boolean. In most case it will be a boolean,
* but it can be defined as a string. When it is a string the archive_slug will be overwritten to
* define another endpoint.
*
* @param WP_Post_Type $post_type The post type object.
*
* @return bool True whether the post type has an archive.
*/
public static function has_archive( $post_type ) {
return ( ! empty( $post_type->has_archive ) );
}
/**
* Checks if the Yoast Metabox has been enabled for the post type.
*
* @param string $post_type The post type name.
*
* @return bool True whether the metabox is enabled.
*/
public static function has_metabox_enabled( $post_type ) {
return WPSEO_Options::get( 'display-metabox-pt-' . $post_type, false );
}
}

View File

@@ -0,0 +1,238 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend
*/
/**
* This code handles the category rewrites.
*/
class WPSEO_Rewrite {
/**
* Class constructor.
*/
public function __construct() {
add_filter( 'query_vars', [ $this, 'query_vars' ] );
add_filter( 'category_link', [ $this, 'no_category_base' ] );
add_filter( 'request', [ $this, 'request' ] );
add_filter( 'category_rewrite_rules', [ $this, 'category_rewrite_rules' ] );
add_action( 'created_category', [ $this, 'schedule_flush' ] );
add_action( 'edited_category', [ $this, 'schedule_flush' ] );
add_action( 'delete_category', [ $this, 'schedule_flush' ] );
add_action( 'init', [ $this, 'flush' ], 999 );
}
/**
* Save an option that triggers a flush on the next init.
*
* @since 1.2.8
*/
public function schedule_flush() {
update_option( 'wpseo_flush_rewrite', 1 );
}
/**
* If the flush option is set, flush the rewrite rules.
*
* @since 1.2.8
*
* @return bool
*/
public function flush() {
if ( get_option( 'wpseo_flush_rewrite' ) ) {
add_action( 'shutdown', 'flush_rewrite_rules' );
delete_option( 'wpseo_flush_rewrite' );
return true;
}
return false;
}
/**
* Override the category link to remove the category base.
*
* @param string $link Unused, overridden by the function.
*
* @return string
*/
public function no_category_base( $link ) {
$category_base = get_option( 'category_base' );
if ( empty( $category_base ) ) {
$category_base = 'category';
}
/*
* Remove initial slash, if there is one (we remove the trailing slash
* in the regex replacement and don't want to end up short a slash).
*/
if ( substr( $category_base, 0, 1 ) === '/' ) {
$category_base = substr( $category_base, 1 );
}
$category_base .= '/';
return preg_replace( '`' . preg_quote( $category_base, '`' ) . '`u', '', $link, 1 );
}
/**
* Update the query vars with the redirect var when stripcategorybase is active.
*
* @param array $query_vars Main query vars to filter.
*
* @return array
*/
public function query_vars( $query_vars ) {
if ( WPSEO_Options::get( 'stripcategorybase' ) === true ) {
$query_vars[] = 'wpseo_category_redirect';
}
return $query_vars;
}
/**
* Checks whether the redirect needs to be created.
*
* @param array $query_vars Query vars to check for existence of redirect var.
*
* @return array|void The query vars.
*/
public function request( $query_vars ) {
if ( ! isset( $query_vars['wpseo_category_redirect'] ) ) {
return $query_vars;
}
$this->redirect( $query_vars['wpseo_category_redirect'] );
}
/**
* This function taken and only slightly adapted from WP No Category Base plugin by Saurabh Gupta.
*
* @return array
*/
public function category_rewrite_rules() {
global $wp_rewrite;
$category_rewrite = [];
$taxonomy = get_taxonomy( 'category' );
$permalink_structure = get_option( 'permalink_structure' );
$blog_prefix = '';
if ( is_multisite() && ! is_subdomain_install() && is_main_site() && strpos( $permalink_structure, '/blog/' ) === 0 ) {
$blog_prefix = 'blog/';
}
$categories = get_categories( [ 'hide_empty' => false ] );
if ( is_array( $categories ) && $categories !== [] ) {
foreach ( $categories as $category ) {
$category_nicename = $category->slug;
if ( $category->parent == $category->cat_ID ) {
// Recursive recursion.
$category->parent = 0;
}
elseif ( $taxonomy->rewrite['hierarchical'] != 0 && $category->parent !== 0 ) {
$parents = get_category_parents( $category->parent, false, '/', true );
if ( ! is_wp_error( $parents ) ) {
$category_nicename = $parents . $category_nicename;
}
unset( $parents );
}
$category_rewrite = $this->add_category_rewrites( $category_rewrite, $category_nicename, $blog_prefix, $wp_rewrite->pagination_base );
// Adds rules for the uppercase encoded URIs.
$category_nicename_filtered = $this->convert_encoded_to_upper( $category_nicename );
if ( $category_nicename_filtered !== $category_nicename ) {
$category_rewrite = $this->add_category_rewrites( $category_rewrite, $category_nicename_filtered, $blog_prefix, $wp_rewrite->pagination_base );
}
}
unset( $categories, $category, $category_nicename, $category_nicename_filtered );
}
// Redirect support from Old Category Base.
$old_base = $wp_rewrite->get_category_permastruct();
$old_base = str_replace( '%category%', '(.+)', $old_base );
$old_base = trim( $old_base, '/' );
$category_rewrite[ $old_base . '$' ] = 'index.php?wpseo_category_redirect=$matches[1]';
return $category_rewrite;
}
/**
* Adds required category rewrites rules.
*
* @param array $rewrites The current set of rules.
* @param string $category_name Category nicename.
* @param string $blog_prefix Multisite blog prefix.
* @param string $pagination_base WP_Query pagination base.
*
* @return array The added set of rules.
*/
protected function add_category_rewrites( $rewrites, $category_name, $blog_prefix, $pagination_base ) {
$rewrite_name = $blog_prefix . '(' . $category_name . ')';
$rewrites[ $rewrite_name . '/(?:feed/)?(feed|rdf|rss|rss2|atom)/?$' ] = 'index.php?category_name=$matches[1]&feed=$matches[2]';
$rewrites[ $rewrite_name . '/' . $pagination_base . '/?([0-9]{1,})/?$' ] = 'index.php?category_name=$matches[1]&paged=$matches[2]';
$rewrites[ $rewrite_name . '/?$' ] = 'index.php?category_name=$matches[1]';
return $rewrites;
}
/**
* Walks through category nicename and convert encoded parts
* into uppercase using $this->encode_to_upper().
*
* @param string $name The encoded category URI string.
*
* @return string The convered URI string.
*/
protected function convert_encoded_to_upper( $name ) {
// Checks if name has any encoding in it.
if ( strpos( $name, '%' ) === false ) {
return $name;
}
$names = explode( '/', $name );
$names = array_map( [ $this, 'encode_to_upper' ], $names );
return implode( '/', $names );
}
/**
* Converts the encoded URI string to uppercase.
*
* @param string $encoded The encoded string.
*
* @return string The uppercased string.
*/
public function encode_to_upper( $encoded ) {
if ( strpos( $encoded, '%' ) === false ) {
return $encoded;
}
return strtoupper( $encoded );
}
/**
* Redirect the "old" category URL to the new one.
*
* @codeCoverageIgnore
*
* @param string $category_redirect The category page to redirect to.
* @return void
*/
protected function redirect( $category_redirect ) {
$catlink = trailingslashit( get_option( 'home' ) ) . user_trailingslashit( $category_redirect, 'category' );
wp_redirect( $catlink, 301, 'Yoast SEO' );
exit;
}
} /* End of class */

View File

@@ -0,0 +1,81 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Class to load assets required for structured data blocks.
*/
class WPSEO_Structured_Data_Blocks implements WPSEO_WordPress_Integration {
/**
* An instance of the WPSEO_Admin_Asset_Manager class.
*
* @var WPSEO_Admin_Asset_Manager
*/
protected $asset_manager;
/**
* Registers hooks for Structured Data Blocks with WordPress.
*/
public function register_hooks() {
add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_block_editor_assets' ] );
add_filter( 'block_categories', [ $this, 'add_block_category' ] );
}
/**
* Checks whether the Structured Data Blocks are disabled.
*
* @return boolean
*/
private function check_enabled() {
/**
* Filter: 'wpseo_enable_structured_data_blocks' - Allows disabling Yoast's schema blocks entirely.
*
* @api bool If false, our structured data blocks won't show.
*/
$enabled = apply_filters( 'wpseo_enable_structured_data_blocks', true );
return $enabled;
}
/**
* Enqueue Gutenberg block assets for backend editor.
*/
public function enqueue_block_editor_assets() {
if ( ! $this->check_enabled() ) {
return;
}
if ( ! $this->asset_manager ) {
$this->asset_manager = new WPSEO_Admin_Asset_Manager();
}
$this->asset_manager->enqueue_script( 'structured-data-blocks' );
$this->asset_manager->enqueue_style( 'structured-data-blocks' );
}
/**
* Adds the structured data blocks category to the Gutenberg categories.
*
* @param array $categories The current categories.
*
* @return array The updated categories.
*/
public function add_block_category( $categories ) {
if ( $this->check_enabled() ) {
$categories[] = [
'slug' => 'yoast-structured-data-blocks',
'title' => sprintf(
/* translators: %1$s expands to Yoast. */
__( '%1$s Structured Data Blocks', 'wordpress-seo' ),
'Yoast'
),
];
}
return $categories;
}
}

View File

@@ -0,0 +1,133 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internal
*/
/**
* This class handles storing the current options for future reference.
*
* This should only be used during an upgrade routine.
*/
class WPSEO_Upgrade_History {
/**
* Option to use to store/retrieve data from.
*
* @var string
*/
protected $option_name = 'wpseo_upgrade_history';
/**
* WPSEO_Upgrade_History constructor.
*
* @param null|string $option_name Optional. Custom option to use to store/retrieve history from.
*/
public function __construct( $option_name = null ) {
if ( $option_name !== null ) {
$this->option_name = $option_name;
}
}
/**
* Retrieves the content of the history items currently stored.
*
* @return array The contents of the history option.
*/
public function get() {
$data = get_option( $this->get_option_name(), [] );
if ( ! is_array( $data ) ) {
return [];
}
return $data;
}
/**
* Adds a new history entry in the storage.
*
* @param string $old_version The version we are upgrading from.
* @param string $new_version The version we are upgrading to.
* @param array $option_names The options that need to be stored.
*/
public function add( $old_version, $new_version, array $option_names ) {
$option_data = [];
if ( $option_names !== [] ) {
$option_data = $this->get_options_data( $option_names );
}
// Retrieve current history.
$data = $this->get();
// Add new entry.
$data[ time() ] = [
'options' => $option_data,
'old_version' => $old_version,
'new_version' => $new_version,
];
// Store the data.
$this->set( $data );
}
/**
* Retrieves the data for the specified option names from the database.
*
* @param array $option_names The option names to retrieve.
*
* @return array
*/
protected function get_options_data( array $option_names ) {
$wpdb = $this->get_wpdb();
$sql = $wpdb->prepare(
'
SELECT option_value, option_name FROM ' . $wpdb->options . ' WHERE
option_name IN ( ' . implode( ',', array_fill( 0, count( $option_names ), '%s' ) ) . ' )
',
$option_names
);
$results = $wpdb->get_results( $sql, ARRAY_A );
$data = [];
foreach ( $results as $result ) {
$data[ $result['option_name'] ] = maybe_unserialize( $result['option_value'] );
}
return $data;
}
/**
* Stores the new history state.
*
* @param array $data The data to store.
*
* @return void
*/
protected function set( array $data ) {
// This should not be autoloaded!
update_option( $this->get_option_name(), $data, false );
}
/**
* Retrieves the WPDB object.
*
* @return wpdb The WPDB object to use.
*/
protected function get_wpdb() {
global $wpdb;
return $wpdb;
}
/**
* Retrieves the option name to store the history in.
*
* @return string The option name to store the history in.
*/
protected function get_option_name() {
return $this->option_name;
}
}

View File

@@ -0,0 +1,865 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internal
*/
/**
* This code handles the option upgrades.
*/
class WPSEO_Upgrade {
/**
* Class constructor.
*/
public function __construct() {
$version = WPSEO_Options::get( 'version' );
WPSEO_Options::maybe_set_multisite_defaults( false );
$routines = [
'1.5.0' => 'upgrade_15',
'2.0' => 'upgrade_20',
'2.1' => 'upgrade_21',
'2.2' => 'upgrade_22',
'2.3' => 'upgrade_23',
'3.0' => 'upgrade_30',
'3.3' => 'upgrade_33',
'3.6' => 'upgrade_36',
'4.0' => 'upgrade_40',
'4.4' => 'upgrade_44',
'4.7' => 'upgrade_47',
'4.9' => 'upgrade_49',
'5.0' => 'upgrade_50',
'5.1' => 'upgrade_50_51',
'5.5' => 'upgrade_55',
'5.6' => 'upgrade_56',
'6.1' => 'upgrade_61',
'6.3' => 'upgrade_63',
'7.0-RC0' => 'upgrade_70',
'7.1-RC0' => 'upgrade_71',
'7.3-RC0' => 'upgrade_73',
'7.4-RC0' => 'upgrade_74',
'7.5.3' => 'upgrade_753',
'7.7-RC0' => 'upgrade_77',
'7.7.2-RC0' => 'upgrade_772',
'9.0-RC0' => 'upgrade_90',
'10.0-RC0' => 'upgrade_100',
'11.1-RC0' => 'upgrade_111',
/** Reset notifications because we removed the AMP Glue plugin notification */
'12.1-RC0' => 'clean_all_notifications',
'12.3-RC0' => 'upgrade_123',
'12.4-RC0' => 'upgrade_124',
'12.8-RC0' => 'upgrade_128',
];
array_walk( $routines, [ $this, 'run_upgrade_routine' ], $version );
if ( version_compare( $version, '12.5-RC0', '<' ) ) {
/*
* We have to run this by hook, because otherwise:
* - the theme support check isn't available.
* - the notification center notifications are not filled yet.
*/
add_action( 'init', [ $this, 'upgrade_125' ] );
}
// Since 3.7.
$upsell_notice = new WPSEO_Product_Upsell_Notice();
$upsell_notice->set_upgrade_notice();
/**
* Filter: 'wpseo_run_upgrade' - Runs the upgrade hook which are dependent on Yoast SEO.
*
* @api string - The current version of Yoast SEO
*/
do_action( 'wpseo_run_upgrade', $version );
$this->finish_up();
}
/**
* Runs the upgrade routine.
*
* @param string $routine The method to call.
* @param string $version The new version.
* @param string $current_version The current set version.
*
* @return void
*/
protected function run_upgrade_routine( $routine, $version, $current_version ) {
if ( version_compare( $current_version, $version, '<' ) ) {
$this->$routine( $current_version );
}
}
/**
* Adds a new upgrade history entry.
*
* @param string $current_version The old version from which we are upgrading.
* @param string $new_version The version we are upgrading to.
*/
protected function add_upgrade_history( $current_version, $new_version ) {
$upgrade_history = new WPSEO_Upgrade_History();
$upgrade_history->add( $current_version, $new_version, array_keys( WPSEO_Options::$options ) );
}
/**
* Runs the needed cleanup after an update, setting the DB version to latest version, flushing caches etc.
*/
protected function finish_up() {
WPSEO_Options::set( 'version', WPSEO_VERSION );
// Just flush rewrites, always, to at least make them work after an upgrade.
add_action( 'shutdown', 'flush_rewrite_rules' );
// Flush the sitemap cache.
WPSEO_Sitemaps_Cache::clear();
// Make sure all our options always exist - issue #1245.
WPSEO_Options::ensure_options_exist();
}
/**
* Run the Yoast SEO 1.5 upgrade routine.
*
* @param string $version Current plugin version.
*/
private function upgrade_15( $version ) {
// Clean up options and meta.
WPSEO_Options::clean_up( null, $version );
WPSEO_Meta::clean_up();
}
/**
* Moves options that moved position in WPSEO 2.0.
*/
private function upgrade_20() {
/**
* Clean up stray wpseo_ms options from the options table, option should only exist in the sitemeta table.
* This could have been caused in many version of Yoast SEO, so deleting it for everything below 2.0.
*/
delete_option( 'wpseo_ms' );
$wpseo = $this->get_option_from_database( 'wpseo' );
$this->save_option_setting( $wpseo, 'pinterestverify' );
// Re-save option to trigger sanitization.
$this->cleanup_option_data( 'wpseo' );
}
/**
* Detects if taxonomy terms were split and updates the corresponding taxonomy meta's accordingly.
*/
private function upgrade_21() {
$taxonomies = get_option( 'wpseo_taxonomy_meta', [] );
if ( ! empty( $taxonomies ) ) {
foreach ( $taxonomies as $taxonomy => $tax_metas ) {
foreach ( $tax_metas as $term_id => $tax_meta ) {
if ( function_exists( 'wp_get_split_term' ) ) {
$new_term_id = wp_get_split_term( $term_id, $taxonomy );
if ( $new_term_id !== false ) {
$taxonomies[ $taxonomy ][ $new_term_id ] = $taxonomies[ $taxonomy ][ $term_id ];
unset( $taxonomies[ $taxonomy ][ $term_id ] );
}
}
}
}
update_option( 'wpseo_taxonomy_meta', $taxonomies );
}
}
/**
* Performs upgrade functions to Yoast SEO 2.2.
*/
private function upgrade_22() {
// Unschedule our tracking.
wp_clear_scheduled_hook( 'yoast_tracking' );
$this->cleanup_option_data( 'wpseo' );
}
/**
* Schedules upgrade function to Yoast SEO 2.3.
*/
private function upgrade_23() {
add_action( 'wp', [ $this, 'upgrade_23_query' ], 90 );
add_action( 'admin_head', [ $this, 'upgrade_23_query' ], 90 );
}
/**
* Performs upgrade query to Yoast SEO 2.3.
*/
public function upgrade_23_query() {
$wp_query = new WP_Query( 'post_type=any&meta_key=_yoast_wpseo_sitemap-include&meta_value=never&order=ASC' );
if ( ! empty( $wp_query->posts ) ) {
$options = get_option( 'wpseo_xml' );
$excluded_posts = [];
if ( $options['excluded-posts'] !== '' ) {
$excluded_posts = explode( ',', $options['excluded-posts'] );
}
foreach ( $wp_query->posts as $post ) {
if ( ! in_array( $post->ID, $excluded_posts ) ) {
$excluded_posts[] = $post->ID;
}
}
// Updates the meta value.
$options['excluded-posts'] = implode( ',', $excluded_posts );
// Update the option.
update_option( 'wpseo_xml', $options );
}
// Remove the meta fields.
delete_post_meta_by_key( '_yoast_wpseo_sitemap-include' );
}
/**
* Performs upgrade functions to Yoast SEO 3.0.
*/
private function upgrade_30() {
// Remove the meta fields for sitemap prio.
delete_post_meta_by_key( '_yoast_wpseo_sitemap-prio' );
}
/**
* Performs upgrade functions to Yoast SEO 3.3.
*/
private function upgrade_33() {
// Notification dismissals have been moved to User Meta instead of global option.
delete_option( Yoast_Notification_Center::STORAGE_KEY );
}
/**
* Performs upgrade functions to Yoast SEO 3.6.
*/
private function upgrade_36() {
global $wpdb;
// Between 3.2 and 3.4 the sitemap options were saved with autoloading enabled.
$wpdb->query( 'DELETE FROM ' . $wpdb->options . ' WHERE option_name LIKE "wpseo_sitemap_%" AND autoload = "yes"' );
}
/**
* Removes the about notice when its still in the database.
*/
private function upgrade_40() {
$center = Yoast_Notification_Center::get();
$center->remove_notification_by_id( 'wpseo-dismiss-about' );
}
/**
* Moves the content-analysis-active and keyword-analysis-acive options from wpseo-titles to wpseo.
*/
private function upgrade_44() {
$wpseo_titles = $this->get_option_from_database( 'wpseo_titles' );
$this->save_option_setting( $wpseo_titles, 'content-analysis-active', 'content_analysis_active' );
$this->save_option_setting( $wpseo_titles, 'keyword-analysis-active', 'keyword_analysis_active' );
// Remove irrelevant content from the option.
$this->cleanup_option_data( 'wpseo_titles' );
}
/**
* Renames the meta name for the cornerstone content. It was a public meta field and it has to be private.
*/
private function upgrade_47() {
global $wpdb;
// The meta key has to be private, so prefix it.
$wpdb->query(
$wpdb->prepare(
'UPDATE ' . $wpdb->postmeta . ' SET meta_key = %s WHERE meta_key = "yst_is_cornerstone"',
WPSEO_Cornerstone_Filter::META_NAME
)
);
}
/**
* Removes the 'wpseo-dismiss-about' notice for every user that still has it.
*/
private function upgrade_49() {
global $wpdb;
/*
* Using a filter to remove the notification for the current logged in user. The notification center is
* initializing the notifications before the upgrade routine has been executedd and is saving the stored
* notifications on shutdown. This causes the returning notification. By adding this filter the shutdown
* routine on the notification center will remove the notification.
*/
add_filter( 'yoast_notifications_before_storage', [ $this, 'remove_about_notice' ] );
$meta_key = $wpdb->get_blog_prefix() . Yoast_Notification_Center::STORAGE_KEY;
$usermetas = $wpdb->get_results(
$wpdb->prepare(
'
SELECT user_id, meta_value
FROM ' . $wpdb->usermeta . '
WHERE meta_key = %s AND meta_value LIKE %s
',
$meta_key,
'%wpseo-dismiss-about%'
),
ARRAY_A
);
if ( empty( $usermetas ) ) {
return;
}
foreach ( $usermetas as $usermeta ) {
$notifications = maybe_unserialize( $usermeta['meta_value'] );
foreach ( $notifications as $notification_key => $notification ) {
if ( ! empty( $notification['options']['id'] ) && $notification['options']['id'] === 'wpseo-dismiss-about' ) {
unset( $notifications[ $notification_key ] );
}
}
update_user_option( $usermeta['user_id'], Yoast_Notification_Center::STORAGE_KEY, array_values( $notifications ) );
}
}
/**
* Removes the wpseo-dismiss-about notice from a list of notifications.
*
* @param Yoast_Notification[] $notifications The notifications to filter.
*
* @return Yoast_Notification[] The filtered list of notifications. Excluding the wpseo-dismiss-about notification.
*/
public function remove_about_notice( $notifications ) {
foreach ( $notifications as $notification_key => $notification ) {
if ( $notification->get_id() === 'wpseo-dismiss-about' ) {
unset( $notifications[ $notification_key ] );
}
}
return $notifications;
}
/**
* Adds the yoast_seo_links table to the database.
*/
private function upgrade_50() {
global $wpdb;
$link_installer = new WPSEO_Link_Installer();
$link_installer->install();
// Trigger reindex notification.
$notifier = new WPSEO_Link_Notifier();
$notifier->manage_notification();
// Deletes the post meta value, which might created in the RC.
$wpdb->query( 'DELETE FROM ' . $wpdb->postmeta . ' WHERE meta_key = "_yst_content_links_processed"' );
}
/**
* Updates the internal_link_count column to support improved functionality.
*
* @param string $version The current version to compare with.
*/
private function upgrade_50_51( $version ) {
global $wpdb;
if ( version_compare( $version, '5.0', '>=' ) ) {
$count_storage = new WPSEO_Meta_Storage();
$wpdb->query( 'ALTER TABLE ' . $count_storage->get_table_name() . ' MODIFY internal_link_count int(10) UNSIGNED NULL DEFAULT NULL' );
}
}
/**
* Register new capabilities and roles.
*/
private function upgrade_55() {
// Register roles.
do_action( 'wpseo_register_roles' );
WPSEO_Role_Manager_Factory::get()->add();
// Register capabilities.
do_action( 'wpseo_register_capabilities' );
WPSEO_Capability_Manager_Factory::get()->add();
}
/**
* Updates legacy license page options to the latest version.
*/
private function upgrade_56() {
global $wpdb;
// Make sure License Server checks are on the latest server version by default.
update_option( 'wpseo_license_server_version', WPSEO_License_Page_Manager::VERSION_BACKWARDS_COMPATIBILITY );
// Make sure incoming link count entries are at least 0, not NULL.
$count_storage = new WPSEO_Meta_Storage();
$wpdb->query( 'UPDATE ' . $count_storage->get_table_name() . ' SET incoming_link_count = 0 WHERE incoming_link_count IS NULL' );
}
/**
* Updates the links for the link count when there is a difference between the site and home url.
* We've used the site url instead of the home url.
*
* @return void
*/
private function upgrade_61() {
// When the home url is the same as the site url, just do nothing.
if ( home_url() === site_url() ) {
return;
}
global $wpdb;
$link_storage = new WPSEO_Link_Storage();
$wpdb->query( 'DELETE FROM ' . $link_storage->get_table_name() );
$meta_storage = new WPSEO_Meta_Storage();
$wpdb->query( 'DELETE FROM ' . $meta_storage->get_table_name() );
}
/**
* Removes some no longer used options for noindexing subpages and for meta keywords and its associated templates.
*
* @return void
*/
private function upgrade_63() {
$this->cleanup_option_data( 'wpseo_titles' );
}
/**
* Perform the 7.0 upgrade, moves settings around, deletes several options.
*
* @return void
*/
private function upgrade_70() {
$wpseo_permalinks = $this->get_option_from_database( 'wpseo_permalinks' );
$wpseo_xml = $this->get_option_from_database( 'wpseo_xml' );
$wpseo_rss = $this->get_option_from_database( 'wpseo_rss' );
$wpseo = $this->get_option_from_database( 'wpseo' );
$wpseo_internallinks = $this->get_option_from_database( 'wpseo_internallinks' );
// Move some permalink settings, then delete the option.
$this->save_option_setting( $wpseo_permalinks, 'redirectattachment', 'disable-attachment' );
$this->save_option_setting( $wpseo_permalinks, 'stripcategorybase' );
// Move one XML sitemap setting, then delete the option.
$this->save_option_setting( $wpseo_xml, 'enablexmlsitemap', 'enable_xml_sitemap' );
// Move the RSS settings to the search appearance settings, then delete the RSS option.
$this->save_option_setting( $wpseo_rss, 'rssbefore' );
$this->save_option_setting( $wpseo_rss, 'rssafter' );
$this->save_option_setting( $wpseo, 'company_logo' );
$this->save_option_setting( $wpseo, 'company_name' );
$this->save_option_setting( $wpseo, 'company_or_person' );
$this->save_option_setting( $wpseo, 'person_name' );
// Remove the website name and altername name as we no longer need them.
$this->cleanup_option_data( 'wpseo' );
// All the breadcrumbs settings have moved to the search appearance settings.
foreach ( array_keys( $wpseo_internallinks ) as $key ) {
$this->save_option_setting( $wpseo_internallinks, $key );
}
// Convert hidden metabox options to display metabox options.
$title_options = get_option( 'wpseo_titles' );
foreach ( $title_options as $key => $value ) {
if ( strpos( $key, 'hideeditbox-tax-' ) === 0 ) {
$taxonomy = substr( $key, strlen( 'hideeditbox-tax-' ) );
WPSEO_Options::set( 'display-metabox-tax-' . $taxonomy, ! $value );
continue;
}
if ( strpos( $key, 'hideeditbox-' ) === 0 ) {
$post_type = substr( $key, strlen( 'hideeditbox-' ) );
WPSEO_Options::set( 'display-metabox-pt-' . $post_type, ! $value );
continue;
}
}
// Cleanup removed options.
delete_option( 'wpseo_xml' );
delete_option( 'wpseo_permalinks' );
delete_option( 'wpseo_rss' );
delete_option( 'wpseo_internallinks' );
// Remove possibly present plugin conflict notice for plugin that was removed from the list of conflicting plugins.
$yoast_plugin_conflict = WPSEO_Plugin_Conflict::get_instance();
$yoast_plugin_conflict->clear_error( 'header-footer/plugin.php' );
// Moves the user meta for excluding from the XML sitemap to a noindex.
global $wpdb;
$wpdb->query( "UPDATE $wpdb->usermeta SET meta_key = 'wpseo_noindex_author' WHERE meta_key = 'wpseo_excludeauthorsitemap'" );
}
/**
* Perform the 7.1 upgrade.
*
* @return void
*/
private function upgrade_71() {
$this->cleanup_option_data( 'wpseo_social' );
// Move the breadcrumbs setting and invert it.
$title_options = $this->get_option_from_database( 'wpseo_titles' );
if ( array_key_exists( 'breadcrumbs-blog-remove', $title_options ) ) {
WPSEO_Options::set( 'breadcrumbs-display-blog-page', ! $title_options['breadcrumbs-blog-remove'] );
$this->cleanup_option_data( 'wpseo_titles' );
}
}
/**
* Perform the 7.3 upgrade.
*
* @return void
*/
private function upgrade_73() {
global $wpdb;
// We've moved the cornerstone checkbox to our proper namespace.
$wpdb->query( "UPDATE $wpdb->postmeta SET meta_key = '_yoast_wpseo_is_cornerstone' WHERE meta_key = '_yst_is_cornerstone'" );
// Remove the previous Whip dismissed message, as this is a new one regarding PHP 5.2.
delete_option( 'whip_dismiss_timestamp' );
}
/**
* Performs the 7.4 upgrade.
*
* @return void
*/
private function upgrade_74() {
$this->remove_sitemap_validators();
}
/**
* Performs the 7.5.3 upgrade.
*
* When upgrading purging media is potentially relevant.
*
* @return void
*/
private function upgrade_753() {
// Only when attachments are not disabled.
if ( WPSEO_Options::get( 'disable-attachment' ) === true ) {
return;
}
// Only when attachments are not no-indexed.
if ( WPSEO_Options::get( 'noindex-attachment' ) === true ) {
return;
}
// Set purging relevancy.
WPSEO_Options::set( 'is-media-purge-relevant', true );
}
/**
* Performs the 7.7 upgrade.
*
* @return void
*/
private function upgrade_77() {
// Remove all OpenGraph content image cache.
$this->delete_post_meta( '_yoast_wpseo_post_image_cache' );
}
/**
* Performs the 7.7.2 upgrade.
*
* @return void
*/
private function upgrade_772() {
if ( WPSEO_Utils::is_woocommerce_active() ) {
$this->migrate_woocommerce_archive_setting_to_shop_page();
}
}
/**
* Performs the 9.0 upgrade.
*
* @return void
*/
private function upgrade_90() {
global $wpdb;
// Invalidate all sitemap cache transients.
WPSEO_Sitemaps_Cache_Validator::cleanup_database();
// Removes all scheduled tasks for hitting the sitemap index.
wp_clear_scheduled_hook( 'wpseo_hit_sitemap_index' );
$wpdb->query( 'DELETE FROM ' . $wpdb->options . ' WHERE option_name LIKE "wpseo_sitemap_%"' );
}
/**
* Performs the 10.0 upgrade.
*
* @return void
*/
private function upgrade_100() {
// Removes recalibration notifications.
$this->clean_all_notifications();
// Removes recalibration options.
WPSEO_Options::clean_up( 'wpseo' );
delete_option( 'wpseo_recalibration_beta_mailinglist_subscription' );
}
/**
* Performs the 11.1 upgrade.
*
* @return void
*/
private function upgrade_111() {
// Set company_or_person to company when it's an invalid value.
$company_or_person = WPSEO_Options::get( 'company_or_person', '' );
if ( ! in_array( $company_or_person, [ 'company', 'person' ], true ) ) {
WPSEO_Options::set( 'company_or_person', 'company' );
}
}
/**
* Performs the 12.3 upgrade.
*
* Removes the about notice when its still in the database.
*/
private function upgrade_123() {
$plugins = [
'yoast-seo-premium',
'video-seo-for-wordpress-seo-by-yoast',
'yoast-news-seo',
'local-seo-for-yoast-seo',
'yoast-woocommerce-seo',
'yoast-acf-analysis',
];
$center = Yoast_Notification_Center::get();
foreach ( $plugins as $plugin ) {
$center->remove_notification_by_id( 'wpseo-outdated-yoast-seo-plugin-' . $plugin );
}
}
/**
* Performs the 12.4 upgrade.
*
* Removes the Google plus defaults from the database.
*/
private function upgrade_124() {
$this->cleanup_option_data( 'wpseo_social' );
}
/**
* Performs the 12.5 upgrade.
*/
public function upgrade_125() {
// Disables the force rewrite title when the theme supports it through WordPress.
if ( WPSEO_Options::get( 'forcerewritetitle', false ) && current_theme_supports( 'title-tag' ) ) {
WPSEO_Options::set( 'forcerewritetitle', false );
}
global $wpdb;
$wpdb->query( "DELETE FROM $wpdb->usermeta WHERE meta_key = 'wp_yoast_promo_hide_premium_upsell_admin_block'" );
// Removes the WordPress update notification, because it is no longer necessary when WordPress 5.3 is released.
$center = Yoast_Notification_Center::get();
$center->remove_notification_by_id( 'wpseo-dismiss-wordpress-upgrade' );
}
/**
* Performs the 12.8 upgrade.
*/
private function upgrade_128() {
// Re-save wpseo to make sure bf_banner_2019_dismissed key is gone.
$this->cleanup_option_data( 'wpseo' );
Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-page_comments-notice' );
Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-wordpress-upgrade' );
}
/**
* Removes all notifications saved in the database under 'wp_yoast_notifications'.
*
* @return void
*/
private function clean_all_notifications() {
global $wpdb;
delete_metadata( 'user', 0, $wpdb->get_blog_prefix() . Yoast_Notification_Center::STORAGE_KEY, '', true );
}
/**
* Removes the post meta fields for a given meta key.
*
* @param string $meta_key The meta key.
*
* @return void
*/
private function delete_post_meta( $meta_key ) {
global $wpdb;
$deleted = $wpdb->delete( $wpdb->postmeta, [ 'meta_key' => $meta_key ], [ '%s' ] );
if ( $deleted ) {
wp_cache_set( 'last_changed', microtime(), 'posts' );
}
}
/**
* Removes all sitemap validators.
*
* This should be executed on every upgrade routine until we have removed the sitemap caching in the database.
*
* @return void
*/
private function remove_sitemap_validators() {
global $wpdb;
// Remove all sitemap validators.
$wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE 'wpseo_sitemap%validator%'" );
}
/**
* Retrieves the option value directly from the database.
*
* @param string $option_name Option to retrieve.
*
* @return array|mixed The content of the option if exists, otherwise an empty array.
*/
protected function get_option_from_database( $option_name ) {
global $wpdb;
// Load option directly from the database, to avoid filtering and sanitization.
$sql = $wpdb->prepare( 'SELECT option_value FROM ' . $wpdb->options . ' WHERE option_name = %s', $option_name );
$results = $wpdb->get_results( $sql, ARRAY_A );
if ( ! empty( $results ) ) {
return maybe_unserialize( $results[0]['option_value'] );
}
return [];
}
/**
* Cleans the option to make sure only relevant settings are there.
*
* @param string $option_name Option name save.
*
* @return void
*/
protected function cleanup_option_data( $option_name ) {
$data = get_option( $option_name, [] );
if ( ! is_array( $data ) || $data === [] ) {
return;
}
/*
* Clean up the option by re-saving it.
*
* The option framework will remove any settings that are not configured
* for this option, removing any migrated settings.
*/
update_option( $option_name, $data );
}
/**
* Saves an option setting to where it should be stored.
*
* @param array $source_data The option containing the value to be migrated.
* @param string $source_setting Name of the key in the "from" option.
* @param string|null $target_setting Name of the key in the "to" option.
*
* @return void
*/
protected function save_option_setting( $source_data, $source_setting, $target_setting = null ) {
if ( $target_setting === null ) {
$target_setting = $source_setting;
}
if ( isset( $source_data[ $source_setting ] ) ) {
WPSEO_Options::set( $target_setting, $source_data[ $source_setting ] );
}
}
/**
* Migrates WooCommerce archive settings to the WooCommerce Shop page meta-data settings.
*
* If no Shop page is defined, nothing will be migrated.
*
* @return void
*/
private function migrate_woocommerce_archive_setting_to_shop_page() {
$shop_page_id = wc_get_page_id( 'shop' );
if ( $shop_page_id === -1 ) {
return;
}
$title = WPSEO_Meta::get_value( 'title', $shop_page_id );
if ( empty( $title ) ) {
$option_title = WPSEO_Options::get( 'title-ptarchive-product' );
WPSEO_Meta::set_value(
'title',
$option_title,
$shop_page_id
);
WPSEO_Options::set( 'title-ptarchive-product', '' );
}
$meta_description = WPSEO_Meta::get_value( 'metadesc', $shop_page_id );
if ( empty( $meta_description ) ) {
$option_metadesc = WPSEO_Options::get( 'metadesc-ptarchive-product' );
WPSEO_Meta::set_value(
'metadesc',
$option_metadesc,
$shop_page_id
);
WPSEO_Options::set( 'metadesc-ptarchive-product', '' );
}
$bc_title = WPSEO_Meta::get_value( 'bctitle', $shop_page_id );
if ( empty( $bc_title ) ) {
$option_bctitle = WPSEO_Options::get( 'bctitle-ptarchive-product' );
WPSEO_Meta::set_value(
'bctitle',
$option_bctitle,
$shop_page_id
);
WPSEO_Options::set( 'bctitle-ptarchive-product', '' );
}
$noindex = WPSEO_Meta::get_value( 'meta-robots-noindex', $shop_page_id );
if ( $noindex === '0' ) {
$option_noindex = WPSEO_Options::get( 'noindex-ptarchive-product' );
WPSEO_Meta::set_value(
'meta-robots-noindex',
$option_noindex,
$shop_page_id
);
WPSEO_Options::set( 'noindex-ptarchive-product', false );
}
}
}

View File

@@ -0,0 +1,676 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO
*/
/**
* Class for the Yoast SEO admin bar menu.
*/
class WPSEO_Admin_Bar_Menu implements WPSEO_WordPress_Integration {
/**
* The identifier used for the menu.
*
* @var string
*/
const MENU_IDENTIFIER = 'wpseo-menu';
/**
* The identifier used for the Keyword Research submenu.
*
* @var string
*/
const KEYWORD_RESEARCH_SUBMENU_IDENTIFIER = 'wpseo-kwresearch';
/**
* The identifier used for the Analysis submenu.
*
* @var string
*/
const ANALYSIS_SUBMENU_IDENTIFIER = 'wpseo-analysis';
/**
* The identifier used for the Settings submenu.
*
* @var string
*/
const SETTINGS_SUBMENU_IDENTIFIER = 'wpseo-settings';
/**
* The identifier used for the Network Settings submenu.
*
* @var string
*/
const NETWORK_SETTINGS_SUBMENU_IDENTIFIER = 'wpseo-network-settings';
/**
* Asset manager instance.
*
* @var WPSEO_Admin_Asset_Manager
*/
protected $asset_manager;
/**
* Constructor.
*
* Sets the asset manager to use.
*
* @param WPSEO_Admin_Asset_Manager $asset_manager Optional. Asset manager to use.
*/
public function __construct( WPSEO_Admin_Asset_Manager $asset_manager = null ) {
if ( ! $asset_manager ) {
$asset_manager = new WPSEO_Admin_Asset_Manager();
}
$this->asset_manager = $asset_manager;
}
/**
* Adds the admin bar menu.
*
* @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to.
*
* @return void
*/
public function add_menu( WP_Admin_Bar $wp_admin_bar ) {
// If the current user can't write posts, this is all of no use, so let's not output an admin menu.
if ( ! current_user_can( 'edit_posts' ) ) {
return;
}
$this->add_root_menu( $wp_admin_bar );
$this->add_keyword_research_submenu( $wp_admin_bar );
if ( ! is_admin() ) {
$this->add_analysis_submenu( $wp_admin_bar );
}
if ( ! is_admin() || is_blog_admin() ) {
$this->add_settings_submenu( $wp_admin_bar );
}
elseif ( is_network_admin() ) {
$this->add_network_settings_submenu( $wp_admin_bar );
}
}
/**
* Enqueues admin bar assets.
*
* @return void
*/
public function enqueue_assets() {
if ( ! is_admin_bar_showing() ) {
return;
}
// If the current user can't write posts, this is all of no use, so let's not output an admin menu.
if ( ! current_user_can( 'edit_posts' ) ) {
return;
}
$this->asset_manager->register_assets();
$this->asset_manager->enqueue_style( 'adminbar' );
}
/**
* Registers the hooks.
*
* @return void
*/
public function register_hooks() {
if ( ! $this->meets_requirements() ) {
return;
}
add_action( 'admin_bar_menu', [ $this, 'add_menu' ], 95 );
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
}
/**
* Checks whether the requirements to use this class are met.
*
* @return bool True if requirements are met, false otherwise.
*/
public function meets_requirements() {
if ( is_network_admin() ) {
return WPSEO_Utils::is_plugin_network_active();
}
if ( WPSEO_Options::get( 'enable_admin_bar_menu' ) !== true ) {
return false;
}
return ! is_admin() || is_blog_admin();
}
/**
* Adds the admin bar root menu.
*
* @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to.
*
* @return void
*/
protected function add_root_menu( WP_Admin_Bar $wp_admin_bar ) {
$title = $this->get_title();
$score = '';
$settings_url = '';
$counter = '';
$alert_popup = '';
$post = $this->get_singular_post();
if ( $post ) {
$score = $this->get_post_score( $post );
}
$term = $this->get_singular_term();
if ( $term ) {
$score = $this->get_term_score( $term );
}
$can_manage_options = $this->can_manage_options();
if ( $can_manage_options ) {
$settings_url = $this->get_settings_page_url();
}
if ( empty( $score ) && ! is_network_admin() && $can_manage_options ) {
$counter = $this->get_notification_counter();
$alert_popup = $this->get_notification_alert_popup();
}
$admin_bar_menu_args = [
'id' => self::MENU_IDENTIFIER,
'title' => $title . $score . $counter . $alert_popup,
'href' => $settings_url,
'meta' => [ 'tabindex' => ! empty( $settings_url ) ? false : '0' ],
];
$wp_admin_bar->add_menu( $admin_bar_menu_args );
if ( ! empty( $counter ) ) {
$admin_bar_menu_args = [
'parent' => self::MENU_IDENTIFIER,
'id' => 'wpseo-notifications',
'title' => __( 'Notifications', 'wordpress-seo' ) . $counter,
'href' => $settings_url,
'meta' => [ 'tabindex' => ! empty( $settings_url ) ? false : '0' ],
];
$wp_admin_bar->add_menu( $admin_bar_menu_args );
}
if ( ! is_network_admin() && $can_manage_options ) {
$admin_bar_menu_args = [
'parent' => self::MENU_IDENTIFIER,
'id' => 'wpseo-configuration-wizard',
'title' => __( 'Configuration Wizard', 'wordpress-seo' ),
'href' => admin_url( 'admin.php?page=' . WPSEO_Configuration_Page::PAGE_IDENTIFIER ),
];
$wp_admin_bar->add_menu( $admin_bar_menu_args );
}
}
/**
* Adds the admin bar keyword research submenu.
*
* @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to.
*
* @return void
*/
protected function add_keyword_research_submenu( WP_Admin_Bar $wp_admin_bar ) {
$adwords_url = 'https://yoa.st/keywordplanner';
$trends_url = 'https://yoa.st/google-trends';
$post = $this->get_singular_post();
if ( $post ) {
$focus_keyword = $this->get_post_focus_keyword( $post );
if ( ! empty( $focus_keyword ) ) {
$trends_url .= '#q=' . urlencode( $focus_keyword );
}
}
$menu_args = [
'parent' => self::MENU_IDENTIFIER,
'id' => self::KEYWORD_RESEARCH_SUBMENU_IDENTIFIER,
'title' => __( 'Keyword Research', 'wordpress-seo' ),
'meta' => [ 'tabindex' => '0' ],
];
$wp_admin_bar->add_menu( $menu_args );
$submenu_items = [
[
'id' => 'wpseo-kwresearchtraining',
'title' => __( 'Keyword research training', 'wordpress-seo' ),
'href' => WPSEO_Shortlinker::get( 'https://yoa.st/wp-admin-bar' ),
],
[
'id' => 'wpseo-adwordsexternal',
'title' => __( 'Google Ads', 'wordpress-seo' ),
'href' => $adwords_url,
],
[
'id' => 'wpseo-googleinsights',
'title' => __( 'Google Trends', 'wordpress-seo' ),
'href' => $trends_url,
],
];
foreach ( $submenu_items as $menu_item ) {
$menu_args = [
'parent' => self::KEYWORD_RESEARCH_SUBMENU_IDENTIFIER,
'id' => $menu_item['id'],
'title' => $menu_item['title'],
'href' => $menu_item['href'],
'meta' => [ 'target' => '_blank' ],
];
$wp_admin_bar->add_menu( $menu_args );
}
}
/**
* Adds the admin bar analysis submenu.
*
* @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to.
*
* @return void
*/
protected function add_analysis_submenu( WP_Admin_Bar $wp_admin_bar ) {
$url = WPSEO_Frontend::get_instance()->canonical( false );
$focus_keyword = '';
if ( ! $url ) {
return;
}
$post = $this->get_singular_post();
if ( $post ) {
$focus_keyword = $this->get_post_focus_keyword( $post );
}
$menu_args = [
'parent' => self::MENU_IDENTIFIER,
'id' => self::ANALYSIS_SUBMENU_IDENTIFIER,
'title' => __( 'Analyze this page', 'wordpress-seo' ),
'meta' => [ 'tabindex' => '0' ],
];
$wp_admin_bar->add_menu( $menu_args );
$encoded_url = urlencode( $url );
$submenu_items = [
[
'id' => 'wpseo-inlinks',
'title' => __( 'Check links to this URL', 'wordpress-seo' ),
'href' => 'https://search.google.com/search-console/links/drilldown?resource_id=' . urlencode( get_option( 'siteurl' ) ) . '&type=EXTERNAL&target=' . $encoded_url . '&domain=',
],
[
'id' => 'wpseo-kwdensity',
'title' => __( 'Check Keyphrase Density', 'wordpress-seo' ),
// HTTPS not available.
'href' => 'http://www.zippy.co.uk/keyworddensity/index.php?url=' . $encoded_url . '&keyword=' . urlencode( $focus_keyword ),
],
[
'id' => 'wpseo-cache',
'title' => __( 'Check Google Cache', 'wordpress-seo' ),
'href' => '//webcache.googleusercontent.com/search?strip=1&q=cache:' . $encoded_url,
],
[
'id' => 'wpseo-header',
'title' => __( 'Check Headers', 'wordpress-seo' ),
'href' => '//quixapp.com/headers/?r=' . urlencode( $url ),
],
[
'id' => 'wpseo-structureddata',
'title' => __( 'Google Structured Data Test', 'wordpress-seo' ),
'href' => 'https://search.google.com/structured-data/testing-tool#url=' . $encoded_url,
],
[
'id' => 'wpseo-facebookdebug',
'title' => __( 'Facebook Debugger', 'wordpress-seo' ),
'href' => '//developers.facebook.com/tools/debug/og/object?q=' . $encoded_url,
],
[
'id' => 'wpseo-pinterestvalidator',
'title' => __( 'Pinterest Rich Pins Validator', 'wordpress-seo' ),
'href' => 'https://developers.pinterest.com/tools/url-debugger/?link=' . $encoded_url,
],
[
'id' => 'wpseo-htmlvalidation',
'title' => __( 'HTML Validator', 'wordpress-seo' ),
'href' => '//validator.w3.org/check?uri=' . $encoded_url,
],
[
'id' => 'wpseo-cssvalidation',
'title' => __( 'CSS Validator', 'wordpress-seo' ),
'href' => '//jigsaw.w3.org/css-validator/validator?uri=' . $encoded_url,
],
[
'id' => 'wpseo-pagespeed',
'title' => __( 'Google Page Speed Test', 'wordpress-seo' ),
'href' => '//developers.google.com/speed/pagespeed/insights/?url=' . $encoded_url,
],
[
'id' => 'wpseo-google-mobile-friendly',
'title' => __( 'Mobile-Friendly Test', 'wordpress-seo' ),
'href' => 'https://www.google.com/webmasters/tools/mobile-friendly/?url=' . $encoded_url,
],
];
foreach ( $submenu_items as $menu_item ) {
$menu_args = [
'parent' => self::ANALYSIS_SUBMENU_IDENTIFIER,
'id' => $menu_item['id'],
'title' => $menu_item['title'],
'href' => $menu_item['href'],
'meta' => [ 'target' => '_blank' ],
];
$wp_admin_bar->add_menu( $menu_args );
}
}
/**
* Adds the admin bar settings submenu.
*
* @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to.
*
* @return void
*/
protected function add_settings_submenu( WP_Admin_Bar $wp_admin_bar ) {
if ( ! $this->can_manage_options() ) {
return;
}
$admin_menu = new WPSEO_Admin_Menu( new WPSEO_Menu() );
$submenu_pages = $admin_menu->get_submenu_pages();
$menu_args = [
'parent' => self::MENU_IDENTIFIER,
'id' => self::SETTINGS_SUBMENU_IDENTIFIER,
'title' => __( 'SEO Settings', 'wordpress-seo' ),
'meta' => [ 'tabindex' => '0' ],
];
$wp_admin_bar->add_menu( $menu_args );
foreach ( $submenu_pages as $submenu_page ) {
if ( ! current_user_can( $submenu_page[3] ) ) {
continue;
}
$id = 'wpseo-' . str_replace( '_', '-', str_replace( 'wpseo_', '', $submenu_page[4] ) );
if ( $id === 'wpseo-dashboard' ) {
$id = 'wpseo-general';
}
$menu_args = [
'parent' => self::SETTINGS_SUBMENU_IDENTIFIER,
'id' => $id,
'title' => $submenu_page[2],
'href' => admin_url( 'admin.php?page=' . urlencode( $submenu_page[4] ) ),
];
$wp_admin_bar->add_menu( $menu_args );
}
}
/**
* Adds the admin bar network settings submenu.
*
* @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to.
*
* @return void
*/
protected function add_network_settings_submenu( WP_Admin_Bar $wp_admin_bar ) {
if ( ! $this->can_manage_options() ) {
return;
}
$network_admin_menu = new WPSEO_Network_Admin_Menu( new WPSEO_Menu() );
$submenu_pages = $network_admin_menu->get_submenu_pages();
$menu_args = [
'parent' => self::MENU_IDENTIFIER,
'id' => self::NETWORK_SETTINGS_SUBMENU_IDENTIFIER,
'title' => __( 'SEO Settings', 'wordpress-seo' ),
'meta' => [ 'tabindex' => '0' ],
];
$wp_admin_bar->add_menu( $menu_args );
foreach ( $submenu_pages as $submenu_page ) {
if ( ! current_user_can( $submenu_page[3] ) ) {
continue;
}
$id = 'wpseo-' . str_replace( '_', '-', str_replace( 'wpseo_', '', $submenu_page[4] ) );
if ( $id === 'wpseo-dashboard' ) {
$id = 'wpseo-general';
}
$menu_args = [
'parent' => self::NETWORK_SETTINGS_SUBMENU_IDENTIFIER,
'id' => $id,
'title' => $submenu_page[2],
'href' => network_admin_url( 'admin.php?page=' . urlencode( $submenu_page[4] ) ),
];
$wp_admin_bar->add_menu( $menu_args );
}
}
/**
* Gets the menu title markup.
*
* @return string Admin bar title markup.
*/
protected function get_title() {
return '<div id="yoast-ab-icon" class="ab-item yoast-logo svg"><span class="screen-reader-text">' . __( 'SEO', 'wordpress-seo' ) . '</span></div>';
}
/**
* Gets the current post if in a singular post context.
*
* @global string $pagenow Current page identifier.
* @global WP_Post|null $post Current post object, or null if none available.
*
* @return WP_Post|null Post object, or null if not in singular context.
*/
protected function get_singular_post() {
global $pagenow, $post;
if ( ! is_singular() && ( ! is_blog_admin() || ! WPSEO_Metabox::is_post_edit( $pagenow ) ) ) {
return null;
}
if ( ! isset( $post ) || ! is_object( $post ) || ! $post instanceof WP_Post ) {
return null;
}
return $post;
}
/**
* Gets the focus keyword for a given post.
*
* @param WP_Post $post Post object to get its focus keyword.
*
* @return string Focus keyword, or empty string if none available.
*/
protected function get_post_focus_keyword( $post ) {
if ( ! is_object( $post ) || ! property_exists( $post, 'ID' ) ) {
return '';
}
/**
* Filter: 'wpseo_use_page_analysis' Determines if the analysis should be enabled.
*
* @api bool Determines if the analysis should be enabled.
*/
if ( apply_filters( 'wpseo_use_page_analysis', true ) !== true ) {
return '';
}
return WPSEO_Meta::get_value( 'focuskw', $post->ID );
}
/**
* Gets the score for a given post.
*
* @param WP_Post $post Post object to get its score.
*
* @return string Score markup, or empty string if none available.
*/
protected function get_post_score( $post ) {
if ( ! is_object( $post ) || ! property_exists( $post, 'ID' ) ) {
return '';
}
if ( apply_filters( 'wpseo_use_page_analysis', true ) !== true ) {
return '';
}
$analysis_seo = new WPSEO_Metabox_Analysis_SEO();
$analysis_readability = new WPSEO_Metabox_Analysis_Readability();
if ( $analysis_seo->is_enabled() ) {
return $this->get_score( WPSEO_Meta::get_value( 'linkdex', $post->ID ) );
}
if ( $analysis_readability->is_enabled() ) {
return $this->get_score( WPSEO_Meta::get_value( 'content_score', $post->ID ) );
}
return '';
}
/**
* Gets the current term if in a singular term context.
*
* @global string $pagenow Current page identifier.
* @global WP_Query $wp_query Current query object.
* @global WP_Term|null $tag Current term object, or null if none available.
*
* @return WP_Term|null Term object, or null if not in singular context.
*/
protected function get_singular_term() {
global $pagenow, $wp_query, $tag;
if ( is_category() || is_tag() || is_tax() ) {
return $wp_query->get_queried_object();
}
if ( WPSEO_Taxonomy::is_term_edit( $pagenow ) && ! WPSEO_Taxonomy::is_term_overview( $pagenow ) && isset( $tag ) && is_object( $tag ) && ! is_wp_error( $tag ) ) {
return get_term( $tag->term_id );
}
return null;
}
/**
* Gets the score for a given term.
*
* @param WP_Term $term Term object to get its score.
*
* @return string Score markup, or empty string if none available.
*/
protected function get_term_score( $term ) {
if ( ! is_object( $term ) || ! property_exists( $term, 'term_id' ) || ! property_exists( $term, 'taxonomy' ) ) {
return '';
}
$analysis_seo = new WPSEO_Metabox_Analysis_SEO();
$analysis_readability = new WPSEO_Metabox_Analysis_Readability();
if ( $analysis_seo->is_enabled() ) {
return $this->get_score( WPSEO_Taxonomy_Meta::get_term_meta( $term->term_id, $term->taxonomy, 'linkdex' ) );
}
if ( $analysis_readability->is_enabled() ) {
return $this->get_score( WPSEO_Taxonomy_Meta::get_term_meta( $term->term_id, $term->taxonomy, 'content_score' ) );
}
return '';
}
/**
* Takes the SEO score and makes the score icon for the admin bar for it.
*
* @param int $score The 0-100 rating of the score. Can be either SEO score or content score.
*
* @return string Score markup.
*/
protected function get_score( $score ) {
$score_class = WPSEO_Utils::translate_score( $score );
$translated_score = WPSEO_Utils::translate_score( $score, false );
/* translators: %s expands to the SEO score. */
$screen_reader_text = sprintf( __( 'SEO score: %s', 'wordpress-seo' ), $translated_score );
$score_adminbar_element = '<div class="wpseo-score-icon adminbar-seo-score ' . $score_class . '"><span class="adminbar-seo-score-text screen-reader-text">' . $screen_reader_text . '</span></div>';
return $score_adminbar_element;
}
/**
* Gets the URL to the main admin settings page.
*
* @return string Admin settings page URL.
*/
protected function get_settings_page_url() {
return self_admin_url( 'admin.php?page=' . WPSEO_Admin::PAGE_IDENTIFIER );
}
/**
* Gets the notification counter if in a valid context.
*
* @return string Notification counter markup, or empty string if not available.
*/
protected function get_notification_counter() {
$notification_center = Yoast_Notification_Center::get();
$notification_count = $notification_center->get_notification_count();
if ( ! $notification_count ) {
return '';
}
/* translators: %s: number of notifications */
$counter_screen_reader_text = sprintf( _n( '%s notification', '%s notifications', $notification_count, 'wordpress-seo' ), number_format_i18n( $notification_count ) );
return sprintf( ' <div class="wp-core-ui wp-ui-notification yoast-issue-counter"><span aria-hidden="true">%d</span><span class="screen-reader-text">%s</span></div>', $notification_count, $counter_screen_reader_text );
}
/**
* Gets the notification alert popup if in a valid context.
*
* @return string Notification alert popup markup, or empty string if not available.
*/
protected function get_notification_alert_popup() {
$notification_center = Yoast_Notification_Center::get();
$new_notifications = $notification_center->get_new_notifications();
$new_notifications_count = count( $new_notifications );
if ( ! $new_notifications_count ) {
return '';
}
$notification = sprintf(
_n(
'There is a new notification.',
'There are new notifications.',
$new_notifications_count,
'wordpress-seo'
),
$new_notifications_count
);
return '<div class="yoast-issue-added">' . $notification . '</div>';
}
/**
* Checks whether the current user can manage options in the current context.
*
* @return bool True if capabilities are sufficient, false otherwise.
*/
protected function can_manage_options() {
return is_network_admin() && current_user_can( 'wpseo_manage_network_options' ) || ! is_network_admin() && WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' );
}
}

View File

@@ -0,0 +1,139 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO
*/
/**
* WPSEO_Content_Images.
*/
class WPSEO_Content_Images {
/**
* Retrieves images from the post content.
*
* @param int $post_id The post ID.
* @param \WP_Post $post The post object.
*
* @return array An array of images found in this post.
*/
public function get_images( $post_id, $post = null ) {
return $this->get_images_from_content( $this->get_post_content( $post_id, $post ) );
}
/**
* Grabs the images from the content.
*
* @param string $content The post content string.
*
* @return array An array of image URLs.
*/
public function get_images_from_content( $content ) {
if ( ! is_string( $content ) ) {
return [];
}
$content_images = $this->get_img_tags_from_content( $content );
$images = array_map( [ $this, 'get_img_tag_source' ], $content_images );
$images = array_filter( $images );
$images = array_unique( $images );
$images = array_values( $images ); // Reset the array keys.
return $images;
}
/**
* Gets the image tags from a given content string.
*
* @param string $content The content to search for image tags.
*
* @return array An array of `<img>` tags.
*/
private function get_img_tags_from_content( $content ) {
if ( strpos( $content, '<img' ) === false ) {
return [];
}
preg_match_all( '`<img [^>]+>`', $content, $matches );
if ( isset( $matches[0] ) ) {
return $matches[0];
}
return [];
}
/**
* Retrieves the image URL from an image tag.
*
* @param string $image Image HTML element.
*
* @return string|bool The image URL on success, false on failure.
*/
private function get_img_tag_source( $image ) {
preg_match( '`src=(["\'])(.*?)\1`', $image, $matches );
if ( isset( $matches[2] ) ) {
return $matches[2];
}
return false;
}
/**
* Retrieves the post content we want to work with.
*
* @param int $post_id The post ID.
* @param WP_Post|array|null $post The post.
*
* @return string The content of the supplied post.
*/
private function get_post_content( $post_id, $post ) {
if ( $post === null ) {
$post = get_post( $post_id );
}
if ( $post === null ) {
return '';
}
/**
* Filter: 'wpseo_pre_analysis_post_content' - Allow filtering the content before analysis.
*
* @api string $post_content The Post content string.
*/
$content = apply_filters( 'wpseo_pre_analysis_post_content', $post->post_content, $post );
if ( ! is_string( $content ) ) {
$content = '';
}
return $content;
}
/* ********************* DEPRECATED METHODS ********************* */
/**
* Removes the cached images on post save.
*
* @deprecated 7.7
* @codeCoverageIgnore
*
* @param int $post_id The post id to remove the images from.
*
* @return void
*/
public function clear_cached_images( $post_id ) {
_deprecated_function( __METHOD__, '7.7.0' );
}
/**
* Registers the hooks.
*
* @deprecated 9.6
* @codeCoverageIgnore
*
* @return void
*/
public function register_hooks() {
_deprecated_function( __METHOD__, 'WPSEO 9.6' );
}
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO
*/
/**
* WPSEO_Custom_Fields.
*/
class WPSEO_Custom_Fields {
/**
* Custom fields cache.
*
* @var array
*/
protected static $custom_fields = null;
/**
* Retrieves the custom field names as an array.
*
* @link WordPress core: wp-admin/includes/template.php. Reused query from it.
*
* @return array The custom fields.
*/
public static function get_custom_fields() {
global $wpdb;
// Use cached value if available.
if ( ! is_null( self::$custom_fields ) ) {
return self::$custom_fields;
}
self::$custom_fields = [];
/**
* Filters the number of custom fields to retrieve for the drop-down
* in the Custom Fields meta box.
*
* @param int $limit Number of custom fields to retrieve. Default 30.
*/
$limit = apply_filters( 'postmeta_form_limit', 30 );
$sql = "SELECT DISTINCT meta_key
FROM $wpdb->postmeta
WHERE meta_key NOT BETWEEN '_' AND '_z'
HAVING meta_key NOT LIKE %s
ORDER BY meta_key
LIMIT %d";
$fields = $wpdb->get_col( $wpdb->prepare( $sql, $wpdb->esc_like( '_' ) . '%', $limit ) );
if ( is_array( $fields ) ) {
self::$custom_fields = array_map( [ 'WPSEO_Custom_Fields', 'add_custom_field_prefix' ], $fields );
}
return self::$custom_fields;
}
/**
* Adds the cf_ prefix to a field.
*
* @param string $field The field to prefix.
*
* @return string The prefixed field.
*/
private static function add_custom_field_prefix( $field ) {
return 'cf_' . $field;
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO
*/
/**
* WPSEO_Custom_Taxonomies.
*/
class WPSEO_Custom_Taxonomies {
/**
* Custom taxonomies cache.
*
* @var array
*/
protected static $custom_taxonomies = null;
/**
* Gets the names of the custom taxonomies, prepends 'ct_' and 'ct_desc', and returns them in an array.
*
* @return array The custom taxonomy prefixed names.
*/
public static function get_custom_taxonomies() {
// Use cached value if available.
if ( ! is_null( self::$custom_taxonomies ) ) {
return self::$custom_taxonomies;
}
self::$custom_taxonomies = [];
$args = [
'public' => true,
'_builtin' => false,
];
$custom_taxonomies = get_taxonomies( $args, 'names', 'and' );
if ( is_array( $custom_taxonomies ) ) {
foreach ( $custom_taxonomies as $custom_taxonomy ) {
array_push(
self::$custom_taxonomies,
self::add_custom_taxonomies_prefix( $custom_taxonomy ),
self::add_custom_taxonomies_description_prefix( $custom_taxonomy )
);
}
}
return self::$custom_taxonomies;
}
/**
* Adds the ct_ prefix to a taxonomy.
*
* @param string $taxonomy The taxonomy to prefix.
*
* @return string The prefixed taxonomy.
*/
private static function add_custom_taxonomies_prefix( $taxonomy ) {
return 'ct_' . $taxonomy;
}
/**
* Adds the ct_desc_ prefix to a taxonomy.
*
* @param string $taxonomy The taxonomy to prefix.
*
* @return string The prefixed taxonomy.
*/
private static function add_custom_taxonomies_description_prefix( $taxonomy ) {
return 'ct_desc_' . $taxonomy;
}
}

View File

@@ -0,0 +1,187 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
*/
/**
* Class WPSEO_Endpoint_Factory.
*/
class WPSEO_Endpoint_Factory {
/**
* The valid HTTP methods.
*
* @var array
*/
private $valid_http_methods = [
'GET',
'PATCH',
'POST',
'PUT',
'DELETE',
];
/**
* The arguments.
*
* @var array
*/
protected $args = [];
/**
* The namespace.
*
* @var string
*/
private $namespace;
/**
* The endpoint URL.
*
* @var string
*/
private $endpoint;
/**
* The callback to execute if the endpoint is called.
*
* @var callable
*/
private $callback;
/**
* The permission callback to execute to determine permissions.
*
* @var callable
*/
private $permission_callback;
/**
* The HTTP method to use.
*
* @var string
*/
private $method;
/**
* WPSEO_Endpoint_Factory constructor.
*
* @param string $namespace The endpoint's namespace.
* @param string $endpoint The endpoint's URL.
* @param callable $callback The callback function to execute.
* @param callable $permission_callback The permission callback to execute to determine permissions.
* @param string $method The HTTP method to use. Defaults to GET.
*
* @throws WPSEO_Invalid_Argument_Exception The invalid argument exception.
*/
public function __construct( $namespace, $endpoint, $callback, $permission_callback, $method = WP_REST_Server::READABLE ) {
if ( ! WPSEO_Validator::is_string( $namespace ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_string_parameter( $namespace, 'namespace' );
}
$this->namespace = $namespace;
if ( ! WPSEO_Validator::is_string( $endpoint ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_string_parameter( $endpoint, 'endpoint' );
}
$this->endpoint = $endpoint;
if ( ! is_callable( $callback ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_callable_parameter( $callback, 'callback' );
}
$this->callback = $callback;
if ( ! is_callable( $permission_callback ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_callable_parameter( $permission_callback, 'callback' );
}
$this->permission_callback = $permission_callback;
$this->method = $this->validate_method( $method );
}
/**
* Gets the associated arguments.
*
* @return array The arguments.
*/
public function get_arguments() {
return $this->args;
}
/**
* Determines whether or not there are any arguments present.
*
* @return bool Whether or not any arguments are present.
*/
public function has_arguments() {
return count( $this->args ) > 0;
}
/**
* Registers the endpoint with WordPress.
*
* @return void
*/
public function register() {
$config = [
'methods' => $this->method,
'callback' => $this->callback,
'permission_callback' => $this->permission_callback,
];
if ( $this->has_arguments() ) {
$config['args'] = $this->args;
}
register_rest_route( $this->namespace, $this->endpoint, $config );
}
/**
* Validates the method parameter.
*
* @param string $method The set method parameter.
*
* @return string The validated method.
*
* @throws WPSEO_Invalid_Argument_Exception The invalid argument exception.
* @throws InvalidArgumentException The invalid argument exception.
*/
protected function validate_method( $method ) {
if ( ! WPSEO_Validator::is_string( $method ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_string_parameter( $method, 'method' );
}
if ( ! in_array( $method, $this->valid_http_methods, true ) ) {
throw new InvalidArgumentException( sprintf( '%s is not a valid HTTP method', $method ) );
}
return $method;
}
/**
* Adds an argument to the endpoint.
*
* @param string $name The name of the argument.
* @param string $description The description associated with the argument.
* @param string $type The type of value that can be assigned to the argument.
* @param bool $required Whether or not it's a required argument. Defaults to true.
*
* @return void
*/
protected function add_argument( $name, $description, $type, $required = true ) {
if ( in_array( $name, array_keys( $this->args ), true ) ) {
return;
}
$this->args[ $name ] = [
'description' => $description,
'type' => $type,
'required' => $required,
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internal
*/
/**
* Class containing method for WPSEO Features.
*/
class WPSEO_Features {
/**
* Checks if the premium constant exists to make sure if plugin is the premium one.
*
* @return bool
*/
public function is_premium() {
return ( defined( 'WPSEO_PREMIUM_FILE' ) );
}
/**
* Checks if using the free version of the plugin.
*
* @return bool
*/
public function is_free() {
return ! $this->is_premium();
}
}

View File

@@ -0,0 +1,454 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO
*/
/**
* WPSEO_Image_Utils.
*/
class WPSEO_Image_Utils {
/**
* Find an attachment ID for a given URL.
*
* @param string $url The URL to find the attachment for.
*
* @return int The found attachment ID, or 0 if none was found.
*/
public static function get_attachment_by_url( $url ) {
/*
* As get_attachment_by_url won't work on resized versions of images,
* we strip out the size part of an image URL.
*/
$url = preg_replace( '/(.*)-\d+x\d+\.(jpg|png|gif)$/', '$1.$2', $url );
// Don't try to do this for external URLs.
if ( strpos( $url, get_site_url() ) !== 0 ) {
return 0;
}
if ( function_exists( 'wpcom_vip_attachment_url_to_postid' ) ) {
// @codeCoverageIgnoreStart -- We can't test this properly.
return (int) wpcom_vip_attachment_url_to_postid( $url );
// @codeCoverageIgnoreEnd -- The rest we _can_ test.
}
return self::attachment_url_to_postid( $url );
}
/**
* Implements the attachment_url_to_postid with use of WP Cache.
*
* @param string $url The attachment URL for which we want to know the Post ID.
*
* @return int The Post ID belonging to the attachment, 0 if not found.
*/
protected static function attachment_url_to_postid( $url ) {
$cache_key = sprintf( 'yoast_attachment_url_post_id_%s', md5( $url ) );
// Set the ID based on the hashed URL in the cache.
$id = wp_cache_get( $cache_key );
if ( $id === 'not_found' ) {
return 0;
}
// ID is found in cache, return.
if ( $id !== false ) {
return $id;
}
// phpcs:ignore WordPress.VIP.RestrictedFunctions -- We use the WP COM version if we can, see above.
$id = attachment_url_to_postid( $url );
if ( empty( $id ) ) {
wp_cache_set( $cache_key, 'not_found', '', ( 12 * HOUR_IN_SECONDS + wp_rand( 0, ( 4 * HOUR_IN_SECONDS ) ) ) );
return 0;
}
// We have the Post ID, but it's not in the cache yet. We do that here and return.
wp_cache_set( $cache_key, $id, '', ( 24 * HOUR_IN_SECONDS + wp_rand( 0, ( 12 * HOUR_IN_SECONDS ) ) ) );
return $id;
}
/**
* Retrieves the image data.
*
* @param array $image Image array with URL and metadata.
* @param int $attachment_id Attachment ID.
*
* @return false|array $image {
* Array of image data
*
* @type string $alt Image's alt text.
* @type string $alt Image's alt text.
* @type int $width Width of image.
* @type int $height Height of image.
* @type string $type Image's MIME type.
* @type string $url Image's URL.
* @type int $filesize The file size in bytes, if already set.
* }
*/
public static function get_data( $image, $attachment_id ) {
if ( ! is_array( $image ) ) {
return false;
}
// Deals with non-set keys and values being null or false.
if ( empty( $image['width'] ) || empty( $image['height'] ) ) {
return false;
}
$image['id'] = $attachment_id;
$image['alt'] = self::get_alt_tag( $attachment_id );
$image['pixels'] = ( (int) $image['width'] * (int) $image['height'] );
if ( ! isset( $image['type'] ) ) {
$image['type'] = get_post_mime_type( $attachment_id );
}
// Keep only the keys we need, and nothing else.
return array_intersect_key( $image, array_flip( [ 'id', 'alt', 'path', 'width', 'height', 'pixels', 'type', 'size', 'url', 'filesize' ] ) );
}
/**
* Checks a size version of an image to see if it's not too heavy.
*
* @param array $image Image to check the file size of.
*
* @return bool True when the image is within limits, false if not.
*/
public static function has_usable_file_size( $image ) {
if ( ! is_array( $image ) || $image === [] ) {
return false;
}
/**
* Filter: 'wpseo_image_image_weight_limit' - Determines what the maximum weight
* (in bytes) of an image is allowed to be, default is 2 MB.
*
* @api int - The maximum weight (in bytes) of an image.
*/
$max_size = apply_filters( 'wpseo_image_image_weight_limit', 2097152 );
// We cannot check without a path, so assume it's fine.
if ( ! isset( $image['path'] ) ) {
return true;
}
return ( self::get_file_size( $image ) <= $max_size );
}
/**
* Find the right version of an image based on size.
*
* @param int $attachment_id Attachment ID.
* @param string $size Size name.
*
* @return array|false Returns an array with image data on success, false on failure.
*/
public static function get_image( $attachment_id, $size ) {
$image = false;
if ( $size === 'full' ) {
$image = self::get_full_size_image_data( $attachment_id );
}
if ( ! $image ) {
$image = image_get_intermediate_size( $attachment_id, $size );
$image['size'] = $size;
}
if ( ! $image ) {
return false;
}
return self::get_data( $image, $attachment_id );
}
/**
* Returns the image data for the full size image.
*
* @param int $attachment_id Attachment ID.
*
* @return array|false Array when there is a full size image. False if not.
*/
protected static function get_full_size_image_data( $attachment_id ) {
$image = wp_get_attachment_metadata( $attachment_id );
if ( ! is_array( $image ) ) {
return false;
}
$image['url'] = wp_get_attachment_image_url( $attachment_id, 'full' );
$image['path'] = get_attached_file( $attachment_id );
$image['size'] = 'full';
return $image;
}
/**
* Finds the full file path for a given image file.
*
* @param string $path The relative file path.
*
* @return string The full file path.
*/
public static function get_absolute_path( $path ) {
static $uploads;
if ( $uploads === null ) {
$uploads = wp_get_upload_dir();
}
// Add the uploads basedir if the path does not start with it.
if ( empty( $uploads['error'] ) && strpos( $path, $uploads['basedir'] ) !== 0 ) {
return $uploads['basedir'] . DIRECTORY_SEPARATOR . ltrim( $path, DIRECTORY_SEPARATOR );
}
return $path;
}
/**
* Get the relative path of the image.
*
* @param string $img Image URL.
*
* @return string The expanded image URL.
*/
public static function get_relative_path( $img ) {
if ( $img[0] !== '/' ) {
return $img;
}
// If it's a relative URL, it's relative to the domain, not necessarily to the WordPress install, we
// want to preserve domain name and URL scheme (http / https) though.
$parsed_url = wp_parse_url( home_url() );
$img = $parsed_url['scheme'] . '://' . $parsed_url['host'] . $img;
return $img;
}
/**
* Get the image file size.
*
* @param array $image An image array object.
*
* @return int The file size in bytes.
*/
public static function get_file_size( $image ) {
if ( isset( $image['filesize'] ) ) {
return $image['filesize'];
}
// If the file size for the file is over our limit, we're going to go for a smaller version.
// @todo Save the filesize to the image metadata.
// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- If file size doesn't properly return, we'll not fail.
return @filesize( self::get_absolute_path( $image['path'] ) );
}
/**
* Returns the different image variations for consideration.
*
* @param int $attachment_id The attachment to return the variations for.
*
* @return array The different variations possible for this attachment ID.
*/
public static function get_variations( $attachment_id ) {
$variations = [];
foreach ( self::get_sizes() as $size ) {
$variation = self::get_image( $attachment_id, $size );
// The get_image function returns false if the size doesn't exist for this attachment.
if ( $variation ) {
$variations[] = $variation;
}
}
return $variations;
}
/**
* Check original size of image. If original image is too small, return false, else return true.
*
* Filters a list of variations by a certain set of usable dimensions.
*
* @param array $usable_dimensions {
* The parameters to check against.
*
* @type int $min_width Minimum width of image.
* @type int $max_width Maximum width of image.
* @type int $min_height Minimum height of image.
* @type int $max_height Maximum height of image.
* }
* @param array $variations The variations that should be considered.
*
* @return array Whether a variation is fit for display or not.
*/
public static function filter_usable_dimensions( $usable_dimensions, $variations ) {
$filtered = [];
foreach ( $variations as $variation ) {
$dimensions = $variation;
if ( self::has_usable_dimensions( $dimensions, $usable_dimensions ) ) {
$filtered[] = $variation;
}
}
return $filtered;
}
/**
* Filters a list of variations by (disk) file size.
*
* @param array $variations The variations to consider.
*
* @return array The validations that pass the required file size limits.
*/
public static function filter_usable_file_size( $variations ) {
foreach ( $variations as $variation ) {
// We return early to prevent measuring the file size of all the variations.
if ( self::has_usable_file_size( $variation ) ) {
return [ $variation ];
}
}
return [];
}
/**
* Retrieve the internal WP image file sizes.
*
* @return array $image_sizes An array of image sizes.
*/
public static function get_sizes() {
/**
* Filter: 'wpseo_image_sizes' - Determines which image sizes we'll loop through to get an appropriate image.
*
* @api array - The array of image sizes to loop through.
*/
return apply_filters( 'wpseo_image_sizes', [ 'full', 'large', 'medium_large' ] );
}
/**
* Grabs an image alt text.
*
* @param int $attachment_id The attachment ID.
*
* @return string The image alt text.
*/
public static function get_alt_tag( $attachment_id ) {
return (string) get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
}
/**
* Checks whether an img sizes up to the parameters.
*
* @param array $dimensions The image values.
* @param array $usable_dimensions The parameters to check against.
*
* @return bool True if the image has usable measurements, false if not.
*/
private static function has_usable_dimensions( $dimensions, $usable_dimensions ) {
foreach ( [ 'width', 'height' ] as $param ) {
$minimum = $usable_dimensions[ 'min_' . $param ];
$maximum = $usable_dimensions[ 'max_' . $param ];
$current = $dimensions[ $param ];
if ( ( $current < $minimum ) || ( $current > $maximum ) ) {
return false;
}
}
return true;
}
/**
* Gets the post's first usable content image. Null if none is available.
*
* @param int $post_id The post id.
*
* @return string|null The image URL.
*/
public static function get_first_usable_content_image_for_post( $post_id = null ) {
$post = get_post( $post_id );
// We know get_post() returns the post or null.
if ( ! $post ) {
return null;
}
$image_finder = new WPSEO_Content_Images();
$images = $image_finder->get_images( $post->ID, $post );
return self::get_first_image( $images );
}
/**
* Gets the term's first usable content image. Null if none is available.
*
* @param int $term_id The term id.
*
* @return string|null The image URL.
*/
public static function get_first_content_image_for_term( $term_id ) {
$term_description = term_description( $term_id );
// We know term_description() returns a string which may be empty.
if ( $term_description === '' ) {
return null;
}
$image_finder = new WPSEO_Content_Images();
$images = $image_finder->get_images_from_content( $term_description );
return self::get_first_image( $images );
}
/**
* Retrieves an attachment ID for an image uploaded in the settings.
*
* Due to self::get_attachment_by_url returning 0 instead of false.
* 0 is also a possibility when no ID is available.
*
* @param string $setting The setting the image is stored in.
*
* @return int|bool The attachment id, or false or 0 if no ID is available.
*/
public static function get_attachment_id_from_settings( $setting ) {
$image_id = WPSEO_Options::get( $setting . '_id', false );
if ( ! $image_id ) {
$image = WPSEO_Options::get( $setting, false );
if ( $image ) {
// There is not an option to put a URL in an image field in the settings anymore, only to upload it through the media manager.
// This means an attachment always exists, so doing this is only needed once.
$image_id = self::get_attachment_by_url( $image );
WPSEO_Options::set( $setting . '_id', $image_id );
}
}
return $image_id;
}
/**
* Retrieves the first possible image url from an array of images.
*
* @param array $images The array to extract image url from.
*
* @return string|null The extracted image url when found, null when not found.
*/
protected static function get_first_image( $images ) {
if ( ! is_array( $images ) ) {
return null;
}
$images = array_filter( $images );
if ( empty( $images ) ) {
return null;
}
return reset( $images );
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
* @since 3.6
*/
/**
* This class checks if the wpseo option doesn't exists. In the case it doesn't it will set a property that is
* accessible via a method to check if the installation is fresh.
*/
class WPSEO_Installation {
/**
* Checks if Yoast SEO is installed for the first time.
*/
public function __construct() {
$is_first_install = $this->is_first_install();
if ( $is_first_install && WPSEO_Utils::is_api_available() ) {
add_action( 'wpseo_activate', [ $this, 'set_first_install_options' ] );
}
}
/**
* When the option doesn't exist, it should be a new install.
*
* @return bool
*/
private function is_first_install() {
return ( get_option( 'wpseo' ) === false );
}
/**
* Sets the options on first install for showing the installation notice and disabling of the settings pages.
*/
public function set_first_install_options() {
$options = get_option( 'wpseo' );
$options['show_onboarding_notice'] = true;
$options['first_activated_on'] = time();
update_option( 'wpseo', $options );
}
}

View File

@@ -0,0 +1,997 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
* @since 1.5.0
*/
/**
* This class implements defaults and value validation for all WPSEO Post Meta values.
*
* Some guidelines:
* - To update a meta value, you can just use update_post_meta() with the full (prefixed) meta key
* or the convenience method WPSEO_Meta::set_value() with the internal key.
* All updates will be automatically validated.
* Meta values will only be saved to the database if they are *not* the same as the default to
* keep database load low.
* - To retrieve a WPSEO meta value, you **must** use WPSEO_Meta::get_value() which will always return a
* string value, either the saved value or the default.
* This method can also retrieve a complete set of WPSEO meta values for one specific post, see
* the method documentation for the parameters.
*
* {@internal Unfortunately there isn't a filter available to hook into before returning the results
* for get_post_meta(), get_post_custom() and the likes. That would have been the
* preferred solution.}}
*
* {@internal All WP native get_meta() results get cached internally, so no need to cache locally.}}
* {@internal Use $key when the key is the WPSEO internal name (without prefix), $meta_key when it
* includes the prefix.}}
*/
class WPSEO_Meta {
/**
* Prefix for all WPSEO meta values in the database.
*
* {@internal If at any point this would change, quite apart from an upgrade routine,
* this also will need to be changed in the wpml-config.xml file.}}
*
* @var string
*/
public static $meta_prefix = '_yoast_wpseo_';
/**
* Prefix for all WPSEO meta value form field names and ids.
*
* @var string
*/
public static $form_prefix = 'yoast_wpseo_';
/**
* Allowed length of the meta description.
*
* @var int
*/
public static $meta_length = 156;
/**
* Reason the meta description is not the default length.
*
* @var string
*/
public static $meta_length_reason = '';
/**
* Meta box field definitions for the meta box form.
*
* {@internal
* - Titles, help texts, description text and option labels are added via a translate_meta_boxes() method
* in the relevant child classes (WPSEO_Metabox and WPSEO_Social_admin) as they are only needed there.
* - Beware: even though the meta keys are divided into subsets, they still have to be uniquely named!}}
*
* @var array $meta_fields
* Array format:
* (required) 'type' => (string) field type. i.e. text / textarea / checkbox /
* radio / select / multiselect / upload etc.
* (required) 'title' => (string) table row title.
* (recommended) 'default_value' => (string|array) default value for the field.
* IMPORTANT:
* - if the field has options, the default has to be the
* key of one of the options.
* - if the field is a text field, the default **has** to be
* an empty string as otherwise the user can't save
* an empty value/delete the meta value.
* - if the field is a checkbox, the only valid values
* are 'on' or 'off'.
* (semi-required) 'options' => (array) options for used with (multi-)select and radio
* fields, required if that's the field type.
* key = (string) value which will be saved to db.
* value = (string) text label for the option.
* (optional) 'autocomplete' => (bool) whether autocomplete is on for text fields,
* defaults to true.
* (optional) 'class' => (string) classname(s) to add to the actual <input> tag.
* (optional) 'description' => (string) description to show underneath the field.
* (optional) 'expl' => (string) label for a checkbox.
* (optional) 'help' => (string) help text to show on mouse over ? image.
* (optional) 'rows' => (int) number of rows for a textarea, defaults to 3.
* (optional) 'placeholder' => (string) Currently only used by add-on plugins.
* (optional) 'serialized' => (bool) whether the value is expected to be serialized,
* i.e. an array or object, defaults to false.
* Currently only used by add-on plugins.
*/
public static $meta_fields = [
'general' => [
'focuskw' => [
'type' => 'hidden',
'title' => '',
],
'title' => [
'type' => 'hidden',
'title' => '', // Translation added later.
'default_value' => '',
'description' => '', // Translation added later.
'help' => '', // Translation added later.
],
'metadesc' => [
'type' => 'hidden',
'title' => '', // Translation added later.
'default_value' => '',
'class' => 'metadesc',
'rows' => 2,
'description' => '', // Translation added later.
'help' => '', // Translation added later.
],
'linkdex' => [
'type' => 'hidden',
'title' => 'linkdex',
'default_value' => '0',
'description' => '',
],
'content_score' => [
'type' => 'hidden',
'title' => 'content_score',
'default_value' => '0',
'description' => '',
],
'is_cornerstone' => [
'type' => 'hidden',
'title' => 'is_cornerstone',
'default_value' => 'false',
'description' => '',
],
],
'advanced' => [
'meta-robots-noindex' => [
'type' => 'select',
'title' => '', // Translation added later.
'default_value' => '0', // = post-type default.
'options' => [
'0' => '', // Post type default - translation added later.
'2' => '', // Index - translation added later.
'1' => '', // No-index - translation added later.
],
],
'meta-robots-nofollow' => [
'type' => 'radio',
'title' => '', // Translation added later.
'default_value' => '0', // = follow.
'options' => [
'0' => '', // Follow - translation added later.
'1' => '', // No-follow - translation added later.
],
],
'meta-robots-adv' => [
'type' => 'multiselect',
'title' => '', // Translation added later.
'default_value' => '',
'description' => '', // Translation added later.
'options' => [
'noimageindex' => '', // Translation added later.
'noarchive' => '', // Translation added later.
'nosnippet' => '', // Translation added later.
],
],
'bctitle' => [
'type' => 'text',
'title' => '', // Translation added later.
'default_value' => '',
'description' => '', // Translation added later.
],
'canonical' => [
'type' => 'text',
'title' => '', // Translation added later.
'default_value' => '',
'description' => '', // Translation added later.
],
'redirect' => [
'type' => 'text',
'title' => '', // Translation added later.
'default_value' => '',
'description' => '', // Translation added later.
],
],
'social' => [],
/* Fields we should validate & save, but not show on any form. */
'non_form' => [
'linkdex' => [
'type' => null,
'default_value' => '0',
],
],
];
/**
* Helper property - reverse index of the definition array.
*
* Format: [full meta key including prefix] => array
* ['subset'] => (string) primary index
* ['key'] => (string) internal key
*
* @var array
*/
public static $fields_index = [];
/**
* Helper property - array containing only the defaults in the format:
* [full meta key including prefix] => (string) default value
*
* @var array
*/
public static $defaults = [];
/**
* Helper property to define the social network meta field definitions - networks.
*
* @var array
*/
private static $social_networks = [
'opengraph' => 'opengraph',
'twitter' => 'twitter',
];
/**
* Helper property to define the social network meta field definitions - fields and their type.
*
* @var array
*/
private static $social_fields = [
'title' => 'text',
'description' => 'textarea',
'image' => 'upload',
'image-id' => 'hidden',
];
/**
* Register our actions and filters.
*
* @return void
*/
public static function init() {
foreach ( self::$social_networks as $option => $network ) {
if ( WPSEO_Options::get( $option, false ) === true ) {
foreach ( self::$social_fields as $box => $type ) {
self::$meta_fields['social'][ $network . '-' . $box ] = [
'type' => $type,
'title' => '', // Translation added later.
'default_value' => '',
'description' => '', // Translation added later.
];
}
}
}
unset( $option, $network, $box, $type );
/**
* Allow add-on plugins to register their meta fields for management by this class.
* Calls to add_filter() must be made before plugins_loaded prio 14.
*/
$extra_fields = apply_filters( 'add_extra_wpseo_meta_fields', [] );
if ( is_array( $extra_fields ) ) {
self::$meta_fields = self::array_merge_recursive_distinct( $extra_fields, self::$meta_fields );
}
unset( $extra_fields );
foreach ( self::$meta_fields as $subset => $field_group ) {
foreach ( $field_group as $key => $field_def ) {
register_meta(
'post',
self::$meta_prefix . $key,
[ 'sanitize_callback' => [ __CLASS__, 'sanitize_post_meta' ] ]
);
// Set the $fields_index property for efficiency.
self::$fields_index[ self::$meta_prefix . $key ] = [
'subset' => $subset,
'key' => $key,
];
// Set the $defaults property for efficiency.
if ( isset( $field_def['default_value'] ) ) {
self::$defaults[ self::$meta_prefix . $key ] = $field_def['default_value'];
}
else {
// Meta will always be a string, so let's make the meta meta default also a string.
self::$defaults[ self::$meta_prefix . $key ] = '';
}
}
}
unset( $subset, $field_group, $key, $field_def );
add_filter( 'update_post_metadata', [ __CLASS__, 'remove_meta_if_default' ], 10, 5 );
add_filter( 'add_post_metadata', [ __CLASS__, 'dont_save_meta_if_default' ], 10, 4 );
}
/**
* Retrieve the meta box form field definitions for the given tab and post type.
*
* @param string $tab Tab for which to retrieve the field definitions.
* @param string $post_type Post type of the current post.
*
* @return array Array containing the meta box field definitions.
*/
public static function get_meta_field_defs( $tab, $post_type = 'post' ) {
if ( ! isset( self::$meta_fields[ $tab ] ) ) {
return [];
}
$field_defs = self::$meta_fields[ $tab ];
switch ( $tab ) {
case 'non-form':
// Prevent non-form fields from being passed to forms.
$field_defs = [];
break;
case 'advanced':
global $post;
if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_edit_advanced_metadata' ) && WPSEO_Options::get( 'disableadvanced_meta' ) ) {
return [];
}
$post_type = '';
if ( isset( $post->post_type ) ) {
$post_type = $post->post_type;
}
elseif ( ! isset( $post->post_type ) && isset( $_GET['post_type'] ) ) {
$post_type = sanitize_text_field( $_GET['post_type'] );
}
if ( $post_type === '' ) {
return [];
}
/* Adjust the no-index text strings based on the post type. */
$post_type_object = get_post_type_object( $post_type );
$field_defs['meta-robots-noindex']['title'] = sprintf( $field_defs['meta-robots-noindex']['title'], $post_type_object->labels->singular_name );
$field_defs['meta-robots-noindex']['options']['0'] = sprintf( $field_defs['meta-robots-noindex']['options']['0'], ( ( WPSEO_Options::get( 'noindex-' . $post_type, false ) === true ) ? $field_defs['meta-robots-noindex']['options']['1'] : $field_defs['meta-robots-noindex']['options']['2'] ), $post_type_object->label );
$field_defs['meta-robots-nofollow']['title'] = sprintf( $field_defs['meta-robots-nofollow']['title'], $post_type_object->labels->singular_name );
/* Don't show the breadcrumb title field if breadcrumbs aren't enabled. */
if ( WPSEO_Options::get( 'breadcrumbs-enable', false ) !== true && ! current_theme_supports( 'yoast-seo-breadcrumbs' ) ) {
unset( $field_defs['bctitle'] );
}
global $post;
if ( empty( $post->ID ) || ( ! empty( $post->ID ) && self::get_value( 'redirect', $post->ID ) === '' ) ) {
unset( $field_defs['redirect'] );
}
break;
}
/**
* Filter the WPSEO metabox form field definitions for a tab.
* {tab} can be 'general', 'advanced' or 'social'.
*
* @param array $field_defs Metabox form field definitions.
* @param string $post_type Post type of the post the metabox is for, defaults to 'post'.
*
* @return array
*/
return apply_filters( 'wpseo_metabox_entries_' . $tab, $field_defs, $post_type );
}
/**
* Validate the post meta values.
*
* @param mixed $meta_value The new value.
* @param string $meta_key The full meta key (including prefix).
*
* @return string Validated meta value.
*/
public static function sanitize_post_meta( $meta_value, $meta_key ) {
$field_def = self::$meta_fields[ self::$fields_index[ $meta_key ]['subset'] ][ self::$fields_index[ $meta_key ]['key'] ];
$clean = self::$defaults[ $meta_key ];
switch ( true ) {
case ( $meta_key === self::$meta_prefix . 'linkdex' ):
$int = WPSEO_Utils::validate_int( $meta_value );
if ( $int !== false && $int >= 0 ) {
$clean = strval( $int ); // Convert to string to make sure default check works.
}
break;
case ( $field_def['type'] === 'checkbox' ):
// Only allow value if it's one of the predefined options.
if ( in_array( $meta_value, [ 'on', 'off' ], true ) ) {
$clean = $meta_value;
}
break;
case ( $field_def['type'] === 'select' || $field_def['type'] === 'radio' ):
// Only allow value if it's one of the predefined options.
if ( isset( $field_def['options'][ $meta_value ] ) ) {
$clean = $meta_value;
}
break;
case ( $field_def['type'] === 'multiselect' && $meta_key === self::$meta_prefix . 'meta-robots-adv' ):
$clean = self::validate_meta_robots_adv( $meta_value );
break;
case ( $field_def['type'] === 'text' && $meta_key === self::$meta_prefix . 'canonical' ):
case ( $field_def['type'] === 'text' && $meta_key === self::$meta_prefix . 'redirect' ):
// Validate as url(-part).
$url = WPSEO_Utils::sanitize_url( $meta_value );
if ( $url !== '' ) {
$clean = $url;
}
break;
case ( $field_def['type'] === 'upload' && in_array( $meta_key, [ self::$meta_prefix . 'opengraph-image', self::$meta_prefix . 'twitter-image' ], true ) ):
// Validate as url.
$url = WPSEO_Utils::sanitize_url( $meta_value, [ 'http', 'https', 'ftp', 'ftps' ] );
if ( $url !== '' ) {
$clean = $url;
}
break;
case ( $field_def['type'] === 'hidden' && $meta_key === self::$meta_prefix . 'is_cornerstone' ):
$clean = $meta_value;
/*
* This used to be a checkbox, then became a hidden input.
* To make sure the value remains consistent, we cast 'true' to '1'.
*/
if ( $meta_value === 'true' ) {
$clean = '1';
}
break;
case ( $field_def['type'] === 'textarea' ):
if ( is_string( $meta_value ) ) {
// Remove line breaks and tabs.
// @todo [JRF => Yoast] Verify that line breaks and the likes aren't allowed/recommended in meta header fields.
$meta_value = str_replace( [ "\n", "\r", "\t", ' ' ], ' ', $meta_value );
$clean = WPSEO_Utils::sanitize_text_field( trim( $meta_value ) );
}
break;
case ( $field_def['type'] === 'multiselect' ):
$clean = $meta_value;
break;
case ( $field_def['type'] === 'text' ):
default:
if ( is_string( $meta_value ) ) {
$clean = WPSEO_Utils::sanitize_text_field( trim( $meta_value ) );
}
break;
}
$clean = apply_filters( 'wpseo_sanitize_post_meta_' . $meta_key, $clean, $meta_value, $field_def, $meta_key );
return $clean;
}
/**
* Validate a meta-robots-adv meta value.
*
* @todo [JRF => Yoast] Verify that this logic for the prioritisation is correct.
*
* @param array|string $meta_value The value to validate.
*
* @return string Clean value.
*/
public static function validate_meta_robots_adv( $meta_value ) {
$clean = self::$meta_fields['advanced']['meta-robots-adv']['default_value'];
$options = self::$meta_fields['advanced']['meta-robots-adv']['options'];
if ( is_string( $meta_value ) ) {
$meta_value = explode( ',', $meta_value );
}
if ( is_array( $meta_value ) && $meta_value !== [] ) {
$meta_value = array_map( 'trim', $meta_value );
// Individual selected entries.
$cleaning = [];
foreach ( $meta_value as $value ) {
if ( isset( $options[ $value ] ) ) {
$cleaning[] = $value;
}
}
if ( $cleaning !== [] ) {
$clean = implode( ',', $cleaning );
}
unset( $cleaning, $value );
}
return $clean;
}
/**
* Prevent saving of default values and remove potential old value from the database if replaced by a default.
*
* @param bool $check The current status to allow updating metadata for the given type.
* @param int $object_id ID of the current object for which the meta is being updated.
* @param string $meta_key The full meta key (including prefix).
* @param string $meta_value New meta value.
* @param string $prev_value The old meta value.
*
* @return null|bool True = stop saving, null = continue saving.
*/
public static function remove_meta_if_default( $check, $object_id, $meta_key, $meta_value, $prev_value = '' ) {
/* If it's one of our meta fields, check against default. */
if ( isset( self::$fields_index[ $meta_key ] ) && self::meta_value_is_default( $meta_key, $meta_value ) === true ) {
if ( $prev_value !== '' ) {
delete_post_meta( $object_id, $meta_key, $prev_value );
}
else {
delete_post_meta( $object_id, $meta_key );
}
return true; // Stop saving the value.
}
return $check; // Go on with the normal execution (update) in meta.php.
}
/**
* Prevent adding of default values to the database.
*
* @param bool $check The current status to allow adding metadata for the given type.
* @param int $object_id ID of the current object for which the meta is being added.
* @param string $meta_key The full meta key (including prefix).
* @param string $meta_value New meta value.
*
* @return null|bool True = stop saving, null = continue saving.
*/
public static function dont_save_meta_if_default( $check, $object_id, $meta_key, $meta_value ) {
/* If it's one of our meta fields, check against default. */
if ( isset( self::$fields_index[ $meta_key ] ) && self::meta_value_is_default( $meta_key, $meta_value ) === true ) {
return true; // Stop saving the value.
}
return $check; // Go on with the normal execution (add) in meta.php.
}
/**
* Is the given meta value the same as the default value ?
*
* @param string $meta_key The full meta key (including prefix).
* @param mixed $meta_value The value to check.
*
* @return bool
*/
public static function meta_value_is_default( $meta_key, $meta_value ) {
return ( isset( self::$defaults[ $meta_key ] ) && $meta_value === self::$defaults[ $meta_key ] );
}
/**
* Get a custom post meta value.
*
* Returns the default value if the meta value has not been set.
*
* {@internal Unfortunately there isn't a filter available to hook into before returning
* the results for get_post_meta(), get_post_custom() and the likes. That
* would have been the preferred solution.}}
*
* @param string $key Internal key of the value to get (without prefix).
* @param int $postid Post ID of the post to get the value for.
*
* @return string All 'normal' values returned from get_post_meta() are strings.
* Objects and arrays are possible, but not used by this plugin
* and therefore discarted (except when the special 'serialized' field def
* value is set to true - only used by add-on plugins for now).
* Will return the default value if no value was found.
* Will return empty string if no default was found (not one of our keys) or
* if the post does not exist.
*/
public static function get_value( $key, $postid = 0 ) {
global $post;
$postid = absint( $postid );
if ( $postid === 0 ) {
if ( ( isset( $post ) && is_object( $post ) ) && ( isset( $post->post_status ) && $post->post_status !== 'auto-draft' ) ) {
$postid = $post->ID;
}
else {
return '';
}
}
$custom = get_post_custom( $postid ); // Array of strings or empty array.
if ( isset( $custom[ self::$meta_prefix . $key ][0] ) ) {
$unserialized = maybe_unserialize( $custom[ self::$meta_prefix . $key ][0] );
if ( $custom[ self::$meta_prefix . $key ][0] === $unserialized ) {
return $custom[ self::$meta_prefix . $key ][0];
}
if ( isset( self::$fields_index[ self::$meta_prefix . $key ] ) ) {
$field_def = self::$meta_fields[ self::$fields_index[ self::$meta_prefix . $key ]['subset'] ][ self::$fields_index[ self::$meta_prefix . $key ]['key'] ];
if ( isset( $field_def['serialized'] ) && $field_def['serialized'] === true ) {
// Ok, serialize value expected/allowed.
return $unserialized;
}
}
}
// Meta was either not found or found, but object/array while not allowed to be.
if ( isset( self::$defaults[ self::$meta_prefix . $key ] ) ) {
return self::$defaults[ self::$meta_prefix . $key ];
}
/*
* Shouldn't ever happen, means not one of our keys as there will always be a default available
* for all our keys.
*/
return '';
}
/**
* Update a meta value for a post.
*
* @param string $key The internal key of the meta value to change (without prefix).
* @param mixed $meta_value The value to set the meta to.
* @param int $post_id The ID of the post to change the meta for.
*
* @return bool Whether the value was changed.
*/
public static function set_value( $key, $meta_value, $post_id ) {
/*
* Slash the data, because `update_metadata` will unslash it and we have already unslashed it.
* Related issue: https://github.com/Yoast/YoastSEO.js/issues/2158
*/
$meta_value = wp_slash( $meta_value );
return update_post_meta( $post_id, self::$meta_prefix . $key, $meta_value );
}
/**
* Deletes a meta value for a post.
*
* @param string $key The internal key of the meta value to change (without prefix).
* @param int $post_id The ID of the post to change the meta for.
*
* @return bool Whether the value was changed.
*/
public static function delete( $key, $post_id ) {
return delete_post_meta( $post_id, self::$meta_prefix . $key );
}
/**
* Used for imports, this functions imports the value of $old_metakey into $new_metakey for those post
* where no WPSEO meta data has been set.
* Optionally deletes the $old_metakey values.
*
* @param string $old_metakey The old key of the meta value.
* @param string $new_metakey The new key, usually the WPSEO meta key (including prefix).
* @param bool $delete_old Whether to delete the old meta key/value-sets.
*
* @return void
*/
public static function replace_meta( $old_metakey, $new_metakey, $delete_old = false ) {
global $wpdb;
/*
* Get only those rows where no wpseo meta values exist for the same post
* (with the exception of linkdex as that will be set independently of whether the post has been edited).
*
* {@internal Query is pretty well optimized this way.}}
*/
$query = $wpdb->prepare(
"
SELECT `a`.*
FROM {$wpdb->postmeta} AS a
WHERE `a`.`meta_key` = %s
AND NOT EXISTS (
SELECT DISTINCT `post_id` , count( `meta_id` ) AS count
FROM {$wpdb->postmeta} AS b
WHERE `a`.`post_id` = `b`.`post_id`
AND `meta_key` LIKE %s
AND `meta_key` <> %s
GROUP BY `post_id`
)
;",
$old_metakey,
$wpdb->esc_like( self::$meta_prefix . '%' ),
self::$meta_prefix . 'linkdex'
);
$oldies = $wpdb->get_results( $query );
if ( is_array( $oldies ) && $oldies !== [] ) {
foreach ( $oldies as $old ) {
update_post_meta( $old->post_id, $new_metakey, $old->meta_value );
}
}
// Delete old keys.
if ( $delete_old === true ) {
delete_post_meta_by_key( $old_metakey );
}
}
/**
* General clean-up of the saved meta values.
* - Remove potentially lingering old meta keys;
* - Remove all default and invalid values.
*
* @return void
*/
public static function clean_up() {
global $wpdb;
/*
* Clean up '_yoast_wpseo_meta-robots'.
*
* Retrieve all '_yoast_wpseo_meta-robots' meta values and convert if no new values found.
*
* {@internal Query is pretty well optimized this way.}}
*
* @todo [JRF => Yoast] Find out all possible values which the old '_yoast_wpseo_meta-robots' could contain
* to convert the data correctly.
*/
$query = $wpdb->prepare(
"
SELECT `a`.*
FROM {$wpdb->postmeta} AS a
WHERE `a`.`meta_key` = %s
AND NOT EXISTS (
SELECT DISTINCT `post_id` , count( `meta_id` ) AS count
FROM {$wpdb->postmeta} AS b
WHERE `a`.`post_id` = `b`.`post_id`
AND ( `meta_key` = %s
OR `meta_key` = %s )
GROUP BY `post_id`
)
;",
self::$meta_prefix . 'meta-robots',
self::$meta_prefix . 'meta-robots-noindex',
self::$meta_prefix . 'meta-robots-nofollow'
);
$oldies = $wpdb->get_results( $query );
if ( is_array( $oldies ) && $oldies !== [] ) {
foreach ( $oldies as $old ) {
$old_values = explode( ',', $old->meta_value );
foreach ( $old_values as $value ) {
if ( $value === 'noindex' ) {
update_post_meta( $old->post_id, self::$meta_prefix . 'meta-robots-noindex', 1 );
}
elseif ( $value === 'nofollow' ) {
update_post_meta( $old->post_id, self::$meta_prefix . 'meta-robots-nofollow', 1 );
}
}
}
}
unset( $query, $oldies, $old, $old_values, $value );
// Delete old keys.
delete_post_meta_by_key( self::$meta_prefix . 'meta-robots' );
/*
* Remove all default values and (most) invalid option values.
* Invalid option values for the multiselect (meta-robots-adv) field will be dealt with seperately.
*
* {@internal Some of the defaults have changed in v1.5, but as the defaults will
* be removed and new defaults will now automatically be passed when no
* data found, this update is automatic (as long as we remove the old
* values which we do in the below routine).}}
*
* {@internal Unfortunately we can't use the normal delete_meta() with key/value combination
* as '' (empty string) values will be ignored and would result in all metas
* with that key being deleted, not just the empty fields.
* Still, the below implementation is largely based on the delete_meta() function.}}
*/
$query = [];
foreach ( self::$meta_fields as $subset => $field_group ) {
foreach ( $field_group as $key => $field_def ) {
if ( ! isset( $field_def['default_value'] ) ) {
continue;
}
if ( isset( $field_def['options'] ) && is_array( $field_def['options'] ) && $field_def['options'] !== [] ) {
$valid = $field_def['options'];
// Remove the default value from the valid options.
unset( $valid[ $field_def['default_value'] ] );
$valid = array_keys( $valid );
$query[] = $wpdb->prepare(
"( meta_key = %s AND meta_value NOT IN ( '" . implode( "','", esc_sql( $valid ) ) . "' ) )",
self::$meta_prefix . $key
);
unset( $valid );
}
elseif ( is_string( $field_def['default_value'] ) && $field_def['default_value'] !== '' ) {
$query[] = $wpdb->prepare(
'( meta_key = %s AND meta_value = %s )',
self::$meta_prefix . $key,
$field_def['default_value']
);
}
else {
$query[] = $wpdb->prepare(
"( meta_key = %s AND meta_value = '' )",
self::$meta_prefix . $key
);
}
}
}
unset( $subset, $field_group, $key, $field_def );
$query = "SELECT meta_id FROM {$wpdb->postmeta} WHERE " . implode( ' OR ', $query ) . ';';
$meta_ids = $wpdb->get_col( $query );
if ( is_array( $meta_ids ) && $meta_ids !== [] ) {
// WP native action.
do_action( 'delete_post_meta', $meta_ids, null, null, null );
$query = "DELETE FROM {$wpdb->postmeta} WHERE meta_id IN( " . implode( ',', $meta_ids ) . ' )';
$count = $wpdb->query( $query );
if ( $count ) {
foreach ( $meta_ids as $object_id ) {
wp_cache_delete( $object_id, 'post_meta' );
}
// WP native action.
do_action( 'deleted_post_meta', $meta_ids, null, null, null );
}
}
unset( $query, $meta_ids, $count, $object_id );
/*
* Deal with the multiselect (meta-robots-adv) field.
*
* Removes invalid option combinations, such as 'none,noarchive'.
*
* Default values have already been removed, so we should have a small result set and
* (hopefully) even smaller set of invalid results.
*/
$query = $wpdb->prepare(
"SELECT meta_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s",
self::$meta_prefix . 'meta-robots-adv'
);
$oldies = $wpdb->get_results( $query );
if ( is_array( $oldies ) && $oldies !== [] ) {
foreach ( $oldies as $old ) {
$clean = self::validate_meta_robots_adv( $old->meta_value );
if ( $clean !== $old->meta_value ) {
if ( $clean !== self::$meta_fields['advanced']['meta-robots-adv']['default_value'] ) {
update_metadata_by_mid( 'post', $old->meta_id, $clean );
}
else {
delete_metadata_by_mid( 'post', $old->meta_id );
}
}
}
}
unset( $query, $oldies, $old, $clean );
do_action( 'wpseo_meta_clean_up' );
}
/**
* Recursively merge a variable number of arrays, using the left array as base,
* giving priority to the right array.
*
* Difference with native array_merge_recursive():
* array_merge_recursive converts values with duplicate keys to arrays rather than
* overwriting the value in the first array with the duplicate value in the second array.
*
* array_merge_recursive_distinct does not change the data types of the values in the arrays.
* Matching keys' values in the second array overwrite those in the first array, as is the
* case with array_merge.
*
* Freely based on information found on http://www.php.net/manual/en/function.array-merge-recursive.php
*
* {@internal Should be moved to a general utility class.}}
*
* @return array
*/
public static function array_merge_recursive_distinct() {
$arrays = func_get_args();
if ( count( $arrays ) < 2 ) {
if ( $arrays === [] ) {
return [];
}
else {
return $arrays[0];
}
}
$merged = array_shift( $arrays );
foreach ( $arrays as $array ) {
foreach ( $array as $key => $value ) {
if ( is_array( $value ) && ( isset( $merged[ $key ] ) && is_array( $merged[ $key ] ) ) ) {
$merged[ $key ] = self::array_merge_recursive_distinct( $merged[ $key ], $value );
}
else {
$merged[ $key ] = $value;
}
}
unset( $key, $value );
}
return $merged;
}
/**
* Counts the total of all the keywords being used for posts except the given one.
*
* @param string $keyword The keyword to be counted.
* @param integer $post_id The is of the post to which the keyword belongs.
*
* @return array
*/
public static function keyword_usage( $keyword, $post_id ) {
if ( empty( $keyword ) ) {
return [];
}
$query = [
'meta_query' => [
'relation' => 'OR',
[
'key' => '_yoast_wpseo_focuskw',
'value' => $keyword,
],
],
'post__not_in' => [ $post_id ],
'fields' => 'ids',
'post_type' => 'any',
/*
* We only need to return zero, one or two results:
* - Zero: keyword hasn't been used before
* - One: Keyword has been used once before
* - Two or more: Keyword has been used twice before
*/
'posts_per_page' => 2,
];
// If Yoast SEO Premium is active, get the additional keywords as well.
if ( WPSEO_Utils::is_yoast_seo_premium() ) {
$query['meta_query'][] = [
'key' => '_yoast_wpseo_focuskeywords',
'value' => sprintf( '"keyword":"%s"', $keyword ),
'compare' => 'LIKE',
];
}
$get_posts = new WP_Query( $query );
return $get_posts->posts;
}
/* ********************* DEPRECATED METHODS ********************* */
/**
* Get a value from $_POST for a given key.
*
* Returns the $_POST value if exists, returns an empty string if key does not exist.
*
* @deprecated 9.6
* @codeCoverageIgnore
*
* @param string $key Key of the value to get from $_POST.
*
* @return string Returns $_POST value, which will be a string the majority of the time.
* Will return empty string if key does not exists in $_POST.
*/
public static function get_post_value( $key ) {
_deprecated_function( __METHOD__, 'WPSEO 9.6' );
// @codingStandardsIgnoreLine
return ( array_key_exists( $key, $_POST ) ) ? $_POST[ $key ] : '';
}
} /* End of class */

View File

@@ -0,0 +1,80 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO
*/
/**
* Represents a post's primary term.
*/
class WPSEO_Primary_Term {
/**
* Taxonomy name for the term.
*
* @var string
*/
protected $taxonomy_name;
/**
* Post ID for the term.
*
* @var int
*/
protected $post_ID;
/**
* The taxonomy this term is part of.
*
* @param string $taxonomy_name Taxonomy name for the term.
* @param int $post_id Post ID for the term.
*/
public function __construct( $taxonomy_name, $post_id ) {
$this->taxonomy_name = $taxonomy_name;
$this->post_ID = $post_id;
}
/**
* Returns the primary term ID.
*
* @return int|bool
*/
public function get_primary_term() {
$primary_term = get_post_meta( $this->post_ID, WPSEO_Meta::$meta_prefix . 'primary_' . $this->taxonomy_name, true );
$terms = $this->get_terms();
if ( ! in_array( $primary_term, wp_list_pluck( $terms, 'term_id' ) ) ) {
$primary_term = false;
}
$primary_term = (int) $primary_term;
return ( $primary_term ) ? ( $primary_term ) : false;
}
/**
* Sets the new primary term ID.
*
* @param int $new_primary_term New primary term ID.
*/
public function set_primary_term( $new_primary_term ) {
update_post_meta( $this->post_ID, WPSEO_Meta::$meta_prefix . 'primary_' . $this->taxonomy_name, $new_primary_term );
}
/**
* Get the terms for the current post ID.
* When $terms is not an array, set $terms to an array.
*
* @return array
*/
protected function get_terms() {
$terms = get_the_terms( $this->post_ID, $this->taxonomy_name );
if ( ! is_array( $terms ) ) {
$terms = [];
}
return $terms;
}
}

View File

@@ -0,0 +1,289 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
*/
/**
* Holder for SEO Rank information.
*/
class WPSEO_Rank {
/**
* Constant used for determining a bad SEO rating.
*
* @var string
*/
const BAD = 'bad';
/**
* Constant used for determining an OK SEO rating.
*
* @var string
*/
const OK = 'ok';
/**
* Constant used for determining a good SEO rating.
*
* @var string
*/
const GOOD = 'good';
/**
* Constant used for determining that no focus keyphrase is set.
*
* @var string
*/
const NO_FOCUS = 'na';
/**
* Constant used for determining that this content is not indexed.
*
* @var string
*/
const NO_INDEX = 'noindex';
/**
* All possible ranks.
*
* @var array
*/
protected static $ranks = [
self::BAD,
self::OK,
self::GOOD,
self::NO_FOCUS,
self::NO_INDEX,
];
/**
* Holds the translation from seo score slug to actual score range.
*
* @var array
*/
protected static $ranges = [
self::NO_FOCUS => [
'start' => 0,
'end' => 0,
],
self::BAD => [
'start' => 1,
'end' => 40,
],
self::OK => [
'start' => 41,
'end' => 70,
],
self::GOOD => [
'start' => 71,
'end' => 100,
],
];
/**
* The current rank.
*
* @var int
*/
protected $rank;
/**
* WPSEO_Rank constructor.
*
* @param int $rank The actual rank.
*/
public function __construct( $rank ) {
if ( ! in_array( $rank, self::$ranks, true ) ) {
$rank = self::BAD;
}
$this->rank = $rank;
}
/**
* Returns the saved rank for this rank.
*
* @return string
*/
public function get_rank() {
return $this->rank;
}
/**
* Returns a CSS class for this rank.
*
* @return string
*/
public function get_css_class() {
$labels = [
self::NO_FOCUS => 'na',
self::NO_INDEX => 'noindex',
self::BAD => 'bad',
self::OK => 'ok',
self::GOOD => 'good',
];
return $labels[ $this->rank ];
}
/**
* Returns a label for this rank.
*
* @return string
*/
public function get_label() {
$labels = [
self::NO_FOCUS => __( 'Not available', 'wordpress-seo' ),
self::NO_INDEX => __( 'No index', 'wordpress-seo' ),
self::BAD => __( 'Needs improvement', 'wordpress-seo' ),
self::OK => __( 'OK', 'wordpress-seo' ),
self::GOOD => __( 'Good', 'wordpress-seo' ),
];
return $labels[ $this->rank ];
}
/**
* Returns a label for use in a drop down.
*
* @return mixed
*/
public function get_drop_down_label() {
$labels = [
self::NO_FOCUS => sprintf(
/* translators: %s expands to the SEO score */
__( 'SEO: %s', 'wordpress-seo' ),
__( 'No Focus Keyphrase', 'wordpress-seo' )
),
self::BAD => sprintf(
/* translators: %s expands to the SEO score */
__( 'SEO: %s', 'wordpress-seo' ),
__( 'Needs improvement', 'wordpress-seo' )
),
self::OK => sprintf(
/* translators: %s expands to the SEO score */
__( 'SEO: %s', 'wordpress-seo' ),
__( 'OK', 'wordpress-seo' )
),
self::GOOD => sprintf(
/* translators: %s expands to the SEO score */
__( 'SEO: %s', 'wordpress-seo' ),
__( 'Good', 'wordpress-seo' )
),
self::NO_INDEX => sprintf(
/* translators: %s expands to the SEO score */
__( 'SEO: %s', 'wordpress-seo' ),
__( 'Post Noindexed', 'wordpress-seo' )
),
];
return $labels[ $this->rank ];
}
/**
* Gets the drop down labels for the readability score.
*
* @return string The readability rank label.
*/
public function get_drop_down_readability_labels() {
$labels = [
self::BAD => sprintf(
/* translators: %s expands to the readability score */
__( 'Readability: %s', 'wordpress-seo' ),
__( 'Needs improvement', 'wordpress-seo' )
),
self::OK => sprintf(
/* translators: %s expands to the readability score */
__( 'Readability: %s', 'wordpress-seo' ),
__( 'OK', 'wordpress-seo' )
),
self::GOOD => sprintf(
/* translators: %s expands to the readability score */
__( 'Readability: %s', 'wordpress-seo' ),
__( 'Good', 'wordpress-seo' )
),
];
return $labels[ $this->rank ];
}
/**
* Get the starting score for this rank.
*
* @return int The start score.
*/
public function get_starting_score() {
// No index does not have a starting score.
if ( self::NO_INDEX === $this->rank ) {
return -1;
}
return self::$ranges[ $this->rank ]['start'];
}
/**
* Get the ending score for this rank.
*
* @return int The end score.
*/
public function get_end_score() {
// No index does not have an end score.
if ( self::NO_INDEX === $this->rank ) {
return -1;
}
return self::$ranges[ $this->rank ]['end'];
}
/**
* Returns a rank for a specific numeric score.
*
* @param int $score The score to determine a rank for.
*
* @return self
*/
public static function from_numeric_score( $score ) {
// Set up the default value.
$rank = new self( self::BAD );
foreach ( self::$ranges as $rank_index => $range ) {
if ( $range['start'] <= $score && $score <= $range['end'] ) {
$rank = new self( $rank_index );
break;
}
}
return $rank;
}
/**
* Returns a list of all possible SEO Ranks.
*
* @return WPSEO_Rank[]
*/
public static function get_all_ranks() {
return array_map( [ 'WPSEO_Rank', 'create_rank' ], self::$ranks );
}
/**
* Returns a list of all possible Readability Ranks.
*
* @return WPSEO_Rank[]
*/
public static function get_all_readability_ranks() {
return array_map( [ 'WPSEO_Rank', 'create_rank' ], [ self::BAD, self::OK, self::GOOD ] );
}
/**
* Converts a numeric rank into a WPSEO_Rank object, for use in functional array_* functions.
*
* @param string $rank SEO Rank.
*
* @return WPSEO_Rank
*/
private static function create_rank( $rank ) {
return new self( $rank );
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,78 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
* @since 7.7
*/
/**
* Class WPSEO_Replacement_Variable.
*
* This class stores the data of a single snippet variable.
*/
class WPSEO_Replacement_Variable {
/**
* The variable to use.
*
* @var string
*/
protected $variable;
/**
* The label of the replacement variable.
*
* @var string
*/
protected $label;
/**
* The description of the replacement variable.
*
* @var string
*/
protected $description;
/**
* WPSEO_Replacement_Variable constructor.
*
* @param string $variable The variable that is replaced.
* @param string $label The label of the replacement variable.
* @param string $description The description of the replacement variable.
*
* @return \WPSEO_Replacement_Variable
*/
public function __construct( $variable, $label, $description ) {
$this->variable = $variable;
$this->label = $label;
$this->description = $description;
}
/**
* Returns the variable to use.
*
* @return string
*/
public function get_variable() {
return $this->variable;
}
/**
* Returns the label of the replacement variable.
*
* @return string
*/
public function get_label() {
return $this->label;
}
/**
* Returns the description of the replacement variable.
*
* @return string
*/
public function get_description() {
return $this->description;
}
}

View File

@@ -0,0 +1,136 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO
*/
/**
* Helps with creating shortlinks in the plugin.
*/
class WPSEO_Shortlinker {
/**
* Collects the additional data necessary for the shortlink.
*
* @return array The shortlink data.
*/
protected function collect_additional_shortlink_data() {
return [
'php_version' => $this->get_php_version(),
'platform' => 'wordpress',
'platform_version' => $GLOBALS['wp_version'],
'software' => $this->get_software(),
'software_version' => WPSEO_VERSION,
'days_active' => $this->get_days_active(),
'user_language' => $this->get_user_language(),
];
}
/**
* Builds a URL to use in the plugin as shortlink.
*
* @param string $url The URL to build upon.
*
* @return string The final URL.
*/
public function build_shortlink( $url ) {
return add_query_arg( $this->collect_additional_shortlink_data(), $url );
}
/**
* Returns a version of the URL with a utm_content with the current version.
*
* @param string $url The URL to build upon.
*
* @return string The final URL.
*/
public static function get( $url ) {
$shortlinker = new self();
return $shortlinker->build_shortlink( $url );
}
/**
* Echoes a version of the URL with a utm_content with the current version.
*
* @param string $url The URL to build upon.
*/
public static function show( $url ) {
echo esc_url( self::get( $url ) );
}
/**
* Gets the shortlink's query params.
*
* @return array The shortlink's query params.
*/
public static function get_query_params() {
$shortlinker = new self();
return $shortlinker->collect_additional_shortlink_data();
}
/**
* Gets the current site's PHP version, without the extra info.
*
* @return string The PHP version.
*/
private function get_php_version() {
$version = explode( '.', PHP_VERSION );
return (int) $version[0] . '.' . (int) $version[1];
}
/**
* Get our software and whether it's active or not.
*
* @return string The software name + activation state.
*/
private function get_software() {
if ( WPSEO_Utils::is_yoast_seo_premium() ) {
return 'premium';
}
return 'free';
}
/**
* Gets the number of days the plugin has been active.
*
* @return int The number of days the plugin is active.
*/
private function get_days_active() {
$date_activated = WPSEO_Options::get( 'first_activated_on' );
$datediff = ( time() - $date_activated );
$days = (int) round( $datediff / DAY_IN_SECONDS );
switch ( $days ) {
case 0:
case 1:
$cohort = '0-1';
break;
case ( $days < 5 ):
$cohort = '2-5';
break;
case ( $days < 30 ):
$cohort = '6-30';
break;
default:
$cohort = '30plus';
}
return $cohort;
}
/**
* Gets the user's language.
*
* @return string The user's language.
*/
private function get_user_language() {
if ( function_exists( 'get_user_locale' ) ) {
return get_user_locale();
}
return false;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
*/
/**
* Class that generates interesting statistics about things.
*/
class WPSEO_Statistics {
/**
* Returns the post count for a certain SEO rank.
*
* @todo Merge/DRY this with the logic virtually the same in WPSEO_Metabox::column_sort_orderby().
*
* @param WPSEO_Rank $rank The SEO rank to get the post count for.
*
* @return int
*/
public function get_post_count( $rank ) {
if ( WPSEO_Rank::NO_FOCUS === $rank->get_rank() ) {
$posts = [
'meta_query' => [
'relation' => 'OR',
[
'key' => WPSEO_Meta::$meta_prefix . 'focuskw',
'value' => 'needs-a-value-anyway',
'compare' => 'NOT EXISTS',
],
],
];
}
elseif ( WPSEO_Rank::NO_INDEX === $rank->get_rank() ) {
$posts = [
'meta_key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex',
'meta_value' => '1',
'compare' => '=',
];
}
else {
$posts = [
'meta_key' => WPSEO_Meta::$meta_prefix . 'linkdex',
'meta_value' => [ $rank->get_starting_score(), $rank->get_end_score() ],
'meta_compare' => 'BETWEEN',
'meta_type' => 'NUMERIC',
];
}
$posts['fields'] = 'ids';
$posts['post_status'] = 'publish';
if ( current_user_can( 'edit_others_posts' ) === false ) {
$posts['author'] = get_current_user_id();
}
$posts = new WP_Query( $posts );
return (int) $posts->found_posts;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,72 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
*/
/**
* Class WPSEO_Validator.
*/
class WPSEO_Validator {
/**
* Validates whether the passed variable is a boolean.
*
* @param mixed $variable The variable to validate.
*
* @return bool Whether or not the passed variable is a valid boolean.
*/
public static function is_boolean( $variable ) {
if ( is_bool( $variable ) ) {
return true;
}
return filter_var( $variable, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ) !== null;
}
/**
* Validates whether the passed variable is a string.
*
* @param mixed $variable The variable to validate.
*
* @return bool Whether or not the passed variable is a string.
*/
public static function is_string( $variable ) {
return is_string( $variable );
}
/**
* Validates whether the passed variable is a non-empty string.
*
* @param mixed $variable The variable to validate.
*
* @return bool Whether or not the passed value is a non-empty string.
*/
public static function is_non_empty_string( $variable ) {
return self::is_string( $variable ) && $variable !== '';
}
/**
* Validates whether the passed variable is an integer.
*
* @param mixed $variable The variable to validate.
*
* @return bool Whether or not the passed variable is an integer.
*/
public static function is_integer( $variable ) {
return filter_var( $variable, FILTER_VALIDATE_INT ) || filter_var( $variable, FILTER_VALIDATE_INT ) === 0;
}
/**
* Determines whether a particular key exists within the passed dataset.
*
* @param array $data The dataset to search through.
* @param string $key The key to search for.
*
* @return bool Whether or not the key exists.
*/
public static function key_exists( array $data, $key ) {
return array_key_exists( $key, $data );
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* Date helper class.
*
* @package WPSEO\Internals
*/
/**
* Class WPSEO_Date_Helper
*
* Note: Move this class with namespace to the src/helpers directory and add a class_alias for BC.
*/
class WPSEO_Date_Helper {
/**
* Formats a given date in UTC TimeZone format.
*
* @param string $date String representing the date / time.
* @param string $format The format that the passed date should be in.
*
* @return string The formatted date.
*/
public function format( $date, $format = DATE_W3C ) {
$immutable_date = date_create_immutable_from_format( 'Y-m-d H:i:s', $date, new DateTimeZone( 'UTC' ) );
if ( ! $immutable_date ) {
return $date;
}
return $immutable_date->format( $format );
}
/**
* Formats the given timestamp to the needed format.
*
* @param int $timestamp The timestamp to use for the formatting.
* @param string $format The format that the passed date should be in.
*
* @return string The formatted date.
*/
public function format_timestamp( $timestamp, $format = DATE_W3C ) {
$immutable_date = date_create_immutable_from_format( 'U', $timestamp, new DateTimeZone( 'UTC' ) );
if ( ! $immutable_date ) {
return $timestamp;
}
return $immutable_date->format( $format );
}
/**
* Formats a given date in UTC TimeZone format and translate it to the set language.
*
* @param string $date String representing the date / time.
* @param string $format The format that the passed date should be in.
*
* @return string The formatted and translated date.
*/
public function format_translated( $date, $format = DATE_W3C ) {
return date_i18n( $format, $this->format( $date, 'U' ) );
}
/**
* Check if a string is a valid datetime.
*
* @param string $datetime String input to check as valid input for DateTime class.
*
* @return bool True when datatime is valid.
*/
public function is_valid_datetime( $datetime ) {
if ( substr( $datetime, 0, 1 ) === '-' ) {
return false;
}
try {
return new DateTime( $datetime ) !== false;
} catch ( Exception $exception ) {
return false;
}
}
}

View File

@@ -0,0 +1,140 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Endpoint
*/
/**
* Represents an implementation of the WPSEO_Endpoint interface to register one or multiple endpoints.
*/
class WPSEO_Endpoint_MyYoast_Connect implements WPSEO_Endpoint {
/**
* The namespace to use.
*
* @var string
*/
const REST_NAMESPACE = 'yoast/v1/myyoast';
/**
* Registers the REST routes that are available on the endpoint.
*
* @codeCoverageIgnore
*
* @return void
*/
public function register() {
register_rest_route(
self::REST_NAMESPACE,
'connect',
[
'methods' => 'POST',
'callback' => [ $this, 'handle_request' ],
'permission_callback' => [ $this, 'can_retrieve_data' ],
]
);
}
/**
* Determines whether or not data can be retrieved for the registered endpoints.
*
* @param WP_REST_Request $request The current request.
*
* @return WP_REST_Response The response.
*/
public function handle_request( WP_REST_Request $request ) {
if ( $request->get_param( 'url' ) !== $this->get_home_url() ) {
return new WP_REST_Response(
'Bad request: URL mismatch.',
403
);
}
if ( $request->get_param( 'clientId' ) !== $this->get_client_id() ) {
return new WP_REST_Response(
'Bad request: ClientID mismatch.',
403
);
}
$client_secret = $request->get_param( 'clientSecret' );
if ( empty( $client_secret ) ) {
return new WP_REST_Response(
'Bad request: ClientSecret missing.',
403
);
}
$this->save_secret( $client_secret );
return new WP_REST_Response( 'Connection successful established.' );
}
/**
* Determines whether or not data can be retrieved for the registered endpoints.
*
* @return bool Whether or not data can be retrieved.
*/
public function can_retrieve_data() {
return true;
}
/**
* Saves the client secret.
*
* @codeCoverageIgnore
*
* @param string $client_secret The secret to save.
*
* @return void
*/
protected function save_secret( $client_secret ) {
$this->get_client()->save_configuration(
[
'secret' => $client_secret,
]
);
}
/**
* Retrieves the current client ID.
*
* @codeCoverageIgnore
*
* @return array The client ID.
*/
protected function get_client_id() {
$config = $this->get_client()->get_configuration();
return $config['clientId'];
}
/**
* Retrieves an instance of the client.
*
* @codeCoverageIgnore
*
* @return WPSEO_MyYoast_Client Instance of client.
*/
protected function get_client() {
static $client;
if ( ! $client ) {
$client = new WPSEO_MyYoast_Client();
}
return $client;
}
/**
* Wraps the method for retrieving the home URL.
*
* @codeCoverageIgnore
*
* @return string Home URL.
*/
protected function get_home_url() {
return WPSEO_Utils::get_home_url();
}
}

View File

@@ -0,0 +1,153 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
*/
/**
* Class WPSEO_Invalid_Argument_Exception.
*/
class WPSEO_Invalid_Argument_Exception extends InvalidArgumentException {
/**
* Creates an invalid empty parameter exception.
*
* @param string $name The name of the parameter.
*
* @return WPSEO_Invalid_Argument_Exception The exception.
*/
public static function empty_parameter( $name ) {
return new self(
sprintf(
/* translators: %1$s expands to the parameter name. */
__( 'The parameter `%1$s` cannot be empty.', 'wordpress-seo' ),
$name
)
);
}
/**
* Creates an invalid parameter exception.
*
* @param mixed $parameter The parameter value of the field.
* @param string $name The name of the field.
* @param string $expected The expected type.
*
* @return WPSEO_Invalid_Argument_Exception The exception.
*/
public static function invalid_parameter_type( $parameter, $name, $expected ) {
return new self(
sprintf(
/* translators: %1$s expands to the parameter name. %2$s expands to the expected type and %3$s expands to the expected type. */
__( 'Invalid type for parameter `%1$s` passed. Expected `%2$s`, but got `%3$s`', 'wordpress-seo' ),
$name,
$expected,
gettype( $parameter )
)
);
}
/**
* Creates an invalid integer parameter exception.
*
* @param mixed $parameter The parameter value of the field.
* @param string $name The name of the field.
*
* @return WPSEO_Invalid_Argument_Exception The exception.
*/
public static function invalid_integer_parameter( $parameter, $name ) {
return self::invalid_parameter_type( $parameter, $name, 'integer' );
}
/**
* Creates an invalid string parameter exception.
*
* @param mixed $parameter The parameter value of the field.
* @param string $name The name of the field.
*
* @return WPSEO_Invalid_Argument_Exception The exception.
*/
public static function invalid_string_parameter( $parameter, $name ) {
return self::invalid_parameter_type( $parameter, $name, 'string' );
}
/**
* Creates an invalid boolean parameter exception.
*
* @param mixed $parameter The parameter value of the field.
* @param string $name The name of the field.
*
* @return WPSEO_Invalid_Argument_Exception The exception.
*/
public static function invalid_boolean_parameter( $parameter, $name ) {
return self::invalid_parameter_type( $parameter, $name, 'boolean' );
}
/**
* Creates an invalid callable parameter exception.
*
* @param mixed $parameter The parameter value of the field.
* @param string $name The name of the field.
*
* @return WPSEO_Invalid_Argument_Exception The exception.
*/
public static function invalid_callable_parameter( $parameter, $name ) {
return self::invalid_parameter_type( $parameter, $name, 'callable' );
}
/**
* Creates an invalid object type exception.
*
* @param string $type The type of the field.
*
* @return WPSEO_Invalid_Argument_Exception The exception.
*/
public static function invalid_type( $type ) {
return new self(
sprintf(
/* translators: %1$s expands to the object type. */
__( 'The object type `%1$s` is invalid', 'wordpress-seo' ),
$type
)
);
}
/**
* Creates an invalid object subtype exception.
*
* @param string $subtype The invalid subtype.
* @param string $type The parent type of the subtype.
*
* @return WPSEO_Invalid_Argument_Exception The exception.
*/
public static function invalid_subtype( $subtype, $type ) {
return new self(
sprintf(
/* translators: %1$s expands to the object subtype. %2$s resolved to the object type. */
__( '`%1$s` is not a valid subtype of `%2$s`', 'wordpress-seo' ),
$subtype,
$type
)
);
}
/**
* Creates an unknown object exception.
*
* @param int $id The ID that was searched for.
* @param string $type The type of object that was being searched for.
*
* @return WPSEO_Invalid_Argument_Exception The exception.
*/
public static function unknown_object( $id, $type ) {
return new self(
sprintf(
/* translators: %1$s expands to the object ID. %2$s resolved to the object type. */
__( 'No object with ID %1$s and %2$s could be found', 'wordpress-seo' ),
$id,
$type
)
);
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
*/
/**
* Class WPSEO_Invalid_Indexable_Exception.
*/
class WPSEO_Invalid_Indexable_Exception extends InvalidArgumentException {
/**
* Creates an invalid indexable exception.
*
* @param int $id The ID that was passed.
*
* @return WPSEO_Invalid_Indexable_Exception The exception.
*/
public static function non_existing_indexable( $id ) {
return new self(
sprintf(
/* translators: %1$s expands to the indexable's ID. */
__( 'Indexable with ID `%1$s` does not exist', 'wordpress-seo' ),
$id
)
);
}
/**
* Creates an invalid POST request exception.
*
* @param int $id The ID that was passed.
*
* @return WPSEO_Invalid_Indexable_Exception The exception.
*/
public static function invalid_post_request( $id ) {
return new self(
sprintf(
/* translators: %1$s expands to the indexable's ID. */
__( 'Invalid POST request. Meta values already exist for object with ID %1$s.', 'wordpress-seo' ),
$id
)
);
}
}

View File

@@ -0,0 +1,13 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
*/
/**
* Class WPSEO_MyYoast_Authentication_Exception.
*/
class WPSEO_MyYoast_Authentication_Exception extends WPSEO_MyYoast_Bad_Request_Exception {
}

View File

@@ -0,0 +1,13 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
*/
/**
* Class WPSEO_MyYoast_Bad_Request_Exception.
*/
class WPSEO_MyYoast_Bad_Request_Exception extends Exception {
}

View File

@@ -0,0 +1,13 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
*/
/**
* Class WPSEO_MyYoast_Invalid_JSON_Exception.
*/
class WPSEO_MyYoast_Invalid_JSON_Exception extends WPSEO_MyYoast_Bad_Request_Exception {
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
*/
/**
* Class WPSEO_REST_Request_Exception.
*/
class WPSEO_REST_Request_Exception extends Exception {
/**
* Creates a patch failure exception.
*
* @param string $object_type The name of the parameter.
* @param string $object_id The ID of the parameter.
*
* @return WPSEO_REST_Request_Exception The exception.
*/
public static function patch( $object_type, $object_id ) {
return new self(
sprintf(
/* translators: %1$s expands to object type. %2$s expands to the object ID. */
__( '%1$s with ID %2$s couldn\'t be patched', 'wordpress-seo' ),
$object_type,
$object_id
)
);
}
}

View File

@@ -0,0 +1,58 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
*/
/**
* Represents the health check for paginated comments.
*/
class WPSEO_Health_Check_Page_Comments extends WPSEO_Health_Check {
/**
* The name of the test.
*
* @var string
*/
protected $test = 'yoast-health-check-page-comments';
/**
* Runs the test.
*/
public function run() {
if ( ! $this->has_page_comments() ) {
$this->label = esc_html__( 'Paging comments is properly disabled', 'wordpress-seo' );
$this->status = self::STATUS_GOOD;
$this->badge['color'] = 'blue';
$this->description = esc_html__( 'Paging comments is disabled. As this is not needed in 999 out of 1000 cases, we recommend to keep it disabled.', 'wordpress-seo' );
$this->add_yoast_signature();
return;
}
$this->label = esc_html__( 'Paging comments is enabled', 'wordpress-seo' );
$this->status = self::STATUS_RECOMMENDED;
$this->badge['color'] = 'red';
$this->description = esc_html__(
'Paging comments is enabled. As this is not needed in 999 out of 1000 cases, we recommend you disable it.
To fix this, uncheck the box in front of "Break comments into pages..." on the Discussion Settings page.',
'wordpress-seo'
);
$this->actions = sprintf(
/* translators: 1: Opening tag of the link to the discussion settings page, 2: Link closing tag. */
esc_html__( '%1$sGo to the Discussion Settings page%2$s', 'wordpress-seo' ),
'<a href="' . esc_url( admin_url( 'options-discussion.php' ) ) . '">',
'</a>'
);
$this->add_yoast_signature();
}
/**
* Are page comments enabled.
*
* @return bool True when page comments are enabled.
*/
protected function has_page_comments() {
return '1' === get_option( 'page_comments' );
}
}

View File

@@ -0,0 +1,213 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
*/
/**
* Represents the abstract class for the health check.
*/
abstract class WPSEO_Health_Check {
/**
* The health check section in which 'good' results should be shown.
*
* @var string
*/
const STATUS_GOOD = 'good';
/**
* The health check section in which 'recommended' results should be shown.
*
* @var string
*/
const STATUS_RECOMMENDED = 'recommended';
/**
* The health check section in which 'critical' results should be shown.
*
* @var string
*/
const STATUS_CRITICAL = 'critical';
/**
* The value of the section header in the Health check.
*
* @var string
*/
protected $label = '';
/**
* Section the result should be displayed in.
*
* @var string
*/
protected $status = '';
/**
* What the badge should say with a color.
*
* @var array
*/
protected $badge = [
'label' => '',
'color' => '',
];
/**
* Additional details about the results of the test.
*
* @var string
*/
protected $description = '';
/**
* A link or button to allow the end user to take action on the result.
*
* @var string
*/
protected $actions = '';
/**
* The name of the test.
*
* @var string
*/
protected $test = '';
/**
* Whether or not the test should be ran on AJAX as well.
*
* @var bool True when is async, default false.
*/
protected $async = false;
/**
* Runs the test and returns the result.
*/
abstract public function run();
/**
* Registers the test to WordPress.
*/
public function register_test() {
if ( $this->is_async() ) {
add_filter( 'site_status_tests', [ $this, 'add_async_test' ] );
add_action( 'wp_ajax_health-check-' . $this->get_test_name(), [ $this, 'get_async_test_result' ] );
return;
}
add_filter( 'site_status_tests', [ $this, 'add_test' ] );
}
/**
* Runs the test.
*
* @param array $tests Array with the current tests.
*
* @return array The extended array.
*/
public function add_test( $tests ) {
$tests['direct'][ $this->get_test_name() ] = [
'test' => [ $this, 'get_test_result' ],
];
return $tests;
}
/**
* Runs the test in async mode.
*
* @param array $tests Array with the current tests.
*
* @return array The extended array.
*/
public function add_async_test( $tests ) {
$tests['async'][ $this->get_test_name() ] = [
'test' => $this->get_test_name(),
];
return $tests;
}
/**
* Formats the test result as an array.
*
* @return array The formatted test result.
*/
public function get_test_result() {
$this->run();
return [
'label' => $this->label,
'status' => $this->status,
'badge' => $this->get_badge(),
'description' => $this->description,
'actions' => $this->actions,
'test' => $this->test,
];
}
/**
* Formats the test result as an array.
*/
public function get_async_test_result() {
wp_send_json_success( $this->get_test_result() );
}
/**
* Retrieves the badge and ensure usable values are set.
*
* @return array The proper formatted badge.
*/
protected function get_badge() {
if ( ! is_array( $this->badge ) ) {
$this->badge = [];
}
if ( empty( $this->badge['label'] ) ) {
$this->badge['label'] = __( 'SEO', 'wordpress-seo' );
}
if ( empty( $this->badge['color'] ) ) {
$this->badge['color'] = 'green';
}
return $this->badge;
}
/**
* WordPress converts the underscores to dashes. To prevent issues we have
* to do it as well.
*
* @return string The formatted testname.
*/
protected function get_test_name() {
return str_replace( '_', '-', $this->test );
}
/**
* Checks if the health check is async.
*
* @return bool True when check is async.
*/
protected function is_async() {
return ! empty( $this->async );
}
/**
* Adds a text to the bottom of the Site Health check to indicate it is a Yoast SEO Site Health Check.
*/
protected function add_yoast_signature() {
$this->actions .= sprintf(
/* translators: 1: Start of a paragraph beginning with the Yoast icon, 2: Expands to 'Yoast SEO', 3: Paragraph closing tag. */
esc_html__( '%1$sThis was reported by the %2$s plugin%3$s', 'wordpress-seo' ),
'<p class="yoast-site-health__signature"><img src="' . esc_url( plugin_dir_url( WPSEO_FILE ) . 'images/Yoast_SEO_Icon.svg' ) . '" alt="" height="20" width="20" class="yoast-site-health__signature-icon">',
'Yoast SEO',
'</p>'
);
}
}

View File

@@ -0,0 +1,4 @@
<?php
/**
* Nothing to see here.
*/

View File

@@ -0,0 +1,130 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Indexables
*/
/**
* Class WPSEO_Indexable.
*/
abstract class WPSEO_Indexable {
/**
* The updateable fields.
*
* @var array
*/
protected $updateable_fields = [];
/**
* The indexable's data.
*
* @var array
*/
protected $data;
/**
* The available validators to run.
*
* @var array
*/
protected $validators = [
'WPSEO_Object_Type_Validator',
'WPSEO_Link_Validator',
'WPSEO_Keyword_Validator',
'WPSEO_Meta_Values_Validator',
'WPSEO_OpenGraph_Validator',
'WPSEO_Robots_Validator',
'WPSEO_Twitter_Validator',
];
/**
* Indexable constructor.
*
* @param array $data The data to use to construct the indexable.
*/
public function __construct( $data ) {
$this->validate_data( $data );
$this->data = $data;
}
/**
* Converts the meta value to a boolean value.
*
* @param string $value The value to convert.
*
* @return bool|null The converted value.
*/
protected static function get_robots_noindex_value( $value ) {
if ( $value === '1' ) {
return true;
}
if ( $value === '2' ) {
return false;
}
return null;
}
/**
* Determines whether the advanced robot metas value contains the passed value.
*
* @param int $object_id The ID of the object to check.
* @param string $value The name of the advanced robots meta value to look for.
*
* @return bool Whether or not the advanced robots meta values contains the passed string.
*/
protected static function has_advanced_meta_value( $object_id, $value ) {
return strpos( WPSEO_Meta::get_value( 'meta-robots-adv', $object_id ), $value ) !== false;
}
/**
* Validates the data.
*
* @param array $data The data to validate.
*
* @return bool True if all validators have successfully validated.
*/
protected function validate_data( $data ) {
foreach ( $this->validators as $validator ) {
// This is necessary to run under PHP 5.2.
$validator_instance = new $validator();
$validator_instance->validate( $data );
}
return true;
}
/**
* Updates the data and returns a new instance.
*
* @param array $data The data to update into a new instance.
*
* @return WPSEO_Indexable A new instance with the updated data.
*/
abstract public function update( $data );
/**
* Filters out data that isn't considered updateable and returns a valid dataset.
*
* @param array $data The dataset to filter.
*
* @return array The updateable dataset.
*/
public function filter_updateable_data( $data ) {
return array_intersect_key( $data, array_flip( $this->updateable_fields ) );
}
/**
* Returns the data as an array.
*
* @return array The data as an array.
*/
public function to_array() {
return $this->data;
}
}

View File

@@ -0,0 +1,113 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Indexables
*/
/**
* Class WPSEO_Post_Indexable.
*/
abstract class WPSEO_Object_Type {
/**
* The ID of the object.
*
* @var int
*/
protected $id;
/**
* The type of the object.
*
* @var string
*/
protected $type;
/**
* The subtype of the object.
*
* @var string
*/
protected $sub_type;
/**
* The permalink of the object.
*
* @var string
*/
protected $permalink;
/**
* WPSEO_Object_Type constructor.
*
* @param int $id The ID of the object.
* @param string $type The type of object.
* @param string $subtype The subtype of the object.
* @param string $permalink The permalink of the object.
*/
public function __construct( $id, $type, $subtype, $permalink ) {
$this->id = (int) $id;
$this->type = $type;
$this->sub_type = $subtype;
$this->permalink = $permalink;
}
/**
* Gets the ID.
*
* @return int The ID.
*/
public function get_id() {
return $this->id;
}
/**
* Gets the type.
*
* @return string The type.
*/
public function get_type() {
return $this->type;
}
/**
* Gets the subtype.
*
* @return string The subtype.
*/
public function get_subtype() {
return $this->sub_type;
}
/**
* Gets the permalink.
*
* @return string The permalink.
*/
public function get_permalink() {
return $this->permalink;
}
/**
* Determines whether the passed type is equal to the object's type.
*
* @param string $type The type to check.
*
* @return bool Whether or not the passed type is equal.
*/
public function is_type( $type ) {
return $this->type === $type;
}
/**
* Determines whether the passed subtype is equal to the object's subtype.
*
* @param string $sub_type The subtype to check.
*
* @return bool Whether or not the passed subtype is equal.
*/
public function is_subtype( $sub_type ) {
return $this->sub_type === $sub_type;
}
}

View File

@@ -0,0 +1,103 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Indexables
*/
/**
* Class WPSEO_Post_Indexable.
*/
class WPSEO_Post_Indexable extends WPSEO_Indexable {
/**
* The updateable fields.
*
* @var array
*/
protected $updateable_fields = [
'canonical',
'title',
'description',
'breadcrumb_title',
'og_title',
'og_description',
'og_image',
'twitter_title',
'twitter_description',
'twitter_image',
'is_robots_noindex',
'is_robots_nofollow',
'is_robots_noarchive',
'is_robots_noimageindex',
'is_robots_nosnippet',
'primary_focus_keyword',
'primary_focus_keyword',
'primary_focus_keyword_score',
'readability_score',
'is_cornerstone',
];
/**
* Creates a new Indexable from a passed object.
*
* @param int $object_id The object ID to create the object for.
*
* @return WPSEO_Indexable The indexable.
*
* @throws WPSEO_Invalid_Argument_Exception Thrown if the passed ID is not for an object of type 'post'.
*/
public static function from_object( $object_id ) {
$post = WPSEO_Post_Object_Type::from_object( $object_id );
$link_count = new WPSEO_Link_Column_Count();
$link_count->set( [ $object_id ] );
$post_object_id = $post->get_id();
return new self(
[
'object_id' => $post_object_id,
'object_type' => $post->get_type(),
'object_subtype' => $post->get_subtype(),
'permalink' => $post->get_permalink(),
'canonical' => WPSEO_Meta::get_value( 'canonical', $post_object_id ),
'title' => WPSEO_Meta::get_value( 'title', $post_object_id ),
'description' => WPSEO_Meta::get_value( 'metadesc', $post_object_id ),
'breadcrumb_title' => WPSEO_Meta::get_value( 'bctitle', $post_object_id ),
'og_title' => WPSEO_Meta::get_value( 'opengraph-title', $post_object_id ),
'og_description' => WPSEO_Meta::get_value( 'opengraph-description', $post_object_id ),
'og_image' => WPSEO_Meta::get_value( 'opengraph-image', $post_object_id ),
'twitter_title' => WPSEO_Meta::get_value( 'twitter-title', $post_object_id ),
'twitter_description' => WPSEO_Meta::get_value( 'twitter-description', $post_object_id ),
'twitter_image' => WPSEO_Meta::get_value( 'twitter-image', $post_object_id ),
'is_robots_noindex' => self::get_robots_noindex_value( WPSEO_Meta::get_value( 'meta-robots-noindex', $post_object_id ) ),
'is_robots_nofollow' => WPSEO_Meta::get_value( 'meta-robots-nofollow', $post_object_id ) === '1',
'is_robots_noarchive' => self::has_advanced_meta_value( $post_object_id, 'noarchive' ),
'is_robots_noimageindex' => self::has_advanced_meta_value( $post_object_id, 'noimageindex' ),
'is_robots_nosnippet' => self::has_advanced_meta_value( $post_object_id, 'nosnippet' ),
'primary_focus_keyword' => WPSEO_Meta::get_value( 'focuskw', $post_object_id ),
'primary_focus_keyword_score' => (int) WPSEO_Meta::get_value( 'linkdex', $post_object_id ),
'readability_score' => (int) WPSEO_Meta::get_value( 'content_score', $post_object_id ),
'is_cornerstone' => WPSEO_Meta::get_value( 'is_cornerstone', $post_object_id ) === '1',
'link_count' => (int) $link_count->get( $post_object_id ),
'incoming_link_count' => (int) $link_count->get( $post_object_id, 'incoming_link_count' ),
'created_at' => null,
'updated_at' => null,
]
);
}
/**
* Updates the data and returns a new instance.
*
* @param array $data The data to update into a new instance.
*
* @return WPSEO_Indexable A new instance with the updated data.
*/
public function update( $data ) {
$data = array_merge( $this->data, $this->filter_updateable_data( $data ) );
return new self( $data );
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Indexables
*/
/**
* Class WPSEO_Post_Object_Type.
*/
class WPSEO_Post_Object_Type extends WPSEO_Object_Type {
/**
* Creates a new instance based on the passed object ID.
*
* @param int $object_id The object ID to base the object on.
*
* @return WPSEO_Post_Object_Type The class instance.
*
* @throws WPSEO_Invalid_Argument_Exception Thrown if the post is null.
*/
public static function from_object( $object_id ) {
$post = get_post( $object_id );
if ( $post === null ) {
throw WPSEO_Invalid_Argument_Exception::unknown_object( $object_id, 'post' );
}
return new self( $object_id, 'post', get_post_type( $object_id ), get_permalink( $object_id ) );
}
}

View File

@@ -0,0 +1,126 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Indexables
*/
/**
* Class WPSEO_Term_Indexable.
*/
class WPSEO_Term_Indexable extends WPSEO_Indexable {
/**
* The updateable fields.
*
* @var array
*/
protected $updateable_fields = [
'canonical',
'title',
'description',
'breadcrumb_title',
'og_title',
'og_description',
'og_image',
'twitter_title',
'twitter_description',
'twitter_image',
'is_robots_noindex',
'primary_focus_keyword',
'primary_focus_keyword',
'primary_focus_keyword_score',
'readability_score',
];
/**
* Creates a new Indexable from a passed object.
*
* @param int $object_id The object ID to create the object for.
*
* @return WPSEO_Indexable The indexable.
*
* @throws WPSEO_Invalid_Argument_Exception Thrown if the passed ID is not for an object of type 'term'.
*/
public static function from_object( $object_id ) {
$term = WPSEO_Term_Object_Type::from_object( $object_id );
$term_object_id = $term->get_id();
return new self(
[
'object_id' => $term_object_id,
'object_type' => $term->get_type(),
'object_subtype' => $term->get_subtype(),
'permalink' => $term->get_permalink(),
'canonical' => self::get_meta_value( 'canonical', $term ),
'title' => self::get_meta_value( 'title', $term ),
'description' => self::get_meta_value( 'desc', $term ),
'breadcrumb_title' => self::get_meta_value( 'bctitle', $term ),
'og_title' => self::get_meta_value( 'opengraph-title', $term ),
'og_description' => self::get_meta_value( 'opengraph-description', $term ),
'og_image' => self::get_meta_value( 'opengraph-image', $term ),
'twitter_title' => self::get_meta_value( 'twitter-title', $term ),
'twitter_description' => self::get_meta_value( 'twitter-description', $term ),
'twitter_image' => self::get_meta_value( 'twitter-image', $term ),
'is_robots_noindex' => self::get_robots_noindex_value( self::get_meta_value( 'noindex', $term ) ),
'is_robots_nofollow' => null,
'is_robots_noarchive' => null,
'is_robots_noimageindex' => null,
'is_robots_nosnippet' => null,
'primary_focus_keyword' => self::get_meta_value( 'focuskw', $term ),
'primary_focus_keyword_score' => (int) self::get_meta_value( 'linkdex', $term ),
'readability_score' => (int) self::get_meta_value( 'content_score', $term ),
'is_cornerstone' => false,
'link_count' => null,
'incoming_link_count' => null,
'created_at' => null,
'updated_at' => null,
]
);
}
/**
* Updates the data and returns a new instance.
*
* @param array $data The data to update into a new instance.
*
* @return WPSEO_Indexable A new instance with the updated data.
*/
public function update( $data ) {
$data = array_merge( $this->data, $this->filter_updateable_data( $data ) );
return new self( $data );
}
/**
* Returns the needed term meta field.
*
* @param string $field The requested field.
* @param WPSEO_Term_Object_Type $term The term object.
*
* @return bool|mixed The value of the requested field.
*/
protected static function get_meta_value( $field, $term ) {
return WPSEO_Taxonomy_Meta::get_term_meta( $term->get_id(), $term->get_subtype(), $field );
}
/**
* Converts the meta value to a boolean value.
*
* @param string $value The value to convert.
*
* @return bool|null The converted value.
*/
protected static function get_robots_noindex_value( $value ) {
if ( $value === 'noindex' ) {
return true;
}
if ( $value === 'index' ) {
return false;
}
return null;
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Indexables
*/
/**
* Class WPSEO_Term_Object_Type.
*/
class WPSEO_Term_Object_Type extends WPSEO_Object_Type {
/**
* Creates a new instance based on the passed object ID.
*
* @param int $object_id The object ID to base the object on.
*
* @return WPSEO_Term_Object_Type The class instance.
*
* @throws WPSEO_Invalid_Argument_Exception Thrown if the term is null or if a WordPress error is thrown.
*/
public static function from_object( $object_id ) {
$term = get_term( $object_id );
if ( $term === null || is_wp_error( $term ) ) {
throw WPSEO_Invalid_Argument_Exception::unknown_object( $object_id, 'term' );
}
return new self( $object_id, 'term', $term->taxonomy, get_term_link( $term ) );
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* WPSEO interface file.
*
* @package WPSEO\Indexables
*/
/**
* Interface WPSEO_Endpoint_Validator.
*/
interface WPSEO_Endpoint_Validator {
/**
* Validates the passed request data.
*
* @param array $request_data The request data to validate.
*
* @return void
*/
public function validate( $request_data );
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Indexables
*/
/**
* Class WPSEO_Keyword_Validator.
*/
class WPSEO_Keyword_Validator implements WPSEO_Endpoint_Validator {
/**
* Validates the keyword-related data.
*
* @param array $request_data The request data to validate.
*
* @throws WPSEO_Invalid_Argument_Exception Thrown if the keyword or the score is of an invalid value type.
*/
public function validate( $request_data ) {
if ( WPSEO_Validator::key_exists( $request_data, 'keyword' ) && ! WPSEO_Validator::is_string( $request_data['keyword'] ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_string_parameter( $request_data['keyword'], 'keyword' );
}
if ( WPSEO_Validator::key_exists( $request_data, 'score' ) && ! WPSEO_Validator::is_integer( $request_data['score'] ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_integer_parameter( $request_data['score'], 'score' );
}
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Indexables
*/
/**
* Class WPSEO_Link_Validator.
*/
class WPSEO_Link_Validator implements WPSEO_Endpoint_Validator {
/**
* Validates the link-related data.
*
* @param array $request_data The request data to validate.
*
* @throws WPSEO_Invalid_Argument_Exception Thrown if the link-data count or incoming count is of an invalid value type.
*/
public function validate( $request_data ) {
if ( WPSEO_Validator::key_exists( $request_data, 'count' ) && ! WPSEO_Validator::is_integer( $request_data['count'] ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_integer_parameter( $request_data['count'], 'count' );
}
if ( WPSEO_Validator::key_exists( $request_data, 'incoming_count' ) && ! WPSEO_Validator::is_integer( $request_data['incoming_count'] ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_integer_parameter( $request_data['incoming_count'], 'incoming_count' );
}
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Indexables
*/
/**
* Class WPSEO_Meta_Values_Validator.
*/
class WPSEO_Meta_Values_Validator implements WPSEO_Endpoint_Validator {
/**
* Validates the meta values data.
*
* @param array $request_data The request data to validate.
*
* @return void
*
* @throws WPSEO_Invalid_Argument_Exception Thrown if a field from the request data is of an invalid value type.
*/
public function validate( $request_data ) {
if ( WPSEO_Validator::key_exists( $request_data, 'title' ) && ! WPSEO_Validator::is_string( $request_data['title'] ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_string_parameter( $request_data['title'], 'title' );
}
if ( WPSEO_Validator::key_exists( $request_data, 'metadesc' ) && ! WPSEO_Validator::is_string( $request_data['metadesc'] ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_string_parameter( $request_data['metadesc'], 'metadesc' );
}
if ( WPSEO_Validator::key_exists( $request_data, 'permalink' ) && ! WPSEO_Validator::is_string( $request_data['permalink'] ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_string_parameter( $request_data['permalink'], 'permalink' );
}
if ( WPSEO_Validator::key_exists( $request_data, 'readability_score' ) && ! WPSEO_Validator::is_integer( $request_data['readability_score'] ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_integer_parameter( $request_data['readability_score'], 'readability_score' );
}
if ( WPSEO_Validator::key_exists( $request_data, 'is_cornerstone' ) && ! WPSEO_Validator::is_boolean( $request_data['is_cornerstone'] ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_boolean_parameter( $request_data['is_cornerstone'], 'is_cornerstone' );
}
if ( WPSEO_Validator::key_exists( $request_data, 'canonical' ) && ! WPSEO_Validator::is_string( $request_data['canonical'] ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_string_parameter( $request_data['canonical'], 'canonical' );
}
if ( WPSEO_Validator::key_exists( $request_data, 'breadcrumb_title' ) && ! WPSEO_Validator::is_string( $request_data['breadcrumb_title'] ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_string_parameter( $request_data['breadcrumb_title'], 'breadcrumb_title' );
}
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Indexables
*/
/**
* Class WPSEO_Object_Type_Validator.
*/
class WPSEO_Object_Type_Validator implements WPSEO_Endpoint_Validator {
/**
* Validates the object_type parameter.
*
* @param string $object_type The object type to validate.
*
* @return void
*
* @throws WPSEO_Invalid_Argument_Exception Thrown is the object type is invalid.
*/
private static function validate_type( $object_type ) {
if ( ! in_array( $object_type, [ 'post', 'term' ], true ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_type( $object_type );
}
}
/**
* Validates whether the passed subtype is valid or not.
*
* @param string $type The type to validate.
* @param string $subtype The subtype to validate.
*
* @return void
*
* @throws WPSEO_Invalid_Argument_Exception Thrown if the subtype doesn't exist for the given type.
*/
private static function validate_subtype( $type, $subtype ) {
if ( $type === 'post' && ! post_type_exists( $subtype ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_subtype( $subtype, $type );
}
if ( $type === 'term' && ! taxonomy_exists( $subtype ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_subtype( $subtype, $type );
}
}
/**
* Validates the object type-related data.
*
* @param array $request_data The request data to validate.
*
* @return void
*
* @throws WPSEO_Invalid_Argument_Exception Thrown if the type or subtype are invalid.
*/
public function validate( $request_data ) {
if ( WPSEO_Validator::key_exists( $request_data, 'object_type' ) ) {
self::validate_type( $request_data['object_type'] );
}
if ( WPSEO_Validator::key_exists( $request_data, 'object_subtype' ) ) {
self::validate_subtype( $request_data['object_type'], $request_data['object_subtype'] );
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Indexables
*/
/**
* Class WPSEO_OpenGraph_Validator.
*/
class WPSEO_OpenGraph_Validator implements WPSEO_Endpoint_Validator {
/**
* Validates the OpenGraph-related data.
*
* @param array $request_data The request data to validate.
*
* @return void
*
* @throws WPSEO_Invalid_Argument_Exception Thrown if one of the OpenGraph properties is of an invalid value type.
*/
public function validate( $request_data ) {
if ( WPSEO_Validator::key_exists( $request_data, 'og_title' ) && ! WPSEO_Validator::is_string( $request_data['og_title'] ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_string_parameter( $request_data['og_title'], 'og_title' );
}
if ( WPSEO_Validator::key_exists( $request_data, 'og_description' ) && ! WPSEO_Validator::is_string( $request_data['og_description'] ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_string_parameter( $request_data['og_description'], 'og_description' );
}
if ( WPSEO_Validator::key_exists( $request_data, 'og_image' ) && ! WPSEO_Validator::is_string( $request_data['og_image'] ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_string_parameter( $request_data['og_image'], 'og_image' );
}
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Indexables
*/
/**
* Class WPSEO_Robots_Validator.
*/
class WPSEO_Robots_Validator implements WPSEO_Endpoint_Validator {
/**
* The robots keys to validate.
*
* @var array
*/
private $robots_to_validate = [
'is_robots_nofollow',
'is_robots_noarchive',
'is_robots_noimageindex',
'is_robots_nosnippet',
'is_robots_noindex',
];
/**
* Validates the passed request data.
*
* @param array $request_data The request data to validate.
*
* @return void
*
* @throws WPSEO_Invalid_Argument_Exception Thrown if the robots values are not a boolean type.
*/
public function validate( $request_data ) {
foreach ( $this->robots_to_validate as $item ) {
if ( ! WPSEO_Validator::key_exists( $request_data, $item ) ) {
continue;
}
if ( ! is_null( $request_data[ $item ] ) && ! WPSEO_Validator::is_boolean( $request_data[ $item ] ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_boolean_parameter( $request_data[ $item ], $item );
}
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Indexables
*/
/**
* Class WPSEO_Twitter_Validator.
*/
class WPSEO_Twitter_Validator implements WPSEO_Endpoint_Validator {
/**
* Validates the Twitter-related data.
*
* @param array $request_data The request data to validate.
*
* @return void
*
* @throws WPSEO_Invalid_Argument_Exception Thrown if one of the Twitter properties is of an invalid value type.
*/
public function validate( $request_data ) {
if ( WPSEO_Validator::key_exists( $request_data, 'twitter_title' ) && ! WPSEO_Validator::is_string( $request_data['twitter_title'] ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_string_parameter( $request_data['twitter_title'], 'twitter_title' );
}
if ( WPSEO_Validator::key_exists( $request_data, 'twitter_description' ) && ! WPSEO_Validator::is_string( $request_data['twitter_description'] ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_string_parameter( $request_data['twitter_description'], 'twitter_description' );
}
if ( WPSEO_Validator::key_exists( $request_data, 'twitter_image' ) && ! WPSEO_Validator::is_string( $request_data['twitter_image'] ) ) {
throw WPSEO_Invalid_Argument_Exception::invalid_string_parameter( $request_data['twitter_image'], 'twitter_image' );
}
}
}

View File

@@ -0,0 +1,19 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO
*/
/**
* An interface for registering AJAX integrations with WordPress.
*/
interface WPSEO_WordPress_AJAX_Integration {
/**
* Registers all AJAX hooks to WordPress.
*
* @return void
*/
public function register_ajax_hooks();
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO
*/
if ( ! interface_exists( 'WPSEO_WordPress_Integration' ) ) {
/**
* An interface for registering integrations with WordPress.
*/
interface WPSEO_WordPress_Integration {
/**
* Registers all hooks to WordPress.
*
* @return void
*/
public function register_hooks();
}
}

View File

@@ -0,0 +1,87 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
* @since 5.9.0
*/
/**
* Group of language utility methods for use by WPSEO.
* All methods are static, this is just a sort of namespacing class wrapper.
*/
class WPSEO_Language_Utils {
/**
* Returns the language part of a given locale, defaults to english when the $locale is empty.
*
* @param string $locale The locale to get the language of.
*
* @return string The language part of the locale.
*/
public static function get_language( $locale = null ) {
$language = 'en';
if ( empty( $locale ) || ! is_string( $locale ) ) {
return $language;
}
$locale_parts = explode( '_', $locale );
if ( ! empty( $locale_parts[0] ) && ( strlen( $locale_parts[0] ) === 2 || strlen( $locale_parts[0] ) === 3 ) ) {
$language = $locale_parts[0];
}
return $language;
}
/**
* Returns the user locale for the language to be used in the admin.
*
* WordPress 4.7 introduced the ability for users to specify an Admin language
* different from the language used on the front end. This checks if the feature
* is available and returns the user's language, with a fallback to the site's language.
* Can be removed when support for WordPress 4.6 will be dropped, in favor
* of WordPress get_user_locale() that already fallbacks to the site's locale.
*
* @return string The locale.
*/
public static function get_user_locale() {
if ( function_exists( 'get_user_locale' ) ) {
return get_user_locale();
}
return get_locale();
}
/**
* Returns the full name for the sites' language.
*
* @return string The language name.
*/
public static function get_site_language_name() {
require_once ABSPATH . 'wp-admin/includes/translation-install.php';
$translations = wp_get_available_translations();
$locale = get_locale();
$language = isset( $translations[ $locale ] ) ? $translations[ $locale ]['native_name'] : 'English (US)';
return $language;
}
/**
* Returns the l10n array for the knowledge graph company info missing.
*
* @return array The l10n array.
*/
public static function get_knowledge_graph_company_info_missing_l10n() {
return [
'URL' => esc_url( WPSEO_Shortlinker::get( 'https://yoa.st/3r3' ) ),
/* translators: 1: expands to a link opening tag; 2: expands to a link closing tag */
'message' => esc_html__(
'A company name and logo need to be set for structured data to work properly. %1$sLearn more about the importance of structured data.%2$s',
'wordpress-seo'
),
];
}
}

View File

@@ -0,0 +1,245 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals\Options
*/
/**
* Site option for Multisite installs only
*
* Overloads a number of methods of the abstract class to ensure the use of the correct site_option
* WP functions.
*/
class WPSEO_Option_MS extends WPSEO_Option {
/**
* Option name.
*
* @var string
*/
public $option_name = 'wpseo_ms';
/**
* Option group name for use in settings forms.
*
* @var string
*/
public $group_name = 'yoast_wpseo_multisite_options';
/**
* Whether to include the option in the return for WPSEO_Options::get_all().
*
* @var bool
*/
public $include_in_all = false;
/**
* Whether this option is only for when the install is multisite.
*
* @var bool
*/
public $multisite_only = true;
/**
* Array of defaults for the option.
*
* Shouldn't be requested directly, use $this->get_defaults();
*
* @var array
*/
protected $defaults = [];
/**
* Available options for the 'access' setting. Used for input validation.
*
* {@internal Important: Make sure the options added to the array here are in line
* with the keys for the options set for the select box in the
* admin/pages/network.php file.}}
*
* @var array
*/
public static $allowed_access_options = [
'admin',
'superadmin',
];
/**
* Get the singleton instance of this class.
*
* @return object
*/
public static function get_instance() {
if ( ! ( self::$instance instanceof self ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Only run parent constructor in multisite context.
*/
public function __construct() {
$allow_prefix = self::ALLOW_KEY_PREFIX;
$this->defaults = [
'access' => 'admin',
'defaultblog' => '', // Numeric blog ID or empty.
"{$allow_prefix}disableadvanced_meta" => true,
"{$allow_prefix}onpage_indexability" => true,
"{$allow_prefix}content_analysis_active" => true,
"{$allow_prefix}keyword_analysis_active" => true,
"{$allow_prefix}enable_admin_bar_menu" => true,
"{$allow_prefix}enable_cornerstone_content" => true,
"{$allow_prefix}enable_xml_sitemap" => true,
"{$allow_prefix}enable_text_link_counter" => true,
];
if ( is_multisite() ) {
parent::__construct();
add_filter( 'admin_title', [ 'Yoast_Input_Validation', 'add_yoast_admin_document_title_errors' ] );
}
}
/**
* Add filters to make sure that the option default is returned if the option is not set
*
* @return void
*/
public function add_default_filters() {
// Don't change, needs to check for false as could return prio 0 which would evaluate to false.
if ( has_filter( 'default_site_option_' . $this->option_name, [ $this, 'get_defaults' ] ) === false ) {
add_filter( 'default_site_option_' . $this->option_name, [ $this, 'get_defaults' ] );
}
}
/**
* Remove the default filters.
* Called from the validate() method to prevent failure to add new options.
*
* @return void
*/
public function remove_default_filters() {
remove_filter( 'default_site_option_' . $this->option_name, [ $this, 'get_defaults' ] );
}
/**
* Add filters to make sure that the option is merged with its defaults before being returned.
*
* @return void
*/
public function add_option_filters() {
// Don't change, needs to check for false as could return prio 0 which would evaluate to false.
if ( has_filter( 'site_option_' . $this->option_name, [ $this, 'get_option' ] ) === false ) {
add_filter( 'site_option_' . $this->option_name, [ $this, 'get_option' ] );
}
}
/**
* Remove the option filters.
* Called from the clean_up methods to make sure we retrieve the original old option.
*
* @return void
*/
public function remove_option_filters() {
remove_filter( 'site_option_' . $this->option_name, [ $this, 'get_option' ] );
}
/* *********** METHODS influencing add_uption(), update_option() and saving from admin pages *********** */
/**
* Validate the option.
*
* @param array $dirty New value for the option.
* @param array $clean Clean value for the option, normally the defaults.
* @param array $old Old value of the option.
*
* @return array Validated clean value for the option to be saved to the database.
*/
protected function validate_option( $dirty, $clean, $old ) {
foreach ( $clean as $key => $value ) {
switch ( $key ) {
case 'access':
if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], self::$allowed_access_options, true ) ) {
$clean[ $key ] = $dirty[ $key ];
}
elseif ( function_exists( 'add_settings_error' ) ) {
add_settings_error(
$this->group_name, // Slug title of the setting.
$key, // Suffix-ID for the error message box.
/* translators: %1$s expands to the option name and %2$sexpands to Yoast SEO */
sprintf( __( '%1$s is not a valid choice for who should be allowed access to the %2$s settings. Value reset to the default.', 'wordpress-seo' ), esc_html( sanitize_text_field( $dirty[ $key ] ) ), 'Yoast SEO' ), // The error message.
'error' // Message type.
);
}
break;
case 'defaultblog':
if ( isset( $dirty[ $key ] ) && ( $dirty[ $key ] !== '' && $dirty[ $key ] !== '-' ) ) {
$int = WPSEO_Utils::validate_int( $dirty[ $key ] );
if ( $int !== false && $int > 0 ) {
// Check if a valid blog number has been received.
$exists = get_blog_details( $int, false );
if ( $exists && $exists->deleted === '0' ) {
$clean[ $key ] = $int;
}
elseif ( function_exists( 'add_settings_error' ) ) {
add_settings_error(
$this->group_name, // Slug title of the setting.
$key, // Suffix-ID for the error message box.
esc_html__( 'The default blog setting must be the numeric blog id of the blog you want to use as default.', 'wordpress-seo' )
. '<br>'
. sprintf(
/* translators: %s is the ID number of a blog. */
esc_html__( 'This must be an existing blog. Blog %s does not exist or has been marked as deleted.', 'wordpress-seo' ),
'<strong>' . esc_html( sanitize_text_field( $dirty[ $key ] ) ) . '</strong>'
), // The error message.
'error' // Message type.
);
}
unset( $exists );
}
elseif ( function_exists( 'add_settings_error' ) ) {
add_settings_error(
$this->group_name, // Slug title of the setting.
$key, // Suffix-ID for the error message box.
esc_html__( 'The default blog setting must be the numeric blog id of the blog you want to use as default.', 'wordpress-seo' ) . '<br>' . esc_html__( 'No numeric value was received.', 'wordpress-seo' ), // The error message.
'error' // Message type.
);
}
unset( $int );
}
break;
default:
$clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : false );
break;
}
}
return $clean;
}
/**
* Clean a given option value.
*
* @param array $option_value Old (not merged with defaults or filtered) option value to
* clean according to the rules for this option.
* @param string $current_version (optional) Version from which to upgrade, if not set,
* version specific upgrades will be disregarded.
* @param array $all_old_option_values (optional) Only used when importing old options to have
* access to the real old values, in contrast to the saved ones.
*
* @return array Cleaned option.
*/
/*
Protected function clean_option( $option_value, $current_version = null, $all_old_option_values = null ) {
return $option_value;
}
*/
}

View File

@@ -0,0 +1,274 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals\Options
*/
/**
* Option: wpseo_social.
*/
class WPSEO_Option_Social extends WPSEO_Option {
/**
* Option name.
*
* @var string
*/
public $option_name = 'wpseo_social';
/**
* Array of defaults for the option.
*
* Shouldn't be requested directly, use $this->get_defaults();
*
* @var array
*/
protected $defaults = [
// Form fields.
'facebook_site' => '', // Text field.
'instagram_url' => '',
'linkedin_url' => '',
'myspace_url' => '',
'og_default_image' => '', // Text field.
'og_default_image_id' => '',
'og_frontpage_title' => '', // Text field.
'og_frontpage_desc' => '', // Text field.
'og_frontpage_image' => '', // Text field.
'og_frontpage_image_id' => '',
'opengraph' => true,
'pinterest_url' => '',
'pinterestverify' => '',
'twitter' => true,
'twitter_site' => '', // Text field.
'twitter_card_type' => 'summary_large_image',
'youtube_url' => '',
'wikipedia_url' => '',
// Form field, but not always available.
'fbadminapp' => '', // Facebook app ID.
];
/**
* Array of sub-options which should not be overloaded with multi-site defaults.
*
* @var array
*/
public $ms_exclude = [
/* Privacy. */
'pinterestverify',
'fbadminapp',
];
/**
* Array of allowed twitter card types.
*
* While we only have the options summary and summary_large_image in the
* interface now, we might change that at some point.
*
* {@internal Uncomment any of these to allow them in validation *and* automatically
* add them as a choice in the options page.}}
*
* @var array
*/
public static $twitter_card_types = [
'summary' => '',
'summary_large_image' => '',
// 'photo' => '',
// 'gallery' => '',
// 'app' => '',
// 'player' => '',
// 'product' => '',
];
/**
* Add the actions and filters for the option.
*/
protected function __construct() {
parent::__construct();
add_filter( 'admin_title', [ 'Yoast_Input_Validation', 'add_yoast_admin_document_title_errors' ] );
}
/**
* Get the singleton instance of this class.
*
* @return object
*/
public static function get_instance() {
if ( ! ( self::$instance instanceof self ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Translate/set strings used in the option defaults.
*
* @return void
*/
public function translate_defaults() {
self::$twitter_card_types['summary'] = __( 'Summary', 'wordpress-seo' );
self::$twitter_card_types['summary_large_image'] = __( 'Summary with large image', 'wordpress-seo' );
}
/**
* Validate the option.
*
* @param array $dirty New value for the option.
* @param array $clean Clean value for the option, normally the defaults.
* @param array $old Old value of the option.
*
* @return array Validated clean value for the option to be saved to the database.
*/
protected function validate_option( $dirty, $clean, $old ) {
foreach ( $clean as $key => $value ) {
switch ( $key ) {
/* Text fields. */
case 'og_frontpage_desc':
case 'og_frontpage_title':
if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
$clean[ $key ] = WPSEO_Utils::sanitize_text_field( $dirty[ $key ] );
}
break;
case 'og_default_image_id':
case 'og_frontpage_image_id':
if ( isset( $dirty[ $key ] ) ) {
$clean[ $key ] = (int) $dirty[ $key ];
if ( $dirty[ $key ] === '' ) {
$clean[ $key ] = $dirty[ $key ];
}
}
break;
/* URL text fields - no ftp allowed. */
case 'facebook_site':
case 'instagram_url':
case 'linkedin_url':
case 'myspace_url':
case 'pinterest_url':
case 'og_default_image':
case 'og_frontpage_image':
case 'youtube_url':
case 'wikipedia_url':
$this->validate_url( $key, $dirty, $old, $clean );
break;
case 'pinterestverify':
$this->validate_verification_string( $key, $dirty, $old, $clean );
break;
/* Twitter user name. */
case 'twitter_site':
if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
$twitter_id = sanitize_text_field( ltrim( $dirty[ $key ], '@' ) );
/*
* From the Twitter documentation about twitter screen names:
* Typically a maximum of 15 characters long, but some historical accounts
* may exist with longer names.
* A username can only contain alphanumeric characters (letters A-Z, numbers 0-9)
* with the exception of underscores.
*
* @link https://support.twitter.com/articles/101299-why-can-t-i-register-certain-usernames
* @link https://dev.twitter.com/docs/platform-objects/users
*/
if ( preg_match( '`^[A-Za-z0-9_]{1,25}$`', $twitter_id ) ) {
$clean[ $key ] = $twitter_id;
}
elseif ( preg_match( '`^http(?:s)?://(?:www\.)?twitter\.com/(?P<handle>[A-Za-z0-9_]{1,25})/?$`', $twitter_id, $matches ) ) {
$clean[ $key ] = $matches['handle'];
}
else {
if ( isset( $old[ $key ] ) && $old[ $key ] !== '' ) {
$twitter_id = sanitize_text_field( ltrim( $old[ $key ], '@' ) );
if ( preg_match( '`^[A-Za-z0-9_]{1,25}$`', $twitter_id ) ) {
$clean[ $key ] = $twitter_id;
}
}
if ( function_exists( 'add_settings_error' ) ) {
add_settings_error(
$this->group_name, // Slug title of the setting.
$key, // Suffix-ID for the error message box.
sprintf(
/* translators: %s expands to a twitter user name. */
__( '%s does not seem to be a valid Twitter Username. Please correct.', 'wordpress-seo' ),
'<strong>' . esc_html( sanitize_text_field( $dirty[ $key ] ) ) . '</strong>'
), // The error message.
'error' // Message type.
);
}
}
unset( $twitter_id );
Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $dirty[ $key ] );
}
break;
case 'twitter_card_type':
if ( isset( $dirty[ $key ], self::$twitter_card_types[ $dirty[ $key ] ] ) && $dirty[ $key ] !== '' ) {
$clean[ $key ] = $dirty[ $key ];
}
break;
/* Boolean fields. */
case 'opengraph':
case 'twitter':
$clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : false );
break;
case 'fbadminapp':
$this->validate_facebook_app_id( $key, $dirty, $old, $clean );
break;
}
}
return $clean;
}
/**
* Clean a given option value.
*
* @param array $option_value Old (not merged with defaults or filtered) option value to
* clean according to the rules for this option.
* @param string $current_version Optional. Version from which to upgrade, if not set,
* version specific upgrades will be disregarded.
* @param array $all_old_option_values Optional. Only used when importing old options to have
* access to the real old values, in contrast to the saved ones.
*
* @return array Cleaned option.
*/
protected function clean_option( $option_value, $current_version = null, $all_old_option_values = null ) {
/* Move options from very old option to this one. */
$old_option = null;
if ( isset( $all_old_option_values ) ) {
// Ok, we have an import.
if ( isset( $all_old_option_values['wpseo_indexation'] ) && is_array( $all_old_option_values['wpseo_indexation'] ) && $all_old_option_values['wpseo_indexation'] !== [] ) {
$old_option = $all_old_option_values['wpseo_indexation'];
}
}
else {
$old_option = get_option( 'wpseo_indexation' );
}
if ( is_array( $old_option ) && $old_option !== [] ) {
$move = [
'opengraph',
];
foreach ( $move as $key ) {
if ( isset( $old_option[ $key ] ) && ! isset( $option_value[ $key ] ) ) {
$option_value[ $key ] = $old_option[ $key ];
}
}
unset( $move, $key );
}
unset( $old_option );
return $option_value;
}
}

View File

@@ -0,0 +1,905 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals\Options
*/
/**
* Option: wpseo_titles.
*/
class WPSEO_Option_Titles extends WPSEO_Option {
/**
* Option name.
*
* @var string
*/
public $option_name = 'wpseo_titles';
/**
* Array of defaults for the option.
*
* Shouldn't be requested directly, use $this->get_defaults();
*
* {@internal Note: Some of the default values are added via the translate_defaults() method.}}
*
* @var array
*/
protected $defaults = [
// Non-form fields, set via (ajax) function.
'title_test' => 0,
// Form fields.
'forcerewritetitle' => false,
'separator' => 'sc-dash',
'title-home-wpseo' => '%%sitename%% %%page%% %%sep%% %%sitedesc%%', // Text field.
'title-author-wpseo' => '', // Text field.
'title-archive-wpseo' => '%%date%% %%page%% %%sep%% %%sitename%%', // Text field.
'title-search-wpseo' => '', // Text field.
'title-404-wpseo' => '', // Text field.
'metadesc-home-wpseo' => '', // Text area.
'metadesc-author-wpseo' => '', // Text area.
'metadesc-archive-wpseo' => '', // Text area.
'rssbefore' => '', // Text area.
'rssafter' => '', // Text area.
'noindex-author-wpseo' => false,
'noindex-author-noposts-wpseo' => true,
'noindex-archive-wpseo' => true,
'disable-author' => false,
'disable-date' => false,
'disable-post_format' => false,
'disable-attachment' => true,
'is-media-purge-relevant' => false,
'breadcrumbs-404crumb' => '', // Text field.
'breadcrumbs-display-blog-page' => true,
'breadcrumbs-boldlast' => false,
'breadcrumbs-archiveprefix' => '', // Text field.
'breadcrumbs-enable' => false,
'breadcrumbs-home' => '', // Text field.
'breadcrumbs-prefix' => '', // Text field.
'breadcrumbs-searchprefix' => '', // Text field.
'breadcrumbs-sep' => '&raquo;', // Text field.
'website_name' => '',
'person_name' => '',
'person_logo' => '',
'person_logo_id' => 0,
'alternate_website_name' => '',
'company_logo' => '',
'company_logo_id' => 0,
'company_name' => '',
'company_or_person' => 'company',
'company_or_person_user_id' => false,
'stripcategorybase' => false,
/**
* Uses enrich_defaults to add more along the lines of:
* - 'title-' . $pt->name => ''; // Text field.
* - 'metadesc-' . $pt->name => ''; // Text field.
* - 'noindex-' . $pt->name => false;
* - 'showdate-' . $pt->name => false;
* - 'display-metabox-pt-' . $pt->name => false;
*
* - 'title-ptarchive-' . $pt->name => ''; // Text field.
* - 'metadesc-ptarchive-' . $pt->name => ''; // Text field.
* - 'bctitle-ptarchive-' . $pt->name => ''; // Text field.
* - 'noindex-ptarchive-' . $pt->name => false;
*
* - 'title-tax-' . $tax->name => '''; // Text field.
* - 'metadesc-tax-' . $tax->name => ''; // Text field.
* - 'noindex-tax-' . $tax->name => false;
* - 'display-metabox-tax-' . $tax->name => false;
*/
];
/**
* Used for "caching" during pageload.
*
* @var array
*/
protected $enriched_defaults = null;
/**
* Array of variable option name patterns for the option.
*
* @var array
*/
protected $variable_array_key_patterns = [
'title-',
'metadesc-',
'noindex-',
'showdate-',
'display-metabox-pt-',
'bctitle-ptarchive-',
'post_types-',
'taxonomy-',
];
/**
* Array of sub-options which should not be overloaded with multi-site defaults.
*
* @var array
*/
public $ms_exclude = [
/* Theme dependent. */
'title_test',
'forcerewritetitle',
];
/**
* Add the actions and filters for the option.
*
* @todo [JRF => testers] Check if the extra actions below would run into problems if an option
* is updated early on and if so, change the call to schedule these for a later action on add/update
* instead of running them straight away.
*/
protected function __construct() {
parent::__construct();
add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Utils', 'clear_cache' ] );
add_action( 'init', [ $this, 'end_of_init' ], 999 );
add_action( 'registered_post_type', [ $this, 'invalidate_enrich_defaults_cache' ] );
add_action( 'unregistered_post_type', [ $this, 'invalidate_enrich_defaults_cache' ] );
add_action( 'registered_taxonomy', [ $this, 'invalidate_enrich_defaults_cache' ] );
add_action( 'unregistered_taxonomy', [ $this, 'invalidate_enrich_defaults_cache' ] );
add_filter( 'admin_title', [ 'Yoast_Input_Validation', 'add_yoast_admin_document_title_errors' ] );
}
/**
* Make sure we can recognize the right action for the double cleaning.
*/
public function end_of_init() {
do_action( 'wpseo_double_clean_titles' );
}
/**
* Get the singleton instance of this class.
*
* @return self
*/
public static function get_instance() {
if ( ! ( self::$instance instanceof self ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Get the available separator options.
*
* @return array
*/
public function get_separator_options() {
$separators = wp_list_pluck( self::get_separator_option_list(), 'option' );
/**
* Allow altering the array with separator options.
*
* @api array $separator_options Array with the separator options.
*/
$filtered_separators = apply_filters( 'wpseo_separator_options', $separators );
if ( is_array( $filtered_separators ) && $filtered_separators !== [] ) {
$separators = array_merge( $separators, $filtered_separators );
}
return $separators;
}
/**
* Get the available separator options aria-labels.
*
* @return array Array with the separator options aria-labels.
*/
public function get_separator_options_for_display() {
$separators = $this->get_separator_options();
$separator_list = self::get_separator_option_list();
$separator_options = [];
foreach ( $separators as $key => $label ) {
$aria_label = isset( $separator_list[ $key ]['label'] ) ? $separator_list[ $key ]['label'] : '';
$separator_options[ $key ] = [
'label' => $label,
'aria_label' => $aria_label,
];
}
return $separator_options;
}
/**
* Translate strings used in the option defaults.
*
* @return void
*/
public function translate_defaults() {
/* translators: 1: Author name; 2: Site name. */
$this->defaults['title-author-wpseo'] = sprintf( __( '%1$s, Author at %2$s', 'wordpress-seo' ), '%%name%%', '%%sitename%%' ) . ' %%page%% ';
/* translators: %s expands to the search phrase. */
$this->defaults['title-search-wpseo'] = sprintf( __( 'You searched for %s', 'wordpress-seo' ), '%%searchphrase%%' ) . ' %%page%% %%sep%% %%sitename%%';
$this->defaults['title-404-wpseo'] = __( 'Page not found', 'wordpress-seo' ) . ' %%sep%% %%sitename%%';
/* translators: 1: link to post; 2: link to blog. */
$this->defaults['rssafter'] = sprintf( __( 'The post %1$s appeared first on %2$s.', 'wordpress-seo' ), '%%POSTLINK%%', '%%BLOGLINK%%' );
$this->defaults['breadcrumbs-404crumb'] = __( 'Error 404: Page not found', 'wordpress-seo' );
$this->defaults['breadcrumbs-archiveprefix'] = __( 'Archives for', 'wordpress-seo' );
$this->defaults['breadcrumbs-home'] = __( 'Home', 'wordpress-seo' );
$this->defaults['breadcrumbs-searchprefix'] = __( 'You searched for', 'wordpress-seo' );
}
/**
* Add dynamically created default options based on available post types and taxonomies.
*
* @return void
*/
public function enrich_defaults() {
$enriched_defaults = $this->enriched_defaults;
if ( $enriched_defaults !== null ) {
$this->defaults += $enriched_defaults;
return;
}
$enriched_defaults = [];
/*
* Retrieve all the relevant post type and taxonomy arrays.
*
* WPSEO_Post_Type::get_accessible_post_types() should *not* be used here.
* These are the defaults and can be prepared for any public post type.
*/
$post_type_objects = get_post_types( [ 'public' => true ], 'objects' );
if ( $post_type_objects ) {
/* translators: %s expands to the name of a post type (plural). */
$archive = sprintf( __( '%s Archive', 'wordpress-seo' ), '%%pt_plural%%' );
foreach ( $post_type_objects as $pt ) {
$enriched_defaults[ 'title-' . $pt->name ] = '%%title%% %%page%% %%sep%% %%sitename%%'; // Text field.
$enriched_defaults[ 'metadesc-' . $pt->name ] = ''; // Text area.
$enriched_defaults[ 'noindex-' . $pt->name ] = false;
$enriched_defaults[ 'showdate-' . $pt->name ] = false;
$enriched_defaults[ 'display-metabox-pt-' . $pt->name ] = true;
$enriched_defaults[ 'post_types-' . $pt->name . '-maintax' ] = 0; // Select box.
if ( ! $pt->_builtin && WPSEO_Post_Type::has_archive( $pt ) ) {
$enriched_defaults[ 'title-ptarchive-' . $pt->name ] = $archive . ' %%page%% %%sep%% %%sitename%%'; // Text field.
$enriched_defaults[ 'metadesc-ptarchive-' . $pt->name ] = ''; // Text area.
$enriched_defaults[ 'bctitle-ptarchive-' . $pt->name ] = ''; // Text field.
$enriched_defaults[ 'noindex-ptarchive-' . $pt->name ] = false;
}
}
}
$taxonomy_objects = get_taxonomies( [ 'public' => true ], 'object' );
if ( $taxonomy_objects ) {
/* translators: %s expands to the variable used for term title. */
$archives = sprintf( __( '%s Archives', 'wordpress-seo' ), '%%term_title%%' );
foreach ( $taxonomy_objects as $tax ) {
$enriched_defaults[ 'title-tax-' . $tax->name ] = $archives . ' %%page%% %%sep%% %%sitename%%'; // Text field.
$enriched_defaults[ 'metadesc-tax-' . $tax->name ] = ''; // Text area.
$enriched_defaults[ 'display-metabox-tax-' . $tax->name ] = true;
$enriched_defaults[ 'noindex-tax-' . $tax->name ] = ( $tax->name === 'post_format' );
if ( ! $tax->_builtin ) {
$enriched_defaults[ 'taxonomy-' . $tax->name . '-ptparent' ] = 0; // Select box;.
}
}
}
$this->enriched_defaults = $enriched_defaults;
$this->defaults += $enriched_defaults;
}
/**
* Invalidates enrich_defaults() cache.
*
* Called from actions:
* - (un)registered_post_type
* - (un)registered_taxonomy
*
* @return void
*/
public function invalidate_enrich_defaults_cache() {
$this->enriched_defaults = null;
}
/**
* Validate the option.
*
* @param array $dirty New value for the option.
* @param array $clean Clean value for the option, normally the defaults.
* @param array $old Old value of the option.
*
* @return array Validated clean value for the option to be saved to the database.
*/
protected function validate_option( $dirty, $clean, $old ) {
$allowed_post_types = $this->get_allowed_post_types();
foreach ( $clean as $key => $value ) {
$switch_key = $this->get_switch_key( $key );
switch ( $switch_key ) {
/* Breadcrumbs text fields. */
case 'breadcrumbs-404crumb':
case 'breadcrumbs-archiveprefix':
case 'breadcrumbs-home':
case 'breadcrumbs-prefix':
case 'breadcrumbs-searchprefix':
case 'breadcrumbs-sep':
if ( isset( $dirty[ $key ] ) ) {
$clean[ $key ] = wp_kses_post( $dirty[ $key ] );
}
break;
/*
* Text fields.
*/
/*
* Covers:
* 'title-home-wpseo', 'title-author-wpseo', 'title-archive-wpseo',
* 'title-search-wpseo', 'title-404-wpseo'
* 'title-' . $pt->name
* 'title-ptarchive-' . $pt->name
* 'title-tax-' . $tax->name
*/
case 'website_name':
case 'alternate_website_name':
case 'title-':
if ( isset( $dirty[ $key ] ) ) {
$clean[ $key ] = WPSEO_Utils::sanitize_text_field( $dirty[ $key ] );
}
break;
case 'company_or_person':
if ( isset( $dirty[ $key ] ) ) {
if ( in_array( $dirty[ $key ], [ 'company', 'person' ], true ) ) {
$clean[ $key ] = $dirty[ $key ];
}
else {
$defaults = $this->get_defaults();
$clean[ $key ] = $defaults['company_or_person'];
}
}
break;
case 'company_logo':
case 'person_logo':
$this->validate_url( $key, $dirty, $old, $clean );
break;
/*
* Covers:
* 'metadesc-home-wpseo', 'metadesc-author-wpseo', 'metadesc-archive-wpseo'
* 'metadesc-' . $pt->name
* 'metadesc-ptarchive-' . $pt->name
* 'metadesc-tax-' . $tax->name
* and also:
* 'bctitle-ptarchive-' . $pt->name
*/
case 'metadesc-':
case 'bctitle-ptarchive-':
case 'company_name':
case 'person_name':
if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
$clean[ $key ] = WPSEO_Utils::sanitize_text_field( $dirty[ $key ] );
}
break;
/*
* Covers: 'rssbefore', 'rssafter'
*/
case 'rssbefore':
case 'rssafter':
if ( isset( $dirty[ $key ] ) ) {
$clean[ $key ] = wp_kses_post( $dirty[ $key ] );
}
break;
/* 'post_types-' . $pt->name . '-maintax' fields. */
case 'post_types-':
$post_type = str_replace( [ 'post_types-', '-maintax' ], '', $key );
$taxonomies = get_object_taxonomies( $post_type, 'names' );
if ( isset( $dirty[ $key ] ) ) {
if ( $taxonomies !== [] && in_array( $dirty[ $key ], $taxonomies, true ) ) {
$clean[ $key ] = $dirty[ $key ];
}
elseif ( (string) $dirty[ $key ] === '0' || (string) $dirty[ $key ] === '' ) {
$clean[ $key ] = 0;
}
elseif ( sanitize_title_with_dashes( $dirty[ $key ] ) === $dirty[ $key ] ) {
// Allow taxonomies which may not be registered yet.
$clean[ $key ] = $dirty[ $key ];
}
else {
if ( isset( $old[ $key ] ) ) {
$clean[ $key ] = sanitize_title_with_dashes( $old[ $key ] );
}
/*
* @todo [JRF => whomever] Maybe change the untranslated $pt name in the
* error message to the nicely translated label ?
*/
add_settings_error(
$this->group_name, // Slug title of the setting.
$key, // Suffix-id for the error message box.
/* translators: %s expands to a post type. */
sprintf( __( 'Please select a valid taxonomy for post type "%s"', 'wordpress-seo' ), $post_type ), // The error message.
'error' // Message type.
);
}
}
elseif ( isset( $old[ $key ] ) ) {
$clean[ $key ] = sanitize_title_with_dashes( $old[ $key ] );
}
unset( $taxonomies, $post_type );
break;
/* 'taxonomy-' . $tax->name . '-ptparent' fields. */
case 'taxonomy-':
if ( isset( $dirty[ $key ] ) ) {
if ( $allowed_post_types !== [] && in_array( $dirty[ $key ], $allowed_post_types, true ) ) {
$clean[ $key ] = $dirty[ $key ];
}
elseif ( (string) $dirty[ $key ] === '0' || (string) $dirty[ $key ] === '' ) {
$clean[ $key ] = 0;
}
elseif ( sanitize_key( $dirty[ $key ] ) === $dirty[ $key ] ) {
// Allow taxonomies which may not be registered yet.
$clean[ $key ] = $dirty[ $key ];
}
else {
if ( isset( $old[ $key ] ) ) {
$clean[ $key ] = sanitize_key( $old[ $key ] );
}
/*
* @todo [JRF =? whomever] Maybe change the untranslated $tax name in the
* error message to the nicely translated label ?
*/
$tax = str_replace( [ 'taxonomy-', '-ptparent' ], '', $key );
add_settings_error(
$this->group_name, // Slug title of the setting.
'_' . $tax, // Suffix-ID for the error message box.
/* translators: %s expands to a taxonomy slug. */
sprintf( __( 'Please select a valid post type for taxonomy "%s"', 'wordpress-seo' ), $tax ), // The error message.
'error' // Message type.
);
unset( $tax );
}
}
elseif ( isset( $old[ $key ] ) ) {
$clean[ $key ] = sanitize_key( $old[ $key ] );
}
break;
case 'company_or_person_user_id':
case 'company_logo_id':
case 'person_logo_id':
case 'title_test': /* Integer field - not in form. */
if ( isset( $dirty[ $key ] ) ) {
$int = WPSEO_Utils::validate_int( $dirty[ $key ] );
if ( $int !== false && $int >= 0 ) {
$clean[ $key ] = $int;
}
}
elseif ( isset( $old[ $key ] ) ) {
$int = WPSEO_Utils::validate_int( $old[ $key ] );
if ( $int !== false && $int >= 0 ) {
$clean[ $key ] = $int;
}
}
break;
/* Separator field - Radio. */
case 'separator':
if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
// Get separator fields.
$separator_fields = $this->get_separator_options();
// Check if the given separator exists.
if ( isset( $separator_fields[ $dirty[ $key ] ] ) ) {
$clean[ $key ] = $dirty[ $key ];
}
}
break;
/*
* Boolean fields.
*/
/*
* Covers:
* 'noindex-author-wpseo', 'noindex-author-noposts-wpseo', 'noindex-archive-wpseo'
* 'noindex-' . $pt->name
* 'noindex-ptarchive-' . $pt->name
* 'noindex-tax-' . $tax->name
* 'forcerewritetitle':
* 'noodp':
* 'noydir':
* 'disable-author':
* 'disable-date':
* 'disable-post_format';
* 'noindex-'
* 'showdate-'
* 'showdate-'. $pt->name
* 'display-metabox-pt-'
* 'display-metabox-pt-'. $pt->name
* 'display-metabox-tax-'
* 'display-metabox-tax-' . $tax->name
* 'breadcrumbs-display-blog-page'
* 'breadcrumbs-boldlast'
* 'breadcrumbs-enable'
* 'stripcategorybase'
* 'is-media-purge-relevant'
*/
default:
$clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : false );
break;
}
}
return $clean;
}
/**
* Retrieve a list of the allowed post types as breadcrumb parent for a taxonomy.
* Helper method for validation.
*
* {@internal Don't make static as new types may still be registered.}}
*
* @return array
*/
protected function get_allowed_post_types() {
$allowed_post_types = [];
/*
* WPSEO_Post_Type::get_accessible_post_types() should *not* be used here.
*/
$post_types = get_post_types( [ 'public' => true ], 'objects' );
if ( get_option( 'show_on_front' ) === 'page' && get_option( 'page_for_posts' ) > 0 ) {
$allowed_post_types[] = 'post';
}
if ( is_array( $post_types ) && $post_types !== [] ) {
foreach ( $post_types as $type ) {
if ( WPSEO_Post_Type::has_archive( $type ) ) {
$allowed_post_types[] = $type->name;
}
}
}
return $allowed_post_types;
}
/**
* Clean a given option value.
*
* @param array $option_value Old (not merged with defaults or filtered) option value to
* clean according to the rules for this option.
* @param string $current_version Optional. Version from which to upgrade, if not set,
* version specific upgrades will be disregarded.
* @param array $all_old_option_values Optional. Only used when importing old options to have
* access to the real old values, in contrast to the saved ones.
*
* @return array Cleaned option.
*/
protected function clean_option( $option_value, $current_version = null, $all_old_option_values = null ) {
static $original = null;
// Double-run this function to ensure renaming of the taxonomy options will work.
if ( ! isset( $original )
&& has_action( 'wpseo_double_clean_titles', [ $this, 'clean' ] ) === false
) {
add_action( 'wpseo_double_clean_titles', [ $this, 'clean' ] );
$original = $option_value;
}
/*
* Move options from very old option to this one.
*
* {@internal Don't rename to the 'current' names straight away as that would prevent
* the rename/unset combi below from working.}}
*
* @todo [JRF] Maybe figure out a smarter way to deal with this.
*/
$old_option = null;
if ( isset( $all_old_option_values ) ) {
// Ok, we have an import.
if ( isset( $all_old_option_values['wpseo_indexation'] ) && is_array( $all_old_option_values['wpseo_indexation'] ) && $all_old_option_values['wpseo_indexation'] !== [] ) {
$old_option = $all_old_option_values['wpseo_indexation'];
}
}
else {
$old_option = get_option( 'wpseo_indexation' );
}
if ( is_array( $old_option ) && $old_option !== [] ) {
$move = [
'noindexauthor' => 'noindex-author',
'disableauthor' => 'disable-author',
'noindexdate' => 'noindex-archive',
'noindexcat' => 'noindex-category',
'noindextag' => 'noindex-post_tag',
'noindexpostformat' => 'noindex-post_format',
];
foreach ( $move as $old => $new ) {
if ( isset( $old_option[ $old ] ) && ! isset( $option_value[ $new ] ) ) {
$option_value[ $new ] = $old_option[ $old ];
}
}
unset( $move, $old, $new );
}
unset( $old_option );
// Fix wrongness created by buggy version 1.2.2.
if ( isset( $option_value['title-home'] ) && $option_value['title-home'] === '%%sitename%% - %%sitedesc%% - 12345' ) {
$option_value['title-home-wpseo'] = '%%sitename%% - %%sitedesc%%';
}
/*
* Renaming these options to avoid ever overwritting these if a (bloody stupid) user /
* programmer would use any of the following as a custom post type or custom taxonomy:
* 'home', 'author', 'archive', 'search', '404', 'subpages'.
*
* Similarly, renaming the tax options to avoid a custom post type and a taxonomy
* with the same name occupying the same option.
*/
$rename = [
'title-home' => 'title-home-wpseo',
'title-author' => 'title-author-wpseo',
'title-archive' => 'title-archive-wpseo',
'title-search' => 'title-search-wpseo',
'title-404' => 'title-404-wpseo',
'metadesc-home' => 'metadesc-home-wpseo',
'metadesc-author' => 'metadesc-author-wpseo',
'metadesc-archive' => 'metadesc-archive-wpseo',
'noindex-author' => 'noindex-author-wpseo',
'noindex-archive' => 'noindex-archive-wpseo',
];
foreach ( $rename as $old => $new ) {
if ( isset( $option_value[ $old ] ) && ! isset( $option_value[ $new ] ) ) {
$option_value[ $new ] = $option_value[ $old ];
unset( $option_value[ $old ] );
}
}
unset( $rename, $old, $new );
/*
* {@internal This clean-up action can only be done effectively once the taxonomies
* and post_types have been registered, i.e. at the end of the init action.}}
*/
if ( isset( $original ) && current_filter() === 'wpseo_double_clean_titles' || did_action( 'wpseo_double_clean_titles' ) > 0 ) {
$rename = [
'title-' => 'title-tax-',
'metadesc-' => 'metadesc-tax-',
'noindex-' => 'noindex-tax-',
'tax-hideeditbox-' => 'hideeditbox-tax-',
];
$taxonomy_names = get_taxonomies( [ 'public' => true ], 'names' );
$post_type_names = get_post_types( [ 'public' => true ], 'names' );
$defaults = $this->get_defaults();
if ( $taxonomy_names !== [] ) {
foreach ( $taxonomy_names as $tax ) {
foreach ( $rename as $old_prefix => $new_prefix ) {
if (
( isset( $original[ $old_prefix . $tax ] ) && ! isset( $original[ $new_prefix . $tax ] ) )
&& ( ! isset( $option_value[ $new_prefix . $tax ] )
|| ( isset( $option_value[ $new_prefix . $tax ] )
&& $option_value[ $new_prefix . $tax ] === $defaults[ $new_prefix . $tax ] ) )
) {
$option_value[ $new_prefix . $tax ] = $original[ $old_prefix . $tax ];
/*
* Check if there is a cpt with the same name as the tax,
* if so, we should make sure that the old setting hasn't been removed.
*/
if ( ! isset( $post_type_names[ $tax ] ) && isset( $option_value[ $old_prefix . $tax ] ) ) {
unset( $option_value[ $old_prefix . $tax ] );
}
else {
if ( isset( $post_type_names[ $tax ] ) && ! isset( $option_value[ $old_prefix . $tax ] ) ) {
$option_value[ $old_prefix . $tax ] = $original[ $old_prefix . $tax ];
}
}
if ( $old_prefix === 'tax-hideeditbox-' ) {
unset( $option_value[ $old_prefix . $tax ] );
}
}
}
}
}
unset( $rename, $taxonomy_names, $post_type_names, $defaults, $tax, $old_prefix, $new_prefix );
}
/*
* Make sure the values of the variable option key options are cleaned as they
* may be retained and would not be cleaned/validated then.
*/
if ( is_array( $option_value ) && $option_value !== [] ) {
foreach ( $option_value as $key => $value ) {
$switch_key = $this->get_switch_key( $key );
// Similar to validation routine - any changes made there should be made here too.
switch ( $switch_key ) {
/* Text fields. */
case 'title-':
case 'metadesc-':
case 'bctitle-ptarchive-':
$option_value[ $key ] = WPSEO_Utils::sanitize_text_field( $value );
break;
case 'separator':
if ( ! array_key_exists( $value, $this->get_separator_options() ) ) {
$option_value[ $key ] = false;
}
break;
/*
* Boolean fields.
*/
/*
* Covers:
* 'noindex-'
* 'showdate-'
* 'hideeditbox-'
*/
default:
$option_value[ $key ] = WPSEO_Utils::validate_bool( $value );
break;
}
}
unset( $key, $value, $switch_key );
}
return $option_value;
}
/**
* Make sure that any set option values relating to post_types and/or taxonomies are retained,
* even when that post_type or taxonomy may not yet have been registered.
*
* {@internal Overrule the abstract class version of this to make sure one extra renamed
* variable key does not get removed. IMPORTANT: keep this method in line with
* the parent on which it is based!}}
*
* @param array $dirty Original option as retrieved from the database.
* @param array $clean Filtered option where any options which shouldn't be in our option
* have already been removed and any options which weren't set
* have been set to their defaults.
*
* @return array
*/
protected function retain_variable_keys( $dirty, $clean ) {
if ( ( is_array( $this->variable_array_key_patterns ) && $this->variable_array_key_patterns !== [] ) && ( is_array( $dirty ) && $dirty !== [] ) ) {
// Add the extra pattern.
$patterns = $this->variable_array_key_patterns;
$patterns[] = 'tax-hideeditbox-';
/**
* Allow altering the array with variable array key patterns.
*
* @api array $patterns Array with the variable array key patterns.
*/
$patterns = apply_filters( 'wpseo_option_titles_variable_array_key_patterns', $patterns );
foreach ( $dirty as $key => $value ) {
// Do nothing if already in filtered option array.
if ( isset( $clean[ $key ] ) ) {
continue;
}
foreach ( $patterns as $pattern ) {
if ( strpos( $key, $pattern ) === 0 ) {
$clean[ $key ] = $value;
break;
}
}
}
}
return $clean;
}
/**
* Retrieves a list of separator options.
*
* @return array An array of the separator options.
*/
protected static function get_separator_option_list() {
$separators = [
'sc-dash' => [
'option' => '-',
'label' => __( 'Dash', 'wordpress-seo' ),
],
'sc-ndash' => [
'option' => '&ndash;',
'label' => __( 'En dash', 'wordpress-seo' ),
],
'sc-mdash' => [
'option' => '&mdash;',
'label' => __( 'Em dash', 'wordpress-seo' ),
],
'sc-colon' => [
'option' => ':',
'label' => __( 'Colon', 'wordpress-seo' ),
],
'sc-middot' => [
'option' => '&middot;',
'label' => __( 'Middle dot', 'wordpress-seo' ),
],
'sc-bull' => [
'option' => '&bull;',
'label' => __( 'Bullet', 'wordpress-seo' ),
],
'sc-star' => [
'option' => '*',
'label' => __( 'Asterisk', 'wordpress-seo' ),
],
'sc-smstar' => [
'option' => '&#8902;',
'label' => __( 'Low asterisk', 'wordpress-seo' ),
],
'sc-pipe' => [
'option' => '|',
'label' => __( 'Vertical bar', 'wordpress-seo' ),
],
'sc-tilde' => [
'option' => '~',
'label' => __( 'Small tilde', 'wordpress-seo' ),
],
'sc-laquo' => [
'option' => '&laquo;',
'label' => __( 'Left angle quotation mark', 'wordpress-seo' ),
],
'sc-raquo' => [
'option' => '&raquo;',
'label' => __( 'Right angle quotation mark', 'wordpress-seo' ),
],
'sc-lt' => [
'option' => '&lt;',
'label' => __( 'Less than sign', 'wordpress-seo' ),
],
'sc-gt' => [
'option' => '&gt;',
'label' => __( 'Greater than sign', 'wordpress-seo' ),
],
];
/**
* Allows altering the separator options array.
*
* @api array $separators Array with the separator options.
*/
$separator_list = apply_filters( 'wpseo_separator_option_list', $separators );
if ( ! is_array( $separator_list ) ) {
return $separators;
}
return $separator_list;
}
}

View File

@@ -0,0 +1,392 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals\Options
*/
/**
* Option: wpseo.
*/
class WPSEO_Option_Wpseo extends WPSEO_Option {
/**
* Option name.
*
* @var string
*/
public $option_name = 'wpseo';
/**
* Array of defaults for the option.
*
* {@internal Shouldn't be requested directly, use $this->get_defaults();}}
*
* @var array
*/
protected $defaults = [
// Non-form fields, set via (ajax) function.
'ms_defaults_set' => false,
// Non-form field, should only be set via validation routine.
'version' => '', // Leave default as empty to ensure activation/upgrade works.
// Form fields.
'disableadvanced_meta' => true,
'onpage_indexability' => true,
'baiduverify' => '', // Text field.
'googleverify' => '', // Text field.
'msverify' => '', // Text field.
'yandexverify' => '',
'site_type' => '', // List of options.
'has_multiple_authors' => '',
'environment_type' => '',
'content_analysis_active' => true,
'keyword_analysis_active' => true,
'enable_admin_bar_menu' => true,
'enable_cornerstone_content' => true,
'enable_xml_sitemap' => true,
'enable_text_link_counter' => true,
'show_onboarding_notice' => false,
'first_activated_on' => false,
'myyoast-oauth' => [
'config' => [
'clientId' => null,
'secret' => null,
],
'access_tokens' => [],
],
];
/**
* Sub-options which should not be overloaded with multi-site defaults.
*
* @var array
*/
public $ms_exclude = [
/* Privacy. */
'baiduverify',
'googleverify',
'msverify',
'yandexverify',
];
/**
* Possible values for the site_type option.
*
* @var array
*/
protected $site_types = [
'',
'blog',
'shop',
'news',
'smallBusiness',
'corporateOther',
'personalOther',
];
/**
* Possible environment types.
*
* @var array
*/
protected $environment_types = [
'',
'production',
'staging',
'development',
];
/**
* Possible has_multiple_authors options.
*
* @var array
*/
protected $has_multiple_authors_options = [
'',
true,
false,
];
/**
* Name for an option higher in the hierarchy to override setting access.
*
* @var string
*/
protected $override_option_name = 'wpseo_ms';
/**
* Add the actions and filters for the option.
*
* @todo [JRF => testers] Check if the extra actions below would run into problems if an option
* is updated early on and if so, change the call to schedule these for a later action on add/update
* instead of running them straight away.
*
* @return \WPSEO_Option_Wpseo
*/
protected function __construct() {
parent::__construct();
/* Clear the cache on update/add. */
add_action( 'add_option_' . $this->option_name, [ 'WPSEO_Utils', 'clear_cache' ] );
add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Utils', 'clear_cache' ] );
add_filter( 'admin_title', [ 'Yoast_Input_Validation', 'add_yoast_admin_document_title_errors' ] );
/**
* Filter the `wpseo` option defaults.
*
* @param array $defaults Array the defaults for the `wpseo` option attributes.
*/
$this->defaults = apply_filters( 'wpseo_option_wpseo_defaults', $this->defaults );
}
/**
* Get the singleton instance of this class.
*
* @return object
*/
public static function get_instance() {
if ( ! ( self::$instance instanceof self ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Add filters to make sure that the option is merged with its defaults before being returned.
*
* @return void
*/
public function add_option_filters() {
parent::add_option_filters();
list( $hookname, $callback, $priority ) = $this->get_verify_features_option_filter_hook();
if ( has_filter( $hookname, $callback ) === false ) {
add_filter( $hookname, $callback, $priority );
}
}
/**
* Remove the option filters.
* Called from the clean_up methods to make sure we retrieve the original old option.
*
* @return void
*/
public function remove_option_filters() {
parent::remove_option_filters();
list( $hookname, $callback, $priority ) = $this->get_verify_features_option_filter_hook();
remove_filter( $hookname, $callback, $priority );
}
/**
* Add filters to make sure that the option default is returned if the option is not set.
*
* @return void
*/
public function add_default_filters() {
parent::add_default_filters();
list( $hookname, $callback, $priority ) = $this->get_verify_features_default_option_filter_hook();
if ( has_filter( $hookname, $callback ) === false ) {
add_filter( $hookname, $callback, $priority );
}
}
/**
* Remove the default filters.
* Called from the validate() method to prevent failure to add new options.
*
* @return void
*/
public function remove_default_filters() {
parent::remove_default_filters();
list( $hookname, $callback, $priority ) = $this->get_verify_features_default_option_filter_hook();
remove_filter( $hookname, $callback, $priority );
}
/**
* Validate the option.
*
* @param array $dirty New value for the option.
* @param array $clean Clean value for the option, normally the defaults.
* @param array $old Old value of the option.
*
* @return array Validated clean value for the option to be saved to the database.
*/
protected function validate_option( $dirty, $clean, $old ) {
foreach ( $clean as $key => $value ) {
switch ( $key ) {
case 'version':
$clean[ $key ] = WPSEO_VERSION;
break;
/* Verification strings. */
case 'baiduverify':
case 'googleverify':
case 'msverify':
case 'yandexverify':
$this->validate_verification_string( $key, $dirty, $old, $clean );
break;
/*
* Boolean dismiss warnings - not fields - may not be in form
* (and don't need to be either as long as the default is false).
*/
case 'ms_defaults_set':
if ( isset( $dirty[ $key ] ) ) {
$clean[ $key ] = WPSEO_Utils::validate_bool( $dirty[ $key ] );
}
elseif ( isset( $old[ $key ] ) ) {
$clean[ $key ] = WPSEO_Utils::validate_bool( $old[ $key ] );
}
break;
case 'site_type':
$clean[ $key ] = $old[ $key ];
if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], $this->site_types, true ) ) {
$clean[ $key ] = $dirty[ $key ];
}
break;
case 'environment_type':
$clean[ $key ] = $old[ $key ];
if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], $this->environment_types, true ) ) {
$clean[ $key ] = $dirty[ $key ];
}
break;
case 'has_multiple_authors':
$clean[ $key ] = $old[ $key ];
if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], $this->has_multiple_authors_options, true ) ) {
$clean[ $key ] = $dirty[ $key ];
}
break;
case 'first_activated_on':
$clean[ $key ] = false;
if ( isset( $dirty[ $key ] ) ) {
if ( $dirty[ $key ] === false || WPSEO_Utils::validate_int( $dirty[ $key ] ) ) {
$clean[ $key ] = $dirty[ $key ];
}
}
break;
case 'myyoast_oauth':
$clean[ $key ] = $old[ $key ];
if ( isset( $dirty[ $key ] ) ) {
$myyoast_oauth = $dirty[ $key ];
if ( ! is_array( $myyoast_oauth ) ) {
$myyoast_oauth = json_decode( $dirty[ $key ], true );
}
if ( is_array( $myyoast_oauth ) ) {
$clean[ $key ] = $dirty[ $key ];
}
}
break;
/*
* Boolean (checkbox) fields.
*/
/*
* Covers:
* 'disableadvanced_meta'
* 'yoast_tracking'
*/
default:
$clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : false );
break;
}
}
return $clean;
}
/**
* Verifies that the feature variables are turned off if the network is configured so.
*
* @param mixed $options Value of the option to be returned. Typically an array.
*
* @return mixed Filtered $options value.
*/
public function verify_features_against_network( $options = [] ) {
if ( ! is_array( $options ) || empty( $options ) ) {
return $options;
}
// For the feature variables, set their values to off in case they are disabled.
$feature_vars = [
'disableadvanced_meta' => false,
'onpage_indexability' => false,
'content_analysis_active' => false,
'keyword_analysis_active' => false,
'enable_admin_bar_menu' => false,
'enable_cornerstone_content' => false,
'enable_xml_sitemap' => false,
'enable_text_link_counter' => false,
];
// We can reuse this logic from the base class with the above defaults to parse with the correct feature values.
$options = $this->prevent_disabled_options_update( $options, $feature_vars );
return $options;
}
/**
* Gets the filter hook name and callback for adjusting the retrieved option value
* against the network-allowed features.
*
* @return array Array where the first item is the hook name, the second is the hook callback,
* and the third is the hook priority.
*/
protected function get_verify_features_option_filter_hook() {
return [
"option_{$this->option_name}",
[ $this, 'verify_features_against_network' ],
11,
];
}
/**
* Gets the filter hook name and callback for adjusting the default option value against the network-allowed features.
*
* @return array Array where the first item is the hook name, the second is the hook callback,
* and the third is the hook priority.
*/
protected function get_verify_features_default_option_filter_hook() {
return [
"default_option_{$this->option_name}",
[ $this, 'verify_features_against_network' ],
11,
];
}
/**
* Clean a given option value.
*
* @param array $option_value Old (not merged with defaults or filtered) option value to
* clean according to the rules for this option.
* @param string $current_version Optional. Version from which to upgrade, if not set,
* version specific upgrades will be disregarded.
* @param array $all_old_option_values Optional. Only used when importing old options to have
* access to the real old values, in contrast to the saved ones.
*
* @return array Cleaned option.
*/
protected function clean_option( $option_value, $current_version = null, $all_old_option_values = null ) {
return $option_value;
}
}

View File

@@ -0,0 +1,911 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals\Options
*/
/**
* This abstract class and it's concrete classes implement defaults and value validation for
* all WPSEO options and subkeys within options.
*
* Some guidelines:
* [Retrieving options]
* - Use the normal get_option() to retrieve an option. You will receive a complete array for the option.
* Any subkeys which were not set, will have their default values in place.
* - In other words, you will normally not have to check whether a subkey isset() as they will *always* be set.
* They will also *always* be of the correct variable type.
* The only exception to this are the options with variable option names based on post_type or taxonomy
* as those will not always be available before the taxonomy/post_type is registered.
* (they will be available if a value was set, they won't be if it wasn't as the class won't know
* that a default needs to be injected).
*
* [Updating/Adding options]
* - For multisite site_options, please use the WPSEO_Options::update_site_option() method.
* - For normal options, use the normal add/update_option() functions. As long a the classes here
* are instantiated, validation for all options and their subkeys will be automatic.
* - On (succesfull) update of a couple of options, certain related actions will be run automatically.
* Some examples:
* - on change of wpseo[yoast_tracking], the cron schedule will be adjusted accordingly
* - on change of wpseo and wpseo_title, some caches will be cleared
*
* [Important information about add/updating/changing these classes]
* - Make sure that option array key names are unique across options. The WPSEO_Options::get_all()
* method merges most options together. If any of them have non-unique names, even if they
* are in a different option, they *will* overwrite each other.
* - When you add a new array key in an option: make sure you add proper defaults and add the key
* to the validation routine in the proper place or add a new validation case.
* You don't need to do any upgrading as any option returned will always be merged with the
* defaults, so new options will automatically be available.
* If the default value is a string which need translating, add this to the concrete class
* translate_defaults() method.
* - When you remove an array key from an option: if it's important that the option is really removed,
* add the WPSEO_Option::clean_up( $option_name ) method to the upgrade run.
* This will re-save the option and automatically remove the array key no longer in existance.
* - When you rename a sub-option: add it to the clean_option() routine and run that in the upgrade run.
* - When you change the default for an option sub-key, make sure you verify that the validation routine will
* still work the way it should.
* Example: changing a default from '' (empty string) to 'text' with a validation routine with tests
* for an empty string will prevent a user from saving an empty string as the real value. So the
* test for '' with the validation routine would have to be removed in that case.
* - If an option needs specific actions different from defined in this abstract class, you can just overrule
* a method by defining it in the concrete class.
*
* @todo [JRF => testers] Double check that validation will not cause errors when called
* from upgrade routine (some of the WP functions may not yet be available).
*/
abstract class WPSEO_Option {
/**
* Prefix for override option keys that allow or disallow the option key of the same name.
*
* @var string
*/
const ALLOW_KEY_PREFIX = 'allow_';
/**
* Option name - MUST be set in concrete class and set to public.
*
* @var string
*/
protected $option_name;
/**
* Option group name for use in settings forms.
*
* Will be set automagically if not set in concrete class (i.e.
* if it confirm to the normal pattern 'yoast' . $option_name . 'options',
* only set in conrete class if it doesn't).
*
* @var string
*/
public $group_name;
/**
* Whether to include the option in the return for WPSEO_Options::get_all().
*
* Also determines which options are copied over for ms_(re)set_blog().
*
* @var bool
*/
public $include_in_all = true;
/**
* Whether this option is only for when the install is multisite.
*
* @var bool
*/
public $multisite_only = false;
/**
* Array of defaults for the option - MUST be set in concrete class.
*
* Shouldn't be requested directly, use $this->get_defaults();
*
* @var array
*/
protected $defaults;
/**
* Array of variable option name patterns for the option - if any -.
*
* Set this when the option contains array keys which vary based on post_type
* or taxonomy.
*
* @var array
*/
protected $variable_array_key_patterns;
/**
* Array of sub-options which should not be overloaded with multi-site defaults.
*
* @var array
*/
public $ms_exclude = [];
/**
* Name for an option higher in the hierarchy to override setting access.
*
* @var string
*/
protected $override_option_name;
/**
* Instance of this class.
*
* @var object
*/
protected static $instance;
/* *********** INSTANTIATION METHODS *********** */
/**
* Add all the actions and filters for the option.
*
* @return \WPSEO_Option
*/
protected function __construct() {
/* Add filters which get applied to the get_options() results. */
$this->add_default_filters(); // Return defaults if option not set.
$this->add_option_filters(); // Merge with defaults if option *is* set.
if ( $this->multisite_only !== true ) {
/**
* The option validation routines remove the default filters to prevent failing
* to insert an option if it's new. Let's add them back afterwards.
*/
add_action( 'add_option', [ $this, 'add_default_filters' ] ); // Adding back after INSERT.
add_action( 'update_option', [ $this, 'add_default_filters' ] );
// Refills the cache when the option has been updated.
add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Options', 'clear_cache' ], 10 );
}
elseif ( is_multisite() ) {
/*
* The option validation routines remove the default filters to prevent failing
* to insert an option if it's new. Let's add them back afterwards.
*
* For site_options, this method is not foolproof as these actions are not fired
* on an insert/update failure. Please use the WPSEO_Options::update_site_option() method
* for updating site options to make sure the filters are in place.
*/
add_action( 'add_site_option_' . $this->option_name, [ $this, 'add_default_filters' ] );
add_action( 'update_site_option_' . $this->option_name, [ $this, 'add_default_filters' ] );
// Refills the cache when the option has been updated.
add_action( 'update_site_option_' . $this->option_name, [ 'WPSEO_Options', 'clear_cache' ], 1, 0 );
}
/*
* Make sure the option will always get validated, independently of register_setting()
* (only available on back-end).
*/
add_filter( 'sanitize_option_' . $this->option_name, [ $this, 'validate' ] );
// Flushes the rewrite rules when option is updated.
add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Utils', 'clear_rewrites' ] );
/* Register our option for the admin pages */
add_action( 'admin_init', [ $this, 'register_setting' ] );
/* Set option group name if not given */
if ( ! isset( $this->group_name ) || $this->group_name === '' ) {
$this->group_name = 'yoast_' . $this->option_name . '_options';
}
/* Translate some defaults as early as possible - textdomain is loaded in init on priority 1. */
if ( method_exists( $this, 'translate_defaults' ) ) {
add_action( 'init', [ $this, 'translate_defaults' ], 2 );
}
/**
* Enrich defaults once custom post types and taxonomies have been registered
* which is normally done on the init action.
*
* @todo [JRF/testers] Verify that none of the options which are only available after
* enrichment are used before the enriching.
*/
if ( method_exists( $this, 'enrich_defaults' ) ) {
add_action( 'init', [ $this, 'enrich_defaults' ], 99 );
}
}
// @codingStandardsIgnoreStart
/**
* All concrete classes *must* contain the get_instance method.
*
* {@internal Unfortunately I can't define it as an abstract as it also *has* to be static...}}
*/
// abstract protected static function get_instance();
/**
* Concrete classes *may* contain a translate_defaults method.
*/
// abstract public function translate_defaults();
/**
* Concrete classes *may* contain a enrich_defaults method to add additional defaults once
* all post_types and taxonomies have been registered.
*/
// abstract public function enrich_defaults();
/* *********** METHODS INFLUENCING get_option() *********** */
/**
* Add filters to make sure that the option default is returned if the option is not set.
*
* @return void
*/
public function add_default_filters() {
// Don't change, needs to check for false as could return prio 0 which would evaluate to false.
if ( has_filter( 'default_option_' . $this->option_name, [ $this, 'get_defaults' ] ) === false ) {
add_filter( 'default_option_' . $this->option_name, [ $this, 'get_defaults' ] );
}
}
// @codingStandardsIgnoreStart
/**
* Validate webmaster tools & Pinterest verification strings.
*
* @param string $key Key to check, by type of service.
* @param array $dirty Dirty data with the new values.
* @param array $old Old data.
* @param array $clean Clean data by reference, normally the default values.
*/
public function validate_verification_string( $key, $dirty, $old, &$clean ) {
if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
$meta = $dirty[ $key ];
if ( strpos( $meta, 'content=' ) ) {
// Make sure we only have the real key, not a complete meta tag.
preg_match( '`content=([\'"])?([^\'"> ]+)(?:\1|[ />])`', $meta, $match );
if ( isset( $match[2] ) ) {
$meta = $match[2];
}
unset( $match );
}
$meta = sanitize_text_field( $meta );
if ( $meta !== '' ) {
$regex = '`^[A-Fa-f0-9_-]+$`';
$service = '';
switch ( $key ) {
case 'baiduverify':
$regex = '`^[A-Za-z0-9_-]+$`';
$service = 'Baidu Webmaster tools';
break;
case 'googleverify':
$regex = '`^[A-Za-z0-9_-]+$`';
$service = 'Google Webmaster tools';
break;
case 'msverify':
$service = 'Bing Webmaster tools';
break;
case 'pinterestverify':
$service = 'Pinterest';
break;
case 'yandexverify':
$service = 'Yandex Webmaster tools';
break;
}
if ( preg_match( $regex, $meta ) ) {
$clean[ $key ] = $meta;
}
else {
// Restore the previous value, if any.
if ( isset( $old[ $key ] ) && preg_match( $regex, $old[ $key ] ) ) {
$clean[ $key ] = $old[ $key ];
}
if ( function_exists( 'add_settings_error' ) ) {
add_settings_error(
$this->group_name, // Slug title of the setting.
$key, // Suffix-ID for the error message box. WordPress prepends `setting-error-`.
/* translators: 1: Verification string from user input; 2: Service name. */
sprintf( __( '%1$s does not seem to be a valid %2$s verification string. Please correct.', 'wordpress-seo' ), '<strong>' . esc_html( $meta ) . '</strong>', $service ), // The error message.
'error' // CSS class for the WP notice, either the legacy 'error' / 'updated' or the new `notice-*` ones.
);
}
Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $meta );
}
}
}
}
/**
* Validates an option as a valid URL. Prints out a WordPress settings error
* notice if the URL is invalid.
*
* @param string $key Key to check, by type of URL setting.
* @param array $dirty Dirty data with the new values.
* @param array $old Old data.
* @param array $clean Clean data by reference, normally the default values.
*/
public function validate_url( $key, $dirty, $old, &$clean ) {
if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
$submitted_url = trim( htmlspecialchars( $dirty[ $key ], ENT_COMPAT, get_bloginfo( 'charset' ), true ) );
$validated_url = filter_var( WPSEO_Utils::sanitize_url( $submitted_url ), FILTER_VALIDATE_URL );
if ( $validated_url === false ) {
if ( function_exists( 'add_settings_error' ) ) {
add_settings_error(
// Slug title of the setting.
$this->group_name,
// Suffix-ID for the error message box. WordPress prepends `setting-error-`.
$key,
// The error message.
sprintf(
/* translators: %s expands to an invalid URL. */
__( '%s does not seem to be a valid url. Please correct.', 'wordpress-seo' ),
'<strong>' . esc_html( $submitted_url ) . '</strong>'
),
// Message type.
'error'
);
}
// Restore the previous URL value, if any.
if ( isset( $old[ $key ] ) && $old[ $key ] !== '' ) {
$url = WPSEO_Utils::sanitize_url( $old[ $key ] );
if ( $url !== '' ) {
$clean[ $key ] = $url;
}
}
Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $submitted_url );
return;
}
// The URL format is valid, let's sanitize it.
$url = WPSEO_Utils::sanitize_url( $validated_url );
if ( $url !== '' ) {
$clean[ $key ] = $url;
}
}
}
/**
* Validates a Facebook App ID.
*
* @param string $key Key to check, in this case: the Facebook App ID field name.
* @param array $dirty Dirty data with the new values.
* @param array $old Old data.
* @param array $clean Clean data by reference, normally the default values.
*/
public function validate_facebook_app_id( $key, $dirty, $old, &$clean ) {
if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
$url = 'https://graph.facebook.com/' . $dirty[ $key ];
$response = wp_remote_get( $url );
// These filters are used in the tests.
/**
* Filter: 'validate_facebook_app_id_api_response_code' - Allows to filter the Faceboook API response code.
*
* @api int $response_code The Facebook API response header code.
*/
$response_code = apply_filters( 'validate_facebook_app_id_api_response_code', wp_remote_retrieve_response_code( $response ) );
/**
* Filter: 'validate_facebook_app_id_api_response_body' - Allows to filter the Faceboook API response body.
*
* @api string $response_body The Facebook API JSON response body.
*/
$response_body = apply_filters( 'validate_facebook_app_id_api_response_body', wp_remote_retrieve_body( $response ) );
$response_object = json_decode( $response_body );
/*
* When the request is successful the response code will be 200 and
* the response object will contain an `id` property.
*/
if ( $response_code === 200 && isset( $response_object->id ) ) {
$clean[ $key ] = $dirty[ $key ];
return;
}
// Restore the previous value, if any.
if ( isset( $old[ $key ] ) && $old[ $key ] !== '' ) {
$clean[ $key ] = $old[ $key ];
}
if ( function_exists( 'add_settings_error' ) ) {
add_settings_error(
$this->group_name, // Slug title of the setting.
$key, // Suffix-ID for the error message box. WordPress prepends `setting-error-`.
sprintf(
/* translators: %s expands to an invalid Facebook App ID. */
__( '%s does not seem to be a valid Facebook App ID. Please correct.', 'wordpress-seo' ),
'<strong>' . esc_html( $dirty[ $key ] ) . '</strong>'
), // The error message.
'error' // CSS class for the WP notice, either the legacy 'error' / 'updated' or the new `notice-*` ones.
);
}
Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $dirty[ $key ] );
}
}
/**
* Remove the default filters.
* Called from the validate() method to prevent failure to add new options.
*
* @return void
*/
public function remove_default_filters() {
remove_filter( 'default_option_' . $this->option_name, [ $this, 'get_defaults' ] );
}
/**
* Get the enriched default value for an option.
*
* Checks if the concrete class contains an enrich_defaults() method and if so, runs it.
*
* {@internal The enrich_defaults method is used to set defaults for variable array keys
* in an option, such as array keys depending on post_types and/or taxonomies.}}
*
* @return array
*/
public function get_defaults() {
if ( method_exists( $this, 'translate_defaults' ) ) {
$this->translate_defaults();
}
if ( method_exists( $this, 'enrich_defaults' ) ) {
$this->enrich_defaults();
}
return apply_filters( 'wpseo_defaults', $this->defaults, $this->option_name );
}
/**
* Add filters to make sure that the option is merged with its defaults before being returned.
*
* @return void
*/
public function add_option_filters() {
// Don't change, needs to check for false as could return prio 0 which would evaluate to false.
if ( has_filter( 'option_' . $this->option_name, [ $this, 'get_option' ] ) === false ) {
add_filter( 'option_' . $this->option_name, [ $this, 'get_option' ] );
}
}
/**
* Remove the option filters.
* Called from the clean_up methods to make sure we retrieve the original old option.
*
* @return void
*/
public function remove_option_filters() {
remove_filter( 'option_' . $this->option_name, [ $this, 'get_option' ] );
}
/**
* Merge an option with its default values.
*
* This method should *not* be called directly!!! It is only meant to filter the get_option() results.
*
* @param mixed $options Option value.
*
* @return mixed Option merged with the defaults for that option.
*/
public function get_option( $options = null ) {
$filtered = $this->array_filter_merge( $options );
/*
* If the option contains variable option keys, make sure we don't remove those settings
* - even if the defaults are not complete yet.
* Unfortunately this means we also won't be removing the settings for post types or taxonomies
* which are no longer in the WP install, but rather that than the other way around.
*/
if ( isset( $this->variable_array_key_patterns ) ) {
$filtered = $this->retain_variable_keys( $options, $filtered );
}
return $filtered;
}
/* *********** METHODS influencing add_uption(), update_option() and saving from admin pages. *********** */
/**
* Register (whitelist) the option for the configuration pages.
* The validation callback is already registered separately on the sanitize_option hook,
* so no need to double register.
*
* @return void
*/
public function register_setting() {
if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ) ) {
return;
}
if ( $this->multisite_only === true ) {
$network_settings_api = Yoast_Network_Settings_API::get();
if ( $network_settings_api->meets_requirements() ) {
$network_settings_api->register_setting( $this->group_name, $this->option_name );
}
return;
}
register_setting( $this->group_name, $this->option_name );
}
/**
* Validate the option
*
* @param mixed $option_value The unvalidated new value for the option.
*
* @return array Validated new value for the option.
*/
public function validate( $option_value ) {
$clean = $this->get_defaults();
/* Return the defaults if the new value is empty. */
if ( ! is_array( $option_value ) || $option_value === [] ) {
return $clean;
}
$option_value = array_map( [ 'WPSEO_Utils', 'trim_recursive' ], $option_value );
$old = $this->get_original_option();
if ( ! is_array( $old ) ) {
$old = [];
}
$old = array_merge( $clean, $old );
$clean = $this->validate_option( $option_value, $clean, $old );
// Prevent updates to variables that are disabled via the override option.
$clean = $this->prevent_disabled_options_update( $clean, $old );
/* Retain the values for variable array keys even when the post type/taxonomy is not yet registered. */
if ( isset( $this->variable_array_key_patterns ) ) {
$clean = $this->retain_variable_keys( $option_value, $clean );
}
$this->remove_default_filters();
return $clean;
}
/**
* Checks whether a specific option key is disabled.
*
* This is determined by whether an override option is available with a key that equals the given key prefixed
* with 'allow_'.
*
* @param string $key Option key.
*
* @return bool True if option key is disabled, false otherwise.
*/
public function is_disabled( $key ) {
$override_option = $this->get_override_option();
if ( empty( $override_option ) ) {
return false;
}
return isset( $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) && ! $override_option[ self::ALLOW_KEY_PREFIX . $key ];
}
/**
* All concrete classes must contain a validate_option() method which validates all
* values within the option.
*
* @param array $dirty New value for the option.
* @param array $clean Clean value for the option, normally the defaults.
* @param array $old Old value of the option.
*/
abstract protected function validate_option( $dirty, $clean, $old );
/* *********** METHODS for ADDING/UPDATING/UPGRADING the option. *********** */
/**
* Retrieve the real old value (unmerged with defaults).
*
* @return array|bool The original option value (which can be false if the option doesn't exist).
*/
protected function get_original_option() {
$this->remove_default_filters();
$this->remove_option_filters();
// Get (unvalidated) array, NOT merged with defaults.
if ( $this->multisite_only !== true ) {
$option_value = get_option( $this->option_name );
}
else {
$option_value = get_site_option( $this->option_name );
}
$this->add_option_filters();
$this->add_default_filters();
return $option_value;
}
/**
* Add the option if it doesn't exist for some strange reason.
*
* @uses WPSEO_Option::get_original_option()
*
* @return void
*/
public function maybe_add_option() {
if ( $this->get_original_option() === false ) {
if ( $this->multisite_only !== true ) {
update_option( $this->option_name, $this->get_defaults() );
}
else {
$this->update_site_option( $this->get_defaults() );
}
}
}
/**
* Update a site_option.
*
* {@internal This special method is only needed for multisite options, but very needed indeed there.
* The order in which certain functions and hooks are run is different between
* get_option() and get_site_option() which means in practice that the removing
* of the default filters would be done too late and the re-adding of the default
* filters might not be done at all.
* Aka: use the WPSEO_Options::update_site_option() method (which calls this method)
* for safely adding/updating multisite options.}}
*
* @param mixed $value The new value for the option.
*
* @return bool Whether the update was succesfull.
*/
public function update_site_option( $value ) {
if ( $this->multisite_only === true && is_multisite() ) {
$this->remove_default_filters();
$result = update_site_option( $this->option_name, $value );
$this->add_default_filters();
return $result;
}
else {
return false;
}
}
/**
* Retrieve the real old value (unmerged with defaults), clean and re-save the option.
*
* @uses WPSEO_Option::get_original_option()
* @uses WPSEO_Option::import()
*
* @param string $current_version Optional. Version from which to upgrade, if not set,
* version specific upgrades will be disregarded.
*
* @return void
*/
public function clean( $current_version = null ) {
$option_value = $this->get_original_option();
$this->import( $option_value, $current_version );
}
/**
* Clean and re-save the option.
*
* @uses clean_option() method from concrete class if it exists.
*
* @todo [JRF/whomever] Figure out a way to show settings error during/after the upgrade - maybe
* something along the lines of:
* -> add them to a property in this class
* -> if that property isset at the end of the routine and add_settings_error function does not exist,
* save as transient (or update the transient if one already exists)
* -> next time an admin is in the WP back-end, show the errors and delete the transient or only delete it
* once the admin has dismissed the message (add ajax function)
* Important: all validation routines which add_settings_errors would need to be changed for this to work
*
* @param array $option_value Option value to be imported.
* @param string $current_version Optional. Version from which to upgrade, if not set,
* version specific upgrades will be disregarded.
* @param array $all_old_option_values Optional. Only used when importing old options to
* have access to the real old values, in contrast to
* the saved ones.
*
* @return void
*/
public function import( $option_value, $current_version = null, $all_old_option_values = null ) {
if ( $option_value === false ) {
$option_value = $this->get_defaults();
}
elseif ( is_array( $option_value ) && method_exists( $this, 'clean_option' ) ) {
$option_value = $this->clean_option( $option_value, $current_version, $all_old_option_values );
}
/*
* Save the cleaned value - validation will take care of cleaning out array keys which
* should no longer be there.
*/
if ( $this->multisite_only !== true ) {
update_option( $this->option_name, $option_value );
}
else {
$this->update_site_option( $this->option_name, $option_value );
}
}
/**
* Returns the variable array key patterns for an options class.
*
* @return array
*/
public function get_patterns() {
return (array) $this->variable_array_key_patterns;
}
/**
* Retrieves the option name.
*
* @return string The set option name.
*/
public function get_option_name() {
return $this->option_name;
}
/**
* Concrete classes *may* contain a clean_option method which will clean out old/renamed
* values within the option.
*/
// abstract public function clean_option( $option_value, $current_version = null, $all_old_option_values = null );
/* *********** HELPER METHODS for internal use. *********** */
/**
* Helper method - Combines a fixed array of default values with an options array
* while filtering out any keys which are not in the defaults array.
*
* @todo [JRF] - shouldn't this be a straight array merge ? at the end of the day, the validation
* removes any invalid keys on save.
*
* @param array $options Optional. Current options. If not set, the option defaults
* for the $option_key will be returned.
*
* @return array Combined and filtered options array.
*/
protected function array_filter_merge( $options = null ) {
$defaults = $this->get_defaults();
if ( ! isset( $options ) || $options === false || $options === [] ) {
return $defaults;
}
$options = (array) $options;
/*
$filtered = array();
if ( $defaults !== array() ) {
foreach ( $defaults as $key => $default_value ) {
// @todo should this walk through array subkeys ?
$filtered[ $key ] = ( isset( $options[ $key ] ) ? $options[ $key ] : $default_value );
}
}
*/
$filtered = array_merge( $defaults, $options );
return $filtered;
}
/**
* Sets updated values for variables that are disabled via the override option back to their previous values.
*
* @param array $updated Updated option value.
* @param array $old Old option value.
*
* @return array Updated option value, with all disabled variables set to their old values.
*/
protected function prevent_disabled_options_update( $updated, $old ) {
$override_option = $this->get_override_option();
if ( empty( $override_option ) ) {
return $updated;
}
/*
* This loop could as well call `is_disabled( $key )` for each iteration,
* however this would be worse performance-wise.
*/
foreach ( $old as $key => $value ) {
if ( isset( $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) && ! $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) {
$updated[ $key ] = $old[ $key ];
}
}
return $updated;
}
/**
* Retrieves the value of the override option, if available.
*
* An override option contains values that may determine access to certain sub-variables
* of this option.
*
* Only regular options in multisite can have override options, which in that case
* would be network options.
*
* @return array Override option value, or empty array if unavailable.
*/
protected function get_override_option() {
if ( empty( $this->override_option_name ) || $this->multisite_only === true || ! is_multisite() ) {
return [];
}
return get_site_option( $this->override_option_name, [] );
}
/**
* Make sure that any set option values relating to post_types and/or taxonomies are retained,
* even when that post_type or taxonomy may not yet have been registered.
*
* {@internal The wpseo_titles concrete class overrules this method. Make sure that any
* changes applied here, also get ported to that version.}}
*
* @param array $dirty Original option as retrieved from the database.
* @param array $clean Filtered option where any options which shouldn't be in our option
* have already been removed and any options which weren't set
* have been set to their defaults.
*
* @return array
*/
protected function retain_variable_keys( $dirty, $clean ) {
if ( ( is_array( $this->variable_array_key_patterns ) && $this->variable_array_key_patterns !== [] ) && ( is_array( $dirty ) && $dirty !== [] ) ) {
foreach ( $dirty as $key => $value ) {
// Do nothing if already in filtered options.
if ( isset( $clean[ $key ] ) ) {
continue;
}
foreach ( $this->variable_array_key_patterns as $pattern ) {
if ( strpos( $key, $pattern ) === 0 ) {
$clean[ $key ] = $value;
break;
}
}
}
}
return $clean;
}
/**
* Check whether a given array key conforms to one of the variable array key patterns for this option.
*
* @usedby validate_option() methods for options with variable array keys.
*
* @param string $key Array key to check.
*
* @return string Pattern if it conforms, original array key if it doesn't or if the option
* does not have variable array keys.
*/
protected function get_switch_key( $key ) {
if ( ! isset( $this->variable_array_key_patterns ) || ( ! is_array( $this->variable_array_key_patterns ) || $this->variable_array_key_patterns === [] ) ) {
return $key;
}
foreach ( $this->variable_array_key_patterns as $pattern ) {
if ( strpos( $key, $pattern ) === 0 ) {
return $pattern;
}
}
return $key;
}
}

View File

@@ -0,0 +1,609 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals\Options
*/
/**
* Overal Option Management class.
*
* Instantiates all the options and offers a number of utility methods to work with the options.
*/
class WPSEO_Options {
/**
* The option values.
*
* @var null
*/
protected static $option_values = null;
/**
* Options this class uses.
*
* @var array Array format: (string) option_name => (string) name of concrete class for the option.
*/
public static $options = [
'wpseo' => 'WPSEO_Option_Wpseo',
'wpseo_titles' => 'WPSEO_Option_Titles',
'wpseo_social' => 'WPSEO_Option_Social',
'wpseo_ms' => 'WPSEO_Option_MS',
'wpseo_taxonomy_meta' => 'WPSEO_Taxonomy_Meta',
];
/**
* Array of instantiated option objects.
*
* @var array
*/
protected static $option_instances = [];
/**
* Array with the option names.
*
* @var array
*/
protected static $option_names = [];
/**
* Instance of this class.
*
* @var object
*/
protected static $instance;
/**
* Instantiate all the WPSEO option management classes.
*/
protected function __construct() {
$this->register_hooks();
foreach ( static::$options as $option_name => $option_class ) {
static::register_option( call_user_func( [ $option_class, 'get_instance' ] ) );
}
}
/**
* Register our hooks.
*/
public function register_hooks() {
add_action( 'registered_taxonomy', [ $this, 'clear_cache' ] );
add_action( 'unregistered_taxonomy', [ $this, 'clear_cache' ] );
add_action( 'registered_post_type', [ $this, 'clear_cache' ] );
add_action( 'unregistered_post_type', [ $this, 'clear_cache' ] );
}
/**
* Get the singleton instance of this class.
*
* @return object
*/
public static function get_instance() {
if ( ! ( static::$instance instanceof self ) ) {
static::$instance = new self();
}
return static::$instance;
}
/**
* Registers an option to the options list.
*
* @param WPSEO_Option $option_instance Instance of the option.
*/
public static function register_option( WPSEO_Option $option_instance ) {
$option_name = $option_instance->get_option_name();
if ( $option_instance->multisite_only && ! static::is_multisite() ) {
unset( static::$options[ $option_name ], static::$option_names[ $option_name ] );
return;
}
$is_already_registered = array_key_exists( $option_name, static::$options );
if ( ! $is_already_registered ) {
static::$options[ $option_name ] = get_class( $option_instance );
}
if ( $option_instance->include_in_all === true ) {
static::$option_names[ $option_name ] = $option_name;
}
static::$option_instances[ $option_name ] = $option_instance;
if ( ! $is_already_registered ) {
static::clear_cache();
}
}
/**
* Get the group name of an option for use in the settings form.
*
* @param string $option_name The option for which you want to retrieve the option group name.
*
* @return string|bool
*/
public static function get_group_name( $option_name ) {
if ( isset( static::$option_instances[ $option_name ] ) ) {
return static::$option_instances[ $option_name ]->group_name;
}
return false;
}
/**
* Get a specific default value for an option.
*
* @param string $option_name The option for which you want to retrieve a default.
* @param string $key The key within the option who's default you want.
*
* @return mixed
*/
public static function get_default( $option_name, $key ) {
if ( isset( static::$option_instances[ $option_name ] ) ) {
$defaults = static::$option_instances[ $option_name ]->get_defaults();
if ( isset( $defaults[ $key ] ) ) {
return $defaults[ $key ];
}
}
return null;
}
/**
* Update a site_option.
*
* @param string $option_name The option name of the option to save.
* @param mixed $value The new value for the option.
*
* @return bool
*/
public static function update_site_option( $option_name, $value ) {
if ( is_multisite() && isset( static::$option_instances[ $option_name ] ) ) {
return static::$option_instances[ $option_name ]->update_site_option( $value );
}
return false;
}
/**
* Get the instantiated option instance.
*
* @param string $option_name The option for which you want to retrieve the instance.
*
* @return object|bool
*/
public static function get_option_instance( $option_name ) {
if ( isset( static::$option_instances[ $option_name ] ) ) {
return static::$option_instances[ $option_name ];
}
return false;
}
/**
* Retrieve an array of the options which should be included in get_all() and reset().
*
* @return array Array of option names.
*/
public static function get_option_names() {
$option_names = array_values( static::$option_names );
if ( $option_names === [] ) {
foreach ( static::$option_instances as $option_name => $option_object ) {
if ( $option_object->include_in_all === true ) {
$option_names[] = $option_name;
}
}
}
/**
* Filter: wpseo_options - Allow developers to change the option name to include.
*
* @api array The option names to include in get_all and reset().
*/
return apply_filters( 'wpseo_options', $option_names );
}
/**
* Retrieve all the options for the SEO plugin in one go.
*
* @return array Array combining the values of all the options.
*/
public static function get_all() {
static::$option_values = static::get_options( static::get_option_names() );
return static::$option_values;
}
/**
* Retrieve one or more options for the SEO plugin.
*
* @param array $option_names An array of option names of the options you want to get.
*
* @return array Array combining the values of the requested options.
*/
public static function get_options( array $option_names ) {
$options = [];
$option_names = array_filter( $option_names, 'is_string' );
foreach ( $option_names as $option_name ) {
if ( isset( static::$option_instances[ $option_name ] ) ) {
$option = static::get_option( $option_name );
$options = array_merge( $options, $option );
}
}
return $options;
}
/**
* Retrieve a single option for the SEO plugin.
*
* @param string $option_name The name of the option you want to get.
*
* @return array Array containing the requested option.
*/
public static function get_option( $option_name ) {
$option = null;
if ( is_string( $option_name ) && ! empty( $option_name ) ) {
if ( isset( static::$option_instances[ $option_name ] ) ) {
if ( static::$option_instances[ $option_name ]->multisite_only !== true ) {
$option = get_option( $option_name );
}
else {
$option = get_site_option( $option_name );
}
}
}
return $option;
}
/**
* Retrieve a single field from any option for the SEO plugin. Keys are always unique.
*
* @param string $key The key it should return.
* @param mixed $default The default value that should be returned if the key isn't set.
*
* @return mixed|null Returns value if found, $default if not.
*/
public static function get( $key, $default = null ) {
if ( static::$option_values === null ) {
static::prime_cache();
}
if ( isset( static::$option_values[ $key ] ) ) {
return static::$option_values[ $key ];
}
return $default;
}
/**
* Resets the cache to null.
*/
public static function clear_cache() {
static::$option_values = null;
}
/**
* Primes our cache.
*/
private static function prime_cache() {
static::$option_values = static::get_all();
static::$option_values = static::add_ms_option( static::$option_values );
}
/**
* Retrieve a single field from an option for the SEO plugin.
*
* @param string $key The key to set.
* @param mixed $value The value to set.
*
* @return mixed|null Returns value if found, $default if not.
*/
public static function set( $key, $value ) {
$lookup_table = static::get_lookup_table();
if ( isset( $lookup_table[ $key ] ) ) {
return static::save_option( $lookup_table[ $key ], $key, $value );
}
$patterns = static::get_pattern_table();
foreach ( $patterns as $pattern => $option ) {
if ( strpos( $key, $pattern ) === 0 ) {
return static::save_option( $option, $key, $value );
}
}
static::$option_values[ $key ] = $value;
}
/**
* Get an option only if it's been auto-loaded.
*
* @param string $option The option to retrieve.
* @param bool|mixed $default A default value to return.
*
* @return bool|mixed
*/
public static function get_autoloaded_option( $option, $default = false ) {
$value = wp_cache_get( $option, 'options' );
if ( $value === false ) {
$passed_default = func_num_args() > 1;
return apply_filters( "default_option_{$option}", $default, $option, $passed_default );
}
return apply_filters( "option_{$option}", maybe_unserialize( $value ), $option );
}
/**
* Run the clean up routine for one or all options.
*
* @param array|string $option_name Optional. the option you want to clean or an array of
* option names for the options you want to clean.
* If not set, all options will be cleaned.
* @param string $current_version Optional. Version from which to upgrade, if not set,
* version specific upgrades will be disregarded.
*
* @return void
*/
public static function clean_up( $option_name = null, $current_version = null ) {
if ( isset( $option_name ) && is_string( $option_name ) && $option_name !== '' ) {
if ( isset( static::$option_instances[ $option_name ] ) ) {
static::$option_instances[ $option_name ]->clean( $current_version );
}
}
elseif ( isset( $option_name ) && is_array( $option_name ) && $option_name !== [] ) {
foreach ( $option_name as $option ) {
if ( isset( static::$option_instances[ $option ] ) ) {
static::$option_instances[ $option ]->clean( $current_version );
}
}
unset( $option );
}
else {
foreach ( static::$option_instances as $instance ) {
$instance->clean( $current_version );
}
unset( $instance );
// If we've done a full clean-up, we can safely remove this really old option.
delete_option( 'wpseo_indexation' );
}
}
/**
* Check that all options exist in the database and add any which don't.
*
* @return void
*/
public static function ensure_options_exist() {
foreach ( static::$option_instances as $instance ) {
$instance->maybe_add_option();
}
}
/**
* Initialize some options on first install/activate/reset.
*
* @return void
*/
public static function initialize() {
/* Force WooThemes to use Yoast SEO data. */
if ( function_exists( 'woo_version_init' ) ) {
update_option( 'seo_woo_use_third_party_data', 'true' );
}
}
/**
* Reset all options to their default values and rerun some tests.
*
* @return void
*/
public static function reset() {
if ( ! is_multisite() ) {
$option_names = static::get_option_names();
if ( is_array( $option_names ) && $option_names !== [] ) {
foreach ( $option_names as $option_name ) {
delete_option( $option_name );
update_option( $option_name, get_option( $option_name ) );
}
}
unset( $option_names );
}
else {
// Reset MS blog based on network default blog setting.
static::reset_ms_blog( get_current_blog_id() );
}
static::initialize();
}
/**
* Initialize default values for a new multisite blog.
*
* @param bool $force_init Whether to always do the initialization routine (title/desc test).
*
* @return void
*/
public static function maybe_set_multisite_defaults( $force_init = false ) {
$option = get_option( 'wpseo' );
if ( is_multisite() ) {
if ( $option['ms_defaults_set'] === false ) {
static::reset_ms_blog( get_current_blog_id() );
static::initialize();
}
elseif ( $force_init === true ) {
static::initialize();
}
}
}
/**
* Reset all options for a specific multisite blog to their default values based upon a
* specified default blog if one was chosen on the network page or the plugin defaults if it was not.
*
* @param int|string $blog_id Blog id of the blog for which to reset the options.
*
* @return void
*/
public static function reset_ms_blog( $blog_id ) {
if ( is_multisite() ) {
$options = get_site_option( 'wpseo_ms' );
$option_names = static::get_option_names();
if ( is_array( $option_names ) && $option_names !== [] ) {
$base_blog_id = $blog_id;
if ( $options['defaultblog'] !== '' && $options['defaultblog'] !== 0 ) {
$base_blog_id = $options['defaultblog'];
}
foreach ( $option_names as $option_name ) {
delete_blog_option( $blog_id, $option_name );
$new_option = get_blog_option( $base_blog_id, $option_name );
/* Remove sensitive, theme dependent and site dependent info. */
if ( isset( static::$option_instances[ $option_name ] ) && static::$option_instances[ $option_name ]->ms_exclude !== [] ) {
foreach ( static::$option_instances[ $option_name ]->ms_exclude as $key ) {
unset( $new_option[ $key ] );
}
}
if ( $option_name === 'wpseo' ) {
$new_option['ms_defaults_set'] = true;
}
update_blog_option( $blog_id, $option_name, $new_option );
}
}
}
}
/**
* Saves the option to the database.
*
* @param string $wpseo_options_group_name The name for the wpseo option group in the database.
* @param string $option_name The name for the option to set.
* @param mixed $option_value The value for the option.
*
* @return boolean Returns true if the option is successfully saved in the database.
*/
public static function save_option( $wpseo_options_group_name, $option_name, $option_value ) {
$options = static::get_option( $wpseo_options_group_name );
$options[ $option_name ] = $option_value;
if ( isset( static::$option_instances[ $wpseo_options_group_name ] ) && static::$option_instances[ $wpseo_options_group_name ]->multisite_only === true ) {
static::update_site_option( $wpseo_options_group_name, $options );
}
else {
update_option( $wpseo_options_group_name, $options );
}
// Check if everything got saved properly.
$saved_option = static::get_option( $wpseo_options_group_name );
// Clear our cache.
static::clear_cache();
return $saved_option[ $option_name ] === $options[ $option_name ];
}
/**
* Adds the multisite options to the option stack if relevant.
*
* @param array $option The currently present options settings.
*
* @return array Options possibly including multisite.
*/
protected static function add_ms_option( $option ) {
if ( ! is_multisite() ) {
return $option;
}
$ms_option = static::get_option( 'wpseo_ms' );
if ( $ms_option === null ) {
return $option;
}
return array_merge( $option, $ms_option );
}
/**
* Checks if installation is multisite.
*
* @return bool True when is multisite.
*/
protected static function is_multisite() {
static $is_multisite;
if ( $is_multisite === null ) {
$is_multisite = is_multisite();
}
return $is_multisite;
}
/**
* Retrieves a lookup table to find in which option_group a key is stored.
*
* @return array The lookup table.
*/
private static function get_lookup_table() {
$lookup_table = [];
foreach ( array_keys( static::$options ) as $option_name ) {
$full_option = static::get_option( $option_name );
foreach ( $full_option as $key => $value ) {
$lookup_table[ $key ] = $option_name;
}
}
return $lookup_table;
}
/**
* Retrieves a lookup table to find in which option_group a key is stored.
*
* @return array The lookup table.
*/
private static function get_pattern_table() {
$pattern_table = [];
foreach ( static::$options as $option_name => $option_class ) {
$instance = call_user_func( [ $option_class, 'get_instance' ] );
foreach ( $instance->get_patterns() as $key ) {
$pattern_table[ $key ] = $option_name;
}
}
return $pattern_table;
}
/* ********************* DEPRECATED METHODS ********************* */
/**
* Fills our option cache.
*
* @deprecated 12.8.1
* @codeCoverageIgnore
*/
public static function fill_cache() {
_deprecated_function( __METHOD__, 'WPSEO 12.8.1', '::clear_cache' );
static::clear_cache();
}
/**
* Correct the inadvertent removal of the fallback to default values from the breadcrumbs.
*
* @since 1.5.2.3
*
* @deprecated 7.0
* @codeCoverageIgnore
*/
public static function bring_back_breadcrumb_defaults() {
_deprecated_function( __METHOD__, 'WPSEO 7.0' );
}
}

View File

@@ -0,0 +1,614 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals\Options
*/
/**
* Option: wpseo_taxonomy_meta.
*/
class WPSEO_Taxonomy_Meta extends WPSEO_Option {
/**
* Option name.
*
* @var string
*/
public $option_name = 'wpseo_taxonomy_meta';
/**
* Whether to include the option in the return for WPSEO_Options::get_all().
*
* @var bool
*/
public $include_in_all = false;
/**
* Array of defaults for the option.
*
* Shouldn't be requested directly, use $this->get_defaults();
*
* {@internal Important: in contrast to most defaults, the below array format is
* very bare. The real option is in the format [taxonomy_name][term_id][...]
* where [...] is any of the $defaults_per_term options shown below.
* This is of course taken into account in the below methods.}}
*
* @var array
*/
protected $defaults = [];
/**
* Option name - same as $option_name property, but now also available to static methods.
*
* @var string
*/
public static $name;
/**
* Array of defaults for individual taxonomy meta entries.
*
* @var array
*/
public static $defaults_per_term = [
'wpseo_title' => '',
'wpseo_desc' => '',
'wpseo_canonical' => '',
'wpseo_bctitle' => '',
'wpseo_noindex' => 'default',
'wpseo_focuskw' => '',
'wpseo_linkdex' => '',
'wpseo_content_score' => '',
'wpseo_focuskeywords' => '[]',
'wpseo_keywordsynonyms' => '[]',
// Social fields.
'wpseo_opengraph-title' => '',
'wpseo_opengraph-description' => '',
'wpseo_opengraph-image' => '',
'wpseo_opengraph-image-id' => '',
'wpseo_twitter-title' => '',
'wpseo_twitter-description' => '',
'wpseo_twitter-image' => '',
'wpseo_twitter-image-id' => '',
];
/**
* Available index options.
*
* Used for form generation and input validation.
*
* {@internal Labels (translation) added on admin_init via WPSEO_Taxonomy::translate_meta_options().}}
*
* @var array
*/
public static $no_index_options = [
'default' => '',
'index' => '',
'noindex' => '',
];
/**
* Add the actions and filters for the option.
*
* @todo [JRF => testers] Check if the extra actions below would run into problems if an option
* is updated early on and if so, change the call to schedule these for a later action on add/update
* instead of running them straight away.
*/
protected function __construct() {
parent::__construct();
self::$name = $this->option_name;
/* On succesfull update/add of the option, flush the W3TC cache. */
add_action( 'add_option_' . $this->option_name, [ 'WPSEO_Utils', 'flush_w3tc_cache' ] );
add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Utils', 'flush_w3tc_cache' ] );
}
/**
* Get the singleton instance of this class.
*
* @return object
*/
public static function get_instance() {
if ( ! ( self::$instance instanceof self ) ) {
self::$instance = new self();
self::$name = self::$instance->option_name;
}
return self::$instance;
}
/**
* Add extra default options received from a filter.
*/
public function enrich_defaults() {
$extra_defaults_per_term = apply_filters( 'wpseo_add_extra_taxmeta_term_defaults', [] );
if ( is_array( $extra_defaults_per_term ) ) {
self::$defaults_per_term = array_merge( $extra_defaults_per_term, self::$defaults_per_term );
}
}
/**
* Helper method - Combines a fixed array of default values with an options array
* while filtering out any keys which are not in the defaults array.
*
* @param string $option_key Option name of the option we're doing the merge for.
* @param array $options Optional. Current options. If not set, the option defaults
* for the $option_key will be returned.
*
* @return array Combined and filtered options array.
*/
/*
Public function array_filter_merge( $option_key, $options = null ) {
$defaults = $this->get_defaults( $option_key );
if ( ! isset( $options ) || $options === false ) {
return $defaults;
}
/ *
{@internal Adding the defaults to all taxonomy terms each time the option is retrieved
will be quite inefficient if there are a lot of taxonomy terms.
As long as taxonomy_meta is only retrieved via methods in this class, we shouldn't need this.}}
$options = (array) $options;
$filtered = array();
if ( $options !== array() ) {
foreach ( $options as $taxonomy => $terms ) {
if ( is_array( $terms ) && $terms !== array() ) {
foreach ( $terms as $id => $term_meta ) {
foreach ( self::$defaults_per_term as $name => $default ) {
if ( isset( $options[ $taxonomy ][ $id ][ $name ] ) ) {
$filtered[ $taxonomy ][ $id ][ $name ] = $options[ $taxonomy ][ $id ][ $name ];
}
else {
$filtered[ $name ] = $default;
}
}
}
}
}
unset( $taxonomy, $terms, $id, $term_meta, $name, $default );
}
// end of may be remove.
return $filtered;
* /
return (array) $options;
}
*/
/**
* Validate the option.
*
* @param array $dirty New value for the option.
* @param array $clean Clean value for the option, normally the defaults.
* @param array $old Old value of the option.
*
* @return array Validated clean value for the option to be saved to the database.
*/
protected function validate_option( $dirty, $clean, $old ) {
/*
* Prevent complete validation (which can be expensive when there are lots of terms)
* if only one item has changed and has already been validated.
*/
if ( isset( $dirty['wpseo_already_validated'] ) && $dirty['wpseo_already_validated'] === true ) {
unset( $dirty['wpseo_already_validated'] );
return $dirty;
}
foreach ( $dirty as $taxonomy => $terms ) {
/* Don't validate taxonomy - may not be registered yet and we don't want to remove valid ones. */
if ( is_array( $terms ) && $terms !== [] ) {
foreach ( $terms as $term_id => $meta_data ) {
/* Only validate term if the taxonomy exists. */
if ( taxonomy_exists( $taxonomy ) && get_term_by( 'id', $term_id, $taxonomy ) === false ) {
/* Is this term id a special case ? */
if ( has_filter( 'wpseo_tax_meta_special_term_id_validation_' . $term_id ) !== false ) {
$clean[ $taxonomy ][ $term_id ] = apply_filters( 'wpseo_tax_meta_special_term_id_validation_' . $term_id, $meta_data, $taxonomy, $term_id );
}
continue;
}
if ( is_array( $meta_data ) && $meta_data !== [] ) {
/* Validate meta data. */
$old_meta = self::get_term_meta( $term_id, $taxonomy );
$meta_data = self::validate_term_meta_data( $meta_data, $old_meta );
if ( $meta_data !== [] ) {
$clean[ $taxonomy ][ $term_id ] = $meta_data;
}
}
// Deal with special cases (for when taxonomy doesn't exist yet).
if ( ! isset( $clean[ $taxonomy ][ $term_id ] ) && has_filter( 'wpseo_tax_meta_special_term_id_validation_' . $term_id ) !== false ) {
$clean[ $taxonomy ][ $term_id ] = apply_filters( 'wpseo_tax_meta_special_term_id_validation_' . $term_id, $meta_data, $taxonomy, $term_id );
}
}
}
}
return $clean;
}
/**
* Validate the meta data for one individual term and removes default values (no need to save those).
*
* @param array $meta_data New values.
* @param array $old_meta The original values.
*
* @return array Validated and filtered value.
*/
public static function validate_term_meta_data( $meta_data, $old_meta ) {
$clean = self::$defaults_per_term;
$meta_data = array_map( [ 'WPSEO_Utils', 'trim_recursive' ], $meta_data );
if ( ! is_array( $meta_data ) || $meta_data === [] ) {
return $clean;
}
foreach ( $clean as $key => $value ) {
switch ( $key ) {
case 'wpseo_noindex':
if ( isset( $meta_data[ $key ] ) ) {
if ( isset( self::$no_index_options[ $meta_data[ $key ] ] ) ) {
$clean[ $key ] = $meta_data[ $key ];
}
}
elseif ( isset( $old_meta[ $key ] ) ) {
// Retain old value if field currently not in use.
$clean[ $key ] = $old_meta[ $key ];
}
break;
case 'wpseo_canonical':
if ( isset( $meta_data[ $key ] ) && $meta_data[ $key ] !== '' ) {
$url = WPSEO_Utils::sanitize_url( $meta_data[ $key ] );
if ( $url !== '' ) {
$clean[ $key ] = $url;
}
unset( $url );
}
break;
case 'wpseo_bctitle':
if ( isset( $meta_data[ $key ] ) ) {
$clean[ $key ] = WPSEO_Utils::sanitize_text_field( $meta_data[ $key ] );
}
elseif ( isset( $old_meta[ $key ] ) ) {
// Retain old value if field currently not in use.
$clean[ $key ] = $old_meta[ $key ];
}
break;
case 'wpseo_keywordsynonyms':
if ( isset( $meta_data[ $key ] ) && is_string( $meta_data[ $key ] ) ) {
// The data is stringified JSON. Use `json_decode` and `json_encode` around the sanitation.
$input = json_decode( $meta_data[ $key ], true );
$sanitized = array_map( [ 'WPSEO_Utils', 'sanitize_text_field' ], $input );
$clean[ $key ] = WPSEO_Utils::format_json_encode( $sanitized );
}
elseif ( isset( $old_meta[ $key ] ) ) {
// Retain old value if field currently not in use.
$clean[ $key ] = $old_meta[ $key ];
}
break;
case 'wpseo_focuskeywords':
if ( isset( $meta_data[ $key ] ) && is_string( $meta_data[ $key ] ) ) {
// The data is stringified JSON. Use `json_decode` and `json_encode` around the sanitation.
$input = json_decode( $meta_data[ $key ], true );
// This data has two known keys: `keyword` and `score`.
$sanitized = [];
foreach ( $input as $entry ) {
$sanitized[] = [
'keyword' => WPSEO_Utils::sanitize_text_field( $entry['keyword'] ),
'score' => WPSEO_Utils::sanitize_text_field( $entry['score'] ),
];
}
$clean[ $key ] = WPSEO_Utils::format_json_encode( $sanitized );
}
elseif ( isset( $old_meta[ $key ] ) ) {
// Retain old value if field currently not in use.
$clean[ $key ] = $old_meta[ $key ];
}
break;
case 'wpseo_focuskw':
case 'wpseo_title':
case 'wpseo_desc':
case 'wpseo_linkdex':
default:
if ( isset( $meta_data[ $key ] ) && is_string( $meta_data[ $key ] ) ) {
$clean[ $key ] = WPSEO_Utils::sanitize_text_field( $meta_data[ $key ] );
}
if ( $key === 'wpseo_focuskw' ) {
$search = [
'&lt;',
'&gt;',
'&#96',
'<',
'>',
'`',
];
$clean[ $key ] = str_replace( $search, '', $clean[ $key ] );
}
break;
}
$clean[ $key ] = apply_filters( 'wpseo_sanitize_tax_meta_' . $key, $clean[ $key ], ( isset( $meta_data[ $key ] ) ? $meta_data[ $key ] : null ), ( isset( $old_meta[ $key ] ) ? $old_meta[ $key ] : null ) );
}
// Only save the non-default values.
return array_diff_assoc( $clean, self::$defaults_per_term );
}
/**
* Clean a given option value.
* - Convert old option values to new
* - Fixes strings which were escaped (should have been sanitized - escaping is for output)
*
* @param array $option_value Old (not merged with defaults or filtered) option value to
* clean according to the rules for this option.
* @param string $current_version Optional. Version from which to upgrade, if not set,
* version specific upgrades will be disregarded.
* @param array $all_old_option_values Optional. Only used when importing old options to have
* access to the real old values, in contrast to the saved ones.
*
* @return array Cleaned option.
*/
protected function clean_option( $option_value, $current_version = null, $all_old_option_values = null ) {
/* Clean up old values and remove empty arrays. */
if ( is_array( $option_value ) && $option_value !== [] ) {
foreach ( $option_value as $taxonomy => $terms ) {
if ( is_array( $terms ) && $terms !== [] ) {
foreach ( $terms as $term_id => $meta_data ) {
if ( ! is_array( $meta_data ) || $meta_data === [] ) {
// Remove empty term arrays.
unset( $option_value[ $taxonomy ][ $term_id ] );
}
else {
foreach ( $meta_data as $key => $value ) {
switch ( $key ) {
case 'noindex':
if ( $value === 'on' ) {
// Convert 'on' to 'noindex'.
$option_value[ $taxonomy ][ $term_id ][ $key ] = 'noindex';
}
break;
case 'canonical':
case 'wpseo_bctitle':
case 'wpseo_title':
case 'wpseo_desc':
case 'wpseo_linkdex':
// @todo [JRF => whomever] Needs checking, I don't have example data [JRF].
if ( $value !== '' ) {
// Fix incorrectly saved (encoded) canonical urls and texts.
$option_value[ $taxonomy ][ $term_id ][ $key ] = wp_specialchars_decode( stripslashes( $value ), ENT_QUOTES );
}
break;
default:
// @todo [JRF => whomever] Needs checking, I don't have example data [JRF].
if ( $value !== '' ) {
// Fix incorrectly saved (escaped) text strings.
$option_value[ $taxonomy ][ $term_id ][ $key ] = wp_specialchars_decode( $value, ENT_QUOTES );
}
break;
}
}
}
}
}
else {
// Remove empty taxonomy arrays.
unset( $option_value[ $taxonomy ] );
}
}
}
return $option_value;
}
/**
* Retrieve a taxonomy term's meta value(s).
*
* @param mixed $term Term to get the meta value for
* either (string) term name, (int) term id or (object) term.
* @param string $taxonomy Name of the taxonomy to which the term is attached.
* @param string $meta Optional. Meta value to get (without prefix).
*
* @return mixed|bool Value for the $meta if one is given, might be the default.
* If no meta is given, an array of all the meta data for the term.
* False if the term does not exist or the $meta provided is invalid.
*/
public static function get_term_meta( $term, $taxonomy, $meta = null ) {
/* Figure out the term id. */
if ( is_int( $term ) ) {
$term = get_term_by( 'id', $term, $taxonomy );
}
elseif ( is_string( $term ) ) {
$term = get_term_by( 'slug', $term, $taxonomy );
}
if ( is_object( $term ) && isset( $term->term_id ) ) {
$term_id = $term->term_id;
}
else {
return false;
}
$tax_meta = self::get_term_tax_meta( $term_id, $taxonomy );
/*
* Either return the complete array or a single value from it or false if the value does not exist
* (shouldn't happen after merge with defaults, indicates typo in request).
*/
if ( ! isset( $meta ) ) {
return $tax_meta;
}
if ( isset( $tax_meta[ 'wpseo_' . $meta ] ) ) {
return $tax_meta[ 'wpseo_' . $meta ];
}
return false;
}
/**
* Get the current queried object and return the meta value.
*
* @param string $meta The meta field that is needed.
*
* @return bool|mixed
*/
public static function get_meta_without_term( $meta ) {
$term = $GLOBALS['wp_query']->get_queried_object();
if ( ! $term || empty( $term->taxonomy ) ) {
return false;
}
return self::get_term_meta( $term, $term->taxonomy, $meta );
}
/**
* Saving the values for the given term_id.
*
* @param int $term_id ID of the term to save data for.
* @param string $taxonomy The taxonomy the term belongs to.
* @param array $meta_values The values that will be saved.
*/
public static function set_values( $term_id, $taxonomy, array $meta_values ) {
/* Validate the post values */
$old = self::get_term_meta( $term_id, $taxonomy );
$clean = self::validate_term_meta_data( $meta_values, $old );
self::save_clean_values( $term_id, $taxonomy, $clean );
}
/**
* Setting a single value to the term meta.
*
* @param int $term_id ID of the term to save data for.
* @param string $taxonomy The taxonomy the term belongs to.
* @param string $meta_key The target meta key to store the value in.
* @param string $meta_value The value of the target meta key.
*/
public static function set_value( $term_id, $taxonomy, $meta_key, $meta_value ) {
if ( substr( strtolower( $meta_key ), 0, 6 ) !== 'wpseo_' ) {
$meta_key = 'wpseo_' . $meta_key;
}
self::set_values( $term_id, $taxonomy, [ $meta_key => $meta_value ] );
}
/**
* Find the keyword usages in the metas for the taxonomies/terms.
*
* @param string $keyword The keyword to look for.
* @param string $current_term_id The current term id.
* @param string $current_taxonomy The current taxonomy name.
*
* @return array
*/
public static function get_keyword_usage( $keyword, $current_term_id, $current_taxonomy ) {
$tax_meta = self::get_tax_meta();
$found = [];
// @todo Check for terms of all taxonomies, not only the current taxonomy.
foreach ( $tax_meta as $taxonomy_name => $terms ) {
foreach ( $terms as $term_id => $meta_values ) {
$is_current = ( $current_taxonomy === $taxonomy_name && (string) $current_term_id === (string) $term_id );
if ( ! $is_current && ! empty( $meta_values['wpseo_focuskw'] ) && $meta_values['wpseo_focuskw'] === $keyword ) {
$found[] = $term_id;
}
}
}
return [ $keyword => $found ];
}
/**
* Saving the values for the given term_id.
*
* @param int $term_id ID of the term to save data for.
* @param string $taxonomy The taxonomy the term belongs to.
* @param array $clean Array with clean values.
*/
private static function save_clean_values( $term_id, $taxonomy, array $clean ) {
$tax_meta = self::get_tax_meta();
/* Add/remove the result to/from the original option value. */
if ( $clean !== [] ) {
$tax_meta[ $taxonomy ][ $term_id ] = $clean;
}
else {
unset( $tax_meta[ $taxonomy ][ $term_id ] );
if ( isset( $tax_meta[ $taxonomy ] ) && $tax_meta[ $taxonomy ] === [] ) {
unset( $tax_meta[ $taxonomy ] );
}
}
// Prevent complete array validation.
$tax_meta['wpseo_already_validated'] = true;
self::save_tax_meta( $tax_meta );
}
/**
* Getting the meta from the options.
*
* @return void|array
*/
private static function get_tax_meta() {
return get_option( self::$name );
}
/**
* Saving the tax meta values to the database.
*
* @param array $tax_meta Array with the meta values for taxonomy.
*/
private static function save_tax_meta( $tax_meta ) {
update_option( self::$name, $tax_meta );
}
/**
* Getting the taxonomy meta for the given term_id and taxonomy.
*
* @param int $term_id The id of the term.
* @param string $taxonomy Name of the taxonomy to which the term is attached.
*
* @return array
*/
private static function get_term_tax_meta( $term_id, $taxonomy ) {
$tax_meta = self::get_tax_meta();
/* If we have data for the term, merge with defaults for complete array, otherwise set defaults. */
if ( isset( $tax_meta[ $taxonomy ][ $term_id ] ) ) {
return array_merge( self::$defaults_per_term, $tax_meta[ $taxonomy ][ $term_id ] );
}
return self::$defaults_per_term;
}
}

View File

@@ -0,0 +1,265 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
use Yoast\WP\SEO\Helpers\Author_Archive_Helper;
/**
* Sitemap provider for author archives.
*/
class WPSEO_Author_Sitemap_Provider implements WPSEO_Sitemap_Provider {
/**
* The date helper.
*
* @var WPSEO_Date_Helper
*/
protected $date;
/**
* WPSEO_Author_Sitemap_Provider constructor.
*/
public function __construct() {
$this->date = new WPSEO_Date_Helper();
}
/**
* Check if provider supports given item type.
*
* @param string $type Type string to check for.
*
* @return boolean
*/
public function handles_type( $type ) {
// If the author archives have been disabled, we don't do anything.
if ( WPSEO_Options::get( 'disable-author', false ) || WPSEO_Options::get( 'noindex-author-wpseo', false ) ) {
return false;
}
return $type === 'author';
}
/**
* Get the links for the sitemap index.
*
* @param int $max_entries Entries per sitemap.
*
* @return array
*/
public function get_index_links( $max_entries ) {
if ( ! $this->handles_type( 'author' ) ) {
return [];
}
// @todo Consider doing this less often / when necessary. R.
$this->update_user_meta();
$has_exclude_filter = has_filter( 'wpseo_sitemap_exclude_author' );
$query_arguments = [];
if ( ! $has_exclude_filter ) { // We only need full users if legacy filter(s) hooked to exclusion logic. R.
$query_arguments['fields'] = 'ID';
}
$users = $this->get_users( $query_arguments );
if ( $has_exclude_filter ) {
$users = $this->exclude_users( $users );
$users = wp_list_pluck( $users, 'ID' );
}
if ( empty( $users ) ) {
return [];
}
$index = [];
$page = 1;
$user_pages = array_chunk( $users, $max_entries );
if ( count( $user_pages ) === 1 ) {
$page = '';
}
foreach ( $user_pages as $users_page ) {
$user_id = array_shift( $users_page ); // Time descending, first user on page is most recently updated.
$user = get_user_by( 'id', $user_id );
$index[] = [
'loc' => WPSEO_Sitemaps_Router::get_base_url( 'author-sitemap' . $page . '.xml' ),
'lastmod' => ( $user->_yoast_wpseo_profile_updated ) ? $this->date->format_timestamp( $user->_yoast_wpseo_profile_updated ) : null,
];
$page++;
}
return $index;
}
/**
* Retrieve users, taking account of all necessary exclusions.
*
* @param array $arguments Arguments to add.
*
* @return array
*/
protected function get_users( $arguments = [] ) {
global $wpdb;
$defaults = [
'who' => 'authors',
'meta_key' => '_yoast_wpseo_profile_updated',
'orderby' => 'meta_value_num',
'order' => 'DESC',
'meta_query' => [
'relation' => 'AND',
[
'key' => $wpdb->get_blog_prefix() . 'user_level',
'value' => '0',
'compare' => '!=',
],
[
'relation' => 'OR',
[
'key' => 'wpseo_noindex_author',
'value' => 'on',
'compare' => '!=',
],
[
'key' => 'wpseo_noindex_author',
'compare' => 'NOT EXISTS',
],
],
],
];
if ( WPSEO_Options::get( 'noindex-author-noposts-wpseo', true ) ) {
$defaults['who'] = ''; // Otherwise it cancels out next argument.
$author_archive = new Author_Archive_Helper();
$defaults['has_published_posts'] = $author_archive->get_author_archive_post_types();
}
return get_users( array_merge( $defaults, $arguments ) );
}
/**
* Get set of sitemap link data.
*
* @param string $type Sitemap type.
* @param int $max_entries Entries per sitemap.
* @param int $current_page Current page of the sitemap.
*
* @throws OutOfBoundsException When an invalid page is requested.
*
* @return array
*/
public function get_sitemap_links( $type, $max_entries, $current_page ) {
$links = [];
if ( ! $this->handles_type( 'author' ) ) {
return $links;
}
$user_criteria = [
'offset' => ( ( $current_page - 1 ) * $max_entries ),
'number' => $max_entries,
];
$users = $this->get_users( $user_criteria );
// Throw an exception when there are no users in the sitemap.
if ( count( $users ) === 0 ) {
throw new OutOfBoundsException( 'Invalid sitemap page requested' );
}
$users = $this->exclude_users( $users );
if ( empty( $users ) ) {
$users = [];
}
$time = time();
foreach ( $users as $user ) {
$author_link = get_author_posts_url( $user->ID );
if ( empty( $author_link ) ) {
continue;
}
$mod = $time;
if ( isset( $user->_yoast_wpseo_profile_updated ) ) {
$mod = $user->_yoast_wpseo_profile_updated;
}
$url = [
'loc' => $author_link,
'mod' => date( DATE_W3C, $mod ),
// Deprecated, kept for backwards data compat. R.
'chf' => 'daily',
'pri' => 1,
];
/** This filter is documented at inc/sitemaps/class-post-type-sitemap-provider.php */
$url = apply_filters( 'wpseo_sitemap_entry', $url, 'user', $user );
if ( ! empty( $url ) ) {
$links[] = $url;
}
}
return $links;
}
/**
* Update any users that don't have last profile update timestamp.
*
* @return int Count of users updated.
*/
protected function update_user_meta() {
$user_criteria = [
'who' => 'authors',
'meta_query' => [
[
'key' => '_yoast_wpseo_profile_updated',
'compare' => 'NOT EXISTS',
],
],
];
$users = get_users( $user_criteria );
$time = time();
foreach ( $users as $user ) {
update_user_meta( $user->ID, '_yoast_wpseo_profile_updated', $time );
}
return count( $users );
}
/**
* Wrap legacy filter to deduplicate calls.
*
* @param array $users Array of user objects to filter.
*
* @return array
*/
protected function exclude_users( $users ) {
/**
* Filter the authors, included in XML sitemap.
*
* @param array $users Array of user objects to filter.
*/
return apply_filters( 'wpseo_sitemap_exclude_author', $users );
}
}

View File

@@ -0,0 +1,706 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Sitemap provider for author archives.
*/
class WPSEO_Post_Type_Sitemap_Provider implements WPSEO_Sitemap_Provider {
/**
* Holds image parser instance.
*
* @var WPSEO_Sitemap_Image_Parser
*/
protected static $image_parser;
/**
* Holds instance of classifier for a link.
*
* @var object
*/
protected static $classifier;
/**
* Determines whether images should be included in the XML sitemap.
*
* @var bool
*/
private $include_images;
/**
* Set up object properties for data reuse.
*/
public function __construct() {
add_filter( 'save_post', [ $this, 'save_post' ] );
/**
* Filter - Allows excluding images from the XML sitemap.
*
* @param bool unsigned True to include, false to exclude.
*/
$this->include_images = apply_filters( 'wpseo_xml_sitemap_include_images', true );
}
/**
* Get the Image Parser.
*
* @return WPSEO_Sitemap_Image_Parser
*/
protected function get_image_parser() {
if ( ! isset( self::$image_parser ) ) {
self::$image_parser = new WPSEO_Sitemap_Image_Parser();
}
return self::$image_parser;
}
/**
* Get the Classifier for a link.
*
* @return WPSEO_Link_Type_Classifier
*/
protected function get_classifier() {
if ( ! isset( self::$classifier ) ) {
self::$classifier = new WPSEO_Link_Type_Classifier( home_url() );
}
return self::$classifier;
}
/**
* Check if provider supports given item type.
*
* @param string $type Type string to check for.
*
* @return boolean
*/
public function handles_type( $type ) {
return post_type_exists( $type );
}
/**
* Retrieves the sitemap links.
*
* @param int $max_entries Entries per sitemap.
*
* @return array
*/
public function get_index_links( $max_entries ) {
global $wpdb;
$post_types = WPSEO_Post_Type::get_accessible_post_types();
$post_types = array_filter( $post_types, [ $this, 'is_valid_post_type' ] );
$last_modified_times = WPSEO_Sitemaps::get_last_modified_gmt( $post_types, true );
$index = [];
foreach ( $post_types as $post_type ) {
$total_count = $this->get_post_type_count( $post_type );
$max_pages = 1;
if ( $total_count > $max_entries ) {
$max_pages = (int) ceil( $total_count / $max_entries );
}
$all_dates = [];
if ( $max_pages > 1 ) {
$post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses( $post_type ) );
$sql = "
SELECT post_modified_gmt
FROM ( SELECT @rownum:=0 ) init
JOIN {$wpdb->posts} USE INDEX( type_status_date )
WHERE post_status IN ('" . implode( "','", $post_statuses ) . "')
AND post_type = %s
AND ( @rownum:=@rownum+1 ) %% %d = 0
ORDER BY post_modified_gmt ASC
";
$all_dates = $wpdb->get_col( $wpdb->prepare( $sql, $post_type, $max_entries ) );
}
for ( $page_counter = 0; $page_counter < $max_pages; $page_counter++ ) {
$current_page = ( $max_pages > 1 ) ? ( $page_counter + 1 ) : '';
$date = false;
if ( empty( $current_page ) || $current_page === $max_pages ) {
if ( ! empty( $last_modified_times[ $post_type ] ) ) {
$date = $last_modified_times[ $post_type ];
}
}
else {
$date = $all_dates[ $page_counter ];
}
$index[] = [
'loc' => WPSEO_Sitemaps_Router::get_base_url( $post_type . '-sitemap' . $current_page . '.xml' ),
'lastmod' => $date,
];
}
}
return $index;
}
/**
* Get set of sitemap link data.
*
* @param string $type Sitemap type.
* @param int $max_entries Entries per sitemap.
* @param int $current_page Current page of the sitemap.
*
* @throws OutOfBoundsException When an invalid page is requested.
*
* @return array
*/
public function get_sitemap_links( $type, $max_entries, $current_page ) {
$links = [];
$post_type = $type;
if ( ! $this->is_valid_post_type( $post_type ) ) {
throw new OutOfBoundsException( 'Invalid sitemap page requested' );
}
$steps = min( 100, $max_entries );
$offset = ( $current_page > 1 ) ? ( ( $current_page - 1 ) * $max_entries ) : 0;
$total = ( $offset + $max_entries );
$post_type_entries = $this->get_post_type_count( $post_type );
if ( $total > $post_type_entries ) {
$total = $post_type_entries;
}
if ( $current_page === 1 ) {
$links = array_merge( $links, $this->get_first_links( $post_type ) );
}
// If total post type count is lower than the offset, an invalid page is requested.
if ( $post_type_entries < $offset ) {
throw new OutOfBoundsException( 'Invalid sitemap page requested' );
}
if ( $post_type_entries === 0 ) {
return $links;
}
$posts_to_exclude = $this->get_excluded_posts( $type );
while ( $total > $offset ) {
$posts = $this->get_posts( $post_type, $steps, $offset );
$offset += $steps;
if ( empty( $posts ) ) {
continue;
}
foreach ( $posts as $post ) {
if ( in_array( $post->ID, $posts_to_exclude, true ) ) {
continue;
}
if ( WPSEO_Meta::get_value( 'meta-robots-noindex', $post->ID ) === '1' ) {
continue;
}
$url = $this->get_url( $post );
if ( ! isset( $url['loc'] ) ) {
continue;
}
/**
* Filter URL entry before it gets added to the sitemap.
*
* @param array $url Array of URL parts.
* @param string $type URL type.
* @param object $post Data object for the URL.
*/
$url = apply_filters( 'wpseo_sitemap_entry', $url, 'post', $post );
if ( ! empty( $url ) ) {
$links[] = $url;
}
}
unset( $post, $url );
}
return $links;
}
/**
* Check for relevant post type before invalidation.
*
* @param int $post_id Post ID to possibly invalidate for.
*/
public function save_post( $post_id ) {
if ( $this->is_valid_post_type( get_post_type( $post_id ) ) ) {
WPSEO_Sitemaps_Cache::invalidate_post( $post_id );
}
}
/**
* Check if post type should be present in sitemaps.
*
* @param string $post_type Post type string to check for.
*
* @return bool
*/
public function is_valid_post_type( $post_type ) {
if ( ! WPSEO_Post_Type::is_post_type_accessible( $post_type ) || ! WPSEO_Post_Type::is_post_type_indexable( $post_type ) ) {
return false;
}
/**
* Filter decision if post type is excluded from the XML sitemap.
*
* @param bool $exclude Default false.
* @param string $post_type Post type name.
*/
if ( apply_filters( 'wpseo_sitemap_exclude_post_type', false, $post_type ) ) {
return false;
}
return true;
}
/**
* Retrieves a list with the excluded post ids.
*
* @param string $post_type Post type.
*
* @return array Array with post ids to exclude.
*/
protected function get_excluded_posts( $post_type ) {
$excluded_posts_ids = [];
$page_on_front_id = ( $post_type === 'page' ) ? (int) get_option( 'page_on_front' ) : 0;
if ( $page_on_front_id > 0 ) {
$excluded_posts_ids[] = $page_on_front_id;
}
/**
* Filter: 'wpseo_exclude_from_sitemap_by_post_ids' - Allow extending and modifying the posts to exclude.
*
* @api array $posts_to_exclude The posts to exclude.
*/
$excluded_posts_ids = apply_filters( 'wpseo_exclude_from_sitemap_by_post_ids', $excluded_posts_ids );
if ( ! is_array( $excluded_posts_ids ) ) {
$excluded_posts_ids = [];
}
$excluded_posts_ids = array_map( 'intval', $excluded_posts_ids );
$page_for_posts_id = ( $post_type === 'page' ) ? (int) get_option( 'page_for_posts' ) : 0;
if ( $page_for_posts_id > 0 ) {
$excluded_posts_ids[] = $page_for_posts_id;
}
return array_unique( $excluded_posts_ids );
}
/**
* Get count of posts for post type.
*
* @param string $post_type Post type to retrieve count for.
*
* @return int
*/
protected function get_post_type_count( $post_type ) {
global $wpdb;
/**
* Filter JOIN query part for type count of post type.
*
* @param string $join SQL part, defaults to empty string.
* @param string $post_type Post type name.
*/
$join_filter = apply_filters( 'wpseo_typecount_join', '', $post_type );
/**
* Filter WHERE query part for type count of post type.
*
* @param string $where SQL part, defaults to empty string.
* @param string $post_type Post type name.
*/
$where_filter = apply_filters( 'wpseo_typecount_where', '', $post_type );
$where = $this->get_sql_where_clause( $post_type );
$sql = "
SELECT COUNT({$wpdb->posts}.ID)
FROM {$wpdb->posts}
{$join_filter}
{$where}
{$where_filter}
";
return (int) $wpdb->get_var( $sql );
}
/**
* Produces set of links to prepend at start of first sitemap page.
*
* @param string $post_type Post type to produce links for.
*
* @return array
*/
protected function get_first_links( $post_type ) {
$links = [];
$archive_url = false;
if ( $post_type === 'page' ) {
$page_on_front_id = (int) get_option( 'page_on_front' );
if ( $page_on_front_id > 0 ) {
$front_page = $this->get_url(
get_post( $page_on_front_id )
);
}
if ( empty( $front_page ) ) {
$front_page = [
'loc' => WPSEO_Utils::home_url(),
];
}
// Deprecated, kept for backwards data compat. R.
$front_page['chf'] = 'daily';
$front_page['pri'] = 1;
$links[] = $front_page;
}
elseif ( $post_type !== 'page' ) {
/**
* Filter the URL Yoast SEO uses in the XML sitemap for this post type archive.
*
* @param string $archive_url The URL of this archive
* @param string $post_type The post type this archive is for.
*/
$archive_url = apply_filters(
'wpseo_sitemap_post_type_archive_link',
$this->get_post_type_archive_link( $post_type ),
$post_type
);
}
if ( $archive_url ) {
$links[] = [
'loc' => $archive_url,
'mod' => WPSEO_Sitemaps::get_last_modified_gmt( $post_type ),
// Deprecated, kept for backwards data compat. R.
'chf' => 'daily',
'pri' => 1,
];
}
return $links;
}
/**
* Get URL for a post type archive.
*
* @since 5.3
*
* @param string $post_type Post type.
*
* @return string|bool URL or false if it should be excluded.
*/
protected function get_post_type_archive_link( $post_type ) {
$pt_archive_page_id = -1;
if ( $post_type === 'post' ) {
if ( get_option( 'show_on_front' ) === 'posts' ) {
return WPSEO_Utils::home_url();
}
$pt_archive_page_id = (int) get_option( 'page_for_posts' );
// Post archive should be excluded if posts page isn't set.
if ( $pt_archive_page_id <= 0 ) {
return false;
}
}
if ( ! $this->is_post_type_archive_indexable( $post_type, $pt_archive_page_id ) ) {
return false;
}
return get_post_type_archive_link( $post_type );
}
/**
* Determines whether a post type archive is indexable.
*
* @since 11.5
*
* @param string $post_type Post type.
* @param int $archive_page_id The page id.
*
* @return bool True when post type archive is indexable.
*/
protected function is_post_type_archive_indexable( $post_type, $archive_page_id = -1 ) {
if ( WPSEO_Options::get( 'noindex-ptarchive-' . $post_type, false ) ) {
return false;
}
/**
* Filter the page which is dedicated to this post type archive.
*
* @since 9.3
*
* @param string $archive_page_id The post_id of the page.
* @param string $post_type The post type this archive is for.
*/
$archive_page_id = (int) apply_filters( 'wpseo_sitemap_page_for_post_type_archive', $archive_page_id, $post_type );
if ( $archive_page_id > 0 && WPSEO_Meta::get_value( 'meta-robots-noindex', $archive_page_id ) === '1' ) {
return false;
}
return true;
}
/**
* Retrieve set of posts with optimized query routine.
*
* @param string $post_type Post type to retrieve.
* @param int $count Count of posts to retrieve.
* @param int $offset Starting offset.
*
* @return object[]
*/
protected function get_posts( $post_type, $count, $offset ) {
global $wpdb;
static $filters = [];
if ( ! isset( $filters[ $post_type ] ) ) {
// Make sure you're wpdb->preparing everything you throw into this!!
$filters[ $post_type ] = [
/**
* Filter JOIN query part for the post type.
*
* @param string $join SQL part, defaults to false.
* @param string $post_type Post type name.
*/
'join' => apply_filters( 'wpseo_posts_join', false, $post_type ),
/**
* Filter WHERE query part for the post type.
*
* @param string $where SQL part, defaults to false.
* @param string $post_type Post type name.
*/
'where' => apply_filters( 'wpseo_posts_where', false, $post_type ),
];
}
$join_filter = $filters[ $post_type ]['join'];
$where_filter = $filters[ $post_type ]['where'];
$where = $this->get_sql_where_clause( $post_type );
/*
* Optimized query per this thread:
* {@link http://wordpress.org/support/topic/plugin-wordpress-seo-by-yoast-performance-suggestion}.
* Also see {@link http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/}.
*/
$sql = "
SELECT l.ID, post_title, post_content, post_name, post_parent, post_author, post_status, post_modified_gmt, post_date, post_date_gmt
FROM (
SELECT {$wpdb->posts}.ID
FROM {$wpdb->posts}
{$join_filter}
{$where}
{$where_filter}
ORDER BY {$wpdb->posts}.post_modified ASC LIMIT %d OFFSET %d
)
o JOIN {$wpdb->posts} l ON l.ID = o.ID
";
$posts = $wpdb->get_results( $wpdb->prepare( $sql, $count, $offset ) );
$post_ids = [];
foreach ( $posts as $post ) {
$post->post_type = $post_type;
$post->filter = 'sample';
$post->ID = (int) $post->ID;
$post->post_parent = (int) $post->post_parent;
$post->post_author = (int) $post->post_author;
$post_ids[] = $post->ID;
}
update_meta_cache( 'post', $post_ids );
return $posts;
}
/**
* Constructs an SQL where clause for a given post type.
*
* @param string $post_type Post type slug.
*
* @return string
*/
protected function get_sql_where_clause( $post_type ) {
global $wpdb;
$join = '';
$post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses( $post_type ) );
$status_where = "{$wpdb->posts}.post_status IN ('" . implode( "','", $post_statuses ) . "')";
// Based on WP_Query->get_posts(). R.
if ( $post_type === 'attachment' ) {
$join = " LEFT JOIN {$wpdb->posts} AS p2 ON ({$wpdb->posts}.post_parent = p2.ID) ";
$parent_statuses = array_diff( $post_statuses, [ 'inherit' ] );
$status_where = "p2.post_status IN ('" . implode( "','", $parent_statuses ) . "') AND p2.post_password = ''";
}
$where_clause = "
{$join}
WHERE {$status_where}
AND {$wpdb->posts}.post_type = %s
AND {$wpdb->posts}.post_password = ''
AND {$wpdb->posts}.post_date != '0000-00-00 00:00:00'
";
return $wpdb->prepare( $where_clause, $post_type );
}
/**
* Produce array of URL parts for given post object.
*
* @param object $post Post object to get URL parts for.
*
* @return array|bool
*/
protected function get_url( $post ) {
$url = [];
/**
* Filter the URL Yoast SEO uses in the XML sitemap.
*
* Note that only absolute local URLs are allowed as the check after this removes external URLs.
*
* @param string $url URL to use in the XML sitemap
* @param object $post Post object for the URL.
*/
$url['loc'] = apply_filters( 'wpseo_xml_sitemap_post_url', get_permalink( $post ), $post );
/*
* Do not include external URLs.
*
* {@link https://wordpress.org/plugins/page-links-to/} can rewrite permalinks to external URLs.
*/
if ( $this->get_classifier()->classify( $url['loc'] ) === WPSEO_Link::TYPE_EXTERNAL ) {
return false;
}
$modified = max( $post->post_modified_gmt, $post->post_date_gmt );
if ( $modified !== '0000-00-00 00:00:00' ) {
$url['mod'] = $modified;
}
$url['chf'] = 'daily'; // Deprecated, kept for backwards data compat. R.
$canonical = WPSEO_Meta::get_value( 'canonical', $post->ID );
if ( $canonical !== '' && $canonical !== $url['loc'] ) {
/*
* Let's assume that if a canonical is set for this page and it's different from
* the URL of this post, that page is either already in the XML sitemap OR is on
* an external site, either way, we shouldn't include it here.
*/
return false;
}
unset( $canonical );
$url['pri'] = 1; // Deprecated, kept for backwards data compat. R.
if ( $this->include_images ) {
$url['images'] = $this->get_image_parser()->get_images( $post );
}
return $url;
}
/* ********************* DEPRECATED METHODS ********************* */
/**
* Get all the options.
*
* @deprecated 7.0
* @codeCoverageIgnore
*/
protected function get_options() {
_deprecated_function( __METHOD__, 'WPSEO 7.0', 'WPSEO_Options::get' );
}
/**
* Get Home URL.
*
* @deprecated 11.5
* @codeCoverageIgnore
*
* @return string
*/
protected function get_home_url() {
_deprecated_function( __METHOD__, 'WPSEO 11.5', 'WPSEO_Utils::home_url' );
return WPSEO_Utils::home_url();
}
/**
* Get front page ID.
*
* @deprecated 11.5
* @codeCoverageIgnore
*
* @return int
*/
protected function get_page_on_front_id() {
_deprecated_function( __METHOD__, 'WPSEO 11.5' );
return (int) get_option( 'page_on_front' );
}
/**
* Get page for posts ID.
*
* @deprecated 11.5
* @codeCoverageIgnore
*
* @return int
*/
protected function get_page_for_posts_id() {
_deprecated_function( __METHOD__, 'WPSEO 11.5' );
return (int) get_option( 'page_for_posts' );
}
}

View File

@@ -0,0 +1,142 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Sitemap Cache Data object, manages sitemap data stored in cache.
*/
class WPSEO_Sitemap_Cache_Data implements WPSEO_Sitemap_Cache_Data_Interface, Serializable {
/**
* Sitemap XML data.
*
* @var string
*/
private $sitemap = '';
/**
* Status of the sitemap, usable or not.
*
* @var string
*/
private $status = self::UNKNOWN;
/**
* Set the sitemap XML data
*
* @param string $sitemap XML Content of the sitemap.
*/
public function set_sitemap( $sitemap ) {
if ( ! is_string( $sitemap ) ) {
$sitemap = '';
}
$this->sitemap = $sitemap;
/*
* Empty sitemap is not usable.
*/
if ( ! empty( $sitemap ) ) {
$this->set_status( self::OK );
}
else {
$this->set_status( self::ERROR );
}
}
/**
* Set the status of the sitemap, is it usable.
*
* @param bool|string $valid Is the sitemap valid or not.
*
* @return void
*/
public function set_status( $valid ) {
if ( self::OK === $valid ) {
$this->status = self::OK;
return;
}
if ( self::ERROR === $valid ) {
$this->status = self::ERROR;
$this->sitemap = '';
return;
}
$this->status = self::UNKNOWN;
}
/**
* Is the sitemap usable.
*
* @return bool True if usable, False if bad or unknown.
*/
public function is_usable() {
return self::OK === $this->status;
}
/**
* Get the XML content of the sitemap.
*
* @return string The content of the sitemap.
*/
public function get_sitemap() {
return $this->sitemap;
}
/**
* Get the status of the sitemap.
*
* @return string Status of the sitemap, 'ok'/'error'/'unknown'.
*/
public function get_status() {
return $this->status;
}
/**
* String representation of object.
*
* @link http://php.net/manual/en/serializable.serialize.php
*
* @since 5.1.0
*
* @return string The string representation of the object or null.
*/
public function serialize() {
$data = [
'status' => $this->status,
'xml' => $this->sitemap,
];
return serialize( $data );
}
/**
* Constructs the object.
*
* @link http://php.net/manual/en/serializable.unserialize.php
*
* @since 5.1.0
*
* @param string $serialized The string representation of the object.
*
* @return void
*/
public function unserialize( $serialized ) {
$data = unserialize( $serialized );
$this->set_sitemap( $data['xml'] );
$this->set_status( $data['status'] );
}
}

View File

@@ -0,0 +1,510 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Parses images from the given post.
*/
class WPSEO_Sitemap_Image_Parser {
/**
* Holds the home_url() value to speed up loops.
*
* @var string
*/
protected $home_url = '';
/**
* Holds site URL hostname.
*
* @var string
*/
protected $host = '';
/**
* Holds site URL protocol.
*
* @var string
*/
protected $scheme = 'http';
/**
* Cached set of attachments for multiple posts.
*
* @var array
*/
protected $attachments = [];
/**
* Holds blog charset value for use in DOM parsing.
*
* @var string
*/
protected $charset = 'UTF-8';
/**
* Set up URL properties for reuse.
*/
public function __construct() {
$this->home_url = home_url();
$parsed_home = wp_parse_url( $this->home_url );
if ( ! empty( $parsed_home['host'] ) ) {
$this->host = str_replace( 'www.', '', $parsed_home['host'] );
}
if ( ! empty( $parsed_home['scheme'] ) ) {
$this->scheme = $parsed_home['scheme'];
}
$this->charset = esc_attr( get_bloginfo( 'charset' ) );
}
/**
* Get set of image data sets for the given post.
*
* @param object $post Post object to get images for.
*
* @return array
*/
public function get_images( $post ) {
$images = [];
if ( ! is_object( $post ) ) {
return $images;
}
$thumbnail_id = get_post_thumbnail_id( $post->ID );
if ( $thumbnail_id ) {
$src = $this->get_absolute_url( $this->image_url( $thumbnail_id ) );
$alt = WPSEO_Image_Utils::get_alt_tag( $thumbnail_id );
$title = get_post_field( 'post_title', $thumbnail_id );
$images[] = $this->get_image_item( $post, $src, $title, $alt );
}
/**
* Filter: 'wpseo_sitemap_content_before_parse_html_images' - Filters the post content
* before it is parsed for images.
*
* @param string $content The raw/unprocessed post content.
*/
$content = apply_filters( 'wpseo_sitemap_content_before_parse_html_images', $post->post_content );
$unfiltered_images = $this->parse_html_images( $content );
foreach ( $unfiltered_images as $image ) {
$images[] = $this->get_image_item( $post, $image['src'], $image['title'], $image['alt'] );
}
foreach ( $this->parse_galleries( $content, $post->ID ) as $attachment ) {
$src = $this->get_absolute_url( $this->image_url( $attachment->ID ) );
$alt = WPSEO_Image_Utils::get_alt_tag( $attachment->ID );
$images[] = $this->get_image_item( $post, $src, $attachment->post_title, $alt );
}
if ( $post->post_type === 'attachment' && wp_attachment_is_image( $post ) ) {
$src = $this->get_absolute_url( $this->image_url( $post->ID ) );
$alt = WPSEO_Image_Utils::get_alt_tag( $post->ID );
$images[] = $this->get_image_item( $post, $src, $post->post_title, $alt );
}
foreach ( $images as $key => $image ) {
if ( empty( $image['src'] ) ) {
unset( $images[ $key ] );
}
}
/**
* Filter images to be included for the post in XML sitemap.
*
* @param array $images Array of image items.
* @param int $post_id ID of the post.
*/
$images = apply_filters( 'wpseo_sitemap_urlimages', $images, $post->ID );
return $images;
}
/**
* Get the images in the term description.
*
* @param object $term Term to get images from description for.
*
* @return array
*/
public function get_term_images( $term ) {
$images = $this->parse_html_images( $term->description );
foreach ( $this->parse_galleries( $term->description ) as $attachment ) {
$images[] = [
'src' => $this->get_absolute_url( $this->image_url( $attachment->ID ) ),
'title' => $attachment->post_title,
'alt' => WPSEO_Image_Utils::get_alt_tag( $attachment->ID ),
];
}
return $images;
}
/**
* Parse `<img />` tags in content.
*
* @param string $content Content string to parse.
*
* @return array
*/
private function parse_html_images( $content ) {
$images = [];
if ( ! class_exists( 'DOMDocument' ) ) {
return $images;
}
if ( empty( $content ) ) {
return $images;
}
// Prevent DOMDocument from bubbling warnings about invalid HTML.
libxml_use_internal_errors( true );
$post_dom = new DOMDocument();
$post_dom->loadHTML( '<?xml encoding="' . $this->charset . '">' . $content );
// Clear the errors, so they don't get kept in memory.
libxml_clear_errors();
/** @var DOMElement $img */
foreach ( $post_dom->getElementsByTagName( 'img' ) as $img ) {
$src = $img->getAttribute( 'src' );
if ( empty( $src ) ) {
continue;
}
$class = $img->getAttribute( 'class' );
if ( // This detects WP-inserted images, which we need to upsize. R.
! empty( $class )
&& ( false === strpos( $class, 'size-full' ) )
&& preg_match( '|wp-image-(?P<id>\d+)|', $class, $matches )
&& get_post_status( $matches['id'] )
) {
$src = $this->image_url( $matches['id'] );
}
$src = $this->get_absolute_url( $src );
if ( strpos( $src, $this->host ) === false ) {
continue;
}
if ( $src !== esc_url( $src ) ) {
continue;
}
$images[] = [
'src' => $src,
'title' => $img->getAttribute( 'title' ),
'alt' => $img->getAttribute( 'alt' ),
];
}
return $images;
}
/**
* Parse gallery shortcodes in a given content.
*
* @param string $content Content string.
* @param int $post_id Optional. ID of post being parsed.
*
* @return array Set of attachment objects.
*/
protected function parse_galleries( $content, $post_id = 0 ) {
$attachments = [];
$galleries = $this->get_content_galleries( $content );
foreach ( $galleries as $gallery ) {
$id = $post_id;
if ( ! empty( $gallery['id'] ) ) {
$id = intval( $gallery['id'] );
}
// Forked from core gallery_shortcode() to have exact same logic. R.
if ( ! empty( $gallery['ids'] ) ) {
$gallery['include'] = $gallery['ids'];
}
$gallery_attachments = $this->get_gallery_attachments( $id, $gallery );
$attachments = array_merge( $attachments, $gallery_attachments );
}
return array_unique( $attachments, SORT_REGULAR );
}
/**
* Retrieves galleries from the passed content.
*
* Forked from core to skip executing shortcodes for performance.
*
* @param string $content Content to parse for shortcodes.
*
* @return array A list of arrays, each containing gallery data.
*/
protected function get_content_galleries( $content ) {
$galleries = [];
if ( ! preg_match_all( '/' . get_shortcode_regex( [ 'gallery' ] ) . '/s', $content, $matches, PREG_SET_ORDER ) ) {
return $galleries;
}
foreach ( $matches as $shortcode ) {
$attributes = shortcode_parse_atts( $shortcode[3] );
if ( $attributes === '' ) { // Valid shortcode without any attributes. R.
$attributes = [];
}
$galleries[] = $attributes;
}
return $galleries;
}
/**
* Get image item array with filters applied.
*
* @param WP_Post $post Post object for the context.
* @param string $src Image URL.
* @param string $title Optional image title.
* @param string $alt Optional image alt text.
*
* @return array
*/
protected function get_image_item( $post, $src, $title = '', $alt = '' ) {
$image = [];
/**
* Filter image URL to be included in XML sitemap for the post.
*
* @param string $src Image URL.
* @param object $post Post object.
*/
$image['src'] = apply_filters( 'wpseo_xml_sitemap_img_src', $src, $post );
if ( ! empty( $title ) ) {
$image['title'] = $title;
}
if ( ! empty( $alt ) ) {
$image['alt'] = $alt;
}
/**
* Filter image data to be included in XML sitemap for the post.
*
* @param array $image {
* Array of image data.
*
* @type string $src Image URL.
* @type string $title Image title attribute (optional).
* @type string $alt Image alt attribute (optional).
* }
*
* @param object $post Post object.
*/
return apply_filters( 'wpseo_xml_sitemap_img', $image, $post );
}
/**
* Get attached image URL with filters applied. Adapted from core for speed.
*
* @param int $post_id ID of the post.
*
* @return string
*/
private function image_url( $post_id ) {
static $uploads;
if ( empty( $uploads ) ) {
$uploads = wp_upload_dir();
}
if ( $uploads['error'] !== false ) {
return '';
}
$file = get_post_meta( $post_id, '_wp_attached_file', true );
if ( empty( $file ) ) {
return '';
}
// Check that the upload base exists in the file location.
if ( strpos( $file, $uploads['basedir'] ) === 0 ) {
$src = str_replace( $uploads['basedir'], $uploads['baseurl'], $file );
}
elseif ( strpos( $file, 'wp-content/uploads' ) !== false ) {
$src = $uploads['baseurl'] . substr( $file, ( strpos( $file, 'wp-content/uploads' ) + 18 ) );
}
else {
// It's a newly uploaded file, therefore $file is relative to the baseurl.
$src = $uploads['baseurl'] . '/' . $file;
}
return apply_filters( 'wp_get_attachment_url', $src, $post_id );
}
/**
* Make absolute URL for domain or protocol-relative one.
*
* @param string $src URL to process.
*
* @return string
*/
protected function get_absolute_url( $src ) {
if ( empty( $src ) || ! is_string( $src ) ) {
return $src;
}
if ( WPSEO_Utils::is_url_relative( $src ) === true ) {
if ( $src[0] !== '/' ) {
return $src;
}
// The URL is relative, we'll have to make it absolute.
return $this->home_url . $src;
}
if ( strpos( $src, 'http' ) !== 0 ) {
// Protocol relative URL, we add the scheme as the standard requires a protocol.
return $this->scheme . ':' . $src;
}
return $src;
}
/**
* Returns the attachments for a gallery.
*
* @param int $id The post ID.
* @param array $gallery The gallery config.
*
* @return array The selected attachments.
*/
protected function get_gallery_attachments( $id, $gallery ) {
// When there are attachments to include.
if ( ! empty( $gallery['include'] ) ) {
return $this->get_gallery_attachments_for_included( $gallery['include'] );
}
// When $id is empty, just return empty array.
if ( empty( $id ) ) {
return [];
}
return $this->get_gallery_attachments_for_parent( $id, $gallery );
}
/**
* Returns the attachments for the given ID.
*
* @param int $id The post ID.
* @param array $gallery The gallery config.
*
* @return array The selected attachments.
*/
protected function get_gallery_attachments_for_parent( $id, $gallery ) {
$query = [
'posts_per_page' => -1,
'post_parent' => $id,
];
// When there are posts that should be excluded from result set.
if ( ! empty( $gallery['exclude'] ) ) {
$query['post__not_in'] = wp_parse_id_list( $gallery['exclude'] );
}
return $this->get_attachments( $query );
}
/**
* Returns an array with attachments for the post IDs that will be included.
*
* @param array $include Array with IDs to include.
*
* @return array The found attachments.
*/
protected function get_gallery_attachments_for_included( $include ) {
$ids_to_include = wp_parse_id_list( $include );
$attachments = $this->get_attachments(
[
'posts_per_page' => count( $ids_to_include ),
'post__in' => $ids_to_include,
]
);
$gallery_attachments = [];
foreach ( $attachments as $key => $val ) {
$gallery_attachments[ $val->ID ] = $val;
}
return $gallery_attachments;
}
/**
* Returns the attachments.
*
* @param array $args Array with query args.
*
* @return array The found attachments.
*/
protected function get_attachments( $args ) {
$default_args = [
'post_status' => 'inherit',
'post_type' => 'attachment',
'post_mime_type' => 'image',
// Defaults taken from function get_posts.
'orderby' => 'date',
'order' => 'DESC',
'meta_key' => '',
'meta_value' => '',
'suppress_filters' => true,
'ignore_sticky_posts' => true,
'no_found_rows' => true,
];
$args = wp_parse_args( $args, $default_args );
$get_attachments = new WP_Query();
return $get_attachments->query( $args );
}
}

View File

@@ -0,0 +1,157 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\XML Sitemaps
*/
/**
* Class that handles the Admin side of XML sitemaps.
*/
class WPSEO_Sitemaps_Admin {
/**
* Post_types that are being imported.
*
* @var array
*/
private $importing_post_types = [];
/**
* Class constructor.
*/
public function __construct() {
add_action( 'transition_post_status', [ $this, 'status_transition' ], 10, 3 );
add_action( 'admin_footer', [ $this, 'status_transition_bulk_finished' ] );
WPSEO_Sitemaps_Cache::register_clear_on_option_update( 'wpseo_titles', '' );
WPSEO_Sitemaps_Cache::register_clear_on_option_update( 'wpseo', '' );
}
/**
* Hooked into transition_post_status. Will initiate search engine pings
* if the post is being published, is a post type that a sitemap is built for
* and is a post that is included in sitemaps.
*
* @param string $new_status New post status.
* @param string $old_status Old post status.
* @param \WP_Post $post Post object.
*/
public function status_transition( $new_status, $old_status, $post ) {
if ( $new_status !== 'publish' ) {
return;
}
if ( defined( 'WP_IMPORTING' ) ) {
$this->status_transition_bulk( $new_status, $old_status, $post );
return;
}
$post_type = get_post_type( $post );
wp_cache_delete( 'lastpostmodified:gmt:' . $post_type, 'timeinfo' ); // #17455.
// Not something we're interested in.
if ( $post_type === 'nav_menu_item' ) {
return;
}
// If the post type is excluded in options, we can stop.
if ( WPSEO_Options::get( 'noindex-' . $post_type, false ) ) {
return;
}
/**
* Filter: 'wpseo_allow_xml_sitemap_ping' - Check if pinging is not allowed (allowed by default).
*
* @api boolean $allow_ping The boolean that is set to true by default.
*/
if ( apply_filters( 'wpseo_allow_xml_sitemap_ping', true ) === false ) {
return;
}
if ( defined( 'YOAST_SEO_PING_IMMEDIATELY' ) && YOAST_SEO_PING_IMMEDIATELY ) {
WPSEO_Sitemaps::ping_search_engines();
}
elseif ( ! wp_next_scheduled( 'wpseo_ping_search_engines' ) ) {
wp_schedule_single_event( ( time() + 300 ), 'wpseo_ping_search_engines' );
}
}
/**
* While bulk importing, just save unique post_types.
*
* When importing is done, if we have a post_type that is saved in the sitemap
* try to ping the search engines.
*
* @param string $new_status New post status.
* @param string $old_status Old post status.
* @param \WP_Post $post Post object.
*/
private function status_transition_bulk( $new_status, $old_status, $post ) {
$this->importing_post_types[] = get_post_type( $post );
$this->importing_post_types = array_unique( $this->importing_post_types );
}
/**
* After import finished, walk through imported post_types and update info.
*/
public function status_transition_bulk_finished() {
if ( ! defined( 'WP_IMPORTING' ) ) {
return;
}
if ( empty( $this->importing_post_types ) ) {
return;
}
$ping_search_engines = false;
foreach ( $this->importing_post_types as $post_type ) {
wp_cache_delete( 'lastpostmodified:gmt:' . $post_type, 'timeinfo' ); // #17455.
// Just have the cache deleted for nav_menu_item.
if ( $post_type === 'nav_menu_item' ) {
continue;
}
if ( WPSEO_Options::get( 'noindex-' . $post_type, false ) === false ) {
$ping_search_engines = true;
}
}
// Nothing to do.
if ( $ping_search_engines === false ) {
return;
}
if ( WP_CACHE ) {
do_action( 'wpseo_hit_sitemap_index' );
}
WPSEO_Sitemaps::ping_search_engines();
}
/* ********************* DEPRECATED METHODS ********************* */
/**
* Find sitemaps residing on disk as they will block our rewrite.
*
* @deprecated 7.0
* @codeCoverageIgnore
*/
public function delete_sitemaps() {
_deprecated_function( 'WPSEO_Sitemaps_Admin::delete_sitemaps', '7.0' );
}
/**
* Find sitemaps residing on disk as they will block our rewrite.
*
* @deprecated 7.0
* @codeCoverageIgnore
*/
public function detect_blocking_filesystem_sitemaps() {
_deprecated_function( 'WPSEO_Sitemaps_Admin::delete_sitemaps', '7.0' );
}
} /* End of class */

View File

@@ -0,0 +1,319 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Handles storage keys for sitemaps caching and invalidation.
*
* @since 3.2
*/
class WPSEO_Sitemaps_Cache_Validator {
/**
* Prefix of the transient key for sitemap caches.
*
* @var string
*/
const STORAGE_KEY_PREFIX = 'yst_sm_';
/**
* Name of the option that holds the global validation value.
*
* @var string
*/
const VALIDATION_GLOBAL_KEY = 'wpseo_sitemap_cache_validator_global';
/**
* The format which creates the key of the option that holds the type validation value.
*
* @var string
*/
const VALIDATION_TYPE_KEY_FORMAT = 'wpseo_sitemap_%s_cache_validator';
/**
* Get the cache key for a certain type and page.
*
* A type of cache would be something like 'page', 'post' or 'video'.
*
* Example key format for sitemap type "post", page 1: wpseo_sitemap_post_1:akfw3e_23azBa .
*
* @since 3.2
*
* @param null|string $type The type to get the key for. Null or self::SITEMAP_INDEX_TYPE for index cache.
* @param int $page The page of cache to get the key for.
*
* @return bool|string The key where the cache is stored on. False if the key could not be generated.
*/
public static function get_storage_key( $type = null, $page = 1 ) {
// Using SITEMAP_INDEX_TYPE for sitemap index cache.
$type = is_null( $type ) ? WPSEO_Sitemaps::SITEMAP_INDEX_TYPE : $type;
$global_cache_validator = self::get_validator();
$type_cache_validator = self::get_validator( $type );
$prefix = self::STORAGE_KEY_PREFIX;
$postfix = sprintf( '_%d:%s_%s', $page, $global_cache_validator, $type_cache_validator );
try {
$type = self::truncate_type( $type, $prefix, $postfix );
} catch ( OutOfBoundsException $exception ) {
// Maybe do something with the exception, for now just mark as invalid.
return false;
}
// Build key.
$full_key = $prefix . $type . $postfix;
return $full_key;
}
/**
* If the type is over length make sure we compact it so we don't have any database problems.
*
* When there are more 'extremely long' post types, changes are they have variations in either the start or ending.
* Because of this, we cut out the excess in the middle which should result in less chance of collision.
*
* @since 3.2
*
* @param string $type The type of sitemap to be used.
* @param string $prefix The part before the type in the cache key. Only the length is used.
* @param string $postfix The part after the type in the cache key. Only the length is used.
*
* @return string The type with a safe length to use
*
* @throws OutOfRangeException When there is less than 15 characters of space for a key that is originally longer.
*/
public static function truncate_type( $type, $prefix = '', $postfix = '' ) {
/*
* This length has been restricted by the database column length of 64 in the past.
* The prefix added by WordPress is '_transient_' because we are saving to a transient.
* We need to use a timeout on the transient, otherwise the values get autoloaded, this adds
* another restriction to the length.
*/
$max_length = 45; // 64 - 19 ('_transient_timeout_')
$max_length -= strlen( $prefix );
$max_length -= strlen( $postfix );
if ( strlen( $type ) > $max_length ) {
if ( $max_length < 15 ) {
/*
* If this happens the most likely cause is a page number that is too high.
*
* So this would not happen unintentionally.
* Either by trying to cause a high server load, finding backdoors or misconfiguration.
*/
throw new OutOfRangeException(
__(
'Trying to build the sitemap cache key, but the postfix and prefix combination leaves too little room to do this. You are probably requesting a page that is way out of the expected range.',
'wordpress-seo'
)
);
}
$half = ( $max_length / 2 );
$first_part = substr( $type, 0, ( ceil( $half ) - 1 ) );
$last_part = substr( $type, ( 1 - floor( $half ) ) );
$type = $first_part . '..' . $last_part;
}
return $type;
}
/**
* Invalidate sitemap cache.
*
* @since 3.2
*
* @param null|string $type The type to get the key for. Null for all caches.
*
* @return void
*/
public static function invalidate_storage( $type = null ) {
// Global validator gets cleared when no type is provided.
$old_validator = null;
// Get the current type validator.
if ( ! is_null( $type ) ) {
$old_validator = self::get_validator( $type );
}
// Refresh validator.
self::create_validator( $type );
if ( ! wp_using_ext_object_cache() ) {
// Clean up current cache from the database.
self::cleanup_database( $type, $old_validator );
}
// External object cache pushes old and unretrieved items out by itself so we don't have to do anything for that.
}
/**
* Cleanup invalidated database cache.
*
* @since 3.2
*
* @param null|string $type The type of sitemap to clear cache for.
* @param null|string $validator The validator to clear cache of.
*
* @return void
*/
public static function cleanup_database( $type = null, $validator = null ) {
global $wpdb;
if ( is_null( $type ) ) {
// Clear all cache if no type is provided.
$like = sprintf( '%s%%', self::STORAGE_KEY_PREFIX );
}
else {
// Clear type cache for all type keys.
$like = sprintf( '%1$s%2$s_%%', self::STORAGE_KEY_PREFIX, $type );
}
/*
* Add slashes to the LIKE "_" single character wildcard.
*
* We can't use `esc_like` here because we need the % in the query.
*/
$where = [];
$where[] = sprintf( "option_name LIKE '%s'", addcslashes( '_transient_' . $like, '_' ) );
$where[] = sprintf( "option_name LIKE '%s'", addcslashes( '_transient_timeout_' . $like, '_' ) );
// Delete transients.
$query = sprintf( 'DELETE FROM %1$s WHERE %2$s', $wpdb->options, implode( ' OR ', $where ) );
$wpdb->query( $query );
wp_cache_delete( 'alloptions', 'options' );
}
/**
* Get the current cache validator.
*
* Without the type the global validator is returned.
* This can invalidate -all- keys in cache at once.
*
* With the type parameter the validator for that specific type can be invalidated.
*
* @since 3.2
*
* @param string $type Provide a type for a specific type validator, empty for global validator.
*
* @return null|string The validator for the supplied type.
*/
public static function get_validator( $type = '' ) {
$key = self::get_validator_key( $type );
$current = get_option( $key, null );
if ( ! is_null( $current ) ) {
return $current;
}
if ( self::create_validator( $type ) ) {
return self::get_validator( $type );
}
return null;
}
/**
* Get the cache validator option key for the specified type.
*
* @since 3.2
*
* @param string $type Provide a type for a specific type validator, empty for global validator.
*
* @return string Validator to be used to generate the cache key.
*/
public static function get_validator_key( $type = '' ) {
if ( empty( $type ) ) {
return self::VALIDATION_GLOBAL_KEY;
}
return sprintf( self::VALIDATION_TYPE_KEY_FORMAT, $type );
}
/**
* Refresh the cache validator value.
*
* @since 3.2
*
* @param string $type Provide a type for a specific type validator, empty for global validator.
*
* @return bool True if validator key has been saved as option.
*/
public static function create_validator( $type = '' ) {
$key = self::get_validator_key( $type );
// Generate new validator.
$microtime = microtime();
// Remove space.
list( $milliseconds, $seconds ) = explode( ' ', $microtime );
// Transients are purged every 24h.
$seconds = ( $seconds % DAY_IN_SECONDS );
$milliseconds = intval( substr( $milliseconds, 2, 3 ), 10 );
// Combine seconds and milliseconds and convert to integer.
$validator = intval( $seconds . '' . $milliseconds, 10 );
// Apply base 61 encoding.
$compressed = self::convert_base10_to_base61( $validator );
return update_option( $key, $compressed, false );
}
/**
* Encode to base61 format.
*
* @since 3.2
*
* This is base64 (numeric + alpha + alpha upper case) without the 0.
*
* @param int $base10 The number that has to be converted to base 61.
*
* @return string Base 61 converted string.
*
* @throws InvalidArgumentException When the input is not an integer.
*/
public static function convert_base10_to_base61( $base10 ) {
if ( ! is_int( $base10 ) ) {
throw new InvalidArgumentException( __( 'Expected an integer as input.', 'wordpress-seo' ) );
}
// Characters that will be used in the conversion.
$characters = '123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$length = strlen( $characters );
$remainder = $base10;
$output = '';
do {
// Building from right to left in the result.
$index = ( $remainder % $length );
// Prepend the character to the output.
$output = $characters[ $index ] . $output;
// Determine the remainder after removing the applied number.
$remainder = floor( $remainder / $length );
// Keep doing it until we have no remainder left.
} while ( $remainder );
return $output;
}
}

View File

@@ -0,0 +1,347 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Handles sitemaps caching and invalidation.
*
* @since 3.2
*/
class WPSEO_Sitemaps_Cache {
/**
* Holds the options that, when updated, should cause the cache to clear.
*
* @var array
*/
protected static $cache_clear = [];
/**
* Mirror of enabled status for static calls.
*
* @var bool
*/
protected static $is_enabled = false;
/**
* Holds the flag to clear all cache.
*
* @var bool
*/
protected static $clear_all = false;
/**
* Holds the array of types to clear.
*
* @var array
*/
protected static $clear_types = [];
/**
* Hook methods for invalidation on necessary events.
*/
public function __construct() {
add_action( 'init', [ $this, 'init' ] );
add_action( 'deleted_term_relationships', [ __CLASS__, 'invalidate' ] );
add_action( 'update_option', [ __CLASS__, 'clear_on_option_update' ] );
add_action( 'edited_terms', [ __CLASS__, 'invalidate_helper' ], 10, 2 );
add_action( 'clean_term_cache', [ __CLASS__, 'invalidate_helper' ], 10, 2 );
add_action( 'clean_object_term_cache', [ __CLASS__, 'invalidate_helper' ], 10, 2 );
add_action( 'user_register', [ __CLASS__, 'invalidate_author' ] );
add_action( 'delete_user', [ __CLASS__, 'invalidate_author' ] );
add_action( 'shutdown', [ __CLASS__, 'clear_queued' ] );
}
/**
* Setup context for static calls.
*/
public function init() {
self::$is_enabled = $this->is_enabled();
}
/**
* If cache is enabled.
*
* @since 3.2
*
* @return boolean
*/
public function is_enabled() {
/**
* Filter if XML sitemap transient cache is enabled.
*
* @param bool $unsigned Enable cache or not, defaults to true.
*/
return apply_filters( 'wpseo_enable_xml_sitemap_transient_caching', false );
}
/**
* Retrieve the sitemap page from cache.
*
* @since 3.2
*
* @param string $type Sitemap type.
* @param int $page Page number to retrieve.
*
* @return string|boolean
*/
public function get_sitemap( $type, $page ) {
$transient_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page );
if ( $transient_key === false ) {
return false;
}
return get_transient( $transient_key );
}
/**
* Get the sitemap that is cached.
*
* @param string $type Sitemap type.
* @param int $page Page number to retrieve.
*
* @return null|WPSEO_Sitemap_Cache_Data Null on no cache found otherwise object containing sitemap and meta data.
*/
public function get_sitemap_data( $type, $page ) {
$sitemap = $this->get_sitemap( $type, $page );
if ( empty( $sitemap ) ) {
return null;
}
// Unserialize Cache Data object (is_serialized doesn't recognize classes).
if ( is_string( $sitemap ) && strpos( $sitemap, 'C:24:"WPSEO_Sitemap_Cache_Data"' ) === 0 ) {
$sitemap = unserialize( $sitemap );
}
// What we expect it to be if it is set.
if ( $sitemap instanceof WPSEO_Sitemap_Cache_Data_Interface ) {
return $sitemap;
}
return null;
}
/**
* Store the sitemap page from cache.
*
* @since 3.2
*
* @param string $type Sitemap type.
* @param int $page Page number to store.
* @param string $sitemap Sitemap body to store.
* @param bool $usable Is this a valid sitemap or a cache of an invalid sitemap.
*
* @return bool
*/
public function store_sitemap( $type, $page, $sitemap, $usable = true ) {
$transient_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page );
if ( $transient_key === false ) {
return false;
}
$status = ( $usable ) ? WPSEO_Sitemap_Cache_Data::OK : WPSEO_Sitemap_Cache_Data::ERROR;
$sitemap_data = new WPSEO_Sitemap_Cache_Data();
$sitemap_data->set_sitemap( $sitemap );
$sitemap_data->set_status( $status );
return set_transient( $transient_key, $sitemap_data, DAY_IN_SECONDS );
}
/**
* Delete cache transients for index and specific type.
*
* Always deletes the main index sitemaps cache, as that's always invalidated by any other change.
*
* @since 1.5.4
* @since 3.2 Changed from function wpseo_invalidate_sitemap_cache() to method in this class.
*
* @param string $type Sitemap type to invalidate.
*
* @return void
*/
public static function invalidate( $type ) {
self::clear( [ $type ] );
}
/**
* Helper to invalidate in hooks where type is passed as second argument.
*
* @since 3.2
*
* @param int $unused Unused term ID value.
* @param string $type Taxonomy to invalidate.
*
* @return void
*/
public static function invalidate_helper( $unused, $type ) {
if (
WPSEO_Options::get( 'noindex-' . $type ) === false ||
WPSEO_Options::get( 'noindex-tax-' . $type ) === false
) {
self::invalidate( $type );
}
}
/**
* Invalidate sitemap cache for authors.
*
* @param int $user_id User ID.
*
* @return bool True if the sitemap was properly invalidated. False otherwise.
*/
public static function invalidate_author( $user_id ) {
$user = get_user_by( 'id', $user_id );
if ( $user === false ) {
return false;
}
if ( current_action() === 'user_register' ) {
update_user_meta( $user_id, '_yoast_wpseo_profile_updated', time() );
}
if ( empty( $user->roles ) || in_array( 'subscriber', $user->roles, true ) ) {
return false;
}
self::invalidate( 'author' );
return true;
}
/**
* Invalidate sitemap cache for the post type of a post.
*
* Don't invalidate for revisions.
*
* @since 1.5.4
* @since 3.2 Changed from function wpseo_invalidate_sitemap_cache_on_save_post() to method in this class.
*
* @param int $post_id Post ID to invalidate type for.
*
* @return void
*/
public static function invalidate_post( $post_id ) {
if ( wp_is_post_revision( $post_id ) ) {
return;
}
self::invalidate( get_post_type( $post_id ) );
}
/**
* Delete cache transients for given sitemaps types or all by default.
*
* @since 1.8.0
* @since 3.2 Moved from WPSEO_Utils to this class.
*
* @param array $types Set of sitemap types to delete cache transients for.
*
* @return void
*/
public static function clear( $types = [] ) {
if ( ! self::$is_enabled ) {
return;
}
// No types provided, clear all.
if ( empty( $types ) ) {
self::$clear_all = true;
return;
}
// Always invalidate the index sitemap as well.
if ( ! in_array( WPSEO_Sitemaps::SITEMAP_INDEX_TYPE, $types ) ) {
array_unshift( $types, WPSEO_Sitemaps::SITEMAP_INDEX_TYPE );
}
foreach ( $types as $type ) {
if ( ! in_array( $type, self::$clear_types ) ) {
self::$clear_types[] = $type;
}
}
}
/**
* Invalidate storage for cache types queued to clear.
*/
public static function clear_queued() {
if ( self::$clear_all ) {
WPSEO_Sitemaps_Cache_Validator::invalidate_storage();
self::$clear_all = false;
self::$clear_types = [];
return;
}
foreach ( self::$clear_types as $type ) {
WPSEO_Sitemaps_Cache_Validator::invalidate_storage( $type );
}
self::$clear_types = [];
}
/**
* Adds a hook that when given option is updated, the cache is cleared.
*
* @since 3.2
*
* @param string $option Option name.
* @param string $type Sitemap type.
*/
public static function register_clear_on_option_update( $option, $type = '' ) {
self::$cache_clear[ $option ] = $type;
}
/**
* Clears the transient cache when a given option is updated, if that option has been registered before.
*
* @since 3.2
*
* @param string $option The option name that's being updated.
*
* @return void
*/
public static function clear_on_option_update( $option ) {
if ( array_key_exists( $option, self::$cache_clear ) ) {
if ( empty( self::$cache_clear[ $option ] ) ) {
// Clear all caches.
self::clear();
}
else {
// Clear specific provided type(s).
$types = (array) self::$cache_clear[ $option ];
self::clear( $types );
}
}
}
}

View File

@@ -0,0 +1,386 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Renders XML output for sitemaps.
*/
class WPSEO_Sitemaps_Renderer {
/**
* XSL stylesheet for styling a sitemap for web browsers.
*
* @var string
*/
protected $stylesheet = '';
/**
* Holds the get_bloginfo( 'charset' ) value to reuse for performance.
*
* @var string
*/
protected $charset = 'UTF-8';
/**
* Holds charset of output, might be converted.
*
* @var string
*/
protected $output_charset = 'UTF-8';
/**
* If data encoding needs to be converted for output.
*
* @var bool
*/
protected $needs_conversion = false;
/**
* The date helper.
*
* @var WPSEO_Date_Helper
*/
protected $date;
/**
* Set up object properties.
*/
public function __construct() {
$stylesheet_url = preg_replace( '/(^http[s]?:)/', '', $this->get_xsl_url() );
$this->stylesheet = '<?xml-stylesheet type="text/xsl" href="' . esc_url( $stylesheet_url ) . '"?>';
$this->charset = get_bloginfo( 'charset' );
$this->output_charset = $this->charset;
$this->date = new WPSEO_Date_Helper();
if (
$this->charset !== 'UTF-8'
&& function_exists( 'mb_list_encodings' )
&& in_array( $this->charset, mb_list_encodings(), true )
) {
$this->output_charset = 'UTF-8';
}
$this->needs_conversion = $this->output_charset !== $this->charset;
}
/**
* Builds the sitemap index.
*
* @param array $links Set of sitemaps index links.
*
* @return string
*/
public function get_index( $links ) {
$xml = '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
foreach ( $links as $link ) {
$xml .= $this->sitemap_index_url( $link );
}
/**
* Filter to append sitemaps to the index.
*
* @param string $index String to append to sitemaps index, defaults to empty.
*/
$xml .= apply_filters( 'wpseo_sitemap_index', '' );
$xml .= '</sitemapindex>';
return $xml;
}
/**
* Builds the sitemap.
*
* @param array $links Set of sitemap links.
* @param string $type Sitemap type.
* @param int $current_page Current sitemap page number.
*
* @return string
*/
public function get_sitemap( $links, $type, $current_page ) {
$urlset = '<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" '
. 'xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd '
. 'http://www.google.com/schemas/sitemap-image/1.1 http://www.google.com/schemas/sitemap-image/1.1/sitemap-image.xsd" '
. 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
/**
* Filters the `urlset` for a sitemap by type.
*
* @api string $urlset The output for the sitemap's `urlset`.
*/
$xml = apply_filters( "wpseo_sitemap_{$type}_urlset", $urlset );
foreach ( $links as $url ) {
$xml .= $this->sitemap_url( $url );
}
/**
* Filter to add extra URLs to the XML sitemap by type.
*
* Only runs for the first page, not on all.
*
* @param string $content String content to add, defaults to empty.
*/
if ( $current_page === 1 ) {
$xml .= apply_filters( "wpseo_sitemap_{$type}_content", '' );
}
$xml .= '</urlset>';
return $xml;
}
/**
* Produce final XML output with debug information.
*
* @param string $sitemap Sitemap XML.
* @param boolean $transient Transient cache flag.
*
* @return string
*/
public function get_output( $sitemap, $transient ) {
$output = '<?xml version="1.0" encoding="' . esc_attr( $this->output_charset ) . '"?>';
if ( $this->stylesheet ) {
/**
* Filter the stylesheet URL for the XML sitemap.
*
* @param string $stylesheet Stylesheet URL.
*/
$output .= apply_filters( 'wpseo_stylesheet_url', $this->stylesheet ) . "\n";
}
$output .= $sitemap;
$output .= "\n<!-- XML Sitemap generated by Yoast SEO -->";
$output .= $this->get_debug( $transient );
return $output;
}
/**
* Get charset for the output.
*
* @return string
*/
public function get_output_charset() {
return $this->output_charset;
}
/**
* Set a custom stylesheet for this sitemap. Set to empty to just remove the default stylesheet.
*
* @param string $stylesheet Full XML-stylesheet declaration.
*/
public function set_stylesheet( $stylesheet ) {
$this->stylesheet = $stylesheet;
}
/**
* Build the `<sitemap>` tag for a given URL.
*
* @param array $url Array of parts that make up this entry.
*
* @return string
*/
protected function sitemap_index_url( $url ) {
$date = null;
if ( ! empty( $url['lastmod'] ) ) {
$date = $this->date->format( $url['lastmod'] );
}
$url['loc'] = htmlspecialchars( $url['loc'], ENT_COMPAT, $this->output_charset, false );
$output = "\t<sitemap>\n";
$output .= "\t\t<loc>" . $url['loc'] . "</loc>\n";
$output .= empty( $date ) ? '' : "\t\t<lastmod>" . htmlspecialchars( $date, ENT_COMPAT, $this->output_charset, false ) . "</lastmod>\n";
$output .= "\t</sitemap>\n";
return $output;
}
/**
* Build the `<url>` tag for a given URL.
*
* Public access for backwards compatibility reasons.
*
* @param array $url Array of parts that make up this entry.
*
* @return string
*/
public function sitemap_url( $url ) {
$date = null;
if ( ! empty( $url['mod'] ) ) {
// Create a DateTime object date in the correct timezone.
$date = $this->date->format( $url['mod'] );
}
$url['loc'] = htmlspecialchars( $url['loc'], ENT_COMPAT, $this->output_charset, false );
$output = "\t<url>\n";
$output .= "\t\t<loc>" . $this->encode_url_rfc3986( $url['loc'] ) . "</loc>\n";
$output .= empty( $date ) ? '' : "\t\t<lastmod>" . htmlspecialchars( $date, ENT_COMPAT, $this->output_charset, false ) . "</lastmod>\n";
if ( empty( $url['images'] ) ) {
$url['images'] = [];
}
foreach ( $url['images'] as $img ) {
if ( empty( $img['src'] ) ) {
continue;
}
$output .= "\t\t<image:image>\n";
$output .= "\t\t\t<image:loc>" . esc_html( $this->encode_url_rfc3986( $img['src'] ) ) . "</image:loc>\n";
if ( ! empty( $img['title'] ) ) {
$title = $img['title'];
if ( $this->needs_conversion ) {
$title = mb_convert_encoding( $title, $this->output_charset, $this->charset );
}
$title = _wp_specialchars( html_entity_decode( $title, ENT_QUOTES, $this->output_charset ) );
$output .= "\t\t\t<image:title><![CDATA[{$title}]]></image:title>\n";
}
if ( ! empty( $img['alt'] ) ) {
$alt = $img['alt'];
if ( $this->needs_conversion ) {
$alt = mb_convert_encoding( $alt, $this->output_charset, $this->charset );
}
$alt = _wp_specialchars( html_entity_decode( $alt, ENT_QUOTES, $this->output_charset ) );
$output .= "\t\t\t<image:caption><![CDATA[{$alt}]]></image:caption>\n";
}
$output .= "\t\t</image:image>\n";
}
unset( $img, $title, $alt );
$output .= "\t</url>\n";
/**
* Filters the output for the sitemap URL tag.
*
* @api string $output The output for the sitemap url tag.
*
* @param array $url The sitemap URL array on which the output is based.
*/
return apply_filters( 'wpseo_sitemap_url', $output, $url );
}
/**
* Apply some best effort conversion to comply with RFC3986.
*
* @param string $url URL to encode.
*
* @return string
*/
protected function encode_url_rfc3986( $url ) {
if ( filter_var( $url, FILTER_VALIDATE_URL ) ) {
return $url;
}
$path = wp_parse_url( $url, PHP_URL_PATH );
if ( ! empty( $path ) && $path !== '/' ) {
$encoded_path = explode( '/', $path );
// First decode the path, to prevent double encoding.
$encoded_path = array_map( 'rawurldecode', $encoded_path );
$encoded_path = array_map( 'rawurlencode', $encoded_path );
$encoded_path = implode( '/', $encoded_path );
$encoded_path = str_replace( '%7E', '~', $encoded_path ); // PHP < 5.3.
$url = str_replace( $path, $encoded_path, $url );
}
$query = wp_parse_url( $url, PHP_URL_QUERY );
if ( ! empty( $query ) ) {
parse_str( $query, $parsed_query );
if ( defined( 'PHP_QUERY_RFC3986' ) ) { // PHP 5.4+.
$parsed_query = http_build_query( $parsed_query, null, '&amp;', PHP_QUERY_RFC3986 );
}
else {
$parsed_query = http_build_query( $parsed_query, null, '&amp;' );
$parsed_query = str_replace( '+', '%20', $parsed_query );
$parsed_query = str_replace( '%7E', '~', $parsed_query );
}
$url = str_replace( $query, $parsed_query, $url );
}
return $url;
}
/**
* Retrieves the XSL URL that should be used in the current environment
*
* When home_url and site_url are not the same, the home_url should be used.
* This is because the XSL needs to be served from the same domain, protocol and port
* as the XML file that is loading it.
*
* @return string The XSL URL that needs to be used.
*/
protected function get_xsl_url() {
if ( home_url() !== site_url() ) {
return home_url( 'main-sitemap.xsl' );
}
/*
* Fallback to circumvent a cross-domain security problem when the XLS file is
* loaded from a different (sub)domain.
*/
if ( strpos( plugins_url(), home_url() ) !== 0 ) {
return home_url( 'main-sitemap.xsl' );
}
return plugin_dir_url( WPSEO_FILE ) . 'css/main-sitemap.xsl';
}
/**
* Adds debugging information to the output.
*
* @param bool $transient Transient cache was used or not.
*
* @return string Information about the functionality used to build the sitemap.
*/
protected function get_debug( $transient ) {
$debug = defined( 'YOAST_SEO_DEBUG_SITEMAPS' ) && YOAST_SEO_DEBUG_SITEMAPS === true;
if ( ! $debug ) {
return '';
}
$memory_used = number_format( ( memory_get_peak_usage() / 1048576 ), 2 );
$queries_run = ( $transient ) ? 'Served from transient cache' : 'Queries executed ' . absint( $GLOBALS['wpdb']->num_queries );
$output = "\n<!-- {$memory_used}MB | {$queries_run} -->";
if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
$queries = print_r( $GLOBALS['wpdb']->queries, true );
$output .= "\n<!-- {$queries} -->";
}
return $output;
}
}

View File

@@ -0,0 +1,127 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Rewrite setup and handling for sitemaps functionality.
*/
class WPSEO_Sitemaps_Router {
/**
* Sets up init logic.
*/
public function __construct() {
add_action( 'init', [ $this, 'init' ], 1 );
add_filter( 'redirect_canonical', [ $this, 'redirect_canonical' ] );
add_action( 'template_redirect', [ $this, 'template_redirect' ], 0 );
}
/**
* Sets up rewrite rules.
*/
public function init() {
global $wp;
$wp->add_query_var( 'sitemap' );
$wp->add_query_var( 'sitemap_n' );
$wp->add_query_var( 'yoast-sitemap-xsl' );
add_rewrite_rule( 'sitemap_index\.xml$', 'index.php?sitemap=1', 'top' );
add_rewrite_rule( '([^/]+?)-sitemap([0-9]+)?\.xml$', 'index.php?sitemap=$matches[1]&sitemap_n=$matches[2]', 'top' );
add_rewrite_rule( '([a-z]+)?-?sitemap\.xsl$', 'index.php?yoast-sitemap-xsl=$matches[1]', 'top' );
}
/**
* Stop trailing slashes on sitemap.xml URLs.
*
* @param string $redirect The redirect URL currently determined.
*
* @return bool|string $redirect
*/
public function redirect_canonical( $redirect ) {
if ( get_query_var( 'sitemap' ) || get_query_var( 'yoast-sitemap-xsl' ) ) {
return false;
}
return $redirect;
}
/**
* Redirects sitemap.xml to sitemap_index.xml.
*/
public function template_redirect() {
if ( ! $this->needs_sitemap_index_redirect() ) {
return;
}
wp_redirect( home_url( '/sitemap_index.xml' ), 301, 'Yoast SEO' );
exit;
}
/**
* Checks whether the current request needs to be redirected to sitemap_index.xml.
*
* @global WP_Query $wp_query Current query.
*
* @return bool True if redirect is needed, false otherwise.
*/
public function needs_sitemap_index_redirect() {
global $wp_query;
$protocol = 'http://';
if ( ! empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] === 'on' ) {
$protocol = 'https://';
}
$domain = '';
if ( isset( $_SERVER['SERVER_NAME'] ) ) {
$domain = sanitize_text_field( wp_unslash( $_SERVER['SERVER_NAME'] ) );
}
$path = '';
if ( isset( $_SERVER['REQUEST_URI'] ) ) {
$path = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
}
// Due to different environment configurations, we need to check both SERVER_NAME and HTTP_HOST.
$check_urls = [ $protocol . $domain . $path ];
if ( ! empty( $_SERVER['HTTP_HOST'] ) ) {
$check_urls[] = $protocol . sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) . $path;
}
return $wp_query->is_404 && in_array( home_url( '/sitemap.xml' ), $check_urls, true );
}
/**
* Create base URL for the sitemap.
*
* @param string $page Page to append to the base URL.
*
* @return string base URL (incl page)
*/
public static function get_base_url( $page ) {
global $wp_rewrite;
$base = $wp_rewrite->using_index_permalinks() ? 'index.php/' : '/';
/**
* Filter the base URL of the sitemaps.
*
* @param string $base The string that should be added to home_url() to make the full base URL.
*/
$base = apply_filters( 'wpseo_sitemaps_base_url', $base );
/*
* Get the scheme from the configured home URL instead of letting WordPress
* determine the scheme based on the requested URI.
*/
return home_url( $base . $page, wp_parse_url( get_option( 'home' ), PHP_URL_SCHEME ) );
}
}

View File

@@ -0,0 +1,645 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Class WPSEO_Sitemaps.
*
* @todo This class could use a general description with some explanation on sitemaps. OR.
*/
class WPSEO_Sitemaps {
/**
* Sitemap index identifier.
*
* @var string
*/
const SITEMAP_INDEX_TYPE = '1';
/**
* Content of the sitemap to output.
*
* @var string
*/
protected $sitemap = '';
/**
* Flag to indicate if this is an invalid or empty sitemap.
*
* @var bool
*/
public $bad_sitemap = false;
/**
* Whether or not the XML sitemap was served from a transient or not.
*
* @var bool
*/
private $transient = false;
/**
* HTTP protocol to use in headers.
*
* @since 3.2
*
* @var string
*/
protected $http_protocol = 'HTTP/1.1';
/**
* Holds the n variable.
*
* @var int
*/
private $current_page = 1;
/**
* The sitemaps router.
*
* @since 3.2
*
* @var WPSEO_Sitemaps_Router
*/
public $router;
/**
* The sitemap renderer.
*
* @since 3.2
*
* @var WPSEO_Sitemaps_Renderer
*/
public $renderer;
/**
* The sitemap cache.
*
* @since 3.2
*
* @var WPSEO_Sitemaps_Cache
*/
public $cache;
/**
* The sitemap providers.
*
* @since 3.2
*
* @var WPSEO_Sitemap_Provider[]
*/
public $providers;
/**
* The date helper.
*
* @var WPSEO_Date_Helper
*/
protected $date;
/**
* Class constructor.
*/
public function __construct() {
add_action( 'after_setup_theme', [ $this, 'init_sitemaps_providers' ] );
add_action( 'after_setup_theme', [ $this, 'reduce_query_load' ], 99 );
add_action( 'pre_get_posts', [ $this, 'redirect' ], 1 );
add_action( 'wpseo_hit_sitemap_index', [ $this, 'hit_sitemap_index' ] );
add_action( 'wpseo_ping_search_engines', [ __CLASS__, 'ping_search_engines' ] );
$this->router = new WPSEO_Sitemaps_Router();
$this->renderer = new WPSEO_Sitemaps_Renderer();
$this->cache = new WPSEO_Sitemaps_Cache();
$this->date = new WPSEO_Date_Helper();
if ( ! empty( $_SERVER['SERVER_PROTOCOL'] ) ) {
$this->http_protocol = sanitize_text_field( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) );
}
}
/**
* Initialize sitemap providers classes.
*
* @since 5.3
*/
public function init_sitemaps_providers() {
$this->providers = [
new WPSEO_Post_Type_Sitemap_Provider(),
new WPSEO_Taxonomy_Sitemap_Provider(),
new WPSEO_Author_Sitemap_Provider(),
];
$external_providers = apply_filters( 'wpseo_sitemaps_providers', [] );
foreach ( $external_providers as $provider ) {
if ( is_object( $provider ) && $provider instanceof WPSEO_Sitemap_Provider ) {
$this->providers[] = $provider;
}
}
}
/**
* Check the current request URI, if we can determine it's probably an XML sitemap, kill loading the widgets.
*/
public function reduce_query_load() {
if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
return;
}
$request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
$extension = substr( $request_uri, -4 );
if ( stripos( $request_uri, 'sitemap' ) !== false && in_array( $extension, [ '.xml', '.xsl' ], true ) ) {
remove_all_actions( 'widgets_init' );
}
}
/**
* Register your own sitemap. Call this during 'init'.
*
* @param string $name The name of the sitemap.
* @param callback $function Function to build your sitemap.
* @param string $rewrite Optional. Regular expression to match your sitemap with.
*/
public function register_sitemap( $name, $function, $rewrite = '' ) {
add_action( 'wpseo_do_sitemap_' . $name, $function );
if ( ! empty( $rewrite ) ) {
add_rewrite_rule( $rewrite, 'index.php?sitemap=' . $name, 'top' );
}
}
/**
* Register your own XSL file. Call this during 'init'.
*
* @since 1.4.23
*
* @param string $name The name of the XSL file.
* @param callback $function Function to build your XSL file.
* @param string $rewrite Optional. Regular expression to match your sitemap with.
*/
public function register_xsl( $name, $function, $rewrite = '' ) {
add_action( 'wpseo_xsl_' . $name, $function );
if ( ! empty( $rewrite ) ) {
add_rewrite_rule( $rewrite, 'index.php?yoast-sitemap-xsl=' . $name, 'top' );
}
}
/**
* Set the sitemap current page to allow creating partial sitemaps with WP-CLI
* in a one-off process.
*
* @param integer $current_page The part that should be generated.
*/
public function set_n( $current_page ) {
if ( is_scalar( $current_page ) && intval( $current_page ) > 0 ) {
$this->current_page = intval( $current_page );
}
}
/**
* Set the sitemap content to display after you have generated it.
*
* @param string $sitemap The generated sitemap to output.
*/
public function set_sitemap( $sitemap ) {
$this->sitemap = $sitemap;
}
/**
* Set as true to make the request 404. Used stop the display of empty sitemaps or invalid requests.
*
* @param bool $bool Is this a bad request. True or false.
*/
public function set_bad_sitemap( $bool ) {
$this->bad_sitemap = (bool) $bool;
}
/**
* Prevent stupid plugins from running shutdown scripts when we're obviously not outputting HTML.
*
* @since 1.4.16
*/
public function sitemap_close() {
remove_all_actions( 'wp_footer' );
die();
}
/**
* Hijack requests for potential sitemaps and XSL files.
*
* @param \WP_Query $query Main query instance.
*/
public function redirect( $query ) {
if ( ! $query->is_main_query() ) {
return;
}
$yoast_sitemap_xsl = get_query_var( 'yoast-sitemap-xsl' );
if ( ! empty( $yoast_sitemap_xsl ) ) {
/*
* This is a method to provide the XSL via the home_url.
* Needed when the site_url and home_url are not the same.
* Loading the XSL needs to come from the same domain, protocol and port as the XML.
*
* Whenever home_url and site_url are the same, the file can be loaded directly.
*/
$this->xsl_output( $yoast_sitemap_xsl );
$this->sitemap_close();
return;
}
$type = get_query_var( 'sitemap' );
if ( empty( $type ) ) {
return;
}
$this->set_n( get_query_var( 'sitemap_n' ) );
if ( ! $this->get_sitemap_from_cache( $type, $this->current_page ) ) {
$this->build_sitemap( $type );
}
if ( $this->bad_sitemap ) {
$query->set_404();
status_header( 404 );
return;
}
$this->output();
$this->sitemap_close();
}
/**
* Try to get the sitemap from cache.
*
* @param string $type Sitemap type.
* @param int $page_number The page number to retrieve.
*
* @return bool If the sitemap has been retrieved from cache.
*/
private function get_sitemap_from_cache( $type, $page_number ) {
$this->transient = false;
if ( $this->cache->is_enabled() !== true ) {
return false;
}
/**
* Fires before the attempt to retrieve XML sitemap from the transient cache.
*
* @param WPSEO_Sitemaps $sitemaps Sitemaps object.
*/
do_action( 'wpseo_sitemap_stylesheet_cache_' . $type, $this );
$sitemap_cache_data = $this->cache->get_sitemap_data( $type, $page_number );
// No cache was found, refresh it because cache is enabled.
if ( empty( $sitemap_cache_data ) ) {
return $this->refresh_sitemap_cache( $type, $page_number );
}
// Cache object was found, parse information.
$this->transient = true;
$this->sitemap = $sitemap_cache_data->get_sitemap();
$this->bad_sitemap = ! $sitemap_cache_data->is_usable();
return true;
}
/**
* Build and save sitemap to cache.
*
* @param string $type Sitemap type.
* @param int $page_number The page number to save to.
*
* @return bool
*/
private function refresh_sitemap_cache( $type, $page_number ) {
$this->set_n( $page_number );
$this->build_sitemap( $type );
return $this->cache->store_sitemap( $type, $page_number, $this->sitemap, ! $this->bad_sitemap );
}
/**
* Attempts to build the requested sitemap.
*
* Sets $bad_sitemap if this isn't for the root sitemap, a post type or taxonomy.
*
* @param string $type The requested sitemap's identifier.
*/
public function build_sitemap( $type ) {
/**
* Filter the type of sitemap to build.
*
* @param string $type Sitemap type, determined by the request.
*/
$type = apply_filters( 'wpseo_build_sitemap_post_type', $type );
if ( $type === '1' ) {
$this->build_root_map();
return;
}
$entries_per_page = $this->get_entries_per_page();
foreach ( $this->providers as $provider ) {
if ( ! $provider->handles_type( $type ) ) {
continue;
}
try {
$links = $provider->get_sitemap_links( $type, $entries_per_page, $this->current_page );
} catch ( OutOfBoundsException $exception ) {
$this->bad_sitemap = true;
return;
}
$this->sitemap = $this->renderer->get_sitemap( $links, $type, $this->current_page );
return;
}
if ( has_action( 'wpseo_do_sitemap_' . $type ) ) {
/**
* Fires custom handler, if hooked to generate sitemap for the type.
*/
do_action( 'wpseo_do_sitemap_' . $type );
return;
}
$this->bad_sitemap = true;
}
/**
* Build the root sitemap (example.com/sitemap_index.xml) which lists sub-sitemaps for other content types.
*/
public function build_root_map() {
$links = [];
$entries_per_page = $this->get_entries_per_page();
foreach ( $this->providers as $provider ) {
$links = array_merge( $links, $provider->get_index_links( $entries_per_page ) );
}
if ( empty( $links ) ) {
$this->bad_sitemap = true;
$this->sitemap = '';
return;
}
$this->sitemap = $this->renderer->get_index( $links );
}
/**
* Spits out the XSL for the XML sitemap.
*
* @param string $type Type to output.
*
* @since 1.4.13
*/
public function xsl_output( $type ) {
if ( $type !== 'main' ) {
/**
* Fires for the output of XSL for XML sitemaps, other than type "main".
*/
do_action( 'wpseo_xsl_' . $type );
return;
}
header( $this->http_protocol . ' 200 OK', true, 200 );
// Prevent the search engines from indexing the XML Sitemap.
header( 'X-Robots-Tag: noindex, follow', true );
header( 'Content-Type: text/xml' );
// Make the browser cache this file properly.
$expires = YEAR_IN_SECONDS;
header( 'Pragma: public' );
header( 'Cache-Control: maxage=' . $expires );
header( 'Expires: ' . $this->date->format_timestamp( ( time() + $expires ), 'D, d M Y H:i:s' ) . ' GMT' );
readfile( WPSEO_PATH . 'css/main-sitemap.xsl' );
}
/**
* Spit out the generated sitemap.
*/
public function output() {
$this->send_headers();
echo $this->renderer->get_output( $this->sitemap, $this->transient );
}
/**
* Makes a request to the sitemap index to cache it before the arrival of the search engines.
*
* @return void
*/
public function hit_sitemap_index() {
if ( ! $this->cache->is_enabled() ) {
return;
}
wp_remote_get( WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ) );
}
/**
* Get the GMT modification date for the last modified post in the post type.
*
* @since 3.2
*
* @param string|array $post_types Post type or array of types.
* @param bool $return_all Flag to return array of values.
*
* @return string|array|false
*/
public static function get_last_modified_gmt( $post_types, $return_all = false ) {
global $wpdb;
static $post_type_dates = null;
if ( ! is_array( $post_types ) ) {
$post_types = [ $post_types ];
}
foreach ( $post_types as $post_type ) {
if ( ! isset( $post_type_dates[ $post_type ] ) ) { // If we hadn't seen post type before. R.
$post_type_dates = null;
break;
}
}
if ( is_null( $post_type_dates ) ) {
$post_type_dates = [];
$post_type_names = WPSEO_Post_Type::get_accessible_post_types();
if ( ! empty( $post_type_names ) ) {
$post_statuses = array_map( 'esc_sql', self::get_post_statuses() );
$sql = "
SELECT post_type, MAX(post_modified_gmt) AS date
FROM $wpdb->posts
WHERE post_status IN ('" . implode( "','", $post_statuses ) . "')
AND post_type IN ('" . implode( "','", $post_type_names ) . "')
GROUP BY post_type
ORDER BY post_modified_gmt DESC
";
foreach ( $wpdb->get_results( $sql ) as $obj ) {
$post_type_dates[ $obj->post_type ] = $obj->date;
}
}
}
$dates = array_intersect_key( $post_type_dates, array_flip( $post_types ) );
if ( count( $dates ) > 0 ) {
if ( $return_all ) {
return $dates;
}
return max( $dates );
}
return false;
}
/**
* Get the modification date for the last modified post in the post type.
*
* @param array $post_types Post types to get the last modification date for.
*
* @return string
*/
public function get_last_modified( $post_types ) {
return $this->date->format( self::get_last_modified_gmt( $post_types ) );
}
/**
* Notify search engines of the updated sitemap.
*
* @param string|null $url Optional URL to make the ping for.
*/
public static function ping_search_engines( $url = null ) {
/**
* Filter: 'wpseo_allow_xml_sitemap_ping' - Check if pinging is not allowed (allowed by default)
*
* @api boolean $allow_ping The boolean that is set to true by default.
*/
if ( apply_filters( 'wpseo_allow_xml_sitemap_ping', true ) === false ) {
return;
}
if ( get_option( 'blog_public' ) === '0' ) { // Don't ping if blog is not public.
return;
}
if ( empty( $url ) ) {
$url = urlencode( WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ) );
}
// Ping Google and Bing.
wp_remote_get( 'https://www.google.com/ping?sitemap=' . $url, [ 'blocking' => false ] );
wp_remote_get( 'https://www.bing.com/ping?sitemap=' . $url, [ 'blocking' => false ] );
}
/**
* Get the maximum number of entries per XML sitemap.
*
* @return int The maximum number of entries.
*/
protected function get_entries_per_page() {
/**
* Filter the maximum number of entries per XML sitemap.
*
* After changing the output of the filter, make sure that you disable and enable the
* sitemaps to make sure the value is picked up for the sitemap cache.
*
* @param int $entries The maximum number of entries per XML sitemap.
*/
$entries = (int) apply_filters( 'wpseo_sitemap_entries_per_page', 1000 );
return $entries;
}
/**
* Get post statuses for post_type or the root sitemap.
*
* @param string $type Provide a type for a post_type sitemap, SITEMAP_INDEX_TYPE for the root sitemap.
*
* @since 10.2
*
* @return array List of post statuses.
*/
public static function get_post_statuses( $type = self::SITEMAP_INDEX_TYPE ) {
/**
* Filter post status list for sitemap query for the post type.
*
* @param array $post_statuses Post status list, defaults to array( 'publish' ).
* @param string $type Post type or SITEMAP_INDEX_TYPE.
*/
$post_statuses = apply_filters( 'wpseo_sitemap_post_statuses', [ 'publish' ], $type );
if ( ! is_array( $post_statuses ) || empty( $post_statuses ) ) {
$post_statuses = [ 'publish' ];
}
if ( ( $type === self::SITEMAP_INDEX_TYPE || $type === 'attachment' )
&& ! in_array( 'inherit', $post_statuses, true )
) {
$post_statuses[] = 'inherit';
}
return $post_statuses;
}
/**
* Sends all the required HTTP Headers.
*/
private function send_headers() {
if ( headers_sent() ) {
return;
}
$headers = [
$this->http_protocol . ' 200 OK' => 200,
// Prevent the search engines from indexing the XML Sitemap.
'X-Robots-Tag: noindex, follow' => '',
'Content-Type: text/xml; charset=' . esc_attr( $this->renderer->get_output_charset() ) => '',
];
/**
* Filter the HTTP headers we send before an XML sitemap.
*
* @param array $headers The HTTP headers we're going to send out.
*/
$headers = apply_filters( 'wpseo_sitemap_http_headers', $headers );
foreach ( $headers as $header => $status ) {
if ( is_numeric( $status ) ) {
header( $header, true, $status );
continue;
}
header( $header, true );
}
}
}

View File

@@ -0,0 +1,330 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Sitemap provider for author archives.
*/
class WPSEO_Taxonomy_Sitemap_Provider implements WPSEO_Sitemap_Provider {
/**
* Holds image parser instance.
*
* @var WPSEO_Sitemap_Image_Parser
*/
protected static $image_parser;
/**
* Determines whether images should be included in the XML sitemap.
*
* @var bool
*/
private $include_images;
/**
* Set up object properties for data reuse.
*/
public function __construct() {
/**
* Filter - Allows excluding images from the XML sitemap.
*
* @param bool unsigned True to include, false to exclude.
*/
$this->include_images = apply_filters( 'wpseo_xml_sitemap_include_images', true );
}
/**
* Check if provider supports given item type.
*
* @param string $type Type string to check for.
*
* @return boolean
*/
public function handles_type( $type ) {
$taxonomy = get_taxonomy( $type );
if ( $taxonomy === false || ! $this->is_valid_taxonomy( $taxonomy->name ) || ! $taxonomy->public ) {
return false;
}
return true;
}
/**
* Retrieves the links for the sitemap.
*
* @param int $max_entries Entries per sitemap.
*
* @return array
*/
public function get_index_links( $max_entries ) {
$taxonomies = get_taxonomies( [ 'public' => true ], 'objects' );
if ( empty( $taxonomies ) ) {
return [];
}
$taxonomy_names = array_filter( array_keys( $taxonomies ), [ $this, 'is_valid_taxonomy' ] );
$taxonomies = array_intersect_key( $taxonomies, array_flip( $taxonomy_names ) );
// Retrieve all the taxonomies and their terms so we can do a proper count on them.
/**
* Filter the setting of excluding empty terms from the XML sitemap.
*
* @param boolean $exclude Defaults to true.
* @param array $taxonomy_names Array of names for the taxonomies being processed.
*/
$hide_empty = apply_filters( 'wpseo_sitemap_exclude_empty_terms', true, $taxonomy_names );
$all_taxonomies = [];
foreach ( $taxonomy_names as $taxonomy_name ) {
/**
* Filter the setting of excluding empty terms from the XML sitemap for a specific taxonomy.
*
* @param boolean $exclude Defaults to the sitewide setting.
* @param string $taxonomy_name The name of the taxonomy being processed.
*/
$hide_empty_tax = apply_filters( 'wpseo_sitemap_exclude_empty_terms_taxonomy', $hide_empty, $taxonomy_name );
$term_args = [
'hide_empty' => $hide_empty_tax,
'fields' => 'ids',
];
$taxonomy_terms = get_terms( $taxonomy_name, $term_args );
if ( count( $taxonomy_terms ) > 0 ) {
$all_taxonomies[ $taxonomy_name ] = $taxonomy_terms;
}
}
$index = [];
foreach ( $taxonomies as $tax_name => $tax ) {
if ( ! isset( $all_taxonomies[ $tax_name ] ) ) { // No eligible terms found.
continue;
}
$total_count = ( isset( $all_taxonomies[ $tax_name ] ) ) ? count( $all_taxonomies[ $tax_name ] ) : 1;
$max_pages = 1;
if ( $total_count > $max_entries ) {
$max_pages = (int) ceil( $total_count / $max_entries );
}
$last_modified_gmt = WPSEO_Sitemaps::get_last_modified_gmt( $tax->object_type );
for ( $page_counter = 0; $page_counter < $max_pages; $page_counter++ ) {
$current_page = ( $max_pages > 1 ) ? ( $page_counter + 1 ) : '';
if ( ! is_array( $tax->object_type ) || count( $tax->object_type ) === 0 ) {
continue;
}
$terms = array_splice( $all_taxonomies[ $tax_name ], 0, $max_entries );
if ( ! $terms ) {
continue;
}
$args = [
'post_type' => $tax->object_type,
'tax_query' => [
[
'taxonomy' => $tax_name,
'terms' => $terms,
],
],
'orderby' => 'modified',
'order' => 'DESC',
'posts_per_page' => 1,
];
$query = new WP_Query( $args );
if ( $query->have_posts() ) {
$date = $query->posts[0]->post_modified_gmt;
}
else {
$date = $last_modified_gmt;
}
$index[] = [
'loc' => WPSEO_Sitemaps_Router::get_base_url( $tax_name . '-sitemap' . $current_page . '.xml' ),
'lastmod' => $date,
];
}
}
return $index;
}
/**
* Get set of sitemap link data.
*
* @param string $type Sitemap type.
* @param int $max_entries Entries per sitemap.
* @param int $current_page Current page of the sitemap.
*
* @throws OutOfBoundsException When an invalid page is requested.
*
* @return array
*/
public function get_sitemap_links( $type, $max_entries, $current_page ) {
global $wpdb;
$links = [];
if ( ! $this->handles_type( $type ) ) {
return $links;
}
$taxonomy = get_taxonomy( $type );
$steps = $max_entries;
$offset = ( $current_page > 1 ) ? ( ( $current_page - 1 ) * $max_entries ) : 0;
/** This filter is documented in inc/sitemaps/class-taxonomy-sitemap-provider.php */
$hide_empty = apply_filters( 'wpseo_sitemap_exclude_empty_terms', true, [ $taxonomy->name ] );
/** This filter is documented in inc/sitemaps/class-taxonomy-sitemap-provider.php */
$hide_empty_tax = apply_filters( 'wpseo_sitemap_exclude_empty_terms_taxonomy', $hide_empty, $taxonomy->name );
$terms = get_terms( $taxonomy->name, [ 'hide_empty' => $hide_empty_tax ] );
// If the total term count is lower than the offset, we are on an invalid page.
if ( count( $terms ) < $offset ) {
throw new OutOfBoundsException( 'Invalid sitemap page requested' );
}
$terms = array_splice( $terms, $offset, $steps );
if ( empty( $terms ) ) {
return $links;
}
$post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses() );
// Grab last modified date.
$sql = "
SELECT MAX(p.post_modified_gmt) AS lastmod
FROM $wpdb->posts AS p
INNER JOIN $wpdb->term_relationships AS term_rel
ON term_rel.object_id = p.ID
INNER JOIN $wpdb->term_taxonomy AS term_tax
ON term_tax.term_taxonomy_id = term_rel.term_taxonomy_id
AND term_tax.taxonomy = %s
AND term_tax.term_id = %d
WHERE p.post_status IN ('" . implode( "','", $post_statuses ) . "')
AND p.post_password = ''
";
/**
* Filter: 'wpseo_exclude_from_sitemap_by_term_ids' - Allow excluding terms by ID.
*
* @api array $terms_to_exclude The terms to exclude.
*/
$terms_to_exclude = apply_filters( 'wpseo_exclude_from_sitemap_by_term_ids', [] );
foreach ( $terms as $term ) {
if ( in_array( $term->term_id, $terms_to_exclude, true ) ) {
continue;
}
$url = [];
$tax_noindex = WPSEO_Taxonomy_Meta::get_term_meta( $term, $term->taxonomy, 'noindex' );
if ( $tax_noindex === 'noindex' ) {
continue;
}
$url['loc'] = WPSEO_Taxonomy_Meta::get_term_meta( $term, $term->taxonomy, 'canonical' );
if ( ! is_string( $url['loc'] ) || $url['loc'] === '' ) {
$url['loc'] = get_term_link( $term, $term->taxonomy );
}
$url['mod'] = $wpdb->get_var( $wpdb->prepare( $sql, $term->taxonomy, $term->term_id ) );
if ( $this->include_images ) {
$url['images'] = $this->get_image_parser()->get_term_images( $term );
}
// Deprecated, kept for backwards data compat. R.
$url['chf'] = 'daily';
$url['pri'] = 1;
/** This filter is documented at inc/sitemaps/class-post-type-sitemap-provider.php */
$url = apply_filters( 'wpseo_sitemap_entry', $url, 'term', $term );
if ( ! empty( $url ) ) {
$links[] = $url;
}
}
return $links;
}
/**
* Check if taxonomy by name is valid to appear in sitemaps.
*
* @param string $taxonomy_name Taxonomy name to check.
*
* @return bool
*/
public function is_valid_taxonomy( $taxonomy_name ) {
if ( WPSEO_Options::get( "noindex-tax-{$taxonomy_name}" ) === true ) {
return false;
}
if ( in_array( $taxonomy_name, [ 'link_category', 'nav_menu' ], true ) ) {
return false;
}
if ( $taxonomy_name === 'post_format' && WPSEO_Options::get( 'disable-post_format', false ) ) {
return false;
}
/**
* Filter to exclude the taxonomy from the XML sitemap.
*
* @param boolean $exclude Defaults to false.
* @param string $taxonomy_name Name of the taxonomy to exclude..
*/
if ( apply_filters( 'wpseo_sitemap_exclude_taxonomy', false, $taxonomy_name ) ) {
return false;
}
return true;
}
/**
* Get the Image Parser.
*
* @return WPSEO_Sitemap_Image_Parser
*/
protected function get_image_parser() {
if ( ! isset( self::$image_parser ) ) {
self::$image_parser = new WPSEO_Sitemap_Image_Parser();
}
return self::$image_parser;
}
/* ********************* DEPRECATED METHODS ********************* */
/**
* Get all the options.
*
* @deprecated 7.0
* @codeCoverageIgnore
*/
protected function get_options() {
_deprecated_function( __METHOD__, 'WPSEO 7.0', 'WPSEO_Options::get' );
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Cache Data interface.
*/
interface WPSEO_Sitemap_Cache_Data_Interface {
/**
* Status for normal, usable sitemap.
*
* @var string
*/
const OK = 'ok';
/**
* Status for unusable sitemap.
*
* @var string
*/
const ERROR = 'error';
/**
* Status for unusable sitemap because it cannot be identified.
*
* @var string
*/
const UNKNOWN = 'unknown';
/**
* Set the content of the sitemap.
*
* @param string $sitemap The XML content of the sitemap.
*
* @return void
*/
public function set_sitemap( $sitemap );
/**
* Set the status of the sitemap.
*
* @param bool|string $usable True/False or 'ok'/'error' for status.
*
* @return void
*/
public function set_status( $usable );
/**
* Builds the sitemap.
*
* @return string The XML content of the sitemap.
*/
public function get_sitemap();
/**
* Get the status of this sitemap.
*
* @return string Status 'ok', 'error' or 'unknown'.
*/
public function get_status();
/**
* Is the sitemap content usable ?
*
* @return bool True if the sitemap is usable, False if not.
*/
public function is_usable();
}

View File

@@ -0,0 +1,41 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Sitemap Provider interface.
*/
interface WPSEO_Sitemap_Provider {
/**
* Check if provider supports given item type.
*
* @param string $type Type string to check for.
*
* @return boolean
*/
public function handles_type( $type );
/**
* Get set of sitemaps index link data.
*
* @param int $max_entries Entries per sitemap.
*
* @return array
*/
public function get_index_links( $max_entries );
/**
* Get set of sitemap link data.
*
* @param string $type Sitemap type.
* @param int $max_entries Entries per sitemap.
* @param int $current_page Current page of the sitemap.
*
* @return array
*/
public function get_sitemap_links( $type, $max_entries, $current_page );
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Deprecated
*/
/**
* Adds help tabs.
*
* @deprecated 7.6.0
* @codeCoverageIgnore
*
* @param array $tabs Current help center tabs.
*
* @return array List containing all the additional tabs.
*/
function yoast_add_meta_options_help_center_tabs( $tabs ) {
_deprecated_function( __FUNCTION__, 'WPSEO 7.6.0', 'WPSEO_Help_Center_Template_Variables_Tab::add_meta_options_help_center_tabs' );
return $tabs;
}
/**
* Adds template variables to the help center.
*
* @deprecated 7.6.0
* @codeCoverageIgnore
*
* @return string The content for the template variables tab.
*/
function wpseo_add_template_variables_helpcenter() {
_deprecated_function( __FUNCTION__, 'WPSEO 7.6.0' );
return '';
}

View File

@@ -0,0 +1,258 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
*/
if ( ! defined( 'WPSEO_VERSION' ) ) {
header( 'Status: 403 Forbidden' );
header( 'HTTP/1.1 403 Forbidden' );
exit();
}
if ( ! function_exists( 'initialize_wpseo_front' ) ) {
/**
* Wraps frontend class.
*/
function initialize_wpseo_front() {
WPSEO_Frontend::get_instance();
}
}
if ( ! function_exists( 'yoast_breadcrumb' ) ) {
/**
* Template tag for breadcrumbs.
*
* @param string $before What to show before the breadcrumb.
* @param string $after What to show after the breadcrumb.
* @param bool $display Whether to display the breadcrumb (true) or return it (false).
*
* @return string
*/
function yoast_breadcrumb( $before = '', $after = '', $display = true ) {
$breadcrumbs_enabled = current_theme_supports( 'yoast-seo-breadcrumbs' );
if ( ! $breadcrumbs_enabled ) {
$breadcrumbs_enabled = WPSEO_Options::get( 'breadcrumbs-enable', false );
}
if ( $breadcrumbs_enabled ) {
return WPSEO_Breadcrumbs::breadcrumb( $before, $after, $display );
}
}
}
if ( ! function_exists( 'yoast_get_primary_term_id' ) ) {
/**
* Get the primary term ID.
*
* @param string $taxonomy Optional. The taxonomy to get the primary term ID for. Defaults to category.
* @param null|int|WP_Post $post Optional. Post to get the primary term ID for.
*
* @return bool|int
*/
function yoast_get_primary_term_id( $taxonomy = 'category', $post = null ) {
$post = get_post( $post );
$primary_term = new WPSEO_Primary_Term( $taxonomy, $post->ID );
return $primary_term->get_primary_term();
}
}
if ( ! function_exists( 'yoast_get_primary_term' ) ) {
/**
* Get the primary term name.
*
* @param string $taxonomy Optional. The taxonomy to get the primary term for. Defaults to category.
* @param null|int|WP_Post $post Optional. Post to get the primary term for.
*
* @return string Name of the primary term.
*/
function yoast_get_primary_term( $taxonomy = 'category', $post = null ) {
$primary_term_id = yoast_get_primary_term_id( $taxonomy, $post );
$term = get_term( $primary_term_id );
if ( ! is_wp_error( $term ) && ! empty( $term ) ) {
return $term->name;
}
return '';
}
}
/**
* Replace `%%variable_placeholders%%` with their real value based on the current requested page/post/cpt.
*
* @param string $string The string to replace the variables in.
* @param object $args The object some of the replacement values might come from,
* could be a post, taxonomy or term.
* @param array $omit Variables that should not be replaced by this function.
*
* @return string
*/
function wpseo_replace_vars( $string, $args, $omit = [] ) {
$replacer = new WPSEO_Replace_Vars();
return $replacer->replace( $string, $args, $omit );
}
/**
* Register a new variable replacement.
*
* This function is for use by other plugins/themes to easily add their own additional variables to replace.
* This function should be called from a function on the 'wpseo_register_extra_replacements' action hook.
* The use of this function is preferred over the older 'wpseo_replacements' filter as a way to add new replacements.
* The 'wpseo_replacements' filter should still be used to adjust standard WPSEO replacement values.
* The function can not be used to replace standard WPSEO replacement value functions and will thrown a warning
* if you accidently try.
* To avoid conflicts with variables registered by WPSEO and other themes/plugins, try and make the
* name of your variable unique. Variable names also can not start with "%%cf_" or "%%ct_" as these are reserved
* for the standard WPSEO variable variables 'cf_<custom-field-name>', 'ct_<custom-tax-name>' and
* 'ct_desc_<custom-tax-name>'.
* The replacement function will be passed the undelimited name (i.e. stripped of the %%) of the variable
* to replace in case you need it.
*
* Example code:
* <code>
* <?php
* function retrieve_var1_replacement( $var1 ) {
* return 'your replacement value';
* }
*
* function register_my_plugin_extra_replacements() {
* wpseo_register_var_replacement( '%%myvar1%%', 'retrieve_var1_replacement', 'advanced', 'this is a help text for myvar1' );
* wpseo_register_var_replacement( 'myvar2', array( 'class', 'method_name' ), 'basic', 'this is a help text for myvar2' );
* }
* add_action( 'wpseo_register_extra_replacements', 'register_my_plugin_extra_replacements' );
* ?>
* </code>
*
* @since 1.5.4
*
* @param string $var The name of the variable to replace, i.e. '%%var%%'.
* Note: the surrounding %% are optional, name can only contain [A-Za-z0-9_-].
* @param mixed $replace_function Function or method to call to retrieve the replacement value for the variable.
* Uses the same format as add_filter/add_action function parameter and
* should *return* the replacement value. DON'T echo it.
* @param string $type Type of variable: 'basic' or 'advanced', defaults to 'advanced'.
* @param string $help_text Help text to be added to the help tab for this variable.
*
* @return bool Whether the replacement function was successfully registered.
*/
function wpseo_register_var_replacement( $var, $replace_function, $type = 'advanced', $help_text = '' ) {
return WPSEO_Replace_Vars::register_replacement( $var, $replace_function, $type, $help_text );
}
/**
* WPML plugin support: Set titles for custom types / taxonomies as translatable.
*
* It adds new keys to a wpml-config.xml file for a custom post type title, metadesc,
* title-ptarchive and metadesc-ptarchive fields translation.
* Documentation: http://wpml.org/documentation/support/language-configuration-files/
*
* @global $sitepress
*
* @param array $config WPML configuration data to filter.
*
* @return array
*/
function wpseo_wpml_config( $config ) {
global $sitepress;
if ( ( is_array( $config ) && isset( $config['wpml-config']['admin-texts']['key'] ) ) && ( is_array( $config['wpml-config']['admin-texts']['key'] ) && $config['wpml-config']['admin-texts']['key'] !== [] ) ) {
$admin_texts = $config['wpml-config']['admin-texts']['key'];
foreach ( $admin_texts as $k => $val ) {
if ( $val['attr']['name'] === 'wpseo_titles' ) {
$translate_cp = array_keys( $sitepress->get_translatable_documents() );
if ( is_array( $translate_cp ) && $translate_cp !== [] ) {
foreach ( $translate_cp as $post_type ) {
$admin_texts[ $k ]['key'][]['attr']['name'] = 'title-' . $post_type;
$admin_texts[ $k ]['key'][]['attr']['name'] = 'metadesc-' . $post_type;
$admin_texts[ $k ]['key'][]['attr']['name'] = 'title-ptarchive-' . $post_type;
$admin_texts[ $k ]['key'][]['attr']['name'] = 'metadesc-ptarchive-' . $post_type;
$translate_tax = $sitepress->get_translatable_taxonomies( false, $post_type );
if ( is_array( $translate_tax ) && $translate_tax !== [] ) {
foreach ( $translate_tax as $taxonomy ) {
$admin_texts[ $k ]['key'][]['attr']['name'] = 'title-tax-' . $taxonomy;
$admin_texts[ $k ]['key'][]['attr']['name'] = 'metadesc-tax-' . $taxonomy;
}
}
}
}
break;
}
}
$config['wpml-config']['admin-texts']['key'] = $admin_texts;
}
return $config;
}
add_filter( 'icl_wpml_config_array', 'wpseo_wpml_config' );
/**
* Yoast SEO breadcrumb shortcode.
* [wpseo_breadcrumb]
*
* @return string
*/
function wpseo_shortcode_yoast_breadcrumb() {
return yoast_breadcrumb( '', '', false );
}
add_shortcode( 'wpseo_breadcrumb', 'wpseo_shortcode_yoast_breadcrumb' );
if ( ! extension_loaded( 'ctype' ) || ! function_exists( 'ctype_digit' ) ) {
/**
* Emulate PHP native ctype_digit() function for when the ctype extension would be disabled *sigh*.
* Only emulates the behaviour for when the input is a string, does not handle integer input as ascii value.
*
* @param string $string String input to validate.
*
* @return bool
*/
function ctype_digit( $string ) {
$return = false;
if ( ( is_string( $string ) && $string !== '' ) && preg_match( '`^\d+$`', $string ) === 1 ) {
$return = true;
}
return $return;
}
}
/**
* Makes sure the taxonomy meta is updated when a taxonomy term is split.
*
* @link https://make.wordpress.org/core/2015/02/16/taxonomy-term-splitting-in-4-2-a-developer-guide/ Article explaining the taxonomy term splitting in WP 4.2.
*
* @param string $old_term_id Old term id of the taxonomy term that was splitted.
* @param string $new_term_id New term id of the taxonomy term that was splitted.
* @param string $term_taxonomy_id Term taxonomy id for the taxonomy that was affected.
* @param string $taxonomy The taxonomy that the taxonomy term was splitted for.
*/
function wpseo_split_shared_term( $old_term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) {
$tax_meta = get_option( 'wpseo_taxonomy_meta', [] );
if ( ! empty( $tax_meta[ $taxonomy ][ $old_term_id ] ) ) {
$tax_meta[ $taxonomy ][ $new_term_id ] = $tax_meta[ $taxonomy ][ $old_term_id ];
unset( $tax_meta[ $taxonomy ][ $old_term_id ] );
update_option( 'wpseo_taxonomy_meta', $tax_meta );
}
}
add_action( 'split_shared_term', 'wpseo_split_shared_term', 10, 4 );
/**
* Get all WPSEO related capabilities.
*
* @since 8.3
* @return array
*/
function wpseo_get_capabilities() {
if ( ! did_action( 'wpseo_register_capabilities' ) ) {
do_action( 'wpseo_register_capabilities' );
}
return WPSEO_Capability_Manager_Factory::get()->get_capabilities();
}

View File

@@ -0,0 +1,211 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
*/
if ( ! defined( 'WPSEO_VERSION' ) ) {
header( 'Status: 403 Forbidden' );
header( 'HTTP/1.1 403 Forbidden' );
exit();
}
/**
* Initializes the admin bar.
*
* @return void
*/
function wpseo_initialize_admin_bar() {
$admin_bar_menu = new WPSEO_Admin_Bar_Menu();
$admin_bar_menu->register_hooks();
}
add_action( 'wp_loaded', 'wpseo_initialize_admin_bar' );
/**
* Allows editing of the meta fields through weblog editors like Marsedit.
*
* @param array $required_capabilities Capabilities that must all be true to allow action.
* @param array $capabilities Array of capabilities to be checked, unused here.
* @param array $args List of arguments for the specific capabilities to be checked.
*
* @return array $required_capabilities Filtered capabilities.
*/
function allow_custom_field_edits( $required_capabilities, $capabilities, $args ) {
if ( ! in_array( $args[0], [ 'edit_post_meta', 'add_post_meta' ], true ) ) {
return $required_capabilities;
}
// If this is provided, it is the post ID.
if ( empty( $args[2] ) ) {
return $required_capabilities;
}
// If this is provided, it is the custom field.
if ( empty( $args[3] ) ) {
return $required_capabilities;
}
// If the meta key is part of the plugin, grant capabilities accordingly.
if ( strpos( $args[3], WPSEO_Meta::$meta_prefix ) === 0 && current_user_can( 'edit_post', $args[2] ) ) {
$required_capabilities[ $args[0] ] = true;
}
return $required_capabilities;
}
add_filter( 'user_has_cap', 'allow_custom_field_edits', 0, 3 );
/* ********************* DEPRECATED FUNCTIONS ********************* */
/**
* Adds an SEO admin bar menu to the site admin, with several options.
*
* If the current user is an admin they can also go straight to several settings menus from here.
*
* @deprecated 7.9 Use WPSEO_Admin_Bar_Menu::add_menu() instead.
* @codeCoverageIgnore
*
* @return void
*/
function wpseo_admin_bar_menu() {
_deprecated_function( __FUNCTION__, 'WPSEO 7.9', 'WPSEO_Admin_Bar_Menu::add_menu()' );
// Only use this admin bar menu for the site admin.
if ( is_admin() && ! is_blog_admin() ) {
return;
}
if ( WPSEO_Options::get( 'enable_admin_bar_menu', true ) ) {
return;
}
global $wp_admin_bar;
$admin_bar_menu = new WPSEO_Admin_Bar_Menu();
$admin_bar_menu->add_menu( $wp_admin_bar );
}
/**
* Returns the SEO score element for the admin bar.
*
* @deprecated 7.9
* @codeCoverageIgnore
*
* @return string
*/
function wpseo_adminbar_seo_score() {
_deprecated_function( __FUNCTION__, 'WPSEO 7.9', null );
$rating = WPSEO_Meta::get_value( 'linkdex', get_the_ID() );
return wpseo_adminbar_score( $rating );
}
/**
* Returns the content score element for the adminbar.
*
* @deprecated 7.9
* @codeCoverageIgnore
*
* @return string
*/
function wpseo_adminbar_content_score() {
_deprecated_function( __FUNCTION__, 'WPSEO 7.9', null );
$rating = WPSEO_Meta::get_value( 'content_score', get_the_ID() );
return wpseo_adminbar_score( $rating );
}
/**
* Returns the SEO score element for the adminbar.
*
* @deprecated 7.9
* @codeCoverageIgnore
*
* @return string
*/
function wpseo_tax_adminbar_seo_score() {
_deprecated_function( __FUNCTION__, 'WPSEO 7.9', null );
$rating = 0;
if ( is_tax() || is_category() || is_tag() ) {
$rating = WPSEO_Taxonomy_Meta::get_meta_without_term( 'linkdex' );
}
return wpseo_adminbar_score( $rating );
}
/**
* Returns the Content score element for the adminbar.
*
* @deprecated 7.9
* @codeCoverageIgnore
*
* @return string
*/
function wpseo_tax_adminbar_content_score() {
_deprecated_function( __FUNCTION__, 'WPSEO 7.9', null );
$rating = 0;
if ( is_tax() || is_category() || is_tag() ) {
$rating = WPSEO_Taxonomy_Meta::get_meta_without_term( 'content_score' );
}
return wpseo_adminbar_score( $rating );
}
/**
* Takes The SEO score and makes the score icon for the adminbar with it.
*
* @deprecated 7.9
* @codeCoverageIgnore
*
* @param int $score The 0-100 rating of the score. Can be either SEO score or content score.
*
* @return string $score_adminbar_element
*/
function wpseo_adminbar_score( $score ) {
_deprecated_function( __FUNCTION__, 'WPSEO 7.9', null );
$score = WPSEO_Utils::translate_score( $score );
$score_adminbar_element = '<div class="wpseo-score-icon adminbar-seo-score ' . $score . '"><span class="adminbar-seo-score-text screen-reader-text"></span></div>';
return $score_adminbar_element;
}
/**
* Enqueue CSS to format the Yoast SEO adminbar item.
*
* @deprecated 7.9 Use WPSEO_Admin_Bar_Menu::enqueue_assets() instead.
* @codeCoverageIgnore
*/
function wpseo_admin_bar_style() {
_deprecated_function( __FUNCTION__, 'WPSEO 7.9', 'WPSEO_Admin_Bar_Menu::enqueue_assets()' );
if ( ! is_admin_bar_showing() || WPSEO_Options::get( 'enable_admin_bar_menu' ) !== true ) {
return;
}
if ( is_admin() && ! is_blog_admin() ) {
return;
}
$admin_bar_menu = new WPSEO_Admin_Bar_Menu();
$admin_bar_menu->enqueue_assets();
}
/**
* Detects if the advanced settings are enabled.
*
* @deprecated 7.0
* @codeCoverageIgnore
*/
function wpseo_advanced_settings_enabled() {
_deprecated_function( __FUNCTION__, 'WPSEO 7.0', null );
}