PDF rausgenommen

This commit is contained in:
aschwarz
2023-01-23 11:03:31 +01:00
parent 82d562a322
commit a6523903eb
28078 changed files with 4247552 additions and 2 deletions

View File

@ -0,0 +1,495 @@
<?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 = array(
'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,
);
/**
* Hooks into WordPress.
*
* @codeCoverageIgnore
*
* @return void
*/
public function register_hooks() {
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_for_updates' ) );
add_filter( 'plugins_api', array( $this, 'get_plugin_information' ), 10, 3 );
}
/**
* Retrieves the site information.
*
* @return stdClass The site information.
*/
public function get_site_information() {
static $site_information = null;
if ( $site_information === null ) {
$site_information = $this->get_site_information_transient();
}
if ( $site_information ) {
return $site_information;
}
$site_information = $this->request_current_sites();
if ( $site_information ) {
$site_information = $this->map_site_information( $site_information );
$this->set_site_information_transient( $site_information );
return $site_information;
}
return $this->get_site_information_default();
}
/**
* Maps the plugin API response.
*
* @param object $site_information Site information as received from the API.
*
* @return object Mapped site information.
*/
public function map_site_information( $site_information ) {
return (object) array(
'url' => $site_information->url,
'subscriptions' => array_map( array( $this, 'map_subscription' ), $site_information->subscriptions ),
);
}
/**
* Maps a plugin subscription.
*
* @param object $subscription Subscription information as received from the API.
*
* @return object Mapped subscription.
*/
public 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
}
/**
* 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( array( $this, 'get_slug_by_plugin_file' ), $active_addons );
$subscriptions = array();
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 = array();
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) array(
'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' =>
array(
'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(), array( $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(), array( $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.
*
* @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 );
}
/**
* 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 = array();
foreach ( $keys_to_filter as $filtered_key ) {
$filtered_array[ $filtered_key ] = $array_to_filter[ $filtered_key ];
}
return $filtered_array;
}
/**
* Returns an object with no subscriptions.
*
* @return stdClass Site information.
*/
protected function get_site_information_default() {
return (object) array(
'url' => WPSEO_Utils::get_home_url(),
'subscriptions' => array(),
);
}
}

View File

