first commit

This commit is contained in:
Rachit Bhargava
2023-07-21 17:12:10 -04:00
parent d0fe47dde4
commit 5d0f0734d8
14003 changed files with 2829464 additions and 0 deletions

View File

@@ -0,0 +1,258 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Statistics
*/
/**
* Class WPSEO_Statistics_Service.
*/
class WPSEO_Statistics_Service {
/**
* Cache transient id.
*
* @var string
*/
const CACHE_TRANSIENT_KEY = 'wpseo-statistics-totals';
/**
* Class that generates interesting statistics about things.
*
* @var WPSEO_Statistics
*/
protected $statistics;
/**
* Statistics labels.
*
* @var string[]
*/
protected $labels;
/**
* WPSEO_Statistics_Service contructor.
*
* @param WPSEO_Statistics $statistics The statistics class to retrieve statistics from.
*/
public function __construct( WPSEO_Statistics $statistics ) {
$this->statistics = $statistics;
}
/**
* Fetches statistics by REST request.
*
* @return WP_REST_Response The response object.
*/
public function get_statistics() {
// Switch to the user locale with fallback to the site locale.
switch_to_locale( \get_user_locale() );
$this->labels = $this->labels();
$statistics = $this->statistic_items();
$data = [
'header' => $this->get_header_from_statistics( $statistics ),
'seo_scores' => $statistics['scores'],
];
return new WP_REST_Response( $data );
}
/**
* Gets a header summarizing the given statistics results.
*
* @param array $statistics The statistics results.
*
* @return string The header summing up the statistics results.
*/
private function get_header_from_statistics( array $statistics ) {
// Personal interpretation to allow release, should be looked at later.
if ( $statistics['division'] === false ) {
return __( 'You don\'t have any published posts, your SEO scores will appear here once you make your first post!', 'wordpress-seo' );
}
if ( $statistics['division']['good'] > 0.66 ) {
return __( 'Hey, your SEO is doing pretty well! Check out the stats:', 'wordpress-seo' );
}
return __( 'Below are your published posts\' SEO scores. Now is as good a time as any to start improving some of your posts!', 'wordpress-seo' );
}
/**
* An array representing items to be added to the At a Glance dashboard widget.
*
* @return array The statistics for the current user.
*/
private function statistic_items() {
$transient = $this->get_transient();
$user_id = get_current_user_id();
if ( isset( $transient[ $user_id ] ) ) {
return $transient[ $user_id ];
}
return $this->set_statistic_items_for_user( $transient, $user_id );
}
/**
* Gets the statistics transient value. Returns array if transient wasn't set.
*
* @return array|mixed Returns the transient or an empty array if the transient doesn't exist.
*/
private function get_transient() {
$transient = get_transient( self::CACHE_TRANSIENT_KEY );
if ( $transient === false ) {
return [];
}
return $transient;
}
/**
* Set the statistics transient cache for a specific user.
*
* @param array $transient The current stored transient with the cached data.
* @param int $user The user's ID to assign the retrieved values to.
*
* @return array The statistics transient for the user.
*/
private function set_statistic_items_for_user( $transient, $user ) {
$scores = $this->get_seo_scores_with_post_count();
$division = $this->get_seo_score_division( $scores );
$transient[ $user ] = [
// Use array_values because array_filter may return non-zero indexed arrays.
'scores' => array_values( array_filter( $scores, [ $this, 'filter_items' ] ) ),
'division' => $division,
];
set_transient( self::CACHE_TRANSIENT_KEY, $transient, DAY_IN_SECONDS );
return $transient[ $user ];
}
/**
* Gets the division of SEO scores.
*
* @param array $scores The SEO scores.
*
* @return array|bool The division of SEO scores, false if there are no posts.
*/
private function get_seo_score_division( array $scores ) {
$total = 0;
$division = [];
foreach ( $scores as $score ) {
$total += $score['count'];
}
if ( $total === 0 ) {
return false;
}
foreach ( $scores as $score ) {
$division[ $score['seo_rank'] ] = ( $score['count'] / $total );
}
return $division;
}
/**
* Get all SEO ranks and data associated with them.
*
* @return array An array of SEO scores and associated data.
*/
private function get_seo_scores_with_post_count() {
$ranks = WPSEO_Rank::get_all_ranks();
return array_map( [ $this, 'map_rank_to_widget' ], $ranks );
}
/**
* Converts a rank to data usable in the dashboard widget.
*
* @param WPSEO_Rank $rank The rank to map.
*
* @return array The mapped rank.
*/
private function map_rank_to_widget( WPSEO_Rank $rank ) {
return [
'seo_rank' => $rank->get_rank(),
'label' => $this->get_label_for_rank( $rank ),
'count' => $this->statistics->get_post_count( $rank ),
'link' => $this->get_link_for_rank( $rank ),
];
}
/**
* Returns a dashboard widget label to use for a certain rank.
*
* @param WPSEO_Rank $rank The rank to return a label for.
*
* @return string The label for the rank.
*/
private function get_label_for_rank( WPSEO_Rank $rank ) {
return $this->labels[ $rank->get_rank() ];
}
/**
* Determines the labels for the various scoring ranks that are known within Yoast SEO.
*
* @return array Array containing the translatable labels.
*/
private function labels() {
return [
WPSEO_Rank::NO_FOCUS => sprintf(
/* translators: %1$s expands to an opening strong tag, %2$s expands to a closing strong tag */
__( 'Posts %1$swithout%2$s a focus keyphrase', 'wordpress-seo' ),
'<strong>',
'</strong>'
),
WPSEO_Rank::BAD => sprintf(
/* translators: %s expands to the score */
__( 'Posts with the SEO score: %s', 'wordpress-seo' ),
'<strong>' . __( 'Needs improvement', 'wordpress-seo' ) . '</strong>'
),
WPSEO_Rank::OK => sprintf(
/* translators: %s expands to the score */
__( 'Posts with the SEO score: %s', 'wordpress-seo' ),
'<strong>' . __( 'OK', 'wordpress-seo' ) . '</strong>'
),
WPSEO_Rank::GOOD => sprintf(
/* translators: %s expands to the score */
__( 'Posts with the SEO score: %s', 'wordpress-seo' ),
'<strong>' . __( 'Good', 'wordpress-seo' ) . '</strong>'
),
WPSEO_Rank::NO_INDEX => __( 'Posts that should not show up in search results', 'wordpress-seo' ),
];
}
/**
* Filter items if they have a count of zero.
*
* @param array $item The item to potentially filter out.
*
* @return bool Whether or not the count is zero.
*/
private function filter_items( $item ) {
return $item['count'] !== 0;
}
/**
* Returns a link for the overview of posts of a certain rank.
*
* @param WPSEO_Rank $rank The rank to return a link for.
*
* @return string The link that shows an overview of posts with that rank.
*/
private function get_link_for_rank( WPSEO_Rank $rank ) {
if ( current_user_can( 'edit_others_posts' ) === false ) {
return esc_url( admin_url( 'edit.php?post_status=publish&post_type=post&seo_filter=' . $rank->get_rank() . '&author=' . get_current_user_id() ) );
}
return esc_url( admin_url( 'edit.php?post_status=publish&post_type=post&seo_filter=' . $rank->get_rank() ) );
}
}

View File

@@ -0,0 +1,85 @@
<?php
// phpcs:ignore Yoast.NamingConventions.NamespaceName.Invalid
namespace Yoast\WP\SEO\Integrations\Blocks;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Presenters\Url_List_Presenter;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* Siblings block class
*/
class Siblings_Block extends Dynamic_Block {
/**
* The name of the block.
*
* @var string
*/
protected $block_name = 'siblings';
/**
* The editor script for the block.
*
* @var string
*/
protected $script = 'wp-seo-premium-dynamic-blocks';
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
private $indexable_repository;
/**
* Siblings_Block constructor.
*
* @param Indexable_Repository $indexable_repository The indexable repository.
*/
public function __construct( Indexable_Repository $indexable_repository ) {
$this->indexable_repository = $indexable_repository;
}
/**
* Presents the block output.
*
* @param array $attributes The block attributes.
*
* @return string The block output.
*/
public function present( $attributes ) {
$post_parent_id = \wp_get_post_parent_id( null );
if ( $post_parent_id === false || $post_parent_id === 0 ) {
return '';
}
$indexables = $this->indexable_repository->get_subpages_by_post_parent(
$post_parent_id,
[ \get_the_ID() ]
);
$links = array_map(
static function( Indexable $indexable ) {
return [
'title' => $indexable->breadcrumb_title,
'permalink' => $indexable->permalink,
];
},
$indexables
);
if ( empty( $links ) ) {
return '';
}
$class_name = 'yoast-url-list';
if ( ! empty( $attributes['className'] ) ) {
$class_name .= ' ' . \esc_attr( $attributes['className'] );
}
$presenter = new Url_List_Presenter( $links, $class_name );
return $presenter->present();
}
}

View File

@@ -0,0 +1,78 @@
<?php
// phpcs:ignore Yoast.NamingConventions.NamespaceName.Invalid
namespace Yoast\WP\SEO\Integrations\Blocks;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Presenters\Url_List_Presenter;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* Subpages block class
*/
class Subpages_Block extends Dynamic_Block {
/**
* The name of the block.
*
* @var string
*/
protected $block_name = 'subpages';
/**
* The editor script for the block.
*
* @var string
*/
protected $script = 'wp-seo-premium-dynamic-blocks';
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
private $indexable_repository;
/**
* Subpages_Block constructor.
*
* @param Indexable_Repository $indexable_repository The indexable repository.
*/
public function __construct( Indexable_Repository $indexable_repository ) {
$this->indexable_repository = $indexable_repository;
}
/**
* Presents the block output.
*
* @param array $attributes The block attributes.
*
* @return string The block output.
*/
public function present( $attributes ) {
$indexables = $this->indexable_repository->get_subpages_by_post_parent( \get_the_ID() );
$links = array_map(
static function( Indexable $indexable ) {
return [
'title' => $indexable->breadcrumb_title,
'permalink' => $indexable->permalink,
];
},
$indexables
);
if ( empty( $links ) ) {
return '';
}
$class_name = 'yoast-url-list';
if ( ! empty( $attributes['className'] ) ) {
$class_name .= ' ' . \esc_attr( $attributes['className'] );
}
$presenter = new Url_List_Presenter( $links, $class_name );
return $presenter->present();
}
}

View File

