PDF rausgenommen

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

View File

@ -0,0 +1,404 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
if ( ! defined( 'WPSEO_VERSION' ) ) {
header( 'Status: 403 Forbidden' );
header( 'HTTP/1.1 403 Forbidden' );
exit();
}
/**
* @todo this whole thing should probably be a proper class.
*/
/**
* Convenience function to JSON encode and echo results and then die.
*
* @param array $results Results array for encoding.
*/
function wpseo_ajax_json_echo_die( $results ) {
echo WPSEO_Utils::format_json_encode( $results );
die();
}
/**
* Function used from AJAX calls, takes it variables from $_POST, dies on exit.
*/
function wpseo_set_option() {
if ( ! current_user_can( 'manage_options' ) ) {
die( '-1' );
}
check_ajax_referer( 'wpseo-setoption' );
$option = sanitize_text_field( filter_input( INPUT_POST, 'option' ) );
if ( $option !== 'page_comments' ) {
die( '-1' );
}
update_option( $option, 0 );
die( '1' );
}
add_action( 'wp_ajax_wpseo_set_option', 'wpseo_set_option' );
/**
* Since 3.2 Notifications are dismissed in the Notification Center.
*/
add_action( 'wp_ajax_yoast_dismiss_notification', array( 'Yoast_Notification_Center', 'ajax_dismiss_notification' ) );
/**
* Function used to remove the admin notices for several purposes, dies on exit.
*/
function wpseo_set_ignore() {
if ( ! current_user_can( 'manage_options' ) ) {
die( '-1' );
}
check_ajax_referer( 'wpseo-ignore' );
$ignore_key = sanitize_text_field( filter_input( INPUT_POST, 'option' ) );
WPSEO_Options::set( 'ignore_' . $ignore_key, true );
die( '1' );
}
add_action( 'wp_ajax_wpseo_set_ignore', 'wpseo_set_ignore' );
/**
* Hides the default tagline notice for a specific user.
*/
function wpseo_dismiss_tagline_notice() {
if ( ! current_user_can( 'manage_options' ) ) {
die( '-1' );
}
check_ajax_referer( 'wpseo-dismiss-tagline-notice' );
update_user_meta( get_current_user_id(), 'wpseo_seen_tagline_notice', 'seen' );
die( '1' );
}
add_action( 'wp_ajax_wpseo_dismiss_tagline_notice', 'wpseo_dismiss_tagline_notice' );
/**
* Used in the editor to replace vars for the snippet preview.
*/
function wpseo_ajax_replace_vars() {
global $post;
check_ajax_referer( 'wpseo-replace-vars' );
$post = get_post( intval( filter_input( INPUT_POST, 'post_id' ) ) );
global $wp_query;
$wp_query->queried_object = $post;
$wp_query->queried_object_id = $post->ID;
$omit = array( 'excerpt', 'excerpt_only', 'title' );
echo wpseo_replace_vars( stripslashes( filter_input( INPUT_POST, 'string' ) ), $post, $omit );
die;
}
add_action( 'wp_ajax_wpseo_replace_vars', 'wpseo_ajax_replace_vars' );
/**
* Save an individual SEO title from the Bulk Editor.
*/
function wpseo_save_title() {
wpseo_save_what( 'title' );
}
add_action( 'wp_ajax_wpseo_save_title', 'wpseo_save_title' );
/**
* Save an individual meta description from the Bulk Editor.
*/
function wpseo_save_description() {
wpseo_save_what( 'metadesc' );
}
add_action( 'wp_ajax_wpseo_save_metadesc', 'wpseo_save_description' );
/**
* Save titles & descriptions.
*
* @param string $what Type of item to save (title, description).
*/
function wpseo_save_what( $what ) {
check_ajax_referer( 'wpseo-bulk-editor' );
$new = filter_input( INPUT_POST, 'new_value' );
$post_id = intval( filter_input( INPUT_POST, 'wpseo_post_id' ) );
$original = filter_input( INPUT_POST, 'existing_value' );
$results = wpseo_upsert_new( $what, $post_id, $new, $original );
wpseo_ajax_json_echo_die( $results );
}
/**
* Helper function to update a post's meta data, returning relevant information
* about the information updated and the results or the meta update.
*
* @param int $post_id Post ID.
* @param string $new_meta_value New meta value to record.
* @param string $orig_meta_value Original meta value.
* @param string $meta_key Meta key string.
* @param string $return_key Return key string to use in results.
*
* @return string
*/
function wpseo_upsert_meta( $post_id, $new_meta_value, $orig_meta_value, $meta_key, $return_key ) {
$post_id = intval( $post_id );
$sanitized_new_meta_value = wp_strip_all_tags( $new_meta_value );
$orig_meta_value = wp_strip_all_tags( $orig_meta_value );
$upsert_results = array(
'status' => 'success',
'post_id' => $post_id,
"new_{$return_key}" => $sanitized_new_meta_value,
"original_{$return_key}" => $orig_meta_value,
);
$the_post = get_post( $post_id );
if ( empty( $the_post ) ) {
$upsert_results['status'] = 'failure';
$upsert_results['results'] = __( 'Post doesn\'t exist.', 'wordpress-seo' );
return $upsert_results;
}
$post_type_object = get_post_type_object( $the_post->post_type );
if ( ! $post_type_object ) {
$upsert_results['status'] = 'failure';
$upsert_results['results'] = sprintf(
/* translators: %s expands to post type. */
__( 'Post has an invalid Content Type: %s.', 'wordpress-seo' ),
$the_post->post_type
);
return $upsert_results;
}
if ( ! current_user_can( $post_type_object->cap->edit_posts ) ) {
$upsert_results['status'] = 'failure';
$upsert_results['results'] = sprintf(
/* translators: %s expands to post type name. */
__( 'You can\'t edit %s.', 'wordpress-seo' ),
$post_type_object->label
);
return $upsert_results;
}
if ( ! current_user_can( $post_type_object->cap->edit_others_posts ) && (int) $the_post->post_author !== get_current_user_id() ) {
$upsert_results['status'] = 'failure';
$upsert_results['results'] = sprintf(
/* translators: %s expands to the name of a post type (plural). */
__( 'You can\'t edit %s that aren\'t yours.', 'wordpress-seo' ),
$post_type_object->label
);
return $upsert_results;
}
if ( $sanitized_new_meta_value === $orig_meta_value && $sanitized_new_meta_value !== $new_meta_value ) {
$upsert_results['status'] = 'failure';
$upsert_results['results'] = __( 'You have used HTML in your value which is not allowed.', 'wordpress-seo' );
return $upsert_results;
}
$res = update_post_meta( $post_id, $meta_key, $sanitized_new_meta_value );
$upsert_results['status'] = ( $res !== false ) ? 'success' : 'failure';
$upsert_results['results'] = $res;
return $upsert_results;
}
/**
* Save all titles sent from the Bulk Editor.
*/
function wpseo_save_all_titles() {
wpseo_save_all( 'title' );
}
add_action( 'wp_ajax_wpseo_save_all_titles', 'wpseo_save_all_titles' );
/**
* Save all description sent from the Bulk Editor.
*/
function wpseo_save_all_descriptions() {
wpseo_save_all( 'metadesc' );
}
add_action( 'wp_ajax_wpseo_save_all_descriptions', 'wpseo_save_all_descriptions' );
/**
* Utility function to save values.
*
* @param string $what Type of item so save.
*/
function wpseo_save_all( $what ) {
check_ajax_referer( 'wpseo-bulk-editor' );
$results = array();
if ( ! isset( $_POST['items'], $_POST['existingItems'] ) ) {
wpseo_ajax_json_echo_die( $results );
}
$new_values = array_map( array( 'WPSEO_Utils', 'sanitize_text_field' ), wp_unslash( (array) $_POST['items'] ) );
$original_values = array_map( array( 'WPSEO_Utils', 'sanitize_text_field' ), wp_unslash( (array) $_POST['existingItems'] ) );
foreach ( $new_values as $post_id => $new_value ) {
$original_value = $original_values[ $post_id ];
$results[] = wpseo_upsert_new( $what, $post_id, $new_value, $original_value );
}
wpseo_ajax_json_echo_die( $results );
}
/**
* Insert a new value.
*
* @param string $what Item type (such as title).
* @param int $post_id Post ID.
* @param string $new New value to record.
* @param string $original Original value.
*
* @return string
*/
function wpseo_upsert_new( $what, $post_id, $new, $original ) {
$meta_key = WPSEO_Meta::$meta_prefix . $what;
return wpseo_upsert_meta( $post_id, $new, $original, $meta_key, $what );
}
/**
* Retrieves the keyword for the keyword doubles.
*/
function ajax_get_keyword_usage() {
$post_id = filter_input( INPUT_POST, 'post_id' );
$keyword = filter_input( INPUT_POST, 'keyword' );
if ( ! current_user_can( 'edit_post', $post_id ) ) {
die( '-1' );
}
wp_die(
WPSEO_Utils::format_json_encode( WPSEO_Meta::keyword_usage( $keyword, $post_id ) )
);
}
add_action( 'wp_ajax_get_focus_keyword_usage', 'ajax_get_keyword_usage' );
/**
* Retrieves the keyword for the keyword doubles of the termpages.
*/
function ajax_get_term_keyword_usage() {
$post_id = filter_input( INPUT_POST, 'post_id' );
$keyword = filter_input( INPUT_POST, 'keyword' );
$taxonomy_name = filter_input( INPUT_POST, 'taxonomy' );
$taxonomy = get_taxonomy( $taxonomy_name );
if ( ! $taxonomy ) {
wp_die( 0 );
}
if ( ! current_user_can( $taxonomy->cap->edit_terms ) ) {
wp_die( -1 );
}
$usage = WPSEO_Taxonomy_Meta::get_keyword_usage( $keyword, $post_id, $taxonomy_name );
// Normalize the result so it it the same as the post keyword usage AJAX request.
$usage = $usage[ $keyword ];
wp_die(
WPSEO_Utils::format_json_encode( $usage )
);
}
add_action( 'wp_ajax_get_term_keyword_usage', 'ajax_get_term_keyword_usage' );
/**
* Registers hooks for all AJAX integrations.
*
* @return void
*/
function wpseo_register_ajax_integrations() {
$integrations = array( new Yoast_Network_Admin() );
foreach ( $integrations as $integration ) {
$integration->register_ajax_hooks();
}
}
wpseo_register_ajax_integrations();
// Crawl Issue Manager AJAX hooks.
new WPSEO_GSC_Ajax();
// SEO Score Recalculations.
new WPSEO_Recalculate_Scores_Ajax();
new Yoast_OnPage_Ajax();
new WPSEO_Shortcode_Filter();
new WPSEO_Taxonomy_Columns();
// Setting the notice for the recalculate the posts.
new Yoast_Dismissable_Notice_Ajax( 'recalculate', Yoast_Dismissable_Notice_Ajax::FOR_SITE );
/* ********************* DEPRECATED FUNCTIONS ********************* */
/**
* Removes stopword from the sample permalink that is generated in an AJAX request.
*
* @deprecated 6.3
* @codeCoverageIgnore
*/
function wpseo_remove_stopwords_sample_permalink() {
_deprecated_function( __FUNCTION__, 'WPSEO 6.3', 'This method is deprecated.' );
wpseo_ajax_json_echo_die( '' );
}
/**
* Function used to delete blocking files, dies on exit.
*
* @deprecated 7.0
* @codeCoverageIgnore
*/
function wpseo_kill_blocking_files() {
_deprecated_function( __FUNCTION__, 'WPSEO 7.0', 'This method is deprecated.' );
wpseo_ajax_json_echo_die( '' );
}
/**
* Handles the posting of a new FB admin.
*
* @deprecated 7.1
* @codeCoverageIgnore
*/
function wpseo_add_fb_admin() {
if ( ! current_user_can( 'manage_options' ) ) {
die( '-1' );
}
_deprecated_function( __FUNCTION__, 'WPSEO 7.0', 'This method is deprecated.' );
wpseo_ajax_json_echo_die( '' );
}

View File

@ -0,0 +1,121 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Ajax
*/
/**
* Class WPSEO_Recalculate_Scores.
*
* This class handles the SEO score recalculation for all posts with a filled focus keyword.
*/
class WPSEO_Recalculate_Scores_Ajax {
/**
* Initialize the AJAX hooks.
*/
public function __construct() {
add_action( 'wp_ajax_wpseo_recalculate_scores', array( $this, 'recalculate_scores' ) );
add_action( 'wp_ajax_wpseo_update_score', array( $this, 'save_score' ) );
add_action( 'wp_ajax_wpseo_recalculate_total', array( $this, 'get_total' ) );
}
/**
* Get the totals for the posts and the terms.
*/
public function get_total() {
check_ajax_referer( 'wpseo_recalculate', 'nonce' );
wp_die(
WPSEO_Utils::format_json_encode(
array(
'posts' => $this->calculate_posts(),
'terms' => $this->calculate_terms(),
)
)
);
}
/**
* Start recalculation.
*/
public function recalculate_scores() {
check_ajax_referer( 'wpseo_recalculate', 'nonce' );
$fetch_object = $this->get_fetch_object();
if ( ! empty( $fetch_object ) ) {
$paged = filter_input( INPUT_POST, 'paged', FILTER_VALIDATE_INT );
$response = $fetch_object->get_items_to_recalculate( $paged );
if ( ! empty( $response ) ) {
wp_die( WPSEO_Utils::format_json_encode( $response ) );
}
}
wp_die( '' );
}
/**
* Saves the new linkdex score for given post.
*/
public function save_score() {
check_ajax_referer( 'wpseo_recalculate', 'nonce' );
$fetch_object = $this->get_fetch_object();
if ( ! empty( $fetch_object ) ) {
$scores = filter_input( INPUT_POST, 'scores', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
$fetch_object->save_scores( $scores );
}
wp_die();
}
/**
* Returns the needed object for recalculating scores.
*
* @return WPSEO_Recalculate_Posts|WPSEO_Recalculate_Terms
*/
private function get_fetch_object() {
switch ( filter_input( INPUT_POST, 'type' ) ) {
case 'post':
return new WPSEO_Recalculate_Posts();
case 'term':
return new WPSEO_Recalculate_Terms();
}
return null;
}
/**
* Gets the total number of posts.
*
* @return int
*/
private function calculate_posts() {
$count_posts_query = new WP_Query(
array(
'post_type' => 'any',
'meta_key' => '_yoast_wpseo_focuskw',
'posts_per_page' => 1,
'fields' => 'ids',
)
);
return $count_posts_query->found_posts;
}
/**
* Get the total number of terms.
*
* @return int
*/
private function calculate_terms() {
$total = 0;
foreach ( get_taxonomies( array(), 'objects' ) as $taxonomy ) {
$total += wp_count_terms( $taxonomy->name, array( 'hide_empty' => false ) );
}
return $total;
}
}

View File

@ -0,0 +1,41 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Ajax
*/
/**
* Class WPSEO_Shortcode_Filter.
*
* Used for parsing WP shortcodes with AJAX.
*/
class WPSEO_Shortcode_Filter {
/**
* Initialize the AJAX hooks.
*/
public function __construct() {
add_action( 'wp_ajax_wpseo_filter_shortcodes', array( $this, 'do_filter' ) );
}
/**
* Parse the shortcodes.
*/
public function do_filter() {
check_ajax_referer( 'wpseo-filter-shortcodes', 'nonce' );
$shortcodes = filter_input( INPUT_POST, 'data', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
$parsed_shortcodes = array();
foreach ( $shortcodes as $shortcode ) {
$parsed_shortcodes[] = array(
'shortcode' => $shortcode,
'output' => do_shortcode( $shortcode ),
);
}
wp_die( WPSEO_Utils::format_json_encode( $parsed_shortcodes ) );
}
}

View File

@ -0,0 +1,92 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Ajax
*/
/**
* This class will catch the request to dismiss the target notice (set by notice_name)
* and will store the dismiss status as an user meta in the database.
*/
class Yoast_Dismissable_Notice_Ajax {
/**
* Notice type toggle value for user notices.
*
* @var string
*/
const FOR_USER = 'user_meta';
/**
* Notice type toggle value for network notices.
*
* @var string
*/
const FOR_NETWORK = 'site_option';
/**
* Notice type toggle value for site notices.
*
* @var string
*/
const FOR_SITE = 'option';
/**
* Name of the notice that will be dismissed.
*
* @var string
*/
private $notice_name;
/**
* The type of the current notice.
*
* @var string
*/
private $notice_type;
/**
* Initialize the hooks for the AJAX request.
*
* @param string $notice_name The name for the hook to catch the notice.
* @param string $notice_type The notice type.
*/
public function __construct( $notice_name, $notice_type = self::FOR_USER ) {
$this->notice_name = $notice_name;
$this->notice_type = $notice_type;
add_action( 'wp_ajax_wpseo_dismiss_' . $notice_name, array( $this, 'dismiss_notice' ) );
}
/**
* Handles the dismiss notice request.
*/
public function dismiss_notice() {
check_ajax_referer( 'wpseo-dismiss-' . $this->notice_name );
$this->save_dismissed();
wp_die( 'true' );
}
/**
* Storing the dismissed value in the database. The target location is based on the set notification type.
*/
private function save_dismissed() {
if ( $this->notice_type === self::FOR_SITE ) {
update_option( 'wpseo_dismiss_' . $this->notice_name, 1 );
return;
}
if ( $this->notice_type === self::FOR_NETWORK ) {
update_site_option( 'wpseo_dismiss_' . $this->notice_name, 1 );
return;
}
update_user_meta( get_current_user_id(), 'wpseo_dismiss_' . $this->notice_name, 1 );
}
}

View File

@ -0,0 +1,40 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Ajax
*/
/**
* Class Yoast_OnPage_Ajax.
*
* This class will catch the request to dismiss the Ryte notice and will store
* the dismiss status as an user meta in the database.
*/
class Yoast_OnPage_Ajax {
/**
* Initialize the hooks for the AJAX request.
*/
public function __construct() {
add_action( 'wp_ajax_wpseo_dismiss_onpageorg', array( $this, 'dismiss_notice' ) );
}
/**
* Handles the dismiss notice request.
*/
public function dismiss_notice() {
check_ajax_referer( 'wpseo-dismiss-onpageorg' );
$this->save_dismissed();
wp_die( 'true' );
}
/**
* Storing the dismissed value as an user option in the database.
*/
private function save_dismissed() {
update_user_meta( get_current_user_id(), WPSEO_OnPage::USER_META_KEY, 1 );
}
}

View File

@ -0,0 +1,107 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Ajax
*/
/**
* Class Yoast_Plugin_Conflict_Ajax.
*/
class Yoast_Plugin_Conflict_Ajax {
/**
* @var string
*/
private $option_name = 'wpseo_dismissed_conflicts';
/**
* @var array
*/
private $dismissed_conflicts = array();
/**
* Initialize the hooks for the AJAX request.
*/
public function __construct() {
add_action( 'wp_ajax_wpseo_dismiss_plugin_conflict', array( $this, 'dismiss_notice' ) );
}
/**
* Handles the dismiss notice request.
*/
public function dismiss_notice() {
check_ajax_referer( 'dismiss-plugin-conflict' );
$conflict_data = filter_input( INPUT_POST, 'data', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
$this->dismissed_conflicts = $this->get_dismissed_conflicts( $conflict_data['section'] );
$this->compare_plugins( $conflict_data['plugins'] );
$this->save_dismissed_conflicts( $conflict_data['section'] );
wp_die( 'true' );
}
/**
* Getting the user option from the database.
*
* @return bool|array
*/
private function get_dismissed_option() {
return get_user_meta( get_current_user_id(), $this->option_name, true );
}
/**
* Getting the dismissed conflicts from the database
*
* @param string $plugin_section Type of conflict group (such as Open Graph or sitemap).
*
* @return array
*/
private function get_dismissed_conflicts( $plugin_section ) {
$dismissed_conflicts = $this->get_dismissed_option();
if ( is_array( $dismissed_conflicts ) && array_key_exists( $plugin_section, $dismissed_conflicts ) ) {
return $dismissed_conflicts[ $plugin_section ];
}
return array();
}
/**
* Storing the conflicting plugins as an user option in the database.
*
* @param string $plugin_section Plugin conflict type (such as Open Graph or sitemap).
*/
private function save_dismissed_conflicts( $plugin_section ) {
$dismissed_conflicts = $this->get_dismissed_option();
$dismissed_conflicts[ $plugin_section ] = $this->dismissed_conflicts;
update_user_meta( get_current_user_id(), $this->option_name, $dismissed_conflicts );
}
/**
* Loop through the plugins to compare them with the already stored dismissed plugin conflicts.
*
* @param array $posted_plugins Plugin set to check.
*/
public function compare_plugins( array $posted_plugins ) {
foreach ( $posted_plugins as $posted_plugin ) {
$this->compare_plugin( $posted_plugin );
}
}
/**
* Check if plugin is already dismissed, if not store it in the array that will be saved later.
*
* @param string $posted_plugin Plugin to check against dismissed conflicts.
*/
private function compare_plugin( $posted_plugin ) {
if ( ! in_array( $posted_plugin, $this->dismissed_conflicts, true ) ) {
$this->dismissed_conflicts[] = $posted_plugin;
}
}
}

View File

@ -0,0 +1,89 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Capabilities
*/
/**
* Abstract Capability Manager shared code.
*/
abstract class WPSEO_Abstract_Capability_Manager implements WPSEO_Capability_Manager {
/**
* Registered capabilities.
*
* @var array
*/
protected $capabilities = array();
/**
* Registers a capability.
*
* @param string $capability Capability to register.
* @param array $roles Roles to add the capability to.
* @param bool $overwrite Optional. Use add or overwrite as registration method.
*/
public function register( $capability, array $roles, $overwrite = false ) {
if ( $overwrite || ! isset( $this->capabilities[ $capability ] ) ) {
$this->capabilities[ $capability ] = $roles;
return;
}
// Combine configurations.
$this->capabilities[ $capability ] = array_merge( $roles, $this->capabilities[ $capability ] );
// Remove doubles.
$this->capabilities[ $capability ] = array_unique( $this->capabilities[ $capability ] );
}
/**
* Returns the list of registered capabilitities.
*
* @return string[] Registered capabilities.
*/
public function get_capabilities() {
return array_keys( $this->capabilities );
}
/**
* Returns a list of WP_Role roles.
*
* The string array of role names are converted to actual WP_Role objects.
* These are needed to be able to use the API on them.
*
* @param array $roles Roles to retrieve the objects for.
*
* @return WP_Role[] List of WP_Role objects.
*/
protected function get_wp_roles( array $roles ) {
$wp_roles = array_map( 'get_role', $roles );
return array_filter( $wp_roles );
}
/**
* Filter capability roles.
*
* @param string $capability Capability to filter roles for.
* @param array $roles List of roles which can be filtered.
*
* @return array Filtered list of roles for the capability.
*/
protected function filter_roles( $capability, array $roles ) {
/**
* Filter: Allow changing roles that a capability is added to.
*
* @api array $roles The default roles to be filtered.
*/
$filtered = apply_filters( $capability . '_roles', $roles );
// Make sure we have the expected type.
if ( ! is_array( $filtered ) ) {
return array();
}
return $filtered;
}
}

View File

@ -0,0 +1,33 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Capabilities
*/
/**
* Capability Manager Factory.
*/
class WPSEO_Capability_Manager_Factory {
/**
* Returns the Manager to use.
*
* @return WPSEO_Capability_Manager Manager to use.
*/
public static function get() {
static $manager = null;
if ( $manager === null ) {
if ( function_exists( 'wpcom_vip_add_role_caps' ) ) {
$manager = new WPSEO_Capability_Manager_VIP();
}
if ( ! function_exists( 'wpcom_vip_add_role_caps' ) ) {
$manager = new WPSEO_Capability_Manager_WP();
}
}
return $manager;
}
}

View File

@ -0,0 +1,119 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Capabilities
*/
/**
* Integrates Yoast SEO capabilities with third party role manager plugins.
*
* Integrates with: Members
* Integrates with: User Role Editor
*/
class WPSEO_Capability_Manager_Integration implements WPSEO_WordPress_Integration {
/**
* Capability manager to use.
*
* @var WPSEO_Capability_Manager
*/
public $manager;
/**
* WPSEO_Capability_Manager_Integration constructor.
*
* @param WPSEO_Capability_Manager $manager The capability manager to use.
*/
public function __construct( WPSEO_Capability_Manager $manager ) {
$this->manager = $manager;
}
/**
* Registers the hooks.
*
* @return void
*/
public function register_hooks() {
add_filter( 'members_get_capabilities', array( $this, 'get_capabilities' ) );
add_action( 'members_register_cap_groups', array( $this, 'action_members_register_cap_group' ) );
add_filter( 'ure_capabilities_groups_tree', array( $this, 'filter_ure_capabilities_groups_tree' ) );
add_filter( 'ure_custom_capability_groups', array( $this, 'filter_ure_custom_capability_groups' ), 10, 2 );
}
/**
* Get the Yoast SEO capabilities.
* Optionally append them to an existing array.
*
* @param array $caps Optional existing capability list.
* @return array
*/
public function get_capabilities( array $caps = array() ) {
if ( ! did_action( 'wpseo_register_capabilities' ) ) {
do_action( 'wpseo_register_capabilities' );
}
return array_merge( $caps, $this->manager->get_capabilities() );
}
/**
* Add capabilities to its own group in the Members plugin.
*
* @see members_register_cap_group()
*/
public function action_members_register_cap_group() {
if ( ! function_exists( 'members_register_cap_group' ) ) {
return;
}
// Register the yoast group.
$args = array(
'label' => esc_html__( 'Yoast SEO', 'wordpress-seo' ),
'caps' => $this->get_capabilities(),
'icon' => 'dashicons-admin-plugins',
'diff_added' => true,
);
members_register_cap_group( 'wordpress-seo', $args );
}
/**
* Adds Yoast SEO capability group in the User Role Editor plugin.
*
* @see URE_Capabilities_Groups_Manager::get_groups_tree()
*
* @param array $groups Current groups.
*
* @return array Filtered list of capabilty groups.
*/
public function filter_ure_capabilities_groups_tree( $groups = array() ) {
$groups = (array) $groups;
$groups['wordpress-seo'] = array(
'caption' => 'Yoast SEO',
'parent' => 'custom',
'level' => 3,
);
return $groups;
}
/**
* Adds capabilities to the Yoast SEO group in the User Role Editor plugin.
*
* @see URE_Capabilities_Groups_Manager::get_cap_groups()
*
* @param array $groups Current capability groups.
* @param string $cap_id Capability identifier.
*
* @return array List of filtered groups.
*/
public function filter_ure_custom_capability_groups( $groups = array(), $cap_id = '' ) {
if ( in_array( $cap_id, $this->get_capabilities(), true ) ) {
$groups = (array) $groups;
$groups[] = 'wordpress-seo';
}
return $groups;
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Capabilities
*/
/**
* VIP implementation of the Capability Manager.
*/
final class WPSEO_Capability_Manager_VIP extends WPSEO_Abstract_Capability_Manager {
/**
* Adds the registered capabilities to the system.
*
* @return void
*/
public function add() {
$role_capabilities = array();
foreach ( $this->capabilities as $capability => $roles ) {
$role_capabilities = $this->get_role_capabilities( $role_capabilities, $capability, $roles );
}
foreach ( $role_capabilities as $role => $capabilities ) {
wpcom_vip_add_role_caps( $role, $capabilities );
}
}
/**
* Removes the registered capabilities from the system
*
* @return void
*/
public function remove() {
// Remove from any role it has been added to.
$roles = wp_roles()->get_names();
$roles = array_keys( $roles );
$role_capabilities = array();
foreach ( array_keys( $this->capabilities ) as $capability ) {
// Allow filtering of roles.
$role_capabilities = $this->get_role_capabilities( $role_capabilities, $capability, $roles );
}
foreach ( $role_capabilities as $role => $capabilities ) {
wpcom_vip_remove_role_caps( $role, $capabilities );
}
}
/**
* Returns the roles which the capability is registered on.
*
* @param array $role_capabilities List of all roles with their capabilities.
* @param string $capability Capability to filter roles for.
* @param array $roles List of default roles.
*
* @return array List of capabilities.
*/
protected function get_role_capabilities( $role_capabilities, $capability, $roles ) {
// Allow filtering of roles.
$filtered_roles = $this->filter_roles( $capability, $roles );
foreach ( $filtered_roles as $role ) {
if ( ! isset( $add_role_caps[ $role ] ) ) {
$role_capabilities[ $role ] = array();
}
$role_capabilities[ $role ][] = $capability;
}
return $role_capabilities;
}
}

View File

@ -0,0 +1,51 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Capabilities
*/
/**
* Default WordPress capability manager implementation.
*/
final class WPSEO_Capability_Manager_WP extends WPSEO_Abstract_Capability_Manager {
/**
* Adds the capabilities to the roles.
*
* @return void
*/
public function add() {
foreach ( $this->capabilities as $capability => $roles ) {
$filtered_roles = $this->filter_roles( $capability, $roles );
$wp_roles = $this->get_wp_roles( $filtered_roles );
foreach ( $wp_roles as $wp_role ) {
$wp_role->add_cap( $capability );
}
}
}
/**
* Unregisters the capabilities from the system.
*
* @return void
*/
public function remove() {
// Remove from any roles it has been added to.
$roles = wp_roles()->get_names();
$roles = array_keys( $roles );
foreach ( $this->capabilities as $capability => $_roles ) {
$registered_roles = array_unique( array_merge( $roles, $this->capabilities[ $capability ] ) );
// Allow filtering of roles.
$filtered_roles = $this->filter_roles( $capability, $registered_roles );
$wp_roles = $this->get_wp_roles( $filtered_roles );
foreach ( $wp_roles as $wp_role ) {
$wp_role->remove_cap( $capability );
}
}
}
}

View File

@ -0,0 +1,38 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Capabilities
*/
/**
* Capability Manager interface.
*/
interface WPSEO_Capability_Manager {
/**
* Registers a capability.
*
* @param string $capability Capability to register.
* @param array $roles Roles to add the capability to.
* @param bool $overwrite Optional. Use add or overwrite as registration method.
*/
public function register( $capability, array $roles, $overwrite = false );
/**
* Adds the registerd capabilities to the system.
*/
public function add();
/**
* Removes the registered capabilities from the system.
*/
public function remove();
/**
* Returns the list of registered capabilities.
*
* @return string[] List of registered capabilities.
*/
public function get_capabilities();
}

View File

@ -0,0 +1,55 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Capabilities
*/
/**
* Capability Utils collection.
*/
class WPSEO_Capability_Utils {
/**
* Checks if the user has the proper capabilities.
*
* @param string $capability Capability to check.
*
* @return bool True if the user has the proper rights.
*/
public static function current_user_can( $capability ) {
if ( $capability === 'wpseo_manage_options' ) {
return self::has( $capability );
}
return self::has_any( array( 'wpseo_manage_options', $capability ) );
}
/**
* Checks if the current user has at least one of the supplied capabilities.
*
* @param array $capabilities Capabilities to check against.
*
* @return bool True if the user has at least one capability.
*/
protected static function has_any( array $capabilities ) {
foreach ( $capabilities as $capability ) {
if ( self::has( $capability ) ) {
return true;
}
}
return false;
}
/**
* Checks if the user has a certain capability.
*
* @param string $capability Capability to check against.
*
* @return bool True if the user has the capability.
*/
protected static function has( $capability ) {
return current_user_can( $capability );
}
}

View File

@ -0,0 +1,76 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Capabilities
*/
/**
* Capabilities registration class.
*/
class WPSEO_Register_Capabilities implements WPSEO_WordPress_Integration {
/**
* Registers the hooks.
*
* @return void
*/
public function register_hooks() {
add_action( 'wpseo_register_capabilities', array( $this, 'register' ) );
if ( is_multisite() ) {
add_action( 'user_has_cap', array( $this, 'filter_user_has_wpseo_manage_options_cap' ), 10, 4 );
}
}
/**
* Registers the capabilities.
*
* @return void
*/
public function register() {
$manager = WPSEO_Capability_Manager_Factory::get();
$manager->register( 'wpseo_bulk_edit', array( 'editor', 'wpseo_editor', 'wpseo_manager' ) );
$manager->register( 'wpseo_edit_advanced_metadata', array( 'wpseo_editor', 'wpseo_manager' ) );
$manager->register( 'wpseo_manage_options', array( 'administrator', 'wpseo_manager' ) );
}
/**
* Revokes the 'wpseo_manage_options' capability from administrator users if it should only
* only be granted to network administrators.
*
* @param array $allcaps An array of all the user's capabilities.
* @param array $caps Actual capabilities being checked.
* @param array $args Optional parameters passed to has_cap(), typically object ID.
* @param WP_User $user The user object.
*
* @return array Possibly modified array of the user's capabilities.
*/
public function filter_user_has_wpseo_manage_options_cap( $allcaps, $caps, $args, $user ) {
// We only need to do something if 'wpseo_manage_options' is being checked.
if ( ! in_array( 'wpseo_manage_options', $caps, true ) ) {
return $allcaps;
}
// If the user does not have 'wpseo_manage_options' anyway, we don't need to revoke access.
if ( empty( $allcaps['wpseo_manage_options'] ) ) {
return $allcaps;
}
// If the user does not have 'delete_users', they are not an administrator.
if ( empty( $allcaps['delete_users'] ) ) {
return $allcaps;
}
$options = WPSEO_Options::get_instance();
if ( $options->get( 'access' ) === 'superadmin' && ! is_super_admin( $user->ID ) ) {
unset( $allcaps['wpseo_manage_options'] );
}
return $allcaps;
}
}

View File

@ -0,0 +1,63 @@
<?php
/**
* @package WPSEO\Admin
*/
/**
* Class to print out the translatable strings for the Add Keyword modal.
*/
class WPSEO_Add_Keyword_Modal {
/**
* Returns the translations for the Add Keyword modal.
*
* These strings are not escaped because they're meant to be used with React
* which already takes care of that. If used in PHP, they should be escaped.
*
* @return array Translated text strings for the Add Keyword modal.
*/
public function get_translations() {
return array(
'title' => __( 'Would you like to add more than one keyphrase?', 'wordpress-seo' ),
'intro' => sprintf(
/* translators: %1$s expands to a 'Yoast SEO Premium' text linked to the yoast.com website. */
__( 'Great news: you can, with %1$s!', 'wordpress-seo' ),
'{{link}}Yoast SEO Premium{{/link}}'
),
'link' => WPSEO_Shortlinker::get( 'https://yoa.st/pe-premium-page' ),
'other' => sprintf(
/* translators: %s expands to 'Yoast SEO Premium'. */
__( 'Other benefits of %s for you:', 'wordpress-seo' ),
'Yoast SEO Premium'
),
'buylink' => WPSEO_Shortlinker::get( 'https://yoa.st/add-keywords-popup' ),
'buy' => sprintf(
/* translators: %s expands to 'Yoast SEO Premium'. */
__( 'Get %s', 'wordpress-seo' ),
'Yoast SEO Premium'
),
'small' => __( '1 year free support and updates included!', 'wordpress-seo' ),
'a11yNotice.opensInNewTab' => __( '(Opens in a new browser tab)', 'wordpress-seo' ),
);
}
/**
* Passes translations to JS for the Add Keyword modal component.
*
* @return array Translated text strings for the Add Keyword modal component.
*/
public function get_translations_for_js() {
$translations = $this->get_translations();
return array(
'locale' => WPSEO_Language_Utils::get_user_locale(),
'intl' => $translations,
);
}
/**
* Prints the localized Add Keyword modal translations for JS.
*/
public function enqueue_translations() {
wp_localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'admin-global-script', 'yoastAddKeywordModalL10n', $this->get_translations_for_js() );
}
}

View File

@ -0,0 +1,71 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Represents a way to determine the analysis worker asset location.
*/
final class WPSEO_Admin_Asset_Analysis_Worker_Location implements WPSEO_Admin_Asset_Location {
/**
* @var WPSEO_Admin_Asset_Location $asset_location.
*/
private $asset_location;
/**
* @var WPSEO_Admin_Asset $asset.
*/
private $asset;
/**
* Constructs the location of the analysis worker asset.
*
* @param string $flat_version The flat version of the asset.
* @param string $name The name of the analysis worker asset.
*/
public function __construct( $flat_version = '', $name = 'analysis-worker' ) {
if ( $flat_version === '' ) {
$asset_manager = new WPSEO_Admin_Asset_Manager();
$flat_version = $asset_manager->flatten_version( WPSEO_VERSION );
}
$analysis_worker = 'wp-seo-' . $name . '-' . $flat_version;
$this->asset_location = WPSEO_Admin_Asset_Manager::create_default_location();
$this->asset = new WPSEO_Admin_Asset(
array(
'name' => $name,
'src' => $analysis_worker,
)
);
}
/**
* Retrieves the analysis worker asset.
*
* @return WPSEO_Admin_Asset The analysis worker asset.
*/
public function get_asset() {
return $this->asset;
}
/**
* Determines the URL of the asset on the dev server.
*
* @param WPSEO_Admin_Asset $asset The asset to determine the URL for.
* @param string $type The type of asset. Usually JS or CSS.
*
* @return string The URL of the asset.
*/
public function get_url( WPSEO_Admin_Asset $asset, $type ) {
$scheme = wp_parse_url( $asset->get_src(), PHP_URL_SCHEME );
if ( in_array( $scheme, array( 'http', 'https' ), true ) ) {
return $asset->get_src();
}
return $this->asset_location->get_url( $asset, $type );
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Changes the asset paths to dev server paths.
*/
final class WPSEO_Admin_Asset_Dev_Server_Location implements WPSEO_Admin_Asset_Location {
/**
* @var string
*/
const DEFAULT_URL = 'http://localhost:8080';
/**
* @var string
*/
private $url;
/**
* @param string $url Where the dev server is located.
*/
public function __construct( $url = null ) {
if ( $url === null ) {
$url = self::DEFAULT_URL;
}
$this->url = $url;
}
/**
* Determines the URL of the asset on the dev server.
*
* @param WPSEO_Admin_Asset $asset The asset to determine the URL for.
* @param string $type The type of asset. Usually JS or CSS.
*
* @return string The URL of the asset.
*/
public function get_url( WPSEO_Admin_Asset $asset, $type ) {
if ( WPSEO_Admin_Asset::TYPE_CSS === $type ) {
return $this->get_default_url( $asset, $type );
}
$asset_manager = new WPSEO_Admin_Asset_Manager();
$flat_version = $asset_manager->flatten_version( WPSEO_VERSION );
$version_less_source = str_replace( '-' . $flat_version, '', $asset->get_src() );
if ( false !== strpos( $version_less_source, 'select2' ) ) {
return $this->get_default_url( $asset, $type );
}
$path = sprintf( '%s%s.js', $asset->get_src(), $asset->get_suffix() );
return trailingslashit( $this->url ) . $path;
}
/**
* Determines the URL of the asset not using the dev server.
*
* @param WPSEO_Admin_Asset $asset The asset to determine the URL for.
* @param string $type The type of asset.
*
* @return string The URL of the asset file.
*/
public function get_default_url( WPSEO_Admin_Asset $asset, $type ) {
$default_location = new WPSEO_Admin_Asset_SEO_Location( WPSEO_FILE );
return $default_location->get_url( $asset, $type );
}
}

View File

@ -0,0 +1,22 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Represents a way to determine an assets location.
*/
interface WPSEO_Admin_Asset_Location {
/**
* Determines the URL of the asset on the dev server.
*
* @param WPSEO_Admin_Asset $asset The asset to determine the URL for.
* @param string $type The type of asset. Usually JS or CSS.
*
* @return string The URL of the asset.
*/
public function get_url( WPSEO_Admin_Asset $asset, $type );
}

View File

@ -0,0 +1,771 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* This class registers all the necessary styles and scripts.
*
* Also has methods for the enqueing of scripts and styles.
* It automatically adds a prefix to the handle.
*/
class WPSEO_Admin_Asset_Manager {
/**
* @var WPSEO_Admin_Asset_Location
*/
protected $asset_location;
/**
* Prefix for naming the assets.
*
* @var string
*/
const PREFIX = 'yoast-seo-';
/**
* Prefix for naming the assets.
*
* @var string
*/
private $prefix;
/**
* Constructs a manager of assets. Needs a location to know where to register assets at.
*
* @param WPSEO_Admin_Asset_Location $asset_location The provider of the asset location.
* @param string $prefix The prefix for naming assets.
*/
public function __construct( WPSEO_Admin_Asset_Location $asset_location = null, $prefix = self::PREFIX ) {
if ( $asset_location === null ) {
$asset_location = self::create_default_location();
}
$this->asset_location = $asset_location;
$this->prefix = $prefix;
}
/**
* Enqueues scripts.
*
* @param string $script The name of the script to enqueue.
*/
public function enqueue_script( $script ) {
wp_enqueue_script( $this->prefix . $script );
}
/**
* Enqueues styles.
*
* @param string $style The name of the style to enqueue.
*/
public function enqueue_style( $style ) {
wp_enqueue_style( $this->prefix . $style );
}
/**
* Registers scripts based on it's parameters.
*
* @param WPSEO_Admin_Asset $script The script to register.
*/
public function register_script( WPSEO_Admin_Asset $script ) {
wp_register_script(
$this->prefix . $script->get_name(),
$this->get_url( $script, WPSEO_Admin_Asset::TYPE_JS ),
$script->get_deps(),
$script->get_version(),
$script->is_in_footer()
);
}
/**
* Registers styles based on it's parameters.
*
* @param WPSEO_Admin_Asset $style The style to register.
*/
public function register_style( WPSEO_Admin_Asset $style ) {
wp_register_style(
$this->prefix . $style->get_name(),
$this->get_url( $style, WPSEO_Admin_Asset::TYPE_CSS ),
$style->get_deps(),
$style->get_version(),
$style->get_media()
);
}
/**
* Calls the functions that register scripts and styles with the scripts and styles to be registered as arguments.
*/
public function register_assets() {
$this->register_scripts( $this->scripts_to_be_registered() );
$this->register_styles( $this->styles_to_be_registered() );
}
/**
* Registers all the scripts passed to it.
*
* @param array $scripts The scripts passed to it.
*/
public function register_scripts( $scripts ) {
foreach ( $scripts as $script ) {
$script = new WPSEO_Admin_Asset( $script );
$this->register_script( $script );
}
}
/**
* Registers all the styles it receives.
*
* @param array $styles Styles that need to be registered.
*/
public function register_styles( $styles ) {
foreach ( $styles as $style ) {
$style = new WPSEO_Admin_Asset( $style );
$this->register_style( $style );
}
}
/**
* A list of styles that shouldn't be registered but are needed in other locations in the plugin.
*
* @return array
*/
public function special_styles() {
$flat_version = $this->flatten_version( WPSEO_VERSION );
$asset_args = array(
'name' => 'inside-editor',
'src' => 'inside-editor-' . $flat_version,
);
return array( 'inside-editor' => new WPSEO_Admin_Asset( $asset_args ) );
}
/**
* Flattens a version number for use in a filename.
*
* @param string $version The original version number.
*
* @return string The flattened version number.
*/
public function flatten_version( $version ) {
$parts = explode( '.', $version );
if ( count( $parts ) === 2 && preg_match( '/^\d+$/', $parts[1] ) === 1 ) {
$parts[] = '0';
}
return implode( '', $parts );
}
/**
* Creates a default location object for use in the admin asset manager.
*
* @return WPSEO_Admin_Asset_Location The location to use in the asset manager.
*/
public static function create_default_location() {
if ( defined( 'YOAST_SEO_DEV_SERVER' ) && YOAST_SEO_DEV_SERVER ) {
$url = defined( 'YOAST_SEO_DEV_SERVER_URL' ) ? YOAST_SEO_DEV_SERVER_URL : WPSEO_Admin_Asset_Dev_Server_Location::DEFAULT_URL;
return new WPSEO_Admin_Asset_Dev_Server_Location( $url );
}
return new WPSEO_Admin_Asset_SEO_Location( WPSEO_FILE );
}
/**
* Registers the WordPress dependencies that exist in 5.0 in case they are not present.
*
* This function can be removed when WordPress 5.1 has been released, because from 5.0 wp-elements will be
* registered earlier, which means we don't have to reregister things.
*
* @return void
*/
public function register_wp_assets() {
global $wp_scripts;
$script = $wp_scripts->query( 'react' );
// IE11 needs wp-polyfill to be registered before react.
if ( $script && ! in_array( 'wp-polyfill', $script->deps, true ) ) {
$script->deps[] = 'wp-polyfill';
}
$flat_version = $this->flatten_version( WPSEO_VERSION );
wp_register_script(
'react',
plugins_url( 'js/vendor/react.min.js', WPSEO_FILE ),
array(),
'v16.6.1',
true
);
wp_register_script(
'react-dom',
plugins_url( 'js/vendor/react-dom.min.js', WPSEO_FILE ),
array( 'react' ),
'v16.6.1',
true
);
wp_register_script(
'lodash-base',
plugins_url( 'js/vendor/lodash.min.js', WPSEO_FILE ),
array(),
'4.17.5',
true
);
wp_register_script(
'lodash',
plugins_url( 'js/vendor/lodash-noconflict.js', WPSEO_FILE ),
array( 'lodash-base' ),
WPSEO_VERSION,
true
);
wp_register_script(
'wp-polyfill',
plugins_url( 'js/dist/babel-polyfill-' . $flat_version . '.min.js', WPSEO_FILE ),
array(),
WPSEO_VERSION,
true
);
wp_register_script(
'wp-element',
plugins_url( 'js/dist/wp-element-' . $flat_version . '.min.js', WPSEO_FILE ),
array( 'lodash', 'wp-polyfill', 'react', 'react-dom' ),
WPSEO_VERSION,
true
);
wp_register_script(
'wp-api-fetch',
plugins_url( 'js/dist/wp-apiFetch-' . $flat_version . '.min.js', WPSEO_FILE ),
array( 'wp-i18n', 'wp-polyfill' ),
WPSEO_VERSION,
true
);
wp_register_script(
'wp-components',
plugins_url( 'js/dist/wp-components-' . $flat_version . '.min.js', WPSEO_FILE ),
array( 'lodash', 'wp-api-fetch', 'wp-i18n', 'wp-polyfill', 'wp-compose' ),
WPSEO_VERSION,
true
);
wp_register_script(
'wp-data',
plugins_url( 'js/dist/wp-data-' . $flat_version . '.min.js', WPSEO_FILE ),
array( 'lodash', 'wp-element', 'wp-polyfill', 'wp-compose' ),
WPSEO_VERSION,
true
);
wp_register_script(
'wp-i18n',
plugins_url( 'js/dist/wp-i18n-' . $flat_version . '.min.js', WPSEO_FILE ),
array( 'wp-polyfill' ),
WPSEO_VERSION,
true
);
wp_register_script(
'wp-rich-text',
plugins_url( 'js/dist/wp-rich-text-' . $flat_version . '.min.js', WPSEO_FILE ),
array( 'lodash', 'wp-polyfill', 'wp-data' ),
WPSEO_VERSION,
true
);
wp_register_script(
'wp-compose',
plugins_url( 'js/dist/wp-compose-' . $flat_version . '.min.js', WPSEO_FILE ),
array( 'lodash', 'wp-polyfill' ),
WPSEO_VERSION,
true
);
/*
* wp-annotations only exists from Gutenberg 4.3 and onwards, so we register a no-op in earlier versions.
* The no-op achieves that our scripts that depend on this are actually loaded. Because WordPress doesn't
* load a script if any of the dependencies are missing.
*
* @phpcs:disable WordPress.WP.EnqueuedResourceParameters -- The no-op does not require these settings.
*/
wp_register_script(
'wp-annotations',
null
);
// phpcs:enable -- End of disable.
}
/**
* Returns the scripts that need to be registered.
*
* @todo Data format is not self-documenting. Needs explanation inline. R.
*
* @return array The scripts that need to be registered.
*/
protected function scripts_to_be_registered() {
$select2_language = 'en';
$user_locale = WPSEO_Language_Utils::get_user_locale();
$language = WPSEO_Language_Utils::get_language( $user_locale );
if ( file_exists( WPSEO_PATH . "js/dist/select2/i18n/{$user_locale}.js" ) ) {
$select2_language = $user_locale; // Chinese and some others use full locale.
}
elseif ( file_exists( WPSEO_PATH . "js/dist/select2/i18n/{$language}.js" ) ) {
$select2_language = $language;
}
$flat_version = $this->flatten_version( WPSEO_VERSION );
return array(
array(
'name' => 'commons',
// Load webpack-commons for bundle support.
'src' => 'commons-' . $flat_version,
'deps' => array(
'wp-polyfill',
),
),
array(
'name' => 'search-appearance',
'src' => 'search-appearance-' . $flat_version,
'deps' => array(
'wp-api',
self::PREFIX . 'components',
self::PREFIX . 'commons',
),
),
array(
'name' => 'yoast-modal',
'src' => 'wp-seo-modal-' . $flat_version,
'deps' => array(
'jquery',
'wp-element',
'wp-i18n',
self::PREFIX . 'components',
self::PREFIX . 'commons',
),
),
array(
'name' => 'help-center',
'src' => 'wp-seo-help-center-' . $flat_version,
'deps' => array(
'jquery',
'wp-element',
'wp-i18n',
self::PREFIX . 'components',
self::PREFIX . 'commons',
),
),
array(
'name' => 'admin-script',
'src' => 'wp-seo-admin-' . $flat_version,
'deps' => array(
'jquery',
'jquery-ui-core',
'jquery-ui-progressbar',
self::PREFIX . 'select2',
self::PREFIX . 'select2-translations',
self::PREFIX . 'commons',
),
),
array(
'name' => 'admin-media',
'src' => 'wp-seo-admin-media-' . $flat_version,
'deps' => array(
'jquery',
'jquery-ui-core',
self::PREFIX . 'commons',
),
),
array(
'name' => 'network-admin-script',
'src' => 'wp-seo-network-admin-' . $flat_version,
'deps' => array(
'jquery',
self::PREFIX . 'commons',
),
),
array(
'name' => 'bulk-editor',
'src' => 'wp-seo-bulk-editor-' . $flat_version,
'deps' => array(
'jquery',
self::PREFIX . 'commons',
),
),
array(
'name' => 'admin-global-script',
'src' => 'wp-seo-admin-global-' . $flat_version,
'deps' => array(
'jquery',
self::PREFIX . 'commons',
),
),
array(
'name' => 'metabox',
'src' => 'wp-seo-metabox-' . $flat_version,
'deps' => array(
'jquery',
'wp-element',
'wp-i18n',
'wp-data',
'wp-components',
self::PREFIX . 'select2',
self::PREFIX . 'select2-translations',
self::PREFIX . 'commons',
),
'in_footer' => false,
),
array(
'name' => 'featured-image',
'src' => 'wp-seo-featured-image-' . $flat_version,
'deps' => array(
'jquery',
self::PREFIX . 'commons',
),
),
array(
'name' => 'admin-gsc',
'src' => 'wp-seo-admin-gsc-' . $flat_version,
'deps' => array(
'wp-element',
'wp-i18n',
self::PREFIX . 'styled-components',
self::PREFIX . 'components',
self::PREFIX . 'commons',
),
'in_footer' => false,
),
array(
'name' => 'post-scraper',
'src' => 'wp-seo-post-scraper-' . $flat_version,
'deps' => array(
'wp-util',
'wp-api',
'wp-element',
'wp-i18n',
'wp-data',
'wp-api-fetch',
'wp-annotations',
'wp-compose',
self::PREFIX . 'replacevar-plugin',
self::PREFIX . 'shortcode-plugin',
self::PREFIX . 'analysis',
self::PREFIX . 'components',
self::PREFIX . 'commons',
),
),
array(
'name' => 'term-scraper',
'src' => 'wp-seo-term-scraper-' . $flat_version,
'deps' => array(
'wp-element',
'wp-i18n',
'wp-data',
'wp-api-fetch',
'wp-compose',
self::PREFIX . 'replacevar-plugin',
self::PREFIX . 'analysis',
self::PREFIX . 'components',
self::PREFIX . 'commons',
),
),
array(
'name' => 'replacevar-plugin',
'src' => 'wp-seo-replacevar-plugin-' . $flat_version,
'deps' => array(
self::PREFIX . 'analysis',
self::PREFIX . 'components',
self::PREFIX . 'commons',
),
),
array(
'name' => 'shortcode-plugin',
'src' => 'wp-seo-shortcode-plugin-' . $flat_version,
'deps' => array(
self::PREFIX . 'analysis',
self::PREFIX . 'commons',
),
),
array(
'name' => 'recalculate',
'src' => 'wp-seo-recalculate-' . $flat_version,
'deps' => array(
'jquery',
'jquery-ui-core',
'jquery-ui-progressbar',
self::PREFIX . 'analysis',
self::PREFIX . 'commons',
),
),
array(
'name' => 'primary-category',
'src' => 'wp-seo-metabox-category-' . $flat_version,
'deps' => array(
'jquery',
'wp-util',
'wp-element',
'wp-i18n',
'wp-components',
'wp-data',
self::PREFIX . 'analysis',
self::PREFIX . 'components',
self::PREFIX . 'commons',
),
),
array(
'name' => 'select2',
'src' => 'select2/select2.full',
'suffix' => '.min',
'deps' => array(
'jquery',
),
'version' => '4.0.3',
),
array(
'name' => 'select2-translations',
'src' => 'select2/i18n/' . $select2_language,
'deps' => array(
'jquery',
self::PREFIX . 'select2',
),
'version' => '4.0.3',
'suffix' => '',
),
array(
'name' => 'configuration-wizard',
'src' => 'configuration-wizard-' . $flat_version,
'deps' => array(
'jquery',
'wp-element',
'wp-i18n',
'wp-api',
self::PREFIX . 'components',
self::PREFIX . 'commons',
),
),
array(
'name' => 'reindex-links',
'src' => 'wp-seo-reindex-links-' . $flat_version,
'deps' => array(
'jquery',
'jquery-ui-core',
'jquery-ui-progressbar',
self::PREFIX . 'commons',
),
),
array(
'name' => 'edit-page-script',
'src' => 'wp-seo-edit-page-' . $flat_version,
'deps' => array(
'jquery',
self::PREFIX . 'commons',
),
),
array(
'name' => 'quick-edit-handler',
'src' => 'wp-seo-quick-edit-handler-' . $flat_version,
'deps' => array(
'jquery',
self::PREFIX . 'commons',
),
'in_footer' => true,
),
array(
'name' => 'api',
'src' => 'wp-seo-api-' . $flat_version,
'deps' => array(
'wp-api',
'jquery',
self::PREFIX . 'commons',
),
),
array(
'name' => 'dashboard-widget',
'src' => 'wp-seo-dashboard-widget-' . $flat_version,
'deps' => array(
self::PREFIX . 'api',
'jquery',
'wp-element',
'wp-i18n',
self::PREFIX . 'components',
self::PREFIX . 'commons',
),
),
array(
'name' => 'filter-explanation',
'src' => 'wp-seo-filter-explanation-' . $flat_version,
'deps' => array(
'jquery',
self::PREFIX . 'commons',
),
),
array(
'name' => 'analysis',
'src' => 'analysis-' . $flat_version,
'deps' => array(
'lodash',
self::PREFIX . 'commons',
),
),
array(
'name' => 'components',
'src' => 'components-' . $flat_version,
'deps' => array(
self::PREFIX . 'analysis',
self::PREFIX . 'styled-components',
self::PREFIX . 'commons',
),
),
array(
'name' => 'structured-data-blocks',
'src' => 'wp-seo-structured-data-blocks-' . $flat_version,
'deps' => array(
'wp-blocks',
'wp-i18n',
'wp-element',
self::PREFIX . 'styled-components',
self::PREFIX . 'commons',
),
),
array(
'name' => 'styled-components',
'src' => 'styled-components-' . $flat_version,
'deps' => array(
'wp-element',
),
),
array(
'name' => 'courses-overview',
'src' => 'wp-seo-courses-overview-' . $flat_version,
'deps' => array(
'wp-element',
'wp-i18n',
self::PREFIX . 'styled-components',
self::PREFIX . 'components',
),
),
);
}
/**
* Returns the styles that need to be registered.
*
* @todo Data format is not self-documenting. Needs explanation inline. R.
*
* @return array Styles that need to be registered.
*/
protected function styles_to_be_registered() {
$flat_version = $this->flatten_version( WPSEO_VERSION );
return array(
array(
'name' => 'admin-css',
'src' => 'yst_plugin_tools-' . $flat_version,
'deps' => array( self::PREFIX . 'toggle-switch' ),
),
array(
'name' => 'toggle-switch',
'src' => 'toggle-switch-' . $flat_version,
),
array(
'name' => 'dismissible',
'src' => 'wpseo-dismissible-' . $flat_version,
),
array(
'name' => 'alerts',
'src' => 'alerts-' . $flat_version,
),
array(
'name' => 'edit-page',
'src' => 'edit-page-' . $flat_version,
),
array(
'name' => 'featured-image',
'src' => 'featured-image-' . $flat_version,
),
array(
'name' => 'metabox-css',
'src' => 'metabox-' . $flat_version,
'deps' => array(
self::PREFIX . 'select2',
),
),
array(
'name' => 'wp-dashboard',
'src' => 'dashboard-' . $flat_version,
),
array(
'name' => 'scoring',
'src' => 'yst_seo_score-' . $flat_version,
),
array(
'name' => 'adminbar',
'src' => 'adminbar-' . $flat_version,
),
array(
'name' => 'primary-category',
'src' => 'metabox-primary-category-' . $flat_version,
),
array(
'name' => 'select2',
'src' => 'select2/select2',
'suffix' => '.min',
'version' => '4.0.1',
'rtl' => false,
),
array(
'name' => 'admin-global',
'src' => 'admin-global-' . $flat_version,
),
array(
'name' => 'yoast-components',
'src' => 'yoast-components-' . $flat_version,
),
array(
'name' => 'extensions',
'src' => 'yoast-extensions-' . $flat_version,
),
array(
'name' => 'filter-explanation',
'src' => 'filter-explanation-' . $flat_version,
),
array(
'name' => 'search-appearance',
'src' => 'search-appearance-' . $flat_version,
),
array(
'name' => 'structured-data-blocks',
'src' => 'structured-data-blocks-' . $flat_version,
'deps' => array( 'wp-edit-blocks' ),
),
);
}
/**
* Determines the URL of the asset.
*
* @param WPSEO_Admin_Asset $asset The asset to determine the URL for.
* @param string $type The type of asset. Usually JS or CSS.
*
* @return string The URL of the asset.
*/
protected function get_url( WPSEO_Admin_Asset $asset, $type ) {
$scheme = wp_parse_url( $asset->get_src(), PHP_URL_SCHEME );
if ( in_array( $scheme, array( 'http', 'https' ), true ) ) {
return $asset->get_src();
}
return $this->asset_location->get_url( $asset, $type );
}
}

View File

@ -0,0 +1,97 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Determines the location of an asset within the SEO plugin.
*/
final class WPSEO_Admin_Asset_SEO_Location implements WPSEO_Admin_Asset_Location {
/**
* @var string
*/
protected $plugin_file;
/**
* The plugin file to base the asset location upon.
*
* @param string $plugin_file The plugin file string.
*/
public function __construct( $plugin_file ) {
$this->plugin_file = $plugin_file;
}
/**
* Determines the URL of the asset on the dev server.
*
* @param WPSEO_Admin_Asset $asset The asset to determine the URL for.
* @param string $type The type of asset. Usually JS or CSS.
*
* @return string The URL of the asset.
*/
public function get_url( WPSEO_Admin_Asset $asset, $type ) {
$path = $this->get_path( $asset, $type );
if ( empty( $path ) ) {
return '';
}
if ( YOAST_ENVIRONMENT !== 'development' && ! $asset->get_suffix() ) {
$plugin_path = plugin_dir_path( $this->plugin_file );
if ( ! file_exists( $plugin_path . $path ) ) {
// Give a notice to the user in the console (only once).
WPSEO_Utils::javascript_console_notification(
'Development Files',
sprintf(
/* translators: %1$s resolves to https://github.com/Yoast/wordpress-seo */
__( 'You are trying to load non-minified files. These are only available in our development package. Check out %1$s to see all the source files.', 'wordpress-seo' ),
'https://github.com/Yoast/wordpress-seo'
),
true
);
// Just load the .min file.
$path = $this->get_path( $asset, $type, '.min' );
}
}
return plugins_url( $path, $this->plugin_file );
}
/**
* Determines the path relative to the plugin folder of an asset.
*
* @param WPSEO_Admin_Asset $asset The asset to determine the path
* for.
* @param string $type The type of asset.
* @param string|null $force_suffix The suffix to force, or null when
* to use the default suffix.
*
* @return string The path to the asset file.
*/
protected function get_path( WPSEO_Admin_Asset $asset, $type, $force_suffix = null ) {
$relative_path = '';
$rtl_suffix = '';
$suffix = ( $force_suffix === null ) ? $asset->get_suffix() : $force_suffix;
switch ( $type ) {
case WPSEO_Admin_Asset::TYPE_JS:
$relative_path = 'js/dist/' . $asset->get_src() . $suffix . '.js';
break;
case WPSEO_Admin_Asset::TYPE_CSS:
// Path and suffix for RTL stylesheets.
if ( function_exists( 'is_rtl' ) && is_rtl() && $asset->has_rtl() ) {
$rtl_suffix = '-rtl';
}
$relative_path = 'css/dist/' . $asset->get_src() . $rtl_suffix . $suffix . '.css';
break;
}
return $relative_path;
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* @package WPSEO\Admin
*/
/**
* Localizes JavaScript files.
*/
final class WPSEO_Admin_Asset_Yoast_Components_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 = array(
'yoast-components' => $this->get_translations( 'yoast-components' ),
'wordpress-seo' => $this->get_translations( 'wordpress-seojs' ),
);
wp_localize_script( $script_handle, 'wpseoYoastJSL10n', $translations );
}
/**
* Returns translations necessary for JS files.
*
* @param string $component The component to retrieve the translations for.
* @return object The translations in a Jed format for JS files.
*/
protected function get_translations( $component ) {
$locale = WPSEO_Language_Utils::get_user_locale();
$file = plugin_dir_path( WPSEO_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,225 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Determines the editor specific replacement variables.
*/
class WPSEO_Admin_Editor_Specific_Replace_Vars {
/**
* @var array The editor specific replacement variables.
*/
protected $replacement_variables = array(
// Posts types.
'page' => array( 'id', 'pt_single', 'pt_plural', 'parent_title' ),
'post' => array( 'id', 'term404', 'pt_single', 'pt_plural' ),
// Custom post type.
'custom_post_type' => array( 'id', 'term404', 'pt_single', 'pt_plural', 'parent_title' ),
// Settings - archive pages.
'custom-post-type_archive' => array( 'pt_single', 'pt_plural' ),
// Taxonomies.
'category' => array( 'term_title', 'term_description', 'category_description', 'parent_title' ),
'post_tag' => array( 'term_title', 'term_description', 'tag_description' ),
'post_format' => array(),
// Custom taxonomy.
'term-in-custom-taxonomy' => array( 'term_title', 'term_description', 'category_description', 'parent_title' ),
// Settings - special pages.
'search' => array( 'searchphrase' ),
);
/**
* WPSEO_Admin_Editor_Specific_Replace_Vars constructor.
*/
public function __construct() {
$this->add_for_page_types(
array( 'page', 'post', 'custom_post_type' ),
WPSEO_Custom_Fields::get_custom_fields()
);
$this->add_for_page_types(
array( 'post', 'term-in-custom-taxonomies' ),
WPSEO_Custom_Taxonomies::get_custom_taxonomies()
);
}
/**
* Retrieves the editor specific replacement variables.
*
* @return array The editor specific replacement variables.
*/
public function get() {
/**
* Filter: Adds the possibility to add extra editor specific replacement variables.
*
* @api array $replacement_variables Empty array to add the editor specific replace vars to.
*/
$replacement_variables = apply_filters(
'wpseo_editor_specific_replace_vars',
$this->replacement_variables
);
if ( ! is_array( $replacement_variables ) ) {
$replacement_variables = $this->replacement_variables;
}
return array_filter( $replacement_variables, 'is_array' );
}
/**
* Retrieves the generic replacement variable names.
*
* Which are the replacement variables without the editor specific ones.
*
* @param array $replacement_variables Possibly generic replacement variables.
*
* @return array The generic replacement variable names.
*/
public function get_generic( $replacement_variables ) {
$shared_variables = array_diff(
$this->extract_names( $replacement_variables ),
$this->get_unique_replacement_variables()
);
return array_values( $shared_variables );
}
/**
* Determines the page type of the current term.
*
* @param string $taxonomy The taxonomy name.
*
* @return string The page type.
*/
public function determine_for_term( $taxonomy ) {
$replacement_variables = $this->get();
if ( array_key_exists( $taxonomy, $replacement_variables ) ) {
return $taxonomy;
}
return 'term-in-custom-taxonomy';
}
/**
* Determines the page type of the current post.
*
* @param WP_Post $post A WordPress post instance.
*
* @return string The page type.
*/
public function determine_for_post( $post ) {
if ( $post instanceof WP_Post === false ) {
return 'post';
}
$replacement_variables = $this->get();
if ( array_key_exists( $post->post_type, $replacement_variables ) ) {
return $post->post_type;
}
return 'custom_post_type';
}
/**
* Determines the page type for a post type.
*
* @param string $post_type The name of the post_type.
* @param string $fallback The page type to fall back to.
*
* @return string The page type.
*/
public function determine_for_post_type( $post_type, $fallback = 'custom_post_type' ) {
if ( ! $this->has_for_page_type( $post_type ) ) {
return $fallback;
}
return $post_type;
}
/**
* Determines the page type for an archive page.
*
* @param string $name The name of the archive.
* @param string $fallback The page type to fall back to.
*
* @return string The page type.
*/
public function determine_for_archive( $name, $fallback = 'custom-post-type_archive' ) {
$page_type = $name . '_archive';
if ( ! $this->has_for_page_type( $page_type ) ) {
return $fallback;
}
return $page_type;
}
/**
* Adds the replavement variables for the given page types.
*
* @param array $page_types Page types to add variables for.
* @param array $replacement_variables_to_add The variables to add.
*
* @return void
*/
protected function add_for_page_types( array $page_types, array $replacement_variables_to_add ) {
if ( empty( $replacement_variables_to_add ) ) {
return;
}
$replacement_variables_to_add = array_fill_keys( $page_types, $replacement_variables_to_add );
$replacement_variables = $this->replacement_variables;
$this->replacement_variables = array_merge_recursive( $replacement_variables, $replacement_variables_to_add );
}
/**
* Extracts the names from the given replacements variables.
*
* @param array $replacement_variables Replacement variables to extract the name from.
*
* @return array Extracted names.
*/
protected function extract_names( $replacement_variables ) {
$extracted_names = array();
foreach ( $replacement_variables as $replacement_variable ) {
if ( empty( $replacement_variable['name'] ) ) {
continue;
}
$extracted_names[] = $replacement_variable['name'];
}
return $extracted_names;
}
/**
* Returns whether the given page type has editor specific replace vars.
*
* @param string $page_type The page type to check.
*
* @return bool True if there are associated editor specific replace vars.
*/
protected function has_for_page_type( $page_type ) {
$replacement_variables = $this->get();
return ( ! empty( $replacement_variables[ $page_type ] ) && is_array( $replacement_variables[ $page_type ] ) );
}
/**
* Merges all editor specific replacement variables into one array and removes duplicates.
*
* @return array The list of unique editor specific replacement variables.
*/
protected function get_unique_replacement_variables() {
$merged_replacement_variables = call_user_func_array( 'array_merge', $this->get() );
return array_unique( $merged_replacement_variables );
}
}

View File

@ -0,0 +1,89 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Handles the Gutenberg Compatibility notification showing and hiding.
*/
class WPSEO_Admin_Gutenberg_Compatibility_Notification implements WPSEO_WordPress_Integration {
/**
* Notification ID to use.
*
* @var string
*/
private $notification_id = 'wpseo-outdated-gutenberg-plugin';
/**
* @var WPSEO_Gutenberg_Compatibility
*/
private $compatibility_checker;
/**
* @var Yoast_Notification_Center
*/
private $notification_center;
/**
* WPSEO_Admin_Gutenberg_Compatibility_Notification constructor.
*/
public function __construct() {
$this->compatibility_checker = new WPSEO_Gutenberg_Compatibility();
$this->notification_center = Yoast_Notification_Center::get();
}
/**
* Registers all hooks to WordPress.
*
* @return void
*/
public function register_hooks() {
add_action( 'admin_init', array( $this, 'manage_notification' ) );
}
/**
* Manages if the notification should be shown or removed.
*
* @return void
*/
public function manage_notification() {
if ( ! $this->compatibility_checker->is_installed() || $this->compatibility_checker->is_fully_compatible() ) {
$this->notification_center->remove_notification_by_id( $this->notification_id );
return;
}
$this->add_notification();
}
/**
* Adds the notification to the notificaton center.
*
* @return void
*/
private function add_notification() {
$level = $this->compatibility_checker->is_below_minimum() ? Yoast_Notification::ERROR : Yoast_Notification::WARNING;
$message = sprintf(
/* translators: %1$s expands to Yoast SEO, %2$s expands to the installed version, %3$s expands to Gutenberg */
__( '%1$s detected you are using version %2$s of %3$s, please update to the latest version to prevent compatibility issues.', 'wordpress-seo' ),
'Yoast SEO',
$this->compatibility_checker->get_installed_version(),
'Gutenberg'
);
$notification = new Yoast_Notification(
$message,
array(
'id' => $this->notification_id,
'type' => $level,
'priority' => 1,
)
);
$this->notification_center->add_notification( $notification );
}
}

View File

@ -0,0 +1,94 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Generates the HTML for an inline Help Button and Panel.
*/
class WPSEO_Admin_Help_Panel {
/**
* @var string
*/
private $id;
/**
* @var string
*/
private $help_button_text;
/**
* @var string
*/
private $help_content;
/**
* @var string
*/
private $wrapper;
/**
* Constructor.
*
* @param string $id Unique identifier of the element the inline help refers to, used as an identifier in the html.
* @param string $help_button_text The Help Button text. Needs a properly escaped string.
* @param string $help_content The Help Panel content. Needs a properly escaped string (might contain HTML).
* @param string $wrapper Optional Whether to print out a container div element for the Help Panel, used for styling.
* Pass a `has-wrapper` value to print out the container. Default: no container.
*/
public function __construct( $id, $help_button_text, $help_content, $wrapper = '' ) {
$this->id = $id;
$this->help_button_text = $help_button_text;
$this->help_content = $help_content;
$this->wrapper = $wrapper;
}
/**
* Returns the html for the Help Button.
*
* @return string
*/
public function get_button_html() {
if ( ! $this->id || ! $this->help_button_text || ! $this->help_content ) {
return '';
}
return sprintf(
' <button type="button" class="yoast_help yoast-help-button dashicons" id="%1$s-help-toggle" aria-expanded="false" aria-controls="%1$s-help"><span class="yoast-help-icon" aria-hidden="true"></span><span class="screen-reader-text">%2$s</span></button>',
esc_attr( $this->id ),
$this->help_button_text
);
}
/**
* Returns the html for the Help Panel.
*
* @return string
*/
public function get_panel_html() {
if ( ! $this->id || ! $this->help_button_text || ! $this->help_content ) {
return '';
}
$wrapper_start = '';
$wrapper_end = '';
if ( 'has-wrapper' === $this->wrapper ) {
$wrapper_start = '<div class="yoast-seo-help-container">';
$wrapper_end = '</div>';
}
return sprintf(
'%1$s<p id="%2$s-help" class="yoast-help-panel">%3$s</p>%4$s',
$wrapper_start,
esc_attr( $this->id ),
$this->help_content,
$wrapper_end
);
}
}

View File

@ -0,0 +1,675 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Performs the load on admin side.
*/
class WPSEO_Admin_Init {
/**
* Holds the global `$pagenow` variable's value.
*
* @var string
*/
private $pagenow;
/**
* Holds the asset manager.
*
* @var WPSEO_Admin_Asset_Manager
*/
private $asset_manager;
/**
* Class constructor.
*/
public function __construct() {
$GLOBALS['wpseo_admin'] = new WPSEO_Admin();
$this->pagenow = $GLOBALS['pagenow'];
$this->asset_manager = new WPSEO_Admin_Asset_Manager();
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_dismissible' ) );
add_action( 'admin_init', array( $this, 'tagline_notice' ), 15 );
add_action( 'admin_init', array( $this, 'blog_public_notice' ), 15 );
add_action( 'admin_init', array( $this, 'permalink_notice' ), 15 );
add_action( 'admin_init', array( $this, 'page_comments_notice' ), 15 );
add_action( 'admin_init', array( $this, 'ga_compatibility_notice' ), 15 );
add_action( 'admin_init', array( $this, 'yoast_plugin_compatibility_notification' ), 15 );
add_action( 'admin_init', array( $this, 'yoast_plugin_suggestions_notification' ), 15 );
add_action( 'admin_init', array( $this, 'recalculate_notice' ), 15 );
add_action( 'admin_init', array( $this, 'unsupported_php_notice' ), 15 );
add_action( 'admin_init', array( $this->asset_manager, 'register_assets' ) );
add_action( 'admin_init', array( $this, 'show_hook_deprecation_warnings' ) );
add_action( 'admin_init', array( 'WPSEO_Plugin_Conflict', 'hook_check_for_plugin_conflicts' ) );
add_action( 'admin_init', array( $this, 'handle_notifications' ), 15 );
add_action( 'admin_notices', array( $this, 'permalink_settings_notice' ) );
add_action( 'admin_enqueue_scripts', array( $this->asset_manager, 'register_wp_assets' ), PHP_INT_MAX );
$listeners = array();
$listeners[] = new WPSEO_Post_Type_Archive_Notification_Handler();
/** @var WPSEO_Listener $listener */
foreach ( $listeners as $listener ) {
$listener->listen();
}
$this->load_meta_boxes();
$this->load_taxonomy_class();
$this->load_admin_page_class();
$this->load_admin_user_class();
$this->load_xml_sitemaps_admin();
$this->load_plugin_suggestions();
}
/**
* Handles the notifiers for the dashboard page.
*
* @return void
*/
public function handle_notifications() {
/**
* @var WPSEO_Notification_Handler[] $handlers
*/
$handlers = array();
$handlers[] = new WPSEO_Post_Type_Archive_Notification_Handler();
$notification_center = Yoast_Notification_Center::get();
foreach ( $handlers as $handler ) {
$handler->handle( $notification_center );
}
}
/**
* Enqueue our styling for dismissible yoast notifications.
*/
public function enqueue_dismissible() {
$this->asset_manager->enqueue_style( 'dismissible' );
}
/**
* Notify about the default tagline if the user hasn't changed it.
*/
public function tagline_notice() {
$query_args = array(
'autofocus[control]' => 'blogdescription',
);
$customize_url = add_query_arg( $query_args, wp_customize_url() );
$info_message = sprintf(
/* translators: 1: link open tag; 2: link close tag. */
__( 'You still have the default WordPress tagline, even an empty one is probably better. %1$sYou can fix this in the customizer%2$s.', 'wordpress-seo' ),
'<a href="' . esc_attr( $customize_url ) . '">',
'</a>'
);
$notification_options = array(
'type' => Yoast_Notification::ERROR,
'id' => 'wpseo-dismiss-tagline-notice',
'capabilities' => 'wpseo_manage_options',
);
$tagline_notification = new Yoast_Notification( $info_message, $notification_options );
$notification_center = Yoast_Notification_Center::get();
$notification_center->remove_notification( $tagline_notification );
}
/**
* Add an alert if the blog is not publicly visible.
*/
public function blog_public_notice() {
$info_message = '<strong>' . __( 'Huge SEO Issue: You\'re blocking access to robots.', 'wordpress-seo' ) . '</strong> ';
$info_message .= sprintf(
/* translators: %1$s resolves to the opening tag of the link to the reading settings, %1$s resolves to the closing tag for the link */
__( 'You must %1$sgo to your Reading Settings%2$s and uncheck the box for Search Engine Visibility.', 'wordpress-seo' ),
'<a href="' . esc_url( admin_url( 'options-reading.php' ) ) . '">',
'</a>'
);
$notification_options = array(
'type' => Yoast_Notification::ERROR,
'id' => 'wpseo-dismiss-blog-public-notice',
'priority' => 1.0,
'capabilities' => 'wpseo_manage_options',
);
$notification = new Yoast_Notification( $info_message, $notification_options );
$notification_center = Yoast_Notification_Center::get();
if ( ! $this->is_blog_public() ) {
$notification_center->add_notification( $notification );
}
else {
$notification_center->remove_notification( $notification );
}
}
/**
* Display notice to disable comment pagination.
*/
public function page_comments_notice() {
$info_message = __( 'Paging comments is enabled, this is not needed in 999 out of 1000 cases, we recommend to disable it.', 'wordpress-seo' );
$info_message .= '<br/>';
$info_message .= sprintf(
/* translators: %1$s resolves to the opening tag of the link to the comment setting page, %2$s resolves to the closing tag of the link */
__( 'To fix this uncheck the box in front of the "Break comments into pages..." on the %1$sComment settings page%2$s.', 'wordpress-seo' ),
'<a href="' . esc_url( admin_url( 'options-discussion.php' ) ) . '">',
'</a>'
);
$notification_options = array(
'type' => Yoast_Notification::WARNING,
'id' => 'wpseo-dismiss-page_comments-notice',
'capabilities' => 'wpseo_manage_options',
);
$tagline_notification = new Yoast_Notification( $info_message, $notification_options );
$notification_center = Yoast_Notification_Center::get();
if ( $this->has_page_comments() ) {
$notification_center->add_notification( $tagline_notification );
}
else {
$notification_center->remove_notification( $tagline_notification );
}
}
/**
* Returns whether or not the site has the default tagline.
*
* @return bool
*/
public function has_default_tagline() {
$blog_description = get_bloginfo( 'description' );
$default_blog_description = 'Just another WordPress site';
// We are checking against the WordPress internal translation.
// @codingStandardsIgnoreLine
$translated_blog_description = __( 'Just another WordPress site', 'default' );
return $translated_blog_description === $blog_description || $default_blog_description === $blog_description;
}
/**
* Show alert when the permalink doesn't contain %postname%.
*/
public function permalink_notice() {
$info_message = __( 'You do not have your postname in the URL of your posts and pages, it is highly recommended that you do. Consider setting your permalink structure to <strong>/%postname%/</strong>.', 'wordpress-seo' );
$info_message .= '<br/>';
$info_message .= sprintf(
/* translators: %1$s resolves to the starting tag of the link to the permalink settings page, %2$s resolves to the closing tag of the link */
__( 'You can fix this on the %1$sPermalink settings page%2$s.', 'wordpress-seo' ),
'<a href="' . admin_url( 'options-permalink.php' ) . '">',
'</a>'
);
$notification_options = array(
'type' => Yoast_Notification::WARNING,
'id' => 'wpseo-dismiss-permalink-notice',
'capabilities' => 'wpseo_manage_options',
'priority' => 0.8,
);
$notification = new Yoast_Notification( $info_message, $notification_options );
$notification_center = Yoast_Notification_Center::get();
if ( ! $this->has_postname_in_permalink() ) {
$notification_center->add_notification( $notification );
}
else {
$notification_center->remove_notification( $notification );
}
}
/**
* Are page comments enabled.
*
* @return bool
*/
public function has_page_comments() {
return '1' === get_option( 'page_comments' );
}
/**
* Shows a notice to the user if they have Google Analytics for WordPress 5.4.3 installed because it causes an error
* on the google search console page.
*/
public function ga_compatibility_notice() {
$notification = $this->get_compatibility_notification();
$notification_center = Yoast_Notification_Center::get();
if ( defined( 'GAWP_VERSION' ) && '5.4.3' === GAWP_VERSION ) {
$notification_center->add_notification( $notification );
}
else {
$notification_center->remove_notification( $notification );
}
}
/**
* Build compatibility problem notification.
*
* @return Yoast_Notification
*/
private function get_compatibility_notification() {
$info_message = sprintf(
/* translators: %1$s expands to Yoast SEO, %2$s expands to 5.4.3, %3$s expands to Google Analytics by Yoast */
__( '%1$s detected you are using version %2$s of %3$s, please update to the latest version to prevent compatibility issues.', 'wordpress-seo' ),
'Yoast SEO',
'5.4.3',
'Google Analytics by Yoast'
);
return new Yoast_Notification(
$info_message,
array(
'id' => 'gawp-compatibility-notice',
'type' => Yoast_Notification::ERROR,
)
);
}
/**
* Determines whether a suggested plugins notification needs to be displayed.
*
* @return void
*/
public function yoast_plugin_suggestions_notification() {
$checker = new WPSEO_Plugin_Availability();
$notification_center = Yoast_Notification_Center::get();
// Get all Yoast plugins that have dependencies.
$plugins = $checker->get_plugins_with_dependencies();
foreach ( $plugins as $plugin_name => $plugin ) {
$dependency_names = $checker->get_dependency_names( $plugin );
$notification = $this->get_yoast_seo_suggested_plugins_notification( $plugin_name, $plugin, $dependency_names[0] );
if ( $checker->dependencies_are_satisfied( $plugin ) && ! $checker->is_installed( $plugin ) ) {
$notification_center->add_notification( $notification );
continue;
}
$notification_center->remove_notification( $notification );
}
}
/**
* Build Yoast SEO suggested plugins notification.
*
* @param string $name The plugin name to use for the unique ID.
* @param array $plugin The plugin to retrieve the data from.
* @param string $dependency_name The name of the dependency.
*
* @return Yoast_Notification The notification containing the suggested plugin.
*/
private function get_yoast_seo_suggested_plugins_notification( $name, $plugin, $dependency_name ) {
$info_message = sprintf(
/* translators: %1$s expands to Yoast SEO, %2$s expands to the plugin version, %3$s expands to the plugin name */
__( '%1$s and %2$s can work together a lot better by adding a helper plugin. Please install %3$s to make your life better.', 'wordpress-seo' ),
'Yoast SEO',
$dependency_name,
sprintf( '<a href="%s">%s</a>', $plugin['url'], $plugin['title'] )
);
return new Yoast_Notification(
$info_message,
array(
'id' => 'wpseo-suggested-plugin-' . $name,
'type' => Yoast_Notification::WARNING,
)
);
}
/**
* Add an alert if outdated versions of Yoast SEO plugins are running.
*/
public function yoast_plugin_compatibility_notification() {
$compatibility_checker = new WPSEO_Plugin_Compatibility( WPSEO_VERSION );
$plugins = $compatibility_checker->get_installed_plugins_compatibility();
$notification_center = Yoast_Notification_Center::get();
foreach ( $plugins as $name => $plugin ) {
$type = ( $plugin['active'] ) ? Yoast_Notification::ERROR : Yoast_Notification::WARNING;
$notification = $this->get_yoast_seo_compatibility_notification( $name, $plugin, $type );
if ( $plugin['compatible'] === false ) {
$notification_center->add_notification( $notification );
continue;
}
$notification_center->remove_notification( $notification );
}
}
/**
* Build Yoast SEO compatibility problem notification.
*
* @param string $name The plugin name to use for the unique ID.
* @param array $plugin The plugin to retrieve the data from.
* @param string $level The severity level to use for the notification.
*
* @return Yoast_Notification
*/
private function get_yoast_seo_compatibility_notification( $name, $plugin, $level = Yoast_Notification::WARNING ) {
$info_message = sprintf(
/* translators: %1$s expands to Yoast SEO, %2$s expands to the plugin version, %3$s expands to the plugin name */
__( '%1$s detected you are using version %2$s of %3$s, please update to the latest version to prevent compatibility issues.', 'wordpress-seo' ),
'Yoast SEO',
$plugin['version'],
$plugin['title']
);
return new Yoast_Notification(
$info_message,
array(
'id' => 'wpseo-outdated-yoast-seo-plugin-' . $name,
'type' => $level,
)
);
}
/**
* Shows the notice for recalculating the post. the Notice will only be shown if the user hasn't dismissed it before.
*/
public function recalculate_notice() {
// Just a return, because we want to temporary disable this notice (#3998 and #4532).
return;
if ( filter_input( INPUT_GET, 'recalculate' ) === '1' ) {
update_option( 'wpseo_dismiss_recalculate', '1' );
return;
}
if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ) ) {
return;
}
if ( $this->is_site_notice_dismissed( 'wpseo_dismiss_recalculate' ) ) {
return;
}
Yoast_Notification_Center::get()->add_notification(
new Yoast_Notification(
sprintf(
/* translators: 1: is a link to 'admin_url / admin.php?page=wpseo_tools&recalculate=1' 2: closing link tag */
__( 'We\'ve updated our SEO score algorithm. %1$sRecalculate the SEO scores%2$s for all posts and pages.', 'wordpress-seo' ),
'<a href="' . admin_url( 'admin.php?page=wpseo_tools&recalculate=1' ) . '">',
'</a>'
),
array(
'type' => 'updated yoast-dismissible',
'id' => 'wpseo-dismiss-recalculate',
'nonce' => wp_create_nonce( 'wpseo-dismiss-recalculate' ),
)
)
);
}
/**
* Creates an unsupported PHP version notification in the notification center.
*
* @return void
*/
public function unsupported_php_notice() {
$notification_center = Yoast_Notification_Center::get();
$notification_center->remove_notification_by_id( 'wpseo-dismiss-unsupported-php' );
}
/**
* Check if the user has dismissed the given notice (by $notice_name).
*
* @param string $notice_name The name of the notice that might be dismissed.
*
* @return bool
*/
private function is_site_notice_dismissed( $notice_name ) {
return '1' === get_option( $notice_name, true );
}
/**
* Helper to verify if the user is currently visiting one of our admin pages.
*
* @return bool
*/
private function on_wpseo_admin_page() {
return 'admin.php' === $this->pagenow && strpos( filter_input( INPUT_GET, 'page' ), 'wpseo' ) === 0;
}
/**
* Determine whether we should load the meta box class and if so, load it.
*/
private function load_meta_boxes() {
$is_editor = WPSEO_Metabox::is_post_overview( $this->pagenow ) || WPSEO_Metabox::is_post_edit( $this->pagenow );
$is_inline_save = filter_input( INPUT_POST, 'action' ) === 'inline-save';
/**
* Filter: 'wpseo_always_register_metaboxes_on_admin' - Allow developers to change whether
* the WPSEO metaboxes are only registered on the typical pages (lean loading) or always
* registered when in admin.
*
* @api bool Whether to always register the metaboxes or not. Defaults to false.
*/
if ( $is_editor || $is_inline_save || apply_filters( 'wpseo_always_register_metaboxes_on_admin', false )
) {
$GLOBALS['wpseo_metabox'] = new WPSEO_Metabox();
$GLOBALS['wpseo_meta_columns'] = new WPSEO_Meta_Columns();
}
}
/**
* Determine if we should load our taxonomy edit class and if so, load it.
*/
private function load_taxonomy_class() {
if (
WPSEO_Taxonomy::is_term_edit( $this->pagenow )
|| WPSEO_Taxonomy::is_term_overview( $this->pagenow )
) {
new WPSEO_Taxonomy();
}
}
/**
* Determine if we should load our admin pages class and if so, load it.
*
* Loads admin page class for all admin pages starting with `wpseo_`.
*/
private function load_admin_user_class() {
if ( in_array( $this->pagenow, array( 'user-edit.php', 'profile.php' ), true )
&& current_user_can( 'edit_users' )
) {
new WPSEO_Admin_User_Profile();
}
}
/**
* Determine if we should load our admin pages class and if so, load it.
*
* Loads admin page class for all admin pages starting with `wpseo_`.
*/
private function load_admin_page_class() {
if ( $this->on_wpseo_admin_page() ) {
// For backwards compatabilty, this still needs a global, for now...
$GLOBALS['wpseo_admin_pages'] = new WPSEO_Admin_Pages();
// Only register the yoast i18n when the page is a Yoast SEO page.
if ( WPSEO_Utils::is_yoast_seo_free_page( filter_input( INPUT_GET, 'page' ) ) ) {
$this->register_i18n_promo_class();
$this->register_premium_upsell_admin_block();
}
}
}
/**
* Loads the plugin suggestions.
*/
private function load_plugin_suggestions() {
$suggestions = new WPSEO_Suggested_Plugins( new WPSEO_Plugin_Availability(), Yoast_Notification_Center::get() );
$suggestions->register_hooks();
}
/**
* Registers the Premium Upsell Admin Block.
*
* @return void
*/
private function register_premium_upsell_admin_block() {
if ( ! WPSEO_Utils::is_yoast_seo_premium() ) {
$upsell_block = new WPSEO_Premium_Upsell_Admin_Block( 'wpseo_admin_promo_footer' );
$upsell_block->register_hooks();
}
}
/**
* Registers the promotion class for our GlotPress instance, then creates a notification with the i18n promo.
*
* @link https://github.com/Yoast/i18n-module
*/
private function register_i18n_promo_class() {
// BC, because an older version of the i18n-module didn't have this class.
$i18n_module = new Yoast_I18n_WordPressOrg_v3(
array(
'textdomain' => 'wordpress-seo',
'plugin_name' => 'Yoast SEO',
'hook' => 'wpseo_admin_promo_footer',
),
false
);
$message = $i18n_module->get_promo_message();
if ( $message !== '' ) {
$message .= $i18n_module->get_dismiss_i18n_message_button();
}
$notification_center = Yoast_Notification_Center::get();
$notification = new Yoast_Notification(
$message,
array(
'type' => Yoast_Notification::WARNING,
'id' => 'i18nModuleTranslationAssistance',
)
);
if ( $message ) {
$notification_center->add_notification( $notification );
return;
}
$notification_center->remove_notification( $notification );
}
/**
* See if we should start our XML Sitemaps Admin class.
*/
private function load_xml_sitemaps_admin() {
if ( WPSEO_Options::get( 'enable_xml_sitemap', false ) ) {
new WPSEO_Sitemaps_Admin();
}
}
/**
* Check if the site is set to be publicly visible.
*
* @return bool
*/
private function is_blog_public() {
return '1' === (string) get_option( 'blog_public' );
}
/**
* Shows deprecation warnings to the user if a plugin has registered a filter we have deprecated.
*/
public function show_hook_deprecation_warnings() {
global $wp_filter;
if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
return;
}
// WordPress hooks that have been deprecated since a Yoast SEO version.
$deprecated_filters = array(
'wpseo_metakey' => array(
'version' => '6.3',
'alternative' => null,
),
'wpseo_metakeywords' => array(
'version' => '6.3',
'alternative' => null,
),
'wpseo_stopwords' => array(
'version' => '7.0',
'alternative' => null,
),
'wpseo_redirect_orphan_attachment' => array(
'version' => '7.0',
'alternative' => null,
),
'wpseo_genesis_force_adjacent_rel_home' => array(
'version' => '9.4',
'alternative' => null,
),
);
// Determine which filters have been registered.
$deprecated_notices = array_intersect(
array_keys( $deprecated_filters ),
array_keys( $wp_filter )
);
// Show notice for each deprecated filter or action that has been registered.
foreach ( $deprecated_notices as $deprecated_filter ) {
$deprecation_info = $deprecated_filters[ $deprecated_filter ];
_deprecated_hook(
$deprecated_filter,
'WPSEO ' . $deprecation_info['version'],
$deprecation_info['alternative']
);
}
}
/**
* Check if the permalink uses %postname%.
*
* @return bool
*/
private function has_postname_in_permalink() {
return ( false !== strpos( get_option( 'permalink_structure' ), '%postname%' ) );
}
/**
* Shows a notice on the permalink settings page.
*/
public function permalink_settings_notice() {
global $pagenow;
if ( $pagenow === 'options-permalink.php' ) {
$warning = esc_html__( 'WARNING:', 'wordpress-seo' );
/* translators: %1$s and %2$s expand to <i> items to emphasize the word in the middle. */
$message = esc_html__( 'Changing your permalinks settings can seriously impact your search engine visibility. It should almost %1$s never %2$s be done on a live website.', 'wordpress-seo' );
$link = esc_html__( 'Learn about why permalinks are important for SEO.', 'wordpress-seo' );
$url = WPSEO_Shortlinker::get( 'https://yoa.st/why-permalinks/' );
echo '<div class="notice notice-warning"><p><strong>' . $warning . '</strong><br>' . sprintf( $message, '<i>', '</i>' ) . '<br><a href="' . $url . '" target="_blank">' . $link . '</a></p></div>';
}
}
}

View File

@ -0,0 +1,112 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Handles the media purge notification showing and hiding.
*/
class WPSEO_Admin_Media_Purge_Notification implements WPSEO_WordPress_Integration {
/**
* Notification ID to use.
*
* @var string
*/
private $notification_id = 'wpseo_media_purge';
/**
* Registers all hooks to WordPress.
*
* @return void
*/
public function register_hooks() {
add_action( 'admin_init', array( $this, 'manage_notification' ) );
add_filter( 'wpseo_option_tab-metas_media', array( $this, 'output_hidden_setting' ) );
// Dismissing is just setting the relevancy to false, which cancels out any functionality.
if ( WPSEO_Utils::is_yoast_seo_page() && filter_input( INPUT_GET, 'dismiss' ) === $this->notification_id ) {
WPSEO_Options::set( 'is-media-purge-relevant', false );
}
}
/**
* Adds a hidden setting to the media tab.
*
* To make sure the setting is not reverted to the default when -anything-
* is saved on the entire page (not just the media tab).
*
* @param string|null $input Current filter value.
*
* @return string|null
*/
public function output_hidden_setting( $input ) {
$form = Yoast_Form::get_instance();
$form->hidden( 'is-media-purge-relevant' );
return $input;
}
/**
* Manages if the notification should be shown or removed.
*
* @return void
*/
public function manage_notification() {
$this->remove_notification();
}
/**
* Retrieves the notification that should be shown or removed.
*
* @return Yoast_Notification The notification to use.
*/
private function get_notification() {
$content = sprintf(
/* translators: %1$s expands to the link to the article, %2$s closes the link tag. */
__( 'Your site\'s settings currently allow attachment URLs on your site to exist. Please read %1$sthis post about a potential issue%2$s with attachment URLs and check whether you have the correct setting for your site.', 'wordpress-seo' ),
'<a href="' . esc_url( WPSEO_Shortlinker::get( 'https://yoa.st/2r8' ) ) . '" rel="noopener noreferrer" target="_blank">',
'</a>'
);
$content .= '<br><br>';
$content .= sprintf(
/* translators: %1$s dismiss link open tag, %2$s closes the link tag. */
__( 'If you know what this means and you do not want to see this message anymore, you can %1$sdismiss this message%2$s.', 'wordpress-seo' ),
'<a href="' . esc_url( admin_url( 'admin.php?page=wpseo_dashboard&dismiss=' . $this->notification_id ) ) . '">',
'</a>'
);
return new Yoast_Notification(
$content,
array(
'type' => Yoast_Notification::ERROR,
'id' => $this->notification_id,
'capabilities' => 'wpseo_manage_options',
'priority' => 1,
)
);
}
/**
* Adds the notification to the notificaton center.
*
* @return void
*/
private function add_notification() {
$notification_center = Yoast_Notification_Center::get();
$notification_center->add_notification( $this->get_notification() );
}
/**
* Removes the notification from the notification center.
*
* @return void
*/
private function remove_notification() {
$notification_center = Yoast_Notification_Center::get();
$notification_center->remove_notification( $this->get_notification() );
}
}

View File

@ -0,0 +1,205 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Determines the recommended replacement variables based on the context.
*/
class WPSEO_Admin_Recommended_Replace_Vars {
/**
* The recommended replacement variables.
*
* @var array
*/
protected $recommended_replace_vars = array(
// Posts types.
'page' => array( 'sitename', 'title', 'sep', 'primary_category' ),
'post' => array( 'sitename', 'title', 'sep', 'primary_category' ),
// Homepage.
'homepage' => array( 'sitename', 'sitedesc', 'sep' ),
// Custom post type.
'custom_post_type' => array( 'sitename', 'title', 'sep' ),
// Taxonomies.
'category' => array( 'sitename', 'term_title', 'sep' ),
'post_tag' => array( 'sitename', 'term_title', 'sep' ),
'post_format' => array( 'sitename', 'term_title', 'sep', 'page' ),
// Custom taxonomy.
'term-in-custom-taxomomy' => array( 'sitename', 'term_title', 'sep' ),
// Settings - archive pages.
'author_archive' => array( 'sitename', 'title', 'sep', 'page' ),
'date_archive' => array( 'sitename', 'sep', 'date', 'page' ),
'custom-post-type_archive' => array( 'sitename', 'title', 'sep', 'page' ),
// Settings - special pages.
'search' => array( 'sitename', 'searchphrase', 'sep', 'page' ),
'404' => array( 'sitename', 'sep' ),
);
/**
* Determines the page type of the current term.
*
* @param string $taxonomy The taxonomy name.
*
* @return string The page type.
*/
public function determine_for_term( $taxonomy ) {
$recommended_replace_vars = $this->get_recommended_replacevars();
if ( array_key_exists( $taxonomy, $recommended_replace_vars ) ) {
return $taxonomy;
}
return 'term-in-custom-taxomomy';
}
/**
* Determines the page type of the current post.
*
* @param WP_Post $post A WordPress post instance.
*
* @return string The page type.
*/
public function determine_for_post( $post ) {
if ( $post instanceof WP_Post === false ) {
return 'post';
}
if ( $post->post_type === 'page' && $this->is_homepage( $post ) ) {
return 'homepage';
}
$recommended_replace_vars = $this->get_recommended_replacevars();
if ( array_key_exists( $post->post_type, $recommended_replace_vars ) ) {
return $post->post_type;
}
return 'custom_post_type';
}
/**
* Determines the page type for a post type.
*
* @param string $post_type The name of the post_type.
* @param string $fallback The page type to fall back to.
*
* @return string The page type.
*/
public function determine_for_post_type( $post_type, $fallback = 'custom_post_type' ) {
$page_type = $post_type;
$recommended_replace_vars = $this->get_recommended_replacevars();
$has_recommended_replacevars = $this->has_recommended_replace_vars( $recommended_replace_vars, $page_type );
if ( ! $has_recommended_replacevars ) {
return $fallback;
}
return $page_type;
}
/**
* Determines the page type for an archive page.
*
* @param string $name The name of the archive.
* @param string $fallback The page type to fall back to.
*
* @return string The page type.
*/
public function determine_for_archive( $name, $fallback = 'custom-post-type_archive' ) {
$page_type = $name . '_archive';
$recommended_replace_vars = $this->get_recommended_replacevars();
$has_recommended_replacevars = $this->has_recommended_replace_vars( $recommended_replace_vars, $page_type );
if ( ! $has_recommended_replacevars ) {
return $fallback;
}
return $page_type;
}
/**
* Retrieves the recommended replacement variables for the given page type.
*
* @param string $page_type The page type.
*
* @return array The recommended replacement variables.
*/
public function get_recommended_replacevars_for( $page_type ) {
$recommended_replace_vars = $this->get_recommended_replacevars();
$has_recommended_replace_vars = $this->has_recommended_replace_vars( $recommended_replace_vars, $page_type );
if ( ! $has_recommended_replace_vars ) {
return array();
}
return $recommended_replace_vars[ $page_type ];
}
/**
* Retrieves the recommended replacement variables.
*
* @return array The recommended replacement variables.
*/
public function get_recommended_replacevars() {
/**
* Filter: Adds the possibility to add extra recommended replacement variables.
*
* @api array $additional_replace_vars Empty array to add the replacevars to.
*/
$recommended_replace_vars = apply_filters( 'wpseo_recommended_replace_vars', $this->recommended_replace_vars );
if ( ! is_array( $recommended_replace_vars ) ) {
return $this->recommended_replace_vars;
}
return $recommended_replace_vars;
}
/**
* Returns whether the given page type has recommended replace vars.
*
* @param array $recommended_replace_vars The recommended replace vars
* to check in.
* @param string $page_type The page type to check.
*
* @return bool True if there are associated recommended replace vars.
*/
private function has_recommended_replace_vars( $recommended_replace_vars, $page_type ) {
if ( ! isset( $recommended_replace_vars[ $page_type ] ) ) {
return false;
}
if ( ! is_array( $recommended_replace_vars[ $page_type ] ) ) {
return false;
}
return true;
}
/**
* Determines whether or not a post is the homepage.
*
* @param WP_Post $post The WordPress global post object.
*
* @return bool True if the given post is the homepage.
*/
private function is_homepage( $post ) {
if ( $post instanceof WP_Post === false ) {
return false;
}
/*
* The page on front returns a string with normal WordPress interaction, while the post ID is an int.
* This way we make sure we always compare strings.
*/
$post_id = (int) $post->ID;
$page_on_front = (int) get_option( 'page_on_front' );
return get_option( 'show_on_front' ) === 'page' && $page_on_front === $post_id;
}
}

View File

@ -0,0 +1,89 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
* @since 1.8.0
*/
/**
* Customizes user profile.
*/
class WPSEO_Admin_User_Profile {
/**
* Class constructor.
*/
public function __construct() {
add_action( 'show_user_profile', array( $this, 'user_profile' ) );
add_action( 'edit_user_profile', array( $this, 'user_profile' ) );
add_action( 'personal_options_update', array( $this, 'process_user_option_update' ) );
add_action( 'edit_user_profile_update', array( $this, 'process_user_option_update' ) );
add_action( 'update_user_meta', array( $this, 'clear_author_sitemap_cache' ), 10, 3 );
}
/**
* Clear author sitemap cache when settings are changed.
*
* @since 3.1
*
* @param int $meta_id The ID of the meta option changed.
* @param int $object_id The ID of the user.
* @param string $meta_key The key of the meta field changed.
*/
public function clear_author_sitemap_cache( $meta_id, $object_id, $meta_key ) {
if ( '_yoast_wpseo_profile_updated' === $meta_key ) {
WPSEO_Sitemaps_Cache::clear( array( 'author' ) );
}
}
/**
* Filter POST variables.
*
* @param string $var_name Name of the variable to filter.
*
* @return mixed
*/
private function filter_input_post( $var_name ) {
$val = filter_input( INPUT_POST, $var_name );
if ( $val ) {
return WPSEO_Utils::sanitize_text_field( $val );
}
return '';
}
/**
* Updates the user metas that (might) have been set on the user profile page.
*
* @param int $user_id User ID of the updated user.
*/
public function process_user_option_update( $user_id ) {
update_user_meta( $user_id, '_yoast_wpseo_profile_updated', time() );
$nonce_value = $this->filter_input_post( 'wpseo_nonce' );
if ( empty( $nonce_value ) ) { // Submit from alternate forms.
return;
}
check_admin_referer( 'wpseo_user_profile_update', 'wpseo_nonce' );
update_user_meta( $user_id, 'wpseo_title', $this->filter_input_post( 'wpseo_author_title' ) );
update_user_meta( $user_id, 'wpseo_metadesc', $this->filter_input_post( 'wpseo_author_metadesc' ) );
update_user_meta( $user_id, 'wpseo_noindex_author', $this->filter_input_post( 'wpseo_noindex_author' ) );
update_user_meta( $user_id, 'wpseo_content_analysis_disable', $this->filter_input_post( 'wpseo_content_analysis_disable' ) );
update_user_meta( $user_id, 'wpseo_keyword_analysis_disable', $this->filter_input_post( 'wpseo_keyword_analysis_disable' ) );
}
/**
* Add the inputs needed for SEO values to the User Profile page.
*
* @param WP_User $user User instance to output for.
*/
public function user_profile( $user ) {
wp_nonce_field( 'wpseo_user_profile_update', 'wpseo_nonce' );
require_once WPSEO_PATH . 'admin/views/user-profile.php';
}
}

View File

@ -0,0 +1,96 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Represents the utils for the admin.
*/
class WPSEO_Admin_Utils {
/**
* Gets the install URL for the passed plugin slug.
*
* @param string $slug The slug to create an install link for.
*
* @return string The install URL. Empty string if the current user doesn't have the proper capabilities.
*/
public static function get_install_url( $slug ) {
if ( ! current_user_can( 'install_plugins' ) ) {
return '';
}
return wp_nonce_url(
self_admin_url( 'update.php?action=install-plugin&plugin=' . dirname( $slug ) ),
'install-plugin_' . dirname( $slug )
);
}
/**
* Gets the activation URL for the passed plugin slug.
*
* @param string $slug The slug to create an activation link for.
*
* @return string The activation URL. Empty string if the current user doesn't have the proper capabilities.
*/
public static function get_activation_url( $slug ) {
if ( ! current_user_can( 'install_plugins' ) ) {
return '';
}
return wp_nonce_url(
self_admin_url( 'plugins.php?action=activate&plugin_status=all&paged=1&s&plugin=' . $slug ),
'activate-plugin_' . $slug
);
}
/**
* Creates a link if the passed plugin is deemend a directly-installable plugin.
*
* @param array $plugin The plugin to create the link for.
*
* @return string The link to the plugin install. Returns the title if the plugin is deemed a Premium product.
*/
public static function get_install_link( $plugin ) {
$install_url = self::get_install_url( $plugin['slug'] );
if ( $install_url === '' || ( isset( $plugin['premium'] ) && $plugin['premium'] === true ) ) {
return $plugin['title'];
}
return sprintf(
'<a href="%s">%s</a>',
$install_url,
$plugin['title']
);
}
/**
* Gets a visually hidden accessible message for links that open in a new browser tab.
*
* @return string The visually hidden accessible message.
*/
public static function get_new_tab_message() {
return sprintf(
'<span class="screen-reader-text">%s</span>',
esc_html__( '(Opens in a new browser tab)', 'wordpress-seo' )
);
}
/* ********************* DEPRECATED METHODS ********************* */
/**
* Determines whether or not the user has an invalid version of PHP installed.
*
* @deprecated 8.1
* @codeCoverageIgnore
*
* @return bool Whether or not PHP 5.2 or lower is installed.
*/
public static function is_supported_php_version_installed() {
// Intentionally left blank.
return true;
}
}

View File

@ -0,0 +1,453 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Class that holds most of the admin functionality for Yoast SEO.
*/
class WPSEO_Admin {
/**
* The page identifier used in WordPress to register the admin page.
*
* !DO NOT CHANGE THIS!
*
* @var string
*/
const PAGE_IDENTIFIER = 'wpseo_dashboard';
/**
* Array of classes that add admin functionality.
*
* @var array
*/
protected $admin_features;
/**
* Class constructor.
*/
public function __construct() {
$integrations = array();
global $pagenow;
$wpseo_menu = new WPSEO_Menu();
$wpseo_menu->register_hooks();
if ( is_multisite() ) {
WPSEO_Options::maybe_set_multisite_defaults( false );
}
if ( WPSEO_Options::get( 'stripcategorybase' ) === true ) {
add_action( 'created_category', array( $this, 'schedule_rewrite_flush' ) );
add_action( 'edited_category', array( $this, 'schedule_rewrite_flush' ) );
add_action( 'delete_category', array( $this, 'schedule_rewrite_flush' ) );
}
if ( WPSEO_Options::get( 'disable-attachment' ) === true ) {
add_filter( 'wpseo_accessible_post_types', array( 'WPSEO_Post_Type', 'filter_attachment_post_type' ) );
}
if ( filter_input( INPUT_GET, 'page' ) === 'wpseo_tools' && filter_input( INPUT_GET, 'tool' ) === null ) {
new WPSEO_Recalculate_Scores();
}
add_filter( 'plugin_action_links_' . WPSEO_BASENAME, array( $this, 'add_action_link' ), 10, 2 );
add_action( 'admin_enqueue_scripts', array( $this, 'config_page_scripts' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_global_style' ) );
add_filter( 'user_contactmethods', array( $this, 'update_contactmethods' ), 10, 1 );
add_action( 'after_switch_theme', array( $this, 'switch_theme' ) );
add_action( 'switch_theme', array( $this, 'switch_theme' ) );
add_filter( 'set-screen-option', array( $this, 'save_bulk_edit_options' ), 10, 3 );
add_action( 'admin_init', array( 'WPSEO_Plugin_Conflict', 'hook_check_for_plugin_conflicts' ), 10, 1 );
add_action( 'admin_init', array( $this, 'map_manage_options_cap' ) );
WPSEO_Sitemaps_Cache::register_clear_on_option_update( 'wpseo' );
WPSEO_Sitemaps_Cache::register_clear_on_option_update( 'home' );
if ( WPSEO_Utils::is_yoast_seo_page() ) {
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
}
if ( WPSEO_Utils::is_api_available() ) {
$configuration = new WPSEO_Configuration_Page();
$configuration->set_hooks();
$configuration->catch_configuration_request();
}
$this->set_upsell_notice();
$this->initialize_cornerstone_content();
if ( WPSEO_Utils::is_plugin_network_active() ) {
$integrations[] = new Yoast_Network_Admin();
}
$this->admin_features = array(
'google_search_console' => new WPSEO_GSC(),
'dashboard_widget' => new Yoast_Dashboard_Widget(),
);
if ( WPSEO_Metabox::is_post_overview( $pagenow ) || WPSEO_Metabox::is_post_edit( $pagenow ) ) {
$this->admin_features['primary_category'] = new WPSEO_Primary_Term_Admin();
}
$integrations[] = new WPSEO_Yoast_Columns();
$integrations[] = new WPSEO_License_Page_Manager();
$integrations[] = new WPSEO_Statistic_Integration();
$integrations[] = new WPSEO_Capability_Manager_Integration( WPSEO_Capability_Manager_Factory::get() );
$integrations[] = new WPSEO_Admin_Media_Purge_Notification();
$integrations[] = new WPSEO_Admin_Gutenberg_Compatibility_Notification();
$integrations[] = new WPSEO_Expose_Shortlinks();
$integrations[] = new WPSEO_MyYoast_Proxy();
$integrations[] = new WPSEO_MyYoast_Route();
$integrations[] = new WPSEO_Schema_Person_Upgrade_Notification();
$integrations = array_merge(
$integrations,
$this->get_admin_features(),
$this->initialize_seo_links(),
$this->initialize_cornerstone_content()
);
/** @var WPSEO_WordPress_Integration $integration */
foreach ( $integrations as $integration ) {
$integration->register_hooks();
}
}
/**
* Schedules a rewrite flush to happen at shutdown.
*/
public function schedule_rewrite_flush() {
add_action( 'shutdown', 'flush_rewrite_rules' );
}
/**
* Returns all the classes for the admin features.
*
* @return array
*/
public function get_admin_features() {
return $this->admin_features;
}
/**
* Register assets needed on admin pages.
*/
public function enqueue_assets() {
if ( 'wpseo_licenses' === filter_input( INPUT_GET, 'page' ) ) {
$asset_manager = new WPSEO_Admin_Asset_Manager();
$asset_manager->enqueue_style( 'extensions' );
}
}
/**
* Returns the manage_options capability.
*
* @return string The capability to use.
*/
public function get_manage_options_cap() {
/**
* Filter: 'wpseo_manage_options_capability' - Allow changing the capability users need to view the settings pages.
*
* @api string unsigned The capability.
*/
return apply_filters( 'wpseo_manage_options_capability', 'wpseo_manage_options' );
}
/**
* Maps the manage_options cap on saving an options page to wpseo_manage_options.
*/
public function map_manage_options_cap() {
$option_page = ! empty( $_POST['option_page'] ) ? $_POST['option_page'] : ''; // WPCS: CSRF ok.
if ( strpos( $option_page, 'yoast_wpseo' ) === 0 ) {
add_filter( 'option_page_capability_' . $option_page, array( $this, 'get_manage_options_cap' ) );
}
}
/**
* Adds the ability to choose how many posts are displayed per page
* on the bulk edit pages.
*/
public function bulk_edit_options() {
$option = 'per_page';
$args = array(
'label' => __( 'Posts', 'wordpress-seo' ),
'default' => 10,
'option' => 'wpseo_posts_per_page',
);
add_screen_option( $option, $args );
}
/**
* Saves the posts per page limit for bulk edit pages.
*
* @param int $status Status value to pass through.
* @param string $option Option name.
* @param int $value Count value to check.
*
* @return int
*/
public function save_bulk_edit_options( $status, $option, $value ) {
if ( 'wpseo_posts_per_page' === $option && ( $value > 0 && $value < 1000 ) ) {
return $value;
}
return $status;
}
/**
* Adds links to Premium Support and FAQ under the plugin in the plugin overview page.
*
* @staticvar string $this_plugin Holds the directory & filename for the plugin.
*
* @param array $links Array of links for the plugins, adapted when the current plugin is found.
* @param string $file The filename for the current plugin, which the filter loops through.
*
* @return array $links
*/
public function add_action_link( $links, $file ) {
if ( WPSEO_BASENAME === $file && WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ) ) {
$settings_link = '<a href="' . esc_url( admin_url( 'admin.php?page=' . self::PAGE_IDENTIFIER ) ) . '">' . __( 'Settings', 'wordpress-seo' ) . '</a>';
array_unshift( $links, $settings_link );
}
$addon_manager = new WPSEO_Addon_Manager();
if ( WPSEO_Utils::is_yoast_seo_premium() && $addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ) ) {
return $links;
}
// Add link to premium support landing page.
$premium_link = '<a style="font-weight: bold;" href="' . esc_url( WPSEO_Shortlinker::get( 'https://yoa.st/1yb' ) ) . '" target="_blank">' . __( 'Premium Support', 'wordpress-seo' ) . '</a>';
array_unshift( $links, $premium_link );
// Add link to docs.
$faq_link = '<a href="' . esc_url( WPSEO_Shortlinker::get( 'https://yoa.st/1yc' ) ) . '" target="_blank">' . __( 'FAQ', 'wordpress-seo' ) . '</a>';
array_unshift( $links, $faq_link );
return $links;
}
/**
* Enqueues the (tiny) global JS needed for the plugin.
*/
public function config_page_scripts() {
$asset_manager = new WPSEO_Admin_Asset_Manager();
$asset_manager->enqueue_script( 'admin-global-script' );
wp_localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'admin-global-script', 'wpseoAdminGlobalL10n', $this->localize_admin_global_script() );
}
/**
* Enqueues the (tiny) global stylesheet needed for the plugin.
*/
public function enqueue_global_style() {
$asset_manager = new WPSEO_Admin_Asset_Manager();
$asset_manager->enqueue_style( 'admin-global' );
}
/**
* Filter the $contactmethods array and add a set of social profiles.
*
* These are used with the Facebook author, rel="author" and Twitter cards implementation.
*
* @link https://developers.google.com/search/docs/data-types/social-profile
*
* @param array $contactmethods Currently set contactmethods.
*
* @return array $contactmethods with added contactmethods.
*/
public function update_contactmethods( $contactmethods ) {
$contactmethods['facebook'] = __( 'Facebook profile URL', 'wordpress-seo' );
$contactmethods['instagram'] = __( 'Instagram profile URL', 'wordpress-seo' );
$contactmethods['linkedin'] = __( 'LinkedIn profile URL', 'wordpress-seo' );
$contactmethods['myspace'] = __( 'MySpace profile URL', 'wordpress-seo' );
$contactmethods['pinterest'] = __( 'Pinterest profile URL', 'wordpress-seo' );
$contactmethods['soundcloud'] = __( 'SoundCloud profile URL', 'wordpress-seo' );
$contactmethods['tumblr'] = __( 'Tumblr profile URL', 'wordpress-seo' );
$contactmethods['twitter'] = __( 'Twitter username (without @)', 'wordpress-seo' );
$contactmethods['youtube'] = __( 'YouTube profile URL', 'wordpress-seo' );
$contactmethods['wikipedia'] = __( 'Wikipedia page about you', 'wordpress-seo' ) . '<br/><small>' . __( '(if one exists)', 'wordpress-seo' ) . '</small>';
return $contactmethods;
}
/**
* Log the updated timestamp for user profiles when theme is changed.
*/
public function switch_theme() {
$users = get_users( array( 'who' => 'authors' ) );
if ( is_array( $users ) && $users !== array() ) {
foreach ( $users as $user ) {
update_user_meta( $user->ID, '_yoast_wpseo_profile_updated', time() );
}
}
}
/**
* Localization for the dismiss urls.
*
* @return array
*/
private function localize_admin_global_script() {
return array(
/* translators: %s: '%%term_title%%' variable used in titles and meta's template that's not compatible with the given template */
'variable_warning' => sprintf( __( 'Warning: the variable %s cannot be used in this template. See the help center for more info.', 'wordpress-seo' ), '<code>%s</code>' ),
'dismiss_about_url' => $this->get_dismiss_url( 'wpseo-dismiss-about' ),
'dismiss_tagline_url' => $this->get_dismiss_url( 'wpseo-dismiss-tagline-notice' ),
'help_video_iframe_title' => __( 'Yoast SEO video tutorial', 'wordpress-seo' ),
'scrollable_table_hint' => __( 'Scroll to see the table content.', 'wordpress-seo' ),
);
}
/**
* Extending the current page URL with two params to be able to ignore the notice.
*
* @param string $dismiss_param The param used to dismiss the notification.
*
* @return string
*/
private function get_dismiss_url( $dismiss_param ) {
$arr_params = array(
$dismiss_param => '1',
'nonce' => wp_create_nonce( $dismiss_param ),
);
return esc_url( add_query_arg( $arr_params ) );
}
/**
* Sets the upsell notice.
*/
protected function set_upsell_notice() {
$upsell = new WPSEO_Product_Upsell_Notice();
$upsell->dismiss_notice_listener();
$upsell->initialize();
}
/**
* Whether we are on the admin dashboard page.
*
* @returns bool
*/
protected function on_dashboard_page() {
return 'index.php' === $GLOBALS['pagenow'];
}
/**
* Loads the cornerstone filter.
*
* @return WPSEO_WordPress_Integration[] The integrations to initialize.
*/
protected function initialize_cornerstone_content() {
if ( ! WPSEO_Options::get( 'enable_cornerstone_content' ) ) {
return array();
}
return array(
'cornerstone_filter' => new WPSEO_Cornerstone_Filter(),
);
}
/**
* Initializes the seo link watcher.
*
* @returns WPSEO_WordPress_Integration[]
*/
protected function initialize_seo_links() {
$integrations = array();
$link_table_compatibility_notifier = new WPSEO_Link_Compatibility_Notifier();
$link_table_accessible_notifier = new WPSEO_Link_Table_Accessible_Notifier();
if ( ! WPSEO_Options::get( 'enable_text_link_counter' ) ) {
$link_table_compatibility_notifier->remove_notification();
return $integrations;
}
$integrations[] = new WPSEO_Link_Cleanup_Transient();
// Only use the link module for PHP 5.3 and higher and show a notice when version is wrong.
if ( version_compare( phpversion(), '5.3', '<' ) ) {
$link_table_compatibility_notifier->add_notification();
return $integrations;
}
$link_table_compatibility_notifier->remove_notification();
// When the table doesn't exists, just add the notification and return early.
if ( ! WPSEO_Link_Table_Accessible::is_accessible() ) {
WPSEO_Link_Table_Accessible::cleanup();
}
if ( ! WPSEO_Meta_Table_Accessible::is_accessible() ) {
WPSEO_Meta_Table_Accessible::cleanup();
}
if ( ! WPSEO_Link_Table_Accessible::is_accessible() || ! WPSEO_Meta_Table_Accessible::is_accessible() ) {
$link_table_accessible_notifier->add_notification();
return $integrations;
}
$link_table_accessible_notifier->remove_notification();
$integrations[] = new WPSEO_Link_Columns( new WPSEO_Meta_Storage() );
$integrations[] = new WPSEO_Link_Reindex_Dashboard();
$integrations[] = new WPSEO_Link_Notifier();
// Adds a filter to exclude the attachments from the link count.
add_filter( 'wpseo_link_count_post_types', array( 'WPSEO_Post_Type', 'filter_attachment_post_type' ) );
return $integrations;
}
/* ********************* DEPRECATED METHODS ********************* */
/**
* Cleans stopwords out of the slug, if the slug hasn't been set yet.
*
* @deprecated 7.0
* @codeCoverageIgnore
*
* @return void
*/
public function remove_stopwords_from_slug() {
_deprecated_function( __METHOD__, 'WPSEO 7.0' );
}
/**
* Filter the stopwords from the slug.
*
* @deprecated 7.0
* @codeCoverageIgnore
*
* @return void
*/
public function filter_stopwords_from_slug() {
_deprecated_function( __METHOD__, 'WPSEO 7.0' );
}
/**
* Initializes WHIP to show a notice for outdated PHP versions.
*
* @deprecated 8.1
* @codeCoverageIgnore
*
* @return void
*/
public function check_php_version() {
// Intentionally left empty.
}
}

View File

@ -0,0 +1,216 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Represents a WPSEO asset
*/
class WPSEO_Admin_Asset {
/**
* @var string
*/
const TYPE_JS = 'js';
/**
* @var string
*/
const TYPE_CSS = 'css';
/**
* @var string
*/
const NAME = 'name';
/**
* @var string
*/
const SRC = 'src';
/**
* @var string
*/
const DEPS = 'deps';
/**
* @var string
*/
const VERSION = 'version';
/* Style specific. */
/**
* @var string
*/
const MEDIA = 'media';
/**
* @var string
*/
const RTL = 'rtl';
/* Script specific. */
/**
* @var string
*/
const IN_FOOTER = 'in_footer';
/**
* @var string
*/
protected $name;
/**
* @var string
*/
protected $src;
/**
* @var string|array
*/
protected $deps;
/**
* @var string
*/
protected $version;
/**
* @var string
*/
protected $media;
/**
* @var boolean
*/
protected $in_footer;
/**
* @var boolean
*/
protected $rtl;
/**
* @var string
*/
protected $suffix;
/**
* Default asset arguments.
*
* @var array
*/
private $defaults = array(
'deps' => array(),
'version' => WPSEO_VERSION,
'in_footer' => true,
'rtl' => true,
'media' => 'all',
'suffix' => WPSEO_CSSJS_SUFFIX,
);
/**
* @param array $args The arguments for this asset.
*
* @throws InvalidArgumentException Throws when no name or src has been provided.
*/
public function __construct( array $args ) {
if ( ! isset( $args['name'] ) ) {
throw new InvalidArgumentException( 'name is a required argument' );
}
if ( ! isset( $args['src'] ) ) {
throw new InvalidArgumentException( 'src is a required argument' );
}
$args = array_merge( $this->defaults, $args );
$this->name = $args['name'];
$this->src = $args['src'];
$this->deps = $args['deps'];
$this->version = $args['version'];
$this->media = $args['media'];
$this->in_footer = $args['in_footer'];
$this->rtl = $args['rtl'];
$this->suffix = $args['suffix'];
}
/**
* @return string
*/
public function get_name() {
return $this->name;
}
/**
* @return string
*/
public function get_src() {
return $this->src;
}
/**
* @return array|string
*/
public function get_deps() {
return $this->deps;
}
/**
* @return string
*/
public function get_version() {
return $this->version;
}
/**
* @return string
*/
public function get_media() {
return $this->media;
}
/**
* @return boolean
*/
public function is_in_footer() {
return $this->in_footer;
}
/**
* @return boolean
*/
public function has_rtl() {
return $this->rtl;
}
/**
* @return string
*/
public function get_suffix() {
return $this->suffix;
}
/**
* Returns the full URL for this asset based on the path to the plugin file.
*
* @deprecated 6.2
* @codeCoverageIgnore
*
* @param string $type Type of asset.
* @param string $plugin_file Absolute path to the plugin file.
*
* @return string The full URL to the asset.
*/
public function get_url( $type, $plugin_file ) {
_deprecated_function( __CLASS__ . '::get_url', '6.2', 'WPSEO_Admin_Asset_SEO_Location::get_url' );
$asset_location = new WPSEO_Admin_Asset_SEO_Location( $plugin_file );
return $asset_location->get_url( $this, $type );
}
}

View File

@ -0,0 +1,77 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Bulk Editor
* @since 1.5.0
*/
/**
* Implements table for bulk description editing.
*/
class WPSEO_Bulk_Description_List_Table extends WPSEO_Bulk_List_Table {
/**
* Current type for this class will be (meta) description.
*
* @var string
*/
protected $page_type = 'description';
/**
* Settings with are used in __construct.
*
* @var array
*/
protected $settings = array(
'singular' => 'wpseo_bulk_description',
'plural' => 'wpseo_bulk_descriptions',
'ajax' => true,
);
/**
* The field in the database where meta field is saved.
*
* @var string
*/
protected $target_db_field = 'metadesc';
/**
* The columns shown on the table.
*
* @return array
*/
public function get_columns() {
$columns = array(
'col_existing_yoast_seo_metadesc' => __( 'Existing Yoast Meta Description', 'wordpress-seo' ),
'col_new_yoast_seo_metadesc' => __( 'New Yoast Meta Description', 'wordpress-seo' ),
);
return $this->merge_columns( $columns );
}
/**
* Parse the metadescription.
*
* @param string $column_name Column name.
* @param object $record Data object.
* @param string $attributes HTML attributes.
*
* @return string
*/
protected function parse_page_specific_column( $column_name, $record, $attributes ) {
switch ( $column_name ) {
case 'col_new_yoast_seo_metadesc':
return sprintf(
'<textarea id="%1$s" name="%1$s" class="wpseo-new-metadesc" data-id="%2$s" aria-labelledby="col_new_yoast_seo_metadesc"></textarea>',
esc_attr( 'wpseo-new-metadesc-' . $record->ID ),
esc_attr( $record->ID )
);
case 'col_existing_yoast_seo_metadesc':
// @todo Inconsistent return/echo behavior R.
echo $this->parse_meta_data_field( $record->ID, $attributes );
break;
}
}
} /* End of class */

View File

@ -0,0 +1,999 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Bulk Editor
* @since 1.5.0
*/
/**
* Implements table for bulk editing.
*/
class WPSEO_Bulk_List_Table extends WP_List_Table {
/**
* The nonce that was passed with the request.
*
* @var string
*/
private $nonce;
/**
* Array of post types for which the current user has `edit_others_posts` capabilities.
*
* @var array
*/
private $all_posts;
/**
* Array of post types for which the current user has `edit_posts` capabilities, but not `edit_others_posts`.
*
* @var array
*/
private $own_posts;
/**
* Saves all the metadata into this array.
*
* @var array
*/
protected $meta_data = array();
/**
* The current requested page_url.
*
* @var string
*/
private $request_url = '';
/**
* The current page (depending on $_GET['paged']) if current tab is for current page_type, else it will be 1.
*
* @var integer
*/
private $current_page;
/**
* The current post filter, if is used (depending on $_GET['post_type_filter']).
*
* @var string
*/
private $current_filter;
/**
* The current post status, if is used (depending on $_GET['post_status']).
*
* @var string
*/
private $current_status;
/**
* The current sorting, if used (depending on $_GET['order'] and $_GET['orderby']).
*
* @var string
*/
private $current_order;
/**
* The page_type for current class instance (for example: title / description).
*
* @var string
*/
protected $page_type;
/**
* Based on the page_type ($this->page_type) there will be constructed an url part, for subpages and
* navigation.
*
* @var string
*/
protected $page_url;
/**
* The settings which will be used in the __construct.
*
* @var array
*/
protected $settings;
/**
* @var array
*/
protected $pagination = array();
/**
* Holds the sanitized data from the user input.
*
* @var array
*/
protected $input_fields = array();
/**
* Class constructor.
*
* @param array $args The arguments.
*/
public function __construct( $args = array() ) {
parent::__construct( $this->settings );
$args = wp_parse_args(
$args,
array(
'nonce' => '',
'input_fields' => array(),
)
);
$this->input_fields = $args['input_fields'];
if ( isset( $_SERVER['REQUEST_URI'] ) ) {
$this->request_url = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
}
$this->current_page = ( ! empty( $this->input_fields['paged'] ) ) ? $this->input_fields['paged'] : 1;
$this->current_filter = ( ! empty( $this->input_fields['post_type_filter'] ) ) ? $this->input_fields['post_type_filter'] : 1;
$this->current_status = ( ! empty( $this->input_fields['post_status'] ) ) ? $this->input_fields['post_status'] : 1;
$this->current_order = array(
'order' => ( ! empty( $this->input_fields['order'] ) ) ? $this->input_fields['order'] : 'asc',
'orderby' => ( ! empty( $this->input_fields['orderby'] ) ) ? $this->input_fields['orderby'] : 'post_title',
);
$this->nonce = $args['nonce'];
$this->page_url = "&nonce={$this->nonce}&type={$this->page_type}#top#{$this->page_type}";
$this->populate_editable_post_types();
}
/**
* Prepares the data and renders the page.
*/
public function show_page() {
$this->prepare_page_navigation();
$this->prepare_items();
$this->views();
$this->display();
}
/**
* Used in the constructor to build a reference list of post types the current user can edit.
*/
protected function populate_editable_post_types() {
$post_types = get_post_types(
array(
'public' => true,
'exclude_from_search' => false,
),
'object'
);
$this->all_posts = array();
$this->own_posts = array();
if ( is_array( $post_types ) && $post_types !== array() ) {
foreach ( $post_types as $post_type ) {
if ( ! current_user_can( $post_type->cap->edit_posts ) ) {
continue;
}
if ( current_user_can( $post_type->cap->edit_others_posts ) ) {
$this->all_posts[] = esc_sql( $post_type->name );
}
else {
$this->own_posts[] = esc_sql( $post_type->name );
}
}
}
}
/**
* Will shown the navigation for the table like pagenavigation and pagefilter.
*
* @param string $which Table nav location (such as top).
*/
public function display_tablenav( $which ) {
$post_status = sanitize_text_field( filter_input( INPUT_GET, 'post_status' ) );
?>
<div class="tablenav <?php echo esc_attr( $which ); ?>">
<?php if ( 'top' === $which ) { ?>
<form id="posts-filter" action="" method="get">
<input type="hidden" name="nonce" value="<?php echo esc_attr( $this->nonce ); ?>"/>
<input type="hidden" name="page" value="wpseo_tools"/>
<input type="hidden" name="tool" value="bulk-editor"/>
<input type="hidden" name="type" value="<?php echo esc_attr( $this->page_type ); ?>"/>
<input type="hidden" name="orderby"
value="<?php echo esc_attr( filter_input( INPUT_GET, 'orderby' ) ); ?>"/>
<input type="hidden" name="order"
value="<?php echo esc_attr( filter_input( INPUT_GET, 'order' ) ); ?>"/>
<input type="hidden" name="post_type_filter"
value="<?php echo esc_attr( filter_input( INPUT_GET, 'post_type_filter' ) ); ?>"/>
<?php if ( ! empty( $post_status ) ) { ?>
<input type="hidden" name="post_status" value="<?php echo esc_attr( $post_status ); ?>"/>
<?php } ?>
<?php } ?>
<?php
$this->extra_tablenav( $which );
$this->pagination( $which );
?>
<br class="clear"/>
<?php if ( 'top' === $which ) { ?>
</form>
<?php } ?>
</div>
<?php
}
/**
* This function builds the base sql subquery used in this class.
*
* This function takes into account the post types in which the current user can
* edit all posts, and the ones the current user can only edit his/her own.
*
* @return string The subquery, which should always be used in $wpdb->prepare(),
* passing the current user_id in as the first parameter.
*/
public function get_base_subquery() {
global $wpdb;
$all_posts_string = "'" . implode( "', '", $this->all_posts ) . "'";
$own_posts_string = "'" . implode( "', '", $this->own_posts ) . "'";
$post_author = esc_sql( (int) get_current_user_id() );
$subquery = "(
SELECT *
FROM {$wpdb->posts}
WHERE post_type IN ({$all_posts_string})
UNION ALL
SELECT *
FROM {$wpdb->posts}
WHERE post_type IN ({$own_posts_string}) AND post_author = {$post_author}
) sub_base";
return $subquery;
}
/**
* @return array
*/
public function get_views() {
global $wpdb;
$status_links = array();
$states = get_post_stati( array( 'show_in_admin_all_list' => true ) );
$states = esc_sql( $states );
$all_states = "'" . implode( "', '", $states ) . "'";
$subquery = $this->get_base_subquery();
$total_posts = $wpdb->get_var(
"
SELECT COUNT(ID) FROM {$subquery}
WHERE post_status IN ({$all_states})
"
);
$post_status = filter_input( INPUT_GET, 'post_status' );
$current_link_attributes = empty( $post_status ) ? ' class="current" aria-current="page"' : '';
$localized_text = sprintf(
/* translators: %s expands to the number of posts in localized format. */
_nx( 'All <span class="count">(%s)</span>', 'All <span class="count">(%s)</span>', $total_posts, 'posts', 'wordpress-seo' ),
number_format_i18n( $total_posts )
);
$status_links['all'] = '<a href="' . esc_url( admin_url( 'admin.php?page=wpseo_tools&tool=bulk-editor' . $this->page_url ) ) . '"' . $current_link_attributes . '>' . $localized_text . '</a>';
$post_stati = get_post_stati( array( 'show_in_admin_all_list' => true ), 'objects' );
if ( is_array( $post_stati ) && $post_stati !== array() ) {
foreach ( $post_stati as $status ) {
$status_name = esc_sql( $status->name );
$total = (int) $wpdb->get_var(
$wpdb->prepare(
"
SELECT COUNT(ID) FROM {$subquery}
WHERE post_status = %s
",
$status_name
)
);
if ( $total === 0 ) {
continue;
}
$current_link_attributes = '';
if ( $status_name === $post_status ) {
$current_link_attributes = ' class="current" aria-current="page"';
}
$status_links[ $status_name ] = '<a href="' . esc_url( add_query_arg( array( 'post_status' => $status_name ), admin_url( 'admin.php?page=wpseo_tools&tool=bulk-editor' . $this->page_url ) ) ) . '"' . $current_link_attributes . '>' . sprintf( translate_nooped_plural( $status->label_count, $total ), number_format_i18n( $total ) ) . '</a>';
}
}
unset( $post_stati, $status, $status_name, $total, $current_link_attributes );
$trashed_posts = $wpdb->get_var(
"
SELECT COUNT(ID) FROM {$subquery}
WHERE post_status IN ('trash')
"
);
$current_link_attributes = '';
if ( 'trash' === $post_status ) {
$current_link_attributes = 'class="current" aria-current="page"';
}
$localized_text = sprintf(
/* translators: %s expands to the number of trashed posts in localized format. */
_nx( 'Trash <span class="count">(%s)</span>', 'Trash <span class="count">(%s)</span>', $trashed_posts, 'posts', 'wordpress-seo' ),
number_format_i18n( $trashed_posts )
);
$status_links['trash'] = '<a href="' . esc_url( admin_url( 'admin.php?page=wpseo_tools&tool=bulk-editor&post_status=trash' . $this->page_url ) ) . '"' . $current_link_attributes . '>' . $localized_text . '</a>';
return $status_links;
}
/**
* @param string $which Table nav location (such as top).
*/
public function extra_tablenav( $which ) {
if ( 'top' === $which ) {
$post_types = get_post_types(
array(
'public' => true,
'exclude_from_search' => false,
)
);
$instance_type = esc_attr( $this->page_type );
if ( is_array( $post_types ) && $post_types !== array() ) {
global $wpdb;
echo '<div class="alignleft actions">';
$post_types = esc_sql( $post_types );
$post_types = "'" . implode( "', '", $post_types ) . "'";
$states = get_post_stati( array( 'show_in_admin_all_list' => true ) );
$states['trash'] = 'trash';
$states = esc_sql( $states );
$all_states = "'" . implode( "', '", $states ) . "'";
$subquery = $this->get_base_subquery();
$post_types = $wpdb->get_results(
"
SELECT DISTINCT post_type FROM {$subquery}
WHERE post_status IN ({$all_states})
ORDER BY 'post_type' ASC
"
);
$post_type_filter = filter_input( INPUT_GET, 'post_type_filter' );
$selected = ( ! empty( $post_type_filter ) ) ? sanitize_text_field( $post_type_filter ) : '-1';
$options = '<option value="-1">' . esc_html__( 'Show All Content Types', 'wordpress-seo' ) . '</option>';
if ( is_array( $post_types ) && $post_types !== array() ) {
foreach ( $post_types as $post_type ) {
$obj = get_post_type_object( $post_type->post_type );
$options .= sprintf(
'<option value="%2$s" %3$s>%1$s</option>',
esc_html( $obj->labels->name ),
esc_attr( $post_type->post_type ),
selected( $selected, $post_type->post_type, false )
);
}
}
printf(
'<label for="%1$s" class="screen-reader-text">%2$s</label>',
esc_attr( 'post-type-filter-' . $instance_type ),
esc_html__( 'Filter by content type', 'wordpress-seo' )
);
printf(
'<select name="post_type_filter" id="%2$s">%1$s</select>',
$options,
esc_attr( 'post-type-filter-' . $instance_type )
);
submit_button( __( 'Filter', 'wordpress-seo' ), 'button', false, false, array( 'id' => 'post-query-submit' ) );
echo '</div>';
}
}
}
/**
* Gets a list of sortable columns.
*
* The format is: 'internal-name' => array( 'orderby', bool ).
*
* @return array
*/
public function get_sortable_columns() {
return array(
'col_page_title' => array( 'post_title', true ),
'col_post_type' => array( 'post_type', false ),
'col_post_date' => array( 'post_date', false ),
);
}
/**
* Sets the correct pagenumber and pageurl for the navigation.
*/
public function prepare_page_navigation() {
$request_url = $this->request_url . $this->page_url;
$current_page = $this->current_page;
$current_filter = $this->current_filter;
$current_status = $this->current_status;
$current_order = $this->current_order;
/*
* If current type doesn't compare with objects page_type, then we have to unset
* some vars in the requested url (which will be used for internal table urls).
*/
if ( isset( $this->input_fields['type'] ) && $this->input_fields['type'] !== $this->page_type ) {
$request_url = remove_query_arg( 'paged', $request_url ); // Page will be set with value 1 below.
$request_url = remove_query_arg( 'post_type_filter', $request_url );
$request_url = remove_query_arg( 'post_status', $request_url );
$request_url = remove_query_arg( 'orderby', $request_url );
$request_url = remove_query_arg( 'order', $request_url );
$request_url = add_query_arg( 'pages', 1, $request_url );
$current_page = 1;
$current_filter = '-1';
$current_status = '';
$current_order = array(
'orderby' => 'post_title',
'order' => 'asc',
);
}
$_SERVER['REQUEST_URI'] = $request_url;
$_GET['paged'] = $current_page;
$_REQUEST['paged'] = $current_page;
$_REQUEST['post_type_filter'] = $current_filter;
$_GET['post_type_filter'] = $current_filter;
$_GET['post_status'] = $current_status;
$_GET['orderby'] = $current_order['orderby'];
$_GET['order'] = $current_order['order'];
}
/**
* Preparing the requested pagerows and setting the needed variables.
*/
public function prepare_items() {
$post_type_clause = $this->get_post_type_clause();
$all_states = $this->get_all_states();
$subquery = $this->get_base_subquery();
// Setting the column headers.
$this->set_column_headers();
// Count the total number of needed items and setting pagination given $total_items.
$total_items = $this->count_items( $subquery, $all_states, $post_type_clause );
$this->set_pagination( $total_items );
// Getting items given $query.
$query = $this->parse_item_query( $subquery, $all_states, $post_type_clause );
$this->get_items( $query );
// Get the metadata for the current items ($this->items).
$this->get_meta_data();
}
/**
* Getting the columns for first row.
*
* @return array
*/
public function get_columns() {
return $this->merge_columns();
}
/**
* Setting the column headers.
*/
protected function set_column_headers() {
$columns = $this->get_columns();
$hidden = array();
$sortable = $this->get_sortable_columns();
$this->_column_headers = array( $columns, $hidden, $sortable );
}
/**
* Counting total items.
*
* @param string $subquery SQL FROM part.
* @param string $all_states SQL IN part.
* @param string $post_type_clause SQL post type part.
*
* @return mixed
*/
protected function count_items( $subquery, $all_states, $post_type_clause ) {
global $wpdb;
$total_items = $wpdb->get_var(
"
SELECT COUNT(ID)
FROM {$subquery}
WHERE post_status IN ({$all_states}) $post_type_clause
"
);
return $total_items;
}
/**
* Getting the post_type_clause filter.
*
* @return string
*/
protected function get_post_type_clause() {
// Filter Block.
$post_types = null;
$post_type_clause = '';
$post_type_filter = filter_input( INPUT_GET, 'post_type_filter' );
if ( ! empty( $post_type_filter ) && get_post_type_object( sanitize_text_field( $post_type_filter ) ) ) {
$post_types = esc_sql( sanitize_text_field( $post_type_filter ) );
$post_type_clause = "AND post_type IN ('{$post_types}')";
}
return $post_type_clause;
}
/**
* Setting the pagination.
*
* Total items is the number of all visible items.
*
* @param int $total_items Total items counts.
*/
protected function set_pagination( $total_items ) {
// Calculate items per page.
$per_page = $this->get_items_per_page( 'wpseo_posts_per_page', 10 );
$paged = esc_sql( sanitize_text_field( filter_input( INPUT_GET, 'paged' ) ) );
if ( empty( $paged ) || ! is_numeric( $paged ) || $paged <= 0 ) {
$paged = 1;
}
$this->set_pagination_args(
array(
'total_items' => $total_items,
'total_pages' => ceil( $total_items / $per_page ),
'per_page' => $per_page,
)
);
$this->pagination = array(
'per_page' => $per_page,
'offset' => ( $paged - 1 ) * $per_page,
);
}
/**
* Parse the query to get items from database.
*
* Based on given parameters there will be parse a query which will get all the pages/posts and other post_types
* from the database.
*
* @param string $subquery SQL FROM part.
* @param string $all_states SQL IN part.
* @param string $post_type_clause SQL post type part.
*
* @return string
*/
protected function parse_item_query( $subquery, $all_states, $post_type_clause ) {
// Order By block.
$orderby = filter_input( INPUT_GET, 'orderby' );
$orderby = ! empty( $orderby ) ? esc_sql( sanitize_text_field( $orderby ) ) : 'post_title';
$orderby = $this->sanitize_orderby( $orderby );
// Order clause.
$order = filter_input( INPUT_GET, 'order' );
$order = ! empty( $order ) ? esc_sql( strtoupper( sanitize_text_field( $order ) ) ) : 'ASC';
$order = $this->sanitize_order( $order );
// Get all needed results.
$query = "
SELECT ID, post_title, post_type, post_status, post_modified, post_date
FROM {$subquery}
WHERE post_status IN ({$all_states}) $post_type_clause
ORDER BY {$orderby} {$order}
LIMIT %d,%d
";
return $query;
}
/**
* Heavily restricts the possible columns by which a user can order the table
* in the bulk editor, thereby preventing a possible CSRF vulnerability.
*
* @param string $orderby The column by which we want to order.
*
* @return string $orderby
*/
protected function sanitize_orderby( $orderby ) {
$valid_column_names = array(
'post_title',
'post_type',
'post_date',
);
if ( in_array( $orderby, $valid_column_names, true ) ) {
return $orderby;
}
return 'post_title';
}
/**
* Makes sure the order clause is always ASC or DESC for the bulk editor table,
* thereby preventing a possible CSRF vulnerability.
*
* @param string $order Whether we want to sort ascending or descending.
*
* @return string $order SQL order string (ASC, DESC).
*/
protected function sanitize_order( $order ) {
if ( in_array( strtoupper( $order ), array( 'ASC', 'DESC' ), true ) ) {
return $order;
}
return 'ASC';
}
/**
* Getting all the items.
*
* @param string $query SQL query to use.
*/
protected function get_items( $query ) {
global $wpdb;
$this->items = $wpdb->get_results(
$wpdb->prepare(
$query,
$this->pagination['offset'],
$this->pagination['per_page']
)
);
}
/**
* Getting all the states.
*
* @return string
*/
protected function get_all_states() {
$states = get_post_stati( array( 'show_in_admin_all_list' => true ) );
$states['trash'] = 'trash';
if ( ! empty( $this->input_fields['post_status'] ) ) {
$requested_state = $this->input_fields['post_status'];
if ( in_array( $requested_state, $states, true ) ) {
$states = array( $requested_state );
}
if ( $requested_state !== 'trash' ) {
unset( $states['trash'] );
}
}
$states = esc_sql( $states );
$all_states = "'" . implode( "', '", $states ) . "'";
return $all_states;
}
/**
* Based on $this->items and the defined columns, the table rows will be displayed.
*/
public function display_rows() {
$records = $this->items;
list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
if ( ( is_array( $records ) && $records !== array() ) && ( is_array( $columns ) && $columns !== array() ) ) {
foreach ( $records as $rec ) {
echo '<tr id="', esc_attr( 'record_' . $rec->ID ), '">';
foreach ( $columns as $column_name => $column_display_name ) {
$classes = '';
if ( $primary === $column_name ) {
$classes .= ' has-row-actions column-primary';
}
$attributes = $this->column_attributes( $column_name, $hidden, $classes, $column_display_name );
$column_value = $this->parse_column( $column_name, $rec );
if ( method_exists( $this, 'parse_page_specific_column' ) && empty( $column_value ) ) {
$column_value = $this->parse_page_specific_column( $column_name, $rec, $attributes );
}
if ( ! empty( $column_value ) ) {
printf( '<td %2$s>%1$s</td>', $column_value, $attributes );
}
}
echo '</tr>';
}
}
}
/**
* Getting the attributes for each table cell.
*
* @param string $column_name Column name string.
* @param array $hidden Set of hidden columns.
* @param string $classes Additional CSS classes.
* @param string $column_display_name Column display name string.
*
* @return string
*/
protected function column_attributes( $column_name, $hidden, $classes, $column_display_name ) {
$attributes = '';
$class = array( $column_name, "column-$column_name$classes" );
if ( in_array( $column_name, $hidden, true ) ) {
$class[] = 'hidden';
}
if ( ! empty( $class ) ) {
$attributes = 'class="' . implode( ' ', $class ) . '"';
}
$attributes .= ' data-colname="' . esc_attr( $column_display_name ) . '"';
return $attributes;
}
/**
* Parsing the title.
*
* @param WP_Post $rec Post object.
*
* @return string
*/
protected function parse_page_title_column( $rec ) {
$title = empty( $rec->post_title ) ? __( '(no title)', 'wordpress-seo' ) : $rec->post_title;
$return = sprintf( '<strong>%1$s</strong>', stripslashes( wp_strip_all_tags( $title ) ) );
$post_type_object = get_post_type_object( $rec->post_type );
$can_edit_post = current_user_can( $post_type_object->cap->edit_post, $rec->ID );
$actions = array();
if ( $can_edit_post && 'trash' !== $rec->post_status ) {
$actions['edit'] = sprintf(
'<a href="%s" aria-label="%s">%s</a>',
esc_url( get_edit_post_link( $rec->ID, true ) ),
/* translators: %s: post title */
esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;', 'wordpress-seo' ), $title ) ),
__( 'Edit', 'wordpress-seo' )
);
}
if ( $post_type_object->public ) {
if ( in_array( $rec->post_status, array( 'pending', 'draft', 'future' ), true ) ) {
if ( $can_edit_post ) {
$actions['view'] = sprintf(
'<a href="%s" aria-label="%s">%s</a>',
esc_url( add_query_arg( 'preview', 'true', get_permalink( $rec->ID ) ) ),
/* translators: %s: post title */
esc_attr( sprintf( __( 'Preview &#8220;%s&#8221;', 'wordpress-seo' ), $title ) ),
__( 'Preview', 'wordpress-seo' )
);
}
}
elseif ( 'trash' !== $rec->post_status ) {
$actions['view'] = sprintf(
'<a href="%s" aria-label="%s" rel="bookmark">%s</a>',
esc_url( get_permalink( $rec->ID ) ),
/* translators: %s: post title */
esc_attr( sprintf( __( 'View &#8220;%s&#8221;', 'wordpress-seo' ), $title ) ),
__( 'View', 'wordpress-seo' )
);
}
}
$return .= $this->row_actions( $actions );
return $return;
}
/**
* Parsing the column based on the $column_name.
*
* @param string $column_name Column name.
* @param WP_Post $rec Post object.
*
* @return string
*/
protected function parse_column( $column_name, $rec ) {
static $date_format;
if ( ! isset( $date_format ) ) {
$date_format = get_option( 'date_format' );
}
switch ( $column_name ) {
case 'col_page_title':
$column_value = $this->parse_page_title_column( $rec );
break;
case 'col_page_slug':
$permalink = get_permalink( $rec->ID );
$display_slug = str_replace( get_bloginfo( 'url' ), '', $permalink );
$column_value = sprintf( '<a href="%2$s" target="_blank">%1$s</a>', stripslashes( rawurldecode( $display_slug ) ), esc_url( $permalink ) );
break;
case 'col_post_type':
$post_type = get_post_type_object( $rec->post_type );
$column_value = $post_type->labels->singular_name;
break;
case 'col_post_status':
$post_status = get_post_status_object( $rec->post_status );
$column_value = $post_status->label;
break;
case 'col_post_date':
$column_value = date_i18n( $date_format, strtotime( $rec->post_date ) );
break;
case 'col_row_action':
$column_value = sprintf(
'<a href="#" role="button" class="wpseo-save" data-id="%1$s">%2$s</a> <span aria-hidden="true">|</span> <a href="#" role="button" class="wpseo-save-all">%3$s</a>',
$rec->ID,
esc_html__( 'Save', 'wordpress-seo' ),
esc_html__( 'Save all', 'wordpress-seo' )
);
break;
}
if ( ! empty( $column_value ) ) {
return $column_value;
}
}
/**
* Parse the field where the existing meta-data value is displayed.
*
* @param integer $record_id Record ID.
* @param string $attributes HTML attributes.
* @param bool|array $values Optional values data array.
*
* @return string
*/
protected function parse_meta_data_field( $record_id, $attributes, $values = false ) {
// Fill meta data if exists in $this->meta_data.
$meta_data = ( ! empty( $this->meta_data[ $record_id ] ) ) ? $this->meta_data[ $record_id ] : array();
$meta_key = WPSEO_Meta::$meta_prefix . $this->target_db_field;
$meta_value = ( ! empty( $meta_data[ $meta_key ] ) ) ? $meta_data[ $meta_key ] : '';
if ( ! empty( $values ) ) {
$meta_value = $values[ $meta_value ];
}
return sprintf( '<td %2$s id="wpseo-existing-%4$s-%3$s">%1$s</td>', $meta_value, $attributes, $record_id, $this->target_db_field );
}
/**
* Method for setting the meta data, which belongs to the records that will be shown on the current page.
*
* This method will loop through the current items ($this->items) for getting the post_id. With this data
* ($needed_ids) the method will query the meta-data table for getting the title.
*/
protected function get_meta_data() {
$post_ids = $this->get_post_ids();
$meta_data = $this->get_meta_data_result( $post_ids );
$this->parse_meta_data( $meta_data );
// Little housekeeping.
unset( $post_ids, $meta_data );
}
/**
* Getting all post_ids from to $this->items.
*
* @return string
*/
protected function get_post_ids() {
$needed_ids = array();
foreach ( $this->items as $item ) {
$needed_ids[] = $item->ID;
}
$post_ids = "'" . implode( "', '", $needed_ids ) . "'";
return $post_ids;
}
/**
* Getting the meta_data from database.
*
* @param string $post_ids Post IDs string for SQL IN part.
*
* @return mixed
*/
protected function get_meta_data_result( $post_ids ) {
global $wpdb;
$meta_data = $wpdb->get_results(
"
SELECT *
FROM {$wpdb->postmeta}
WHERE post_id IN({$post_ids}) && meta_key = '" . WPSEO_Meta::$meta_prefix . $this->target_db_field . "'
"
);
return $meta_data;
}
/**
* Setting $this->meta_data.
*
* @param array $meta_data Meta data set.
*/
protected function parse_meta_data( $meta_data ) {
foreach ( $meta_data as $row ) {
$this->meta_data[ $row->post_id ][ $row->meta_key ] = $row->meta_value;
}
}
/**
* This method will merge general array with given parameter $columns.
*
* @param array $columns Optional columns set.
*
* @return array
*/
protected function merge_columns( $columns = array() ) {
$columns = array_merge(
array(
'col_page_title' => __( 'WP Page Title', 'wordpress-seo' ),
'col_post_type' => __( 'Content Type', 'wordpress-seo' ),
'col_post_status' => __( 'Post Status', 'wordpress-seo' ),
'col_post_date' => __( 'Publication date', 'wordpress-seo' ),
'col_page_slug' => __( 'Page URL/Slug', 'wordpress-seo' ),
),
$columns
);
$columns['col_row_action'] = __( 'Action', 'wordpress-seo' );
return $columns;
}
} /* End of class */

View File

@ -0,0 +1,87 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Bulk Editor
* @since 1.5.0
*/
/**
* Implements table for bulk title editing.
*/
class WPSEO_Bulk_Title_Editor_List_Table extends WPSEO_Bulk_List_Table {
/**
* Current type for this class will be title.
*
* @var string
*/
protected $page_type = 'title';
/**
* Settings with are used in __construct.
*
* @var array
*/
protected $settings = array(
'singular' => 'wpseo_bulk_title',
'plural' => 'wpseo_bulk_titles',
'ajax' => true,
);
/**
* The field in the database where meta field is saved.
*
* @var string
*/
protected $target_db_field = 'title';
/**
* The columns shown on the table.
*
* @return array
*/
public function get_columns() {
$columns = array(
/* translators: %1$s expands to Yoast SEO */
'col_existing_yoast_seo_title' => sprintf( __( 'Existing %1$s Title', 'wordpress-seo' ), 'Yoast SEO' ),
/* translators: %1$s expands to Yoast SEO */
'col_new_yoast_seo_title' => sprintf( __( 'New %1$s Title', 'wordpress-seo' ), 'Yoast SEO' ),
);
return $this->merge_columns( $columns );
}
/**
* Parse the title columns.
*
* @param string $column_name Column name.
* @param object $record Data object.
* @param string $attributes HTML attributes.
*
* @return string
*/
protected function parse_page_specific_column( $column_name, $record, $attributes ) {
// Fill meta data if exists in $this->meta_data.
$meta_data = ( ! empty( $this->meta_data[ $record->ID ] ) ) ? $this->meta_data[ $record->ID ] : array();
switch ( $column_name ) {
case 'col_existing_yoast_seo_title':
// @todo Inconsistent echo/return behavior R.
echo $this->parse_meta_data_field( $record->ID, $attributes );
break;
case 'col_new_yoast_seo_title':
return sprintf(
'<input type="text" id="%1$s" name="%1$s" class="wpseo-new-title" data-id="%2$s" aria-labelledby="col_new_yoast_seo_title" />',
'wpseo-new-title-' . $record->ID,
$record->ID
);
}
unset( $meta_data );
}
} /* End of class */

View File

@ -0,0 +1,50 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Collects the data from the added collection objects.
*/
class WPSEO_Collector {
/**
* @var WPSEO_Collection[]
*/
protected $collections = array();
/**
* Adds a collection object to the collections.
*
* @param WPSEO_Collection $collection The collection object to add.
*/
public function add_collection( WPSEO_Collection $collection ) {
$this->collections[] = $collection;
}
/**
* Collects the data from the collection objects.
*
* @return array The collected data.
*/
public function collect() {
$data = array();
foreach ( $this->collections as $collection ) {
$data = array_merge( $data, $collection->get() );
}
return $data;
}
/**
* Returns the collected data as a JSON encoded string.
*
* @return false|string The encode string.
*/
public function get_as_json() {
return WPSEO_Utils::format_json_encode( $this->collect() );
}
}

View File

@ -0,0 +1,179 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Class WPSEO_Admin_Pages.
*
* Class with functionality for the Yoast SEO admin pages.
*/
class WPSEO_Admin_Pages {
/**
* The option in use for the current admin page.
*
* @var string
*/
public $currentoption = 'wpseo';
/**
* Holds the asset manager.
*
* @var WPSEO_Admin_Asset_Manager
*/
private $asset_manager;
/**
* Class constructor, which basically only hooks the init function on the init hook.
*/
public function __construct() {
add_action( 'init', array( $this, 'init' ), 20 );
$this->asset_manager = new WPSEO_Admin_Asset_Manager();
}
/**
* Make sure the needed scripts are loaded for admin pages.
*/
public function init() {
if ( filter_input( INPUT_GET, 'wpseo_reset_defaults' ) && wp_verify_nonce( filter_input( INPUT_GET, 'nonce' ), 'wpseo_reset_defaults' ) && current_user_can( 'manage_options' ) ) {
WPSEO_Options::reset();
wp_redirect( admin_url( 'admin.php?page=' . WPSEO_Configuration_Page::PAGE_IDENTIFIER ) );
}
add_action( 'admin_enqueue_scripts', array( $this, 'config_page_scripts' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'config_page_styles' ) );
}
/**
* Loads the required styles for the config page.
*/
public function config_page_styles() {
wp_enqueue_style( 'dashboard' );
wp_enqueue_style( 'thickbox' );
wp_enqueue_style( 'global' );
wp_enqueue_style( 'wp-admin' );
$this->asset_manager->enqueue_style( 'select2' );
$this->asset_manager->enqueue_style( 'admin-css' );
}
/**
* Loads the required scripts for the config page.
*/
public function config_page_scripts() {
$this->asset_manager->enqueue_script( 'admin-script' );
$this->asset_manager->enqueue_script( 'help-center' );
$page = filter_input( INPUT_GET, 'page' );
if ( $page === 'wpseo_titles' ) {
wp_localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'search-appearance', 'wpseoReplaceVarsL10n', $this->localize_replace_vars_script() );
wp_localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'search-appearance', 'wpseoSearchAppearance', $this->localize_search_appearance_script() );
$this->asset_manager->enqueue_script( 'search-appearance' );
$this->asset_manager->enqueue_style( 'search-appearance' );
/**
* Remove the emoji script as it is incompatible with both React and any
* contenteditable fields.
*/
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
$yoast_components_l10n = new WPSEO_Admin_Asset_Yoast_Components_L10n();
$yoast_components_l10n->localize_script( 'search-appearance' );
}
wp_enqueue_script( 'dashboard' );
wp_enqueue_script( 'thickbox' );
wp_localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'admin-script', 'wpseoSelect2Locale', WPSEO_Language_Utils::get_language( WPSEO_Language_Utils::get_user_locale() ) );
if ( in_array( $page, array( 'wpseo_social', WPSEO_Admin::PAGE_IDENTIFIER, 'wpseo_titles' ), true ) ) {
wp_enqueue_media();
$this->asset_manager->enqueue_script( 'admin-media' );
wp_localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'admin-media', 'wpseoMediaL10n', $this->localize_media_script() );
}
if ( 'wpseo_tools' === $page ) {
$this->enqueue_tools_scripts();
}
}
/**
* Retrieves some variables that are needed for the upload module in JS.
*
* @return array The upload module variables.
*/
public function localize_media_script() {
return array(
'choose_image' => __( 'Use Image', 'wordpress-seo' ),
);
}
/**
* Retrieves some variables that are needed for replacing variables in JS.
*
* @return array The replacement and recommended replacement variables.
*/
public function localize_replace_vars_script() {
$replace_vars = new WPSEO_Replace_Vars();
$recommended_replace_vars = new WPSEO_Admin_Recommended_Replace_Vars();
$editor_specific_replace_vars = new WPSEO_Admin_Editor_Specific_Replace_Vars();
$replace_vars_list = $replace_vars->get_replacement_variables_list();
return array(
'replace_vars' => $replace_vars_list,
'recommended_replace_vars' => $recommended_replace_vars->get_recommended_replacevars(),
'editor_specific_replace_vars' => $editor_specific_replace_vars->get(),
'shared_replace_vars' => $editor_specific_replace_vars->get_generic( $replace_vars_list ),
);
}
/**
* Retrieves some variables that are needed for the search appearance in JS.
*
* @return array The search appearance variables.
*/
public function localize_search_appearance_script() {
return array(
'isRtl' => is_rtl(),
'userEditUrl' => add_query_arg( 'user_id', '{user_id}', admin_url( 'user-edit.php' ) ),
'brushstrokeBackgroundURL' => plugins_url( 'images/brushstroke_background.svg', WPSEO_FILE ),
'showLocalSEOUpsell' => $this->should_show_local_seo_upsell(),
'localSEOUpsellURL' => WPSEO_Shortlinker::get( 'https://yoa.st/3mp' ),
);
}
/**
* Determines whether the Local SEO upsell should be shown.
*
* The Local SEO upsell should:
* - Only be shown in Free, not when Premium is active.
* - Not be shown when Local SEO is active.
*
* @return bool Whether the Local SEO upsell should be shown.
*/
private function should_show_local_seo_upsell() {
$addon_manager = new WPSEO_Addon_Manager();
return ! WPSEO_Utils::is_yoast_seo_premium() &&
! $addon_manager->has_valid_subscription( WPSEO_Addon_Manager::LOCAL_SLUG );
}
/**
* Enqueues and handles all the tool dependencies.
*/
private function enqueue_tools_scripts() {
$tool = filter_input( INPUT_GET, 'tool' );
if ( empty( $tool ) ) {
$this->asset_manager->enqueue_script( 'yoast-seo' );
}
if ( 'bulk-editor' === $tool ) {
$this->asset_manager->enqueue_script( 'bulk-editor' );
}
}
} /* End of class */

View File

@ -0,0 +1,227 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Customizer
*/
/**
* Class with functionality to support WP SEO settings in WordPress Customizer.
*/
class WPSEO_Customizer {
/**
* @var WP_Customize_Manager
*/
protected $wp_customize;
/**
* Template for the setting IDs used for the customizer.
*
* @var string
*/
private $setting_template = 'wpseo_titles[%s]';
/**
* Default arguments for the breadcrumbs customizer settings object.
*
* @var array
*/
private $default_setting_args = array(
'default' => '',
'type' => 'option',
'transport' => 'refresh',
);
/**
* Default arguments for the breadcrumbs customizer control object.
*
* @var array
*/
private $default_control_args = array(
'label' => '',
'type' => 'text',
'section' => 'wpseo_breadcrumbs_customizer_section',
'settings' => '',
'context' => '',
);
/**
* Construct Method.
*/
public function __construct() {
add_action( 'customize_register', array( $this, 'wpseo_customize_register' ) );
}
/**
* Function to support WordPress Customizer.
*
* @param WP_Customize_Manager $wp_customize Manager class instance.
*/
public function wpseo_customize_register( $wp_customize ) {
if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ) ) {
return;
}
$this->wp_customize = $wp_customize;
$this->breadcrumbs_section();
$this->breadcrumbs_blog_remove_setting();
$this->breadcrumbs_separator_setting();
$this->breadcrumbs_home_setting();
$this->breadcrumbs_prefix_setting();
$this->breadcrumbs_archiveprefix_setting();
$this->breadcrumbs_searchprefix_setting();
$this->breadcrumbs_404_setting();
}
/**
* Add the breadcrumbs section to the customizer.
*/
private function breadcrumbs_section() {
$section_args = array(
/* translators: %s is the name of the plugin */
'title' => sprintf( __( '%s Breadcrumbs', 'wordpress-seo' ), 'Yoast SEO' ),
'priority' => 999,
'active_callback' => array( $this, 'breadcrumbs_active_callback' ),
);
$this->wp_customize->add_section( 'wpseo_breadcrumbs_customizer_section', $section_args );
}
/**
* Returns whether or not the breadcrumbs are active.
*
* @return bool
*/
public function breadcrumbs_active_callback() {
return true === ( current_theme_supports( 'yoast-seo-breadcrumbs' ) || WPSEO_Options::get( 'breadcrumbs-enable' ) );
}
/**
* Adds the breadcrumbs remove blog checkbox.
*/
private function breadcrumbs_blog_remove_setting() {
$index = 'breadcrumbs-display-blog-page';
$control_args = array(
'label' => __( 'Remove blog page from breadcrumbs', 'wordpress-seo' ),
'type' => 'checkbox',
'active_callback' => array( $this, 'breadcrumbs_blog_remove_active_cb' ),
);
$this->add_setting_and_control( $index, $control_args );
}
/**
* Returns whether or not to show the breadcrumbs blog remove option.
*
* @return bool
*/
public function breadcrumbs_blog_remove_active_cb() {
return 'page' === get_option( 'show_on_front' );
}
/**
* Adds the breadcrumbs separator text field.
*/
private function breadcrumbs_separator_setting() {
$index = 'breadcrumbs-sep';
$control_args = array(
'label' => __( 'Breadcrumbs separator:', 'wordpress-seo' ),
);
$id = 'wpseo-breadcrumbs-separator';
$this->add_setting_and_control( $index, $control_args, $id );
}
/**
* Adds the breadcrumbs home anchor text field.
*/
private function breadcrumbs_home_setting() {
$index = 'breadcrumbs-home';
$control_args = array(
'label' => __( 'Anchor text for the homepage:', 'wordpress-seo' ),
);
$this->add_setting_and_control( $index, $control_args );
}
/**
* Adds the breadcrumbs prefix text field.
*/
private function breadcrumbs_prefix_setting() {
$index = 'breadcrumbs-prefix';
$control_args = array(
'label' => __( 'Prefix for breadcrumbs:', 'wordpress-seo' ),
);
$this->add_setting_and_control( $index, $control_args );
}
/**
* Adds the breadcrumbs archive prefix text field.
*/
private function breadcrumbs_archiveprefix_setting() {
$index = 'breadcrumbs-archiveprefix';
$control_args = array(
'label' => __( 'Prefix for archive pages:', 'wordpress-seo' ),
);
$this->add_setting_and_control( $index, $control_args );
}
/**
* Adds the breadcrumbs search prefix text field.
*/
private function breadcrumbs_searchprefix_setting() {
$index = 'breadcrumbs-searchprefix';
$control_args = array(
'label' => __( 'Prefix for search result pages:', 'wordpress-seo' ),
);
$this->add_setting_and_control( $index, $control_args );
}
/**
* Adds the breadcrumb 404 prefix text field.
*/
private function breadcrumbs_404_setting() {
$index = 'breadcrumbs-404crumb';
$control_args = array(
'label' => __( 'Breadcrumb for 404 pages:', 'wordpress-seo' ),
);
$this->add_setting_and_control( $index, $control_args );
}
/**
* Adds the customizer setting and control.
*
* @param string $index Array key index to use for the customizer setting.
* @param array $control_args Customizer control object arguments.
* Only those different from the default need to be passed.
* @param string $id Optional. Customizer control object ID.
* Will default to 'wpseo-' . $index.
* @param array $custom_settings Optional. Customizer setting arguments.
* Only those different from the default need to be passed.
*/
private function add_setting_and_control( $index, $control_args, $id = null, $custom_settings = array() ) {
$setting = sprintf( $this->setting_template, $index );
$control_args = array_merge( $this->default_control_args, $control_args );
$control_args['settings'] = $setting;
$settings_args = $this->default_setting_args;
if ( ! empty( $custom_settings ) ) {
$settings_args = array_merge( $settings_args, $custom_settings );
}
if ( ! isset( $id ) ) {
$id = 'wpseo-' . $index;
}
$this->wp_customize->add_setting( $setting, $settings_args );
$control = new WP_Customize_Control( $this->wp_customize, $id, $control_args );
$this->wp_customize->add_control( $control );
}
}

View File

@ -0,0 +1,288 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Represents the proxy for communicating with the database.
*/
class WPSEO_Database_Proxy {
/**
* @var string
*/
protected $table_name;
/**
* @var bool
*/
protected $suppress_errors = true;
/**
* @var bool
*/
protected $is_multisite_table = false;
/**
* @var bool
*/
protected $last_suppressed_state;
/**
* @var wpdb
*/
protected $database;
/**
* Sets the class attributes and registers the table.
*
* @param wpdb $database The database object.
* @param string $table_name The table name that is represented.
* @param bool $suppress_errors Should the errors be suppressed.
* @param bool $is_multisite_table Should the table be global in multisite.
*/
public function __construct( $database, $table_name, $suppress_errors = true, $is_multisite_table = false ) {
$this->table_name = $table_name;
$this->suppress_errors = (bool) $suppress_errors;
$this->is_multisite_table = (bool) $is_multisite_table;
$this->database = $database;
// If the table prefix was provided, strip it as it's handled automatically.
$table_prefix = $this->get_table_prefix();
if ( ! empty( $table_prefix ) && strpos( $this->table_name, $table_prefix ) === 0 ) {
$this->table_prefix = substr( $this->table_name, strlen( $table_prefix ) );
}
if ( ! $this->is_table_registered() ) {
$this->register_table();
}
}
/**
* Inserts data into the database.
*
* @param array $data Data to insert.
* @param null $format Formats for the data.
*
* @return false|int Total amount of inserted rows or false on error.
*/
public function insert( array $data, $format = null ) {
$this->pre_execution();
$result = $this->database->insert( $this->get_table_name(), $data, $format );
$this->post_execution();
return $result;
}
/**
* Updates data in the database.
*
* @param array $data Data to update on the table.
* @param array $where Where condition as key => value array.
* @param null $format Optional. data prepare format.
* @param null $where_format Optional. Where prepare format.
*
* @return false|int False when the update request is invalid, int on number of rows changed.
*/
public function update( array $data, array $where, $format = null, $where_format = null ) {
$this->pre_execution();
$result = $this->database->update( $this->get_table_name(), $data, $where, $format, $where_format );
$this->post_execution();
return $result;
}
/**
* Upserts data in the database.
*
* Performs an insert into and if key is duplicate it will update the existing record.
*
* @param array $data Data to update on the table.
* @param array $where Unused. Where condition as key => value array.
* @param null $format Optional. Data prepare format.
* @param null $where_format Deprecated. Where prepare format.
*
* @return false|int False when the upsert request is invalid, int on number of rows changed.
*/
public function upsert( array $data, array $where = null, $format = null, $where_format = null ) {
if ( $where_format !== null ) {
_deprecated_argument( __METHOD__, '7.7.0', 'The where_format argument is deprecated' );
}
$this->pre_execution();
$update = array();
$keys = array();
$columns = array_keys( $data );
foreach ( $columns as $column ) {
$keys[] = '`' . $column . '`';
$update[] = sprintf( '`%1$s` = VALUES(`%1$s`)', $column );
}
$query = sprintf(
'INSERT INTO `%1$s` (%2$s) VALUES ( %3$s ) ON DUPLICATE KEY UPDATE %4$s',
$this->get_table_name(),
implode( ', ', $keys ),
implode( ', ', array_fill( 0, count( $data ), '%s' ) ),
implode( ', ', $update )
);
$result = $this->database->query(
$this->database->prepare(
$query,
array_values( $data )
)
);
$this->post_execution();
return $result;
}
/**
* Deletes a record from the database.
*
* @param array $where Where clauses for the query.
* @param null|array $format Formats for the data.
*
* @return false|int
*/
public function delete( array $where, $format = null ) {
$this->pre_execution();
$result = $this->database->delete( $this->get_table_name(), $where, $format );
$this->post_execution();
return $result;
}
/**
* Executes the given query and returns the results.
*
* @param string $query The query to execute.
*
* @return array|null|object The resultset
*/
public function get_results( $query ) {
$this->pre_execution();
$results = $this->database->get_results( $query );
$this->post_execution();
return $results;
}
/**
* Creates a table to the database.
*
* @param array $columns The columns to create.
* @param array $indexes The indexes to use.
*
* @return bool True when creation is successful.
*/
public function create_table( array $columns, array $indexes = array() ) {
$create_table = sprintf(
'CREATE TABLE IF NOT EXISTS %1$s ( %2$s ) %3$s',
$this->get_table_name(),
implode( ',', array_merge( $columns, $indexes ) ),
$this->database->get_charset_collate()
);
$this->pre_execution();
$is_created = (bool) $this->database->query( $create_table );
$this->post_execution();
return $is_created;
}
/**
* Checks if there is an error.
*
* @return bool Returns true when there is an error.
*/
public function has_error() {
return ( $this->database->last_error !== '' );
}
/**
* Executed before a query will be ran.
*/
protected function pre_execution() {
if ( $this->suppress_errors ) {
$this->last_suppressed_state = $this->database->suppress_errors();
}
}
/**
* Executed after a query has been ran.
*/
protected function post_execution() {
if ( $this->suppress_errors ) {
$this->database->suppress_errors( $this->last_suppressed_state );
}
}
/**
* Returns the full table name.
*
* @return string Full table name including prefix.
*/
public function get_table_name() {
return $this->get_table_prefix() . $this->table_name;
}
/**
* Returns the prefix to use for the table.
*
* @return string The table prefix depending on the database context.
*/
protected function get_table_prefix() {
if ( $this->is_multisite_table ) {
return $this->database->base_prefix;
}
return $this->database->get_blog_prefix();
}
/**
* Registers the table with WordPress.
*
* @return void
*/
protected function register_table() {
$table_name = $this->table_name;
$full_table_name = $this->get_table_name();
$this->database->$table_name = $full_table_name;
if ( $this->is_multisite_table ) {
$this->database->ms_global_tables[] = $table_name;
return;
}
$this->database->tables[] = $table_name;
}
/**
* Checks if the table has been registered with WordPress.
*
* @return bool True if the table is registered, false otherwise.
*/
protected function is_table_registered() {
if ( $this->is_multisite_table ) {
return in_array( $this->table_name, $this->database->ms_global_tables, true );
}
return in_array( $this->table_name, $this->database->tables, true );
}
}

View File

@ -0,0 +1,168 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Export
*/
/**
* Class WPSEO_Export.
*
* Class with functionality to export the WP SEO settings.
*/
class WPSEO_Export {
/**
* @var string
*/
const NONCE_ACTION = 'wpseo_export';
/**
* @var string
*/
private $export = '';
/**
* @var string
*/
private $error = '';
/**
* @var boolean
*/
public $success;
/**
* Handles the export request.
*/
public function export() {
check_admin_referer( self::NONCE_ACTION );
$this->export_settings();
$this->output();
}
/**
* Outputs the export.
*/
public function output() {
if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ) ) {
esc_html_e( 'You do not have the required rights to export settings.', 'wordpress-seo' );
return;
}
echo '<p>';
printf(
/* translators: %1$s expands to Import settings */
esc_html__(
'Copy all these settings to another site\'s %1$s tab and click "%1$s" there.',
'wordpress-seo'
),
esc_html__(
'Import settings',
'wordpress-seo'
)
);
echo '</p>';
echo '<textarea id="wpseo-export" rows="20" cols="100">' . $this->export . '</textarea>';
}
/**
* Returns true when the property error has a value.
*
* @return bool
*/
public function has_error() {
return ( $this->error !== '' );
}
/**
* Sets the error hook, to display the error to the user.
*/
public function set_error_hook() {
/* translators: %1$s expands to Yoast SEO */
$message = sprintf( __( 'Error creating %1$s export: ', 'wordpress-seo' ), 'Yoast SEO' ) . $this->error;
printf(
'<div class="notice notice-error"><p>%1$s</p></div>',
$message
);
}
/**
* Exports the current site's WP SEO settings.
*/
private function export_settings() {
$this->export_header();
foreach ( WPSEO_Options::get_option_names() as $opt_group ) {
$this->write_opt_group( $opt_group );
}
}
/**
* Writes the header of the export.
*/
private function export_header() {
$header = sprintf(
/* translators: %1$s expands to Yoast SEO, %2$s expands to Yoast.com */
esc_html__( 'These are settings for the %1$s plugin by %2$s', 'wordpress-seo' ),
'Yoast SEO',
'Yoast.com'
);
$this->write_line( '; ' . $header );
}
/**
* Writes a line to the export.
*
* @param string $line Line string.
* @param boolean $newline_first Boolean flag whether to prepend with new line.
*/
private function write_line( $line, $newline_first = false ) {
if ( $newline_first ) {
$this->export .= PHP_EOL;
}
$this->export .= $line . PHP_EOL;
}
/**
* Writes an entire option group to the export.
*
* @param string $opt_group Option group name.
*/
private function write_opt_group( $opt_group ) {
$this->write_line( '[' . $opt_group . ']', true );
$options = get_option( $opt_group );
if ( ! is_array( $options ) ) {
return;
}
foreach ( $options as $key => $elem ) {
if ( is_array( $elem ) ) {
$count = count( $elem );
for ( $i = 0; $i < $count; $i++ ) {
$this->write_setting( $key . '[]', $elem[ $i ] );
}
}
else {
$this->write_setting( $key, $elem );
}
}
}
/**
* Writes a settings line to the export.
*
* @param string $key Key string.
* @param string $val Value string.
*/
private function write_setting( $key, $val ) {
if ( is_string( $val ) ) {
$val = '"' . $val . '"';
}
$this->write_line( $key . ' = ' . $val );
}
}

View File

@ -0,0 +1,101 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Exposes shortlinks in a global, so that we can pass them to our Javascript components.
*/
class WPSEO_Expose_Shortlinks implements WPSEO_WordPress_Integration {
/**
* Array containing the keys and shortlinks.
*
* @var array
*/
private $shortlinks = array(
'shortlinks.focus_keyword_info' => 'https://yoa.st/focus-keyword',
'shortlinks.snippet_preview_info' => 'https://yoa.st/snippet-preview',
'shortlinks.cornerstone_content_info' => 'https://yoa.st/1i9',
'shortlinks.upsell.sidebar.focus_keyword_synonyms_link' => 'https://yoa.st/textlink-synonyms-popup-sidebar',
'shortlinks.upsell.sidebar.focus_keyword_synonyms_button' => 'https://yoa.st/keyword-synonyms-popup-sidebar',
'shortlinks.upsell.sidebar.focus_keyword_additional_link' => 'https://yoa.st/textlink-keywords-popup-sidebar',
'shortlinks.upsell.sidebar.focus_keyword_additional_button' => 'https://yoa.st/add-keywords-popup-sidebar',
'shortlinks.upsell.sidebar.additional_link' => 'https://yoa.st/textlink-keywords-sidebar',
'shortlinks.upsell.sidebar.additional_button' => 'https://yoa.st/add-keywords-sidebar',
'shortlinks.upsell.metabox.go_premium' => 'https://yoa.st/pe-premium-page',
'shortlinks.upsell.metabox.focus_keyword_synonyms_link' => 'https://yoa.st/textlink-synonyms-popup-metabox',
'shortlinks.upsell.metabox.focus_keyword_synonyms_button' => 'https://yoa.st/keyword-synonyms-popup',
'shortlinks.upsell.metabox.focus_keyword_additional_link' => 'https://yoa.st/textlink-keywords-popup-metabox',
'shortlinks.upsell.metabox.focus_keyword_additional_button' => 'https://yoa.st/add-keywords-popup',
'shortlinks.upsell.metabox.additional_link' => 'https://yoa.st/textlink-keywords-metabox',
'shortlinks.upsell.metabox.additional_button' => 'https://yoa.st/add-keywords-metabox',
'shortlinks.upsell.gsc.create_redirect_button' => 'https://yoa.st/redirects',
'shortlinks.readability_analysis_info' => 'https://yoa.st/readability-analysis',
'shortlinks.activate_premium_info' => 'https://yoa.st/activate-subscription',
'shortlinks.upsell.sidebar.morphology_upsell_metabox' => 'https://yoa.st/morphology-upsell-metabox',
'shortlinks.upsell.sidebar.morphology_upsell_sidebar' => 'https://yoa.st/morphology-upsell-sidebar',
);
/**
* Registers all hooks to WordPress.
*
* @return void
*/
public function register_hooks() {
add_filter( 'wpseo_admin_l10n', array( $this, 'expose_shortlinks' ) );
}
/**
* Adds shortlinks to the passed array.
*
* @param array $input The array to add shortlinks to.
*
* @return array The passed array with the additional shortlinks.
*/
public function expose_shortlinks( $input ) {
foreach ( $this->get_shortlinks() as $key => $shortlink ) {
$input[ $key ] = WPSEO_Shortlinker::get( $shortlink );
}
$input['default_query_params'] = WPSEO_Shortlinker::get_query_params();
return $input;
}
/**
* Retrieves the shortlinks.
*
* @return array The shortlinks.
*/
private function get_shortlinks() {
if ( ! $this->is_term_edit() ) {
return $this->shortlinks;
}
$shortlinks = $this->shortlinks;
$shortlinks['shortlinks.upsell.metabox.focus_keyword_synonyms_link'] = 'https://yoa.st/textlink-synonyms-popup-metabox-term';
$shortlinks['shortlinks.upsell.metabox.focus_keyword_synonyms_button'] = 'https://yoa.st/keyword-synonyms-popup-term';
$shortlinks['shortlinks.upsell.metabox.focus_keyword_additional_link'] = 'https://yoa.st/textlink-keywords-popup-metabox-term';
$shortlinks['shortlinks.upsell.metabox.focus_keyword_additional_button'] = 'https://yoa.st/add-keywords-popup-term';
$shortlinks['shortlinks.upsell.metabox.additional_link'] = 'https://yoa.st/textlink-keywords-metabox-term';
$shortlinks['shortlinks.upsell.metabox.additional_button'] = 'https://yoa.st/add-keywords-metabox-term';
$shortlinks['shortlinks.upsell.sidebar.morphology_upsell_metabox'] = 'https://yoa.st/morphology-upsell-metabox-term';
return $shortlinks;
}
/**
* Checks if the current page is a term edit page.
*
* @return bool True when page is term edit.
*/
private function is_term_edit() {
global $pagenow;
return WPSEO_Taxonomy::is_term_edit( $pagenow );
}
}

View File

@ -0,0 +1,150 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Represents the class that contains the available extensions for Yoast SEO.
*/
class WPSEO_Extension_Manager {
/**
* The transient key to save the cache in.
*
* @var string
*/
const TRANSIENT_CACHE_KEY = 'wpseo_license_active_extensions';
/**
* @var WPSEO_Extension[]
*/
protected $extensions = array();
/**
* List of active plugins.
*
* @var array
*/
protected static $active_extensions;
/**
* Adds an extension to the manager.
*
* @param string $extension_name The extension name.
* @param WPSEO_Extension $extension The extension value object.
*
* @return void
*/
public function add( $extension_name, WPSEO_Extension $extension = null ) {
$this->extensions[ $extension_name ] = $extension;
}
/**
* Removes an extension from the manager.
*
* @param string $extension_name The name of the extension to remove.
*
* @return void
*/
public function remove( $extension_name ) {
if ( array_key_exists( $extension_name, $this->extensions ) ) {
unset( $this->extensions[ $extension_name ] );
}
}
/**
* Returns the extension for the given extension name.
*
* @param string $extension_name The name of the extension to get.
*
* @return null|WPSEO_Extension The extension object or null when it doesn't exist.
*/
public function get( $extension_name ) {
if ( array_key_exists( $extension_name, $this->extensions ) ) {
return $this->extensions[ $extension_name ];
}
return null;
}
/**
* Returns all set extension.
*
* @return WPSEO_Extension[] Array with the extensions.
*/
public function get_all() {
return $this->extensions;
}
/**
* Checks if the plugin is activated within My Yoast.
*
* @param string $extension_name The extension name to check.
*
* @return bool True when the plugin is activated.
*/
public function is_activated( $extension_name ) {
if ( self::$active_extensions === null ) {
// Force re-check on license & dashboard pages.
$current_page = $this->get_current_page();
// Check whether the licenses are valid or whether we need to show notifications.
$exclude_cache = ( $current_page === 'wpseo_licenses' || $current_page === 'wpseo_dashboard' );
// Fetch transient data on any other page.
if ( ! $exclude_cache ) {
self::$active_extensions = $this->get_cached_extensions();
}
// If the active extensions is still NULL, we need to set it.
if ( ! is_array( self::$active_extensions ) ) {
self::$active_extensions = $this->retrieve_active_extensions();
$this->set_cached_extensions( self::$active_extensions );
}
}
return in_array( $extension_name, self::$active_extensions, true );
}
/**
* Retrieves the active extensions via an external request.
*
* @return array Array containing the active extensions.
*/
protected function retrieve_active_extensions() {
return (array) apply_filters( 'yoast-active-extensions', array() );
}
/**
* Returns the current page.
*
* @return string The current page.
*/
protected function get_current_page() {
return filter_input( INPUT_GET, 'page' );
}
/**
* Gets a cached list of active extensions.
*
* @return boolean|array The cached extensions.
*/
protected function get_cached_extensions() {
return get_transient( self::TRANSIENT_CACHE_KEY );
}
/**
* Sets the active extensions transient for the set duration.
*
* @param array $extensions The extensions to add.
* @param int $duration The duration that the list of extensions needs to remain cached.
*
* @return void
*/
protected function set_cached_extensions( $extensions, $duration = DAY_IN_SECONDS ) {
set_transient( self::TRANSIENT_CACHE_KEY, $extensions, $duration );
}
}

View File

@ -0,0 +1,93 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Represents the values for a single Yoast Premium extension plugin.
*/
class WPSEO_Extension {
/**
* @var array
*/
protected $config = array();
/**
* WPSEO_Extension constructor.
*
* @param array $config The config to use.
*/
public function __construct( array $config ) {
$this->config = $config;
}
/**
* Returns the product title.
*
* @return string The set title.
*/
public function get_title() {
return $this->config['title'];
}
/**
* Returns the product title to display.
*
* @return string The title to display on the license page.
*/
public function get_display_title() {
return empty( $this->config['display_title'] ) ? $this->get_title() : $this->config['display_title'];
}
/**
* Returns URL to the page where the product can be bought.
*
* @return string The buy url.
*/
public function get_buy_url() {
return $this->config['buyUrl'];
}
/**
* Returns URL to the page with more info.
*
* @return string The url to the info page.
*/
public function get_info_url() {
return $this->config['infoUrl'];
}
/**
* Returns the image.
*
* @return string The image.
*/
public function get_image() {
return $this->config['image'];
}
/**
* Returns the buy button value if set, otherwise fallback to the title.
*
* @return string The buy button.
*/
public function get_buy_button() {
if ( isset( $this->config['buy_button'] ) ) {
return $this->config['buy_button'];
}
return $this->get_title();
}
/**
* Returns the benefits.
*
* @return array The array with benefits.
*/
public function get_benefits() {
return $this->config['benefits'];
}
}

View File

@ -0,0 +1,112 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Represents the class that contains the list of possible extensions for Yoast SEO.
*/
class WPSEO_Extensions {
/**
* Array with the Yoast extensions.
*
* @var array
*/
protected $extensions = array(
'Yoast SEO Premium' => array(
'slug' => 'yoast-seo-premium',
'identifier' => 'wordpress-seo-premium',
'classname' => 'WPSEO_Premium',
'my-yoast-slug' => WPSEO_Addon_Manager::PREMIUM_SLUG,
),
'News SEO' => array(
'slug' => 'news-seo',
'identifier' => 'wpseo-news',
'classname' => 'WPSEO_News',
'my-yoast-slug' => WPSEO_Addon_Manager::NEWS_SLUG,
),
'Yoast WooCommerce SEO' => array(
'slug' => 'woocommerce-yoast-seo',
'identifier' => 'wpseo-woocommerce',
'classname' => 'Yoast_WooCommerce_SEO',
'my-yoast-slug' => WPSEO_Addon_Manager::WOOCOMMERCE_SLUG,
),
'Video SEO' => array(
'slug' => 'video-seo-for-wordpress',
'identifier' => 'wpseo-video',
'classname' => 'WPSEO_Video_Sitemap',
'my-yoast-slug' => WPSEO_Addon_Manager::VIDEO_SLUG,
),
'Local SEO' => array(
'slug' => 'local-seo-for-wordpress',
'identifier' => 'wpseo-local',
'classname' => 'WPSEO_Local_Core',
'my-yoast-slug' => WPSEO_Addon_Manager::LOCAL_SLUG,
),
);
/**
* Returns the set extensions.
*
* @return array All the extension names.
*/
public function get() {
return array_keys( $this->extensions );
}
/**
* Checks if the extension is valid.
*
* @param string $extension The extension to get the name for.
*
* @return bool Returns true when valid.
*/
public function is_valid( $extension ) {
$addon_manager = new WPSEO_Addon_Manager();
return $addon_manager->has_valid_subscription( $this->extensions[ $extension ]['my-yoast-slug'] );
}
/**
* Invalidates the extension by removing its option.
*
* @param string $extension The extension to invalidate.
*/
public function invalidate( $extension ) {
/*
* Make sure we clear the current site and multisite options.
*
* Because plugins can be site-activated or multi-site activated we need to clear
* all possible options.
*
* If we knew here that the extension in question was network activated
* we could do this a lot more easily.
*/
delete_option( $this->get_option_name( $extension ) );
delete_site_option( $this->get_option_name( $extension ) );
}
/**
* Checks if the plugin has been installed.
*
* @param string $extension The name of the plugin to check.
*
* @return bool Returns true when installed.
*/
public function is_installed( $extension ) {
return class_exists( $this->extensions[ $extension ]['classname'] );
}
/**
* Converts the extension to the required option name.
*
* @param string $extension The extension name to convert.
*
* @return string Returns the option name.
*/
protected function get_option_name( $extension ) {
return sanitize_title_with_dashes( $this->extensions[ $extension ]['slug'] . '_', null, 'save' ) . 'license';
}
}

View File

@ -0,0 +1,105 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Gutenberg_Compatibility
*/
/**
* Class WPSEO_Gutenberg_Compatibility
*/
class WPSEO_Gutenberg_Compatibility {
/**
* The currently released version of Gutenberg.
*
* @var string
*/
const CURRENT_RELEASE = '5.6.1';
/**
* The minimally supported version of Gutenberg by the plugin.
*
* @var string
*/
const MINIMUM_SUPPORTED = '5.6.1';
/**
* @var string
*/
protected $current_version;
/**
* WPSEO_Gutenberg_Compatibility constructor.
*/
public function __construct() {
$this->current_version = $this->detect_installed_gutenberg_version();
}
/**
* Determines whether or not Gutenberg is installed.
*
* @return bool Whether or not Gutenberg is installed.
*/
public function is_installed() {
return $this->current_version !== '';
}
/**
* Determines whether or not the currently installed version of Gutenberg is below the minimum supported version.
*
* @return bool True if the currently installed version is below the minimum supported version. False otherwise.
*/
public function is_below_minimum() {
return version_compare( $this->current_version, $this->get_minimum_supported_version(), '<' );
}
/**
* Gets the currently installed version.
*
* @return string The currently installed version.
*/
public function get_installed_version() {
return $this->current_version;
}
/**
* Determines whether or not the currently installed version of Gutenberg is the latest, fully compatible version.
*
* @return bool Whether or not the currently installed version is fully compatible.
*/
public function is_fully_compatible() {
return version_compare( $this->current_version, $this->get_latest_release(), '>=' );
}
/**
* Gets the latest released version of Gutenberg.
*
* @return string The latest release.
*/
protected function get_latest_release() {
return self::CURRENT_RELEASE;
}
/**
* Gets the minimum supported version of Gutenberg.
*
* @return string The minumum supported release.
*/
protected function get_minimum_supported_version() {
return self::MINIMUM_SUPPORTED;
}
/**
* Detects the currently installed Gutenberg version.
*
* @return string The currently installed Gutenberg version. Empty if the version couldn't be detected.
*/
protected function detect_installed_gutenberg_version() {
if ( defined( 'GUTENBERG_VERSION' ) ) {
return GUTENBERG_VERSION;
}
return '';
}
}

View File

@ -0,0 +1,112 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Options\Tabs
*/
/**
* Class WPSEO_Help_Center_Item.
*/
class WPSEO_Help_Center_Item {
/**
* Identifier for this tab.
*
* @var string
*/
private $identifier;
/**
* Label to display.
*
* @var string
*/
private $label;
/**
* The dashicon classname to display in front of the label.
*
* @var string
*/
private $dashicon;
/**
* Optional arguments.
*
* @var array
*/
private $args = array();
/**
* WPSEO_Help_Center_Item constructor.
*
* @param string $identifier Unique identifier for this tab.
* @param string $label Label to display.
* @param array $args Optional. Settings for this tab.
* @param string $dashicon Optional. The classname of the dahsicon to put in front of the label.
*/
public function __construct( $identifier, $label, $args = array(), $dashicon = '' ) {
$this->identifier = $identifier;
$this->label = $label;
$this->dashicon = $dashicon;
$this->args = $args;
}
/**
* Get the label.
*
* @return string
*/
public function get_label() {
return $this->label;
}
/**
* Get the identifier.
*
* @return string
*/
public function get_identifier() {
return $this->identifier;
}
/**
* Get the dashicon.
*
* @return string
*/
public function get_dashicon() {
return $this->dashicon;
}
/**
* Get the content of this tab.
*
* @return mixed|string
*/
public function get_content() {
if ( ! empty( $this->args['content'] ) ) {
return $this->args['content'];
}
if ( ! empty( $this->args['callback'] ) ) {
return call_user_func_array( $this->args['callback'], array( $this ) );
}
if ( ! empty( $this->args['view'] ) ) {
$view = $this->args['view'];
if ( substr( $view, - 4 ) === '.php' ) {
$view = substr( $view, 0, - 4 );
}
if ( ! empty( $this->args['view_arguments'] ) ) {
extract( $this->args['view_arguments'] );
}
include WPSEO_PATH . 'admin/views/' . $view . '.php';
}
return '';
}
}

View File

@ -0,0 +1,273 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Class WPSEO_Help_Center
*/
class WPSEO_Help_Center {
/**
* The tabs in the help center.
*
* @var WPSEO_Option_Tab[] $tab
*/
private $tabs;
/**
* Mount point in the HTML.
*
* @var string
*/
private $identifier = 'yoast-help-center-container';
/**
* Additional help center items.
*
* @var array
*/
protected $help_center_items = array();
/**
* Show premium support tab.
*
* @var bool
*/
protected $premium_support;
/**
* WPSEO_Help_Center constructor.
*
* @param string $unused Backwards compatible argument.
* @param WPSEO_Option_Tabs|WPSEO_Option_Tab $option_tabs Currently displayed tabs.
* @param boolean $premium_support Show premium support tab.
*/
public function __construct( $unused, $option_tabs, $premium_support = false ) {
$this->premium_support = $premium_support;
$tabs = new WPSEO_Option_Tabs( '' );
if ( $option_tabs instanceof WPSEO_Option_Tabs ) {
$tabs = $option_tabs;
}
if ( $option_tabs instanceof WPSEO_Option_Tab ) {
$tabs = new WPSEO_Option_Tabs( '', $option_tabs->get_name() );
$tabs->add_tab( $option_tabs );
}
$this->tabs = $tabs;
}
/**
* Localize data required by the help center component.
*/
public function localize_data() {
$this->add_contact_support_item();
$this->enqueue_localized_data( $this->format_data( $this->tabs->get_tabs() ) );
}
/**
* Format the required data for localized script.
*
* @param WPSEO_Option_Tab[] $tabs Yoast admin pages navigational tabs.
*
* @return array Associative array containing data for help center component.
*/
protected function format_data( array $tabs ) {
$formatted_data = array( 'tabs' => array() );
foreach ( $tabs as $tab ) {
$formatted_data['tabs'][ $tab->get_name() ] = array(
'label' => $tab->get_label(),
'videoUrl' => $tab->get_video_url(),
'id' => $tab->get_name(),
);
}
$active_tab = $this->tabs->get_active_tab();
$active_tab = ( null === $active_tab ) ? $tabs[0] : $active_tab;
$formatted_data['mountId'] = $this->identifier;
$formatted_data['initialTab'] = $active_tab->get_name();
$is_premium = WPSEO_Utils::is_yoast_seo_premium();
// Will translate to either empty string or "1" in localised script.
$formatted_data['isPremium'] = $is_premium;
$formatted_data['pluginVersion'] = WPSEO_VERSION;
// Open HelpScout on activating this tab ID.
$formatted_data['shouldDisplayContactForm'] = $this->premium_support;
$formatted_data['translations'] = self::get_translated_texts();
$formatted_data['videoDescriptions'] = array();
if ( $is_premium === false ) {
$formatted_data['videoDescriptions'][] = array(
'title' => __( 'Need some help?', 'wordpress-seo' ),
'description' => __( 'Go Premium and our experts will be there for you to answer any questions you might have about the setup and use of the plugin.', 'wordpress-seo' ),
'link' => WPSEO_Shortlinker::get( 'https://yoa.st/seo-premium-vt' ),
'linkText' => __( 'Get Yoast SEO Premium now »', 'wordpress-seo' ),
);
$formatted_data['videoDescriptions'][] = array(
'title' => __( 'Want to be a Yoast SEO Expert?', 'wordpress-seo' ),
'description' => __( 'Follow our Yoast SEO for WordPress training and become a certified Yoast SEO Expert!', 'wordpress-seo' ),
'link' => WPSEO_Shortlinker::get( 'https://yoa.st/wordpress-training-vt' ),
'linkText' => __( 'Enroll in the Yoast SEO for WordPress training »', 'wordpress-seo' ),
);
}
$formatted_data['contactSupportParagraphs'] = array(
array(
'image' => array(
'src' => esc_url( plugin_dir_url( WPSEO_FILE ) . 'images/support-team.svg' ),
'width' => 100,
'height' => 100,
'alt' => '',
),
'content' => null,
),
array(
'image' => null,
'content' => __( 'If you have a problem that you can\'t solve with our video tutorials or knowledge base, you can send a message to our support team. They can be reached 24/7.', 'wordpress-seo' ),
),
array(
'image' => null,
'content' => __( 'Support requests you create here are sent directly into our support system, which is secured with 256 bit SSL, so communication is 100% secure.', 'wordpress-seo' ),
),
);
$formatted_data['extraTabs'] = $this->get_extra_tabs();
return $formatted_data;
}
/**
* Get additional tabs for the help center component.
*
* @return array Additional help center tabs.
*/
protected function get_extra_tabs() {
$help_center_items = apply_filters( 'wpseo_help_center_items', $this->help_center_items );
return array_map( array( $this, 'format_helpcenter_tab' ), $help_center_items );
}
/**
* Convert WPSEO_Help_Center_Item into help center format.
*
* @param WPSEO_Help_Center_Item $item The item to convert.
*
* @return array Formatted item.
*/
protected function format_helpcenter_tab( WPSEO_Help_Center_Item $item ) {
return array(
'identifier' => $item->get_identifier(),
'label' => $item->get_label(),
'content' => $item->get_content(),
);
}
/**
* Enqueue localized script for help center component.
*
* @param array $data Data to localize.
*/
protected function enqueue_localized_data( $data ) {
wp_localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'help-center', 'wpseoHelpCenterData', $data );
$yoast_components_l10n = new WPSEO_Admin_Asset_Yoast_Components_L10n();
$yoast_components_l10n->localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'help-center' );
}
/**
* Outputs the help center div.
*/
public function mount() {
echo '<div id="' . esc_attr( $this->identifier ) . '">' . esc_html__( 'Loading help center.', 'wordpress-seo' ) . '</div>';
}
/**
* Add the contact support help center item to the help center.
*/
private function add_contact_support_item() {
/* translators: %s: expands to 'Yoast SEO Premium'. */
$popup_title = sprintf( __( 'Email support is a %s feature', 'wordpress-seo' ), 'Yoast SEO Premium' );
$popup_content = '<p class="yoast-measure">' . __( 'Go Premium and our experts will be there for you to answer any questions you might have about the setup and use of the plugin.', 'wordpress-seo' ) . '</p>';
/* translators: %1$s: expands to 'Yoast SEO Premium'. */
$popup_content .= '<p>' . sprintf( __( 'Other benefits of %1$s for you:', 'wordpress-seo' ), 'Yoast SEO Premium' ) . '</p>';
$popup_content .= '<ul class="wpseo-premium-advantages-list">';
$popup_content .= '<li>' . sprintf(
// We don't use strong text here, but we do use it in the "Add keyword" popup, this is just to have the same translatable strings.
/* translators: %1$s expands to a 'strong' start tag, %2$s to a 'strong' end tag. */
__( '%1$sNo more dead links%2$s: easy redirect manager', 'wordpress-seo' ),
'',
''
) . '</li>';
$popup_content .= '<li>' . __( 'Superfast internal links suggestions', 'wordpress-seo' ) . '</li>';
$popup_content .= '<li>' . sprintf(
// We don't use strong text here, but we do use it in the "Add keyword" popup, this is just to have the same translatable strings.
/* translators: %1$s expands to a 'strong' start tag, %2$s to a 'strong' end tag. */
__( '%1$sSocial media preview%2$s: Facebook &amp; Twitter', 'wordpress-seo' ),
'',
''
) . '</li>';
$popup_content .= '<li>' . __( '24/7 support', 'wordpress-seo' ) . '</li>';
$popup_content .= '<li>' . __( 'No ads!', 'wordpress-seo' ) . '</li>';
$popup_content .= '</ul>';
$premium_popup = new WPSEO_Premium_Popup( 'contact-support', 'h2', $popup_title, $popup_content, WPSEO_Shortlinker::get( 'https://yoa.st/contact-support' ) );
$contact_support_help_center_item = new WPSEO_Help_Center_Item(
'contact-support',
__( 'Get support', 'wordpress-seo' ),
array( 'content' => $premium_popup->get_premium_message( false ) ),
'dashicons-email-alt'
);
$this->help_center_items[] = $contact_support_help_center_item;
}
/**
* Pass text variables to js for the help center JS module.
*
* %s is replaced with <code>%s</code> and replaced again in the javascript with the actual variable.
*
* @return array Translated text strings for the help center.
*/
public static function get_translated_texts() {
// Esc_html is not needed because React already handles HTML in the (translations of) these strings.
return array(
'locale' => WPSEO_Language_Utils::get_user_locale(),
'videoTutorial' => __( 'Video tutorial', 'wordpress-seo' ),
'knowledgeBase' => __( 'Knowledge base', 'wordpress-seo' ),
'getSupport' => __( 'Get support', 'wordpress-seo' ),
'algoliaSearcher.loadingPlaceholder' => __( 'Loading...', 'wordpress-seo' ),
'algoliaSearcher.errorMessage' => __( 'Something went wrong. Please try again later.', 'wordpress-seo' ),
'searchBar.headingText' => __( 'Search the Yoast Knowledge Base for answers to your questions:', 'wordpress-seo' ),
'searchBar.placeholderText' => __( 'Type here to search...', 'wordpress-seo' ),
'searchBar.buttonText' => __( 'Search', 'wordpress-seo' ),
'searchResultDetail.openButton' => __( 'View in KB', 'wordpress-seo' ),
'searchResultDetail.openButtonLabel' => __( 'Open the knowledge base article in a new window or read it in the iframe below', 'wordpress-seo' ),
'searchResultDetail.backButton' => __( 'Go back', 'wordpress-seo' ),
'searchResultDetail.backButtonLabel' => __( 'Go back to the search results', 'wordpress-seo' ),
'searchResultDetail.iframeTitle' => __( 'Knowledge base article', 'wordpress-seo' ),
'searchResultDetail.searchResult' => __( 'Search result', 'wordpress-seo' ),
'searchResult.noResultsText' => __( 'No results found.', 'wordpress-seo' ),
'searchResult.foundResultsText' => sprintf(
/* translators: %s expands to the number of results found . */
__( 'Number of results found: %s', 'wordpress-seo' ),
'{ resultsCount }'
),
'searchResult.searchResultsHeading' => __( 'Search results', 'wordpress-seo' ),
'a11yNotice.opensInNewTab' => __( '(Opens in a new browser tab)', 'wordpress-seo' ),
'contactSupport.button' => __( 'New support request', 'wordpress-seo' ),
'helpCenter.buttonText' => __( 'Need help?', 'wordpress-seo' ),
);
}
}

View File

@ -0,0 +1,66 @@
<?php
/**
* @package WPSEO\Admin
*/
/**
* Class to print out the translatable strings for the Keyword Synonyms modal.
*/
class WPSEO_Keyword_Synonyms_Modal {
/**
* Returns the translations for the Keyword Synonyms modal.
*
* These strings are not escaped because they're meant to be used with React
* which already takes care of that. If used in PHP, they should be escaped.
*
* @return array Translated text strings for the Keyword Synonyms modal.
*/
public function get_translations() {
return array(
'title' => __( 'Would you like to add keyphrase synonyms?', 'wordpress-seo' ),
'intro' => sprintf(
/* translators: %1$s expands to a 'Yoast SEO Premium' text linked to the yoast.com website. */
__( 'Great news: you can, with %1$s!', 'wordpress-seo' ),
'{{link}}Yoast SEO Premium{{/link}}'
),
'link' => WPSEO_Shortlinker::get( 'https://yoa.st/pe-premium-page' ),
'other' => sprintf(
/* translators: %s expands to 'Yoast SEO Premium'. */
__( 'Other benefits of %s for you:', 'wordpress-seo' ),
'Yoast SEO Premium'
),
'buylink' => WPSEO_Shortlinker::get( 'https://yoa.st/keyword-synonyms-popup' ),
'buy' => sprintf(
/* translators: %s expands to 'Yoast SEO Premium'. */
__( 'Get %s', 'wordpress-seo' ),
'Yoast SEO Premium'
),
'small' => __( '1 year free support and updates included!', 'wordpress-seo' ),
'a11yNotice.opensInNewTab' => __( '(Opens in a new browser tab)', 'wordpress-seo' ),
);
}
/**
* Passes translations to JS for the Keyword Synonyms modal component.
*
* @return array Translated text strings for the Keyword Synonyms modal component.
*/
public function get_translations_for_js() {
$translations = $this->get_translations();
return array(
'locale' => WPSEO_Language_Utils::get_user_locale(),
'intl' => $translations,
);
}
/**
* Prints the localized Keyword Synonyms modal translations for JS.
*
* @return void
*/
public function enqueue_translations() {
wp_localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'admin-global-script', 'yoastKeywordSynonymsModalL10n', $this->get_translations_for_js() );
}
}

View File

@ -0,0 +1,215 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Represents the values for a single Yoast Premium extension plugin.
*/
class WPSEO_License_Page_Manager implements WPSEO_WordPress_Integration {
/**
* Version number for License Page Manager.
*
* @var string
*/
const VERSION_LEGACY = '1';
/**
* Version number for License Page Manager.
*
* @var string
*/
const VERSION_BACKWARDS_COMPATIBILITY = '2';
/**
* Registers all hooks to WordPress.
*/
public function register_hooks() {
add_filter( 'http_response', array( $this, 'handle_response' ), 10, 3 );
if ( $this->get_version() === self::VERSION_BACKWARDS_COMPATIBILITY ) {
add_filter( 'yoast-license-valid', '__return_true' );
add_filter( 'yoast-show-license-notice', '__return_false' );
add_action( 'admin_init', array( $this, 'validate_extensions' ), 15 );
}
else {
add_action( 'admin_init', array( $this, 'remove_faulty_notifications' ), 15 );
}
}
/**
* Validates the extensions and show a notice for the invalid extensions.
*/
public function validate_extensions() {
if ( filter_input( INPUT_GET, 'page' ) === WPSEO_Admin::PAGE_IDENTIFIER ) {
/**
* Filter: 'yoast-active-extensions' - Collects all active extensions. This hook is implemented in the
* license manager.
*
* @api array $extensions The array with extensions.
*/
apply_filters( 'yoast-active-extensions', array() );
}
$extension_list = new WPSEO_Extensions();
$extensions = $extension_list->get();
$notification_center = Yoast_Notification_Center::get();
foreach ( $extensions as $product_name ) {
$notification = $this->create_notification( $product_name );
// Add a notification when the installed plugin isn't activated in My Yoast.
if ( $extension_list->is_installed( $product_name ) && ! $extension_list->is_valid( $product_name ) ) {
$notification_center->add_notification( $notification );
continue;
}
$notification_center->remove_notification( $notification );
}
}
/**
* Removes the faulty set notifications.
*/
public function remove_faulty_notifications() {
$extension_list = new WPSEO_Extensions();
$extensions = $extension_list->get();
$notification_center = Yoast_Notification_Center::get();
foreach ( $extensions as $product_name ) {
$notification = $this->create_notification( $product_name );
$notification_center->remove_notification( $notification );
}
}
/**
* Handles the response.
*
* @param array $response HTTP response.
* @param array $request_arguments HTTP request arguments. Unused.
* @param string $url The request URL.
*
* @return array The response array.
*/
public function handle_response( array $response, $request_arguments, $url ) {
$response_code = wp_remote_retrieve_response_code( $response );
if ( $response_code === 200 && $this->is_expected_endpoint( $url ) ) {
$response_data = $this->parse_response( $response );
$this->detect_version( $response_data );
}
return $response;
}
/**
* Returns the license page to use based on the version number.
*
* @return string The page to use.
*/
public function get_license_page() {
return 'licenses';
}
/**
* Returns the version number of the license server.
*
* @return int The version number
*/
protected function get_version() {
return get_option( $this->get_option_name(), self::VERSION_BACKWARDS_COMPATIBILITY );
}
/**
* Returns the option name.
*
* @return string The option name.
*/
protected function get_option_name() {
return 'wpseo_license_server_version';
}
/**
* Sets the version when there is a value in the response.
*
* @param array $response The response to extract the version from.
*/
protected function detect_version( $response ) {
if ( ! empty( $response['serverVersion'] ) ) {
$this->set_version( $response['serverVersion'] );
}
}
/**
* Sets the version.
*
* @param string $server_version The version number to save.
*/
protected function set_version( $server_version ) {
update_option( $this->get_option_name(), $server_version );
}
/**
* Parses the response by getting its body and do a unserialize of it.
*
* @param array $response The response to parse.
*
* @return mixed|string|false The parsed response.
*/
protected function parse_response( $response ) {
$response = json_decode( wp_remote_retrieve_body( $response ), true );
$response = maybe_unserialize( $response );
return $response;
}
/**
* Checks if the given url matches the expected endpoint.
*
* @param string $url The url to check.
*
* @return bool True when url matches the endpoint.
*/
protected function is_expected_endpoint( $url ) {
$url_parts = wp_parse_url( $url );
$is_yoast_com = ( in_array( $url_parts['host'], array( 'yoast.com', 'my.yoast.com' ), true ) );
$is_edd_api = ( isset( $url_parts['path'] ) && $url_parts['path'] === '/edd-sl-api' );
return $is_yoast_com && $is_edd_api;
}
/**
* Creates an instance of Yoast_Notification.
*
* @param string $product_name The product to create the notification for.
*
* @return Yoast_Notification The created notification.
*/
protected function create_notification( $product_name ) {
$notification_options = array(
'type' => Yoast_Notification::ERROR,
'id' => 'wpseo-dismiss-' . sanitize_title_with_dashes( $product_name, null, 'save' ),
'capabilities' => 'wpseo_manage_options',
);
$notification = new Yoast_Notification(
sprintf(
/* translators: %1$s expands to the product name. %2$s expands to a link to My Yoast */
__( 'You are not receiving updates or support! Fix this problem by adding this site and enabling %1$s for it in %2$s.', 'wordpress-seo' ),
$product_name,
'<a href="' . WPSEO_Shortlinker::get( 'https://yoa.st/13j' ) . '" target="_blank">My Yoast</a>'
),
$notification_options
);
return $notification;
}
}

View File

@ -0,0 +1,754 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Class WPSEO_Meta_Columns.
*/
class WPSEO_Meta_Columns {
/**
* @var WPSEO_Metabox_Analysis_SEO
*/
private $analysis_seo;
/**
* @var WPSEO_Metabox_Analysis_Readability
*/
private $analysis_readability;
/**
* When page analysis is enabled, just initialize the hooks.
*/
public function __construct() {
if ( apply_filters( 'wpseo_use_page_analysis', true ) === true ) {
add_action( 'admin_init', array( $this, 'setup_hooks' ) );
}
$this->analysis_seo = new WPSEO_Metabox_Analysis_SEO();
$this->analysis_readability = new WPSEO_Metabox_Analysis_Readability();
}
/**
* Sets up up the hooks.
*/
public function setup_hooks() {
$this->set_post_type_hooks();
if ( $this->analysis_seo->is_enabled() ) {
add_action( 'restrict_manage_posts', array( $this, 'posts_filter_dropdown' ) );
}
if ( $this->analysis_readability->is_enabled() ) {
add_action( 'restrict_manage_posts', array( $this, 'posts_filter_dropdown_readability' ) );
}
add_filter( 'request', array( $this, 'column_sort_orderby' ) );
}
/**
* Adds the column headings for the SEO plugin for edit posts / pages overview.
*
* @param array $columns Already existing columns.
*
* @return array Array containing the column headings.
*/
public function column_heading( $columns ) {
if ( $this->display_metabox() === false ) {
return $columns;
}
$added_columns = array();
if ( $this->analysis_seo->is_enabled() ) {
$added_columns['wpseo-score'] = '<span class="yoast-tooltip yoast-tooltip-n yoast-tooltip-alt" data-label="' . esc_attr__( 'SEO score', 'wordpress-seo' ) . '"><span class="yoast-column-seo-score yoast-column-header-has-tooltip"><span class="screen-reader-text">' . __( 'SEO score', 'wordpress-seo' ) . '</span></span></span>';
}
if ( $this->analysis_readability->is_enabled() ) {
$added_columns['wpseo-score-readability'] = '<span class="yoast-tooltip yoast-tooltip-n yoast-tooltip-alt" data-label="' . esc_attr__( 'Readability score', 'wordpress-seo' ) . '"><span class="yoast-column-readability yoast-column-header-has-tooltip"><span class="screen-reader-text">' . __( 'Readability score', 'wordpress-seo' ) . '</span></span></span>';
}
$added_columns['wpseo-title'] = __( 'SEO Title', 'wordpress-seo' );
$added_columns['wpseo-metadesc'] = __( 'Meta Desc.', 'wordpress-seo' );
if ( $this->analysis_seo->is_enabled() ) {
$added_columns['wpseo-focuskw'] = __( 'Keyphrase', 'wordpress-seo' );
}
return array_merge( $columns, $added_columns );
}
/**
* Displays the column content for the given column.
*
* @param string $column_name Column to display the content for.
* @param int $post_id Post to display the column content for.
*/
public function column_content( $column_name, $post_id ) {
if ( $this->display_metabox() === false ) {
return;
}
switch ( $column_name ) {
case 'wpseo-score':
echo $this->parse_column_score( $post_id );
return;
case 'wpseo-score-readability':
echo $this->parse_column_score_readability( $post_id );
return;
case 'wpseo-title':
$post = get_post( $post_id, ARRAY_A );
$title = wpseo_replace_vars( $this->page_title( $post_id ), $post );
$title = apply_filters( 'wpseo_title', $title );
echo esc_html( $title );
return;
case 'wpseo-metadesc':
$post = get_post( $post_id, ARRAY_A );
$metadesc_val = wpseo_replace_vars( WPSEO_Meta::get_value( 'metadesc', $post_id ), $post );
$metadesc_val = apply_filters( 'wpseo_metadesc', $metadesc_val );
if ( '' === $metadesc_val ) {
echo '<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">',
esc_html__( 'Meta description not set.', 'wordpress-seo' ),
'</span>';
return;
}
echo esc_html( $metadesc_val );
return;
case 'wpseo-focuskw':
$focuskw_val = WPSEO_Meta::get_value( 'focuskw', $post_id );
if ( '' === $focuskw_val ) {
echo '<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">',
esc_html__( 'Focus keyphrase not set.', 'wordpress-seo' ),
'</span>';
return;
}
echo esc_html( $focuskw_val );
return;
}
}
/**
* Indicates which of the SEO columns are sortable.
*
* @param array $columns Appended with their orderby variable.
*
* @return array Array containing the sortable columns.
*/
public function column_sort( $columns ) {
if ( $this->display_metabox() === false ) {
return $columns;
}
$columns['wpseo-metadesc'] = 'wpseo-metadesc';
if ( $this->analysis_seo->is_enabled() ) {
$columns['wpseo-focuskw'] = 'wpseo-focuskw';
}
return $columns;
}
/**
* Hides the SEO title, meta description and focus keyword columns if the user hasn't chosen which columns to hide.
*
* @param array|false $result The hidden columns.
* @param string $option The option name used to set which columns should be hidden.
* @param WP_User $user The User.
*
* @return array $result Array containing the columns to hide.
*/
public function column_hidden( $result, $option, $user ) {
global $wpdb;
if ( $user->has_prop( $wpdb->get_blog_prefix() . $option ) || $user->has_prop( $option ) ) {
return $result;
}
if ( ! is_array( $result ) ) {
$result = array();
}
array_push( $result, 'wpseo-title', 'wpseo-metadesc' );
if ( $this->analysis_seo->is_enabled() ) {
array_push( $result, 'wpseo-focuskw' );
}
return $result;
}
/**
* Adds a dropdown that allows filtering on the posts SEO Quality.
*/
public function posts_filter_dropdown() {
if ( ! $this->can_display_filter() ) {
return;
}
$ranks = WPSEO_Rank::get_all_ranks();
echo '<label class="screen-reader-text" for="wpseo-filter">' . esc_html__( 'Filter by SEO Score', 'wordpress-seo' ) . '</label>';
echo '<select name="seo_filter" id="wpseo-filter">';
echo $this->generate_option( '', __( 'All SEO Scores', 'wordpress-seo' ) );
foreach ( $ranks as $rank ) {
$selected = selected( $this->get_current_seo_filter(), $rank->get_rank(), false );
echo $this->generate_option( $rank->get_rank(), $rank->get_drop_down_label(), $selected );
}
echo '</select>';
}
/**
* Adds a dropdown that allows filtering on the posts Readability Quality.
*
* @return void
*/
public function posts_filter_dropdown_readability() {
if ( ! $this->can_display_filter() ) {
return;
}
$ranks = WPSEO_Rank::get_all_readability_ranks();
echo '<label class="screen-reader-text" for="wpseo-readability-filter">' . esc_html__( 'Filter by Readability Score', 'wordpress-seo' ) . '</label>';
echo '<select name="readability_filter" id="wpseo-readability-filter">';
echo $this->generate_option( '', __( 'All Readability Scores', 'wordpress-seo' ) );
foreach ( $ranks as $rank ) {
$selected = selected( $this->get_current_readability_filter(), $rank->get_rank(), false );
echo $this->generate_option( $rank->get_rank(), $rank->get_drop_down_readability_labels(), $selected );
}
echo '</select>';
}
/**
* Generates an <option> element.
*
* @param string $value The option's value.
* @param string $label The option's label.
* @param string $selected HTML selected attribute for an option.
*
* @return string The generated <option> element.
*/
protected function generate_option( $value, $label, $selected = '' ) {
return '<option ' . $selected . ' value="' . esc_attr( $value ) . '">' . esc_html( $label ) . '</option>';
}
/**
* Determines the SEO score filter to be later used in the meta query, based on the passed SEO filter.
*
* @param string $seo_filter The SEO filter to use to determine what further filter to apply.
*
* @return array The SEO score filter.
*/
protected function determine_seo_filters( $seo_filter ) {
if ( $seo_filter === WPSEO_Rank::NO_FOCUS ) {
return $this->create_no_focus_keyword_filter();
}
if ( $seo_filter === WPSEO_Rank::NO_INDEX ) {
return $this->create_no_index_filter();
}
$rank = new WPSEO_Rank( $seo_filter );
return $this->create_seo_score_filter( $rank->get_starting_score(), $rank->get_end_score() );
}
/**
* Determines the Readability score filter to the meta query, based on the passed Readability filter.
*
* @param string $readability_filter The Readability filter to use to determine what further filter to apply.
*
* @return array The Readability score filter.
*/
protected function determine_readability_filters( $readability_filter ) {
$rank = new WPSEO_Rank( $readability_filter );
return $this->create_readability_score_filter( $rank->get_starting_score(), $rank->get_end_score() );
}
/**
* Creates a keyword filter for the meta query, based on the passed Keyword filter.
*
* @param string $keyword_filter The keyword filter to use.
*
* @return array The keyword filter.
*/
protected function get_keyword_filter( $keyword_filter ) {
return array(
'post_type' => get_query_var( 'post_type', 'post' ),
'key' => WPSEO_Meta::$meta_prefix . 'focuskw',
'value' => sanitize_text_field( $keyword_filter ),
);
}
/**
* Determines whether the passed filter is considered to be valid.
*
* @param mixed $filter The filter to check against.
*
* @return bool Whether or not the filter is considered valid.
*/
protected function is_valid_filter( $filter ) {
return ! empty( $filter ) && is_string( $filter );
}
/**
* Collects the filters and merges them into a single array.
*
* @return array Array containing all the applicable filters.
*/
protected function collect_filters() {
$active_filters = array();
$seo_filter = $this->get_current_seo_filter();
$readability_filter = $this->get_current_readability_filter();
$current_keyword_filter = $this->get_current_keyword_filter();
if ( $this->is_valid_filter( $seo_filter ) ) {
$active_filters = array_merge(
$active_filters,
$this->determine_seo_filters( $seo_filter )
);
}
if ( $this->is_valid_filter( $readability_filter ) ) {
$active_filters = array_merge(
$active_filters,
$this->determine_readability_filters( $readability_filter )
);
}
if ( $this->is_valid_filter( $current_keyword_filter ) ) {
$active_filters = array_merge(
$active_filters,
$this->get_keyword_filter( $current_keyword_filter )
);
}
return $active_filters;
}
/**
* Modify the query based on the filters that are being passed.
*
* @param array $vars Query variables that need to be modified based on the filters.
*
* @return array Array containing the meta query to use for filtering the posts overview.
*/
public function column_sort_orderby( $vars ) {
$collected_filters = $this->collect_filters();
if ( isset( $vars['orderby'] ) ) {
$vars = array_merge( $vars, $this->filter_order_by( $vars['orderby'] ) );
}
return $this->build_filter_query( $vars, $collected_filters );
}
/**
* Retrieves the meta robots query values to be used within the meta query.
*
* @return array Array containing the query parameters regarding meta robots.
*/
protected function get_meta_robots_query_values() {
return array(
'relation' => 'OR',
array(
'key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex',
'compare' => 'NOT EXISTS',
),
array(
'key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex',
'value' => '1',
'compare' => '!=',
),
);
}
/**
* Determines the score filters to be used. If more than one is passed, it created an AND statement for the query.
*
* @param array $score_filters Array containing the score filters.
*
* @return array Array containing the score filters that need to be applied to the meta query.
*/
protected function determine_score_filters( $score_filters ) {
if ( count( $score_filters ) > 1 ) {
return array_merge( array( 'relation' => 'AND' ), $score_filters );
}
return $score_filters;
}
/**
* Retrieves the post type from the $_GET variable.
*
* @return string The current post type.
*/
public function get_current_post_type() {
return filter_input( INPUT_GET, 'post_type' );
}
/**
* Retrieves the SEO filter from the $_GET variable.
*
* @return string The current post type.
*/
public function get_current_seo_filter() {
return filter_input( INPUT_GET, 'seo_filter' );
}
/**
* Retrieves the Readability filter from the $_GET variable.
*
* @return string The current post type.
*/
public function get_current_readability_filter() {
return filter_input( INPUT_GET, 'readability_filter' );
}
/**
* Retrieves the keyword filter from the $_GET variable.
*
* @return string The current post type.
*/
public function get_current_keyword_filter() {
return filter_input( INPUT_GET, 'seo_kw_filter' );
}
/**
* Uses the vars to create a complete filter query that can later be executed to filter out posts.
*
* @param array $vars Array containing the variables that will be used in the meta query.
* @param array $filters Array containing the filters that we need to apply in the meta query.
*
* @return array Array containing the complete filter query.
*/
protected function build_filter_query( $vars, $filters ) {
// If no filters were applied, just return everything.
if ( count( $filters ) === 0 ) {
return $vars;
}
$result = array( 'meta_query' => array() );
$result['meta_query'] = array_merge( $result['meta_query'], array( $this->determine_score_filters( $filters ) ) );
$current_seo_filter = $this->get_current_seo_filter();
// This only applies for the SEO score filter because it can because the SEO score can be altered by the no-index option.
if ( $this->is_valid_filter( $current_seo_filter ) && ! in_array( $current_seo_filter, array( WPSEO_Rank::NO_INDEX, WPSEO_Rank::NO_FOCUS ), true ) ) {
$result['meta_query'] = array_merge( $result['meta_query'], array( $this->get_meta_robots_query_values() ) );
}
return array_merge( $vars, $result );
}
/**
* Creates a Readability score filter.
*
* @param number $low The lower boundary of the score.
* @param number $high The higher boundary of the score.
*
* @return array The Readability Score filter.
*/
protected function create_readability_score_filter( $low, $high ) {
return array(
array(
'key' => WPSEO_Meta::$meta_prefix . 'content_score',
'value' => array( $low, $high ),
'type' => 'numeric',
'compare' => 'BETWEEN',
),
);
}
/**
* Creates an SEO score filter.
*
* @param number $low The lower boundary of the score.
* @param number $high The higher boundary of the score.
*
* @return array The SEO score filter.
*/
protected function create_seo_score_filter( $low, $high ) {
return array(
array(
'key' => WPSEO_Meta::$meta_prefix . 'linkdex',
'value' => array( $low, $high ),
'type' => 'numeric',
'compare' => 'BETWEEN',
),
);
}
/**
* Creates a filter to retrieve posts that were set to no-index.
*
* @return array Array containin the no-index filter.
*/
protected function create_no_index_filter() {
return array(
array(
'key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex',
'value' => '1',
'compare' => '=',
),
);
}
/**
* Creates a filter to retrieve posts that have no keyword set.
*
* @return array Array containing the no focus keyword filter.
*/
protected function create_no_focus_keyword_filter() {
return array(
array(
'key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex',
'value' => 'needs-a-value-anyway',
'compare' => 'NOT EXISTS',
),
array(
'key' => WPSEO_Meta::$meta_prefix . 'linkdex',
'value' => 'needs-a-value-anyway',
'compare' => 'NOT EXISTS',
),
);
}
/**
* Determines whether a particular post_id is of an indexable post type.
*
* @param string $post_id The post ID to check.
*
* @return bool Whether or not it is indexable.
*/
protected function is_indexable( $post_id ) {
if ( ! empty( $post_id ) && ! $this->uses_default_indexing( $post_id ) ) {
return WPSEO_Meta::get_value( 'meta-robots-noindex', $post_id ) === '2';
}
$post = get_post( $post_id );
if ( is_object( $post ) ) {
// If the option is false, this means we want to index it.
return WPSEO_Options::get( 'noindex-' . $post->post_type, false ) === false;
}
return true;
}
/**
* Determines whether the given post ID uses the default indexing settings.
*
* @param integer $post_id The post ID to check.
*
* @return bool Whether or not the default indexing is being used for the post.
*/
protected function uses_default_indexing( $post_id ) {
return WPSEO_Meta::get_value( 'meta-robots-noindex', $post_id ) === '0';
}
/**
* Returns filters when $order_by is matched in the if-statement.
*
* @param string $order_by The ID of the column by which to order the posts.
*
* @return array Array containing the order filters.
*/
private function filter_order_by( $order_by ) {
switch ( $order_by ) {
case 'wpseo-metadesc':
return array(
'meta_key' => WPSEO_Meta::$meta_prefix . 'metadesc',
'orderby' => 'meta_value',
);
case 'wpseo-focuskw':
return array(
'meta_key' => WPSEO_Meta::$meta_prefix . 'focuskw',
'orderby' => 'meta_value',
);
}
return array();
}
/**
* Parses the score column.
*
* @param integer $post_id The ID of the post for which to show the score.
*
* @return string The HTML for the SEO score indicator.
*/
private function parse_column_score( $post_id ) {
if ( ! $this->is_indexable( $post_id ) ) {
$rank = new WPSEO_Rank( WPSEO_Rank::NO_INDEX );
$title = __( 'Post is set to noindex.', 'wordpress-seo' );
WPSEO_Meta::set_value( 'linkdex', 0, $post_id );
return $this->render_score_indicator( $rank, $title );
}
if ( WPSEO_Meta::get_value( 'focuskw', $post_id ) === '' ) {
$rank = new WPSEO_Rank( WPSEO_Rank::NO_FOCUS );
$title = __( 'Focus keyphrase not set.', 'wordpress-seo' );
return $this->render_score_indicator( $rank, $title );
}
$score = (int) WPSEO_Meta::get_value( 'linkdex', $post_id );
$rank = WPSEO_Rank::from_numeric_score( $score );
$title = $rank->get_label();
return $this->render_score_indicator( $rank, $title );
}
/**
* Parsing the readability score column.
*
* @param int $post_id The ID of the post for which to show the readability score.
*
* @return string The HTML for the readability score indicator.
*/
private function parse_column_score_readability( $post_id ) {
$score = (int) WPSEO_Meta::get_value( 'content_score', $post_id );
$rank = WPSEO_Rank::from_numeric_score( $score );
return $this->render_score_indicator( $rank );
}
/**
* Sets up the hooks for the post_types.
*/
private function set_post_type_hooks() {
$post_types = WPSEO_Post_Type::get_accessible_post_types();
if ( ! is_array( $post_types ) || $post_types === array() ) {
return;
}
foreach ( $post_types as $post_type ) {
if ( $this->display_metabox( $post_type ) === false ) {
continue;
}
add_filter( 'manage_' . $post_type . '_posts_columns', array( $this, 'column_heading' ), 10, 1 );
add_action( 'manage_' . $post_type . '_posts_custom_column', array( $this, 'column_content' ), 10, 2 );
add_action( 'manage_edit-' . $post_type . '_sortable_columns', array( $this, 'column_sort' ), 10, 2 );
/*
* Use the `get_user_option_{$option}` filter to change the output of the get_user_option
* function for the `manage{$screen}columnshidden` option, which is based on the current
* admin screen. The admin screen we want to target is the `edit-{$post_type}` screen.
*/
$filter = sprintf( 'get_user_option_%s', sprintf( 'manage%scolumnshidden', 'edit-' . $post_type ) );
add_filter( $filter, array( $this, 'column_hidden' ), 10, 3 );
}
unset( $post_type );
}
/**
* Wraps the WPSEO_Metabox check to determine whether the metabox should be displayed either by
* choice of the admin or because the post type is not a public post type.
*
* @since 7.0
*
* @param string $post_type Optional. The post type to test, defaults to the current post post_type.
*
* @return bool Whether or not the meta box (and associated columns etc) should be hidden.
*/
private function display_metabox( $post_type = null ) {
$current_post_type = sanitize_text_field( $this->get_current_post_type() );
if ( ! isset( $post_type ) && ! empty( $current_post_type ) ) {
$post_type = $current_post_type;
}
return WPSEO_Utils::is_metabox_active( $post_type, 'post_type' );
}
/**
* Retrieve the page title.
*
* @param int $post_id Post to retrieve the title for.
*
* @return string
*/
private function page_title( $post_id ) {
$fixed_title = WPSEO_Meta::get_value( 'title', $post_id );
if ( $fixed_title !== '' ) {
return $fixed_title;
}
$post = get_post( $post_id );
if ( is_object( $post ) && WPSEO_Options::get( 'title-' . $post->post_type, '' ) !== '' ) {
$title_template = WPSEO_Options::get( 'title-' . $post->post_type );
$title_template = str_replace( ' %%page%% ', ' ', $title_template );
return wpseo_replace_vars( $title_template, $post );
}
return wpseo_replace_vars( '%%title%%', $post );
}
/**
* @param WPSEO_Rank $rank The rank this indicator should have.
* @param string $title Optional. The title for this rank, defaults to the title of the rank.
*
* @return string The HTML for a score indicator.
*/
private function render_score_indicator( $rank, $title = '' ) {
if ( empty( $title ) ) {
$title = $rank->get_label();
}
return '<div aria-hidden="true" title="' . esc_attr( $title ) . '" class="wpseo-score-icon ' . esc_attr( $rank->get_css_class() ) . '"></div><span class="screen-reader-text wpseo-score-text">' . $title . '</span>';
}
/**
* Determines whether or not filter dropdowns should be displayed.
*
* @return bool Whether or the current page can display the filter drop downs.
*/
public function can_display_filter() {
if ( $GLOBALS['pagenow'] === 'upload.php' ) {
return false;
}
if ( $this->display_metabox() === false ) {
return false;
}
$screen = get_current_screen();
if ( null === $screen ) {
return false;
}
return WPSEO_Post_Type::is_post_type_accessible( $screen->post_type );
}
}

View File

@ -0,0 +1,122 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Links
*/
/**
* Represents the link count storage.
*/
class WPSEO_Meta_Storage implements WPSEO_Installable {
/**
* Table name for the meta storage.
*
* @var string
*/
const TABLE_NAME = 'yoast_seo_meta';
/**
* @var WPSEO_Database_Proxy
*/
protected $database_proxy;
/**
* @deprecated 7.4
*
* @var null|string
*/
protected $table_prefix;
/**
* Initializes the database table.
*
* @param string $table_prefix Optional. Deprecated argument.
*/
public function __construct( $table_prefix = null ) {
if ( $table_prefix !== null ) {
_deprecated_argument( __METHOD__, 'WPSEO 7.4' );
}
$this->database_proxy = new WPSEO_Database_Proxy( $GLOBALS['wpdb'], self::TABLE_NAME, true );
}
/**
* Returns the table name to use.
*
* @return string The table name.
*/
public function get_table_name() {
return $this->database_proxy->get_table_name();
}
/**
* Creates the database table.
*
* @return boolean True if the table was created, false if something went wrong.
*/
public function install() {
return $this->database_proxy->create_table(
array(
'object_id bigint(20) UNSIGNED NOT NULL',
'internal_link_count int(10) UNSIGNED NULL DEFAULT NULL',
'incoming_link_count int(10) UNSIGNED NULL DEFAULT NULL',
),
array(
'UNIQUE KEY object_id (object_id)',
)
);
}
/**
* Saves the link count to the database.
*
* @param int $meta_id The id to save the link count for.
* @param array $meta_data The total amount of links.
*/
public function save_meta_data( $meta_id, array $meta_data ) {
$where = array( 'object_id' => $meta_id );
$saved = $this->database_proxy->upsert(
array_merge( $where, $meta_data ),
$where
);
if ( $saved === false ) {
WPSEO_Meta_Table_Accessible::set_inaccessible();
}
}
/**
* Updates the incoming link count.
*
* @param array $post_ids The posts to update the incoming link count for.
* @param WPSEO_Link_Storage $storage The link storage object.
*/
public function update_incoming_link_count( array $post_ids, WPSEO_Link_Storage $storage ) {
global $wpdb;
$query = $wpdb->prepare(
'
SELECT COUNT( id ) AS incoming, target_post_id AS post_id
FROM ' . $storage->get_table_name() . '
WHERE target_post_id IN(' . implode( ',', array_fill( 0, count( $post_ids ), '%d' ) ) . ')
GROUP BY target_post_id',
$post_ids
);
$results = $wpdb->get_results( $query );
$post_ids_non_zero = array();
foreach ( $results as $result ) {
$this->save_meta_data( $result->post_id, array( 'incoming_link_count' => $result->incoming ) );
$post_ids_non_zero[] = $result->post_id;
}
$post_ids_zero = array_diff( $post_ids, $post_ids_non_zero );
foreach ( $post_ids_zero as $post_id ) {
$this->save_meta_data( $post_id, array( 'incoming_link_count' => 0 ) );
}
}
}

View File

@ -0,0 +1,99 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Links
*/
/**
* Represents the state of the table being accessible.
*/
class WPSEO_Meta_Table_Accessible {
/**
* @var string
*/
const ACCESSIBLE = '0';
/**
* @var string
*/
const INACCESSBILE = '1';
/**
* Checks if the given table name exists.
*
* @return bool True when table is accessible.
*/
public static function is_accessible() {
$value = get_transient( self::transient_name() );
// If the value is not set, check the table.
if ( false === $value ) {
return self::check_table();
}
return $value === self::ACCESSIBLE;
}
/**
* Sets the transient value to 1, to indicate the table is not accessible.
*
* @return void
*/
public static function set_inaccessible() {
set_transient( self::transient_name(), self::INACCESSBILE, HOUR_IN_SECONDS );
}
/**
* Removes the transient.
*
* @return void
*/
public static function cleanup() {
delete_transient( self::transient_name() );
}
/**
* Sets the transient value to 0, to indicate the table is accessible.
*
* @return void
*/
protected static function set_accessible() {
/*
* Prefer to set a 0 timeout, but if the timeout was set before WordPress will not delete the transient
* correctly when overridden with a zero value.
*
* Setting a YEAR_IN_SECONDS instead.
*/
set_transient( self::transient_name(), self::ACCESSIBLE, YEAR_IN_SECONDS );
}
/**
* Checks if the table exists if not, set the transient to indicate the inaccessible table.
*
* @return bool True if table is accessible.
*/
protected static function check_table() {
global $wpdb;
$storage = new WPSEO_Meta_Storage();
$query = $wpdb->prepare( 'SHOW TABLES LIKE %s', $storage->get_table_name() );
if ( $wpdb->get_var( $query ) !== $storage->get_table_name() ) {
self::set_inaccessible();
return false;
}
self::set_accessible();
return true;
}
/**
* Returns the name of the transient.
*
* @return string The name of the transient to use.
*/
protected static function transient_name() {
return 'wpseo_meta_table_inaccessible';
}
}

View File

@ -0,0 +1,66 @@
<?php
/**
* @package WPSEO\Admin
*/
/**
* Class to print out the translatable strings for the Multiple Keywords modal.
*/
class WPSEO_Multiple_Keywords_Modal {
/**
* Returns the translations for the Multiple Keywords modal.
*
* These strings are not escaped because they're meant to be used with React
* which already takes care of that. If used in PHP, they should be escaped.
*
* @return array Translated text strings for the Multiple Keywords modal.
*/
public function get_translations() {
return array(
'title' => __( 'Would you like to add another keyphrase?', 'wordpress-seo' ),
'intro' => sprintf(
/* translators: %1$s expands to a 'Yoast SEO Premium' text linked to the yoast.com website. */
__( 'Great news: you can, with %1$s!', 'wordpress-seo' ),
'{{link}}Yoast SEO Premium{{/link}}'
),
'link' => WPSEO_Shortlinker::get( 'https://yoa.st/pe-premium-page' ),
'other' => sprintf(
/* translators: %s expands to 'Yoast SEO Premium'. */
__( 'Other benefits of %s for you:', 'wordpress-seo' ),
'Yoast SEO Premium'
),
'buylink' => WPSEO_Shortlinker::get( 'https://yoa.st/add-keywords-popup' ),
'buy' => sprintf(
/* translators: %s expands to 'Yoast SEO Premium'. */
__( 'Get %s', 'wordpress-seo' ),
'Yoast SEO Premium'
),
'small' => __( '1 year free support and updates included!', 'wordpress-seo' ),
'a11yNotice.opensInNewTab' => __( '(Opens in a new browser tab)', 'wordpress-seo' ),
);
}
/**
* Passes translations to JS for the Multiple Keywords modal component.
*
* @return array Translated text strings for the Multiple Keywords modal component.
*/
public function get_translations_for_js() {
$translations = $this->get_translations();
return array(
'locale' => WPSEO_Language_Utils::get_user_locale(),
'intl' => $translations,
);
}
/**
* Prints the localized Multiple Keywords modal translations for JS.
*
* @return void
*/
public function enqueue_translations() {
wp_localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'admin-global-script', 'yoastMultipleKeywordsModalL10n', $this->get_translations_for_js() );
}
}

View File

@ -0,0 +1,247 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* @class WPSEO_MyYoast_Proxy Loads the MyYoast proxy.
*
* This class registers a proxy page on `admin.php`. Which is reached with the `page=PAGE_IDENTIFIER` parameter.
* It will read external files and serves them like they are located locally.
*/
class WPSEO_MyYoast_Proxy implements WPSEO_WordPress_Integration {
/**
* The page identifier used in WordPress to register the MyYoast proxy page.
*
* @var string
*/
const PAGE_IDENTIFIER = 'wpseo_myyoast_proxy';
/**
* The cache control's max age. Used in the header of a successful proxy response.
*
* @var int
*/
const CACHE_CONTROL_MAX_AGE = DAY_IN_SECONDS;
/**
* Registers the hooks when the user is on the right page.
*
* @codeCoverageIgnore
*
* @return void
*/
public function register_hooks() {
if ( ! $this->is_proxy_page() ) {
return;
}
// Register the page for the proxy.
add_action( 'admin_menu', array( $this, 'add_proxy_page' ) );
add_action( 'admin_init', array( $this, 'handle_proxy_page' ) );
}
/**
* Registers the proxy page. It does not actually add a link to the dashboard.
*
* @codeCoverageIgnore
*
* @return void
*/
public function add_proxy_page() {
add_dashboard_page( '', '', 'read', self::PAGE_IDENTIFIER, '' );
}
/**
* Renders the requested proxy page and exits to prevent the WordPress UI from loading.
*
* @codeCoverageIgnore
*
* @return void
*/
public function handle_proxy_page() {
$this->render_proxy_page();
// Prevent the WordPress UI from loading.
exit;
}
/**
* Renders the requested proxy page.
*
* This is separated from the exits to be able to test it.
*
* @return void
*/
public function render_proxy_page() {
$proxy_options = $this->determine_proxy_options();
if ( $proxy_options === array() ) {
// Do not accept any other file than implemented.
$this->set_header( 'HTTP/1.0 501 Requested file not implemented' );
return;
}
// Set the headers before serving the remote file.
$this->set_header( 'Content-Type: ' . $proxy_options['content_type'] );
$this->set_header( 'Cache-Control: max-age=' . self::CACHE_CONTROL_MAX_AGE );
if ( $this->should_load_url_directly() ) {
/*
* If an error occurred, fallback to the next proxy method (`wp_remote_get`).
* Otherwise, we are done here.
*/
if ( $this->load_url( $proxy_options['url'] ) ) {
return;
}
}
try {
echo $this->get_remote_url_body( $proxy_options['url'] );
}
catch ( Exception $e ) {
/*
* Reset the file headers because the loading failed.
*
* Note: Due to supporting PHP 5.2 `header_remove` can not be used here.
* Overwrite the headers instead.
*/
$this->set_header( 'Content-Type: text/plain' );
$this->set_header( 'Cache-Control: max-age=0' );
$this->set_header( 'HTTP/1.0 500 ' . $e->getMessage() );
}
}
/**
* Tries to load the given url via `wp_remote_get`.
*
* @codeCoverageIgnore
*
* @param string $url The url to load.
*
* @throws Exception When `wp_remote_get` returned an error.
* @throws Exception When the response code is not 200.
*
* @return string The body of the response.
*/
protected function get_remote_url_body( $url ) {
$response = wp_remote_get( $url );
if ( $response instanceof WP_Error ) {
throw new Exception( 'Unable to retrieve file from MyYoast' );
}
if ( wp_remote_retrieve_response_code( $response ) !== 200 ) {
throw new Exception( 'Received unexpected response from MyYoast' );
}
return wp_remote_retrieve_body( $response );
}
/**
* Tries to load the given url.
*
* @link https://php.net/manual/en/function.readfile.php
*
* @codeCoverageIgnore
*
* @param string $url The url to load.
*
* @return bool False if an error occurred.
*/
protected function load_url( $url ) {
return readfile( $url ) !== false;
}
/**
* Determines the proxy options based on the file and plugin version arguments.
*
* When the file is known it returns an array like this:
* <code>
* $array = array(
* 'content_type' => 'the content type'
* 'url' => 'the url, possibly with the plugin version'
* )
* </code>
*
* @return array Empty for an unknown file. See format above for known files.
*/
protected function determine_proxy_options() {
if ( $this->get_proxy_file() === 'research-webworker' ) {
return array(
'content_type' => 'text/javascript; charset=UTF-8',
'url' => 'https://my.yoast.com/api/downloads/file/analysis-worker?plugin_version=' . $this->get_plugin_version(),
);
}
return array();
}
/**
* Checks the PHP configuration of allow_url_fopen.
*
* @codeCoverageIgnore
*
* @link https://php.net/manual/en/filesystem.configuration.php#ini.allow-url-fopen
*
* @return bool True when the PHP configuration allows for url loading via readfile.
*/
protected function should_load_url_directly() {
return ! ! ini_get( 'allow_url_fopen' );
}
/**
* Checks if the current page is the MyYoast proxy page.
*
* @codeCoverageIgnore
*
* @return bool True when the page request parameter equals the proxy page.
*/
protected function is_proxy_page() {
return filter_input( INPUT_GET, 'page' ) === self::PAGE_IDENTIFIER;
}
/**
* Returns the proxy file from the HTTP request parameters.
*
* @codeCoverageIgnore
*
* @return string The sanitized file request parameter.
*/
protected function get_proxy_file() {
return filter_input( INPUT_GET, 'file', FILTER_SANITIZE_STRING );
}
/**
* Returns the plugin version from the HTTP request parameters.
*
* @codeCoverageIgnore
*
* @return string The sanitized plugin_version request parameter.
*/
protected function get_plugin_version() {
$plugin_version = filter_input( INPUT_GET, 'plugin_version', FILTER_SANITIZE_STRING );
// Replace slashes to secure against requiring a file from another path.
$plugin_version = str_replace( array( '/', '\\' ), '_', $plugin_version );
return $plugin_version;
}
/**
* Sets the HTTP header.
*
* This is a tiny helper function to enable better testing.
*
* @codeCoverageIgnore
*
* @param string $header The header to set.
*
* @return void
*/
protected function set_header( $header ) {
header( $header );
}
}

View File

@ -0,0 +1,315 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Represents the route for MyYoast.
*/
class WPSEO_MyYoast_Route implements WPSEO_WordPress_Integration {
/**
* @var string
*/
const PAGE_IDENTIFIER = 'wpseo_myyoast';
/**
* The instance of the MyYoast client.
*
* @var WPSEO_MyYoast_Client
*/
protected $client;
/**
* The actions that are supported.
*
* Each action should have a method named equally to the action.
*
* For example:
* The connect action is handled by a method named 'connect'.
*
* @var array
*/
protected static $allowed_actions = array( 'connect', 'authorize', 'complete' );
/**
* Sets the hooks when the user has enough rights and is on the right page.
*
* @return void
*/
public function register_hooks() {
$route = filter_input( INPUT_GET, 'page' );
if ( ! ( $this->is_myyoast_route( $route ) && $this->can_access_route() ) ) {
return;
}
if ( ! $this->is_valid_action( $this->get_action() ) ) {
return;
}
add_action( 'admin_menu', array( $this, 'register_route' ) );
add_action( 'admin_init', array( $this, 'handle_route' ) );
}
/**
* Registers the page for the MyYoast route.
*
* @codeCoverageIgnore
*
* @return void
*/
public function register_route() {
add_dashboard_page(
'', // Is empty because we don't render a page.
'', // Is empty because we don't want a menu item.
'wpseo_manage_options',
self::PAGE_IDENTIFIER
);
}
/**
* Abstracts the action from the URL and follows the appropriate route.
*
* @return void
*/
public function handle_route() {
$action = $this->get_action();
if ( ! $this->is_valid_action( $action ) || ! method_exists( $this, $action ) ) {
return;
}
// Dynamically call the method.
$this->$action();
}
/**
* Checks if the current page is the MyYoast route.
*
* @param string $route The MyYoast route.
*
* @return bool True when url is the MyYoast route.
*/
protected function is_myyoast_route( $route ) {
return ( $route === self::PAGE_IDENTIFIER );
}
/**
* Compares an action to a list of allowed actions to see if it is valid.
*
* @param string $action The action to check.
*
* @return bool True if the action is valid.
*/
protected function is_valid_action( $action ) {
return in_array( $action, self::$allowed_actions, true );
}
/**
* Connects to MyYoast and generates a new clientId.
*
* @return void
*/
protected function connect() {
$client_id = $this->generate_uuid();
$this->save_client_id( $client_id );
$this->redirect(
'https://my.yoast.com/connect',
array(
'url' => WPSEO_Utils::get_home_url(),
'client_id' => $client_id,
'extensions' => $this->get_extensions(),
'redirect_url' => admin_url( 'admin.php?page=' . self::PAGE_IDENTIFIER . '&action=complete' ),
'credentials_url' => rest_url( 'yoast/v1/myyoast/connect' ),
'type' => 'wordpress',
)
);
}
/**
* Redirects the user to the oAuth authorization page.
*
* @return void
*/
protected function authorize() {
$client = $this->get_client();
if ( ! $client->has_configuration() ) {
return;
}
$this->redirect(
$client->get_provider()->getAuthorizationUrl()
);
}
/**
* Completes the oAuth connection flow.
*
* @return void
*/
protected function complete() {
$client = $this->get_client();
if ( ! $client->has_configuration() ) {
return;
}
try {
$access_token = $client
->get_provider()
->getAccessToken(
'authorization_code',
array(
'code' => $this->get_authorization_code(),
)
);
$client->save_access_token(
$this->get_current_user_id(),
$access_token
);
}
// @codingStandardsIgnoreLine Generic.CodeAnalysis.EmptyStatement.DetectedCATCH -- There is nothing to do.
catch ( Exception $e ) {
// Do nothing.
}
$this->redirect_to_premium_page();
}
/**
* Saves the client id.
*
* @codeCoverageIgnore
*
* @param string $client_id The client id to save.
*
* @return void
*/
protected function save_client_id( $client_id ) {
$this->get_client()->save_configuration(
array(
'clientId' => $client_id,
)
);
}
/**
* Creates a new MyYoast Client instance.
*
* @codeCoverageIgnore
*
* @return WPSEO_MyYoast_Client Instance of the myyoast client.
*/
protected function get_client() {
if ( ! $this->client ) {
$this->client = new WPSEO_MyYoast_Client();
}
return $this->client;
}
/**
* Abstracts the action from the url.
*
* @codeCoverageIgnore
*
* @return string The action from the url.
*/
protected function get_action() {
return filter_input( INPUT_GET, 'action' );
}
/**
* Abstracts the authorization code from the url.
*
* @codeCoverageIgnore
*
* @return string The action from the url.
*/
protected function get_authorization_code() {
return filter_input( INPUT_GET, 'code' );
}
/**
* Retrieves a list of activated extensions slugs.
*
* @codeCoverageIgnore
*
* @return array The extensions slugs.
*/
protected function get_extensions() {
$addon_manager = new WPSEO_Addon_Manager();
return array_keys( $addon_manager->get_subscriptions_for_active_addons() );
}
/**
* Generates an URL-encoded query string, redirects there.
*
* @codeCoverageIgnore
*
* @param string $url The url to redirect to.
* @param array $query_args The additional arguments to build the url from.
*
* @return void
*/
protected function redirect( $url, $query_args = array() ) {
if ( ! empty( $query_args ) ) {
$url .= '?' . http_build_query( $query_args );
}
wp_redirect( $url );
exit;
}
/**
* Checks if current user is allowed to access the route.
*
* @codeCoverageIgnore
*
* @return bool True when current user has rights to manage options.
*/
protected function can_access_route() {
return WPSEO_Utils::has_access_token_support() && current_user_can( 'wpseo_manage_options' );
}
/**
* Generates an unique user id.
*
* @codeCoverageIgnore
*
* @return string The generated unique user id.
*/
protected function generate_uuid() {
return wp_generate_uuid4();
}
/**
* Retrieves the current user id.
*
* @codeCoverageIgnore
*
* @return int The user id.
*/
protected function get_current_user_id() {
return get_current_user_id();
}
/**
* Redirects to the premium page.
*
* @codeCoverageIgnore
*
* @return void
*/
protected function redirect_to_premium_page() {
wp_safe_redirect( admin_url( 'admin.php?page=wpseo_licenses' ) );
exit;
}
}

View File

@ -0,0 +1,103 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Options\Tabs
*/
/**
* Class WPSEO_Option_Tab.
*/
class WPSEO_Option_Tab {
/**
* Name of the tab.
*
* @var string
*/
private $name;
/**
* Label of the tab.
*
* @var string
*/
private $label;
/**
* Optional arguments.
*
* @var array
*/
private $arguments;
/**
* WPSEO_Option_Tab constructor.
*
* @param string $name Name of the tab.
* @param string $label Localized label of the tab.
* @param array $arguments Optional arguments.
*/
public function __construct( $name, $label, array $arguments = array() ) {
$this->name = sanitize_title( $name );
$this->label = $label;
$this->arguments = $arguments;
}
/**
* Gets the name.
*
* @return string The name.
*/
public function get_name() {
return $this->name;
}
/**
* Gets the label.
*
* @return string The label.
*/
public function get_label() {
return $this->label;
}
/**
* Gets the video URL.
*
* @return string The video url.
*/
public function get_video_url() {
return $this->get_argument( 'video_url' );
}
/**
* Retrieves whether the tab needs a save button.
*
* @return bool True whether the tabs needs a save button.
*/
public function has_save_button() {
return (bool) $this->get_argument( 'save_button', true );
}
/**
* Gets the option group.
*
* @return string The option group.
*/
public function get_opt_group() {
return $this->get_argument( 'opt_group' );
}
/**
* Retrieves the variable from the supplied arguments.
*
* @param string $variable Variable to retrieve.
* @param string|mixed $default Default to use when variable not found.
*
* @return mixed|string The retrieved variable.
*/
protected function get_argument( $variable, $default = '' ) {
return array_key_exists( $variable, $this->arguments ) ? $this->arguments[ $variable ] : $default;
}
}

View File

@ -0,0 +1,79 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Options\Tabs
*/
/**
* Class WPSEO_Option_Tabs_Formatter.
*/
class WPSEO_Option_Tabs_Formatter {
/**
* @param WPSEO_Option_Tabs $option_tabs Option Tabs to get base from.
* @param WPSEO_Option_Tab $tab Tab to get name from.
*
* @return string
*/
public function get_tab_view( WPSEO_Option_Tabs $option_tabs, WPSEO_Option_Tab $tab ) {
return WPSEO_PATH . 'admin/views/tabs/' . $option_tabs->get_base() . '/' . $tab->get_name() . '.php';
}
/**
* @param WPSEO_Option_Tabs $option_tabs Option Tabs to get tabs from.
*/
public function run( WPSEO_Option_Tabs $option_tabs ) {
echo '<h2 class="nav-tab-wrapper" id="wpseo-tabs">';
foreach ( $option_tabs->get_tabs() as $tab ) {
printf(
'<a class="nav-tab" id="%1$s" href="%2$s">%3$s</a>',
esc_attr( $tab->get_name() . '-tab' ),
esc_url( '#top#' . $tab->get_name() ),
esc_html( $tab->get_label() )
);
}
echo '</h2>';
$help_center = new WPSEO_Help_Center( '', $option_tabs, WPSEO_Utils::is_yoast_seo_premium() );
$help_center->localize_data();
$help_center->mount();
foreach ( $option_tabs->get_tabs() as $tab ) {
$identifier = $tab->get_name();
$class = 'wpseotab ' . ( $tab->has_save_button() ? 'save' : 'nosave' );
printf( '<div id="%1$s" class="%2$s">', esc_attr( $identifier ), esc_attr( $class ) );
$tab_filter_name = sprintf( '%s_%s', $option_tabs->get_base(), $tab->get_name() );
/**
* Allows to override the content that is display on the specific option tab.
*
* @internal For internal Yoast SEO use only.
*
* @api string|null The content that should be displayed for this tab. Leave empty for default behaviour.
*
* @param WPSEO_Option_Tabs $option_tabs The registered option tabs.
* @param WPSEO_Option_Tab $tab The tab that is being displayed.
*/
$option_tab_content = apply_filters( 'wpseo_option_tab-' . $tab_filter_name, null, $option_tabs, $tab );
if ( ! empty( $option_tab_content ) ) {
echo $option_tab_content;
}
if ( empty( $option_tab_content ) ) {
// Output the settings view for all tabs.
$tab_view = $this->get_tab_view( $option_tabs, $tab );
if ( is_file( $tab_view ) ) {
$yform = Yoast_Form::get_instance();
require $tab_view;
}
}
echo '</div>';
}
}
}

View File

@ -0,0 +1,121 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Options\Tabs
*/
/**
* Class WPSEO_Option_Tabs.
*/
class WPSEO_Option_Tabs {
/**
* Tabs base.
*
* @var string
*/
private $base;
/**
* The tabs in this group.
*
* @var array
*/
private $tabs = array();
/**
* Name of the active tab.
*
* @var string
*/
private $active_tab = '';
/**
* WPSEO_Option_Tabs constructor.
*
* @codeCoverageIgnore
*
* @param string $base Base of the tabs.
* @param string $active_tab Currently active tab.
*/
public function __construct( $base, $active_tab = '' ) {
$this->base = sanitize_title( $base );
$tab = filter_input( INPUT_GET, 'tab' );
$this->active_tab = empty( $tab ) ? $active_tab : $tab;
}
/**
* Get the base.
*
* @return string
*/
public function get_base() {
return $this->base;
}
/**
* Add a tab.
*
* @param WPSEO_Option_Tab $tab Tab to add.
*
* @return $this
*/
public function add_tab( WPSEO_Option_Tab $tab ) {
$this->tabs[] = $tab;
return $this;
}
/**
* Get active tab.
*
* @return null|WPSEO_Option_Tab Get the active tab.
*/
public function get_active_tab() {
if ( empty( $this->active_tab ) ) {
return null;
}
$active_tabs = array_filter( $this->tabs, array( $this, 'is_active_tab' ) );
if ( ! empty( $active_tabs ) ) {
$active_tabs = array_values( $active_tabs );
if ( count( $active_tabs ) === 1 ) {
return $active_tabs[0];
}
}
return null;
}
/**
* Is the tab the active tab.
*
* @param WPSEO_Option_Tab $tab Tab to check for active tab.
*
* @return bool
*/
public function is_active_tab( WPSEO_Option_Tab $tab ) {
return ( $tab->get_name() === $this->active_tab );
}
/**
* Get all tabs.
*
* @return WPSEO_Option_Tab[]
*/
public function get_tabs() {
return $this->tabs;
}
/**
* Display the tabs.
*
* @param Yoast_Form $yform Yoast Form needed in the views.
*/
public function display( Yoast_Form $yform ) {
$formatter = new WPSEO_Option_Tabs_Formatter();
$formatter->run( $this, $yform );
}
}

View File

@ -0,0 +1,123 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Class WPSEO_presenter_paper.
*/
class WPSEO_Paper_Presenter {
/**
* Title of the paper.
*
* @var string
*/
private $title;
/**
* The view variables.
*
* @var array
*/
private $settings;
/**
* The path to the view file.
*
* @var string
*/
private $view_file;
/**
* WPSEO_presenter_paper constructor.
*
* @param string $title The title of the paper.
* @param string $view_file The path to the view file.
* @param array $settings Optional. Settings for the paper.
*/
public function __construct( $title, $view_file, array $settings = array() ) {
$defaults = array(
'paper_id' => null,
'collapsible' => false,
'expanded' => false,
'help_text' => '',
'title_after' => '',
'view_data' => array(),
);
$this->settings = wp_parse_args( $settings, $defaults );
$this->title = $title;
$this->view_file = $view_file;
}
/**
* Renders the collapsible paper and returns it as a string.
*
* @return string The rendered paper.
*/
public function get_output() {
extract( $this->get_view_variables(), EXTR_SKIP );
ob_start();
require WPSEO_PATH . 'admin/views/paper-collapsible.php';
$rendered_output = ob_get_clean();
return $rendered_output;
}
/**
* Retrieves the view variables.
*
* @return array The view variables.
*/
private function get_view_variables() {
if ( $this->settings['help_text'] instanceof WPSEO_Admin_Help_Panel === false ) {
$this->settings['help_text'] = new WPSEO_Admin_Help_Panel( '', '', '' );
}
$view_variables = array(
'collapsible' => $this->settings['collapsible'],
'collapsible_config' => $this->collapsible_config(),
'title_after' => $this->settings['title_after'],
'help_text' => $this->settings['help_text'],
'view_file' => $this->view_file,
'title' => $this->title,
'paper_id' => $this->settings['paper_id'],
'yform' => Yoast_Form::get_instance(),
);
return array_merge( $this->settings['view_data'], $view_variables );
}
/**
* Retrieves the collapsible config based on the settings.
*
* @return array The config.
*/
protected function collapsible_config() {
if ( empty( $this->settings['collapsible'] ) ) {
return array(
'toggle_icon' => '',
'class' => '',
'expanded' => '',
);
}
if ( ! empty( $this->settings['expanded'] ) ) {
return array(
'toggle_icon' => 'dashicons-arrow-up-alt2',
'class' => 'toggleable-container',
'expanded' => 'true',
);
}
return array(
'toggle_icon' => 'dashicons-arrow-down-alt2',
'class' => 'toggleable-container toggleable-container-hidden',
'expanded' => 'false',
);
}
}

View File

@ -0,0 +1,345 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Plugin_Availability
*/
/**
* Class WPSEO_Plugin_Availability
*/
class WPSEO_Plugin_Availability {
/**
* @var array
*/
protected $plugins = array();
/**
* Registers the plugins so we can access them.
*/
public function register() {
$this->register_yoast_plugins();
$this->register_yoast_plugins_status();
}
/**
* Registers all the available Yoast SEO plugins.
*/
protected function register_yoast_plugins() {
$this->plugins = array(
'yoast-seo-premium' => array(
'url' => WPSEO_Shortlinker::get( 'https://yoa.st/1y7' ),
'title' => 'Yoast SEO Premium',
'description' => sprintf(
/* translators: %1$s expands to Yoast SEO */
__( 'The premium version of %1$s with more features & support.', 'wordpress-seo' ),
'Yoast SEO'
),
'installed' => false,
'slug' => 'wordpress-seo-premium/wp-seo-premium.php',
'version_sync' => true,
'premium' => true,
),
'video-seo-for-wordpress-seo-by-yoast' => array(
'url' => WPSEO_Shortlinker::get( 'https://yoa.st/1y8' ),
'title' => 'Video SEO',
'description' => __( 'Optimize your videos to show them off in search results and get more clicks!', 'wordpress-seo' ),
'installed' => false,
'slug' => 'wpseo-video/video-seo.php',
'version_sync' => true,
'premium' => true,
),
'yoast-news-seo' => array(
'url' => WPSEO_Shortlinker::get( 'https://yoa.st/1y9' ),
'title' => 'News SEO',
'description' => __( 'Are you in Google News? Increase your traffic from Google News by optimizing for it!', 'wordpress-seo' ),
'installed' => false,
'slug' => 'wpseo-news/wpseo-news.php',
'version_sync' => true,
'premium' => true,
),
'local-seo-for-yoast-seo' => array(
'url' => WPSEO_Shortlinker::get( 'https://yoa.st/1ya' ),
'title' => 'Local SEO',
'description' => __( 'Rank better locally and in Google Maps, without breaking a sweat!', 'wordpress-seo' ),
'installed' => false,
'slug' => 'wordpress-seo-local/local-seo.php',
'version_sync' => true,
'premium' => true,
),
'yoast-woocommerce-seo' => array(
'url' => WPSEO_Shortlinker::get( 'https://yoa.st/1o0' ),
'title' => 'Yoast WooCommerce SEO',
'description' => sprintf(
/* translators: %1$s expands to Yoast SEO */
__( 'Seamlessly integrate WooCommerce with %1$s and get extra features!', 'wordpress-seo' ),
'Yoast SEO'
),
'_dependencies' => array(
'WooCommerce' => array(
'slug' => 'woocommerce/woocommerce.php',
),
),
'installed' => false,
'slug' => 'wpseo-woocommerce/wpseo-woocommerce.php',
'version_sync' => true,
'premium' => true,
),
'yoast-acf-analysis' => array(
'url' => 'https://wordpress.org/plugins/acf-content-analysis-for-yoast-seo/',
'title' => 'ACF Content Analysis for Yoast SEO',
'description' => sprintf(
/* translators: %1$s expands to Yoast SEO, %2$s expands to Advanced Custom Fields */
__( 'Seamlessly integrate %2$s with %1$s for the content analysis!', 'wordpress-seo' ),
'Yoast SEO',
'Advanced Custom Fields'
),
'installed' => false,
'slug' => 'acf-content-analysis-for-yoast-seo/yoast-acf-analysis.php',
'_dependencies' => array(
'Advanced Custom Fields' => array(
'slug' => 'advanced-custom-fields/acf.php',
),
),
'version_sync' => false,
),
'yoastseo-amp' => array(
'url' => 'https://wordpress.org/plugins/glue-for-yoast-seo-amp/',
'title' => 'Yoast SEO AMP Glue',
'description' => sprintf(
/* translators: %1$s expands to Yoast SEO */
__( 'Seamlessly integrate %1$s into your AMP pages!', 'wordpress-seo' ),
'Yoast SEO'
),
'installed' => false,
'slug' => 'glue-for-yoast-seo-amp/yoastseo-amp.php',
'_dependencies' => array(
'AMP' => array(
'slug' => 'amp/amp.php',
),
),
'version_sync' => false,
),
);
}
/**
* Sets certain plugin properties based on WordPress' status.
*/
protected function register_yoast_plugins_status() {
foreach ( $this->plugins as $name => $plugin ) {
$plugin_slug = $plugin['slug'];
$plugin_path = WP_PLUGIN_DIR . '/' . $plugin_slug;
if ( file_exists( $plugin_path ) ) {
$plugin_data = get_plugin_data( $plugin_path, false, false );
$this->plugins[ $name ]['installed'] = true;
$this->plugins[ $name ]['version'] = $plugin_data['Version'];
$this->plugins[ $name ]['active'] = is_plugin_active( $plugin_slug );
}
}
}
/**
* Checks whether or not a plugin is known within the Yoast SEO collection.
*
* @param string $plugin The plugin to search for.
*
* @return bool Whether or not the plugin is exists.
*/
protected function plugin_exists( $plugin ) {
return isset( $this->plugins[ $plugin ] );
}
/**
* Gets all the possibly available plugins.
*
* @return array Array containing the information about the plugins.
*/
public function get_plugins() {
return $this->plugins;
}
/**
* Gets a specific plugin. Returns an empty array if it cannot be found.
*
* @param string $plugin The plugin to search for.
*
* @return array The plugin properties.
*/
public function get_plugin( $plugin ) {
if ( ! $this->plugin_exists( $plugin ) ) {
return array();
}
return $this->plugins[ $plugin ];
}
/**
* Gets the version of the plugin.
*
* @param array $plugin The information available about the plugin.
*
* @return string The version associated with the plugin.
*/
public function get_version( $plugin ) {
if ( ! isset( $plugin['version'] ) ) {
return '';
}
return $plugin['version'];
}
/**
* Checks if there are dependencies available for the plugin.
*
* @param array $plugin The information available about the plugin.
*
* @return bool Whether or not there is a dependency present.
*/
public function has_dependencies( $plugin ) {
return ( isset( $plugin['_dependencies'] ) && ! empty( $plugin['_dependencies'] ) );
}
/**
* Gets the dependencies for the plugin.
*
* @param array $plugin The information available about the plugin.
*
* @return array Array containing all the dependencies associated with the plugin.
*/
public function get_dependencies( $plugin ) {
if ( ! $this->has_dependencies( $plugin ) ) {
return array();
}
return $plugin['_dependencies'];
}
/**
* Checks if all dependencies are satisfied.
*
* @param array $plugin The information available about the plugin.
*
* @return bool Whether or not the dependencies are satisfied.
*/
public function dependencies_are_satisfied( $plugin ) {
if ( ! $this->has_dependencies( $plugin ) ) {
return true;
}
$dependencies = $this->get_dependencies( $plugin );
$installed_dependencies = array_filter( $dependencies, array( $this, 'is_dependency_available' ) );
return count( $installed_dependencies ) === count( $dependencies );
}
/**
* Checks whether or not one of the plugins is properly installed and usable.
*
* @param array $plugin The information available about the plugin.
*
* @return bool Whether or not the plugin is properly installed.
*/
public function is_installed( $plugin ) {
if ( empty( $plugin ) ) {
return false;
}
return $this->is_available( $plugin );
}
/**
* Gets all installed plugins.
*
* @return array The installed plugins.
*/
public function get_installed_plugins() {
$installed = array();
foreach ( $this->plugins as $plugin_key => $plugin ) {
if ( $this->is_installed( $plugin ) ) {
$installed[ $plugin_key ] = $plugin;
}
}
return $installed;
}
/**
* Checks for the availability of the plugin.
*
* @param array $plugin The information available about the plugin.
*
* @return bool Whether or not the plugin is available.
*/
public function is_available( $plugin ) {
return isset( $plugin['installed'] ) && $plugin['installed'] === true;
}
/**
* Checks whether a dependency is available.
*
* @param array $dependency The information about the dependency to look for.
*
* @return bool Whether or not the dependency is available.
*/
public function is_dependency_available( $dependency ) {
return in_array( $dependency['slug'], array_keys( get_plugins() ), true );
}
/**
* Gets the names of the dependencies.
*
* @param array $plugin The plugin to get the dependency names from.
*
* @return array Array containing the names of the associated dependencies.
*/
public function get_dependency_names( $plugin ) {
if ( ! $this->has_dependencies( $plugin ) ) {
return array();
}
return array_keys( $plugin['_dependencies'] );
}
/**
* Gets an array of plugins that have defined dependencies.
*
* @return array Array of the plugins that have dependencies.
*/
public function get_plugins_with_dependencies() {
return array_filter( $this->plugins, array( $this, 'has_dependencies' ) );
}
/**
* Determines whether or not a plugin is active.
*
* @param string $plugin The plugin slug to check.
*
* @return bool Whether or not the plugin is active.
*/
public function is_active( $plugin ) {
return is_plugin_active( $plugin );
}
/**
* Determines whether or not a plugin is a Premium product.
*
* @param array $plugin The plugin to check.
*
* @return bool Whether or not the plugin is a Premium product.
*/
public function is_premium( $plugin ) {
return isset( $plugin['premium'] ) && $plugin['premium'] === true;
}
}

View File

@ -0,0 +1,109 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Plugin_Compatibility
*/
/**
* Class WPSEO_Plugin_Compatibility.
*/
class WPSEO_Plugin_Compatibility {
/**
* @var string
*/
protected $current_wpseo_version;
/**
* @var WPSEO_Plugin_Availability
*/
protected $availability_checker;
/**
* @var array
*/
protected $installed_plugins;
/**
* WPSEO_Plugin_Compatibility constructor.
*
* @param string $version The version to check against.
* @param null|class $availability_checker The checker to use.
*/
public function __construct( $version, $availability_checker = null ) {
// We trim off the patch version, as this shouldn't break the comparison.
$this->current_wpseo_version = $this->get_major_minor_version( $version );
$this->availability_checker = $this->retrieve_availability_checker( $availability_checker );
$this->installed_plugins = $this->availability_checker->get_installed_plugins();
}
/**
* Retrieves the availability checker.
*
* @param null|object $checker The checker to set.
*
* @return WPSEO_Plugin_Availability The checker to use.
*/
private function retrieve_availability_checker( $checker ) {
if ( is_null( $checker ) || ! is_object( $checker ) ) {
$checker = new WPSEO_Plugin_Availability();
$checker->register();
}
return $checker;
}
/**
* Wraps the availability checker's get_installed_plugins method.
*
* @return array Array containing all the installed plugins.
*/
public function get_installed_plugins() {
return $this->installed_plugins;
}
/**
* Creates a list of installed plugins and whether or not they are compatible.
*
* @return array Array containing the installed plugins and compatibility.
*/
public function get_installed_plugins_compatibility() {
foreach ( $this->installed_plugins as $key => $plugin ) {
$this->installed_plugins[ $key ]['compatible'] = $this->is_compatible( $key );
}
return $this->installed_plugins;
}
/**
* Checks whether or not a plugin is compatible.
*
* @param string $plugin The plugin to look for and match.
*
* @return bool Whether or not the plugin is compatible.
*/
public function is_compatible( $plugin ) {
$plugin = $this->availability_checker->get_plugin( $plugin );
// If we are not syncing versions, we are always compatible.
if ( ! isset( $plugin['version_sync'] ) || $plugin['version_sync'] !== true ) {
return true;
}
$plugin_version = $this->availability_checker->get_version( $plugin );
return $this->get_major_minor_version( $plugin_version ) === $this->current_wpseo_version;
}
/**
* Gets the major/minor version of the plugin for easier comparing.
*
* @param string $version The version to trim.
*
* @return string The major/minor version of the plugin.
*/
protected function get_major_minor_version( $version ) {
return substr( $version, 0, 3 );
}
}

View File

@ -0,0 +1,169 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
* @since 1.7.0
*/
/**
* Contains list of conflicting plugins.
*/
class WPSEO_Plugin_Conflict extends Yoast_Plugin_Conflict {
/**
* The plugins must be grouped per section.
*
* It's possible to check for each section if there are conflicting plugin
*
* @var array
*/
protected $plugins = array(
// The plugin which are writing OG metadata.
'open_graph' => array(
'2-click-socialmedia-buttons/2-click-socialmedia-buttons.php',
// 2 Click Social Media Buttons.
'add-link-to-facebook/add-link-to-facebook.php', // Add Link to Facebook.
'add-meta-tags/add-meta-tags.php', // Add Meta Tags.
'easy-facebook-share-thumbnails/esft.php', // Easy Facebook Share Thumbnail.
'facebook/facebook.php', // Facebook (official plugin).
'facebook-awd/AWD_facebook.php', // Facebook AWD All in one.
'facebook-featured-image-and-open-graph-meta-tags/fb-featured-image.php',
// Facebook Featured Image & OG Meta Tags.
'facebook-meta-tags/facebook-metatags.php', // Facebook Meta Tags.
'wonderm00ns-simple-facebook-open-graph-tags/wonderm00n-open-graph.php',
// Facebook Open Graph Meta Tags for WordPress.
'facebook-revised-open-graph-meta-tag/index.php', // Facebook Revised Open Graph Meta Tag.
'facebook-thumb-fixer/_facebook-thumb-fixer.php', // Facebook Thumb Fixer.
'facebook-and-digg-thumbnail-generator/facebook-and-digg-thumbnail-generator.php',
// Fedmich's Facebook Open Graph Meta.
'network-publisher/networkpub.php', // Network Publisher.
'nextgen-facebook/nextgen-facebook.php', // NextGEN Facebook OG.
'opengraph/opengraph.php', // Open Graph.
'open-graph-protocol-framework/open-graph-protocol-framework.php',
// Open Graph Protocol Framework.
'seo-facebook-comments/seofacebook.php', // SEO Facebook Comments.
'sexybookmarks/sexy-bookmarks.php', // Shareaholic.
'shareaholic/sexy-bookmarks.php', // Shareaholic.
'sharepress/sharepress.php', // SharePress.
'simple-facebook-connect/sfc.php', // Simple Facebook Connect.
'social-discussions/social-discussions.php', // Social Discussions.
'social-sharing-toolkit/social_sharing_toolkit.php', // Social Sharing Toolkit.
'socialize/socialize.php', // Socialize.
'only-tweet-like-share-and-google-1/tweet-like-plusone.php',
// Tweet, Like, Google +1 and Share.
'wordbooker/wordbooker.php', // Wordbooker.
'wpsso/wpsso.php', // WordPress Social Sharing Optimization.
'wp-caregiver/wp-caregiver.php', // WP Caregiver.
'wp-facebook-like-send-open-graph-meta/wp-facebook-like-send-open-graph-meta.php',
// WP Facebook Like Send & Open Graph Meta.
'wp-facebook-open-graph-protocol/wp-facebook-ogp.php', // WP Facebook Open Graph protocol.
'wp-ogp/wp-ogp.php', // WP-OGP.
'zoltonorg-social-plugin/zosp.php', // Zolton.org Social Plugin.
),
'xml_sitemaps' => array(
'google-sitemap-plugin/google-sitemap-plugin.php',
// Google Sitemap (BestWebSoft).
'xml-sitemaps/xml-sitemaps.php',
// XML Sitemaps (Denis de Bernardy and Mike Koepke).
'bwp-google-xml-sitemaps/bwp-simple-gxs.php',
// Better WordPress Google XML Sitemaps (Khang Minh).
'google-sitemap-generator/sitemap.php',
// Google XML Sitemaps (Arne Brachhold).
'xml-sitemap-feed/xml-sitemap.php',
// XML Sitemap & Google News feeds (RavanH).
'google-monthly-xml-sitemap/monthly-xml-sitemap.php',
// Google Monthly XML Sitemap (Andrea Pernici).
'simple-google-sitemap-xml/simple-google-sitemap-xml.php',
// Simple Google Sitemap XML (iTx Technologies).
'another-simple-xml-sitemap/another-simple-xml-sitemap.php',
// Another Simple XML Sitemap.
'xml-maps/google-sitemap.php',
// Xml Sitemap (Jason Martens).
'google-xml-sitemap-generator-by-anton-dachauer/adachauer-google-xml-sitemap.php',
// Google XML Sitemap Generator by Anton Dachauer (Anton Dachauer).
'wp-xml-sitemap/wp-xml-sitemap.php',
// WP XML Sitemap (Team Vivacity).
'sitemap-generator-for-webmasters/sitemap.php',
// Sitemap Generator for Webmasters (iwebslogtech).
'xml-sitemap-xml-sitemapcouk/xmls.php',
// XML Sitemap - XML-Sitemap.co.uk (Simon Hancox).
'sewn-in-xml-sitemap/sewn-xml-sitemap.php',
// Sewn In XML Sitemap (jcow).
'rps-sitemap-generator/rps-sitemap-generator.php',
// RPS Sitemap Generator (redpixelstudios).
),
'cloaking' => array(
'rs-head-cleaner/rs-head-cleaner.php',
// RS Head Cleaner Plus https://wordpress.org/plugins/rs-head-cleaner/.
'rs-head-cleaner-lite/rs-head-cleaner-lite.php',
// RS Head Cleaner Lite https://wordpress.org/plugins/rs-head-cleaner-lite/.
),
'seo' => array(
'all-in-one-seo-pack/all_in_one_seo_pack.php', // All in One SEO Pack.
'seo-ultimate/seo-ultimate.php', // SEO Ultimate.
),
);
/**
* Overrides instance to set with this class as class.
*
* @param string $class_name Optional class name.
*
* @return Yoast_Plugin_Conflict
*/
public static function get_instance( $class_name = __CLASS__ ) {
return parent::get_instance( $class_name );
}
/**
* After activating any plugin, this method will be executed by a hook.
*
* If the activated plugin is conflicting with ours a notice will be shown.
*
* @param string|bool $plugin Optional plugin basename to check.
*/
public static function hook_check_for_plugin_conflicts( $plugin = false ) {
// The instance of itself.
$instance = self::get_instance();
// Only add plugin as active plugin if $plugin isn't false.
if ( $plugin && is_string( $plugin ) ) {
// Because it's just activated.
$instance->add_active_plugin( $instance->find_plugin_category( $plugin ), $plugin );
}
$plugin_sections = array();
// Only check for open graph problems when they are enabled.
if ( WPSEO_Options::get( 'opengraph' ) ) {
/* translators: %1$s expands to Yoast SEO, %2$s: 'Facebook' plugin name of possibly conflicting plugin with regard to creating OpenGraph output. */
$plugin_sections['open_graph'] = __( 'Both %1$s and %2$s create OpenGraph output, which might make Facebook, Twitter, LinkedIn and other social networks use the wrong texts and images when your pages are being shared.', 'wordpress-seo' )
. '<br/><br/>'
. '<a class="button" href="' . admin_url( 'admin.php?page=wpseo_social#top#facebook' ) . '">'
/* translators: %1$s expands to Yoast SEO. */
. sprintf( __( 'Configure %1$s\'s OpenGraph settings', 'wordpress-seo' ), 'Yoast SEO' )
. '</a>';
}
// Only check for XML conflicts if sitemaps are enabled.
if ( WPSEO_Options::get( 'enable_xml_sitemap' ) ) {
/* translators: %1$s expands to Yoast SEO, %2$s: 'Google XML Sitemaps' plugin name of possibly conflicting plugin with regard to the creation of sitemaps. */
$plugin_sections['xml_sitemaps'] = __( 'Both %1$s and %2$s can create XML sitemaps. Having two XML sitemaps is not beneficial for search engines and might slow down your site.', 'wordpress-seo' )
. '<br/><br/>'
. '<a class="button" href="' . admin_url( 'admin.php?page=wpseo_dashboard#top#features' ) . '">'
/* translators: %1$s expands to Yoast SEO. */
. sprintf( __( 'Toggle %1$s\'s XML Sitemap', 'wordpress-seo' ), 'Yoast SEO' )
. '</a>';
}
/* translators: %2$s expands to 'RS Head Cleaner' plugin name of possibly conflicting plugin with regard to differentiating output between search engines and normal users. */
$plugin_sections['cloaking'] = __( 'The plugin %2$s changes your site\'s output and in doing that differentiates between search engines and normal users, a process that\'s called cloaking. We highly recommend that you disable it.', 'wordpress-seo' );
/* translators: %1$s expands to Yoast SEO, %2$s: 'SEO' plugin name of possibly conflicting plugin with regard to the creation of duplicate SEO meta. */
$plugin_sections['seo'] = __( 'Both %1$s and %2$s manage the SEO of your site. Running two SEO plugins at the same time is detrimental.', 'wordpress-seo' );
$instance->check_plugin_conflicts( $plugin_sections );
}
}

View File

@ -0,0 +1,104 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Class WPSEO_Premium_popup.
*/
class WPSEO_Premium_Popup {
/**
* An unique identifier for the popup
*
* @var string
*/
private $identifier = '';
/**
* The heading level of the title of the popup.
*
* @var String
*/
private $heading_level = '';
/**
* The title of the popup.
*
* @var String
*/
private $title = '';
/**
* The content of the popup.
*
* @var String
*/
private $content = '';
/**
* The URL for where the button should link to.
*
* @var String
*/
private $url = '';
/**
* Wpseo_Premium_Popup constructor.
*
* @param String $identifier An unique identifier for the popup.
* @param String $heading_level The heading level for the title of the popup.
* @param String $title The title of the popup.
* @param String $content The content of the popup.
* @param String $url The URL for where the button should link to.
*/
public function __construct( $identifier, $heading_level, $title, $content, $url ) {
$this->identifier = $identifier;
$this->heading_level = $heading_level;
$this->title = $title;
$this->content = $content;
$this->url = $url;
}
/**
* Returns the premium popup as an HTML string.
*
* @param bool $popup Show this message as a popup show it straight away.
*
* @return string
*/
public function get_premium_message( $popup = true ) {
// Don't show in Premium.
if ( defined( 'WPSEO_PREMIUM_FILE' ) ) {
return '';
}
$assets_uri = trailingslashit( plugin_dir_url( WPSEO_FILE ) );
/* translators: %s expands to Yoast SEO Premium */
$cta_text = esc_html( sprintf( __( 'Get %s', 'wordpress-seo' ), 'Yoast SEO Premium' ) );
$new_tab_message = '<span class="screen-reader-text">' . esc_html__( '(Opens in a new browser tab)', 'wordpress-seo' ) . '</span>';
$caret_icon = '<span aria-hidden="true" class="yoast-button-upsell__caret"></span>';
$classes = '';
if ( $popup ) {
$classes = ' hidden';
}
$micro_copy = __( '1 year free support and updates included!', 'wordpress-seo' );
$popup = <<<EO_POPUP
<div id="wpseo-{$this->identifier}-popup" class="wpseo-premium-popup wp-clearfix$classes">
<img class="alignright wpseo-premium-popup-icon" src="{$assets_uri}images/Yoast_SEO_Icon.svg" width="150" height="150" alt="Yoast SEO"/>
<{$this->heading_level} id="wpseo-contact-support-popup-title" class="wpseo-premium-popup-title">{$this->title}</{$this->heading_level}>
{$this->content}
<a id="wpseo-{$this->identifier}-popup-button" class="yoast-button-upsell" href="{$this->url}" target="_blank">
{$cta_text} {$new_tab_message} {$caret_icon}
</a><br/>
<small>{$micro_copy}</small>
</div>
EO_POPUP;
return $popup;
}
}

View File

@ -0,0 +1,171 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Class WPSEO_Premium_Upsell_Admin_Block
*/
class WPSEO_Premium_Upsell_Admin_Block {
/**
* Hook to display the block on.
*
* @var string
*/
protected $hook;
/**
* Identifier to use in the dismissal functionality.
*
* @var string
*/
protected $identifier = 'premium_upsell_admin_block';
/**
* Registers which hook the block will be displayed on.
*
* @param string $hook Hook to display the block on.
*/
public function __construct( $hook ) {
$this->hook = $hook;
}
/**
* Registers WordPress hooks.
*
* @return void
*/
public function register_hooks() {
if ( ! $this->is_hidden() ) {
add_action( $this->hook, array( $this, 'render' ) );
}
}
/**
* Renders the upsell block.
*
* @return void
*/
public function render() {
$url = WPSEO_Shortlinker::get( 'https://yoa.st/17h' );
$arguments = array(
'<strong>' . esc_html__( 'Multiple keyphrases', 'wordpress-seo' ) . '</strong>: ' . esc_html__( 'Increase your SEO reach', 'wordpress-seo' ),
'<strong>' . esc_html__( 'No more dead links', 'wordpress-seo' ) . '</strong>: ' . esc_html__( 'Easy redirect manager', 'wordpress-seo' ),
'<strong>' . esc_html__( 'Superfast internal linking suggestions', 'wordpress-seo' ) . '</strong>',
'<strong>' . esc_html__( 'Social media preview', 'wordpress-seo' ) . '</strong>: ' . esc_html__( 'Facebook & Twitter', 'wordpress-seo' ),
'<strong>' . esc_html__( '24/7 support', 'wordpress-seo' ) . '</strong>',
'<strong>' . esc_html__( 'No ads!', 'wordpress-seo' ) . '</strong>',
);
$arguments_html = implode( '', array_map( array( $this, 'get_argument_html' ), $arguments ) );
$class = $this->get_html_class();
/* translators: %s expands to "Yoast SEO Premium". */
$dismiss_msg = sprintf( __( 'Dismiss %s upgrade notice', 'wordpress-seo' ), 'Yoast SEO Premium' );
/* translators: %s expands to Yoast SEO Premium */
$button_text = esc_html( sprintf( __( 'Get %s', 'wordpress-seo' ), 'Yoast SEO Premium' ) );
$button_text .= '<span class="screen-reader-text">' . esc_html__( '(Opens in a new browser tab)', 'wordpress-seo' ) . '</span>' .
'<span aria-hidden="true" class="yoast-button-upsell__caret"></span>';
$upgrade_button = sprintf(
'<a id="wpseo-%1$s-popup-button" class="yoast-button-upsell" href="%2$s" target="_blank">%3$s</a>',
$this->identifier,
esc_url( $url ),
$button_text
);
echo '<div class="' . esc_attr( $class ) . '">';
printf(
'<a href="%1$s" style="" class="alignright button %2$s" aria-label="%3$s"><span class="dashicons dashicons-no-alt"></span></a>',
esc_url( add_query_arg( array( $this->get_query_variable_name() => 1 ) ) ),
esc_attr( $class . '--close' ),
esc_attr( $dismiss_msg )
);
echo '<div>';
echo '<h2 class="' . esc_attr( $class . '--header' ) . '">' .
sprintf(
/* translators: %s expands to Yoast SEO Premium */
esc_html__( 'Upgrade to %s', 'wordpress-seo' ),
'Yoast SEO Premium'
) .
'</h2>';
echo '<ul class="' . esc_attr( $class . '--motivation' ) . '">' . $arguments_html . '</ul>';
echo '<p>' . $upgrade_button . '</p>';
echo '</div>';
echo '</div>';
}
/**
* Formats the argument to a HTML list item.
*
* @param string $argument The argument to format.
*
* @return string Formatted argument in HTML.
*/
protected function get_argument_html( $argument ) {
$class = $this->get_html_class();
return sprintf(
'<li><div class="%1$s">%2$s</div></li>',
esc_attr( $class . '--argument' ),
$argument
);
}
/**
* Checks if the block is hidden by the user.
*
* @return bool False when it should be shown, True if it should be hidden.
*/
protected function is_hidden() {
$transient_name = $this->get_option_name();
$hide = (bool) get_user_option( $transient_name );
if ( ! $hide ) {
$query_variable_name = $this->get_query_variable_name();
if ( filter_input( INPUT_GET, $query_variable_name, FILTER_VALIDATE_INT ) === 1 ) {
// No expiration time, so this would normally not expire, but it wouldn't be copied to other sites etc.
update_user_option( get_current_user_id(), $transient_name, true );
$hide = true;
}
}
return $hide;
}
/**
* Retrieves the option name to use.
*
* @return string The name of the option to save the data in.
*/
protected function get_option_name() {
return 'yoast_promo_hide_' . $this->identifier;
}
/**
* Retrieves the query variable to use for dismissing the block.
*
* @return string The name of the query variable to use.
*/
protected function get_query_variable_name() {
return 'yoast_promo_hide_' . $this->identifier;
}
/**
* Returns the HTML base class to use.
*
* @return string The HTML base class.
*/
protected function get_html_class() {
return 'yoast_' . $this->identifier;
}
}

View File

@ -0,0 +1,305 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Adds the UI to change the primary term for a post.
*/
class WPSEO_Primary_Term_Admin implements WPSEO_WordPress_Integration {
/**
* Constructor.
*/
public function register_hooks() {
add_filter( 'wpseo_content_meta_section_content', array( $this, 'add_input_fields' ) );
add_action( 'admin_footer', array( $this, 'wp_footer' ), 10 );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'save_post', array( $this, 'save_primary_terms' ) );
$primary_term = new WPSEO_Frontend_Primary_Category();
$primary_term->register_hooks();
}
/**
* Gets the current post ID.
*
* @return integer The post ID.
*/
protected function get_current_id() {
$post_id = filter_input( INPUT_GET, 'post', FILTER_SANITIZE_NUMBER_INT );
if ( empty( $post_id ) && isset( $GLOBALS['post_ID'] ) ) {
$post_id = filter_var( $GLOBALS['post_ID'], FILTER_SANITIZE_NUMBER_INT );
}
return $post_id;
}
/**
* Adds hidden fields for primary taxonomies.
*
* @param string $content The metabox content.
*
* @return string The HTML content.
*/
public function add_input_fields( $content ) {
$taxonomies = $this->get_primary_term_taxonomies();
foreach ( $taxonomies as $taxonomy ) {
$content .= $this->primary_term_field( $taxonomy->name );
$content .= wp_nonce_field( 'save-primary-term', WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy->name . '_nonce', false, false );
}
return $content;
}
/**
* Generates the HTML for a hidden field for a primary taxonomy.
*
* @param string $taxonomy_name The taxonomy's slug.
*
* @return string The HTML for a hidden primary taxonomy field.
*/
protected function primary_term_field( $taxonomy_name ) {
return sprintf(
'<input class="yoast-wpseo-primary-term" type="hidden" id="%1$s" name="%2$s" value="%3$s" />',
esc_attr( $this->generate_field_id( $taxonomy_name ) ),
esc_attr( $this->generate_field_name( $taxonomy_name ) ),
esc_attr( $this->get_primary_term( $taxonomy_name ) )
);
}
/**
* Generates an id for a primary taxonomy's hidden field.
*
* @param string $taxonomy_name The taxonomy's slug.
*
* @return string The field id.
*/
protected function generate_field_id( $taxonomy_name ) {
return 'yoast-wpseo-primary-' . $taxonomy_name;
}
/**
* Generates a name for a primary taxonomy's hidden field.
*
* @param string $taxonomy_name The taxonomy's slug.
*
* @return string The field id.
*/
protected function generate_field_name( $taxonomy_name ) {
return WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy_name . '_term';
}
/**
* Adds primary term templates.
*/
public function wp_footer() {
$taxonomies = $this->get_primary_term_taxonomies();
if ( ! empty( $taxonomies ) ) {
$this->include_js_templates();
}
}
/**
* Enqueues all the assets needed for the primary term interface.
*
* @return void
*/
public function enqueue_assets() {
global $pagenow;
if ( ! WPSEO_Metabox::is_post_edit( $pagenow ) ) {
return;
}
$taxonomies = $this->get_primary_term_taxonomies();
// Only enqueue if there are taxonomies that need a primary term.
if ( empty( $taxonomies ) ) {
return;
}
$asset_manager = new WPSEO_Admin_Asset_Manager();
$asset_manager->enqueue_style( 'primary-category' );
$asset_manager->enqueue_script( 'primary-category' );
$mapped_taxonomies = $this->get_mapped_taxonomies_for_js( $taxonomies );
$data = array(
'taxonomies' => $mapped_taxonomies,
);
wp_localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'primary-category', 'wpseoPrimaryCategoryL10n', $data );
}
/**
* Saves all selected primary terms.
*
* @param int $post_id Post ID to save primary terms for.
*/
public function save_primary_terms( $post_id ) {
// Bail if this is a multisite installation and the site has been switched.
if ( is_multisite() && ms_is_switched() ) {
return;
}
$taxonomies = $this->get_primary_term_taxonomies( $post_id );
foreach ( $taxonomies as $taxonomy ) {
$this->save_primary_term( $post_id, $taxonomy );
}
}
/**
* Gets the id of the primary term.
*
* @param string $taxonomy_name Taxonomy name for the term.
*
* @return int primary term id
*/
protected function get_primary_term( $taxonomy_name ) {
$primary_term = new WPSEO_Primary_Term( $taxonomy_name, $this->get_current_id() );
return $primary_term->get_primary_term();
}
/**
* Returns all the taxonomies for which the primary term selection is enabled.
*
* @param int $post_id Default current post ID.
* @return array
*/
protected function get_primary_term_taxonomies( $post_id = null ) {
if ( null === $post_id ) {
$post_id = $this->get_current_id();
}
$taxonomies = wp_cache_get( 'primary_term_taxonomies_' . $post_id, 'wpseo' );
if ( false !== $taxonomies ) {
return $taxonomies;
}
$taxonomies = $this->generate_primary_term_taxonomies( $post_id );
wp_cache_set( 'primary_term_taxonomies_' . $post_id, $taxonomies, 'wpseo' );
return $taxonomies;
}
/**
* Includes templates file.
*/
protected function include_js_templates() {
include_once WPSEO_PATH . 'admin/views/js-templates-primary-term.php';
}
/**
* Saves the primary term for a specific taxonomy.
*
* @param int $post_id Post ID to save primary term for.
* @param WP_Term $taxonomy Taxonomy to save primary term for.
*/
protected function save_primary_term( $post_id, $taxonomy ) {
$primary_term = filter_input( INPUT_POST, WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy->name . '_term', FILTER_SANITIZE_NUMBER_INT );
// We accept an empty string here because we need to save that if no terms are selected.
if ( null !== $primary_term && check_admin_referer( 'save-primary-term', WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy->name . '_nonce' ) ) {
$primary_term_object = new WPSEO_Primary_Term( $taxonomy->name, $post_id );
$primary_term_object->set_primary_term( $primary_term );
}
}
/**
* Generates the primary term taxonomies.
*
* @param int $post_id ID of the post.
*
* @return array
*/
protected function generate_primary_term_taxonomies( $post_id ) {
$post_type = get_post_type( $post_id );
$all_taxonomies = get_object_taxonomies( $post_type, 'objects' );
$all_taxonomies = array_filter( $all_taxonomies, array( $this, 'filter_hierarchical_taxonomies' ) );
/**
* Filters which taxonomies for which the user can choose the primary term.
*
* @api array $taxonomies An array of taxonomy objects that are primary_term enabled.
*
* @param string $post_type The post type for which to filter the taxonomies.
* @param array $all_taxonomies All taxonomies for this post types, even ones that don't have primary term
* enabled.
*/
$taxonomies = (array) apply_filters( 'wpseo_primary_term_taxonomies', $all_taxonomies, $post_type, $all_taxonomies );
return $taxonomies;
}
/**
* Creates a map of taxonomies for localization.
*
* @param array $taxonomies The taxononmies that should be mapped.
*
* @return array The mapped taxonomies.
*/
protected function get_mapped_taxonomies_for_js( $taxonomies ) {
return array_map( array( $this, 'map_taxonomies_for_js' ), $taxonomies );
}
/**
* Returns an array suitable for use in the javascript.
*
* @param stdClass $taxonomy The taxonomy to map.
*
* @return array The mapped taxonomy.
*/
private function map_taxonomies_for_js( $taxonomy ) {
$primary_term = $this->get_primary_term( $taxonomy->name );
if ( empty( $primary_term ) ) {
$primary_term = '';
}
$terms = get_terms( $taxonomy->name );
return array(
'title' => $taxonomy->labels->singular_name,
'name' => $taxonomy->name,
'primary' => $primary_term,
'singularLabel' => $taxonomy->labels->singular_name,
'fieldId' => $this->generate_field_id( $taxonomy->name ),
'restBase' => ( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name,
'terms' => array_map( array( $this, 'map_terms_for_js' ), $terms ),
);
}
/**
* Returns an array suitable for use in the javascript.
*
* @param stdClass $term The term to map.
*
* @return array The mapped terms.
*/
private function map_terms_for_js( $term ) {
return array(
'id' => $term->term_id,
'name' => $term->name,
);
}
/**
* Returns whether or not a taxonomy is hierarchical.
*
* @param stdClass $taxonomy Taxonomy object.
*
* @return bool
*/
private function filter_hierarchical_taxonomies( $taxonomy ) {
return (bool) $taxonomy->hierarchical;
}
}

View File

@ -0,0 +1,203 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Represents the upsell notice.
*/
class WPSEO_Product_Upsell_Notice {
/**
* @var string
*/
const USER_META_DISMISSED = 'wpseo-remove-upsell-notice';
/**
* @var string
*/
const OPTION_NAME = 'wpseo';
/**
* @var array
*/
protected $options;
/**
* Sets the options, because they always have to be there on instance.
*/
public function __construct() {
$this->options = $this->get_options();
}
/**
* Checks if the notice should be added or removed.
*/
public function initialize() {
$this->remove_notification();
}
/**
* Sets the upgrade notice.
*/
public function set_upgrade_notice() {
if ( $this->has_first_activated_on() ) {
return;
}
$this->set_first_activated_on();
$this->add_notification();
}
/**
* Listener for the upsell notice.
*/
public function dismiss_notice_listener() {
if ( filter_input( INPUT_GET, 'yoast_dismiss' ) !== 'upsell' ) {
return;
}
$this->dismiss_notice();
wp_redirect( admin_url( 'admin.php?page=wpseo_dashboard' ) );
exit;
}
/**
* When the notice should be shown.
*
* @return bool
*/
protected function should_add_notification() {
return ( $this->options['first_activated_on'] < strtotime( '-2weeks' ) );
}
/**
* Checks if the options has a first activated on date value.
*/
protected function has_first_activated_on() {
return $this->options['first_activated_on'] !== false;
}
/**
* Sets the first activated on.
*/
protected function set_first_activated_on() {
$this->options['first_activated_on'] = strtotime( '-2weeks' );
$this->save_options();
}
/**
* Adds a notification to the notification center.
*/
protected function add_notification() {
$notification_center = Yoast_Notification_Center::get();
$notification_center->add_notification( $this->get_notification() );
}
/**
* Removes a notification to the notification center.
*/
protected function remove_notification() {
$notification_center = Yoast_Notification_Center::get();
$notification_center->remove_notification( $this->get_notification() );
}
/**
* Returns a premium upsell section if using the free plugin.
*
* @return string
*/
protected function get_premium_upsell_section() {
$features = new WPSEO_Features();
if ( $features->is_free() ) {
return sprintf(
/* translators: %1$s expands anchor to premium plugin page, %2$s expands to </a> */
__( 'By the way, did you know we also have a %1$sPremium plugin%2$s? It offers advanced features, like a redirect manager and support for multiple keyphrases. It also comes with 24/7 personal support.', 'wordpress-seo' ),
"<a href='" . WPSEO_Shortlinker::get( 'https://yoa.st/premium-notification' ) . "'>",
'</a>'
);
}
return '';
}
/**
* Gets the notification value.
*
* @return Yoast_Notification
*/
protected function get_notification() {
$message = sprintf(
/* translators: %1$s expands to Yoast SEO, %2$s is a link start tag to the plugin page on WordPress.org, %3$s is the link closing tag. */
__( 'We\'ve noticed you\'ve been using %1$s for some time now; we hope you love it! We\'d be thrilled if you could %2$sgive us a 5 stars rating on WordPress.org%3$s!', 'wordpress-seo' ),
'Yoast SEO',
'<a href="' . WPSEO_Shortlinker::get( 'https://yoa.st/rate-yoast-seo' ) . '">',
'</a>'
) . "\n\n";
$message .= sprintf(
/* translators: %1$s is a link start tag to the bugreport guidelines on the Yoast knowledge base, %2$s is the link closing tag. */
__( 'If you are experiencing issues, %1$splease file a bug report%2$s and we\'ll do our best to help you out.', 'wordpress-seo' ),
'<a href="' . WPSEO_Shortlinker::get( 'https://yoa.st/bugreport' ) . '">',
'</a>'
) . "\n\n";
$message .= $this->get_premium_upsell_section() . "\n\n";
$message .= sprintf(
/* translators: %1$s is the notification dismissal link start tag, %2$s is the link closing tag. */
__( '%1$sPlease don\'t show me this notification anymore%2$s', 'wordpress-seo' ),
'<a class="button" href="' . admin_url( '?page=' . WPSEO_Admin::PAGE_IDENTIFIER . '&yoast_dismiss=upsell' ) . '">',
'</a>'
);
$notification = new Yoast_Notification(
$message,
array(
'type' => Yoast_Notification::WARNING,
'id' => 'wpseo-upsell-notice',
'capabilities' => 'wpseo_manage_options',
'priority' => 0.8,
)
);
return $notification;
}
/**
* Dismisses the notice.
*
* @return string
*/
protected function is_notice_dismissed() {
return get_user_meta( get_current_user_id(), self::USER_META_DISMISSED, true ) === '1';
}
/**
* Dismisses the notice.
*/
protected function dismiss_notice() {
update_user_meta( get_current_user_id(), self::USER_META_DISMISSED, true );
}
/**
* Returns the set options.
*
* @return mixed|void
*/
protected function get_options() {
return get_option( self::OPTION_NAME );
}
/**
* Saves the options to the database.
*/
protected function save_options() {
update_option( self::OPTION_NAME, $this->options );
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Class WPSEO_Recalculate_Scores.
*
* This class handles the SEO score recalculation for all posts with a filled focus keyword.
*/
class WPSEO_Recalculate_Scores {
/**
* Constructing the object by modalbox, the localization and the totals.
*/
public function __construct() {
add_action( 'admin_enqueue_scripts', array( $this, 'recalculate_assets' ) );
add_action( 'admin_footer', array( $this, 'modal_box' ), 20 );
}
/**
* Run the localize script.
*/
public function recalculate_assets() {
$asset_manager = new WPSEO_Admin_Asset_Manager();
$asset_manager->enqueue_script( 'recalculate' );
}
/**
* Initialize the modal box to be displayed when needed.
*/
public function modal_box() {
// Adding the thickbox.
add_thickbox();
$progress = sprintf(
/* translators: 1: expands to a <span> containing the number of posts recalculated. 2: expands to a <strong> containing the total number of posts. */
esc_html__( '%1$s of %2$s done.', 'wordpress-seo' ),
'<span id="wpseo_count">0</span>',
'<strong id="wpseo_count_total">0</strong>'
);
?>
<div id="wpseo_recalculate" class="hidden">
<p><?php esc_html_e( 'Recalculating SEO scores for all pieces of content with a focus keyphrase.', 'wordpress-seo' ); ?></p>
<div id="wpseo_progressbar"></div>
<p><?php echo $progress; ?></p>
</div>
<?php
}
}

View File

@ -0,0 +1,145 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* This class handles a post request being send to a given endpoint.
*/
class WPSEO_Remote_Request {
/**
* @var string
*/
const METHOD_POST = 'post';
/**
* @var string
*/
const METHOD_GET = 'get';
/**
* @var string
*/
protected $endpoint = '';
/**
* @var array
*/
protected $args = array(
'blocking' => false,
'sslverify' => false,
'timeout' => 2,
);
/**
* @var WP_Error|null
*/
protected $response_error;
/**
* @var mixed
*/
protected $response_body;
/**
* Sets the endpoint and arguments.
*
* @param string $endpoint The endpoint to send the request to.
* @param array $args The arguments to use in this request.
*/
public function __construct( $endpoint, array $args = array() ) {
$this->endpoint = $endpoint;
$this->args = wp_parse_args( $this->args, $args );
}
/**
* Sets the request body.
*
* @param mixed $body The body to set.
*/
public function set_body( $body ) {
$this->args['body'] = $body;
}
/**
* Sends the data to the given endpoint.
*
* @param string $method The type of request to send.
*
* @return bool True when sending data has been successful.
*/
public function send( $method = self::METHOD_POST ) {
switch ( $method ) {
case self::METHOD_POST:
$response = $this->post();
break;
case self::METHOD_GET:
$response = $this->get();
break;
default:
/* translators: %1$s expands to the request method */
$response = new WP_Error( 1, sprintf( __( 'Request method %1$s is not valid.', 'wordpress-seo' ), $method ) );
break;
}
return $this->process_response( $response );
}
/**
* Returns the value of the response error.
*
* @return null|WP_Error The response error.
*/
public function get_response_error() {
return $this->response_error;
}
/**
* Returns the response body.
*
* @return mixed The response body.
*/
public function get_response_body() {
return $this->response_body;
}
/**
* Processes the given response.
*
* @param mixed $response The response to process.
*
* @return bool True when response is valid.
*/
protected function process_response( $response ) {
if ( $response instanceof WP_Error ) {
$this->response_error = $response;
return false;
}
$this->response_body = wp_remote_retrieve_body( $response );
return ( wp_remote_retrieve_response_code( $response ) === 200 );
}
/**
* Performs a post request to the specified endpoint with set arguments.
*
* @return WP_Error|array The response or WP_Error on failure.
*/
protected function post() {
return wp_remote_post( $this->endpoint, $this->args );
}
/**
* Performs a post request to the specified endpoint with set arguments.
*
* @return WP_Error|array The response or WP_Error on failure.
*/
protected function get() {
return wp_remote_get( $this->endpoint, $this->args );
}
}

View File

@ -0,0 +1,76 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Notifies the user to update the Person on the publish entity in the Configuration Wizard.
*/
class WPSEO_Schema_Person_Upgrade_Notification implements WPSEO_WordPress_Integration {
/**
* Registers all hooks to WordPress
*
* @return void
*/
public function register_hooks() {
add_action( 'admin_init', array( $this, 'handle_notification' ) );
}
/**
* Handles if the notification should be added or removed.
*/
public function handle_notification() {
$company_or_person_user_id = WPSEO_Options::get( 'company_or_person_user_id', false );
if ( WPSEO_Options::get( 'company_or_person' ) === 'person' && empty( $company_or_person_user_id ) ) {
$this->add_notification();
return;
}
$this->remove_notification();
}
/**
* Adds a notification to the notification center.
*/
protected function add_notification() {
$notification_center = Yoast_Notification_Center::get();
$notification_center->add_notification( $this->get_notification() );
}
/**
* Removes a notification to the notification center.
*/
protected function remove_notification() {
$notification_center = Yoast_Notification_Center::get();
$notification_center->remove_notification( $this->get_notification() );
}
/**
* Gets the notification object.
*
* @return Yoast_Notification
*/
protected function get_notification() {
$message = sprintf(
/* translators: %1$s is a link start tag to the Configuration Wizard, %2$s is the link closing tag. */
__( 'You have previously set your site to represent a person. Weve improved our functionality around Schema and the Knowledge Graph, so you should go in and %1$scomplete those settings%2$s.', 'wordpress-seo' ),
'<a href="' . admin_url( 'admin.php?page=wpseo_titles' ) . '">',
'</a>'
);
$notification = new Yoast_Notification(
$message,
array(
'type' => Yoast_Notification::WARNING,
'id' => 'wpseo-schema-person-upgrade',
'capabilities' => 'wpseo_manage_options',
'priority' => 0.8,
)
);
return $notification;
}
}

View File

@ -0,0 +1,261 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* This class adds the Social tab to the Yoast SEO metabox and makes sure the settings are saved.
*/
class WPSEO_Social_Admin extends WPSEO_Metabox {
/**
* Class constructor.
*/
public function __construct() {
self::translate_meta_boxes();
add_filter( 'wpseo_save_metaboxes', array( $this, 'save_meta_boxes' ), 10, 1 );
add_action( 'wpseo_save_compare_data', array( $this, 'og_data_compare' ), 10, 1 );
}
/**
* Translate text strings for use in the meta box.
*
* IMPORTANT: if you want to add a new string (option) somewhere, make sure you add that array key to
* the main meta box definition array in the class WPSEO_Meta() as well!!!!
*/
public static function translate_meta_boxes() {
/* translators: %s expands to the social network's name. */
$title_text = __( 'If you don\'t want to use the post title for sharing the post on %s but instead want another title there, write it here.', 'wordpress-seo' );
/* translators: %s expands to the social network's name. */
$description_text = __( 'If you don\'t want to use the meta description for sharing the post on %s but want another description there, write it here.', 'wordpress-seo' );
/* translators: %s expands to the social network's name. */
$image_text = __( 'If you want to override the image used on %s for this post, upload / choose an image here.', 'wordpress-seo' );
/* translators: %1$s expands to the social network, %2$s to the recommended image size. */
$image_size_text = __( 'The recommended image size for %1$s is %2$s pixels.', 'wordpress-seo' );
$social_networks = array(
'opengraph' => __( 'Facebook', 'wordpress-seo' ),
'twitter' => __( 'Twitter', 'wordpress-seo' ),
);
// Source: https://blog.bufferapp.com/ideal-image-sizes-social-media-posts.
$recommended_image_sizes = array(
/* translators: %1$s expands to the image recommended width, %2$s to its height. */
'opengraph' => sprintf( __( '%1$s by %2$s', 'wordpress-seo' ), '1200', '630' ),
// Source: https://developers.facebook.com/docs/sharing/best-practices#images.
/* translators: %1$s expands to the image recommended width, %2$s to its height. */
'twitter' => sprintf( __( '%1$s by %2$s', 'wordpress-seo' ), '1024', '512' ),
);
foreach ( $social_networks as $network => $label ) {
if ( true === WPSEO_Options::get( $network, false ) ) {
/* translators: %s expands to the name of a social network. */
WPSEO_Meta::$meta_fields['social'][ $network . '-title' ]['title'] = sprintf( __( '%s Title', 'wordpress-seo' ), $label );
WPSEO_Meta::$meta_fields['social'][ $network . '-title' ]['description'] = sprintf( $title_text, $label );
/* translators: %s expands to the name of a social network. */
WPSEO_Meta::$meta_fields['social'][ $network . '-description' ]['title'] = sprintf( __( '%s Description', 'wordpress-seo' ), $label );
WPSEO_Meta::$meta_fields['social'][ $network . '-description' ]['description'] = sprintf( $description_text, $label );
/* translators: %s expands to the name of a social network. */
WPSEO_Meta::$meta_fields['social'][ $network . '-image' ]['title'] = sprintf( __( '%s Image', 'wordpress-seo' ), $label );
WPSEO_Meta::$meta_fields['social'][ $network . '-image' ]['description'] = sprintf( $image_text, $label ) . ' ' . sprintf( $image_size_text, $label, $recommended_image_sizes[ $network ] );
}
}
}
/**
* Returns the metabox section for the social settings.
*
* @return WPSEO_Metabox_Tab_Section
*/
public function get_meta_section() {
$tabs = array();
$social_meta_fields = WPSEO_Meta::get_meta_field_defs( 'social' );
$single = true;
$opengraph = WPSEO_Options::get( 'opengraph' );
$twitter = WPSEO_Options::get( 'twitter' );
if ( $opengraph === true && $twitter === true ) {
$single = null;
}
wp_nonce_field( 'yoast_free_metabox_social', 'yoast_free_metabox_social_nonce' );
if ( $opengraph === true ) {
$tabs[] = new WPSEO_Metabox_Form_Tab(
'facebook',
$this->get_social_tab_content( 'opengraph', $social_meta_fields ),
'<span class="screen-reader-text">' . __( 'Facebook / Open Graph metadata', 'wordpress-seo' ) . '</span><span class="dashicons dashicons-facebook-alt"></span>',
array(
'link_aria_label' => __( 'Facebook / Open Graph metadata', 'wordpress-seo' ),
'link_class' => 'yoast-tooltip yoast-tooltip-se',
'single' => $single,
)
);
}
if ( $twitter === true ) {
$tabs[] = new WPSEO_Metabox_Form_Tab(
'twitter',
$this->get_social_tab_content( 'twitter', $social_meta_fields ),
'<span class="screen-reader-text">' . __( 'Twitter metadata', 'wordpress-seo' ) . '</span><span class="dashicons dashicons-twitter"></span>',
array(
'link_aria_label' => __( 'Twitter metadata', 'wordpress-seo' ),
'link_class' => 'yoast-tooltip yoast-tooltip-se',
'single' => $single,
)
);
}
return new WPSEO_Metabox_Tab_Section(
'social',
'<span class="screen-reader-text">' . __( 'Social', 'wordpress-seo' ) . '</span><span class="dashicons dashicons-share"></span>',
$tabs,
array(
'link_aria_label' => __( 'Social', 'wordpress-seo' ),
'link_class' => 'yoast-tooltip yoast-tooltip-e',
)
);
}
/**
* Generates the html for a social settings tab for one of the supported social media.
*
* @param string $medium Medium. Can be 'opengraph' or 'twitter'.
* @param array $meta_field_defs The social meta field definitions.
*
* @return string
*/
private function get_social_tab_content( $medium, $meta_field_defs ) {
$field_names = array(
$medium . '-title',
$medium . '-description',
$medium . '-image',
$medium . '-image-id',
);
$tab_content = $this->get_premium_notice( $medium );
foreach ( $field_names as $field_name ) {
$tab_content .= $this->do_meta_box( $meta_field_defs[ $field_name ], $field_name );
}
return $tab_content;
}
/**
* Returns the Upgrade to Premium notice.
*
* @param string $network The social network.
*
* @return string The notice HTML on the free version, empty string on premium.
*/
public function get_premium_notice( $network ) {
$features = new WPSEO_Features();
if ( $features->is_premium() ) {
return '';
}
$network_name = __( 'Facebook', 'wordpress-seo' );
if ( 'twitter' === $network ) {
$network_name = __( 'Twitter', 'wordpress-seo' );
}
return sprintf(
'<div class="notice inline yoast-notice yoast-notice-go-premium">
<p>%1$s</p>
<p><a href="%2$s" target="_blank">%3$s</a></p>
</div>',
sprintf(
/* translators: %1$s expands to the social network's name, %2$s to Yoast SEO Premium. */
esc_html__( 'Do you want to preview what it will look like if people share this post on %1$s? You can, with %2$s.', 'wordpress-seo' ),
esc_html( $network_name ),
'<strong>Yoast SEO Premium</strong>'
),
esc_url( WPSEO_Shortlinker::get( 'https://yoa.st/179' ) ),
sprintf(
/* translators: %s expands to Yoast SEO Premium. */
esc_html__( 'Find out why you should upgrade to %s', 'wordpress-seo' ),
'Yoast SEO Premium'
)
);
}
/**
* Filter over the meta boxes to save, this function adds the Social meta boxes.
*
* @param array $field_defs Array of metaboxes to save.
*
* @return array
*/
public function save_meta_boxes( $field_defs ) {
if ( ! isset( $_POST['yoast_free_metabox_social_nonce'] ) || ! wp_verify_nonce( $_POST['yoast_free_metabox_social_nonce'], 'yoast_free_metabox_social' ) ) {
return $field_defs;
}
return array_merge( $field_defs, WPSEO_Meta::get_meta_field_defs( 'social' ) );
}
/**
* This method will compare opengraph fields with the posted values.
*
* When fields are changed, the facebook cache will be purged.
*
* @param WP_Post $post Post instance.
*/
public function og_data_compare( $post ) {
if ( empty( $_POST ) ) {
return;
}
if ( empty( $post->ID ) || $post->post_status !== 'publish' ) {
return;
}
if ( ! isset( $_POST['yoast_free_metabox_social_nonce'] ) || ! wp_verify_nonce( $_POST['yoast_free_metabox_social_nonce'], 'yoast_free_metabox_social' ) ) {
return;
}
if ( ! isset( $_POST['original_post_status'] ) || $_POST['original_post_status'] !== 'publish' ) {
return;
}
$fields_to_compare = array(
'opengraph-title',
'opengraph-description',
'opengraph-image',
);
$reset_facebook_cache = false;
foreach ( $fields_to_compare as $field_to_compare ) {
$old_value = WPSEO_Meta::get_value( $field_to_compare, $post->ID );
$new_value = '';
$post_key = WPSEO_Meta::$form_prefix . $field_to_compare;
if ( isset( $_POST[ $post_key ] ) ) {
$new_value = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) );
}
if ( $old_value !== $new_value ) {
$reset_facebook_cache = true;
break;
}
}
unset( $old_value, $new_value );
if ( $reset_facebook_cache ) {
wp_remote_get(
'https://graph.facebook.com/?id=' . get_permalink( $post->ID ) . '&scrape=true&method=post'
);
}
}
} /* End of class */

View File

@ -0,0 +1,159 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Suggested_Plugins
*/
/**
* Class WPSEO_Suggested_Plugins
*/
class WPSEO_Suggested_Plugins implements WPSEO_WordPress_Integration {
/**
* @var WPSEO_Plugin_Availability
*/
protected $availability_checker;
/**
* @var Yoast_Notification_Center
*/
protected $notification_center;
/**
* WPSEO_Suggested_Plugins constructor.
*
* @param WPSEO_Plugin_Availability $availability_checker The availability checker to use.
* @param Yoast_Notification_Center $notification_center The notification center to add notifications to.
*/
public function __construct( WPSEO_Plugin_Availability $availability_checker, Yoast_Notification_Center $notification_center ) {
$this->availability_checker = $availability_checker;
$this->notification_center = $notification_center;
}
/**
* Registers all hooks to WordPress.
*
* @return void
*/
public function register_hooks() {
add_action( 'admin_init', array( $this->availability_checker, 'register' ) );
add_action( 'admin_init', array( $this, 'add_notifications' ) );
}
/**
* Adds notifications (when necessary).
*
* @return void
*/
public function add_notifications() {
$checker = $this->availability_checker;
// Get all Yoast plugins that have dependencies.
$plugins = $checker->get_plugins_with_dependencies();
foreach ( $plugins as $plugin_name => $plugin ) {
if ( ! $checker->dependencies_are_satisfied( $plugin ) ) {
continue;
}
$dependency_names = $checker->get_dependency_names( $plugin );
$notification = $this->get_yoast_seo_suggested_plugins_notification( $plugin_name, $plugin, $dependency_names[0] );
if ( ! WPSEO_Utils::is_yoast_seo_premium() && ( ! $checker->is_installed( $plugin ) || ! $checker->is_active( $plugin['slug'] ) ) ) {
$this->notification_center->add_notification( $notification );
continue;
}
$this->notification_center->remove_notification( $notification );
}
}
/**
* Build Yoast SEO suggested plugins notification.
*
* @param string $name The plugin name to use for the unique ID.
* @param array $plugin The plugin to retrieve the data from.
* @param string $dependency_name The name of the dependency.
*
* @return Yoast_Notification The notification containing the suggested plugin.
*/
protected function get_yoast_seo_suggested_plugins_notification( $name, $plugin, $dependency_name ) {
$message = $this->create_install_suggested_plugin_message( $plugin, $dependency_name );
if ( $this->availability_checker->is_installed( $plugin ) && ! $this->availability_checker->is_active( $plugin['slug'] ) ) {
$message = $this->create_activate_suggested_plugin_message( $plugin, $dependency_name );
}
return new Yoast_Notification(
$message,
array(
'id' => 'wpseo-suggested-plugin-' . $name,
'type' => Yoast_Notification::WARNING,
'capabilities' => array( 'install_plugins' ),
)
);
}
/**
* Creates a message to suggest the installation of a particular plugin.
*
* @param array $suggested_plugin The suggested plugin.
* @param array $third_party_plugin The third party plugin that we have a suggested plugin for.
*
* @return string The install suggested plugin message.
*/
protected function create_install_suggested_plugin_message( $suggested_plugin, $third_party_plugin ) {
/* translators: %1$s expands to Yoast SEO, %2$s expands to the dependency name, %3$s expands to the install link, %4$s expands to the more info link. */
$message = __( '%1$s and %2$s can work together a lot better by adding a helper plugin. Please install %3$s to make your life better. %4$s.', 'wordpress-seo' );
$install_link = WPSEO_Admin_Utils::get_install_link( $suggested_plugin );
return sprintf(
$message,
'Yoast SEO',
$third_party_plugin,
$install_link,
$this->create_more_information_link( $suggested_plugin['url'], $suggested_plugin['title'] )
);
}
/**
* Creates a more information link that directs the user to WordPress.org Plugin repository.
*
* @param string $url The URL to the plugin's page.
* @param string $name The name of the plugin.
*
* @return string The more information link.
*/
protected function create_more_information_link( $url, $name ) {
return sprintf(
'<a href="%s" aria-label="%s" target="_blank" rel="noopener noreferrer">%s</a>',
$url,
/* translators: %1$s expands to the dependency name. */
sprintf( __( 'More information about %1$s', 'wordpress-seo' ), $name ),
__( 'More information', 'wordpress-seo' )
);
}
/**
* Creates a message to suggest the activation of a particular plugin.
*
* @param array $suggested_plugin The suggested plugin.
* @param array $third_party_plugin The third party plugin that we have a suggested plugin for.
*
* @return string The activate suggested plugin message.
*/
protected function create_activate_suggested_plugin_message( $suggested_plugin, $third_party_plugin ) {
/* translators: %1$s expands to Yoast SEO, %2$s expands to the dependency name, %3$s expands to activation link. */
$message = __( '%1$s and %2$s can work together a lot better by adding a helper plugin. Please activate %3$s to make your life better.', 'wordpress-seo' );
$activation_url = WPSEO_Admin_Utils::get_activation_url( $suggested_plugin['slug'] );
return sprintf(
$message,
'Yoast SEO',
$third_party_plugin,
sprintf( '<a href="%s">%s</a>', $activation_url, $suggested_plugin['title'] )
);
}
}

View File

@ -0,0 +1,296 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Notifications
*/
/**
* Class Yoast_Alerts.
*/
class Yoast_Alerts {
/**
* @var string
*/
const ADMIN_PAGE = 'wpseo_dashboard';
/**
* Total notifications count.
*
* @var int
*/
private static $notification_count = 0;
/**
* All error notifications.
*
* @var array
*/
private static $errors = array();
/**
* Active errors.
*
* @var array
*/
private static $active_errors = array();
/**
* Dismissed errors.
*
* @var array
*/
private static $dismissed_errors = array();
/**
* All warning notifications.
*
* @var array
*/
private static $warnings = array();
/**
* Active warnings.
*
* @var array
*/
private static $active_warnings = array();
/**
* Dismissed warnings.
*
* @var array
*/
private static $dismissed_warnings = array();
/**
* Yoast_Alerts constructor.
*/
public function __construct() {
$this->add_hooks();
}
/**
* Add hooks
*/
private function add_hooks() {
$page = filter_input( INPUT_GET, 'page' );
if ( self::ADMIN_PAGE === $page ) {
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
}
// Needed for adminbar and Alerts page.
add_action( 'admin_init', array( __CLASS__, 'collect_alerts' ), 99 );
// Add AJAX hooks.
add_action( 'wp_ajax_yoast_dismiss_alert', array( $this, 'ajax_dismiss_alert' ) );
add_action( 'wp_ajax_yoast_restore_alert', array( $this, 'ajax_restore_alert' ) );
}
/**
* Enqueue assets.
*/
public function enqueue_assets() {
$asset_manager = new WPSEO_Admin_Asset_Manager();
$asset_manager->enqueue_style( 'alerts' );
}
/**
* Handle ajax request to dismiss an alert.
*/
public function ajax_dismiss_alert() {
$notification = $this->get_notification_from_ajax_request();
if ( $notification ) {
$notification_center = Yoast_Notification_Center::get();
$notification_center->maybe_dismiss_notification( $notification );
$this->output_ajax_response( $notification->get_type() );
}
wp_die();
}
/**
* Handle ajax request to restore an alert.
*/
public function ajax_restore_alert() {
$notification = $this->get_notification_from_ajax_request();
if ( $notification ) {
$notification_center = Yoast_Notification_Center::get();
$notification_center->restore_notification( $notification );
$this->output_ajax_response( $notification->get_type() );
}
wp_die();
}
/**
* Create AJAX response data.
*
* @param string $type Alert type.
*/
private function output_ajax_response( $type ) {
$html = $this->get_view_html( $type );
echo WPSEO_Utils::format_json_encode(
array(
'html' => $html,
'total' => self::get_active_alert_count(),
)
);
}
/**
* Get the HTML to return in the AJAX request.
*
* @param string $type Alert type.
*
* @return bool|string
*/
private function get_view_html( $type ) {
switch ( $type ) {
case 'error':
$view = 'errors';
break;
case 'warning':
default:
$view = 'warnings';
break;
}
// Re-collect alerts.
self::collect_alerts();
/** @noinspection PhpUnusedLocalVariableInspection */
$alerts_data = self::get_template_variables();
ob_start();
include WPSEO_PATH . 'admin/views/partial-alerts-' . $view . '.php';
$html = ob_get_clean();
return $html;
}
/**
* Extract the Yoast Notification from the AJAX request.
*
* @return null|Yoast_Notification
*/
private function get_notification_from_ajax_request() {
$notification_center = Yoast_Notification_Center::get();
$notification_id = filter_input( INPUT_POST, 'notification' );
return $notification_center->get_notification_by_id( $notification_id );
}
/**
* Show the alerts overview page.
*/
public static function show_overview_page() {
/** @noinspection PhpUnusedLocalVariableInspection */
$alerts_data = self::get_template_variables();
include WPSEO_PATH . 'admin/views/alerts-dashboard.php';
}
/**
* Collect the alerts and group them together.
*/
public static function collect_alerts() {
$notification_center = Yoast_Notification_Center::get();
$notifications = $notification_center->get_sorted_notifications();
self::$notification_count = count( $notifications );
self::$errors = array_filter( $notifications, array( __CLASS__, 'filter_error_alerts' ) );
self::$dismissed_errors = array_filter( self::$errors, array( __CLASS__, 'filter_dismissed_alerts' ) );
self::$active_errors = array_diff( self::$errors, self::$dismissed_errors );
self::$warnings = array_filter( $notifications, array( __CLASS__, 'filter_warning_alerts' ) );
self::$dismissed_warnings = array_filter( self::$warnings, array( __CLASS__, 'filter_dismissed_alerts' ) );
self::$active_warnings = array_diff( self::$warnings, self::$dismissed_warnings );
}
/**
* Get the variables needed in the views.
*
* @return array
*/
public static function get_template_variables() {
return array(
'metrics' => array(
'total' => self::$notification_count,
'active' => self::get_active_alert_count(),
'errors' => count( self::$errors ),
'warnings' => count( self::$warnings ),
),
'errors' => array(
'dismissed' => self::$dismissed_errors,
'active' => self::$active_errors,
),
'warnings' => array(
'dismissed' => self::$dismissed_warnings,
'active' => self::$active_warnings,
),
);
}
/**
* Get the number of active alerts.
*
* @return int
*/
public static function get_active_alert_count() {
return ( count( self::$active_errors ) + count( self::$active_warnings ) );
}
/**
* Filter out any non-errors.
*
* @param Yoast_Notification $notification Notification to test.
*
* @return bool
*/
private static function filter_error_alerts( Yoast_Notification $notification ) {
return $notification->get_type() === 'error';
}
/**
* Filter out any non-warnings.
*
* @param Yoast_Notification $notification Notification to test.
*
* @return bool
*/
private static function filter_warning_alerts( Yoast_Notification $notification ) {
return $notification->get_type() !== 'error';
}
/**
* Filter out any dismissed notifications.
*
* @param Yoast_Notification $notification Notification to test.
*
* @return bool
*/
private static function filter_dismissed_alerts( Yoast_Notification $notification ) {
return Yoast_Notification_Center::is_notification_dismissed( $notification );
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Represents the yoast columns.
*/
class WPSEO_Yoast_Columns implements WPSEO_WordPress_Integration {
/**
* Registers all hooks to WordPress.
*/
public function register_hooks() {
add_action( 'load-edit.php', array( $this, 'add_help_tab' ) );
}
/**
* Adds the help tab to the help center for current screen.
*/
public function add_help_tab() {
$screen = get_current_screen();
$screen->add_help_tab(
array(
/* translators: %s expands to Yoast */
'title' => sprintf( __( '%s Columns', 'wordpress-seo' ), 'Yoast' ),
'id' => 'yst-columns',
'content' => sprintf(
/* translators: %1$s: Yoast SEO, %2$s: Link to article about content analysis, %3$s: Anchor closing, %4$s: Link to article about text links, %5$s: Emphasis open tag, %6$s: Emphasis close tag */
'<p>' . __( '%1$s adds several columns to this page. We\'ve written an article about %2$show to use the SEO score and Readability score%3$s. The links columns show the number of articles on this site linking %5$sto%6$s this article and the number of URLs linked %5$sfrom%6$s this article. Learn more about %4$show to use these features to improve your internal linking%3$s, which greatly enhances your SEO.', 'wordpress-seo' ) . '</p>',
'Yoast SEO',
'<a href="' . WPSEO_Shortlinker::get( 'https://yoa.st/16p' ) . '">',
'</a>',
'<a href="' . WPSEO_Shortlinker::get( 'https://yoa.st/16q' ) . '">',
'<em>',
'</em>'
),
'priority' => 15,
)
);
}
}

View File

@ -0,0 +1,155 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Class to change or add WordPress dashboard widgets.
*/
class Yoast_Dashboard_Widget implements WPSEO_WordPress_Integration {
/**
* @var string
*/
const CACHE_TRANSIENT_KEY = 'wpseo-dashboard-totals';
/**
* @var WPSEO_Admin_Asset_Manager
*/
protected $asset_manager;
/**
* @var WPSEO_Statistics
*/
protected $statistics;
/**
* Yoast_Dashboard_Widget constructor.
*
* @param WPSEO_Statistics|null $statistics WPSEO_Statistics instance.
*/
public function __construct( WPSEO_Statistics $statistics = null ) {
if ( $statistics === null ) {
$statistics = new WPSEO_Statistics();
}
$this->statistics = $statistics;
$this->asset_manager = new WPSEO_Admin_Asset_Manager();
}
/**
* Register WordPress hooks.
*/
public function register_hooks() {
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_dashboard_assets' ) );
add_action( 'admin_init', array( $this, 'queue_dashboard_widget' ) );
}
/**
* Adds the dashboard widget if it should be shown.
*
* @return void
*/
public function queue_dashboard_widget() {
if ( $this->show_widget() ) {
add_action( 'wp_dashboard_setup', array( $this, 'add_dashboard_widget' ) );
}
}
/**
* Adds dashboard widget to WordPress.
*/
public function add_dashboard_widget() {
add_filter( 'postbox_classes_dashboard_wpseo-dashboard-overview', array( $this, 'wpseo_dashboard_overview_class' ) );
wp_add_dashboard_widget(
'wpseo-dashboard-overview',
/* translators: %s is the plugin name */
sprintf( __( '%s Posts Overview', 'wordpress-seo' ), 'Yoast SEO' ),
array( $this, 'display_dashboard_widget' )
);
}
/**
* Adds CSS classes to the dashboard widget.
*
* @param array $classes An array of postbox CSS classes.
*
* @return array
*/
public function wpseo_dashboard_overview_class( $classes ) {
$classes[] = 'yoast wpseo-dashboard-overview';
return $classes;
}
/**
* Displays the dashboard widget.
*/
public function display_dashboard_widget() {
echo '<div id="yoast-seo-dashboard-widget"></div>';
}
/**
* Enqueues assets for the dashboard if the current page is the dashboard.
*/
public function enqueue_dashboard_assets() {
if ( ! $this->is_dashboard_screen() ) {
return;
}
wp_localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'dashboard-widget', 'wpseoDashboardWidgetL10n', $this->localize_dashboard_script() );
$yoast_components_l10n = new WPSEO_Admin_Asset_Yoast_Components_L10n();
$yoast_components_l10n->localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'dashboard-widget' );
$this->asset_manager->enqueue_script( 'dashboard-widget' );
$this->asset_manager->enqueue_style( 'wp-dashboard' );
}
/**
* Translates strings used in the dashboard widget.
*
* @return array The translated strings.
*/
public function localize_dashboard_script() {
return array(
'feed_header' => sprintf(
/* translators: %1$s resolves to Yoast.com */
__( 'Latest blog posts on %1$s', 'wordpress-seo' ),
'Yoast.com'
),
'feed_footer' => __( 'Read more like this on our SEO blog', 'wordpress-seo' ),
'ryte_header' => sprintf(
/* translators: %1$s expands to Ryte. */
__( 'Indexability check by %1$s', 'wordpress-seo' ),
'Ryte'
),
'ryteEnabled' => ( WPSEO_Options::get( 'onpage_indexability' ) === true ),
'ryte_fetch' => __( 'Fetch the current status', 'wordpress-seo' ),
'ryte_analyze' => __( 'Analyze entire site', 'wordpress-seo' ),
'ryte_fetch_url' => esc_attr( add_query_arg( 'wpseo-redo-onpage', '1' ) ) . '#wpseo-dashboard-overview',
'ryte_landing_url' => WPSEO_Shortlinker::get( 'https://yoa.st/rytelp' ),
);
}
/**
* Checks if the current screen is the dashboard screen.
*
* @return bool Whether or not this is the dashboard screen.
*/
private function is_dashboard_screen() {
$current_screen = get_current_screen();
return ( $current_screen instanceof WP_Screen && $current_screen->id === 'dashboard' );
}
/**
* Returns true when the dashboard widget should be shown.
*
* @return bool
*/
private function show_widget() {
$analysis_seo = new WPSEO_Metabox_Analysis_SEO();
return $analysis_seo->is_enabled() && current_user_can( 'edit_posts' );
}
}

View File

@ -0,0 +1,788 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* Admin form class.
*
* @since 2.0
*/
class Yoast_Form {
/**
* Instance of this class
*
* @var object
* @since 2.0
*/
public static $instance;
/**
* @var string
* @since 2.0
*/
public $option_name;
/**
* @var array
* @since 2.0
*/
public $options = array();
/**
* Option instance.
*
* @since 8.4
* @var WPSEO_Option|null
*/
protected $option_instance = null;
/**
* Get the singleton instance of this class.
*
* @since 2.0
*
* @return Yoast_Form
*/
public static function get_instance() {
if ( ! ( self::$instance instanceof self ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Generates the header for admin pages.
*
* @since 2.0
*
* @param bool $form Whether or not the form start tag should be included.
* @param string $option The short name of the option to use for the current page.
* @param bool $contains_files Whether the form should allow for file uploads.
* @param bool $option_long_name Group name of the option.
*/
public function admin_header( $form = true, $option = 'wpseo', $contains_files = false, $option_long_name = false ) {
if ( ! $option_long_name ) {
$option_long_name = WPSEO_Options::get_group_name( $option );
}
?>
<div class="wrap yoast wpseo-admin-page <?php echo esc_attr( 'page-' . $option ); ?>">
<?php
/**
* Display the updated/error messages.
* Only needed as our settings page is not under options, otherwise it will automatically be included.
*
* @see settings_errors()
*/
require_once ABSPATH . 'wp-admin/options-head.php';
?>
<h1 id="wpseo-title"><?php echo esc_html( get_admin_page_title() ); ?></h1>
<div class="wpseo_content_wrapper">
<div class="wpseo_content_cell" id="wpseo_content_top">
<?php
if ( $form === true ) {
$enctype = ( $contains_files ) ? ' enctype="multipart/form-data"' : '';
$network_admin = new Yoast_Network_Admin();
if ( $network_admin->meets_requirements() ) {
$action_url = network_admin_url( 'settings.php' );
$hidden_fields_cb = array( $network_admin, 'settings_fields' );
}
else {
$action_url = admin_url( 'options.php' );
$hidden_fields_cb = 'settings_fields';
}
echo '<form action="' . esc_url( $action_url ) . '" method="post" id="wpseo-conf"' . $enctype . ' accept-charset="' . esc_attr( get_bloginfo( 'charset' ) ) . '">';
call_user_func( $hidden_fields_cb, $option_long_name );
}
$this->set_option( $option );
}
/**
* Set the option used in output for form elements.
*
* @since 2.0
*
* @param string $option_name Option key.
*/
public function set_option( $option_name ) {
$this->option_name = $option_name;
$this->options = WPSEO_Options::get_option( $option_name );
if ( $this->options === null ) {
$this->options = (array) get_option( $option_name, array() );
}
$this->option_instance = WPSEO_Options::get_option_instance( $option_name );
if ( ! $this->option_instance ) {
$this->option_instance = null;
}
}
/**
* Sets a value in the options.
*
* @since 5.4
*
* @param string $key The key of the option to set.
* @param mixed $value The value to set the option to.
* @param bool $overwrite Whether to overwrite existing options. Default is false.
*/
public function set_options_value( $key, $value, $overwrite = false ) {
if ( $overwrite || ! array_key_exists( $key, $this->options ) ) {
$this->options[ $key ] = $value;
}
}
/**
* Generates the footer for admin pages.
*
* @since 2.0
*
* @param bool $submit Whether or not a submit button and form end tag should be shown.
* @param bool $show_sidebar Whether or not to show the banner sidebar - used by premium plugins to disable it.
*/
public function admin_footer( $submit = true, $show_sidebar = true ) {
if ( $submit ) {
submit_button( __( 'Save changes', 'wordpress-seo' ) );
echo '
</form>';
}
/**
* Apply general admin_footer hooks.
*/
do_action( 'wpseo_admin_footer', $this );
/**
* Run possibly set actions to add for example an i18n box.
*/
do_action( 'wpseo_admin_promo_footer' );
echo '
</div><!-- end of div wpseo_content_top -->';
if ( $show_sidebar ) {
$this->admin_sidebar();
}
echo '</div><!-- end of div wpseo_content_wrapper -->';
do_action( 'wpseo_admin_below_content', $this );
echo '
</div><!-- end of wrap -->';
}
/**
* Generates the sidebar for admin pages.
*
* @since 2.0
*/
public function admin_sidebar() {
// No banners in Premium.
$addon_manager = new WPSEO_Addon_Manager();
if ( WPSEO_Utils::is_yoast_seo_premium() && $addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ) ) {
return;
}
require_once 'views/sidebar.php';
}
/**
* Output a label element.
*
* @since 2.0
*
* @param string $text Label text string.
* @param array $attr HTML attributes set.
*/
public function label( $text, $attr ) {
$defaults = array(
'class' => 'checkbox',
'close' => true,
'for' => '',
'aria_label' => '',
);
$attr = wp_parse_args( $attr, $defaults );
$aria_label = '';
if ( $attr['aria_label'] !== '' ) {
$aria_label = ' aria-label="' . esc_attr( $attr['aria_label'] ) . '"';
}
echo "<label class='" . esc_attr( $attr['class'] ) . "' for='" . esc_attr( $attr['for'] ) . "'$aria_label>$text";
if ( $attr['close'] ) {
echo '</label>';
}
}
/**
* Output a legend element.
*
* @since 3.4
*
* @param string $text Legend text string.
* @param array $attr HTML attributes set.
*/
public function legend( $text, $attr ) {
$defaults = array(
'id' => '',
'class' => '',
);
$attr = wp_parse_args( $attr, $defaults );
$id = ( '' === $attr['id'] ) ? '' : ' id="' . esc_attr( $attr['id'] ) . '"';
echo '<legend class="yoast-form-legend ' . esc_attr( $attr['class'] ) . '"' . $id . '>' . $text . '</legend>';
}
/**
* Create a Checkbox input field.
*
* @since 2.0
*
* @param string $var The variable within the option to create the checkbox for.
* @param string $label The label to show for the variable.
* @param bool $label_left Whether the label should be left (true) or right (false).
*/
public function checkbox( $var, $label, $label_left = false ) {
if ( ! isset( $this->options[ $var ] ) ) {
$this->options[ $var ] = false;
}
if ( $this->options[ $var ] === true ) {
$this->options[ $var ] = 'on';
}
$class = '';
if ( $label_left !== false ) {
if ( ! empty( $label_left ) ) {
$label_left .= ':';
}
$this->label( $label_left, array( 'for' => $var ) );
}
else {
$class = 'double';
}
echo '<input class="checkbox ', esc_attr( $class ), '" type="checkbox" id="', esc_attr( $var ), '" name="', esc_attr( $this->option_name ), '[', esc_attr( $var ), ']" value="on"', checked( $this->options[ $var ], 'on', false ), disabled( $this->is_control_disabled( $var ), true, false ), '/>';
if ( ! empty( $label ) ) {
$this->label( $label, array( 'for' => $var ) );
}
echo '<br class="clear" />';
}
/**
* Create a light switch input field using a single checkbox.
*
* @since 3.1
*
* @param string $var The variable within the option to create the checkbox for.
* @param string $label The label element text for the checkbox.
* @param array $buttons Array of two visual labels for the buttons (defaults Disabled/Enabled).
* @param bool $reverse Reverse order of buttons (default true).
* @param string $help Inline Help that will be printed out before the visible toggles text.
*/
public function light_switch( $var, $label, $buttons = array(), $reverse = true, $help = '' ) {
if ( ! isset( $this->options[ $var ] ) ) {
$this->options[ $var ] = false;
}
if ( $this->options[ $var ] === true ) {
$this->options[ $var ] = 'on';
}
$class = 'switch-light switch-candy switch-yoast-seo';
if ( $reverse ) {
$class .= ' switch-yoast-seo-reverse';
}
if ( empty( $buttons ) ) {
$buttons = array( __( 'Disabled', 'wordpress-seo' ), __( 'Enabled', 'wordpress-seo' ) );
}
list( $off_button, $on_button ) = $buttons;
$help_class = '';
$screen_reader_text_class = '';
$help_class = ! empty( $help ) ? ' switch-container__has-help' : '';
echo '<div class="switch-container', $help_class, '">',
'<span class="switch-light-visual-label" id="', esc_attr( $var . '-label' ), '">', esc_html( $label ), '</span>' . $help,
'<label class="', $class, '"><b class="switch-yoast-seo-jaws-a11y">&nbsp;</b>',
'<input type="checkbox" aria-labelledby="', esc_attr( $var . '-label' ), '" id="', esc_attr( $var ), '" name="', esc_attr( $this->option_name ), '[', esc_attr( $var ), ']" value="on"', checked( $this->options[ $var ], 'on', false ), disabled( $this->is_control_disabled( $var ), true, false ), '/>',
'<span aria-hidden="true">
<span>', esc_html( $off_button ) ,'</span>
<span>', esc_html( $on_button ) ,'</span>
<a></a>
</span>
</label><div class="clear"></div></div>';
}
/**
* Create a Text input field.
*
* @since 2.0
* @since 2.1 Introduced the `$attr` parameter.
*
* @param string $var The variable within the option to create the text input field for.
* @param string $label The label to show for the variable.
* @param array|string $attr Extra attributes to add to the input field. Can be class, disabled, autocomplete.
*/
public function textinput( $var, $label, $attr = array() ) {
if ( ! is_array( $attr ) ) {
$attr = array(
'class' => $attr,
'disabled' => false,
);
}
$defaults = array(
'placeholder' => '',
'class' => '',
);
$attr = wp_parse_args( $attr, $defaults );
$val = isset( $this->options[ $var ] ) ? $this->options[ $var ] : '';
$attributes = isset( $attr['autocomplete'] ) ? ' autocomplete="' . esc_attr( $attr['autocomplete'] ) . '"' : '';
if ( isset( $attr['disabled'] ) && $attr['disabled'] ) {
$attributes .= ' disabled';
}
$this->label(
$label . ':',
array(
'for' => $var,
'class' => 'textinput',
)
);
echo '<input' . $attributes . ' class="textinput ' . esc_attr( $attr['class'] ) . ' " placeholder="' . esc_attr( $attr['placeholder'] ) . '" type="text" id="', esc_attr( $var ), '" name="', esc_attr( $this->option_name ), '[', esc_attr( $var ), ']" value="', esc_attr( $val ), '"', disabled( $this->is_control_disabled( $var ), true, false ), '/>', '<br class="clear" />';
}
/**
* Create a textarea.
*
* @since 2.0
*
* @param string $var The variable within the option to create the textarea for.
* @param string $label The label to show for the variable.
* @param string|array $attr The CSS class or an array of attributes to assign to the textarea.
*/
public function textarea( $var, $label, $attr = array() ) {
if ( ! is_array( $attr ) ) {
$attr = array(
'class' => $attr,
);
}
$defaults = array(
'cols' => '',
'rows' => '',
'class' => '',
);
$attr = wp_parse_args( $attr, $defaults );
$val = ( isset( $this->options[ $var ] ) ) ? $this->options[ $var ] : '';
$this->label(
$label . ':',
array(
'for' => $var,
'class' => 'textinput',
)
);
echo '<textarea cols="' . esc_attr( $attr['cols'] ) . '" rows="' . esc_attr( $attr['rows'] ) . '" class="textinput ' . esc_attr( $attr['class'] ) . '" id="' . esc_attr( $var ) . '" name="' . esc_attr( $this->option_name ) . '[' . esc_attr( $var ) . ']"', disabled( $this->is_control_disabled( $var ), true, false ), '>' . esc_textarea( $val ) . '</textarea><br class="clear" />';
}
/**
* Create a hidden input field.
*
* @since 2.0
*
* @param string $var The variable within the option to create the hidden input for.
* @param string $id The ID of the element.
*/
public function hidden( $var, $id = '' ) {
$val = ( isset( $this->options[ $var ] ) ) ? $this->options[ $var ] : '';
if ( is_bool( $val ) ) {
$val = ( $val === true ) ? 'true' : 'false';
}
if ( '' === $id ) {
$id = 'hidden_' . $var;
}
echo '<input type="hidden" id="' . esc_attr( $id ) . '" name="' . esc_attr( $this->option_name ) . '[' . esc_attr( $var ) . ']" value="' . esc_attr( $val ) . '"/>';
}
/**
* Create a Select Box.
*
* @since 2.0
*
* @param string $var The variable within the option to create the select for.
* @param string $label The label to show for the variable.
* @param array $select_options The select options to choose from.
* @param string $styled The select style. Use 'styled' to get a styled select. Default 'unstyled'.
* @param bool $show_label Whether or not to show the label, if not, it will be applied as an aria-label.
*/
public function select( $var, $label, array $select_options, $styled = 'unstyled', $show_label = true ) {
if ( empty( $select_options ) ) {
return;
}
if ( $show_label ) {
$this->label(
$label . ':',
array(
'for' => $var,
'class' => 'select',
)
);
}
$select_name = esc_attr( $this->option_name ) . '[' . esc_attr( $var ) . ']';
$active_option = ( isset( $this->options[ $var ] ) ) ? $this->options[ $var ] : '';
$wrapper_start_tag = '';
$wrapper_end_tag = '';
$select = new Yoast_Input_Select( $var, $select_name, $select_options, $active_option );
$select->add_attribute( 'class', 'select' );
if ( $this->is_control_disabled( $var ) ) {
$select->add_attribute( 'disabled', 'disabled' );
}
if ( ! $show_label ) {
$select->add_attribute( 'aria-label', $label );
}
if ( $styled === 'styled' ) {
$wrapper_start_tag = '<span class="yoast-styled-select">';
$wrapper_end_tag = '</span>';
}
echo $wrapper_start_tag;
$select->output_html();
echo $wrapper_end_tag;
echo '<br class="clear"/>';
}
/**
* Create a File upload field.
*
* @since 2.0
*
* @param string $var The variable within the option to create the file upload field for.
* @param string $label The label to show for the variable.
*/
public function file_upload( $var, $label ) {
$val = '';
if ( isset( $this->options[ $var ] ) && is_array( $this->options[ $var ] ) ) {
$val = $this->options[ $var ]['url'];
}
$var_esc = esc_attr( $var );
$this->label(
$label . ':',
array(
'for' => $var,
'class' => 'select',
)
);
echo '<input type="file" value="' . esc_attr( $val ) . '" class="textinput" name="' . esc_attr( $this->option_name ) . '[' . $var_esc . ']" id="' . $var_esc . '"', disabled( $this->is_control_disabled( $var ), true, false ), '/>';
// Need to save separate array items in hidden inputs, because empty file inputs type will be deleted by settings API.
if ( ! empty( $this->options[ $var ] ) ) {
$this->hidden( 'file', $this->option_name . '_file' );
$this->hidden( 'url', $this->option_name . '_url' );
$this->hidden( 'type', $this->option_name . '_type' );
}
echo '<br class="clear"/>';
}
/**
* Media input.
*
* @since 2.0
*
* @param string $var Option name.
* @param string $label Label message.
*/
public function media_input( $var, $label ) {
$val = '';
if ( isset( $this->options[ $var ] ) ) {
$val = $this->options[ $var ];
}
$id_value = '';
if ( isset( $this->options[ $var . '_id' ] ) ) {
$id_value = $this->options[ $var . '_id' ];
}
$var_esc = esc_attr( $var );
$this->label(
$label . ':',
array(
'for' => 'wpseo_' . $var,
'class' => 'select',
)
);
$id_field_id = 'wpseo_' . $var_esc . '_id';
echo '<span>';
echo '<input',
' class="textinput"',
' id="wpseo_', $var_esc, '"',
' type="text" size="36"',
' name="', esc_attr( $this->option_name ), '[', $var_esc, ']"',
' value="', esc_attr( $val ), '"',
' readonly="readonly"',
' /> ';
echo '<input',
' id="wpseo_', $var_esc, '_button"',
' class="wpseo_image_upload_button button"',
' type="button"',
' value="', esc_attr__( 'Upload Image', 'wordpress-seo' ), '"',
' data-target-id="', esc_attr( $id_field_id ), '"',
disabled( $this->is_control_disabled( $var ), true, false ),
' /> ';
echo '<input',
' class="wpseo_image_remove_button button"',
' type="button"',
' value="', esc_attr__( 'Clear Image', 'wordpress-seo' ), '"',
disabled( $this->is_control_disabled( $var ), true, false ),
' />';
echo '<input',
' type="hidden"',
' id="', esc_attr( $id_field_id ), '"',
' name="', esc_attr( $this->option_name ), '[', $var_esc, '_id]"',
' value="', esc_attr( $id_value ), '"',
' />';
echo '</span>';
echo '<br class="clear"/>';
}
/**
* Create a Radio input field.
*
* @since 2.0
*
* @param string $var The variable within the option to create the radio button for.
* @param array $values The radio options to choose from.
* @param string $legend Optional. The legend to show for the field set, if any.
* @param array $legend_attr Optional. The attributes for the legend, if any.
*/
public function radio( $var, $values, $legend = '', $legend_attr = array() ) {
if ( ! is_array( $values ) || $values === array() ) {
return;
}
if ( ! isset( $this->options[ $var ] ) ) {
$this->options[ $var ] = false;
}
$var_esc = esc_attr( $var );
echo '<fieldset class="yoast-form-fieldset wpseo_radio_block" id="' . $var_esc . '">';
if ( is_string( $legend ) && '' !== $legend ) {
$defaults = array(
'id' => '',
'class' => 'radiogroup',
);
$legend_attr = wp_parse_args( $legend_attr, $defaults );
$this->legend( $legend, $legend_attr );
}
foreach ( $values as $key => $value ) {
$label = $value;
$aria_label = '';
if ( is_array( $value ) ) {
$label = isset( $value['label'] ) ? $value['label'] : '';
$aria_label = isset( $value['aria_label'] ) ? $value['aria_label'] : '';
}
$key_esc = esc_attr( $key );
echo '<input type="radio" class="radio" id="' . $var_esc . '-' . $key_esc . '" name="' . esc_attr( $this->option_name ) . '[' . $var_esc . ']" value="' . $key_esc . '" ' . checked( $this->options[ $var ], $key_esc, false ) . disabled( $this->is_control_disabled( $var ), true, false ) . ' />';
$this->label(
$label,
array(
'for' => $var_esc . '-' . $key_esc,
'class' => 'radio',
'aria_label' => $aria_label,
)
);
}
echo '</fieldset>';
}
/**
* Create a toggle switch input field using two radio buttons.
*
* @since 3.1
*
* @param string $var The variable within the option to create the radio buttons for.
* @param array $values Associative array of on/off keys and their values to be used as
* the label elements text for the radio buttons. Optionally, each
* value can be an array of visible label text and screen reader text.
* @param string $label The visual label for the radio buttons group, used as the fieldset legend.
* @param string $help Inline Help that will be printed out before the visible toggles text.
*/
public function toggle_switch( $var, $values, $label, $help = '' ) {
if ( ! is_array( $values ) || $values === array() ) {
return;
}
if ( ! isset( $this->options[ $var ] ) ) {
$this->options[ $var ] = false;
}
if ( $this->options[ $var ] === true ) {
$this->options[ $var ] = 'on';
}
if ( $this->options[ $var ] === false ) {
$this->options[ $var ] = 'off';
}
$help_class = ! empty( $help ) ? ' switch-container__has-help' : '';
$var_esc = esc_attr( $var );
printf( '<div class="%s">', esc_attr( 'switch-container' . $help_class ) );
echo '<fieldset id="', $var_esc, '" class="fieldset-switch-toggle"><legend>', $label, '</legend>', $help;
echo $this->get_disabled_note( $var );
echo '<div class="switch-toggle switch-candy switch-yoast-seo">';
foreach ( $values as $key => $value ) {
$screen_reader_text_html = '';
if ( is_array( $value ) ) {
$screen_reader_text = $value['screen_reader_text'];
$screen_reader_text_html = '<span class="screen-reader-text"> ' . esc_html( $screen_reader_text ) . '</span>';
$value = $value['text'];
}
$key_esc = esc_attr( $key );
$for = $var_esc . '-' . $key_esc;
echo '<input type="radio" id="' . $for . '" name="' . esc_attr( $this->option_name ) . '[' . $var_esc . ']" value="' . $key_esc . '" ' . checked( $this->options[ $var ], $key_esc, false ) . disabled( $this->is_control_disabled( $var ), true, false ) . ' />',
'<label for="', $for, '">', esc_html( $value ), $screen_reader_text_html,'</label>';
}
echo '<a></a></div></fieldset><div class="clear"></div></div>' . PHP_EOL . PHP_EOL;
}
/**
* Creates a toggle switch to define whether an indexable should be indexed or not.
*
* @param string $var The variable within the option to create the radio buttons for.
* @param string $label The visual label for the radio buttons group, used as the fieldset legend.
* @param string $help Inline Help that will be printed out before the visible toggles text.
*
* @return void
*/
public function index_switch( $var, $label, $help = '' ) {
$index_switch_values = array(
'off' => __( 'Yes', 'wordpress-seo' ),
'on' => __( 'No', 'wordpress-seo' ),
);
$this->toggle_switch(
$var,
$index_switch_values,
sprintf(
/* translators: %s expands to an indexable object's name, like a post type or taxonomy */
esc_html__( 'Show %s in search results?', 'wordpress-seo' ),
'<strong>' . esc_html( $label ) . '</strong>'
),
$help
);
}
/**
* Creates a toggle switch to show hide certain options.
*
* @param string $var The variable within the option to create the radio buttons for.
* @param string $label The visual label for the radio buttons group, used as the fieldset legend.
* @param bool $inverse_keys Whether or not the option keys need to be inverted to support older functions.
* @param string $help Inline Help that will be printed out before the visible toggles text.
*
* @return void
*/
public function show_hide_switch( $var, $label, $inverse_keys = false, $help = '' ) {
$on_key = ( $inverse_keys ) ? 'off' : 'on';
$off_key = ( $inverse_keys ) ? 'on' : 'off';
$show_hide_switch = array(
$on_key => __( 'Show', 'wordpress-seo' ),
$off_key => __( 'Hide', 'wordpress-seo' ),
);
$this->toggle_switch( $var, $show_hide_switch, $label, $help );
}
/**
* Checks whether a given control should be disabled.
*
* @param string $var The variable within the option to check whether its control should be disabled.
*
* @return bool True if control should be disabled, false otherwise.
*/
protected function is_control_disabled( $var ) {
if ( $this->option_instance === null ) {
return false;
}
return $this->option_instance->is_disabled( $var );
}
/**
* Gets the explanation note to print if a given control is disabled.
*
* @param string $var The variable within the option to print a disabled note for.
*
* @return string Explanation note HTML string, or empty string if no note necessary.
*/
protected function get_disabled_note( $var ) {
if ( ! $this->is_control_disabled( $var ) ) {
return '';
}
return '<p class="disabled-note">' . esc_html__( 'This feature has been disabled by the network admin.', 'wordpress-seo' ) . '</p>';
}
/* ********************* DEPRECATED METHODS ********************* */
/**
* Retrieve options based on whether we're on multisite or not.
*
* @since 1.2.4
* @since 2.0 Moved to this class.
* @deprecated 8.4
* @codeCoverageIgnore
*
* @return array The option's value.
*/
public function get_option() {
_deprecated_function( __METHOD__, 'WPSEO 8.4' );
if ( is_network_admin() ) {
return get_site_option( $this->option_name );
}
return get_option( $this->option_name );
}
}

View File

@ -0,0 +1,321 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
*/
/**
* Multisite utility class for network admin functionality.
*/
class Yoast_Network_Admin implements WPSEO_WordPress_Integration, WPSEO_WordPress_AJAX_Integration {
/**
* Action identifier for updating plugin network options.
*
* @var string
*/
const UPDATE_OPTIONS_ACTION = 'yoast_handle_network_options';
/**
* Action identifier for restoring a site.
*
* @var string
*/
const RESTORE_SITE_ACTION = 'yoast_restore_site';
/**
* Gets the available sites as choices, e.g. for a dropdown.
*
* @param bool $include_empty Optional. Whether to include an initial placeholder choice.
* Default false.
* @param bool $show_title Optional. Whether to show the title for each site. This requires
* switching through the sites, so has performance implications for
* sites that do not use a persistent cache.
* Default false.
*
* @return array Choices as $site_id => $site_label pairs.
*/
public function get_site_choices( $include_empty = false, $show_title = false ) {
$choices = array();
if ( $include_empty ) {
$choices['-'] = __( 'None', 'wordpress-seo' );
}
$criteria = array(
'deleted' => 0,
'network_id' => get_current_network_id(),
);
$sites = get_sites( $criteria );
foreach ( $sites as $site ) {
$site_name = $site->domain . $site->path;
if ( $show_title ) {
$site_name = $site->blogname . ' (' . $site->domain . $site->path . ')';
}
$choices[ $site->blog_id ] = $site->blog_id . ': ' . $site_name;
$site_states = $this->get_site_states( $site );
if ( ! empty( $site_states ) ) {
$choices[ $site->blog_id ] .= ' [' . implode( ', ', $site_states ) . ']';
}
}
return $choices;
}
/**
* Gets the states of a site.
*
* @param WP_Site $site Site object.
*
* @return array Array of $state_slug => $state_label pairs.
*/
public function get_site_states( $site ) {
$available_states = array(
'public' => __( 'public', 'wordpress-seo' ),
'archived' => __( 'archived', 'wordpress-seo' ),
'mature' => __( 'mature', 'wordpress-seo' ),
'spam' => __( 'spam', 'wordpress-seo' ),
'deleted' => __( 'deleted', 'wordpress-seo' ),
);
$site_states = array();
foreach ( $available_states as $state_slug => $state_label ) {
if ( $site->$state_slug === '1' ) {
$site_states[ $state_slug ] = $state_label;
}
}
return $site_states;
}
/**
* Handles a request to update plugin network options.
*
* This method works similar to how option updates are handled in `wp-admin/options.php` and
* `wp-admin/network/settings.php`.
*
* @return void
*/
public function handle_update_options_request() {
$option_group = filter_input( INPUT_POST, 'network_option_group', FILTER_SANITIZE_STRING );
$this->verify_request( "{$option_group}-network-options" );
$whitelist_options = Yoast_Network_Settings_API::get()->get_whitelist_options( $option_group );
if ( empty( $whitelist_options ) ) {
add_settings_error( $option_group, 'settings_updated', __( 'You are not allowed to modify unregistered network settings.', 'wordpress-seo' ), 'error' );
$this->terminate_request();
return;
}
foreach ( $whitelist_options as $option_name ) {
$value = null;
if ( isset( $_POST[ $option_name ] ) ) { // WPCS: CSRF ok.
// Adding sanitize_text_field around this will break the saving of settings because it expects a string: https://github.com/Yoast/wordpress-seo/issues/12440.
$value = wp_unslash( $_POST[ $option_name ] ); // WPCS: CSRF ok.
}
WPSEO_Options::update_site_option( $option_name, $value );
}
$settings_errors = get_settings_errors();
if ( empty( $settings_errors ) ) {
add_settings_error( $option_group, 'settings_updated', __( 'Settings Updated.', 'wordpress-seo' ), 'updated' );
}
$this->terminate_request();
}
/**
* Handles a request to restore a site's default settings.
*
* @return void
*/
public function handle_restore_site_request() {
$this->verify_request( 'wpseo-network-restore', 'restore_site_nonce' );
$option_group = 'wpseo_ms';
$site_id = ! empty( $_POST[ $option_group ]['site_id'] ) ? (int) $_POST[ $option_group ]['site_id'] : 0; // WPCS: CSRF ok.
if ( ! $site_id ) {
add_settings_error( $option_group, 'settings_updated', __( 'No site has been selected to restore.', 'wordpress-seo' ), 'error' );
$this->terminate_request();
return;
}
$site = get_site( $site_id );
if ( ! $site ) {
/* translators: %s expands to the ID of a site within a multisite network. */
add_settings_error( $option_group, 'settings_updated', sprintf( __( 'Site with ID %d not found.', 'wordpress-seo' ), $site_id ), 'error' );
}
else {
WPSEO_Options::reset_ms_blog( $site_id );
/* translators: %s expands to the name of a site within a multisite network. */
add_settings_error( $option_group, 'settings_updated', sprintf( __( '%s restored to default SEO settings.', 'wordpress-seo' ), esc_html( $site->blogname ) ), 'updated' );
}
$this->terminate_request();
}
/**
* Outputs nonce, action and option group fields for a network settings page in the plugin.
*
* @param string $option_group Option group name for the current page.
*
* @return void
*/
public function settings_fields( $option_group ) {
?>
<input type="hidden" name="network_option_group" value="<?php echo esc_attr( $option_group ); ?>" />
<input type="hidden" name="action" value="<?php echo esc_attr( self::UPDATE_OPTIONS_ACTION ); ?>" />
<?php
wp_nonce_field( "$option_group-network-options" );
}
/**
* Enqueues network admin assets.
*
* @return void
*/
public function enqueue_assets() {
$asset_manager = new WPSEO_Admin_Asset_Manager();
$asset_manager->enqueue_script( 'network-admin-script' );
$translations = array(
/* translators: %s: success message */
'success_prefix' => __( 'Success: %s', 'wordpress-seo' ),
/* translators: %s: error message */
'error_prefix' => __( 'Error: %s', 'wordpress-seo' ),
);
wp_localize_script(
WPSEO_Admin_Asset_Manager::PREFIX . 'network-admin-script',
'wpseoNetworkAdminGlobalL10n',
$translations
);
}
/**
* Hooks in the necessary actions and filters.
*
* @return void
*/
public function register_hooks() {
if ( ! $this->meets_requirements() ) {
return;
}
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'admin_action_' . self::UPDATE_OPTIONS_ACTION, array( $this, 'handle_update_options_request' ) );
add_action( 'admin_action_' . self::RESTORE_SITE_ACTION, array( $this, 'handle_restore_site_request' ) );
}
/**
* Hooks in the necessary AJAX actions.
*
* @return void
*/
public function register_ajax_hooks() {
add_action( 'wp_ajax_' . self::UPDATE_OPTIONS_ACTION, array( $this, 'handle_update_options_request' ) );
add_action( 'wp_ajax_' . self::RESTORE_SITE_ACTION, array( $this, 'handle_restore_site_request' ) );
}
/**
* Checks whether the requirements to use this class are met.
*
* @return bool True if requirements are met, false otherwise.
*/
public function meets_requirements() {
return is_multisite() && is_network_admin();
}
/**
* Verifies that the current request is valid.
*
* @param string $action Nonce action.
* @param string $query_arg Optional. Nonce query argument. Default '_wpnonce'.
*
* @return void
*/
public function verify_request( $action, $query_arg = '_wpnonce' ) {
$has_access = current_user_can( 'wpseo_manage_network_options' );
if ( wp_doing_ajax() ) {
check_ajax_referer( $action, $query_arg );
if ( ! $has_access ) {
wp_die( -1, 403 );
}
return;
}
check_admin_referer( $action, $query_arg );
if ( ! $has_access ) {
wp_die( esc_html__( 'You are not allowed to perform this action.', 'wordpress-seo' ) );
}
}
/**
* Terminates the current request by either redirecting back or sending an AJAX response.
*
* @return void
*/
public function terminate_request() {
if ( wp_doing_ajax() ) {
$settings_errors = get_settings_errors();
if ( ! empty( $settings_errors ) && $settings_errors[0]['type'] === 'updated' ) {
wp_send_json_success( $settings_errors, 200 );
}
wp_send_json_error( $settings_errors, 400 );
}
$this->persist_settings_errors();
$this->redirect_back( array( 'settings-updated' => 'true' ) );
}
/**
* Persists settings errors.
*
* Settings errors are stored in a transient for 30 seconds so that this transient
* can be retrieved on the next page load.
*
* @return void
*/
protected function persist_settings_errors() {
/*
* A regular transient is used here, since it is automatically cleared right after the redirect.
* A network transient would be cleaner, but would require a lot of copied code from core for
* just a minor adjustment when displaying settings errors.
*/
set_transient( 'settings_errors', get_settings_errors(), 30 );
}
/**
* Redirects back to the referer URL, with optional query arguments.
*
* @param array $query_args Optional. Query arguments to add to the redirect URL. Default none.
*
* @return void
*/
protected function redirect_back( $query_args = array() ) {
$sendback = wp_get_referer();
if ( ! empty( $query_args ) ) {
$sendback = add_query_arg( $query_args, $sendback );
}
wp_safe_redirect( $sendback );
exit;
}
}

View File

@ -0,0 +1,164 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Network
*/
/**
* Implements a network settings API for the plugin's multisite settings.
*/
class Yoast_Network_Settings_API {
/**
* Registered network settings.
*
* @var array
*/
private $registered_settings = array();
/**
* Options whitelist, keyed by option group.
*
* @var array
*/
private $whitelist_options = array();
/**
* The singleton instance of this class.
*
* @var Yoast_Network_Settings_API
*/
private static $instance = null;
/**
* Registers a network setting and its data.
*
* @param string $option_group The group the network option is part of.
* @param string $option_name The name of the network option to sanitize and save.
* @param array $args {
* Optional. Data used to describe the network setting when registered.
*
* @type callable $sanitize_callback A callback function that sanitizes the network option's value.
* @type mixed $default Default value when calling `get_network_option()`.
* }
*
* @return void
*/
public function register_setting( $option_group, $option_name, $args = array() ) {
$defaults = array(
'group' => $option_group,
'sanitize_callback' => null,
);
$args = wp_parse_args( $args, $defaults );
if ( ! isset( $this->whitelist_options[ $option_group ] ) ) {
$this->whitelist_options[ $option_group ] = array();
}
$this->whitelist_options[ $option_group ][] = $option_name;
if ( ! empty( $args['sanitize_callback'] ) ) {
add_filter( "sanitize_option_{$option_name}", array( $this, 'filter_sanitize_option' ), 10, 2 );
}
if ( array_key_exists( 'default', $args ) ) {
add_filter( "default_site_option_{$option_name}", array( $this, 'filter_default_option' ), 10, 2 );
}
$this->registered_settings[ $option_name ] = $args;
}
/**
* Gets the registered settings and their data.
*
* @return array Array of $option_name => $data pairs.
*/
public function get_registered_settings() {
return $this->registered_settings;
}
/**
* Gets the whitelisted options for a given option group.
*
* @param string $option_group Option group.
*
* @return array List of option names, or empty array if unknown option group.
*/
public function get_whitelist_options( $option_group ) {
if ( ! isset( $this->whitelist_options[ $option_group ] ) ) {
return array();
}
return $this->whitelist_options[ $option_group ];
}
/**
* Filters sanitization for a network option value.
*
* This method is added as a filter to `sanitize_option_{$option}` for network options that are
* registered with a sanitize callback.
*
* @param string $value The sanitized option value.
* @param string $option The option name.
*
* @return string The filtered sanitized option value.
*/
public function filter_sanitize_option( $value, $option ) {
if ( empty( $this->registered_settings[ $option ] ) ) {
return $value;
}
return call_user_func( $this->registered_settings[ $option ]['sanitize_callback'], $value );
}
/**
* Filters the default value for a network option.
*
* This function is added as a filter to `default_site_option_{$option}` for network options that
* are registered with a default.
*
* @param mixed $default Existing default value to return.
* @param string $option The option name.
*
* @return mixed The filtered default value.
*/
public function filter_default_option( $default, $option ) {
// If a default value was manually passed to the function, allow it to override.
if ( $default !== false ) {
return $default;
}
if ( empty( $this->registered_settings[ $option ] ) ) {
return $default;
}
return $this->registered_settings[ $option ]['default'];
}
/**
* Checks whether the requirements to use this class are met.
*
* @return bool True if requirements are met, false otherwise.
*/
public function meets_requirements() {
return is_multisite();
}
/**
* Gets the singleton instance of this class.
*
* @return Yoast_Network_Settings_API The singleton instance.
*/
public static function get() {
if ( self::$instance === null ) {
self::$instance = new self();
}
return self::$instance;
}
}

View File

@ -0,0 +1,800 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Notifications
*/
/**
* Handles notifications storage and display.
*/
class Yoast_Notification_Center {
/**
* Option name to store notifications on.
*
* @var string
*/
const STORAGE_KEY = 'yoast_notifications';
/**
* The singleton instance of this object.
*
* @var \Yoast_Notification_Center
*/
private static $instance = null;
/**
* @var \Yoast_Notification[]
*/
private $notifications = array();
/**
* Notifications there are newly added.
*
* @var array
*/
private $new = array();
/**
* Notifications that were resolved this execution.
*
* @var array
*/
private $resolved = 0;
/**
* Internal storage for transaction before notifications have been retrieved from storage.
*
* @var array
*/
private $queued_transactions = array();
/**
* Internal flag for whether notifications have been retrieved from storage.
*
* @var bool
*/
private $notifications_retrieved = false;
/**
* Construct.
*/
private function __construct() {
add_action( 'init', array( $this, 'setup_current_notifications' ), 1 );
add_action( 'all_admin_notices', array( $this, 'display_notifications' ) );
add_action( 'wp_ajax_yoast_get_notifications', array( $this, 'ajax_get_notifications' ) );
add_action( 'wpseo_deactivate', array( $this, 'deactivate_hook' ) );
add_action( 'shutdown', array( $this, 'update_storage' ) );
}
/**
* Singleton getter.
*
* @return Yoast_Notification_Center
*/
public static function get() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Dismiss a notification.
*/
public static function ajax_dismiss_notification() {
$notification_center = self::get();
$notification_id = filter_input( INPUT_POST, 'notification' );
if ( empty( $notification_id ) ) {
die( '-1' );
}
$notification = $notification_center->get_notification_by_id( $notification_id );
if ( false === ( $notification instanceof Yoast_Notification ) ) {
// Permit legacy.
$options = array(
'id' => $notification_id,
'dismissal_key' => $notification_id,
);
$notification = new Yoast_Notification( '', $options );
}
if ( self::maybe_dismiss_notification( $notification ) ) {
die( '1' );
}
die( '-1' );
}
/**
* Check if the user has dismissed a notification.
*
* @param Yoast_Notification $notification The notification to check for dismissal.
* @param null|int $user_id User ID to check on.
*
* @return bool
*/
public static function is_notification_dismissed( Yoast_Notification $notification, $user_id = null ) {
$user_id = ( ! is_null( $user_id ) ? $user_id : get_current_user_id() );
$dismissal_key = $notification->get_dismissal_key();
// This checks both the site-specific user option and the meta value.
$current_value = get_user_option( $dismissal_key, $user_id );
// Migrate old user meta to user option on-the-fly.
if ( ! empty( $current_value )
&& metadata_exists( 'user', $user_id, $dismissal_key )
&& update_user_option( $user_id, $dismissal_key, $current_value ) ) {
delete_user_meta( $user_id, $dismissal_key );
}
return ! empty( $current_value );
}
/**
* Check if the nofitication is being dismissed.
*
* @param string|Yoast_Notification $notification Notification to check dismissal of.
* @param string $meta_value Value to set the meta value to if dismissed.
*
* @return bool True if dismissed.
*/
public static function maybe_dismiss_notification( Yoast_Notification $notification, $meta_value = 'seen' ) {
// Only persistent notifications are dismissible.
if ( ! $notification->is_persistent() ) {
return false;
}
// If notification is already dismissed, we're done.
if ( self::is_notification_dismissed( $notification ) ) {
return true;
}
$dismissal_key = $notification->get_dismissal_key();
$notification_id = $notification->get_id();
$is_dismissing = ( $dismissal_key === self::get_user_input( 'notification' ) );
if ( ! $is_dismissing ) {
$is_dismissing = ( $notification_id === self::get_user_input( 'notification' ) );
}
// Fallback to ?dismissal_key=1&nonce=bla when JavaScript fails.
if ( ! $is_dismissing ) {
$is_dismissing = ( '1' === self::get_user_input( $dismissal_key ) );
}
if ( ! $is_dismissing ) {
return false;
}
$user_nonce = self::get_user_input( 'nonce' );
if ( false === wp_verify_nonce( $user_nonce, $notification_id ) ) {
return false;
}
return self::dismiss_notification( $notification, $meta_value );
}
/**
* Dismisses a notification.
*
* @param Yoast_Notification $notification Notification to dismiss.
* @param string $meta_value Value to save in the dismissal.
*
* @return bool True if dismissed, false otherwise.
*/
public static function dismiss_notification( Yoast_Notification $notification, $meta_value = 'seen' ) {
// Dismiss notification.
return update_user_option( get_current_user_id(), $notification->get_dismissal_key(), $meta_value ) !== false;
}
/**
* Restores a notification.
*
* @param Yoast_Notification $notification Notification to restore.
*
* @return bool True if restored, false otherwise.
*/
public static function restore_notification( Yoast_Notification $notification ) {
$user_id = get_current_user_id();
$dismissal_key = $notification->get_dismissal_key();
// Restore notification.
$restored = delete_user_option( $user_id, $dismissal_key );
// Delete unprefixed user meta too for backward-compatibility.
if ( metadata_exists( 'user', $user_id, $dismissal_key ) ) {
$restored = delete_user_meta( $user_id, $dismissal_key ) && $restored;
}
return $restored;
}
/**
* Clear dismissal information for the specified Notification.
*
* When a cause is resolved, the next time it is present we want to show
* the message again.
*
* @param string|Yoast_Notification $notification Notification to clear the dismissal of.
*
* @return bool
*/
public function clear_dismissal( $notification ) {
global $wpdb;
if ( $notification instanceof Yoast_Notification ) {
$dismissal_key = $notification->get_dismissal_key();
}
if ( is_string( $notification ) ) {
$dismissal_key = $notification;
}
if ( empty( $dismissal_key ) ) {
return false;
}
// Remove notification dismissal for all users.
$deleted = delete_metadata( 'user', 0, $wpdb->get_blog_prefix() . $dismissal_key, '', true );
// Delete unprefixed user meta too for backward-compatibility.
$deleted = delete_metadata( 'user', 0, $dismissal_key, '', true ) || $deleted;
return $deleted;
}
/**
* Retrieves notifications from the storage and merges in previous notification changes.
*
* The current user in WordPress is not loaded shortly before the 'init' hook, but the plugin
* sometimes needs to add or remove notifications before that. In such cases, the transactions
* are not actually executed, but added to a queue. That queue is then handled in this method,
* after notifications for the current user have been set up.
*
* @return void
*/
public function setup_current_notifications() {
$this->retrieve_notifications_from_storage();
foreach ( $this->queued_transactions as $transaction ) {
list( $callback, $args ) = $transaction;
call_user_func_array( $callback, $args );
}
$this->queued_transactions = array();
}
/**
* Add notification to the cookie.
*
* @param Yoast_Notification $notification Notification object instance.
*/
public function add_notification( Yoast_Notification $notification ) {
$callback = array( $this, __METHOD__ );
$args = func_get_args();
if ( $this->queue_transaction( $callback, $args ) ) {
return;
}
// Don't add if the user can't see it.
if ( ! $notification->display_for_current_user() ) {
return;
}
$notification_id = $notification->get_id();
// Empty notifications are always added.
if ( $notification_id !== '' ) {
// If notification ID exists in notifications, don't add again.
$present_notification = $this->get_notification_by_id( $notification_id );
if ( ! is_null( $present_notification ) ) {
$this->remove_notification( $present_notification, false );
}
if ( is_null( $present_notification ) ) {
$this->new[] = $notification_id;
}
}
// Add to list.
$this->notifications[] = $notification;
}
/**
* Get the notification by ID.
*
* @param string $notification_id The ID of the notification to search for.
*
* @return null|Yoast_Notification
*/
public function get_notification_by_id( $notification_id ) {
foreach ( $this->notifications as & $notification ) {
if ( $notification_id === $notification->get_id() ) {
return $notification;
}
}
return null;
}
/**
* Display the notifications.
*
* @param bool $echo_as_json True when notifications should be printed directly.
*
* @return void
*/
public function display_notifications( $echo_as_json = false ) {
// Never display notifications for network admin.
if ( function_exists( 'is_network_admin' ) && is_network_admin() ) {
return;
}
$sorted_notifications = $this->get_sorted_notifications();
$notifications = array_filter( $sorted_notifications, array( $this, 'is_notification_persistent' ) );
if ( empty( $notifications ) ) {
return;
}
array_walk( $notifications, array( $this, 'remove_notification' ) );
$notifications = array_unique( $notifications );
if ( $echo_as_json ) {
$notification_json = array();
/**
* @var Yoast_Notification[] $notifications
*/
foreach ( $notifications as $notification ) {
$notification_json[] = $notification->render();
}
echo WPSEO_Utils::format_json_encode( $notification_json );
return;
}
foreach ( $notifications as $notification ) {
echo $notification;
}
}
/**
* Remove notification after it has been displayed.
*
* @param Yoast_Notification $notification Notification to remove.
* @param bool $resolve Resolve as fixed.
*/
public function remove_notification( Yoast_Notification $notification, $resolve = true ) {
$callback = array( $this, __METHOD__ );
$args = func_get_args();
if ( $this->queue_transaction( $callback, $args ) ) {
return;
}
$index = false;
// Match persistent Notifications by ID, non persistent by item in the array.
if ( $notification->is_persistent() ) {
foreach ( $this->notifications as $current_index => $present_notification ) {
if ( $present_notification->get_id() === $notification->get_id() ) {
$index = $current_index;
break;
}
}
}
else {
$index = array_search( $notification, $this->notifications, true );
}
if ( false === $index ) {
return;
}
if ( $notification->is_persistent() && $resolve ) {
$this->resolved++;
$this->clear_dismissal( $notification );
}
unset( $this->notifications[ $index ] );
$this->notifications = array_values( $this->notifications );
}
/**
* Removes a notification by its ID.
*
* @param string $notification_id The notification id.
* @param bool $resolve Resolve as fixed.
*
* @return void
*/
public function remove_notification_by_id( $notification_id, $resolve = true ) {
$notification = $this->get_notification_by_id( $notification_id );
if ( $notification === null ) {
return;
}
$this->remove_notification( $notification, $resolve );
}
/**
* Get the notification count.
*
* @param bool $dismissed Count dismissed notifications.
*
* @return int Number of notifications
*/
public function get_notification_count( $dismissed = false ) {
$notifications = $this->get_notifications();
$notifications = array_filter( $notifications, array( $this, 'filter_persistent_notifications' ) );
if ( ! $dismissed ) {
$notifications = array_filter( $notifications, array( $this, 'filter_dismissed_notifications' ) );
}
return count( $notifications );
}
/**
* Get the number of notifications resolved this execution.
*
* These notifications have been resolved and should be counted when active again.
*
* @return int
*/
public function get_resolved_notification_count() {
return $this->resolved;
}
/**
* Return the notifications sorted on type and priority.
*
* @return array|Yoast_Notification[] Sorted Notifications
*/
public function get_sorted_notifications() {
$notifications = $this->get_notifications();
if ( empty( $notifications ) ) {
return array();
}
// Sort by severity, error first.
usort( $notifications, array( $this, 'sort_notifications' ) );
return $notifications;
}
/**
* AJAX display notifications.
*/
public function ajax_get_notifications() {
$echo = filter_input( INPUT_POST, 'version' ) === '2';
// Display the notices.
$this->display_notifications( $echo );
// AJAX die.
exit;
}
/**
* Remove storage when the plugin is deactivated.
*/
public function deactivate_hook() {
$this->clear_notifications();
}
/**
* Save persistent notifications to storage.
*
* We need to be able to retrieve these so they can be dismissed at any time during the execution.
*
* @since 3.2
*
* @return void
*/
public function update_storage() {
$notifications = $this->get_notifications();
/**
* Filter: 'yoast_notifications_before_storage' - Allows developer to filter notifications before saving them.
*
* @api Yoast_Notification[] $notifications
*/
$notifications = apply_filters( 'yoast_notifications_before_storage', $notifications );
// No notifications to store, clear storage if it was previously present.
if ( empty( $notifications ) ) {
$this->remove_storage();
return;
}
$notifications = array_map( array( $this, 'notification_to_array' ), $notifications );
// Save the notifications to the storage.
update_user_option( get_current_user_id(), self::STORAGE_KEY, $notifications );
}
/**
* Provide a way to verify present notifications.
*
* @return array|Yoast_Notification[] Registered notifications.
*/
public function get_notifications() {
return $this->notifications;
}
/**
* Get newly added notifications.
*
* @return array
*/
public function get_new_notifications() {
return array_map( array( $this, 'get_notification_by_id' ), $this->new );
}
/**
* Get information from the User input.
*
* @param string $key Key to retrieve.
*
* @return mixed value of key if set.
*/
private static function get_user_input( $key ) {
$filter_input_type = INPUT_GET;
if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === strtoupper( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) {
$filter_input_type = INPUT_POST;
}
return filter_input( $filter_input_type, $key );
}
/**
* Retrieve the notifications from storage.
*
* @return array|void Yoast_Notification[] Notifications.
*/
private function retrieve_notifications_from_storage() {
if ( $this->notifications_retrieved ) {
return;
}
$this->notifications_retrieved = true;
$stored_notifications = get_user_option( self::STORAGE_KEY, get_current_user_id() );
// Check if notifications are stored.
if ( empty( $stored_notifications ) ) {
return;
}
if ( is_array( $stored_notifications ) ) {
$notifications = array_map( array( $this, 'array_to_notification' ), $stored_notifications );
// Apply array_values to ensure we get a 0-indexed array.
$notifications = array_values( array_filter( $notifications, array( $this, 'filter_notification_current_user' ) ) );
$this->notifications = $notifications;
}
}
/**
* Sort on type then priority.
*
* @param Yoast_Notification $a Compare with B.
* @param Yoast_Notification $b Compare with A.
*
* @return int 1, 0 or -1 for sorting offset.
*/
private function sort_notifications( Yoast_Notification $a, Yoast_Notification $b ) {
$a_type = $a->get_type();
$b_type = $b->get_type();
if ( $a_type === $b_type ) {
return WPSEO_Utils::calc( $b->get_priority(), 'compare', $a->get_priority() );
}
if ( 'error' === $a_type ) {
return -1;
}
if ( 'error' === $b_type ) {
return 1;
}
return 0;
}
/**
* Clear local stored notifications.
*/
private function clear_notifications() {
$this->notifications = array();
$this->notifications_retrieved = false;
}
/**
* Filter out non-persistent notifications.
*
* @param Yoast_Notification $notification Notification to test for persistent.
*
* @since 3.2
*
* @return bool
*/
private function filter_persistent_notifications( Yoast_Notification $notification ) {
return $notification->is_persistent();
}
/**
* Filter out dismissed notifications.
*
* @param Yoast_Notification $notification Notification to check.
*
* @return bool
*/
private function filter_dismissed_notifications( Yoast_Notification $notification ) {
return ! $this->maybe_dismiss_notification( $notification );
}
/**
* Convert Notification to array representation.
*
* @param Yoast_Notification $notification Notification to convert.
*
* @since 3.2
*
* @return array
*/
private function notification_to_array( Yoast_Notification $notification ) {
$notification_data = $notification->to_array();
if ( isset( $notification_data['nonce'] ) ) {
unset( $notification_data['nonce'] );
}
return $notification_data;
}
/**
* Convert stored array to Notification.
*
* @param array $notification_data Array to convert to Notification.
*
* @return Yoast_Notification
*/
private function array_to_notification( $notification_data ) {
if ( isset( $notification_data['options']['nonce'] ) ) {
unset( $notification_data['options']['nonce'] );
}
return new Yoast_Notification(
$notification_data['message'],
$notification_data['options']
);
}
/**
* Filter notifications that should not be displayed for the current user.
*
* @param Yoast_Notification $notification Notification to test.
*
* @return bool
*/
private function filter_notification_current_user( Yoast_Notification $notification ) {
return $notification->display_for_current_user();
}
/**
* Checks if given notification is persistent.
*
* @param Yoast_Notification $notification The notification to check.
*
* @return bool True when notification is not persistent.
*/
private function is_notification_persistent( Yoast_Notification $notification ) {
return ! $notification->is_persistent();
}
/**
* Queues a notification transaction for later execution if notifications are not yet set up.
*
* @param callable $callback Callback that performs the transaction.
* @param array $args Arguments to pass to the callback.
*
* @return bool True if transaction was queued, false if it can be performed immediately.
*/
private function queue_transaction( $callback, $args ) {
if ( $this->notifications_retrieved ) {
return false;
}
$this->add_transaction_to_queue( $callback, $args );
return true;
}
/**
* Adds a notification transaction to the queue for later execution.
*
* @param callable $callback Callback that performs the transaction.
* @param array $args Arguments to pass to the callback.
*/
private function add_transaction_to_queue( $callback, $args ) {
$this->queued_transactions[] = array( $callback, $args );
}
/**
* Removes all notifications from storage.
*
* @return bool True when notifications got removed.
*/
protected function remove_storage() {
if ( ! $this->has_stored_notifications() ) {
return false;
}
delete_user_option( get_current_user_id(), self::STORAGE_KEY );
return true;
}
/**
* Checks if there are stored notifications.
*
* @return bool True when there are stored notifications.
*/
protected function has_stored_notifications() {
$stored_notifications = $this->get_stored_notifications();
return ! empty( $stored_notifications );
}
/**
* Retrieves the stored notifications.
*
* @codeCoverageIgnore
*
* @return array|false Array with notifications or false when not set.
*/
protected function get_stored_notifications() {
return get_user_option( self::STORAGE_KEY, get_current_user_id() );
}
}

View File

@ -0,0 +1,386 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Notifications
* @since 1.5.3
*/
/**
* Implements individual notification.
*/
class Yoast_Notification {
/**
* Type of capability check.
*
* @var string
*/
const MATCH_ALL = 'all';
/**
* Type of capability check.
*
* @var string
*/
const MATCH_ANY = 'any';
/**
* Notification type.
*
* @var string
*/
const ERROR = 'error';
/**
* Notification type.
*
* @var string
*/
const WARNING = 'warning';
/**
* Notification type.
*
* @var string
*/
const UPDATED = 'updated';
/**
* Options of this Notification.
*
* Contains optional arguments:
*
* - type: The notification type, i.e. 'updated' or 'error'
* - id: The ID of the notification
* - nonce: Security nonce to use in case of dismissible notice.
* - priority: From 0 to 1, determines the order of Notifications.
* - dismissal_key: Option name to save dismissal information in, ID will be used if not supplied.
* - capabilities: Capabilities that a user must have for this Notification to show.
* - capability_check: How to check capability pass: all or any.
* - wpseo_page_only: Only display on wpseo page or on every page.
*
* @var array
*/
private $options = array();
/**
* Contains default values for the optional arguments.
*
* @var array
*/
private $defaults = array(
'type' => self::UPDATED,
'id' => '',
'nonce' => null,
'priority' => 0.5,
'data_json' => array(),
'dismissal_key' => null,
'capabilities' => array(),
'capability_check' => self::MATCH_ALL,
'yoast_branding' => false,
);
/**
* The message for the notification.
*
* @var string
*/
private $message;
/**
* Notification class constructor.
*
* @param string $message Message string.
* @param array $options Set of options.
*/
public function __construct( $message, $options = array() ) {
$this->message = $message;
$this->options = $this->normalize_options( $options );
}
/**
* Retrieve notification ID string.
*
* @return string
*/
public function get_id() {
return $this->options['id'];
}
/**
* Retrieve nonce identifier.
*
* @return null|string Nonce for this Notification.
*/
public function get_nonce() {
if ( $this->options['id'] && empty( $this->options['nonce'] ) ) {
$this->options['nonce'] = wp_create_nonce( $this->options['id'] );
}
return $this->options['nonce'];
}
/**
* Make sure the nonce is up to date.
*/
public function refresh_nonce() {
if ( $this->options['id'] ) {
$this->options['nonce'] = wp_create_nonce( $this->options['id'] );
}
}
/**
* Get the type of the notification.
*
* @return string
*/
public function get_type() {
return $this->options['type'];
}
/**
* Priority of the notification.
*
* Relative to the type.
*
* @return float Returns the priority between 0 and 1.
*/
public function get_priority() {
return $this->options['priority'];
}
/**
* Get the User Meta key to check for dismissal of notification.
*
* @return string User Meta Option key that registers dismissal.
*/
public function get_dismissal_key() {
if ( empty( $this->options['dismissal_key'] ) ) {
return $this->options['id'];
}
return $this->options['dismissal_key'];
}
/**
* Is this Notification persistent.
*
* @return bool True if persistent, False if fire and forget.
*/
public function is_persistent() {
$id = $this->get_id();
return ! empty( $id );
}
/**
* Check if the notification is relevant for the current user.
*
* @return bool True if a user needs to see this notification, false if not.
*/
public function display_for_current_user() {
// If the notification is for the current page only, always show.
if ( ! $this->is_persistent() ) {
return true;
}
// If the current user doesn't match capabilities.
return $this->match_capabilities();
}
/**
* Does the current user match required capabilities.
*
* @return bool
*/
public function match_capabilities() {
// Super Admin can do anything.
if ( is_multisite() && is_super_admin() ) {
return true;
}
/**
* Filter capabilities that enable the displaying of this notification.
*
* @since 3.2
*
* @param array $capabilities The capabilities that must be present for this notification.
* @param Yoast_Notification $notification The notification object.
*
* @return Array of capabilities or empty for no restrictions.
*/
$capabilities = apply_filters( 'wpseo_notification_capabilities', $this->options['capabilities'], $this );
// Should be an array.
if ( ! is_array( $capabilities ) ) {
$capabilities = (array) $capabilities;
}
/**
* Filter capability check to enable all or any capabilities.
*
* @since 3.2
*
* @param string $capability_check The type of check that will be used to determine if an capability is present.
* @param Yoast_Notification $notification The notification object.
*
* @return string self::MATCH_ALL or self::MATCH_ANY.
*/
$capability_check = apply_filters( 'wpseo_notification_capability_check', $this->options['capability_check'], $this );
if ( ! in_array( $capability_check, array( self::MATCH_ALL, self::MATCH_ANY ), true ) ) {
$capability_check = self::MATCH_ALL;
}
if ( ! empty( $capabilities ) ) {
$has_capabilities = array_filter( $capabilities, array( $this, 'has_capability' ) );
switch ( $capability_check ) {
case self::MATCH_ALL:
return $has_capabilities === $capabilities;
case self::MATCH_ANY:
return ! empty( $has_capabilities );
}
}
return true;
}
/**
* Array filter function to find matched capabilities.
*
* @param string $capability Capability to test.
*
* @return bool
*/
private function has_capability( $capability ) {
return current_user_can( $capability );
}
/**
* Return the object properties as an array.
*
* @return array
*/
public function to_array() {
return array(
'message' => $this->message,
'options' => $this->options,
);
}
/**
* Adds string (view) behaviour to the notification.
*
* @return string
*/
public function __toString() {
return $this->render();
}
/**
* Renders the notification as a string.
*
* @return string The rendered notification.
*/
public function render() {
$attributes = array();
// Default notification classes.
$classes = array(
'yoast-alert',
);
// Maintain WordPress visualisation of alerts when they are not persistent.
if ( ! $this->is_persistent() ) {
$classes[] = 'notice';
$classes[] = $this->get_type();
}
if ( ! empty( $classes ) ) {
$attributes['class'] = implode( ' ', $classes );
}
// Combined attribute key and value into a string.
array_walk( $attributes, array( $this, 'parse_attributes' ) );
$message = null;
if ( $this->options['yoast_branding'] ) {
$message = $this->wrap_yoast_seo_icon( $this->message );
}
if ( $message === null ) {
$message = wpautop( $this->message );
}
// Build the output DIV.
return '<div ' . implode( ' ', $attributes ) . '>' . $message . '</div>' . PHP_EOL;
}
/**
* Wraps the message with a Yoast SEO icon.
*
* @param string $message The message to wrap.
*
* @return string The wrapped message.
*/
private function wrap_yoast_seo_icon( $message ) {
$out = sprintf(
'<img src="%1$s" height="%2$d" width="%3$d" class="yoast-seo-icon" />',
esc_url( plugin_dir_url( WPSEO_FILE ) . 'images/Yoast_SEO_Icon.svg' ),
60,
60
);
$out .= '<div class="yoast-seo-icon-wrap">';
$out .= $message;
$out .= '</div>';
return $out;
}
/**
* Get the JSON if provided.
*
* @return false|string
*/
public function get_json() {
if ( empty( $this->options['data_json'] ) ) {
return '';
}
return WPSEO_Utils::format_json_encode( $this->options['data_json'] );
}
/**
* Make sure we only have values that we can work with.
*
* @param array $options Options to normalize.
*
* @return array
*/
private function normalize_options( $options ) {
$options = wp_parse_args( $options, $this->defaults );
// Should not exceed 0 or 1.
$options['priority'] = min( 1, max( 0, $options['priority'] ) );
// Set default capabilities when not supplied.
if ( empty( $options['capabilities'] ) || array() === $options['capabilities'] ) {
$options['capabilities'] = array( 'wpseo_manage_options' );
}
return $options;
}
/**
* Format HTML element attributes.
*
* @param string $value Attribute value.
* @param string $key Attribute name.
*/
private function parse_attributes( & $value, $key ) {
$value = sprintf( '%s="%s"', $key, esc_attr( $value ) );
}
}

View File

@ -0,0 +1,331 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
* @since 1.7.0
*/
/**
* Base class for handling plugin conflicts.
*/
class Yoast_Plugin_Conflict {
/**
* The plugins must be grouped per section.
*
* It's possible to check for each section if there are conflicting plugins.
*
* @var array
*/
protected $plugins = array();
/**
* All the current active plugins will be stored in this private var.
*
* @var array
*/
protected $all_active_plugins = array();
/**
* After searching for active plugins that are in $this->plugins the active plugins will be stored in this
* property.
*
* @var array
*/
protected $active_plugins = array();
/**
* Property for holding instance of itself.
*
* @var Yoast_Plugin_Conflict
*/
protected static $instance;
/**
* For the use of singleton pattern. Create instance of itself and return this instance.
*
* @param string $class_name Give the classname to initialize. If classname is
* false (empty) it will use it's own __CLASS__.
*
* @return Yoast_Plugin_Conflict
*/
public static function get_instance( $class_name = '' ) {
if ( is_null( self::$instance ) ) {
if ( ! is_string( $class_name ) || $class_name === '' ) {
$class_name = __CLASS__;
}
self::$instance = new $class_name();
}
return self::$instance;
}
/**
* Setting instance, all active plugins and search for active plugins.
*
* Protected constructor to prevent creating a new instance of the
* *Singleton* via the `new` operator from outside of this class.
*/
protected function __construct() {
// Set active plugins.
$this->all_active_plugins = get_option( 'active_plugins' );
if ( filter_input( INPUT_GET, 'action' ) === 'deactivate' ) {
$this->remove_deactivated_plugin();
}
// Search for active plugins.
$this->search_active_plugins();
}
/**
* Check if there are conflicting plugins for given $plugin_section.
*
* @param string $plugin_section Type of plugin conflict (such as Open Graph or sitemap).
*
* @return bool
*/
public function check_for_conflicts( $plugin_section ) {
static $sections_checked;
if ( $sections_checked === null ) {
$sections_checked = array();
}
if ( ! in_array( $plugin_section, $sections_checked, true ) ) {
$sections_checked[] = $plugin_section;
$has_conflicts = ( ! empty( $this->active_plugins[ $plugin_section ] ) );
return $has_conflicts;
}
return false;
}
/**
* Getting all the conflicting plugins and return them as a string.
*
* This method will loop through all conflicting plugins to get the details of each plugin. The plugin name
* will be taken from the details to parse a comma separated string, which can be use for by example a notice
*
* @param string $plugin_section Plugin conflict type (such as Open Graph or sitemap).
*
* @return string
*/
public function get_conflicting_plugins_as_string( $plugin_section ) {
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
// Getting the active plugins by given section.
$plugins = $this->active_plugins[ $plugin_section ];
$plugin_names = array();
foreach ( $plugins as $plugin ) {
$name = WPSEO_Utils::get_plugin_name( $plugin );
if ( ! empty( $name ) ) {
$plugin_names[] = '<em>' . $name . '</em>';
}
}
unset( $plugins, $plugin );
if ( ! empty( $plugin_names ) ) {
return implode( ' &amp; ', $plugin_names );
}
}
/**
* Checks for given $plugin_sections for conflicts.
*
* @param array $plugin_sections Set of sections.
*/
public function check_plugin_conflicts( $plugin_sections ) {
foreach ( $plugin_sections as $plugin_section => $readable_plugin_section ) {
// Check for conflicting plugins and show error if there are conflicts.
if ( $this->check_for_conflicts( $plugin_section ) ) {
$this->set_error( $plugin_section, $readable_plugin_section );
}
}
// List of all active sections.
$sections = array_keys( $plugin_sections );
// List of all sections.
$all_plugin_sections = array_keys( $this->plugins );
/*
* Get all sections that are inactive.
* These plugins need to be cleared.
*
* This happens when Sitemaps or OpenGraph implementations toggle active/disabled.
*/
$inactive_sections = array_diff( $all_plugin_sections, $sections );
if ( ! empty( $inactive_sections ) ) {
foreach ( $inactive_sections as $section ) {
array_walk( $this->plugins[ $section ], array( $this, 'clear_error' ) );
}
}
// For active sections clear errors for inactive plugins.
foreach ( $sections as $section ) {
// By default clear errors for all plugins of the section.
$inactive_plugins = $this->plugins[ $section ];
// If there are active plugins, filter them from being cleared.
if ( isset( $this->active_plugins[ $section ] ) ) {
$inactive_plugins = array_diff( $this->plugins[ $section ], $this->active_plugins[ $section ] );
}
array_walk( $inactive_plugins, array( $this, 'clear_error' ) );
}
}
/**
* Setting an error on the screen.
*
* @param string $plugin_section Type of conflict group (such as Open Graph or sitemap).
* @param string $readable_plugin_section This is the value for the translation.
*/
protected function set_error( $plugin_section, $readable_plugin_section ) {
$notification_center = Yoast_Notification_Center::get();
foreach ( $this->active_plugins[ $plugin_section ] as $plugin_file ) {
$plugin_name = WPSEO_Utils::get_plugin_name( $plugin_file );
$error_message = '';
/* translators: %1$s: 'Facebook & Open Graph' plugin name(s) of possibly conflicting plugin(s), %2$s to Yoast SEO */
$error_message .= '<p>' . sprintf( __( 'The %1$s plugin might cause issues when used in conjunction with %2$s.', 'wordpress-seo' ), '<em>' . $plugin_name . '</em>', 'Yoast SEO' ) . '</p>';
$error_message .= '<p>' . sprintf( $readable_plugin_section, 'Yoast SEO', $plugin_name ) . '</p>';
/* translators: %s: 'Facebook' plugin name of possibly conflicting plugin */
$error_message .= '<a class="button button-primary" href="' . wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . $plugin_file . '&amp;plugin_status=all', 'deactivate-plugin_' . $plugin_file ) . '">' . sprintf( __( 'Deactivate %s', 'wordpress-seo' ), WPSEO_Utils::get_plugin_name( $plugin_file ) ) . '</a> ';
$identifier = $this->get_notification_identifier( $plugin_file );
// Add the message to the notifications center.
$notification_center->add_notification(
new Yoast_Notification(
$error_message,
array(
'type' => Yoast_Notification::ERROR,
'id' => 'wpseo-conflict-' . $identifier,
)
)
);
}
}
/**
* Clear the notification for a plugin.
*
* @param string $plugin_file Clear the optional notification for this plugin.
*/
public function clear_error( $plugin_file ) {
$identifier = $this->get_notification_identifier( $plugin_file );
$notification_center = Yoast_Notification_Center::get();
$notification_center->remove_notification_by_id( 'wpseo-conflict-' . $identifier );
}
/**
* Loop through the $this->plugins to check if one of the plugins is active.
*
* This method will store the active plugins in $this->active_plugins.
*/
protected function search_active_plugins() {
foreach ( $this->plugins as $plugin_section => $plugins ) {
$this->check_plugins_active( $plugins, $plugin_section );
}
}
/**
* Loop through plugins and check if each plugin is active.
*
* @param array $plugins Set of plugins.
* @param string $plugin_section Type of conflict group (such as Open Graph or sitemap).
*/
protected function check_plugins_active( $plugins, $plugin_section ) {
foreach ( $plugins as $plugin ) {
if ( $this->check_plugin_is_active( $plugin ) ) {
$this->add_active_plugin( $plugin_section, $plugin );
}
}
}
/**
* Check if given plugin exists in array with all_active_plugins.
*
* @param string $plugin Plugin basename string.
*
* @return bool
*/
protected function check_plugin_is_active( $plugin ) {
return in_array( $plugin, $this->all_active_plugins, true );
}
/**
* Add plugin to the list of active plugins.
*
* This method will check first if key $plugin_section exists, if not it will create an empty array
* If $plugin itself doesn't exist it will be added.
*
* @param string $plugin_section Type of conflict group (such as Open Graph or sitemap).
* @param string $plugin Plugin basename string.
*/
protected function add_active_plugin( $plugin_section, $plugin ) {
if ( ! array_key_exists( $plugin_section, $this->active_plugins ) ) {
$this->active_plugins[ $plugin_section ] = array();
}
if ( ! in_array( $plugin, $this->active_plugins[ $plugin_section ], true ) ) {
$this->active_plugins[ $plugin_section ][] = $plugin;
}
}
/**
* Search in $this->plugins for the given $plugin.
*
* If there is a result it will return the plugin category.
*
* @param string $plugin Plugin basename string.
*
* @return int|string
*/
protected function find_plugin_category( $plugin ) {
foreach ( $this->plugins as $plugin_section => $plugins ) {
if ( in_array( $plugin, $plugins, true ) ) {
return $plugin_section;
}
}
}
/**
* When being in the deactivation process the currently deactivated plugin has to be removed.
*/
private function remove_deactivated_plugin() {
$deactivated_plugin = filter_input( INPUT_GET, 'plugin' );
$key_to_remove = array_search( $deactivated_plugin, $this->all_active_plugins, true );
if ( $key_to_remove !== false ) {
unset( $this->all_active_plugins[ $key_to_remove ] );
}
}
/**
* Get the identifier from the plugin file.
*
* @param string $plugin_file Plugin file to get Identifier from.
*
* @return string
*/
private function get_notification_identifier( $plugin_file ) {
return md5( $plugin_file );
}
}

View File

@ -0,0 +1,80 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\ConfigurationUI
*/
/**
* Class WPSEO_Configuration_Components.
*/
class WPSEO_Configuration_Components {
/**
* List of registered components.
*
* @var WPSEO_Config_Component[]
*/
protected $components = array();
/**
* Adapter.
*
* @var WPSEO_Configuration_Options_Adapter
*/
protected $adapter;
/**
* Add default components.
*/
public function initialize() {
$this->add_component( new WPSEO_Config_Component_Connect_Google_Search_Console() );
$this->add_component( new WPSEO_Config_Component_Mailchimp_Signup() );
$this->add_component( new WPSEO_Config_Component_Suggestions() );
}
/**
* Add a component.
*
* @param WPSEO_Config_Component $component Component to add.
*/
public function add_component( WPSEO_Config_Component $component ) {
$this->components[] = $component;
}
/**
* Sets the storage to use.
*
* @param WPSEO_Configuration_Storage $storage Storage to use.
*/
public function set_storage( WPSEO_Configuration_Storage $storage ) {
$this->set_adapter( $storage->get_adapter() );
foreach ( $this->components as $component ) {
$storage->add_field( $component->get_field() );
}
}
/**
* Sets the adapter to use.
*
* @param WPSEO_Configuration_Options_Adapter $adapter Adapter to use.
*/
public function set_adapter( WPSEO_Configuration_Options_Adapter $adapter ) {
$this->adapter = $adapter;
foreach ( $this->components as $component ) {
$adapter->add_custom_lookup(
$component->get_field()->get_identifier(),
array(
$component,
'get_data',
),
array(
$component,
'set_data',
)
);
}
}
}

View File

@ -0,0 +1,92 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\ConfigurationUI
*/
/**
* Class WPSEO_Configuration_Endpoint.
*/
class WPSEO_Configuration_Endpoint {
/**
* @var string
*/
const REST_NAMESPACE = 'yoast/v1';
/**
* @var string
*/
const ENDPOINT_RETRIEVE = 'configurator';
/**
* @var string
*/
const ENDPOINT_STORE = 'configurator';
/**
* @var string
*/
const CAPABILITY_RETRIEVE = 'wpseo_manage_options';
/**
* @var string
*/
const CAPABILITY_STORE = 'wpseo_manage_options';
/**
* Service to use.
*
* @var WPSEO_Configuration_Service
*/
protected $service;
/**
* Sets the service to use.
*
* @param WPSEO_Configuration_Service $service Service to use.
*/
public function set_service( WPSEO_Configuration_Service $service ) {
$this->service = $service;
}
/**
* Register REST routes.
*/
public function register() {
// Register fetch config.
$route_args = array(
'methods' => 'GET',
'callback' => array( $this->service, 'get_configuration' ),
'permission_callback' => array( $this, 'can_retrieve_data' ),
);
register_rest_route( self::REST_NAMESPACE, self::ENDPOINT_RETRIEVE, $route_args );
// Register save changes.
$route_args = array(
'methods' => 'POST',
'callback' => array( $this->service, 'set_configuration' ),
'permission_callback' => array( $this, 'can_save_data' ),
);
register_rest_route( self::REST_NAMESPACE, self::ENDPOINT_STORE, $route_args );
}
/**
* Permission callback implementation.
*
* @return bool
*/
public function can_retrieve_data() {
return current_user_can( self::CAPABILITY_RETRIEVE );
}
/**
* Permission callback implementation.
*
* @return bool
*/
public function can_save_data() {
return current_user_can( self::CAPABILITY_STORE );
}
}

View File

@ -0,0 +1,218 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\ConfigurationUI
*/
/**
* Class WPSEO_Configuration_Options_Adapter.
*
* Convert Configuration settings to WPSEO Options.
*
* @since 3.6
*/
class WPSEO_Configuration_Options_Adapter {
/**
* @var string
*/
const OPTION_TYPE_WORDPRESS = 'wordpress';
/**
* @var string
*/
const OPTION_TYPE_YOAST = 'yoast';
/**
* @var string
*/
const OPTION_TYPE_CUSTOM = 'custom';
/**
* List of registered lookups.
*
* @var array
*/
protected $lookup = array();
/**
* Add a lookup for a WordPress native option.
*
* @param string $class_name Class to bind to an option.
* @param string $option Option name to use.
*
* @throws InvalidArgumentException Thrown when invalid input is provided.
*/
public function add_wordpress_lookup( $class_name, $option ) {
if ( ! is_string( $option ) ) {
throw new InvalidArgumentException( 'WordPress option must be a string.' );
}
$this->add_lookup( $class_name, self::OPTION_TYPE_WORDPRESS, $option );
}
/**
* Add a lookup for a Yoast option.
*
* @param string $class_name Class to bind to the lookup.
* @param string $key Key in the option group to bind to.
*
* @throws InvalidArgumentException Thrown when invalid input is provided.
*/
public function add_option_lookup( $class_name, $key ) {
$test = WPSEO_Options::get( $key );
if ( is_null( $test ) ) {
/* translators: %1$s resolves to the option name passed to the lookup registration */
throw new InvalidArgumentException( sprintf( __( 'Yoast option %1$s not found.', 'wordpress-seo' ), $key ) );
}
$this->add_lookup( $class_name, self::OPTION_TYPE_YOAST, $key );
}
/**
* Add a lookup for a custom implementation.
*
* @param string $class_name Class to bind to the lookup.
* @param callable $callback_get Callback to retrieve data.
* @param callable $callback_set Callback to save data.
*
* @throws InvalidArgumentException Thrown when invalid input is provided.
*/
public function add_custom_lookup( $class_name, $callback_get, $callback_set ) {
if ( ! is_callable( $callback_get ) || ! is_callable( $callback_set ) ) {
throw new InvalidArgumentException( 'Custom option must be callable.' );
}
$this->add_lookup(
$class_name,
self::OPTION_TYPE_CUSTOM,
array( $callback_get, $callback_set )
);
}
/**
* Add a field lookup.
*
* @param string $class_name Class to add lookup for.
* @param string $type Type of lookup.
* @param string|array $option Implementation of the lookup.
*
* @throws Exception Thrown when invalid input is provided.
*/
protected function add_lookup( $class_name, $type, $option ) {
$this->lookup[ $class_name ] = array(
'type' => $type,
'option' => $option,
);
}
/**
* Get the data for the provided field.
*
* @param WPSEO_Config_Field $field Field to get data for.
*
* @return mixed
*/
public function get( WPSEO_Config_Field $field ) {
$identifier = $field->get_identifier();
// Lookup option and retrieve value.
$type = $this->get_option_type( $identifier );
$option = $this->get_option( $identifier );
switch ( $type ) {
case self::OPTION_TYPE_WORDPRESS:
return get_option( $option );
case self::OPTION_TYPE_YOAST:
return WPSEO_Options::get( $option );
case self::OPTION_TYPE_CUSTOM:
return call_user_func( $option[0] );
}
return null;
}
/**
* Save data from a field.
*
* @param WPSEO_Config_Field $field Field to use for lookup.
* @param mixed $value Value to save to the lookup of the field.
*
* @return bool
*/
public function set( WPSEO_Config_Field $field, $value ) {
$identifier = $field->get_identifier();
// Lookup option and retrieve value.
$type = $this->get_option_type( $identifier );
$option = $this->get_option( $identifier );
switch ( $type ) {
case self::OPTION_TYPE_WORDPRESS:
return update_option( $option, $value );
case self::OPTION_TYPE_YOAST:
return WPSEO_Options::set( $option, $value );
case self::OPTION_TYPE_CUSTOM:
return call_user_func( $option[1], $value );
}
return false;
}
/**
* Get the lookup type for a specific class.
*
* @param string $class_name Class to get the type of.
*
* @return null|string
*/
protected function get_option_type( $class_name ) {
if ( ! isset( $this->lookup[ $class_name ] ) ) {
return null;
}
return $this->lookup[ $class_name ]['type'];
}
/**
* Get the option for a specific class.
*
* @param string $class_name Class to get the option of.
*
* @return null|string|array
*/
protected function get_option( $class_name ) {
if ( ! isset( $this->lookup[ $class_name ] ) ) {
return null;
}
return $this->lookup[ $class_name ]['option'];
}
/* ********************* DEPRECATED METHODS ********************* */
/**
* Add a lookup for a Yoast option.
*
* @deprecated 7.0
* @codeCoverageIgnore
*
* @param string $class_name Class to bind to the lookup.
* @param string $option Option group to use.
* @param string $key Key in the option group to bind to.
*
* @throws InvalidArgumentException Thrown when invalid input is provided.
*/
public function add_yoast_lookup( $class_name, $option, $key ) {
_deprecated_function( __METHOD__, 'WPSEO 7.0', 'WPSEO_Configuration_Options_Adapter::add_option_lookup' );
$this->add_option_lookup( $class_name, $key );
}
}

View File

@ -0,0 +1,257 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin
*/
/**
* @class WPSEO_Configuration_Wizard Loads the Yoast configuration wizard.
*/
class WPSEO_Configuration_Page {
/**
* @var string
*/
const PAGE_IDENTIFIER = 'wpseo_configurator';
/**
* Sets the hooks when the user has enough rights and is on the right page.
*/
public function set_hooks() {
if ( ! ( $this->is_config_page() && current_user_can( WPSEO_Configuration_Endpoint::CAPABILITY_RETRIEVE ) ) ) {
return;
}
if ( $this->should_add_notification() ) {
$this->add_notification();
}
// Register the page for the wizard.
add_action( 'admin_menu', array( $this, 'add_wizard_page' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'admin_init', array( $this, 'render_wizard_page' ) );
}
/**
* Check if the configuration is finished. If so, just remove the notification.
*/
public function catch_configuration_request() {
$configuration_page = filter_input( INPUT_GET, 'configuration' );
$page = filter_input( INPUT_GET, 'page' );
if ( ! ( $configuration_page === 'finished' && ( $page === WPSEO_Admin::PAGE_IDENTIFIER ) ) ) {
return;
}
$this->remove_notification();
$this->remove_notification_option();
wp_redirect( admin_url( 'admin.php?page=' . WPSEO_Admin::PAGE_IDENTIFIER ) );
exit;
}
/**
* Registers the page for the wizard.
*/
public function add_wizard_page() {
add_dashboard_page( '', '', 'wpseo_manage_options', self::PAGE_IDENTIFIER, '' );
}
/**
* Renders the wizard page and exits to prevent the WordPress UI from loading.
*/
public function render_wizard_page() {
$this->show_wizard();
exit;
}
/**
* Enqueues the assets needed for the wizard.
*/
public function enqueue_assets() {
wp_enqueue_media();
if ( ! wp_script_is( 'wp-element', 'registered' ) && function_exists( 'gutenberg_register_scripts_and_styles' ) ) {
gutenberg_register_scripts_and_styles();
}
/*
* Print the `forms.css` WP stylesheet before any Yoast style, this way
* it's easier to override selectors with the same specificity later.
*/
wp_enqueue_style( 'forms' );
$asset_manager = new WPSEO_Admin_Asset_Manager();
$asset_manager->register_wp_assets();
$asset_manager->register_assets();
$asset_manager->enqueue_script( 'configuration-wizard' );
$asset_manager->enqueue_style( 'yoast-components' );
$config = $this->get_config();
wp_localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'configuration-wizard', 'yoastWizardConfig', $config );
$yoast_components_l10n = new WPSEO_Admin_Asset_Yoast_Components_L10n();
$yoast_components_l10n->localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'configuration-wizard' );
}
/**
* Setup Wizard Header.
*/
public function show_wizard() {
$this->enqueue_assets();
$dashboard_url = admin_url( '/admin.php?page=wpseo_dashboard' );
?>
<!DOCTYPE html>
<!--[if IE 9]>
<html class="ie9" <?php language_attributes(); ?> >
<![endif]-->
<!--[if !(IE 9) ]><!-->
<html <?php language_attributes(); ?>>
<!--<![endif]-->
<head>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title><?php
printf(
/* translators: %s expands to Yoast SEO. */
esc_html__( '%s &rsaquo; Configuration Wizard', 'wordpress-seo' ),
'Yoast SEO' );
?></title>
<?php
wp_print_head_scripts();
wp_print_styles( 'yoast-seo-yoast-components' );
/**
* Is called before the closing </head> tag in the Yoast Configuration wizard.
*
* Allows users to add their own scripts or styles.
*
* @since 4.0
*/
do_action( 'wpseo_configuration_wizard_head' );
?>
</head>
<body class="wp-admin wp-core-ui">
<div id="wizard"></div>
<div role="contentinfo" class="yoast-wizard-return-link-container">
<a class="button yoast-wizard-return-link" href="<?php echo esc_url( $dashboard_url ); ?>">
<span aria-hidden="true" class="dashicons dashicons-no"></span>
<?php
esc_html_e( 'Close wizard', 'wordpress-seo' );
?>
</a>
</div>
<?php
wp_print_media_templates();
wp_print_footer_scripts();
/**
* Is called before the closing </body> tag in the Yoast Configuration wizard.
*
* Allows users to add their own scripts.
*
* @since 4.0
*/
do_action( 'wpseo_configuration_wizard_footer' );
wp_print_scripts( 'yoast-seo-configuration-wizard' );
?>
</body>
</html>
<?php
}
/**
* Get the API config for the wizard.
*
* @return array The API endpoint config.
*/
public function get_config() {
$service = new WPSEO_GSC_Service();
$config = array(
'namespace' => WPSEO_Configuration_Endpoint::REST_NAMESPACE,
'endpoint_retrieve' => WPSEO_Configuration_Endpoint::ENDPOINT_RETRIEVE,
'endpoint_store' => WPSEO_Configuration_Endpoint::ENDPOINT_STORE,
'nonce' => wp_create_nonce( 'wp_rest' ),
'root' => esc_url_raw( rest_url() ),
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'finishUrl' => admin_url( 'admin.php?page=wpseo_dashboard&configuration=finished' ),
'gscAuthURL' => $service->get_client()->createAuthUrl(),
'gscProfiles' => $service->get_sites(),
'gscNonce' => wp_create_nonce( 'wpseo-gsc-ajax-security' ),
);
return $config;
}
/**
* Checks if the current page is the configuration page.
*
* @return bool
*/
protected function is_config_page() {
return ( filter_input( INPUT_GET, 'page' ) === self::PAGE_IDENTIFIER );
}
/**
* Adds a notification to the notification center.
*/
private function add_notification() {
$notification_center = Yoast_Notification_Center::get();
$notification_center->add_notification( self::get_notification() );
}
/**
* Removes the notification from the notification center.
*/
private function remove_notification() {
$notification_center = Yoast_Notification_Center::get();
$notification_center->remove_notification( self::get_notification() );
}
/**
* Gets the notification.
*
* @return Yoast_Notification
*/
private static function get_notification() {
$message = __( 'The configuration wizard helps you to easily configure your site to have the optimal SEO settings.', 'wordpress-seo' );
$message .= '<br/>';
$message .= sprintf(
/* translators: %1$s resolves to Yoast SEO, %2$s resolves to the starting tag of the link to the wizard, %3$s resolves to the closing link tag */
__( 'We have detected that you have not finished this wizard yet, so we recommend you to %2$sstart the configuration wizard to configure %1$s%3$s.', 'wordpress-seo' ),
'Yoast SEO',
'<a href="' . admin_url( '?page=' . self::PAGE_IDENTIFIER ) . '">',
'</a>'
);
$notification = new Yoast_Notification(
$message,
array(
'type' => Yoast_Notification::WARNING,
'id' => 'wpseo-dismiss-onboarding-notice',
'capabilities' => 'wpseo_manage_options',
'priority' => 0.8,
)
);
return $notification;
}
/**
* When the notice should be shown.
*
* @return bool
*/
private function should_add_notification() {
return ( WPSEO_Options::get( 'show_onboarding_notice' ) === true );
}
/**
* Remove the options that triggers the notice for the configuration wizard.
*/
private function remove_notification_option() {
WPSEO_Options::set( 'show_onboarding_notice', false );
}
}

View File

@ -0,0 +1,172 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\ConfigurationUI
*/
/**
* Class WPSEO_Configuration_Service.
*/
class WPSEO_Configuration_Service {
/**
* @var WPSEO_Configuration_Structure
*/
protected $structure;
/**
* @var WPSEO_Configuration_Components
*/
protected $components;
/**
* @var WPSEO_Configuration_Storage
*/
protected $storage;
/**
* @var WPSEO_Configuration_Endpoint
*/
protected $endpoint;
/**
* @var WPSEO_Configuration_Options_Adapter
*/
protected $adapter;
/**
* @var WPSEO_Configuration_Translations
*/
protected $translations;
/**
* Hook into the REST API and switch language.
*/
public function initialize() {
$this->set_default_providers();
$this->endpoint->register();
}
/**
* Set default handlers.
*/
public function set_default_providers() {
$this->set_storage( new WPSEO_Configuration_Storage() );
$this->set_options_adapter( new WPSEO_Configuration_Options_Adapter() );
$this->set_components( new WPSEO_Configuration_Components() );
$this->set_endpoint( new WPSEO_Configuration_Endpoint() );
$this->set_structure( new WPSEO_Configuration_Structure() );
$this->set_translations( new WPSEO_Configuration_Translations( WPSEO_Language_Utils::get_user_locale() ) );
}
/**
* Set storage handler.
*
* @param WPSEO_Configuration_Storage $storage Storage handler to use.
*/
public function set_storage( WPSEO_Configuration_Storage $storage ) {
$this->storage = $storage;
}
/**
* Set endpoint handler.
*
* @param WPSEO_Configuration_Endpoint $endpoint Endpoint implementation to use.
*/
public function set_endpoint( WPSEO_Configuration_Endpoint $endpoint ) {
$this->endpoint = $endpoint;
$this->endpoint->set_service( $this );
}
/**
* Set the options adapter.
*
* @param WPSEO_Configuration_Options_Adapter $adapter Adapter to use.
*/
public function set_options_adapter( WPSEO_Configuration_Options_Adapter $adapter ) {
$this->adapter = $adapter;
}
/**
* Set components provider.
*
* @param WPSEO_Configuration_Components $components Component provider to use.
*/
public function set_components( WPSEO_Configuration_Components $components ) {
$this->components = $components;
}
/**
* Set structure provider.
*
* @param WPSEO_Configuration_Structure $structure Structure provider to use.
*/
public function set_structure( WPSEO_Configuration_Structure $structure ) {
$this->structure = $structure;
}
/**
* Sets the translations object.
*
* @param WPSEO_Configuration_Translations $translations The translations object.
*/
public function set_translations( WPSEO_Configuration_Translations $translations ) {
$this->translations = $translations;
}
/**
* Populate the configuration.
*/
protected function populate_configuration() {
// Switch to the user locale with fallback to the site locale.
switch_to_locale( WPSEO_Language_Utils::get_user_locale() );
// Make sure we have our translations available.
wpseo_load_textdomain();
$this->structure->initialize();
$this->storage->set_adapter( $this->adapter );
$this->storage->add_default_fields();
$this->components->initialize();
$this->components->set_storage( $this->storage );
// @todo: check if this is really needed, since the switch happens only in the API.
if ( function_exists( 'restore_current_locale' ) ) {
restore_current_locale();
}
}
/**
* Used by endpoint to retrieve configuration.
*
* @return array List of settings.
*/
public function get_configuration() {
$this->populate_configuration();
$fields = $this->storage->retrieve();
$steps = $this->structure->retrieve();
$translations = $this->translations->retrieve();
return array(
'fields' => $fields,
'steps' => $steps,
'translations' => $translations,
);
}
/**
* Used by endpoint to store changes.
*
* @param WP_REST_Request $request Request from the REST API.
*
* @return array List of feedback per option if saving succeeded.
*/
public function set_configuration( WP_REST_Request $request ) {
$this->populate_configuration();
return $this->storage->store( $request->get_json_params() );
}
}

View File

@ -0,0 +1,203 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\ConfigurationUI
*/
/**
* Class WPSEO_Configuration_Storage.
*/
class WPSEO_Configuration_Storage {
/**
* @var \WPSEO_Configuration_Options_Adapter
*/
protected $adapter;
/**
* @var \WPSEO_Config_Field[]
*/
protected $fields = array();
/**
* Add default fields.
*/
public function add_default_fields() {
$fields = array(
new WPSEO_Config_Field_Upsell_Configuration_Service(),
new WPSEO_Config_Field_Upsell_Site_Review(),
new WPSEO_Config_Field_Success_Message(),
new WPSEO_Config_Field_Mailchimp_Signup(),
new WPSEO_Config_Field_Environment(),
new WPSEO_Config_Field_Site_Type(),
new WPSEO_Config_Field_Multiple_Authors(),
new WPSEO_Config_Field_Title_Intro(),
new WPSEO_Config_Field_Site_Name(),
new WPSEO_Config_Field_Separator(),
new WPSEO_Config_Field_Google_Search_Console_Intro(),
new WPSEO_Config_Field_Profile_URL_Facebook(),
new WPSEO_Config_Field_Profile_URL_Twitter(),
new WPSEO_Config_Field_Profile_URL_Instagram(),
new WPSEO_Config_Field_Profile_URL_LinkedIn(),
new WPSEO_Config_Field_Profile_URL_MySpace(),
new WPSEO_Config_Field_Profile_URL_Pinterest(),
new WPSEO_Config_Field_Profile_URL_YouTube(),
new WPSEO_Config_Field_Profile_URL_Wikipedia(),
new WPSEO_Config_Field_Company_Or_Person(),
new WPSEO_Config_Field_Company_Name(),
new WPSEO_Config_Field_Company_Logo(),
new WPSEO_Config_Field_Person(),
new WPSEO_Config_Field_Post_Type_Visibility(),
);
$post_type_factory = new WPSEO_Config_Factory_Post_Type();
$fields = array_merge( $fields, $post_type_factory->get_fields() );
foreach ( $fields as $field ) {
$this->add_field( $field );
}
}
/**
* Allow for field injections.
*
* @param WPSEO_Config_Field $field Field to add to the stack.
*/
public function add_field( WPSEO_Config_Field $field ) {
$this->fields[] = $field;
if ( isset( $this->adapter ) ) {
$field->set_adapter( $this->adapter );
}
}
/**
* Set the adapter to use.
*
* @param WPSEO_Configuration_Options_Adapter $adapter Adapter to use.
*/
public function set_adapter( WPSEO_Configuration_Options_Adapter $adapter ) {
$this->adapter = $adapter;
foreach ( $this->fields as $field ) {
$field->set_adapter( $this->adapter );
}
}
/**
* Retrieve the current adapter.
*
* @return WPSEO_Configuration_Options_Adapter
*/
public function get_adapter() {
return $this->adapter;
}
/**
* Retrieve the registered fields.
*
* @returns array List of settings.
*/
public function retrieve() {
$output = array();
/** @var WPSEO_Config_Field $field */
foreach ( $this->fields as $field ) {
$build = $field->to_array();
$data = $this->get_field_data( $field );
if ( ! is_null( $data ) ) {
$build['data'] = $data;
}
$output[ $field->get_identifier() ] = $build;
}
return $output;
}
/**
* Save the data.
*
* @param array $data_to_store Data provided by the API which needs to be processed for saving.
*
* @return string Results
*/
public function store( $data_to_store ) {
$output = array();
/** @var WPSEO_Config_Field $field */
foreach ( $this->fields as $field ) {
$field_identifier = $field->get_identifier();
if ( ! array_key_exists( $field_identifier, $data_to_store ) ) {
continue;
}
$field_data = array();
if ( isset( $data_to_store[ $field_identifier ] ) ) {
$field_data = $data_to_store[ $field_identifier ];
}
$result = $this->adapter->set( $field, $field_data );
$build = array(
'result' => $result,
);
// Set current data to object to be displayed.
$data = $this->get_field_data( $field );
if ( ! is_null( $data ) ) {
$build['data'] = $data;
}
$output[ $field_identifier ] = $build;
}
return $output;
}
/**
* Filter out null input values.
*
* @param mixed $input Input to test against.
*
* @return bool
*/
protected function is_not_null( $input ) {
return ! is_null( $input );
}
/**
* Get data from a specific field.
*
* @param WPSEO_Config_Field $field Field to get data for.
*
* @return array|mixed
*/
protected function get_field_data( WPSEO_Config_Field $field ) {
$data = $this->adapter->get( $field );
if ( is_array( $data ) ) {
$defaults = $field->get_data();
// Remove 'null' values from input.
$data = array_filter( $data, array( $this, 'is_not_null' ) );
// Merge defaults with data.
$data = array_merge( $defaults, $data );
}
if ( is_null( $data ) ) {
// Get default if no data was set.
$data = $field->get_data();
return $data;
}
return $data;
}
}

View File

@ -0,0 +1,128 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\ConfigurationUI
*/
/**
* Class WPSEO_Configuration_Structure
*/
class WPSEO_Configuration_Structure {
/**
* Registered steps.
*
* @var array
*/
protected $steps = array();
/**
* List of fields for each configuration step.
*
* This list does not include the fields for the 'postTypeVisibility'
* step as that list will be generated on the fly.
*
* @var array
*/
private $fields = array(
'environment_type' => array( 'environment_type' ),
'siteType' => array( 'siteType' ),
'publishingEntity' => array(
'publishingEntity',
'publishingEntityType',
'publishingEntityCompanyName',
'publishingEntityCompanyLogo',
'publishingEntityPersonId',
'profileUrlFacebook',
'profileUrlTwitter',
'profileUrlInstagram',
'profileUrlLinkedIn',
'profileUrlMySpace',
'profileUrlPinterest',
'profileUrlYouTube',
'profileUrlWikipedia',
),
'multipleAuthors' => array( 'multipleAuthors' ),
'connectGoogleSearchConsole' => array(
'googleSearchConsoleIntro',
'connectGoogleSearchConsole',
),
'titleTemplate' => array(
'titleIntro',
'siteName',
'separator',
),
'newsletter' => array( 'mailchimpSignup' ),
'suggestions' => array( 'suggestions' ),
'success' => array( 'successMessage' ),
);
/**
* WPSEO_Configuration_Structure constructor.
*/
public function initialize() {
$this->add_step( 'environment-type', __( 'Environment', 'wordpress-seo' ), $this->fields['environment_type'] );
$this->add_step( 'site-type', __( 'Site type', 'wordpress-seo' ), $this->fields['siteType'] );
$this->add_step(
'publishing-entity',
__( 'Organization or person', 'wordpress-seo' ),
$this->fields['publishingEntity']
);
$fields = array( 'postTypeVisibility' );
$post_type_factory = new WPSEO_Config_Factory_Post_Type();
foreach ( $post_type_factory->get_fields() as $post_type_field ) {
$fields[] = $post_type_field->get_identifier();
}
$this->add_step( 'post-type-visibility', __( 'Search engine visibility', 'wordpress-seo' ), $fields );
$this->add_step(
'multiple-authors',
__( 'Multiple authors', 'wordpress-seo' ),
$this->fields['multipleAuthors']
);
// @codingStandardsIgnoreStart -- These lines are commented out temporarily, see next line.
// Commented out since 11.1.1 patch because Google removed their GSC API.
// $this->add_step(
// 'connect-google-search-console',
// __( 'Google Search Console', 'wordpress-seo' ),
// $this->fields['connectGoogleSearchConsole']
// );
// @codingStandardsIgnoreEnd
$this->add_step( 'title-template', __( 'Title settings', 'wordpress-seo' ), $this->fields['titleTemplate'] );
$this->add_step( 'newsletter', __( 'Newsletter', 'wordpress-seo' ), $this->fields['newsletter'], true, true );
$this->add_step( 'suggestions', __( 'You might like', 'wordpress-seo' ), $this->fields['suggestions'], true, true );
$this->add_step( 'success', __( 'Success!', 'wordpress-seo' ), $this->fields['success'], true, true );
}
/**
* Add a step to the structure
*
* @param string $identifier Identifier for this step.
* @param string $title Title to display for this step.
* @param array $fields Fields to use on the step.
* @param bool $navigation Show navigation buttons.
* @param bool $full_width Wheter the step content is full width or not.
*/
protected function add_step( $identifier, $title, $fields, $navigation = true, $full_width = false ) {
$this->steps[ $identifier ] = array(
'title' => $title,
'fields' => $fields,
'hideNavigation' => ! (bool) $navigation,
'fullWidth' => $full_width,
);
}
/**
* Retrieve the registered steps.
*
* @return array
*/
public function retrieve() {
return $this->steps;
}
}

View File

@ -0,0 +1,63 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\ConfigurationUI
*/
/**
* Class WPSEO_Configuration_Structure.
*/
class WPSEO_Configuration_Translations {
/**
* Registered steps.
*
* @var array
*/
protected $translations = array();
/**
* The locale.
*
* @var string
*/
protected $locale;
/**
* Sets the translations based on the file.
*
* @param string $locale The locale to retreive the translations for.
*/
public function __construct( $locale ) {
$this->locale = $locale;
$this->translations = $this->get_translations_from_file();
}
/**
* Retrieve the translations.
*
* @return array
*/
public function retrieve() {
return $this->translations;
}
/**
* Retrieves the translations from the JSON-file.
*
* @return array Array with the translations.
*/
protected function get_translations_from_file() {
$file = plugin_dir_path( WPSEO_FILE ) . 'languages/yoast-components-' . $this->locale . '.json';
if ( file_exists( $file ) ) {
$file = file_get_contents( $file );
if ( is_string( $file ) && $file !== '' ) {
return json_decode( $file, true );
}
}
return array();
}
}

View File

@ -0,0 +1,161 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\ConfigurationUI
*/
/**
* Class WPSEO_Config_Component_Connect_Google_Search_Console.
*/
class WPSEO_Config_Component_Connect_Google_Search_Console implements WPSEO_Config_Component {
/**
* @var string
*/
const OPTION_ACCESS_TOKEN = 'wpseo-gsc-access_token';
/**
* @var string
*/
const OPTION_REFRESH_TOKEN = 'wpseo-gsc-refresh_token';
/**
* Service to use.
*
* @var WPSEO_GSC_Service
*/
protected $gsc_service;
/**
* WPSEO_Config_Component_Connect_Google_Search_Console constructor.
*/
public function __construct() {
$this->gsc_service = new WPSEO_GSC_Service( $this->get_profile() );
}
/**
* Set the Google Search Console service.
*
* @param WPSEO_GSC_Service $service Set service to use.
*/
public function set_gsc_service( WPSEO_GSC_Service $service ) {
$this->gsc_service = $service;
}
/**
* Gets the component identifier.
*
* @return string
*/
public function get_identifier() {
return 'ConnectGoogleSearchConsole';
}
/**
* Gets the field.
*
* @return WPSEO_Config_Field
*/
public function get_field() {
return new WPSEO_Config_Field_Connect_Google_Search_Console();
}
/**
* Get the data for the field.
*
* @return mixed
*/
public function get_data() {
$data = array(
'profileList' => $this->get_profilelist(),
'profile' => $this->get_profile(),
'hasAccessToken' => $this->hasAccessToken(),
);
return $data;
}
/**
* Save data.
*
* @param array $data Data containing changes.
*
* @return mixed
*/
public function set_data( $data ) {
$current_data = $this->get_data();
$this->handle_profile_change( $current_data, $data );
// Save profile.
$has_saved = update_option(
WPSEO_GSC::OPTION_WPSEO_GSC,
array( 'profile' => $data['profile'] )
);
// Collect results to return to the configurator.
$results = array(
'profile' => $has_saved,
);
return $results;
}
/**
* Remove issues when the profile has changed.
*
* @param array $current_data Saved data before changes.
* @param array $data Data after changes.
*/
protected function handle_profile_change( $current_data, $data ) {
// If the profile has been changed, remove issues.
if ( $current_data['profile'] === $data['profile'] ) {
return;
}
$this->reload_issues();
}
/**
* Get the current GSC profile.
*
* @return string
*/
protected function get_profile() {
return WPSEO_GSC_Settings::get_profile();
}
/**
* Reload GSC issues.
*/
protected function reload_issues() {
WPSEO_GSC_Settings::reload_issues();
}
/**
* Gets a list with the profiles.
*
* @return array
*/
protected function get_profilelist() {
$profiles = array();
$sites = $this->gsc_service->get_sites();
foreach ( $sites as $site_key => $site_value ) {
$profiles[ untrailingslashit( $site_key ) ] = untrailingslashit( $site_value );
}
return $profiles;
}
/**
* Checks if there is an access token. If so, there is a connection.
*
* @return bool
*/
private function hasAccessToken() {
return ( null !== $this->gsc_service->get_client()->getAccessToken() );
}
}

View File

@ -0,0 +1,84 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\ConfigurationUI
*/
/**
* Represents the mailchimp signup components.
*/
class WPSEO_Config_Component_Mailchimp_Signup implements WPSEO_Config_Component {
/**
* @var string
*/
const META_NAME = 'wpseo-has-mailchimp-signup';
/**
* Gets the component identifier.
*
* @return string
*/
public function get_identifier() {
return 'MailchimpSignup';
}
/**
* Gets the field.
*
* @return WPSEO_Config_Field
*/
public function get_field() {
return new WPSEO_Config_Field_Mailchimp_Signup();
}
/**
* Get the data for the field.
*
* @return mixed
*/
public function get_data() {
$data = array(
'hasSignup' => $this->has_mailchimp_signup(),
);
return $data;
}
/**
* Save data.
*
* @param array $data Data containing changes.
*
* @return mixed
*/
public function set_data( $data ) {
$has_saved = false;
if ( ! empty( $data['hasSignup'] ) ) {
// Saves the user meta.
update_user_meta( get_current_user_id(), self::META_NAME, true );
$has_saved = ( $data['hasSignup'] === $this->has_mailchimp_signup() );
}
// Collect results to return to the configurator.
$results = array(
'hasSignup' => $has_saved,
);
return $results;
}
/**
* Checks if the user has entered their email for mailchimp already.
*
* @return bool
*/
protected function has_mailchimp_signup() {
$user_meta = get_user_meta( get_current_user_id(), self::META_NAME, true );
return ( ! empty( $user_meta ) );
}
}

View File

@ -0,0 +1,139 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\ConfigurationUI
*/
/**
* Represents the configuration suggestions component.
*/
class WPSEO_Config_Component_Suggestions implements WPSEO_Config_Component {
/**
* Gets the component identifier.
*
* @return string
*/
public function get_identifier() {
return 'Suggestions';
}
/**
* Gets the field.
*
* @return WPSEO_Config_Field
*/
public function get_field() {
$field = new WPSEO_Config_Field_Suggestions();
// Only show Premium upsell when we are not inside a Premium install.
if ( ! WPSEO_Utils::is_yoast_seo_premium() ) {
$field->add_suggestion(
/* translators: %s resolves to Yoast SEO Premium */
sprintf( __( 'Outrank the competition with %s', 'wordpress-seo' ), 'Yoast SEO Premium' ),
/* translators: %1$s resolves to Yoast SEO Premium */
sprintf( __( 'Do you want to outrank your competition? %1$s gives you awesome additional features that\'ll help you to set up your SEO strategy like a professional. Add synonyms and related keywords, use our Premium SEO analysis, the redirect manager and our internal linking tool. %1$s will also give you access to premium support.', 'wordpress-seo' ), 'Yoast SEO Premium' ),
array(
'label' => __( 'Upgrade to Premium', 'wordpress-seo' ),
'type' => 'primary',
'url' => WPSEO_Shortlinker::get( 'https://yoa.st/wizard-suggestion-premium' ),
),
array(
'url' => WPSEO_Shortlinker::get( 'https://yoa.st/video-yoast-seo-premium' ),
'title' => sprintf(
/* translators: %1$s expands to Yoast SEO Premium. */
__( '%1$s video', 'wordpress-seo' ),
'Yoast SEO Premium'
),
)
);
}
$field->add_suggestion(
__( 'Find out what words your audience uses to find you', 'wordpress-seo' ),
sprintf(
/* translators: %1$s resolves to Keyword research training */
__( 'Keyword research is essential in any SEO strategy. You decide the search terms you want to be found for, and figure out what words your audience uses to find you. Great keyword research tells you what content you need to start ranking for the terms you want to rank for. Make sure your efforts go into the keywords you actually have a chance at ranking for! The %1$s walks you through this process, step by step.', 'wordpress-seo' ),
'Keyword research training'
),
array(
'label' => 'Keyword research training',
'type' => 'link',
'url' => WPSEO_Shortlinker::get( 'https://yoa.st/3lg' ),
),
array(
'url' => WPSEO_Shortlinker::get( 'https://yoa.st/3lf' ),
'title' => sprintf(
/* translators: %1$s expands to Keyword research training. */
__( '%1$s video', 'wordpress-seo' ),
'Keyword research training'
),
)
);
$field->add_suggestion(
/* translators: %1$s resolves to Yoast SEO, %2$s resolves to Yoast SEO plugin training */
sprintf( __( 'Get the most out of %1$s with the %2$s', 'wordpress-seo' ), 'Yoast SEO', 'Yoast SEO plugin training' ),
/* translators: %1$s resolves to Yoast SEO */
sprintf( __( 'Do you want to know all the ins and outs of the %1$s plugin? Do you want to learn all about our advanced settings? Want to be able to really get the most out of the %1$s plugin? Check out our %1$s plugin training and start outranking the competition!', 'wordpress-seo' ), 'Yoast SEO' ),
array(
'label' => 'Yoast SEO plugin training',
'type' => 'link',
'url' => WPSEO_Shortlinker::get( 'https://yoa.st/wizard-suggestion-plugin-course' ),
),
array(
'url' => WPSEO_Shortlinker::get( 'https://yoa.st/video-plugin-course' ),
'title' => sprintf(
/* translators: %1$s expands to Yoast SEO plugin training. */
__( '%1$s video', 'wordpress-seo' ),
'Yoast SEO plugin training'
),
)
);
// When we are running in Yoast SEO Premium and don't have Local SEO installed, show Local SEO as suggestion.
if ( WPSEO_Utils::is_yoast_seo_premium() && ! defined( 'WPSEO_LOCAL_FILE' ) ) {
$field->add_suggestion(
__( 'Attract more customers near you', 'wordpress-seo' ),
/* translators: %1$s resolves to Local SEO */
sprintf( __( 'If you want to outrank the competition in a specific town or region, check out our %1$s plugin! Youll be able to easily insert Google maps, opening hours, contact information and a store locator. Besides that %1$s helps you to improve the usability of your contact page.', 'wordpress-seo' ), 'Local SEO' ),
array(
'label' => 'Local SEO',
'type' => 'link',
'url' => WPSEO_Shortlinker::get( 'https://yoa.st/wizard-suggestion-localseo' ),
),
array(
'url' => WPSEO_Shortlinker::get( 'https://yoa.st/video-localseo' ),
'title' => sprintf(
/* translators: %1$s expands to Local SEO. */
__( '%1$s video', 'wordpress-seo' ),
'Local SEO'
),
)
);
}
return $field;
}
/**
* Get the data for the field.
*
* @return array
*/
public function get_data() {
return array();
}
/**
* Save data.
*
* @param array $data Data containing changes.
*
* @return bool
*/
public function set_data( $data ) {
return true;
}
}

View File

@ -0,0 +1,36 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\ConfigurationUI
*/
/**
* Config Component interface.
*/
interface WPSEO_Config_Component {
/**
* @return string
*/
public function get_identifier();
/**
* @return mixed
*/
public function get_data();
/**
* Save changes.
*
* @param array $data Data provided by the API.
*
* @return mixed
*/
public function set_data( $data );
/**
* @return WPSEO_Config_Field
*/
public function get_field();
}

View File

@ -0,0 +1,74 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Configurator
*/
/**
* Class WPSEO_Config_Factory_Post_Type.
*/
class WPSEO_Config_Factory_Post_Type {
/**
* List of fields.
*
* @var WPSEO_Config_Field_Choice_Post_Type[]
*/
protected static $fields = array();
/**
* @return WPSEO_Config_Field_Choice_Post_Type[] List of fields.
*/
public function get_fields() {
if ( empty( self::$fields ) ) {
$fields = array();
// WPSEO_Post_type::get_accessible_post_types() should *not* be used to get a similar experience from the settings.
$post_types = get_post_types( array( 'public' => true ), 'objects' );
$post_types = WPSEO_Post_Type::filter_attachment_post_type( $post_types );
if ( ! empty( $post_types ) ) {
foreach ( $post_types as $post_type => $post_type_object ) {
$label = $this->decode_html_entities( $post_type_object->label );
$field = new WPSEO_Config_Field_Choice_Post_Type( $post_type, $label );
$this->add_custom_properties( $post_type, $field );
$fields[] = $field;
}
}
self::$fields = $fields;
}
return self::$fields;
}
/**
* Add custom properties for specific post types.
*
* @param string $post_type Post type of field that is being added.
* @param WPSEO_Config_Field $field Field that corresponds to the post type.
*/
private function add_custom_properties( $post_type, $field ) {
if ( 'attachment' === $post_type ) {
$field->set_property( 'explanation', __( 'WordPress automatically generates an URL for each media item in the library. Enabling this will allow for google to index the generated URL.', 'wordpress-seo' ) );
}
}
/**
* Replaces the HTML entity with it's actual symbol.
*
* Because we do not not know what consequences it will have if we convert every HTML entity,
* we will only replace the characters that we have known problems with in text's.
*
* @param string $text The text to decode.
*
* @return string String with decoded HTML entities.
*/
private function decode_html_entities( $text ) {
return str_replace( '&#39;', '', $text );
}
}

View File

@ -0,0 +1,85 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\ConfigurationUI
*/
/**
* Class WPSEO_Config_Field_Choice_Post_Type.
*/
class WPSEO_Config_Field_Choice_Post_Type extends WPSEO_Config_Field_Choice {
/**
* Post type.
*
* @var string
*/
protected $post_type;
/**
* WPSEO_Config_Field_Choice_Post_Type constructor.
*
* @param string $post_type The post type to add.
* @param string $label Label to show (translated post type).
*/
public function __construct( $post_type, $label ) {
parent::__construct( 'postType' . ucfirst( $post_type ) );
$this->post_type = $post_type;
/* Translators: %1$s expands to the name of the post type. The options given to the user are "visible" and "hidden" */
$this->set_property( 'label', sprintf( __( 'Search engines should show "%1$s" in search results:', 'wordpress-seo' ), $label ) );
$this->add_choice( 'true', __( 'Yes', 'wordpress-seo' ) );
$this->add_choice( 'false', __( 'No', 'wordpress-seo' ) );
}
/**
* Set adapter.
*
* @param WPSEO_Configuration_Options_Adapter $adapter Adapter to register lookup on.
*/
public function set_adapter( WPSEO_Configuration_Options_Adapter $adapter ) {
$adapter->add_custom_lookup(
$this->get_identifier(),
array( $this, 'get_data' ),
array( $this, 'set_data' )
);
}
/**
* Get the post type of this field.
*
* @return string Post type.
*/
public function get_post_type() {
return $this->post_type;
}
/**
* @return bool
*/
public function get_data() {
$key = 'noindex-' . $this->get_post_type();
if ( WPSEO_Options::get( $key, false ) === false ) {
return 'true';
}
return 'false';
}
/**
* Set new data.
*
* @param string $visible Visible (true) or hidden (false).
*
* @return bool
*/
public function set_data( $visible ) {
$post_type = $this->get_post_type();
return WPSEO_Options::set( 'noindex-' . $post_type, ( $visible === 'false' ) );
}
}

View File

@ -0,0 +1,42 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\ConfigurationUI
*/
/**
* Class WPSEO_Config_Field_Choice.
*/
class WPSEO_Config_Field_Choice extends WPSEO_Config_Field {
/**
* WPSEO_Config_Field_Choice constructor.
*
* @param string $field Field name to use.
*/
public function __construct( $field ) {
parent::__construct( $field, 'Choice' );
$this->properties['choices'] = array();
}
/**
* Add a choice to the properties.
*
* @param string $value Value op the option.
* @param string $label Label to display for the value.
* @param string $aria_label Optional. Aria label text to use.
*/
public function add_choice( $value, $label, $aria_label = '' ) {
$choice = array(
'label' => $label,
);
if ( $aria_label ) {
$choice['screenReaderText'] = $aria_label;
}
$this->properties['choices'][ $value ] = $choice;
}
}

View File

@ -0,0 +1,32 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Configurator
*/
/**
* Class WPSEO_Config_Field_Company_Logo.
*/
class WPSEO_Config_Field_Company_Logo extends WPSEO_Config_Field {
/**
* WPSEO_Config_Field_Company_Logo constructor.
*/
public function __construct() {
parent::__construct( 'publishingEntityCompanyLogo', 'MediaUpload' );
$this->set_property( 'label', __( 'Provide an image of the organization logo', 'wordpress-seo' ) );
$this->set_requires( 'publishingEntityType', 'company' );
}
/**
* Sets the adapter.
*
* @param WPSEO_Configuration_Options_Adapter $adapter Adapter to register lookup on.
*/
public function set_adapter( WPSEO_Configuration_Options_Adapter $adapter ) {
$adapter->add_option_lookup( $this->get_identifier(), 'company_logo' );
}
}

View File

@ -0,0 +1,33 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Configurator
*/
/**
* Class WPSEO_Config_Field_Company_Name.
*/
class WPSEO_Config_Field_Company_Name extends WPSEO_Config_Field {
/**
* WPSEO_Config_Field_Company_Name constructor.
*/
public function __construct() {
parent::__construct( 'publishingEntityCompanyName', 'Input' );
$this->set_property( 'label', __( 'The name of the organization', 'wordpress-seo' ) );
$this->set_property( 'autoComplete', 'organization' );
$this->set_requires( 'publishingEntityType', 'company' );
}
/**
* Sets the adapter.
*
* @param WPSEO_Configuration_Options_Adapter $adapter Adapter to register lookup on.
*/
public function set_adapter( WPSEO_Configuration_Options_Adapter $adapter ) {
$adapter->add_option_lookup( $this->get_identifier(), 'company_name' );
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\Configurator
*/
/**
* Class WPSEO_Config_Field_Company_Or_Person.
*/
class WPSEO_Config_Field_Company_Or_Person extends WPSEO_Config_Field_Choice {
/**
* WPSEO_Config_Field_Company_Or_Person constructor.
*/
public function __construct() {
parent::__construct( 'publishingEntityType' );
$this->set_property( 'label', __( 'Does your site represent a person or an organization?', 'wordpress-seo' ) );
$this->set_property( 'description', __( 'This information will be used in Google\'s Knowledge Graph Card, the big block of information you see on the right side of the search results.', 'wordpress-seo' ) );
$this->add_choice( 'company', __( 'Organization', 'wordpress-seo' ) );
$this->add_choice( 'person', __( 'Person', 'wordpress-seo' ) );
}
/**
* Sets the adapter.
*
* @param WPSEO_Configuration_Options_Adapter $adapter Adapter to register lookup on.
*/
public function set_adapter( WPSEO_Configuration_Options_Adapter $adapter ) {
$adapter->add_option_lookup( $this->get_identifier(), 'company_or_person' );
}
}

View File

@ -0,0 +1,31 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\ConfigurationUI
*/
/**
* Class WPSEO_Config_Field_Connect_Google_Search_Console.
*/
class WPSEO_Config_Field_Connect_Google_Search_Console extends WPSEO_Config_Field {
/**
* WPSEO_Config_Field_Connect_Google_Search_Console constructor.
*/
public function __construct() {
parent::__construct( 'connectGoogleSearchConsole', 'ConnectGoogleSearchConsole' );
}
/**
* Get the data.
*
* @return array
*/
public function get_data() {
return array(
'profile' => '',
'profileList' => '',
);
}
}

View File

@ -0,0 +1,91 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\ConfigurationUI
*/
/**
* Class WPSEO_Config_Field_Environment.
*/
class WPSEO_Config_Field_Environment extends WPSEO_Config_Field_Choice {
/**
* WPSEO_Config_Field_Environment constructor.
*/
public function __construct() {
parent::__construct( 'environment_type' );
$this->set_property( 'label', __( 'Please specify if your site is under construction or already active.', 'wordpress-seo' ) );
$this->set_property( 'description', __( 'Choose under construction if you want to keep the site out of the index of search engines. Don\'t forget to activate it once you\'re ready to publish your site.', 'wordpress-seo' ) );
$this->add_choice( 'production', __( 'Option A: My site is live and ready to be indexed', 'wordpress-seo' ) );
$this->add_choice( 'staging', __( 'Option B: My site is under construction and should not be indexed', 'wordpress-seo' ) );
}
/**
* Set adapter.
*
* @param WPSEO_Configuration_Options_Adapter $adapter Adapter to register lookup on.
*/
public function set_adapter( WPSEO_Configuration_Options_Adapter $adapter ) {
$adapter->add_custom_lookup(
$this->get_identifier(),
array( $this, 'get_data' ),
array( $this, 'set_data' )
);
}
/**
* Gets the option that is set for this field.
*
* @return string The value for the environment_type wpseo option.
*/
public function get_data() {
return WPSEO_Options::get( 'environment_type' );
}
/**
* Set new data.
*
* @param string $environment_type The site's environment type.
*
* @return bool Returns whether the value is successfully set.
*/
public function set_data( $environment_type ) {
$return = true;
if ( $this->get_data() !== $environment_type ) {
$return = WPSEO_Options::set( 'environment_type', $environment_type );
if ( ! $this->set_indexation( $environment_type ) ) {
return false;
}
}
return $return;
}
/**
* Set the WordPress Search Engine Visibility option based on the environment type.
*
* @param string $environment_type The environment the site is running in.
*
* @return bool Returns if the options is set successfully.
*/
protected function set_indexation( $environment_type ) {
$new_blog_public_value = 0;
$current_blog_public_value = get_option( 'blog_public' );
if ( $environment_type === 'production' ) {
$new_blog_public_value = 1;
}
if ( $current_blog_public_value !== $new_blog_public_value ) {
update_option( 'blog_public', $new_blog_public_value );
return true;
}
return ( get_option( 'blog_public' ) === $new_blog_public_value );
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\ConfigurationUI
*/
/**
* Class WPSEO_Config_Field_Google_Search_Console_Intro.
*/
class WPSEO_Config_Field_Google_Search_Console_Intro extends WPSEO_Config_Field {
/**
* WPSEO_Config_Field_Social_Profiles_Intro constructor.
*/
public function __construct() {
parent::__construct( 'googleSearchConsoleIntro', 'HTML' );
$html =
sprintf(
/* translators: %1$s is the plugin name. %2$s is a link start tag to a Yoast help page, %3$s is the link closing tag. */
esc_html__( '%1$s integrates with Google Search Console, a must-have tool for site owners. It provides you with information about the health of your site. Don\'t have a Google account or is your site not activated yet? Find out %2$show to connect Google Search Console to your site%3$s.', 'wordpress-seo' ),
'Yoast SEO',
'<a href="' . esc_url( WPSEO_Shortlinker::get( 'https://yoa.st/1ex' ) ) . '" target="_blank">',
'</a>'
);
$disclaimer = __( 'Note: we don\'t store your data in any way and don\'t have full access to your account. Your privacy is safe with us.', 'wordpress-seo' );
$html = '<p>' . $html . '</p><small>' . esc_html( $disclaimer ) . '</small>';
$this->set_property( 'html', $html );
}
}

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