@ -0,0 +1,364 @@
<?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 = array(
'method' => 'GET',
'timeout' => 1,
'sslverify' => false,
'headers' => array(
'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 = array() ) {
$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, array( 'headers' => array() ) );
$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 !== array() ) {
$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 array( 'url' => WPSEO_Utils::get_home_url() );
}
try {
$access_token = $this->get_access_token();
if ( $access_token ) {
return array( '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 array();
}
/**
* 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',
array(
'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( array( '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 array();
}
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 ( false === WPSEO_Options::get( 'noindex-' . $post_type_name, 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 ( null === $post_type_object ) {
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,239 @@
<?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', array( $this, 'query_vars' ) );
add_filter( 'category_link', array( $this, 'no_category_base' ) );
add_filter( 'request', array( $this, 'request' ) );
add_filter( 'category_rewrite_rules', array( $this, 'category_rewrite_rules' ) );
add_action( 'created_category', array( $this, 'schedule_flush' ) );
add_action( 'edited_category', array( $this, 'schedule_flush' ) );
add_action( 'delete_category', array( $this, 'schedule_flush' ) );
add_action( 'init', array( $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 = array();
$taxonomy = get_taxonomy( 'category' );
$permalink_structure = get_option( 'permalink_structure' );
$blog_prefix = '';
if ( is_multisite() && ! is_subdomain_install() && is_main_site() && 0 === strpos( $permalink_structure, '/blog/' ) ) {
$blog_prefix = 'blog/';
}
$categories = get_categories( array( 'hide_empty' => false ) );
if ( is_array( $categories ) && $categories !== array() ) {
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( array( $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' );
header( 'X-Redirect-By: Yoast SEO' );
wp_redirect( $catlink, 301, 'Yoast SEO' );
exit;
}
} /* End of class */

View File

@ -0,0 +1,69 @@
<?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 {
/**
* @var WPSEO_Admin_Asset_Manager
*/
protected $asset_manager;
/**
* WPSEO_Structured_Data_Blocks constructor.
*/
public function __construct() {
$this->asset_manager = new WPSEO_Admin_Asset_Manager();
}
/**
* Registers hooks for Structured Data Blocks with WordPress.
*/
public function register_hooks() {
add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ) );
add_filter( 'block_categories', array( $this, 'add_block_category' ) );
$block_integrations = array(
new WPSEO_How_To_Block(),
new WPSEO_FAQ_Block(),
);
foreach ( $block_integrations as $block_integration ) {
$block_integration->register_hooks();
}
}
/**
* Enqueue Gutenberg block assets for backend editor.
*/
public function enqueue_block_editor_assets() {
$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 ) {
$categories[] = array(
'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,134 @@
<?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(), array() );
if ( ! is_array( $data ) ) {
return array();
}
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 = array();
if ( array() !== $option_names ) {
$option_data = $this->get_options_data( $option_names );
}
// Retrieve current history.
$data = $this->get();
// Add new entry.
$data[ time() ] = array(
'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 ) {
/** @var WPDB $wpdb */
$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 = array();
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,855 @@
<?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 );
if ( version_compare( $version, '1.5.0', '<' ) ) {
$this->upgrade_15( $version );
}
if ( version_compare( $version, '2.0', '<' ) ) {
$this->upgrade_20();
}
if ( version_compare( $version, '2.1', '<' ) ) {
$this->upgrade_21();
}
if ( version_compare( $version, '2.2', '<' ) ) {
$this->upgrade_22();
}
if ( version_compare( $version, '2.3', '<' ) ) {
$this->upgrade_23();
}
if ( version_compare( $version, '3.0', '<' ) ) {
$this->upgrade_30();
}
if ( version_compare( $version, '3.3', '<' ) ) {
$this->upgrade_33();
}
if ( version_compare( $version, '3.6', '<' ) ) {
$this->upgrade_36();
}
if ( version_compare( $version, '4.0', '<' ) ) {
$this->upgrade_40();
}
if ( version_compare( $version, '4.4', '<' ) ) {
$this->upgrade_44();
}
if ( version_compare( $version, '4.7', '<' ) ) {
$this->upgrade_47();
}
if ( version_compare( $version, '4.9', '<' ) ) {
$this->upgrade_49();
}
if ( version_compare( $version, '5.0', '<' ) ) {
$this->upgrade_50();
}
if ( version_compare( $version, '5.0', '>=' )
&& version_compare( $version, '5.1', '<' )
) {
$this->upgrade_50_51();
}
if ( version_compare( $version, '5.5', '<' ) ) {
$this->upgrade_55();
}
if ( version_compare( $version, '5.6', '<' ) ) {
$this->upgrade_56();
}
if ( version_compare( $version, '6.1', '<' ) ) {
$this->upgrade_61();
}
if ( version_compare( $version, '6.3', '<' ) ) {
$this->upgrade_63();
}
if ( version_compare( $version, '7.0-RC0', '<' ) ) {
$this->upgrade_70();
}
if ( version_compare( $version, '7.1-RC0', '<' ) ) {
$this->upgrade_71();
}
if ( version_compare( $version, '7.3-RC0', '<' ) ) {
$this->upgrade_73();
}
if ( version_compare( $version, '7.4-RC0', '<' ) ) {
$this->upgrade_74();
}
if ( version_compare( $version, '7.5.3', '<' ) ) {
$this->upgrade_753();
}
if ( version_compare( $version, '7.7-RC0', '<' ) ) {
$this->upgrade_77();
}
if ( version_compare( $version, '7.7.2-RC0', '<' ) ) {
$this->upgrade_772();
}
if ( version_compare( $version, '9.0-RC0', '<' ) ) {
$this->upgrade90();
}
if ( version_compare( $version, '10.0-RC0', '<' ) ) {
$this->upgrade_100();
}
if ( version_compare( $version, '11.1-RC0', '<' ) ) {
$this->upgrade_111();
}
// 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();
}
/**
* 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', array() );
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', array( $this, 'upgrade_23_query' ), 90 );
add_action( 'admin_head', array( $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 = array();
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', array( $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.
*/
private function upgrade_50_51() {
global $wpdb;
$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 upgrade90() {
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, array( 'company', 'person' ), true ) ) {
WPSEO_Options::set( 'company_or_person', 'company' );
}
}
/**
* 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, array( 'meta_key' => $meta_key ), array( '%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 array();
}
/**
* 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, array() );
if ( ! is_array( $data ) || $data === array() ) {
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', array( $this, 'add_menu' ), 95 );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'admin_enqueue_scripts', array( $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 = array(
'id' => self::MENU_IDENTIFIER,
'title' => $title . $score . $counter . $alert_popup,
'href' => $settings_url,
'meta' => array( 'tabindex' => ! empty( $settings_url ) ? false : '0' ),
);
$wp_admin_bar->add_menu( $admin_bar_menu_args );
if ( ! empty( $counter ) ) {
$admin_bar_menu_args = array(
'parent' => self::MENU_IDENTIFIER,
'id' => 'wpseo-notifications',
'title' => __( 'Notifications', 'wordpress-seo' ) . $counter,
'href' => $settings_url,
'meta' => array( '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 = array(
'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 = array(
'parent' => self::MENU_IDENTIFIER,
'id' => self::KEYWORD_RESEARCH_SUBMENU_IDENTIFIER,
'title' => __( 'Keyword Research', 'wordpress-seo' ),
'meta' => array( 'tabindex' => '0' ),
);
$wp_admin_bar->add_menu( $menu_args );
$submenu_items = array(
array(
'id' => 'wpseo-kwresearchtraining',
'title' => __( 'Keyword research training', 'wordpress-seo' ),
'href' => WPSEO_Shortlinker::get( 'https://yoa.st/wp-admin-bar' ),
),
array(
'id' => 'wpseo-adwordsexternal',
'title' => __( 'Google Ads', 'wordpress-seo' ),
'href' => $adwords_url,
),
array(
'id' => 'wpseo-googleinsights',
'title' => __( 'Google Trends', 'wordpress-seo' ),
'href' => $trends_url,
),
);
foreach ( $submenu_items as $menu_item ) {
$menu_args = array(
'parent' => self::KEYWORD_RESEARCH_SUBMENU_IDENTIFIER,
'id' => $menu_item['id'],
'title' => $menu_item['title'],
'href' => $menu_item['href'],
'meta' => array( '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 = array(
'parent' => self::MENU_IDENTIFIER,
'id' => self::ANALYSIS_SUBMENU_IDENTIFIER,
'title' => __( 'Analyze this page', 'wordpress-seo' ),
'meta' => array( 'tabindex' => '0' ),
);
$wp_admin_bar->add_menu( $menu_args );
$encoded_url = urlencode( $url );
$submenu_items = array(
array(
'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=',
),
array(
'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 ),
),
array(
'id' => 'wpseo-cache',
'title' => __( 'Check Google Cache', 'wordpress-seo' ),
'href' => '//webcache.googleusercontent.com/search?strip=1&q=cache:' . $encoded_url,
),
array(
'id' => 'wpseo-header',
'title' => __( 'Check Headers', 'wordpress-seo' ),
'href' => '//quixapp.com/headers/?r=' . urlencode( $url ),
),
array(
'id' => 'wpseo-structureddata',
'title' => __( 'Google Structured Data Test', 'wordpress-seo' ),
'href' => 'https://search.google.com/structured-data/testing-tool#url=' . $encoded_url,
),
array(
'id' => 'wpseo-facebookdebug',
'title' => __( 'Facebook Debugger', 'wordpress-seo' ),
'href' => '//developers.facebook.com/tools/debug/og/object?q=' . $encoded_url,
),
array(
'id' => 'wpseo-pinterestvalidator',
'title' => __( 'Pinterest Rich Pins Validator', 'wordpress-seo' ),
'href' => 'https://developers.pinterest.com/tools/url-debugger/?link=' . $encoded_url,
),
array(
'id' => 'wpseo-htmlvalidation',
'title' => __( 'HTML Validator', 'wordpress-seo' ),
'href' => '//validator.w3.org/check?uri=' . $encoded_url,
),
array(
'id' => 'wpseo-cssvalidation',
'title' => __( 'CSS Validator', 'wordpress-seo' ),
'href' => '//jigsaw.w3.org/css-validator/validator?uri=' . $encoded_url,
),
array(
'id' => 'wpseo-pagespeed',
'title' => __( 'Google Page Speed Test', 'wordpress-seo' ),
'href' => '//developers.google.com/speed/pagespeed/insights/?url=' . $encoded_url,
),
array(
'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 = array(
'parent' => self::ANALYSIS_SUBMENU_IDENTIFIER,
'id' => $menu_item['id'],
'title' => $menu_item['title'],
'href' => $menu_item['href'],
'meta' => array( '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 = array(
'parent' => self::MENU_IDENTIFIER,
'id' => self::SETTINGS_SUBMENU_IDENTIFIER,
'title' => __( 'SEO Settings', 'wordpress-seo' ),
'meta' => array( '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 = array(
'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 = array(
'parent' => self::MENU_IDENTIFIER,
'id' => self::NETWORK_SETTINGS_SUBMENU_IDENTIFIER,
'title' => __( 'SEO Settings', 'wordpress-seo' ),
'meta' => array( '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 = array(
'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,134 @@
<?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.
*/
protected function get_images_from_content( $content ) {
$content_images = $this->get_img_tags_from_content( $content );
$images = array_map( array( $this, 'get_img_tag_source' ), $content_images );
$images = array_filter( $images );
$images = array_unique( $images );
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 array();
}
preg_match_all( '`<img [^>]+>`', $content, $matches );
if ( isset( $matches[0] ) ) {
return $matches[0];
}
return array();
}
/**
* 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 = array();
/**
* 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( array( '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 = array();
$args = array(
'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 = array(
'GET',
'PATCH',
'POST',
'PUT',
'DELETE',
);
/**
* The arguments.
*
* @var array
*/
protected $args = array();
/**
* 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 = array(
'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 ] = array(
'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,361 @@
<?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 );
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( array( '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 === array() ) {
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'] . DIRECTORY_SEPARATOR ) !== 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 = array();
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 = array();
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 array( $variation );
}
}
return array();
}
/**
* 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', array( '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 ( array( '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;
}
}

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', array( $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 );
}
}

File diff suppressed because it is too large Load Diff

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 = array();
}
return $terms;
}
}

View File

@ -0,0 +1,239 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
*/
/**
* Holder for SEO Rank information.
*/
class WPSEO_Rank {
/**
* @var string
*/
const BAD = 'bad';
/**
* @var string
*/
const OK = 'ok';
/**
* @var string
*/
const GOOD = 'good';
/**
* @var string
*/
const NO_FOCUS = 'na';
/**
* @var string
*/
const NO_INDEX = 'noindex';
/**
* All possible ranks.
*
* @var array
*/
protected static $ranks = array(
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 = array(
self::NO_FOCUS => array(
'start' => 0,
'end' => 0,
),
self::BAD => array(
'start' => 1,
'end' => 40,
),
self::OK => array(
'start' => 41,
'end' => 70,
),
self::GOOD => array(
'start' => 71,
'end' => 100,
),
);
/**
* @var int
*/
protected $rank;
/**
* @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 = array(
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 = array(
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 = array(
self::NO_FOCUS => __( 'SEO: No Focus Keyphrase', 'wordpress-seo' ),
self::BAD => __( 'SEO: Needs improvement', 'wordpress-seo' ),
self::OK => __( 'SEO: OK', 'wordpress-seo' ),
self::GOOD => __( 'SEO: Good', 'wordpress-seo' ),
self::NO_INDEX => __( '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 = array(
self::BAD => __( 'Readability: Needs improvement', 'wordpress-seo' ),
self::OK => __( 'Readability: OK', 'wordpress-seo' ),
self::GOOD => __( 'Readability: Good', 'wordpress-seo' ),
);
return $labels[ $this->rank ];
}
/**
* @return int The starting score for this rank.
*/
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'];
}
/**
* @return int The end score for this rank.
*/
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( array( '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( array( 'WPSEO_Rank', 'create_rank' ), array( 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,122 @@
<?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 array(
'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(),
);
}
/**
* 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 = '>30';
}
return $cohort;
}
}

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 = array(
'meta_query' => array(
'relation' => 'OR',
array(
'key' => WPSEO_Meta::$meta_prefix . 'focuskw',
'value' => 'needs-a-value-anyway',
'compare' => 'NOT EXISTS',
),
),
);
}
elseif ( WPSEO_Rank::NO_INDEX === $rank->get_rank() ) {
$posts = array(
'meta_key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex',
'meta_value' => '1',
'compare' => '=',
);
}
else {
$posts = array(
'meta_key' => WPSEO_Meta::$meta_prefix . 'linkdex',
'meta_value' => array( $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 $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,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',
array(
'methods' => 'POST',
'callback' => array( $this, 'handle_request' ),
'permission_callback' => array( $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(
array(
'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,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 = array();
/**
* The indexable's data.
*
* @var array
*/
protected $data;
/**
* The available validators to run.
*
* @var array
*/
protected $validators = array(
'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 = array(
'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( array( $object_id ) );
$post_object_id = $post->get_id();
return new self(
array(
'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 = array(
'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(
array(
'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, array( '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 = array(
'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,71 @@
<?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.
*
* @returns 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.
*
* @returns 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;
}
}

View File

@ -0,0 +1,243 @@
<?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 = array();
/**
* 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 = array(
'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 = array(
'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 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, array( $this, 'get_defaults' ) ) === false ) {
add_filter( 'default_site_option_' . $this->option_name, array( $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, array( $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, array( $this, 'get_option' ) ) === false ) {
add_filter( 'site_option_' . $this->option_name, array( $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, array( $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' // Error type, either 'error' or 'updated'.
);
}
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' // Error type, either 'error' or 'updated'.
);
}
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' // Error type, either 'error' or 'updated'.
);
}
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,265 @@
<?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 = array(
// 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 = array(
/* 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 = array(
'summary' => '',
'summary_large_image' => '',
// 'photo' => '',
// 'gallery' => '',
// 'app' => '',
// 'player' => '',
// 'product' => '',
);
/**
* 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 user-id. Please correct.', 'wordpress-seo' ),
'<strong>' . esc_html( sanitize_text_field( $dirty[ $key ] ) ) . '</strong>'
), // The error message.
'error' // Error type, either 'error' or 'updated'.
);
}
}
unset( $twitter_id );
}
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':
if ( isset( $dirty[ $key ] ) && ! empty( $dirty[ $key ] ) ) {
$clean[ $key ] = $dirty[ $key ];
}
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'] !== array() ) {
$old_option = $all_old_option_values['wpseo_indexation'];
}
}
else {
$old_option = get_option( 'wpseo_indexation' );
}
if ( is_array( $old_option ) && $old_option !== array() ) {
$move = array(
'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,890 @@
<?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 = array(
// 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' => '',
'alternate_website_name' => '',
'company_logo' => '',
'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;
*/
);
/**
* Array of variable option name patterns for the option.
*
* @var array
*/
protected $variable_array_key_patterns = array(
'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 = array(
/* 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, array( 'WPSEO_Utils', 'clear_cache' ) );
add_action( 'init', array( $this, 'end_of_init' ), 999 );
add_action( 'registered_post_type', array( $this, 'invalidate_enrich_defaults_cache' ) );
add_action( 'unregistered_post_type', array( $this, 'invalidate_enrich_defaults_cache' ) );
add_action( 'registered_taxonomy', array( $this, 'invalidate_enrich_defaults_cache' ) );
add_action( 'unregistered_taxonomy', array( $this, 'invalidate_enrich_defaults_cache' ) );
}
/**
* 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 !== array() ) {
$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 = array();
foreach ( $separators as $key => $label ) {
$aria_label = isset( $separator_list[ $key ]['label'] ) ? $separator_list[ $key ]['label'] : '';
$separator_options[ $key ] = array(
'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() {
$cache_key = 'yoast_titles_rich_defaults_' . $this->option_name;
$enriched_defaults = wp_cache_get( $cache_key );
if ( false !== $enriched_defaults ) {
$this->defaults += $enriched_defaults;
return;
}
$enriched_defaults = array();
/*
* 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( array( '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( array( '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;.
}
}
}
wp_cache_set( $cache_key, $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() {
wp_cache_delete( 'yoast_titles_rich_defaults_' . $this->option_name );
}
/**
* 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 ], array( 'company', 'person' ), true ) ) {
$clean[ $key ] = $dirty[ $key ];
}
else {
$defaults = $this->get_defaults();
$clean[ $key ] = $defaults['company_or_person'];
}
}
break;
case 'company_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( array( 'post_types-', '-maintax' ), '', $key );
$taxonomies = get_object_taxonomies( $post_type, 'names' );
if ( isset( $dirty[ $key ] ) ) {
if ( $taxonomies !== array() && 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' // Error type, either 'error' or 'updated'.
);
}
}
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 !== array() && 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( array( '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' // Error type, either 'error' or 'updated'.
);
unset( $tax );
}
}
elseif ( isset( $old[ $key ] ) ) {
$clean[ $key ] = sanitize_key( $old[ $key ] );
}
break;
case 'company_or_person_user_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 = array();
/*
* WPSEO_Post_Type::get_accessible_post_types() should *not* be used here.
*/
$post_types = get_post_types( array( '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 !== array() ) {
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', array( $this, 'clean' ) ) === false
) {
add_action( 'wpseo_double_clean_titles', array( $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'] !== array() ) {
$old_option = $all_old_option_values['wpseo_indexation'];
}
}
else {
$old_option = get_option( 'wpseo_indexation' );
}
if ( is_array( $old_option ) && $old_option !== array() ) {
$move = array(
'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 = array(
'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 = array(
'title-' => 'title-tax-',
'metadesc-' => 'metadesc-tax-',
'noindex-' => 'noindex-tax-',
'tax-hideeditbox-' => 'hideeditbox-tax-',
);
$taxonomy_names = get_taxonomies( array( 'public' => true ), 'names' );
$post_type_names = get_post_types( array( 'public' => true ), 'names' );
$defaults = $this->get_defaults();
if ( $taxonomy_names !== array() ) {
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 !== array() ) {
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 !== array() ) && ( is_array( $dirty ) && $dirty !== array() ) ) {
// 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 = array(
'sc-dash' => array(
'option' => '-',
'label' => __( 'Dash', 'wordpress-seo' ),
),
'sc-ndash' => array(
'option' => '&ndash;',
'label' => __( 'En dash', 'wordpress-seo' ),
),
'sc-mdash' => array(
'option' => '&mdash;',
'label' => __( 'Em dash', 'wordpress-seo' ),
),
'sc-colon' => array(
'option' => ':',
'label' => __( 'Colon', 'wordpress-seo' ),
),
'sc-middot' => array(
'option' => '&middot;',
'label' => __( 'Middle dot', 'wordpress-seo' ),
),
'sc-bull' => array(
'option' => '&bull;',
'label' => __( 'Bullet', 'wordpress-seo' ),
),
'sc-star' => array(
'option' => '*',
'label' => __( 'Asterisk', 'wordpress-seo' ),
),
'sc-smstar' => array(
'option' => '&#8902;',
'label' => __( 'Low asterisk', 'wordpress-seo' ),
),
'sc-pipe' => array(
'option' => '|',
'label' => __( 'Vertical bar', 'wordpress-seo' ),
),
'sc-tilde' => array(
'option' => '~',
'label' => __( 'Small tilde', 'wordpress-seo' ),
),
'sc-laquo' => array(
'option' => '&laquo;',
'label' => __( 'Left angle quotation mark', 'wordpress-seo' ),
),
'sc-raquo' => array(
'option' => '&raquo;',
'label' => __( 'Right angle quotation mark', 'wordpress-seo' ),
),
'sc-lt' => array(
'option' => '&lt;',
'label' => __( 'Less than sign', 'wordpress-seo' ),
),
'sc-gt' => array(
'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,390 @@
<?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 = array(
// 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' => array(
'config' => array(
'clientId' => null,
'secret' => null,
),
'access_tokens' => array(),
),
);
/**
* Sub-options which should not be overloaded with multi-site defaults.
*
* @var array
*/
public $ms_exclude = array(
/* Privacy. */
'baiduverify',
'googleverify',
'msverify',
'yandexverify',
);
/**
* Possible values for the site_type option.
*
* @var array
*/
protected $site_types = array(
'',
'blog',
'shop',
'news',
'smallBusiness',
'corporateOther',
'personalOther',
);
/**
* Possible environment types.
*
* @var array
*/
protected $environment_types = array(
'',
'production',
'staging',
'development',
);
/**
* Possible has_multiple_authors options.
*
* @var array
*/
protected $has_multiple_authors_options = array(
'',
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, array( 'WPSEO_Utils', 'clear_cache' ) );
add_action( 'update_option_' . $this->option_name, array( 'WPSEO_Utils', 'clear_cache' ) );
/**
* 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 = array() ) {
if ( ! is_array( $options ) || empty( $options ) ) {
return $options;
}
// For the feature variables, set their values to off in case they are disabled.
$feature_vars = array(
'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 array(
"option_{$this->option_name}",
array( $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 array(
"default_option_{$this->option_name}",
array( $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,816 @@
<?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 = array();
/**
* 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', array( $this, 'add_default_filters' ) ); // Adding back after INSERT.
add_action( 'update_option', array( $this, 'add_default_filters' ) );
}
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, array( $this, 'add_default_filters' ) );
add_action( 'update_site_option_' . $this->option_name, array( $this, 'add_default_filters' ) );
}
/*
* Make sure the option will always get validated, independently of register_setting()
* (only available on back-end).
*/
add_filter( 'sanitize_option_' . $this->option_name, array( $this, 'validate' ) );
// Flushes the rewrite rules when option is updated.
add_action( 'update_option_' . $this->option_name, array( 'WPSEO_Utils', 'clear_rewrites' ) );
/* Register our option for the admin pages */
add_action( 'admin_init', array( $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', array( $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', array( $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, array( $this, 'get_defaults' ) ) === false ) {
add_filter( 'default_option_' . $this->option_name, array( $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.
* @param array $old Old data.
* @param array $clean Clean data by reference.
*/
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 {
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.
/* 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' // Error type, either 'error' or 'updated'.
);
}
}
}
}
}
/**
* @param string $key Key to check, by type of service.
* @param array $dirty Dirty data.
* @param array $old Old data.
* @param array $clean Clean data by reference.
*/
public function validate_url( $key, $dirty, $old, &$clean ) {
if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
$url = WPSEO_Utils::sanitize_url( $dirty[ $key ] );
if ( $url !== '' ) {
$clean[ $key ] = $url;
}
else {
if ( isset( $old[ $key ] ) && $old[ $key ] !== '' ) {
$url = WPSEO_Utils::sanitize_url( $old[ $key ] );
if ( $url !== '' ) {
$clean[ $key ] = $url;
}
}
if ( function_exists( 'add_settings_error' ) ) {
$url = WPSEO_Utils::sanitize_url( $dirty[ $key ] );
add_settings_error(
$this->group_name, // Slug title of the setting.
'_' . $key, // Suffix-ID for the error message box.
sprintf(
/* translators: %s expands to an invalid URL. */
__( '%s does not seem to be a valid url. Please correct.', 'wordpress-seo' ),
'<strong>' . esc_html( $url ) . '</strong>'
), // The error message.
'error' // Error type, either 'error' or 'updated'.
);
}
}
}
}
/**
* 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, array( $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, array( $this, 'get_option' ) ) === false ) {
add_filter( 'option_' . $this->option_name, array( $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, array( $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 === array() ) {
return $clean;
}
$option_value = array_map( array( 'WPSEO_Utils', 'trim_recursive' ), $option_value );
$old = $this->get_original_option();
if ( ! is_array( $old ) ) {
$old = array();
}
$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;
}
/**
* 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 === array() ) {
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 array();
}
return get_site_option( $this->override_option_name, array() );
}
/**
* 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 !== array() ) && ( is_array( $dirty ) && $dirty !== array() ) ) {
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 === array() ) ) {
return $key;
}
foreach ( $this->variable_array_key_patterns as $pattern ) {
if ( strpos( $key, $pattern ) === 0 ) {
return $pattern;
}
}
return $key;
}
}

View File

@ -0,0 +1,313 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals\Options
*/
/**
* Backfill the removed options.
*
* @since 7.0.2
*/
class WPSEO_Options_Backfill implements WPSEO_WordPress_Integration {
/**
* Are the filters hooked or not.
*
* @var bool
*/
protected $hooked = false;
/**
* Registers all hooks to WordPress.
*/
public function register_hooks() {
// Make sure we don't hook multiple times.
if ( $this->hooked ) {
return;
}
$this->hooked = true;
// Backfill options that were removed.
foreach ( $this->get_lookups() as $option ) {
add_filter( 'pre_option_' . $option, array( $this, 'backfill_option' ), 10, 2 );
}
// Make sure renamed meta key is backfilled.
add_filter( 'get_user_metadata', array( $this, 'backfill_usermeta' ), 10, 3 );
// Extend the options that have removed items.
add_filter( 'option_wpseo_titles', array( $this, 'extend_wpseo_titles' ), 10, 1 );
add_filter( 'option_wpseo', array( $this, 'extend_wpseo' ), 10, 1 );
}
/**
* Removes the option filters.
*/
public function remove_hooks() {
// Remove backfill options filter.
foreach ( $this->get_lookups() as $option ) {
remove_filter( 'pre_option_' . $option, array( $this, 'backfill_option' ), 10 );
}
// Remove user meta filter.
remove_filter( 'get_user_metadata', array( $this, 'backfill_usermeta' ), 10 );
// Remove option extending filters.
remove_filter( 'option_wpseo_titles', array( $this, 'extend_wpseo_titles' ), 10 );
remove_filter( 'option_wpseo', array( $this, 'extend_wpseo' ), 10 );
$this->hooked = false;
}
/**
* Retrieves the options that need to be backfilled.
*
* @since 7.0.2
*
* @return array List of options that need to be backfilled.
*/
protected function get_lookups() {
return array(
'wpseo_internallinks',
'wpseo_rss',
'wpseo_xml',
'wpseo_permalinks',
);
}
/**
* Retrieves the settings for the specified option.
*
* @since 7.0.2
*
* @param string $option The option to get the settings for.
*
* @return array The settings for the provided option.
*/
protected function get_settings( $option ) {
$settings = array(
'wpseo' => array(
'website_name' => 'website_name',
'alternate_website_name' => 'alternate_website_name',
'company_logo' => 'company_logo',
'company_name' => 'company_name',
'company_or_person' => 'company_or_person',
'person_name' => 'person_name',
),
'wpseo_internallinks' => array(
'breadcrumbs-404crumb' => 'breadcrumbs-404crumb',
'breadcrumbs-blog-remove' => 'breadcrumbs-display-blog-page',
'breadcrumbs-boldlast' => 'breadcrumbs-boldlast',
'breadcrumbs-archiveprefix' => 'breadcrumbs-archiveprefix',
'breadcrumbs-enable' => 'breadcrumbs-enable',
'breadcrumbs-home' => 'breadcrumbs-home',
'breadcrumbs-prefix' => 'breadcrumbs-prefix',
'breadcrumbs-searchprefix' => 'breadcrumbs-searchprefix',
'breadcrumbs-sep' => 'breadcrumbs-sep',
),
'wpseo_rss' => array(
'rssbefore' => 'rssbefore',
'rssafter' => 'rssafter',
),
'wpseo_xml' => array(
'enablexmlsitemap' => 'enable_xml_sitemap',
'disable_author_sitemap' => 'noindex-author-wpseo',
'disable_author_noposts' => 'noindex-author-noposts-wpseo',
),
'wpseo_permalinks' => array(
'redirectattachment' => 'disable-attachment',
'stripcategorybase' => 'stripcategorybase',
),
);
if ( ! isset( $settings[ $option ] ) ) {
return array();
}
return $settings[ $option ];
}
/**
* Extends the WPSEO option with the removed option values.
*
* @since 7.0.2
*
* @param array $data The data of the option.
*
* @return array Modified data.
*/
public function extend_wpseo( $data ) {
// Make sure we don't get stuck in an infinite loop.
static $running = false;
// If we are already running, don't run again.
if ( $running ) {
return $data;
}
$running = true;
foreach ( $this->get_settings( 'wpseo' ) as $old_key => $new_key ) {
$data[ $old_key ] = WPSEO_Options::get( $new_key );
}
// Ended running.
$running = false;
return $data;
}
/**
* Extends the WPSEO Title with removed attributes.
*
* @since 7.0.2
*
* @param array $data Data of the option.
*
* @return array Extended data.
*/
public function extend_wpseo_titles( $data ) {
// Make sure we don't get stuck in an infinite loop.
static $running = false;
// If we are already running, don't run again.
if ( $running ) {
return $data;
}
$running = true;
$data['breadcrumbs-blog-remove'] = ! WPSEO_Options::get( 'breadcrumbs-display-blog-page' );
$running = false;
$data = $this->add_hideeditbox( $data );
return $data;
}
/**
* Backfills the options that have been removed with the current values.
*
* @since 7.0.2
*
* @param mixed $value Current value for the option.
* @param string $option Name of the option.
*
* @return array Option data.
*/
public function backfill_option( $value, $option ) {
$output = array();
foreach ( $this->get_settings( $option ) as $old_key => $new_key ) {
$output[ $old_key ] = WPSEO_Options::get( $new_key );
}
$output = $this->apply_permalinks_settings( $output, $option );
$output = $this->apply_xml_settings( $output, $option );
return $output;
}
/**
* Backfills removed user meta fields.
*
* @since 7.0.2
*
* @param mixed $value The current value.
* @param int $object_id The user ID.
* @param string $meta_key The meta key.
*
* @return mixed The backfilled value if applicable.
*/
public function backfill_usermeta( $value, $object_id, $meta_key ) {
if ( $meta_key !== 'wpseo_excludeauthorsitemap' ) {
return $value;
}
return get_user_meta( $object_id, 'wpseo_noindex_author' );
}
/**
* Extends the data of the option with the deprecated values.
*
* @since 7.0.2
*
* @param array $data Current data of the option.
*
* @return array Extended data.
*/
protected function add_hideeditbox( $data ) {
foreach ( $data as $key => $value ) {
if ( strpos( $key, 'display-metabox-tax-' ) === 0 ) {
$taxonomy = substr( $key, strlen( 'display-metabox-tax-' ) );
$data[ 'hideeditbox-tax-' . $taxonomy ] = ! $value;
continue;
}
if ( strpos( $key, 'display-metabox-pt-' ) === 0 ) {
$post_type = substr( $key, strlen( 'display-metabox-pt-' ) );
$data[ 'hideeditbox-' . $post_type ] = ! $value;
continue;
}
}
return $data;
}
/**
* Adds the permalinks specific data to the option when requested.
*
* @since 7.0.2
*
* @param array $data Current data.
* @param string $option The option that is being parsed.
*
* @return array Extended data.
*/
protected function apply_permalinks_settings( $data, $option ) {
if ( $option !== 'wpseo_permalinks' ) {
return $data;
}
// Add defaults for completely removed settings in the option.
return array_merge(
$data,
array(
'cleanpermalinks' => false,
'cleanpermalink-extravars' => '',
'cleanpermalink-googlecampaign' => false,
'cleanpermalink-googlesitesearch' => false,
'cleanreplytocom' => false,
'cleanslugs' => false,
'trailingslash' => false,
)
);
}
/**
* Adds the XML specific data to the option when requested.
*
* @since 7.0.2
*
* @param array $data Current data.
* @param string $option The option that is being parsed.
*
* @return array Extended data.
*/
protected function apply_xml_settings( $data, $option ) {
if ( $option !== 'wpseo_xml' ) {
return $data;
}
// Add dynamic implementations for settings that are not in any option anymore.
return array_merge(
$data,
array(
'entries-per-page' => (int) apply_filters( 'wpseo_sitemap_entries_per_page', 1000 ),
'excluded-posts' => apply_filters( 'wpseo_exclude_from_sitemap_by_post_ids', array() ),
)
);
}
}

View File

@ -0,0 +1,530 @@
<?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 {
/**
* Options this class uses.
*
* @var array Array format: (string) option_name => (string) name of concrete class for the option.
*/
public static $options = array(
'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();
/**
* Instance of this class.
*
* @var object
*/
protected static $instance;
/**
* Backfill instance.
*
* @var WPSEO_Options_Backfill
*/
protected static $backfill;
/**
* Instantiate all the WPSEO option management classes.
*/
protected function __construct() {
// Backfill option values after transferring them to another base.
self::$backfill = new WPSEO_Options_Backfill();
self::$backfill->register_hooks();
$is_multisite = is_multisite();
foreach ( self::$options as $option_name => $option_class ) {
$instance = call_user_func( array( $option_class, 'get_instance' ) );
if ( ! $instance->multisite_only || $is_multisite ) {
self::$option_instances[ $option_name ] = $instance;
}
else {
unset( self::$options[ $option_name ] );
}
}
}
/**
* 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;
}
/**
* 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( self::$option_instances[ $option_name ] ) ) {
return self::$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( self::$option_instances[ $option_name ] ) ) {
$defaults = self::$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( self::$option_instances[ $option_name ] ) ) {
return self::$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( self::$option_instances[ $option_name ] ) ) {
return self::$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() {
static $option_names = array();
if ( $option_names === array() ) {
foreach ( self::$option_instances as $option_name => $option_object ) {
if ( $option_object->include_in_all === true ) {
$option_names[] = $option_name;
}
}
$option_names = apply_filters( 'wpseo_options', $option_names );
}
return $option_names;
}
/**
* Retrieve all the options for the SEO plugin in one go.
*
* @todo [JRF] See if we can get some extra efficiency for this one, though probably not as options may
* well change between calls (enriched defaults and such).
*
* @return array Array combining the values of all the options.
*/
public static function get_all() {
return self::get_options( self::get_option_names() );
}
/**
* 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 = array();
$option_names = array_filter( $option_names, 'is_string' );
foreach ( $option_names as $option_name ) {
if ( isset( self::$option_instances[ $option_name ] ) ) {
$option = self::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( self::$option_instances[ $option_name ] ) ) {
if ( self::$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 ) {
self::$backfill->remove_hooks();
$option = self::get_all();
$option = self::add_ms_option( $option );
self::$backfill->register_hooks();
if ( isset( $option[ $key ] ) ) {
return $option[ $key ];
}
return $default;
}
/**
* 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 = self::get_lookup_table();
if ( isset( $lookup_table[ $key ] ) ) {
return self::save_option( $lookup_table[ $key ], $key, $value );
}
$patterns = self::get_pattern_table();
foreach ( $patterns as $pattern => $option ) {
if ( strpos( $key, $pattern ) === 0 ) {
return self::save_option( $option, $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 ( false === $value ) {
$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( self::$option_instances[ $option_name ] ) ) {
self::$option_instances[ $option_name ]->clean( $current_version );
}
}
elseif ( isset( $option_name ) && is_array( $option_name ) && $option_name !== array() ) {
foreach ( $option_name as $option ) {
if ( isset( self::$option_instances[ $option ] ) ) {
self::$option_instances[ $option ]->clean( $current_version );
}
}
unset( $option );
}
else {
foreach ( self::$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 ( self::$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 = self::get_option_names();
if ( is_array( $option_names ) && $option_names !== array() ) {
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.
self::reset_ms_blog( get_current_blog_id() );
}
self::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 ) {
self::reset_ms_blog( get_current_blog_id() );
self::initialize();
}
elseif ( $force_init === true ) {
self::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 = self::get_option_names();
if ( is_array( $option_names ) && $option_names !== array() ) {
$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( self::$option_instances[ $option_name ] ) && self::$option_instances[ $option_name ]->ms_exclude !== array() ) {
foreach ( self::$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 = self::get_option( $wpseo_options_group_name );
$options[ $option_name ] = $option_value;
if ( isset( self::$option_instances[ $wpseo_options_group_name ] ) && self::$option_instances[ $wpseo_options_group_name ]->multisite_only === true ) {
self::update_site_option( $wpseo_options_group_name, $options );
}
else {
update_option( $wpseo_options_group_name, $options );
}
// Check if everything got saved properly.
$saved_option = self::get_option( $wpseo_options_group_name );
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 = self::get_option( 'wpseo_ms' );
return array_merge( $option, $ms_option );
}
/**
* 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 = array();
self::$backfill->remove_hooks();
foreach ( array_keys( self::$options ) as $option_name ) {
$full_option = self::get_option( $option_name );
foreach ( $full_option as $key => $value ) {
$lookup_table[ $key ] = $option_name;
}
}
self::$backfill->register_hooks();
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 = array();
foreach ( self::$options as $option_name => $option_class ) {
/** @var WPSEO_Option $instance */
$instance = call_user_func( array( $option_class, 'get_instance' ) );
foreach ( $instance->get_patterns() as $key ) {
$pattern_table[ $key ] = $option_name;
}
}
return $pattern_table;
}
/* ********************* DEPRECATED METHODS ********************* */
/**
* 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,616 @@
<?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 = array();
/**
* 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 = array(
'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 = array(
'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.
*
* @return \WPSEO_Taxonomy_Meta
*/
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, array( 'WPSEO_Utils', 'flush_w3tc_cache' ) );
add_action( 'update_option_' . $this->option_name, array( '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', array() );
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 !== array() ) {
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 !== array() ) {
/* 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 !== array() ) {
$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( array( 'WPSEO_Utils', 'trim_recursive' ), $meta_data );
if ( ! is_array( $meta_data ) || $meta_data === array() ) {
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( array( '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 = array();
foreach ( $input as $entry ) {
$sanitized[] = array(
'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 ( 'wpseo_focuskw' === $key ) {
$search = array(
'&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 !== array() ) {
foreach ( $option_value as $taxonomy => $terms ) {
if ( is_array( $terms ) && $terms !== array() ) {
foreach ( $terms as $term_id => $meta_data ) {
if ( ! is_array( $meta_data ) || $meta_data === array() ) {
// 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, array( $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 = array();
// @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 array( $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 !== array() ) {
$tax_meta[ $taxonomy ][ $term_id ] = $clean;
}
else {
unset( $tax_meta[ $taxonomy ][ $term_id ] );
if ( isset( $tax_meta[ $taxonomy ] ) && $tax_meta[ $taxonomy ] === array() ) {
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,235 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Sitemap provider for author archives.
*/
class WPSEO_Author_Sitemap_Provider implements 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 ) {
return $type === 'author';
}
/**
* @param int $max_entries Entries per sitemap.
*
* @return array
*/
public function get_index_links( $max_entries ) {
if ( WPSEO_Options::get( 'disable-author', false ) || WPSEO_Options::get( 'noindex-author-wpseo', false ) ) {
return array();
}
// @todo Consider doing this less often / when necessary. R.
$this->update_user_meta();
$has_exclude_filter = has_filter( 'wpseo_sitemap_exclude_author' );
$query_arguments = array();
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 array();
}
$index = array();
$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[] = array(
'loc' => WPSEO_Sitemaps_Router::get_base_url( 'author-sitemap' . $page . '.xml' ),
'lastmod' => '@' . $user->_yoast_wpseo_profile_updated, // @ for explicit timestamp format
);
$page++;
}
return $index;
}
/**
* Retrieve users, taking account of all necessary exclusions.
*
* @param array $arguments Arguments to add.
*
* @return array
*/
protected function get_users( $arguments = array() ) {
global $wpdb;
$defaults = array(
// @todo Re-enable after plugin requirements raised to WP 4.6 with the fix.
// 'who' => 'authors', Breaks meta keys, {@link https://core.trac.wordpress.org/ticket/36724#ticket} R.
'meta_key' => '_yoast_wpseo_profile_updated',
'orderby' => 'meta_value_num',
'order' => 'DESC',
'meta_query' => array(
'relation' => 'AND',
array(
'key' => $wpdb->get_blog_prefix() . 'user_level',
'value' => '0',
'compare' => '!=',
),
array(
'relation' => 'OR',
array(
'key' => 'wpseo_noindex_author',
'value' => 'on',
'compare' => '!=',
),
array(
'key' => 'wpseo_noindex_author',
'compare' => 'NOT EXISTS',
),
),
),
);
if ( WPSEO_Options::get( 'noindex-author-noposts-wpseo', true ) ) {
// $defaults['who'] = ''; // Otherwise it cancels out next argument.
$defaults['has_published_posts'] = true;
}
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.
*
* @return array
*/
public function get_sitemap_links( $type, $max_entries, $current_page ) {
$links = array();
if ( WPSEO_Options::get( 'disable-author', false ) || WPSEO_Options::get( 'noindex-author-wpseo', false ) ) {
return $links;
}
$user_criteria = array(
'offset' => ( $current_page - 1 ) * $max_entries,
'number' => $max_entries,
);
$users = $this->get_users( $user_criteria );
$users = $this->exclude_users( $users );
if ( empty( $users ) ) {
$users = array();
}
$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 = array(
'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 = array(
'who' => 'authors',
'meta_query' => array(
array(
'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,673 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Sitemap provider for author archives.
*/
class WPSEO_Post_Type_Sitemap_Provider implements WPSEO_Sitemap_Provider {
/**
* Holds the home_url() value.
*
* @var string
*/
protected static $home_url;
/**
* 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;
/**
* Static front page ID.
*
* @var int
*/
protected static $page_on_front_id;
/**
* Posts page ID.
*
* @var int
*/
protected static $page_for_posts_id;
/**
* Set up object properties for data reuse.
*/
public function __construct() {
add_filter( 'save_post', array( $this, 'save_post' ) );
}
/**
* Get front page ID.
*
* @return int
*/
protected function get_page_on_front_id() {
if ( ! isset( self::$page_on_front_id ) ) {
self::$page_on_front_id = (int) get_option( 'page_on_front' );
}
return self::$page_on_front_id;
}
/**
* Get page for posts ID.
*
* @return int
*/
protected function get_page_for_posts_id() {
if ( ! isset( self::$page_for_posts_id ) ) {
self::$page_for_posts_id = (int) get_option( 'page_for_posts' );
}
return self::$page_for_posts_id;
}
/**
* 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( $this->get_home_url() );
}
return self::$classifier;
}
/**
* Get Home URL.
*
* This has been moved from the constructor because wp_rewrite is not available on plugins_loaded in multisite.
* It will now be requested on need and not on initialization.
*
* @return string
*/
protected function get_home_url() {
if ( ! isset( self::$home_url ) ) {
self::$home_url = WPSEO_Utils::home_url();
}
return self::$home_url;
}
/**
* 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 );
}
/**
* @param int $max_entries Entries per sitemap.
*
* @return array
*/
public function get_index_links( $max_entries ) {
global $wpdb;
// Consider using WPSEO_Post_Type::get_accessible_post_types() to filter out any `no-index` post-types.
$post_types = WPSEO_Post_Type::get_accessible_post_types();
$post_types = array_filter( $post_types, array( $this, 'is_valid_post_type' ) );
$last_modified_times = WPSEO_Sitemaps::get_last_modified_gmt( $post_types, true );
$index = array();
foreach ( $post_types as $post_type ) {
$total_count = $this->get_post_type_count( $post_type );
if ( $total_count === 0 ) {
continue;
}
$max_pages = 1;
if ( $total_count > $max_entries ) {
$max_pages = (int) ceil( $total_count / $max_entries );
}
$all_dates = array();
if ( $max_pages > 1 ) {
$sql = "
SELECT post_modified_gmt
FROM ( SELECT @rownum:=0 ) init
JOIN {$wpdb->posts} USE INDEX( type_status_date )
WHERE post_status IN ( 'publish', 'inherit' )
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[] = array(
'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.
*
* @return array
*/
public function get_sitemap_links( $type, $max_entries, $current_page ) {
$links = array();
$post_type = $type;
if ( ! $this->is_valid_post_type( $post_type ) ) {
return $links;
}
$steps = min( 100, $max_entries );
$offset = ( $current_page > 1 ) ? ( ( $current_page - 1 ) * $max_entries ) : 0;
$total = ( $offset + $max_entries );
$typecount = $this->get_post_type_count( $post_type );
if ( $total > $typecount ) {
$total = $typecount;
}
if ( $current_page === 1 ) {
$links = array_merge( $links, $this->get_first_links( $post_type ) );
}
if ( $typecount === 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 = array();
if ( $post_type === 'page' && $this->get_page_for_posts_id() ) {
$excluded_posts_ids[] = $this->get_page_for_posts_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 = array();
}
$excluded_posts_ids = array_map( 'intval', $excluded_posts_ids );
if ( $post_type === 'page' && $this->get_page_on_front_id() ) {
$excluded_posts_ids[] = $this->get_page_on_front_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 = array();
$archive_url = false;
if ( $post_type === 'page' ) {
if ( $this->get_page_on_front_id() ) {
$front_page = $this->get_url(
get_post( $this->get_page_on_front_id() )
);
}
if ( empty( $front_page ) ) {
$front_page = array(
'loc' => $this->get_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[] = array(
'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 ) {
if ( WPSEO_Options::get( 'noindex-ptarchive-' . $post_type, false ) ) {
return false;
}
// Post archive should be excluded if it isn't front page or posts page.
if ( $post_type === 'post' && get_option( 'show_on_front' ) !== 'posts' && ! $this->get_page_for_posts_id() ) {
return false;
}
/**
* Filter the page which is dedicated to this post type archive.
*
* @param string $post_id The post_id of the page.
* @param string $post_type The post type this archive is for.
*/
$post_id = (int) apply_filters(
'wpseo_sitemap_page_for_post_type_archive',
( 'post' === $post_type ) ? $this->get_page_for_posts_id() : ( -1 ),
$post_type
);
if ( $post_id > 0 && WPSEO_Meta::get_value( 'meta-robots-noindex', $post_id ) === '1' ) {
return false;
}
return get_post_type_archive_link( $post_type );
}
/**
* 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 = array();
if ( ! isset( $filters[ $post_type ] ) ) {
// Make sure you're wpdb->preparing everything you throw into this!!
$filters[ $post_type ] = array(
/**
* 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_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 = array();
foreach ( $posts as $post ) {
$post->post_type = $post_type;
$post->post_status = 'publish';
$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;
}
/**
* @param string $post_type Post type slug.
*
* @return string
*/
protected function get_sql_where_clause( $post_type ) {
global $wpdb;
$join = '';
$status = "{$wpdb->posts}.post_status = 'publish'";
// Based on WP_Query->get_posts(). R.
if ( 'attachment' === $post_type ) {
$join = " LEFT JOIN {$wpdb->posts} AS p2 ON ({$wpdb->posts}.post_parent = p2.ID) ";
$status = "p2.post_status = 'publish' AND p2.post_password = ''";
}
$where_clause = "
{$join}
WHERE {$status}
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 = array();
/**
* 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.
$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' );
}
}

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 = array(
'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,520 @@
<?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 = array();
/**
* 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 = array();
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( $post->post_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 ( 'attachment' === $post->post_type && 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;
}
/**
* @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[] = array(
'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 = array();
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[] = array(
'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 = array();
$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 );
}
if ( PHP_VERSION_ID >= 50209 ) {
// phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.array_unique_sort_flagsFound -- Wrapped in version check.
return array_unique( $attachments, SORT_REGULAR );
}
return $attachments;
}
/**
* 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 ) {
if ( ! has_shortcode( $content, 'gallery' ) ) {
return array();
}
$galleries = array();
if ( ! preg_match_all( '/' . get_shortcode_regex() . '/s', $content, $matches, PREG_SET_ORDER ) ) {
return $galleries;
}
foreach ( $matches as $shortcode ) {
if ( 'gallery' === $shortcode[2] ) {
$attributes = shortcode_parse_atts( $shortcode[3] );
if ( '' === $attributes ) { // Valid shortcode without any attributes. R.
$attributes = array();
}
$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 = array();
/**
* 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 ( false !== $uploads['error'] ) {
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 ( 0 === strpos( $file, $uploads['basedir'] ) ) {
$src = str_replace( $uploads['basedir'], $uploads['baseurl'], $file );
}
elseif ( false !== strpos( $file, 'wp-content/uploads' ) ) {
$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 array();
}
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 = array(
'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(
array(
'posts_per_page' => count( $ids_to_include ),
'post__in' => $ids_to_include,
)
);
$gallery_attachments = array();
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 = array(
'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,123 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Class WPSEO_Sitemap_Timezone.
*/
class WPSEO_Sitemap_Timezone {
/**
* Holds the timezone string value to reuse for performance.
*
* @var string $timezone_string
*/
private $timezone_string = '';
/**
* Format arbitrary UTC datetime string to desired form in site's time zone.
*
* @param string $datetime_string The input datetime string in UTC time zone.
* @param string $format Date format to use.
*
* @return string
*/
public function format_date( $datetime_string, $format = 'c' ) {
$date_time = $this->get_datetime_with_timezone( $datetime_string );
if ( is_null( $date_time ) ) {
return '';
}
return $date_time->format( $format );
}
/**
* Get the datetime object, in site's time zone, if the datetime string was valid
*
* @param string $datetime_string The datetime string in UTC time zone, that needs
* to be converted to a DateTime object.
*
* @return DateTime|null DateTime object in site's time zone.
*/
public function get_datetime_with_timezone( $datetime_string ) {
static $utc_timezone, $local_timezone;
if ( ! isset( $utc_timezone ) ) {
$utc_timezone = new DateTimeZone( 'UTC' );
$local_timezone = new DateTimeZone( $this->get_timezone_string() );
}
if ( ! empty( $datetime_string ) && WPSEO_Utils::is_valid_datetime( $datetime_string ) ) {
$datetime = new DateTime( $datetime_string, $utc_timezone );
$datetime->setTimezone( $local_timezone );
return $datetime;
}
return null;
}
/**
* Returns the timezone string for a site, even if it's set to a UTC offset.
*
* Adapted from {@link http://www.php.net/manual/en/function.timezone-name-from-abbr.php#89155}.
*
* @return string Valid PHP timezone string.
*/
private function determine_timezone_string() {
// If site timezone string exists, return it.
$timezone = get_option( 'timezone_string' );
if ( ! empty( $timezone ) ) {
return $timezone;
}
// Get UTC offset, if it isn't set then return UTC.
$utc_offset = (int) get_option( 'gmt_offset', 0 );
if ( 0 === $utc_offset ) {
return 'UTC';
}
// Adjust UTC offset from hours to seconds.
$utc_offset *= HOUR_IN_SECONDS;
// Attempt to guess the timezone string from the UTC offset.
$timezone = timezone_name_from_abbr( '', $utc_offset );
if ( false !== $timezone ) {
return $timezone;
}
// Last try, guess timezone string manually.
$timezone_list = timezone_abbreviations_list();
foreach ( $timezone_list as $abbr ) {
foreach ( $abbr as $city ) {
if ( $city['offset'] === $utc_offset ) {
return $city['timezone_id'];
}
}
}
// Fallback to UTC.
return 'UTC';
}
/**
* Returns the correct timezone string.
*
* @return string
*/
private function get_timezone_string() {
if ( '' === $this->timezone_string ) {
$this->timezone_string = $this->determine_timezone_string();
}
return $this->timezone_string;
}
}

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 = array();
/**
* Class constructor.
*/
public function __construct() {
add_action( 'transition_post_status', array( $this, 'status_transition' ), 10, 3 );
add_action( 'admin_footer', array( $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 ( 'nav_menu_item' === $post_type ) {
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 ( 'nav_menu_item' === $post_type ) {
continue;
}
if ( WPSEO_Options::get( 'noindex-' . $post_type, false ) === false ) {
$ping_search_engines = true;
}
}
// Nothing to do.
if ( false === $ping_search_engines ) {
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 = array();
$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 = array();
/**
* 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 = array();
/**
* Hook methods for invalidation on necessary events.
*/
public function __construct() {
add_action( 'init', array( $this, 'init' ) );
add_action( 'deleted_term_relationships', array( __CLASS__, 'invalidate' ) );
add_action( 'update_option', array( __CLASS__, 'clear_on_option_update' ) );
add_action( 'edited_terms', array( __CLASS__, 'invalidate_helper' ), 10, 2 );
add_action( 'clean_term_cache', array( __CLASS__, 'invalidate_helper' ), 10, 2 );
add_action( 'clean_object_term_cache', array( __CLASS__, 'invalidate_helper' ), 10, 2 );
add_action( 'user_register', array( __CLASS__, 'invalidate_author' ) );
add_action( 'delete_user', array( __CLASS__, 'invalidate_author' ) );
add_action( 'shutdown', array( __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 ( false === $transient_key ) {
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 ) && 0 === strpos( $sitemap, 'C:24:"WPSEO_Sitemap_Cache_Data"' ) ) {
$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 ( false === $transient_key ) {
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( array( $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 ( 'user_register' === current_action() ) {
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 = array() ) {
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 = array();
return;
}
foreach ( self::$clear_types as $type ) {
WPSEO_Sitemaps_Cache_Validator::invalidate_storage( $type );
}
self::$clear_types = array();
}
/**
* 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,372 @@
<?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;
/**
* Timezone.
*
* @var WPSEO_Sitemap_Timezone
*/
protected $timezone;
/**
* 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->timezone = new WPSEO_Sitemap_Timezone();
if (
'UTF-8' !== $this->charset
&& 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;
}
/**
* @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;
}
/**
* @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 -->";
$debug = WP_DEBUG || ( defined( 'WPSEO_DEBUG' ) && true === WPSEO_DEBUG );
if ( ! WP_DEBUG_DISPLAY || ! $debug ) {
return $output;
}
$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;
}
/**
* 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->timezone->format_date( $url['lastmod'] );
}
$url['loc'] = htmlspecialchars( $url['loc'] );
$output = "\t<sitemap>\n";
$output .= "\t\t<loc>" . $url['loc'] . "</loc>\n";
$output .= empty( $date ) ? '' : "\t\t<lastmod>" . htmlspecialchars( $date ) . "</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->timezone->format_date( $url['mod'] );
}
$url['loc'] = htmlspecialchars( $url['loc'] );
$output = "\t<url>\n";
$output .= "\t\t<loc>" . $this->encode_url_rfc3986( $url['loc'] ) . "</loc>\n";
$output .= empty( $date ) ? '' : "\t\t<lastmod>" . htmlspecialchars( $date ) . "</lastmod>\n";
if ( empty( $url['images'] ) ) {
$url['images'] = array();
}
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';
}
}

View File

@ -0,0 +1,128 @@
<?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', array( $this, 'init' ), 1 );
add_filter( 'redirect_canonical', array( $this, 'redirect_canonical' ) );
add_action( 'template_redirect', array( $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( '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?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( 'xsl' ) ) {
return false;
}
return $redirect;
}
/**
* Redirects sitemap.xml to sitemap_index.xml.
*/
public function template_redirect() {
if ( ! $this->needs_sitemap_index_redirect() ) {
return;
}
header( 'X-Redirect-By: Yoast SEO' );
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 = array( $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,585 @@
<?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;
/**
* @var WPSEO_Sitemap_Timezone
*/
private $timezone;
/**
* @since 3.2
*
* @var WPSEO_Sitemaps_Router
*/
public $router;
/**
* @since 3.2
*
* @var WPSEO_Sitemaps_Renderer
*/
public $renderer;
/**
* @since 3.2
*
* @var WPSEO_Sitemaps_Cache
*/
public $cache;
/**
* @since 3.2
*
* @var WPSEO_Sitemap_Provider[]
*/
public $providers;
/**
* Class constructor.
*/
public function __construct() {
add_action( 'after_setup_theme', array( $this, 'init_sitemaps_providers' ) );
add_action( 'after_setup_theme', array( $this, 'reduce_query_load' ), 99 );
add_action( 'pre_get_posts', array( $this, 'redirect' ), 1 );
add_action( 'wpseo_hit_sitemap_index', array( $this, 'hit_sitemap_index' ) );
add_action( 'wpseo_ping_search_engines', array( __CLASS__, 'ping_search_engines' ) );
$this->timezone = new WPSEO_Sitemap_Timezone();
$this->router = new WPSEO_Sitemaps_Router();
$this->renderer = new WPSEO_Sitemaps_Renderer();
$this->cache = new WPSEO_Sitemaps_Cache();
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 = array(
new WPSEO_Post_Type_Sitemap_Provider(),
new WPSEO_Taxonomy_Sitemap_Provider(),
new WPSEO_Author_Sitemap_Provider(),
);
$external_providers = apply_filters( 'wpseo_sitemaps_providers', array() );
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 ( false !== stripos( $request_uri, 'sitemap' ) && in_array( $extension, array( '.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?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;
}
$xsl = get_query_var( 'xsl' );
if ( ! empty( $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( $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 ( true !== $this->cache->is_enabled() ) {
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;
}
$links = $provider->get_sitemap_links( $type, $entries_per_page, $this->current_page );
if ( empty( $links ) ) {
$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 = array();
$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: ' . gmdate( 'D, d M Y H:i:s', ( time() + $expires ) ) . ' GMT' );
readfile( WPSEO_PATH . 'css/main-sitemap.xsl' );
}
/**
* Spit out the generated sitemap and relevant headers and encoding information.
*/
public function output() {
if ( ! headers_sent() ) {
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; charset=' . esc_attr( $this->renderer->get_output_charset() ) );
}
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 = array( $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 = array();
// Consider using WPSEO_Post_Type::get_accessible_post_types() to filter out any `no-index` post-types.
$post_type_names = get_post_types( array( 'public' => true ) );
if ( ! empty( $post_type_names ) ) {
$sql = "
SELECT post_type, MAX(post_modified_gmt) AS date
FROM $wpdb->posts
WHERE post_status IN ('publish','inherit')
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->timezone->format_date( 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 ( '0' === get_option( 'blog_public' ) ) { // 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( 'http://www.google.com/webmasters/tools/ping?sitemap=' . $url, array( 'blocking' => false ) );
wp_remote_get( 'http://www.bing.com/ping?sitemap=' . $url, array( '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;
}
}

View File

@ -0,0 +1,277 @@
<?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;
/**
* 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;
}
/**
* @param int $max_entries Entries per sitemap.
*
* @return array
*/
public function get_index_links( $max_entries ) {
$taxonomies = get_taxonomies( array( 'public' => true ), 'objects' );
if ( empty( $taxonomies ) ) {
return array();
}
$taxonomy_names = array_filter( array_keys( $taxonomies ), array( $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 = array();
$term_args = array(
'hide_empty' => $hide_empty,
'fields' => 'ids',
);
foreach ( $taxonomy_names as $taxonomy_name ) {
$taxonomy_terms = get_terms( $taxonomy_name, $term_args );
if ( count( $taxonomy_terms ) > 0 ) {
$all_taxonomies[ $taxonomy_name ] = $taxonomy_terms;
}
}
$index = array();
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 = array(
'post_type' => $tax->object_type,
'tax_query' => array(
array(
'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[] = array(
'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.
*
* @return array
*/
public function get_sitemap_links( $type, $max_entries, $current_page ) {
global $wpdb;
$links = array();
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, array( $taxonomy->name ) );
$terms = get_terms( $taxonomy->name, array( 'hide_empty' => $hide_empty ) );
$terms = array_splice( $terms, $offset, $steps );
if ( empty( $terms ) ) {
$terms = array();
}
// 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 ('publish','inherit')
AND p.post_password = ''
";
foreach ( $terms as $term ) {
$url = array();
$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 ) );
$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, array( 'link_category', 'nav_menu' ), true ) ) {
return false;
}
if ( 'post_format' === $taxonomy_name && 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,70 @@
<?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 );
/**
* @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,117 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Structured_Data_Blocks
*/
/**
* Class WPSEO_FAQ_Block.
*/
class WPSEO_FAQ_Block implements WPSEO_WordPress_Integration {
/**
* Registers the how-to block as a server-side rendered block.
*
* @return void
*/
public function register_hooks() {
if ( ! function_exists( 'register_block_type' ) ) {
return;
}
register_block_type(
'yoast/faq-block',
array( 'render_callback' => array( $this, 'render' ) )
);
}
/**
* Renders the block.
*
* Because we can't save script tags in Gutenberg without sufficient user permissions,
* we render these server-side.
*
* @param array $attributes The attributes of the block.
* @param string $content The HTML content of the block.
*
* @return string The block preceded by its JSON-LD script.
*/
public function render( $attributes, $content ) {
if ( ! is_array( $attributes ) || ! is_singular() ) {
return $content;
}
$json_ld = $this->get_json_ld( $attributes );
return '<script type="application/ld+json">' . WPSEO_Utils::format_json_encode( $json_ld ) . '</script>' . $content;
}
/**
* Returns the JSON-LD for a FAQ block in array form.
*
* @param array $attributes The attributes of the FAQ block.
*
* @return array The JSON-LD representation of the FAQ block in array form.
*/
protected function get_json_ld( array $attributes ) {
$json_ld = array(
'@context' => 'https://schema.org',
'@type' => 'FAQPage',
);
$post_title = get_the_title();
if ( ! empty( $post_title ) ) {
$json_ld['name'] = $post_title;
}
if ( ! array_key_exists( 'questions', $attributes ) || ! is_array( $attributes['questions'] ) ) {
return $json_ld;
}
$main_entity = array();
$questions = array_filter( $attributes['questions'], 'is_array' );
foreach ( $questions as $question ) {
$main_entity[] = $this->get_question_json_ld( $question );
}
$json_ld['mainEntity'] = $main_entity;
return $json_ld;
}
/**
* Returns the JSON-LD for a question in a FAQ block in array form.
*
* @param array $question The attributes of a question in the FAQ block.
*
* @return array The JSON-LD representation of the question in a FAQ block in array form.
*/
protected function get_question_json_ld( array $question ) {
$json_ld = array(
'@type' => 'Question',
);
if ( ! empty( $question['jsonQuestion'] ) ) {
$json_ld['name'] = $question['jsonQuestion'];
}
if ( ! empty( $question['jsonAnswer'] ) ) {
$json_ld['answerCount'] = 1;
$json_ld['acceptedAnswer'] = array(
'@type' => 'Answer',
'text' => $question['jsonAnswer'],
);
if ( ! empty( $question['jsonImageSrc'] ) ) {
$json_ld['acceptedAnswer']['image'] = array(
'@type' => 'ImageObject',
'contentUrl' => $question['jsonImageSrc'],
);
}
}
return $json_ld;
}
}

View File

@ -0,0 +1,138 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Structured_Data_Blocks
*/
/**
* Class WPSEO_How_To_Block.
*/
class WPSEO_How_To_Block implements WPSEO_WordPress_Integration {
/**
* Registers the how-to block as a server-side rendered block.
*
* @return void
*/
public function register_hooks() {
if ( ! function_exists( 'register_block_type' ) ) {
return;
}
register_block_type(
'yoast/how-to-block',
array( 'render_callback' => array( $this, 'render' ) )
);
}
/**
* Renders the block.
*
* Because we can't save script tags in Gutenberg without sufficient user permissions,
* we render these server-side.
*
* @param array $attributes The attributes of the block.
* @param string $content The HTML content of the block.
*
* @return string The block preceded by its JSON-LD script.
*/
public function render( $attributes, $content ) {
if ( ! is_array( $attributes ) || ! is_singular() ) {
return $content;
}
$json_ld = $this->get_json_ld( $attributes );
return '<script type="application/ld+json">' . WPSEO_Utils::format_json_encode( $json_ld ) . '</script>' . $content;
}
/**
* Returns the JSON-LD for a how-to block.
*
* @param array $attributes The attributes of the how-to block.
*
* @return array The JSON-LD representation of the how-to block.
*/
protected function get_json_ld( array $attributes ) {
$json_ld = array(
'@context' => 'https://schema.org',
'@type' => 'HowTo',
);
$post_title = get_the_title();
if ( ! empty( $post_title ) ) {
$json_ld['name'] = $post_title;
}
if ( ! empty( $attributes['hasDuration'] ) && $attributes['hasDuration'] === true ) {
$days = empty( $attributes['days'] ) ? 0 : $attributes['days'];
$hours = empty( $attributes['hours'] ) ? 0 : $attributes['hours'];
$minutes = empty( $attributes['minutes'] ) ? 0 : $attributes['minutes'];
if ( ( $days + $hours + $minutes ) > 0 ) {
$json_ld['totalTime'] = 'P' . $days . 'DT' . $hours . 'H' . $minutes . 'M';
}
}
if ( ! empty( $attributes['jsonDescription'] ) ) {
$json_ld['description'] = $attributes['jsonDescription'];
}
if ( ! empty( $attributes['steps'] ) && is_array( $attributes['steps'] ) ) {
$json_ld['step'] = array();
$steps = array_filter( $attributes['steps'], 'is_array' );
foreach ( $steps as $step ) {
$json_ld['step'][] = $this->get_section_json_ld( $step );
}
}
return $json_ld;
}
/**
* Returns the JSON-LD for a step-section in a how-to block.
*
* @param array $step The attributes of a step-section in the how-to block.
*
* @return array The JSON-LD representation of the step-section in a how-to block.
*/
protected function get_section_json_ld( array $step ) {
$section_json_ld = array(
'@type' => 'HowToSection',
'itemListElement' => $this->get_step_json_ld( $step ),
);
if ( ! empty( $step['jsonName'] ) ) {
$section_json_ld['name'] = $step['jsonName'];
}
if ( ! empty( $step['jsonImageSrc'] ) ) {
$section_json_ld['image'] = array(
'@type' => 'ImageObject',
'contentUrl' => $step['jsonImageSrc'],
);
}
return $section_json_ld;
}
/**
* Returns the JSON-LD for a step's description in a how-to block.
*
* @param array $step The attributes of a step(-section) in the how-to block.
*
* @return array The JSON-LD representation of the step's description in a how-to block.
*/
protected function get_step_json_ld( array $step ) {
$step_json_ld = array(
'@type' => 'HowToStep',
);
if ( ! empty( $step['jsonText'] ) ) {
$step_json_ld['text'] = $step['jsonText'];
}
return $step_json_ld;
}
}

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 = array() ) {
$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'] !== array() ) ) {
$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 !== array() ) {
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 !== array() ) {
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', array() );
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,213 @@
<?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], array( '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;
}
$options = WPSEO_Options::get_options( array( 'wpseo', 'wpseo_ms' ) );
if ( $options['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 );
}