@@ -0,0 +1,112 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Enqueues a JavaScript plugin for YoastSEO.js that adds custom fields to the content that were defined in the titles
* and meta's section of the Yoast SEO settings when those fields are available.
*/
class WPSEO_Custom_Fields_Plugin implements WPSEO_WordPress_Integration {
/**
* Initialize the AJAX hooks.
*
* @codeCoverageIgnore Method relies on dependencies.
*
* @return void
*/
public function register_hooks() {
global $pagenow;
if ( ! WPSEO_Metabox::is_post_edit( $pagenow ) && ! WPSEO_Metabox::is_post_overview( $pagenow ) ) {
return;
}
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
}
/**
* Enqueues all the needed JS scripts.
*
* @codeCoverageIgnore Method relies on WordPress functions.
*
* @return void
*/
public function enqueue() {
wp_enqueue_script( 'wp-seo-premium-custom-fields-plugin' );
wp_localize_script( 'wp-seo-premium-custom-fields-plugin', 'YoastCustomFieldsPluginL10', $this->localize_script() );
}
/**
* Loads the custom fields translations.
*
* @return array The fields to localize.
*/
public function localize_script() {
return [
'custom_field_names' => $this->get_custom_field_names(),
];
}
/**
* Retrieve all custom field names set in SEO ->
*
* @return array The custom field names.
*/
protected function get_custom_field_names() {
$custom_field_names = [];
$post = $this->get_post();
if ( ! is_object( $post ) ) {
return $custom_field_names;
}
$options = $this->get_titles_from_options();
$target_option = 'page-analyse-extra-' . $post->post_type;
if ( array_key_exists( $target_option, $options ) ) {
$custom_field_names = explode( ',', $options[ $target_option ] );
}
return $custom_field_names;
}
/**
* Retrieves post data given a post ID or the global.
*
* @codeCoverageIgnore Method relies on dependencies.
*
* @return WP_Post|array|null Returns a post if found, otherwise returns an empty array.
*/
protected function get_post() {
$post = filter_input( INPUT_GET, 'post' );
if ( isset( $post ) && $post !== false ) {
$post_id = (int) WPSEO_Utils::validate_int( $post );
return get_post( $post_id );
}
if ( isset( $GLOBALS['post'] ) ) {
return $GLOBALS['post'];
}
return [];
}
/**
* Retrieves the value of the WPSEO_Titles option.
*
* @codeCoverageIgnore Method relies on the options.
*
* @return array The value from WPSEO_Titles option.
*/
protected function get_titles_from_options() {
$option_name = WPSEO_Options::get_option_instance( 'wpseo_titles' )->option_name;
return get_option( $option_name, [] );
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium
*/
/**
* Class WPSEO_Premium_Prominent_Words_Language_Support.
*
* @deprecated 14.7
* @codeCoverageIgnore
*/
class WPSEO_Premium_Prominent_Words_Language_Support {
/**
* List of supported languages.
*
* @var string[]
*/
protected $supported_languages = [ 'en', 'de', 'nl', 'es', 'fr', 'it', 'pt', 'ru', 'pl', 'sv', 'id' ];
/**
* Returns whether the current language is supported for the link suggestions.
*
* @deprecated 14.7
* @codeCoverageIgnore
*
* @param string $language The language to check for.
*
* @return bool Whether the current language is supported for the link suggestions.
*/
public function is_language_supported( $language ) {
_deprecated_function( __METHOD__, 'WPSEO Premium 14.7' );
return in_array( $language, $this->supported_languages, true );
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium
*/
_deprecated_file( __FILE__, 'WPSEO Premium 14.5' );
/**
* Registers the endpoint for the prominent words recalculation to WordPress.
*/
class WPSEO_Premium_Prominent_Words_Recalculation_Endpoint implements WPSEO_WordPress_Integration {
/**
* The REST API namespace.
*
* @var string
*/
const REST_NAMESPACE = 'yoast/v1';
/**
* The REST API endpoint.
*
* @var string
*/
const ENDPOINT_QUERY = 'complete_recalculation';
/**
* The capability needed to retrieve the recalculation data.
*
* @var string
*/
const CAPABILITY_RETRIEVE = 'edit_posts';
/**
* WPSEO_Premium_Prominent_Words_Recalculation_Endpoint constructor.
*
* @deprecated 14.5
* @codeCoverageIgnore
*
* @param WPSEO_Premium_Prominent_Words_Recalculation_Service $service Unused. The service to handle the requests to the endpoint.
*/
public function __construct( WPSEO_Premium_Prominent_Words_Recalculation_Service $service ) {
_deprecated_function( __METHOD__, 'WPSEO Premium 14.5' );
}
/**
* Registers all hooks to WordPress.
*
* @deprecated 14.5
* @codeCoverageIgnore
*/
public function register_hooks() {
_deprecated_function( __METHOD__, 'WPSEO Premium 14.5' );
}
/**
* Register the REST endpoint to WordPress.
*
* @deprecated 14.5
* @codeCoverageIgnore
*/
public function register() {
_deprecated_function( __METHOD__, 'WPSEO Premium 14.5' );
}
/**
* Determines if the current user is allowed to use this endpoint.
*
* @deprecated 14.5
* @codeCoverageIgnore
*
* @return bool
*/
public function can_retrieve_data() {
_deprecated_function( __METHOD__, 'WPSEO Premium 14.5' );
return current_user_can( self::CAPABILITY_RETRIEVE );
}
}

View File

@@ -0,0 +1,86 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium
*/
_deprecated_file( __FILE__, 'WPSEO Premium 14.5' );
/**
* Handles adding site wide analysis UI to the WordPress admin.
*/
class WPSEO_Premium_Prominent_Words_Recalculation_Notifier implements WPSEO_WordPress_Integration {
const NOTIFICATION_ID = 'wpseo-premium-prominent-words-recalculate';
const UNINDEXED_THRESHOLD = 10;
/**
* Registers all hooks to WordPress
*
* @deprecated 14.5
* @codeCoverageIgnore
*/
public function register_hooks() {
_deprecated_function( __METHOD__, 'WPSEO Premium 14.5' );
}
/**
* Removes the notification when it is set and the amount of unindexed items is lower than the threshold.
*
* @deprecated 14.5
* @codeCoverageIgnore
*/
public function cleanup_notification() {
_deprecated_function( __METHOD__, 'WPSEO Premium 14.5' );
}
/**
* Adds the notification when it isn't set already and the amount of unindexed items is greater than the set.
* threshold.
*
* @deprecated 14.5
* @codeCoverageIgnore
*/
public function manage_notification() {
_deprecated_function( __METHOD__, 'WPSEO Premium 14.5' );
}
/**
* Handles the option change to make sure the notification will be removed when link suggestions are disabled.
*
* @deprecated 14.5
* @codeCoverageIgnore
*
* @param mixed $old_value The old value.
* @param mixed $new_value The new value.
*/
public function handle_option_change( $old_value, $new_value ) {
_deprecated_function( __METHOD__, 'WPSEO Premium 14.5' );
}
/**
* Checks if the notification has been set already.
*
* @deprecated 14.5
* @codeCoverageIgnore
*
* @return bool True when there is a notification.
*/
public function has_notification() {
_deprecated_function( __METHOD__, 'WPSEO Premium 14.5' );
return false;
}
/**
* Migration for removing the persistent notification.
*
* @deprecated 14.5
* @codeCoverageIgnore
*/
public static function upgrade_12_8() {
_deprecated_function( __METHOD__, 'WPSEO Premium 14.5' );
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium
*/
_deprecated_file( __FILE__, 'WPSEO Premium 14.5' );
/**
* Represents the service for the recalculation.
*/
class WPSEO_Premium_Prominent_Words_Recalculation_Service {
/**
* Removes the recalculation notification.
*
* @deprecated 14.5
* @codeCoverageIgnore
*
* @param WP_REST_Request $request The current request. Unused.
*
* @return WP_REST_Response The response to give.
*/
public function remove_notification( WP_REST_Request $request ) {
_deprecated_function( __METHOD__, 'WPSEO Premium 14.5' );
return new WP_REST_Response( '1' );
}
}

View File

@@ -0,0 +1,228 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Export
*/
/**
* Class WPSEO_Export_Keywords_CSV
*
* Exports data as returned by WPSEO_Export_Keywords_Presenter to CSV.
*/
class WPSEO_Export_Keywords_CSV {
/**
* The columns that should be presented.
*
* @var array
*/
protected $columns;
/**
* Data to be exported.
*
* @var array
*/
protected $data = '';
/**
* WPSEO_Export_Keywords_CSV constructor.
*
* Supported values for columns are 'title', 'url', 'keywords', 'readability_score' and 'keywords_score'.
* Requesting 'keywords_score' will always also return 'keywords'.
*
* @param array $columns An array of columns that should be presented.
*/
public function __construct( array $columns ) {
$this->columns = array_filter( $columns, 'is_string' );
}
/**
* Echoes the CSV headers
*/
public function print_headers() {
// phpcs:ignore WordPress.Security.EscapeOutput -- Correctly escaped in get_headers() method below.
echo $this->get_headers();
}
/**
* Echoes a formatted row.
*
* @param array $row Row to add to the export.
*
* @return void
*/
public function print_row( $row ) {
echo $this->format( $row );
}
/**
* Returns the CSV headers based on the queried columns.
*
* @return string The headers in CSV format.
*/
protected function get_headers() {
$header_columns = [
'title' => esc_html__( 'title', 'wordpress-seo-premium' ),
'url' => esc_html__( 'url', 'wordpress-seo-premium' ),
'readability_score' => esc_html__( 'readability score', 'wordpress-seo-premium' ),
'keywords' => esc_html__( 'keyphrase', 'wordpress-seo-premium' ),
'keywords_score' => esc_html__( 'keyphrase score', 'wordpress-seo-premium' ),
'seo_title' => esc_html__( 'seo title', 'wordpress-seo-premium' ),
'meta_description' => esc_html__( 'meta description', 'wordpress-seo-premium' ),
];
$csv = $this->sanitize_csv_column( esc_html__( 'ID', 'wordpress-seo-premium' ) );
$csv .= ',' . $this->sanitize_csv_column( esc_html_x( 'type', 'post_type of a post or the taxonomy of a term', 'wordpress-seo-premium' ) );
foreach ( $this->columns as $column ) {
if ( array_key_exists( $column, $header_columns ) ) {
$csv .= ',' . $this->sanitize_csv_column( $header_columns[ $column ] );
}
}
$csv .= PHP_EOL;
return $csv;
}
/**
* Formats a WPSEO_Export_Keywords_Query result as a CSV line.
* In case of multiple keywords it will return multiple lines.
*
* @param array $result A result as returned from WPSEO_Export_Keywords_Query::get_data.
*
* @return string A line of CSV, beginning with EOL.
*/
protected function format( array $result ) {
// If our input is malformed return an empty string.
if ( ! array_key_exists( 'ID', $result ) || ! array_key_exists( 'type', $result ) ) {
return '';
}
// Ensure we have arrays in the keywords.
$result['keywords'] = $this->get_array_from_result( $result, 'keywords' );
$result['keywords_score'] = $this->get_array_from_result( $result, 'keywords_score' );
$csv = '';
// Add at least one row plus additional ones if we have more keywords.
$keywords = max( 1, count( $result['keywords'] ) );
for ( $keywords_index = 0; $keywords_index < $keywords; $keywords_index++ ) {
// Add static columns.
$csv .= $this->sanitize_csv_column( $result['ID'] );
$csv .= ',' . $this->sanitize_csv_column( $result['type'] );
// Add dynamic columns.
foreach ( $this->columns as $column ) {
$csv .= $this->get_csv_column_from_result( $result, $column, $keywords_index );
}
$csv .= PHP_EOL;
}
return $csv;
}
/**
* Returns a CSV column, including comma, from the result object based on the specified key
*
* @param array $result The result object.
* @param string $key The key of the value to get the CSV column for.
* @param int $keywords_index The number keyword to output.
*
* @return string CSV formatted column.
*/
protected function get_csv_column_from_result( array $result, $key, $keywords_index ) {
if ( in_array( $key, [ 'title', 'url', 'seo_title', 'meta_description', 'readability_score' ], true ) ) {
return $this->get_csv_string_column_from_result( $result, $key );
}
if ( in_array( $key, [ 'keywords', 'keywords_score' ], true ) ) {
return $this->get_csv_array_column_from_result( $result, $key, $keywords_index );
}
return '';
}
/**
* Returns an array from the result object.
*
* @param array $result The result object.
* @param string $key The key of the array to retrieve.
*
* @return array Contents of the key in the object.
*/
protected function get_array_from_result( array $result, $key ) {
if ( array_key_exists( $key, $result ) && is_array( $result[ $key ] ) ) {
return $result[ $key ];
}
return [];
}
/**
* Returns a CSV column, including comma, from the result object by the specified key.
* Expects the value to be a string.
*
* @param array $result The result object to get the CSV column from.
* @param string $key The key of the value to get the CSV column for.
*
* @return string A CSV formatted column.
*/
protected function get_csv_string_column_from_result( array $result, $key ) {
if ( array_key_exists( $key, $result ) ) {
return ',' . $this->sanitize_csv_column( $result[ $key ] );
}
return ',';
}
/**
* Returns a CSV column, including comma, from the result object by the specified key.
* Expects the value to be inside an array.
*
* @param array $result The result object to get the CSV column from.
* @param string $key The key of the array to get the CSV column for.
* @param int $index The index of the value in the array.
*
* @return string A CSV formatted column.
*/
protected function get_csv_array_column_from_result( array $result, $key, $index ) {
// If the array has an element at $index.
if ( $index < count( $result[ $key ] ) ) {
return ',' . $this->sanitize_csv_column( $result[ $key ][ $index ] );
}
return ',';
}
/**
* Sanitizes a value to be output as a CSV value.
*
* @param string $value The value to sanitize.
*
* @return string The sanitized value.
*/
protected function sanitize_csv_column( $value ) {
// Return an empty string if value is null.
if ( $value === null ) {
return '';
}
// Convert non-string values to strings.
if ( ! is_string( $value ) ) {
$value = var_export( $value, true );
}
// Replace all whitespace with spaces because Excel can't deal with newlines and tabs even if escaped.
$value = preg_replace( '/\s/', ' ', $value );
// Escape double quotes.
$value = str_replace( '"', '""', $value );
// Return the value enclosed in double quotes.
return '"' . $value . '"';
}
}

View File

@@ -0,0 +1,228 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Export
*/
/**
* Class WPSEO_Export_Keywords_Presenter
*
* Readies data as returned by WPSEO_Export_Keywords_Post_Query for exporting.
*/
class WPSEO_Export_Keywords_Post_Presenter implements WPSEO_Export_Keywords_Presenter {
/**
* The columns to query for.
*
* @var array
*/
protected $columns;
/**
* WPSEO_Export_Keywords_Post_Presenter constructor.
*
* Supported values for columns are 'title', 'url', 'keywords', 'readability_score' and 'keywords_score'.
* Requesting 'keywords_score' will always also return 'keywords'.
*
* @param array $columns The columns we want our query to return.
*/
public function __construct( array $columns ) {
$this->columns = array_filter( $columns, 'is_string' );
}
/**
* Creates a presentable result by modifying and adding the requested fields.
*
* @param array $result The result to modify.
*
* @return array The modified result or an empty array if the result is considered invalid.
*/
public function present( array $result ) {
if ( ! $this->validate_result( $result ) ) {
return [];
}
foreach ( $this->columns as $column ) {
$result = $this->prepare_column_result( $result, $column );
}
$result['type'] = $result['post_type'];
unset( $result['post_type'] );
return $result;
}
/**
* Prepares the passed result to make it more presentable.
*
* @param array $result The result to modify.
* @param string $column The requested column.
*
* @return array The prepared result.
*/
protected function prepare_column_result( array $result, $column ) {
switch ( $column ) {
case 'title':
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Using WP native filter.
$result['title'] = apply_filters( 'the_title', $result['post_title'], $result['ID'] );
unset( $result['post_title'] );
break;
case 'url':
$result['url'] = get_permalink( $result['ID'] );
break;
case 'readability_score':
$result['readability_score'] = WPSEO_Rank::from_numeric_score( (int) $result['readability_score'] )->get_label();
break;
case 'keywords':
$result = $this->convert_result_keywords( $result );
break;
}
return $result;
}
/**
* Returns whether a result to present is a valid result.
*
* @param array $result The result to validate.
*
* @return bool True for a value valid result.
*/
protected function validate_result( array $result ) {
// If there is no ID then it's not valid.
if ( ! array_key_exists( 'ID', $result ) ) {
return false;
}
// If a title is requested but not present then it's not valid.
if ( $this->column_is_present( 'title' ) && $this->has_title( $result ) === false ) {
return false;
}
return true;
}
/**
* Determines if the result contains a valid title.
*
* @param array $result The result array to check for a title.
*
* @return bool Whether or not a title is valid.
*/
protected function has_title( $result ) {
if ( ! is_array( $result ) || ! array_key_exists( 'post_title', $result ) ) {
return false;
}
return is_string( $result['post_title'] );
}
/**
* Determines if the wanted column exists within the $this->columns class variable.
*
* @param string $column The column to search for.
*
* @return bool Whether or not the column exists.
*/
protected function column_is_present( $column ) {
if ( ! is_string( $column ) ) {
return false;
}
return in_array( $column, $this->columns, true );
}
/**
* Converts the results of the query from strings and JSON string to keyword arrays.
*
* @param array $result The result to convert.
*
* @return array The converted result.
*/
protected function convert_result_keywords( array $result ) {
$result['keywords'] = [];
if ( $this->column_is_present( 'keywords_score' ) ) {
$result['keywords_score'] = [];
}
if ( $this->has_primary_keyword( $result ) ) {
$result['keywords'][] = $result['primary_keyword'];
// Convert multiple keywords from the Premium plugin from json to string arrays.
$keywords = $this->parse_result_keywords_json( $result, 'other_keywords' );
$other_keywords = wp_list_pluck( $keywords, 'keyword' );
$result['keywords'] = array_merge( $result['keywords'], $other_keywords );
if ( $this->column_is_present( 'keywords_score' ) ) {
$result['keywords_score'] = $this->get_result_keywords_scores( $result, $keywords );
}
}
// Unset all old variables, if they do not exist nothing will happen.
unset( $result['primary_keyword'], $result['primary_keyword_score'], $result['other_keywords'] );
return $result;
}
/**
* Determines whether there's a valid primary keyword present in the result array.
*
* @param array $result The result array to check for the primary_keyword key.
*
* @return bool Whether or not a valid primary keyword is present.
*/
protected function has_primary_keyword( $result ) {
if ( ! is_array( $result ) || ! array_key_exists( 'primary_keyword', $result ) ) {
return false;
}
return is_string( $result['primary_keyword'] ) && ! empty( $result['primary_keyword'] );
}
/**
* Parses then keywords JSON string in the result object for the specified key.
*
* @param array $result The result object.
* @param string $key The key containing the JSON.
*
* @return array The parsed keywords.
*/
protected function parse_result_keywords_json( array $result, $key ) {
if ( empty( $result[ $key ] ) ) {
return [];
}
$parsed = json_decode( $result[ $key ], true );
if ( ! $parsed ) {
return [];
}
return $parsed;
}
/**
* Returns an array of all scores from the result object and the parsed keywords JSON.
*
* @param array $result The result object.
* @param array $keywords The parsed keywords.
*
* @return array The keyword scores.
*/
protected function get_result_keywords_scores( array $result, $keywords ) {
$scores = [];
$rank = WPSEO_Rank::from_numeric_score( (int) $result['primary_keyword_score'] );
$scores[] = $rank->get_label();
foreach ( $keywords as $keyword ) {
$rank = new WPSEO_Rank( $keyword['score'] );
$scores[] = $rank->get_label();
}
return $scores;
}
}

View File

@@ -0,0 +1,179 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Export
*/
/**
* Class WPSEO_Export_Keywords_Query
*
* Creates an SQL query to gather all post data for a keywords export.
*/
class WPSEO_Export_Keywords_Post_Query implements WPSEO_Export_Keywords_Query {
/**
* The WordPress database object.
*
* @var wpdb
*/
protected $wpdb;
/**
* The columns to query for.
*
* @var array
*/
protected $columns;
/**
* The database columns to select in the query.
*
* @var array
*/
protected $selects;
/**
* The database tables to join in the query.
*
* @var array
*/
protected $joins = [];
/**
* Number of items to fetch per page.
*
* @var int
*/
protected $page_size;
/**
* Escaped list of post types.
*
* @var string
*/
protected $escaped_post_types;
/**
* WPSEO_Export_Keywords_Query constructor.
*
* Supported values for columns are 'title', 'url', 'keywords', 'readability_score' and 'keywords_score'.
* Requesting 'keywords_score' will always also return 'keywords'.
*
* @param wpdb $wpdb A WordPress Database object.
* @param array $columns List of columns that need to be retrieved.
* @param int $page_size Number of items to retrieve.
*/
public function __construct( $wpdb, array $columns, $page_size = 1000 ) {
$this->wpdb = $wpdb;
$this->page_size = max( 1, (int) $page_size );
$this->set_columns( $columns );
}
/**
* Constructs the query and executes it, returning an array of objects containing the columns this object was constructed with.
* Every object will always contain the ID column.
*
* @param int $page Paginated page to retrieve.
*
* @return array An array of associative arrays containing the keys as requested in the constructor.
*/
public function get_data( $page = 1 ) {
if ( $this->columns === [] ) {
return [];
}
$post_types = WPSEO_Post_Type::get_accessible_post_types();
if ( empty( $post_types ) ) {
return [];
}
// Pages have a starting index of 1, we need to convert to a 0 based offset.
$offset_multiplier = max( 0, ( $page - 1 ) );
$replacements = $post_types;
$replacements[] = $this->page_size;
$replacements[] = ( $offset_multiplier * $this->page_size );
// Construct the query.
$query = $this->wpdb->prepare(
'SELECT ' . implode( ', ', $this->selects )
. ' FROM ' . $this->wpdb->prefix . 'posts AS posts '
. implode( ' ', $this->joins )
. ' WHERE posts.post_status = "publish" AND posts.post_type IN ('
. implode( ',', array_fill( 0, count( $post_types ), '%s' ) ) . ')'
. ' LIMIT %d OFFSET %d',
$replacements
);
return $this->wpdb->get_results( $query, ARRAY_A );
}
/**
* Prepares the necessary selects and joins to get all data in a single query.
*
* @param array $columns The columns we want our query to return.
*/
public function set_columns( array $columns ) {
$this->columns = $columns;
$this->joins = [];
$this->selects = [ 'posts.ID', 'posts.post_type' ];
if ( in_array( 'title', $this->columns, true ) ) {
$this->selects[] = 'posts.post_title';
}
// If we're selecting keywords_score then we always want the keywords as well.
if ( in_array( 'keywords', $this->columns, true ) || in_array( 'keywords_score', $this->columns, true ) ) {
$this->add_meta_join( 'primary_keyword', WPSEO_Meta::$meta_prefix . 'focuskw' );
$this->add_meta_join( 'other_keywords', WPSEO_Meta::$meta_prefix . 'focuskeywords' );
}
if ( in_array( 'readability_score', $this->columns, true ) ) {
$this->add_meta_join( 'readability_score', WPSEO_Meta::$meta_prefix . 'content_score' );
}
if ( in_array( 'keywords_score', $this->columns, true ) ) {
// Score for other keywords is already in the other_keywords select so only join for the primary_keyword_score.
$this->add_meta_join( 'primary_keyword_score', WPSEO_Meta::$meta_prefix . 'linkdex' );
}
if ( in_array( 'seo_title', $this->columns, true ) ) {
$this->add_meta_join( 'seo_title', WPSEO_Meta::$meta_prefix . 'title' );
}
if ( in_array( 'meta_description', $this->columns, true ) ) {
$this->add_meta_join( 'meta_description', WPSEO_Meta::$meta_prefix . 'metadesc' );
}
}
/**
* Returns the page size for the query.
*
* @return int Page size that is being used.
*/
public function get_page_size() {
return $this->page_size;
}
/**
* Adds an aliased join to the $wpdb->postmeta table so that multiple meta values can be selected in a single row.
*
* While this function should never be used with user input,
* all non-word non-digit characters are removed from both params for increased robustness.
*
* @param string $alias The alias to use in our query output.
* @param string $key The meta_key to select.
*/
protected function add_meta_join( $alias, $key ) {
$alias = preg_replace( '/[^\w\d]/', '', $alias );
$key = preg_replace( '/[^\w\d]/', '', $key );
$this->selects[] = $alias . '_join.meta_value AS ' . $alias;
$this->joins[] = 'LEFT OUTER JOIN ' . $this->wpdb->prefix . 'postmeta AS ' . $alias . '_join '
. 'ON ' . $alias . '_join.post_id = posts.ID '
. 'AND ' . $alias . '_join.meta_key = "' . $key . '"';
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Export
*/
/**
* Interface WPSEO_Export_Keywords_Presenter
*
* Readies data as returned by WPSEO_Export_Keywords_Query for exporting.
*/
interface WPSEO_Export_Keywords_Presenter {
/**
* WPSEO_Export_Keywords_Presenter constructor.
*
* Supported values for columns are 'title', 'url', 'keywords', 'readability_score' and 'keywords_score'.
* Requesting 'keywords_score' will always also return 'keywords'.
*
* @param array $columns The columns we want our query to return.
*/
public function __construct( array $columns );
/**
* Updates a result by modifying and adding the requested fields.
*
* @param array $result The result to modify.
*
* @return array The modified result.
*/
public function present( array $result );
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Export
*/
/**
* Interface WPSEO_Export_Keywords_Query
*
* Creates a SQL query to gather all data for a keywords export.
*/
interface WPSEO_Export_Keywords_Query {
/**
* Returns the page size for the query.
*
* @return int Page size that is being used.
*/
public function get_page_size();
/**
* Constructs the query and executes it, returning an array of objects containing the columns this object was constructed with.
* Every object will always contain the ID column.
*
* @param int $page Paginated page to retrieve.
*
* @return array An array of associative arrays containing the keys as requested in the constructor.
*/
public function get_data( $page = 1 );
/**
* Prepares the necessary selects and joins to get all data in a single query.
*
* @param array $columns The columns we want our query to return.
*/
public function set_columns( array $columns );
}

View File

@@ -0,0 +1,177 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Export
*/
/**
* Class WPSEO_Export_Keywords_Term_Presenter
*
* Readies data as returned by WPSEO_Export_Keywords_Term_Query for exporting.
*/
class WPSEO_Export_Keywords_Term_Presenter implements WPSEO_Export_Keywords_Presenter {
/**
* The columns to query for.
*
* @var array
*/
protected $columns;
/**
* WPSEO_Export_Keywords_Term_Presenter constructor.
*
* Supported values for columns are 'title', 'url', 'keywords', 'readability_score' and 'keywords_score'.
* Requesting 'keywords_score' will always also return 'keywords'.
*
* @param array $columns The columns we want our query to return.
*/
public function __construct( array $columns ) {
$this->columns = array_filter( $columns, 'is_string' );
}
/**
* Creates a presentable result by modifying and adding the requested fields.
*
* @param array $result The result to modify.
*
* @return array The modified result or an empty array if the result is considered invalid.
*/
public function present( array $result ) {
if ( ! $this->validate_result( $result ) ) {
return [];
}
$result['ID'] = (int) $result['term_id'];
unset( $result['term_id'] );
foreach ( $this->columns as $column ) {
$result = $this->prepare_column_result( $result, $column );
}
$result['type'] = $result['taxonomy'];
unset( $result['taxonomy'] );
return $result;
}
/**
* Prepares the passed result to make it more presentable.
*
* @param array $result The result to modify.
* @param string $column The requested column.
*
* @return array The prepared result.
*/
protected function prepare_column_result( array $result, $column ) {
switch ( $column ) {
case 'title':
$result['title'] = $result['name'];
unset( $result['name'] );
break;
case 'url':
$result['url'] = get_term_link( $result['ID'], $result['taxonomy'] );
break;
case 'readability_score':
$content_score = WPSEO_Taxonomy_Meta::get_term_meta( $result['ID'], $result['taxonomy'], 'content_score' );
$result['readability_score'] = WPSEO_Rank::from_numeric_score( (int) $content_score )->get_label();
break;
case 'keywords':
$result['keywords'] = $this->get_result_keywords( $result );
break;
case 'keywords_score':
$result['keywords_score'] = $this->get_result_keywords_score( $result );
break;
}
return $result;
}
/**
* Returns whether a result to present is a valid result.
*
* @param array $result The result to validate.
*
* @return bool True if the result is validated.
*/
protected function validate_result( array $result ) {
// If there is no ID then it's not valid.
if ( ! array_key_exists( 'term_id', $result ) ) {
return false;
}
// If a title is requested but not present then it's not valid.
if ( $this->column_is_present( 'title' ) && $this->has_title( $result ) === false ) {
return false;
}
return true;
}
/**
* Determines if the result contains a valid title.
*
* @param array $result The result array to check for a title.
*
* @return bool Whether or not a title is valid.
*/
protected function has_title( $result ) {
if ( ! is_array( $result ) || ! array_key_exists( 'name', $result ) ) {
return false;
}
return is_string( $result['name'] );
}
/**
* Determines if the wanted column exists within the $this->columns class variable.
*
* @param string $column The column to search for.
*
* @return bool Whether or not the column exists.
*/
protected function column_is_present( $column ) {
if ( ! is_string( $column ) ) {
return false;
}
return in_array( $column, $this->columns, true );
}
/**
* Gets the result keywords from WPSEO_Taxonomy_Meta.
*
* @param array $result The result to get the keywords for.
*
* @return array The keywords.
*/
protected function get_result_keywords( array $result ) {
$keyword = WPSEO_Taxonomy_Meta::get_term_meta( $result['ID'], $result['taxonomy'], 'focuskw' );
if ( $keyword === false || empty( $keyword ) ) {
return [];
}
return [ (string) $keyword ];
}
/**
* Gets the result keyword scores from WPSEO_Taxonomy_Meta.
*
* @param array $result The result to get the keyword scores for.
*
* @return array The keyword scores.
*/
protected function get_result_keywords_score( array $result ) {
$keyword_score = WPSEO_Taxonomy_Meta::get_term_meta( $result['ID'], $result['taxonomy'], 'linkdex' );
if ( $keyword_score === false || empty( $keyword_score ) ) {
return [];
}
$keyword_score_label = WPSEO_Rank::from_numeric_score( (int) $keyword_score )->get_label();
return [ $keyword_score_label ];
}
}

View File

@@ -0,0 +1,129 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Export
*/
/**
* Class WPSEO_Export_Keywords_Term_Query
*
* Creates an SQL query to gather all term data for a keywords export.
*/
class WPSEO_Export_Keywords_Term_Query implements WPSEO_Export_Keywords_Query {
/**
* The WordPress database object.
*
* @var wpdb
*/
protected $wpdb;
/**
* The columns to query for, an array of strings.
*
* @var array
*/
protected $columns;
/**
* The database columns to select in the query, an array of strings.
*
* @var array
*/
protected $selects;
/**
* Number of items to fetch per page.
*
* @var int
*/
protected $page_size;
/**
* WPSEO_Export_Keywords_Query constructor.
*
* Supported values for columns are 'title', 'url', 'keywords', 'readability_score' and 'keywords_score'.
* Requesting 'keywords_score' will always also return 'keywords'.
*
* @param wpdb $wpdb A WordPress Database object.
* @param array $columns List of columns that need to be retrieved.
* @param int $page_size Number of items to retrieve.
*/
public function __construct( $wpdb, array $columns, $page_size = 1000 ) {
$this->wpdb = $wpdb;
$this->page_size = max( 1, (int) $page_size );
$this->set_columns( $columns );
}
/**
* Returns the page size for the query.
*
* @return int Page size that is being used.
*/
public function get_page_size() {
return $this->page_size;
}
/**
* Constructs the query and executes it, returning an array of objects containing the columns this object was constructed with.
* Every object will always contain the ID column.
*
* @param int $page Paginated page to retrieve.
*
* @return array An array of associative arrays containing the keys as requested in the constructor.
*/
public function get_data( $page = 1 ) {
if ( $this->columns === [] ) {
return [];
}
$taxonomies = get_taxonomies(
[
'public' => true,
'show_ui' => true,
],
'names'
);
if ( empty( $taxonomies ) ) {
return [];
}
// Pages have a starting index of 1, we need to convert to a 0 based offset.
$offset_multiplier = max( 0, ( $page - 1 ) );
$replacements = $taxonomies;
$replacements[] = $this->page_size;
$replacements[] = ( $offset_multiplier * $this->page_size );
// Construct the query.
$query = $this->wpdb->prepare(
'SELECT ' . implode( ', ', $this->selects )
. ' FROM ' . $this->wpdb->prefix . 'terms AS terms'
. ' INNER JOIN ' . $this->wpdb->prefix . 'term_taxonomy AS taxonomies'
. ' ON terms.term_id = taxonomies.term_id AND taxonomies.taxonomy IN ('
. implode( ',', array_fill( 0, count( $taxonomies ), '%s' ) ) . ')'
. ' LIMIT %d OFFSET %d',
$replacements
);
return $this->wpdb->get_results( $query, ARRAY_A );
}
/**
* Prepares the necessary selects and joins to get all data in a single query.
*
* @param array $columns The columns we want our query to return.
*/
public function set_columns( array $columns ) {
$this->columns = $columns;
$this->selects = [ 'terms.term_id', 'taxonomies.taxonomy' ];
if ( in_array( 'title', $this->columns, true ) ) {
$this->selects[] = 'terms.name';
}
}
}

View File

@@ -0,0 +1,162 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* This class represents the fetching of a full name for a user who has filled in a Facebook profile url. The class
* will try to fetch the full name via the Facebook following plugin (widget). If the user has chosen to disallow
* following of his profile, there isn't returned any name - only an empty string.
*
* To prevent doing request all the time, the obtained name will be stored as user meta for the user.
*/
class WPSEO_Facebook_Profile {
const TRANSIENT_NAME = 'yoast_facebook_profiles';
/**
* URL providing us the full name belonging to the user.
*
* @var string
*/
private $facebook_endpoint = 'https://www.facebook.com/plugins/follow.php?href=';
/**
* Sets the AJAX action hook, to catch the AJAX request for getting the name on Facebook.
*/
public function set_hooks() {
add_action( 'wp_ajax_wpseo_get_facebook_name', [ $this, 'ajax_get_facebook_name' ] );
}
/**
* Sets the user id and prints the full Facebook name.
*/
public function ajax_get_facebook_name() {
if ( wp_doing_ajax() ) {
check_ajax_referer( 'get_facebook_name' );
$user_id = (int) filter_input( INPUT_GET, 'user_id' );
$facebook_profile = $this->get_facebook_profile( $user_id );
// Only try to get the name when the user has a profile set.
if ( $facebook_profile !== '' ) {
wp_die( esc_html( $this->get_name( $facebook_profile ) ) );
}
wp_die();
}
}
/**
* Get the Facebook profile url from the user profile.
*
* @param int $user_id The user to get the Facebook profile field for.
*
* @return string URL or empty string if the field is not set or empty.
*/
private function get_facebook_profile( $user_id ) {
$facebook_profile = get_the_author_meta( 'facebook', $user_id );
if ( ! empty( $facebook_profile ) ) {
return $facebook_profile;
}
return '';
}
/**
* Get the name used on Facebook from the transient cache, if the name isn't
* fetched already get it from the Facebook follow widget.
*
* @param string $facebook_profile The profile to get.
*
* @return string
*/
private function get_name( $facebook_profile ) {
$cached_facebook_name = $this->get_cached_name( $facebook_profile );
if ( $cached_facebook_name !== false ) {
return $cached_facebook_name;
}
$facebook_name = $this->get_name_from_facebook( $facebook_profile );
$this->set_cached_name( $facebook_profile, $facebook_name );
return $facebook_name;
}
/**
* Returns the stored name from the user meta.
*
* @param string $facebook_profile The Facebook profile to look for.
*
* @return string|bool
*/
private function get_cached_name( $facebook_profile ) {
$facebook_profiles = get_transient( self::TRANSIENT_NAME );
if ( is_array( $facebook_profiles ) && array_key_exists( $facebook_profile, $facebook_profiles ) ) {
return $facebook_profiles[ $facebook_profile ];
}
return false;
}
/**
* Stores the fetched Facebook name to the user meta.
*
* @param string $facebook_profile The Facebook profile belonging to the name.
* @param string $facebook_name The name the user got on Facebook.
*/
private function set_cached_name( $facebook_profile, $facebook_name ) {
$facebook_profiles = get_transient( self::TRANSIENT_NAME );
$facebook_profiles[ $facebook_profile ] = $facebook_name;
set_transient( self::TRANSIENT_NAME, $facebook_profiles, DAY_IN_SECONDS );
}
/**
* Do request to Facebook to get the HTML for the follow widget.
*
* @param string $facebook_profile The profile URL to lookup.
*
* @return string
*/
private function get_name_from_facebook( $facebook_profile ) {
$response = wp_remote_get(
$this->facebook_endpoint . $facebook_profile,
[
'headers' => [ 'Accept-Language' => 'en_US' ],
]
);
if ( wp_remote_retrieve_response_code( $response ) === 200 ) {
return $this->extract_name_from_response(
wp_remote_retrieve_body( $response )
);
}
return '';
}
/**
* Try to extract the full name from the response.
*
* @param string $response_body The response HTML to lookup for the full name.
*
* @return string
*/
private function extract_name_from_response( $response_body ) {
$full_name_regex = '/<div class="pluginButton pluginButtonInline pluginConnectButtonDisconnected" title="Follow(.*)&#039;s public updates">/i';
if ( preg_match( $full_name_regex, $response_body, $matches ) ) {
if ( ! empty( $matches[1] ) ) {
return $matches[1];
}
}
return '';
}
}

View File

@@ -0,0 +1,127 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Represents the class for adding the link suggestions metabox for each post type.
*/
class WPSEO_Metabox_Link_Suggestions implements WPSEO_WordPress_Integration {
/**
* Sets the hooks for adding the metaboxes.
*
* @return void
*/
public function register_hooks() {
add_action( 'add_meta_boxes', [ $this, 'add_meta_boxes' ] );
}
/**
* Adds a meta for each public post type.
*
* @return void
*/
public function add_meta_boxes() {
/*
* Since the link suggestions are already added in the Yoast sidebar.
* Do not add them to the metabox when in the block editor.
*/
if ( WP_Screen::get()->is_block_editor() ) {
return;
}
$post_types = $this->get_post_types();
array_map( [ $this, 'add_meta_box' ], $post_types );
}
/**
* Returns whether the link suggestions are available for the given post type.
*
* @param string $post_type The post type for which to check if the link suggestions are available.
*
* @return bool Whether the link suggestions are available for the given post type.
*/
public function is_available( $post_type ) {
$allowed_post_types = $this->get_post_types();
return in_array( $post_type, $allowed_post_types, true );
}
/**
* Renders the content for the metabox. We leave this empty because we render with React.
*
* @return void
*/
public function render_metabox_content() {
echo '';
}
/**
* Returns all the public post types.
*
* @return array The supported post types.
*/
protected function get_post_types() {
$prominent_words_support = new WPSEO_Premium_Prominent_Words_Support();
return $prominent_words_support->get_supported_post_types();
}
/**
* Returns whether or not the Link Suggestions are enabled.
*
* @return bool Whether or not the link suggestions are enabled.
*/
public function is_enabled() {
return WPSEO_Options::get( 'enable_link_suggestions', false );
}
/**
* Adds a meta box for the given post type.
*
* @param string $post_type The post type to add a meta box for.
*/
protected function add_meta_box( $post_type ) {
if ( ! $this->is_available( $post_type ) || ! $this->is_enabled() ) {
return;
}
if ( ! WPSEO_Premium_Metabox::are_content_endpoints_available() ) {
return;
}
add_meta_box(
'yoast_internal_linking',
sprintf(
/* translators: %s expands to Yoast */
__( '%s internal linking', 'wordpress-seo-premium' ),
'Yoast'
),
[ $this, 'render_metabox_content' ],
$post_type,
'side',
'low',
[
'__block_editor_compatible_meta_box' => true,
]
);
}
/**
* Returns whether or not we need to index more posts for correct link suggestion functionality
*
* @deprecated 14.7
* @codeCoverageIgnore
*
* @return bool Whether or not we need to index more posts.
*/
public function is_site_unindexed() {
_deprecated_function( __METHOD__, 'WPSEO Premium 14.7' );
return false;
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Implements multi keyword int he admin.
*/
class WPSEO_Multi_Keyword implements WPSEO_WordPress_Integration {
/**
* Sets WordPress hooks.
*
* @codeCoverageIgnore It relies on dependencies.
*/
public function register_hooks() {
add_filter( 'wpseo_metabox_entries_general', [ $this, 'add_focus_keywords_input' ] );
add_filter( 'wpseo_metabox_entries_general', [ $this, 'add_keyword_synonyms_input' ] );
add_filter( 'wpseo_taxonomy_content_fields', [ $this, 'add_focus_keywords_taxonomy_input' ] );
add_filter( 'wpseo_taxonomy_content_fields', [ $this, 'add_keyword_synonyms_taxonomy_input' ] );
add_filter( 'wpseo_add_extra_taxmeta_term_defaults', [ $this, 'register_taxonomy_metafields' ] );
}
/**
* Add field in which we can save multiple keywords.
*
* @param array $field_defs The current fields definitions.
*
* @return array Field definitions with our added field.
*/
public function add_focus_keywords_input( $field_defs ) {
if ( is_array( $field_defs ) ) {
$field_defs['focuskeywords'] = [
'type' => 'hidden',
'title' => 'focuskeywords',
];
}
return $field_defs;
}
/**
* Add field in which we can save multiple keyword synonyms.
*
* @param array $field_defs The current fields definitions.
*
* @return array Field definitions with our added field.
*/
public function add_keyword_synonyms_input( $field_defs ) {
if ( is_array( $field_defs ) ) {
$field_defs['keywordsynonyms'] = [
'type' => 'hidden',
'title' => 'keywordsynonyms',
];
}
return $field_defs;
}
/**
* Adds a field to the taxonomy metabox in which we can save multiple keywords.
*
* @param array $fields The current fields.
*
* @return array Fields including our added field.
*/
public function add_focus_keywords_taxonomy_input( $fields ) {
if ( is_array( $fields ) ) {
$fields['focuskeywords'] = [
'label' => '',
'description' => '',
'type' => 'hidden',
'options' => '',
];
}
return $fields;
}
/**
* Adds a field in which we can save multiple keyword synonyms.
*
* @param array $fields The current fields.
*
* @return array Fields including our added field.
*/
public function add_keyword_synonyms_taxonomy_input( $fields ) {
if ( is_array( $fields ) ) {
$fields['keywordsynonyms'] = [
'label' => '',
'description' => '',
'type' => 'hidden',
'options' => '',
];
}
return $fields;
}
/**
* Extends the taxonomy defaults.
*
* @param array $defaults The defaults to extend.
*
* @return array The extended defaults.
*/
public function register_taxonomy_metafields( $defaults ) {
$defaults['wpseo_focuskeywords'] = '';
$defaults['wpseo_keywordsynonyms'] = '';
return $defaults;
}
}

View File

@@ -0,0 +1,625 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Class WPSEO_Post_Watcher.
*/
class WPSEO_Post_Watcher extends WPSEO_Watcher implements WPSEO_WordPress_Integration {
/**
* Type of watcher, will be used for the filters.
*
* @var string
*/
protected $watch_type = 'post';
/**
* Registers the hooks.
*
* @codeCoverageIgnore Method used WordPress functions.
*
* @return void
*/
public function register_hooks() {
global $pagenow;
add_action( 'admin_enqueue_scripts', [ $this, 'page_scripts' ] );
// Only set the hooks for the page where they are needed.
if ( ! $this->is_rest_request() && ! $this->post_redirect_can_be_made( $pagenow ) ) {
return;
}
// Detect a post slug change.
add_action( 'post_updated', [ $this, 'detect_slug_change' ], 12, 3 );
// Detect a post trash.
add_action( 'wp_trash_post', [ $this, 'detect_post_trash' ] );
// Detect a post untrash.
add_action( 'untrashed_post', [ $this, 'detect_post_untrash' ] );
// Detect a post delete.
add_action( 'before_delete_post', [ $this, 'detect_post_delete' ] );
}
/**
* Registers the page scripts.
*
* @codeCoverageIgnore Method used WordPress functions.
*
* @param string $current_page The page that is opened at the moment.
*
* @return void
*/
public function page_scripts( $current_page ) {
// Register the scripts.
parent::page_scripts( $current_page );
/**
* If in Gutenberg, always load these scripts.
*/
if ( WPSEO_Metabox::is_post_edit( $current_page ) && wp_script_is( 'wp-editor', 'enqueued' ) ) {
wp_enqueue_script( 'wp-seo-premium-redirect-notifications' );
wp_enqueue_script( 'wp-seo-premium-redirect-notifications-gutenberg' );
return;
}
if ( ! $this->post_redirect_can_be_made( $current_page ) ) {
return;
}
if ( WPSEO_Metabox::is_post_overview( $current_page ) ) {
wp_enqueue_script( 'wp-seo-premium-quickedit-notification' );
}
if ( WPSEO_Metabox::is_post_edit( $current_page ) ) {
wp_enqueue_script( 'wp-seo-premium-redirect-notifications' );
}
}
/**
* Detect if the slug changed, hooked into 'post_updated'.
*
* @param int $post_id The ID of the post.
* @param WP_Post $post The post with the new values.
* @param WP_Post $post_before The post with the previous values.
*
* @return bool
*/
public function detect_slug_change( $post_id, $post, $post_before ) {
// Bail if this is a multisite installation and the site has been switched.
if ( is_multisite() && ms_is_switched() ) {
return false;
}
if ( ! $this->is_redirect_relevant( $post, $post_before ) ) {
return false;
}
$this->remove_colliding_redirect( $post, $post_before );
/**
* Filter: 'wpseo_premium_post_redirect_slug_change' - Check if a redirect should be created
* on post slug change.
*
* @deprecated 12.9.0. Use the {@see 'Yoast\WP\SEO\post_redirect_slug_change'} filter instead.
*
* @api bool Determines if a redirect should be created for this post slug change.
* @api int The ID of the post.
* @api WP_Post The current post object.
* @api WP_Post The previous post object.
*/
$create_redirect = apply_filters_deprecated(
'wpseo_premium_post_redirect_slug_change',
[ false, $post_id, $post, $post_before ],
'YoastSEO Premium 12.9.0',
'Yoast\WP\SEO\post_redirect_slug_change'
);
/**
* Filter: 'Yoast\WP\SEO\post_redirect_slug_change' - Check if a redirect should be created
* on post slug change.
*
* Note: This is a Premium plugin-only hook.
*
* @since 12.9.0
*
* @api bool Determines if a redirect should be created for this post slug change.
* @api int The ID of the post.
* @api WP_Post The current post object.
* @api WP_Post The previous post object.
*/
$create_redirect = apply_filters( 'Yoast\WP\SEO\post_redirect_slug_change', $create_redirect, $post_id, $post, $post_before );
if ( $create_redirect === true ) {
return true;
}
$old_url = $this->get_target_url( $post_before );
if ( ! $old_url ) {
return false;
}
// If the post URL wasn't public before, or isn't public now, don't even check if we have to redirect.
if ( ! $this->check_public_post_status( $post_before->ID ) || ! $this->check_public_post_status( $post->ID ) ) {
return false;
}
// Get the new URL.
$new_url = $this->get_target_url( $post_id );
// Maybe we can undo the created redirect.
$created_redirect = $this->notify_undo_slug_redirect( $old_url, $new_url, $post_id, 'post' );
if ( $created_redirect ) {
$redirect_info = [
'origin' => $created_redirect->get_origin(),
'target' => $created_redirect->get_target(),
'type' => $created_redirect->get_type(),
'format' => $created_redirect->get_format(),
];
update_post_meta( $post_id, '_yoast_post_redirect_info', $redirect_info );
}
}
/**
* Removes a colliding redirect if it is found.
*
* @param WP_Post $post The post with the new values.
* @param WP_Post $post_before The post with the previous values.
*
* @return void
*/
protected function remove_colliding_redirect( $post, $post_before ) {
$redirect = $this->get_redirect_manager()->get_redirect( $this->get_target_url( $post ) );
if ( $redirect === false ) {
return;
}
if ( $redirect->get_target() !== trim( $this->get_target_url( $post_before ), '/' ) ) {
return;
}
$this->get_redirect_manager()->delete_redirects( [ $redirect ] );
}
/**
* Determines if redirect is relevant for the provided post.
*
* @param WP_Post $post The post with the new values.
* @param WP_Post $post_before The post with the previous values.
*
* @return bool True if a redirect might be relevant.
*/
protected function is_redirect_relevant( $post, $post_before ) {
// Check if the post type is enabled for redirects.
$post_type = get_post_type( $post );
/**
* Filter: 'wpseo_premium_redirect_post_type' - Check if a redirect should be created
* on post slug change for specified post type.
*
* @deprecated 12.9.0. Use the {@see 'Yoast\WP\SEO\redirect_post_type'} filter instead.
*
* @api bool Determines if a redirect should be created for this post type.
* @api string The post type that is being checked for.
*/
$post_type_accessible = apply_filters_deprecated(
'wpseo_premium_redirect_post_type',
[ WPSEO_Post_Type::is_post_type_accessible( $post_type ), $post_type ],
'YoastSEO Premium 12.9.0',
'Yoast\WP\SEO\redirect_post_type'
);
/**
* Filter: 'Yoast\WP\SEO\redirect_post_type' - Check if a redirect should be created
* on post slug change for specified post type.
*
* Note: This is a Premium plugin-only hook.
*
* @since 12.9.0
*
* @api bool Determines if a redirect should be created for this post type.
* @api string The post type that is being checked for.
*/
$post_type_accessible = apply_filters( 'Yoast\WP\SEO\redirect_post_type', $post_type_accessible, $post_type );
if ( ! $post_type_accessible ) {
return false;
}
// If post is a revision do not create redirect.
if ( wp_is_post_revision( $post_before ) !== false && wp_is_post_revision( $post ) !== false ) {
return false;
}
// There is no slug change.
if ( $this->get_target_url( $post ) === $this->get_target_url( $post_before ) ) {
return false;
}
return true;
}
/**
* Checks whether the given post is public or not.
*
* @param int $post_id The current post ID.
*
* @return bool
*/
private function check_public_post_status( $post_id ) {
$public_post_statuses = [
'publish',
'static',
'private',
];
/**
* Filter: 'wpseo_public_post_statuses' - Allow changing the statuses that are expected
* to have caused a URL to be public.
*
* @deprecated 12.9.0. Use the {@see 'Yoast\WP\SEO\public_post_statuses'} filter instead.
*
* @api array $published_post_statuses The statuses that'll be treated as published.
* @param object $post The post object we're doing the published check for.
*/
$public_post_statuses = apply_filters_deprecated(
'wpseo_public_post_statuses',
[ $public_post_statuses, $post_id ],
'YoastSEO Premium 12.9.0',
'Yoast\WP\SEO\public_post_statuses'
);
/**
* Filter: 'Yoast\WP\SEO\public_post_statuses' - Allow changing the statuses that are expected
* to have caused a URL to be public.
*
* Note: This is a Premium plugin-only hook.
*
* @since 12.9.0
*
* @api array $published_post_statuses The statuses that'll be treated as published.
* @param object $post The post object we're doing the published check for.
*/
$public_post_statuses = apply_filters( 'Yoast\WP\SEO\public_post_statuses', $public_post_statuses, $post_id );
return ( in_array( get_post_status( $post_id ), $public_post_statuses, true ) );
}
/**
* Offer to create a redirect from the post that is about to get trashed.
*
* @param int $post_id The current post ID.
*/
public function detect_post_trash( $post_id ) {
$url = $this->check_if_redirect_needed( $post_id );
if ( ! empty( $url ) ) {
$id = 'wpseo_redirect_' . md5( $url );
// Format the message.
$message = sprintf(
/* translators: %1$s: Yoast SEO Premium, %2$s: List with actions, %3$s: <a href=''>, %4$s: </a>, %5$s: Slug to post */
__( '%1$s detected that you moved a post (%5$s) to the trash. You can either: %2$s Don\'t know what to do? %3$sRead this post%4$s.', 'wordpress-seo-premium' ),
'Yoast SEO Premium',
$this->get_delete_action_list( $url, $id ),
'<a target="_blank" href="' . WPSEO_Shortlinker::get( 'https://yoa.st/2jd' ) . '">',
'</a>',
'<code>' . $url . '</code>'
);
$this->create_notification( $message, 'trash' );
}
}
/**
* Offer to create a redirect from the post that is about to get restored from the trash.
*
* @param int $post_id The current post ID.
*/
public function detect_post_untrash( $post_id ) {
$redirect = $this->check_if_redirect_needed( $post_id, true );
if ( $redirect ) {
$id = 'wpseo_undo_redirect_' . md5( $redirect->get_origin() );
// Format the message.
$message = sprintf(
/* translators: %1$s: Yoast SEO Premium, %2$s: <a href='{undo_redirect_url}'>, %3$s: </a>, %4$s: Slug to post */
__( '%1$s detected that you restored a post (%4$s) from the trash, for which a redirect was created. %2$sClick here to remove the redirect%3$s', 'wordpress-seo-premium' ),
'Yoast SEO Premium',
'<button type="button" class="button" onclick=\'' . $this->javascript_undo_redirect( $redirect, $id ) . '\'>',
'</button>',
'<code>' . $redirect->get_origin() . '</code>'
);
$this->create_notification( $message, 'untrash' );
}
}
/**
* Offer to create a redirect from the post that is about to get deleted.
*
* @param int $post_id The current post ID.
*/
public function detect_post_delete( $post_id ) {
// We don't want to redirect menu items.
if ( is_nav_menu_item( $post_id ) ) {
return;
}
// When the post comes from the trash or if the post is a revision then skip further execution.
if ( get_post_status( $post_id ) === 'trash' || wp_is_post_revision( $post_id ) ) {
return;
}
// Is a redirect needed.
$url = $this->check_if_redirect_needed( $post_id );
if ( ! empty( $url ) ) {
$this->set_delete_notification( $url );
}
}
/**
* Look up if URL does exists in the current redirects.
*
* @param string $url URL to search for.
*
* @return bool
*/
protected function get_redirect( $url ) {
return $this->get_redirect_manager()->get_redirect( $url );
}
/**
* This method checks if a redirect is needed.
*
* This method will check if URL as redirect already exists.
*
* @param int $post_id The current post ID.
* @param bool $should_exist Boolean to determine if the URL should be exist as a redirect.
*
* @return WPSEO_Redirect|string|bool
*/
protected function check_if_redirect_needed( $post_id, $should_exist = false ) {
// If the post type is not public, don't redirect.
$post_type = get_post_type_object( get_post_type( $post_id ) );
if ( ! $post_type ) {
return false;
}
if ( ! in_array( $post_type->name, $this->get_included_automatic_redirection_post_types(), true ) ) {
return false;
}
// The post types should be a public one.
if ( $this->check_public_post_status( $post_id ) ) {
// Get the right URL.
$url = $this->get_target_url( $post_id );
// If $url is not a single /, there may be the option to create a redirect.
if ( $url !== '/' ) {
// Message should only be shown if there isn't already a redirect.
$redirect = $this->get_redirect( $url );
if ( is_a( $redirect, 'WPSEO_Redirect' ) === $should_exist ) {
if ( $should_exist === false ) {
return $url;
}
return $redirect;
}
}
}
return false;
}
/**
* Retrieves the post types to create automatic redirects for.
*
* @return array Post types to include to create automatic redirects for.
*/
protected function get_included_automatic_redirection_post_types() {
$post_types = WPSEO_Post_Type::get_accessible_post_types();
/**
* Filter: 'wpseo_premium_include_automatic_redirection_post_types' - Post types to create
* automatic redirects for.
*
* @deprecated 12.9.0. Use the {@see 'Yoast\WP\SEO\automatic_redirection_post_types'} filter instead.
*
* @api array $included_post_types Array with the post type names to include to automatic redirection.
*/
$included_post_types = apply_filters_deprecated(
'wpseo_premium_include_automatic_redirection_post_types',
[ $post_types ],
'YoastSEO Premium 12.9.0',
'Yoast\WP\SEO\automatic_redirection_post_types'
);
/**
* Filter: 'Yoast\WP\SEO\automatic_redirection_post_types' - Post types to create
* automatic redirects for.
*
* Note: This is a Premium plugin-only hook.
*
* @since 12.9.0
*
* @api array $included_post_types Array with the post type names to include to automatic redirection.
*/
$included_post_types = apply_filters( 'Yoast\WP\SEO\automatic_redirection_post_types', $included_post_types );
if ( ! is_array( $included_post_types ) ) {
$included_post_types = [];
}
return $included_post_types;
}
/**
* Retrieves the path of the URL for the supplied post.
*
* @param int|WP_Post $post The current post ID.
*
* @return string The URL for the supplied post.
*/
protected function get_target_url( $post ) {
// Use the correct URL path.
$url = wp_parse_url( get_permalink( $post ) );
if ( is_array( $url ) && isset( $url['path'] ) ) {
return $url['path'];
}
return '';
}
/**
* Get the old URL.
*
* @param object $post The post object with the new values.
* @param object $post_before The post object with the old values.
*
* @return bool|string
*/
protected function get_old_url( $post, $post_before ) {
$wpseo_old_post_url = $this->get_post_old_post_url();
if ( ! empty( $wpseo_old_post_url ) ) {
return $wpseo_old_post_url;
}
// Check if request is inline action and new slug is not old slug, if so set wpseo_post_old_url.
$action = $this->get_post_action();
$url_before = $this->get_target_url( $post_before );
if ( ! empty( $action ) && $action === 'inline-save' && $this->get_target_url( $post ) !== $url_before ) {
return $url_before;
}
return false;
}
/**
* Determines whether we're dealing with a REST request or not.
*
* @return bool Whether or not the current request is a REST request.
*/
private function is_rest_request() {
return defined( 'REST_REQUEST' ) && REST_REQUEST === true;
}
/**
* Returns the undo message for the post.
*
* @return string
*/
protected function get_undo_slug_notification() {
/* translators: %1$s: Yoast SEO Premium, %2$s and %3$s expand to a link to the admin page. */
return __(
'%1$s created a %2$sredirect%3$s from the old post URL to the new post URL.',
'wordpress-seo-premium'
);
}
/**
* Returns the delete message for the post.
*
* @return string
*/
protected function get_delete_notification() {
/* translators: %1$s: Yoast SEO Premium, %2$s: List with actions, %3$s: <a href='{post_with_explaination.}'>, %4$s: </a>, %5%s: The removed url. */
return __(
'%1$s detected that you deleted a post (%5$s). You can either: %2$s Don\'t know what to do? %3$sRead this post %4$s.',
'wordpress-seo-premium'
);
}
/**
* Is the current page valid to make a redirect from.
*
* @param string $current_page The currently opened page.
*
* @return bool True when a redirect can be made on this page.
*/
protected function post_redirect_can_be_made( $current_page ) {
return $this->is_post_page( $current_page ) || $this->is_action_inline_save() || $this->is_nested_pages( $current_page );
}
/**
* Is the current page related to a post (edit/overview).
*
* @param string $current_page The current opened page.
*
* @return bool True when page is a post edit/overview page.
*/
protected function is_post_page( $current_page ) {
return ( in_array( $current_page, [ 'edit.php', 'post.php' ], true ) );
}
/**
* Is the page in an AJAX-request and is the action "inline save".
*
* @return bool True when in an AJAX-request and the action is inline-save.
*/
protected function is_action_inline_save() {
return ( wp_doing_ajax() && $this->get_post_action() === 'inline-save' );
}
/**
* Checks if current page is loaded by nested pages.
*
* @param string $current_page The current page.
*
* @return bool True when the current page is nested pages.
*/
protected function is_nested_pages( $current_page ) {
return ( $current_page === 'admin.php' && filter_input( INPUT_GET, 'page' ) === 'nestedpages' );
}
/**
* Retrieves wpseo_old_post_url field from the post.
*
* @return mixed.
*/
protected function get_post_old_post_url() {
return filter_input( INPUT_POST, 'wpseo_old_post_url' );
}
/**
* Retrieves action field from the post.
*
* @return mixed.
*/
protected function get_post_action() {
return filter_input( INPUT_POST, 'action' );
}
/**
* Display the undo redirect notification
*
* @param WPSEO_Redirect $redirect The old URL to the post.
* @param int $object_id The post or term ID.
* @param string $object_type The object type: post or term.
*/
protected function set_undo_slug_notification( WPSEO_Redirect $redirect, $object_id, $object_type ) {
if ( ! $this->is_rest_request() && ! \wp_doing_ajax() ) {
parent::set_undo_slug_notification( $redirect, $object_id, $object_type );
return;
}
header( 'X-Yoast-Redirect-Created: 1; origin=' . $redirect->get_origin() . '; target=' . $redirect->get_target() . '; type=' . $redirect->get_type() . '; objectId=' . $object_id . '; objectType=' . $object_type );
}
}

View File

@@ -0,0 +1,47 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Localizes JavaScript files.
*/
final class WPSEO_Premium_Asset_JS_L10n {
/**
* Localizes the given script with the JavaScript translations.
*
* @param string $script_handle The script handle to localize for.
*
* @return void
*/
public function localize_script( $script_handle ) {
$translations = [
'wordpress-seo-premium' => $this->get_translations( 'wordpress-seo-premiumjs' ),
];
wp_localize_script( $script_handle, 'wpseoPremiumJSL10n', $translations );
}
/**
* Returns translations necessary for JS files.
*
* @param string $component The component to retrieve the translations for.
* @return object|null The translations in a Jed format for JS files or null
* if the translation file could not be found.
*/
protected function get_translations( $component ) {
$locale = \get_user_locale();
$file = plugin_dir_path( WPSEO_PREMIUM_FILE ) . '/languages/' . $component . '-' . $locale . '.json';
if ( file_exists( $file ) ) {
$file = file_get_contents( $file );
if ( is_string( $file ) && $file !== '' ) {
return json_decode( $file, true );
}
}
return null;
}
}

View File

@@ -0,0 +1,330 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Loads the Premium assets.
*/
class WPSEO_Premium_Assets implements WPSEO_WordPress_Integration {
/**
* Registers the hooks.
*
* @codeCoverageIgnore Method relies on a WordPress function.
*
* @return void
*/
public function register_hooks() {
add_action( 'admin_init', [ $this, 'register_assets' ] );
}
/**
* Registers the assets for premium.
*
* @return void
*/
public function register_assets() {
$version = $this->get_version();
$scripts = $this->get_scripts( $version );
$styles = $this->get_styles( $version );
array_walk( $scripts, [ $this, 'register_script' ] );
array_walk( $styles, [ $this, 'register_style' ] );
}
/**
* Retrieves a flatten version.
*
* @codeCoverageIgnore Method uses a dependency.
*
* @return string The flatten version.
*/
protected function get_version() {
$asset_manager = new WPSEO_Admin_Asset_Manager();
return $asset_manager->flatten_version( WPSEO_PREMIUM_VERSION );
}
/**
* Retrieves an array of script to register.
*
* @codeCoverageIgnore Returns a simple dataset.
*
* @param string $version Current version number.
*
* @return array The scripts.
*/
protected function get_scripts( $version ) {
return [
[
'name' => 'yoast-seo-premium-commons',
'path' => 'assets/js/dist/',
'filename' => 'commons-premium-' . $version . WPSEO_CSSJS_SUFFIX . '.js',
'dependencies' => [],
],
[
'name' => 'yoast-seo-premium-metabox',
'path' => 'assets/js/dist/',
'filename' => 'wp-seo-premium-metabox-' . $version . WPSEO_CSSJS_SUFFIX . '.js',
'dependencies' => [
'clipboard',
'jquery',
'underscore',
'wp-api-fetch',
'wp-components',
'wp-data',
'wp-element',
'wp-i18n',
'wp-util',
'yoast-seo-premium-commons',
WPSEO_Admin_Asset_Manager::PREFIX . 'analysis',
WPSEO_Admin_Asset_Manager::PREFIX . 'editor-modules',
WPSEO_Admin_Asset_Manager::PREFIX . 'help-scout-beacon',
WPSEO_Admin_Asset_Manager::PREFIX . 'legacy-components',
WPSEO_Admin_Asset_Manager::PREFIX . 'search-metadata-previews',
WPSEO_Admin_Asset_Manager::PREFIX . 'social-metadata-forms',
WPSEO_Admin_Asset_Manager::PREFIX . 'social-metadata-previews-package',
WPSEO_Admin_Asset_Manager::PREFIX . 'yoast-components',
],
],
[
'name' => 'yoast-seo-social-metadata-previews-package',
'path' => 'assets/js/dist/yoast/',
'filename' => 'social-metadata-previews-' . $version . WPSEO_CSSJS_SUFFIX . '.js',
'in_footer' => true,
'dependencies' => [
'lodash',
'wp-a11y',
'wp-components',
'wp-element',
'wp-i18n',
WPSEO_Admin_Asset_Manager::PREFIX . 'analysis',
WPSEO_Admin_Asset_Manager::PREFIX . 'draft-js',
WPSEO_Admin_Asset_Manager::PREFIX . 'editor-modules',
WPSEO_Admin_Asset_Manager::PREFIX . 'helpers',
WPSEO_Admin_Asset_Manager::PREFIX . 'replacement-variable-editor',
WPSEO_Admin_Asset_Manager::PREFIX . 'social-metadata-forms',
WPSEO_Admin_Asset_Manager::PREFIX . 'style-guide',
WPSEO_Admin_Asset_Manager::PREFIX . 'styled-components',
WPSEO_Admin_Asset_Manager::PREFIX . 'yoast-components',
],
],
[
'name' => 'yoast-social-metadata-previews',
'path' => 'assets/js/dist/',
'filename' => 'yoast-premium-social-metadata-previews-' . $version . WPSEO_CSSJS_SUFFIX . '.js',
'in_footer' => true,
'dependencies' => [
'wp-components',
'wp-element',
'wp-plugins',
WPSEO_Admin_Asset_Manager::PREFIX . 'editor-modules',
WPSEO_Admin_Asset_Manager::PREFIX . 'search-metadata-previews',
WPSEO_Admin_Asset_Manager::PREFIX . 'social-metadata-previews-package',
],
],
[
'name' => 'wp-seo-premium-custom-fields-plugin',
'path' => 'assets/js/dist/',
'filename' => 'wp-seo-premium-custom-fields-plugin-' . $version . WPSEO_CSSJS_SUFFIX . '.js',
'dependencies' => [
'jquery',
'yoast-seo-premium-commons',
],
],
[
'name' => 'wp-seo-premium-quickedit-notification',
'path' => 'assets/js/dist/',
'filename' => 'wp-seo-premium-quickedit-notification-' . $version . WPSEO_CSSJS_SUFFIX . '.js',
'dependencies' => [
'jquery',
'wp-api',
'wp-api-fetch',
'yoast-seo-premium-commons',
],
],
[
'name' => 'wp-seo-premium-redirect-notifications',
'path' => 'assets/js/dist/',
'filename' => 'wp-seo-premium-redirect-notifications-' . $version . WPSEO_CSSJS_SUFFIX . '.js',
'dependencies' => [
'jquery',
'wp-api',
'wp-api-fetch',
'yoast-seo-premium-commons',
],
],
[
'name' => 'wp-seo-premium-redirect-notifications-gutenberg',
'path' => 'assets/js/dist/',
'filename' => 'wp-seo-premium-redirect-notifications-gutenberg-' . $version . WPSEO_CSSJS_SUFFIX . '.js',
'dependencies' => [
'wp-api-fetch',
'wp-components',
'wp-element',
'wp-i18n',
'wp-plugins',
WPSEO_Admin_Asset_Manager::PREFIX . 'yoast-components',
],
],
[
'name' => 'wp-seo-premium-dynamic-blocks',
'path' => 'assets/js/dist/',
'filename' => 'dynamic-blocks-' . $version . WPSEO_CSSJS_SUFFIX . '.js',
'dependencies' => [
'lodash',
'wp-blocks',
'wp-data',
'wp-dom-ready',
'wp-hooks',
'wp-server-side-render',
],
],
[
'name' => 'wp-seo-premium-blocks',
'path' => 'assets/js/dist/',
'filename' => 'blocks-' . $version . WPSEO_CSSJS_SUFFIX . '.js',
'dependencies' => [
'wp-block-editor',
'wp-blocks',
'wp-components',
'wp-data',
'wp-dom-ready',
'wp-element',
'wp-i18n',
'yoast-seo-premium-metabox',
WPSEO_Admin_Asset_Manager::PREFIX . 'editor-modules',
WPSEO_Admin_Asset_Manager::PREFIX . 'legacy-components',
WPSEO_Admin_Asset_Manager::PREFIX . 'yoast-components',
],
],
[
'name' => 'yoast-premium-prominent-words-indexation',
'path' => 'assets/js/dist/',
'filename' => 'yoast-premium-prominent-words-indexation-' . $version . WPSEO_CSSJS_SUFFIX . '.js',
'dependencies' => [
'yoast-seo-premium-commons',
WPSEO_Admin_Asset_Manager::PREFIX . 'analysis',
WPSEO_Admin_Asset_Manager::PREFIX . 'editor-modules',
WPSEO_Admin_Asset_Manager::PREFIX . 'indexation',
],
],
[
'name' => 'elementor-premium',
'path' => 'assets/js/dist/',
'filename' => 'wp-seo-premium-elementor-' . $version . WPSEO_CSSJS_SUFFIX . '.js',
'dependencies' => [
'clipboard',
'jquery',
'underscore',
'wp-api-fetch',
'wp-components',
'wp-data',
'wp-element',
'wp-hooks',
'wp-i18n',
'wp-util',
'yoast-seo-premium-commons',
WPSEO_Admin_Asset_Manager::PREFIX . 'analysis',
WPSEO_Admin_Asset_Manager::PREFIX . 'editor-modules',
WPSEO_Admin_Asset_Manager::PREFIX . 'help-scout-beacon',
WPSEO_Admin_Asset_Manager::PREFIX . 'legacy-components',
WPSEO_Admin_Asset_Manager::PREFIX . 'search-metadata-previews',
WPSEO_Admin_Asset_Manager::PREFIX . 'social-metadata-forms',
WPSEO_Admin_Asset_Manager::PREFIX . 'social-metadata-previews-package',
WPSEO_Admin_Asset_Manager::PREFIX . 'yoast-components',
],
'footer' => true,
],
[
'name' => 'wp-seo-premium-schema-blocks',
'path' => 'assets/js/dist/',
'filename' => 'wp-seo-premium-schema-blocks-' . $version . WPSEO_CSSJS_SUFFIX . '.js',
'dependencies' => [
WPSEO_Admin_Asset_Manager::PREFIX . 'schema-blocks-package',
],
],
];
}
/**
* Retrieves an array of styles to register.
*
* @codeCoverageIgnore Returns a simple dataset.
*
* @param string $version Current version number.
*
* @return array The styles.
*/
protected function get_styles( $version ) {
return [
[
'name' => WPSEO_Admin_Asset_Manager::PREFIX . 'premium-metabox',
'source' => 'assets/css/dist/premium-metabox-' . $version . '.css',
'dependencies' => [],
],
[
'name' => 'elementor-premium',
'source' => 'assets/css/dist/premium-elementor-' . $version . '.css',
'dependencies' => [
WPSEO_Admin_Asset_Manager::PREFIX . 'premium-metabox',
],
],
[
'name' => WPSEO_Admin_Asset_Manager::PREFIX . 'premium-schema-blocks',
'source' => 'assets/css/dist/premium-schema-blocks-' . $version . '.css',
'dependencies' => [
WPSEO_Admin_Asset_Manager::PREFIX . 'schema-blocks',
],
],
];
}
/**
* Registers the given script to WordPress.
*
* @codeCoverageIgnore Method calls a WordPress function.
*
* @param array $script The script to register.
*
* @return void
*/
protected function register_script( $script ) {
$url = plugin_dir_url( WPSEO_PREMIUM_FILE ) . $script['path'] . $script['filename'];
if ( defined( 'YOAST_SEO_PREMIUM_DEV_SERVER' ) && YOAST_SEO_PREMIUM_DEV_SERVER ) {
$url = 'http://localhost:8081/' . $script['filename'];
}
$in_footer = isset( $script['in_footer'] ) ? $script['in_footer'] : false;
wp_register_script(
$script['name'],
$url,
$script['dependencies'],
WPSEO_PREMIUM_VERSION,
$in_footer
);
}
/**
* Registers the given style to WordPress.
*
* @codeCoverageIgnore Method calls a WordPress function.
*
* @param array $style The style to register.
*
* @return void
*/
protected function register_style( $style ) {
wp_register_style(
$style['name'],
plugin_dir_url( WPSEO_PREMIUM_FILE ) . $style['source'],
$style['dependencies'],
WPSEO_PREMIUM_VERSION
);
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium
*/
/**
* Exposes shortlinks to wpseoAdminL10n.
*/
class WPSEO_Premium_Expose_Shortlinks implements WPSEO_WordPress_Integration {
/**
* Registers all hooks to WordPress
*/
public function register_hooks() {
add_filter( 'wpseo_admin_l10n', [ $this, 'expose_shortlinks' ] );
}
/**
* Filter that adds the keyword synonyms shortlink to the localization object.
*
* @param array $input Admin localization object.
*
* @return array Admin localization object.
*/
public function expose_shortlinks( $input ) {
$input['shortlinks.keyword_synonyms_info'] = WPSEO_Shortlinker::get( 'https://yoa.st/kd1' );
return $input;
}
}

View File

@@ -0,0 +1,65 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium
*/
/**
* Load WordPress SEO translations from WordPress.org for the Free part of the plugin, to make sure the translations
* are present.
*/
class WPSEO_Premium_Free_Translations implements WPSEO_WordPress_Integration {
/**
* Registers all hooks to WordPress.
*/
public function register_hooks() {
add_filter( 'http_request_args', [ $this, 'request_wordpress_seo_translations' ], 10, 2 );
}
/**
* Adds Yoast SEO (Free) to the update checklist of installed plugins, to check for new translations.
*
* @param array $args HTTP Request arguments to modify.
* @param string $url The HTTP request URI that is executed.
*
* @return array The modified Request arguments to use in the update request.
*/
public function request_wordpress_seo_translations( $args, $url ) {
// Only do something on upgrade requests.
if ( strpos( $url, 'api.wordpress.org/plugins/update-check' ) === false ) {
return $args;
}
/*
* If Yoast SEO is already in the list, don't add it again.
*
* Checking this by name because the install path is not guaranteed.
* The capitalized json data defines the array keys, therefore we need to check and define these as such.
*/
$plugins = json_decode( $args['body']['plugins'], true );
foreach ( $plugins['plugins'] as $data ) {
if ( isset( $data['Name'] ) && $data['Name'] === 'Yoast SEO' ) {
return $args;
}
}
/*
* Add an entry to the list that matches the WordPress.org slug for Yoast SEO Free.
*
* This entry is based on the currently present data from this plugin, to make sure the version and textdomain
* settings are as expected. Take care of the capitalized array key as before.
*/
$plugins['plugins']['wordpress-seo/wp-seo.php'] = $plugins['plugins'][ WPSEO_PREMIUM_BASENAME ];
// Override the name of the plugin.
$plugins['plugins']['wordpress-seo/wp-seo.php']['Name'] = 'Yoast SEO';
// Override the version of the plugin to prevent increasing the update count.
$plugins['plugins']['wordpress-seo/wp-seo.php']['Version'] = '9999.0';
// Overwrite the plugins argument in the body to be sent in the upgrade request.
$args['body']['plugins'] = WPSEO_Utils::format_json_encode( $plugins );
return $args;
}
}

View File

@@ -0,0 +1,47 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Represents a premium Google Search Console modal.
*
* @deprecated 12.5
*
* @codeCoverageIgnore
*/
class WPSEO_Premium_GSC_Modal {
const EXISTING_REDIRECT_HEIGHT = 160;
const CREATE_REDIRECT_HEIGHT = 380;
/**
* Constructor, sets the redirect manager instance.
*
* @deprecated 12.5
*
* @codeCoverageIgnore
*/
public function __construct() {
_deprecated_function( __METHOD__, 'WPSEO 12.5' );
}
/**
* Returns a GSC modal for the given URL.
*
* @deprecated 12.5
*
* @codeCoverageIgnore
*
* @param string $url The URL to get the modal for.
*
* @return null
*/
public function show( $url ) {
_deprecated_function( __METHOD__, 'WPSEO 12.5' );
return null;
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Registers the premium WordPress implementation of Google Search Console.
*
* @deprecated 12.5
*
* @codeCoverageIgnore
*/
class WPSEO_Premium_GSC implements WPSEO_WordPress_Integration {
/**
* Registers all hooks to WordPress
*
* @deprecated 12.5
*
* @codeCoverageIgnore
*/
public function register_hooks() {
_deprecated_function( __METHOD__, 'WPSEO 12.5' );
}
/**
* Enqueues site wide analysis script
*
* @deprecated 12.5
*
* @codeCoverageIgnore
*/
public function enqueue() {
_deprecated_function( __METHOD__, 'WPSEO 12.5' );
}
}

View File

@@ -0,0 +1,367 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Class WPSEO_Premium_Import_Manager
*/
class WPSEO_Premium_Import_Manager implements WPSEO_WordPress_Integration {
/**
* Holds the import object.
*
* @var stdClass
*/
protected $import;
/**
* Registers the hooks.
*
* @codeCoverageIgnore
*
* @return void
*/
public function register_hooks() {
// Handle premium imports.
add_filter( 'wpseo_handle_import', [ $this, 'do_premium_imports' ] );
// Add htaccess import block.
add_action( 'wpseo_import_tab_content', [ $this, 'add_redirect_import_block' ] );
add_action( 'wpseo_import_tab_header', [ $this, 'redirects_import_header' ] );
}
/**
* Imports redirects from specified file or location.
*
* @param stdClass|bool $import The import object.
*
* @return stdClass The import status object.
*/
public function do_premium_imports( $import ) {
if ( ! $import ) {
$import = (object) [
'msg' => '',
'success' => false,
'status' => null,
];
}
$this->import = $import;
$this->htaccess_import();
$this->do_plugin_imports();
$this->do_csv_imports();
return $this->import;
}
/**
* Outputs a tab header for the htaccess import block.
*
* @return void
*/
public function redirects_import_header() {
/* translators: %s: '.htaccess' file name */
echo '<a class="nav-tab" id="import-htaccess-tab" href="#top#import-htaccess">' . esc_html__( 'Import redirects', 'wordpress-seo-premium' ) . '</a>';
}
/**
* Adding the import block for redirects.
*
* @return void
*/
public function add_redirect_import_block() {
$import = $this->import;
// Display the forms.
require WPSEO_PREMIUM_PATH . 'classes/views/import-redirects.php';
}
/**
* Do .htaccess file import.
*
* @return void
*/
protected function htaccess_import() {
$htaccess = $this->get_posted_htaccess();
if ( ! $htaccess || $htaccess === '' ) {
return;
}
try {
$loader = new WPSEO_Redirect_HTAccess_Loader( $htaccess );
$result = $this->import_redirects_from_loader( $loader );
$this->set_import_success( $result );
}
catch ( WPSEO_Redirect_Import_Exception $e ) {
$this->set_import_message( $e->getMessage() );
}
}
/**
* Handles plugin imports.
*
* @return void
*/
protected function do_plugin_imports() {
$import_plugin = $this->get_posted_import_plugin();
if ( ! $import_plugin ) {
return;
}
try {
$loader = $this->get_plugin_loader( $import_plugin );
$result = $this->import_redirects_from_loader( $loader );
$this->set_import_success( $result );
}
catch ( WPSEO_Redirect_Import_Exception $e ) {
$this->set_import_message( $e->getMessage() );
}
}
/**
* Processes a CSV import.
*
* @return void
*/
protected function do_csv_imports() {
$redirects_csv_file = $this->get_posted_csv_file();
if ( ! $redirects_csv_file ) {
return;
}
try {
$this->validate_uploaded_csv_file( $redirects_csv_file );
// Load the redirects from the uploaded file.
$loader = new WPSEO_Redirect_CSV_Loader( $redirects_csv_file['tmp_name'] );
$result = $this->import_redirects_from_loader( $loader );
$this->set_import_success( $result );
}
catch ( WPSEO_Redirect_Import_Exception $e ) {
$this->set_import_message( $e->getMessage() );
}
}
/**
* Sets the import message.
*
* @param string $import_message The message.
*
* @return void
*/
protected function set_import_message( $import_message ) {
$this->import->msg .= $import_message;
}
/**
* Sets the import success state to true.
*
* @param array $result The import result.
*
* @return void.
*/
protected function set_import_success( array $result ) {
$this->import->success = true;
$this->set_import_message(
$this->get_success_message( $result['total_imported'], $result['total_redirects'] )
);
}
/**
* Retrieves the success message when import has been successful.
*
* @param int $total_imported The number of imported redirects.
* @param int $total_redirects The total amount of redirects.
*
* @return string The generated message.
*/
protected function get_success_message( $total_imported, $total_redirects ) {
if ( $total_imported === $total_redirects ) {
return sprintf(
/* translators: 1: link to redirects overview, 2: closing link tag */
__( 'All redirects have been imported successfully. Go to the %1$sredirects overview%2$s to see the imported redirects.', 'wordpress-seo-premium' ),
'<a href="' . esc_url( admin_url( 'admin.php?page=wpseo_redirects' ) ) . '">',
'</a>'
);
}
if ( $total_imported === 0 ) {
return sprintf(
/* translators: 1: link to redirects overview, 2: closing link tag */
__( 'No redirects have been imported. Probably they already exist as a redirect. Go to the %1$sredirects overview%2$s to see the existing redirects.', 'wordpress-seo-premium' ),
'<a href="' . esc_url( admin_url( 'admin.php?page=wpseo_redirects' ) ) . '">',
'</a>'
);
}
return sprintf(
/* translators: 1: amount of imported redirects, 2: total amount of redirects, 3: link to redirects overview, 4: closing link tag */
_n(
'Imported %1$s/%2$s redirects successfully. Go to the %3$sredirects overview%4$s to see the imported redirect.',
'Imported %1$s/%2$s redirects successfully. Go to the %3$sredirects overview%4$s to see the imported redirects.',
$total_imported,
'wordpress-seo-premium'
),
$total_imported,
$total_redirects,
'<a href="' . esc_url( admin_url( 'admin.php?page=wpseo_redirects' ) ) . '">',
'</a>'
);
}
/**
* Returns a loader for the given plugin.
*
* @codeCoverageIgnore
*
* @param string $plugin_name The plugin we want to load redirects from.
*
* @return bool|WPSEO_Redirect_Abstract_Loader The redirect loader.
*
* @throws WPSEO_Redirect_Import_Exception When the plugin is not installed or activated.
*/
protected function get_plugin_loader( $plugin_name ) {
global $wpdb;
switch ( $plugin_name ) {
case 'redirection':
// Only do import if Redirections is active.
if ( ! defined( 'REDIRECTION_VERSION' ) ) {
throw new WPSEO_Redirect_Import_Exception(
__( 'Redirect import failed: the Redirection plugin is not installed or activated.', 'wordpress-seo-premium' )
);
}
return new WPSEO_Redirect_Redirection_Loader( $wpdb );
case 'safe_redirect_manager':
return new WPSEO_Redirect_Safe_Redirect_Loader();
case 'simple-301-redirects':
return new WPSEO_Redirect_Simple_301_Redirect_Loader();
default:
throw new WPSEO_Redirect_Import_Exception(
__( 'Redirect import failed: the selected redirect plugin is not installed or activated.', 'wordpress-seo-premium' )
);
}
}
/**
* Validates an uploaded CSV file.
*
* @param array $csv_file The file to upload, from the $_FILES object.
*
* @return void
*
* @throws WPSEO_Redirect_Import_Exception When the given file is invalid.
*/
protected function validate_uploaded_csv_file( $csv_file ) {
// If no file is selected.
if ( array_key_exists( 'name', $csv_file ) && $csv_file['name'] === '' ) {
$error_message = __( 'CSV import failed: No file selected.', 'wordpress-seo-premium' );
throw new WPSEO_Redirect_Import_Exception( $error_message );
}
// If the file upload failed for any other reason.
if ( array_key_exists( 'error', $csv_file ) && $csv_file['error'] !== UPLOAD_ERR_OK ) {
$error_message = __( 'CSV import failed: the provided file could not be parsed using a CSV parser.', 'wordpress-seo-premium' );
throw new WPSEO_Redirect_Import_Exception( $error_message );
}
// If somehow the file is larger than it should be.
if ( $csv_file['size'] > wp_max_upload_size() ) {
$max_size_formatted = size_format( wp_max_upload_size() );
/* translators: 1: The maximum file size */
$error_message = sprintf( __( 'CSV import failed: the provided file is larger than %1$s.', 'wordpress-seo-premium' ), $max_size_formatted );
throw new WPSEO_Redirect_Import_Exception( $error_message );
}
// If it's not a CSV file (send the csv mimetype along for multisite installations).
$filetype = wp_check_filetype( $csv_file['name'], [ 'csv' => 'text/csv' ] );
if ( strtolower( $filetype['ext'] ) !== 'csv' ) {
$error_message = __( 'CSV import failed: the provided file is not a CSV file.', 'wordpress-seo-premium' );
throw new WPSEO_Redirect_Import_Exception( $error_message );
}
}
/**
* Imports all redirects from the loader.
*
* @codeCoverageIgnore
*
* @param WPSEO_Redirect_Loader $loader The loader to import redirects from.
*
* @return array The result of the import.
*
* @throws WPSEO_Redirect_Import_Exception When there is no loader given or when there are no redirects.
*/
protected function import_redirects_from_loader( WPSEO_Redirect_Loader $loader ) {
if ( ! $loader ) {
throw new WPSEO_Redirect_Import_Exception(
__( 'Redirect import failed: we can\'t recognize this type of import.', 'wordpress-seo-premium' )
);
}
$redirects = $loader->load();
if ( count( $redirects ) === 0 ) {
throw new WPSEO_Redirect_Import_Exception(
__( 'Redirect import failed: no redirects found.', 'wordpress-seo-premium' )
);
}
$importer = new WPSEO_Redirect_Importer();
return $importer->import( $redirects );
}
/**
* Retrieves the posted htaccess.
*
* @codeCoverageIgnore
*
* @return string The posted htaccess.
*/
protected function get_posted_htaccess() {
return stripcslashes( filter_input( INPUT_POST, 'htaccess' ) );
}
/**
* Retrieves the posted import plugin.
*
* @codeCoverageIgnore
*
* @return string|null The posted import plugin.
*/
protected function get_posted_import_plugin() {
$wpseo_post = filter_input( INPUT_POST, 'wpseo', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
if ( ! isset( $wpseo_post['import_plugin'] ) ) {
return null;
}
return $wpseo_post['import_plugin'];
}
/**
* Retrieves the posted CSV file.
*
* @codeCoverageIgnore
*
* @return array|null The posted CSV file.
*/
protected function get_posted_csv_file() {
if ( ! isset( $_FILES['redirects_csv_file'] ) ) {
return null;
}
return $_FILES['redirects_csv_file'];
}
}

View File

@@ -0,0 +1,78 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Class WPSEO_Premium_Javascript_Strings.
*/
class WPSEO_Premium_Javascript_Strings {
/**
* List containing the localized JavaScript translations.
*
* @var string[]|null
*/
private static $strings = null;
/**
* Fill the value of self::$strings with translated strings.
*/
private static function fill() {
self::$strings = [
'error_circular' => __( 'You can\'t redirect a URL to itself.', 'wordpress-seo-premium' ),
'error_old_url' => __( 'The old URL field can\'t be empty.', 'wordpress-seo-premium' ),
'error_regex' => __( 'The Regular Expression field can\'t be empty.', 'wordpress-seo-premium' ),
'error_new_url' => __( 'The new URL field can\'t be empty.', 'wordpress-seo-premium' ),
'error_saving_redirect' => __( 'Error while saving this redirect', 'wordpress-seo-premium' ),
'error_new_type' => __( 'New type can\'t be empty.', 'wordpress-seo-premium' ),
'unsaved_redirects' => __( 'You have unsaved redirects, are you sure you want to leave?', 'wordpress-seo-premium' ),
/* translators: %s is replaced with the URL that will be deleted. */
'enter_new_url' => __( 'Please enter the new URL for %s', 'wordpress-seo-premium' ),
/* translators: variables will be replaced with from and to URLs. */
'redirect_saved' => __( 'Redirect created from %1$s to %2$s!', 'wordpress-seo-premium' ),
/* translators: %1$s will be replaced with the from URL. */
'redirect_saved_no_target' => __( '410 Redirect created from %1$s!', 'wordpress-seo-premium' ),
'redirect_added' => [
'title' => __( 'Redirect added.', 'wordpress-seo-premium' ),
'message' => __( 'The redirect was added successfully.', 'wordpress-seo-premium' ),
],
'redirect_updated' => [
'title' => __( 'Redirect updated.', 'wordpress-seo-premium' ),
'message' => __( 'The redirect was updated successfully.', 'wordpress-seo-premium' ),
],
'redirect_deleted' => [
'title' => __( 'Redirect deleted.', 'wordpress-seo-premium' ),
'message' => __( 'The redirect was deleted successfully.', 'wordpress-seo-premium' ),
],
'button_ok' => __( 'OK', 'wordpress-seo-premium' ),
'button_cancel' => __( 'Cancel', 'wordpress-seo-premium' ),
'button_save' => __( 'Save', 'wordpress-seo-premium' ),
'button_save_anyway' => __( 'Save anyway', 'wordpress-seo-premium' ),
'edit_redirect' => __( 'Edit redirect', 'wordpress-seo-premium' ),
'editing_redirect' => __( 'You are already editing a redirect, please finish this one first', 'wordpress-seo-premium' ),
'editAction' => __( 'Edit', 'wordpress-seo-premium' ),
'deleteAction' => __( 'Delete', 'wordpress-seo-premium' ),
];
}
/**
* Returns an array with all the translated strings.
*
* @return string[]
*/
public static function strings() {
if ( self::$strings === null ) {
self::fill();
}
return self::$strings;
}
}

View File

@@ -0,0 +1,204 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Class WPSEO_Export_Keywords_Manager.
*
* Manages exporting keywords.
*/
class WPSEO_Premium_Keyword_Export_Manager implements WPSEO_WordPress_Integration {
/**
* A WordPress database object.
*
* @var wpdb instance
*/
protected $wpdb;
/**
* Registers all hooks to WordPress.
*/
public function register_hooks() {
// Hook into the request in case of CSV download and return our generated CSV instead.
add_action( 'admin_init', [ $this, 'keywords_csv_export' ] );
// Add htaccess import block.
add_action( 'wpseo_import_tab_content', [ $this, 'add_keyword_export_tab_block' ] );
add_action( 'wpseo_import_tab_header', [ $this, 'keywords_export_tab_header' ] );
}
/**
* Outputs a tab header for the CSV export block.
*/
public function keywords_export_tab_header() {
if ( current_user_can( 'export' ) ) {
echo '<a class="nav-tab" id="keywords-export-tab" href="#top#keywords-export">'
. esc_html__( 'Export keyphrases', 'wordpress-seo-premium' )
. '</a>';
}
}
/**
* Adds the export block for CSV. Makes it able to export redirects to CSV.
*/
public function add_keyword_export_tab_block() {
// Display the forms.
if ( current_user_can( 'export' ) ) {
$yform = Yoast_Form::get_instance();
require WPSEO_PREMIUM_PATH . 'classes/views/export-keywords.php';
}
}
/**
* Hooks into the request and returns a CSV file if we're on the right page with the right method and the right capabilities.
*/
public function keywords_csv_export() {
global $wpdb;
if ( ! $this->is_valid_csv_export_request() || ! current_user_can( 'export' ) ) {
return;
}
// Check if we have a valid nonce.
check_admin_referer( 'wpseo-export' );
$this->wpdb = $wpdb;
// Clean any content that has been already outputted, for example by other plugins or faulty PHP files.
if ( ob_get_contents() ) {
ob_clean();
}
// Make sure we don't time out during the collection of items.
set_time_limit( 0 );
// Set CSV headers and content.
$this->set_csv_headers();
echo $this->get_csv_contents();
// And exit so we don't start appending HTML to our CSV file.
// NOTE: this makes this entire class untestable as it will exit all tests but WordPress seems to have no elegant way of handling this.
exit;
}
/**
* Returns whether this is a POST request for a CSV export of posts and keywords.
*
* @return bool True if this is a valid CSV export request.
*/
protected function is_valid_csv_export_request() {
return filter_input( INPUT_GET, 'page' ) === 'wpseo_tools'
&& filter_input( INPUT_GET, 'tool' ) === 'import-export'
&& filter_input( INPUT_POST, 'export-posts' );
}
/**
* Sets the headers to trigger a CSV download in the browser.
*/
protected function set_csv_headers() {
header( 'Content-type: text/csv' );
header( 'Content-Disposition: attachment; filename=' . gmdate( 'Y-m-d' ) . '-yoast-seo-keywords.csv' );
header( 'Pragma: no-cache' );
header( 'Expires: 0' );
}
/**
* Generates the CSV to be exported.
*
* @return void
*/
protected function get_csv_contents() {
$columns = [ 'keywords' ];
$post_wpseo = filter_input( INPUT_POST, 'wpseo', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
if ( is_array( $post_wpseo ) ) {
$columns = array_merge( $columns, $this->get_export_columns( $post_wpseo ) );
}
$builder = new WPSEO_Export_Keywords_CSV( $columns );
$builder->print_headers();
$this->prepare_export( $builder, $columns );
}
/**
* Returns an array of the requested columns.
*
* @param array $post_object An associative array with the post data.
*
* @return array List of export columns.
*/
protected function get_export_columns( array $post_object ) {
$exportable_columns = [
'export-keywords-score' => 'keywords_score',
'export-url' => 'url',
'export-title' => 'title',
'export-seo-title' => 'seo_title',
'export-meta-description' => 'meta_description',
'export-readability-score' => 'readability_score',
];
// Need to call array_values to ensure that we get a numerical key back.
return array_values( array_intersect_key( $exportable_columns, $post_object ) );
}
/**
* Feeds post and term items to the CSV builder.
*
* @param WPSEO_Export_Keywords_CSV $builder The builder to use.
* @param array $columns The columns that need to be exported.
*
* @return void
*/
protected function prepare_export( WPSEO_Export_Keywords_CSV $builder, array $columns ) {
$this->feed_to_builder(
$builder,
new WPSEO_Export_Keywords_Post_Query( $this->wpdb, $columns, 1000 ),
new WPSEO_Export_Keywords_Post_Presenter( $columns )
);
$this->feed_to_builder(
$builder,
new WPSEO_Export_Keywords_Term_Query( $this->wpdb, $columns, 1000 ),
new WPSEO_Export_Keywords_Term_Presenter( $columns )
);
}
/**
* Fetches the items and feeds them to the builder.
*
* @param WPSEO_Export_Keywords_CSV $builder Builder to feed the items to.
* @param WPSEO_Export_Keywords_Query $export_query Query to use to get the items.
* @param WPSEO_Export_Keywords_Presenter $presenter Presenter to present the items in the builder format.
*
* @return void
*/
protected function feed_to_builder( WPSEO_Export_Keywords_CSV $builder, WPSEO_Export_Keywords_Query $export_query, WPSEO_Export_Keywords_Presenter $presenter ) {
$page_size = $export_query->get_page_size();
$page = 1;
do {
$results = $export_query->get_data( $page );
if ( ! is_array( $results ) ) {
break;
}
$result_count = count( $results );
// Present the result.
$presented = array_map( [ $presenter, 'present' ], $results );
// Feed presented item to the builder.
array_walk( $presented, [ $builder, 'print_row' ] );
++$page;
// If we have the number of items per page, there will be more items ahead.
} while ( $result_count === $page_size );
}
}

View File

@@ -0,0 +1,391 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium|Classes
*/
use Yoast\WP\SEO\Helpers\Prominent_Words_Helper;
use Yoast\WP\SEO\Integrations\Admin\Prominent_Words\Indexing_Integration;
/**
* The metabox for premium.
*/
class WPSEO_Premium_Metabox implements WPSEO_WordPress_Integration {
/**
* Instance of the WPSEO_Metabox_Link_Suggestions class.
*
* @var WPSEO_Metabox_Link_Suggestions
*/
protected $link_suggestions;
/**
* The prominent words helper.
*
* @var Prominent_Words_Helper
*/
protected $prominent_words_helper;
/**
* Creates the meta box class.
*
* @param Prominent_Words_Helper $prominent_words_helper The prominent words helper.
* @param WPSEO_Metabox_Link_Suggestions|null $link_suggestions The link suggestions meta box.
*/
public function __construct(
Prominent_Words_Helper $prominent_words_helper,
WPSEO_Metabox_Link_Suggestions $link_suggestions = null
) {
if ( $link_suggestions === null ) {
$link_suggestions = new WPSEO_Metabox_Link_Suggestions();
}
$this->prominent_words_helper = $prominent_words_helper;
$this->link_suggestions = $link_suggestions;
}
/**
* Registers relevant hooks to WordPress.
*
* @codeCoverageIgnore Method uses dependencies.
*
* @return void
*/
public function register_hooks() {
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
add_action( 'admin_init', [ $this, 'initialize' ] );
$this->link_suggestions->register_hooks();
}
/**
* Checks if the content endpoints are available.
*
* @return bool Returns true if the content endpoints are available
*/
public static function are_content_endpoints_available() {
if ( function_exists( 'rest_get_server' ) ) {
$namespaces = rest_get_server()->get_namespaces();
return in_array( 'wp/v2', $namespaces, true );
}
return false;
}
/**
* Initializes the metabox by loading the register_hooks for the dependencies.
*
* @return void
*/
public function initialize() {
if ( ! $this->load_metabox( $this->get_current_page() ) ) {
return;
}
foreach ( $this->get_metabox_integrations() as $integration ) {
$integration->register_hooks();
}
}
/**
* Enqueues assets when relevant.
*
* @codeCoverageIgnore Method uses dependencies.
*
* @return void
*/
public function enqueue_assets() {
if ( ! $this->load_metabox( $this->get_current_page() ) ) {
return;
}
wp_enqueue_script( WPSEO_Admin_Asset_Manager::PREFIX . 'premium-metabox' );
wp_enqueue_style( WPSEO_Admin_Asset_Manager::PREFIX . 'premium-metabox' );
$localization = new WPSEO_Admin_Asset_Yoast_Components_L10n();
$localization->localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'premium-metabox' );
$premium_localization = new WPSEO_Premium_Asset_JS_L10n();
$premium_localization->localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'premium-metabox' );
$this->send_data_to_assets();
}
/**
* Send data to assets by using wp_localize_script.
*
* @return void
*/
public function send_data_to_assets() {
$analysis_seo = new WPSEO_Metabox_Analysis_SEO();
$data = [
'restApi' => $this->get_rest_api_config(),
'seoAnalysisEnabled' => $analysis_seo->is_enabled(),
'licensedURL' => WPSEO_Utils::get_home_url(),
'settingsPageUrl' => admin_url( 'admin.php?page=wpseo_dashboard#top#features' ),
'integrationsTabURL' => admin_url( 'admin.php?page=wpseo_dashboard#top#integrations' ),
];
if ( WPSEO_Metabox::is_post_edit( $this->get_current_page() ) ) {
$data = array_merge( $data, $this->get_post_metabox_config() );
}
elseif ( WPSEO_Taxonomy::is_term_edit( $this->get_current_page() ) ) {
$data = array_merge( $data, $this->get_term_metabox_config() );
}
// Use an extra level in the array to preserve booleans. WordPress sanitizes scalar values in the first level of the array.
wp_localize_script( 'yoast-seo-premium-metabox', 'wpseoPremiumMetaboxData', [ 'data' => $data ] );
}
/**
* Retrieves the metabox config for a post.
*
* @return array The config.
*/
protected function get_post_metabox_config() {
$insights_enabled = WPSEO_Options::get( 'enable_metabox_insights', false );
$link_suggestions_enabled = WPSEO_Options::get( 'enable_link_suggestions', false );
$post = $this->get_post();
$prominent_words_support = new WPSEO_Premium_Prominent_Words_Support();
if ( ! $prominent_words_support->is_post_type_supported( $post->post_type ) ) {
$insights_enabled = false;
}
$site_locale = \get_locale();
$language = WPSEO_Language_Utils::get_language( $site_locale );
return [
'insightsEnabled' => ( $insights_enabled ) ? 'enabled' : 'disabled',
'currentObjectId' => $this->get_post_ID(),
'currentObjectType' => 'post',
'linkSuggestionsEnabled' => ( $link_suggestions_enabled ) ? 'enabled' : 'disabled',
'linkSuggestionsAvailable' => $prominent_words_support->is_post_type_supported( $post->post_type ),
'linkSuggestionsUnindexed' => ! $this->is_prominent_words_indexing_completed() && WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ),
'perIndexableLimit' => $this->per_indexable_limit( $language ),
];
}
/**
* Retrieves the metabox config for a term.
*
* @return array The config.
*/
protected function get_term_metabox_config() {
$term = null;
if ( isset( $GLOBALS['tag_ID'], $GLOBALS['taxonomy'] ) ) {
$term = get_term( $GLOBALS['tag_ID'], $GLOBALS['taxonomy'] );
}
if ( $term === null || is_wp_error( $term ) ) {
return [
'insightsEnabled' => 'disabled',
'linkSuggestionsEnabled' => 'disabled',
'linkSuggestionsAvailable' => false,
'linkSuggestionsUnindexed' => false,
];
}
$link_suggestions_enabled = WPSEO_Options::get( 'enable_link_suggestions', false );
$insights_enabled = WPSEO_Options::get( 'enable_metabox_insights', false );
$prominent_words_support = new WPSEO_Premium_Prominent_Words_Support();
if ( ! $prominent_words_support->is_taxonomy_supported( $term->taxonomy ) ) {
$insights_enabled = false;
}
$site_locale = \get_locale();
$language = WPSEO_Language_Utils::get_language( $site_locale );
return [
'insightsEnabled' => ( $insights_enabled ) ? 'enabled' : 'disabled',
'currentObjectId' => $term->term_id,
'currentObjectType' => 'term',
'linkSuggestionsEnabled' => ( $link_suggestions_enabled ) ? 'enabled' : 'disabled',
'linkSuggestionsAvailable' => $prominent_words_support->is_taxonomy_supported( $term->taxonomy ),
'linkSuggestionsUnindexed' => ! $this->is_prominent_words_indexing_completed() && WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ),
'perIndexableLimit' => $this->per_indexable_limit( $language ),
];
}
/**
* Retrieves the REST API configuration.
*
* @return array The configuration.
*/
protected function get_rest_api_config() {
return [
'available' => WPSEO_Utils::is_api_available(),
'contentEndpointsAvailable' => self::are_content_endpoints_available(),
'root' => esc_url_raw( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
];
}
/**
* Returns the post for the current admin page.
*
* @codeCoverageIgnore
*
* @return WP_Post The post for the current admin page.
*/
protected function get_post() {
return get_post( $this->get_post_ID() );
}
/**
* Retrieves the post ID from the globals.
*
* @codeCoverageIgnore
*
* @return int The post ID.
*/
protected function get_post_ID() {
if ( ! isset( $GLOBALS['post_ID'] ) ) {
return 0;
}
return $GLOBALS['post_ID'];
}
/**
* Retrieves the metabox specific integrations.
*
* @codeCoverageIgnore
*
* @return WPSEO_WordPress_Integration[] The metabox integrations.
*/
protected function get_metabox_integrations() {
return [
'social-previews' => new WPSEO_Social_Previews(),
// Add custom fields plugin to post and page edit pages.
'premium-custom-fields' => new WPSEO_Custom_Fields_Plugin(),
];
}
/**
* Checks whether or not the metabox related scripts should be loaded.
*
* @codeCoverageIgnore
*
* @param string $current_page The page we are on.
*
* @return bool True when it should be loaded.
*/
protected function load_metabox( $current_page ) {
// When the current page is a term related one.
if ( WPSEO_Taxonomy::is_term_edit( $current_page ) || WPSEO_Taxonomy::is_term_overview( $current_page ) ) {
return WPSEO_Options::get( 'display-metabox-tax-' . $this->get_current_taxonomy() );
}
// When the current page isn't a post related one.
if ( WPSEO_Metabox::is_post_edit( $current_page ) || WPSEO_Metabox::is_post_overview( $current_page ) ) {
return WPSEO_Post_Type::has_metabox_enabled( $this->get_current_post_type() );
}
// Make sure ajax integrations are loaded.
return wp_doing_ajax();
}
/**
* Retrieves the current post type.
*
* @codeCoverageIgnore It depends on external request input.
*
* @return string The post type.
*/
protected function get_current_post_type() {
$post = filter_input( INPUT_GET, 'post', FILTER_SANITIZE_STRING );
if ( $post ) {
return get_post_type( get_post( $post ) );
}
return filter_input(
INPUT_GET,
'post_type',
FILTER_SANITIZE_STRING,
[
'options' => [
'default' => 'post',
],
]
);
}
/**
* Retrieves the current taxonomy.
*
* @codeCoverageIgnore This function depends on external request input.
*
* @return string The taxonomy.
*/
protected function get_current_taxonomy() {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- doing a strict in_array check should be sufficient.
if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || ! in_array( $_SERVER['REQUEST_METHOD'], [ 'GET', 'POST' ], true ) ) {
return '';
}
if ( $_SERVER['REQUEST_METHOD'] === 'POST' ) {
return (string) filter_input(
INPUT_POST,
'taxonomy',
FILTER_SANITIZE_STRING
);
}
return (string) filter_input(
INPUT_GET,
'taxonomy',
FILTER_SANITIZE_STRING
);
}
/**
* Retrieves the value of the pagenow variable.
*
* @codeCoverageIgnore
*
* @return string The value of pagenow.
*/
private function get_current_page() {
global $pagenow;
return $pagenow;
}
/**
* Returns whether or not we need to index more posts for correct link suggestion functionality.
*
* @return bool Whether or not we need to index more posts.
*/
protected function is_prominent_words_indexing_completed() {
$is_indexing_completed = $this->prominent_words_helper->is_indexing_completed();
if ( $is_indexing_completed === null ) {
$indexation_integration = YoastSEOPremium()->classes->get( Indexing_Integration::class );
$is_indexing_completed = $indexation_integration->get_unindexed_count( 0 ) === 0;
$this->prominent_words_helper->set_indexing_completed( $is_indexing_completed );
}
return $is_indexing_completed;
}
/**
* Returns the number of prominent words to store for content written in the given language.
*
* @param string $language The current language.
*
* @return int The number of words to store.
*/
protected function per_indexable_limit( $language ) {
if ( YoastSEO()->helpers->language->has_function_word_support( $language ) ) {
return Indexing_Integration::PER_INDEXABLE_LIMIT;
}
return Indexing_Integration::PER_INDEXABLE_LIMIT_NO_FUNCTION_WORD_SUPPORT;
}
}

View File

@@ -0,0 +1,76 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Represents the premium option.
*/
class WPSEO_Premium_Option extends WPSEO_Option {
/**
* Option name.
*
* @var string
*/
public $option_name = 'wpseo_premium';
/**
* Array of defaults for the option.
*
* {@internal Shouldn't be requested directly, use $this->get_defaults();}}
*
* @var array
*/
protected $defaults = [
// Form fields.
'prominent_words_indexing_completed' => null,
];
/**
* Registers the option to the WPSEO Options framework.
*/
public static function register_option() {
WPSEO_Options::register_option( static::get_instance() );
}
/**
* Get the singleton instance of this class.
*
* @return static Returns instance of itself.
*/
public static function get_instance() {
if ( ! ( static::$instance instanceof static ) ) {
static::$instance = new static();
}
return static::$instance;
}
/**
* 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.
*
* @return array The clean option value.
*/
protected function validate_option( $dirty, $clean, $old ) {
foreach ( $clean as $key => $value ) {
switch ( $key ) {
case 'prominent_words_indexing_completed':
if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== null ) {
$clean[ $key ] = WPSEO_Utils::validate_bool( $dirty[ $key ] );
}
break;
}
}
return $clean;
}
}

View File

@@ -0,0 +1,61 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium
*/
/**
* Represents the functionality for the orphaned content support.
*/
class WPSEO_Premium_Orphaned_Content_Support {
/**
* Returns an array with the supported post types.
*
* @return array The supported post types.
*/
public function get_supported_post_types() {
/**
* Filter: 'wpseo_orphaned_post_types' - Allows changes for the accessible post types.
*
* @deprecated 12.9.0. Use the {@see 'Yoast\WP\SEO\orphaned_post_types'} filter instead.
*
* @api array The accessible post types.
*/
$orphaned_post_types = apply_filters_deprecated(
'wpseo_orphaned_post_types',
[ WPSEO_Post_Type::get_accessible_post_types() ],
'YoastSEO Premium 12.9.0',
'Yoast\WP\SEO\orphaned_post_types'
);
/**
* Filter: 'Yoast\WP\SEO\orphaned_post_types' - Allows changes for the accessible post types.
*
* Note: This is a Premium plugin-only hook.
*
* @since 12.9.0
*
* @api array The accessible post types.
*/
$orphaned_post_types = apply_filters( 'Yoast\WP\SEO\orphaned_post_types', $orphaned_post_types );
if ( ! is_array( $orphaned_post_types ) || empty( $orphaned_post_types ) ) {
$orphaned_post_types = [];
}
return $orphaned_post_types;
}
/**
* Checks if the post type is supported.
*
* @param string $post_type The post type to look up.
*
* @return bool True when post type is supported.
*/
public function is_post_type_supported( $post_type ) {
return in_array( $post_type, $this->get_supported_post_types(), true );
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium
*/
use Yoast\WP\SEO\Actions\Indexing\Post_Link_Indexing_Action;
use Yoast\WP\SEO\Config\Migration_Status;
/**
* Represents some util helpers for the orphaned posts.
*/
class WPSEO_Premium_Orphaned_Content_Utils {
/**
* Checks if the orphaned content feature is enabled.
*
* @return bool True when the text link counter is enabled.
*/
public static function is_feature_enabled() {
if ( ! YoastSEO()->classes->get( Migration_Status::class )->is_version( 'free', WPSEO_VERSION ) ) {
return false;
}
return WPSEO_Options::get( 'enable_text_link_counter', false );
}
/**
* Checks if there are unprocessed objects.
*
* @return bool True when there are unprocessed objects.
*/
public static function has_unprocessed_content() {
static $has_unprocessed_posts;
if ( $has_unprocessed_posts === null ) {
$post_link_action = YoastSEO()->classes->get( Post_Link_Indexing_Action::class );
$has_unprocessed_posts = $post_link_action->get_total_unindexed();
}
return $has_unprocessed_posts;
}
}

View File

@@ -0,0 +1,190 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium
*/
use Yoast\WP\SEO\Config\Migration_Status;
/**
* Registers the filter for filtering posts by orphaned content.
*/
class WPSEO_Premium_Orphaned_Post_Filter extends WPSEO_Abstract_Post_Filter {
/**
* Returns the query value this filter uses.
*
* @return string The query value this filter uses.
*/
public function get_query_val() {
return 'orphaned';
}
/**
* Registers the hooks when the link feature is enabled.
*/
public function register_hooks() {
if ( ! YoastSEO()->classes->get( Migration_Status::class )->is_version( 'free', WPSEO_VERSION ) ) {
return;
}
if ( WPSEO_Premium_Orphaned_Content_Utils::is_feature_enabled() ) {
parent::register_hooks();
}
}
/**
* Returns a text explaining this filter.
*
* @return string|null The explanation or null if the current post stype is unknown.
*/
protected function get_explanation() {
$post_type_object = get_post_type_object( $this->get_current_post_type() );
if ( $post_type_object === null ) {
return null;
}
$unprocessed = WPSEO_Premium_Orphaned_Content_Utils::has_unprocessed_content();
$can_recalculate = WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' );
$learn_more = sprintf(
/* translators: %1$s expands to the link to an article to read more about orphaned content, %2$s expands to </a> */
__( '%1$sLearn more about orphaned content%2$s.', 'wordpress-seo-premium' ),
'<a href="' . WPSEO_Shortlinker::get( 'https://yoa.st/1ja' ) . '" target="_blank">',
'</a>'
);
if ( $unprocessed && ! $can_recalculate ) {
return sprintf(
/* translators: %1$s: plural form of the current post type, %2$s: a Learn more about link */
__( 'Ask your SEO Manager or Site Administrator to count links in all texts, so we can identify orphaned %1$s. %2$s', 'wordpress-seo-premium' ),
strtolower( $post_type_object->labels->name ),
$learn_more
);
}
if ( $unprocessed ) {
return sprintf(
/* translators: %1$s expands to link to the recalculation option, %2$s: anchor closing, %3$s: plural form of the current post type, %4$s: a Learn more about link */
__( '%1$sClick here%2$s to index your links, so we can identify orphaned %3$s. %4$s', 'wordpress-seo-premium' ),
'<a href="' . esc_url( admin_url( 'admin.php?page=wpseo_tools&reIndexLinks=1' ) ) . '">',
'</a>',
strtolower( $post_type_object->labels->name ),
$learn_more
);
}
return sprintf(
/* translators: %1$s: plural form of the current post type, %2$s: a Learn more about link */
__( '\'Orphaned content\' refers to %1$s that have no inbound links, consider adding links towards these %1$s. %2$s', 'wordpress-seo-premium' ),
strtolower( $post_type_object->labels->name ),
$learn_more
);
}
/**
* Modifies the query based on the seo_filter variable in $_GET.
*
* @param string $where Query variables.
*
* @return string The modified query.
*/
public function filter_posts( $where ) {
if ( $this->is_filter_active() ) {
$where .= $this->get_where_filter();
$where .= $this->filter_published_posts();
}
return $where;
}
/**
* Returns the where clause to use.
*
* @return string The where clause.
*/
protected function get_where_filter() {
global $wpdb;
if ( WPSEO_Premium_Orphaned_Content_Utils::has_unprocessed_content() ) {
// Hide all posts, because we cannot tell anything for certain.
return 'AND 1 = 0';
}
$subquery = WPSEO_Premium_Orphaned_Post_Query::get_orphaned_content_query();
return ' AND ' . $wpdb->posts . '.ID IN ( ' . $subquery . ' ) ';
}
/**
* Adds a published posts filter so we don't show unpublished posts in the orphaned pages results.
*
* @return string A published posts filter.
*/
protected function filter_published_posts() {
global $wpdb;
return " AND {$wpdb->posts}.post_status = 'publish' AND {$wpdb->posts}.post_password = ''";
}
/**
* Returns the label for this filter.
*
* @return string The label for this filter.
*/
protected function get_label() {
static $label;
if ( $label === null ) {
$label = __( 'Orphaned content', 'wordpress-seo-premium' );
}
return $label;
}
/**
* Returns the total amount of articles that are orphaned content.
*
* @return int
*/
protected function get_post_total() {
global $wpdb;
static $count;
if ( WPSEO_Premium_Orphaned_Content_Utils::has_unprocessed_content() ) {
return '?';
}
if ( $count === null ) {
$subquery = WPSEO_Premium_Orphaned_Post_Query::get_orphaned_content_query();
$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(ID)
FROM `{$wpdb->posts}`
WHERE ID IN ( $subquery )
AND post_status = 'publish'
AND post_password = ''
AND post_type = %s",
$this->get_current_post_type()
)
);
$count = (int) $count;
}
return $count;
}
/**
* Returns the post types to which this filter should be added.
*
* @return array The post types to which this filter should be added.
*/
protected function get_post_types() {
$orphaned_content_support = new WPSEO_Premium_Orphaned_Content_Support();
return $orphaned_content_support->get_supported_post_types();
}
}

View File

@@ -0,0 +1,201 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Represents the notifier when there is orphaned content present for one of the post types.
*/
class WPSEO_Premium_Orphaned_Post_Notifier implements WPSEO_WordPress_Integration {
/**
* Instance of the Yoast_Notification_Center class.
*
* @var Yoast_Notification_Center
*/
protected $notification_center;
/**
* WPSEO_Premium_Orphaned_Content_Notifier constructor.
*
* @param array $post_types Unused. The supported post types.
* @param Yoast_Notification_Center $notification_center The notification center object.
*/
public function __construct( array $post_types, Yoast_Notification_Center $notification_center ) {
$this->notification_center = $notification_center;
}
/**
* Registers the hooks.
*
* @return void
*/
public function register_hooks() {
// Joost de Valk, April 6th 2019.
// Disabling this until we've found a better way to display this data that doesn't become annoying when you have a lot of post types.
return;
if ( filter_input( INPUT_GET, 'page' ) === 'wpseo_dashboard' ) {
add_action( 'admin_init', [ $this, 'notify' ] );
}
if ( ! wp_next_scheduled( 'wpseo-premium-orphaned-content' ) ) {
wp_schedule_event( time(), 'daily', 'wpseo-premium-orphaned-content' );
}
add_action( 'wpseo-premium-orphaned-content', [ $this, 'notify' ] );
}
/**
* Handles the notifications for all post types.
*
* @return void
*/
public function notify() {
// Joost de Valk, April 6th 2019.
// Disabling this until we've found a better way to display this data that doesn't become annoying when you have a lot of post types.
return;
$post_types = $this->get_post_types();
$post_types = $this->format_post_types( $post_types );
// Walks over the posts types and handle the notification.
array_walk( $post_types, [ $this, 'notify_for_post_type' ] );
}
/**
* Returns the post types to which this filter should be added.
*
* @return array The post types to which this filter should be added.
*/
protected function get_post_types() {
$orphaned_content_support = new WPSEO_Premium_Orphaned_Content_Support();
return $orphaned_content_support->get_supported_post_types();
}
/**
* Formats the array with post types as an array with post type objects.
*
* It also filters out the null values, because these are unknown post types.
*
* @param array $post_types Array with post type names.
*
* @return WP_Post_Type[] The formatted posttypes.
*/
protected function format_post_types( array $post_types ) {
// First convert the array to post type objects.
$post_type_objects = array_map( 'get_post_type_object', $post_types );
// The unknown post types will have a value of null, filter these.
return array_filter( $post_type_objects );
}
/**
* Handles the notification for the given post type.
*
* @param WP_Post_Type $post_type The post type.
*
* @return void
*/
protected function notify_for_post_type( WP_Post_Type $post_type ) {
$notification_id = sprintf( 'wpseo-premium-orphaned-content-%1$s', $post_type->name );
$message = $this->get_notification( $notification_id, $post_type );
$show_notification = WPSEO_Premium_Orphaned_Content_Utils::is_feature_enabled() && ! WPSEO_Premium_Orphaned_Content_Utils::has_unprocessed_content();
if ( $show_notification && $this->requires_notification( $post_type ) ) {
Yoast_Notification_Center::get()->add_notification( $message );
return;
}
Yoast_Notification_Center::get()->remove_notification( $message );
}
/**
* Checks if the notification is required for the passed post type.
*
* @param WP_Post_Type $post_type The post type.
*
* @return bool True if a notification is required.
*/
protected function requires_notification( WP_Post_Type $post_type ) {
return $this->get_count_by_post_type( $post_type->name ) > 0;
}
/**
* Returns the notification for the passed post type.
*
* @param string $notification_id The id for the notification.
* @param WP_Post_Type $post_type The post type to generate the message for.
*
* @return Yoast_Notification The notification.
*/
protected function get_notification( $notification_id, $post_type ) {
$total_orphaned = $this->get_count_by_post_type( $post_type->name );
$post_type_value = ( $total_orphaned === 1 ) ? $post_type->labels->singular_name : $post_type->labels->name;
$message = sprintf(
/* translators: %1$s: Link to the filter page, %2$d: amount of orphaned items, %3$s: plural/singular form of post type, %4$s closing tag. */
_n(
'We\'ve detected %1$s%2$d \'orphaned\' %3$s%4$s (no inbound links). Consider adding links towards this %3$s.',
'We\'ve detected %1$s%2$d \'orphaned\' %3$s%4$s (no inbound links). Consider adding links towards these %3$s.',
$total_orphaned,
'wordpress-seo-premium'
),
'<a href="' . $this->get_filter_url( $post_type->name ) . '">',
$total_orphaned,
strtolower( $post_type_value ),
'</a>'
);
return new Yoast_Notification(
$message,
[
'type' => Yoast_Notification::WARNING,
'id' => $notification_id,
'capabilities' => 'wpseo_manage_options',
'priority' => 0.8,
]
);
}
/**
* Returns the total number of orphaned items for given post type name.
*
* @param string $post_type_name The name of the post type.
*
* @return int Total orphaned items.
*/
protected function get_count_by_post_type( $post_type_name ) {
static $post_type_counts;
if ( ! is_array( $post_type_counts ) ) {
$post_type_counts = WPSEO_Premium_Orphaned_Post_Query::get_counts( $this->get_post_types() );
}
if ( array_key_exists( $post_type_name, $post_type_counts ) ) {
return (int) $post_type_counts[ $post_type_name ];
}
return 0;
}
/**
* Returns the URL to the page with the filtered items for the given post type.
*
* @param string $post_type_name The name of the post type.
*
* @return string The URL containing the required filter.
*/
protected function get_filter_url( $post_type_name ) {
$query_args = [
'post_type' => $post_type_name,
'yoast_filter' => 'orphaned',
];
return add_query_arg( $query_args, admin_url( 'edit.php' ) );
}
}

View File

@@ -0,0 +1,139 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* Represents the orphaned post query methods.
*/
class WPSEO_Premium_Orphaned_Post_Query {
/**
* Returns the total number of orphaned items for the given post types.
*
* @param array $post_types The post types to get the counts for.
*
* @return int[] The counts for all post types.
*/
public static function get_counts( array $post_types ) {
global $wpdb;
$post_type_counts = array_fill_keys( $post_types, 0 );
$subquery = self::get_orphaned_content_query();
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT COUNT( ID ) as total_orphaned, post_type
FROM {$wpdb->posts}
WHERE
ID IN ( " . $subquery . " )
AND post_status = 'publish'
AND post_type IN ( " . implode( ',', array_fill( 0, count( $post_types ), '%s' ) ) . ' )
GROUP BY post_type',
$post_types
)
);
foreach ( $results as $result ) {
$post_type_counts[ $result->post_type ] = (int) $result->total_orphaned;
}
return $post_type_counts;
}
/**
* Returns a query to retrieve the object ids from the records with an incoming link count of 0.
*
* @return string Query for get all objects with an incoming link count of 0 from the DB.
*/
public static function get_orphaned_content_query() {
static $query;
if ( $query === null ) {
$repository = YoastSEO()->classes->get( Indexable_Repository::class );
$query = $repository->query()
->select( 'object_id' )
->where( 'object_type', 'post' )
->where_any_is(
[
[ 'incoming_link_count' => 0 ],
[ 'incoming_link_count' => null ],
]
);
$frontpage_id = self::get_frontpage_id();
if ( $frontpage_id ) {
$query = $query->where_not_equal( 'object_id', $frontpage_id );
}
$query = sprintf( $query->get_sql(), '\'post\'', 0, $frontpage_id );
}
return $query;
}
/**
* Returns all the object ids from the records with an incoming link count of 0.
*
* @return array Array with the object ids.
*/
public static function get_orphaned_object_ids() {
$repository = YoastSEO()->classes->get( Indexable_Repository::class );
$results = $repository->query()
->select( 'object_id' )
->where( 'object_type', 'post' )
->where( 'incoming_link_count', 0 )
->find_array();
$object_ids = wp_list_pluck( $results, 'object_id' );
$object_ids = self::remove_frontpage_id( $object_ids );
return $object_ids;
}
/**
* Removes the frontpage id from orphaned id's when the frontpage is a static page.
*
* @param array $object_ids The orphaned object ids.
*
* @return array The orphaned object ids, without frontpage id.
*/
protected static function remove_frontpage_id( $object_ids ) {
// When the frontpage is a static page, remove it from the object ids.
if ( get_option( 'show_on_front' ) !== 'page' ) {
return $object_ids;
}
$frontpage_id = get_option( 'page_on_front' );
// If the frontpage ID exists in the list, remove it.
$object_id_key = array_search( $frontpage_id, $object_ids, true );
if ( $object_id_key !== false ) {
unset( $object_ids[ $object_id_key ] );
}
return $object_ids;
}
/**
* Retrieves the frontpage id when set, otherwise null.
*
* @return int|null The frontpage id when set.
*/
protected static function get_frontpage_id() {
if ( get_option( 'show_on_front' ) !== 'page' ) {
return null;
}
$page_on_front = get_option( 'page_on_front', null );
if ( empty( $page_on_front ) ) {
return null;
}
return (int) $page_on_front;
}
}

View File

@@ -0,0 +1,89 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium
*/
// Mark this file as deprecated.
_deprecated_file( __FILE__, 'WPSEO Premium 14.5' );
/**
* Handles adding site wide analysis UI to the WordPress admin.
*/
class WPSEO_Premium_Prominent_Words_Recalculation implements WPSEO_WordPress_Integration {
/**
* Base height of the recalculation modal in pixels.
*
* @var int
*/
const MODAL_DIALOG_HEIGHT_BASE = 220;
/**
* Height of the recalculation progressbar in pixels.
*
* @var int
*/
const PROGRESS_BAR_HEIGHT = 32;
/**
* WPSEO_Premium_Prominent_Words_Recalculation constructor.
*
* @deprecated 14.5
* @codeCoverageIgnore
*
* @param WPSEO_Premium_Prominent_Words_Support $prominent_words_support Unused. The prominent words support class to determine supported posts types to index.
*/
public function __construct( WPSEO_Premium_Prominent_Words_Support $prominent_words_support ) {
_deprecated_function( __METHOD__, 'WPSEO Premium 14.5' );
}
/**
* Registers all hooks to WordPress.
*
* @deprecated 14.5
* @codeCoverageIgnore
*
* @return void
*/
public function register_hooks() {
_deprecated_function( __METHOD__, 'WPSEO Premium 14.5' );
}
/**
* Adds an item on the tools page list.
*
* @deprecated 14.5
* @codeCoverageIgnore
*
* @return void
*/
public function show_tools_overview_item() {
_deprecated_function( __METHOD__, 'WPSEO Premium 14.5' );
}
/**
* Initialize the modal box to be displayed when needed.
*
* @deprecated 14.5
* @codeCoverageIgnore
*
* @return void
*/
public function modal_box() {
_deprecated_function( __METHOD__, 'WPSEO Premium 14.5' );
}
/**
* Enqueues site wide analysis script.
*
* @deprecated 14.5
* @codeCoverageIgnore
*
* @return void
*/
public function enqueue() {
_deprecated_function( __METHOD__, 'WPSEO Premium 14.5' );
}
}

View File

@@ -0,0 +1,127 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium
*/
/**
* Represents the functionality for the prominent words support.
*/
class WPSEO_Premium_Prominent_Words_Support {
/**
* Returns an array with the supported post types.
*
* @return array The supported post types.
*/
public function get_supported_post_types() {
/**
* Filter: 'wpseo_prominent_words_post_types' - Allows changes for the accessible post types.
*
* @deprecated 12.9.0. Use the {@see 'Yoast\WP\SEO\prominent_words_post_types'} filter instead.
*
* @api array The accessible post types.
*/
$prominent_words_post_types = apply_filters_deprecated(
'wpseo_prominent_words_post_types',
[ WPSEO_Post_Type::get_accessible_post_types() ],
'YoastSEO Premium 12.9.0',
'Yoast\WP\SEO\prominent_words_post_types'
);
/**
* Filter: 'Yoast\WP\SEO\prominent_words_post_types' - Allows changes for the accessible post types.
*
* Note: This is a Premium plugin-only hook.
*
* @since 12.9.0
*
* @api array The accessible post types.
*/
$prominent_words_post_types = apply_filters(
'Yoast\WP\SEO\prominent_words_post_types',
$prominent_words_post_types
);
if ( ! is_array( $prominent_words_post_types ) || empty( $prominent_words_post_types ) ) {
$prominent_words_post_types = [];
}
$prominent_words_post_types = WPSEO_Post_Type::filter_attachment_post_type( $prominent_words_post_types );
$prominent_words_post_types = array_filter( $prominent_words_post_types, [ 'WPSEO_Post_Type', 'has_metabox_enabled' ] );
/*
* The above combination of functions results in array looking like this:
* [
* `post` => `post`
* `page` => `page`
* ]
*
* This can result in problems downstream when trying to array_merge this twice.
* array_values prevents this issue by ensuring numeric keys.
*/
$prominent_words_post_types = array_values( $prominent_words_post_types );
return $prominent_words_post_types;
}
/**
* Checks if the post type is supported.
*
* @param string $post_type The post type to look up.
*
* @return bool True when post type is supported.
*/
public function is_post_type_supported( $post_type ) {
return in_array( $post_type, $this->get_supported_post_types(), true );
}
/**
* Retrieves a list of taxonomies that are public, viewable and have the metabox enabled.
*
* @return array The supported taxonomies.
*/
public function get_supported_taxonomies() {
$taxonomies = get_taxonomies( [ 'public' => true ] );
$taxonomies = array_filter( $taxonomies, 'is_taxonomy_viewable' );
/**
* Filter: 'Yoast\WP\SEO\prominent_words_taxonomies' - Allows to filter from which taxonomies terms are eligible for generating prominent words.
*
* Note: This is a Premium plugin-only hook.
*
* @since 14.7.0
*
* @api array The accessible taxonomies.
*/
$prominent_words_taxonomies = apply_filters(
'Yoast\WP\SEO\prominent_words_taxonomies',
$taxonomies
);
if ( ! is_array( $prominent_words_taxonomies ) || empty( $prominent_words_taxonomies ) ) {
return [];
}
$prominent_words_taxonomies = array_filter(
$prominent_words_taxonomies,
static function( $taxonomy ) {
return (bool) WPSEO_Options::get( 'display-metabox-tax-' . $taxonomy, true );
}
);
return array_values( $prominent_words_taxonomies );
}
/**
* Checks if the taxonomy is supported.
*
* @param string $taxonomy The taxonomy to look up.
*
* @return bool True when taxonomy is supported.
*/
public function is_taxonomy_supported( $taxonomy ) {
return in_array( $taxonomy, $this->get_supported_taxonomies(), true );
}
}

View File

@@ -0,0 +1,190 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium
*/
/**
* Represents the unindexed posts.
*/
class WPSEO_Premium_Prominent_Words_Unindexed_Post_Query {
/**
* List containing unindexed posts totals per post type.
*
* @var array
*/
protected $totals = [];
/**
* Returns the total amount of posts.
*
* @since 4.6.0
*
* @param int $limit The offset for the query.
*
* @return bool True when the limit has been exceeded.
*/
public function exceeds_limit( $limit ) {
$unindexed_post_ids = $this->get_unindexed_post_ids( $this->get_post_types(), ( $limit + 1 ) );
return count( $unindexed_post_ids ) > $limit;
}
/**
* Returns the total unindexed posts for given post type.
*
* @since 4.6.0
*
* @param string $post_type The posttype to fetch.
*
* @return int The total amount of unindexed posts.
*/
public function get_total( $post_type ) {
if ( ! array_key_exists( $post_type, $this->totals ) ) {
$totals = $this->get_totals( $this->get_post_types() );
foreach ( $totals as $total_post_type => $total ) {
$this->totals[ $total_post_type ] = $total;
}
}
if ( ! array_key_exists( $post_type, $this->totals ) ) {
$this->totals[ $post_type ] = 0;
}
return $this->totals[ $post_type ];
}
/**
* Returns the totals for each posttype by counting them.
*
* @since 4.6.0
*
* @param array $post_types The posttype to limit the resultset for.
*
* @return array Array with the totals for the requested posttypes.
*/
public function get_totals( $post_types ) {
global $wpdb;
if ( $post_types === [] ) {
return $post_types;
}
$replacements = [
WPSEO_Premium_Prominent_Words_Versioning::POST_META_NAME,
WPSEO_Premium_Prominent_Words_Versioning::get_version_number(),
];
$replacements = array_merge( $replacements, $post_types );
$results = $wpdb->get_results(
$wpdb->prepare(
'
SELECT COUNT( ID ) as total, post_type
FROM ' . $wpdb->posts . '
WHERE ID NOT IN( SELECT post_id FROM ' . $wpdb->postmeta . ' WHERE meta_key = %s AND meta_value = %s )
AND post_status IN( "future", "draft", "pending", "private", "publish" )
AND post_type IN( ' . implode( ',', array_fill( 0, count( $post_types ), '%s' ) ) . ' )
GROUP BY post_type
',
$replacements
)
);
$totals = [];
foreach ( $results as $result ) {
$totals[ $this->determine_rest_endpoint_for_post_type( $result->post_type ) ] = (int) $result->total;
}
return $totals;
}
/**
* Determines the REST endpoint for the given post type.
*
* @param string $post_type The post type to determine the endpoint for.
*
* @return string The endpoint. Returns empty string if post type doesn't exist.
*/
protected function determine_rest_endpoint_for_post_type( $post_type ) {
$post_type_object = get_post_type_object( $post_type );
if ( is_null( $post_type_object ) ) {
return '';
}
if ( isset( $post_type_object->rest_base ) && ! empty( $post_type_object->rest_base ) ) {
return $post_type_object->rest_base;
}
return $post_type_object->name;
}
/**
* Returns the array with supported posttypes.
*
* @return array The supported posttypes.
*/
protected function get_post_types() {
$prominent_words_support = new WPSEO_Premium_Prominent_Words_Support();
return array_filter( $prominent_words_support->get_supported_post_types(), [ 'WPSEO_Post_Type', 'is_rest_enabled' ] );
}
/**
* Gets the Post IDs of un-indexed objects.
*
* @param array|string $post_types The post type(s) to fetch.
* @param int $limit Limit the number of results.
*
* @return int[] Post IDs found which are un-indexed.
*/
public function get_unindexed_post_ids( $post_types, $limit ) {
global $wpdb;
if ( is_string( $post_types ) ) {
$post_types = (array) $post_types;
}
if ( $post_types === [] ) {
return $post_types;
}
$replacements = [
WPSEO_Premium_Prominent_Words_Versioning::POST_META_NAME,
WPSEO_Premium_Prominent_Words_Versioning::get_version_number(),
];
$replacements = array_merge( $replacements, $post_types );
$replacements[] = $limit;
$results = $wpdb->get_results(
$wpdb->prepare(
'
SELECT ID
FROM ' . $wpdb->posts . '
WHERE ID NOT IN( SELECT post_id FROM ' . $wpdb->postmeta . ' WHERE meta_key = %s AND meta_value = %s )
AND post_status IN( "future", "draft", "pending", "private", "publish" )
AND post_type IN( ' . implode( ',', array_fill( 0, count( $post_types ), '%s' ) ) . ' )
LIMIT %d',
$replacements
),
ARRAY_A
);
// Make sure we return a list of IDs.
$results = wp_list_pluck( $results, 'ID' );
return $results;
}
/**
* Returns the array with supported post statuses.
*
* @return string[] The supported post statuses.
*/
public function get_supported_post_statuses() {
return [ 'future', 'draft', 'pending', 'private', 'publish' ];
}
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Admin
*/
/**
* Keeps track of the prominent words version.
*/
class WPSEO_Premium_Prominent_Words_Versioning {
// Needs to be manually updated in case of a major change.
const VERSION_NUMBER = 2;
const POST_META_NAME = '_yst_prominent_words_version';
/**
* Gets the version number.
*
* @return int The version number that was set in WPSEO_Premium_Prominent_Words_Versioning.
*/
public static function get_version_number() {
return self::VERSION_NUMBER;
}
/**
* Renames the meta key for the prominent words version. It was a public meta field and it has to be private.
*/
public static function upgrade_4_7() {
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_prominent_words_version"',
self::POST_META_NAME
)
);
}
/**
* Removes the meta key for the prominent words version for the unsupported languages that might have this value
* set.
*/
public static function upgrade_4_8() {
$supported_languages = [ 'en', 'de', 'nl', 'es', 'fr', 'it', 'pt', 'ru', 'pl', 'sv', 'id' ];
if ( in_array( WPSEO_Language_Utils::get_language( get_locale() ), $supported_languages, true ) ) {
return;
}
global $wpdb;
// The remove all post metas.
$wpdb->query(
$wpdb->prepare(
'DELETE FROM ' . $wpdb->postmeta . ' WHERE meta_key = %s',
self::POST_META_NAME
)
);
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium
*/
/**
* Registers the endpoint for the redirects to WordPress.
*/
class WPSEO_Premium_Redirect_EndPoint implements WPSEO_WordPress_Integration {
const REST_NAMESPACE = 'yoast/v1';
const ENDPOINT_QUERY = 'redirects';
const ENDPOINT_UNDO = 'redirects/delete';
const CAPABILITY_STORE = 'wpseo_manage_redirects';
/**
* Instance of the WPSEO_Premium_Redirect_Service class.
*
* @var WPSEO_Premium_Redirect_Service
*/
protected $service;
/**
* Sets the service to handle the request.
*
* @param WPSEO_Premium_Redirect_Service $service The service to handle the requests to the endpoint.
*/
public function __construct( WPSEO_Premium_Redirect_Service $service ) {
$this->service = $service;
}
/**
* Registers all hooks to WordPress.
*/
public function register_hooks() {
add_action( 'rest_api_init', [ $this, 'register' ] );
}
/**
* Register the REST endpoint to WordPress.
*/
public function register() {
$args = [
'origin' => [
'required' => true,
'type' => 'string',
'description' => 'The origin to redirect',
],
'target' => [
'required' => false,
'type' => 'string',
'description' => 'The redirect target',
],
'type' => [
'required' => true,
'type' => 'integer',
'description' => 'The redirect type',
],
];
register_rest_route(
self::REST_NAMESPACE,
self::ENDPOINT_QUERY,
[
'methods' => 'POST',
'args' => $args,
'callback' => [
$this->service,
'save',
],
'permission_callback' => [
$this,
'can_save_data',
],
]
);
register_rest_route(
self::REST_NAMESPACE,
self::ENDPOINT_UNDO,
[
'methods' => 'POST',
'args' => array_merge(
$args,
[
'type' => [
'required' => false,
'type' => 'string',
'description' => 'The redirect format',
],
]
),
'callback' => [
$this->service,
'delete',
],
'permission_callback' => [
$this,
'can_save_data',
],
]
);
}
/**
* Determines if the current user is allowed to use this endpoint.
*
* @return bool True user is allowed to use this endpoint.
*/
public function can_save_data() {
return current_user_can( self::CAPABILITY_STORE );
}
}

View File

@@ -0,0 +1,103 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Class WPSEO_Premium_Redirect_Export_Manager
*/
class WPSEO_Premium_Redirect_Export_Manager implements WPSEO_WordPress_Integration {
/**
* Registers all hooks to WordPress.
*/
public function register_hooks() {
// Add export CSV block, the import and export settings are confusingly named only import.
add_action( 'wpseo_import_tab_content', [ $this, 'add_redirect_export_block' ] );
add_action( 'wpseo_import_tab_header', [ $this, 'redirects_export_header' ] );
// Hijack the request in case of CSV download and return our generated CSV instead.
add_action( 'admin_init', [ $this, 'redirects_csv_export' ] );
}
/**
* Outputs a tab header for the CSV export block.
*/
public function redirects_export_header() {
if ( current_user_can( 'export' ) ) {
echo '<a class="nav-tab" id="export-redirects-tab" href="#top#export-redirects">'
. esc_html__( 'Export redirects', 'wordpress-seo-premium' )
. '</a>';
}
}
/**
* Adding the export block for CSV. Makes it able to export redirects to CSV.
*/
public function add_redirect_export_block() {
// Display the forms.
if ( current_user_can( 'export' ) ) {
require WPSEO_PREMIUM_PATH . 'classes/views/export-redirects.php';
}
}
/**
* Hijacks the request and returns a CSV file if we're on the right page with the right method and the right capabilities.
*/
public function redirects_csv_export() {
if ( $this->is_valid_csv_export_request() && current_user_can( 'export' ) ) {
// Check if we have a valid nonce.
check_admin_referer( 'wpseo-export' );
// Clean any content that has been already outputted, for example by other plugins or faulty PHP files.
if ( ob_get_contents() ) {
ob_clean();
}
// Set CSV headers and content.
$this->set_csv_headers();
echo $this->get_csv_contents();
// And exit so we don't start appending HTML to our CSV file.
// NOTE: this makes this entire class untestable as it will exit all tests but WordPress seems to have no elegant way of handling this.
exit();
}
}
/**
* Are we on the wpseo_tools page in the import-export tool and have we received an export post request?
*
* @return bool
*/
protected function is_valid_csv_export_request() {
return filter_input( INPUT_GET, 'page' ) === 'wpseo_tools'
&& filter_input( INPUT_GET, 'tool' ) === 'import-export'
&& filter_input( INPUT_POST, 'export' );
}
/**
* Sets the headers to trigger an CSV download in the browser.
*/
protected function set_csv_headers() {
header( 'Content-type: text/csv' );
header( 'Content-Disposition: attachment; filename=wordpress-seo-redirects.csv' );
header( 'Pragma: no-cache' );
header( 'Expires: 0' );
}
/**
* Generates CSV from all redirects.
*
* @return string
*/
protected function get_csv_contents() {
// Grab all our redirects.
$redirect_manager = new WPSEO_Redirect_Manager();
$redirects = $redirect_manager->get_all_redirects();
$csv_exporter = new WPSEO_Redirect_CSV_Exporter();
return $csv_exporter->export( $redirects );
}
}

View File

@@ -0,0 +1,78 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Represents the premium redirect option
*/
class WPSEO_Premium_Redirect_Option extends WPSEO_Option {
/**
* Option name.
*
* @var string
*/
public $option_name = 'wpseo_redirect';
/**
* Array of defaults for the option.
*
* {@internal Shouldn't be requested directly, use $this->get_defaults();}}
*
* @var array
*/
protected $defaults = [
// Form fields.
'disable_php_redirect' => 'off',
'separate_file' => 'off',
];
/**
* Registers the option to the WPSEO Options framework.
*/
public static function register_option() {
WPSEO_Options::register_option( static::get_instance() );
}
/**
* Get the singleton instance of this class.
*
* @return static Returns instance of itself.
*/
public static function get_instance() {
if ( ! ( static::$instance instanceof static ) ) {
static::$instance = new static();
}
return static::$instance;
}
/**
* 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.
*
* @return array The clean option with the saved value.
*/
protected function validate_option( $dirty, $clean, $old ) {
foreach ( $clean as $key => $value ) {
switch ( $key ) {
case 'disable_php_redirect':
case 'separate_file':
if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], [ 'on', 'off' ], true ) ) {
$clean[ $key ] = $dirty[ $key ];
}
break;
}
}
return $clean;
}
}

View File

@@ -0,0 +1,94 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium
*/
/**
* The service for the redirects to WordPress.
*/
class WPSEO_Premium_Redirect_Service {
/**
* Saves the redirect to the redirects.
*
* This save function is only used in the deprecated google-search-console integration.
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response to send back.
*/
public function save( WP_REST_Request $request ) {
$redirect = $this->map_request_to_redirect( $request );
if ( $this->get_redirect_manager()->create_redirect( $redirect ) ) {
return new WP_REST_Response( 'true' );
}
return new WP_REST_Response( 'false' );
}
/**
* Deletes the redirect from the redirects.
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response to send back.
*/
public function delete( WP_REST_Request $request ) {
$redirect = $this->map_request_to_redirect( $request );
$redirects = [ $redirect ];
$redirect_format = $request->get_param( 'format' );
if ( ! $redirect_format ) {
$redirect_format = WPSEO_Redirect_Formats::PLAIN;
}
if ( $this->get_redirect_manager( $redirect_format )->delete_redirects( $redirects ) ) {
return new WP_REST_Response(
[
'title' => __( 'Redirect deleted.', 'wordpress-seo-premium' ),
'message' => __( 'The redirect was deleted successfully.', 'wordpress-seo-premium' ),
'success' => true,
]
);
}
return new WP_REST_Response(
[
'title' => __( 'Redirect not deleted.', 'wordpress-seo-premium' ),
'message' => __( 'Something went wrong when deleting this redirect.', 'wordpress-seo-premium' ),
'success' => false,
],
400
);
}
/**
* Creates and returns an instance of the redirect manager.
*
* @param string $format The redirect format.
*
* @return WPSEO_Redirect_Manager The redirect maanger.
*/
protected function get_redirect_manager( $format = WPSEO_Redirect_Formats::PLAIN ) {
return new WPSEO_Redirect_Manager( $format );
}
/**
* Maps the given request to an instance of the WPSEO_Redirect.
*
* @param WP_REST_Request $request The request object.
*
* @return WPSEO_Redirect Redirect instance.
*/
protected function map_request_to_redirect( WP_REST_Request $request ) {
$origin = $request->get_param( 'origin' );
$target = $request->get_param( 'target' );
$type = $request->get_param( 'type' );
$format = $request->get_param( 'format' );
return new WPSEO_Redirect( $origin, $target, $type, $format );
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Admin\Capabilities
*/
/**
* Capabilities registration class.
*/
class WPSEO_Premium_Register_Capabilities implements WPSEO_WordPress_Integration {
/**
* Registers the hooks.
*
* @return void
*/
public function register_hooks() {
add_action( 'wpseo_register_capabilities_premium', [ $this, 'register' ] );
}
/**
* Registers the capabilities.
*
* @return void
*/
public function register() {
$manager = WPSEO_Capability_Manager_Factory::get( 'premium' );
$manager->register( 'wpseo_manage_redirects', [ 'administrator', 'editor', 'wpseo_editor', 'wpseo_manager' ] );
}
}

View File

@@ -0,0 +1,121 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Registers the filter for filtering stale cornerstone content.
*/
class WPSEO_Premium_Stale_Cornerstone_Content_Filter extends WPSEO_Abstract_Post_Filter {
/**
* Returns the query value this filter uses.
*
* @return string The query value this filter uses.
*/
public function get_query_val() {
return 'stale-cornerstone-content';
}
/**
* Modifies the query based on the seo_filter variable in $_GET.
*
* @param string $where The where statement.
*
* @return string The modified query.
*/
public function filter_posts( $where ) {
if ( ! $this->is_filter_active() ) {
return $where;
}
global $wpdb;
$where .= sprintf(
' AND ' . $wpdb->posts . '.ID IN( SELECT post_id FROM ' . $wpdb->postmeta . ' WHERE meta_key = "%s" AND meta_value = "1" ) AND ' . $wpdb->posts . '.post_modified < "%s" ',
WPSEO_Meta::$meta_prefix . 'is_cornerstone',
$this->date_threshold()
);
return $where;
}
/**
* Returns the label for this filter.
*
* @return string The label for this filter.
*/
protected function get_label() {
return __( 'Stale cornerstone content', 'wordpress-seo-premium' );
}
/**
* Returns a text explaining this filter.
*
* @return string|null The explanation for this filter.
*/
protected function get_explanation() {
$post_type_object = get_post_type_object( $this->get_current_post_type() );
if ( $post_type_object === null ) {
return null;
}
return sprintf(
/* translators: %s expands to the posttype label, %2$s expands anchor to blog post about cornerstone content, %3$s expands to </a> */
__( 'Stale cornerstone content refers to cornerstone content that hasnt been updated in the last 6 months. Make sure to keep these %1$s up-to-date. %2$sLearn more about cornerstone content%3$s.', 'wordpress-seo-premium' ),
strtolower( $post_type_object->labels->name ),
'<a href="' . WPSEO_Shortlinker::get( 'https://yoa.st/1i9' ) . '" target="_blank">',
'</a>'
);
}
/**
* Returns the total amount of stale cornerstone content.
*
* @return int The total amount of stale cornerstone content.
*/
protected function get_post_total() {
global $wpdb;
return (int) $wpdb->get_var(
$wpdb->prepare(
'
SELECT COUNT( 1 )
FROM ' . $wpdb->postmeta . '
WHERE post_id IN( SELECT ID FROM ' . $wpdb->posts . ' WHERE post_type = %s && post_modified < %s ) &&
meta_value = "1" AND meta_key = %s
',
$this->get_current_post_type(),
$this->date_threshold(),
WPSEO_Meta::$meta_prefix . 'is_cornerstone'
)
);
}
/**
* Returns the post types to which this filter should be added.
*
* @return array The post types to which this filter should be added.
*/
protected function get_post_types() {
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Using YoastSEO hook.
$post_types = apply_filters( 'wpseo_cornerstone_post_types', parent::get_post_types() );
if ( ! is_array( $post_types ) ) {
return [];
}
return $post_types;
}
/**
* Returns the date 6 months ago.
*
* @return string The formatted date.
*/
protected function date_threshold() {
return gmdate( 'Y-m-d', strtotime( '-6months' ) );
}
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
if ( class_exists( 'Yoast_Product' ) && ! class_exists( 'WPSEO_Product_Premium', false ) ) {
/**
* Class WPSEO_Product_Premium
*/
class WPSEO_Product_Premium extends Yoast_Product {
/**
* Plugin author name.
*
* @var string
*/
const PLUGIN_AUTHOR = 'Yoast';
/**
* License endpoint.
*
* @var string
*/
const EDD_STORE_URL = 'http://my.yoast.com';
/**
* Product name to use for license checks.
*
* @var string
*/
const EDD_PLUGIN_NAME = 'Yoast SEO Premium';
/**
* Construct the Product Premium class
*/
public function __construct() {
$file = plugin_basename( WPSEO_FILE );
$slug = dirname( $file );
parent::__construct(
trailingslashit( self::EDD_STORE_URL ) . 'edd-sl-api',
self::EDD_PLUGIN_NAME,
$slug,
WPSEO_Premium::PLUGIN_VERSION_NAME,
'https://yoast.com/wordpress/plugins/seo-premium/',
'admin.php?page=wpseo_licenses#top#licenses',
'wordpress-seo',
self::PLUGIN_AUTHOR,
$file
);
if ( method_exists( $this, 'set_extension_url' ) ) {
$this->set_extension_url( 'https://my.yoast.com/licenses/' );
}
}
}
}

View File

@@ -0,0 +1,172 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium
*/
/**
* Registers the endpoint to delete the redirect for a Post to WordPress.
*/
class WPSEO_Premium_Redirect_Undo_EndPoint implements WPSEO_WordPress_Integration {
const REST_NAMESPACE = 'yoast/v1';
const ENDPOINT_UNDO = 'redirects/undo-for-object';
/**
* Instance of the WPSEO_Redirect_Manager class.
*
* @var WPSEO_Redirect_Manager
*/
protected $manager;
/**
* Sets the service to handle the request.
*
* @param WPSEO_Redirect_Manager $manager The manager for working with redirects.
*/
public function __construct( WPSEO_Redirect_Manager $manager ) {
$this->manager = $manager;
}
/**
* Registers all hooks to WordPress.
*/
public function register_hooks() {
add_action( 'rest_api_init', [ $this, 'register' ] );
}
/**
* Register the REST endpoint to WordPress.
*/
public function register() {
register_rest_route(
self::REST_NAMESPACE,
self::ENDPOINT_UNDO,
[
'methods' => 'POST',
'args' => [
'obj_id' => [
'required' => true,
'type' => 'int',
'description' => 'The id of the post or term',
],
'obj_type' => [
'required' => true,
'type' => 'string',
'description' => 'The object type: post or term',
],
],
'callback' => [ $this, 'undo_redirect' ],
'permission_callback' => [ $this, 'can_save_data' ],
]
);
}
/**
* Deletes the latest redirect to the post or term referenced in the request.
*
* @param WP_REST_Request $request The request.
*
* @return WP_REST_Response The response.
*/
public function undo_redirect( WP_REST_Request $request ) {
$object_id = $request->get_param( 'obj_id' );
$object_type = $request->get_param( 'obj_type' );
$redirect_info = $this->retrieve_post_or_term_redirect_info( $object_type, $object_id );
$redirect = $this->map_redirect_info_to_redirect( $redirect_info );
if ( ! $redirect->get_origin() ) {
return new WP_REST_Response(
[
'title' => __( 'Redirect not deleted.', 'wordpress-seo-premium' ),
'message' => __( 'Something went wrong when deleting this redirect.', 'wordpress-seo-premium' ),
'success' => false,
],
400
);
}
if ( $this->manager->delete_redirects( [ $redirect ] ) ) {
return new WP_REST_Response(
[
'title' => __( 'Redirect deleted.', 'wordpress-seo-premium' ),
'message' => __( 'The redirect was deleted successfully.', 'wordpress-seo-premium' ),
'success' => true,
]
);
}
return new WP_REST_Response(
[
'title' => __( 'Redirect not deleted.', 'wordpress-seo-premium' ),
'message' => __( 'Something went wrong when deleting this redirect.', 'wordpress-seo-premium' ),
'success' => false,
],
400
);
}
/**
* Maps the given redirect info to an instance of the WPSEO_Redirect.
*
* @param array $redirect_info The redirect info array.
*
* @return WPSEO_Redirect Redirect instance.
*/
protected function map_redirect_info_to_redirect( $redirect_info ) {
$origin = isset( $redirect_info['origin'] ) ? $redirect_info['origin'] : null;
$target = isset( $redirect_info['target'] ) ? $redirect_info['target'] : null;
$type = isset( $redirect_info['type'] ) ? $redirect_info['type'] : null;
$format = isset( $redirect_info['format'] ) ? $redirect_info['format'] : null;
return new WPSEO_Redirect( $origin, $target, $type, $format );
}
/**
* Retrieve the redirect info from the meta for the specified object and id.
*
* @param string $object_type The type of object: post or term.
* @param int $object_id The post or term ID.
*
* @return array
*/
private function retrieve_post_or_term_redirect_info( $object_type, $object_id ) {
if ( $object_type === 'post' ) {
$redirect_info = get_post_meta( $object_id, '_yoast_post_redirect_info', true );
delete_post_meta( $object_id, '_yoast_post_redirect_info' );
return $redirect_info;
}
if ( $object_type === 'term' ) {
$redirect_info = get_term_meta( $object_id, '_yoast_term_redirect_info', true );
delete_term_meta( $object_id, '_yoast_term_redirect_info' );
return $redirect_info;
}
return [];
}
/**
* Determines if the current user is allowed to use this endpoint.
*
* @param WP_REST_Request $request The request.
*
* @return bool True user is allowed to use this endpoint.
*/
public function can_save_data( WP_REST_Request $request ) {
$object_id = $request->get_param( 'obj_id' );
$object_type = $request->get_param( 'obj_type' );
if ( $object_type === 'post' ) {
return current_user_can( 'edit_post', $object_id );
}
if ( $object_type === 'term' ) {
return current_user_can( 'edit_term', $object_id );
}
return false;
}
}

View File

@@ -0,0 +1,104 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirects
*/
/**
* Represents an executable redirect.
*/
final class WPSEO_Executable_Redirect {
/**
* Redirect origin.
*
* @var string
*/
private $origin;
/**
* Redirect target.
*
* @var string
*/
private $target;
/**
* A HTTP code determining the redirect type.
*
* @var int
*/
private $type;
/**
* A string determining the redirect format (plain or regex).
*
* @var string
*/
private $format;
/**
* WPSEO_Redirect constructor.
*
* @codeCoverageIgnore
*
* @param string $origin The origin of the redirect.
* @param string $target The target of the redirect.
* @param int $type The type of the redirect.
* @param string $format The format of the redirect.
*/
public function __construct( $origin, $target, $type, $format ) {
$this->origin = $origin;
$this->target = $target;
$this->type = $type;
$this->format = $format;
}
/**
* Creates an instance based on the given data.
*
* @param array $data The redirect data.
*
* @return WPSEO_Executable_Redirect The created object.
*/
public static function from_array( $data ) {
return new self( $data['origin'], $data['target'], $data['type'], $data['format'] );
}
/**
* Retrieves the origin.
*
* @return string The origin.
*/
public function get_origin() {
return $this->origin;
}
/**
* Retrieves the target.
*
* @return string The target.
*/
public function get_target() {
return $this->target;
}
/**
* Retrieves the type.
*
* @return int The redirect type.
*/
public function get_type() {
return $this->type;
}
/**
* Retrieves the redirect format.
*
* @return string The redirect format.
*/
public function get_format() {
return $this->format;
}
}

View File

@@ -0,0 +1,136 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Exporters
*/
/**
* This exporter class will format the redirects for apache files.
*/
class WPSEO_Redirect_Apache_Exporter extends WPSEO_Redirect_File_Exporter {
/**
* %1$s is the old URL
* %2$s is the new URL
* %3$s is the redirect type
*
* @var string
*/
protected $url_format = 'Redirect %3$s "%1$s" "%2$s"';
/**
* %1$s is the old URL
* %2$s is the redirect type
*
* @var string
*/
protected $url_non_target_format = 'Redirect %2$s "%1$s"';
/**
* %1$s is the old URL
* %2$s is the new URL
* %3$s is the redirect type
*
* @var string
*/
protected $regex_format = 'RedirectMatch %3$s %1$s %2$s';
/**
* %1$s is the old URL
* %2$s is the redirect type
*
* @var string
*/
protected $regex_non_target_format = 'RedirectMatch %2$s %1$s';
/**
* Overrides the parent method. This method will in case of URL redirects add slashes to the URL.
*
* @param WPSEO_Redirect $redirect The redirect data.
*
* @return string
*/
public function format( WPSEO_Redirect $redirect ) {
// 4xx redirects don't have a target.
$redirect_type = intval( $redirect->get_type() );
if ( $redirect_type >= 400 && $redirect_type < 500 ) {
return $this->format_non_target( $redirect );
}
if ( $redirect->get_format() === WPSEO_Redirect_Formats::PLAIN ) {
return sprintf(
$this->get_format( $redirect->get_format() ),
$this->format_url( $redirect->get_origin() ),
$this->format_url( $redirect->get_target() ),
$redirect->get_type()
);
}
return parent::format( $redirect );
}
/**
* Format the URL before it is added to the redirects.
*
* @param string $url The URL.
*
* @return string Formatted URL.
*/
protected function format_url( $url ) {
return $this->add_url_slash( $url );
}
/**
* Build the redirect output for non-target status codes (4xx)
*
* @param WPSEO_Redirect $redirect The redirect data.
*
* @return string
*/
public function format_non_target( WPSEO_Redirect $redirect ) {
$target = $redirect->get_origin();
if ( $redirect->get_format() === WPSEO_Redirect_Formats::PLAIN ) {
$target = $this->add_url_slash( $target );
}
return sprintf(
$this->get_non_target_format( $redirect->get_format() ),
$target,
$redirect->get_type()
);
}
/**
* Get the format the redirect needs to output
*
* @param string $redirect_format The format of the redirect.
*
* @return string
*/
public function get_non_target_format( $redirect_format ) {
if ( $redirect_format === WPSEO_Redirect_Formats::PLAIN ) {
return $this->url_non_target_format;
}
return $this->regex_non_target_format;
}
/**
* Check if first character is a slash, adds a slash if it ain't so
*
* @param string $url The URL add the slashes to.
*
* @return string mixed
*/
private function add_url_slash( $url ) {
$scheme = wp_parse_url( $url, PHP_URL_SCHEME );
if ( substr( $url, 0, 1 ) !== '/' && empty( $scheme ) ) {
$url = '/' . $url;
}
return $url;
}
}

View File

@@ -0,0 +1,93 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Exporters
*/
/**
* This exporter class will format the redirects for csv files.
*
* Does not implement WPSEO_Redirect_File_Exporter as the CSV is intended to be streamed, not saved.
*/
class WPSEO_Redirect_CSV_Exporter implements WPSEO_Redirect_Exporter {
/**
* Exports an array of redirects to a CSV string.
*
* @param WPSEO_Redirect[] $redirects The redirects to export.
*
* @return string CSV string of all exported redirects with headers.
*/
public function export( $redirects ) {
$csv = $this->get_headers();
if ( ! empty( $redirects ) ) {
foreach ( $redirects as $redirect ) {
if ( $redirect instanceof WPSEO_Redirect ) {
$csv .= PHP_EOL . $this->format( $redirect );
}
}
}
return $csv;
}
/**
* Formats a redirect for use in the export, returns a line of CSV.
*
* @param WPSEO_Redirect $redirect The redirect to format.
*
* @return string CSV line of the redirect for format.
*/
public function format( WPSEO_Redirect $redirect ) {
$target = $redirect->get_target();
if ( WPSEO_Redirect_Util::is_relative_url( $target ) ) {
$target = '/' . $target;
}
if ( WPSEO_Redirect_Util::requires_trailing_slash( $target ) ) {
$target = trailingslashit( $target );
}
$origin = $redirect->get_origin();
if ( $redirect->get_format() === WPSEO_Redirect_Formats::PLAIN && WPSEO_Redirect_Util::is_relative_url( $origin ) ) {
$origin = '/' . $origin;
}
$redirect_details = [
$this->format_csv_column( $origin ),
$this->format_csv_column( $target ),
$this->format_csv_column( $redirect->get_type() ),
$this->format_csv_column( $redirect->get_format() ),
];
return implode( ',', $redirect_details );
}
/**
* Returns the headers to add to the first line of the generated CSV.
*
* @return string CSV line of the headers.
*/
protected function get_headers() {
$headers = [
__( 'Origin', 'wordpress-seo-premium' ),
__( 'Target', 'wordpress-seo-premium' ),
__( 'Type', 'wordpress-seo-premium' ),
__( 'Format', 'wordpress-seo-premium' ),
];
return implode( ',', $headers );
}
/**
* Surrounds a value with double quotes and escapes existing double quotes.
*
* @param string $value The value to sanitize.
*
* @return string The sanitized value.
*/
protected function format_csv_column( $value ) {
return '"' . str_replace( '"', '""', (string) $value ) . '"';
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Exporters
*/
/**
* Represents a redirect export
*/
interface WPSEO_Redirect_Exporter {
/**
* Exports an array of redirects.
*
* @param WPSEO_Redirect[] $redirects The redirects to export.
*/
public function export( $redirects );
/**
* Formats a redirect for use in the export.
*
* @param WPSEO_Redirect $redirect The redirect to format.
*
* @return mixed
*/
public function format( WPSEO_Redirect $redirect );
}

View File

@@ -0,0 +1,92 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Exporters
*/
/**
* Class WPSEO_Redirect_File.
*/
abstract class WPSEO_Redirect_File_Exporter implements WPSEO_Redirect_Exporter {
/**
* The URL format.
*
* @var string
*/
protected $url_format = '';
/**
* The regex format.
*
* @var string
*/
protected $regex_format = '';
/**
* Exports an array of redirects.
*
* @param WPSEO_Redirect[] $redirects The redirects to export.
*
* @return bool
*/
public function export( $redirects ) {
$file_content = '';
if ( ! empty( $redirects ) ) {
foreach ( $redirects as $redirect ) {
$file_content .= $this->format( $redirect ) . PHP_EOL;
}
}
// Check if the file content isset.
return $this->save( $file_content );
}
/**
* Formats a redirect for use in the export.
*
* @param WPSEO_Redirect $redirect The redirect to format.
*
* @return string
*/
public function format( WPSEO_Redirect $redirect ) {
return sprintf(
$this->get_format( $redirect->get_format() ),
$redirect->get_origin(),
$redirect->get_target(),
$redirect->get_type()
);
}
/**
* Returns the needed format for the redirect.
*
* @param string $redirect_format The format of the redirect.
*
* @return string
*/
protected function get_format( $redirect_format ) {
if ( $redirect_format === WPSEO_Redirect_Formats::PLAIN ) {
return $this->url_format;
}
return $this->regex_format;
}
/**
* Save the redirect file.
*
* @param string $file_content The file content that will be saved.
*
* @return bool
*/
protected function save( $file_content ) {
// Save the actual file.
if ( is_writable( WPSEO_Redirect_File_Util::get_file_path() ) ) {
WPSEO_Redirect_File_Util::write_file( WPSEO_Redirect_File_Util::get_file_path(), $file_content );
}
return true;
}
}

View File

@@ -0,0 +1,101 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Exporters
*/
/**
* Class WPSEO_Htaccess_Redirect_File
*/
class WPSEO_Redirect_Htaccess_Exporter extends WPSEO_Redirect_Apache_Exporter {
/**
* Save the redirect file
*
* @param string $file_content The file content that will be saved.
*
* @return bool
*/
protected function save( $file_content ) {
$file_path = WPSEO_Redirect_Htaccess_Util::get_htaccess_file_path();
// Update the .htaccess file.
if ( is_writable( $file_path ) ) {
$htaccess = $this->get_htaccess_content( $file_path, $file_content );
$return = (bool) WPSEO_Redirect_File_Util::write_file( $file_path, $htaccess );
// Make sure defines are created.
WP_Filesystem();
chmod( $file_path, FS_CHMOD_FILE );
return $return;
}
return false;
}
/**
* Getting the content from current .htaccess
*
* @param string $file_path The location of the htaccess file.
* @param string $file_content THe content to save in the htaccess file.
*
* @return string
*/
private function get_htaccess_content( $file_path, $file_content ) {
// Read current htaccess.
$htaccess = '';
if ( file_exists( $file_path ) ) {
$htaccess = file_get_contents( $file_path );
}
$htaccess = preg_replace( '`# BEGIN YOAST REDIRECTS.*# END YOAST REDIRECTS' . PHP_EOL . '`is', '', $htaccess );
// Only add redirect code when redirects are present.
if ( ! empty( $file_content ) ) {
$file_content = '# BEGIN YOAST REDIRECTS' . PHP_EOL . '<IfModule mod_rewrite.c>' . PHP_EOL . 'RewriteEngine On' . PHP_EOL . $file_content . '</IfModule>' . PHP_EOL . '# END YOAST REDIRECTS' . PHP_EOL;
// Prepend our redirects to htaccess file.
$htaccess = $file_content . $htaccess;
}
return $htaccess;
}
/**
* Escape special characters in the URL that will cause problems in .htaccess.
*
* Overrides WPSEO_Redirect_Apache_Exporter::format_url.
*
* @param string $url The URL.
*
* @return string The escaped URL.
*/
protected function format_url( $url ) {
$url = parent::format_url( $url );
return $this->sanitize( $url );
}
/**
* Escape special characters that will cause problems in .htaccess.
*
* @param string $unsanitized The unsanitized value.
*
* @return string The sanitized value.
*/
private function sanitize( $unsanitized ) {
return str_replace(
[
'\\',
'"',
],
[
'/',
'\"',
],
$unsanitized
);
}
}

View File

@@ -0,0 +1,87 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Exporters
*/
/**
* Exporter for Nginx, only declares the two formats
*/
class WPSEO_Redirect_Nginx_Exporter extends WPSEO_Redirect_File_Exporter {
/**
* %1$s is the origin
* %2$s is the target
* %3$s is the redirect type
* %4$s is the optional x-redirect-by filter.
*
* @var string
*/
protected $url_format = 'location /%1$s { %4$s return %3$s %2$s; }';
/**
* %1$s is the origin
* %2$s is the target
* %3$s is the redirect type
* %4$s is the optional x-redirect-by filter.
*
* @var string
*/
protected $regex_format = 'location ~ %1$s { %4$s return %3$s %2$s; }';
/**
* Formats a redirect for use in the export.
*
* @param WPSEO_Redirect $redirect The redirect to format.
*
* @return string
*/
public function format( WPSEO_Redirect $redirect ) {
return sprintf(
$this->get_format( $redirect->get_format() ),
$redirect->get_origin(),
$redirect->get_target(),
$redirect->get_type(),
$this->add_x_redirect_header()
);
}
/**
* Adds an X-Redirect-By header if allowed by the filter.
*
* @return string
*/
private function add_x_redirect_header() {
/**
* Filter: 'wpseo_add_x_redirect' - can be used to remove the X-Redirect-By header
* Yoast SEO Premium creates (defaults to true, which is adding it)
*
* @deprecated 12.9.0. Use the {@see 'Yoast\WP\SEO\add_x_redirect'} filter instead.
*
* @api bool
*/
$add_x_redirect = apply_filters_deprecated(
'wpseo_add_x_redirect',
[ true ],
'YoastSEO Premium 12.9.0',
'Yoast\WP\SEO\add_x_redirect'
);
/**
* Filter: 'Yoast\WP\SEO\add_x_redirect' - can be used to remove the X-Redirect-By header
* Yoast SEO Premium creates (defaults to true, which is adding it)
*
* Note: This is a Premium plugin-only hook.
*
* @since 12.9.0
*
* @api bool
*/
if ( apply_filters( 'Yoast\WP\SEO\add_x_redirect', $add_x_redirect ) === true ) {
return 'add_header X-Redirect-By "Yoast SEO Premium";';
}
return '';
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirects\Redirect\Exporters
*/
/**
* Saving the redirects from a single file into two smaller options files.
*/
class WPSEO_Redirect_Option_Exporter implements WPSEO_Redirect_Exporter {
/**
* This method will split the redirects in separate arrays and store them in an option.
*
* @param WPSEO_Redirect[] $redirects The redirects to export.
*
* @return bool
*/
public function export( $redirects ) {
$formatted_redirects = [
WPSEO_Redirect_Formats::PLAIN => [],
WPSEO_Redirect_Formats::REGEX => [],
];
foreach ( $redirects as $redirect ) {
$formatted_redirects[ $redirect->get_format() ][ $redirect->get_origin() ] = $this->format( $redirect );
}
// Save the plain redirects. No need to autoload, since the option is fetched straight from the DB.
update_option( WPSEO_Redirect_Option::OPTION_PLAIN, $formatted_redirects[ WPSEO_Redirect_Formats::PLAIN ], 'no' );
// Save the regex redirects. No need to autoload, since the option is fetched straight from the DB.
update_option( WPSEO_Redirect_Option::OPTION_REGEX, $formatted_redirects[ WPSEO_Redirect_Formats::REGEX ], 'no' );
return true;
}
/**
* Formats a redirect for use in the export.
*
* @param WPSEO_Redirect $redirect The redirect to format.
*
* @return array
*/
public function format( WPSEO_Redirect $redirect ) {
return [
'url' => $redirect->get_target(),
'type' => $redirect->get_type(),
];
}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Loaders
*/
/**
* Base class for loading redirects from an external source and validating them.
*/
abstract class WPSEO_Redirect_Abstract_Loader implements WPSEO_Redirect_Loader {
/**
* Validates if the given value is a http status code.
*
* @param string|int $status_code The status code to validate.
*
* @return bool Whether or not the status code is valid.
*/
protected function validate_status_code( $status_code ) {
if ( is_string( $status_code ) ) {
if ( ! preg_match( '/\A\d+\Z/', $status_code, $matches ) ) {
return false;
}
$status_code = (int) $status_code;
}
$status_codes = new WPSEO_Redirect_Types();
return $status_codes->has( $status_code );
}
/**
* Validates if the given value is a redirect format.
*
* @param string $format The format to validate.
*
* @return bool Whether or not the format is valid.
*/
protected function validate_format( $format ) {
$redirect_formats = new WPSEO_Redirect_Formats();
return $redirect_formats->has( $format );
}
}

View File

@@ -0,0 +1,78 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Loaders
*/
/**
* Class for loading redirects from a CSV file and validating them.
*/
class WPSEO_Redirect_CSV_Loader extends WPSEO_Redirect_Abstract_Loader {
/**
* Path of the CSV file to load.
*
* @var string
*/
protected $csv_file;
/**
* WPSEO_Redirect_CSV_Loader constructor.
*
* @param string $csv_file Path of the CSV file to load.
*/
public function __construct( $csv_file ) {
$this->csv_file = $csv_file;
}
/**
* Loads all redirects from the CSV file.
*
* @return WPSEO_Redirect[] The redirects loaded from the CSV file.
*/
public function load() {
$handle = fopen( $this->csv_file, 'r' );
if ( ! $handle ) {
return [];
}
$redirects = [];
while ( $item = fgetcsv( $handle, 10000 ) ) {
if ( ! $this->validate_item( $item ) ) {
continue;
}
$redirects[] = new WPSEO_Redirect( $item[0], $item[1], $item[2], $item[3] );
}
return $redirects;
}
/**
* Checks if a parsed CSV row is has a valid redirect format.
* It should have exactly 4 values.
* The third value should be a http status code.
* The last value should be a redirect format.
*
* @param array $item The parsed CSV row.
*
* @return bool Whether or not the parsed CSV row is valid.
*/
protected function validate_item( $item ) {
if ( count( $item ) !== 4 ) {
return false;
}
if ( ! $this->validate_status_code( $item[2] ) ) {
return false;
}
if ( ! $this->validate_format( $item[3] ) ) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,145 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Loaders
*/
/**
* Class for loading redirects from .htaccess files.
*/
class WPSEO_Redirect_HTAccess_Loader extends WPSEO_Redirect_Abstract_Loader {
/**
* The contents of the htaccess file to import.
*
* @var string
*/
protected $htaccess;
/**
* WPSEO_Redirect_HTAccess_Loader constructor.
*
* @param string $htaccess The contents of the htaccess file to import.
*/
public function __construct( $htaccess ) {
$this->htaccess = $htaccess;
}
/**
* Loads redirects as WPSEO_Redirects from the .htaccess file given to the constructor.
*
* @return WPSEO_Redirect[] The loaded redirects.
*/
public function load() {
$redirects = [];
// Loop through patterns.
foreach ( self::regex_patterns() as $regex ) {
$matches = $this->match_redirects( $regex['pattern'] );
if ( is_array( $matches ) ) {
$redirects = array_merge( $redirects, $this->convert_redirects_from_matches( $matches, $regex['type'] ) );
}
}
return $redirects;
}
/**
* Matches the string (containing redirects) for the given regex.
*
* @param string $pattern The regular expression to match redirects.
*
* @return array[]
*/
protected function match_redirects( $pattern ) {
preg_match_all( $pattern, $this->htaccess, $matches, PREG_SET_ORDER );
return $matches;
}
/**
* Converts matches to WPSEO_Redirect objects.
*
* @param array[] $matches The redirects to save.
* @param string $format The format for the redirects.
*
* @return WPSEO_Redirect[] The redirects.
*/
protected function convert_redirects_from_matches( $matches, $format ) {
$redirects = [];
foreach ( $matches as $match ) {
$type = trim( $match[1] );
$source = trim( $match[2] );
$target = $this->parse_target( $type, $match );
if ( $target === false || $source === '' || ! $this->validate_status_code( $type ) ) {
continue;
}
$redirects[] = new WPSEO_Redirect( $source, $target, $type, $format );
}
return $redirects;
}
/**
* Parses the target from a match.
*
* @param string $type The status code of the redirect.
* @param string[] $matched The match.
*
* @return bool|string The status code, false if no status code could be parsed.
*/
protected function parse_target( $type, $matched ) {
// If it's a gone status code that doesn't need a target.
if ( $type === '410' ) {
return '';
}
$target = trim( $matched[3] );
// There is no target, skip it.
if ( $target === '' ) {
return false;
}
return $target;
}
/**
* Returns regex patterns to match redirects in .htaccess files.
*
* @return array[] The regex patterns to test against.
*/
protected static function regex_patterns() {
return [
[
'type' => WPSEO_Redirect_Formats::PLAIN,
'pattern' => '`^Redirect ([0-9]{3}) ([^"\s]+) ([a-z0-9-_+/.:%&?=#\][]+)`im',
],
[
'type' => WPSEO_Redirect_Formats::PLAIN,
'pattern' => '`^Redirect ([0-9]{3}) "([^"]+)" ([a-z0-9-_+/.:%&?=#\][]+)`im',
],
[
'type' => WPSEO_Redirect_Formats::PLAIN,
'pattern' => '`^Redirect (410) ([^"\s]+)`im', // Matches a redirect without a target.
],
[
'type' => WPSEO_Redirect_Formats::PLAIN,
'pattern' => '`^Redirect (410) "([^"]+)"`im', // Matches a redirect without a target.
],
[
'type' => WPSEO_Redirect_Formats::REGEX,
'pattern' => '`^RedirectMatch ([0-9]{3}) ([^"\s]+) ([^\s]+)`im',
],
[
'type' => WPSEO_Redirect_Formats::REGEX,
'pattern' => '`^RedirectMatch ([0-9]{3}) "([^"]+)" ([^\s]+)`im',
],
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Loaders
*/
/**
* Represents a redirect loader for external sources.
*/
interface WPSEO_Redirect_Loader {
/**
* Loads the redirects from an external source and validates them.
*
* @return WPSEO_Redirect[] The loaded redirects.
*/
public function load();
}

View File

@@ -0,0 +1,61 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Loaders
*/
/**
* Class for loading redirects from the Redirection plugin.
*/
class WPSEO_Redirect_Redirection_Loader extends WPSEO_Redirect_Abstract_Loader {
/**
* A WordPress database object.
*
* @var wpdb
*/
protected $wpdb;
/**
* WPSEO_Redirect_Redirection_Loader constructor.
*
* @param wpdb $wpdb A WordPress database object.
*/
public function __construct( $wpdb ) {
$this->wpdb = $wpdb;
}
/**
* Loads redirects as WPSEO_Redirects from the Redirection plugin.
*
* @return WPSEO_Redirect[] The loaded redirects.
*/
public function load() {
// Get redirects.
// phpcs:disable WordPress.DB.PreparedSQL -- Prefix variable comes from wpdb, query is fine without preparing.
$items = $this->wpdb->get_results(
"SELECT `url`, `action_data`, `regex`, `action_code`
FROM {$this->wpdb->prefix}redirection_items
WHERE `status` = 'enabled' AND `action_type` = 'url'"
);
// phpcs:enable
$redirects = [];
foreach ( $items as $item ) {
$format = WPSEO_Redirect_Formats::PLAIN;
if ( (int) $item->regex === 1 ) {
$format = WPSEO_Redirect_Formats::REGEX;
}
if ( ! $this->validate_status_code( $item->action_code ) ) {
continue;
}
$redirects[] = new WPSEO_Redirect( $item->url, $item->action_data, $item->action_code, $format );
}
return $redirects;
}
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Loaders
*/
/**
* Class for loading redirects from the Safe Redirect Manager plugin.
*
* @link https://wordpress.org/plugins/safe-redirect-manager/
*/
class WPSEO_Redirect_Safe_Redirect_Loader extends WPSEO_Redirect_Abstract_Loader {
/**
* Loads redirects as WPSEO_Redirects from the Safe Redirect Manager plugin.
*
* @return WPSEO_Redirect[] The loaded redirects.
*/
public function load() {
$items = get_transient( '_srm_redirects' );
$redirects = [];
if ( ! is_array( $items ) ) {
return $redirects;
}
foreach ( $items as $item ) {
$item = $this->convert_wildcards( $item );
$format = WPSEO_Redirect_Formats::PLAIN;
if ( (int) $item['enable_regex'] === 1 ) {
$format = WPSEO_Redirect_Formats::REGEX;
}
$status_code = $this->convert_status_code( $item['status_code'] );
if ( ! $this->validate_status_code( $status_code ) ) {
continue;
}
$redirects[] = new WPSEO_Redirect( $item['redirect_from'], $item['redirect_to'], $status_code, $format );
}
return $redirects;
}
/**
* Converts unsupported 404 and 403 status codes to a 410 status code.
* Also converts unsupported 303 status codes to a 302 status code.
*
* @param int $status_code The original status code.
*
* @return int A status code Yoast supports.
*/
protected function convert_status_code( $status_code ) {
switch ( $status_code ) {
case 303:
return 302;
case 403:
case 404:
return 410;
default:
return (int) $status_code;
}
}
/**
* Converts unsupported wildcard format to supported regex format.
*
* @param array $item A Safe Redirect Manager redirect.
*
* @return array A converted redirect.
*/
protected function convert_wildcards( $item ) {
if ( substr( $item['redirect_from'], -1, 1 ) === '*' ) {
$item['redirect_from'] = preg_replace( '/(\*)$/', '.*', $item['redirect_from'] );
$item['enable_regex'] = 1;
}
return $item;
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Loaders
*/
/**
* Class for loading redirects from the Simple 301 Redirects plugin.
*
* @link https://wordpress.org/plugins/simple-301-redirects/
*/
class WPSEO_Redirect_Simple_301_Redirect_Loader extends WPSEO_Redirect_Abstract_Loader {
/**
* Loads redirects as WPSEO_Redirects from the Simple 301 Redirects plugin.
*
* @return WPSEO_Redirect[] The loaded redirects.
*/
public function load() {
$items = get_option( '301_redirects' );
$uses_wildcards = get_option( '301_redirects_wildcard' );
$redirects = [];
if ( ! is_array( $items ) ) {
return $redirects;
}
foreach ( $items as $origin => $target ) {
$format = WPSEO_Redirect_Formats::PLAIN;
// If wildcard redirects had been used, and this is one, flip it.
if ( $uses_wildcards && strpos( $origin, '*' ) !== false ) {
$format = WPSEO_Redirect_Formats::REGEX;
$origin = str_replace( '*', '(.*)', $origin );
$target = str_replace( '*', '$1', $target );
}
$redirects[] = new WPSEO_Redirect( $origin, $target, 301, $format );
}
return $redirects;
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Presenters
*/
/**
* The presenter for the form, this form will be used for adding and updating the redirects.
*/
class WPSEO_Redirect_Form_Presenter implements WPSEO_Redirect_Presenter {
/**
* Variables to be passed to the form view.
*
* @var array
*/
private $view_vars;
/**
* Setting up the view_vars.
*
* @param array $view_vars The variables to pass into the view.
*/
public function __construct( array $view_vars ) {
$this->view_vars = $view_vars;
$this->view_vars['redirect_types'] = $this->get_redirect_types();
}
/**
* Display the form.
*
* @param array $display Additional display variables.
*
* @return void
*/
public function display( array $display = [] ) {
$display_vars = $this->view_vars;
if ( ! empty( $display ) ) {
$display_vars = array_merge_recursive( $display_vars, $display );
}
require WPSEO_PREMIUM_PATH . 'classes/redirect/views/redirects-form.php';
}
/**
* Getting array with the available redirect types.
*
* @return array Array with the redirect types.
*/
private function get_redirect_types() {
$types = new WPSEO_Redirect_Types();
return $types->get();
}
}

View File

@@ -0,0 +1,89 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Presenters
*/
/**
* Class WPSEO_Redirect_Page_Presenter
*/
class WPSEO_Redirect_Page_Presenter implements WPSEO_Redirect_Presenter {
/**
* Displays the redirect page.
*
* @param array $display Contextual display data.
*
* @return void
*/
public function display( array $display = [] ) {
$current_tab = ! empty( $display['current_tab'] ) ? $display['current_tab'] : '';
$tab_presenter = $this->get_tab_presenter( $current_tab );
$redirect_tabs = $this->navigation_tabs( $current_tab );
include WPSEO_PREMIUM_PATH . 'classes/redirect/views/redirects.php';
}
/**
* Returns a tab presenter.
*
* @param string $tab_to_display The tab that will be shown.
*
* @return WPSEO_Redirect_Tab_Presenter|null Tab presenter instance, or null if invalid tab given.
*/
private function get_tab_presenter( $tab_to_display ) {
$tab_presenter = null;
switch ( $tab_to_display ) {
case WPSEO_Redirect_Formats::PLAIN:
case WPSEO_Redirect_Formats::REGEX:
$tab_presenter = new WPSEO_Redirect_Table_Presenter( $tab_to_display );
break;
case 'settings':
if ( current_user_can( 'wpseo_manage_options' ) ) {
$tab_presenter = new WPSEO_Redirect_Settings_Presenter( $tab_to_display );
}
break;
}
return $tab_presenter;
}
/**
* Returning the anchors html for the tabs
*
* @param string $current_tab The tab that will be active.
*
* @return array {
* Associative array of navigation tabs data.
*
* @type array $tabs Array of $tab_slug => $tab_label pairs.
* @type string $current_tab The currently active tab slug.
* @type string $page_url Base URL of the current page, to append the tab slug to.
* }
*/
private function navigation_tabs( $current_tab ) {
$tabs = $this->get_redirect_formats();
if ( current_user_can( 'wpseo_manage_options' ) ) {
$tabs['settings'] = __( 'Settings', 'wordpress-seo-premium' );
}
return [
'tabs' => $tabs,
'current_tab' => $current_tab,
'page_url' => admin_url( 'admin.php?page=wpseo_redirects&tab=' ),
];
}
/**
* Gets the available redirect formats.
*
* @return array Redirect formats as $slug => $label pairs.
*/
protected function get_redirect_formats() {
$redirect_formats = new WPSEO_Redirect_Formats();
return $redirect_formats->get();
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Presenters
*/
/**
* Represents a presenter for a redirects UI component.
*/
interface WPSEO_Redirect_Presenter {
/**
* Displaying the table URL or regex. Depends on the current active tab.
*
* @param array $display Contextual display data.
*
* @return void
*/
public function display( array $display = [] );
}

View File

@@ -0,0 +1,23 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Presenters
*/
/**
* Presenter for the quick edit
*/
class WPSEO_Redirect_Quick_Edit_Presenter implements WPSEO_Redirect_Presenter {
/**
* Displays the table
*
* @param array $display_data Data to display on the table.
*
* @return void
*/
public function display( array $display_data = [] ) {
require WPSEO_PREMIUM_PATH . 'classes/redirect/views/redirects-quick-edit.php';
}
}

View File

@@ -0,0 +1,68 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Presenters
*/
/**
* Class WPSEO_Redirect_Settings_Presenter
*/
class WPSEO_Redirect_Settings_Presenter extends WPSEO_Redirect_Tab_Presenter {
/**
* Extending the view vars with pre settings key
*
* @param array $passed_vars Optional. View data manually passed. Default empty array.
*
* @return array Contextual variables to pass to the view.
*/
protected function get_view_vars( array $passed_vars = [] ) {
return array_merge(
$passed_vars,
[
'file_path' => WPSEO_Redirect_File_Util::get_file_path(),
'redirect_file' => $this->writable_redirect_file(),
]
);
}
/**
* Check if it is possible to write to the files
*
* @return false|string
*/
private function writable_redirect_file() {
if ( WPSEO_Options::get( 'disable_php_redirect' ) !== 'on' ) {
return false;
}
// Do file checks.
$file_exists = file_exists( WPSEO_Redirect_File_Util::get_file_path() );
if ( WPSEO_Utils::is_apache() ) {
$separate_file = ( WPSEO_Options::get( 'separate_file' ) === 'on' );
if ( $separate_file && $file_exists ) {
return 'apache_include_file';
}
if ( ! $separate_file ) {
// Everything is as expected.
if ( is_writable( WPSEO_Redirect_Htaccess_Util::get_htaccess_file_path() ) ) {
return false;
}
}
return 'cannot_write_htaccess';
}
if ( WPSEO_Utils::is_nginx() ) {
if ( $file_exists ) {
return 'nginx_include_file';
}
return 'cannot_write_file';
}
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Presenters
*/
/**
* Class WPSEO_Redirect_Tab_Presenter.
*/
abstract class WPSEO_Redirect_Tab_Presenter implements WPSEO_Redirect_Presenter {
/**
* The view to be rendered in the tab.
*
* @var string
*/
protected $view;
/**
* Constructor.
*
* Sets the view.
*
* @param string $view The view to display.
*/
public function __construct( $view ) {
$this->view = $view;
}
/**
* Displaying the table URL or regex. Depends on the current active tab.
*
* @param array $display Contextual display data.
*
* @return void
*/
public function display( array $display = [] ) {
$view_vars = $this->get_view_vars( $display );
include WPSEO_PREMIUM_PATH . 'classes/redirect/views/redirects-tab-' . $this->view . '.php';
}
/**
* The method to get the variables for the view. This method should return an array, because this will be extracted.
*
* @param array $passed_vars Optional. View data manually passed. Default empty array.
*
* @return array Contextual variables to pass to the view.
*/
abstract protected function get_view_vars( array $passed_vars = [] );
}

View File

@@ -0,0 +1,70 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Presenters
*/
/**
* Class WPSEO_Redirect_Table_Presenter.
*/
class WPSEO_Redirect_Table_Presenter extends WPSEO_Redirect_Tab_Presenter {
/**
* Gets the variables for the view.
*
* @param array $passed_vars Optional. View data manually passed. Default empty array.
*
* @return array Contextual variables to pass to the view.
*/
protected function get_view_vars( array $passed_vars = [] ) {
$redirect_manager = new WPSEO_Redirect_Manager( $this->view );
return array_merge(
$passed_vars,
[
'redirect_table' => new WPSEO_Redirect_Table(
$this->view,
$this->get_first_column_value(),
$redirect_manager->get_redirects()
),
'origin_from_url' => $this->get_old_url(),
'quick_edit_table' => new WPSEO_Redirect_Quick_Edit_Presenter(),
'form_presenter' => new WPSEO_Redirect_Form_Presenter(
[
'origin_label_value' => $this->get_first_column_value(),
]
),
]
);
}
/**
* Get the old URL from the URL.
*
* @return string The old URL.
*/
private function get_old_url() {
// Check if there's an old URL set.
$old_url = filter_input( INPUT_GET, 'old_url', FILTER_DEFAULT, [ 'default' => '' ] );
if ( $old_url !== '' ) {
return esc_attr( rawurldecode( $old_url ) );
}
return $old_url;
}
/**
* Return the value of the first column based on the table type.
*
* @return string The value of the first column.
*/
private function get_first_column_value() {
if ( $this->view === 'regex' ) {
return __( 'Regular Expression', 'wordpress-seo-premium' );
}
return __( 'Old URL', 'wordpress-seo-premium' );
}
}

View File

@@ -0,0 +1,200 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Class WPSEO_Redirect_Ajax.
*/
class WPSEO_Redirect_Ajax {
/**
* Instance of the WPSEO_Redirect_Manager instance.
*
* @var WPSEO_Redirect_Manager
*/
private $redirect_manager;
/**
* Format of the redirect, might be plain or regex.
*
* @var string
*/
private $redirect_format;
/**
* Setting up the object by instantiate the redirect manager and setting the hooks.
*
* @param string $redirect_format The redirects format.
*/
public function __construct( $redirect_format ) {
$this->redirect_manager = new WPSEO_Redirect_Manager( $redirect_format );
$this->redirect_format = $redirect_format;
$this->set_hooks( $redirect_format );
}
/**
* Function that handles the AJAX 'wpseo_add_redirect' action.
*/
public function ajax_add_redirect() {
$this->valid_ajax_check();
// Save the redirect.
$redirect = $this->get_redirect_from_post( 'redirect' );
$this->validate( $redirect );
// The method always returns the added redirect.
if ( $this->redirect_manager->create_redirect( $redirect ) ) {
$response = [
'origin' => $redirect->get_origin(),
'target' => $redirect->get_target(),
'type' => $redirect->get_type(),
'info' => [
'hasTrailingSlash' => WPSEO_Redirect_Util::requires_trailing_slash( $redirect->get_target() ),
'isTargetRelative' => WPSEO_Redirect_Util::is_relative_url( $redirect->get_target() ),
],
];
}
else {
// Set the value error.
$error = [
'type' => 'error',
'message' => __( 'Unknown error. Failed to create redirect.', 'wordpress-seo-premium' ),
];
$response = [ 'error' => $error ];
}
// Response.
// phpcs:ignore WordPress.Security.EscapeOutput -- WPCS bug/methods can't be whitelisted yet.
wp_die( WPSEO_Utils::format_json_encode( $response ) );
}
/**
* Function that handles the AJAX 'wpseo_update_redirect' action.
*/
public function ajax_update_redirect() {
$this->valid_ajax_check();
$current_redirect = $this->get_redirect_from_post( 'old_redirect' );
$new_redirect = $this->get_redirect_from_post( 'new_redirect' );
$this->validate( $new_redirect, $current_redirect );
// The method always returns the added redirect.
if ( $this->redirect_manager->update_redirect( $current_redirect, $new_redirect ) ) {
$response = [
'origin' => $new_redirect->get_origin(),
'target' => $new_redirect->get_target(),
'type' => $new_redirect->get_type(),
];
}
else {
// Set the value error.
$error = [
'type' => 'error',
'message' => __( 'Unknown error. Failed to update redirect.', 'wordpress-seo-premium' ),
];
$response = [ 'error' => $error ];
}
// Response.
// phpcs:ignore WordPress.Security.EscapeOutput -- WPCS bug/methods can't be whitelisted yet.
wp_die( WPSEO_Utils::format_json_encode( $response ) );
}
/**
* Run the validation.
*
* @param WPSEO_Redirect $redirect The redirect to save.
* @param WPSEO_Redirect|null $current_redirect The current redirect.
*/
private function validate( WPSEO_Redirect $redirect, WPSEO_Redirect $current_redirect = null ) {
$validator = new WPSEO_Redirect_Validator();
if ( $validator->validate( $redirect, $current_redirect ) === true ) {
return;
}
$ignore_warning = filter_input( INPUT_POST, 'ignore_warning' );
$error = $validator->get_error();
if ( $error->get_type() === 'error' || ( $error->get_type() === 'warning' && $ignore_warning === 'false' ) ) {
wp_die(
// phpcs:ignore WordPress.Security.EscapeOutput -- WPCS bug/methods can't be whitelisted yet.
WPSEO_Utils::format_json_encode( [ 'error' => $error->to_array() ] )
);
}
}
/**
* Setting the AJAX hooks.
*
* @param string $hook_suffix The piece that will be stitched after the hooknames.
*/
private function set_hooks( $hook_suffix ) {
// Add the new redirect.
add_action( 'wp_ajax_wpseo_add_redirect_' . $hook_suffix, [ $this, 'ajax_add_redirect' ] );
// Update an existing redirect.
add_action( 'wp_ajax_wpseo_update_redirect_' . $hook_suffix, [ $this, 'ajax_update_redirect' ] );
// Add URL response code check AJAX.
if ( ! has_action( 'wp_ajax_wpseo_check_url' ) ) {
add_action( 'wp_ajax_wpseo_check_url', [ $this, 'ajax_check_url' ] );
}
}
/**
* Check if the posted nonce is valid and if the user has the needed rights.
*/
private function valid_ajax_check() {
// Check nonce.
check_ajax_referer( 'wpseo-redirects-ajax-security', 'ajax_nonce' );
$this->permission_check();
}
/**
* Checks whether the current user is allowed to do what he's doing.
*/
private function permission_check() {
if ( ! current_user_can( 'edit_posts' ) ) {
wp_die( '0' );
}
}
/**
* Get the redirect from the post values.
*
* @param string $post_value The key where the post values are located in the $_POST.
*
* @return WPSEO_Redirect
*/
private function get_redirect_from_post( $post_value ) {
$post_values = filter_input( INPUT_POST, $post_value, FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
return new WPSEO_Redirect(
$this->sanitize_url( $post_values['origin'] ),
$this->sanitize_url( $post_values['target'] ),
urldecode( $post_values['type'] ),
$this->redirect_format
);
}
/**
* Sanitize the URL for displaying on the window.
*
* @param string $url The URL to sanitize.
*
* @return string
*/
private function sanitize_url( $url ) {
return trim( htmlspecialchars_decode( rawurldecode( $url ) ) );
}
}

View File

@@ -0,0 +1,120 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Class WPSEO_Redirect_File_Manager
*/
class WPSEO_Redirect_File_Util {
/**
* Get the full path to the WPSEO redirect directory
*
* @return string
*/
public static function get_dir() {
$wp_upload_dir = wp_upload_dir();
return $wp_upload_dir['basedir'] . '/wpseo-redirects';
}
/**
* Get the full path to the redirect file
*
* @return string
*/
public static function get_file_path() {
return self::get_dir() . '/.redirects';
}
/**
* Function that creates the WPSEO redirect directory
*/
public static function create_upload_dir() {
$basedir = self::get_dir();
// Create the Redirect file dir.
if ( ! wp_mkdir_p( $basedir ) ) {
Yoast_Notification_Center::get()->add_notification(
new Yoast_Notification(
/* translators: %s expands to the file path that we tried to write to */
sprintf( __( "We're unable to create the directory %s", 'wordpress-seo-premium' ), $basedir ),
[ 'type' => 'error' ]
)
);
return;
}
// Create the .htaccess file.
if ( ! file_exists( $basedir . '/.htaccess' ) ) {
self::write_file( $basedir . '/.htaccess', "Options -Indexes\ndeny from all" );
}
// Create an empty index.php file.
if ( ! file_exists( $basedir . '/index.php' ) ) {
self::write_file( $basedir . '/index.php', '<?php' . PHP_EOL . '// Silence is golden.' );
}
// Create an empty redirect file.
if ( ! file_exists( self::get_file_path() ) ) {
self::write_file( self::get_file_path(), '' );
}
}
/**
* Wrapper method for file_put_contents. Catches the result, if result is false add notification.
*
* @param string $file_path The path to write the content to.
* @param string $file_content The content that will be saved.
*
* @return bool True on successful file write.
*/
public static function write_file( $file_path, $file_content ) {
$has_written = false;
if ( is_writable( dirname( $file_path ) ) ) {
$has_written = file_put_contents( $file_path, $file_content );
}
if ( $has_written === false ) {
Yoast_Notification_Center::get()->add_notification(
new Yoast_Notification(
/* translators: %s expands to the file path that we tried to write to */
sprintf( __( "We're unable to write data to the file %s", 'wordpress-seo-premium' ), $file_path ),
[ 'type' => 'error' ]
)
);
return false;
}
return true;
}
/**
* Getting the object which will save the redirects file
*
* @param string $separate_file Saving the redirects in an separate apache file.
*
* @return WPSEO_Redirect_File_Exporter|null
*/
public static function get_file_exporter( $separate_file ) {
// Create the correct file object.
if ( WPSEO_Utils::is_apache() ) {
if ( $separate_file === 'on' ) {
return new WPSEO_Redirect_Apache_Exporter();
}
return new WPSEO_Redirect_Htaccess_Exporter();
}
if ( WPSEO_Utils::is_nginx() ) {
return new WPSEO_Redirect_Nginx_Exporter();
}
return null;
}
}

View File

@@ -0,0 +1,40 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Class representing a list of redirect formats.
*/
class WPSEO_Redirect_Formats {
const PLAIN = 'plain';
const REGEX = 'regex';
/**
* Returns the redirect formats.
*
* @return string[] Array with the redirect formats.
*/
public function get() {
return [
self::PLAIN => __( 'Redirects', 'wordpress-seo-premium' ),
self::REGEX => __( 'Regex Redirects', 'wordpress-seo-premium' ),
];
}
/**
* Checks whether the given value is a valid redirect format.
*
* @param string $value Value to check.
*
* @return bool True if a redirect format, false otherwise.
*/
public function has( $value ) {
$formats = $this->get();
return isset( $formats[ $value ] );
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Class for formatting redirects.
*/
class WPSEO_Redirect_Formatter {
/**
* Formats a redirect into a executable redirect.
*
* @param WPSEO_Redirect $redirect The original redirect.
*
* @return WPSEO_Executable_Redirect The executable redirect.
*/
public function format( WPSEO_Redirect $redirect ) {
return new WPSEO_Executable_Redirect(
$redirect->get_origin(),
$redirect->get_target(),
$redirect->get_type(),
$redirect->get_format()
);
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Class WPSEO_Redirect_Htaccess
*/
class WPSEO_Redirect_Htaccess_Util {
/**
* Clear the WPSEO added entries added in the .htaccess file
*/
public static function clear_htaccess_entries() {
$htaccess = '';
if ( file_exists( self::get_htaccess_file_path() ) ) {
$htaccess = file_get_contents( self::get_htaccess_file_path() );
}
$cleaned = preg_replace( '`# BEGIN YOAST REDIRECTS.*# END YOAST REDIRECTS' . PHP_EOL . '`is', '', $htaccess );
// If nothing changed, don't even try to save it.
if ( $cleaned === $htaccess ) {
return;
}
WPSEO_Redirect_File_Util::write_file( self::get_htaccess_file_path(), $cleaned );
}
/**
* Get the full path to the .htaccess file
*
* @return string
*/
public static function get_htaccess_file_path() {
if ( ! function_exists( 'get_home_path' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
return get_home_path() . '.htaccess';
}
}

View File

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

View File

@@ -0,0 +1,91 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* This exporter class will import.
*/
class WPSEO_Redirect_Importer {
/**
* Instance of the redirect option.
*
* @var WPSEO_Redirect_Option
*/
protected $redirect_option;
/**
* Total amount of successfully imported redirects
*
* @var int
*/
protected $total_imported = 0;
/**
* WPSEO_Redirect_Importer constructor.
*
* @codeCoverageIgnore
*
* @param WPSEO_Redirect_Option|null $redirect_option The redirect option.
*/
public function __construct( $redirect_option = null ) {
if ( ! $redirect_option ) {
$redirect_option = new WPSEO_Redirect_Option();
}
$this->redirect_option = $redirect_option;
}
/**
* Imports the redirects and retrieves the import statistics.
*
* @param WPSEO_Redirect[] $redirects The redirects to import.
*
* @return int[] The import statistics.
*/
public function import( array $redirects ) {
array_walk( $redirects, [ $this, 'add_redirect' ] );
if ( $this->total_imported > 0 ) {
$this->save_import();
}
return [
'total_redirects' => count( $redirects ),
'total_imported' => $this->total_imported,
];
}
/**
* Saves the redirects to the database and exports them to the necessary configuration file.
*
* @codeCoverageIgnore Because it contains dependencies
*
* @return void
*/
protected function save_import() {
$this->redirect_option->save();
// Export the redirects to .htaccess, Apache or NGinx configuration files depending on plugin settings.
$redirect_manager = new WPSEO_Redirect_Manager();
$redirect_manager->export_redirects();
}
/**
* Adds a redirect to the option.
*
* @param WPSEO_Redirect $redirect The redirect to add.
*
* @return void
*/
protected function add_redirect( $redirect ) {
if ( ! $this->redirect_option->add( $redirect ) ) {
return;
}
++$this->total_imported;
}
}

View File

@@ -0,0 +1,193 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Class WPSEO_Redirect_Manager.
*/
class WPSEO_Redirect_Manager {
/**
* Model object to handle the redirects.
*
* @var WPSEO_Redirect_Option
*/
protected $redirect_option;
/**
* The redirect format, this might be plain or regex.
*
* @var string
*/
protected $redirect_format;
/**
* List of redirect exporters.
*
* @var WPSEO_Redirect_Exporter[]
*/
protected $exporters;
/**
* Returns the default exporters.
*
* @return WPSEO_Redirect_Exporter[]
*/
public static function default_exporters() {
$exporters = [ new WPSEO_Redirect_Option_Exporter() ];
if ( WPSEO_Options::get( 'disable_php_redirect' ) === 'on' ) {
$file_exporter = WPSEO_Redirect_File_Util::get_file_exporter( WPSEO_Options::get( 'separate_file' ) );
if ( isset( $file_exporter ) && $file_exporter instanceof WPSEO_Redirect_File_Exporter ) {
$exporters[] = $file_exporter;
}
}
return $exporters;
}
/**
* Setting the property with the redirects.
*
* @param string $redirect_format The format for the redirects.
* @param WPSEO_Redirect_Exporter[]|null $exporters The exporters used to save redirects in files.
* @param WPSEO_Redirect_Option|null $option Model object to handle the redirects.
*/
public function __construct( $redirect_format = WPSEO_Redirect_Formats::PLAIN, $exporters = null, WPSEO_Redirect_Option $option = null ) {
if ( $option === null ) {
$option = new WPSEO_Redirect_Option();
}
$this->redirect_option = $option;
$this->redirect_format = $redirect_format;
$this->exporters = $exporters;
}
/**
* Get the redirects.
*
* @return WPSEO_Redirect[]
*/
public function get_redirects() {
// Filter the redirect for the current format.
return array_filter( $this->redirect_option->get_all(), [ $this, 'filter_redirects_by_format' ] );
}
/**
* Returns all redirects.
*
* @return WPSEO_Redirect[]
*/
public function get_all_redirects() {
return $this->redirect_option->get_all();
}
/**
* Export the redirects to the specified sources.
*/
public function export_redirects() {
$redirects = $this->redirect_option->get_all();
$exporters = $this->exporters;
if ( ! $exporters ) {
$exporters = self::default_exporters();
}
foreach ( $exporters as $exporter ) {
$exporter->export( $redirects );
}
}
/**
* Create a new redirect.
*
* @param WPSEO_Redirect $redirect The redirect object to add.
*
* @return bool
*/
public function create_redirect( WPSEO_Redirect $redirect ) {
if ( $this->redirect_option->add( $redirect ) ) {
$this->save_redirects();
return true;
}
return false;
}
/**
* Save the redirect.
*
* @param WPSEO_Redirect $current_redirect The old redirect, the value is a key in the redirects array.
* @param WPSEO_Redirect $redirect New redirect object.
*
* @return bool
*/
public function update_redirect( WPSEO_Redirect $current_redirect, WPSEO_Redirect $redirect ) {
if ( $this->redirect_option->update( $current_redirect, $redirect ) ) {
$this->save_redirects();
return true;
}
return false;
}
/**
* Delete the redirects.
*
* @param WPSEO_Redirect[] $delete_redirects Array with the redirects to remove.
*
* @return bool
*/
public function delete_redirects( $delete_redirects ) {
$deleted = false;
foreach ( $delete_redirects as $delete_redirect ) {
if ( $this->redirect_option->delete( $delete_redirect ) ) {
$deleted = true;
}
}
if ( $deleted === true ) {
$this->save_redirects();
}
return $deleted;
}
/**
* Returns the redirect when it's found, otherwise it will return false.
*
* @param string $origin The origin to search for.
*
* @return bool|WPSEO_Redirect
*/
public function get_redirect( $origin ) {
return $this->redirect_option->get( $origin );
}
/**
* This method will save the redirect option and if necessary the redirect file.
*/
public function save_redirects() {
// Update the database option.
$this->redirect_option->save();
// Save the redirect file.
$this->export_redirects();
}
/**
* Filter the redirects that don't match the needed format.
*
* @param WPSEO_Redirect $redirect The redirect to filter.
*
* @return bool
*/
private function filter_redirects_by_format( WPSEO_Redirect $redirect ) {
return $redirect->get_format() === $this->redirect_format;
}
}

View File

@@ -0,0 +1,314 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Class handling the redirect options.
*/
class WPSEO_Redirect_Option {
/**
* The plain redirect option before 3.1.
*/
const OLD_OPTION_PLAIN = 'wpseo-premium-redirects';
/**
* The regex redirect option before 3.1.
*/
const OLD_OPTION_REGEX = 'wpseo-premium-redirects-regex';
/**
* The option which contains the redirects base.
*
* @since 3.1
*/
const OPTION = 'wpseo-premium-redirects-base';
/**
* The option which contains the plain redirects.
*
* @since 3.1
*/
const OPTION_PLAIN = 'wpseo-premium-redirects-export-plain';
/**
* The option which contains the regex redirects.
*
* @since 3.1
*/
const OPTION_REGEX = 'wpseo-premium-redirects-export-regex';
/**
* List of redirects.
*
* @var WPSEO_Redirect[]
*/
private $redirects = [];
/**
* Constructor.
*
* @param bool $retrieve_redirects Whether to retrieve the redirects on construction.
*/
public function __construct( $retrieve_redirects = true ) {
if ( $retrieve_redirects ) {
$this->redirects = $this->get_all();
}
}
/**
* Getting the array with all the redirects.
*
* @return WPSEO_Redirect[]
*/
public function get_all() {
$redirects = $this->get_from_option();
array_walk( $redirects, [ $this, 'map_option_to_object' ] );
return $redirects;
}
/**
* Check if the old redirect doesn't exist already, if not it will be added.
*
* @param WPSEO_Redirect $redirect The redirect object to save.
*
* @return bool
*/
public function add( WPSEO_Redirect $redirect ) {
if ( $this->search( $redirect->get_origin() ) === false ) {
$this->run_redirects_modified_action( $redirect );
$this->redirects[] = $redirect;
return true;
}
return false;
}
/**
* Check if the $current_redirect exists and remove it if so.
*
* @param WPSEO_Redirect $current_redirect The current redirect value.
* @param WPSEO_Redirect $redirect The redirect object to save.
*
* @return bool
*/
public function update( WPSEO_Redirect $current_redirect, WPSEO_Redirect $redirect ) {
$found = $this->search( $current_redirect->get_origin() );
if ( $found !== false ) {
$this->run_redirects_modified_action( $redirect );
$this->redirects[ $found ] = $redirect;
return true;
}
return false;
}
/**
* Deletes the given redirect from the array.
*
* @param WPSEO_Redirect $current_redirect The redirect that will be removed.
*
* @return bool
*/
public function delete( WPSEO_Redirect $current_redirect ) {
$found = $this->search( $current_redirect->get_origin() );
if ( $found !== false ) {
$this->run_redirects_modified_action( $current_redirect );
unset( $this->redirects[ $found ] );
return true;
}
return false;
}
/**
* Get a redirect from the array.
*
* @param string $origin The redirects origin to search for.
*
* @return WPSEO_Redirect|bool
*/
public function get( $origin ) {
$found = $this->search( $origin );
if ( $found !== false ) {
return $this->redirects[ $found ];
}
return false;
}
/**
* Check if the $origin already exists as a key in the array.
*
* @param string $origin The redirect to search for.
*
* @return int|bool
*/
public function search( $origin ) {
foreach ( $this->redirects as $redirect_key => $redirect ) {
if ( $redirect->origin_is( $origin ) ) {
return $redirect_key;
}
}
return false;
}
/**
* Saving the redirects.
*
* @param bool $retry_upgrade Whether or not to retry the 3.1 upgrade. Used to prevent infinite recursion.
*/
public function save( $retry_upgrade = true ) {
$redirects = $this->redirects;
// Retry the 3.1 upgrade routine to make sure we're always dealing with valid redirects.
$upgrade_manager = new WPSEO_Upgrade_Manager();
if ( $retry_upgrade && $upgrade_manager->should_retry_upgrade_31() ) {
$upgrade_manager->retry_upgrade_31( true );
$redirects = array_merge( $redirects, $this->get_all() );
}
array_walk( $redirects, [ $this, 'map_object_to_option' ] );
/**
* Filter: 'wpseo_premium_save_redirects' - can be used to filter the redirects before saving.
*
* @deprecated 12.9.0. Use the {@see 'Yoast\WP\SEO\save_redirects'} filter instead.
*
* @api array $redirects
*/
$redirects = apply_filters_deprecated(
'wpseo_premium_save_redirects',
[ $redirects ],
'YoastSEO Premium 12.9.0',
'Yoast\WP\SEO\save_redirects'
);
/**
* Filter: 'Yoast\WP\SEO\save_redirects' - can be used to filter the redirects before saving.
*
* Note: This is a Premium plugin-only hook.
*
* @since 12.9.0
*
* @api array $redirects
*/
$redirects = apply_filters( 'Yoast\WP\SEO\save_redirects', $redirects );
// Update the database option.
update_option( self::OPTION, $redirects, false );
}
/**
* Setting the redirects property.
*
* @param string $option_name The target option name.
*
* @return array
*/
public function get_from_option( $option_name = self::OPTION ) {
/**
* Filter: 'wpseo_premium_get_redirects' - can be used to filter the redirects on option retrieval.
*
* @deprecated 12.9.0. Use the {@see 'Yoast\WP\SEO\get_redirects'} filter instead.
*
* @api array $redirects
*/
$redirects = apply_filters_deprecated(
'wpseo_premium_get_redirects',
[ get_option( $option_name ) ],
'YoastSEO Premium 12.9.0',
'Yoast\WP\SEO\get_redirects'
);
/**
* Filter: 'Yoast\WP\SEO\get_redirects' - can be used to filter the redirects on option retrieval.
*
* Note: This is a Premium plugin-only hook.
*
* @since 12.9.0
*
* @api array $redirects
*/
$redirects = apply_filters( 'Yoast\WP\SEO\get_redirects', $redirects );
if ( ! is_array( $redirects ) ) {
$redirects = [];
}
return $redirects;
}
/**
* Runs the redirects modified hook with the altered redirect as input.
*
* @param WPSEO_Redirect $redirect The redirect that has been altered.
*
* @return void
*/
protected function run_redirects_modified_action( WPSEO_Redirect $redirect ) {
/**
* Filter: wpseo_premium_redirects_modified - Allow developers to run actions when the redirects are modified.
*
* @deprecated 12.9.0. Use the {@see 'Yoast\WP\SEO\redirects_modified'} action instead.
*
* @api string $origin The redirect origin.
* @param string $target The redirect target.
* @param int $type The redirect type (301, 404, 410, etc).
*/
do_action_deprecated(
'wpseo_premium_redirects_modified',
[ $redirect->get_origin(), $redirect->get_target(), $redirect->get_type() ],
'YoastSEO Premium 12.9.0',
'Yoast\WP\SEO\redirects_modified'
);
/**
* Filter: Yoast\WP\SEO\redirects_modified - Allow developers to run actions when the redirects are modified.
*
* Note: This is a Premium plugin-only hook.
*
* @since 12.9.0
*
* @api string $origin The redirect origin.
* @param string $target The redirect target.
* @param int $type The redirect type (301, 404, 410, etc).
*/
do_action( 'Yoast\WP\SEO\redirects_modified', $redirect->get_origin(), $redirect->get_target(), $redirect->get_type() );
}
/**
* Maps the array values to a redirect object.
*
* @param array $redirect_values The data for the redirect option.
*/
private function map_option_to_object( array &$redirect_values ) {
$redirect_values = new WPSEO_Redirect( $redirect_values['origin'], $redirect_values['url'], $redirect_values['type'], $redirect_values['format'] );
}
/**
* Maps a redirect object to an array option.
*
* @param WPSEO_Redirect $redirect The redirect to map.
*/
private function map_object_to_option( WPSEO_Redirect &$redirect ) {
$redirect = [
'origin' => $redirect->get_origin(),
'url' => $redirect->get_target(),
'type' => $redirect->get_type(),
'format' => $redirect->get_format(),
];
}
}

View File

@@ -0,0 +1,339 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Class WPSEO_Redirect_Page.
*/
class WPSEO_Redirect_Page {
/**
* Constructing redirect module.
*/
public function __construct() {
if ( is_admin() ) {
$this->initialize_admin();
}
// Only initialize the ajax for all tabs except settings.
if ( wp_doing_ajax() ) {
$this->initialize_ajax();
}
}
/**
* Display the presenter.
*/
public function display() {
$display_args = [ 'current_tab' => $this->get_current_tab() ];
$redirect_presenter = new WPSEO_Redirect_Page_Presenter();
$redirect_presenter->display( $display_args );
}
/**
* Catches possible posted filter values and redirects it to a GET-request.
*
* It catches:
* A search post.
* A redirect-type filter.
*/
public function list_table_search() {
$options = [ 'options' => [ 'default' => '' ] ];
$url = filter_input( INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_URL, $options );
if ( empty( $url ) && isset( $_SERVER['REQUEST_URI'] ) ) {
$url = filter_var( $_SERVER['REQUEST_URI'], FILTER_SANITIZE_URL, $options );
}
$new_url = $this->extract_redirect_type_from_url( $url );
$new_url = $this->extract_search_string_from_url( $new_url );
if ( $url !== $new_url ) {
// Do the redirect.
wp_safe_redirect( $new_url );
exit;
}
}
/**
* Extracts the redirect type from the passed URL.
*
* @param string $url The URL to try and extract the redirect type from.
*
* @return string The newly formatted URL. Returns original URL if filter is null.
*/
protected function extract_redirect_type_from_url( $url ) {
$filter = filter_input( INPUT_POST, 'redirect-type' );
if ( $filter === null ) {
return $url;
}
$new_url = remove_query_arg( 'redirect-type', $url );
if ( $filter !== '0' ) {
$new_url = add_query_arg( 'redirect-type', rawurlencode( $filter ), $new_url );
}
return $new_url;
}
/**
* Extracts the search string from the passed URL.
*
* @param string $url The URL to try and extract the search string from.
*
* @return string The newly formatted URL. Returns original URL if search string is null.
*/
protected function extract_search_string_from_url( $url ) {
$search_string = filter_input( INPUT_POST, 's' );
if ( $search_string === null ) {
return $url;
}
$new_url = remove_query_arg( 's', $url );
if ( $search_string !== '' ) {
$new_url = add_query_arg( 's', rawurlencode( $search_string ), $new_url );
}
return $new_url;
}
/**
* Load the admin redirects scripts.
*/
public function enqueue_assets() {
$asset_manager = new WPSEO_Admin_Asset_Manager();
$version = $asset_manager->flatten_version( WPSEO_PREMIUM_VERSION );
$dependencies = [
'jquery',
'jquery-ui-dialog',
'wp-util',
'underscore',
'yoast-seo-premium-commons',
'wp-api',
'wp-api-fetch',
];
wp_enqueue_script(
'wp-seo-premium-admin-redirects',
plugin_dir_url( WPSEO_PREMIUM_FILE )
. 'assets/js/dist/wp-seo-premium-admin-redirects-' . $version . WPSEO_CSSJS_SUFFIX . '.js',
$dependencies,
WPSEO_PREMIUM_VERSION
);
wp_localize_script( 'wp-seo-premium-admin-redirects', 'wpseoPremiumStrings', WPSEO_Premium_Javascript_Strings::strings() );
wp_localize_script( 'wp-seo-premium-admin-redirects', 'wpseoUserLocale', [ 'code' => substr( \get_user_locale(), 0, 2 ) ] );
wp_enqueue_style( 'wpseo-premium-redirects', plugin_dir_url( WPSEO_PREMIUM_FILE ) . 'assets/css/dist/premium-redirects-' . $version . '.css', [], WPSEO_PREMIUM_VERSION );
wp_enqueue_style( 'wp-jquery-ui-dialog' );
$screen_option_args = [
'label' => __( 'Redirects per page', 'wordpress-seo-premium' ),
'default' => 25,
'option' => 'redirects_per_page',
];
add_screen_option( 'per_page', $screen_option_args );
}
/**
* Catch redirects_per_page.
*
* @param string $status Unused.
* @param string $option The option name where the value is set for.
* @param string $value The new value for the screen option.
*
* @return string|void
*/
public function set_screen_option( $status, $option, $value ) {
if ( $option === 'redirects_per_page' ) {
return $value;
}
}
/**
* Hook that runs after the 'wpseo_redirect' option is updated.
*
* @param array $old_value Unused.
* @param array $value The new saved values.
*/
public function save_redirect_files( $old_value, $value ) {
$is_php = ( empty( $value['disable_php_redirect'] ) || $value['disable_php_redirect'] !== 'on' );
$was_separate_file = ( ! empty( $old_value['separate_file'] ) && $old_value['separate_file'] === 'on' );
$is_separate_file = ( ! empty( $value['separate_file'] ) && $value['separate_file'] === 'on' );
// Check if the 'disable_php_redirect' option set to true/on.
if ( ! $is_php ) {
// The 'disable_php_redirect' option is set to true(on) so we need to generate a file.
// The Redirect Manager will figure out what file needs to be created.
$redirect_manager = new WPSEO_Redirect_Manager();
$redirect_manager->export_redirects();
}
// Check if we need to remove the .htaccess redirect entries.
if ( WPSEO_Utils::is_apache() ) {
if ( $is_php || ( ! $was_separate_file && $is_separate_file ) ) {
// Remove the apache redirect entries.
WPSEO_Redirect_Htaccess_Util::clear_htaccess_entries();
}
if ( $is_php || ( $was_separate_file && ! $is_separate_file ) ) {
// Remove the apache separate file redirect entries.
WPSEO_Redirect_File_Util::write_file( WPSEO_Redirect_File_Util::get_file_path(), '' );
}
}
if ( WPSEO_Utils::is_nginx() && $is_php ) {
// Remove the nginx redirect entries.
$this->clear_nginx_redirects();
}
}
/**
* The server should always be apache. And the php redirects have to be enabled or in case of a separate
* file it should be disabled.
*
* @param bool $disable_php_redirect Are the php redirects disabled.
* @param bool $separate_file Value of the separate file.
*
* @return bool
*/
private function remove_htaccess_entries( $disable_php_redirect, $separate_file ) {
return ( WPSEO_Utils::is_apache() && ( ! $disable_php_redirect || ( $disable_php_redirect && $separate_file ) ) );
}
/**
* Clears the redirects from the nginx config.
*/
private function clear_nginx_redirects() {
$redirect_file = WPSEO_Redirect_File_Util::get_file_path();
if ( is_writable( $redirect_file ) ) {
WPSEO_Redirect_File_Util::write_file( $redirect_file, '' );
}
}
/**
* Initialize admin hooks.
*/
private function initialize_admin() {
$this->fetch_bulk_action();
// Check if we need to save files after updating options.
add_action( 'update_option_wpseo_redirect', [ $this, 'save_redirect_files' ], 10, 2 );
// Convert post into get on search and loading the page scripts.
if ( filter_input( INPUT_GET, 'page' ) === 'wpseo_redirects' ) {
$upgrade_manager = new WPSEO_Upgrade_Manager();
$upgrade_manager->retry_upgrade_31();
add_action( 'admin_init', [ $this, 'list_table_search' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
add_filter( 'set-screen-option', [ $this, 'set_screen_option' ], 11, 3 );
}
}
/**
* Initialize the AJAX redirect files.
*/
private function initialize_ajax() {
// Normal Redirect AJAX.
new WPSEO_Redirect_Ajax( WPSEO_Redirect_Formats::PLAIN );
// Regex Redirect AJAX.
new WPSEO_Redirect_Ajax( WPSEO_Redirect_Formats::REGEX );
}
/**
* Getting the current active tab.
*
* @return string
*/
private function get_current_tab() {
static $current_tab;
if ( $current_tab === null ) {
$current_tab = filter_input(
INPUT_GET,
'tab',
FILTER_VALIDATE_REGEXP,
[
'options' => [
'default' => WPSEO_Redirect_Formats::PLAIN,
'regexp' => '/^(' . WPSEO_Redirect_Formats::PLAIN . '|' . WPSEO_Redirect_Formats::REGEX . '|settings)$/',
],
]
);
}
return $current_tab;
}
/**
* Setting redirect manager, based on the current active tab.
*
* @return WPSEO_Redirect_Manager
*/
private function get_redirect_manager() {
static $redirect_manager;
if ( $redirect_manager === null ) {
$redirects_format = WPSEO_Redirect_Formats::PLAIN;
if ( $this->get_current_tab() === WPSEO_Redirect_Formats::REGEX ) {
$redirects_format = WPSEO_Redirect_Formats::REGEX;
}
$redirect_manager = new WPSEO_Redirect_Manager( $redirects_format );
}
return $redirect_manager;
}
/**
* Fetches the bulk action for removing redirects.
*
* @return void
*/
private function fetch_bulk_action() {
if ( wp_verify_nonce( filter_input( INPUT_POST, 'wpseo_redirects_ajax_nonce' ), 'wpseo-redirects-ajax-security' ) ) {
if ( filter_input( INPUT_POST, 'action' ) === 'delete' || filter_input( INPUT_POST, 'action2' ) === 'delete' ) {
$bulk_delete = filter_input( INPUT_POST, 'wpseo_redirects_bulk_delete', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
$redirects = [];
foreach ( $bulk_delete as $origin ) {
$redirect = $this->get_redirect_manager()->get_redirect( $origin );
if ( $redirect !== false ) {
$redirects[] = $redirect;
}
}
$this->get_redirect_manager()->delete_redirects( $redirects );
}
}
}
/* ********************* DEPRECATED METHODS ********************* */
/**
* Get the Yoast SEO options.
*
* @deprecated 12.9
* @codeCoverageIgnore
*
* @return array
*/
public static function get_options() {
_deprecated_function( __METHOD__, 'WPSEO 12.9' );
return [];
}
}

View File

@@ -0,0 +1,85 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirects
*/
/**
* Represents the filter for removing redirected entries from the sitemaps.
*/
class WPSEO_Redirect_Sitemap_Filter implements WPSEO_WordPress_Integration {
/**
* URL to the homepage.
*
* @var string
*/
protected $home_url;
/**
* Constructs the object.
*
* @param string $home_url The home url.
*/
public function __construct( $home_url ) {
$this->home_url = $home_url;
}
/**
* Registers the hooks.
*
* @return void
*/
public function register_hooks() {
add_filter( 'wpseo_sitemap_entry', [ $this, 'filter_sitemap_entry' ] );
add_action( 'Yoast\WP\SEO\redirects_modified', [ $this, 'clear_sitemap_cache' ] );
}
/**
* Prevents a redirected URL from being added to the sitemap.
*
* @param array $url The url data.
*
* @return bool|array False when entry will be redirected.
*/
public function filter_sitemap_entry( $url ) {
if ( empty( $url['loc'] ) ) {
return $url;
}
$entry_location = str_replace( $this->home_url, '', $url['loc'] );
if ( $this->is_redirect( $entry_location ) !== false ) {
return false;
}
return $url;
}
/**
* Clears the sitemap cache.
*
* @return void
*/
public function clear_sitemap_cache() {
WPSEO_Sitemaps_Cache::clear();
}
/**
* Checks if the given entry location already exists as a redirect.
*
* @param string $entry_location The entry location.
*
* @return bool Whether the entry location exists as a redirect.
*/
protected function is_redirect( $entry_location ) {
static $redirects = null;
if ( $redirects === null ) {
$redirects = new WPSEO_Redirect_Option();
}
return $redirects->search( $entry_location ) !== false;
}
}

View File

@@ -0,0 +1,434 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
if ( ! class_exists( 'WP_List_Table' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
/**
* Class WPSEO_Redirect_Table.
*/
class WPSEO_Redirect_Table extends WP_List_Table {
/**
* List of all redirects.
*
* @var WPSEO_Redirect[]
*/
public $items;
/**
* List containing redirect filter parameters.
*
* @var array
*/
private $filter = [
'redirect_type' => null,
'search_string' => null,
];
/**
* The name of the first column.
*
* @var string
*/
private $current_column;
/**
* The primary column.
*
* @var string
*/
private $primary_column = 'type';
/**
* WPSEO_Redirect_Table constructor.
*
* @param array|string $type Type of the redirects that is opened.
* @param string $current_column The value of the first column.
* @param WPSEO_Redirect[] $redirects The redirects.
*/
public function __construct( $type, $current_column, $redirects ) {
parent::__construct( [ 'plural' => $type ] );
$this->current_column = $current_column;
$this->set_items( $redirects );
add_filter( 'list_table_primary_column', [ $this, 'redirect_list_table_primary_column' ], 10, 2 );
}
/**
* Renders the extra table navigation.
*
* @param string $which Which tablenav is called.
*
* @return void
*/
public function extra_tablenav( $which ) {
if ( $which !== 'top' ) {
return;
}
$selected = filter_input( INPUT_GET, 'redirect-type' );
if ( ! $selected ) {
$selected = 0;
}
?>
<div class="alignleft actions">
<label for="filter-by-redirect" class="screen-reader-text"><?php esc_html_e( 'Filter by redirect type', 'wordpress-seo-premium' ); ?></label>
<select name="redirect-type" id="filter-by-redirect">
<option<?php selected( $selected, 0 ); ?> value="0"><?php esc_html_e( 'All redirect types', 'wordpress-seo-premium' ); ?></option>
<?php
$redirect_types = new WPSEO_Redirect_Types();
foreach ( $redirect_types->get() as $http_code => $redirect_type ) {
printf(
"<option %s value='%s'>%s</option>\n",
selected( $selected, $http_code, false ),
esc_attr( $http_code ),
esc_html( $redirect_type )
);
}
?>
</select>
<?php submit_button( __( 'Filter', 'wordpress-seo-premium' ), '', 'filter_action', false, [ 'id' => 'post-query-submit' ] ); ?>
</div>
<?php
}
/**
* Set the table columns.
*
* @return string[] The table columns.
*/
public function get_columns() {
return [
'cb' => '<input type="checkbox" />',
'type' => _x( 'Type', 'noun', 'wordpress-seo-premium' ),
'old' => $this->current_column,
'new' => __( 'New URL', 'wordpress-seo-premium' ),
];
}
/**
* Counts the total columns for the table.
*
* @return int The total amount of columns.
*/
public function count_columns() {
return count( $this->get_columns() );
}
/**
* Filter for setting the primary table column.
*
* @param string $column The current column.
* @param string $screen The current opened window.
*
* @return string The primary table column.
*/
public function redirect_list_table_primary_column( $column, $screen ) {
if ( $screen === 'seo_page_wpseo_redirects' ) {
$column = $this->primary_column;
}
return $column;
}
/**
* Sets up the table variables, fetch the items from the database, search, sort and format the items.
* Sets the items as the WPSEO_Redirect_Table items variable.
*
* @return void
*/
public function prepare_items() {
// Setup the columns.
$this->_column_headers = [ $this->get_columns(), [], $this->get_sortable_columns() ];
// Get variables needed for pagination.
$per_page = $this->get_items_per_page( 'redirects_per_page', 25 );
$total_items = count( $this->items );
$pagination_args = [
'total_items' => $total_items,
'total_pages' => ceil( $total_items / $per_page ),
'per_page' => $per_page,
];
// Set pagination.
$this->set_pagination_args( $pagination_args );
$paged = filter_input( INPUT_GET, 'paged' );
$current_page = (int) ( ( isset( $paged ) && $paged !== false ) ? $paged : 0 );
// Setting the starting point. If starting point is below 1, overwrite it with value 0, otherwise it will be sliced of at the back.
$slice_start = ( $current_page - 1 );
if ( $slice_start < 0 ) {
$slice_start = 0;
}
// Apply 'pagination'.
$formatted_items = array_slice( $this->items, ( $slice_start * $per_page ), $per_page );
// Set items.
$this->items = $formatted_items;
}
/**
* Returns the columns that are sortable.
*
* @return array[] An array containing the sortable columns.
*/
public function get_sortable_columns() {
return [
'old' => [ 'old', false ],
'new' => [ 'new', false ],
'type' => [ 'type', false ],
];
}
/**
* Reorders the items based on user input.
*
* @param array $a The current sort direction.
* @param array $b The new sort direction.
*
* @return int The order that should be used.
*/
public function do_reorder( $a, $b ) {
// If no sort, default to title.
$orderby = filter_input(
INPUT_GET,
'orderby',
FILTER_VALIDATE_REGEXP,
[
'options' => [
'default' => 'old',
'regexp' => '/^(old|new|type)$/',
],
]
);
// If no order, default to asc.
$order = filter_input(
INPUT_GET,
'order',
FILTER_VALIDATE_REGEXP,
[
'options' => [
'default' => 'asc',
'regexp' => '/^(asc|desc)$/',
],
]
);
// Determine sort order.
$result = strcmp( $a[ $orderby ], $b[ $orderby ] );
// Send final sort direction to usort.
return ( $order === 'asc' ) ? $result : ( -$result );
}
/**
* Creates a column for a checkbox.
*
* @param array $item Array with the row data.
*
* @return string The column with a checkbox.
*/
public function column_cb( $item ) {
return sprintf(
'<label class="screen-reader-text" for="wpseo-redirects-bulk-cb-%2$s">%3$s</label> <input type="checkbox" name="wpseo_redirects_bulk_delete[]" id="wpseo-redirects-bulk-cb-%2$s" value="%1$s" />',
esc_attr( $item['old'] ),
$item['row_number'],
esc_html( __( 'Select this redirect', 'wordpress-seo-premium' ) )
);
}
/**
* Displays a default column.
*
* @param array $item Array with the row data.
* @param string $column_name The name of the needed column.
*
* @return string The default column.
*/
public function column_default( $item, $column_name ) {
$is_regex = ( filter_input( INPUT_GET, 'tab' ) === 'regex' );
$row_actions = $this->get_row_actions( $column_name );
switch ( $column_name ) {
case 'new':
$classes = [ 'val' ];
$new_url = $item['new'];
if ( ! $is_regex && WPSEO_Redirect_Util::requires_trailing_slash( $new_url ) ) {
$classes[] = 'has-trailing-slash';
}
if (
$new_url === ''
|| $new_url === '/'
|| ! WPSEO_Redirect_Util::is_relative_url( $new_url )
) {
$classes[] = 'remove-slashes';
}
return "<div class='" . esc_attr( implode( ' ', $classes ) ) . "'>" . esc_html( $new_url ) . '</div>' . $row_actions;
case 'old':
$classes = '';
if ( $is_regex === true ) {
$classes = ' remove-slashes';
}
return "<div class='val" . $classes . "'>" . esc_html( $item['old'] ) . '</div>' . $row_actions;
case 'type':
return '<div class="val type">' . esc_html( $item['type'] ) . '</div>' . $row_actions;
default:
return $item[ $column_name ];
}
}
/**
* Returns the available bulk actions.
*
* @return string[] Array containing the available bulk actions.
*/
public function get_bulk_actions() {
return [
'delete' => __( 'Delete', 'wordpress-seo-premium' ),
];
}
/**
* Sets the items and orders them.
*
* @param array $items The data that will be showed.
*
* @return void
*/
private function set_items( $items ) {
// Getting the items.
$this->items = $this->filter_items( $items );
$this->format_items();
// Sort the results.
if ( count( $this->items ) > 0 ) {
usort( $this->items, [ $this, 'do_reorder' ] );
}
}
/**
* Filters the given items.
*
* @param WPSEO_Redirect[] $items The items to filter.
*
* @return array The filtered items.
*/
private function filter_items( array $items ) {
$search_string = filter_input( INPUT_GET, 's', FILTER_DEFAULT, [ 'options' => [ 'default' => '' ] ] );
if ( $search_string !== '' ) {
$this->filter['search_string'] = trim( $search_string, '/' );
$items = array_filter( $items, [ $this, 'filter_by_search_string' ] );
}
$redirect_type = (int) filter_input( INPUT_GET, 'redirect-type' );
if ( ! empty( $redirect_type ) ) {
$this->filter['redirect_type'] = $redirect_type;
$items = array_filter( $items, [ $this, 'filter_by_type' ] );
}
return $items;
}
/**
* Formats the items.
*/
private function format_items() {
// Format the data.
$formatted_items = [];
$counter = 1;
foreach ( $this->items as $redirect ) {
$formatted_items[] = [
'old' => $redirect->get_origin(),
'new' => $redirect->get_target(),
'type' => $redirect->get_type(),
'row_number' => $counter,
];
++$counter;
}
$this->items = $formatted_items;
}
/**
* Filters the redirect by entered search string.
*
* @param WPSEO_Redirect $redirect The redirect to filter.
*
* @return bool True when the search strings match.
*/
private function filter_by_search_string( WPSEO_Redirect $redirect ) {
return ( stripos( $redirect->get_origin(), $this->filter['search_string'] ) !== false || stripos( $redirect->get_target(), $this->filter['search_string'] ) !== false );
}
/**
* Filters the redirect by redirect type.
*
* @param WPSEO_Redirect $redirect The redirect to filter.
*
* @return bool True when type matches redirect type.
*/
private function filter_by_type( WPSEO_Redirect $redirect ) {
return $redirect->get_type() === $this->filter['redirect_type'];
}
/**
* The old column actions.
*
* @param string $column The column name to verify.
*
* @return string
*/
private function get_row_actions( $column ) {
if ( $column === $this->primary_column ) {
$actions = [
'edit' => '<a href="#" role="button" class="redirect-edit">' . __( 'Edit', 'wordpress-seo-premium' ) . '</a>',
'trash' => '<a href="#" role="button" class="redirect-delete">' . __( 'Delete', 'wordpress-seo-premium' ) . '</a>',
];
return $this->row_actions( $actions );
}
return '';
}
/**
* Generates and display row actions links for the list table.
*
* We override the parent class method to avoid doubled buttons to be printed out.
*
* @param object $item The item being acted upon.
* @param string $column_name Current column name.
* @param string $primary Primary column name.
* @return string Empty string.
*/
protected function handle_row_actions( $item, $column_name, $primary ) {
return '';
}
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Class representing a list of redirect types.
*/
class WPSEO_Redirect_Types {
const TEMPORARY = 307;
const UNAVAILABLE = 451;
const DELETED = 410;
const FOUND = 302;
const PERMANENT = 301;
/**
* Returns the redirect types.
*
* @return string[] Array with the redirect types.
*/
public function get() {
$redirect_types = [
'301' => __( '301 Moved Permanently', 'wordpress-seo-premium' ),
'302' => __( '302 Found', 'wordpress-seo-premium' ),
'307' => __( '307 Temporary Redirect', 'wordpress-seo-premium' ),
'410' => __( '410 Content Deleted', 'wordpress-seo-premium' ),
'451' => __( '451 Unavailable For Legal Reasons', 'wordpress-seo-premium' ),
];
/**
* Filter: 'wpseo_premium_redirect_types' - can be used to filter the redirect types.
*
* @deprecated 12.9.0. Use the {@see 'Yoast\WP\SEO\redirect_types'} filter instead.
*
* @api array $redirect_types
*/
$redirect_types = apply_filters_deprecated(
'wpseo_premium_redirect_types',
[ $redirect_types ],
'YoastSEO Premium 12.9.0',
'Yoast\WP\SEO\redirect_types'
);
/**
* Filter: 'Yoast\WP\SEO\redirect_types' - can be used to filter the redirect types.
*
* Note: This is a Premium plugin-only hook.
*
* @since 12.9.0
*
* @api array $redirect_types
*/
return apply_filters( 'Yoast\WP\SEO\redirect_types', $redirect_types );
}
/**
* Checks whether the given value is a valid redirect type.
*
* @param string $value Value to check.
*
* @return bool True if a redirect type, false otherwise.
*/
public function has( $value ) {
$types = $this->get();
return isset( $types[ $value ] );
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Class WPSEO_Redirect_Manager.
*/
class WPSEO_Redirect_Upgrade {
/**
* Lookup table for previous redirect format constants to their current counterparts.
*
* @var array
*/
private static $redirect_option_names = [
WPSEO_Redirect_Option::OLD_OPTION_PLAIN => WPSEO_Redirect_Formats::PLAIN,
WPSEO_Redirect_Option::OLD_OPTION_REGEX => WPSEO_Redirect_Formats::REGEX,
];
/**
* Upgrade routine from Yoast SEO premium 1.2.0.
*/
public static function upgrade_1_2_0() {
$redirect_option = self::get_redirect_option();
$redirects = [];
foreach ( self::$redirect_option_names as $redirect_option_name => $redirect_format ) {
$old_redirects = $redirect_option->get_from_option( $redirect_option_name );
foreach ( $old_redirects as $origin => $redirect ) {
// Check if the redirect is not an array yet.
if ( ! is_array( $redirect ) ) {
$redirects[] = new WPSEO_Redirect( $origin, $redirect['url'], $redirect['type'], $redirect_format );
}
}
}
self::import_redirects( $redirects );
}
/**
* Check if redirects should be imported from the free version.
*
* @since 2.3
*/
public static function import_redirects_2_3() {
// phpcs:ignore WordPress.DB.SlowDBQuery -- Upgrade routine, so rarely used, therefore not an issue.
$wp_query = new WP_Query( 'post_type=any&meta_key=_yoast_wpseo_redirect&order=ASC' );
if ( ! empty( $wp_query->posts ) ) {
$redirects = [];
foreach ( $wp_query->posts as $post ) {
$old_url = '/' . $post->post_name . '/';
$new_url = get_post_meta( $post->ID, '_yoast_wpseo_redirect', true );
// Create redirect.
$redirects[] = new WPSEO_Redirect( $old_url, $new_url, 301, WPSEO_Redirect_Formats::PLAIN );
// Remove post meta value.
delete_post_meta( $post->ID, '_yoast_wpseo_redirect' );
}
self::import_redirects( $redirects );
}
}
/**
* Upgrade routine to merge plain and regex redirects in a single option.
*/
public static function upgrade_3_1() {
$redirects = [];
foreach ( self::$redirect_option_names as $redirect_option_name => $redirect_format ) {
$old_redirects = get_option( $redirect_option_name, [] );
foreach ( $old_redirects as $origin => $redirect ) {
// Only when URL and type is set.
if ( array_key_exists( 'url', $redirect ) && array_key_exists( 'type', $redirect ) ) {
$redirects[] = new WPSEO_Redirect( $origin, $redirect['url'], $redirect['type'], $redirect_format );
}
}
}
// Saving the redirects to the option.
self::import_redirects( $redirects, [ new WPSEO_Redirect_Option_Exporter() ] );
}
/**
* Exports the redirects to htaccess or nginx file if needed.
*/
public static function upgrade_13_0() {
$redirect_manager = new WPSEO_Redirect_Manager();
$redirect_manager->export_redirects();
}
/**
* Imports an array of redirect objects.
*
* @param WPSEO_Redirect[] $redirects The redirects.
* @param WPSEO_Redirect_Exporter[]|null $exporters The exporters.
*/
private static function import_redirects( $redirects, $exporters = null ) {
if ( empty( $redirects ) ) {
return;
}
$redirect_option = self::get_redirect_option();
$redirect_manager = new WPSEO_Redirect_Manager( null, $exporters, $redirect_option );
foreach ( $redirects as $redirect ) {
$redirect_option->add( $redirect );
}
$redirect_option->save( false );
$redirect_manager->export_redirects();
}
/**
* Gets and caches the redirect option.
*
* @return WPSEO_Redirect_Option
*/
private static function get_redirect_option() {
static $redirect_option;
if ( empty( $redirect_option ) ) {
$redirect_option = new WPSEO_Redirect_Option( false );
}
return $redirect_option;
}
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Class representing a formatter for an URL.
*/
class WPSEO_Redirect_Url_Formatter {
/**
* The URL to format.
*
* @var string
*/
protected $url = '';
/**
* Sets the URL used for formatting.
*
* @param string $url The URL to format.
*/
public function __construct( $url ) {
$this->url = $this->sanitize_url( $url );
}
/**
* We want to strip the subdirectory from the redirect url.
*
* @param string $home_url The URL to use as the base.
*
* @return string
*/
public function format_without_subdirectory( $home_url ) {
$subdirectory = $this->get_subdirectory( $home_url );
if ( ! empty( $subdirectory ) ) {
$subdirectory = trailingslashit( $subdirectory );
$path_position = strpos( $this->url, $subdirectory );
if ( $path_position === 0 ) {
return '/' . $this->sanitize_url( substr( $this->url, strlen( $subdirectory ) ) );
}
}
return '/' . $this->url;
}
/**
* Removes the slashes at the beginning of an url.
*
* @param string $url The URL to sanitize.
*
* @return string
*/
protected function sanitize_url( $url ) {
return ltrim( $url, '/' );
}
/**
* Returns the subdirectory from the given URL.
*
* @param string $url The URL to get the subdirectory for.
*
* @return string
*/
protected function get_subdirectory( $url ) {
return $this->sanitize_url( wp_parse_url( $url, PHP_URL_PATH ) );
}
}

View File

@@ -0,0 +1,129 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* Helpers for redirects.
*/
class WPSEO_Redirect_Util {
/**
* Whether or not the permalink contains a trailing slash.
*
* @var bool
*/
public static $has_permalink_trailing_slash = null;
/**
* Returns whether or not a URL is a relative URL.
*
* @param string $url The URL to determine the relativity for.
* @return bool
*/
public static function is_relative_url( $url ) {
$url_scheme = wp_parse_url( $url, PHP_URL_SCHEME );
return ! $url_scheme;
}
/**
* Returns whether or not the permalink structure has a trailing slash.
*
* @return bool
*/
public static function has_permalink_trailing_slash() {
if ( self::$has_permalink_trailing_slash === null ) {
$permalink_structure = get_option( 'permalink_structure' );
self::$has_permalink_trailing_slash = substr( $permalink_structure, -1 ) === '/';
}
return self::$has_permalink_trailing_slash;
}
/**
* Returns whether or not the URL has query variables.
*
* @param string $url The URL.
* @return bool
*/
public static function has_query_parameters( $url ) {
return strpos( $url, '?' ) !== false;
}
/**
* Returns whether or not the given URL has a fragment identifier.
*
* @param string $url The URL to parse.
*
* @return bool
*/
public static function has_fragment_identifier( $url ) {
// Deal with this case if the last character is a hash.
if ( substr( $url, -1 ) === '#' ) {
return true;
}
$fragment = wp_parse_url( $url, PHP_URL_FRAGMENT );
return ! empty( $fragment );
}
/**
* Returns whether or not the given URL has an extension.
*
* @param string $url The URL to parse.
*
* @return bool Whether or not the given URL has an extension.
*/
public static function has_extension( $url ) {
$parsed = wp_parse_url( $url, PHP_URL_PATH );
return strpos( $parsed, '.' ) !== false;
}
/**
* Returns whether or not a target URL requires a trailing slash.
*
* @param string $target_url The target URL to check.
*
* @return bool
*/
public static function requires_trailing_slash( $target_url ) {
return $target_url !== '/'
&& self::has_permalink_trailing_slash()
&& self::is_relative_url( $target_url )
&& ! self::has_query_parameters( $target_url )
&& ! self::has_fragment_identifier( $target_url )
&& ! self::has_extension( $target_url );
}
/**
* Removes the base url path from the given URL.
*
* @param string $base_url The base URL that will be stripped.
* @param string $url URL to remove the path from.
*
* @return string The URL without the base url
*/
public static function strip_base_url_path_from_url( $base_url, $url ) {
$base_url_path = wp_parse_url( $base_url, PHP_URL_PATH );
$base_url_path = ltrim( $base_url_path, '/' );
if ( empty( $base_url_path ) ) {
return $url;
}
$url = ltrim( $url, '/' );
// When the url doesn't begin with the base url path.
if ( stripos( trailingslashit( $url ), trailingslashit( $base_url_path ) ) !== 0 ) {
return $url;
}
return substr( $url, strlen( $base_url_path ) );
}
}

View File

@@ -0,0 +1,164 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect
*/
/**
* The validation class.
*/
class WPSEO_Redirect_Validator {
/**
* List containing all possible validation rules.
*
* @var array
*/
protected $validation_rules = [
'relative-origin' => [
'validation_class' => 'WPSEO_Redirect_Relative_Origin_Validation',
'exclude_types' => [],
'exclude_format' => [ WPSEO_Redirect_Formats::REGEX ],
],
'self-redirect' => [
'validation_class' => 'WPSEO_Redirect_Self_Redirect_Validation',
'exclude_types' => [],
'exclude_format' => [ WPSEO_Redirect_Formats::REGEX ],
],
'uniqueness' => [
'validation_class' => 'WPSEO_Redirect_Uniqueness_Validation',
'exclude_types' => [],
'exclude_format' => [],
],
'presence' => [
'validation_class' => 'WPSEO_Redirect_Presence_Validation',
'exclude_types' => [],
'exclude_format' => [],
],
'subdirectory-presence' => [
'validation_class' => 'WPSEO_Redirect_Subdirectory_Validation',
'exclude_types' => [],
'exclude_format' => [],
],
'accessible' => [
'validation_class' => 'WPSEO_Redirect_Accessible_Validation',
'exclude_types' => [ WPSEO_Redirect_Types::DELETED, WPSEO_Redirect_Types::UNAVAILABLE ],
'exclude_format' => [ WPSEO_Redirect_Formats::REGEX ],
],
'endpoint' => [
'validation_class' => 'WPSEO_Redirect_Endpoint_Validation',
'exclude_types' => [ WPSEO_Redirect_Types::DELETED, WPSEO_Redirect_Types::UNAVAILABLE ],
'exclude_format' => [ WPSEO_Redirect_Formats::REGEX ],
],
];
/**
* A string holding a possible redirect validation error.
*
* @var bool|string The validation error.
*/
protected $validation_error = false;
/**
* Validates the old and the new URL.
*
* @param WPSEO_Redirect $redirect The redirect that will be saved.
* @param WPSEO_Redirect|null $current_redirect Redirect that will be used for comparison.
*
* @return bool|string
*/
public function validate( WPSEO_Redirect $redirect, WPSEO_Redirect $current_redirect = null ) {
$validators = $this->get_validations( $this->get_filtered_validation_rules( $this->validation_rules, $redirect ) );
$redirects = $this->get_redirects( $redirect->get_format() );
$this->validation_error = '';
foreach ( $validators as $validator ) {
if ( ! $validator->run( $redirect, $current_redirect, $redirects ) ) {
$this->validation_error = $validator->get_error();
return false;
}
}
return true;
}
/**
* Returns the validation error.
*
* @return WPSEO_Validation_Result
*/
public function get_error() {
return $this->validation_error;
}
/**
* Removes a rule from the validations.
*
* @param array $validations Array with the validations.
* @param string $rule_to_remove The rule that will be removed.
*/
protected function remove_rule( &$validations, $rule_to_remove ) {
if ( array_key_exists( $rule_to_remove, $validations ) ) {
unset( $validations[ $rule_to_remove ] );
}
}
/**
* Filters the validation rules.
*
* @param array $validations Array with validation rules.
* @param WPSEO_Redirect $redirect The redirect that will be saved.
*
* @return array
*/
protected function get_filtered_validation_rules( array $validations, WPSEO_Redirect $redirect ) {
foreach ( $validations as $validation => $validation_rules ) {
$exclude_format = in_array( $redirect->get_format(), $validation_rules['exclude_format'], true );
$exclude_type = in_array( $redirect->get_type(), $validation_rules['exclude_types'], true );
if ( $exclude_format || $exclude_type ) {
$this->remove_rule( $validations, $validation );
}
}
return $validations;
}
/**
* Getting the validations based on the set validation rules.
*
* @param array $validation_rules The rules for the validations that will be run.
*
* @return WPSEO_Redirect_Validation[]
*/
protected function get_validations( $validation_rules ) {
$validations = [];
foreach ( $validation_rules as $validation_rule ) {
$validations[] = new $validation_rule['validation_class']();
}
return $validations;
}
/**
* Fill the redirect property.
*
* @param string $format The format for the redirects.
*
* @return array
*/
protected function get_redirects( $format ) {
$redirect_manager = new WPSEO_Redirect_Manager( $format );
// Format the redirects.
$redirects = [];
foreach ( $redirect_manager->get_all_redirects() as $redirect ) {
$redirects[ $redirect->get_origin() ] = $redirect->get_target();
}
return $redirects;
}
}

View File

@@ -0,0 +1,346 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
use Yoast\WP\SEO\Helpers\Home_Url_Helper;
/**
* Represents a single redirect
*/
class WPSEO_Redirect implements ArrayAccess {
/**
* Redirect origin.
*
* @var string
*/
protected $origin;
/**
* Redirect target.
*
* @var string
*/
protected $target = '';
/**
* A HTTP code determining the redirect type.
*
* @var int
*/
protected $type;
/**
* A string determining the redirect format (plain or regex).
*
* @var string
*/
protected $format;
/**
* A string holding a possible redirect validation error.
*
* @var string
*/
protected $validation_error;
/**
* The home URL helper.
*
* @var Home_Url_Helper
*/
protected static $home_url;
/**
* WPSEO_Redirect constructor.
*
* @param string $origin The origin of the redirect.
* @param string $target The target of the redirect.
* @param int $type The type of the redirect.
* @param string $format The format of the redirect.
*/
public function __construct( $origin, $target = '', $type = WPSEO_Redirect_Types::PERMANENT, $format = WPSEO_Redirect_Formats::PLAIN ) {
if ( static::$home_url === null ) {
static::$home_url = new Home_Url_Helper();
}
$this->origin = ( $format === WPSEO_Redirect_Formats::PLAIN ) ? $this->sanitize_origin_url( $origin ) : $origin;
$this->target = $this->sanitize_target_url( $target );
$this->format = $format;
$this->type = (int) $type;
}
/**
* Returns the origin.
*
* @return string The set origin.
*/
public function get_origin() {
return $this->origin;
}
/**
* Returns the target
*
* @return string The set target.
*/
public function get_target() {
return $this->target;
}
/**
* Returns the type
*
* @return int The set type.
*/
public function get_type() {
return $this->type;
}
/**
* Returns the format
*
* @return string The set format.
*/
public function get_format() {
return $this->format;
}
/**
* Whether a offset exists.
*
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
*
* @param string $offset An offset to check for.
*
* @return bool True on success or false on failure.
* The return value will be cast to boolean if non-boolean was returned.
*/
public function offsetExists( $offset ) {
return in_array( $offset, [ 'url', 'type' ], true );
}
/**
* Offset to retrieve.
*
* @link http://php.net/manual/en/arrayaccess.offsetget.php
*
* @param string $offset The offset to retrieve.
*
* @return mixed Can return all value types.
*/
public function offsetGet( $offset ) {
switch ( $offset ) {
case 'old':
return $this->origin;
case 'url':
return $this->target;
case 'type':
return $this->type;
}
return null;
}
/**
* Offset to set.
*
* @link http://php.net/manual/en/arrayaccess.offsetset.php
*
* @param string $offset The offset to assign the value to.
* @param string $value The value to set.
*
* @return void
*/
public function offsetSet( $offset, $value ) {
switch ( $offset ) {
case 'url':
$this->target = $value;
break;
case 'type':
$this->type = $value;
break;
}
}
/**
* Offset to unset.
*
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
*
* @codeCoverageIgnore
*
* @param string $offset The offset to unset.
*
* @return void
*/
public function offsetUnset( $offset ) {
}
/**
* Compares an URL with the origin of the redirect.
*
* @param string $url The URL to compare.
*
* @return bool True when url matches the origin.
*/
public function origin_is( $url ) {
// Sanitize the slash in case of plain redirect.
if ( $this->format === WPSEO_Redirect_Formats::PLAIN ) {
$url = $this->sanitize_slash( $url, $this->parse_url( $url ) );
}
return (string) $this->origin === (string) $url;
}
/**
* Strip the trailing slashes for relative URLs.
*
* @param string $url_to_sanitize The URL to sanitize.
* @param array $url_pieces The url pieces.
*
* @return string The sanitized url.
*/
private function sanitize_slash( $url_to_sanitize, array $url_pieces = [] ) {
$url = $url_to_sanitize;
if ( $url !== '/' && ! isset( $url_pieces['scheme'] ) ) {
return trim( $url_to_sanitize, '/' );
}
return $url;
}
/**
* Strip the protocol from the URL.
*
* @param string $scheme The scheme to strip.
* @param string $url The URL to remove the scheme from.
*
* @return string The url without the scheme.
*/
private function strip_scheme_from_url( $scheme, $url ) {
return str_replace( $scheme . '://', '', $url );
}
/**
* Remove the home URL from the redirect to ensure that relative URLs are created.
*
* @param string $url The URL to sanitize.
*
* @return string The sanitized url.
*/
private function sanitize_origin_url( $url ) {
$home_url = static::$home_url->get();
$home_url_pieces = static::$home_url->get_parsed();
$url_pieces = $this->parse_url( $url );
if ( $this->match_home_url( $home_url_pieces, $url_pieces ) ) {
$url = substr(
$this->strip_scheme_from_url( $url_pieces['scheme'], $url ),
strlen( $this->strip_scheme_from_url( $home_url_pieces['scheme'], $home_url ) )
);
$url_pieces['scheme'] = null;
}
return $this->sanitize_slash( $url, $url_pieces );
}
/**
* Sanitizes the target url.
*
* @param string $url The url to sanitize.
*
* @return string The sanitized url.
*/
private function sanitize_target_url( $url ) {
$home_url_pieces = static::$home_url->get_parsed();
$url_pieces = $this->parse_url( $url );
if ( $this->match_home_url( $home_url_pieces, $url_pieces ) ) {
$url = substr(
$this->strip_scheme_from_url( $url_pieces['scheme'], $url ),
strlen( $home_url_pieces['host'] )
);
$url_pieces['scheme'] = null;
}
return $this->sanitize_slash( $url, $url_pieces );
}
/**
* Checks if the URL matches the home URL.
*
* @param array $home_url_pieces The pieces (wp_parse_url) from the home_url.
* @param array $url_pieces The pieces (wp_parse_url) from the url to match.
*
* @return bool True when the URL matches the home URL.
*/
private function match_home_url( $home_url_pieces, $url_pieces ) {
if ( ! isset( $url_pieces['scheme'] ) ) {
return false;
}
if ( ! isset( $url_pieces['host'] ) || ! $this->match_home_url_host( $home_url_pieces['host'], $url_pieces['host'] ) ) {
return false;
}
if ( ! isset( $home_url_pieces['path'] ) ) {
return true;
}
return isset( $url_pieces['path'] ) && $this->match_home_url_path( $home_url_pieces['path'], $url_pieces['path'] );
}
/**
* Checks if the URL matches the home URL by comparing their host.
*
* @param string $home_url_host The home URL host.
* @param string $url_host The URL host.
*
* @return bool True when both hosts are equal.
*/
private function match_home_url_host( $home_url_host, $url_host ) {
return $url_host === $home_url_host;
}
/**
* Checks if the URL matches the home URL by comparing their path.
*
* @param string $home_url_path The home URL path.
* @param string $url_path The URL path.
*
* @return bool True when the home URL path is empty or when the URL path begins with the home URL path.
*/
private function match_home_url_path( $home_url_path, $url_path ) {
$home_url_path = trim( $home_url_path, '/' );
if ( empty( $home_url_path ) ) {
return true;
}
return strpos( trim( $url_path, '/' ), $home_url_path ) === 0;
}
/**
* Parses the URL into separate pieces.
*
* @param string $url The URL string.
*
* @return array Array of URL pieces.
*/
private function parse_url( $url ) {
$parsed_url = wp_parse_url( $url );
if ( is_array( $parsed_url ) ) {
return $parsed_url;
}
return [];
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Validation
*/
/**
* Base class for validating redirects
*/
abstract class WPSEO_Redirect_Abstract_Validation implements WPSEO_Redirect_Validation {
/**
* The validation error.
*
* @var WPSEO_Validation_Result
*/
private $error = null;
/**
* Returns the validation error.
*
* @return WPSEO_Validation_Result|null
*/
public function get_error() {
return $this->error;
}
/**
* Sets the validation error.
*
* @param WPSEO_Validation_Result $error Validation error or warning.
*
* @return void
*/
protected function set_error( WPSEO_Validation_Result $error ) {
$this->error = $error;
}
}

View File

@@ -0,0 +1,138 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Validation
*/
/**
* Validates the accessibility of a redirect's target.
*/
class WPSEO_Redirect_Accessible_Validation extends WPSEO_Redirect_Abstract_Validation {
/**
* Validates if the target is accessible and based on its response code it will set a warning (if applicable).
*
* @param WPSEO_Redirect $redirect The redirect to validate.
* @param WPSEO_Redirect|null $old_redirect The old redirect to compare.
* @param array|null $redirects Unused.
*
* @return bool Whether or not the target is valid.
*/
public function run( WPSEO_Redirect $redirect, WPSEO_Redirect $old_redirect = null, array $redirects = null ) {
// Do the request.
$target = $this->parse_target( $redirect->get_target() );
$decoded_url = rawurldecode( $target );
$response = $this->remote_head( $decoded_url, [ 'sslverify' => false ] );
if ( is_wp_error( $response ) ) {
$error = __( 'The URL you entered could not be resolved.', 'wordpress-seo-premium' );
$this->set_error( new WPSEO_Validation_Warning( $error, 'target' ) );
return false;
}
$response_code = $this->retrieve_response_code( $response );
// Check if the target is a temporary location.
if ( $this->is_temporary( $response_code ) ) {
/* translators: %1$s expands to the returned http code */
$error = __( 'The URL you are redirecting to seems to return a %1$s status. You might want to check if the target can be reached manually before saving.', 'wordpress-seo-premium' );
$error = sprintf( $error, $response_code );
$this->set_error( new WPSEO_Validation_Warning( $error, 'target' ) );
return false;
}
// Check if the response code is 301.
if ( $response_code === 301 ) {
$error = __( 'You\'re redirecting to a target that returns a 301 HTTP code (permanently moved). Make sure the target you specify is directly reachable.', 'wordpress-seo-premium' );
$this->set_error( new WPSEO_Validation_Warning( $error, 'target' ) );
return false;
}
if ( $response_code !== 200 ) {
/* translators: %1$s expands to the returned http code */
$error = __( 'The URL you entered returned a HTTP code different than 200(OK). The received HTTP code is %1$s.', 'wordpress-seo-premium' );
$error = sprintf( $error, $response_code );
$this->set_error( new WPSEO_Validation_Warning( $error, 'target' ) );
return false;
}
return true;
}
/**
* Retrieves the response code from the response array.
*
* @param array $response The response.
*
* @return int The response code.
*/
protected function retrieve_response_code( $response ) {
return wp_remote_retrieve_response_code( $response );
}
/**
* Sends a HEAD request to the passed remote URL.
*
* @param string $url The URL to send the request to.
* @param array $options The options to send along with the request.
*
* @return array|WP_Error The response or WP_Error if something goes wrong.
*/
protected function remote_head( $url, $options = [] ) {
return wp_remote_head( $url, $options );
}
/**
* Check if the given response code is a temporary one.
*
* @param int $response_code The response code to check.
*
* @return bool
*/
protected function is_temporary( $response_code ) {
return in_array( $response_code, [ 302, 307 ], true ) || in_array( substr( $response_code, 0, 2 ), [ '40', '50' ], true );
}
/**
* Check if the target is relative, if so just parse a full URL.
*
* @param string $target The target to parse.
*
* @return string
*/
protected function parse_target( $target ) {
$scheme = wp_parse_url( $target, PHP_URL_SCHEME );
// If we have an absolute url return it.
if ( ! empty( $scheme ) ) {
return $target;
}
// Removes the installation directory if present.
$target = WPSEO_Redirect_Util::strip_base_url_path_from_url( $this->get_home_url(), $target );
// If we have a relative url make it absolute.
$absolute = get_home_url( null, $target );
// If the path does not end with an extension then add a trailing slash.
if ( WPSEO_Redirect_Util::requires_trailing_slash( $target ) ) {
return trailingslashit( $absolute );
}
return $absolute;
}
/**
* Returns the home url.
*
* @return string The home url.
*/
protected function get_home_url() {
return home_url();
}
}

View File

@@ -0,0 +1,99 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Validation
*/
/**
* Validates the endpoint of a redirect
*/
class WPSEO_Redirect_Endpoint_Validation extends WPSEO_Redirect_Abstract_Validation {
/**
* List of redirects.
*
* @var array
*/
private $redirects;
/**
* This validation checks if the redirect being created, follows:
* - a path that results in a redirection to it's own origin due to other redirects pointing to the current origin.
* - a path that can be shorten by creating a direct redirect.
*
* @param WPSEO_Redirect $redirect The redirect to validate.
* @param WPSEO_Redirect|null $old_redirect The old redirect to compare.
* @param array|null $redirects Array with redirect to validate against.
*
* @return bool
*/
public function run( WPSEO_Redirect $redirect, WPSEO_Redirect $old_redirect = null, array $redirects = null ) {
$this->redirects = $redirects;
$origin = $redirect->get_origin();
$target = $redirect->get_target();
$endpoint = $this->search_end_point( $target, $origin );
// Check for a redirect loop.
if ( is_string( $endpoint ) && in_array( $endpoint, [ $origin, $target ], true ) ) {
$error = __( 'The redirect you are trying to save will create a redirect loop. This means there probably already exists a redirect that points to the origin of the redirect you are trying to save', 'wordpress-seo-premium' );
$this->set_error( new WPSEO_Validation_Error( $error, [ 'origin', 'target' ] ) );
return false;
}
if ( is_string( $endpoint ) && $target !== $endpoint ) {
/* translators: %1$s: will be the target, %2$s: will be the found endpoint. */
$error = __( '%1$s will be redirected to %2$s. Maybe it\'s worth considering to create a direct redirect to %2$s.', 'wordpress-seo-premium' );
$error = sprintf( $error, $target, $endpoint );
$this->set_error( new WPSEO_Validation_Warning( $error, 'target' ) );
return false;
}
return true;
}
/**
* Will check if the $new_url is redirected also and follows the trace of this redirect
*
* @param string $new_url The new URL to search for.
* @param string $old_url The current URL that is redirected.
*
* @return bool|string
*/
private function search_end_point( $new_url, $old_url ) {
$new_target = $this->find_url( $new_url );
if ( $new_target !== false ) {
// Unset the redirects, because it was found already.
unset( $this->redirects[ $new_url ] );
if ( $new_url !== $old_url ) {
$traced_target = $this->search_end_point( $new_target, $old_url );
if ( $traced_target !== false ) {
return $traced_target;
}
}
return $new_target;
}
return false;
}
/**
* Search for the given $url and returns it target
*
* @param string $url The URL to search for.
*
* @return bool
*/
private function find_url( $url ) {
if ( ! empty( $this->redirects[ $url ] ) ) {
return $this->redirects[ $url ];
}
return false;
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Validation
*/
/**
* Validates that all redirect fields have been correctly filled.
*/
class WPSEO_Redirect_Presence_Validation extends WPSEO_Redirect_Abstract_Validation {
/**
* Validates if the redirect has all the required fields.
* - For a 410 and 451 type redirect the target isn't necessary.
* - For all other redirect types the target is required.
*
* @param WPSEO_Redirect $redirect The redirect to validate.
* @param WPSEO_Redirect|null $old_redirect The old redirect to compare.
* @param array|null $redirects Unused.
*
* @return bool
*/
public function run( WPSEO_Redirect $redirect, WPSEO_Redirect $old_redirect = null, array $redirects = null ) {
// If redirect type is 410 or 451, the target doesn't have to be filled.
if ( $this->allow_empty_target( $redirect->get_type() ) && $redirect->get_origin() !== '' ) {
return true;
}
if ( ( $redirect->get_origin() !== '' && $redirect->get_target() !== '' && $redirect->get_type() !== '' ) ) {
return true;
}
$error = __( 'Not all the required fields are filled.', 'wordpress-seo-premium' );
$this->set_error( new WPSEO_Validation_Error( $error ) );
return false;
}
/**
* Allows an empty target when the given redirect type matches one of the values in the array.
*
* @param string $redirect_type The type to match.
*
* @return bool
*/
private function allow_empty_target( $redirect_type ) {
$allowed_redirect_types = [ WPSEO_Redirect_Types::DELETED, WPSEO_Redirect_Types::UNAVAILABLE ];
return in_array( (int) $redirect_type, $allowed_redirect_types, true );
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Validation
*/
/**
* Validates if the origin is a relative URL.
*/
class WPSEO_Redirect_Relative_Origin_Validation extends WPSEO_Redirect_Abstract_Validation {
/**
* Validate the redirect to check if the origin URL is relative.
*
* @param WPSEO_Redirect $redirect The redirect to validate.
* @param WPSEO_Redirect|null $old_redirect The old redirect to compare.
* @param array|null $redirects Array with redirects to validate against.
*
* @return bool True if the redirect is valid, false otherwise.
*/
public function run( WPSEO_Redirect $redirect, WPSEO_Redirect $old_redirect = null, array $redirects = null ) {
if ( WPSEO_Redirect_Util::is_relative_url( $redirect->get_origin() ) ) {
return true;
}
$error = __( 'The old URL for your redirect is not relative. Only the new URL is allowed to be absolute. Make sure to provide a relative old URL.', 'wordpress-seo-premium' );
$this->set_error( new WPSEO_Validation_Warning( $error, 'origin' ) );
return false;
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Validation
*/
/**
* Validator for validating that the redirect doesn't point to itself.
*/
class WPSEO_Redirect_Self_Redirect_Validation extends WPSEO_Redirect_Abstract_Validation {
/**
* Validate the redirect to check if it doesn't point to itself.
*
* @param WPSEO_Redirect $redirect The redirect to validate.
* @param WPSEO_Redirect|null $old_redirect The old redirect to compare.
* @param array|null $redirects Array with redirect to validate against.
*
* @return bool
*/
public function run( WPSEO_Redirect $redirect, WPSEO_Redirect $old_redirect = null, array $redirects = null ) {
if ( $redirect->get_origin() === $redirect->get_target() ) {
$error = __( 'You are attempting to redirect to the same URL as the origin. Please choose a different URL to redirect to.', 'wordpress-seo-premium' );
$this->set_error( new WPSEO_Validation_Error( $error, 'origin' ) );
return false;
}
return true;
}
}

View File

@@ -0,0 +1,82 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Validation
*/
/**
* Validates if the origin starts with the subdirectory where the WordPress installation is in.
*/
class WPSEO_Redirect_Subdirectory_Validation extends WPSEO_Redirect_Abstract_Validation {
/**
* Validate the redirect to check if the origin already exists.
*
* @param WPSEO_Redirect $redirect The redirect to validate.
* @param WPSEO_Redirect|null $old_redirect The old redirect to compare.
* @param array|null $redirects Array with redirects to validate against.
*
* @return bool
*/
public function run( WPSEO_Redirect $redirect, WPSEO_Redirect $old_redirect = null, array $redirects = null ) {
$subdirectory = $this->get_subdirectory();
// When there is no subdirectory, there is nothing to validate.
if ( $subdirectory === '' ) {
return true;
}
// When the origin starts with subdirectory, it is okay.
if ( $this->origin_starts_with_subdirectory( $subdirectory, $redirect->get_origin() ) ) {
return true;
}
/* translators: %1$s expands to the subdirectory WordPress is installed. */
$error = __( 'Your redirect is missing the subdirectory where WordPress is installed in. This will result in a redirect that won\'t work. Make sure the redirect starts with %1$s', 'wordpress-seo-premium' );
$error = sprintf( $error, '<code>' . $subdirectory . '</code>' );
$this->set_error( new WPSEO_Validation_Warning( $error, 'origin' ) );
return false;
}
/**
* Returns the subdirectory if applicable.
*
* Calculates the difference between the home and site url. It strips of the site_url from the home_url and returns
* the part that remains.
*
* @return string
*/
protected function get_subdirectory() {
$home_url = untrailingslashit( home_url() );
$site_url = untrailingslashit( site_url() );
if ( $home_url === $site_url ) {
return '';
}
// Strips the site_url from the home_url. substr is used because we want it from the start.
$encoding = get_bloginfo( 'charset' );
return mb_substr( $home_url, mb_strlen( $site_url, $encoding ), null, $encoding );
}
/**
* Checks if the origin starts with the given subdirectory. If so, the origin must start with the subdirectory.
*
* @param string $subdirectory The subdirectory that should be present.
* @param string $origin The origin to check for.
*
* @return bool
*/
protected function origin_starts_with_subdirectory( $subdirectory, $origin ) {
// Strip slashes at the beginning because the origin doesn't start with a slash.
$subdirectory = ltrim( $subdirectory, '/' );
if ( strstr( $origin, $subdirectory ) ) {
return substr( $origin, 0, strlen( $subdirectory ) ) === $subdirectory;
}
return false;
}
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Validation
*/
/**
* Validates the uniqueness of a redirect.
*/
class WPSEO_Redirect_Uniqueness_Validation extends WPSEO_Redirect_Abstract_Validation {
/**
* Validates if the redirect already exists as a redirect.
*
* @param WPSEO_Redirect $redirect The redirect to validate.
* @param WPSEO_Redirect|null $old_redirect The old redirect to compare.
* @param array|null $redirects Array with redirect to validate against.
*
* @return bool
*/
public function run( WPSEO_Redirect $redirect, WPSEO_Redirect $old_redirect = null, array $redirects = null ) {
// Remove uniqueness validation when old origin is the same as the current one.
if ( is_a( $old_redirect, 'WPSEO_Redirect' ) && $redirect->get_origin() === $old_redirect->get_origin() ) {
return true;
}
if ( array_key_exists( $redirect->get_origin(), $redirects ) ) {
$this->set_error(
new WPSEO_Validation_Error(
__( 'The old URL already exists as a redirect.', 'wordpress-seo-premium' ),
'origin'
)
);
return false;
}
return true;
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes\Redirect\Validation
*/
/**
* Validate interface for the validation classes.
*/
interface WPSEO_Redirect_Validation {
/**
* Validates the redirect.
*
* @param WPSEO_Redirect $redirect The redirect to validate.
* @param WPSEO_Redirect|null $old_redirect The old redirect to compare.
* @param array|null $redirects Array with redirect to validate against.
*
* @return bool
*/
public function run( WPSEO_Redirect $redirect, WPSEO_Redirect $old_redirect = null, array $redirects = null );
/**
* Returns the validation error.
*
* @return WPSEO_Validation_Result|null
*/
public function get_error();
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Views
*
* @uses array $display_vars {
* @type string origin_label_value The label that reflects the origin of the redirect: Old URL or Regular Expression.
* @type array redirect_types The redirect types to show in the drop-down menu.
* @type string input_suffix The input suffix.
* @type array values The pre-filled values for the input fields.
* }
*/
$yoast_seo_origin_label_value = $display_vars['origin_label_value'];
$yoast_seo_redirect_types = $display_vars['redirect_types'];
$yoast_seo_input_suffix = $display_vars['input_suffix'];
$yoast_seo_values = $display_vars['values'];
?>
<div class="redirect_form_row" id="row-wpseo_redirects_type">
<label class='textinput' for='<?php echo esc_attr( 'wpseo_redirects_type' . $yoast_seo_input_suffix ); ?>'>
<span class="title"><?php echo esc_html_x( 'Type', 'noun', 'wordpress-seo-premium' ); ?></span>
</label>
<select name='wpseo_redirects_type' id='<?php echo esc_attr( 'wpseo_redirects_type' . $yoast_seo_input_suffix ); ?>' class='select'>
<?php
// Loop through the redirect types.
if ( count( $yoast_seo_redirect_types ) > 0 ) {
foreach ( $yoast_seo_redirect_types as $yoast_seo_redirect_type => $yoast_seo_redirect_desc ) {
echo '<option value="' . esc_attr( $yoast_seo_redirect_type ) . '"'
. sprintf( $yoast_seo_values['type'], $yoast_seo_redirect_type ) . '>'
. esc_html( $yoast_seo_redirect_desc ) . '</option>' . "\n";
}
}
?>
</select>
</div>
<p class="label desc description wpseo-redirect-clear">
<?php
printf(
/* translators: 1: opens a link to a related help center article. 2: closes the link. */
esc_html__( 'The redirect type is the HTTP response code sent to the browser telling the browser what type of redirect is served. %1$sLearn more about redirect types%2$s.', 'wordpress-seo-premium' ),
'<a href="' . WPSEO_Shortlinker::get( 'https://yoa.st/2jb' ) . '" target="_blank">',
'</a>'
);
?>
</p>
<div class='redirect_form_row' id="row-wpseo_redirects_origin">
<label class='textinput' for='<?php echo esc_attr( 'wpseo_redirects_origin' . $yoast_seo_input_suffix ); ?>'>
<span class="title"><?php echo esc_html( $yoast_seo_origin_label_value ); ?></span>
</label>
<input type='text' class='textinput' name='wpseo_redirects_origin' id='<?php echo esc_attr( 'wpseo_redirects_origin' . $yoast_seo_input_suffix ); ?>' value='<?php echo esc_attr( $yoast_seo_values['origin'] ); ?>' />
</div>
<br class='clear'/>
<div class="redirect_form_row wpseo_redirect_target_holder" id="row-wpseo_redirects_target">
<label class='textinput' for='<?php echo esc_attr( 'wpseo_redirects_target' . $yoast_seo_input_suffix ); ?>'>
<span class="title"><?php esc_html_e( 'URL', 'wordpress-seo-premium' ); ?></span>
</label>
<input type='text' class='textinput' name='wpseo_redirects_target' id='<?php echo esc_attr( 'wpseo_redirects_target' . $yoast_seo_input_suffix ); ?>' value='<?php echo esc_attr( $yoast_seo_values['target'] ); ?>' />
</div>
<br class='clear'/>

View File

@@ -0,0 +1,47 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Views
*
* @uses array $display_data {
* @type int total_columns The number of columns.
* @type WPSEO_Redirect_Form_Presenter form_presenter Instance of the WPSEO_Redirect_Form_Presenter class.
* }
*/
$yoast_seo_total_columns = $display_data['total_columns'];
$yoast_seo_form_presenter = $display_data['form_presenter'];
?>
<script type="text/plain" id="tmpl-redirects-inline-edit">
<tr id="inline-edit" class="inline-edit-row hidden">
<td colspan="<?php echo (int) $yoast_seo_total_columns; ?>" class="colspanchange">
<fieldset>
<legend class="inline-edit-legend"><?php esc_html_e( 'Edit redirect', 'wordpress-seo-premium' ); ?></legend>
<div class="inline-edit-col">
<div class="wpseo_redirect_form">
<?php
$yoast_seo_form_presenter->display(
[
'input_suffix' => '{{data.suffix}}',
'values' => [
'origin' => '{{data.origin}}',
'target' => '{{data.target}}',
'type' => '<# if(data.type === %1$s) { #> selected="selected"<# } #>',
],
]
);
?>
</div>
</div>
</fieldset>
<p class="inline-edit-save submit">
<button type="button" class="button button-primary save"><?php esc_html_e( 'Update Redirect', 'wordpress-seo-premium' ); ?></button>
<button type="button" class="button cancel"><?php esc_html_e( 'Cancel', 'wordpress-seo-premium' ); ?></button>
</p>
</td>
</tr>
</script>

View File

@@ -0,0 +1,65 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Views
*
* @uses array $view_vars {
* @type string redirect_table The file path to show in the notices.
* @type string nonce The nonce.
* @type WPSEO_Redirect_Form_Presenter form_presenter Instance of the WPSEO_Redirect_Form_Presenter class.
* @type string origin_from_url The redirect origin.
* @type WPSEO_Redirect_Quick_Edit_Presenter quick_edit_table Instance of the WPSEO_Redirect_Quick_Edit_Presenter class.
* }
*/
$yoast_seo_redirect_table = $view_vars['redirect_table'];
$yoast_seo_nonce = $view_vars['nonce'];
$yoast_seo_form_presenter = $view_vars['form_presenter'];
$yoast_seo_origin_from_url = $view_vars['origin_from_url'];
$yoast_seo_quick_edit_table = $view_vars['quick_edit_table'];
?>
<div id="table-plain" class="tab-url redirect-table-tab">
<?php echo '<h2>' . esc_html__( 'Plain redirects', 'wordpress-seo-premium' ) . '</h2>'; ?>
<form class='wpseo-new-redirect-form' method='post'>
<div class='wpseo_redirect_form'>
<?php
$yoast_seo_form_presenter->display(
[
'input_suffix' => '',
'values' => [
'origin' => $yoast_seo_origin_from_url,
'target' => '',
'type' => '',
],
]
);
?>
<button type="button" class="button button-primary"><?php esc_html_e( 'Add Redirect', 'wordpress-seo-premium' ); ?></button>
</div>
</form>
<p class='desc'>&nbsp;</p>
<?php
$yoast_seo_quick_edit_table->display(
[
'form_presenter' => $yoast_seo_form_presenter,
'total_columns' => $yoast_seo_redirect_table->count_columns(),
]
);
?>
<form id='plain' class='wpseo-redirects-table-form' method='post' action=''>
<input type='hidden' class="wpseo_redirects_ajax_nonce" name='wpseo_redirects_ajax_nonce' value='<?php echo esc_attr( $yoast_seo_nonce ); ?>' />
<?php
// The list table.
$yoast_seo_redirect_table->prepare_items();
$yoast_seo_redirect_table->search_box( __( 'Search', 'wordpress-seo-premium' ), 'wpseo-redirect-search' );
$yoast_seo_redirect_table->display();
?>
</form>
</div>

View File

@@ -0,0 +1,76 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Views
*
* @uses array $view_vars {
* @type string redirect_table The file path to show in the notices.
* @type string nonce The nonce.
* @type WPSEO_Redirect_Form_Presenter form_presenter Instance of the WPSEO_Redirect_Form_Presenter class.
* @type string origin_from_url The redirect origin.
* @type WPSEO_Redirect_Quick_Edit_Presenter quick_edit_table Instance of the WPSEO_Redirect_Quick_Edit_Presenter class.
* }
*/
$yoast_seo_redirect_table = $view_vars['redirect_table'];
$yoast_seo_nonce = $view_vars['nonce'];
$yoast_seo_form_presenter = $view_vars['form_presenter'];
$yoast_seo_origin_from_url = $view_vars['origin_from_url'];
$yoast_seo_quick_edit_table = $view_vars['quick_edit_table'];
?>
<div id="table-regex" class="tab-url redirect-table-tab">
<?php echo '<h2>' . esc_html__( 'Regular Expression redirects', 'wordpress-seo-premium' ) . '</h2>'; ?>
<p>
<?php
printf(
/* translators: 1: opens a link to a related help center article. 2: closes the link. */
esc_html__( 'Regular Expression (regex) Redirects are extremely powerful redirects. You should only use them if you know what you are doing. %1$sRead more about regex redirects on our help center%2$s.', 'wordpress-seo-premium' ),
'<a href="https://yoa.st/3lo" target="_blank">',
'</a>'
);
?>
</p>
<form class='wpseo-new-redirect-form' method='post'>
<div class='wpseo_redirect_form'>
<?php
$yoast_seo_form_presenter->display(
[
'input_suffix' => '',
'values' => [
'origin' => $yoast_seo_origin_from_url,
'target' => '',
'type' => '',
],
]
);
?>
<button type="button" class="button button-primary"><?php esc_html_e( 'Add Redirect', 'wordpress-seo-premium' ); ?></button>
</div>
</form>
<p class='desc'>&nbsp;</p>
<?php
$yoast_seo_quick_edit_table->display(
[
'form_presenter' => $yoast_seo_form_presenter,
'total_columns' => $yoast_seo_redirect_table->count_columns(),
]
);
?>
<form id='regex' class='wpseo-redirects-table-form' method='post'>
<input type='hidden' class="wpseo_redirects_ajax_nonce" name='wpseo_redirects_ajax_nonce' value='<?php echo esc_attr( $yoast_seo_nonce ); ?>' />
<?php
// The list table.
$yoast_seo_redirect_table->prepare_items();
$yoast_seo_redirect_table->search_box( __( 'Search', 'wordpress-seo-premium' ), 'wpseo-redirect-search' );
$yoast_seo_redirect_table->display();
?>
</form>
</div>

Some files were not shown because too many files have changed in this diff Show More